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.
 
 
 
 
 

291 line
14 KiB

  1. <%!
  2. from collections import defaultdict
  3. from datetime import datetime
  4. from hashlib import sha256
  5. from itertools import count
  6. from os.path import expanduser
  7. from re import sub, UNICODE
  8. from sys import path
  9. from time import time
  10. from urlparse import parse_qs
  11. from earwigbot import bot, exceptions
  12. import oursql
  13. def get_results(lang, project, title, url, query):
  14. bot = bot.Bot(".earwigbot")
  15. try:
  16. site = bot.wiki.get_site(lang=lang, project=project)
  17. except exceptions.SiteNotFoundError:
  18. try:
  19. site = bot.wiki.add_site(lang=lang, project=project)
  20. except exceptions.APIError:
  21. return None, None
  22. page = site.get_page(title) # TODO: what if the page doesn't exist?
  23. # if url:
  24. # result = get_url_specific_results(page, url)
  25. # else:
  26. # conn = open_sql_connection(bot)
  27. # if not query.get("nocache"):
  28. # result = get_cached_results(page, conn)
  29. # if query.get("nocache") or not result:
  30. # result = get_fresh_results(page, conn)
  31. mc1 = __import__("earwigbot").wiki.copyvios.MarkovChain(page.get())
  32. mc2 = __import__("earwigbot").wiki.copyvios.MarkovChain("This is some random textual content for a page.")
  33. mci = __import__("earwigbot").wiki.copyvios.MarkovChainIntersection(mc1, mc2)
  34. result = __import__("earwigbot").wiki.copyvios.CopyvioCheckResult(
  35. True, 0.67123, "http://example.com/", 7, mc1, (mc2, mci))
  36. return page, result
  37. def get_url_specific_results(page, url):
  38. t_start = time()
  39. result = page.copyvio_compare(url)
  40. result.tdiff = time() - t_start
  41. return result
  42. def open_sql_connection(bot):
  43. conn_args = bot.config.wiki["_toolserverSQLCache"]
  44. if "read_default_file" not in conn_args and "user" not in conn_args and "passwd" not in conn_args:
  45. conn_args["read_default_file"] = expanduser("~/.my.cnf")
  46. if "autoping" not in conn_args:
  47. conn_args["autoping"] = True
  48. if "autoreconnect" not in conn_args:
  49. conn_args["autoreconnect"] = True
  50. return oursql.connect(**conn_args)
  51. def get_cached_results(page, conn):
  52. query1 = "DELETE FROM cache WHERE cache_time < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 DAY)"
  53. query2 = "SELECT cache_url, cache_time, cache_queries, cache_process_time FROM cache WHERE cache_id = ? AND cache_hash = ?"
  54. pageid = page.pageid()
  55. hash = sha256(page.get()).hexdigest()
  56. t_start = time()
  57. with conn.cursor() as cursor:
  58. cursor.execute(query1)
  59. cursor.execute(query2, (pageid, hash))
  60. results = cursor.fetchall()
  61. if not results:
  62. return None
  63. url, cache_time, num_queries, original_tdiff = results[0]
  64. result = page.copyvio_compare(url)
  65. result.cached = True
  66. result.queries = num_queries
  67. result.tdiff = time() - t_start
  68. result.original_tdiff = original_tdiff
  69. result.cache_time = cache_time.strftime("%b %d, %Y %H:%M:%S UTC")
  70. result.cache_age = format_date(cache_time)
  71. return result
  72. def format_date(cache_time):
  73. diff = datetime.utcnow() - cache_time
  74. if diff.seconds > 3600:
  75. return "{0} hours".format(diff.seconds / 3600)
  76. if diff.seconds > 60:
  77. return "{0} minutes".format(diff.seconds / 60)
  78. return "{0} seconds".format(diff.seconds)
  79. def get_fresh_results(page, conn):
  80. t_start = time()
  81. result = page.copyvio_check(max_queries=10)
  82. result.cached = False
  83. result.tdiff = time() - t_start
  84. cache_result(page, result, conn)
  85. return result
  86. def cache_result(page, result, conn):
  87. pageid = page.pageid()
  88. hash = sha256(page.get()).hexdigest()
  89. query1 = "SELECT 1 FROM cache WHERE cache_id = ?"
  90. query2 = "DELETE FROM cache WHERE cache_id = ?"
  91. query3 = "INSERT INTO cache VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, ?)"
  92. with conn.cursor() as cursor:
  93. cursor.execute(query1, (pageid,))
  94. if cursor.fetchall():
  95. cursor.execute(query2, (pageid,))
  96. cursor.execute(query3, (pageid, hash, result.url, result.queries,
  97. result.tdiff))
  98. def highlight_delta(chain, delta):
  99. processed = []
  100. prev_prev = prev = chain.START
  101. i = 0
  102. all_words = chain.text.split()
  103. paragraphs = chain.text.split("\n")
  104. for paragraph in paragraphs:
  105. processed_words = []
  106. words = paragraph.split(" ")
  107. for word, i in zip(words, count(i)):
  108. try:
  109. next = strip_word(all_words[i+1])
  110. except IndexError:
  111. next = chain.END
  112. sword = strip_word(word)
  113. block = [prev_prev, prev] # Block for before
  114. alock = [prev, sword] # Block for after
  115. before = [block in delta.chain and sword in delta.chain[block]]
  116. after = [alock in delta.chain and next in delta.chain[alock]]
  117. is_first = i == 0
  118. is_last = i + 1 == len(all_words)
  119. res = highlight_word(word, before, after, is_first, is_last)
  120. processed_words.append(res)
  121. prev_prev = prev
  122. prev = sword
  123. processed.append(u" ".join(processed_words))
  124. i += 1
  125. return u"<br /><br />".join(processed)
  126. def highlight_word(word, before, after, is_first, is_last):
  127. if before and after:
  128. # Word is in the middle of a highlighted block, so don't change
  129. # anything unless this is the first word (force block to start) or
  130. # the last word (force block to end):
  131. res = word
  132. if is_first:
  133. res = u'<span class="cv-hl">' + res
  134. if is_last:
  135. res += u'</span>'
  136. elif before:
  137. # Word is the last in a highlighted block, so fade it out and then
  138. # end the block; force open a block before the word if this is the
  139. # first word:
  140. res = fade_word(word, u"out") + u"</span>"
  141. if is_first:
  142. res = u'<span class="cv-hl">' + res
  143. elif after:
  144. # Word is the first in a highlighted block, so start the block and
  145. # then fade it in; force close the block after the word if this is
  146. # the last word:
  147. res = u'<span class="cv-hl">' + fade_word(word, u"in")
  148. if is_last:
  149. res += u"</span>"
  150. else:
  151. # Word is completely outside of a highlighted block, so do nothing:
  152. res = word
  153. return res
  154. def fade_word(word, dir):
  155. if len(word) <= 4:
  156. return u'<span class="cv-hl-{0}">{1}</span>'.format(dir, word)
  157. if dir == u"out":
  158. return u'{0}<span class="cv-hl-out">{1}</span>'.format(word[:-4], word[-4:])
  159. return u'<span class="cv-hl-in">{0}</span>{1}'.format(word[:4], word[4:])
  160. def strip_word(word):
  161. return sub("[^\w\s-]", "", word.lower(), flags=UNICODE)
  162. def urlstrip(url):
  163. if url.startswith("http://"):
  164. url = url[7:]
  165. if url.startswith("https://"):
  166. url = url[8:]
  167. if url.startswith("www."):
  168. url = url[4:]
  169. if url.endswith("/"):
  170. url = url[:-1]
  171. return url
  172. %>\
  173. <%
  174. query = parse_qs(environ["QUERY_STRING"])
  175. try:
  176. lang = query["lang"][0]
  177. project = query["project"][0]
  178. title = query["title"][0]
  179. url = query.get("url", [None])[0]
  180. except (KeyError, IndexError):
  181. page = None
  182. else:
  183. page, result = get_results(lang, project, title, url, query)
  184. %>\
  185. <%include file="/support/header.mako" args="environ=environ, title='Copyvio Detector', add_css=('copyvios.css',), add_js=('copyvios.js',)"/>
  186. <h1>Copyvio Detector</h1>
  187. <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 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>
  188. <form action="${environ['PATH_INFO']}" method="get">
  189. <table>
  190. <tr>
  191. <td>Site:</td>
  192. <td>
  193. <select name="lang">
  194. <option value="en" selected="selected">en (English)</option>
  195. </select>
  196. <select name="project">
  197. <option value="wikipedia" selected="selected">Wikipedia</option>
  198. </select>
  199. </td>
  200. </tr>
  201. <tr>
  202. <td>Page title:</td>
  203. % if page:
  204. <td><input type="text" name="title" size="60" value="${page.title() | h}" /></td>
  205. % else:
  206. <td><input type="text" name="title" size="60" /></td>
  207. % endif
  208. </tr>
  209. <tr>
  210. <td>URL:</td>
  211. % if url:
  212. <td><input type="text" name="url" size="120" value="${url | h}" /></td>
  213. % else:
  214. <td><input type="text" name="url" size="120" /></td>
  215. % endif
  216. </tr>
  217. % if query.get("nocache") or page:
  218. <tr>
  219. <td>Bypass cache:</td>
  220. % if query.get("nocache"):
  221. <td><input type="checkbox" name="nocache" value="1" checked="checked" /></td>
  222. % else:
  223. <td><input type="checkbox" name="nocache" value="1" /></td>
  224. % endif
  225. </tr>
  226. % endif
  227. <tr>
  228. <td><button type="submit">Submit</button></td>
  229. </tr>
  230. </table>
  231. </form>
  232. % if page:
  233. <div class="divider"></div>
  234. <div id="cv-result-${'yes' if result.violation else 'no'}">
  235. % if result.violation:
  236. <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>
  237. % else:
  238. <h2 id="cv-result-header">No violations detected in <a href="${page.url()}">${page.title() | h}</a>.</h2>
  239. % endif
  240. <ul id="cv-result-list">
  241. <li><b><tt>${round(result.confidence * 100, 1)}%</tt></b> confidence of a violation.</li>
  242. % if result.cached:
  243. <li>Results are <a id="cv-cached" href="#">cached
  244. <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>
  245. </a> from ${result.cache_time} (${result.cache_age} ago). <a href="${environ['REQUEST_URI'] | h}&amp;nocache=1">Bypass the cache.</a></li>
  246. % else:
  247. <li>Results generated in <tt>${round(result.tdiff, 3)}</tt> seconds using <tt>${result.queries}</tt> queries.</li>
  248. % endif
  249. <li><a id="cv-result-detail-link" href="#cv-result-detail" onclick="copyvio_toggle_details()">Show details:</a></li>
  250. </ul>
  251. <div id="cv-result-detail" style="display: none;">
  252. <ul id="cv-result-detail-list">
  253. <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>
  254. % if result.cached:
  255. % if result.queries:
  256. <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>
  257. % else:
  258. <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>
  259. % endif
  260. % endif
  261. % if result.queries:
  262. <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>
  263. % endif
  264. </ul>
  265. <table id="cv-chain-table">
  266. <tr>
  267. <td>Article: <div class="cv-chain-detail"><p>${highlight_delta(result.article_chain, result.delta_chain)}</p></div></td>
  268. <td>Source: <div class="cv-chain-detail"><p>${highlight_delta(result.source_chain, result.delta_chain)}</p></div></td>
  269. </tr>
  270. </table>
  271. </div>
  272. </div>
  273. % endif
  274. <%include file="/support/footer.mako" args="environ=environ"/>