User:Pathoschild/Scripts/heuristic-script-update.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* global $, pathoschild */

/**
 * Heuristically updates user scripts for recent MediaWiki changes based on the documentation
 * at https://www.mediawiki.org/wiki/ResourceLoader/Migration_guide_(users).
 * 
 * Warnings:
 *   • This *will* break scripts; it should always be paired with manual review and corrections.
 *   • I don't expect anyone else to use this, so it may change without warning. Do let me know
 *     if you use it.
 */
pathoschild.migrateLegacyScripts = function() {
	/*********
	** Properties
	*********/
	var self = {};
	var _patterns = {
		outdatedScripts: /user:pathoschild\/scripts|tools\.wmflabs\.org\/(?:meta|pathoschild)|global\.(?:css|js)/mig,
		
		// derived from https://www.mediawiki.org/wiki/ResourceLoader/Legacy_JavaScript/list
		deprecatedCalls: /sajax_debug_mode|sajax_request_type|sajax_debug|sajax_init_object|sajax_do_call|wfSupportsAjax|wgAjaxWatch|considerChangingExpiryFocus|updateBlockOptions|onNameChange|onNameChangeHook|currentFocused|addButton|mwSetupToolbar|([^\.]|^)insertTags|scrollEditBox|toggleVisibility|historyRadios|diffcheck|histrowinit|htmlforms|doneIETransform|doneIEAlphaFix|expandedURLs|hookit|relativeforfloats|setrelative|onbeforeprint|onafterprint|attachMetadataToggle|os_map|os_cache|os_cur_keypressed|os_keypressed_count|os_timer|os_mouse_pressed|os_mouse_num|os_mouse_moved|os_search_timeout|os_autoload_inputs|os_autoload_forms|os_is_stopped|os_max_lines_per_suggest|os_animation_steps|os_animation_min_step|os_animation_delay|os_container_max_width|os_animation_timer|os_use_datalist|os_Timer|os_Results|os_AnimationTimer|os_MWSuggestInit|os_initHandlers|os_hookEvent|os_eventKeyup|os_processKey|os_eventKeypress|os_eventKeydown|os_eventOnsubmit|os_hideResults|os_decodeValue|os_encodeQuery|os_updateResults|os_setupDatalist|os_getNamespaces|os_updateIfRelevant|os_delayedFetch|os_fetchResults|os_getTarget|os_isNumber|os_enableSuggestionsOn|os_disableSuggestionsOn|os_eventBlur|os_eventFocus|os_setupDiv|os_createResultTable|os_showResults|os_operaWidthFix|f_clientWidth|f_clientHeight|f_scrollLeft|f_scrollTop|f_filterResults|os_availableHeight|os_getElementPosition|os_createContainer|os_fitContainer|os_trimResultText|os_animateChangeWidth|os_changeHighlight|os_HighlightClass|os_updateSearchQuery|os_eventMouseover|os_getNumberSuffix|os_eventMousemove|os_eventMousedown|os_eventMouseup|os_createToggle|os_toggle|tabbedprefs|uncoversection|checkTimezone|timezoneSetup|fetchTimezone|guessTimezone|updateTimezoneSelection|doLivePreview|ProtectionForm|setupRightClickEdit|addRightClickEditHandler|mwSearchHeaderClick|mwToggleSearchCheckboxes|wgUploadWarningObj|wgUploadLicenseObj|licenseSelectorCheck|wgUploadSetup|toggleUploadInputs|fillDestFilename|toggleFilenameFiller|clientPC|is_gecko|is_safari|is_safari_win|is_chrome|is_chrome_mac|is_ff2|is_ff2_win|is_ff2_x11|webkit_match|ff2_bugs|ie6_bugs|doneOnloadHook|onloadFuncts|addOnloadHook|runOnloadHook|killEvt|loadedScripts|importScriptURI|importStylesheetURI|appendCSS|addHandler|addClickHandler|removeHandler|hookEvent|mwEditButtons|tooltipAccessKeyPrefix|tooltipAccessKeyRegexp|updateTooltipAccessKeys|akeytt|ta=\[\]|ta=\{\}|ta=new Array|ta=new Object|ta = \[\]|ta = \{\}|ta = new Array|ta = new Object|lastCheckbox|setupCheckboxShiftClick|addCheckboxClickHandlers|checkboxClickHandler|showTocToggle|toggleToc|changeText|getInnerText|escapeQuotes|escapeQuotesHTML|jsMsg|getElementsByClassName|redirectToFragment/mg
	};

	/*********
	** Public methods
	*********/
	/**
	 * Apply all rules (including the least safe ones) to the page being edited.
	 * @param {object} editor The TemplateScript script helpers.
	 */
	self.applyAll = function(editor) {
		// get text
		var text = editor.get();
		var hasDeprecatedCalls = !!text.match(_patterns.deprecatedCalls);

		// apply changes
		text = self.applySafeRules(text);
		text = self.applyDangerousRules(text);
		text = self.applyPathoschildRules(text);
		self.detectMigrationIssues(text);
		
		// save changes
		editor
			.set(text)
			.setEditSummary('updated scripts');
		if(hasDeprecatedCalls)
			editor.appendEditSummary('migrated [[mw:ResourceLoader/Migration guide (users)|deprecated functions]]');
	};

	/**
	 * Apply the (relatively) safe migration rules.
	 * @param {string} The scripts to modify.
	 * @returns {string} The modified scripts.
	 */
	self.applySafeRules = function(text) {
		/* migrate functions with a one-to-one mapping */
		text = text.replace(/addOnloadHook/g, '$');
		text = text.replace(/(^|[^\.])(addPortletLink|insertTags)/mg, '$1mw.util.$2');

		/* migrate document.write → mw.loader.load */
		var multilineDocumentWrite = /document\.write\(['"](.+)['"][\s\n]+\+\s*['"]/g;
		while(text.match(multilineDocumentWrite))
			text = text.replace(multilineDocumentWrite, 'document.write(\'$1');
		text = text.replace(/document.write\(['"]<script.+? src=['"](.+?)['"]><\/script>['"]\);?/ig, 'mw.loader.load(\'$1\');');

		/* migrate importScriptURI → mw.loader.load */
		text = text.replace(/(?:mw\.loader\.load|importScriptURI)\(['"](?:https?:)?\/\/(.+?)['"]\);?/g, 'mw.loader.load(\'//$1\');');

		/* migrate importStylesheetURI → mw.loader.load */
		text = text.replace(/importStylesheetURI\(['"](?:https?:)?\/\/(.+?)['"]\);?/g, 'mw.loader.load(\'//$1\', \'text/css\');');

		/* remove obsolete dontcountme/smaxage script parameters */
		text = text.replace(/&dontcountme=s|&s?maxage=\d+/g, '');

		/* update safe variables moved into mw.config */
		text = text.replace(/\b(wgAction|wgArticleId|wgArticlePath|wgCanonicalNamespace|wgCanonicalSpecialPageName|wgContentLanguage|wgDBname|wgIsArticle|wgIsRedirect|wgNamespaceNumber|wgPageName|wgRevisionId|wgScript|wgScriptExtension|wgScriptPath|wgServer|wgServerName|wgSiteName|wgTitle|wgUserName|wgUserId|wgUserLanguage|wgUserNamewgVersion)\b(?!['"])/g, 'mw.config.get(\'$1\')');

		/* trim trailing whitespace */
		text = text.replace(/[ \t]+$/mg, '');

		return text;
	};

	/**
	 * Apply migration rules that require significant review and followup adjustment.
	 * Don't run these unless you know exactly what you're doing and are familiar with all
	 * affected functions.
	 * @param {string} The scripts to modify.
	 * @returns {string} The modified scripts.
	 */
	self.applyDangerousRules = function(text) {
		/* mwCustomEditButtons → wikiEditor */
		/* This creates a separate function call for each edit button, which should be combined into one. */
		text = text.replace(/mwCustomEditButtons *\[[^\]]*\] *= *\{[\s\S]+?\}(?:;|\n)/g, function(item) {
			var image = (item.match(/['"]imageFile['"]: ['"](?:http:)?(.*?)['"](?:,|$)/m) || [])[1] || '';
			var label = (item.match(/['"]speedTip['"]: ['"](.*?)['"](?:,|$)/m) || [])[1] || '';
			var preText = (item.match(/['"]tagOpen['"]: ['"](.*?)['"](?:,|$)/m) || [])[1] || '';
			var postText = (item.match(/['"]tagClose['"]: ['"](.*?)['"](?:,|$)/m) || [])[1] || '';
			var sampleText = (item.match(/['"]sampleText['"]: ['"](.*?)['"](?:,|$)/m) || [])[1] || '';

			var result = "$('#wpTextbox1').wikiEditor('addToToolbar', {\n	section: 'main',\n	group: 'format',\n\ttools: {\n\t\t'custom-" + label + "': {\n\t\t\tlabel: '" + label + "',\n\t\t\ttype: 'button',\n\t\t\ticon: '" + image + "',\n\t\t\taction: {\n\t\t\t\ttype: 'encapsulate',\n\t\t\t\toptions: {\n\t\t\t\t\tpre: '" + preText + "',\n\t\t\t\t\tpost: '" + postText + "',\n\t\t\t\t\tsampleText: '" + sampleText + "'\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n});";
			result = result.replace(/editSummary:'', /, '');
			return result;
		});
		text = text.replace(/[\n\s]*\}[\n\s]*\}\);[\n\s]*\$\('#wpTextbox1'\)\.wikiEditor\('addToToolbar', \{[\n\s]*section: 'main',[\n\s]*group: 'format',[\n\s]*tools: \{\n*/mg, ',\n');

		/* heuristically update variables moved into mw.config */
		var deprecatedVariableKeys = Object.keys(mw.config.get()).filter(function(key) { return key in window; }).sort();
		var deprecatedVariablePattern = new RegExp('\\b(' + deprecatedVariableKeys.join('|') + ')\\b(?![\'"])', 'g');
		text = text.replace(deprecatedVariablePattern, 'mw.config.get(\'$1\')');

		return text;
	};

	/**
	 * Apply migration rules for Pathoschild's user scripts. You should probably never run these unless you're Pathoschild.
	 * @param {string} The scripts to modify.
	 * @returns {string} The modified scripts.
	 */
	self.applyPathoschildRules = function(text) {
		// warn about usage trackers disabled by <nowiki>/equivalent tags
		var nowikiBlocks = text.match(/<(nowiki|pre|source)[^>]*>[\s\S]*?(?:<\/\1>|$)/g) || [];
		var disabledTrackers = (nowikiBlocks.join('\n').match(/\[\[file:Pathoschild[^\]]+\]\]/ig) || '').toString();
		if(disabledTrackers)
			alert('These update trackers might be disabled by <nowiki>, <pre>, or <source>: ' + disabledTrackers);

		// replace text
		text = text.replace(/\/\*[\s\S]+?\*\/[\s\n]*/g, function(match) {
		   return match.match(/pathoschild/i) ? '' : match;
		});
		text = text.replace(/mw.loader.load\('[^']+?(?:Ajax_sysop|ajaxsysop)(?:\/experimental)?\.js[^']*'\);/, '/**\n * Ajax sysop\n * @see https://meta.wikimedia.org/wiki/Ajax_sysop\n * @update-token [[File:pathoschild/ajaxsysop.js]]\n */\nmw.loader.load(\'//tools-static.wmflabs.org/meta/scripts/pathoschild.ajaxsysop.js\');');
		text = text.replace(/mw.loader.load\('[^']+?(?:Force_ltr|forceltr)\.js[^']*'\);/, '/**\n * Forces left-to-right layout and editing on RTL wikis.\n * @see https://meta.wikimedia.org/wiki/Force_ltr\n * @update-token [[File:pathoschild/forceltr.js]]\n */\nmw.loader.load(\'//tools-static.wmflabs.org/meta/scripts/pathoschild.forceltr.js\');');
		text = text.replace(/mw.loader.load\('[^']+?[Ss]teward[Ss]cript\.js[^']*'\);/, '/**\n * StewardScript extends the user interface for Wikimedia stewards\' convenience.\n * @see https://meta.wikimedia.org/wiki/StewardScript\n * @update-token [[File:pathoschild/stewardscript.js]]\n */\nmw.loader.load(\'//tools-static.wmflabs.org/meta/scripts/pathoschild.stewardscript.js\');');
		text = text.replace(/mw.loader.load\('[^']+?(?:[Tt]emplate[Ss]cript|[Rr]egex_menu_framework(?:\/experimental)?)\.js[^']*'\);/, '/**\n * TemplateScript adds configurable templates and scripts to the sidebar, and adds an example regex editor.\n * @see https://meta.wikimedia.org/wiki/TemplateScript\n * @update-token [[File:pathoschild/templatescript.js]]\n */\nmw.loader.load(\'//tools-static.wmflabs.org/meta/scripts/pathoschild.templatescript.js\');');
		text = text.replace(/mw.loader.load\('[^']+?[Uu]sejs\.js[^']*'\);/, '/**\n * Imports JaveScript for the current page when the URL contains a &usejs parameter.\n * @see https://meta.wikimedia.org/wiki/UseJS\n * @update-token [[File:pathoschild/usejs.js]]\n */\nmw.loader.load(\'//tools-static.wmflabs.org/meta/scripts/pathoschild.usejs.js\');');
		text = text.replace(/[ \t]*new_template\('(.*?)','(.*?)','(.*?)'(?:,'(.*?)')?\);/g, '		\{ name:\'$2\', template:\'$3\', editSummary:\'$4\', forActions:\'$1\' \},');
		text = text.replace(/, forActions: *'edit'/g, '');
		text = text.replace(/, editSummary: *''/g, '');

		return text;
	};

	/**
	 * Detect code that may still need to be migrated manually and show an alert.
	 * @param {string} The scripts to modify.
	 */
	self.detectMigrationIssues = function(text) {
		var scripts = text.match(_patterns.outdatedScripts);
		if(scripts)
			alert('This script might contain outdated scripts (found these triggers: ' + scripts + ')');
			
		var deprecated = text.match(_patterns.deprecatedCalls);
		if(deprecated)
			alert('This script might contain obsolete function calls(found these triggers: ' + deprecated + ')');
	};

	/*********
	** Private methods
	*********/
	return self;
};