MediaWiki:Gadget-PatrollingEnhancements.js

From Wiktionary, the free dictionary
Jump to navigation Jump to search

Note: You may have to bypass your browser’s cache to see the changes. In addition, after saving a sitewide CSS file such as MediaWiki:Common.css, it will take 5-10 minutes before the changes take effect, even if you clear your cache.

  • Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
  • Konqueror and Chrome: click Reload or press F5;
  • Opera: clear the cache in Tools → Preferences;
  • Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.

See also: Special:Gadgets.


// {{documentation}}

(function PatrollingEnhancements_IIFE() {
	window.GPE = typeof window.GPE == 'object' ? window.GPE : {};
	
	const GPE = window.GPE;
	
	/* </pre>
	==Configuration options==
	<pre> */
	
	// The initial value to put in the "deletion reason" text-field; you can
	// override this in your common.js (or vector.js or whatnot).
	GPE.initialDeleteReason = GPE.initialDeleteReason == undefined ? '' : GPE.initialDeleteReason;
	
	// The value to use as a deletion reason if you leave the text-field blank; you
	// can override it in your common.js (or vector.js or whatnot). If you *don't*
	// override this, then MediaWiki will generate an automatic deletion reason that
	// indicates the entry's last editor and the beginning of its content.
	GPE.deleteReasonIfBlank = GPE.deleteReasonIfBlank == undefined ? '' : GPE.deleteReasonIfBlank;
	
	// By DCDuring's request. If you set this to true, then Special:Watchlist will
	// show the deletion-reason text-input, but *not* the deletion-reason dropdown,
	// when there's an unpatrolled new-page-creation.
	GPE.hideDeleteReasonDropdownOnWatchlist = GPE.hideDeleteReasonDropdownOnWatchlist == undefined ? false : GPE.hideDeleteReasonDropdownOnWatchlist;
	
	/* </pre>
	==Automated patrolling (whitelisting)==
	<pre> */
	
	// set GPE.currMonth and GPE.lastMonth (in the form of, e.g., '2013/February')
	(
	  function ()
	  {
	    var monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June',
	                       'July', 'August', 'September', 'October', 'November',
	                       'December' ];
	
	    var now = new Date();
	
	    var currYear = now.getFullYear();
	    var currMonthNum = now.getMonth();
	
	    GPE.currMonth = currYear + '/' + monthNames[currMonthNum];
	
	    var lastYear = (currMonthNum == 0 ? currYear - 1 : currYear);
	    var lastMonthNum = (currMonthNum == 0 ? 11 : currMonthNum - 1);
	
	    GPE.lastMonth = lastYear + '/' + monthNames[lastMonthNum];
	  }
	)();
	
	GPE.individualWhiteListedPages =
	{
	  "Wiktionary:Requests for cleanup": true,
	  "Wiktionary:Requests for verification": true,
	  "Wiktionary:Requests for deletion": true,
	  "Wiktionary:Requests for deletion/Others": true,
	  "Wiktionary:Requests for moves, mergers and splits": true,
	  "Wiktionary:Information desk": true,
	  "Wiktionary:Tea room": true,
	  "Wiktionary:Etymology scriptorium": true,
	  "Wiktionary:Requested entries (English)": true,
	  "Wiktionary:Requested entries (Spanish)": true,
	  "Wiktionary:List of protologisms": true,
	  "Wiktionary:Translation requests": true,
	  "Wiktionary:Feedback": true,
	  "Wiktionary:Sandbox": true,
	  "Wiktionary talk:Sandbox": true,
	  "Wiktionary:Tutorial (Editing)/sandbox": true,
	  "Wiktionary:Featured word candidates": true,
	  "Wiktionary:Word of the day/Nominations": true
	};
	
	GPE.individualWhiteListedPages["Wiktionary:Beer parlour/" + GPE.currMonth] =
	  GPE.individualWhiteListedPages["Wiktionary:Beer parlour/" + GPE.lastMonth] =
	  GPE.individualWhiteListedPages["Wiktionary:Grease pit/" + GPE.currMonth] =
	  GPE.individualWhiteListedPages["Wiktionary:Grease pit/" + GPE.lastMonth] =
	  true;
	
	// per-user white-listed sub-pages (for example, edits by user Foo
	// to User:Foo/vector.js should be autopatrolled):
	GPE.perUserWhiteListedSubPages =
	{
	  "/Sandbox": true,
	  "/sandbox": true,
	  "/chick.js": true,
	  "/chick.css": true,
	  "/standard.js": true,
	  "/standard.css": true,
	  "/cologneblue.js": true,
	  "/cologneblue.css": true,
	  "/modern.js": true,
	  "/modern.css": true,
	  "/myskin.js": true,
	  "/myskin.css": true,
	  "/nostalgia.js": true,
	  "/nostalgia.css": true,
	  "/simple.js": true,
	  "/simple.css": true,
	  "/vector.js": true,
	  "/vector.css": true,
	  "/common.js": true,
	  "/common.css": true
	};
	
	GPE.individualWhiteListedContributors =
	{
	};
	
	GPE.shouldAutoPatrol = function(link)
	{
	  var pagename = link.title;
	  if(pagename.indexOf('User talk:') == 0)
	    return true;
	  if(pagename in GPE.individualWhiteListedPages)
	    return true;
	
	  var contributor;
	  if(mediaWiki.config.get('wgCanonicalSpecialPageName') === 'Contributions')
	  {
	    contributor =
	      document.getElementById('t-contributions')
	              .getElementsByTagName('a')[0].href.replace(/.*\//, '');
	  }
	  else
	  {
	    var li = link.parentNode;
	    if(li.tagName.toUpperCase() == 'SPAN')
	      li = li.parentNode;
	    var links = li.getElementsByTagName('a');
	    for(var i = 0; i < links.length; ++i)
	      if(links[i].title.indexOf('Special:Contributions/') == 0)
	      {
	        contributor = links[i].title.substr('Special:Contributions/'.length);
	        break;
	      }
	  }
	  if(pagename.indexOf('User:' + contributor + '/') == 0)
	    if(pagename.substr(contributor.length + 5) in GPE.perUserWhiteListedSubPages)
	      return true;
	  if(contributor in GPE.individualWhiteListedContributors)
	    return true;
	
	  return false;
	};
	
	/* </pre>
	
	==Utility functions==
	<pre> */
	
	GPE.newButton = function(text, color, hoverText)
	{
	  var button = newNode('button', text);
	  button.style.background = color;
	  button.style.color = '#FFF';
	  button.style.border = '0';
	  button.style.padding = '0';
	  button.style.cursor = 'pointer';
	  button.title = hoverText;
	  return button;
	};
	
	GPE.disableButton = function(button, text, hoverText)
	{
	  button.onclick = null;
	  button.title = (hoverText || '');
	  button.innerHTML = text;
	  // clear out explicit styling and disable, so we can get appropriate
	  // disabled-button styles:
	  button.style.background = '';
	  button.style.color = '';
	  button.style.cursor = '';
	  button.disabled = 'disabled';
	};
	
	/* </pre>
	==Individual patrol-buttons==
	<pre> */
	
	GPE.addPatrolButton = function(link, rcid)
	{
	  if(link.className.search(/(?:^|\s)gpe-hasPatrolButton(?:\s|$)/) > -1)
	    return;
	  link.className = (link.className + ' gpe-hasPatrolButton').trim();
	
	  var button = GPE.newButton('M', '#009', 'click to mark as patrolled');
	  link.parentNode.insertBefore(button, link.nextSibling);
	  link.parentNode.insertBefore(document.createTextNode(' · '), button);
	  button.onclick =
	    function ()
	    {
	      var token = mediaWiki.user.tokens.get('patrolToken');
	      $.post
	      (
	        '/w/api.php?format=json&action=patrol&assert=user',
	        { token: token, rcid: rcid },
	        function (data)
	        {
	          if(data.patrol)
	            GPE.disableButton(button, 'm', 'marked as patrolled');
	          else if(data.error)
	          {
	            var msg = data.error.code + ': ' + data.error.info;
	            if(data.error.code == 'badtoken')
	              msg += ': "' + token + '"';
	            alert(msg);
	          }
	        },
	        'json'
	      );
	    };
	  if(GPE.shouldAutoPatrol(link))
	    button.click();
	
	  // remove the exclamation point:
	  var tmp = link;
	  while(tmp && tmp.nodeName.toUpperCase() !== 'LI')
	    tmp = tmp.parentNode;
	  if(tmp)
	    tmp = tmp.getElementsByClassName('unpatrolled')[0];
	  if(tmp)
	    tmp.parentNode.removeChild(tmp);
	};
	
	/* </pre>
	==Individual delete-buttons==
	<pre> */
	
	GPE.addDeleteButton = function(link, title)
	{
	  if(link.className.search(/(?:^|\s)gpe-hasDeleteButton(?:\s|$)/) > -1)
	    return;
	  link.className = (link.className + ' gpe-hasDeleteButton').trim();
	
	  var button = GPE.newButton('D', '#900', 'click to delete');
	  link.parentNode.insertBefore(button, link.nextSibling);
	  link.parentNode.insertBefore(document.createTextNode(' · '), button);
	  button.onclick =
	    function ()
	    {
	      var dropdownReason =
	        document.getElementById('deleteReasonsDropdown')
	          ? document.getElementById('deleteReasonsDropdown').value
	          : '';
	      if(dropdownReason == 'other')
	        dropdownReason = '';
	      var textInputReason =
	        document.getElementById('deleteReasonTextInput').value;
	      var reason;
	      if(dropdownReason.length && textInputReason.length)
	        reason = dropdownReason + ': ' + textInputReason;
	      else if(dropdownReason.length || textInputReason.length)
	        reason = dropdownReason + textInputReason;
	      else
	        reason = GPE.deleteReasonIfBlank;
	      var token = mediaWiki.user.tokens.get('deleteToken');
	      $.post
	      (
	        '/w/api.php?format=json&action=delete&assert=user',
	        { title: title, token: token, reason: reason },
	        function (data)
	        {
	          if(data['delete'])
	            GPE.disableButton(button, 'd', 'deleted');
	          else if(data.error)
	          {
	            var msg = data.error.code + ': ' + data.error.info;
	            if(data.error.code == 'badtoken')
	              msg += ': "' + token + '"';
	            alert(msg);
	          }
	        },
	        'json'
	      );
	    };
	};
	
	/* </pre>
	==Delete-reasons==
	<pre> */
	
	GPE.addDeleteReasonInput = function ()
	{ var deleteReasonDiv =
	    ( newNode
	      ( 'div',
	        { style:
	            'background:#900; color:#FFF; ' +
	            'position:fixed; bottom:0; right:0; margin-bottom:0'
	        },
	        '\u00A0Deletion reason:\u00A0'
	      )
	    );
	  deleteReasonDiv.title =
	    'the deletion reason (message/summary) to use when you click "D"';
	  var deleteReasonTextInput =
	  ( newNode
	    ( 'input',
	      { type: 'text',
	        size: 80,
	        id: 'deleteReasonTextInput',
	        value: GPE.initialDeleteReason,
	        style: 'position:fixed; right:0; margin-bottom:0'
	      }
	    )
	  );
	  deleteReasonDiv.appendChild(deleteReasonTextInput);
	  document.getElementById('bodyContent').appendChild(deleteReasonDiv);
	
	  if(GPE.hideDeleteReasonDropdownOnWatchlist)
	    if(mediaWiki.config.get('wgPageName') == 'Special:Watchlist')
	      return;
	
	  // get canned messages from [[MediaWiki:Deletereason-dropdown]]:
	  $.getJSON
	  ( '/w/api.php?format=json&action=query&meta=allmessages&ammessages=Deletereason-dropdown',
	    function (data)
	    { var rawDeleteReasons = data.query.allmessages[0]['*'];
	      var deleteReasonsDropdown =
	        newNode('select',
	          { id: 'deleteReasonsDropdown', style: 'vertical-align: bottom' });
	      deleteReasonsDropdown.appendChild
	        (newNode('option', { value: 'other' }, 'Other reason'));
	      var optGroup = deleteReasonsDropdown;
	      rawDeleteReasons.replace
	      ( /^(\*\*?) *(.+)$/gm,
	        function (s, asterisks, text)
	        { if(asterisks == '*')
	            deleteReasonsDropdown.appendChild
	              (optGroup = newNode('optgroup', { label: text }));
	          else // '**'
	            optGroup.appendChild(newNode('option', { value: text }, text));
	        }
	      );
	      deleteReasonDiv.insertBefore(deleteReasonsDropdown, deleteReasonTextInput);
	      deleteReasonDiv.insertBefore(newNode('br'), deleteReasonTextInput);
	      deleteReasonDiv.insertBefore(document.createTextNode('\u00A0'), deleteReasonTextInput);
	    }
	  );
	};
	
	/* </pre>
	==Namespaces==
	<pre> */
	
	GPE.computeNamespaces = function
	  (selected, includeAssociated, invertSelection)
	{
	  var associated = Number(selected) + (selected % 2 === 0 ? 1 : -1);
	  if(invertSelection)
	  {
	    var selector = document.getElementById('namespace');
	    if(! selector)
	      return [];
	    var ret = [];
	    for(var option = selector.firstChild; option; option = option.nextSibling)
	      if(option.nodeName.toUpperCase() === 'OPTION' && option.value)
	        if(option.value != selected)
	          if(! includeAssociated || option.value != associated)
	            ret.push(option.value);
	    return ret;
	  }
	  else
	  {
	    if(includeAssociated)
	      return [selected, associated];
	    else
	      return [selected];
	  }
	};
	
	GPE.generateRcnamespace = function ()
	{
	  var currUrl = document.location.href;
	  if(! /[?&]namespace=\d+(?:&|$)/.test(currUrl))
	    return;
	  var selected = /[?&]namespace=(\d+)(?:&|$)/.exec(currUrl)[1];
	  var includeAssociated =
	    mediaWiki.config.get('wgPageName') !== 'Special:NewPages'
	    && /[?&]associated=(?!0?&|0?$)/.test(currUrl);
	  var invertSelection = /[?&]invert=(?!0?&|0?$)/.test(currUrl);
	  var namespaces =
	    GPE.computeNamespaces(selected, includeAssociated, invertSelection);
	  if(namespaces.length > 0)
	    return namespaces.join('|');
	};
	
	/* </pre>
	==Find and handle links==
	<pre> */
	
	GPE.handleUnpatrolledEdits = function (rcidsByRevid)
	{
	  var links =
	    document.getElementById('bodyContent').getElementsByTagName('a');
	  for(var i = links.length - 1; i >= 0; --i)
	  {
	    var mapKey = /&diff=(prev&oldid=)?(\d+)(&|$)/.exec(links[i].href);
	    if(mapKey && rcidsByRevid.hasOwnProperty(mapKey[2]))
	      GPE.addPatrolButton(links[i], rcidsByRevid[mapKey[2]]);
	  }
	};
	
	GPE.findLinksToUnpatrolledNewPages = function (rcidsByTitle)
	{
	  if(mediaWiki.config.get('wgPageName') === 'Special:NewPages')
	  {
	    var ret = [];
	    $('li.not-patrolled a.mw-newpages-pagename').each(function () {
	      if (this.title && rcidsByTitle.hasOwnProperty(this.title))
	        ret.push(this);
	    });
	    return ret;
	  }
	  else
	  {
	    var ret = [];
	    var abbrs =
	      document.getElementById('bodyContent').getElementsByTagName('abbr');
	    for(var i = abbrs.length - 1; i >= 0; --i)
	    {
	      if(abbrs[i].className != 'newpage')
	        continue;
	      var link = abbrs[i];
	      while(link && link.nodeName.toUpperCase() != 'A')
	        if(link.nodeName.toUpperCase() === 'SPAN' && link.className === 'mw-title')
	          link = link.firstChild;
	        else
	          link = link.nextSibling;
	      if(link && link.title && rcidsByTitle.hasOwnProperty(link.title))
	        ret.push(link);
	    }
	    return ret;
	  }
	};
	
	GPE.handleUnpatrolledNewPages = function (rcidsByTitle)
	{
	  var userIsSysop =
	    mediaWiki.config.get('wgUserGroups').indexOf('sysop') > -1;
	  var links = GPE.findLinksToUnpatrolledNewPages(rcidsByTitle);
	  for(var i = links.length - 1; i >= 0; --i)
	  {
	    var link = links[i];
	    // 2016-04: Equinox removing red D delete button because it hasn't worked for a year.
	    // if(userIsSysop)
	    //   GPE.addDeleteButton(link, link.title);
	    GPE.addPatrolButton(link, rcidsByTitle[link.title]);
	  }
	  // Remove the rest of the delete features as above
	  // if(userIsSysop && links.length > 0)
	  // {
	  //   GPE.getAndStoreDeleteToken();
	  //   GPE.addDeleteReasonInput();
	  // }
	};
	
	GPE.getAndStoreDeleteToken = function ()
	{
	  $.getJSON
	  (
	    '/w/api.php?format=json&action=tokens&type=delete',
	    function (data)
	    {
	      var token = data.tokens.deletetoken;
	      if(! token || token.search(/^[0-9a-f]{32}\+\\$/) != 0)
	        return;
	      mediaWiki.user.tokens.set('deleteToken', token);
	    }
	  );
	};
	
	GPE.main = function (params)
	{
	  var url =
	    '/w/api.php?format=json&action=query&list=recentchanges' +
	    '&rcprop=ids|title' +
	    '&rcshow=!patrolled' + (params.hasOwnProperty('rcshow') ? '|' + params.rcshow : '') +
	    '&rclimit=' + (params.hasOwnProperty('rclimit') ? params.rclimit : 500) +
	    '&rctype=' + (params.hasOwnProperty('rctype') ? params.rctype : 'edit|new') +
	    (params.hasOwnProperty('rcdir') ? '&rcdir=' + params.rcdir : '') +
	    (params.hasOwnProperty('rcstart') ? '&rcstart=' + params.rcstart : '') +
	    (params.hasOwnProperty('rcnamespace') ? '&rcnamespace=' + params.rcnamespace : '') +
	    (params.hasOwnProperty('rcuser') ? '&rcuser=' + params.rcuser : '');
	  $.getJSON
	  (
	    url,
	    function (data)
	    {
	      data = data.query.recentchanges;
	      var rcidsByRevid = {}; // for unpatrolled edits
	      var rcidsByTitle = {}; // for unpatrolled new pages
	      for(var i = 0; i < data.length; ++i)
	        if(data[i].type == 'edit')
	          rcidsByRevid[data[i].revid] = data[i].rcid;
	        else
	          rcidsByTitle[data[i].title] = data[i].rcid;
	
	      GPE.handleUnpatrolledEdits(rcidsByRevid);
	      GPE.handleUnpatrolledNewPages(rcidsByTitle);
	    }
	  );
	};
	
	/* </pre>
	==Onload-hooks==
	<pre> */
	
	$( document ).ready
	( function ()
	  {
	    if(mediaWiki.config.get('wgPageName') === 'Special:RecentChanges') {
	      var currUrl = document.location.href;
	      var params = {};
	      var rcshow = [];
	      if(currUrl.search(/[?&]hideliu=(?!0?$|0?&)/) > -1)
	        rcshow.push('anon');
	      else if(currUrl.search(/[?&]hideanons=(?!0?$|0?&)/) > -1)
	        rcshow.push('!anon');
	      if(document.getElementsByClassName('minoredit').length === 0)
	        rcshow.push('!minor');
	      if(rcshow.length > 0)
	        params.rcshow = rcshow.join('|');
	      var rcnamespace = GPE.generateRcnamespace();
	      if(rcnamespace)
	        params.rcnamespace = rcnamespace;
	      GPE.main(params);
	    } else if(mediaWiki.config.get('wgPageName') === 'Special:NewPages') {
	      var currUrl = document.location.href;
	      var params = { rctype: 'new' };
	      var rcshow = [];
	      if(currUrl.search(/[?&]hideliu=(?!0?$|0?&)/) > -1)
	        rcshow.push('anon');
	      if(currUrl.search(/[?&]hideredirs=0?(?:$|&)/) === -1)
	        rcshow.push('!redirect');
	      if(rcshow.length > 0)
	        params.rcshow = rcshow.join('|');
	      if(currUrl.search(/[?&]dir=prev(?=$|&)/) > -1) {
	        params.rcdir = 'newer';
	        if(currUrl.search(/[?&]offset=\d+(?=$|&)/) > -1)
	          params.rcstart = currUrl.match(/[?&]offset=(\d+)(?=$|&)/)[1];
	      }
	      var rcnamespace = GPE.generateRcnamespace();
	      if(rcnamespace)
	        params.rcnamespace = rcnamespace;
	      GPE.main(params);
	    } else if(mediaWiki.config.get('wgPageName') === 'Special:Watchlist') {
	      var params = {};
	      var rcnamespace = GPE.generateRcnamespace();
	      if(rcnamespace)
	        params.rcnamespace = rcnamespace;
	      // TODO is this the best way to find what we need for the watchlist?
	      GPE.main(params);
	    } else if(mediaWiki.config.get('wgPageName').search(/^Special:Contributions(\/|$)/) === 0)
	      GPE.main({ rcuser: document.getElementById('t-contributions').firstChild.href.replace(/^.*?\/Special:Contributions\//, '') });
	    else if(mediaWiki.config.get('wgAction') === 'markpatrolled'
	        || mediaWiki.config.get('wgAction') === 'delete'
	        || mediaWiki.config.get('wgAction') === 'rollback')
	      GPE.main({ rclimit: 15 });
	  }
	);
})();