diff options
| author | Omar Roth <omarroth@hotmail.com> | 2019-04-15 23:23:40 -0500 |
|---|---|---|
| committer | Omar Roth <omarroth@hotmail.com> | 2019-04-15 23:23:40 -0500 |
| commit | 26168a9520c8bc2a03f631eb0e53c3ee086d8c98 (patch) | |
| tree | 5575537ee9876400ca7fa5630190d2ff34684907 | |
| parent | 698dfca3199396f59fcd881745f87e79d46f7b58 (diff) | |
| download | invidious-26168a9520c8bc2a03f631eb0e53c3ee086d8c98.tar.gz invidious-26168a9520c8bc2a03f631eb0e53c3ee086d8c98.tar.bz2 invidious-26168a9520c8bc2a03f631eb0e53c3ee086d8c98.zip | |
Refactor CSRF tokens (using format in #473)
| -rw-r--r-- | src/invidious.cr | 169 | ||||
| -rw-r--r-- | src/invidious/users.cr | 73 | ||||
| -rw-r--r-- | src/invidious/views/clear_watch_history.ecr | 3 | ||||
| -rw-r--r-- | src/invidious/views/components/item.ecr | 24 | ||||
| -rw-r--r-- | src/invidious/views/components/subscribe_widget.ecr | 18 | ||||
| -rw-r--r-- | src/invidious/views/components/subscribe_widget_script.ecr | 10 | ||||
| -rw-r--r-- | src/invidious/views/delete_account.ecr | 3 | ||||
| -rw-r--r-- | src/invidious/views/history.ecr | 27 | ||||
| -rw-r--r-- | src/invidious/views/login.ecr | 6 | ||||
| -rw-r--r-- | src/invidious/views/subscription_manager.ecr | 21 | ||||
| -rw-r--r-- | src/invidious/views/subscriptions.ecr | 9 | ||||
| -rw-r--r-- | src/invidious/views/template.ecr | 263 |
12 files changed, 321 insertions, 305 deletions
diff --git a/src/invidious.cr b/src/invidious.cr index b0b512fb..8ed4253f 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -195,39 +195,33 @@ before_all do |env| end if env.request.cookies.has_key? "SID" - headers = HTTP::Headers.new - headers["Cookie"] = env.request.headers["Cookie"] - sid = env.request.cookies["SID"].value # Invidious users only have SID if !env.request.cookies.has_key? "SSID" - email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String) - - if email + if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String) user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User) - challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week) - - env.set "challenge", challenge - env.set "token", token + token = create_response(sid, {"signout", "watch_ajax", "subscription_ajax"}, HMAC_KEY, PG_DB, 1.week) preferences = user.preferences - env.set "user", user env.set "sid", sid + env.set "token", token + env.set "user", user end else + headers = HTTP::Headers.new + headers["Cookie"] = env.request.headers["Cookie"] + begin user, sid = get_user(sid, headers, PG_DB, false) - - challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week) - env.set "challenge", challenge - env.set "token", token + token = create_response(sid, {"signout", "watch_ajax", "subscription_ajax"}, HMAC_KEY, PG_DB, 1.week) preferences = user.preferences - env.set "user", user env.set "sid", sid + env.set "token", token + env.set "user", user rescue ex end end @@ -826,7 +820,7 @@ post "/login" do |env| when "google" tfa_code = env.params.body["tfa"]?.try &.lchop("G-") - # See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L79 + # See https://github.com/ytdl-org/youtube-dl/blob/2019.04.07/youtube_dl/extractor/youtube.py#L82 begin client = make_client(LOGIN_URL) headers = HTTP::Headers.new @@ -1091,8 +1085,7 @@ post "/login" do |env| next templated "login" end - challenges = env.params.body.select { |k, v| k.match(/^challenge\[\d+\]$/) } - tokens = env.params.body.select { |k, v| k.match(/^token\[\d+\]$/) } + tokens = env.params.body.select { |k, v| k.match(/^token\[\d+\]$/) }.map { |k, v| v } answer ||= "" captcha_type ||= "image" @@ -1102,11 +1095,8 @@ post "/login" do |env| answer = answer.lstrip('0') answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer) - challenge = env.params.body["challenge[0]"]? - token = env.params.body["token[0]"]? - begin - validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB, locale) + validate_response(tokens[0], answer, env.request.path, HMAC_KEY, PG_DB, locale) rescue ex error_message = ex.message next templated "error" @@ -1117,11 +1107,9 @@ post "/login" do |env| found_valid_captcha = false error_message = translate(locale, "Invalid CAPTCHA") - challenges.each_with_index do |challenge, i| + tokens.each_with_index do |token, i| begin - challenge = challenge[1] - token = tokens[i][1] - validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB, locale) + validate_response(token, answer, env.request.path, HMAC_KEY, PG_DB, locale) found_valid_captcha = true rescue ex error_message = ex.message @@ -1191,27 +1179,25 @@ post "/login" do |env| end end -get "/signout" do |env| +post "/signout" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? user = env.get? "user" + sid = env.get? "sid" referer = get_referer(env) if user user = user.as(User) - - challenge = env.params.query["challenge"]? - token = env.params.query["token"]? + sid = sid.as(String) + token = env.params.body["token"]? begin - validate_response(challenge, token, user.email, "sign_out", HMAC_KEY, PG_DB, locale) + validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale) rescue ex error_message = ex.message next templated "error" end - user = env.get("user").as(User) - sid = env.get("sid").as(String) PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", sid) env.request.cookies.each do |cookie| @@ -1426,42 +1412,24 @@ get "/toggle_theme" do |env| env.redirect referer end -get "/mark_watched" do |env| +post "/watch_ajax" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? user = env.get? "user" + sid = env.get? "sid" referer = get_referer(env, "/feed/subscriptions") - id = env.params.query["id"]? - if !id - env.response.status_code = 400 - next - end - redirect = env.params.query["redirect"]? - redirect ||= "false" + redirect ||= "true" redirect = redirect == "true" - if user - user = user.as(User) - if !user.watched.includes? id - PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.email) - end - end - - if redirect - env.redirect referer - else - env.response.content_type = "application/json" - "{}" + if !user + next env.redirect referer end -end -get "/mark_unwatched" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - user = env.get? "user" - referer = get_referer(env, "/feed/history") + user = user.as(User) + sid = sid.as(String) + token = env.params.body["token"]? id = env.params.query["id"]? if !id @@ -1469,12 +1437,37 @@ get "/mark_unwatched" do |env| next end - redirect = env.params.query["redirect"]? - redirect ||= "false" - redirect = redirect == "true" + user = user.as(User) + sid = sid.as(String) + token = env.params.body["token"]? - if user - user = user.as(User) + begin + validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale) + rescue ex + if redirect + error_message = ex.message + next templated "error" + else + error_message = {"error" => ex.message}.to_json + env.response.status_code = 500 + next error_message + end + end + + if env.params.query["action_mark_watched"]? + action = "action_mark_watched" + elsif env.params.query["action_mark_unwatched"]? + action = "action_mark_unwatched" + else + next env.redirect referer + end + + case action + when "action_mark_watched" + if !user.watched.includes? id + PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.email) + end + when "action_mark_unwatched" PG_DB.exec("UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2", id, user.email) end @@ -1561,8 +1554,7 @@ get "/modify_notifications" do |env| end end -# TODO: Add CSRF -get "/subscription_ajax" do |env| +post "/subscription_ajax" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? user = env.get? "user" @@ -1570,14 +1562,29 @@ get "/subscription_ajax" do |env| referer = get_referer(env, "/") redirect = env.params.query["redirect"]? - redirect ||= "false" + redirect ||= "true" redirect = redirect == "true" - if !user && !sid + if !user next env.redirect referer end user = user.as(User) + sid = sid.as(String) + token = env.params.body["token"]? + + begin + validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale) + rescue ex + if redirect + error_message = ex.message + next templated "error" + else + error_message = {"error" => ex.message}.to_json + env.response.status_code = 500 + next error_message + end + end if env.params.query["action_create_subscription_to_channel"]? action = "action_create_subscription_to_channel" @@ -1653,7 +1660,7 @@ get "/subscription_manager" do |env| user = env.get? "user" sid = env.get? "sid" - referer = get_referer(env, "/") + referer = get_referer(env, "/subscription_manager") if !user && !sid next env.redirect referer @@ -1843,12 +1850,13 @@ get "/delete_account" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? user = env.get? "user" + sid = env.get? "sid" referer = get_referer(env) if user user = user.as(User) - - challenge, token = create_response(user.email, "delete_account", HMAC_KEY, PG_DB) + sid = sid.as(String) + token = create_response(sid, {"delete_account"}, HMAC_KEY, PG_DB) templated "delete_account" else @@ -1860,16 +1868,16 @@ post "/delete_account" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? user = env.get? "user" + sid = env.get? "sid" referer = get_referer(env) if user user = user.as(User) - - challenge = env.params.body["challenge"]? + sid = sid.as(String) token = env.params.body["token"]? begin - validate_response(challenge, token, user.email, "delete_account", HMAC_KEY, PG_DB, locale) + validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale) rescue ex error_message = ex.message next templated "error" @@ -1893,12 +1901,13 @@ get "/clear_watch_history" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? user = env.get? "user" + sid = env.get? "sid" referer = get_referer(env) if user user = user.as(User) - - challenge, token = create_response(user.email, "clear_watch_history", HMAC_KEY, PG_DB) + sid = sid.as(String) + token = create_response(sid, {"clear_watch_history"}, HMAC_KEY, PG_DB) templated "clear_watch_history" else @@ -1910,16 +1919,16 @@ post "/clear_watch_history" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? user = env.get? "user" + sid = env.get? "sid" referer = get_referer(env) if user user = user.as(User) - - challenge = env.params.body["challenge"]? + sid = sid.as(String) token = env.params.body["token"]? begin - validate_response(challenge, token, user.email, "clear_watch_history", HMAC_KEY, PG_DB, locale) + validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale) rescue ex error_message = ex.message next templated "error" diff --git a/src/invidious/users.cr b/src/invidious/users.cr index f34d14ab..bad2b5c3 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -197,84 +197,79 @@ def create_user(sid, email, password) return user, sid end -def create_response(user_id, operation, key, db, expire = 6.hours) +def create_response(session, scopes, key, db, expire = 6.hours, use_nonce = false) expire = Time.now + expire - nonce = Random::Secure.hex(16) - db.exec("INSERT INTO nonces VALUES ($1, $2) ON CONFLICT DO NOTHING", nonce, expire) - challenge = "#{expire.to_unix}-#{nonce}-#{user_id}-#{operation}" - token = OpenSSL::HMAC.digest(:sha256, key, challenge) + token = { + "session" => session, + "expire" => expire.to_unix, + "scopes" => scopes, + } + + if use_nonce + nonce = Random::Secure.hex(16) + db.exec("INSERT INTO nonces VALUES ($1, $2) ON CONFLICT DO NOTHING", nonce, expire) + token["nonce"] = nonce + end - challenge = Base64.urlsafe_encode(challenge) - token = Base64.urlsafe_encode(token) + token["signature"] = sign_token(key, token) - return challenge, token + return token.to_json end def sign_token(key, hash) string_to_sign = [] of String + hash.each do |key, value| if key == "signature" next end + if value.is_a?(JSON::Any) + case value + when .as_a? + value = value.as_a.map { |item| item.as_s } + end + end + case value when Array string_to_sign << "#{key}=#{value.sort.join(",")}" + when Tuple + string_to_sign << "#{key}=#{value.to_a.sort.join(",")}" else string_to_sign << "#{key}=#{value}" end end string_to_sign = string_to_sign.sort.join("\n") - return Base64.encode(OpenSSL::HMAC.digest(:sha256, key, string_to_sign)).strip + return Base64.urlsafe_encode(OpenSSL::HMAC.digest(:sha256, key, string_to_sign)).strip end -def validate_response(challenge, token, user_id, operation, key, db, locale) - if !challenge - raise translate(locale, "Hidden field \"challenge\" is a required field") - end - +def validate_response(token, session, scope, key, db, locale) if !token raise translate(locale, "Hidden field \"token\" is a required field") end - challenge = Base64.decode_string(challenge) - if challenge.split("-").size == 4 - expire, nonce, challenge_user_id, challenge_operation = challenge.split("-") + token = JSON.parse(URI.unescape(token)).as_h - expire = expire.to_i? - expire ||= 0 - else - raise translate(locale, "Invalid challenge") + if token["signature"]? != sign_token(key, token) + raise translate(locale, "Invalid token") end - challenge = OpenSSL::HMAC.digest(:sha256, key, challenge) - challenge = Base64.urlsafe_encode(challenge) - - if nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", nonce, as: {String, Time}) + if token["nonce"]? && (nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", token["nonce"], as: {String, Time})) if nonce[1] > Time.now db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.new(1990, 1, 1), nonce[0]) else raise translate(locale, "Invalid token") end - else - raise translate(locale, "Invalid token") - end - - if challenge != token - raise translate(locale, "Invalid token") - end - - if challenge_operation != operation - raise translate(locale, "Invalid token") end - if challenge_user_id != user_id + if !token["scopes"].as_a.includes? scope.strip("/") raise translate(locale, "Invalid token") end - if expire < Time.now.to_unix + if token["expire"].as_i < Time.now.to_unix raise translate(locale, "Token is expired, please try again") end end @@ -331,7 +326,7 @@ def generate_captcha(key, db) return { question: image, - tokens: [create_response(answer, "sign_in", key, db)], + tokens: {create_response(answer, {"login"}, key, db, use_nonce: true)}, } end @@ -340,7 +335,7 @@ def generate_text_captcha(key, db) response = JSON.parse(response) tokens = response["a"].as_a.map do |answer| - create_response(answer.as_s, "sign_in", key, db) + create_response(answer.as_s, {"login"}, key, db, use_nonce: true) end return { diff --git a/src/invidious/views/clear_watch_history.ecr b/src/invidious/views/clear_watch_history.ecr index ede5e287..55555bee 100644 --- a/src/invidious/views/clear_watch_history.ecr +++ b/src/invidious/views/clear_watch_history.ecr @@ -19,7 +19,6 @@ </div> </div> - <input type="hidden" name="token" value="<%= token %>"> - <input type="hidden" name="challenge" value="<%= challenge %>"> + <input type="hidden" name="token" value="<%= URI.escape(token) %>"> </form> </div> diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 2dc0bea4..ae1ccb96 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -85,17 +85,19 @@ <div class="thumbnail"> <img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/> <% if env.get? "show_watched" %> - <p class="watched"> - <a onclick="mark_watched(this)" - data-id="<%= item.id %>" - onmouseenter='this["href"]="javascript:void(0)"' - href="/mark_watched?id=<%= item.id %>"> - <i onmouseenter='this.setAttribute("class", "icon ion-ios-eye-off")' - onmouseleave='this.setAttribute("class", "icon ion-ios-eye")' - class="icon ion-ios-eye"> - </i> - </a> - </p> + <form onsubmit="return false;" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post"> + <input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>"> + <p class="watched"> + <a onclick="mark_watched(this)" data-id="<%= item.id %>" href="#"> + <button type="submit" style="all:unset"> + <i onmouseenter='this.setAttribute("class", "icon ion-ios-eye-off")' + onmouseleave='this.setAttribute("class", "icon ion-ios-eye")' + class="icon ion-ios-eye"> + </i> + </button> + </a> + </p> + </form> <% end %> <% if item.responds_to?(:live_now) && item.live_now %> <p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr index df16658d..59849c1a 100644 --- a/src/invidious/views/components/subscribe_widget.ecr +++ b/src/invidious/views/components/subscribe_widget.ecr @@ -1,17 +1,21 @@ <% if user %> <% if subscriptions.includes? ucid %> <p> - <a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary" - href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>"> - <b><%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %></b> - </a> + <form onsubmit="return false;" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> + <input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>"> + <a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary" href="#"> + <b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b> + </a> + </form> </p> <% else %> <p> - <a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary" - href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>"> - <b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b> + <form onsubmit="return false;" action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> + <input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>"> + <a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary" href="#"> + <b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b> </a> + </form> </p> <% end %> <% else %> diff --git a/src/invidious/views/components/subscribe_widget_script.ecr b/src/invidious/views/components/subscribe_widget_script.ecr index 9bfd8ebb..8fe0a653 100644 --- a/src/invidious/views/components/subscribe_widget_script.ecr +++ b/src/invidious/views/components/subscribe_widget_script.ecr @@ -15,8 +15,9 @@ function subscribe(timeouts = 0) { var xhr = new XMLHttpRequest(); xhr.responseType = "json"; xhr.timeout = 20000; - xhr.open("GET", url, true); - xhr.send(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>"); var fallback = subscribe_button.innerHTML; subscribe_button.onclick = unsubscribe; @@ -50,8 +51,9 @@ function unsubscribe(timeouts = 0) { var xhr = new XMLHttpRequest(); xhr.responseType = "json"; xhr.timeout = 20000; - xhr.open("GET", url, true); - xhr.send(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>"); var fallback = subscribe_button.innerHTML; subscribe_button.onclick = subscribe; diff --git a/src/invidious/views/delete_account.ecr b/src/invidious/views/delete_account.ecr index 7cc8de9b..f49eaab9 100644 --- a/src/invidious/views/delete_account.ecr +++ b/src/invidious/views/delete_account.ecr @@ -19,7 +19,6 @@ </div> </div> - <input type="hidden" name="token" value="<%= token %>"> - <input type="hidden" name="challenge" value="<%= challenge %>"> + <input type="hidden" name="token" value="<%= URI.escape(token) %>"> </form> </div> diff --git a/src/invidious/views/history.ecr b/src/invidious/views/history.ecr index 9be40a0d..15a24f4d 100644 --- a/src/invidious/views/history.ecr +++ b/src/invidious/views/history.ecr @@ -28,14 +28,16 @@ <% else %> <div class="thumbnail"> <img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg"/> - <p class="watched"> - <a onclick="mark_unwatched(this)" - data-id="<%= item %>" - onmouseenter='this["href"]="javascript:void(0)"' - href="/mark_unwatched?id=<%= item %>"> - <i class="icon ion-md-trash"></i> - </a> - </p> + <form onsubmit="return false;" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post"> + <input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>"> + <p class="watched"> + <a onclick="mark_unwatched(this)" data-id="<%= item %>" href="#"> + <button type="submit" style="all:unset"> + <i class="icon ion-md-trash"></i> + </button> + </a> + </p> + </form> </div> <p></p> <% end %> @@ -48,17 +50,18 @@ <script> function mark_unwatched(target) { - var tile = target.parentNode.parentNode.parentNode.parentNode; + var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; tile.style.display = "none"; var count = document.getElementById("count") count.innerText = count.innerText - 1; - var url = "/mark_unwatched?redirect=false&id=" + target.getAttribute("data-id"); + var url = "/watch_ajax?action_mark_unwatched=1&redirect=false&id=" + target.getAttribute("data-id"); var xhr = new XMLHttpRequest(); xhr.responseType = "json"; xhr.timeout = 20000; - xhr.open("GET", url, true); - xhr.send(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>"); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { diff --git a/src/invidious/views/login.ecr b/src/invidious/views/login.ecr index ec5dc147..f6a131f9 100644 --- a/src/invidious/views/login.ecr +++ b/src/invidious/views/login.ecr @@ -42,8 +42,7 @@ <% captcha = captcha.not_nil! %> <img style="width:100%" src='<%= captcha[:question] %>'/> <% captcha[:tokens].each_with_index do |token, i| %> - <input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>"> - <input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>"> + <input type="hidden" name="token[<%= i %>]" value="<%= URI.escape(token) %>"> <% end %> <input type="hidden" name="captcha_type" value="image"> <label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label> @@ -51,8 +50,7 @@ <% when "text" %> <% captcha = captcha.not_nil! %> <% captcha[:tokens].each_with_index do |token, i| %> - <input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>"> - <input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>"> + <input type="hidden" name="token[<%= i %>]" value="<%= URI.escape(token) %>"> <% end %> <input type="hidden" name="captcha_type" value="text"> <label for="answer"><%= captcha[:question] %></label> diff --git a/src/invidious/views/subscription_manager.ecr b/src/invidious/views/subscription_manager.ecr index 0f9762f9..c9b90189 100644 --- a/src/invidious/views/subscription_manager.ecr +++ b/src/invidious/views/subscription_manager.ecr @@ -31,12 +31,12 @@ <div class="pure-u-2-5"></div> <div class="pure-u-1-5" style="text-align: right;"> <h3 style="padding-right: 0.5em"> - <a onclick="remove_subscription(this)" - data-id="<%= channel.id %>" - onmouseenter='this["href"]="javascript:void(0)"' - href="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>"> - <%= translate(locale, "unsubscribe") %> - </a> + <form onsubmit="return false;" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post"> + <input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>"> + <a onclick="remove_subscription(this)" data-ucid="<%= channel.id %>" href="#"> + <input style="all:unset" type="submit" value="<%= translate(locale, "unsubscribe") %>"> + </a> + </form> </h3> </div> </div> @@ -49,17 +49,18 @@ <script> function remove_subscription(target) { - var row = target.parentNode.parentNode.parentNode.parentNode; + var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; row.style.display = "none"; var count = document.getElementById("count") count.innerText = count.innerText - 1; - var url = "/subscription_ajax?action_remove_subscriptions=1&redirect=false&c=" + target.getAttribute("data-id"); + var url = "/subscription_ajax?action_remove_subscriptions=1&redirect=false&referer=<%= env.get("current_page") %>&c=" + target.getAttribute("data-ucid"); var xhr = new XMLHttpRequest(); xhr.responseType = "json"; xhr.timeout = 20000; - xhr.open("GET", url, true); - xhr.send(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>"); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { diff --git a/src/invidious/views/subscriptions.ecr b/src/invidious/views/subscriptions.ecr index 88f0f6c4..7fc83b6d 100644 --- a/src/invidious/views/subscriptions.ecr +++ b/src/invidious/views/subscriptions.ecr @@ -53,15 +53,16 @@ <script> function mark_watched(target) { - var tile = target.parentNode.parentNode.parentNode.parentNode; + var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; tile.style.display = "none"; - var url = "/mark_watched?redirect=false&id=" + target.getAttribute("data-id"); + var url = "/watch_ajax?action_mark_watched=1&redirect=false&id=" + target.getAttribute("data-id"); var xhr = new XMLHttpRequest(); xhr.responseType = "json"; xhr.timeout = 20000; - xhr.open("GET", url, true); - xhr.send(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>"); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index df5b94f4..6ab3936c 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -2,143 +2,146 @@ <html> <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta name="referrer" content="no-referrer"> - <%= yield_content "header" %> - <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> - <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> - <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> - <link rel="manifest" href="/site.webmanifest"> - <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#575757"> - <meta name="msapplication-TileColor" content="#575757"> - <meta name="theme-color" content="#575757"> - <link title="Invidious" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml"> - <link rel="stylesheet" href="/css/pure-min.css"> - <link rel="stylesheet" href="/css/grids-responsive-min.css"> - <link rel="stylesheet" href="/css/ionicons.min.css"> - <link rel="stylesheet" href="/css/default.css"> - <% if env.get("preferences").as(Preferences).dark_mode %> - <link rel="stylesheet" href="/css/darktheme.css"> - <% else %> - <link rel="stylesheet" href="/css/lighttheme.css"> - <% end %> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="referrer" content="no-referrer"> + <%= yield_content "header" %> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#575757"> + <meta name="msapplication-TileColor" content="#575757"> + <meta name="theme-color" content="#575757"> + <link title="Invidious" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml"> + <link rel="stylesheet" href="/css/pure-min.css"> + <link rel="stylesheet" href="/css/grids-responsive-min.css"> + <link rel="stylesheet" href="/css/ionicons.min.css"> + <link rel="stylesheet" href="/css/default.css"> + <% if env.get("preferences").as(Preferences).dark_mode %> + <link rel="stylesheet" href="/css/darktheme.css"> + <% else %> + <link rel="stylesheet" href="/css/lighttheme.css"> + <% end %> </head> <% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %> <body> - <div class="pure-g"> - <div class="pure-u-1 pure-u-md-2-24"></div> - <div class="pure-u-1 pure-u-md-20-24"> - <div class="pure-g navbar h-box"> - <div class="pure-u-1 pure-u-md-4-24"> - <a href="/" class="index-link pure-menu-heading">Invidious</a> + <div class="pure-g"> + <div class="pure-u-1 pure-u-md-2-24"></div> + <div class="pure-u-1 pure-u-md-20-24"> + <div class="pure-g navbar h-box"> + <div class="pure-u-1 pure-u-md-4-24"> + <a href="/" class="index-link pure-menu-heading">Invidious</a> + </div> + <div class="pure-u-1 pure-u-md-12-24 searchbar"> + <form class="pure-form" action="/search" method="get"> + <fieldset> + <input type="search" style="width:100%;" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } || env.params.query["q"]?.try {|x| HTML.escape(x)} %>"> + </fieldset> + </form> + </div> + <div class="pure-u-1 pure-u-md-8-24 user-field"> + <% if env.get? "user" %> + <div class="pure-u-1-4"> + <a href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> + <% if env.get("preferences").as(Preferences).dark_mode %> + <i class="icon ion-ios-sunny"></i> + <% else %> + <i class="icon ion-ios-moon"></i> + <% end %> + </a> + </div> + <div class="pure-u-1-4"> + <a title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading"> + <% notification_count = env.get("user").as(User).notifications.size %> + <% if notification_count > 0 %> + <%= notification_count %> <i class="icon ion-ios-notifications"></i> + <% else %> + <i class="icon ion-ios-notifications-outline"></i> + <% end %> + </a> + </div> + <div class="pure-u-1-4"> + <a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> + <i class="icon ion-ios-cog"></i> + </a> + </div> + <div class="pure-u-1-4"> + <form action="/signout?referer=<%= env.get?("current_page") %>" method="post"> + <input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>"> + <a class="pure-menu-heading" href="#"> + <input style="all:unset" type="submit" value="<%= translate(locale, "Sign out") %>"> + </a> + </form> + </div> + <% else %> + <div class="pure-u-1-3"> + <a href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> + <% if env.get("preferences").as(Preferences).dark_mode %> + <i class="icon ion-ios-sunny"></i> + <% else %> + <i class="icon ion-ios-moon"></i> + <% end %> + </a> + </div> + <div class="pure-u-1-3"> + <a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> + <i class="icon ion-ios-cog"></i> + </a> + </div> + <% if config.login_enabled %> + <div class="pure-u-1-3"> + <a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> + <%= translate(locale, "Login") %> + </a> + </div> + <% end %> + <% end %> + </div> + </div> + <%= content %> + <div class="footer"> + <div class="pure-g"> + <div class="pure-u-1 pure-u-md-1-3"> + <a href="https://github.com/omarroth/invidious"> + <%= translate(locale, "Released under the AGPLv3 by Omar Roth.") %> + </a> + </div> + <div class="pure-u-1 pure-u-md-1-3"> + <i class="icon ion-logo-bitcoin"></i> + BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</div> + <div class="pure-u-1 pure-u-md-1-3"> + <i class="icon ion-logo-bitcoin"></i> + BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</div> + <div class="pure-u-1 pure-u-md-1-3"> + <i class="icon ion-logo-usd"></i> + <a href="https://liberapay.com/omarroth">Liberapay</a> + / + <a href="https://patreon.com/omarroth">Patreon</a> + </div> + <div class="pure-u-1 pure-u-md-1-3"> + <i class="icon ion-logo-javascript"></i> + <a rel="jslicense" href="/licenses"> + <%= translate(locale, "View JavaScript license information.") %> + </a> + / + <i class="icon ion-ios-paper"></i> + <a href="/privacy"> + <%= translate(locale, "View privacy policy.") %> + </a> + </div> + <div class="pure-u-1 pure-u-md-1-3"> + <i class="icon ion-logo-github"></i> + <%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> + <i class="icon ion-logo-github"></i> + <%= CURRENT_BRANCH %></div> + </div> + </div> </div> - <div class="pure-u-1 pure-u-md-12-24 searchbar"> - <form class="pure-form" action="/search" method="get"> - <fieldset> - <input type="search" style="width:100%;" name="q" placeholder="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } || env.params.query["q"]?.try {|x| HTML.escape(x)} %>"> - </fieldset> - </form> - </div> - <div class="pure-u-1 pure-u-md-8-24 user-field"> - <% if env.get? "user" %> - <div class="pure-u-1-4"> - <a href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode %> - <i class="icon ion-ios-sunny"></i> - <% else %> - <i class="icon ion-ios-moon"></i> - <% end %> - </a> - </div> - <div class="pure-u-1-4"> - <a title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading"> - <% notification_count = env.get("user").as(User).notifications.size %> - <% if notification_count > 0 %> - <%= notification_count %> <i class="icon ion-ios-notifications"></i> - <% else %> - <i class="icon ion-ios-notifications-outline"></i> - <% end %> - </a> - </div> - <div class="pure-u-1-4"> - <a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> - <i class="icon ion-ios-cog"></i> - </a> - </div> - <div class="pure-u-1-4"> - <a href="/signout?referer=<%= env.get?("current_page") %>&token=<%= env.get?("token") %>&challenge=<%= env.get?("challenge") %>" class="pure-menu-heading"> - <%= translate(locale, "Sign out") %> - </a> - </div> - <% else %> - <div class="pure-u-1-3"> - <a href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode %> - <i class="icon ion-ios-sunny"></i> - <% else %> - <i class="icon ion-ios-moon"></i> - <% end %> - </a> - </div> - <div class="pure-u-1-3"> - <a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> - <i class="icon ion-ios-cog"></i> - </a> - </div> - <% if config.login_enabled %> - <div class="pure-u-1-3"> - <a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> - <%= translate(locale, "Login") %> - </a> - </div> - <% end %> - <% end %> - </div> - </div> - <%= content %> - <div class="footer"> - <div class="pure-g"> - <div class="pure-u-1 pure-u-md-1-3"> - <a href="https://github.com/omarroth/invidious"> - <%= translate(locale, "Released under the AGPLv3 by Omar Roth.") %> - </a> - </div> - <div class="pure-u-1 pure-u-md-1-3"> - <i class="icon ion-logo-bitcoin"></i> - BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</div> - <div class="pure-u-1 pure-u-md-1-3"> - <i class="icon ion-logo-bitcoin"></i> - BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</div> - <div class="pure-u-1 pure-u-md-1-3"> - <i class="icon ion-logo-usd"></i> - <a href="https://liberapay.com/omarroth">Liberapay</a> - / - <a href="https://patreon.com/omarroth">Patreon</a> - </div> - <div class="pure-u-1 pure-u-md-1-3"> - <i class="icon ion-logo-javascript"></i> - <a rel="jslicense" href="/licenses"> - <%= translate(locale, "View JavaScript license information.") %> - </a> - / - <i class="icon ion-ios-paper"></i> - <a href="/privacy"> - <%= translate(locale, "View privacy policy.") %> - </a> - </div> - <div class="pure-u-1 pure-u-md-1-3"> - <i class="icon ion-logo-github"></i> - <%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> - <i class="icon ion-logo-github"></i> - <%= CURRENT_BRANCH %></div> - </div> - </div> + <div class="pure-u-1 pure-u-md-2-24"></div> </div> - <div class="pure-u-1 pure-u-md-2-24"></div> - </div> </body> </html> |
