Jump to content

User:Evad37/Xunlink/sandbox.js

From Wikipedia, the free encyclopedia
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.0'
	},
	// 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'
};

/* ############################################################################################## */
/* ########## Sandbox usage only (remove when updating main script) ############################# */
if ( config.mw.wgUserName === 'Evad37' ) {
	config.script.advert = ' ([[User:Evad37/Xunlink/sandbox.js|Xunlink/sandbox]])';
	config.script.version += '-sandbox';
	config.xfd.ns_unlink = ['3']; // User_talk
	config.allowedNamespaces = [2, 100]; // User, Portal
}
/* ########## End of sandbox usage only (remove when updating script) ########################### */
/* ############################################################################################## */

/* ========== 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 + 
				' ( https://en.wikipedia.org/wiki/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 : allHeadings[allHeadings.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>' + toReview[1] + '</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.revisions[0]['*'];
					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({}, query[qIndex], result.continue));
			return;
		}
		// Start next query, unless this is the final query
		qIndex++;
		if ( qIndex < query.length ) {
			apiQuery(query[qIndex]);
			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(query[qIndex]);
	
};


/* 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.tracking[key] = {
		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.tracking[key].success++;
	} else {
		this.tracking[key].skipped++;
	}

	if ( key === this.showTrackingProgress ) {
		this.updateTaskNotices(); // XFDC: this.updateStatus();
	}

	if ( this.tracking[key].skipped === this.tracking[key].total ) {
		this.tracking[key].dfd.reject();
	} else if ( this.tracking[key].success + this.tracking[key].skipped === this.tracking[key].total ) {
		this.tracking[key].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.tracking[self.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>