From Wikipedia, the free encyclopedia
(Redirected from Wikipedia:Xunlink)
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.

/***************************************************************************************************

 Xunlink --- by Evad37

 > The power of XFDcloser's 'unlink backlinks' function, for any page.

***************************************************************************************************/

/* jshint esversion: 6, laxbreak: true, undef: true, maxerr:999 */

/* globals console, window, $, mw, OO, extraJs */

// <nowiki>

$( function($) {

	

/* ========== Configuration ===================================================================== */

var config = {

	// Script info

	script: {

		// Advert to append to edit summaries

		advert:  ' ([[User:Evad37/Xunlink|Xunlink]])',

		version: '2.0.1'

	},

	// MediaWiki configuration values

	mw: mw.config.get( 

		'wgArticleId',

		'wgPageName',

		'wgUserGroups',

		'wgUserName',

		'wgFormattedNamespaces',

		'wgMonthNames',

		'wgNamespaceNumber'

	 ),

	allowedNamespaces: 0, 6, 100 // article, File, Portal

};

// xfd props, for compatbility with code from XFDcloser

config.xfd = {

	// Namespaces to unlink from: main, Template, Portal, Draft

	ns_unlink: '0', '10', '100', '118'],

	// Type (files get treated differently)

	type: config.mw.wgNamespaceNumber === 6 ? 'ffd' : 'other'

};



/* ========== Validate page suitability ========================================================= */

// Validate namespace 

var isCorrectNamespace = config.allowedNamespaces.includes(config.mw.wgNamespaceNumber);

if ( !isCorrectNamespace ) {

	return;

}



// If a portal, only make available if deleted

var isPortal = config.mw.wgNamespaceNumber === 100;

var notDeleted = config.mw.wgArticleId > 0;

if ( isPortal && notDeleted ) {

	return;

}





/* ========== Dependencies ====================================================================== */

mw.loader.using([

	'mediawiki.util', 'mediawiki.api', 'mediawiki.Title',

	'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'jquery.ui',

	'ext.gadget.libExtraUtil'

]).then(function() {



/* ========== CSS Styles ========================================================================

 * TODO: migrate to css subpage

 */

mw.util.addCSS(

		// Task notices

		'.xfdc-notices { width:80%; font-size:95%; padding-left:2.5em; }',

		'.xfdc-notices > p { margin:0; line-height:1.1em; }',

		'.xfdc-notice-error { color:#D00000; font-size:92% }',

		'.xfdc-notice-warning { color:#9900A2; font-size:92% }',

		'.xfdc-notice-error::before, .xfdc-notice-warning::before { content: " ["; }',

		'.xfdc-notice-error::after,  .xfdc-notice-warning::after  { content: "]"; }',

		'.xfdc-task-waiting { color:#595959; }',

		'.xfdc-task-started { color:#0000D0; }',

		'.xfdc-task-done { color:#006800; }',

		'.xfdc-task-skipped { color:#697000; }',

		'.xfdc-task-aborted { color:#C00049; }',

		'.xfdc-task-failed { color:#D00000; }',

		// Preview of edit summary

		'.xu-preview { background-color:#fafafa; border:1px dotted #777; '+

			'margin-top: 0px; padding:0px 10px; font-size: 90%; width: 100%; }'

	

	.join('\n')

);



/* ========== Helper functions ==================================================================

 * TODO: these should probably be part of one or more script modules/libraries, which could be

 *  loaded with mw.loader.getScript()

 */

 

 /** safeUnescape

 * Un-escapes some HTML tags (<br>, <p>, <ul>, <li>, <hr>, and <pre>); turns wikilinks

 * into real links. Ignores anyting within <pre>...</pre> tags.

 * Input will first be escaped using mw.html.escape() unless specified 

 * @param {String} text

 * @param {Object} config Configuration options

 * @config {Boolean} noEscape - do not escape the input first

 * @returns {String} unescaped text

 */

var safeUnescape = function(text, config) {

	var path = 'https:' + mw.config.get('wgServer') + '/wiki/';



	return ( config && config.noEscape && text || mw.html.escape(text) )

	// Step 1: unescape <pre> tags

	.replace(  

		/&lt;(\/?pre\s?\/?)&gt;/g,

		'<$1>'

	)

	// Step 2: replace piped wikilinks with real links (unless inside <pre> tags)

	.replace( 

		/\[\[([^\|\]]*?)\|([^\|\]]*?)\]\](?![^<]*?<\/pre>)/g,

		'<a href="' + path + mw.util.wikiUrlencode('$1') + '" target="_blank">$2</a>'

	)

	// Step 3: replace other wikilinks with real links (unless inside <pre> tags)

	.replace( 

		/\[\[([^\|\]]+?)]\](?![^<]*?<\/pre>)/g,

		'<a href="' + path + mw.util.wikiUrlencode('$1') + '" target="_blank">$1</a>'

	)

	// Step 4: unescape other tags: <br>, <p>, <ul>, <li>, <hr> (unless inside <pre> tags)

	.replace(

		/&lt;(\/?(?:br|p|ul|li|hr)\s?\/?)&gt;(?![^<]*?<\/pre>)/g,

		'<$1>'

	);

};



/** multiButtonConfirm

 * @param {Object} config

 * @config {String} title  Title for the dialogue

 * @config {String} message  Message for the dialogue. HTML tags (except for <br>, <p>, <ul>,

 *  <li>, <hr>, and <pre> tags) are escaped; wikilinks are turned into real links.

 * @config {Array} actions  Optional. Array of configuration objects for OO.ui.ActionWidget

 *  <https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.ActionWidget>.

 *  If not specified, the default actions are 'accept' (with label 'OK') and 'reject' (with

 *  label 'Cancel').

 * @config {String} size  Symbolic name of the dialog size: small, medium, large, larger or full.

 * @return {Promise<String>} action taken by user

 */

var multiButtonConfirm = function(config) {

	var dialogClosed = $.Deferred();

	

	// Wrap message in a HtmlSnippet to prevent escaping

	var htmlSnippetMessage = new OO.ui.HtmlSnippet(

		safeUnescape(config.message)

	);



	var windowManager = new OO.ui.WindowManager();

	var messageDialog = new OO.ui.MessageDialog();

	$('body').append( windowManager.$element );

	windowManager.addWindows(  messageDialog  );

	windowManager.openWindow( messageDialog, {

		'title': config.title,

		'message': htmlSnippetMessage,

		'actions': config.actions,

		'size': config.size

	} );

	windowManager.on('closing', function(_win, promise) {

		promise.then(function(data) {

			dialogClosed.resolve(data && data.action);

			windowManager.destroy();

		});

	});



	return dialogClosed.promise();

};



var makeErrorMsg = function(code, jqxhr) {

	var details = '';

	if ( code === 'http' && jqxhr.textStatus === 'error' ) {

		details = 'HTTP error ' + jqxhr.xhr.status;

	} else if ( code === 'http' ) {

		details = 'HTTP error: ' + jqxhr.textStatus;

	} else if ( code === 'ok-but-empty' ) {

		details = 'Error: Got an empty response from the server';

	} else {

		details = 'API error: ' + code;

	}

	return details;

};



var arrayFromResponsePages = function(response) {

	return $.map(response.query.pages, function(page) { return page; });

};



