A copyright violation detector running on Wikimedia Cloud Services https://tools.wmflabs.org/copyvios/
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 

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