Wikipedysta:Msz2001/AjaxQuickDelete.js: Różnice pomiędzy wersjami

Z Wikipedii, wolnej encyklopedii
Usunięta treść Dodana treść
m Poprawiam zamianę _ na spacje w tytule
m Jeszcze nowa linia w awaryjnym przypadku
(Nie pokazano 1 pośredniej wersji utworzonej przez tego samego użytkownika)
Linia 852: Linia 852:
var deferred = $.Deferred();
var deferred = $.Deferred();


var appendedText = '\n{{' + requestPageTitle + '}}';
var appendedText = '{{' + requestPageTitle + '}}';
appendTextToMarker(
appendTextToMarker(
lobbyPage,
lobbyPage,
Linia 1012: Linia 1012:
var newContent = revision.content.replace(
var newContent = revision.content.replace(
marker,
marker,
marker + textToAppend
marker + '\n' + textToAppend
);
);


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


return {
return {

Wersja z 12:01, 28 lut 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 = [
        'test', 'substub', 'nieency', 'hoax', 'or', 'reklama', 'bezsens',
        'wandalizm', 'dane osobowe', 'zniesławienie', 'techniczne',
        'translator', 'redir', 'wygłup', 'npa', 'moje', 'nieaktualne',
        'plik', 'brudnopis'
    ];

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