/* ========== API =============================================================================== */

var API = new mw.Api( {

	ajax: {

		headers: { 

			'Api-User-Agent': 'Xunlink/' + config.script.version + 

				' ( /info/en/?search=User:Evad37/Xunlink )'

		}

	}

} );





/* ========== Unlink backlinks ================================================================== */

/**unlinkBacklinks

 *

 * Copied from XFDcloser, with minimal changes. Such changes have the original code in comments

 * beginning `XFDC:`

 *

 * TODO: merge code, and import the same copy here and into XFDcloser

 *

 * @param self Object to hold some input date, and to recieve status messages

 */

var unlinkBacklinks = function(self) {



	// Notify task is started

	self.setStatus('started');

	

	var pageTitles =  config.mw.wgPageName]; // XFDC: self.discussion.getPageTitles(self.pages)

	var redirectTitles = [];

	// Ignore the following titles, and any of their subpages

	var ignoreTitleBases = 

		'Template:WPUnited States Article alerts',

		'Template:Article alerts columns',

		'Template:Did you know nominations'

	];

	var getBase = function(title) {

		return title.split('/')[0];

	};

	var blresults = [];

	var iuresults = [];

	

	//convert results (arrays of objects) to titles (arrays of strings), removing duplicates

	var flattenToTitles = function(results) {

		return results.reduce(

			function(flatTitles, result) {

				if ( result.redirlinks ) {

					if ( !redirectTitles.includes(result.title)) {

						redirectTitles.push(result.title);

					}

					return flatTitles.concat(

						result.redirlinks.reduce(

							function(flatRedirLinks, redirLink) {

								if (

									flatTitles.includes(redirLink.title) ||

									pageTitles.includes(redirLink.title) ||

									ignoreTitleBases.includes(getBase(redirLink.title))

								) {

									return flatRedirLinks;

								} else {

									return flatRedirLinks.concat(redirLink.title);

								}

							},

							[]

						)

					);

				} else if (

					result.redirect === '' ||

					flatTitles.includes(result.title) ||

					pageTitles.includes(result.title) ||

					ignoreTitleBases.includes(getBase(result.title))

				) {

					return flatTitles;

				} else {

					return flatTitles.concat(result.title);

				}

			},

			[]

		);

	};



	var apiEditPage = function(pageTitle, newWikitext) {

		API.postWithToken( 'csrf', {

			action: 'edit',

			title: pageTitle,

			text: newWikitext,

			summary: self.editSummary + config.script.advert, /* XFDC:

				'Removing link(s)' +

				(( config.xfd.type === 'ffd' ) ? ' / file usage(s)' : '' ) +

				': [[' + self.discussion.getNomPageLink() + ']] closed as ' +

				self.inputData.getResult() + config.script.advert, */

			minor: 1,

			nocreate: 1

		} )

		.done( function() {

			self.track('unlink', true);

		} )

		.fail( function(code, jqxhr) {

			self.track('unlink', false);

			self.addApiError(code, jqxhr, 

				'Could not remove backlinks from ',

				extraJs.makeLink(pageTitle)

			]);

		} );

	};



	/**

	 * @param {String} pageTitle

	 * @param {String} wikitext

	 * @returns {Promise(String)} updated wikitext, with any list items either removed or unlinked

	 */

	var checkListItems = function(pageTitle, wikitext) {

		// Find lines marked with {{subst:void}}, and the preceding section heading (if any)

		var toReview = /^{{subst:void}}(.*)$/m.exec(wikitext);

		if ( !toReview ) {

			// None found, no changes needed

			return $.Deferred().resolve(wikitext).promise();

		}

		// Find the preceding heading, if any

		var precendingText = wikitext.split('{{subst:void}}')[0];

		var allHeadings = precendingText.match(/^=+.+?=+$/gm);

		var heading = ( !allHeadings ) ? null : allHeadingsallHeadings.length - 1].replace(/(^=* *| *=*$)/g, '');

		// Prompt user

		return multiButtonConfirm({

			title: 'Review unlinked list item',

			message: '[[' + pageTitle +

				( ( heading ) ? '#' +

					mw.util.wikiUrlencode(

						heading.replace(/\[\[([^\|\]]*?)\|([^\]]*?)\]\]/, '$2')

						.replace(/\[\[([^\|\]]*?)\]\]/, '$1')

					) + ']]' : ']]' ) +

				': ' +

				'<pre>' + toReview1 + '</pre>',

			actions: 

				{ label:'Keep item', action:'keep' },

				{ label:'Remove item', action:'remove'}

			],

			size: 'medium'

		})

		.then(function(action) {

			if ( action === 'keep' ) {

				// Remove the void from the start of the line

				wikitext = wikitext.replace(/^{{subst:void}}/m, '');

			} else {

				// Remove the whole line

				wikitext = wikitext.replace(/^{{subst:void}}.*\n?/m, '');

			}

			// Iterate, in case there is more to be reviewed

			return checkListItems(pageTitle, wikitext);

		});

	};

	

	var processUnlinkPages = function(result) {

		if ( !result.query || !result.query.pages ) {

			// No results

			self.addApiError('result.query.pages not found', null, 'Could not read contents of pages; '+

				'could not remove backlinks');

			console.log('[XFDcloser] API error: result.query.pages not found... result =');

			console.log(result);

			self.setStatus('failed');

			return;

		}

		// For each page, pass the wikitext through the unlink function

		var pages = arrayFromResponsePages(result);

		pages.reduce(

			function(previous, page) {

				return $.when(previous).then(function(){

					var oldWikitext = page.revisions0]['*'];

					var newWikitext = extraJs.unlink(

						oldWikitext,

						pageTitles.concat(redirectTitles),

						page.ns,

						!!page.categories

					);

					if ( oldWikitext !== newWikitext ) {

						var confirmedPromise = checkListItems(page.title, newWikitext);

						confirmedPromise.then(function(updatedWikitext) {

							apiEditPage(page.title, updatedWikitext);

						});

						return confirmedPromise;

					} else {

						self.addWarning(['Skipped ',

							extraJs.makeLink(page.title),

							' (no direct links)'

						]);

						self.track('unlink', false);

						return true;

					}

				});

			},

			true);

	};



	var apiReadFail = function(code, jqxhr) {

		self.addApiError(code, jqxhr, 'Could not read contents of pages; '+

			'could not remove backlinks');

		self.setStatus('failed');

	};

	

	var processResults = function() {

		// Flatten results arrays 

		if ( blresults.length !== 0 ) {

			blresults = flattenToTitles(blresults);

		}

		if ( iuresults.length !== 0 ) {

			iuresults = flattenToTitles(iuresults);

			// Remove image usage titles that are also in backlikns results 

			iuresults = iuresults.filter(function(t) { return $.inArray(t, blresults) === -1; });

		}



		// Check if, after flattening, there are still backlinks or image uses

		if ( blresults.length === 0 && iuresults.length === 0 ) {

			self.addWarning('none found');

			self.setStatus('skipped');

			return;

		}



		

		// Ask user for confirmation

		var heading = 'Unlink backlinks';

		if ( iuresults.length !== 0 ) {

			heading += '('; 

			if ( blresults.length !== 0 ) {

				heading += 'and ';

			}

			heading += 'file usage)';

		}

		heading += ':';

		var para = '<p>All '+ (blresults.length + iuresults.length) + ' pages listed below may be '+

			'edited (unless backlinks are only present due to transclusion of a template).</p>'+

			'<p>To process only some of these pages, use Twinkle\'s unlink tool instead.</p>'+

			'<p>Use with caution, after reviewing the pages listed below. '+

			'Note that the use of high speed, high volume editing software (such as this tool and '+

			'Twinkle\'s unlink tool) is subject to the Bot policy\'s [[WP:ASSISTED|Assisted editing guidelines]] '+

			'</p><hr>';

		var list = '<ul>';

		if ( blresults.length !== 0 ) {

			list += '<li>[[' + blresults.join(']]</li><li>[[') + ']]</li>';

		}

		if ( iuresults.length !== 0 ) {

			list += '<li>[[' + iuresults.join(']]</li><li>[[') + ']]</li>';

		}

		list += '<ul>';

		

		multiButtonConfirm({

			title: heading,

			message: para + list,

			actions: 

				{ label: 'Cancel', flags: 'safe' },

				{ label: 'Remove backlinks', action: 'accept', flags: 'progressive' }

			],

			size: 'medium'

		})

		.then(function(action) {

			if ( action ) {

				var unlinkTitles = iuresults.concat(blresults);

				self.setupTracking('unlink', unlinkTitles.length);

				self.showTrackingProgress = 'unlink';

				// get wikitext of titles, check if disambig - in lots of 50 (max for Api)

				for (var ii=0; ii<unlinkTitles.length; ii+=50) {

					API.get( {

						action: 'query',

						titles: unlinkTitles.slice(ii, ii+49).join('|'),

						prop: 'categories|revisions',

						clcategories: 'Category:All disambiguation pages',

						rvprop: 'content',

						indexpageids: 1

					} )

					.done( processUnlinkPages )

					.fail( apiReadFail );

				}

			} else {

				self.addWarning('Cancelled by user');

				self.setStatus('skipped');

			}

		});

	};



	// Queries

	var blParams = {

		list: 'backlinks',

		blfilterredir: 'nonredirects',

		bllimit: 'max',

		blnamespace: config.xfd.ns_unlink,

		blredirect: 1

	};

	var iuParams = {

		list: 'backlinks|imageusage',

		iutitle: '',

		iufilterredir: 'nonredirects',

		iulimit: 'max',

		iunamespace: config.xfd.ns_unlink,

		iuredirect: 1

	};

	var query = pageTitles.map(function(page) {

		return $.extend(

			{ action: 'query' },

			blParams,

			{ bltitle: page },

			( config.xfd.type === 'ffd' ) ? iuParams : null,

			( config.xfd.type === 'ffd' ) ? { iutitle: page } : null

		);

	});

	// Variable for incrementing current query

	var qIndex = 0;

	// Function to do Api query

	var apiQuery = function(q) {

		API.get( q )

		.done( processBacklinks )

		.fail( function(code, jqxhr) {

			self.addApiError(code, jqxhr, 'Could not retrieve backlinks');

			self.setStatus('failed');

			// Allow delete redirects task to begin

			// XFDC: self.discussion.taskManager.dfd.ublQuery.resolve();

		} );

	};

	// Process api callbacks

	var processBacklinks = function(result) {

		// Gather backlink results into array

		if ( result.query.backlinks ) {

			blresults = blresults.concat(result.query.backlinks);

		}

		// Gather image usage results into array

		if ( result.query.imageusage ) {

			iuresults = iuresults.concat(result.query.imageusage);

		}

		// Continue current query if needed

		if ( result.continue ) {

			apiQuery($.extend({}, queryqIndex], result.continue));

			return;

		}

		// Start next query, unless this is the final query

		qIndex++;

		if ( qIndex < query.length ) {

			apiQuery(queryqIndex]);

			return;

		}

		// Allow delete redirects task to begin

		// XFDC: self.discussion.taskManager.dfd.ublQuery.resolve();

		// Check if any backlinks or image uses were found

		if ( blresults.length === 0 && iuresults.length === 0 ) {

			self.addWarning('none found');

			self.setStatus('skipped');

			return;

		}

		// Process the results

		processResults();

	};

	// Get started

	apiQuery(queryqIndex]);

	

};





