MediaWiki:Gadget-morebits.js: Difference between revisions
Content deleted Content added
Amorymeltzer (talk | contribs) Repo at 8215b50: Note defaults for errorformat, uselang, and json's formatversion; Update deprecation notices; Add a few quick tests for Morebits.unbinder |
Amorymeltzer (talk | contribs) Repo at b60c1b8: Convert Morebits.wiki.page methods to json, not xml; Require matching level markers; Add YYYYMMDDHHmmss to morebits.date constructor; Fixes for internal templates/parser functions; Consolidate largely duplicated code; Fixes for unnamed parameters |
||
Line 1,531:
var args = Array.prototype.slice.call(arguments);
// Check
// Must be first since firefox erroneously accepts the
// format, sans timezone (See also: #921, #936, #1174, #1187), and the
// 14-digit string will be interpreted differently.
if (args.length === 1) {
var param = args[0];
if (/^\d{14}$/.test(param)) {
// YYYYMMDDHHmmss
var digitMatch = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(param);
if (digitMatch) {
// ..... year ... month .. date ... hour .... minute ..... second
this._d = new Date(Date.UTC.apply(null, [digitMatch[1], digitMatch[2] - 1, digitMatch[3], digitMatch[4], digitMatch[5], digitMatch[6]]));
}
} else if (typeof param === 'string') {
// Wikitext signature timestamp
var dateParts = Morebits.date.localeData.signatureTimestampFormat(param);
if (dateParts) {
this._d = new Date(Date.UTC.apply(null, dateParts));
}
}
}
if (!this._d) {
// Try standard date
this._d = new (Function.prototype.bind.apply(Date, [Date].concat(args)));
Line 1,574 ⟶ 1,590:
},
signatureTimestampFormat: function (str) {
// HH:mm, DD Month YYYY (UTC)
var rgx = /(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/;
var match = rgx.exec(str);
Line 1,805 ⟶ 1,822:
*/
monthHeaderRegex: function() {
return new RegExp('^(==+)\\s*(?:' + this.getUTCMonthName() + '|' + this.getUTCMonthNameAbbrev() +
')\\s+' + this.getUTCFullYear() + '\\s*
},
Line 1,994 ⟶ 2,011:
this.statelem = new Morebits.status(currentAction);
}
// JSON is used throughout Morebits/Twinkle, but xml remains the default for backwards compatibility
if (!query.format) {
this.query.format = 'xml';
Line 2,212 ⟶ 2,230:
action: 'query',
meta: 'tokens',
type: 'csrf',
format: 'json'
});
return tokenApi.post().then(function(apiobj) {
return
});
};
Line 2,411 ⟶ 2,430:
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
format: 'json'
// don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default
};
Line 2,493 ⟶ 2,513:
summary: ctx.editSummary,
token: canUseMwUserToken ? mw.user.tokens.get('csrfToken') : ctx.csrfToken,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
Line 3,035 ⟶ 3,056:
'rvlimit': 1,
'rvprop': 'user|timestamp',
'rvdir': 'newer',
'format': 'json'
};
Line 3,137 ⟶ 3,159:
rcprop: 'patrolled',
rctitle: ctx.pageName,
rclimit: 1,
format: 'json'
};
Line 3,371 ⟶ 3,394:
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
format: 'json'
};
// Protection not checked for flagged-revs or non-sysop moves
Line 3,391 ⟶ 3,415:
// callback from loadApi.post()
var fnLoadSuccess = function() {
var
if (!fnCheckPageName(
return; // abort
}
var page = response.pages[0], rev;
ctx.pageExists = !page.missing;
if (ctx.pageExists) {
rev = page.revisions[0];
ctx.
ctx.pageText = rev.content;
ctx.pageID = page.pageid;
} else {
ctx.pageText = ''; // allow for concatenation, etc.
ctx.pageID = 0; // nonexistent in response, matches wgArticleId
}
ctx.csrfToken =
if (!ctx.csrfToken) {
ctx.statusElement.error('Failed to retrieve edit token.');
Line 3,411 ⟶ 3,438:
return;
}
ctx.loadTime =
if (!ctx.loadTime) {
ctx.statusElement.error('Failed to retrieve current timestamp.');
Line 3,418 ⟶ 3,445:
}
ctx.contentModel =
// extract protection info, to alert admins when they are about to edit a protected page
// Includes cascading protection
if (Morebits.userIsSysop) {
var
}).pop();
if (editProt) {
ctx.fullyProtected = editProt.expiry;
} else {
ctx.fullyProtected = false;
Line 3,430 ⟶ 3,460:
}
ctx.revertCurID = page.lastrevid;
var testactions =
ctx.testActions = []; // was null
Object.keys(testactions).forEach(function(action) {
ctx.testActions.push(
}
});
if (ctx.editMode === 'revert') {
ctx.revertCurID =
if (!ctx.revertCurID) {
ctx.statusElement.error('Failed to retrieve current revision ID.');
Line 3,448 ⟶ 3,477:
return;
}
ctx.revertUser =
if (!ctx.revertUser) {
if (
ctx.revertUser = '<username hidden>';
} else {
Line 3,469 ⟶ 3,498:
// helper function to parse the page name returned from the API
var fnCheckPageName = function(
if (!onFailure) {
onFailure = emptyFunction;
}
var page = response.pages[0];
// check for invalid titles
if (
ctx.statusElement.error('The page title is invalid: ' + ctx.pageName);
onFailure(this);
Line 3,482 ⟶ 3,512:
// retrieve actual title of the page after normalization and redirects
if (
var resolvedName =
if (
// check for cross-namespace redirect:
var origNs = new mw.Title(ctx.pageName).namespace;
Line 3,516 ⟶ 3,546:
var fnSaveSuccess = function() {
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
var
// see if the API thinks we were successful
if (
// real success
Line 3,535 ⟶ 3,565:
// errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki,
// which as of 1.34.0-wmf.23 (Sept 2019) should only encompass captcha messages
if (
ctx.statusElement.error('Could not save the page because the wiki server wanted you to fill out a CAPTCHA.');
} else {
Line 3,595 ⟶ 3,625:
case 'abusefilter-disallowed':
ctx.statusElement.error('The edit was disallowed by the edit filter: "' +
break;
case 'abusefilter-warning':
ctx.statusElement.error([ 'A warning was returned by the edit filter: "',
// We should provide the user with a way to automatically retry the action if they so choose -
// I can't see how to do this without creating a UI dependency on Morebits.wiki.page though -- TTO
Line 3,605 ⟶ 3,635:
case 'spamblacklist':
//
var spam =
ctx.statusElement.error('Could not save the page because the URL ' + spam + ' is on the spam blacklist');
break;
Line 3,622 ⟶ 3,652:
var fnLookupCreationSuccess = function() {
var
if (!fnCheckPageName(
return; // abort
}
var rev = response.pages[0].revisions && response.pages[0].revisions[0];
if (!rev) {
ctx.statusElement.error('Could not find any revisions of ' + ctx.pageName);
return;
}
if (!ctx.lookupNonRedirectCreator || !/^\s*#redirect/i.test(rev.content)) {
ctx.creator =
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
return;
}
ctx.timestamp =
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
Line 3,654 ⟶ 3,690:
var fnLookupNonRedirectCreator = function() {
var
var revs = response.pages[0].revisions;
if (!/^\s*#redirect/i.test(rev.textContent)) { // inaccessible revisions also check out
ctx.creator = rev.
ctx.timestamp = rev.
return false; // break
}
Line 3,666 ⟶ 3,703:
if (!ctx.creator) {
// fallback to give first revision author if no non-redirect version in the first 50
ctx.creator =
ctx.timestamp =
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
Line 3,713 ⟶ 3,750:
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @param {string}
* @returns {boolean}
*/
var fnProcessChecks = function(action, onFailure,
var missing =
// No undelete as an existing page could have deleted revisions
Line 3,734 ⟶ 3,771:
var editprot;
if (action === 'undelete') {
editprot =
return pr.type === 'create' && pr.level === 'sysop';
}).pop();
} else if (action === 'delete' || action === 'move') {
editprot =
return pr.type === 'edit' && pr.level === 'sysop';
}).pop();
}
if (editprot
!confirm('You are about to ' + action + ' the fully protected page "' + ctx.pageName +
(editprot.
'. \n\nClick OK to proceed with ' + action + ', or Cancel to skip.')) {
ctx.statusElement.error('Aborted ' + action + ' on fully protected page.');
Line 3,747 ⟶ 3,788:
}
if (!
ctx.statusElement.error('Failed to retrieve token.');
onFailure(this);
Line 3,762 ⟶ 3,803:
pageTitle = ctx.pageName;
} else {
var
if (!fnProcessChecks('move', ctx.onMoveFailure,
return; // abort
}
token =
pageTitle =
}
Line 3,778 ⟶ 3,819:
'token': token,
'reason': ctx.editSummary,
'watchlist': ctx.watchlistOption,
'format': 'json'
};
if (ctx.changeTags) {
Line 3,804 ⟶ 3,846:
var fnProcessPatrol = function() {
var query = {
action: 'patrol',
format: 'json'
};
Line 3,812 ⟶ 3,855:
query.token = mw.user.tokens.get('patrolToken');
} else {
var
// Don't patrol if not unpatrolled
if (
return;
}
var lastrevid =
if (!lastrevid) {
return;
Line 3,825 ⟶ 3,868:
query.revid = lastrevid;
var token =
if (!token) {
return;
}
query.token = token;
}
Line 3,848 ⟶ 3,890:
ctx.csrfToken = mw.user.tokens.get('csrfToken');
} else {
var
ctx.pageID =
if (!ctx.pageID) {
return;
}
ctx.csrfToken =
if (!ctx.csrfToken) {
return;
Line 3,863 ⟶ 3,905:
var query = {
action: 'pagetriagelist',
page_id: ctx.pageID,
format: 'json'
};
Line 3,871 ⟶ 3,914:
};
// callback from triageProcessListApi.post()
var fnProcessTriage = function() {
var
// Exit if not in the queue
if (
return;
}
var page =
//
if (!page || !parseInt(page.
var query = {
action: 'pagetriageaction',
Line 3,887 ⟶ 3,931:
// Could use an adder to modify/create note:
// summaryAd, but that seems overwrought
token: ctx.csrfToken,
format: 'json'
};
var triageStat = new Morebits.status('Marking page as curated');
Line 3,903 ⟶ 3,948:
pageTitle = ctx.pageName;
} else {
var
if (!fnProcessChecks('delete', ctx.onDeleteFailure,
return; // abort
}
token =
pageTitle =
}
Line 3,918 ⟶ 3,963:
'token': token,
'reason': ctx.editSummary,
'watchlist': ctx.watchlistOption,
'format': 'json'
};
if (ctx.changeTags) {
Line 3,965 ⟶ 4,011:
pageTitle = ctx.pageName;
} else {
var
if (!fnProcessChecks('undelete', ctx.onUndeleteFailure,
return; // abort
}
token =
pageTitle =
}
Line 3,980 ⟶ 4,026:
'token': token,
'reason': ctx.editSummary,
'watchlist': ctx.watchlistOption,
'format': 'json'
};
if (ctx.changeTags) {
Line 4,027 ⟶ 4,074:
var fnProcessProtect = function() {
var
if (!fnProcessChecks('protect', ctx.onProtectFailure,
return; // abort
}
var token =
var pageTitle =
// Fetch existing protection levels
var prs =
var editprot,
prs.forEach(function(pr) {
// Filter out protection from cascading
if (pr.type === 'edit' && !pr.source) {
editprot = pr;
} else if (pr.type === 'move') {
moveprot = pr;
} else if (pr.type === 'create') {
createprot = pr;
}
});
// Fall back to current levels if not explicitly set
if (!ctx.protectEdit && editprot
ctx.protectEdit = { level: editprot.
}
if (!ctx.protectMove && moveprot
ctx.protectMove = { level: moveprot.
}
if (!ctx.protectCreate && createprot
ctx.protectCreate = { level: createprot.
}
// Default to pre-existing cascading protection if unchanged (similar to above)
if (ctx.protectCascade === null) {
ctx.protectCascade = !!prs.filter(function(pr) {
return pr.cascade;
}).length;
}
// Warn if cascading protection being applied with an invalid protection level,
// which for edit protection will cause cascading to be silently stripped
if (ctx.protectCascade) {
// On move protection, this is technically stricter than the MW API,
// but seems reasonable to avoid dumb values and misleading log entries (T265626)
Line 4,071 ⟶ 4,132:
ctx.protectEdit.level = 'sysop';
ctx.protectMove.level = 'sysop';
}
Line 4,098 ⟶ 4,158:
expiry: expirys.join('|'),
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
// Only shows up in logs, not page history [[phab:T259983]]
Line 4,124 ⟶ 4,185:
pageTitle = ctx.pageName;
} else {
var
// 'stabilize' as a verb not necessarily well understood
if (!fnProcessChecks('stabilize', ctx.onStabilizeFailure,
return; // abort
}
token =
pageTitle =
}
Line 4,143 ⟶ 4,204:
// tags: ctx.changeTags, // flaggedrevs tag support: [[phab:T247721]]
reason: ctx.editSummary,
watchlist: ctx.watchlistOption, // Doesn't support watchlist expiry [[phab:T263336]]
format: 'json'
};
Line 4,207 ⟶ 4,269:
text: wikitext,
title: pageTitle || mw.config.get('wgPageName'),
disablelimitreport: true,
format: 'json'
};
if (sectionTitle) {
Line 4,218 ⟶ 4,281:
var fnRenderSuccess = function(apiobj) {
var
if (!html) {
apiobj.statelem.error('failed to retrieve preview, or template was blanked');
Line 4,256 ⟶ 4,318:
start = start || 0;
var count = -1; // Number of parameters found
var unnamed = 0; // Keep track of what number an unnamed parameter should receive
var level = -1; // How many levels deep of template code we're in, 0-based
var equals = -1; // After finding "=" before a parameter, the index; otherwise, -1
var current = '';
var result = {
Line 4,265 ⟶ 4,328:
};
var key, value;
/**
* Function to handle finding parameter values.
*
* @param {boolean} [final=false] - Whether this is the final
* parameter and we need to remove the trailing `}}`.
*/
var findParam = function(final) {
// Nothing found yet, this must be the template name
if (count === -1) {
result.name = current.substring(2).trim();
++count;
} else {
// In a parameter
if (equals !== -1) {
// We found an equals, so save the parameter as key: value
key = current.substring(0, equals).trim();
value = final ? current.substring(equals + 1, current.length - 2).trim() : current.substring(equals + 1).trim();
result.parameters[key] = value;
equals = -1;
} else {
// No equals, so it must be unnamed; no trim since whitespace allowed
var param = final ? current.substring(equals + 1, current.length - 2) : current;
if (param) {
result.parameters[++unnamed] = param;
++count;
}
}
}
};
for (var i = start; i < text.length; ++i) {
var test3 = text.substr(i, 3);
if (test3 === '{{{' || test3 === '}}}') {
current +=
i += 2;
test3 === '}}}' ? --level : ++level;
continue;
}
var test2 = text.substr(i, 2);
// Entering a template (or link)
if (test2 === '{{' || test2 === '[[') {
current += test2;
Line 4,287 ⟶ 4,375:
continue;
}
// Leaving a link
if (test2 === ']]') {
current += ']]';
Line 4,293 ⟶ 4,382:
continue;
}
// Either leaving a template or an internal template/parser function
if (test2 === '}}') {
// Regardless, decrement the level
current += test2;
++i;
--level;
// Find the final parameter if this really is the end
findParam(true);
break;
}
Line 4,318 ⟶ 4,397:
}
if (text.charAt(i) === '|' && level
// Another pipe found, toplevel, so parameter coming up!
findParam();
current = '';
} else if (equals === -1 && text.charAt(i) === '=' && level
// Equals found, toplevel
equals = current.length;
current += text.charAt(i);
} else {
// Just advance the position
current += text.charAt(i);
}
|