Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require OAuth for search engine checks #52

Merged
merged 2 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ __pycache__
.earwigbot
logs/*
!logs/.gitinclude
config.py
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 51 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,12 +22,14 @@
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 = {}
Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand Down
3 changes: 3 additions & 0 deletions copyvios/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
__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",
Expand Down Expand Up @@ -116,6 +118,7 @@ def _hook_sites(query):

def handle_api_request():
query = Query()
query.api = True
if query.version:
try:
query.version = int(query.version)
Expand Down
48 changes: 48 additions & 0 deletions copyvios/auth.py
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 4 additions & 0 deletions copyvios/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
ChlodAlejandro marked this conversation as resolved.
Show resolved Hide resolved
return

try:
query.result = page.copyvio_check(
min_confidence=T_SUSPECT, max_queries=8, max_time=30,
Expand Down
3 changes: 2 additions & 1 deletion copyvios/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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

Expand All @@ -19,6 +19,7 @@ def __init__(self, method="GET"):
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)
Expand Down
39 changes: 39 additions & 0 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
2 changes: 1 addition & 1 deletion static/script.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
content: ' ';
font-size: 0.85em;
Expand Down
Loading