/* Task class for `self` object in unlinkBacklinks function

 * Very minimal copy of Task class from XFDcloser

 */

// Constructor

var Task = function(conf) {

	this.description = 'Unlinking backlinks';

	this.status = 'waiting';

	this.errors = [];

	this.warnings = [];

	this.tracking = {};

	this.editSummary = conf.editSummary;

	this.$notices = $('<div>').attr('id','Xunlink-notices');

	$('#mw-content-text').prepend(this.$notices);

	$('<h2>').text('Xunlink').insertBefore(this.$notices);

	$('<hr>').insertAfter(this.$notices);

};

Task.prototype.setStatus = function(s) {

	this.status = s;

	this.updateTaskNotices();

};

Task.prototype.setupTracking = function(key, total, allDoneCallback, allSkippedCallback) {

	var self = this;

	if ( allDoneCallback == null && allSkippedCallback == null ) {

		allDoneCallback = function() { this.setStatus('done'); };

		allSkippedCallback = function() { this.setStatus('skipped'); };

	}

	this.trackingkey = {

		success: 0,

		skipped: 0,

		total: total,

		dfd: $.Deferred()

			.done($.proxy(allDoneCallback, self))

			.fail($.proxy(allSkippedCallback, self))

	};

};

Task.prototype.track = function(key, success) {

	if ( success ) {

		this.trackingkey].success++;

	} else {

		this.trackingkey].skipped++;

	}



	if ( key === this.showTrackingProgress ) {

		this.updateTaskNotices(); // XFDC: this.updateStatus();

	}



	if ( this.trackingkey].skipped === this.trackingkey].total ) {

		this.trackingkey].dfd.reject();

	} else if ( this.trackingkey].success + this.trackingkey].skipped === this.trackingkey].total ) {

		this.trackingkey].dfd.resolve();

	}

};

Task.prototype.addError = function(e, critical) {

	// XFDC: var self = this;

	this.errors.push($('<span>').addClass('xfdc-notice-error').append(e));

	if ( critical ) {

		this.status = 'failed';

	}

	this.updateTaskNotices(); // XFDC: this.discussion.taskManager.updateTaskNotices(self);

};

Task.prototype.addWarning = function(w) {

	// XFDC: var self = this;

	this.warnings.push($('<span>').addClass('xfdc-notice-warning').append(w));

	this.updateTaskNotices(); // XFDC: this.discussion.taskManager.updateTaskNotices(self);

};

Task.prototype.addApiError = function(code, jqxhr, explanation, critical) {

	var self = this;

	self.addError([

		makeErrorMsg(code, jqxhr),

		' – ',

		$('<span>').append(explanation)

	], !!critical);

};

