Require OAuth for search engine checksmain
@@ -7,3 +7,4 @@ __pycache__ | |||||
.earwigbot | .earwigbot | ||||
logs/* | logs/* | ||||
!logs/.gitinclude | !logs/.gitinclude | ||||
config.py |
@@ -13,6 +13,7 @@ Dependencies | |||||
* [flask-mako](https://pythonhosted.org/Flask-Mako/) >= 0.3 | * [flask-mako](https://pythonhosted.org/Flask-Mako/) >= 0.3 | ||||
* [mako](https://www.makotemplates.org/) >= 0.7.2 | * [mako](https://www.makotemplates.org/) >= 0.7.2 | ||||
* [mwparserfromhell](https://github.com/earwig/mwparserfromhell) >= 0.3 | * [mwparserfromhell](https://github.com/earwig/mwparserfromhell) >= 0.3 | ||||
* [mwoauth](https://github.com/mediawiki-utilities/python-mwoauth) == 0.3.8 | |||||
* [oursql](https://pythonhosted.org/oursql/) >= 0.9.3.1 | * [oursql](https://pythonhosted.org/oursql/) >= 0.9.3.1 | ||||
* [requests](https://requests.readthedocs.io/) >= 2.9.1 | * [requests](https://requests.readthedocs.io/) >= 2.9.1 | ||||
* [SQLAlchemy](https://www.sqlalchemy.org/) >= 0.9.6 | * [SQLAlchemy](https://www.sqlalchemy.org/) >= 0.9.6 | ||||
@@ -1,18 +1,19 @@ | |||||
#! /usr/bin/env python | #! /usr/bin/env python | ||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
from functools import wraps | from functools import wraps | ||||
from hashlib import md5 | from hashlib import md5 | ||||
from json import dumps | from json import dumps | ||||
from logging import DEBUG, INFO, getLogger | from logging import DEBUG, INFO, getLogger | ||||
from logging.handlers import TimedRotatingFileHandler | from logging.handlers import TimedRotatingFileHandler | ||||
from multiprocessing import Value | |||||
from os import path | from os import path | ||||
from time import asctime | from time import asctime | ||||
from traceback import format_exc | from traceback import format_exc | ||||
from urllib import quote_plus, quote | |||||
from earwigbot.bot import Bot | from earwigbot.bot import Bot | ||||
from earwigbot.wiki.copyvios import globalize | from earwigbot.wiki.copyvios import globalize | ||||
from flask import Flask, g, make_response, request | |||||
from flask import Flask, g, make_response, request, redirect, session | |||||
from flask_mako import MakoTemplates, render_template, TemplateError | from flask_mako import MakoTemplates, render_template, TemplateError | ||||
from copyvios.api import format_api_error, handle_api_request | from copyvios.api import format_api_error, handle_api_request | ||||
@@ -21,12 +22,14 @@ from copyvios.cookies import parse_cookies | |||||
from copyvios.misc import cache, get_notice | from copyvios.misc import cache, get_notice | ||||
from copyvios.settings import process_settings | from copyvios.settings import process_settings | ||||
from copyvios.sites import update_sites | from copyvios.sites import update_sites | ||||
from copyvios.auth import oauth_login_start, oauth_login_end, clear_login_session | |||||
app = Flask(__name__) | app = Flask(__name__) | ||||
MakoTemplates(app) | MakoTemplates(app) | ||||
hand = TimedRotatingFileHandler("logs/app.log", when="midnight", backupCount=7) | hand = TimedRotatingFileHandler("logs/app.log", when="midnight", backupCount=7) | ||||
hand.setLevel(DEBUG) | hand.setLevel(DEBUG) | ||||
app.config.from_pyfile("config.py", True) | |||||
app.logger.addHandler(hand) | app.logger.addHandler(hand) | ||||
app.logger.info(u"Flask server started " + asctime()) | app.logger.info(u"Flask server started " + asctime()) | ||||
app._hash_cache = {} | app._hash_cache = {} | ||||
@@ -52,6 +55,12 @@ def setup_app(): | |||||
cache.background_data = {} | cache.background_data = {} | ||||
cache.last_background_updates = {} | cache.last_background_updates = {} | ||||
oauth_config = cache.bot.config.wiki.get('copyvios', {}).get('oauth', {}) | |||||
if oauth_config.get('consumer_token') is None: | |||||
raise ValueError("No OAuth consumer token is configured (config.wiki.copyvios.oauth.consumer_token).") | |||||
if oauth_config.get('consumer_secret') is None: | |||||
raise ValueError("No OAuth consumer secret is configured (config.wiki.copyvios.oauth.consumer_secret).") | |||||
globalize(num_workers=8) | globalize(num_workers=8) | ||||
@app.before_request | @app.before_request | ||||
@@ -101,10 +110,50 @@ def index(): | |||||
notice = get_notice() | notice = get_notice() | ||||
update_sites() | update_sites() | ||||
query = do_check() | query = do_check() | ||||
if query.submitted and query.error == "not logged in": | |||||
return redirect("/login?next=" + quote("/?" + request.query_string), 302) | |||||
return render_template( | return render_template( | ||||
"index.mako", notice=notice, query=query, result=query.result, | "index.mako", notice=notice, query=query, result=query.result, | ||||
turnitin_result=query.turnitin_result) | turnitin_result=query.turnitin_result) | ||||
@app.route("/login", methods=["GET", "POST"]) | |||||
@catch_errors | |||||
def login(): | |||||
try: | |||||
redirect_url = oauth_login_start() if request.method == "POST" else None | |||||
if redirect_url: | |||||
return redirect(redirect_url, 302) | |||||
except Exception as e: | |||||
app.log_exception(e) | |||||
print e.message | |||||
kwargs = {"error": e.message} | |||||
else: | |||||
if session.get("username") is not None: | |||||
return redirect("/", 302) | |||||
kwargs = {"error": request.args.get("error")} | |||||
return render_template("login.mako", **kwargs) | |||||
@app.route("/logout", methods=["GET", "POST"]) | |||||
@catch_errors | |||||
def logout(): | |||||
if request.method == "POST": | |||||
clear_login_session() | |||||
return redirect("/", 302) | |||||
else: | |||||
return render_template("logout.mako") | |||||
@app.route("/oauth-callback") | |||||
@catch_errors | |||||
def oauth_callback(): | |||||
try: | |||||
next_url = oauth_login_end() | |||||
except Exception as e: | |||||
app.log_exception(e) | |||||
return redirect("/login?error=" + quote_plus(e.message), 302) | |||||
else: | |||||
return redirect(next_url, 302) | |||||
@app.route("/settings", methods=["GET", "POST"]) | @app.route("/settings", methods=["GET", "POST"]) | ||||
@catch_errors | @catch_errors | ||||
def settings(): | def settings(): | ||||
@@ -10,6 +10,8 @@ from .sites import update_sites | |||||
__all__ = ["format_api_error", "handle_api_request"] | __all__ = ["format_api_error", "handle_api_request"] | ||||
_CHECK_ERRORS = { | _CHECK_ERRORS = { | ||||
"not logged in": "You are required to log in with your Wikipedia account " | |||||
"to perform checks with the search engine", | |||||
"no search method": "Either 'use_engine' or 'use_links' must be true", | "no search method": "Either 'use_engine' or 'use_links' must be true", | ||||
"bad oldid": "The revision ID is invalid", | "bad oldid": "The revision ID is invalid", | ||||
"no URL": "The parameter 'url' is required for URL comparisons", | "no URL": "The parameter 'url' is required for URL comparisons", | ||||
@@ -116,6 +118,7 @@ _HOOKS = { | |||||
def handle_api_request(): | def handle_api_request(): | ||||
query = Query() | query = Query() | ||||
query.api = True | |||||
if query.version: | if query.version: | ||||
try: | try: | ||||
query.version = int(query.version) | query.version = int(query.version) | ||||
@@ -0,0 +1,48 @@ | |||||
import mwoauth | |||||
from flask import session, request | |||||
from .misc import cache | |||||
__all__ = ["oauth_login_start", "oauth_login_end", "clear_login_session"] | |||||
def oauth_login_start(): | |||||
consumer_token = mwoauth.ConsumerToken( | |||||
cache.bot.config.wiki["copyvios"]["oauth"]["consumer_token"], | |||||
cache.bot.config.wiki["copyvios"]["oauth"]["consumer_secret"]) | |||||
redirect, request_token = mwoauth.initiate( | |||||
"https://meta.wikimedia.org/w/index.php", consumer_token) | |||||
session["request_token"] = dict(zip(request_token._fields, request_token)) | |||||
# Take note of where to send the user after logging in | |||||
next_url = (request.form if request.method == "POST" else request.args).get("next", "/") | |||||
if next_url[0] == "/": | |||||
# Only allow internal redirects | |||||
session["next"] = next_url | |||||
return redirect | |||||
def oauth_login_end(): | |||||
if "request_token" not in session: | |||||
raise ValueError("OAuth request token not found in session.") | |||||
consumer_token = mwoauth.ConsumerToken( | |||||
cache.bot.config.wiki["copyvios"]["oauth"]["consumer_token"], | |||||
cache.bot.config.wiki["copyvios"]["oauth"]["consumer_secret"]) | |||||
access_token = mwoauth.complete( | |||||
"https://meta.wikimedia.org/w/index.php", | |||||
consumer_token, | |||||
mwoauth.RequestToken(**session["request_token"]), | |||||
request.query_string) | |||||
identity = mwoauth.identify( | |||||
"https://meta.wikimedia.org/w/index.php", | |||||
consumer_token, | |||||
access_token) | |||||
session["access_token"] = dict(zip(access_token._fields, access_token)) | |||||
session["username"] = identity["username"] | |||||
return session.get("next", "/") | |||||
def clear_login_session(): | |||||
session.clear() |
@@ -144,6 +144,10 @@ def _perform_check(query, page, use_engine, use_links): | |||||
_LOGGER.exception("Failed to retrieve cached results") | _LOGGER.exception("Failed to retrieve cached results") | ||||
if not query.result: | if not query.result: | ||||
if use_engine and not query.requester_username and not query.api: | |||||
query.error = "not logged in" | |||||
return | |||||
try: | try: | ||||
query.result = page.copyvio_check( | query.result = page.copyvio_check( | ||||
min_confidence=T_SUSPECT, max_queries=8, max_time=30, | min_confidence=T_SUSPECT, max_queries=8, max_time=30, | ||||
@@ -5,7 +5,7 @@ import datetime | |||||
from os.path import expanduser, join | from os.path import expanduser, join | ||||
import apsw | import apsw | ||||
from flask import g, request | |||||
from flask import g, request, session | |||||
import oursql | import oursql | ||||
from sqlalchemy.pool import manage | from sqlalchemy.pool import manage | ||||
@@ -19,6 +19,7 @@ class Query(object): | |||||
data = request.form if method == "POST" else request.args | data = request.form if method == "POST" else request.args | ||||
for key in data: | for key in data: | ||||
self.query[key] = data.getlist(key)[-1] | self.query[key] = data.getlist(key)[-1] | ||||
self.query["requester_username"] = session.get("username") | |||||
def __getattr__(self, key): | def __getattr__(self, key): | ||||
return self.query.get(key) | return self.query.get(key) | ||||
@@ -148,5 +148,44 @@ $(document).ready(function() { | |||||
}); | }); | ||||
} | } | ||||
if ($(".login-link").length >= 0) { | |||||
$(".login-link").click(function(e) { | |||||
e.preventDefault(); | |||||
var $loginForm = $("<form>") | |||||
.attr("action", "/login") | |||||
.attr("method", "POST"); | |||||
// Tell `/login` where to go after logging in | |||||
$loginForm.append( | |||||
$("<input>") | |||||
.attr("type", "hidden") | |||||
.attr("name", "next") | |||||
.attr("value", window.location.pathname + window.location.search) | |||||
) | |||||
$("body").after($loginForm); | |||||
$loginForm.trigger("submit"); | |||||
$loginForm.remove(); | |||||
return false; | |||||
}); | |||||
} | |||||
if ($(".logout-link").length >= 0) { | |||||
$(".logout-link").click(function(e) { | |||||
e.preventDefault(); | |||||
if (!confirm("Are you sure you want to log out?")) { | |||||
return; | |||||
} | |||||
var $logoutForm = $("<form>") | |||||
.attr("action", "/logout") | |||||
.attr("method", "POST"); | |||||
$("body").after($logoutForm); | |||||
$logoutForm.trigger("submit"); | |||||
$logoutForm.remove(); | |||||
return false; | |||||
}); | |||||
} | |||||
install_notice(); | install_notice(); | ||||
}); | }); |
@@ -1 +1 @@ | |||||
function update_screen_size(){var cache=cache_cookie(),data={width:window.screen.availWidth,height:window.screen.availHeight};cache&&cache.width==data.width&&cache.height==data.height||set_cookie("CopyviosScreenCache",JSON.stringify(data),1095)}function cache_cookie(){var cookie=get_cookie("CopyviosScreenCache");if(cookie)try{var width=(data=JSON.parse(cookie)).width,height=data.height;if(width&&height)return{width:width,height:height}}catch(SyntaxError){}return!1}function get_cookie(name){for(var nameEQ=name+"=",ca=document.cookie.split(";"),i=0;i<ca.length;i++){for(var c=ca[i];" "==c.charAt(0);)c=c.substring(1,c.length);if(0==c.indexOf(nameEQ)){var value=window.atob(c.substring(nameEQ.length,c.length));if(0==value.indexOf("--cpv2"))return value.substring("--cpv2".length,value.length)}}return null}function set_cookie_with_date(name,value,date){value=window.btoa("--cpv2"+value);var path=window.location.pathname.split("/",2)[1];date=date?"; expires="+date.toUTCString():"",document.cookie=name+"="+value+date+"; path=/"+path}function set_cookie(name,value,days){var date;days?((date=new Date).setTime(date.getTime()+24*days*60*60*1e3),set_cookie_with_date(name,value,date)):set_cookie_with_date(name,value)}function delete_cookie(name){set_cookie(name,"",-1)}function toggle_notice(){var details=$("#notice-collapse-box"),trigger=$("#notice-collapse-trigger");details.is(":hidden")?(details.show(),trigger.text("[hide]")):(details.hide(),trigger.text("[show]"))}function install_notice(){var details=$("#notice-collapse-box"),trigger=$("#notice-collapse-trigger");0<=details.length&&0<=trigger.length&&(trigger.replaceWith($("<a/>",{id:"notice-collapse-trigger",href:"#",text:"[show]",click:function(){return toggle_notice(),!1}})),details.hide())}$(document).ready(function(){$("#action-search").change(function(){$(".cv-search").prop("disabled",!1),$(".cv-compare").prop("disabled",!0),$(".cv-search-oo-ui").addClass("oo-ui-widget-enabled").removeClass("oo-ui-widget-disabled"),$(".cv-compare-oo-ui").addClass("oo-ui-widget-disabled").removeClass("oo-ui-widget-enabled")}),$("#action-compare").change(function(){$(".cv-search").prop("disabled",!0),$(".cv-compare").prop("disabled",!1),$(".cv-search-oo-ui").addClass("oo-ui-widget-disabled").removeClass("oo-ui-widget-enabled"),$(".cv-compare-oo-ui").addClass("oo-ui-widget-enabled").removeClass("oo-ui-widget-disabled")}),$("#action-search").is(":checked")&&$("#action-search").change(),$("#action-compare").is(":checked")&&$("#action-compare").change(),$("#cv-form").submit(function(){$("#action-search").is(":checked")&&$.each([["engine","use_engine"],["links","use_links"],["turnitin","turnitin"]],function(i,val){$("#cv-cb-"+val[0]).is(":checked")&&$("#cv-form input[type='hidden'][name='"+val[1]+"']").prop("disabled",!0)}),$("#cv-form button[type='submit']").prop("disabled",!0).css("cursor","progress").parent().addClass("oo-ui-widget-disabled").removeClass("oo-ui-widget-enabled")}),0<=$("#cv-additional").length&&($("#cv-additional").css("display","block"),$(".source-default-hidden").css("display","none"),$("#show-additional-sources").click(function(){return $(".source-default-hidden").css("display",""),$("#cv-additional").css("display","none"),!1})),install_notice()}); | |||||
function update_screen_size(){var cache=cache_cookie(),data={width:window.screen.availWidth,height:window.screen.availHeight};cache&&cache.width==data.width&&cache.height==data.height||set_cookie("CopyviosScreenCache",JSON.stringify(data),1095)}function cache_cookie(){var cookie=get_cookie("CopyviosScreenCache");if(cookie)try{var width=(data=JSON.parse(cookie)).width,height=data.height;if(width&&height)return{width:width,height:height}}catch(SyntaxError){}return!1}function get_cookie(name){for(var nameEQ=name+"=",ca=document.cookie.split(";"),i=0;i<ca.length;i++){for(var c=ca[i];" "==c.charAt(0);)c=c.substring(1,c.length);if(0==c.indexOf(nameEQ)){var value=window.atob(c.substring(nameEQ.length,c.length));if(0==value.indexOf("--cpv2"))return value.substring("--cpv2".length,value.length)}}return null}function set_cookie_with_date(name,value,date){value=window.btoa("--cpv2"+value);var path=window.location.pathname.split("/",2)[1];date=date?"; expires="+date.toUTCString():"",document.cookie=name+"="+value+date+"; path=/"+path}function set_cookie(name,value,days){var date;days?((date=new Date).setTime(date.getTime()+24*days*60*60*1e3),set_cookie_with_date(name,value,date)):set_cookie_with_date(name,value)}function delete_cookie(name){set_cookie(name,"",-1)}function toggle_notice(){var details=$("#notice-collapse-box"),trigger=$("#notice-collapse-trigger");details.is(":hidden")?(details.show(),trigger.text("[hide]")):(details.hide(),trigger.text("[show]"))}function install_notice(){var details=$("#notice-collapse-box"),trigger=$("#notice-collapse-trigger");0<=details.length&&0<=trigger.length&&(trigger.replaceWith($("<a/>",{id:"notice-collapse-trigger",href:"#",text:"[show]",click:function(){return toggle_notice(),!1}})),details.hide())}$(document).ready(function(){$("#action-search").change(function(){$(".cv-search").prop("disabled",!1),$(".cv-compare").prop("disabled",!0),$(".cv-search-oo-ui").addClass("oo-ui-widget-enabled").removeClass("oo-ui-widget-disabled"),$(".cv-compare-oo-ui").addClass("oo-ui-widget-disabled").removeClass("oo-ui-widget-enabled")}),$("#action-compare").change(function(){$(".cv-search").prop("disabled",!0),$(".cv-compare").prop("disabled",!1),$(".cv-search-oo-ui").addClass("oo-ui-widget-disabled").removeClass("oo-ui-widget-enabled"),$(".cv-compare-oo-ui").addClass("oo-ui-widget-enabled").removeClass("oo-ui-widget-disabled")}),$("#action-search").is(":checked")&&$("#action-search").change(),$("#action-compare").is(":checked")&&$("#action-compare").change(),$("#cv-form").submit(function(){$("#action-search").is(":checked")&&$.each([["engine","use_engine"],["links","use_links"],["turnitin","turnitin"]],function(i,val){$("#cv-cb-"+val[0]).is(":checked")&&$("#cv-form input[type='hidden'][name='"+val[1]+"']").prop("disabled",!0)}),$("#cv-form button[type='submit']").prop("disabled",!0).css("cursor","progress").parent().addClass("oo-ui-widget-disabled").removeClass("oo-ui-widget-enabled")}),0<=$("#cv-additional").length&&($("#cv-additional").css("display","block"),$(".source-default-hidden").css("display","none"),$("#show-additional-sources").click(function(){return $(".source-default-hidden").css("display",""),$("#cv-additional").css("display","none"),!1})),0<=$(".login-link").length&&$(".login-link").click(function(e){e.preventDefault();e=$("<form>").attr("action","/login").attr("method","POST");return e.append($("<input>").attr("type","hidden").attr("name","next").attr("value",window.location.pathname+window.location.search)),$("body").after(e),e.trigger("submit"),e.remove(),!1}),0<=$(".logout-link").length&&$(".logout-link").click(function(e){if(e.preventDefault(),confirm("Are you sure you want to log out?"))return e=$("<form>").attr("action","/logout").attr("method","POST"),$("body").after(e),e.trigger("submit"),e.remove(),!1}),install_notice()}); |
@@ -86,6 +86,21 @@ header h1 { | |||||
} | } | ||||
} | } | ||||
header .login-link, header .logout-link { | |||||
margin-right: 1em; | |||||
} | |||||
header .login-link::before, header .logout-link::before { | |||||
content: ' '; | |||||
font-size: 0.85em; | |||||
color: black; | |||||
opacity: 0.6; | |||||
padding-left: 1.67em; | |||||
background-image: linear-gradient(transparent,transparent), url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%3E%3Ctitle%3Euser%20avatar%3C%2Ftitle%3E%3Cpath%20d%3D%22M10%2011c-5.92%200-8%203-8%205v3h16v-3c0-2-2.08-5-8-5%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%225.5%22%20r%3D%224.5%22%2F%3E%3C%2Fsvg%3E"); | |||||
background-repeat: no-repeat; | |||||
background-size: contain; | |||||
} | |||||
#settings-link::before { | #settings-link::before { | ||||
content: ' '; | content: ' '; | ||||
font-size: 0.85em; | font-size: 0.85em; | ||||
@@ -0,0 +1,39 @@ | |||||
<%! | |||||
from json import dumps, loads | |||||
from flask import g, request | |||||
from copyvios.misc import cache | |||||
from urlparse import parse_qsl | |||||
%>\ | |||||
<%include file="/support/header.mako" args="title='Login | Earwig\'s Copyvio Detector', splash=True"/> | |||||
% if error: | |||||
<div id="info-box" class="red-box"> | |||||
<p>Error trying to log in: ${error | h}</p> | |||||
</div> | |||||
% endif | |||||
<h2>Login</h2> | |||||
<p>You are required to log in with your Wikimedia account to perform checks with the search engine.</p> | |||||
%if request.args.get('next'): | |||||
<p> | |||||
After logging in, | |||||
% if request.args["next"][0:2] == "/?" and dict(parse_qsl(request.args["next"])).get("action") == "search": | |||||
your check will be run. | |||||
% else: | |||||
you will be redirected to: ${request.args.get('next') | h} | |||||
% endif | |||||
</p> | |||||
%endif | |||||
<form action="${request.script_root}/login" method="post"> | |||||
<input type="hidden" name="next" value="${request.args.get('next', '/') | h}" /> | |||||
<div class="oo-ui-layout oo-ui-fieldLayout oo-ui-fieldLayout-align-left"> | |||||
<div class="oo-ui-fieldLayout-body"> | |||||
<span class="oo-ui-fieldLayout-field"> | |||||
<span class="oo-ui-widget oo-ui-widget-enabled oo-ui-inputWidget oo-ui-buttonElement oo-ui-buttonElement-framed oo-ui-labelElement oo-ui-flaggedElement-primary oo-ui-flaggedElement-progressive oo-ui-labelElement oo-ui-buttonInputWidget"> | |||||
<button type="submit" class="oo-ui-inputWidget-input oo-ui-buttonElement-button"> | |||||
<span class="oo-ui-labelElement-label">Login</span> | |||||
</button> | |||||
</span> | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
<%include file="/support/footer.mako"/> |
@@ -0,0 +1,22 @@ | |||||
<%! | |||||
from json import dumps, loads | |||||
from flask import g, request | |||||
from copyvios.misc import cache | |||||
%>\ | |||||
<%include file="/support/header.mako" args="title='Logout | Earwig\'s Copyvio Detector', splash=True"/> | |||||
<h2>Logout</h2> | |||||
<p>Logging out will prevent you from making search engine checks.</p> | |||||
<form action="${request.script_root}/logout" method="post"> | |||||
<div class="oo-ui-layout oo-ui-fieldLayout oo-ui-fieldLayout-align-left"> | |||||
<div class="oo-ui-fieldLayout-body"> | |||||
<span class="oo-ui-fieldLayout-field"> | |||||
<span class="oo-ui-widget oo-ui-widget-enabled oo-ui-inputWidget oo-ui-buttonElement oo-ui-buttonElement-framed oo-ui-labelElement oo-ui-flaggedElement-primary oo-ui-flaggedElement-progressive oo-ui-labelElement oo-ui-buttonInputWidget"> | |||||
<button type="submit" class="oo-ui-inputWidget-input oo-ui-buttonElement-button"> | |||||
<span class="oo-ui-labelElement-label">Logout</span> | |||||
</button> | |||||
</span> | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
<%include file="/support/footer.mako"/> |
@@ -1,6 +1,6 @@ | |||||
<%page args="title, splash=False"/>\ | <%page args="title, splash=False"/>\ | ||||
<%! | <%! | ||||
from flask import g, request, url_for | |||||
from flask import g, request, session, url_for | |||||
from copyvios.background import set_background | from copyvios.background import set_background | ||||
%>\ | %>\ | ||||
<!DOCTYPE html> | <!DOCTYPE html> | ||||
@@ -25,6 +25,12 @@ | |||||
<div id="content"> | <div id="content"> | ||||
<header> | <header> | ||||
<h1><a href="/">Earwig's <strong>Copyvio Detector</strong></a></h1> | <h1><a href="/">Earwig's <strong>Copyvio Detector</strong></a></h1> | ||||
<% logged_in_user = session.get("username") %>\ | |||||
% if logged_in_user: | |||||
<a class="logout-link" href="/logout" title="Log out">${logged_in_user | h}</a> | |||||
% else: | |||||
<a class="login-link" href="/login" title="Log in">Login</a> | |||||
% endif | |||||
<a id="settings-link" href="/settings">Settings</a> | <a id="settings-link" href="/settings">Settings</a> | ||||
</header> | </header> | ||||
<main> | <main> |