MediaWiki:Gadget-AjaxQuickDelete.js: Różnice pomiędzy wersjami

Z Wikipedii, wolnej encyklopedii
Usunięta treść Dodana treść
poprawki odnosnie znaku nowej linii (autor: User:Msz2001, Specjalna:Diff/66424473/66466182)
m alfabetyczne sortowanie SPEEDY_PRESETS
Linia 109: Linia 109:
//! List of presets for speedy deletion
//! List of presets for speedy deletion
var SPEEDY_PRESETS = [
var SPEEDY_PRESETS = [
'test', 'substub', 'nieency', 'hoax', 'or', 'reklama', 'bezsens',
'bezsens', 'brudnopis', 'dane osobowe', 'hoax', 'moje', 'nieaktualne',
'wandalizm', 'dane osobowe', 'zniesławienie', 'techniczne',
'nieency', 'npa', 'or', 'plik', 'redir', 'reklama', 'substub',
'translator', 'redir', 'wygłup', 'npa', 'moje', 'nieaktualne',
'techniczne', 'test', 'translator', 'wandalizm', 'wygłup',
'plik', 'brudnopis'
'zniesławienie'
];
];



Wersja z 12:16, 5 mar 2022

/* eslint-disable array-bracket-newline */
/* eslint-disable array-element-newline */
/* eslint-disable indent */
/* global $, mw, OO */
/**
 * @author [[c:User:Ilmari Karonen]] (original code)
 * @author [[c:User:DieBuche]] (rewritten & extended)
 * @author [[c:User:Lupo]] (bot detection and encoding fixer)
 * @author [[w:pl:User:Lampak]] (adaptation for Polish Wikipedia)
 * @author [[w:pl:User:Msz2001]] (rewrite for OOUI and code cleanup)
 * 
 * Originally written on Wikimedia Commons as a replacement for [[c:MediaWiki:Quick-delete-code.js]]
 * 
 * Full list of authors:
 *  - original version: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-AjaxQuickDelete.js&action=history
 *  - adapted on plwiki: https://pl.wikipedia.org/w/index.php?title=MediaWiki:Gadget-AjaxQuickDelete.js&action=history
 * 
 * Idea for quick reasons for speedy deletion and copyvio source based on [[w:pl:User:Krzysiek 123456789]]'s fork of this gadget
 * 
 * <nowiki>
 */
