mirror of
https://github.com/iv-org/invidious.git
synced 2024-12-23 14:03:37 +00:00
Refactor CSRF tokens (using format in #473)
This commit is contained in:
parent
698dfca319
commit
26168a9520
169
src/invidious.cr
169
src/invidious.cr
@ -195,39 +195,33 @@ before_all do |env|
|
|||||||
end
|
end
|
||||||
|
|
||||||
if env.request.cookies.has_key? "SID"
|
if env.request.cookies.has_key? "SID"
|
||||||
headers = HTTP::Headers.new
|
|
||||||
headers["Cookie"] = env.request.headers["Cookie"]
|
|
||||||
|
|
||||||
sid = env.request.cookies["SID"].value
|
sid = env.request.cookies["SID"].value
|
||||||
|
|
||||||
# Invidious users only have SID
|
# Invidious users only have SID
|
||||||
if !env.request.cookies.has_key? "SSID"
|
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 = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
|
||||||
|
|
||||||
if email
|
|
||||||
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
|
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)
|
token = create_response(sid, {"signout", "watch_ajax", "subscription_ajax"}, HMAC_KEY, PG_DB, 1.week)
|
||||||
|
|
||||||
env.set "challenge", challenge
|
|
||||||
env.set "token", token
|
|
||||||
|
|
||||||
preferences = user.preferences
|
preferences = user.preferences
|
||||||
|
|
||||||
env.set "user", user
|
|
||||||
env.set "sid", sid
|
env.set "sid", sid
|
||||||
|
env.set "token", token
|
||||||
|
env.set "user", user
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
headers = HTTP::Headers.new
|
||||||
|
headers["Cookie"] = env.request.headers["Cookie"]
|
||||||
|
|
||||||
begin
|
begin
|
||||||
user, sid = get_user(sid, headers, PG_DB, false)
|
user, sid = get_user(sid, headers, PG_DB, false)
|
||||||
|
token = create_response(sid, {"signout", "watch_ajax", "subscription_ajax"}, HMAC_KEY, PG_DB, 1.week)
|
||||||
challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week)
|
|
||||||
env.set "challenge", challenge
|
|
||||||
env.set "token", token
|
|
||||||
|
|
||||||
preferences = user.preferences
|
preferences = user.preferences
|
||||||
|
|
||||||
env.set "user", user
|
|
||||||
env.set "sid", sid
|
env.set "sid", sid
|
||||||
|
env.set "token", token
|
||||||
|
env.set "user", user
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -826,7 +820,7 @@ post "/login" do |env|
|
|||||||
when "google"
|
when "google"
|
||||||
tfa_code = env.params.body["tfa"]?.try &.lchop("G-")
|
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
|
begin
|
||||||
client = make_client(LOGIN_URL)
|
client = make_client(LOGIN_URL)
|
||||||
headers = HTTP::Headers.new
|
headers = HTTP::Headers.new
|
||||||
@ -1091,8 +1085,7 @@ post "/login" do |env|
|
|||||||
next templated "login"
|
next templated "login"
|
||||||
end
|
end
|
||||||
|
|
||||||
challenges = env.params.body.select { |k, v| k.match(/^challenge\[\d+\]$/) }
|
tokens = env.params.body.select { |k, v| k.match(/^token\[\d+\]$/) }.map { |k, v| v }
|
||||||
tokens = env.params.body.select { |k, v| k.match(/^token\[\d+\]$/) }
|
|
||||||
|
|
||||||
answer ||= ""
|
answer ||= ""
|
||||||
captcha_type ||= "image"
|
captcha_type ||= "image"
|
||||||
@ -1102,11 +1095,8 @@ post "/login" do |env|
|
|||||||
answer = answer.lstrip('0')
|
answer = answer.lstrip('0')
|
||||||
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
||||||
|
|
||||||
challenge = env.params.body["challenge[0]"]?
|
|
||||||
token = env.params.body["token[0]"]?
|
|
||||||
|
|
||||||
begin
|
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
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
next templated "error"
|
next templated "error"
|
||||||
@ -1117,11 +1107,9 @@ post "/login" do |env|
|
|||||||
found_valid_captcha = false
|
found_valid_captcha = false
|
||||||
|
|
||||||
error_message = translate(locale, "Invalid CAPTCHA")
|
error_message = translate(locale, "Invalid CAPTCHA")
|
||||||
challenges.each_with_index do |challenge, i|
|
tokens.each_with_index do |token, i|
|
||||||
begin
|
begin
|
||||||
challenge = challenge[1]
|
validate_response(token, answer, env.request.path, HMAC_KEY, PG_DB, locale)
|
||||||
token = tokens[i][1]
|
|
||||||
validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB, locale)
|
|
||||||
found_valid_captcha = true
|
found_valid_captcha = true
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
@ -1191,27 +1179,25 @@ post "/login" do |env|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/signout" do |env|
|
post "/signout" do |env|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
referer = get_referer(env)
|
referer = get_referer(env)
|
||||||
|
|
||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
challenge = env.params.query["challenge"]?
|
token = env.params.body["token"]?
|
||||||
token = env.params.query["token"]?
|
|
||||||
|
|
||||||
begin
|
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
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
user = env.get("user").as(User)
|
|
||||||
sid = env.get("sid").as(String)
|
|
||||||
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", sid)
|
PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", sid)
|
||||||
|
|
||||||
env.request.cookies.each do |cookie|
|
env.request.cookies.each do |cookie|
|
||||||
@ -1426,55 +1412,62 @@ get "/toggle_theme" do |env|
|
|||||||
env.redirect referer
|
env.redirect referer
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/mark_watched" do |env|
|
post "/watch_ajax" do |env|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
referer = get_referer(env, "/feed/subscriptions")
|
referer = get_referer(env, "/feed/subscriptions")
|
||||||
|
|
||||||
|
redirect = env.params.query["redirect"]?
|
||||||
|
redirect ||= "true"
|
||||||
|
redirect = redirect == "true"
|
||||||
|
|
||||||
|
if !user
|
||||||
|
next env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
id = env.params.query["id"]?
|
id = env.params.query["id"]?
|
||||||
if !id
|
if !id
|
||||||
env.response.status_code = 400
|
env.response.status_code = 400
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect = env.params.query["redirect"]?
|
user = user.as(User)
|
||||||
redirect ||= "false"
|
sid = sid.as(String)
|
||||||
redirect = redirect == "true"
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
if user
|
begin
|
||||||
user = user.as(User)
|
validate_response(token, sid, env.request.path, HMAC_KEY, PG_DB, locale)
|
||||||
if !user.watched.includes? id
|
rescue ex
|
||||||
PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.email)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
if redirect
|
if env.params.query["action_mark_watched"]?
|
||||||
env.redirect referer
|
action = "action_mark_watched"
|
||||||
|
elsif env.params.query["action_mark_unwatched"]?
|
||||||
|
action = "action_mark_unwatched"
|
||||||
else
|
else
|
||||||
env.response.content_type = "application/json"
|
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")
|
|
||||||
|
|
||||||
id = env.params.query["id"]?
|
|
||||||
if !id
|
|
||||||
env.response.status_code = 400
|
|
||||||
next
|
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect = env.params.query["redirect"]?
|
case action
|
||||||
redirect ||= "false"
|
when "action_mark_watched"
|
||||||
redirect = redirect == "true"
|
if !user.watched.includes? id
|
||||||
|
PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.email)
|
||||||
if user
|
end
|
||||||
user = user.as(User)
|
when "action_mark_unwatched"
|
||||||
PG_DB.exec("UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2", id, user.email)
|
PG_DB.exec("UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2", id, user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1561,8 +1554,7 @@ get "/modify_notifications" do |env|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Add CSRF
|
post "/subscription_ajax" do |env|
|
||||||
get "/subscription_ajax" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
@ -1570,14 +1562,29 @@ get "/subscription_ajax" do |env|
|
|||||||
referer = get_referer(env, "/")
|
referer = get_referer(env, "/")
|
||||||
|
|
||||||
redirect = env.params.query["redirect"]?
|
redirect = env.params.query["redirect"]?
|
||||||
redirect ||= "false"
|
redirect ||= "true"
|
||||||
redirect = redirect == "true"
|
redirect = redirect == "true"
|
||||||
|
|
||||||
if !user && !sid
|
if !user
|
||||||
next env.redirect referer
|
next env.redirect referer
|
||||||
end
|
end
|
||||||
|
|
||||||
user = user.as(User)
|
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"]?
|
if env.params.query["action_create_subscription_to_channel"]?
|
||||||
action = "action_create_subscription_to_channel"
|
action = "action_create_subscription_to_channel"
|
||||||
@ -1653,7 +1660,7 @@ get "/subscription_manager" do |env|
|
|||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
sid = env.get? "sid"
|
sid = env.get? "sid"
|
||||||
referer = get_referer(env, "/")
|
referer = get_referer(env, "/subscription_manager")
|
||||||
|
|
||||||
if !user && !sid
|
if !user && !sid
|
||||||
next env.redirect referer
|
next env.redirect referer
|
||||||
@ -1843,12 +1850,13 @@ get "/delete_account" do |env|
|
|||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
referer = get_referer(env)
|
referer = get_referer(env)
|
||||||
|
|
||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
challenge, token = create_response(user.email, "delete_account", HMAC_KEY, PG_DB)
|
token = create_response(sid, {"delete_account"}, HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
templated "delete_account"
|
templated "delete_account"
|
||||||
else
|
else
|
||||||
@ -1860,16 +1868,16 @@ post "/delete_account" do |env|
|
|||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
referer = get_referer(env)
|
referer = get_referer(env)
|
||||||
|
|
||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
challenge = env.params.body["challenge"]?
|
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
begin
|
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
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
next templated "error"
|
next templated "error"
|
||||||
@ -1893,12 +1901,13 @@ get "/clear_watch_history" do |env|
|
|||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
referer = get_referer(env)
|
referer = get_referer(env)
|
||||||
|
|
||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
challenge, token = create_response(user.email, "clear_watch_history", HMAC_KEY, PG_DB)
|
token = create_response(sid, {"clear_watch_history"}, HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
templated "clear_watch_history"
|
templated "clear_watch_history"
|
||||||
else
|
else
|
||||||
@ -1910,16 +1919,16 @@ post "/clear_watch_history" do |env|
|
|||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
referer = get_referer(env)
|
referer = get_referer(env)
|
||||||
|
|
||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
challenge = env.params.body["challenge"]?
|
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
begin
|
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
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
next templated "error"
|
next templated "error"
|
||||||
|
@ -197,84 +197,79 @@ def create_user(sid, email, password)
|
|||||||
return user, sid
|
return user, sid
|
||||||
end
|
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
|
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 = {
|
||||||
token = OpenSSL::HMAC.digest(:sha256, key, challenge)
|
"session" => session,
|
||||||
|
"expire" => expire.to_unix,
|
||||||
|
"scopes" => scopes,
|
||||||
|
}
|
||||||
|
|
||||||
challenge = Base64.urlsafe_encode(challenge)
|
if use_nonce
|
||||||
token = Base64.urlsafe_encode(token)
|
nonce = Random::Secure.hex(16)
|
||||||
|
db.exec("INSERT INTO nonces VALUES ($1, $2) ON CONFLICT DO NOTHING", nonce, expire)
|
||||||
|
token["nonce"] = nonce
|
||||||
|
end
|
||||||
|
|
||||||
return challenge, token
|
token["signature"] = sign_token(key, token)
|
||||||
|
|
||||||
|
return token.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign_token(key, hash)
|
def sign_token(key, hash)
|
||||||
string_to_sign = [] of String
|
string_to_sign = [] of String
|
||||||
|
|
||||||
hash.each do |key, value|
|
hash.each do |key, value|
|
||||||
if key == "signature"
|
if key == "signature"
|
||||||
next
|
next
|
||||||
end
|
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
|
case value
|
||||||
when Array
|
when Array
|
||||||
string_to_sign << "#{key}=#{value.sort.join(",")}"
|
string_to_sign << "#{key}=#{value.sort.join(",")}"
|
||||||
|
when Tuple
|
||||||
|
string_to_sign << "#{key}=#{value.to_a.sort.join(",")}"
|
||||||
else
|
else
|
||||||
string_to_sign << "#{key}=#{value}"
|
string_to_sign << "#{key}=#{value}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
string_to_sign = string_to_sign.sort.join("\n")
|
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
|
end
|
||||||
|
|
||||||
def validate_response(challenge, token, user_id, operation, key, db, locale)
|
def validate_response(token, session, scope, key, db, locale)
|
||||||
if !challenge
|
|
||||||
raise translate(locale, "Hidden field \"challenge\" is a required field")
|
|
||||||
end
|
|
||||||
|
|
||||||
if !token
|
if !token
|
||||||
raise translate(locale, "Hidden field \"token\" is a required field")
|
raise translate(locale, "Hidden field \"token\" is a required field")
|
||||||
end
|
end
|
||||||
|
|
||||||
challenge = Base64.decode_string(challenge)
|
token = JSON.parse(URI.unescape(token)).as_h
|
||||||
if challenge.split("-").size == 4
|
|
||||||
expire, nonce, challenge_user_id, challenge_operation = challenge.split("-")
|
|
||||||
|
|
||||||
expire = expire.to_i?
|
if token["signature"]? != sign_token(key, token)
|
||||||
expire ||= 0
|
raise translate(locale, "Invalid token")
|
||||||
else
|
|
||||||
raise translate(locale, "Invalid challenge")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
challenge = OpenSSL::HMAC.digest(:sha256, key, challenge)
|
if token["nonce"]? && (nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", token["nonce"], as: {String, Time}))
|
||||||
challenge = Base64.urlsafe_encode(challenge)
|
|
||||||
|
|
||||||
if nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", nonce, as: {String, Time})
|
|
||||||
if nonce[1] > Time.now
|
if nonce[1] > Time.now
|
||||||
db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.new(1990, 1, 1), nonce[0])
|
db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.new(1990, 1, 1), nonce[0])
|
||||||
else
|
else
|
||||||
raise translate(locale, "Invalid token")
|
raise translate(locale, "Invalid token")
|
||||||
end
|
end
|
||||||
else
|
end
|
||||||
|
|
||||||
|
if !token["scopes"].as_a.includes? scope.strip("/")
|
||||||
raise translate(locale, "Invalid token")
|
raise translate(locale, "Invalid token")
|
||||||
end
|
end
|
||||||
|
|
||||||
if challenge != token
|
if token["expire"].as_i < Time.now.to_unix
|
||||||
raise translate(locale, "Invalid token")
|
|
||||||
end
|
|
||||||
|
|
||||||
if challenge_operation != operation
|
|
||||||
raise translate(locale, "Invalid token")
|
|
||||||
end
|
|
||||||
|
|
||||||
if challenge_user_id != user_id
|
|
||||||
raise translate(locale, "Invalid token")
|
|
||||||
end
|
|
||||||
|
|
||||||
if expire < Time.now.to_unix
|
|
||||||
raise translate(locale, "Token is expired, please try again")
|
raise translate(locale, "Token is expired, please try again")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -331,7 +326,7 @@ def generate_captcha(key, db)
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
question: image,
|
question: image,
|
||||||
tokens: [create_response(answer, "sign_in", key, db)],
|
tokens: {create_response(answer, {"login"}, key, db, use_nonce: true)},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -340,7 +335,7 @@ def generate_text_captcha(key, db)
|
|||||||
response = JSON.parse(response)
|
response = JSON.parse(response)
|
||||||
|
|
||||||
tokens = response["a"].as_a.map do |answer|
|
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
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="token" value="<%= token %>">
|
<input type="hidden" name="token" value="<%= URI.escape(token) %>">
|
||||||
<input type="hidden" name="challenge" value="<%= challenge %>">
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,17 +85,19 @@
|
|||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
|
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
|
||||||
<% if env.get? "show_watched" %>
|
<% if env.get? "show_watched" %>
|
||||||
<p class="watched">
|
<form onsubmit="return false;" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
<a onclick="mark_watched(this)"
|
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
||||||
data-id="<%= item.id %>"
|
<p class="watched">
|
||||||
onmouseenter='this["href"]="javascript:void(0)"'
|
<a onclick="mark_watched(this)" data-id="<%= item.id %>" href="#">
|
||||||
href="/mark_watched?id=<%= item.id %>">
|
<button type="submit" style="all:unset">
|
||||||
<i onmouseenter='this.setAttribute("class", "icon ion-ios-eye-off")'
|
<i onmouseenter='this.setAttribute("class", "icon ion-ios-eye-off")'
|
||||||
onmouseleave='this.setAttribute("class", "icon ion-ios-eye")'
|
onmouseleave='this.setAttribute("class", "icon ion-ios-eye")'
|
||||||
class="icon ion-ios-eye">
|
class="icon ion-ios-eye">
|
||||||
</i>
|
</i>
|
||||||
</a>
|
</button>
|
||||||
</p>
|
</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if item.responds_to?(:live_now) && item.live_now %>
|
<% if item.responds_to?(:live_now) && item.live_now %>
|
||||||
<p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
|
<p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
<% if user %>
|
<% if user %>
|
||||||
<% if subscriptions.includes? ucid %>
|
<% if subscriptions.includes? ucid %>
|
||||||
<p>
|
<p>
|
||||||
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary"
|
<form onsubmit="return false;" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
||||||
<b><%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %></b>
|
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary" href="#">
|
||||||
</a>
|
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
</p>
|
</p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>
|
<p>
|
||||||
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary"
|
<form onsubmit="return false;" action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
||||||
<b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b>
|
<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>
|
</a>
|
||||||
|
</form>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
@ -15,8 +15,9 @@ function subscribe(timeouts = 0) {
|
|||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = "json";
|
xhr.responseType = "json";
|
||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("GET", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.send();
|
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;
|
var fallback = subscribe_button.innerHTML;
|
||||||
subscribe_button.onclick = unsubscribe;
|
subscribe_button.onclick = unsubscribe;
|
||||||
@ -50,8 +51,9 @@ function unsubscribe(timeouts = 0) {
|
|||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = "json";
|
xhr.responseType = "json";
|
||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("GET", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.send();
|
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;
|
var fallback = subscribe_button.innerHTML;
|
||||||
subscribe_button.onclick = subscribe;
|
subscribe_button.onclick = subscribe;
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="token" value="<%= token %>">
|
<input type="hidden" name="token" value="<%= URI.escape(token) %>">
|
||||||
<input type="hidden" name="challenge" value="<%= challenge %>">
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,14 +28,16 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg"/>
|
<img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg"/>
|
||||||
<p class="watched">
|
<form onsubmit="return false;" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
<a onclick="mark_unwatched(this)"
|
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
||||||
data-id="<%= item %>"
|
<p class="watched">
|
||||||
onmouseenter='this["href"]="javascript:void(0)"'
|
<a onclick="mark_unwatched(this)" data-id="<%= item %>" href="#">
|
||||||
href="/mark_unwatched?id=<%= item %>">
|
<button type="submit" style="all:unset">
|
||||||
<i class="icon ion-md-trash"></i>
|
<i class="icon ion-md-trash"></i>
|
||||||
</a>
|
</button>
|
||||||
</p>
|
</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<p></p>
|
<p></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -48,17 +50,18 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function mark_unwatched(target) {
|
function mark_unwatched(target) {
|
||||||
var tile = target.parentNode.parentNode.parentNode.parentNode;
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
tile.style.display = "none";
|
tile.style.display = "none";
|
||||||
var count = document.getElementById("count")
|
var count = document.getElementById("count")
|
||||||
count.innerText = count.innerText - 1;
|
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();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = "json";
|
xhr.responseType = "json";
|
||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("GET", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.send();
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
|
@ -42,8 +42,7 @@
|
|||||||
<% captcha = captcha.not_nil! %>
|
<% captcha = captcha.not_nil! %>
|
||||||
<img style="width:100%" src='<%= captcha[:question] %>'/>
|
<img style="width:100%" src='<%= captcha[:question] %>'/>
|
||||||
<% captcha[:tokens].each_with_index do |token, i| %>
|
<% captcha[:tokens].each_with_index do |token, i| %>
|
||||||
<input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>">
|
<input type="hidden" name="token[<%= i %>]" value="<%= URI.escape(token) %>">
|
||||||
<input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>">
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<input type="hidden" name="captcha_type" value="image">
|
<input type="hidden" name="captcha_type" value="image">
|
||||||
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
|
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
|
||||||
@ -51,8 +50,7 @@
|
|||||||
<% when "text" %>
|
<% when "text" %>
|
||||||
<% captcha = captcha.not_nil! %>
|
<% captcha = captcha.not_nil! %>
|
||||||
<% captcha[:tokens].each_with_index do |token, i| %>
|
<% captcha[:tokens].each_with_index do |token, i| %>
|
||||||
<input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>">
|
<input type="hidden" name="token[<%= i %>]" value="<%= URI.escape(token) %>">
|
||||||
<input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>">
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<input type="hidden" name="captcha_type" value="text">
|
<input type="hidden" name="captcha_type" value="text">
|
||||||
<label for="answer"><%= captcha[:question] %></label>
|
<label for="answer"><%= captcha[:question] %></label>
|
||||||
|
@ -31,12 +31,12 @@
|
|||||||
<div class="pure-u-2-5"></div>
|
<div class="pure-u-2-5"></div>
|
||||||
<div class="pure-u-1-5" style="text-align: right;">
|
<div class="pure-u-1-5" style="text-align: right;">
|
||||||
<h3 style="padding-right: 0.5em">
|
<h3 style="padding-right: 0.5em">
|
||||||
<a onclick="remove_subscription(this)"
|
<form onsubmit="return false;" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
data-id="<%= channel.id %>"
|
<input type="hidden" name="token" value="<%= URI.escape(env.get?("token").try &.as(String) || "") %>">
|
||||||
onmouseenter='this["href"]="javascript:void(0)"'
|
<a onclick="remove_subscription(this)" data-ucid="<%= channel.id %>" href="#">
|
||||||
href="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>">
|
<input style="all:unset" type="submit" value="<%= translate(locale, "unsubscribe") %>">
|
||||||
<%= translate(locale, "unsubscribe") %>
|
</a>
|
||||||
</a>
|
</form>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,17 +49,18 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function remove_subscription(target) {
|
function remove_subscription(target) {
|
||||||
var row = target.parentNode.parentNode.parentNode.parentNode;
|
var row = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
row.style.display = "none";
|
row.style.display = "none";
|
||||||
var count = document.getElementById("count")
|
var count = document.getElementById("count")
|
||||||
count.innerText = count.innerText - 1;
|
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();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = "json";
|
xhr.responseType = "json";
|
||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("GET", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.send();
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
|
@ -53,15 +53,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function mark_watched(target) {
|
function mark_watched(target) {
|
||||||
var tile = target.parentNode.parentNode.parentNode.parentNode;
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
tile.style.display = "none";
|
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();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = "json";
|
xhr.responseType = "json";
|
||||||
xhr.timeout = 20000;
|
xhr.timeout = 20000;
|
||||||
xhr.open("GET", url, true);
|
xhr.open("POST", url, true);
|
||||||
xhr.send();
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
xhr.send("token=<%= URI.escape(env.get?("token").try &.as(String) || "") %>");
|
||||||
|
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
|
@ -2,143 +2,146 @@
|
|||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="referrer" content="no-referrer">
|
<meta name="referrer" content="no-referrer">
|
||||||
<%= yield_content "header" %>
|
<%= yield_content "header" %>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<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="32x32" href="/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#575757">
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#575757">
|
||||||
<meta name="msapplication-TileColor" content="#575757">
|
<meta name="msapplication-TileColor" content="#575757">
|
||||||
<meta name="theme-color" content="#575757">
|
<meta name="theme-color" content="#575757">
|
||||||
<link title="Invidious" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml">
|
<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/pure-min.css">
|
||||||
<link rel="stylesheet" href="/css/grids-responsive-min.css">
|
<link rel="stylesheet" href="/css/grids-responsive-min.css">
|
||||||
<link rel="stylesheet" href="/css/ionicons.min.css">
|
<link rel="stylesheet" href="/css/ionicons.min.css">
|
||||||
<link rel="stylesheet" href="/css/default.css">
|
<link rel="stylesheet" href="/css/default.css">
|
||||||
<% if env.get("preferences").as(Preferences).dark_mode %>
|
<% if env.get("preferences").as(Preferences).dark_mode %>
|
||||||
<link rel="stylesheet" href="/css/darktheme.css">
|
<link rel="stylesheet" href="/css/darktheme.css">
|
||||||
<% else %>
|
<% else %>
|
||||||
<link rel="stylesheet" href="/css/lighttheme.css">
|
<link rel="stylesheet" href="/css/lighttheme.css">
|
||||||
<% end %>
|
<% end %>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %>
|
<% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||||
<div class="pure-u-1 pure-u-md-20-24">
|
<div class="pure-u-1 pure-u-md-20-24">
|
||||||
<div class="pure-g navbar h-box">
|
<div class="pure-g navbar h-box">
|
||||||
<div class="pure-u-1 pure-u-md-4-24">
|
<div class="pure-u-1 pure-u-md-4-24">
|
||||||
<a href="/" class="index-link pure-menu-heading">Invidious</a>
|
<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>
|
||||||
<div class="pure-u-1 pure-u-md-12-24 searchbar">
|
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||||
<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>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user