diff --git a/.gitignore b/.gitignore index 17ff478..3b6b992 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__ .earwigbot logs/* !logs/.gitinclude +config.py diff --git a/README.md b/README.md index 230bd66..a11ada3 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Dependencies * [flask-mako](https://pythonhosted.org/Flask-Mako/) >= 0.3 * [mako](https://www.makotemplates.org/) >= 0.7.2 * [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 * [requests](https://requests.readthedocs.io/) >= 2.9.1 * [SQLAlchemy](https://www.sqlalchemy.org/) >= 0.9.6 diff --git a/app.py b/app.py index ff88f48..4e5f899 100755 --- a/app.py +++ b/app.py @@ -1,18 +1,19 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- - from functools import wraps from hashlib import md5 from json import dumps from logging import DEBUG, INFO, getLogger from logging.handlers import TimedRotatingFileHandler +from multiprocessing import Value from os import path from time import asctime from traceback import format_exc +from urllib import quote_plus, quote from earwigbot.bot import Bot 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 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.settings import process_settings from copyvios.sites import update_sites +from copyvios.auth import oauth_login_start, oauth_login_end, clear_login_session app = Flask(__name__) MakoTemplates(app) hand = TimedRotatingFileHandler("logs/app.log", when="midnight", backupCount=7) hand.setLevel(DEBUG) +app.config.from_pyfile("config.py", True) app.logger.addHandler(hand) app.logger.info(u"Flask server started " + asctime()) app._hash_cache = {} @@ -52,6 +55,12 @@ def setup_app(): cache.background_data = {} 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) @app.before_request @@ -101,10 +110,50 @@ def index(): notice = get_notice() update_sites() query = do_check() + if query.submitted and query.error == "not logged in": + return redirect("/login?next=" + quote("/?" + request.query_string), 302) + return render_template( "index.mako", notice=notice, query=query, result=query.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"]) @catch_errors def settings(): diff --git a/copyvios/api.py b/copyvios/api.py index 703a0cb..f3443fe 100644 --- a/copyvios/api.py +++ b/copyvios/api.py @@ -10,6 +10,8 @@ from .sites import update_sites __all__ = ["format_api_error", "handle_api_request"] _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", "bad oldid": "The revision ID is invalid", "no URL": "The parameter 'url' is required for URL comparisons", @@ -116,6 +118,7 @@ _HOOKS = { def handle_api_request(): query = Query() + query.api = True if query.version: try: query.version = int(query.version) diff --git a/copyvios/auth.py b/copyvios/auth.py new file mode 100644 index 0000000..c9793b0 --- /dev/null +++ b/copyvios/auth.py @@ -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() \ No newline at end of file diff --git a/copyvios/checker.py b/copyvios/checker.py index c892db3..35fdbd2 100644 --- a/copyvios/checker.py +++ b/copyvios/checker.py @@ -144,6 +144,10 @@ def _perform_check(query, page, use_engine, use_links): _LOGGER.exception("Failed to retrieve cached results") if not query.result: + if use_engine and not query.requester_username and not query.api: + query.error = "not logged in" + return + try: query.result = page.copyvio_check( min_confidence=T_SUSPECT, max_queries=8, max_time=30, diff --git a/copyvios/misc.py b/copyvios/misc.py index b4cbca2..72e26d7 100644 --- a/copyvios/misc.py +++ b/copyvios/misc.py @@ -5,7 +5,7 @@ import datetime from os.path import expanduser, join import apsw -from flask import g, request +from flask import g, request, session import oursql from sqlalchemy.pool import manage @@ -19,6 +19,7 @@ class Query(object): data = request.form if method == "POST" else request.args for key in data: self.query[key] = data.getlist(key)[-1] + self.query["requester_username"] = session.get("username") def __getattr__(self, key): return self.query.get(key) diff --git a/static/script.js b/static/script.js index 3bdc77f..f5dc1f4 100644 --- a/static/script.js +++ b/static/script.js @@ -148,5 +148,44 @@ $(document).ready(function() { }); } + if ($(".login-link").length >= 0) { + $(".login-link").click(function(e) { + e.preventDefault(); + var $loginForm = $("