User:Iniquity/Gadget-script-installer-core.js

( function () {

// An mw.Api object

var api;

// Keep "common" at beginning

var SKINS = [ "common", "monobook", "minerva", "vector", "vector-2022", "timeless" ];

// How many scripts do we need before we show the quick filter?

var NUM_SCRIPTS_FOR_SEARCH = 5;

// The master import list, keyed by target. (A "target" is a user JS subpage

// where the script is imported, like "common" or "vector".) Set in buildImportList

var imports = {};

// Local scripts, keyed on name; value will be the target. Set in buildImportList.

var localScriptsByName = {};

// How many scripts are installed?

var scriptCount = 0;

// Goes on the end of edit summaries

var ADVERT = " (script-installer)";

/**

* Strings, for translation

*/

var STRINGS = {

skinCommon: "common (applies to all skins)",

backlink: "Backlink:",

installSummary: "Installing $1",

installLinkText: "Install",

installProgressMsg: "Installing...",

uninstallSummary: "Uninstalling $1",

uninstallLinkText: "Uninstall",

uninstallProgressMsg: "Uninstalling...",

disableSummary: "Disabling $1",

disableLinkText: "Disable",

disableProgressMsg: "Disabling...",

enableSummary: "Enabling $1",

enableLinkText: "Enable",

enableProgressMsg: "Enabling...",

moveLinkText: "Move",

moveProgressMsg: "Moving...",

movePrompt: "Destination? Enter one of:", // followed by the names of skins

normalizeSummary: "Normalizing script installs",

remoteUrlDesc: "$1, loaded from $2",

panelHeader: "You currently have the following scripts installed (find more at WP:USL)",

cannotInstall: "Cannot install",

cannotInstallSkin: "This page is one of your user customization pages, and may (will, if common.js) already run on each page load.",

cannotInstallContentModel: "Page content model is $1, not 'javascript'",

insecure: "(insecure)", // used at the end of some messages

notJavaScript: "not JavaScript",

installViaPreferences: "Install via preferences",

showNormalizeLinks: 'Show "normalize" links?',

normalize: "normalize",

showMoveLinks: 'Show "move" links?',

quickFilter: "Quick filter:",

tempWarning: "Installation of non-User, non-MediaWiki protected pages is temporary and may be removed in the future.",

badPageError: "Page is not User: or MediaWiki: and is unprotected",

manageUserScripts: "Manage user scripts",

bigSecurityWarning: "Warning!$1\n\nAll user scripts could contain malicious content capable of compromising your account. Installing a script means it could be changed by others; make sure you trust its author. If you're unsure whether a script is safe, check at the technical village pump. Install this script?",

securityWarningSection: " About to install $1."

};

var USER_NAMESPACE_NAME = mw.config.get( "wgFormattedNamespaces" )[2];

/**

* Constructs an Import. An Import is a line in a JS file that imports a

* user script. Properties:

*

* - "page" is a page name, such as "User:Foo/Bar.js".

* - "wiki" is a wiki from which the script is loaded, such as

* "en.wikipedia". If null, the script is local, on the user's

* wiki.

* - "url" is a URL that can be passed into mw.loader.load.

* - "target" is the title of the user subpage where the script is,

* without the .js ending: for example, "common".

* - "disabled" is whether this import is commented out.

* - "type" is 0 if local, 1 if remotely loaded, and 2 if URL.

*

* EXACTLY one of "page" or "url" are null for every Import. This

* constructor should not be used directly; use the factory

* functions (Import.ofLocal, Import.ofUrl, Import.fromJs) instead.

*/

function Import( page, wiki, url, target, disabled ) {

this.page = page;

this.wiki = wiki;

this.url = url;

this.target = target;

this.disabled = disabled;

this.type = this.url ? 2 : ( this.wiki ? 1 : 0 );

}

Import.ofLocal = function ( page, target, disabled ) {

if( disabled === undefined ) disabled = false;

return new Import( page, null, null, target, disabled );

}

/** URL to Import. Assumes wgScriptPath is "/w" */

Import.ofUrl = function ( url, target, disabled ) {

if( disabled === undefined ) disabled = false;

var URL_RGX = /^(?:https?:)?\/\/(.+?)\.org\/w\/index\.php\?.*?title=(.+?(?:&|$))/;

var match;

if( match = URL_RGX.exec( url ) ) {

var title = decodeURIComponent( match[2].replace( /&$/, "" ) ),

wiki = decodeURIComponent( match[1] );

return new Import( title, wiki, null, target, disabled );

}

return new Import( null, null, url, target, disabled );

}

Import.fromJs = function ( line, target ) {

var IMPORT_RGX = /^\s*(\/\/)?\s*importScript\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/;

var match;

if( match = IMPORT_RGX.exec( line ) ) {

return Import.ofLocal( unescapeForJsString( match[2] ), target, !!match[1] );

}

var LOADER_RGX = /^\s*(\/\/)?\s*mw\.loader\.load\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/;

if( match = LOADER_RGX.exec( line ) ) {

return Import.ofUrl( unescapeForJsString( match[2] ), target, !!match[1] );

}

}

Import.prototype.getDescription = function ( useWikitext ) {

switch( this.type ) {

case 0: return useWikitext ? ( "" + this.page + "" ) : this.page;

case 1: return STRINGS.remoteUrlDesc.replace( "$1", this.page ).replace( "$2", this.wiki );

case 2: return this.url;

}

}

/**

* Human-readable (NOT necessarily suitable for ResourceLoader) URL.

*/

Import.prototype.getHumanUrl = function () {

switch( this.type ) {

case 0: return "/wiki/" + encodeURI( this.page );

case 1: return "//" + this.wiki + ".org/wiki/" + encodeURI( this.page );

case 2: return this.url;

}

}

Import.prototype.toJs = function () {

var dis = this.disabled ? "//" : "",

url = this.url;

switch( this.type ) {

case 0: return dis + "importScript('" + escapeForJsString( this.page ) + "'); // " + STRINGS.backlink + " " + escapeForJsComment( this.page ) + "";

case 1: url = "//" + encodeURIComponent( this.wiki ) + ".org/w/index.php?title=" +

encodeURIComponent( this.page ) + "&action=raw&ctype=text/javascript";

/* FALL THROUGH */

case 2: return dis + "mw.loader.load('" + escapeForJsString( url ) + "');";

}

}

/**

* Installs the import.

*/

Import.prototype.install = function () {

return api.postWithEditToken( {

action: "edit",

title: getFullTarget( this.target ),

summary: STRINGS.installSummary.replace( "$1", this.getDescription( /* useWikitext */ true ) ) + ADVERT,

appendtext: "\n" + this.toJs()

} );

}

/**

* Get all line numbers from the target page that mention

* the specified script.

*/

Import.prototype.getLineNums = function ( targetWikitext ) {

function quoted( s ) {

return new RegExp( "(['\"])" + escapeForRegex( s ) + "\\1" );

}

var toFind;

switch( this.type ) {

case 0: toFind = quoted( escapeForJsString( this.page ) ); break;

case 1: toFind = new RegExp( escapeForRegex( encodeURIComponent( this.wiki ) ) + ".*?" +

escapeForRegex( encodeURIComponent( this.page ) ) ); break;

case 2: toFind = quoted( escapeForJsString( this.url ) ); break;

}

var lineNums = [], lines = targetWikitext.split( "\n" );

for( var i = 0; i < lines.length; i++ ) {

if( toFind.test( lines[i] ) ) {

lineNums.push( i );

}

}

return lineNums;

}

/**

* Uninstalls the given import. That is, delete all lines from the

* target page that import the specified script.

*/

Import.prototype.uninstall = function () {

var that = this;

return getWikitext( getFullTarget( this.target ) ).then( function ( wikitext ) {

var lineNums = that.getLineNums( wikitext ),

newWikitext = wikitext.split( "\n" ).filter( function ( _, idx ) {

return lineNums.indexOf( idx ) < 0;

} ).join( "\n" );

return api.postWithEditToken( {

action: "edit",

title: getFullTarget( that.target ),

summary: STRINGS.uninstallSummary.replace( "$1", that.getDescription( /* useWikitext */ true ) ) + ADVERT,

text: newWikitext

} );

} );

}

/**

* Sets whether the given import is disabled, based on the provided

* boolean value.

*/

Import.prototype.setDisabled = function ( disabled ) {

var that = this;

this.disabled = disabled;

return getWikitext( getFullTarget( this.target ) ).then( function ( wikitext ) {

var lineNums = that.getLineNums( wikitext ),

newWikitextLines = wikitext.split( "\n" );

if( disabled ) {

lineNums.forEach( function ( lineNum ) {

if( newWikitextLines[lineNum].trim().indexOf( "//" ) != 0 ) {

newWikitextLines[lineNum] = "//" + newWikitextLines[lineNum].trim();

}

} );

} else {

lineNums.forEach( function ( lineNum ) {

if( newWikitextLines[lineNum].trim().indexOf( "//" ) == 0 ) {

newWikitextLines[lineNum] = newWikitextLines[lineNum].replace( /^\s*\/\/\s*/, "" );

}

} );

}

var summary = ( disabled ? STRINGS.disableSummary : STRINGS.enableSummary )

.replace( "$1", that.getDescription( /* useWikitext */ true ) ) + ADVERT;

return api.postWithEditToken( {

action: "edit",

title: getFullTarget( that.target ),

summary: summary,

text: newWikitextLines.join( "\n" )

} );

} );

}

Import.prototype.toggleDisabled = function () {

this.disabled = !this.disabled;

return this.setDisabled( this.disabled );

}

/**

* Move this import to another file.

*/

Import.prototype.move = function ( newTarget ) {

if( this.target === newTarget ) return;

var old = new Import( this.page, this.wiki, this.url, this.target, this.disabled );

this.target = newTarget;

return $.when( old.uninstall(), this.install() );

}

function getAllTargetWikitexts() {

return $.getJSON(

mw.util.wikiScript( "api" ),

{

format: "json",

action: "query",

prop: "revisions",

rvprop: "content",

rvslots: "main",

titles: SKINS.map( getFullTarget ).join( "|" )

}

).then( function ( data ) {

if( data && data.query && data.query.pages ) {

var result = {};

prefixLength = mw.config.get( "wgUserName" ).length + 6;

Object.values( data.query.pages ).forEach( function ( moreData ) {

var nameWithoutExtension = new mw.Title( moreData.title ).getNameText();

var targetName = nameWithoutExtension.substring( nameWithoutExtension.indexOf( "/" ) + 1 );

result[targetName] = moreData.revisions ? moreData.revisions[0].slots.main["*"] : null;

} );

return result;

}

} );

}

function buildImportList() {

return getAllTargetWikitexts().then( function ( wikitexts ) {

Object.keys( wikitexts ).forEach( function ( targetName ) {

var targetImports = [];

if( wikitexts[ targetName ] ) {

var lines = wikitexts[ targetName ].split( "\n" );

var currImport;

for( var i = 0; i < lines.length; i++ ) {

if( currImport = Import.fromJs( lines[i], targetName ) ) {

targetImports.push( currImport );

scriptCount++;

if( currImport.type === 0 ) {

if( !localScriptsByName[ currImport.page ] )

localScriptsByName[ currImport.page ] = [];

localScriptsByName[ currImport.page ].push( currImport.target );

}

}

}

}

imports[ targetName ] = targetImports;

} );

} );

}

/*

* "Normalizes" (standardizes the format of) lines in the given

* config page.

*/

function normalize( target ) {

return getWikitext( getFullTarget( target ) ).then( function ( wikitext ) {

var lines = wikitext.split( "\n" ),

newLines = Array( lines.length ),

currImport;

for( var i = 0; i < lines.length; i++ ) {

if( currImport = Import.fromJs( lines[i], target ) ) {

newLines[i] = currImport.toJs();

} else {

newLines[i] = lines[i];

}

}

return api.postWithEditToken( {

action: "edit",

title: getFullTarget( target ),

summary: STRINGS.normalizeSummary,

text: newLines.join( "\n" )

} );

} );

}

function conditionalReload( openPanel ) {

if( window.scriptInstallerAutoReload ) {

if( openPanel ) document.cookie = "open_script_installer=yes";

window.location.reload( true );

}

}

/********************************************

*

* UI code

*

********************************************/

function makePanel() {

var list = $( "

" ).attr( "id", "script-installer-panel" )

.append( $( "

" ).text( STRINGS.panelHeader ) );

var container = $( "

" ).addClass( "container" ).appendTo( list );

// Container for checkboxes

container.append( $( "

" )

.attr( "class", "checkbox-container" )

.append(

$( "" )

.attr( { "id": "siNormalize", "type": "checkbox" } )

.click( function () {

$( ".normalize-wrapper" ).toggle( 0 )

} ),

$( "

.attr( "for", "siNormalize" )

.text( STRINGS.showNormalizeLinks ),

$( "" )

.attr( { "id": "siMove", "type": "checkbox" } )

.click( function () {

$( ".move-wrapper" ).toggle( 0 )

} ),

$( "

.attr( "for", "siMove" )

.text( STRINGS.showMoveLinks ) ) );

if( scriptCount > NUM_SCRIPTS_FOR_SEARCH ) {

container.append( $( "

" )

.attr( "class", "filter-container" )

.append(

$( "

.attr( "for", "siQuickFilter" )

.text( STRINGS.quickFilter ),

$( "" )

.attr( { "id": "siQuickFilter", "type": "text" } )

.on( "input", function () {

var filterString = $( this ).val();

if( filterString ) {

var sel = "#script-installer-panel li[name*='" +

$.escapeSelector( $( this ).val() ) + "']";

$( "#script-installer-panel li.script" ).toggle( false );

$( sel ).toggle( true );

} else {

$( "#script-installer-panel li.script" ).toggle( true );

}

} )

) );

// Now, get the checkboxes out of the way

container.find( ".checkbox-container" )

.css( "float", "right" );

}

$.each( imports, function ( targetName, targetImports ) {

var fmtTargetName = ( targetName === "common"

? STRINGS.skinCommon

: targetName );

if( targetImports.length ) {

container.append(

$( "

" ).append(

fmtTargetName,

$( "" )

.addClass( "normalize-wrapper" )

.append(

" (",

$( "" )

.text( STRINGS.normalize )

.click( function () {

normalize( targetName ).done( function () {

conditionalReload( true );

} );

} ),

")" )

.hide() ),

$( "