Task.prototype.getStatusText = function() {

	var self = this;

	switch ( self.status ) {

		// Not yet started:

		case 'waiting':

			return 'Waiting...';

		// In progress:

		case 'started':

			var $msg = $('<span>').append(

					$('<img>').attr({

					'src':'//upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Ajax-loader%282%29.gif/'+

						'40px-Ajax-loader%282%29.gif',

					'width':'20',

					'height':'5'

				})

			);

			if ( self.showTrackingProgress ) {

				var counts = this.trackingself.showTrackingProgress];

				$msg.append(

					$('<span>')

					.css('font-size', '88%')

					.append(

						'&nbsp;(' +

						(counts.success + counts.skipped) +

						'&thinsp;/&thinsp;' +

						counts.total +

						')'

					)

				);

			}

			return $msg;

		// Finished:

		case 'done':

			return 'Done!';

		case 'aborted':

		case 'failed':

		case 'skipped':

			return extraJs.toSentenceCase(self.status) + '.';

		default:

			// unknown

			return '';

	}

};

// Based on XFDC's taskManager.prototype.updateTaskNotices

Task.prototype.updateTaskNotices = function() {

	var task = this; // XFDC: var self = this;

	var $notices = this.$notices;

	var note = $('<p>')

		.addClass('xfdc-task-' + task.status)

		.addClass(task.name)

		.append(

			$('<span>').append(task.description),

			': ',

			$('<strong>').append(task.getStatusText()),

			$('<span>').append(task.errors),

			$('<span>').append(task.warnings)

		);

	$notices.empty().append(note);

};





/* ========== Main dialog ======================================================================= */

// Make a subclass of ProcessDialog 

function MainDialog( config ) {

	MainDialog.super.call( this, config );

}

OO.inheritClass( MainDialog, OO.ui.ProcessDialog );



// Specify a name for .addWindows()

MainDialog.static.name = 'mainDialog';

// Specify the static configurations: title and action set

MainDialog.static.title = 'Xunlink';

MainDialog.static.actions = 

	{ 

		flags:  'primary', 'progressive' ], 

		label: 'Continue', 

		action: 'continue' 

	},

	{ 

		flags: 'safe', 

		label: 'Cancel' 

	 }

];



// Customize the initialize() function to add content and layouts: 

MainDialog.prototype.initialize = function () {

	MainDialog.super.prototype.initialize.call( this );

	this.panel = new OO.ui.PanelLayout( { 

		padded: true, 

		expanded: false 

	} );

	this.content = new OO.ui.FieldsetLayout();



	this.summaryInput = new OO.ui.TextInputWidget();

	this.summaryPreview = new OO.ui.LabelWidget({classes: 'xu-preview']});



	this.summaryInputField = new OO.ui.FieldLayout( this.summaryInput, { 

		label: 'Enter the reason for link removal', 

		align: 'top' 

	} );

	this.summaryPreviewField = new OO.ui.FieldLayout( this.summaryPreview, { 

		label: 'Edit summary preview:', 

		align: 'top' 

	} );



	this.content.addItems( this.summaryInputField, this.summaryPreviewField );

	this.panel.$element.append( this.content.$element );

	this.$body.append( this.panel.$element );



	this.summaryInput.connect( this, { 'change': 'onSummaryInputChange' } );

};



// Specify any additional functionality required by the window (disable using an empty summary)

MainDialog.prototype.onSummaryInputChange = function ( value ) {

	this.actions.setAbilities( {

		continue: !!value.length

	} );

	var dialog = this;

	if ( !value.length ) {

		dialog.summaryPreviewField.toggle(false);

		dialog.updateSize();

	} else {

		API.get({

			action: 'parse',

			contentmodel: 'wikitext',

			summary: 'Removing link(s): ' + value + config.script.advert,

		})

		.then(function(result) {

			var $preview = $('<p>').append(result.parse.parsedsummary'*']);

			$preview.find('a').attr('target', '_blank');

			dialog.summaryPreview.setLabel($preview);

			dialog.summaryPreviewField.toggle(true);

			dialog.updateSize();

		});

	}

};



// Specify the dialog height (or don't to use the automatically generated height).

MainDialog.prototype.getBodyHeight = function () {

	// Note that "expanded: false" must be set in the panel's configuration for this to work.

	return this.panel.$element.outerHeight( true );

};



// Use getSetupProcess() to set up the window with data passed to it at the time 

// of opening

MainDialog.prototype.getSetupProcess = function ( data ) {

	data = data || {};

	return MainDialog.super.prototype.getSetupProcess.call( this, data )

	.next( function () {

		// Set up contents based on data

		var dataSumamary = data.summary || '';

		this.summaryInput.setValue( dataSumamary );

		this.onSummaryInputChange(dataSumamary);

	}, this );

};



// Specify processes to handle the actions.

MainDialog.prototype.getActionProcess = function ( action ) {

	var dialog = this;

	if ( action === 'continue' ) {

		/* Create a new process to handle the action

		return new OO.ui.Process( function () {

			var task = new Task(this.summaryInput.getValue());

			unlinkBacklinks(task);

		}, this );

		*/

		var task = new Task( {editSummary: 'Removing link(s): ' + this.summaryInput.getValue()} );

		dialog.close();

		task.updateTaskNotices();

		unlinkBacklinks(task);

		

	}

	// Fallback to parent handler

	return MainDialog.super.prototype.getActionProcess.call( this, action );

};



// Use the getTeardownProcess() method to perform actions whenever the dialog is closed. 

// This method provides access to data passed into the window's close() method 

// or the window manager's closeWindow() method.

MainDialog.prototype.getTeardownProcess = function ( data ) {

	return MainDialog.super.prototype.getTeardownProcess.call( this, data )

	.first( function () {

		// Perform any cleanup as needed

		this.summaryInput.setValue("");

	}, this );

};



// Create and append a window manager.

var windowManager = new OO.ui.WindowManager();

$( 'body' ).append( windowManager.$element );



// Create a new process dialog window.

var mainDialog = new MainDialog();



// Add the window to window manager using the addWindows() method.

windowManager.addWindows(  mainDialog  );





/* ========== Portlet link ====================================================================== */

// handlePortletClick

var handlePortletClick = function(e) {

	e.preventDefault();

	// Try to find the deletion log comment

	var comment = '';

	var $commentEl = $('.mw-logline-delete').first().find('.comment').first();

	if ( $commentEl.length ) {

		var commentEl = $commentEl.get()[0];

		var children = commentEl.childNodes;

		for (var child of children) {

			var nodeName = 	child.nodeName;

			if (nodeName == 'A') {

				var target = child.href.replace(/^.*?\/wiki\//, '').replace(/_/g,' ');

				var label = child.textContent;

				var wikilink = ( target === label ) ?

					'[[' + label + ']]' :

					'[[' + target + '|' + label + ']]';

				comment += wikilink;

			} else {

				comment += child.nodeValue;

			}

		}

		comment = comment.replace(' ([[Wikipedia:XFDC|XFDcloser]])', '');

		comment = comment.slice(1,-1);

	}

	// Open the window! 

	windowManager.openWindow( mainDialog, { summary: comment } );

};



var portletLink = mw.util.addPortletLink(

	'p-cactions',

	'#',

	'Xunlink',

	'ca-xu',

	"Unlink this page's backlinks using Xunlink",

	null,

	"#ca-move"

);

$(portletLink).on('click', handlePortletClick);



}); // End of dependencies loaded callback

}); // End of page load callback

// </nowiki>
From Wikipedia, the free encyclopedia
(Redirected from Wikipedia:Xunlink)
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.

