MediaWiki:Gadget-move-to-sandbox.js
Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.
- Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
- Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
- Internet Explorer / Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5
- Opera: Naciśnij klawisze Ctrl+F5.
/* global mw, jQuery, moveToSandboxGadget, OO */
/**
* @author [[w:pl:User:Beau]] (original code)
* @author [[w:pl:User:Wargo]]
* @author [[w:pl:User:Nux]]
* @author [[w:pl:User:Msz2001]] (rewrite for OOUI)
*
* Full list of authors:
* https://pl.wikipedia.org/w/index.php?title=MediaWiki:Gadget-move-to-sandbox.js&action=history
*
* <nowiki>
*
** Interface:
* window.moveToSandboxGadget = {
* initialReason: string | null,
* moveSource: string | null,
* moveDestination: string | null,
* open: (callback: (success: bool) => void, discussion: string) => void,
* version: string
* };
*
** Important: If setting both moveSource and moveDestination, moveSource must be set before moveDestination.
** Otherwise, the moveDestination will be overwritten by the default target title.
*
* null title means the default one, but not computed yet
*
* Setting `initialReason` to null will restore the default move reason
*
* @typedef {{targetPage: string, editSummary: string, message: string, defaultMessage: string, isMessageDisabled: boolean}} FormResult
* @typedef {{_valid: boolean, _errors: string[], targetPage: boolean, editSummary: boolean, message: boolean}} FormValidation
* @typedef {{pageId: number, author: {name: string, isAnon: boolean}}} PageMetadata
*/
(function ($, mw) {
var GADGET_VERSION = '9.0';
// An edit tag to use ([[Special:Tags]])
var EDIT_TAG = 'przeniesienie do brudnopisu';
//! Translatable content
var MSG = {
toolboxLink: 'Przenieś do brudnopisu',
toolboxLinkTooltip: 'Przenieś tę stronę do brudnopisu autora',
dialogTitle: 'Przenieś do brudnopisu',
dialogCancel: 'Anuluj',
dialogSubmit: 'Przenieś',
loading: 'Ładowanie...',
speedyTemplate: '{{ek|1=strona została przeniesiona do [[$1|brudnopisu autora]]}}\n',
speedySummary: 'Oznaczono przekierowanie po przenosinach jako do [[:Kategoria:Ekspresowe kasowanie|skasowania]]',
removeCategoriesSummary: 'Usunięto kategorie z brudnopisu.',
removeTemplatesSummary: 'Usunięto szablony z brudnopisu: $1.',
talkMessageSubject: '[[$2]]', // $1 = permanent link to the page (Special:Redirect), $2 = original page title
guestSandbox: 'Wikipedysta:Brudnopis gościnny/$1',
userMessageBody: 'Witaj. Twój artykuł nie nadaje się jeszcze do publikacji w Wikipedii, dlatego został przeniesiony do twojego \'\'\'[[$link|brudnopisu]]\'\'\', gdzie możesz nad nim popracować. Popraw w nim:\n* ...\n* ...\n* ...\n* ...\n\nW razie problemów skorzystaj z [[Pomoc:Jak napisać nowy artykuł|tego poradnika]], zadaj pytanie na [[Pomoc:Pytania nowicjuszy|tej stronie]] lub spytaj [[Pomoc:Przewodnicy|przewodników]].\n\nPo skończeniu użyj [[Pomoc:Zmiana nazwy strony|zakładki „Przenieś”]], aby ponownie opublikować artykuł. Jeżeli nie masz takiej zakładki (należy mieć konto zarejestrowane od co najmniej 4 dni oraz 10 edycji), napisz na [[Pomoc:Pytania nowicjuszy|tej stronie]] lub zwróć się do [[Pomoc:Przewodnicy|przewodników]].',
userMessageAnonFooter: '\n\nZapisz sobie podany adres strony, aby nie mieć problemu z powrotem do niej: \'\'\'$link\'\'\'',
userMessageSignature: '\n\nPozdrawiam, ~~~~',
authorFieldLabel: 'Autor artykułu',
authorFieldUnknown: 'nieznany',
authorCannotFetch: '<span style="color:#a00">Nie udało się załadować autora. $1</span>',
authorFieldText: '<span><a class="mw-userlink" href="/wiki/User:$1" target="_blank">$1</a> (<a href="/wiki/User_talk:$1" target="_blank">dyskusja</a>, <a href="/wiki/Special:Contributions/$1" target="_blank">wkład</a>)</span>',
targetFieldLabel: 'Docelowa nazwa artykułu',
targetFieldPlaceholder: 'np. Wikipedysta:Ktoś/brudnopis',
summaryFieldLabel: 'Krótkie uzasadnienie',
summaryFieldPlaceholder: 'Ten tekst zostanie użyty jako opis zmian',
summaryFieldDefault: 'Artykuł należy dopracować',
summaryFieldHelp: 'Zawartość tego pola będzie widoczna jako opis edycji w rejestrze przeniesień i historii strony.',
messageFieldLabel: 'Wiadomość dla użytkownika',
messageFieldPlaceholder: 'Tu wpisz komunikat, który zostanie umieszczony na stronie dyskusji użytkownika',
messageFieldHelp: 'Możesz używać znaczników wikikodu. Tekst „$link” zostanie zamieniony na nazwę strony, gdzie przeniesiono artykuł.',
errorNoMoveRight: 'Nie masz uprawnień do przenoszenia stron.',
errorFormInvalid: 'Formularz nie został wypełniony poprawnie: <ul>$1</ul>',
errorTargetEmpty: 'Nazwa strony docelowej nie może być pusta.',
errorSummaryEmpty: 'Uzasadnienie nie może być puste.',
errorMessageDefault: 'Uzupełnij wiadomość dla użytkownika, wskazując, co należy poprawić.',
// $1 is the actual error message
errorMoveFailed: 'Nie udało się przenieść strony do brudnopisu. $1',
errorMarkToDeleteFailed: 'Strona została przeniesiona, ale nie udało się oznaczyć przekierowania jako do skasowania. $1',
errorRemoveCategoriesFailed: 'Strona została przeniesiona, ale nie udało się usunąć kategorii z brudnopisu. $1',
errorTalkMessageFailed: 'Strona została przeniesiona, ale nie udało się wysłać wiadomości do użytkownika. $1',
apiFail: 'Żądanie API zakończyło się błędem:\n$1',
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'
};
// These will be removed from the page content after moving to sandbox
var TEMPLATES_TO_REMOVE = ['EK', 'DNU'];
// The common reasons for moving a page to a sandbox
var QUICK_REASONS = [
{
label: 'Przypisy',
tooltip: 'Brakuje przypisów',
insertedMessage: 'Dodaj [[Wikipedia:Weryfikowalność|źródła]] w formie [[Pomoc:Przypisy|przypisów]] do wszystkich informacji.'
},
{
label: 'Formatowanie',
tooltip: 'Formatowanie wymaga poprawy',
insertedMessage: 'Popraw [[Pomoc:Formatowanie tekstu|formatowanie tekstu]].'
},
{
label: 'Kategorie',
tooltip: 'Brakuje kategorii',
insertedMessage: 'Dodaj [[Pomoc:Kategorie|kategorie]].'
},
{
label: 'Linki',
tooltip: 'Za mało linków wewnętrznych',
insertedMessage: 'Dodaj [[Wikipedia:Linki|linki do innych artykułów]].'
},
{
label: 'ENCY',
tooltip: 'Nie wykazano encyklopedyczności',
insertedMessage: 'Wykaż [[WP:ENCY|encyklopedyczność]] tematu.'
},
{
label: 'Tłumacz',
tooltip: 'Użyto autotranslatora',
insertedMessage: 'Popraw tłumaczenie maszynowe.'
},
{
label: 'Rozbuduj',
tooltip: 'Artykuł jest za krótki',
insertedMessage: '[[Pomoc:Jak napisać nowy artykuł|Rozbuduj]] artykuł.'
},
{
label: 'Styl',
tooltip: 'Należy poprawić styl tekstu',
insertedMessage: '[[Pomoc:Styl – poradnik dla autorów|Popraw styl]] artykułu, tak żeby pasował do encyklopedii.'
},
{
label: 'NPOV',
tooltip: 'Artykuł nie jest napisany w sposób neutralny',
insertedMessage: 'Opisz zagadnienie w sposób [[WP:NPOV|neutralny]].'
}
];
/**
* If there are any additional messages specified, they will go here
* This is a map so that no message will be duplicated
* @type {{[key: string]: {label: string, tooltip: string, insertedMessage: string}}}
*/
var additionalReasons = {};
/**
* Makes the target title, based on the author's name and the current page title
* @param {string | null} author The author's name. Null when the autor cannot be determined
* @param {bool} isAuthorAnon Whether the author is anonymous
* @param {string} title The current page title
* @returns {string} The target title
*/
function makeTargetTitle(author, isAuthorAnon, originalTitle) {
// Use the guests' sandbox if the author is anonymous or unknown
if(!author || isAuthorAnon) return MSG.guestSandbox.replace(/\$1/g, originalTitle);
return 'Wikipedysta:' + author + '/' + originalTitle;
}
//! Start of the proper gadget code
var sourcePage = null;
var targetPage = null;
var sourcePageAuthor = '';
var sourcePageAuthorIsAnon = false;
var sourcePageId = null;
var api = null;
var moveRight = null;
var suppressRedirectRight = null;
var cachedState = null;
var initialReason = MSG.summaryFieldDefault;
/**
* Array of functions that get called when the author is fetched.
* If there is any error, the message is passed as an argument.
* @type {((error: string | undefined) => void)[]}
*/
var onAuthorLoadedHandlers = [];
/**
* Array of functions that get called when the author's rights are fetched.
* @type {(() => void)[]}
*/
var onRightsLoadedHandlers = [];
/** Installs the gadget (entry point) */
function install() {
// The interface is available everywhere (i.e. for the delReqHandler gadget)
buildPublicInterface();
// It's intended to be invoked manually only on article 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, 'move-to-sandbox', MSG.toolboxLinkTooltip);
// bind toolbox link
$(link).on('click', function (e) {
e.preventDefault();
loadDependenciesAndDisplay().then(function () {
// The page is reloaded only if the dialog was invoked directly
// (i.e. not from the delReqHandler gadget)
location.reload();
});
});
}
/** Builds and exposes an interface that may be used by other gadgets */
function buildPublicInterface() {
window.moveToSandboxGadget = {
version: GADGET_VERSION,
open: function (callback, discussion) {
// For safety
callback = callback || function () {};
if(discussion !== undefined){
// Mimick the legacy behavior
additionalReasons['delReqHandler-discussion'] = {
label: 'Dyskusja',
tooltip: 'Link do dyskusji w DNU',
insertedMessage: discussion
};
}else{
additionalReasons['delReqHandler-discussion'] = undefined;
}
loadDependenciesAndDisplay()
.then(function () { callback(true); })
.fail(function () { callback(false); });
}
};
// Changing the source title will invalidate any target title set before.
// This is done in order to ensure that the target title is always up-to-date.
Object.defineProperty(window.moveToSandboxGadget, 'moveSource', {
get: function () { return sourcePage; },
set: function (value) { sourcePage = value; targetPage = null; }
});
Object.defineProperty(window.moveToSandboxGadget, 'moveDestination', {
get: function () { return targetPage; },
set: function (value) { targetPage = value; }
});
// The initial reason can be set to null, which means the default one
Object.defineProperty(window.moveToSandboxGadget, 'initialReason', {
get: function() { return initialReason; },
set: function(value) { initialReason = (value !== null ? value : MSG.summaryFieldDefault); }
});
}
/**
* Ensures that all the dependencies are present and displays the dialog
* @returns {jQuery.Promise<void>} A promise that's resolved when the move process finishes successfully. It's rejected when the user quits the dialog.
*/
function loadDependenciesAndDisplay() {
var deferred = $.Deferred();
mw.loader.using(
['oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'mediawiki.api'],
function () {
// Only now we are certain that API exists
createApiClient();
// Ensure that both the source and destination are set.
// But do not block the dialog from displaying, since
// the data is in fact needed after users fills in the form.
computeAuthorAndPageTitles();
onRightsLoadedHandlers = [];
getCurrentUserRights().then(function (rights) {
moveRight = rights.indexOf('move') !== -1;
suppressRedirectRight = rights.indexOf('suppressredirect') !== -1;
onRightsLoadedHandlers.forEach(function (handler) { handler(); });
if(!moveRight) {
deferred.reject();
}
});
displaySandboxDialog()
.then(deferred.resolve)
.fail(deferred.reject);
}
);
return deferred.promise();
}
/** Creates the API client and sets some default options */
function createApiClient() {
api = new mw.Api({
parameters: {
format: 'json',
formatversion: 2,
errorformat: 'html',
errorlang: mw.config.get('wgUserLanguage'),
errorsuselocal: true,
tags: EDIT_TAG
}
});
}
/**
* Initiate loading of the author's name and then prepare titles of source and
* target pages (if not already set).
*/
function computeAuthorAndPageTitles() {
// Ensure that there are no leftover handlers
onAuthorLoadedHandlers = [];
// If there's no custom source, use the current page
if(!sourcePage) sourcePage = mw.config.get('wgPageName').replace(/_/g, ' ');
// Get metadata and compute target title
getArticleMetadata(sourcePage).done(function (metadata) {
var author = metadata.author;
sourcePageAuthor = author.name;
sourcePageAuthorIsAnon = author.isAnon;
sourcePageId = metadata.pageId;
if(!targetPage) targetPage = makeTargetTitle(author.name, author.isAnon, sourcePage);
onAuthorLoadedHandlers.forEach(function (handler) { handler(undefined); });
}).fail(function (reason) {
sourcePageAuthor = null;
sourcePageAuthorIsAnon = true;
sourcePageId = null;
// Prepare target title as if the user was unknown
if(!targetPage) targetPage = makeTargetTitle(null, true, sourcePage);
reason = MSG.authorCannotFetch.replace(/\$1/g, reason);
onAuthorLoadedHandlers.forEach(function (handler) { handler(reason); });
});
}
//! Start of the dialog window definition
/**
* Displays the dialog
* @returns {jQuery.Promise<void>} A promise that is resolved when the page is moved. Fails if the user quits the dialog.
*/
function displaySandboxDialog() {
var deferred = $.Deferred();
// Initialize the dialog's class
function MoveToSandboxDialog(config) {
MoveToSandboxDialog.super.call(this, config);
}
OO.inheritClass(MoveToSandboxDialog, OO.ui.ProcessDialog);
MoveToSandboxDialog.static.name = 'MoveToSandboxDialog';
MoveToSandboxDialog.static.title = MSG.dialogTitle;
MoveToSandboxDialog.static.actions = [
{
action: 'cancel',
label: MSG.dialogCancel,
flags: 'safe',
},
{
action: 'submit',
label: MSG.dialogSubmit,
flags: ['primary', 'progressive']
}
];
var view = createDialogView();
if(cachedState) view.setValues(cachedState);
// Populate the dialog with controls
MoveToSandboxDialog.prototype.initialize = function () {
MoveToSandboxDialog.super.prototype.initialize.call(this);
// Create layout
this.$body.append(view.panel.$element);
};
MoveToSandboxDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if(action == 'submit') {
var responses = view.getValues();
normalizeInput(responses);
var validity = validateInput(responses);
view.setValidity(validity);
dialog.updateSize();
// If the input is invalid, return a process that does nothing.
if(!validity._valid) {
return new OO.ui.Process(function () { });
}
return moveToSandbox(
sourcePage,
responses.targetPage,
responses.editSummary,
responses.message,
sourcePageAuthor
).next(function () {
targetPage = responses.targetPage;
cachedState = undefined;
deferred.resolve();
dialog.close({ action: action });
});
} else if(action == 'cancel') {
cachedState = view.getValues();
deferred.reject();
dialog.close({ action: action });
}
return MoveToSandboxDialog.super.prototype.getActionProcess.call(this, action);
};
// Make the window
var sandboxDialog = new MoveToSandboxDialog({
size: 'large',
});
// Create and append a window manager
var windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
windowManager.addWindows([sandboxDialog]);
windowManager.openWindow(sandboxDialog);
// In case of very long author name (or IPv6), there may be a vertical overflow, so update the size
onAuthorLoadedHandlers.push(function () {
try {
sandboxDialog.updateSize();
} catch(e) {
// If the user closes dialog before fetching author, the exception is thrown here
}
});
onRightsLoadedHandlers.push(function () {
if(!moveRight) {
OO.ui.alert(MSG.errorNoMoveRight);
sandboxDialog.close();
deferred.reject();
}
});
return deferred.promise();
}
/**
* Creates the dialog view
* @returns {{panel: OO.ui.PanelLayout, getValues: () => FormResult, setValues: (v: FormResult) => void, setValidity: (v: FormValidation) => void}}
*/
function createDialogView() {
var rootPanel = new OO.ui.PanelLayout({
padded: true,
expanded: false,
});
// Here will be displayed any errors
var errorContainer = $('<div class="mw-gadget-mts-errorbox"></div>').hide();
rootPanel.$element.append(errorContainer);
var fieldset = new OO.ui.FieldsetLayout({});
rootPanel.$element.append(fieldset.$element);
// The "Author" field
var authorLabel = new OO.ui.LabelWidget({
label: sourcePageAuthor
? $(MSG.authorFieldText.replace(/\$1/g, sourcePageAuthor))
: MSG.loading
});
var authorField = new OO.ui.FieldLayout(authorLabel, {
label: MSG.authorFieldLabel,
align: 'left'
});
// The "Target page" field
var targetInput = new OO.ui.TextInputWidget({
value: targetPage || '', // Will be filled in later
placeholder: MSG.targetFieldPlaceholder
});
var targetField = new OO.ui.FieldLayout(targetInput, {
label: MSG.targetFieldLabel,
align: 'left'
});
// The "Edit summary" field
var summaryInput = new OO.ui.TextInputWidget({
placeholder: MSG.summaryFieldPlaceholder,
value: initialReason
});
var summaryField = new OO.ui.FieldLayout(summaryInput, {
label: MSG.summaryFieldLabel,
help: MSG.summaryFieldHelp,
align: 'left'
});
// The "Message" field
var defaultMessage = MSG.userMessageBody + MSG.userMessageSignature;
var messageInput = new OO.ui.MultilineTextInputWidget({
placeholder: MSG.messageFieldPlaceholder,
value: defaultMessage,
multiline: true,
rows: 12
});
var messageField = new OO.ui.FieldLayout(messageInput, {
label: MSG.messageFieldLabel,
helpInline: true,
help: MSG.messageFieldHelp,
align: 'top'
});
onAuthorLoadedHandlers.push(function (error) {
authorLabel.setLabel(
sourcePageAuthor
? $(MSG.authorFieldText.replace(/\$1/g, sourcePageAuthor))
: ($(error) || MSG.authorFieldUnknown)
);
if(targetInput.getValue() === '') targetInput.setValue(targetPage);
if(sourcePageAuthorIsAnon && messageInput.getValue() == defaultMessage) {
defaultMessage = MSG.userMessageBody + MSG.userMessageAnonFooter + MSG.userMessageSignature;
messageInput.setValue(defaultMessage);
}
messageInput.setDisabled(error !== undefined);
});
// Display buttons for quick insertion of common messages
var quickReasonsWrapper = $('<div></div>');
rootPanel.$element.append(quickReasonsWrapper);
var reasons = QUICK_REASONS.concat(Object.values(additionalReasons));
reasons.forEach(function (reason) {
if(!reason) return;
var btn = new OO.ui.ButtonWidget({
label: reason.label,
title: reason.tooltip,
flags: 'progressive',
framed: false
});
btn.on('click', function () {
if(messageInput.disabled) return;
// Obtains the current selection and sets it back.
// Without the following two lines, the textbox is scrolled to the end when inserting content.
// Very hacky, but it works. ¯\_(ツ)_/¯
var range = messageInput.getRange();
messageInput.selectRange(range.from, range.to);
messageInput.insertContent(reason.insertedMessage);
});
quickReasonsWrapper.append(btn.$element);
});
// Insert the fields into the fieldset
fieldset.addItems([
authorField,
targetField,
summaryField,
messageField
]);
return {
panel: rootPanel,
getValues: function () {
return {
targetPage: targetInput.getValue(),
editSummary: summaryInput.getValue(),
message: messageInput.getValue(),
defaultMessage: defaultMessage,
isMessageDisabled: messageInput.disabled
};
},
setValues: function (values) {
targetInput.setValue(values.targetPage);
summaryInput.setValue(values.editSummary);
messageInput.setValue(values.message);
messageInput.setDisabled(values.isMessageDisabled);
},
setValidity: function (validationResult) {
targetInput.setValidityFlag(validationResult.targetPage);
summaryInput.setValidityFlag(validationResult.editSummary);
messageInput.setValidityFlag(validationResult.message);
if(!validationResult._valid) {
var errorsList = validationResult._errors.map(function (x) { return '<li>' + x + '</li>'; });
errorContainer.html(
MSG.errorFormInvalid.replace(/\$1/g, errorsList.join(''))
).show();
} else {
errorContainer.html('').hide();
}
}
};
}
//! Prepare the input for the actual move
/**
* Removes empty bullet points from the message
* @param {string} message The string to manipulate
* @returns {string} The same string but without empty bullet points (i.e. "* ...")
*/
function removeEmptyBulletPoints(message) {
return message.replace(/^\* \.\.\.\n/gm, '');
}
/**
* Normalizes the data, i.e. removes leading and trailing whitespace
* @param {FormResult} data The data retrieved from the form
*/
function normalizeInput(data) {
data.targetPage = data.targetPage.trim();
data.editSummary = data.editSummary.trim();
data.message = removeEmptyBulletPoints(data.message.trim());
}
/**
* Checks whether all the fields were filled in correctly
* @param {FormResult} data The data retrieved from the form
* @returns {FormValidation} Whether the specific fields are valid
*/
function validateInput(data) {
var result = {
_valid: true,
_errors: [],
targetPage: true,
editSummary: true,
message: true
};
// Target page cannot be empty
if(!data.targetPage) {
result.targetPage = false;
result._errors.push(MSG.errorTargetEmpty);
}
// Edit summary cannot be empty
if(!data.editSummary) {
result.editSummary = false;
result._errors.push(MSG.errorSummaryEmpty);
}
// Message can be empty but the user should edit the default message
if(data.message == removeEmptyBulletPoints(data.defaultMessage)) {
result.message = false;
result._errors.push(MSG.errorMessageDefault);
}
// Check if there are any problems
Object.values(result).forEach(function (value) {
if(value === false) result._valid = false;
});
return result;
}
//! Do the actual move
/**
* Does all the steps in process of moving to sandbox
* @param {string} sourceTitle The title of the page to move
* @param {string} targetTitle The title of the page to move to
* @param {string} summary The edit summary
* @param {string} message The message to post on the talk page
* @param {string} author User who should be messaged
* @returns {OO.ui.Process} The process representing the steps
*/
function moveToSandbox(sourceTitle, targetTitle, summary, message, author) {
var leaveRedirect = !suppressRedirectRight;
// We use the page ID instead of the title to make sure that user
// can navigate to the page no matter how many times it was moved.
var permalink = 'Special:Redirect/page/' + sourcePageId;
if(!sourcePageId) {
permalink = targetTitle;
}
var messageSubject = MSG.talkMessageSubject.replace(/\$2/g, sourceTitle);
messageSubject = messageSubject.replace(/\$1/g, permalink);
// Use target title directly, because the text calls it "sandbox" (not to confuse user after fixing the article)
message = message.replace(/\$link/g, targetTitle);
return new OO.ui.Process()
.next(function () {
// Move the page
return errorIfRejected(
movePage(sourceTitle, targetTitle, summary, leaveRedirect),
MSG.errorMoveFailed
);
})
.next(function () {
// Remove categories and some templates
return errorIfRejected(
removeCategoriesAndTemplates(targetTitle, TEMPLATES_TO_REMOVE, MSG.removeCategoriesSummary, MSG.removeTemplatesSummary),
MSG.errorRemoveCategoriesFailed,
{ warning: true }
);
})
.next(function () {
if(!leaveRedirect) return true;
// Request deletion of the source redirect
return errorIfRejected(
prependText(sourceTitle, MSG.speedyTemplate.replace(/\$1/g, targetTitle), MSG.speedySummary),
MSG.errorMarkToDeleteFailed,
{ warning: true }
);
})
.next(function () {
// Don't send the message if the author is unknown
if(sourcePageAuthor === null) return true;
if(message === '') return true;
// Send message to the author
return errorIfRejected(
postToUserTalk(author, messageSubject, message),
MSG.errorTalkMessageFailed,
{ warning: true }
);
});
}
/**
* Removes all categories and specified templates from the page
* @param {string} pageTitle The title of the edited page
* @param {string[]} templates The templates to remove
* @param {string} categoriesSummary The edit summary used when removing categories
* @param {string} templatesSummary The edit summary used when removing templates
* @returns {jQuery.Promise<void>} A promise that resolves when the process is finished
*/
function removeCategoriesAndTemplates(pageTitle, templates, categoriesSummary, templatesSummary) {
var deferred = $.Deferred();
getPageContent(pageTitle).then(function (wikitext) {
var wikitextChanged = false;
var editSummaryParts = [];
var removedTemplates = [];
// Remove categories
if(wikitext.search(/\[\[kategoria:/i) >= 0) {
wikitextChanged = true;
wikitext = wikitext.replace(/\[\[kategoria:/gi, '[[:Kategoria:');
editSummaryParts.push(categoriesSummary);
}
// Remove templates
for(var i = 0; i < templates.length; i++) {
var template = templates[i];
var regex = new RegExp('\\{\\{' + template + '[^}]*\\}\\}\\s*', 'gi');
if(wikitext.search(regex) >= 0) {
wikitextChanged = true;
wikitext = wikitext.replace(regex, '');
removedTemplates.push(template);
}
}
if(removedTemplates.length > 0) {
templatesSummary = templatesSummary.replace(/\$1/g, removedTemplates.join(', '));
editSummaryParts.push(templatesSummary);
}
// If nothing changed, don't do a null edit
if(!wikitextChanged) return deferred.resolve();
var editSummary = editSummaryParts.join(' ');
savePage(pageTitle, wikitext, editSummary).then(function () {
deferred.resolve();
}).fail(function (reason) {
deferred.reject(reason);
});
}).fail(function (reason) {
deferred.reject(reason);
});
return deferred.promise();
}
/**
* Wraps a promise so that if it's rejected, the new promise will be rejected with
* OO.ui.Error as a reason.
* @param {jQuery.Promise} promise A promise to wrap
* @param {string?} messageTemplate A messageTempate. `$1` will be replaced with the original promise's rejection reason
* @param {object?} errorOptions Options to pass to OO.ui.Error
* @returns {jQuery.Promise}
*/
function errorIfRejected(promise, messageTemplate, errorOptions) {
var deferred = $.Deferred();
promise.then(function (result) {
deferred.resolve(result);
}).fail(function (reason) {
var message = (messageTemplate || '$1').replace(/\$1/g, reason);
deferred.reject(new OO.ui.Error(message, errorOptions));
});
return deferred.promise();
}
//! Interactions with API
/**
* So that the metadata is fetched only once
* @type {{[pageName: string]: jQuery.Promise}}
*/
var metadataPromises = {};
/**
* Retrieves the current page metadata
* @param {string} title The title of the page to check the metadata
* @returns {jQuery.Promise<PageMetadata>} A promise that resolves to the metadata
*/
function getArticleMetadata(title) {
if(metadataPromises[title]) return metadataPromises[title];
var deferred = $.Deferred();
var params = {
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'user',
rvslots: 'main',
rvlimit: 1,
rvdir: 'newer'
};
doAPICall(params).done(function (data) {
try {
var page = data.query.pages[0];
var revision = page.revisions[0];
var authorName = revision.user;
var isAnon = revision.anon !== undefined;
deferred.resolve({
pageId: page.pageid,
author: {
name: authorName,
isAnon: isAnon
}
});
} catch(e) {
// catch is going to execute only if there are some missing fields in the response
console.error(e);
metadataPromises[title] = undefined; // Don't cache failed requests
deferred.reject(MSG.apiFormatError);
}
}).fail(function (reason) {
metadataPromises[title] = undefined; // Don't cache failed requests
deferred.reject(reason);
});
var promise = deferred.promise();
metadataPromises[title] = promise;
return promise;
}
/** So that the rights are fetched only once */
var rightsPromise = null;
/**
* Fetches the rights of the current user
* @returns {jQuery.Promise<string[]>} A promise that resolves to the list of user rights
*/
function getCurrentUserRights() {
if(rightsPromise) return rightsPromise;
var deferred = $.Deferred();
var params = {
action: 'query',
meta: 'userinfo',
uiprop: 'rights'
};
doAPICall(params).done(function (data) {
try {
var userInfo = data.query.userinfo;
var rights = userInfo.rights;
deferred.resolve(rights);
} catch(e) {
// catch is going to execute only if there are some missing fields in the response
console.error(e);
rightsPromise = null; // Don't cache failed requests
deferred.reject(MSG.apiFormatError);
}
}).fail(function (reason) {
rightsPromise = null; // Don't cache failed requests
deferred.reject(reason);
});
rightsPromise = deferred.promise();
return rightsPromise;
}
/**
* Moves a page
* @param {string} from The title of the page to move
* @param {string} to The new title of the page
* @param {string} summary The edit summary
* @param {boolean} leaveRedirect Whether to leave a redirect on the old page
* @returns {jQuery.Promise<void>} A promise that resolves when the move is done
*/
function movePage(from, to, summary, leaveRedirect) {
var deferred = $.Deferred();
var params = {
action: 'move',
from: from,
to: to,
reason: summary,
movetalk: true,
noredirect: !leaveRedirect,
watchlist: 'nochange'
};
doAPICall(params).done(function (data) {
// If the API returns something different than we expected, treat it as an arror
if(!data.move || !data.move.to) {
return deferred.reject(MSG.apiFormatError);
}
deferred.resolve();
}).fail(function (reason) {
deferred.reject(reason);
});
return deferred.promise();
}
/**
* Prepends a text to the page content
* @param {string} title The page to edit
* @param {string} prependedText The text to prepend
* @param {string} summary The edit summary
* @returns {jQuery.Promise<void>} A promise that resolves when the page is edited
*/
function prependText(title, prependedText, summary) {
var deferred = $.Deferred();
var params = {
action: 'edit',
title: title,
summary: summary,
prependtext: prependedText,
nocreate: true
};
doAPICall(params).then(function (data) {
// If the API returns something different than we expected, treat it as an arror
if(!data.edit || !data.edit.newrevid) {
return deferred.reject(MSG.apiFormatError);
}
deferred.resolve();
}).fail(function (reason) {
deferred.reject(reason);
});
return deferred.promise();
}
/**
* Fetches the page content
* @param {string} title The title of the page to read
* @returns {jQuery.Promise<string>} A promise that resolves to the page content
*/
function getPageContent(title) {
var deferred = $.Deferred();
var params = {
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvslots: 'main',
rvlimit: 1
};
doAPICall(params).done(function (data) {
try {
var pages = data.query.pages;
var page = Object.values(pages)[0];
var revision = page.revisions[0];
var content = revision.slots.main.content;
deferred.resolve(content);
} catch(e) {
// catch is going to execute only if there are some missing fields in the response
console.error(e);
deferred.reject(MSG.apiFormatError);
}
}).fail(function (reason) {
deferred.reject(reason);
});
return deferred.promise();
}
/**
* Saves the page with specified content
* @param {string} title The title of the page to edit
* @param {string} content The new content of the page
* @param {string} summary The edit summary
* @returns {jQuery.Promise<void>} A promise that resolves when the edit is successful
*/
function savePage(title, content, summary) {
var deferred = $.Deferred();
var params = {
action: 'edit',
title: title,
summary: summary,
text: content,
nocreate: true
};
doAPICall(params).then(function (data) {
// If the API returns something different than we expected, treat it as an arror
if(!data.edit || !data.edit.newrevid) {
return deferred.reject(MSG.apiFormatError);
}
deferred.resolve();
}).fail(function (reason) {
deferred.reject(reason);
});
return deferred.promise();
}
/**
* Posts a message to the user's talk page
* @param {string} user The user to post the message to
* @param {string} subject The subject of the message
* @param {string} message The message
* @returns {jQuery.Promise<void>} A promise that resolves when the message is sent
*/
function postToUserTalk(user, subject, message) {
var deferred = $.Deferred();
mw.loader.using(['mediawiki.Title', 'mediawiki.messagePoster'], function () {
var title = new mw.Title('User talk:' + user);
mw.messagePoster.factory.create(title).then(function (poster) {
poster.post(subject, message, { tags: EDIT_TAG }).then(function (data) {
// Resolve the promise if the post was successful
// Usually it's going to be a normal edit
// but it may happen that the talk page is a Flow page - check both
if(data.edit && data.edit.result === 'Success') return deferred.resolve();
if(data.flow && data.flow['new-topic'].status == 'ok') return deferred.resolve();
deferred.reject(MSG.apiFormatError);
}).fail(function (primaryError, secondaryError, details) {
console.error(primaryError, secondaryError, details);
if(primaryError != 'api-fail') return deferred.reject(details);
var errorInfo = api.getErrorMessage(secondaryError);
deferred.reject(errorInfo);
});
});
});
return deferred.promise();
}
/**
* Does a MediaWiki API request.
*
* @param {object} params Query parameters.
* @returns {jQuery.Promise<any>} Promise resolving to the API response
*/
function doAPICall(params) {
var deferred = $.Deferred();
var apiFunc = api.postWithEditToken;
if(params.action === 'query') apiFunc = api.get;
// Ahh... Javascript's this... :/
// .call makes it safe to call apiFunc on its own
apiFunc.call(api, params).done(function (result, jqXHR) {
deferred.resolve(result);
}).fail(function (code, result) {
var errorMessage = processApiError(code, result);
return deferred.reject(
MSG.apiFail.replace(/\$1/g, 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/g, result.exception);
}
case 'parseerror':
console.log(result.exception);
return MSG.apiFormatError;
}
}
// unknown, generic
console.error(result);
var resultInfo = api.getErrorMessage(result);
resultInfo = resultInfo.text();
return MSG.apiGeneralError.replace(/\$1/g, resultInfo);
}
install();
})(jQuery, mw);
// </nowiki>