A copyright violation detector running on Wikimedia Cloud Services https://tools.wmflabs.org/copyvios/
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

229 linhas
6.5 KiB

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