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.
 
 
 
 
 

394 lines
19 KiB

  1. <%!
  2. from datetime import datetime
  3. from hashlib import sha256
  4. from itertools import count
  5. from os.path import expanduser
  6. from re import sub, UNICODE
  7. from sys import path
  8. from time import time
  9. from urlparse import parse_qs
  10. from earwigbot import bot, exceptions
  11. import oursql
  12. def get_results(bot, lang, project, all_projects, title, url, query):
  13. site = get_site(bot, lang, project, all_projects)
  14. if not site:
  15. return None, None
  16. page = site.get_page(title)
  17. if page.exists in [page.PAGE_MISSING, page.PAGE_INVALID]:
  18. return page, None
  19. # if url:
  20. # result = get_url_specific_results(page, url)
  21. # else:
  22. # conn = open_sql_connection(bot, "copyvioCache")
  23. # if not query.get("nocache"):
  24. # result = get_cached_results(page, conn)
  25. # if query.get("nocache") or not result:
  26. # result = get_fresh_results(page, conn)
  27. mc1 = __import__("earwigbot").wiki.copyvios.MarkovChain(page.get())
  28. mc2 = __import__("earwigbot").wiki.copyvios.MarkovChain("This is some random textual content for a page.")
  29. mci = __import__("earwigbot").wiki.copyvios.MarkovChainIntersection(mc1, mc2)
  30. result = __import__("earwigbot").wiki.copyvios.CopyvioCheckResult(
  31. True, 0.67123, "http://example.com/", 7, mc1, (mc2, mci))
  32. return page, result
  33. def get_site(bot, lang, project, all_projects):
  34. if project not in [proj[0] for proj in all_projects]:
  35. return None
  36. if project == "wikimedia": # Special sites:
  37. try:
  38. return bot.wiki.get_site(name=lang)
  39. except exceptions.SiteNotFoundError:
  40. try:
  41. return bot.wiki.add_site(lang=lang, project=project)
  42. except exceptions.APIError:
  43. return None
  44. try:
  45. return bot.wiki.get_site(lang=lang, project=project)
  46. except exceptions.SiteNotFoundError:
  47. try:
  48. return bot.wiki.add_site(lang=lang, project=project)
  49. except exceptions.APIError:
  50. return None
  51. def get_url_specific_results(page, url):
  52. t_start = time()
  53. result = page.copyvio_compare(url)
  54. result.tdiff = time() - t_start
  55. return result
  56. def open_sql_connection(bot, dbname):
  57. conn_args = bot.config.wiki["_toolserverSQL"][dbname]
  58. if "read_default_file" not in conn_args and "user" not in conn_args and "passwd" not in conn_args:
  59. conn_args["read_default_file"] = expanduser("~/.my.cnf")
  60. if "autoping" not in conn_args:
  61. conn_args["autoping"] = True
  62. if "autoreconnect" not in conn_args:
  63. conn_args["autoreconnect"] = True
  64. return oursql.connect(**conn_args)
  65. def get_cached_results(page, conn):
  66. query1 = "DELETE FROM cache WHERE cache_time < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 DAY)"
  67. query2 = "SELECT cache_url, cache_time, cache_queries, cache_process_time FROM cache WHERE cache_id = ? AND cache_hash = ?"
  68. pageid = page.pageid()
  69. hash = sha256(page.get()).hexdigest()
  70. t_start = time()
  71. with conn.cursor() as cursor:
  72. cursor.execute(query1)
  73. cursor.execute(query2, (pageid, hash))
  74. results = cursor.fetchall()
  75. if not results:
  76. return None
  77. url, cache_time, num_queries, original_tdiff = results[0]
  78. result = page.copyvio_compare(url)
  79. result.cached = True
  80. result.queries = num_queries
  81. result.tdiff = time() - t_start
  82. result.original_tdiff = original_tdiff
  83. result.cache_time = cache_time.strftime("%b %d, %Y %H:%M:%S UTC")
  84. result.cache_age = format_date(cache_time)
  85. return result
  86. def format_date(cache_time):
  87. diff = datetime.utcnow() - cache_time
  88. if diff.seconds > 3600:
  89. return "{0} hours".format(diff.seconds / 3600)
  90. if diff.seconds > 60:
  91. return "{0} minutes".format(diff.seconds / 60)
  92. return "{0} seconds".format(diff.seconds)
  93. def get_fresh_results(page, conn):
  94. t_start = time()
  95. result = page.copyvio_check(max_queries=10)
  96. result.cached = False
  97. result.tdiff = time() - t_start
  98. cache_result(page, result, conn)
  99. return result
  100. def cache_result(page, result, conn):
  101. pageid = page.pageid()
  102. hash = sha256(page.get()).hexdigest()
  103. query1 = "SELECT 1 FROM cache WHERE cache_id = ?"
  104. query2 = "DELETE FROM cache WHERE cache_id = ?"
  105. query3 = "INSERT INTO cache VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, ?)"
  106. with conn.cursor() as cursor:
  107. cursor.execute(query1, (pageid,))
  108. if cursor.fetchall():
  109. cursor.execute(query2, (pageid,))
  110. cursor.execute(query3, (pageid, hash, result.url, result.queries,
  111. result.tdiff))
  112. def get_sites(bot):
  113. max_staleness = 60 * 60 * 24 * 7
  114. site = bot.wiki.get_site()
  115. conn = open_sql_connection(site, "globals")
  116. query1 = "SELECT update_time FROM updates WHERE update_service = ?"
  117. query2 = "SELECT lang_code, lang_name FROM languages"
  118. query3 = "SELECT project_code, project_name FROM projects"
  119. with conn.cursor() as cursor:
  120. cursor.execute(query1, ("sites",))
  121. time_since_update = int(time() - cursor.fetchall()[0][0])
  122. if time_since_update > max_staleness:
  123. update_sites(bot, cursor)
  124. cursor.execute(query2)
  125. langs = cursor.fetchall()
  126. cursor.execute(query3)
  127. projects = cursor.fetchall()
  128. return langs, projects
  129. def update_sites(site, cursor):
  130. matrix = site.api_query(action="sitematrix")["sitematrix"]
  131. del matrix["count"]
  132. languages, projects = set(), set()
  133. for site in matrix.itervalues():
  134. if isinstance(site, list): # Special sites
  135. projects.add(("wikimedia", "Wikimedia"))
  136. for special in site:
  137. if "closed" not in special and "private" not in special:
  138. code = special["dbname"]
  139. name = special["code"].capitalize()
  140. languages.add((code, name))
  141. this = set()
  142. for web in site["site"]:
  143. if "closed" in web:
  144. continue
  145. project = "wikipedia" if web["code"] == "wiki" else web["code"]
  146. this.add((project, project.capitalize()))
  147. if this:
  148. code = site["code"].encode("utf8")
  149. name = site["name"].encode("utf8")
  150. languages.add((code, "{0} ({1})".format(code, name)))
  151. projects |= this
  152. save_site_updates(cursor, languages, projects)
  153. def save_site_updates(cursor, languages, projects):
  154. query1 = "SELECT lang_code, lang_name FROM languages"
  155. query2 = "DELETE FROM languages WHERE lang_code = ? AND lang_name = ?"
  156. query3 = "INSERT INTO languages VALUES (?, ?)"
  157. query4 = "SELECT project_code, project_name FROM projects"
  158. query5 = "DELETE FROM projects WHERE project_code = ? AND project_name = ?"
  159. query6 = "INSERT INTO projects VALUES (?, ?)"
  160. query7 = "UPDATE updates SET update_time = ? WHERE update_service = ?"
  161. synchronize_sites_with_db(cursor, languages, query1, query2, query3)
  162. synchronize_sites_with_db(cursor, projects, query4, query5, query6)
  163. cursor.execute(query7, (time(), "sites"))
  164. def synchronize_sites_with_db(cursor, updates, q_list, q_rmv, q_update):
  165. removals = []
  166. for site in cursor.execute(q_list):
  167. updates.remove(site) if site in updates else removals.append(site)
  168. cursor.executemany(q_rmv, removals)
  169. cursor.executemany(q_update, updates)
  170. def highlight_delta(chain, delta):
  171. processed = []
  172. prev_prev = prev = chain.START
  173. i = 0
  174. all_words = chain.text.split()
  175. paragraphs = chain.text.split("\n")
  176. for paragraph in paragraphs:
  177. processed_words = []
  178. words = paragraph.split(" ")
  179. for word, i in zip(words, count(i)):
  180. try:
  181. next = strip_word(all_words[i+1])
  182. except IndexError:
  183. next = chain.END
  184. sword = strip_word(word)
  185. block = [prev_prev, prev] # Block for before
  186. alock = [prev, sword] # Block for after
  187. before = [block in delta.chain and sword in delta.chain[block]]
  188. after = [alock in delta.chain and next in delta.chain[alock]]
  189. is_first = i == 0
  190. is_last = i + 1 == len(all_words)
  191. res = highlight_word(word, before, after, is_first, is_last)
  192. processed_words.append(res)
  193. prev_prev = prev
  194. prev = sword
  195. processed.append(u" ".join(processed_words))
  196. i += 1
  197. return u"<br /><br />".join(processed)
  198. def highlight_word(word, before, after, is_first, is_last):
  199. if before and after:
  200. # Word is in the middle of a highlighted block, so don't change
  201. # anything unless this is the first word (force block to start) or
  202. # the last word (force block to end):
  203. res = word
  204. if is_first:
  205. res = u'<span class="cv-hl">' + res
  206. if is_last:
  207. res += u'</span>'
  208. elif before:
  209. # Word is the last in a highlighted block, so fade it out and then
  210. # end the block; force open a block before the word if this is the
  211. # first word:
  212. res = fade_word(word, u"out") + u"</span>"
  213. if is_first:
  214. res = u'<span class="cv-hl">' + res
  215. elif after:
  216. # Word is the first in a highlighted block, so start the block and
  217. # then fade it in; force close the block after the word if this is
  218. # the last word:
  219. res = u'<span class="cv-hl">' + fade_word(word, u"in")
  220. if is_last:
  221. res += u"</span>"
  222. else:
  223. # Word is completely outside of a highlighted block, so do nothing:
  224. res = word
  225. return res
  226. def fade_word(word, dir):
  227. if len(word) <= 4:
  228. return u'<span class="cv-hl-{0}">{1}</span>'.format(dir, word)
  229. if dir == u"out":
  230. return u'{0}<span class="cv-hl-out">{1}</span>'.format(word[:-4], word[-4:])
  231. return u'<span class="cv-hl-in">{0}</span>{1}'.format(word[:4], word[4:])
  232. def strip_word(word):
  233. return sub("[^\w\s-]", "", word.lower(), flags=UNICODE)
  234. def urlstrip(url):
  235. if url.startswith("http://"):
  236. url = url[7:]
  237. if url.startswith("https://"):
  238. url = url[8:]
  239. if url.startswith("www."):
  240. url = url[4:]
  241. if url.endswith("/"):
  242. url = url[:-1]
  243. return url
  244. %>\
  245. <%
  246. bot = bot.Bot(".earwigbot")
  247. site = bot.wiki.get_site()
  248. query = parse_qs(environ["QUERY_STRING"])
  249. lang = query["lang"][0].lower() if "lang" in query else None
  250. project = query["project"][0].lower() if "project" in query else None
  251. title = query["title"][0] if "title" in query else None
  252. url = query["url"][0] if "url" in query else None
  253. all_langs, all_projects = get_sites(bot)
  254. if lang and project and title:
  255. page, result = get_results(bot, lang, project, all_projects, title,
  256. url, query)
  257. else:
  258. page = result = None
  259. %>\
  260. <%include file="/support/header.mako" args="environ=environ, title='Copyvio Detector', add_css=('copyvios.css',), add_js=('copyvios.js',)"/>
  261. <h1>Copyvio Detector</h1>
  262. <p>This tool attempts to detect <a href="//en.wikipedia.org/wiki/WP:COPYVIO">copyright violations</a> in articles. Simply give the title of the page you want to check and hit Submit. The tool will then search for its content elsewhere on the web and display a report if a similar webpage is found. If you also provide a URL, it will not query any search engines and instead display a report comparing the article to that particular webpage, like the <a href="//toolserver.org/~dcoetzee/duplicationdetector/">Duplication Detector</a>. Check out the <a href="//en.wikipedia.org/wiki/User:EarwigBot/Copyvios/FAQ">FAQ</a> for more information and technical details.</p>
  263. <form action="${environ['PATH_INFO']}" method="get">
  264. <table>
  265. <tr>
  266. <td>Site:</td>
  267. <td>
  268. <% selected_lang = lang if lang else site.lang %>
  269. <select name="lang">
  270. % for code, name in all_langs:
  271. % if code == selected_lang:
  272. <option value="${code}" selected="selected">${name}</option>
  273. % else:
  274. <option value="${code}">${name}</option>
  275. % endif
  276. % endfor
  277. </select>
  278. <% selected_project = project if project else site.project %>
  279. <select name="project">
  280. % for code, name in all_projects:
  281. % if code == selected_project:
  282. <option value="${code}" selected="selected">${name}</option>
  283. % else:
  284. <option value="${code}">${name}</option>
  285. % endif
  286. % endfor
  287. </select>
  288. </td>
  289. </tr>
  290. <tr>
  291. <td>Page title:</td>
  292. % if page:
  293. <td><input type="text" name="title" size="60" value="${page.title() | h}" /></td>
  294. % elif title:
  295. <td><input type="text" name="title" size="60" value="${title | h}" /></td>
  296. % else:
  297. <td><input type="text" name="title" size="60" /></td>
  298. % endif
  299. </tr>
  300. <tr>
  301. <td>URL (optional):</td>
  302. % if url:
  303. <td><input type="text" name="url" size="120" value="${url | h}" /></td>
  304. % else:
  305. <td><input type="text" name="url" size="120" /></td>
  306. % endif
  307. </tr>
  308. % if query.get("nocache") or page:
  309. <tr>
  310. <td>Bypass cache:</td>
  311. % if query.get("nocache"):
  312. <td><input type="checkbox" name="nocache" value="1" checked="checked" /></td>
  313. % else:
  314. <td><input type="checkbox" name="nocache" value="1" /></td>
  315. % endif
  316. </tr>
  317. % endif
  318. <tr>
  319. <td><button type="submit">Submit</button></td>
  320. </tr>
  321. </table>
  322. </form>
  323. % if project and lang and title and not page:
  324. CASE WHEN GIVEN SITE DOESN'T EXIST
  325. % elif project and lang and title and page and not result:
  326. CASE WHEN GIVEN PAGE DOESN'T EXIST
  327. % elif page:
  328. <div class="divider"></div>
  329. <div id="cv-result-${'yes' if result.violation else 'no'}">
  330. % if result.violation:
  331. <h2 id="cv-result-header"><a href="${page.url()}">${page.title() | h}</a> is a suspected violation of <a href="${result.url | h}">${result.url | urlstrip}</a>.</h2>
  332. % else:
  333. <h2 id="cv-result-header">No violations detected in <a href="${page.url()}">${page.title() | h}</a>.</h2>
  334. % endif
  335. <ul id="cv-result-list">
  336. <li><b><tt>${round(result.confidence * 100, 1)}%</tt></b> confidence of a violation.</li>
  337. % if result.cached:
  338. <li>Results are <a id="cv-cached" href="#">cached
  339. <span>To save time (and money), this tool will retain the results of checks for up to 72 hours. This includes the URL of the "violated" source, but neither its content nor the content of the article. Future checks on the same page (assuming it remains unchanged) will not involve additional search queries, but a fresh comparison against the source URL will be made. If the page is modified, a new check will be run.</span>
  340. </a> from ${result.cache_time} (${result.cache_age} ago). <a href="${environ['REQUEST_URI'] | h}&amp;nocache=1">Bypass the cache.</a></li>
  341. % else:
  342. <li>Results generated in <tt>${round(result.tdiff, 3)}</tt> seconds using <tt>${result.queries}</tt> queries.</li>
  343. % endif
  344. <li><a id="cv-result-detail-link" href="#cv-result-detail" onclick="copyvio_toggle_details()">Show details:</a></li>
  345. </ul>
  346. <div id="cv-result-detail" style="display: none;">
  347. <ul id="cv-result-detail-list">
  348. <li>Trigrams: <i>Article:</i> <tt>${result.article_chain.size()}</tt> / <i>Source:</i> <tt>${result.source_chain.size()}</tt> / <i>Delta:</i> <tt>${result.delta_chain.size()}</tt></li>
  349. % if result.cached:
  350. % if result.queries:
  351. <li>Retrieved from cache in <tt>${round(result.tdiff, 3)}</tt> seconds (originally generated in <tt>${round(result.original_tdiff, 3)}</tt>s using <tt>${result.queries}</tt> queries; <tt>${round(result.original_tdiff - result.tdiff, 3)}</tt>s saved).</li>
  352. % else:
  353. <li>Retrieved from cache in <tt>${round(result.tdiff, 3)}</tt> seconds (originally generated in <tt>${round(result.original_tdiff, 3)}</tt>s; <tt>${round(result.original_tdiff - result.tdiff, 3)}</tt>s saved).</li>
  354. % endif
  355. % endif
  356. % if result.queries:
  357. <li><i>Fun fact:</i> The Wikimedia Foundation paid Yahoo! Inc. <a href="http://info.yahoo.com/legal/us/yahoo/search/bosspricing/details.html">$${result.queries * 0.0008} USD</a> for these results.</li>
  358. % endif
  359. </ul>
  360. <table id="cv-chain-table">
  361. <tr>
  362. <td>Article: <div class="cv-chain-detail"><p>${highlight_delta(result.article_chain, result.delta_chain)}</p></div></td>
  363. <td>Source: <div class="cv-chain-detail"><p>${highlight_delta(result.source_chain, result.delta_chain)}</p></div></td>
  364. </tr>
  365. </table>
  366. </div>
  367. </div>
  368. % endif
  369. <%include file="/support/footer.mako" args="environ=environ"/>