From 408b1e65a44596967f978ac7936bcf6fe9fd261c Mon Sep 17 00:00:00 2001 From: "Mr. Stradivarius" Date: Fri, 4 Feb 2022 11:33:14 +0900 Subject: [PATCH] Escape output in Import.toJs We need to escape JavaScript strings, JavaScript comments, and URL components. For the first two, we make our own custom functions based on the js-string-escape library. For the last one, we can use encodeURIComponent. Also, unescape the input when reading from JavaScript files so that round-tripping isn't broken. --- MediaWiki:Gadget-script-installer-core.js | 100 +++++++++++++++++++--- 1 file changed, 89 insertions(+), 11 deletions(-) diff --git a/MediaWiki:Gadget-script-installer-core.js b/MediaWiki:Gadget-script-installer-core.js index dc9de4b..4be1ab1 100644 --- a/MediaWiki:Gadget-script-installer-core.js +++ b/MediaWiki:Gadget-script-installer-core.js @@ -100,7 +100,7 @@ var match; if( match = URL_RGX.exec( url ) ) { var title = decodeURIComponent( match[2].replace( /&$/, "" ) ), - wiki = match[1]; + wiki = decodeURIComponent( match[1] ); return new Import( title, wiki, null, target, disabled ); } return new Import( null, null, url, target, disabled ); @@ -110,12 +110,12 @@ var IMPORT_RGX = /^\s*(\/\/)?\s*importScript\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/; var match; if( match = IMPORT_RGX.exec( line ) ) { - return Import.ofLocal( match[2], target, !!match[1] ); + 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( match[2], target, !!match[1] ); + return Import.ofUrl( unescapeForJsString( match[2] ), target, !!match[1] ); } } @@ -142,11 +142,11 @@ var dis = this.disabled ? "//" : "", url = this.url; switch( this.type ) { - case 0: return dis + "importScript('" + this.page + "'); // Backlink: [[" + this.page + "]]"; - case 1: url = "//" + this.wiki + ".org/w/index.php?title=" + - this.page + "&action=raw&ctype=text/javascript"; + case 0: return dis + "importScript('" + escapeForJsString( this.page ) + "'); // 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('" + url + "');"; + case 2: return dis + "mw.loader.load('" + escapeForJsString( url ) + "');"; } } @@ -172,10 +172,10 @@ } var toFind; switch( this.type ) { - case 0: toFind = quoted( this.page ); break; - case 1: toFind = new RegExp( escapeForRegex( this.wiki ) + ".*?" + - escapeForRegex( this.page ) ); break; - case 2: toFind = quoted( this.url ); break; + 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++ ) { @@ -677,6 +677,84 @@ 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, function ( 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, function ( 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, function ( 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"; -- 2.25.1