(function () {
    //! Translatable messages
    var MSG = {
        toolboxLink: "Zgłoś do usunięcia",
        dialogTitle: "Zgłaszanie do usunięcia",
        dialogCancel: "Anuluj",
        dialogSubmit: "Zatwierdź",

        dnuReasonLabel: "Wyjaśnij dokładnie, dlaczego ta strona nie nadaje się do Wikipedii:",
        dnuReasonPlaceholder: "Uzasadnienie zgłoszenia",
        dnuRequestTypeLabel: "Rodzaj zgłoszenia",
        dnuWikiprojectsToNotify: "Jakie wikiprojekty powiadomić?",
        dnuSpeedyNotice: "Jeżeli strona spełnia <a href='https://pl.wikipedia.org/wiki/Wikipedia:Ekspresowe_kasowanie' target='_blank'><b>zasady ekspresowego kasowania</b></a>, zamiast rozpoczynać nad nią dyskusję możesz zgłosić ją do <b>ekspresowego kasowania</b>. Aby to zrobić, wybierz opcję „Ekspresowe kasowanie” powyżej i podaj powód, a następnie kliknij przycisk „Zatwierdź”.",
        dnuLobbyMarker: "<!-- Nowe zgłoszenia wstawiaj poniżej tej linii. Nie usuwaj tej linii -->",
        dnuSubpageCreateSummary: "Nowe zgłoszenie do usunięcia (AjaxQuickDelete)",
        dnuRequestedPageEditSummary: "Zgłoszono do usunięcia (AjaxQuickDelete)",
        dnuLobbyEditSummary: "Dodano [[:$1]] (AjaxQuickDelete)",
        dnuTalkSectionTitle: "DNU: [[:$1]]",
        dnuTalkSummary: "Strona [[:$1]] została zgłoszona do usunięcia (AjaxQuickDelete)",

        ekReasonLabel: "Uzasadnienie zgłoszenia do ekspresowego kasowania:",
        ekReasonPlaceholder: "Wpisz lub wybierz z listy powód zgłoszenia",
        ekCopyvioLinkLabel: "Jeżeli treść narusza prawa autorskie, możesz podać link do oryginalnej publikacji",
        ekCopyvioLinkPlaceholder: "Wklej link do strony, z której pochodzi artykuł",
        ekSpeedyNotice: "Do ekspresowego kasowania można zgłaszać wyłącznie strony, które spełniają <a href='https://pl.wikipedia.org/wiki/Wikipedia:Ekspresowe_kasowanie' target='_blank'><b>zasady usuwania tym trybem</b></a>. W przeciwnym razie należy rozpocząć <b>dyskusję nad usunięciem</b>. Aby to zrobić, wybierz opcję „Dyskusja nad usunięciem” powyżej i podaj powód oraz rodzaj zgłoszenia, a następnie kliknij przycisk „Zatwierdź”.",
        ekPageEditSummary: "Zgłoszono do [[:Kategoria:Ekspresowe kasowanie|ekspresowego kasowania]] (AjaxQuickDelete)",

        apiFail: "Żądanie API zakończyło się błędem: $1",
        cantGetAuthor: "Nie udało się pobrać autora strony.",
        cantResolveTalkRedirects: "Nie udało się wykryć przekierowań w dyskusjach użytkowników.",
        cantCreateRequestSubpage: "Nie udało się utworzyć podstrony Poczekalni dla zgłoszenia.",
        cantPrependTemplate: "Nie udało się wstawić szablonu do artykułu. Podstrona dla zgłoszenia istnieje, ale proces należy dokończyć ręcznie. Wstaw na początek artykułu szablon $1, a następnie poinformuj autora artykułu o zgłoszeniu go do Poczekalni.",
        cantAddToLobby: "Nie udało się dołączyć zgłoszenia na głównej stronie Poczekalni. Przejdź na stronę <a href='/wiki/$1'>$1</a>, a następnie dopisz na niej kod $2.",
        cantNotifyUsers: "Nie udało się powiadomić autora o zgłoszeniu.",
        cantNotifyWikiprojects: "Nie udało się powiadomić wikiprojektów o zgłoszeniu.",

        apiAbort: "Połączenie zostało przerwane.",
        apiTimeout: "Czas oczekiwania na odpowiedź serwera upłynął.",
        apiNetworkError: "Wystąpił błąd sieci. Upewnij się, że masz połączenie z Internetem.",
        apiHttpError: "Serwer zwrócił błąd $1.",
        apiFormatError: "Serwer odpowiedział w nieprawidłowym formacie. Szczegóły błędu być może znajdują się w konsoli w narzędziach deweloperskich (zazwyczaj dostępnych po naciśnięciu F12).",
        apiPageDeleted: "Strona została usunięta.",
        apiGeneralError: "Serwer nie był w stanie przetworzyć żądania: $1"
    };

    //! List of the available deletion procedures
    // label: the displayed text
    // title: the tooltip text
    // createView: a function with no arguments that creates a view with all the controls. The return type must comply with the interface:
    //  {
    //      content: OO.ui.PanelLayout,
    //      proceed: (OO.ui.ProcessDialog) => OO.ui.Process,
    //      getSynchronizedState: () => object,
    //      applySynchronizedState: (object) => void
    //  }
    var AVAILABLE_PROCEDURES = [
        {
            label: 'Dyskusja nad usunięciem',
            title: 'Zgłoś do Poczekalni',
            createView: createViewDNU
        },
        {
            label: 'Ekspresowe kasowanie',
            title: 'Zgłoś do Ekspresowego kasowania',
            createView: createViewEK
        }
    ];

    //! List of request types available for DNU
    var REQUEST_TYPES = [
        {
            templateParam: 'artykuł',
            subpage: 'artykuły',
            label: 'artykuł (niebędący biografią)'
        },
        {
            templateParam: 'biografia',
            subpage: 'biografie',
            label: 'biografia'
        },
        {
            templateParam: 'technikalia',
            subpage: 'kwestie techniczne',
            label: 'technikalia (np. szablon, kategoria)'
        }
    ];

    //! List of presets for speedy deletion
    var SPEEDY_PRESETS = [
        'bezsens', 'brudnopis', 'dane osobowe', 'hoax', 'moje', 'nieaktualne',
        'nieency', 'npa', 'or', 'plik', 'redir', 'reklama', 'substub',
        'techniczne', 'test', 'translator', 'wandalizm', 'wygłup',
        'zniesławienie'
    ];

    //! List of special wikiprojects
    var TECH_WIKIPROJECTS = [
        {
            label: 'Ilustrowanie',
            page: 'Wikiprojekt:Ilustrowanie'
        },
        {
            label: 'Infoboksy',
            page: 'Wikiprojekt:Infoboksy'
        },
        {
            label: 'Kategoryzacja',
            page: 'Wikiprojekt:Kategoryzacja'
        },
        {
            label: 'Sprzątanie Szablonów',
            page: 'Wikiprojekt:Sprzątanie Szablonów'
        }
    ];

    /**
     * Computes the title of a subpage with the deletion request
     * @param {string} pageTitle The title of the page that is requested to be deleted
     * @param {number} requestType The index of request type in REQUEST_TYPES
     * @returns {{fullTitle: string, subpage: string, lobbyPage: string}}
     */
    function getDnuSubpageTitle(pageTitle, requestType) {
        var pad0 = function (s) { s = "" + s; return (s.length > 1 ? s : "0" + s); };  // zero-pad to two digits
        var date = new Date();

        var requestSubpage = '';
        requestSubpage += date.getUTCFullYear() + ':';
        requestSubpage += pad0(date.getUTCMonth() + 1) + ':';
        requestSubpage += pad0(date.getUTCDate()) + ':';
        requestSubpage += pageTitle;

        var lobbyPage = 'Wikipedia:Poczekalnia/';
        lobbyPage += REQUEST_TYPES[requestType].subpage;

        var requestFullTitle = lobbyPage + '/' + requestSubpage;
        return {
            fullTitle: requestFullTitle,
            subpage: requestSubpage,
            lobbyPage: lobbyPage
        };
    }

    /**
     * Downloads a list of active wikiprojects
     * @returns {jQuery.Promise} A promise resolving to the list of wikiprojects
     */
    function getWikiprojects() {
        var deferred = $.Deferred();

        // Don't download the wikiprojects everytime user visits the site
        try {
            var storage = window.localStorage;
            if(storage) {
                var fetchDate = storage.getItem('AjaxQuickDelete.wikiprojects.fetchDate');
                if(fetchDate) {
                    var now = Date.now();
                    // 7 days * 24 hrs * 60 mins * 60 secs * 1000 ms = 604 800 000
                    if(now - parseInt(fetchDate) <= 604800000) {
                        var data = storage.getItem('AjaxQuickDelete.wikiprojects.list');
                        if(data) {
                            deferred.resolve(JSON.parse(data));
                            return deferred.promise();
                        }
                    }
                }
            }
        } catch(e) {
            console.warn('[AjaxQuickDelete] Error parsing storage data', e);
        }

        $.ajax('/w/index.php?title=Wikipedia:Wikiprojekt/Spis_wikiprojektów&action=raw')
            .done(function (data) {
                if(!data) return;
                var active_wp = data.match(/=== Aktywne wikiprojekty według dziedzin wiedzy ===[\s\S]*?=== Aktywne wikiprojekty specjalne ===/)[0];
                // positive lookbehind alternative (global match) by Adam Katz → https://stackoverflow.com/a/35143111
                var regexp = /\[\[Wikiprojekt:((GLAM\/)?[^:|\]/#]+)\|/g;  // from /(?<=\[\[Wikiprojekt:)[^:|\]\/#]+(?=\|)/g
                var wikiprojects = [];
                var matcher;
                // eslint-disable-next-line no-cond-assign
                while(matcher = regexp.exec(active_wp)) {
                    wikiprojects.push({
                        label: matcher[1],
                        page: 'Wikiprojekt:' + matcher[1]
                    });
                }

                /* Sorting strings with accented characters using "Intl.Collator" or "localeCompare"
                → http://www.jstips.co/en/javascript/sorting-strings-with-accented-characters/
                localeCompare is more backwards compatible with basic support (no locale-sort) extending before Intl.Collator was introduced
                → https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
                */
                wikiprojects.sort(function (a, b) {
                    return a.label.localeCompare(b.label, 'pl');
                });

                // Add special wikiprojects
                for(var i = 0; i < TECH_WIKIPROJECTS.length; i++) {
                    wikiprojects.push(TECH_WIKIPROJECTS[i]);
                }

                // Store data in the localStorage
                try {
                    var storage = window.localStorage;
                    if(storage) {
                        storage.setItem('AjaxQuickDelete.wikiprojects.fetchDate', Date.now());
                        storage.setItem('AjaxQuickDelete.wikiprojects.list', JSON.stringify(wikiprojects));
                    }
                } catch(e) {
                    console.warn('[AjaxQuickDelete] Error saving storage data', e);
                }

                deferred.resolve(wikiprojects);
            }).fail(function (reason) {
                deferred.reject(reason);
            });

        return deferred.promise();
    }

    //! Actual start of the gadget implementation
    /** Installs the gadget (entry point) */
    function install() {
        // Can't delete special pages
        if(mw.config.get('wgNamespaceNumber') < 0) return;

        // Create a toolbox link
        var portletId = mw.config.get('skin') === 'timeless' ? 'p-pagemisc' : 'p-tb';
        var link = mw.util.addPortletLink(portletId, '#', MSG.toolboxLink, 't-ajaxquickdelete', null);
        // bind toolbox link
        $(link).click(function (e) {
            e.preventDefault();
            mw.loader.using(
                ['oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'mediawiki.api'],
                function () {
                    displayRequestDeleteDialog();
                }
            );
        });
    }

    //! Start of the dialog window definition
    function displayRequestDeleteDialog() {
        var currentView = null;

        // Initialize the dialog's class
        function RequestDeleteDialog(config) {
            RequestDeleteDialog.super.call(this, config);
        }
        OO.inheritClass(RequestDeleteDialog, OO.ui.ProcessDialog);

        RequestDeleteDialog.static.name = "RequestDeleteDialog";
        RequestDeleteDialog.static.title = MSG.dialogTitle;
        RequestDeleteDialog.static.actions = [
            {
                label: MSG.dialogCancel,
                flags: "safe",
            },
            {
                action: 'submit',
                label: MSG.dialogSubmit,
                flags: ["destructive", "primary"]
            }
        ];

        // Populate the dialog with controls
        RequestDeleteDialog.prototype.initialize = function () {
            RequestDeleteDialog.super.prototype.initialize.call(this);

            // Create layout
            var rootPanel = new OO.ui.PanelLayout({
                padded: true,
                expanded: false,
            });

            // Procedures select
            var panels = [];
            var procedureOptions = AVAILABLE_PROCEDURES.map(function (p, i) {
                var view = p.createView();
                panels.push(view.content);

                if(i == 0) currentView = view;

                return new OO.ui.ButtonOptionWidget({
                    data: {
                        index: i,
                        view: view
                    },
                    label: p.label,
                    title: p.title,
                    selected: i == 0
                });
            });
            var procedureSelect = new OO.ui.ButtonSelectWidget({
                items: procedureOptions,
                classes: ['center']
            });

            var procedureWrapper = $('<div style="margin-bottom:12px">');
            procedureWrapper.append(procedureSelect.$element);

            // The container for panels for each procedure
            var formBody = new OO.ui.StackLayout({
                expanded: false,
                items: panels
            });
            formBody.setItem(panels[0]);

            rootPanel.$element.append(
                procedureWrapper,
                formBody.$element
            );
            this.$body.append(rootPanel.$element);

            // Handle procedure changes
            procedureSelect.on('select', function (item) {
                var state = currentView.getSynchronizedState();
                currentView = item.data.view;
                formBody.setItem(currentView.content);
                currentView.applySynchronizedState(state);
            });
        };

        RequestDeleteDialog.prototype.getActionProcess = function (action) {
            var dialog = this;
            if(action === 'submit') {
                if(currentView) return currentView.proceed(dialog);
            }
            return RequestDeleteDialog.super.prototype.getActionProcess.call(this, action);
        };

        // Make the window
        var requestDeleteDialog = new RequestDeleteDialog({
            size: "large",
        });

        // Create and append a window manager
        var windowManager = new OO.ui.WindowManager();
        $(document.body).append(windowManager.$element);
        windowManager.addWindows([requestDeleteDialog]);
        windowManager.openWindow(requestDeleteDialog);
    }

    //! Define the views for different procedures of deletion
    /**
     * Creates a view for the standard deletion request
     * @returns {{content: OO.ui.PanelLayout, proceed: (dialog: OO.ui.ProcessDialog) => OO.ui.Process}} The view
     */
    function createViewDNU() {
        // Create layout
        var contentPanel = new OO.ui.PanelLayout({
            expanded: false
        });

        var fieldset = new OO.ui.FieldsetLayout({});

        // Reason field
        var reasonInput = new OO.ui.MultilineTextInputWidget({
            placeholder: MSG.dnuReasonPlaceholder,
            multiline: true,
            rows: 5,
        });

        var reasonLayout = new OO.ui.FieldLayout(reasonInput, {
            label: MSG.dnuReasonLabel,
            align: 'top'
        });

        // Type of request
        var typeOptions = REQUEST_TYPES.map(
            function (t, i) { return { data: i, label: t.label }; }
        );
        var requestTypeSelect = new OO.ui.DropdownInputWidget({
            options: typeOptions
        });

        var requestTypeLayout = new OO.ui.FieldLayout(requestTypeSelect, {
            label: MSG.dnuRequestTypeLabel
        });

        // Notify wikiprojects
        var wikiprojectsSelect = new OO.ui.MenuTagMultiselectWidget({
            inputPosition: 'inline',
            options: []
        });

        var wikiprojectsLayout = new OO.ui.FieldLayout(wikiprojectsSelect, {
            label: MSG.dnuWikiprojectsToNotify
        });

        // The wikiprojects select is populated asynchronously
        getWikiprojects().then(function (wikiprojects) {
            var wikiprojectOptions = wikiprojects.map(
                function (wikiproject) {
                    return {
                        data: wikiproject,
                        label: wikiproject.label
                    };
                }
            );
            wikiprojectsSelect.addOptions(wikiprojectOptions);
        });

        // Speedy deletion notice
        var speedyNotice = $("<p>").html(MSG.dnuSpeedyNotice);

        // Add the inputs to the fieldset
        fieldset.addItems([
            reasonLayout,
            requestTypeLayout,
            wikiprojectsLayout
        ]);

        // And finally append all the controls to the window
        contentPanel.$element.append(
            fieldset.$element,
            speedyNotice
        );
        return {
            content: contentPanel,
            proceed: function (dialog) {
                var reason = reasonInput.getValue();
                var requestType = requestTypeSelect.getValue();
                var wikiprojects = wikiprojectsSelect.getValue();

                return controlDnuRequest({
                    reason: reason,
                    requestType: requestType,
                    wikiprojects: wikiprojects,
                    dialog: dialog
                });
            },
            getSynchronizedState: function () {
                return {
                    reason: reasonInput.getValue()
                };
            },
            applySynchronizedState: function (state) {
                if(state && state.reason) reasonInput.setValue(state.reason);
            }
        };
    }

    /**
     * Creates a view for the speedy deletion request
     * @returns {{content: OO.ui.PanelLayout, proceed: (dialog: OO.ui.ProcessDialog) => OO.ui.Process}} The view
     */
    function createViewEK() {
        // Create layout
        var contentPanel = new OO.ui.PanelLayout({
            expanded: false
        });

        var fieldset = new OO.ui.FieldsetLayout({});

        // Reason field
        var reasonInput = new OO.ui.ComboBoxInputWidget({
            placeholder: MSG.ekReasonPlaceholder,
            options: SPEEDY_PRESETS.map(function (p) { return { data: p }; })
        });
        var reasonLayout = new OO.ui.FieldLayout(reasonInput, {
            label: MSG.ekReasonLabel,
            align: 'top'
        });

        // Copyvio source
        var copyvioLinkInput = new OO.ui.TextInputWidget({
            placeholder: MSG.ekCopyvioLinkPlaceholder
        });
        var copyvioLayout = new OO.ui.FieldLayout(copyvioLinkInput, {
            label: MSG.ekCopyvioLinkLabel,
            align: 'top'
        });

        // Speedy deletion notice
        var speedyNotice = $("<p>").html(MSG.ekSpeedyNotice);

        // Add the inputs to the fieldset
        fieldset.addItems([
            reasonLayout,
            copyvioLayout
        ]);

        // And finally append all the controls to the window
        contentPanel.$element.append(
            fieldset.$element,
            speedyNotice
        );
        return {
            content: contentPanel,
            proceed: function (dialog) {
                var reason = reasonInput.getValue();
                var copyvioLink = copyvioLinkInput.getValue();

                return controlEkRequest({
                    reason: reason,
                    copyvioLink: copyvioLink,
                    dialog: dialog
                });
            },
            getSynchronizedState: function () {
                return {
                    reason: reasonInput.getValue()
                };
            },
            applySynchronizedState: function (state) {
                if(state && state.reason) reasonInput.setValue(state.reason);
            }
        };
    }

    //! The actual steps that are executed during the nomination for deletion
    /**
     * Controls the flow of the DNU request
     * @param {{reason: string, requestType: number, wikiprojects: string[], dialog: OO.ui.ProcessDialog}} data Data about the request
     * @returns {{OO.ui.Process}} A process representing the activity
     */
    function controlDnuRequest(data) {
        var pageTitle = mw.config.get('wgPageName').replaceAll('_', ' ');
        var requestPage = getDnuSubpageTitle(pageTitle, data.requestType);

        // Prepare the template to be added to the article
        var templateRequestType = REQUEST_TYPES[data.requestType].templateParam;
        var pageTemplate = '{{DNU|' + templateRequestType + '|podstrona=' + requestPage.subpage + '}}';
        var talkTemplate = "{{DNUinfo|" + pageTitle + '|' + requestPage.fullTitle + "|" + templateRequestType + "}}";
        var talkMessage = talkTemplate + " ~~~~";

        // Ensure that the reason will have the signature on the end
        var reason = data.reason;
        reason = reason.replace('~~~~', '');
        reason = reason.trim() + ' ~~~~';

        // Prepare the content of the request page
        var subpageText = "=== [[:" + pageTitle + "]] ===\n";
        subpageText += ": {{lnDNU|" + pageTitle + "}}\n";
        subpageText += reason;

        var usersToNotify = [];
        var talkPages = [];
        var wikiprojectPages = data.wikiprojects.map(function (p) { return p.page; });

        // Construct the process to be invoked
        return new OO.ui.Process()
            .next(function () {
                return getCurrentPageCreator().then(function (users) { usersToNotify = users; });
            })
            .next(function () {
                return resolveTalkRedirects(usersToNotify).then(function (talk) { talkPages = talk; });
            })
            .next(function () {
                return createRequestPage(requestPage.fullTitle, subpageText);
            })
            .next(function () {
                return markArticleWithTemplate(pageTemplate, MSG.dnuRequestedPageEditSummary);
            })
            .next(function () {
                return addRequestToLobby(requestPage.lobbyPage, requestPage.fullTitle);
            })
            .next(function () {
                return notifyUsers(talkPages, talkMessage);
            })
            .next(function () {
                return notifyWikiprojects(wikiprojectPages, talkTemplate);
            })
            .next(function () {
                data.dialog.close({ action: 'submit' });
                reloadPage();
            });
    }

    /**
     * Controls the flow of the speedy deletion request
     * @param {{reason: string, copyvioLink: string, dialog: OO.ui.ProcessDialog}} data Data about the request
     * @returns {{OO.ui.Process}} A process representing the activity
     */
    function controlEkRequest(data) {
        var copyvioLink = data.copyvioLink || '';
        if(copyvioLink.length > 0) {
            copyvioLink = '|' + copyvioLink;
        }

        // Just in case, not to break the template
        var reason = data.reason.replace('|', '{{!}}').replace('=', '{{=}}');

        // Prepare the template to be added to the article
        var pageTemplate = '{{ek|' + reason + copyvioLink + '}}';

        return new OO.ui.Process()
            .next(function () {
                return markArticleWithTemplate(pageTemplate, MSG.ekPageEditSummary);
            })
            .next(function () {
                data.dialog.close({ action: 'submit' });
                reloadPage();
            });
    }

    //! Utility functions
    /**
     * Retrieves the creators of the current page.
     * 
     * @returns {jQuery.Promise<string[]>} A promise resolving to the current page creators.
     */
    function getCurrentPageCreator() {
        var deferred = $.Deferred();

        // Prepare the query parameters for the API
        var pageTitle = mw.config.get('wgPageName');
        var query = {
            formatversion: 2,
            curtimestamp: true,
            action: 'query',
            prop: 'revisions',
            rvprop: 'user',
            rvlimit: 1,
            rvdir: 'newer',
            titles: pageTitle
        };

        // Different query for File: namespace
        var NS_FILE = mw.config.get('wgNamespaceIds').file;
        if(mw.config.get('wgNamespaceNumber') === NS_FILE) {
            query = {
                formatversion: 2,
                curtimestamp: true,
                action: 'query',
                meta: 'tokens',
                prop: 'imageinfo',
                iiprop: ['user', 'sha1', 'comment'],
                iilimit: 50,
                titles: pageTitle
            };
        }

        // Invoke the API
        doAPICall(query).done(
            function (result) {
                var usersToNotify = [];

                if(mw.config.get('wgNamespaceNumber') !== NS_FILE) {
                    // For articles, notify only the original author
                    // Provided that he/she is not an anon
                    var revision = result.query.pages[0].revisions[0];
                    if(revision.anon === undefined) {
                        var creator = revision.user;
                        usersToNotify = [creator];
                    }
                } else {
                    // For files, notify each uploader but not anons
                    var info = result.query.pages[0].imageinfo;

                    for(var i = 0; i <= info.length; i++) {
                        if(info[i].anon === undefined) {
                            usersToNotify.push(info[i].user);
                        }
                    }
                }

                // Remove duplicates
                usersToNotify = usersToNotify.filter(function (item, pos, self) {
                    return self.indexOf(item) == pos;
                });

                // Don't notify the user who makes the request nor IPs
                var currentUser = mw.config.get('wgUserName');
                usersToNotify = usersToNotify.filter(function (user) { return user != currentUser; });

                // Resolve the promise with the list of users
                deferred.resolve(usersToNotify);
            }
        ).fail(function (reason) {
            deferred.reject(new OO.ui.Error(MSG.cantGetAuthor + '\n' + reason));
        });

        return deferred.promise();
    }

    /**
     * Resolves the redirects on users' talk pages
     * 
     * @param {string[]} users List of users for whom to resolve talk page redirects
     * @returns {jQuery.Promise<string[]>} A promise resolving to the list of talk pages
     */
    function resolveTalkRedirects(users) {
        var deferred = $.Deferred();

        // Add the 'User talk:' (NS #3) prefix to the user names
        var userTalk = mw.config.get('wgFormattedNamespaces')[3] + ':';
        var talkPages = users.map(function (user) {
            return userTalk + user;
        });

        // Prepare the query
        var query = {
            action: 'query',
            redirects: '',
            titles: talkPages.join('|')
        };

        // Invoke the API
        doAPICall(query).done(
            function (result) {
                // If there are no redirects, do nothing
                if(!result.query || !result.query.redirects) {
                    return deferred.resolve(talkPages);
                }

                var redirs = result.query.redirects;

                // First, get the list of redirected pages
                var redirSources = [];
                redirs.forEach(function (redir) {
                    redirSources.push(redir.from);
                });

                // Remove the redirected pages from the list
                talkPages = talkPages.filter(function (page) {
                    return redirSources.indexOf(page) !== -1;
                });

                // Lastly, add the targets of redirects to the list
                redirs.forEach(function (redir) {
                    talkPages.push(redir.to);
                });

                deferred.resolve(talkPages);
            }
        ).fail(function (reason) {
            deferred.reject(new OO.ui.Error(MSG.cantResolveTalkRedirects + '\n' + reason));
        });

        return deferred.promise();
    }

    /**
     * Creates the page with the request
     * 
     * Expects a context to be bound as `this`.
     * The deletion reason has to be passed as `reason: string` in the context.
     * The name of the request page has to be passed as `requestPageTitle: string` in the context.
     * 
     * @param {string} requestPageTitle The title of the page to be created
     * @param {string} subpageText The content to be put on the new page
     * @returns {jQuery.Promise<void>} A promise representing the API operation.
     */
    function createRequestPage(requestPageTitle, subpageText) {
        var deferred = $.Deferred();

        var watchlist = 'watch';
        if(window.AjaxDeleteDontWatchRequests) watchlist = 'nowatch';

        // Try to create the request subpage
        var api = new mw.Api();
        api.create(
            requestPageTitle,
            {
                summary: MSG.dnuSubpageCreateSummary,
                watchlist: watchlist,
                errorformat: 'html',
                errorlang: mw.config.get('wgUserLanguage'),
                errorsuselocal: true
            },
            subpageText
        ).then(function () {
            deferred.resolve();
        }).fail(function (code, result) {
            var errorMessage = processApiError(code, result);
            return deferred.reject(new OO.ui.Error(MSG.cantCreateRequestSubpage + '\n' + errorMessage));
        });
        return deferred.promise();
    }

    /**
     * Prepends the template to the current page. If the current page is
     * in the Template: namespace, the `template` parameter is automatically
     * wrapped into noinclude tags.
     * 
     * @param {string} template The template to be added
     * @param {string} editSummary The edit summary on the article page
     * @returns {jQuery.Promise<void>} A promise representing the API operation.
     */
    function markArticleWithTemplate(template, editSummary) {
        var deferred = $.Deferred();

        var pageTitle = mw.config.get('wgPageName');
        var pageNamespace = mw.config.get('wgNamespaceNumber');
        var NS_TEMPLATE = mw.config.get('wgNamespaceIds').template;

        if(pageNamespace == NS_TEMPLATE) {
            template = '<noinclude>' + template + '</noinclude>';
        }

        // Prepare parameters
        var params = {
            action: 'edit',
            title: pageTitle,
            prependtext: template,
            summary: editSummary,
            watchlist: 'nochange',
            nocreate: true,
            errorformat: 'html',
            errorlang: mw.config.get('wgUserLanguage'),
            errorsuselocal: true
        };
        var api = new mw.Api();

        if(window.AjaxDeleteWatchReportedPage) params.watchlist = 'watch';

        // Perform an API request to edit the page
        // and resolve the promise on success.
        api.postWithEditToken(params).done(function () {
            deferred.resolve();
        }).fail(function (code, result) {
            var errorMessage = processApiError(code, result);
            return deferred.reject(new OO.ui.Error(
                MSG.cantPrependTemplate.replace('$1', template) + '\n' + errorMessage,
                { recoverable: false }
            ));
        });

        return deferred.promise();
    }

    /**
     * Embeds the deletion request subpage onto the lobby.
     * 
     * @param {string} lobbyPage The lobby page, where to embed the request
     * @param {string} requestPageTitle Full title of the subpage with the request
     * @returns {jQuery.Promise<void>} A promise representing the API operation.
     */
    function addRequestToLobby(lobbyPage, requestPageTitle) {
        var deferred = $.Deferred();

        var appendedText = '{{' + requestPageTitle + '}}';
        appendTextToMarker(
            lobbyPage,
            MSG.dnuLobbyMarker,
            appendedText,
            MSG.dnuLobbyEditSummary.replace('$1', requestPageTitle)
        ).then(function () {
            deferred.resolve();
        }).fail(function (reason) {
            deferred.reject(new OO.ui.Error(
                MSG.cantAddToLobby
                    .replace('$1', lobbyPage)
                    .replace('$2', '{{' + requestPageTitle + '}}')
                + '\n' + reason,
                { recoverable: false }
            ));
        });

        return deferred.promise();
    }

    /**
     * Leaves a message on the users' talk pages
     * 
     * @param {string[]} talkPages The pages to leave the message on
     * @param {string} talkMessage The message to leave on the every talk page
     * @returns {jQuery.Promise<void>} A promise representing the operation.
     */
    function notifyUsers(talkPages, talkMessage) {
        var deferred = $.Deferred();

        var pageTitle = mw.config.get('wgPageName').replaceAll('_', ' ');
        var sectionTitle = MSG.dnuTalkSectionTitle.replace('$1', pageTitle);
        var editSummary = MSG.dnuTalkSummary.replace('$1', pageTitle);

        // Store each of the returned promises in an array and wait for all of them in the end
        var promises = [];
        for(var i = 0; i < talkPages.length; i++) {
            var talkPage = talkPages[i];
            promises.push(
                appendSection(
                    talkPage,
                    sectionTitle,
                    talkMessage,
                    editSummary,
                    window.AjaxDeleteWatchUserTalk ? 'watch' : 'nochange'
                ));
        }

        // Resolve our promise only after all edits to the talk pages are finished
        $.when.apply($, promises).then(function () {
            deferred.resolve();
        }).fail(function (reason) {
            deferred.reject(new OO.ui.Error(
                MSG.cantNotifyUsers + '\n' + reason,
                { recoverable: false }
            ));
        });

        return deferred.promise();
    }

    /**
     * Adds a notification to the wikiproject pages
     * 
     * @param {string[]} wikiprojects List of the wikiproject pages to leave the message on
     * @param {string} message The message to leave
     * @returns {jQuery.Promise<void>} A promise representing the API operation.
     */
    function notifyWikiprojects(wikiprojects, message) {
        var deferred = $.Deferred();

        var pageTitle = mw.config.get('wgPageName').replaceAll('_', ' ');
        var editSummary = MSG.dnuTalkSummary.replace('$1', pageTitle);

        // Notify the wikiprojects asynchronously, at the same time
        var promises = [];
        for(var i = 0; i < wikiprojects.length; i++) {
            promises.push(appendTextToMarker(
                wikiprojects[i],
                MSG.dnuLobbyMarker,
                message,
                editSummary
            ));
        }

        // Resolve our promise only after all edits are finished
        $.when.apply($, promises).then(function () {
            deferred.resolve();
        }).fail(function (reason) {
            deferred.reject(new OO.ui.Error(
                MSG.cantNotifyWikiprojects + '\n' + reason,
                { recoverable: false }
            ));
        });

        return deferred.promise();
    }

    /**
     * Adds a new section to the page
     * 
     * @param {string} pageTitle The page to edit
     * @param {string} sectionTitle Title of the new section
     * @param {string} sectionText The content of the new section
     * @param {string} editSummary Summary of the edit
     * @param {string} watchlist Whether to watch the page
     * @returns {jQuery.Promise}
     */
    function appendSection(pageTitle, sectionTitle, sectionText, editSummary, watchlist) {
        var deferred = $.Deferred();

        watchlist = watchlist || 'nochange';

        // Prepare parameters
        var params = {
            action: 'edit',
            title: pageTitle,
            sectiontitle: sectionTitle,
            text: sectionText,
            section: 'new',
            summary: editSummary,
            watchlist: watchlist,
            errorformat: 'html',
            errorlang: mw.config.get('wgUserLanguage'),
            errorsuselocal: true
        };
        var api = new mw.Api();

        // Perform an API request to edit the page
        // and resolve the promise on success.
        api.postWithToken('csrf', params).done(function () {
            deferred.resolve();
        }).fail(function (code, result) {
            var errorMessage = processApiError(code, result);
            return deferred.reject(
                MSG.apiFail.replace('$1', errorMessage)
            );
        });

        return deferred.promise();
    }

    /**
     * Finds a specific marker on the page and appends to it
     * 
     * @param {string} pageTitle The page to edit
     * @param {string} marker The marker to be found
     * @param {string} textToAppend String that will be inserted just after the marker
     * @param {string} editSummary Summary of the edit
     * @returns {jQuery.Promise}
     */
    function appendTextToMarker(pageTitle, marker, textToAppend, editSummary) {
        var deferred = $.Deferred();

        var api = new mw.Api();
        api.edit(pageTitle, function (revision) {
            // Replace the marker with itself + the new text
            var newContent = revision.content.replace(
                marker,
                marker + '\n' + textToAppend
            );

            // Check if the marker was found; if not, append the text to the end
            if(newContent == revision.content) newContent += '\n' + textToAppend;

            return {
                text: newContent,
                summary: editSummary,
                errorformat: 'html',
                errorlang: mw.config.get('wgUserLanguage'),
                errorsuselocal: true
            };
        }).then(function () {
            deferred.resolve();
        }).fail(function (code, result) {
            var errorMessage = processApiError(code, result);
            return deferred.reject(
                MSG.apiFail.replace('$1', errorMessage)
            );
        });

        return deferred.promise();
    }


    /**
     * Does a MediaWiki API request.
     *
     * Uses POST requests for everything for simplicity.
     *
     * @param {object} params Query parameters.
     * @returns {jQuery.Promise} Promise resolving to the API response
     */
    function doAPICall(params) {
        var deferred = $.Deferred();

        params.errorformat = 'html';
        params.errorlang = mw.config.get('wgUserLanguage');
        params.errorsuselocal = true;

        var api = new mw.Api();
        api.post(params).done(function (result, jqXHR) {
            deferred.resolve(result);
        }).fail(function (code, result) {
            var errorMessage = processApiError(code, result);
            return deferred.reject(
                MSG.apiFail.replace('$1', errorMessage)
            );
        });

        return deferred.promise();
    }

    /**
     * Processes the data obtained from the API in order to prepare the error message
     * 
     * @param {string} code The error code returned by the API
     * @param {object} result The data about the error
     * @returns {string} The error message
     */
    function processApiError(code, result) {
        switch(code) {
            case 'missingtitle':
                return MSG.apiPageDeleted;
            case 'http':
                switch(result.textStatus) {
                    case 'abort':
                        return MSG.apiAbort;
                    case 'timeout':
                        return MSG.apiTimeout;
                    case 'error':
                        if(result.exception === '') {
                            return MSG.apiNetworkError;
                        } else {
                            // Doesn't occur for HTTP/2
                            return MSG.apiHttpError.replace('$1', result.exception);
                        }
                    case 'parseerror':
                        console.log(result.exception);
                        return MSG.apiFormatError;
                }
        }
        // unknown, generic
        console.error(result);
        var resultInfo = new mw.Api().getErrorMessage(result);
        resultInfo = resultInfo.text();

        return MSG.apiGeneralError.replace('$1', resultInfo);
    }

    function reloadPage() {
        var pageTitle = mw.config.get('wgPageName');
        var title = encodeURIComponent(pageTitle).
            replace(/%3A/g, ':').replace(/%20/g, '_').
            replace(/\(/g, '%28').replace(/\)/g, '%29').
            replace(/%2F/g, '/');

        location.href = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace("$1", title);
    }

    // Install the gadget
    install();
})();
// </nowiki>