A copyright violation detector running on Wikimedia Cloud Services https://tools.wmflabs.org/copyvios/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

231 lines
6.6 KiB

  1. #! /usr/bin/env python
  2. import datetime
  3. import functools
  4. import hashlib
  5. import json
  6. import os
  7. import time
  8. import traceback
  9. import urllib.parse
  10. from typing import Any
  11. from earwigbot.wiki.copyvios import globalize
  12. from flask import make_response, redirect, render_template, request, session
  13. from werkzeug import Response
  14. from copyvios import app
  15. from copyvios.api import format_api_error, handle_api_request
  16. from copyvios.attribution import get_attribution_info
  17. from copyvios.auth import clear_login_session, oauth_login_end, oauth_login_start
  18. from copyvios.background import get_background
  19. from copyvios.cache import cache
  20. from copyvios.checker import (
  21. T_POSSIBLE,
  22. T_SUSPECT,
  23. CopyvioCheckError,
  24. ErrorCode,
  25. do_check,
  26. )
  27. from copyvios.cookies import get_cookies, get_new_cookies
  28. from copyvios.highlighter import highlight_delta
  29. from copyvios.misc import get_notice, get_permalink
  30. from copyvios.query import CheckQuery
  31. from copyvios.settings import process_settings
  32. from copyvios.sites import update_sites
  33. AnyResponse = Response | str | bytes
  34. app.config.from_pyfile("config.py", True)
  35. app.config["SESSION_COOKIE_SECURE"] = True
  36. app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
  37. app.config["PERMANENT_SESSION_LIFETIME"] = datetime.timedelta(days=365)
  38. app.logger.info(f"Flask server started {time.asctime()}")
  39. globalize(num_workers=8)
  40. @app.errorhandler(Exception)
  41. def handle_errors(exc: Exception) -> AnyResponse:
  42. if app.debug:
  43. raise # Use built-in debugger
  44. app.logger.exception("Caught exception:")
  45. return render_template("error.html.jinja", traceback=traceback.format_exc())
  46. @app.context_processor
  47. def setup_context() -> dict[str, Any]:
  48. return {
  49. "T_POSSIBLE": T_POSSIBLE,
  50. "T_SUSPECT": T_SUSPECT,
  51. "ErrorCode": ErrorCode,
  52. "cache": cache,
  53. "dump_json": json.dumps,
  54. "get_attribution_info": get_attribution_info,
  55. "get_background": get_background,
  56. "get_cookies": get_cookies,
  57. "get_notice": get_notice,
  58. "get_permalink": get_permalink,
  59. "highlight_delta": highlight_delta,
  60. }
  61. @app.after_request
  62. def add_new_cookies(response: Response) -> Response:
  63. for cookie in get_new_cookies():
  64. response.headers.add("Set-Cookie", cookie)
  65. return response
  66. @app.after_request
  67. def write_access_log(response: Response) -> Response:
  68. app.logger.debug(
  69. f"{time.asctime()} {request.method} {request.path} "
  70. f"{request.values.to_dict()} -> {response.status_code}"
  71. )
  72. return response
  73. @functools.lru_cache
  74. def _get_hash(path: str, mtime: float) -> str:
  75. # mtime is used as part of the cache key
  76. with open(path, "rb") as fp:
  77. return hashlib.sha1(fp.read()).hexdigest()
  78. def external_url_handler(
  79. error: Exception, endpoint: str, values: dict[str, Any]
  80. ) -> str:
  81. if endpoint == "static" and "file" in values:
  82. assert app.static_folder is not None
  83. path = os.path.join(app.static_folder, values["file"])
  84. mtime = os.path.getmtime(path)
  85. hashstr = _get_hash(path, mtime)
  86. return f"/static/{values['file']}?v={hashstr}"
  87. raise error
  88. app.url_build_error_handlers.append(external_url_handler)
  89. @app.route("/")
  90. def index() -> AnyResponse:
  91. update_sites()
  92. query = CheckQuery.from_get_args()
  93. try:
  94. result = do_check(query)
  95. error = None
  96. except CopyvioCheckError as exc:
  97. if exc.code == ErrorCode.NOT_LOGGED_IN:
  98. target = "/login?next=" + urllib.parse.quote_plus(
  99. "/?" + request.query_string.decode(errors="ignore")
  100. )
  101. return redirect(target, 302)
  102. app.logger.exception(f"Copyvio check failed on {query}")
  103. result = None
  104. error = exc
  105. return render_template(
  106. "index.html.jinja",
  107. query=query,
  108. result=result,
  109. error=error,
  110. splash=not result,
  111. )
  112. @app.route("/login", methods=["GET", "POST"])
  113. def login() -> AnyResponse:
  114. try:
  115. redirect_url = oauth_login_start() if request.method == "POST" else None
  116. if redirect_url:
  117. return redirect(redirect_url, 302)
  118. except Exception as e:
  119. app.logger.exception("OAuth login start failed")
  120. error_str = str(e)
  121. else:
  122. if session.get("username") is not None:
  123. return redirect("/", 302)
  124. error_str = request.args.get("error")
  125. is_search = False
  126. if request.args.get("next", "").startswith("/?"):
  127. next_query = dict(urllib.parse.parse_qsl(request.args["next"]))
  128. if next_query.get("action") == "search":
  129. is_search = True
  130. return render_template("login.html.jinja", error=error_str, is_search=is_search)
  131. @app.route("/logout", methods=["GET", "POST"])
  132. def logout() -> AnyResponse:
  133. if request.method == "POST":
  134. clear_login_session()
  135. return redirect("/", 302)
  136. else:
  137. return render_template("logout.html.jinja")
  138. @app.route("/oauth-callback")
  139. def oauth_callback() -> AnyResponse:
  140. try:
  141. next_url = oauth_login_end()
  142. except Exception as e:
  143. app.logger.exception("OAuth login end failed")
  144. return redirect("/login?error=" + urllib.parse.quote_plus(str(e)), 302)
  145. else:
  146. return redirect(next_url, 302)
  147. @app.route("/settings", methods=["GET", "POST"])
  148. def settings() -> AnyResponse:
  149. status = process_settings() if request.method == "POST" else None
  150. update_sites()
  151. return render_template(
  152. "settings.html.jinja",
  153. status=status,
  154. default_site=cache.bot.wiki.get_site(),
  155. splash=True,
  156. )
  157. @app.route("/api")
  158. def api() -> AnyResponse:
  159. return render_template("api_help.html.jinja")
  160. @app.route("/api.json")
  161. def api_json() -> AnyResponse:
  162. if not request.args:
  163. return render_template("api_help.html.jinja")
  164. format = request.args.get("format", "json")
  165. if format in ["json", "jsonfm"]:
  166. update_sites()
  167. try:
  168. result = handle_api_request()
  169. except Exception as exc:
  170. app.logger.exception("API request failed")
  171. result = format_api_error("unhandled_exception", exc)
  172. else:
  173. errmsg = f"Unknown format: {format!r}"
  174. result = format_api_error("unknown_format", errmsg)
  175. if format == "jsonfm":
  176. return render_template("api_result.html.jinja", result=result)
  177. resp = make_response(json.dumps(result))
  178. resp.mimetype = "application/json"
  179. resp.headers["Access-Control-Allow-Origin"] = "*"
  180. return resp
  181. if app.debug:
  182. # Silence browser 404s when testing
  183. @app.route("/favicon.ico")
  184. def favicon() -> AnyResponse:
  185. return app.send_static_file("favicon.ico")
  186. if __name__ == "__main__":
  187. app.run()