MediaWiki talk:Gadget-script-installer-core.js/sandbox
( function () { // An mw.Api object let api;
// Keep "common" at beginning const SKINS = [ 'common', 'monobook', 'minerva', 'vector', 'vector-2022', 'timeless' ];
// How many scripts do we need before we show the quick filter? const NUM_SCRIPTS_FOR_SEARCH = 5;
// The primary import list, keyed by target. (A "target" is a user JS subpage // where the script is imported, like "common" or "vector".) Set in buildImportList const imports = {};
// Local scripts, keyed on name; value will be the target. Set in buildImportList. const localScriptsByName = {};
// How many scripts are installed? let scriptCount = 0;
// Goes on the end of edit summaries const ADVERT = ' (script-installer)';
/** * Strings, for translation */ const 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.' };
const 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: * * 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. * * this.type = 0 if local, 1 if remotely loaded, and 2 if URL. * * @param page a page name, such as "User:Foo/Bar.js". * @param wiki a wiki from which the script is loaded, such as * "en.wikipedia" If null, the script is local, on the user's * wiki. * @param url a URL that can be passed into mw.loader.load. * @param target the title of the user subpage where the script is, * without the .js ending: for example, "common". * @param disabled whether this import is commented out. */ 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; } const URL_RGX = /^(?:https?:)?\/\/(.+?)\.org\/w\/index\.php\?.*?title=(.+?(?:&|$))/; const match = URL_RGX.exec( url ); if ( match ) { const 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 ) { const IMPORT_RGX = /^\s*(\/\/)?\s*importScript\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/; let match = IMPORT_RGX.exec( line ); if ( match ) { return Import.ofLocal( unescapeForJsString( match[ 2 ] ), target, !!match[ 1 ] ); }
const LOADER_RGX = /^\s*(\/\/)?\s*mw\.loader\.load\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/; match = LOADER_RGX.exec( line ); if ( match ) { 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 () { const dis = this.disabled ? '//' : ; let 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' ); } let 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; } const lineNums = [], lines = targetWikitext.split( '\n' ); for ( let 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 () { const that = this; return getWikitext( getFullTarget( this.target ) ).then( ( wikitext ) => { const lineNums = that.getLineNums( wikitext ), newWikitext = wikitext.split( '\n' ).filter( ( _, idx ) => 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 ) { const that = this; this.disabled = disabled; return getWikitext( getFullTarget( this.target ) ).then( ( wikitext ) => { const lineNums = that.getLineNums( wikitext ), newWikitextLines = wikitext.split( '\n' );
if ( disabled ) { lineNums.forEach( ( lineNum ) => { if ( newWikitextLines[ lineNum ].trim().indexOf( '//' ) !== 0 ) { newWikitextLines[ lineNum ] = '//' + newWikitextLines[ lineNum ].trim(); } } ); } else { lineNums.forEach( ( lineNum ) => { if ( newWikitextLines[ lineNum ].trim().indexOf( '//' ) === 0 ) { newWikitextLines[ lineNum ] = newWikitextLines[ lineNum ].replace( /^\s*\/\/\s*/, ); } } ); }
const 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; } const 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( ( data ) => { if ( data && data.query && data.query.pages ) { const result = {}; Object.values( data.query.pages ).forEach( ( moreData ) => { const nameWithoutExtension = new mw.Title( moreData.title ).getNameText(); const targetName = nameWithoutExtension.substring( nameWithoutExtension.indexOf( '/' ) + 1 ); result[ targetName ] = moreData.revisions ? moreData.revisions[ 0 ].slots.main[ '*' ] : null; } ); return result; } } ); }
function buildImportList() { return getAllTargetWikitexts().then( ( wikitexts ) => { Object.keys( wikitexts ).forEach( ( targetName ) => { const targetImports = []; if ( wikitexts[ targetName ] ) { const lines = wikitexts[ targetName ].split( '\n' ); let currImport; for ( let i = 0; i < lines.length; i++ ) { currImport = Import.fromJs( lines[ i ], targetName ); if ( currImport ) { 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( ( wikitext ) => { const lines = wikitext.split( '\n' ), newLines = Array( lines.length ); let currImport; for ( let i = 0; i < lines.length; i++ ) { currImport = Import.fromJs( lines[ i ], target ); if ( currImport ) { 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() {
const $list = $( '
.append( $( '<header>' ).text( STRINGS.panelHeader ) );
const $container = $( '// Container for checkboxes
$container.append( $( '.attr( 'class', 'checkbox-container' ) .append( $( '<input>' ) .attr( { id: 'siNormalize', type: 'checkbox' } ) .on( 'click', () => { $( '.normalize-wrapper' ).toggle( 0 ); } ), $( '<label>' ) .attr( 'for', 'siNormalize' ) .text( STRINGS.showNormalizeLinks ), $( '<input>' ) .attr( { id: 'siMove', type: 'checkbox' } ) .on( 'click', () => { $( '.move-wrapper' ).toggle( 0 ); } ), $( '<label>' ) .attr( 'for', 'siMove' ) .text( STRINGS.showMoveLinks ) ) ); if ( scriptCount > NUM_SCRIPTS_FOR_SEARCH ) {
$container.append( $( '.attr( 'class', 'filter-container' ) .append( $( '<label>' ) .attr( 'for', 'siQuickFilter' ) .text( STRINGS.quickFilter ), $( '<input>' ) .attr( { id: 'siQuickFilter', type: 'text' } ) .on( 'input', function () { const filterString = $( this ).val(); if ( filterString ) { const 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, ( targetName, targetImports ) => { const fmtTargetName = ( targetName === 'common' ? STRINGS.skinCommon : targetName ); if ( targetImports.length ) { $container.append(
$( '' ).append(
fmtTargetName,
$( '' )
.addClass( 'normalize-wrapper' )
.append(
' (',
$( '<a>' )
.text( STRINGS.normalize )
.on( 'click', () => {
normalize( targetName ).done( () => {
conditionalReload( true );
} );
} ),
')' )
.hide() ),
$( '' ).append(
targetImports.map( ( anImport ) => $( '- ' )
.addClass( 'script' )
.attr( 'name', anImport.getDescription() )
.append(
$( '<a>' )
.text( anImport.getDescription() )
.addClass( 'script' )
.attr( 'href', anImport.getHumanUrl() ),
' (',
$( '<a>' )
.text( STRINGS.uninstallLinkText )
.on( 'click', function () {
$( this ).text( STRINGS.uninstallProgressMsg );
anImport.uninstall().done( () => {
conditionalReload( true );
} );
} ),
' | ',
$( '<a>' )
.text( anImport.disabled ? STRINGS.enableLinkText : STRINGS.disableLinkText )
.on( 'click', function () {
$( this ).text( anImport.disabled ? STRINGS.enableProgressMsg : STRINGS.disableProgressMsg );
anImport.toggleDisabled().done( function () {
$( this ).toggleClass( 'disabled' );
conditionalReload( true );
} );
} ),
$( '' )
.addClass( 'move-wrapper' )
.append(
' | ',
$( '<a>' )
.text( STRINGS.moveLinkText )
.on( 'click', function () {
let dest = null;
const PROMPT = STRINGS.movePrompt + ' ' + SKINS.join( ', ' );
do {
dest = ( window.prompt( PROMPT ) || ).toLowerCase();
} while ( dest && SKINS.indexOf( dest ) < 0 );
if ( !dest ) {
return;
}
$( this ).text( STRINGS.moveProgressMsg );
anImport.move( dest ).done( () => {
conditionalReload( true );
} );
} )
)
.hide(),
')' )
.toggleClass( 'disabled', anImport.disabled ) ) ) );
}
} );
return $list;
}
function buildCurrentPageInstallElement() {
let addingInstallLink = false; // will we be adding a legitimate install link?
const $installElement = $( '' ); // only used if addingInstallLink is set to true
const namespaceNumber = mw.config.get( 'wgNamespaceNumber' );
const pageName = mw.config.get( 'wgPageName' );
// Namespace 2 is User
if ( namespaceNumber === 2 &&
pageName.indexOf( '/' ) > 0 ) {
const contentModel = mw.config.get( 'wgPageContentModel' );
if ( contentModel === 'javascript' ) {
const prefixLength = mw.config.get( 'wgUserName' ).length + 6;
if ( pageName.indexOf( USER_NAMESPACE_NAME + ':' + mw.config.get( 'wgUserName' ) ) === 0 ) {
const skinIndex = SKINS.indexOf( pageName.substring( prefixLength ).slice( 0, -3 ) );
if ( skinIndex >= 0 ) {
return $( '' ).text( STRINGS.cannotInstall )
.attr( 'title', STRINGS.cannotInstallSkin );
}
}
addingInstallLink = true;
} else {
return $( '' ).text( STRINGS.cannotInstall + ' (' + STRINGS.notJavaScript + ')' )
.attr( 'title', STRINGS.cannotInstallContentModel.replace( '$1', contentModel ) );
}
}
// Namespace 8 is MediaWiki
if ( namespaceNumber === 8 ) {
return $( '<a>' ).text( STRINGS.installViaPreferences )
.attr( 'href', mw.util.getUrl( 'Special:Preferences' ) + '#mw-prefsection-gadgets' );
}
const editRestriction = mw.config.get( 'wgRestrictionEdit' ) || [];
if ( ( namespaceNumber !== 2 && namespaceNumber !== 8 ) &&
( editRestriction.indexOf( 'sysop' ) >= 0 ||
editRestriction.indexOf( 'editprotected' ) >= 0 ) ) {
$installElement.append( ' ',
$( '' ).append(
$( '<img>' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Achtung-yellow.svg/20px-Achtung-yellow.svg.png' ).addClass( 'warning' ),
STRINGS.insecure )
.attr( 'title', STRINGS.tempWarning ) );
addingInstallLink = true;
}
if ( addingInstallLink ) {
const fixedPageName = mw.config.get( 'wgPageName' ).replace( /_/g, ' ' );
$installElement.prepend( $( '<a>' )
.attr( 'id', 'script-installer-main-install' )
.text( localScriptsByName[ fixedPageName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText )
.on( 'click', makeLocalInstallClickHandler( fixedPageName ) ) );
// If the script is installed but disabled, allow the user to enable it
const allScriptsInTarget = imports[ localScriptsByName[ fixedPageName ] ];
const importObj = allScriptsInTarget && allScriptsInTarget.find( ( anImport ) => anImport.page === fixedPageName );
if ( importObj && importObj.disabled ) {
$installElement.append( ' | ',
$( '<a>' )
.attr( 'id', 'script-installer-main-enable' )
.text( STRINGS.enableLinkText )
.on( 'click', function () {
$( this ).text( STRINGS.enableProgressMsg );
importObj.setDisabled( false ).done( () => {
conditionalReload( false );
} );
} ) );
}
return $installElement;
}
return $( '' ).text( STRINGS.cannotInstall + ' ' + STRINGS.insecure )
.attr( 'title', STRINGS.badPageError );
}
function showUi() {
$( '#firstHeading' ).append( $( '' )
.attr( 'id', 'script-installer-top-container' )
.append(
buildCurrentPageInstallElement(),
' | ',
$( '<a>' )
.text( STRINGS.manageUserScripts ).on( 'click', () => {
if ( !document.getElementById( 'script-installer-panel' ) ) {
$( '#mw-content-text' ).before( makePanel() );
} else {
$( '#script-installer-panel' ).remove();
}
} ) ) );
}
function attachInstallLinks() {
// At the end of each [1] (source) transclusion, there is
//
$( 'span.scriptInstallerLink' ).each( function () {
const scriptName = this.id.replace( /_/g, ' ' ); // if the script name happens to contain underscores, this purges it
$( this ).append( ' | ', $( '<a>' )
.text( localScriptsByName[ scriptName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText )
.on( 'click', makeLocalInstallClickHandler( scriptName ) ) );
} );
$( 'table.infobox-user-script' ).each( function () {
const $infoboxScriptField = $( this ).find( "th:contains('Source')" ).next();
let scriptName = mw.config.get( 'wgPageName' );
const isHyperlink = $infoboxScriptField.html() !== $infoboxScriptField.text();
if ( isHyperlink ) {
const $link = $infoboxScriptField.find( 'a' ).first();
const isExternalLink = $link.hasClass( 'external' );
if ( !isExternalLink ) {
scriptName = /\/wiki\/(.*)/.exec( $link.attr( 'href' ) )[ 1 ];
}
} else {
scriptName = $infoboxScriptField.text();
}
scriptName = scriptName.replace( /_/g, ' ' ); // if the script name happens to contain underscores, this purges it
scriptName = /user:.+?\/.+?.js/i.exec( scriptName )[ 0 ];
$( this ).children( 'tbody' ).append( $( '' ).append( $( '' )
.attr( 'colspan', '2' )
.addClass( 'script-installer-ibx' )
.append( $( '<button>' )
.addClass( 'mw-ui-button mw-ui-progressive mw-ui-big' )
.text( localScriptsByName[ scriptName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText )
.on( 'click', makeLocalInstallClickHandler( scriptName ) ) ) ) );
} );
}
function makeLocalInstallClickHandler( scriptName ) {
return function () {
const $this = $( this );
if ( $this.text() === STRINGS.installLinkText ) {
const okay = window.confirm(
STRINGS.bigSecurityWarning.replace( '$1',
STRINGS.securityWarningSection.replace( '$1', scriptName ) ) );
if ( okay ) {
$( this ).text( STRINGS.installProgressMsg );
Import.ofLocal( scriptName, window.scriptInstallerInstallTarget ).install().done( () => {
$( this ).text( STRINGS.uninstallLinkText );
conditionalReload( false );
} );
}
} else {
$( this ).text( STRINGS.uninstallProgressMsg );
const uninstalls = uniques( localScriptsByName[ scriptName ] )
.map( ( target ) => Import.ofLocal( scriptName, target ).uninstall() );
$.when.apply( $, uninstalls ).then( () => {
$( this ).text( STRINGS.installLinkText );
conditionalReload( false );
} );
}
};
}
/********************************************
*
* Utility functions
*
********************************************/
/**
* Gets the wikitext of a page with the given title (namespace required).
*/
function getWikitext( title ) {
return $.getJSON(
mw.util.wikiScript( 'api' ),
{
format: 'json',
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
rvlimit: 1,
titles: title
}
).then( ( data ) => {
const pageId = Object.keys( data.query.pages )[ 0 ];
if ( data.query.pages[ pageId ].revisions ) {
return data.query.pages[ pageId ].revisions[ 0 ].slots.main[ '*' ];
}
return ;
} );
}
function escapeForRegex( s ) {
return s.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
}
/**
* Escape a string for use in a JavaScript string literal.
* This function is adapted from
* https://github.com/joliss/js-string-escape/blob/6887a69003555edf5c6caaa75f2592228558c595/index.js
* (released under the MIT licence).
*/
function escapeForJsString( s ) {
return s.replace( /["'\\\n\r\u2028\u2029]/g, ( character ) => {
// Escape all characters not included in SingleStringCharacters and
// DoubleStringCharacters on
// http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
switch ( character ) {
case '"':
case "'":
case '\\':
return '\\' + character;
// Four possible LineTerminator characters need to be escaped:
case '\n':
return '\\n';
case '\r':
return '\\r';
case '\u2028':
return '\\u2028';
case '\u2029':
return '\\u2029';
}
} );
}
/**
* Escape a string for use in an inline JavaScript comment (comments that
* start with two slashes "//").
* This function is adapted from
* https://github.com/joliss/js-string-escape/blob/6887a69003555edf5c6caaa75f2592228558c595/index.js
* (released under the MIT licence).
*/
function escapeForJsComment( s ) {
return s.replace( /[\n\r\u2028\u2029]/g, ( character ) => {
switch ( character ) {
// Escape possible LineTerminator characters
case '\n':
return '\\n';
case '\r':
return '\\r';
case '\u2028':
return '\\u2028';
case '\u2029':
return '\\u2029';
}
} );
}
/**
* Unescape a JavaScript string literal.
*
* This is the inverse of escapeForJsString.
*/
function unescapeForJsString( s ) {
return s.replace( /\\"|\\'|\\\\|\\n|\\r|\\u2028|\\u2029/g, ( substring ) => {
switch ( substring ) {
case '\\"':
return '"';
case "\\'":
return "'";
case '\\\\':
return '\\';
case '\\r':
return '\r';
case '\\n':
return '\n';
case '\\u2028':
return '\u2028';
case '\\u2029':
return '\u2029';
}
} );
}
function getFullTarget( target ) {
return USER_NAMESPACE_NAME + ':' + mw.config.get( 'wgUserName' ) + '/' +
target + '.js';
}
// From https://stackoverflow.com/a/10192255
function uniques( array ) {
return array.filter( ( el, index, arr ) => index === arr.indexOf( el ) );
}
if ( window.scriptInstallerAutoReload === undefined ) {
window.scriptInstallerAutoReload = true;
}
if ( window.scriptInstallerInstallTarget === undefined ) {
window.scriptInstallerInstallTarget = 'common'; // by default, install things to the user's common.js
}
const jsPage = mw.config.get( 'wgPageName' ).slice( -3 ) === '.js' ||
mw.config.get( 'wgPageContentModel' ) === 'javascript';
$.when(
$.ready,
mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] )
).then( () => {
api = new mw.Api();
buildImportList().then( () => {
attachInstallLinks();
if ( jsPage ) {
showUi();
}
// Auto-open the panel if we set the cookie to do so (see `conditionalReload()`)
if ( document.cookie.indexOf( 'open_script_installer=yes' ) >= 0 ) {
document.cookie = 'open_script_installer=; expires=Thu, 01 Jan 1970 00:00:01 GMT';
$( "#script-installer-top-container a:contains('Manage')" ).trigger( 'click' );
}
} );
} );
}() );
- ^ Copy the following code, edit your user JavaScript, then paste:
{{subst:lusc|1= .js}}
.attr( 'colspan', '2' ) .addClass( 'script-installer-ibx' ) .append( $( '<button>' ) .addClass( 'mw-ui-button mw-ui-progressive mw-ui-big' ) .text( localScriptsByName[ scriptName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText ) .on( 'click', makeLocalInstallClickHandler( scriptName ) ) ) ) ); } ); }
function makeLocalInstallClickHandler( scriptName ) { return function () { const $this = $( this ); if ( $this.text() === STRINGS.installLinkText ) { const okay = window.confirm( STRINGS.bigSecurityWarning.replace( '$1', STRINGS.securityWarningSection.replace( '$1', scriptName ) ) ); if ( okay ) { $( this ).text( STRINGS.installProgressMsg ); Import.ofLocal( scriptName, window.scriptInstallerInstallTarget ).install().done( () => { $( this ).text( STRINGS.uninstallLinkText ); conditionalReload( false ); } ); } } else { $( this ).text( STRINGS.uninstallProgressMsg ); const uninstalls = uniques( localScriptsByName[ scriptName ] ) .map( ( target ) => Import.ofLocal( scriptName, target ).uninstall() ); $.when.apply( $, uninstalls ).then( () => { $( this ).text( STRINGS.installLinkText ); conditionalReload( false ); } ); } }; }
/******************************************** * * Utility functions * ********************************************/
/** * Gets the wikitext of a page with the given title (namespace required). */ function getWikitext( title ) { return $.getJSON( mw.util.wikiScript( 'api' ), { format: 'json', action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', rvlimit: 1, titles: title } ).then( ( data ) => { const pageId = Object.keys( data.query.pages )[ 0 ]; if ( data.query.pages[ pageId ].revisions ) { return data.query.pages[ pageId ].revisions[ 0 ].slots.main[ '*' ]; } return ; } ); }
function escapeForRegex( s ) { return s.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' ); }
/** * Escape a string for use in a JavaScript string literal. * This function is adapted from * https://github.com/joliss/js-string-escape/blob/6887a69003555edf5c6caaa75f2592228558c595/index.js * (released under the MIT licence). */ function escapeForJsString( s ) { return s.replace( /["'\\\n\r\u2028\u2029]/g, ( character ) => { // Escape all characters not included in SingleStringCharacters and // DoubleStringCharacters on // http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4 switch ( character ) { case '"': case "'": case '\\': return '\\' + character; // Four possible LineTerminator characters need to be escaped: case '\n': return '\\n'; case '\r': return '\\r'; case '\u2028': return '\\u2028'; case '\u2029': return '\\u2029'; } } ); }
/** * Escape a string for use in an inline JavaScript comment (comments that * start with two slashes "//"). * This function is adapted from * https://github.com/joliss/js-string-escape/blob/6887a69003555edf5c6caaa75f2592228558c595/index.js * (released under the MIT licence). */ function escapeForJsComment( s ) { return s.replace( /[\n\r\u2028\u2029]/g, ( character ) => { switch ( character ) { // Escape possible LineTerminator characters case '\n': return '\\n'; case '\r': return '\\r'; case '\u2028': return '\\u2028'; case '\u2029': return '\\u2029'; } } ); }
/** * Unescape a JavaScript string literal. * * This is the inverse of escapeForJsString. */ function unescapeForJsString( s ) { return s.replace( /\\"|\\'|\\\\|\\n|\\r|\\u2028|\\u2029/g, ( substring ) => { switch ( substring ) { case '\\"': return '"'; case "\\'": return "'"; case '\\\\': return '\\'; case '\\r': return '\r'; case '\\n': return '\n'; case '\\u2028': return '\u2028'; case '\\u2029': return '\u2029'; } } ); }
function getFullTarget( target ) { return USER_NAMESPACE_NAME + ':' + mw.config.get( 'wgUserName' ) + '/' +
target + '.js';
}
// From https://stackoverflow.com/a/10192255 function uniques( array ) { return array.filter( ( el, index, arr ) => index === arr.indexOf( el ) ); }
if ( window.scriptInstallerAutoReload === undefined ) { window.scriptInstallerAutoReload = true; }
if ( window.scriptInstallerInstallTarget === undefined ) { window.scriptInstallerInstallTarget = 'common'; // by default, install things to the user's common.js }
const jsPage = mw.config.get( 'wgPageName' ).slice( -3 ) === '.js' ||
mw.config.get( 'wgPageContentModel' ) === 'javascript';
$.when( $.ready, mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ) ).then( () => { api = new mw.Api(); buildImportList().then( () => { attachInstallLinks(); if ( jsPage ) { showUi(); }
// Auto-open the panel if we set the cookie to do so (see `conditionalReload()`) if ( document.cookie.indexOf( 'open_script_installer=yes' ) >= 0 ) { document.cookie = 'open_script_installer=; expires=Thu, 01 Jan 1970 00:00:01 GMT'; $( "#script-installer-top-container a:contains('Manage')" ).trigger( 'click' ); } } ); } ); }() );
- ^ Copy the following code, edit your user JavaScript, then paste:
{{subst:lusc|1= .js}}