User:Evad37/Xunlink.js

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

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 */

//

$( function($) {

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

var config = {

// Script info

script: {

// Advert to append to edit summaries

advert: ' (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 (
,

,

    ,
  • ,
    , and
    ); turns wikilinks

    * into real links. Ignores anyting within

    ...
    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

     tags

    .replace(

    /<(\/?pre\s?\/?)>/g,

    '<$1>'

    )

    // Step 2: replace piped wikilinks with real links (unless inside

     tags)

    .replace(

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

    '$2'

    )

    // Step 3: replace other wikilinks with real links (unless inside

     tags)

    .replace(

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

    '$1'

    )

    // Step 4: unescape other tags:
    ,

    ,

      ,
    • ,
      (unless inside
       tags)

      .replace(

      /<(\/?(?:br|p|ul|li|hr)\s?\/?)>(?![^<]*?<\/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
      ,

      ,

        ,

        *

      • ,
        , and
         tags) are escaped; wikilinks are turned into real links.

        * @config {Array} actions Optional. Array of configuration objects for 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} 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')

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

        ': ' +

        '

        ' + toReview[1] + '
        ',

        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 = '

        All '+ (blresults.length + iuresults.length) + ' pages listed below may be '+

        'edited (unless backlinks are only present due to transclusion of a template).

        '+

        '

        To process only some of these pages, use Twinkle\'s unlink tool instead.

        '+

        '

        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 Assisted editing guidelines '+

        '


        ';

        var list = '

          ';

          if ( blresults.length !== 0 ) {

          list += '

        • ' + blresults.join('
        • ') + '
        • ';

          }

          if ( iuresults.length !== 0 ) {

          list += '

        • ' + iuresults.join('
        • ') + '
        • ';

          }

          list += '

            ';

            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

            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 = $('

            ').attr('id','Xunlink-notices');

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

            $('

            ').text('Xunlink').insertBefore(this.$notices);

            $('


            ').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($('').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($('').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),

            ' – ',

            $('').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 = $('').append(

            $('').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(

            $('')

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

            .append(

            ' (' +

            (counts.success + counts.skipped) +

            ' / ' +

            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 = $('

            ')

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

            .addClass(task.name)

            .append(

            $('').append(task.description),

            ': ',

            $('').append(task.getStatusText()),

            $('').append(task.errors),

            $('').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 = $('

            ').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 + '' :

            '' + label + '';

            comment += wikilink;

            } else {

            comment += child.nodeValue;

            }

            }

            comment = comment.replace(' (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

            //