Predator Alert Tool for FetLife (PAT-FetLife)

Alerts you of people who have allegedly assaulted others as you browse FetLife. Empowers you to anonymously report a consent violation perpetrated by a FetLife user.

  1. /**
  2. *
  3. * This is a Greasemonkey script and must be run using a Greasemonkey-compatible browser.
  4. *
  5. * @author maymay <bitetheappleback@gmail.com>
  6. */
  7. // ==UserScript==
  8. // @name Predator Alert Tool for FetLife (PAT-FetLife)
  9. // @version 0.3.4
  10. // @namespace com.maybemaimed.fetlife.faade
  11. // @description Alerts you of people who have allegedly assaulted others as you browse FetLife. Empowers you to anonymously report a consent violation perpetrated by a FetLife user.
  12. // @include https://fetlife.com/*
  13. // @include http://www.creepshield.com/search*
  14. // @exclude https://fetlife.com/adgear/*
  15. // @exclude https://fetlife.com/chat/*
  16. // @exclude https://fetlife.com/im_sessions*
  17. // @exclude https://fetlife.com/polling/*
  18. // @grant GM_log
  19. // @grant GM_xmlhttpRequest
  20. // @grant GM_addStyle
  21. // @grant GM_getValue
  22. // @grant GM_setValue
  23. // @grant GM_deleteValue
  24. // @grant GM_openInTab
  25. // ==/UserScript==
  26.  
  27. FL_UI = {}; // FetLife User Interface module
  28. FL_UI.Dialog = {};
  29. FL_UI.Dialog.createLink = function (dialog_id, html_content, parent_node) {
  30. var trigger_el = document.createElement('a');
  31. trigger_el.setAttribute('class', 'opens-modal');
  32. trigger_el.setAttribute('data-opens-modal', dialog_id);
  33. trigger_el.innerHTML = html_content;
  34. parent_node.appendChild(trigger_el);
  35. // Attach event listener to trigger element.
  36. parent_node.querySelector('[data-opens-modal="' + dialog_id + '"]').addEventListener('click', function (e) {
  37. parent_node.querySelector('[data-opens-modal="' + dialog_id + '"]').dialog("open");
  38. });
  39. };
  40. FL_UI.Dialog.inject = function (id, title, html_content) {
  41. // Inject dialog box HTML. FetLife currently uses Rails 3, so mimic that.
  42. // See, for instance, Rails Behaviors: http://josh.github.com/rails-behaviors/
  43. var dialog = document.createElement('div');
  44. dialog.setAttribute('style', 'display: none; position: absolute; overflow: hidden; z-index: 1000; outline: 0px none;');
  45. dialog.setAttribute('class', 'ui-dialog ui-widget ui-widget-content ui-corner-all');
  46. dialog.setAttribute('tabindex', '-1');
  47. dialog.setAttribute('role', 'dialog');
  48. dialog.setAttribute('aria-labelledby', 'ui-dialog-title-' + id);
  49. var html_string = '<div class="ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix" unselectable="on" style="-moz-user-select: none;">';
  50. html_string += '<span class="ui-dialog-title" id="ui-dialog-title-' + id + '" unselectable="on" style="-moz-user-select: none;">' + title + '</span>';
  51. html_string += '<a href="#" class="ui-dialog-titlebar-close ui-corner-all" role="button" unselectable="on" style="-moz-user-select: none;">';
  52. html_string += '<span class="ui-icon ui-icon-closethick" unselectable="on" style="-moz-user-select: none;">close</span>';
  53. html_string += '</a>';
  54. html_string += '</div>';
  55. html_string += '<div data-modal-title="' + title + '" data-modal-height="280" data-modal-auto-open="false" class="modal ui-dialog-content ui-widget-content" id="' + id + '">';
  56. html_string += html_content;
  57. html_string += '</div>';
  58. dialog.innerHTML = html_string;
  59. document.body.appendChild(dialog);
  60. };
  61.  
  62. FL_ASL = {};
  63. FL_ASL.users = {};
  64. FL_ASL.getUserProfile = function (id) {
  65. if (FL_ASL.users[id]) {
  66. return FL_ASL.users[id].profile_html;
  67. } else {
  68. FL_ASL.users[id] = {};
  69. GM_xmlhttpRequest({
  70. 'method': 'GET',
  71. 'url': 'https://fetlife.com/users/' + id.toString(),
  72. 'onload': function (response) {
  73. FL_ASL.users[id].profile_html = response.responseText;
  74. }
  75. });
  76. }
  77. };
  78.  
  79. FAADE = {};
  80. FAADE.CONFIG = {
  81. 'debug': false, // switch to true to debug.
  82. 'gdocs_key': '1xJDW-i4oqfCKN02KmOJi8uORiV-xRtw0erXWOw50mOo',
  83. 'gform_key': '1Zpmq4ZgrcUMAcDHgfT4ne_eAq71IKnONIbrQNfCP8gs',
  84. 'gdocs_development_key': '1z53rFX1g0E8DzuyXfyDrK9N1E3D-YFGvyFktqnHpLII',
  85. 'gform_development_key': 'dGxjMUhyR0FzLWJicHNXUFdxckFEQWc6MQ',
  86. };
  87.  
  88. // Utility debugging function.
  89. FAADE.log = function (msg) {
  90. if (!FAADE.CONFIG.debug) { return; }
  91. GM_log('FETLIFE FAADE: ' + msg);
  92. //console.log('FETLIFE FAADE: ' + msg);
  93. };
  94.  
  95. // Initializations.
  96. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility?
  97. GM_addStyle('\
  98. /* Highlight alleged abusers in bright yellow. */\
  99. .faade_alleged_abuser {\
  100. display: inline-block;\
  101. border: 2px solid yellow;\
  102. }\
  103. #faade_abuse_reports caption {\
  104. background: yellow;\
  105. color: red;\
  106. }\
  107. #faade_abuse_reports tfoot td {\
  108. padding-top: 1em;\
  109. text-align: center;\
  110. }\
  111. #faade_abuse_reports tr:target > * {\
  112. border: 1px solid red;\
  113. }\
  114. #faade_abuse_reports tr:target th {\
  115. border-width: 1px 0 1px 1px;\
  116. }\
  117. #faade_abuse_reports tr:target td {\
  118. border-width: 1px 1px 1px 0;\
  119. }\
  120. /* FAADE location broadcast dialog styles. */\
  121. [aria-labelledby="ui-dialog-title-faade"] { border-color: yellow; }\
  122. #ui-dialog-title-faade { color: red; }\
  123. /* General prettiness. */\
  124. #profile #main_content a + a.faade_report_link { padding-left: 5px; }\
  125. footer .faade_report_link,\
  126. .blog_entry p.quiet.small .faade_report_link,\
  127. .byline .faade_report_link {\
  128. display: block;\
  129. color: #777;\
  130. }\
  131. .mini_feed_title .faade_report_link {\
  132. float: left;\
  133. padding-right: 5px;\
  134. }\
  135. ul.pictures li a.faade_report_link,\
  136. #profile ul.friends li { width: auto; }\
  137. ');
  138. FAADE.init = function () {
  139. // Whenever we load CreepShield, just clear the cookies.
  140. if (window.location.hostname.match(/creepshield.com/)) {
  141. FAADE.clearCookies();
  142. return;
  143. }
  144. FL_ASL.getUserProfile(uw.FetLife.currentUser.id); // run early
  145. FAADE.injectDialog();
  146. FAADE.abuser_database = FAADE.getValue('abuser_database', false);
  147. if (FAADE.abuserDatabaseExpired()) {
  148. FAADE.fetchAbuserDatabase();
  149. }
  150. FAADE.main();
  151. };
  152. window.addEventListener('DOMContentLoaded', FAADE.init);
  153.  
  154. // Determines whether the abuser database has expired and needs to be re-fetched.
  155. FAADE.abuserDatabaseExpired = function () {
  156. // If we don't have a database, then of course it's "expired."
  157. if (!FAADE.abuser_database) {
  158. FAADE.log('Abuser database expired because of false-equivalent value.');
  159. return true;
  160. } else if ( (new Date().getTime() > (parseInt(FAADE.getValue('last_fetch_time')) + 86400)) ) {
  161. // Abuser database was last fetched more than 24 hours (86400 seconds) ago, so refresh.
  162. FAADE.log('Abuser database expired because of time.');
  163. return true;
  164. } else {
  165. FAADE.log('Abuser database still fresh.');
  166. return false;
  167. }
  168. };
  169.  
  170. FAADE.getDatabaseConnectionString = function () {
  171. return (FAADE.CONFIG.debug) ?
  172. FAADE.CONFIG.gdocs_development_key :
  173. FAADE.CONFIG.gdocs_key;
  174. };
  175. FAADE.getReportFormKey = function () {
  176. return (FAADE.CONFIG.debug) ?
  177. FAADE.CONFIG.gform_development_key :
  178. FAADE.CONFIG.gform_key;
  179. };
  180.  
  181. FAADE.setValue = function (x, y) {
  182. return (FAADE.CONFIG.debug) ?
  183. GM_setValue(x += '_development', y) :
  184. GM_setValue(x, y);
  185. };
  186. FAADE.getValue = function (x, y) {
  187. if (arguments.length === 1) {
  188. return (FAADE.CONFIG.debug) ?
  189. GM_getValue(x += '_development'):
  190. GM_getValue(x);
  191. } else {
  192. return (FAADE.CONFIG.debug) ?
  193. GM_getValue(x += '_development', y):
  194. GM_getValue(x, y);
  195. }
  196. };
  197.  
  198. FAADE.fetchAbuserDatabase = function () {
  199. var key = FAADE.getDatabaseConnectionString();
  200. var url = 'https://docs.google.com/spreadsheets/d/' + key + '/pub';
  201. FAADE.log('fetching abusers database from ' + url);
  202. GM_xmlhttpRequest({
  203. 'method': 'GET',
  204. 'url': url,
  205. 'onload': function (response) {
  206. if (!response.finalUrl.match(/^https:\/\/docs.google.com\/spreadsheets\/d/)) {
  207. FAADE.log('Failed to fetch abuser database from ' + url);
  208. return false;
  209. }
  210. FAADE.setValue('last_fetch_time', new Date().getTime().toString()); // timestamp this fetch
  211. FAADE.setValue('abuser_database', response.responseText);
  212. FAADE.abuser_database = FAADE.getValue('abuser_database');
  213. }
  214. });
  215. };
  216.  
  217. FAADE.injectDialog = function () {
  218. FL_UI.Dialog.createLink('faade', '', document.body);
  219. var html_string = '<p class="mbm">There have been <span id="faade_reports_to_alert">X</span> new consent violations filed to the Predator Alert Tool for FetLife that may have been perpetrated near your location (<span id="faade_user_loc">X, X, X</span>).</p>';
  220. html_string += '<p>Click "View new nearby PAT-FetLife reports" to view the profiles of the people who have been accused of consent violations near your area in new tabs.</p>';
  221. html_string += '<p id="faade-actions" class="ac">';
  222. html_string += '<a rel="nofollow" class="btnsqr close" data-closes-modal="faade" href="#">View new nearby PAT-FetLife reports</a>';
  223. html_string += '<span class="i s q">&nbsp;-or-&nbsp;</span>';
  224. html_string += '<a data-closes-modal="faade" class="close tdn q" href="#">Cancel</a>';
  225. html_string += '</p>';
  226. html_string += '<p>(Don\'t worry, I\'m not looking for where you actually are. Your location was determined from your FetLife profile.)</p>';
  227. FL_UI.Dialog.inject(
  228. 'faade',
  229. 'Predator Alert Tool for FetLife (PAT-FetLife)',
  230. html_string
  231. );
  232. };
  233.  
  234. FAADE.getLocationFromProfileHtml = function (html) {
  235. var parser = new DOMParser();
  236. var doc = parser.parseFromString(html, 'text/html');
  237. return doc.querySelector('h2.bottom + p > em').textContent.split(', '); // split with comma AND space
  238. };
  239.  
  240. FAADE.broadcastNewProximalReports = function (doc) {
  241. // Recall timestamp of last record checked.
  242. var last_timestamp_checked = parseInt(FAADE.getValue('last_timestamp_checked', '0')); // default is "never!"
  243. // Get latest timestamp in stored alleged abuser database.
  244. var rows = doc.querySelectorAll('table.waffle tr'); // read in every report, in full
  245. var latest_timestamp_filed = Date.parse(rows[rows.length - 1].childNodes[1].textContent);
  246.  
  247. // If never checked, or if there are new records since last timestamp checked
  248. if (last_timestamp_checked < latest_timestamp_filed) {
  249. FAADE.log('Last timestamp checked (' + last_timestamp_checked.toString() + ') is older than latest timestamp filed (' + latest_timestamp_filed.toString() + ').');
  250.  
  251. // count how many new records there are since last check
  252. var num_reports = 0;
  253. for (var i = rows.length - 1; i > 0; i--) {
  254. if (Date.parse(rows[i].childNodes[1].textContent) > last_timestamp_checked) {
  255. num_reports++;
  256. } else {
  257. break; // we've reached the timestamps we've already checked, we're done
  258. }
  259. }
  260. FAADE.log('Total of ' + num_reports + ' new reports since last check.');
  261.  
  262. var user_loc = FAADE.getLocationFromProfileHtml(FL_ASL.users[uw.FetLife.currentUser.id].profile_html);
  263. FAADE.log('Current user location seems to be ' + user_loc.join(', ') + '.');
  264.  
  265. // Loop over all new records one by one
  266. var reports_to_alert = [];
  267. for (var i = rows.length - num_reports; i <= rows.length - 1; i++) {
  268. // extract the location data from the report
  269. report_loc = rows[i].childNodes[6].textContent;
  270. // compare current user's FetLife profile location against alleged abuse location
  271. // and test each substring of the user profile against the reported location
  272. for (var z = 0; z < user_loc.length; z++) {
  273. // if a relevant case insensitive substring matches
  274. if (-1 !== report_loc.toLowerCase().search(user_loc[z].toLowerCase())) {
  275. FAADE.log('Filed report location ' + report_loc + ' matches user location substring ' + user_loc[z] + '!');
  276. // store for future pop-up
  277. reports_to_alert.push(rows[i]);
  278. break; // we found a match, so stop trying on this row
  279. }
  280. }
  281. }
  282.  
  283. // Ask user to view the profiles of the alleged abusers in the user's local vicinity.
  284. if (reports_to_alert.length) {
  285. // Fill in the user-facing message with the appropriate info.
  286. document.getElementById('faade_reports_to_alert').innerHTML = reports_to_alert.length.toString();
  287. document.getElementById('faade_user_loc').innerHTML = user_loc.join(', ');
  288. // Create the click event we're going to use.
  289. var evt = document.createEvent('MouseEvents');
  290. evt.initEvent('click', true, false); // can bubble, can't be cancelled
  291. // "Click" event on hidden code.
  292. document.querySelector('a[data-opens-modal="faade"]').dispatchEvent(evt);
  293. // Attach event listener to "View" button and pass in appropriate URLs.
  294. document.querySelector('.btnsqr[data-closes-modal="faade"]').addEventListener('click', function () {
  295. for (var i = 0; i < reports_to_alert.length; i++) {
  296. // TODO: Add the permalink to the specific report to this URL, so it's highlighted when opened.
  297. var url = 'https://fetlife.com/users/';
  298. GM_openInTab(url + reports_to_alert[i].childNodes[2].textContent.match(/\d+/)[0]);
  299. }
  300. });
  301. }
  302. }
  303.  
  304. // Make a note of the latest timestamp filed, so we start here next time we're loaded.
  305. FAADE.setValue('last_timestamp_checked', latest_timestamp_filed.toString());
  306. };
  307.  
  308. FAADE.creepShield = {};
  309. FAADE.creepShield.checkPhotoUrl = function (url) {
  310. // For Chrome, we need to create the multipart request manually because
  311. // extensions can't decode FormData objects due to its isolated worlds.
  312. // See http://code.google.com/p/tampermonkey/issues/detail?id=183
  313. var multipart_boundary = "---xxx111222333444555666777888999";
  314. var multipart_data = '--' + multipart_boundary + "\n";
  315. multipart_data += 'Content-Disposition: form-data; name="linked_image"';
  316. multipart_data += "\n\n";
  317. multipart_data += url;
  318. multipart_data += "\n";
  319. // Mimic hitting the "Search" button.
  320. multipart_data += '--' + multipart_boundary + "\n";
  321. multipart_data += 'Content-Disposition: form-data; name="submit_linked_image"';
  322. multipart_data += "\n\n";
  323. multipart_data += 'Search';
  324. multipart_data += "\n";
  325. multipart_data += '--' + multipart_boundary + '--'; // end
  326.  
  327. GM_xmlhttpRequest({
  328. 'method': 'POST',
  329. 'url': 'http://www.creepshield.com/search',
  330. 'headers': {
  331. 'Content-Type': 'multipart/form-data; boundary=' + multipart_boundary
  332. },
  333. 'data': multipart_data,
  334. 'onload': function (response) {
  335. var parser = new DOMParser();
  336. var doc = parser.parseFromString(response.responseText, 'text/html');
  337. // If our search was successful,
  338. if (doc.querySelector('.search-details')) {
  339. // Parse the CreepShield results and display on FetLife.
  340. var creep_data = FAADE.creepShield.parseResults(doc);
  341. FAADE.creepShield.displayOnFetLife(creep_data);
  342. } else {
  343. FAADE.log('An error occurred searching CreepShield.com.');
  344. if (doc.getElementById('messages')) {
  345. FAADE.creepShield.displayError(doc.getElementById('messages').textContent);
  346. }
  347. }
  348. }
  349. });
  350. };
  351. FAADE.creepShield.parseResults = function (doc) {
  352. var ret = {
  353. 'searched_url' : doc.querySelector('.searched-image').getAttribute('src'),
  354. 'matches_count': doc.querySelectorAll('.person').length,
  355. 'highest_match': doc.querySelector('.match-percentage p:nth-child(2)').textContent.match(/\d+%/),
  356. 'highest_photo': doc.querySelector('.person-images-inner img'),
  357. 'person_detail': doc.querySelector('.person-name').textContent
  358. };
  359. return ret;
  360. };
  361. FAADE.creepShield.getDisclaimerHtml = function () {
  362. return '<p>This feature is powered by the facial recognition service at <a href="http://creepshield.com/">CreepShield.com</a>. The registered sex offender database is <em>not</em> always a reliable source of information. <a href="https://www.eff.org/deeplinks/2011/04/sexual-predators-please-check-here-match-com-s">Learn more</a>.</p>';
  363. };
  364. FAADE.creepShield.displayOnFetLife = function (creep_data) {
  365. var base_el = document.querySelector('.pan').parentNode.parentNode;
  366. var my_el = document.createElement('div');
  367. my_el.setAttribute('class', 'pat-fetlife-creepshield-results');
  368. var html = '<h3>Possible Registered Sex Offender matches:</h3>';
  369. html += '<ul>';
  370. html += '<li>Highest facial match: ' + creep_data.highest_match + '</li>'
  371. html += '<li>Most likely offender: <img src="' + creep_data.highest_photo.getAttribute('src') + '" alt="" />' + creep_data.person_detail + '</li>';
  372. html += '<li>Total possible matches: ' + creep_data.matches_count + '</li>';
  373. html += '</ul>';
  374. html += '<form method="POST" action="http://www.creepshield.com/search">';
  375. html += '<input type="hidden" name="linked_image" value="' + creep_data.searched_url + '" />';
  376. html += '<p>Search for criminal histories and other possible offenders: ';
  377. html += '<input type="submit" name="submit_linked_image" value="Search" />';
  378. html += '</p>';
  379. html += '</form>';
  380. html += FAADE.creepShield.getDisclaimerHtml();
  381. my_el.innerHTML = html;
  382. base_el.appendChild(my_el);
  383. };
  384. FAADE.creepShield.displayError = function (msg) {
  385. var base_el = document.querySelector('.pan').parentNode.parentNode;
  386. var my_el = document.createElement('div');
  387. my_el.setAttribute('class', 'pat-fetlife-creepshield-results error');
  388. var html = '<p>CreepShield returned an error:</p>';
  389. html += '<blockquote><p>' + msg + '</p></blockquote>';
  390. html += '<p>If you are being told you need to login before you can do more searches, simply <a href="javascript:window.location.reload();void(0);">reload this page</a> to try again.</p>';
  391. html += FAADE.creepShield.getDisclaimerHtml();
  392. my_el.innerHTML = html;
  393. base_el.appendChild(my_el);
  394. // If free search limit was hit, go to CreepShield.com to flush it out.
  395. if (msg.match(/You cannot perform any more searches/i)) {
  396. GM_openInTab('http://www.creepshield.com/search');
  397. }
  398. };
  399.  
  400. FAADE.clearCookies = function () {
  401. var cookie_list = document.cookie.split(';');
  402. for (var i = 0; i < cookie_list.length; i++) {
  403. var cookie_name = cookie_list[i].replace(/\s*(\w+)=.+$/, "$1");
  404. // To delete a cookie, set its expiration date to a past value.
  405. document.cookie = cookie_name + '=;expires=Thu, 01-Jan-1970 00:00:01 GMT;';
  406. }
  407. };
  408.  
  409. // This is the main() function, executed on page load.
  410. FAADE.main = function () {
  411. // Make a list of known alleged abuser user IDs.
  412. var parser = new DOMParser();
  413. var doc = parser.parseFromString(FAADE.abuser_database, 'text/html');
  414. var els = doc.querySelectorAll('table.waffle td:nth-child(3)'); // third child is the column of IDs.
  415. var abuser_ids = [];
  416. for (var i = 1; i < els.length; i++) { // we never need the first (0'th) cell because Google provides it blank.
  417. abuser_ids.push(els[i].innerHTML);
  418. }
  419. FAADE.log('recalled abuser ids ' + abuser_ids);
  420.  
  421. // TODO: Refactor this, it's kludgy.
  422. setTimeout(function() {
  423. FAADE.log('Running time-delayed function.');
  424. if (FL_ASL.users[uw.FetLife.currentUser.id].profile_html) {
  425. FAADE.log('We have the current user\'s FetLife profile HTML. Running broadcast checks.');
  426. FAADE.broadcastNewProximalReports(doc);
  427. }
  428. }, 5000); // give us a few seconds to grab the current user's FetLife profile HTML.
  429.  
  430. // Are we on a user profile page?
  431. if (window.location.href.match(/users\/(\d+)\/?$/)) {
  432.  
  433. var profile_nick = document.querySelector('h2.bottom').childNodes[0].textContent.match(/\S+/)[0];
  434. var id_in_url = window.location.href.match(/users\/(\d+)\/?$/)[1];
  435. var userpic_el = document.querySelector('.pan');
  436. // If we can grab this person's userpic, send it off to CreepSheild for testing.
  437. if (userpic_el) {
  438. var userpic_src = userpic_el.getAttribute('src');
  439. // This will check and call back to the renderer, so we can move on now.
  440. FAADE.creepShield.checkPhotoUrl(userpic_src);
  441. }
  442.  
  443. // If we're not viewing our own profile page, insert a report link.
  444. usr_ops = document.querySelector('#main_content p.quiet');
  445. if (usr_ops) {
  446. usr_ops.appendChild(document.createElement('br'));
  447. usr_ops.appendChild(FAADE.createAbuseReportLink(id_in_url, profile_nick));
  448. }
  449.  
  450. // If this is a profile page of an alleged abuser,
  451. if (-1 !== abuser_ids.indexOf(id_in_url)) {
  452.  
  453. var report_el = document.createElement('table');
  454. report_el.setAttribute('id', 'faade_abuse_reports');
  455. report_el.setAttribute('summary', 'Reported consent violations committed by ' + profile_nick + '.');
  456. var caption = document.createElement('caption');
  457. caption.innerHTML = 'There are reports ' + profile_nick + ' violated others\' consent in these ways:';
  458. var tfoot = document.createElement('tfoot');
  459. tfoot.innerHTML = '<tr><td colspan="2"></td></tr>';
  460. tfoot.querySelector('td').appendChild(FAADE.createAbuseReportLink(id_in_url, profile_nick));
  461. report_el.appendChild(caption);
  462. report_el.appendChild(tfoot);
  463.  
  464. // Find all reports that match ID number.
  465. var abuse_reports = [];
  466. for (var ix = 0; ix < els.length; ix++) {
  467. if (id_in_url === els[ix].innerHTML) {
  468. abuse_reports.push(els[ix].parentNode); // the table row of abuse report
  469. }
  470. }
  471. // Add this information to the top of this user's profile
  472. for (var iy = 0; iy < abuse_reports.length; iy++) {
  473. var num = iy + 1;
  474. var tr = document.createElement('tr');
  475. tr.setAttribute('id', 'faade_abuse_report-' + num.toString());
  476. var details_html = '<ul><li class="faade_abuse_report_datetime">' + abuse_reports[iy].childNodes[7].innerHTML + '</li>';
  477. details_html += '<li class="faade_abuse_report_location">' + abuse_reports[iy].childNodes[6].innerHTML + '</li></ul>';
  478. var permalink_html = '<a class="faade_abuse_reported_datetime" rel="bookmark" href="'
  479. + window.location + '#faade_abuse_report-' + num.toString()
  480. + '" title="Permalink for PAT-FetLife abuse report number ' + num.toString() + ' against '
  481. + profile_nick + '.">' + abuse_reports[iy].childNodes[1].innerHTML + '</a>';
  482. tr.innerHTML += '<th>Abuse report ' + num.toString() + ' (' + permalink_html + '):' + details_html + '</th>';
  483. tr.innerHTML += '<td>' + abuse_reports[iy].childNodes[5].innerHTML + '</td>';
  484. report_el.appendChild(tr);
  485. }
  486.  
  487. var before = document.querySelector('#main_content table:last-child');
  488. before.parentNode.insertBefore(report_el, before);
  489. }
  490.  
  491. }
  492.  
  493. // Collect all user links on this page.
  494. var user_links = [];
  495. for (i = 0; i < document.links.length; i++) {
  496. var l = document.links[i].href.match(/^(https:\/\/fetlife.com)?\/users\/(\d+)\/?$/);
  497. if ( l && (l[2] !== uw.FetLife.currentUser.id.toString()) ) {
  498. user_links.push(document.links[i]);
  499. }
  500. }
  501.  
  502. // For each user ID found,
  503. var last_id = null;
  504. for (i = 0; i < user_links.length; i++) {
  505. // Collect its user ID number.
  506. var id = user_links[i].href.match(/\d+\/?$/);
  507. if (id) { id = id.toString(); } // cast to string for later comparison
  508.  
  509. // Get nickname.
  510. var n;
  511. if (user_links[i].children.length) {
  512. // This is an avatar link, not a text link.
  513. n = user_links[i].childNodes[0].alt;
  514. } else {
  515. // This is a text link. Easy.
  516. n = user_links[i].innerHTML;
  517. }
  518.  
  519. // check the alleged abusers data store for a match.
  520. if (-1 !== abuser_ids.indexOf(id)) {
  521. FAADE.log('found match on this page for alleged abuser ID number ' + id);
  522. // highlight the user's links that matched an alleged abuser using CSS
  523. user_links[i].setAttribute('class', user_links[i].className + ' faade_alleged_abuser');
  524.  
  525. }
  526.  
  527. // Don't create another link if we just made one for that user.
  528. if (id === last_id) { continue; }
  529.  
  530. // Offer a link to add another report for this user.
  531. // See also: https://support.google.com/docs/bin/answer.py?hl=en&answer=160000
  532. // Add link to report this user for a consent violation.
  533. var a = FAADE.createAbuseReportLink(id, n);
  534. user_links[i].parentNode.appendChild(a);
  535. last_id = id;
  536. }
  537. };
  538.  
  539. FAADE.createAbuseReportLink = function (id, nick) {
  540. var a = document.createElement('a');
  541. a.setAttribute('class', 'faade_report_link');
  542. a.setAttribute('target', '_blank');
  543. var href = 'https://docs.google.com/forms/d/' + FAADE.getReportFormKey() + '/viewform';
  544. href += '?entry_2952262=' + id;
  545. href += '&entry_1000001=' + nick;
  546. a.setAttribute('href', href);
  547. a.innerHTML = '(report a consent violation by ' + nick + ')';
  548. return a;
  549. };
  550.  
  551. // The following is required for Chrome compatibility, as we need "text/html" parsing.
  552. /*
  553. * DOMParser HTML extension
  554. * 2012-09-04
  555. *
  556. * By Eli Grey, http://eligrey.com
  557. * Public domain.
  558. * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
  559. */
  560.  
  561. /*! @source https://gist.github.com/1129031 */
  562. /*global document, DOMParser*/
  563.  
  564. (function(DOMParser) {
  565. "use strict";
  566.  
  567. var
  568. DOMParser_proto = DOMParser.prototype
  569. , real_parseFromString = DOMParser_proto.parseFromString
  570. ;
  571.  
  572. // Firefox/Opera/IE throw errors on unsupported types
  573. try {
  574. // WebKit returns null on unsupported types
  575. if ((new DOMParser).parseFromString("", "text/html")) {
  576. // text/html parsing is natively supported
  577. return;
  578. }
  579. } catch (ex) {}
  580.  
  581. DOMParser_proto.parseFromString = function(markup, type) {
  582. if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
  583. var
  584. doc = document.implementation.createHTMLDocument("")
  585. ;
  586.  
  587. doc.body.innerHTML = markup;
  588. return doc;
  589. } else {
  590. return real_parseFromString.apply(this, arguments);
  591. }
  592. };
  593. }(DOMParser));