Advertisement
Guest User

Untitled

a guest
Sep 30th, 2014
187
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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           FetLife ASL Search
  9. // @version        0.3.7
  10. // @namespace      http://maybemaimed.com/playground/fetlife-aslsearch/
  11. // @updateURL      https://userscripts.org/scripts/source/146293.user.js
  12. // @description    Allows you to search for FetLife profiles based on age, sex, location, and role.
  13. // @include        https://fetlife.com/*
  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_ASL = {};
  28. FL_ASL.CONFIG = {
  29.     'debug': false, // switch to true to debug.
  30.     'progress_id': 'fetlife_asl_search_progress',
  31.     'min_matches': 1 // show at least this many matches before offering to search again
  32. };
  33.  
  34. FL_ASL.total_result_count = 0; // How many matches have we found, across all pages, on this load?
  35.  
  36. // Utility debugging function.
  37. FL_ASL.log = function (msg) {
  38.     if (!FL_ASL.CONFIG.debug) { return; }
  39.     GM_log('FETLIFE ASL SEARCH: ' + msg);
  40. };
  41.  
  42. // Initializations.
  43. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility?
  44. GM_addStyle('\
  45. #fetlife_asl_search_options { display: none; }\
  46. #fetlife_asl_search_options fieldset { clear: both; margin: 0; padding: 0; }\
  47. #fetlife_asl_search_options legend { display: none; }\
  48. #fetlife_asl_search_options label {\
  49.   display: inline-block;\
  50.   white-space: nowrap;\
  51. }\
  52. #fetlife_asl_search_options input { width: auto; }\
  53. #fetlife_asl_search_results { clear: both; }\
  54. ');
  55. FL_ASL.users = {};
  56. FL_ASL.init = function () {
  57.     FL_ASL.CONFIG.search_form = document.querySelector('form[action="/search"]').parentNode;
  58.     FL_ASL.getUserProfile(uw.FetLife.currentUser.id);
  59.     FL_ASL.main();
  60. };
  61. window.addEventListener('DOMContentLoaded', FL_ASL.init);
  62.  
  63. FL_ASL.toggleAslSearch = function () {
  64.     var el = document.getElementById('fetlife_asl_search_options');
  65.     if (el.style.display == 'block') {
  66.         el.style.display = 'none';
  67.     } else {
  68.         el.style.display = 'block';
  69.     }
  70. };
  71.  
  72. FL_ASL.toggleLocationFilter = function (e) {
  73.     var el = document.getElementById('fl_asl_loc_filter_label');
  74.     switch (e.currentTarget.value) {
  75.         case 'group':
  76.         case 'event':
  77.         case 'fetish':
  78.         case 'search':
  79.         case 'user':
  80.             if (el.style.display == 'none') {
  81.                 el.style.display = 'inline';
  82.             }
  83.             break;
  84.         default:
  85.             el.style.display = 'none';
  86.             break;
  87.     }
  88. };
  89.  
  90. FL_ASL.aslSubmit = function (e) {
  91.     var el = document.getElementById('fetlife_asl_search');
  92.     if (!el.checked) {
  93.         return false;
  94.     }
  95.  
  96.     // Provide UI feedback.
  97.     var prog = document.getElementById(FL_ASL.CONFIG.progress_id);
  98.     prog.innerHTML = 'Searching&hellip;<br />';
  99.  
  100.     // collect the form parameters
  101.     var search_params = FL_ASL.getSearchParams();
  102.  
  103.     // search one of the geographic regions "/kinksters" list
  104.     FL_ASL.getKinkstersInSet(search_params.loc);
  105.  
  106.     return false;
  107. };
  108.  
  109. /**
  110.  * Reads and saves the search parameters from the provided form.
  111.  */
  112. FL_ASL.getSearchParams = function () {
  113.     var r = {
  114.         'age'   : {'min': null, 'max': null},
  115.         'sex'   : [],
  116.         'role'  : [],
  117.         'loc'   : {},
  118.         'filter': ''
  119.     };
  120.  
  121.     // Collect age parameters, setting wide defaults.
  122.     r.age.min = (document.getElementById('min_age').value) ? parseInt(document.getElementById('min_age').value) : 1;
  123.     r.age.max = (document.getElementById('max_age').value) ? parseInt(document.getElementById('max_age').value) : 99;
  124.  
  125.     // Collect gender/sex parameters.
  126.     var x = FL_ASL.CONFIG.search_form.querySelectorAll('input[name="user[sex]"]');
  127.     for (var i = 0; i < x.length; i++) {
  128.         if (x[i].checked) {
  129.             r.sex.push(x[i].value);
  130.         }
  131.     }
  132.  
  133.     // Collect role orientation parameters.
  134.     var y = FL_ASL.CONFIG.search_form.querySelectorAll('input[name="user[role]"]');
  135.     for (var iy = 0; iy < y.length; iy++) {
  136.         if (y[iy].checked) {
  137.             r.role.push(y[iy].value);
  138.         }
  139.     }
  140.  
  141.     // Collect location parameters.
  142.     var search_in = [];
  143.     var z = FL_ASL.CONFIG.search_form.querySelectorAll('input[name="fl_asl_loc"]');
  144.     for (var iz = 0; iz < z.length; iz++) {
  145.         if (z[iz].checked) {
  146.             search_in.push(z[iz].value);
  147.         }
  148.     }
  149.     // Match location parameter with known location ID.
  150.     switch (search_in[0]) {
  151.         // These cases all use numeric object IDs.
  152.         case 'group':
  153.         case 'event':
  154.         case 'user':
  155.         case 'fetish':
  156.             r.loc[search_in[0]] = parseInt(FL_ASL.CONFIG.search_form.querySelector('input[data-flasl' + search_in[0] + 'id]').getAttribute('data-flasl' + search_in[0] + 'id'));
  157.         break;
  158.         // This case uses a string, so no need to parseInt() it.
  159.         case 'search':
  160.             r.loc[search_in[0]] = FL_ASL.CONFIG.search_form.querySelector('input[data-flasl' + search_in[0] + 'id]').getAttribute('data-flasl' + search_in[0] + 'id');
  161.             break;
  162.         default:
  163.             user_loc = FL_ASL.getLocationForUser(uw.FetLife.currentUser.id);
  164.             for (var xk in user_loc) {
  165.                 if (null !== user_loc[xk] && (-1 !== search_in.indexOf(xk)) ) {
  166.                     r.loc[xk] = user_loc[xk];
  167.                 }
  168.             }
  169.         break;
  170.     }
  171.  
  172.     // Collect location filter, if one was entered.
  173.     if (document.getElementById('fl_asl_loc_filter')) {
  174.         r.filter = document.getElementById('fl_asl_loc_filter').value;
  175.     }
  176.  
  177.     return r;
  178. };
  179.  
  180. FL_ASL.getLocationForUser = function (id) {
  181.     var r = {
  182.         'city_id': null,
  183.         'area_id': null,
  184.         'country': null
  185.     };
  186.     var profile_html = FL_ASL.users[id].profile_html;
  187.     var m = profile_html.match(/href="\/countries\/([0-9]+)/);
  188.    if (m) {
  189.        r.country = m[1];
  190.    }
  191.    m = profile_html.match(/href="\/administrative_areas\/([0-9]+)/);
  192.     if (m) {
  193.         r.area_id = m[1];
  194.     }
  195.     m = profile_html.match(/href="\/cities\/([0-9]+)/);
  196.    if (m) {
  197.        r.city_id = m[1];
  198.    }
  199.  
  200.    return r;
  201. };
  202.  
  203. FL_ASL.getUserProfile = function (id) {
  204.    if (FL_ASL.users[id]) {
  205.        return FL_ASL.users[id].profile_html;
  206.    } else {
  207.        FL_ASL.users[id] = {};
  208.        GM_xmlhttpRequest({
  209.            'method': 'GET',
  210.            'url': 'https://fetlife.com/users/' + id.toString(),
  211.            'onload': function (response) {
  212.                FL_ASL.users[id].profile_html = response.responseText;
  213.            }
  214.        });
  215.    }
  216. };
  217.  
  218. FL_ASL.getKinkstersInSet = function (loc_obj) {
  219.    if (loc_obj.group) {
  220.        FL_ASL.getKinkstersInGroup(loc_obj.group);
  221.    } else if (loc_obj.event) {
  222.        FL_ASL.getKinkstersInEvent(loc_obj.event);
  223.    } else if (loc_obj.user) {
  224.        FL_ASL.getKinkstersInFriend(loc_obj.user);
  225.    } else if (loc_obj.fetish) {
  226.        FL_ASL.getKinkstersInFetish(loc_obj.fetish);
  227.    } else if (loc_obj.search) {
  228.        FL_ASL.getKinkstersInSearch(loc_obj.search);
  229.    } else if (loc_obj.city_id) {
  230.        FL_ASL.getKinkstersInCity(loc_obj.city_id);
  231.    } else if (loc_obj.area_id) {
  232.        FL_ASL.getKinkstersInArea(loc_obj.area_id);
  233.    } else if (loc_obj.country) {
  234.        FL_ASL.getKinkstersInCountry(loc_obj.country);
  235.    } else {
  236.        return false;
  237.    }
  238. };
  239.  
  240. FL_ASL.getKinkstersInCity = function (city_id, page) {
  241.    var url = 'https://fetlife.com/cities/' + city_id.toString() + '/kinksters';
  242.    url = (page) ? url + '?page=' + page.toString() : url ;
  243.    FL_ASL.getKinkstersFromURL(url);
  244. };
  245. FL_ASL.getKinkstersInArea = function (area_id, page) {
  246.    var url = 'https://fetlife.com/administrative_areas/' + area_id.toString() + '/kinksters';
  247.    url = (page) ? url + '?page=' + page.toString() : url ;
  248.    FL_ASL.getKinkstersFromURL(url);
  249. };
  250. FL_ASL.getKinkstersInCountry = function (country, page) {
  251.    var url = 'https://fetlife.com/countries/' + country.toString() + '/kinksters';
  252.    url = (page) ? url + '?page=' + page.toString() : url ;
  253.    FL_ASL.getKinkstersFromURL(url);
  254. };
  255. FL_ASL.getKinkstersInGroup = function (group, page) {
  256.    var url = 'https://fetlife.com/groups/' + group.toString() + '/group_memberships';
  257.    url = (page) ? url + '?page=' + page.toString() : url ;
  258.    FL_ASL.getKinkstersFromURL(url);
  259. };
  260. FL_ASL.getKinkstersInEvent = function (event, page) {
  261.    var url = 'https://fetlife.com/events/' + event.toString() + '/rsvps';
  262.    url = (page) ? url + '?page=' + page.toString() : url ;
  263.    FL_ASL.getKinkstersFromURL(url);
  264. };
  265. FL_ASL.getKinkstersInFriend = function (user_id, page) {
  266.    var url = 'https://fetlife.com/users/' + user_id.toString() + '/friends';
  267.    url = (page) ? url + '?page=' + page.toString() : url ;
  268.    FL_ASL.getKinkstersFromURL(url);
  269. };
  270. FL_ASL.getKinkstersInFetish = function (fetish_id, page) {
  271.    var url = 'https://fetlife.com/fetishes/' + fetish_id.toString() + '/kinksters';
  272.    url = (page) ? url + '?page=' + page.toString() : url ;
  273.    FL_ASL.getKinkstersFromURL(url);
  274. };
  275. FL_ASL.getKinkstersInSearch = function (search_string, page) {
  276.    var url = 'https://fetlife.com/search/kinksters/?q=' + search_string.toString();
  277.    url = (page) ? url + '&page=' + page.toString() : url ;
  278.    FL_ASL.getKinkstersFromURL(url);
  279. };
  280. FL_ASL.getKinkstersFromURL = function (url) {
  281.    FL_ASL.log('Getting Kinksters list from URL: ' + url);
  282.    // Set minimum matches, if that's been asked for.
  283.    if (document.getElementById('fl_asl_min_matches').value) {
  284.        FL_ASL.CONFIG.min_matches = document.getElementById('fl_asl_min_matches').value;
  285.    }
  286.    prog = document.getElementById(FL_ASL.CONFIG.progress_id);
  287.    prog.innerHTML = prog.innerHTML + '.';
  288.    GM_xmlhttpRequest({
  289.        'method': 'GET',
  290.        'url': url,
  291.        'onload': function (response) {
  292.            var parser = new DOMParser();
  293.            var doc = parser.parseFromString(response.responseText, 'text/html');
  294.            var els = doc.querySelectorAll('.user_in_list');
  295.  
  296.            result_count = 0;
  297.            for (var i = 0; i < els.length; i++) {
  298.                // filter the results based on the form parameters
  299.                if (FL_ASL.matchesSearchParams(els[i])) {
  300.                    // display the results in a "results" section in this portion of the page
  301.                    FL_ASL.displayResult(els[i]);
  302.                    result_count++;
  303.                    // note total results found
  304.                    FL_ASL.total_result_count += result_count;
  305.                }
  306.            }
  307.  
  308.            // Set up next request.
  309.            my_page = (url.match(/\d+$/)) ? parseInt(url.match(/\d+$/)[0]) : 1 ;
  310.            next_page = my_page + 1;
  311.            if (next_page > 2) {
  312.                next_url = url.replace(/\d+$/, next_page.toString());
  313.            } else {
  314.                // Already have a query string? If so, append (&) rather than create (?).
  315.                next_url = (url.match(/\?q=/)) ? url + '&page=' : url + '?page=';
  316.                next_url += next_page.toString();
  317.            }
  318.  
  319.            // Automatically search on next page if no or too few results were found.
  320.            if (0 === result_count || FL_ASL.CONFIG.min_matches >= FL_ASL.total_result_count) {
  321.                FL_ASL.getKinkstersFromURL(next_url);
  322.                return false;
  323.            } else {
  324.                // Reset total_result_count for this load.
  325.                FL_ASL.total_result_count = 0;
  326.                // Reset UI search feedback.
  327.                p = prog.parentNode
  328.                p.removeChild(prog);
  329.                new_prog = document.createElement('p');
  330.                new_prog.setAttribute('id', FL_ASL.CONFIG.progress_id);
  331.                p.appendChild(new_prog);
  332.            }
  333.            btn = document.createElement('button');
  334.            btn.setAttribute('id', 'btn_moar');
  335.            btn.setAttribute('onclick', "var xme = document.getElementById('btn_moar'); xme.parentNode.removeChild(xme); return false;");
  336.            btn.innerHTML = 'Show me MOAR&hellip;';
  337.            btn.addEventListener('click', function(){FL_ASL.getKinkstersFromURL(next_url)});
  338.            document.getElementById('fetlife_asl_search_results').appendChild(btn);
  339.        }
  340.    });
  341. };
  342.  
  343. /**
  344. * Determines whether a "user_in_list" block matches the searched-for parameters.
  345. *
  346. * @return True if block matches all search parameters, false otherwise.
  347. */
  348. FL_ASL.matchesSearchParams = function (el) {
  349.    var search_params = FL_ASL.getSearchParams();
  350.  
  351.    // Does block match location string filter?
  352.    if (-1 === FL_ASL.getLocationString(el).toLowerCase().search(search_params.filter.toLowerCase())) {
  353.        return false;
  354.    }
  355.  
  356.    // Does block match age range?
  357.    var age = FL_ASL.getAge(el);
  358.    // Did we supply a minimum age?
  359.    if (search_params.age.min && (search_params.age.min > age) ) {
  360.        return false;
  361.    }
  362.    // Did we supply a maximum age?
  363.    if (search_params.age.max && (search_params.age.max < age) ) {
  364.        return false;
  365.    }
  366.  
  367.    // Does block match gender/sex selection?
  368.    if (-1 === search_params.sex.indexOf(FL_ASL.getSex(el))) {
  369.        return false;
  370.    }
  371.  
  372.    // Does block match role orientation selection?
  373.    if (-1 === search_params.role.indexOf(FL_ASL.getRole(el))) {
  374.        return false;
  375.    }
  376.  
  377.    // All conditions match.
  378.    return true;
  379. };
  380.  
  381. FL_ASL.getSex = function (el) {
  382.    var x = el.querySelector('.quiet').innerHTML;
  383.    var sex = x.match(/^\d\d(\S*)/);
  384.    return sex[1];
  385. };
  386.  
  387. FL_ASL.getAge = function (el) {
  388.    var x = el.querySelector('.quiet').innerHTML;
  389.    var age = x.match(/^\d\d/);
  390.    return parseInt(age);
  391. };
  392.  
  393. FL_ASL.getRole = function (el) {
  394.    var x = el.querySelector('.quiet').innerHTML;
  395.    var role = x.match(/ ?(\S+)?$/);
  396.    return role[1];
  397. };
  398. FL_ASL.getLocationString = function (el) {
  399.    return el.querySelector('em').innerHTML;
  400. };
  401.  
  402. FL_ASL.displayResult = function (el) {
  403.    var id = el.querySelector('a').getAttribute('href').match(/\d+$/);
  404.    var name = el.querySelector('.large a').childNodes[0].nodeValue;
  405.    var a = document.createElement('a');
  406.    a.href = 'https://fetlife.com/conversations/new?with=' + id;
  407.    a.innerHTML = '(send ' + name + ' a message)';
  408.    a.style.textDecoration = 'underline';
  409.    a.setAttribute('target', '_blank');
  410.    el.appendChild(a);
  411.    document.getElementById('fetlife_asl_search_results').appendChild(el);
  412. };
  413.  
  414. // This is the main() function, executed on page load.
  415. FL_ASL.main = function () {
  416.    // Insert ASL search button interface at FetLife "Search" bar.
  417.    var label = document.createElement('label');
  418.    label.innerHTML = 'A/S/L?';
  419.    var input = document.createElement('input');
  420.    input.setAttribute('style', '-webkit-appearance: checkbox');
  421.    input.setAttribute('type', 'checkbox');
  422.    input.setAttribute('id', 'fetlife_asl_search');
  423.    input.setAttribute('name', 'fetlife_asl_search');
  424.    input.setAttribute('value', '1');
  425.    input.addEventListener('click', FL_ASL.toggleAslSearch);
  426.    label.appendChild(input);
  427.    var div = document.createElement('div');
  428.    div.setAttribute('id', 'fetlife_asl_search_options');
  429.    div.setAttribute('style', 'display: none;');
  430.    html_string = '<fieldset><legend>Search for user profiles of the following gender/sex:</legend><p>';
  431.    html_string += 'Show me profiles of people with a gender/sex of&hellip;';
  432.    // NOTE: What if the UI only allowed us to find male-ish identified people, not women? What would the response be? :)
  433.    html_string += '<label><input type="checkbox" name="user[sex]" value="M" checked="checked" /> Male</label>';
  434.    html_string += '<label><input type="checkbox" name="user[sex]" value="F" checked="checked" /> Female</label>';
  435. //    html_string += '<label><input type="checkbox" name="user[sex]" value="CD/TV" />Crossdresser/Transvestite</label>';
  436.    html_string += '<label><input type="checkbox" name="user[sex]" value="MtF" />Trans - Male to Female</label>';
  437.    html_string += '<label><input type="checkbox" name="user[sex]" value="FtM" checked="checked" />Trans - Female to Male</label>';
  438.    html_string += '<label><input type="checkbox" name="user[sex]" value="TG" />Transgender</label>';
  439.    html_string += '<label><input type="checkbox" name="user[sex]" value="GF" />Gender Fluid</label>';
  440.    html_string += '<label><input type="checkbox" name="user[sex]" value="GQ" />Genderqueer</label>';
  441.    html_string += '<label><input type="checkbox" name="user[sex]" value="IS" />Intersex</label>';
  442.    html_string += '<label><input type="checkbox" name="user[sex]" value="B" />Butch</label>';
  443.    html_string += '<label><input type="checkbox" name="user[sex]" value="FEM" />Femme</label>';
  444.    html_string += '</p></fieldset>';
  445.    html_string += '<fieldset><legend>Search for user profiles between the ages of:</legend><p>';
  446.    html_string += '&hellip;who are also <label>at least <input type="text" name="min_age" id="min_age" placeholder="18" size="2" /> years old</label> and <label>at most <input type="text" name="max_age" id="max_age" placeholder="92" size="2" /> years old&hellip;</label>';
  447.    html_string += '</p></fieldset>';
  448.    html_string += '<fieldset><legend>Search for user profiles whose role is:</legend><p>';
  449.    html_string += '&hellip;who identify their role as ';
  450.    // Note that these values are what show up, not necessarily what's sent to the FetLife backend.
  451.    html_string += '<label><input type="checkbox" name="user[role]" value="Dom" />Dominant</label>';
  452.    html_string += '<label><input type="checkbox" name="user[role]" value="Domme" />Domme</label>';
  453.    html_string += '<label><input type="checkbox" name="user[role]" value="Switch" />Switch</label>';
  454.    html_string += '<label><input type="checkbox" name="user[role]" value="sub" checked="checked" />submissive</label>';
  455.    html_string += '<label><input type="checkbox" name="user[role]" value="Master" />Master</label>';
  456.    html_string += '<label><input type="checkbox" name="user[role]" value="Mistress" />Mistress</label>';
  457.    html_string += '<label><input type="checkbox" name="user[role]" value="slave" checked="checked" />slave</label>';
  458.    html_string += '<label><input type="checkbox" name="user[role]" value="pet" checked="checked" />pet</label>';
  459.    html_string += '<label><input type="checkbox" name="user[role]" value="kajira" />kajira</label>';
  460.    html_string += '<label><input type="checkbox" name="user[role]" value="kajirus" />kajirus</label>';
  461.    html_string += '<label><input type="checkbox" name="user[role]" value="Top" />Top</label>';
  462.    html_string += '<label><input type="checkbox" name="user[role]" value="bottom" checked="checked" />Bottom</label>';
  463.    html_string += '<label><input type="checkbox" name="user[role]" value="Sadist" />Sadist</label>';
  464.    html_string += '<label><input type="checkbox" name="user[role]" value="Masochist" checked="checked" />Masochist</label>';
  465.    html_string += '<label><input type="checkbox" name="user[role]" value="Sadomasochist" />Sadomasochist</label>';
  466.    html_string += '<label><input type="checkbox" name="user[role]" value="Ageplayer" />Ageplayer</label>';
  467.     html_string += '<label><input type="checkbox" name="user[role]" value="Daddy" />Daddy</label>';    
  468.    html_string += '<label><input type="checkbox" name="user[role]" value="babygirl" />babygirl</label>';
  469.    html_string += '<label><input type="checkbox" name="user[role]" value="brat" />brat</label>';
  470.    html_string += '<label><input type="checkbox" name="user[role]" value="Primal" />Primal</label>';
  471.    html_string += '<label><input type="checkbox" name="user[role]" value="Fetishist" />Fetishist</label>';
  472.    html_string += '<label><input type="checkbox" name="user[role]" value="Kinkster" />Kinkster</label>';
  473.    html_string += '<label><input type="checkbox" name="user[role]" value="Hedonist" />Hedonist</label>';
  474.    html_string += '<label><input type="checkbox" name="user[role]" value="Vanilla" />Vanilla</label>';
  475.    html_string += '<label><input type="checkbox" name="user[role]" value="Unsure" />Unsure</label>';
  476.    // Note that "Not Applicable" is the equivalent of "it doesn't matter", so we omit this.
  477.    //html_string += '<label><input type="checkbox" name="user[role]" value="" />Not Applicable</label>';
  478.    html_string += '</p></fieldset>';
  479.    html_string += '<fieldset><legend>Search for user profiles located in:</legend><p>';
  480.    html_string += '&hellip;from ';
  481.    // If we're on a "groups" or "events" or "user" or "fetish" or "search" page,
  482.     var which_thing = window.location.toString().match(/(group|event|user|fetish)e?s\/(\d+)/) || window.location.toString().match(/(search)\/kinksters\/?\?(?:page=\d+&)?q=(\S+)/);
  483.     if (null !== which_thing) {
  484.         switch (which_thing[1]) {
  485.             case 'user':
  486.                 var label_text = "user's friends";
  487.                 break;
  488.             case 'group': // fall through
  489.             case 'event':
  490.             case 'fetish':
  491.             case 'search':
  492.             default:
  493.                 var label_text = which_thing[1];
  494.                 break;
  495.         }
  496.         // offer an additional option to search for users associated with this object rather than geography.
  497.         html_string += '<label><input type="radio" name="fl_asl_loc" value="' + which_thing[1] + '" data-flasl' + which_thing[1] + 'id="' + which_thing[2] + '"/>this ' + label_text + '</label>';
  498.         html_string += '<label id="fl_asl_loc_filter_label" style="display: none;"> located in <input type="text" id="fl_asl_loc_filter" name="fl_asl_loc_filter" /></label>';
  499.         html_string += ', or ';
  500.     }
  501.     html_string += ' my <label><input type="radio" name="fl_asl_loc" value="city_id" />city</label>';
  502.     html_string += '<label><input type="radio" name="fl_asl_loc" value="area_id" checked="checked" />state/province</label>';
  503.     html_string += '<label><input type="radio" name="fl_asl_loc" value="country" />country</label>';
  504.     html_string += '.</p></fieldset>';
  505.     html_string += '<fieldset><legend>Result set options:</legend><p>';
  506.     html_string += '<label>Return at least <input id="fl_asl_min_matches" name="fl_asl_min_matches" value="" placeholder="1" size="2" /> matches per search.</label> (Set this lower if no results seem to ever appear.)';
  507.     html_string += '</p></fieldset>';
  508.     div.innerHTML = html_string;
  509.     FL_ASL.CONFIG.search_form.appendChild(label);
  510.     FL_ASL.CONFIG.search_form.appendChild(div);
  511.     var radio_els = document.querySelectorAll('input[name="fl_asl_loc"]');
  512.     for (var i = 0; i < radio_els.length; i++) {
  513.         radio_els[i].addEventListener('click', FL_ASL.toggleLocationFilter);
  514.     }
  515.  
  516.     btn_submit = document.createElement('button');
  517.     btn_submit.setAttribute('id', 'btn_fetlife_asl_search_submit');
  518.     btn_submit.setAttribute('onclick', "var xme = document.getElementById('btn_fetlife_asl_search_submit'); xme.parentNode.removeChild(xme); return false;");
  519.     btn_submit.innerHTML = 'Mine! (I mean, uh, search&hellip;)';
  520.     btn_submit.addEventListener('click', FL_ASL.aslSubmit);
  521.     div.appendChild(btn_submit);
  522.  
  523.     results_container = document.createElement('div');
  524.     results_container.setAttribute('id', 'fetlife_asl_search_results');
  525.     FL_ASL.CONFIG.search_form.appendChild(results_container);
  526.  
  527.     prog = document.createElement('p');
  528.     prog.setAttribute('id', FL_ASL.CONFIG.progress_id);
  529.     FL_ASL.CONFIG.search_form.appendChild(prog);
  530. };
  531.  
  532. FAADE = {};
  533. FAADE.CONFIG = {
  534.     'debug': false, // switch to true to debug.
  535.     'gdocs_key': '0ArYmNHuRadHbdGNVT1kzSzFnOXhHRjh1RnczZVVmMXc',
  536.     'gform_key': 'dGNVT1kzSzFnOXhHRjh1RnczZVVmMXc6MQ',
  537.     'gdocs_development_key': '0ArYmNHuRadHbdGxjMUhyR0FzLWJicHNXUFdxckFEQWc',
  538.     'gform_development_key': 'dGxjMUhyR0FzLWJicHNXUFdxckFEQWc6MQ',
  539. };
  540.  
  541. // Utility debugging function.
  542. FAADE.log = function (msg) {
  543.     if (!FAADE.CONFIG.debug) { return; }
  544.     GM_log('FETLIFE FAADE: ' + msg);
  545. };
  546.  
  547. // Initializations.
  548. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility?
  549. GM_addStyle('\
  550. /* Highlight alleged abusers in bright yellow. */\
  551. .faade_alleged_abuser {\
  552.   display: inline-block;\
  553.   border: 2px solid yellow;\
  554. }\
  555. #faade_abuse_reports caption {\
  556.   background: yellow;\
  557.   color: red;\
  558. }\
  559. #faade_abuse_reports tfoot td {\
  560.   padding-top: 1em;\
  561.   text-align: center;\
  562. }\
  563. #faade_abuse_reports tr:target > * {\
  564.   border: 1px solid red;\
  565. }\
  566. #faade_abuse_reports tr:target th {\
  567.   border-width: 1px 0 1px 1px;\
  568. }\
  569. #faade_abuse_reports tr:target td {\
  570.   border-width: 1px 1px 1px 0;\
  571. }\
  572. /* FAADE location broadcast dialog styles. */\
  573. [aria-labelledby="ui-dialog-title-faade"] { border-color: yellow; }\
  574. #ui-dialog-title-faade { color: red; }\
  575. /* General prettiness. */\
  576. #profile #main_content a + a.faade_report_link { padding-left: 5px; }\
  577. footer .faade_report_link,\
  578. .blog_entry p.quiet.small .faade_report_link,\
  579. .byline .faade_report_link {\
  580.   display: block;\
  581.   color: #777;\
  582. }\
  583. .mini_feed_title .faade_report_link {\
  584.   float: left;\
  585.   padding-right: 5px;\
  586. }\
  587. ul.pictures li a.faade_report_link,\
  588. #profile ul.friends li { width: auto; }\
  589. ');
  590. FAADE.init = function () {
  591.     FL_ASL.getUserProfile(uw.FetLife.currentUser.id); // run early
  592.     FAADE.injectDialog();
  593.     FAADE.abuser_database = FAADE.getValue('abuser_database', false);
  594.     if (FAADE.abuserDatabaseExpired()) {
  595.         FAADE.fetchAbuserDatabase();
  596.     }
  597.     FAADE.main();
  598. };
  599. window.addEventListener('DOMContentLoaded', FAADE.init);
  600.  
  601. // Determines whether the abuser database has expired and needs to be re-fetched.
  602. FAADE.abuserDatabaseExpired = function () {
  603.     // If we don't have a database, then of course it's "expired."
  604.     if (!FAADE.abuser_database) {
  605.         FAADE.log('Abuser database expired because of false-equivalent value.');
  606.         return true;
  607.     } else if ( (new Date().getTime() > (parseInt(FAADE.getValue('last_fetch_time')) + 86400)) ) {
  608.         // Abuser database was last fetched more than 24 hours (86400 seconds) ago, so refresh.
  609.         FAADE.log('Abuser database expired because of time.');
  610.         return true;
  611.     } else {
  612.         FAADE.log('Abuser database still fresh.');
  613.         return false;
  614.     }
  615. };
  616.  
  617. FAADE.getDatabaseConnectionString = function () {
  618.     return (FAADE.CONFIG.debug) ?
  619.         FAADE.CONFIG.gdocs_development_key :
  620.         FAADE.CONFIG.gdocs_key;
  621. };
  622. FAADE.getReportFormKey = function () {
  623.     return (FAADE.CONFIG.debug) ?
  624.         FAADE.CONFIG.gform_development_key :
  625.         FAADE.CONFIG.gform_key;
  626. };
  627.  
  628. FAADE.setValue = function (x, y) {
  629.     return (FAADE.CONFIG.debug) ?
  630.         GM_setValue(x += '_development', y) :
  631.         GM_setValue(x, y);
  632. };
  633. FAADE.getValue = function (x, y) {
  634.     if (arguments.length === 1) {
  635.         return (FAADE.CONFIG.debug) ?
  636.             GM_getValue(x += '_development'):
  637.             GM_getValue(x);
  638.     } else {
  639.         return (FAADE.CONFIG.debug) ?
  640.             GM_getValue(x += '_development', y):
  641.             GM_getValue(x, y);
  642.     }
  643. };
  644.  
  645. FAADE.fetchAbuserDatabase = function () {
  646.     var key = FAADE.getDatabaseConnectionString();
  647.     var url = 'https://docs.google.com/spreadsheet/pub?key=' + key + '&output=html';
  648.     FAADE.log('fetching abusers database from ' + url);
  649.     GM_xmlhttpRequest({
  650.         'method': 'GET',
  651.         'url': url,
  652.         'onload': function (response) {
  653.             if (!response.finalUrl.match(/^https:\/\/docs.google.com\/spreadsheet\/pub/)) {
  654.                 FAADE.log('Failed to fetch abuser database from ' + url);
  655.                 return false;
  656.             }
  657.             FAADE.setValue('last_fetch_time', new Date().getTime().toString()); // timestamp this fetch
  658.             FAADE.setValue('abuser_database', response.responseText);
  659.             FAADE.abuser_database = FAADE.getValue('abuser_database');
  660.         }
  661.     });
  662. };
  663.  
  664. FAADE.injectDialog = function () {
  665.     // Inject hidden dialog box link.
  666.     var trigger_el = document.createElement('a');
  667.     trigger_el.setAttribute('class', 'opens-modal');
  668.     trigger_el.setAttribute('data-opens-modal', 'faade');
  669.     document.body.appendChild(trigger_el);
  670.  
  671.     // Inject dialog box HTML. FetLife currently uses Rails 3, so mimic that.
  672.     // See, for instance, Rails Behaviors: http://josh.github.com/rails-behaviors/
  673.     var faade_dialog = document.createElement('div');
  674.     faade_dialog.setAttribute('style', 'display: none; position: absolute; overflow: hidden; z-index: 1000; outline: 0px none;');
  675.     faade_dialog.setAttribute('class', 'ui-dialog ui-widget ui-widget-content ui-corner-all');
  676.     faade_dialog.setAttribute('tabindex', '-1');
  677.     faade_dialog.setAttribute('role', 'dialog');
  678.     faade_dialog.setAttribute('aria-labelledby', 'ui-dialog-title-faade');
  679.     var html_string = '<div class="ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix" unselectable="on" style="-moz-user-select: none;">';
  680.     html_string += '<span class="ui-dialog-title" id="ui-dialog-title-faade" unselectable="on" style="-moz-user-select: none;">FetLife Alleged Abusers Database Engine (FAADE)</span>';
  681.     html_string += '<a href="#" class="ui-dialog-titlebar-close ui-corner-all" role="button" unselectable="on" style="-moz-user-select: none;">';
  682.     html_string += '<span class="ui-icon ui-icon-closethick" unselectable="on" style="-moz-user-select: none;">close</span>';
  683.     html_string += '</a>';
  684.     html_string += '</div>';
  685.     html_string += '<div data-modal-title="FetLife Alleged Abusers Database Engine (FAADE)" data-modal-height="280" data-modal-auto-open="false" class="modal ui-dialog-content ui-widget-content" id="faade">';
  686.     html_string += '<p class="mbm">There have been <span id="faade_reports_to_alert">X</span> new consent violations filed in FAADE that may have been perpetrated near your location (<span id="faade_user_loc">X, X, X</span>).</p>';
  687.     html_string += '<p>Click "View new nearby FAADE reports" to view the profiles of the people who have been accused of consent violations near your area in new tabs.</p>';
  688.     html_string += '<p id="faade-actions" class="ac">';
  689.     html_string += '<a rel="nofollow" class="btnsqr close" data-closes-modal="faade" href="#">View new nearby FAADE reports</a>';
  690.     html_string += '<span class="i s q">&nbsp;-or-&nbsp;</span>';
  691.     html_string += '<a data-closes-modal="faade" class="close tdn q" href="#">Cancel</a>';
  692.     html_string += '</p>';
  693.     html_string += '<p>(Don\'t worry, I\'m not looking for where you actually are. Your location was determined from your FetLife profile.)</p>';
  694.     html_string += '</div>';
  695.     faade_dialog.innerHTML = html_string;
  696.     document.body.appendChild(faade_dialog);
  697.  
  698.     // Attach event listener to trigger element.
  699.     document.querySelector('[data-opens-modal="faade"]').addEventListener('click', function (e) {
  700.         document.querySelector('[data-opens-modal="faade"]').dialog("open");
  701.     });
  702. };
  703.  
  704. FAADE.getLocationFromProfileHtml = function (html) {
  705.     var parser = new DOMParser();
  706.     var doc = parser.parseFromString(html, 'text/html');
  707.     return doc.querySelector('h2.bottom + p > em').textContent.split(', '); // split with comma AND space
  708. };
  709.  
  710. FAADE.broadcastNewProximalReports = function (doc) {
  711.     // Recall timestamp of last record checked.
  712.     var last_timestamp_checked = parseInt(FAADE.getValue('last_timestamp_checked', '0')); // default is "never!"
  713.     // Get latest timestamp in stored alleged abuser database.
  714.     var rows = doc.querySelectorAll('#tblMain tr'); // read in every report, in full
  715.     var latest_timestamp_filed = Date.parse(rows[rows.length - 1].childNodes[1].textContent);
  716.  
  717.     // If never checked, or if there are new records since last timestamp checked
  718.     if (last_timestamp_checked < latest_timestamp_filed) {
  719.         FAADE.log('Last timestamp checked (' + last_timestamp_checked.toString() + ') is older than latest timestamp filed (' + latest_timestamp_filed.toString() + ').');
  720.  
  721.         // count how many new records there are since last check
  722.         var num_reports = 0;
  723.         for (var i = rows.length - 1; i > 0; i--) {
  724.             if (Date.parse(rows[i].childNodes[1].textContent) > last_timestamp_checked) {
  725.                 num_reports++;
  726.             } else {
  727.                 break; // we've reached the timestamps we've already checked, we're done
  728.             }
  729.         }
  730.         FAADE.log('Total of ' + num_reports + ' new reports since last check.');
  731.  
  732.         var user_loc = FAADE.getLocationFromProfileHtml(FL_ASL.users[uw.FetLife.currentUser.id].profile_html);
  733.         FAADE.log('Current user location seems to be ' + user_loc.join(', ') + '.');
  734.  
  735.         // Loop over all new records one by one
  736.         var reports_to_alert = [];
  737.         for (var i = rows.length - num_reports; i <= rows.length - 1; i++) {
  738.             // extract the location data from the report
  739.             report_loc = rows[i].childNodes[6].textContent;
  740.             // compare current user's FetLife profile location against alleged abuse location
  741.             // and test each substring of the user profile against the reported location
  742.             for (var z = 0; z < user_loc.length; z++) {
  743.                 // if a relevant case insensitive substring matches
  744.                 if (-1 !== report_loc.toLowerCase().search(user_loc[z].toLowerCase())) {
  745.                     FAADE.log('Filed report location ' + report_loc + ' matches user location substring ' + user_loc[z] + '!');
  746.                     // store for future pop-up
  747.                     reports_to_alert.push(rows[i]);
  748.                     break; // we found a match, so stop trying on this row
  749.                 }
  750.             }
  751.         }
  752.  
  753.         // Ask user to view the profiles of the alleged abusers in the user's local vicinity.
  754.         if (reports_to_alert.length) {
  755.             // Fill in the user-facing message with the appropriate info.
  756.             document.getElementById('faade_reports_to_alert').innerHTML = reports_to_alert.length.toString();
  757.             document.getElementById('faade_user_loc').innerHTML = user_loc.join(', ');
  758.             // Create the click event we're going to use.
  759.             var evt = document.createEvent('MouseEvents');
  760.             evt.initEvent('click', true, false); // can bubble, can't be cancelled
  761.             // "Click" event on hidden code.
  762.             document.querySelector('a[data-opens-modal="faade"]').dispatchEvent(evt);
  763.             // Attach event listener to "View" button and pass in appropriate URLs.
  764.             document.querySelector('.btnsqr[data-closes-modal="faade"]').addEventListener('click', function () {
  765.                 for (var i = 0; i < reports_to_alert.length; i++) {
  766.                     // TODO: Add the permalink to the specific report to this URL, so it's highlighted when opened.
  767.                     var url = 'https://fetlife.com/users/';
  768.                     GM_openInTab(url + reports_to_alert[i].childNodes[2].textContent.match(/\d+/)[0]);
  769.                 }
  770.             });
  771.         }
  772.     }
  773.  
  774.     // Make a note of the latest timestamp filed, so we start here next time we're loaded.
  775.     FAADE.setValue('last_timestamp_checked', latest_timestamp_filed.toString());
  776. };
  777.  
  778. // This is the main() function, executed on page load.
  779. FAADE.main = function () {
  780.     // Make a list of known alleged abuser user IDs.
  781.     var parser = new DOMParser();
  782.     var doc = parser.parseFromString(FAADE.abuser_database, 'text/html');
  783.     var els = doc.querySelectorAll('#tblMain td:nth-child(3)'); // third child is the column of IDs.
  784.     var abuser_ids = [];
  785.     for (var i = 1; i < els.length; i++) { // we never need the first (0'th) cell because Google provides it blank.
  786.         abuser_ids.push(els[i].innerHTML);
  787.     }
  788.     FAADE.log('recalled abuser ids ' + abuser_ids);
  789.  
  790.     // TODO: Refactor this, it's kludgy.
  791.     setTimeout(function() {
  792.         FAADE.log('Running time-delayed function.');
  793.         if (FL_ASL.users[uw.FetLife.currentUser.id].profile_html) {
  794.             FAADE.log('We have the current user\'s FetLife profile HTML. Running broadcast checks.');
  795.             FAADE.broadcastNewProximalReports(doc);
  796.         }
  797.     }, 5000); // give us a few seconds to grab the current user's FetLife profile HTML.
  798.  
  799.     // Are we on a user profile page?
  800.     if (window.location.href.match(/users\/(\d+)\/?$/)) {
  801.  
  802.         var profile_nick = document.querySelector('h2.bottom').childNodes[0].textContent.match(/\S+/)[0];
  803.         var id_in_url = window.location.href.match(/users\/(\d+)\/?$/)[1];
  804.  
  805.         // If we're not viewing our own profile page, insert a report link.
  806.         usr_ops = document.querySelector('#main_content p.quiet');
  807.         if (usr_ops) {
  808.             usr_ops.appendChild(document.createElement('br'));
  809.             usr_ops.appendChild(FAADE.createAbuseReportLink(id_in_url, profile_nick));
  810.         }
  811.  
  812.         // If this is a profile page of an alleged abuser,
  813.         if (-1 !== abuser_ids.indexOf(id_in_url)) {
  814.  
  815.             var report_el = document.createElement('table');
  816.             report_el.setAttribute('id', 'faade_abuse_reports');
  817.             report_el.setAttribute('summary', 'Reported consent violations committed by ' + profile_nick + '.');
  818.             var caption = document.createElement('caption');
  819.             caption.innerHTML = 'There are reports ' + profile_nick + ' violated others\' consent in these ways:';
  820.             var tfoot = document.createElement('tfoot');
  821.             tfoot.innerHTML = '<tr><td colspan="2"></td></tr>';
  822.             tfoot.querySelector('td').appendChild(FAADE.createAbuseReportLink(id_in_url, profile_nick));
  823.             report_el.appendChild(caption);
  824.             report_el.appendChild(tfoot);
  825.  
  826.             // Find all reports that match ID number.
  827.             var abuse_reports = [];
  828.             for (var ix = 0; ix < els.length; ix++) {
  829.                 if (id_in_url === els[ix].innerHTML) {
  830.                     abuse_reports.push(els[ix].parentNode); // the table row of abuse report
  831.                 }
  832.             }
  833.             // Add this information to the top of this user's profile
  834.             for (var iy = 0; iy < abuse_reports.length; iy++) {
  835.                 var num = iy + 1;
  836.                 var tr = document.createElement('tr');
  837.                 tr.setAttribute('id', 'faade_abuse_report-' + num.toString());
  838.                 var details_html = '<ul><li class="faade_abuse_report_datetime">' + abuse_reports[iy].childNodes[7].innerHTML + '</li>';
  839.                 details_html += '<li class="faade_abuse_report_location">' + abuse_reports[iy].childNodes[6].innerHTML + '</li></ul>';
  840.                 var permalink_html = '<a class="faade_abuse_reported_datetime" rel="bookmark" href="'
  841.                     + window.location + '#faade_abuse_report-' + num.toString()
  842.                     + '" title="Permalink for FAADE abuse report number ' + num.toString() + ' against '
  843.                     + profile_nick + '.">' +  abuse_reports[iy].childNodes[1].innerHTML + '</a>';
  844.                 tr.innerHTML += '<th>Abuse report ' + num.toString() + ' (' + permalink_html + '):' + details_html + '</th>';
  845.                 tr.innerHTML += '<td>' + abuse_reports[iy].childNodes[5].innerHTML + '</td>';
  846.                 report_el.appendChild(tr);
  847.             }
  848.  
  849.             var before = document.querySelector('#main_content table:last-child');
  850.             before.parentNode.insertBefore(report_el, before);
  851.         }
  852.  
  853.     }
  854.  
  855.     // Collect all user links on this page.
  856.     var user_links = [];
  857.     for (i = 0; i < document.links.length; i++) {
  858.         var l = document.links[i].href.match(/^(https:\/\/fetlife.com)?\/users\/(\d+)\/?$/);
  859.         if ( l && (l[2] !== uw.FetLife.currentUser.id.toString()) ) {
  860.             user_links.push(document.links[i]);
  861.         }
  862.     }
  863.  
  864.     // For each user ID found,
  865.     var last_id = null;
  866.     for (i = 0; i < user_links.length; i++) {
  867.         // Collect its user ID number.
  868.         var id = user_links[i].href.match(/\d+\/?$/);
  869.         if (id) { id = id.toString(); } // cast to string for later comparison
  870.  
  871.         // Get nickname.
  872.         var n;
  873.         if (user_links[i].children.length) {
  874.             // This is an avatar link, not a text link.
  875.             n = user_links[i].childNodes[0].alt;
  876.         } else {
  877.             // This is a text link. Easy.
  878.             n = user_links[i].innerHTML;
  879.         }
  880.  
  881.         // check the alleged abusers data store for a match.
  882.         if (-1 !== abuser_ids.indexOf(id)) {
  883.             FAADE.log('found match on this page for alleged abuser ID number ' + id);
  884.             // highlight the user's links that matched an alleged abuser using CSS
  885.             user_links[i].setAttribute('class', user_links[i].className + ' faade_alleged_abuser');
  886.  
  887.         }
  888.  
  889.         // Don't create another link if we just made one for that user.
  890.         if (id === last_id) { continue; }
  891.  
  892.         // Offer a link to add another report for this user.
  893.         // See also: https://support.google.com/docs/bin/answer.py?hl=en&answer=160000
  894.         // Add link to report this user for a consent violation.
  895.         var a = FAADE.createAbuseReportLink(id, n);
  896.         user_links[i].parentNode.appendChild(a);
  897.         last_id = id;
  898.     }
  899. };
  900.  
  901. FAADE.createAbuseReportLink = function (id, nick) {
  902.     var a = document.createElement('a');
  903.     a.setAttribute('class', 'faade_report_link');
  904.     a.setAttribute('target', '_blank');
  905.     var href = 'https://docs.google.com/spreadsheet/viewform?formkey=' + FAADE.getReportFormKey();
  906.     href += '&entry_0=' + id;
  907.     href += '&entry_1=' + nick;
  908.     a.setAttribute('href', href);
  909.     a.innerHTML = '(report a consent violation by ' + nick + ')';
  910.     return a;
  911. };
  912.  
  913. // The following is required for Chrome compatibility, as we need "text/html" parsing.
  914. /*
  915.  * DOMParser HTML extension
  916.  * 2012-09-04
  917.  *
  918.  * By Eli Grey, http://eligrey.com
  919.  * Public domain.
  920.  * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
  921.  */
  922.  
  923. /*! @source https://gist.github.com/1129031 */
  924. /*global document, DOMParser*/
  925.  
  926. (function(DOMParser) {
  927.         "use strict";
  928.  
  929.         var
  930.           DOMParser_proto = DOMParser.prototype
  931.         , real_parseFromString = DOMParser_proto.parseFromString
  932.         ;
  933.  
  934.         // Firefox/Opera/IE throw errors on unsupported types
  935.         try {
  936.                 // WebKit returns null on unsupported types
  937.                 if ((new DOMParser).parseFromString("", "text/html")) {
  938.                         // text/html parsing is natively supported
  939.                         return;
  940.                 }
  941.         } catch (ex) {}
  942.  
  943.         DOMParser_proto.parseFromString = function(markup, type) {
  944.                 if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
  945.                         var
  946.                           doc = document.implementation.createHTMLDocument("")
  947.                         ;
  948.  
  949.                         doc.body.innerHTML = markup;
  950.                         return doc;
  951.                 } else {
  952.                         return real_parseFromString.apply(this, arguments);
  953.                 }
  954.         };
  955. }(DOMParser));
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement