A copyright violation detector running on Wikimedia Cloud Services https://tools.wmflabs.org/copyvios/
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 

214 рядки
6.2 KiB

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