/***************************************************************************************************

 Xunlink --- by Evad37

 > The power of XFDcloser's 'unlink backlinks' function, for any page.

***************************************************************************************************/

/* jshint esversion: 6, laxbreak: true, undef: true, maxerr:999 */

/* globals console, window, $, mw, OO, extraJs */

// <nowiki>

$( function($) {

	

/* ========== Configuration ===================================================================== */

var config = {

	// Script info

	script: {

		// Advert to append to edit summaries

		advert:  ' ([[User:Evad37/Xunlink|Xunlink]])',

		version: '2.0.1'

	},

	// MediaWiki configuration values

	mw: mw.config.get( 

		'wgArticleId',

		'wgPageName',

		'wgUserGroups',

		'wgUserName',

		'wgFormattedNamespaces',

		'wgMonthNames',

		'wgNamespaceNumber'

	 ),

	allowedNamespaces: 0, 6, 100 // article, File, Portal

};

// xfd props, for compatbility with code from XFDcloser

config.xfd = {

	// Namespaces to unlink from: main, Template, Portal, Draft

	ns_unlink: '0', '10', '100', '118'],

	// Type (files get treated differently)

	type: config.mw.wgNamespaceNumber === 6 ? 'ffd' : 'other'

};



/* ========== Validate page suitability ========================================================= */

// Validate namespace 

var isCorrectNamespace = config.allowedNamespaces.includes(config.mw.wgNamespaceNumber);

if ( !isCorrectNamespace ) {

	return;

}



// If a portal, only make available if deleted

var isPortal = config.mw.wgNamespaceNumber === 100;

var notDeleted = config.mw.wgArticleId > 0;

if ( isPortal && notDeleted ) {

	return;

}





/* ========== Dependencies ====================================================================== */

mw.loader.using([

	'mediawiki.util', 'mediawiki.api', 'mediawiki.Title',

	'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'jquery.ui',

	'ext.gadget.libExtraUtil'

]).then(function() {



/* ========== CSS Styles ========================================================================

 * TODO: migrate to css subpage

 */

mw.util.addCSS(

		// Task notices

		'.xfdc-notices { width:80%; font-size:95%; padding-left:2.5em; }',

		'.xfdc-notices > p { margin:0; line-height:1.1em; }',

		'.xfdc-notice-error { color:#D00000; font-size:92% }',

		'.xfdc-notice-warning { color:#9900A2; font-size:92% }',

		'.xfdc-notice-error::before, .xfdc-notice-warning::before { content: " ["; }',

		'.xfdc-notice-error::after,  .xfdc-notice-warning::after  { content: "]"; }',

		'.xfdc-task-waiting { color:#595959; }',

		'.xfdc-task-started { color:#0000D0; }',

		'.xfdc-task-done { color:#006800; }',

		'.xfdc-task-skipped { color:#697000; }',

		'.xfdc-task-aborted { color:#C00049; }',

		'.xfdc-task-failed { color:#D00000; }',

		// Preview of edit summary

		'.xu-preview { background-color:#fafafa; border:1px dotted #777; '+

			'margin-top: 0px; padding:0px 10px; font-size: 90%; width: 100%; }'

	

	.join('\n')

);



/* ========== Helper functions ==================================================================

 * TODO: these should probably be part of one or more script modules/libraries, which could be

 *  loaded with mw.loader.getScript()

 */

 

 /** safeUnescape

 * Un-escapes some HTML tags (<br>, <p>, <ul>, <li>, <hr>, and <pre>); turns wikilinks

 * into real links. Ignores anyting within <pre>...</pre> tags.

 * Input will first be escaped using mw.html.escape() unless specified 

 * @param {String} text

 * @param {Object} config Configuration options

 * @config {Boolean} noEscape - do not escape the input first

 * @returns {String} unescaped text

 */

var safeUnescape = function(text, config) {

	var path = 'https:' + mw.config.get('wgServer') + '/wiki/';



	return ( config && config.noEscape && text || mw.html.escape(text) )

	// Step 1: unescape <pre> tags

	.replace(  

		/&lt;(\/?pre\s?\/?)&gt;/g,

		'<$1>'

	)

	// Step 2: replace piped wikilinks with real links (unless inside <pre> tags)

	.replace( 

		/\[\[([^\|\]]*?)\|([^\|\]]*?)\]\](?![^<]*?<\/pre>)/g,

		'<a href="' + path + mw.util.wikiUrlencode('$1') + '" target="_blank">$2</a>'

	)

	// Step 3: replace other wikilinks with real links (unless inside <pre> tags)

	.replace( 

		/\[\[([^\|\]]+?)]\](?![^<]*?<\/pre>)/g,

		'<a href="' + path + mw.util.wikiUrlencode('$1') + '" target="_blank">$1</a>'

	)

	// Step 4: unescape other tags: <br>, <p>, <ul>, <li>, <hr> (unless inside <pre> tags)

	.replace(

		/&lt;(\/?(?:br|p|ul|li|hr)\s?\/?)&gt;(?![^<]*?<\/pre>)/g,

		'<$1>'

	);

};



/** multiButtonConfirm

 * @param {Object} config

 * @config {String} title  Title for the dialogue

 * @config {String} message  Message for the dialogue. HTML tags (except for <br>, <p>, <ul>,

 *  <li>, <hr>, and <pre> tags) are escaped; wikilinks are turned into real links.

 * @config {Array} actions  Optional. Array of configuration objects for OO.ui.ActionWidget

 *  <https://doc.wikimedia.org/oojs-ui/master/js/#!/api/OO.ui.ActionWidget>.

 *  If not specified, the default actions are 'accept' (with label 'OK') and 'reject' (with

 *  label 'Cancel').

 * @config {String} size  Symbolic name of the dialog size: small, medium, large, larger or full.

 * @return {Promise<String>} action taken by user

 */

var multiButtonConfirm = function(config) {

	var dialogClosed = $.Deferred();

	

	// Wrap message in a HtmlSnippet to prevent escaping

	var htmlSnippetMessage = new OO.ui.HtmlSnippet(

		safeUnescape(config.message)

	);



	var windowManager = new OO.ui.WindowManager();

	var messageDialog = new OO.ui.MessageDialog();

	$('body').append( windowManager.$element );

	windowManager.addWindows(  messageDialog  );

	windowManager.openWindow( messageDialog, {

		'title': config.title,

		'message': htmlSnippetMessage,

		'actions': config.actions,

		'size': config.size

	} );

	windowManager.on('closing', function(_win, promise) {

		promise.then(function(data) {

			dialogClosed.resolve(data && data.action);

			windowManager.destroy();

		});

	});



	return dialogClosed.promise();

};



var makeErrorMsg = function(code, jqxhr) {

	var details = '';

	if ( code === 'http' && jqxhr.textStatus === 'error' ) {

		details = 'HTTP error ' + jqxhr.xhr.status;

	} else if ( code === 'http' ) {

		details = 'HTTP error: ' + jqxhr.textStatus;

	} else if ( code === 'ok-but-empty' ) {

		details = 'Error: Got an empty response from the server';

	} else {

		details = 'API error: ' + code;

	}

	return details;

};



var arrayFromResponsePages = function(response) {

	return $.map(response.query.pages, function(page) { return page; });

};



/* ========== API =============================================================================== */

