summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOmar Roth <omarroth@hotmail.com>2019-04-15 23:23:40 -0500
committerOmar Roth <omarroth@hotmail.com>2019-04-15 23:23:40 -0500
commit26168a9520c8bc2a03f631eb0e53c3ee086d8c98 (patch)
tree5575537ee9876400ca7fa5630190d2ff34684907
parent698dfca3199396f59fcd881745f87e79d46f7b58 (diff)
downloadinvidious-26168a9520c8bc2a03f631eb0e53c3ee086d8c98.tar.gz
invidious-26168a9520c8bc2a03f631eb0e53c3ee086d8c98.tar.bz2
invidious-26168a9520c8bc2a03f631eb0e53c3ee086d8c98.zip
Refactor CSRF tokens (using format in #473)
-rw-r--r--src/invidious.cr169
-rw-r--r--src/invidious/users.cr73
-rw-r--r--src/invidious/views/clear_watch_history.ecr3
-rw-r--r--src/invidious/views/components/item.ecr24
-rw-r--r--src/invidious/views/components/subscribe_widget.ecr18
-rw-r--r--src/invidious/views/components/subscribe_widget_script.ecr10
-rw-r--r--src/invidious/views/delete_account.ecr3
-rw-r--r--src/invidious/views/history.ecr27
-rw-r--r--src/invidious/views/login.ecr6
-rw-r--r--src/invidious/views/subscription_manager.ecr21
-rw-r--r--src/invidious/views/subscriptions.ecr9
-rw-r--r--src/invidious/views/template.ecr263
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>