var API = new mw.Api( {

	ajax: {

		headers: { 

			'Api-User-Agent': 'Xunlink/' + config.script.version + 

				' ( /info/en/?search=User:Evad37/Xunlink )'

		}

	}

} );





/* ========== Unlink backlinks ================================================================== */

/**unlinkBacklinks

 *

 * Copied from XFDcloser, with minimal changes. Such changes have the original code in comments

 * beginning `XFDC:`

 *

 * TODO: merge code, and import the same copy here and into XFDcloser

 *

 * @param self Object to hold some input date, and to recieve status messages

 */

var unlinkBacklinks = function(self) {



	// Notify task is started

	self.setStatus('started');

	

	var pageTitles =  config.mw.wgPageName]; // XFDC: self.discussion.getPageTitles(self.pages)

	var redirectTitles = [];

	// Ignore the following titles, and any of their subpages

	var ignoreTitleBases = 

		'Template:WPUnited States Article alerts',

		'Template:Article alerts columns',

		'Template:Did you know nominations'

	];

	var getBase = function(title) {

		return title.split('/')[0];

	};

	var blresults = [];

	var iuresults = [];

	

	//convert results (arrays of objects) to titles (arrays of strings), removing duplicates

	var flattenToTitles = function(results) {

		return results.reduce(

			function(flatTitles, result) {

				if ( result.redirlinks ) {

					if ( !redirectTitles.includes(result.title)) {

						redirectTitles.push(result.title);

					}

					return flatTitles.concat(

						result.redirlinks.reduce(

							function(flatRedirLinks, redirLink) {

								if (

									flatTitles.includes(redirLink.title) ||

									pageTitles.includes(redirLink.title) ||

									ignoreTitleBases.includes(getBase(redirLink.title))

								) {

									return flatRedirLinks;

								} else {

									return flatRedirLinks.concat(redirLink.title);

								}

							},

							[]

						)

					);

				} else if (

					result.redirect === '' ||

					flatTitles.includes(result.title) ||

					pageTitles.includes(result.title) ||

					ignoreTitleBases.includes(getBase(result.title))

				) {

					return flatTitles;

				} else {

					return flatTitles.concat(result.title);

				}

			},

			[]

		);

	};



	var apiEditPage = function(pageTitle, newWikitext) {

		API.postWithToken( 'csrf', {

			action: 'edit',

			title: pageTitle,

			text: newWikitext,

			summary: self.editSummary + config.script.advert, /* XFDC:

				'Removing link(s)' +

				(( config.xfd.type === 'ffd' ) ? ' / file usage(s)' : '' ) +

				': [[' + self.discussion.getNomPageLink() + ']] closed as ' +

				self.inputData.getResult() + config.script.advert, */

			minor: 1,

			nocreate: 1

		} )

		.done( function() {

			self.track('unlink', true);

		} )

		.fail( function(code, jqxhr) {

			self.track('unlink', false);

			self.addApiError(code, jqxhr, 

				'Could not remove backlinks from ',

				extraJs.makeLink(pageTitle)

			]);

		} );

	};



	/**

	 * @param {String} pageTitle

	 * @param {String} wikitext

	 * @returns {Promise(String)} updated wikitext, with any list items either removed or unlinked

	 */

	var checkListItems = function(pageTitle, wikitext) {

		// Find lines marked with {{subst:void}}, and the preceding section heading (if any)

		var toReview = /^{{subst:void}}(.*)$/m.exec(wikitext);

		if ( !toReview ) {

			// None found, no changes needed

			return $.Deferred().resolve(wikitext).promise();

		}

		// Find the preceding heading, if any

		var precendingText = wikitext.split('{{subst:void}}')[0];

		var allHeadings = precendingText.match(/^=+.+?=+$/gm);

		var heading = ( !allHeadings ) ? null : allHeadingsallHeadings.length - 1].replace(/(^=* *| *=*$)/g, '');

		// Prompt user

		return multiButtonConfirm({

			title: 'Review unlinked list item',

			message: '[[' + pageTitle +

				( ( heading ) ? '#' +

					mw.util.wikiUrlencode(

						heading.replace(/\[\[([^\|\]]*?)\|([^\]]*?)\]\]/, '$2')

						.replace(/\[\[([^\|\]]*?)\]\]/, '$1')

					) + ']]' : ']]' ) +

				': ' +

				'<pre>' + toReview1 + '</pre>',

			actions: 

				{ label:'Keep item', action:'keep' },

				{ label:'Remove item', action:'remove'}

			],

			size: 'medium'

		})

		.then(function(action) {

			if ( action === 'keep' ) {

				// Remove the void from the start of the line

				wikitext = wikitext.replace(/^{{subst:void}}/m, '');

			} else {

				// Remove the whole line

				wikitext = wikitext.replace(/^{{subst:void}}.*\n?/m, '');

			}

			// Iterate, in case there is more to be reviewed

			return checkListItems(pageTitle, wikitext);

		});

	};

	

	var processUnlinkPages = function(result) {

		if ( !result.query || !result.query.pages ) {

			// No results

			self.addApiError('result.query.pages not found', null, 'Could not read contents of pages; '+

				'could not remove backlinks');

			console.log('[XFDcloser] API error: result.query.pages not found... result =');

			console.log(result);

			self.setStatus('failed');

			return;

		}

		// For each page, pass the wikitext through the unlink function

		var pages = arrayFromResponsePages(result);

		pages.reduce(

			function(previous, page) {

				return $.when(previous).then(function(){

					var oldWikitext = page.revisions0]['*'];

					var newWikitext = extraJs.unlink(

						oldWikitext,

						pageTitles.concat(redirectTitles),

						page.ns,

						!!page.categories

					);

					if ( oldWikitext !== newWikitext ) {

						var confirmedPromise = checkListItems(page.title, newWikitext);

						confirmedPromise.then(function(updatedWikitext) {

							apiEditPage(page.title, updatedWikitext);

						});

						return confirmedPromise;

					} else {

						self.addWarning(['Skipped ',

							extraJs.makeLink(page.title),

							' (no direct links)'

						]);

						self.track('unlink', false);

						return true;

					}

				});

			},

			true);

	};



	var apiReadFail = function(code, jqxhr) {

		self.addApiError(code, jqxhr, 'Could not read contents of pages; '+

			'could not remove backlinks');

		self.setStatus('failed');

	};

	

	var processResults = function() {

		// Flatten results arrays 

		if ( blresults.length !== 0 ) {

			blresults = flattenToTitles(blresults);

		}

		if ( iuresults.length !== 0 ) {

			iuresults = flattenToTitles(iuresults);

			// Remove image usage titles that are also in backlikns results 

			iuresults = iuresults.filter(function(t) { return $.inArray(t, blresults) === -1; });

		}



		// Check if, after flattening, there are still backlinks or image uses

		if ( blresults.length === 0 && iuresults.length === 0 ) {

			self.addWarning('none found');

			self.setStatus('skipped');

			return;

		}



		

		// Ask user for confirmation

		var heading = 'Unlink backlinks';

		if ( iuresults.length !== 0 ) {

			heading += '('; 

			if ( blresults.length !== 0 ) {

				heading += 'and ';

			}

			heading += 'file usage)';

		}

		heading += ':';

		var para = '<p>All '+ (blresults.length + iuresults.length) + ' pages listed below may be '+

			'edited (unless backlinks are only present due to transclusion of a template).</p>'+

			'<p>To process only some of these pages, use Twinkle\'s unlink tool instead.</p>'+

			'<p>Use with caution, after reviewing the pages listed below. '+

			'Note that the use of high speed, high volume editing software (such as this tool and '+

			'Twinkle\'s unlink tool) is subject to the Bot policy\'s [[WP:ASSISTED|Assisted editing guidelines]] '+

			'</p><hr>';

		var list = '<ul>';

		if ( blresults.length !== 0 ) {

			list += '<li>[[' + blresults.join(']]</li><li>[[') + ']]</li>';

		}

		if ( iuresults.length !== 0 ) {

			list += '<li>[[' + iuresults.join(']]</li><li>[[') + ']]</li>';

		}

		list += '<ul>';

		

		multiButtonConfirm({

			title: heading,

			message: para + list,

			actions: 

				{ label: 'Cancel', flags: 'safe' },

				{ label: 'Remove backlinks', action: 'accept', flags: 'progressive' }

			],

			size: 'medium'

		})

		.then(function(action) {

			if ( action ) {

				var unlinkTitles = iuresults.concat(blresults);

				self.setupTracking('unlink', unlinkTitles.length);

				self.showTrackingProgress = 'unlink';

				// get wikitext of titles, check if disambig - in lots of 50 (max for Api)

				for (var ii=0; ii<unlinkTitles.length; ii+=50) {

					API.get( {

						action: 'query',

						titles: unlinkTitles.slice(ii, ii+49).join('|'),

						prop: 'categories|revisions',

						clcategories: 'Category:All disambiguation pages',

						rvprop: 'content',

						indexpageids: 1

					} )

					.done( processUnlinkPages )

					.fail( apiReadFail );

				}

			} else {

				self.addWarning('Cancelled by user');

				self.setStatus('skipped');

			}

		});

	};



	// Queries

	var blParams = {

		list: 'backlinks',

		blfilterredir: 'nonredirects',

		bllimit: 'max',

		blnamespace: config.xfd.ns_unlink,

		blredirect: 1

	};

	var iuParams = {

		list: 'backlinks|imageusage',

		iutitle: '',

		iufilterredir: 'nonredirects',

		iulimit: 'max',

		iunamespace: config.xfd.ns_unlink,

		iuredirect: 1

	};

	var query = pageTitles.map(function(page) {

		return $.extend(

			{ action: 'query' },

			blParams,

			{ bltitle: page },

			( config.xfd.type === 'ffd' ) ? iuParams : null,

			( config.xfd.type === 'ffd' ) ? { iutitle: page } : null

		);

	});

	// Variable for incrementing current query

	var qIndex = 0;

	// Function to do Api query

	var apiQuery = function(q) {

		API.get( q )

		.done( processBacklinks )

		.fail( function(code, jqxhr) {

			self.addApiError(code, jqxhr, 'Could not retrieve backlinks');

			self.setStatus('failed');

			// Allow delete redirects task to begin

			// XFDC: self.discussion.taskManager.dfd.ublQuery.resolve();

		} );

	};

	// Process api callbacks

	var processBacklinks = function(result) {

		// Gather backlink results into array

		if ( result.query.backlinks ) {

			blresults = blresults.concat(result.query.backlinks);

		}

		// Gather image usage results into array

		if ( result.query.imageusage ) {

			iuresults = iuresults.concat(result.query.imageusage);

		}

		// Continue current query if needed

		if ( result.continue ) {

			apiQuery($.extend({}, queryqIndex], result.continue));

			return;

		}

		// Start next query, unless this is the final query

		qIndex++;

		if ( qIndex < query.length ) {

			apiQuery(queryqIndex]);

			return;

		}

		// Allow delete redirects task to begin

		// XFDC: self.discussion.taskManager.dfd.ublQuery.resolve();

		// Check if any backlinks or image uses were found

		if ( blresults.length === 0 && iuresults.length === 0 ) {

			self.addWarning('none found');

			self.setStatus('skipped');

			return;

		}

		// Process the results

		processResults();

	};

	// Get started

	apiQuery(queryqIndex]);

	

};





/* Task class for `self` object in unlinkBacklinks function

 * Very minimal copy of Task class from XFDcloser

 */

// Constructor

var Task = function(conf) {

	this.description = 'Unlinking backlinks';

	this.status = 'waiting';

	this.errors = [];

	this.warnings = [];

	this.tracking = {};

	this.editSummary = conf.editSummary;

	this.$notices = $('<div>').attr('id','Xunlink-notices');

	$('#mw-content-text').prepend(this.$notices);

	$('<h2>').text('Xunlink').insertBefore(this.$notices);

	$('<hr>').insertAfter(this.$notices);

};

Task.prototype.setStatus = function(s) {

	this.status = s;

	this.updateTaskNotices();

};

Task.prototype.setupTracking = function(key, total, allDoneCallback, allSkippedCallback) {

	var self = this;

	if ( allDoneCallback == null && allSkippedCallback == null ) {

		allDoneCallback = function() { this.setStatus('done'); };

		allSkippedCallback = function() { this.setStatus('skipped'); };

	}

	this.trackingkey = {

		success: 0,

		skipped: 0,

		total: total,

		dfd: $.Deferred()

			.done($.proxy(allDoneCallback, self))

			.fail($.proxy(allSkippedCallback, self))

	};

};

Task.prototype.track = function(key, success) {

	if ( success ) {

		this.trackingkey].success++;

	} else {

		this.trackingkey].skipped++;

	}



	if ( key === this.showTrackingProgress ) {

		this.updateTaskNotices(); // XFDC: this.updateStatus();

	}



	if ( this.trackingkey].skipped === this.trackingkey].total ) {

		this.trackingkey].dfd.reject();

	} else if ( this.trackingkey].success + this.trackingkey].skipped === this.trackingkey].total ) {

		this.trackingkey].dfd.resolve();

	}

};

Task.prototype.addError = function(e, critical) {

	// XFDC: var self = this;

	this.errors.push($('<span>').addClass('xfdc-notice-error').append(e));

	if ( critical ) {

		this.status = 'failed';

	}

	this.updateTaskNotices(); // XFDC: this.discussion.taskManager.updateTaskNotices(self);

};

Task.prototype.addWarning = function(w) {

	// XFDC: var self = this;

	this.warnings.push($('<span>').addClass('xfdc-notice-warning').append(w));

	this.updateTaskNotices(); // XFDC: this.discussion.taskManager.updateTaskNotices(self);

};

Task.prototype.addApiError = function(code, jqxhr, explanation, critical) {

	var self = this;

	self.addError([

		makeErrorMsg(code, jqxhr),

		' – ',

		$('<span>').append(explanation)

	], !!critical);

};

Task.prototype.getStatusText = function() {

	var self = this;

	switch ( self.status ) {

		// Not yet started:

		case 'waiting':

			return 'Waiting...';

		// In progress:

		case 'started':

			var $msg = $('<span>').append(

					$('<img>').attr({

					'src':'//upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Ajax-loader%282%29.gif/'+

						'40px-Ajax-loader%282%29.gif',

					'width':'20',

					'height':'5'

				})

			);

			if ( self.showTrackingProgress ) {

				var counts = this.trackingself.showTrackingProgress];

				$msg.append(

					$('<span>')

					.css('font-size', '88%')

					.append(

						'&nbsp;(' +

						(counts.success + counts.skipped) +

						'&thinsp;/&thinsp;' +

						counts.total +

						')'

					)

				);

			}

			return $msg;

		// Finished:

		case 'done':

			return 'Done!';

		case 'aborted':

		case 'failed':

		case 'skipped':

			return extraJs.toSentenceCase(self.status) + '.';

		default:

			// unknown

			return '';

	}

};

// Based on XFDC's taskManager.prototype.updateTaskNotices

Task.prototype.updateTaskNotices = function() {

	var task = this; // XFDC: var self = this;

	var $notices = this.$notices;

	var note = $('<p>')

		.addClass('xfdc-task-' + task.status)

		.addClass(task.name)

		.append(

			$('<span>').append(task.description),

			': ',

			$('<strong>').append(task.getStatusText()),

			$('<span>').append(task.errors),

			$('<span>').append(task.warnings)

		);

	$notices.empty().append(note);

};





/* ========== Main dialog ======================================================================= */

// Make a subclass of ProcessDialog 

function MainDialog( config ) {

	MainDialog.super.call( this, config );

}

OO.inheritClass( MainDialog, OO.ui.ProcessDialog );



// Specify a name for .addWindows()

MainDialog.static.name = 'mainDialog';

// Specify the static configurations: title and action set

MainDialog.static.title = 'Xunlink';

MainDialog.static.actions = 

	{ 

		flags:  'primary', 'progressive' ], 

		label: 'Continue', 

		action: 'continue' 

	},

	{ 

		flags: 'safe', 

		label: 'Cancel' 

	 }

];



// Customize the initialize() function to add content and layouts: 

MainDialog.prototype.initialize = function () {

	MainDialog.super.prototype.initialize.call( this );

	this.panel = new OO.ui.PanelLayout( { 

		padded: true, 

		expanded: false 

	} );

	this.content = new OO.ui.FieldsetLayout();



	this.summaryInput = new OO.ui.TextInputWidget();

	this.summaryPreview = new OO.ui.LabelWidget({classes: 'xu-preview']});



	this.summaryInputField = new OO.ui.FieldLayout( this.summaryInput, { 

		label: 'Enter the reason for link removal', 

		align: 'top' 

	} );

	this.summaryPreviewField = new OO.ui.FieldLayout( this.summaryPreview, { 

		label: 'Edit summary preview:', 

		align: 'top' 

	} );



	this.content.addItems( this.summaryInputField, this.summaryPreviewField );

	this.panel.$element.append( this.content.$element );

	this.$body.append( this.panel.$element );



	this.summaryInput.connect( this, { 'change': 'onSummaryInputChange' } );

};



// Specify any additional functionality required by the window (disable using an empty summary)

MainDialog.prototype.onSummaryInputChange = function ( value ) {

	this.actions.setAbilities( {

		continue: !!value.length

	} );

	var dialog = this;

	if ( !value.length ) {

		dialog.summaryPreviewField.toggle(false);

		dialog.updateSize();

	} else {

		API.get({

			action: 'parse',

			contentmodel: 'wikitext',

			summary: 'Removing link(s): ' + value + config.script.advert,

		})

		.then(function(result) {

			var $preview = $('<p>').append(result.parse.parsedsummary'*']);

			$preview.find('a').attr('target', '_blank');

			dialog.summaryPreview.setLabel($preview);

			dialog.summaryPreviewField.toggle(true);

			dialog.updateSize();

		});

	}

};



// Specify the dialog height (or don't to use the automatically generated height).

MainDialog.prototype.getBodyHeight = function () {

	// Note that "expanded: false" must be set in the panel's configuration for this to work.

	return this.panel.$element.outerHeight( true );

};



// Use getSetupProcess() to set up the window with data passed to it at the time 

// of opening

MainDialog.prototype.getSetupProcess = function ( data ) {

	data = data || {};

	return MainDialog.super.prototype.getSetupProcess.call( this, data )

	.next( function () {

		// Set up contents based on data

		var dataSumamary = data.summary || '';

		this.summaryInput.setValue( dataSumamary );

		this.onSummaryInputChange(dataSumamary);

	}, this );

};



// Specify processes to handle the actions.

MainDialog.prototype.getActionProcess = function ( action ) {

	var dialog = this;

	if ( action === 'continue' ) {

		/* Create a new process to handle the action

		return new OO.ui.Process( function () {

			var task = new Task(this.summaryInput.getValue());

			unlinkBacklinks(task);

		}, this );

		*/

		var task = new Task( {editSummary: 'Removing link(s): ' + this.summaryInput.getValue()} );

		dialog.close();

		task.updateTaskNotices();

		unlinkBacklinks(task);

		

	}

	// Fallback to parent handler

	return MainDialog.super.prototype.getActionProcess.call( this, action );

};



// Use the getTeardownProcess() method to perform actions whenever the dialog is closed. 

// This method provides access to data passed into the window's close() method 

// or the window manager's closeWindow() method.

MainDialog.prototype.getTeardownProcess = function ( data ) {

	return MainDialog.super.prototype.getTeardownProcess.call( this, data )

	.first( function () {

		// Perform any cleanup as needed

		this.summaryInput.setValue("");

	}, this );

};



// Create and append a window manager.

var windowManager = new OO.ui.WindowManager();

$( 'body' ).append( windowManager.$element );



// Create a new process dialog window.

var mainDialog = new MainDialog();



// Add the window to window manager using the addWindows() method.

windowManager.addWindows(  mainDialog  );





/* ========== Portlet link ====================================================================== */

// handlePortletClick

var handlePortletClick = function(e) {

	e.preventDefault();

	// Try to find the deletion log comment

	var comment = '';

	var $commentEl = $('.mw-logline-delete').first().find('.comment').first();

	if ( $commentEl.length ) {

		var commentEl = $commentEl.get()[0];

		var children = commentEl.childNodes;

		for (var child of children) {

			var nodeName = 	child.nodeName;

			if (nodeName == 'A') {

				var target = child.href.replace(/^.*?\/wiki\//, '').replace(/_/g,' ');

				var label = child.textContent;

				var wikilink = ( target === label ) ?

					'[[' + label + ']]' :

					'[[' + target + '|' + label + ']]';

				comment += wikilink;

			} else {

				comment += child.nodeValue;

			}

		}

		comment = comment.replace(' ([[Wikipedia:XFDC|XFDcloser]])', '');

		comment = comment.slice(1,-1);

	}

	// Open the window! 

	windowManager.openWindow( mainDialog, { summary: comment } );

};



var portletLink = mw.util.addPortletLink(

	'p-cactions',

	'#',

	'Xunlink',

	'ca-xu',

	"Unlink this page's backlinks using Xunlink",

	null,

	"#ca-move"

);

$(portletLink).on('click', handlePortletClick);



}); // End of dependencies loaded callback

}); // End of page load callback

// </nowiki>

Videos

Youtube | Vimeo | Bing

Websites

Google | Yahoo | Bing

Encyclopedia

Google | Yahoo | Bing

Facebook