From e81c822c8b7e5e21fac519bc0b74cff1809121ba Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Sun, 7 Nov 2010 23:46:57 +0000 Subject: [PATCH] * Moved htmlEscape from mediawiki.util.js to mediawiki.js so that it can be used in the loader. * Added mediaWiki.html.element(), which provides a safe HTML construction function similar to Xml::element(). * Used element() in various places in mediawiki.js. Fixes escaping issue noted on CR r75170. * Profiled the new mediaWiki.html.escape() at 1.4 MB/s for special characters and 154 MB/s for non-special characters on my humble laptop. Hopefully that's fast enough to convince Trevor that escaping is unlikely to be a significant component of page render time. * Profiled mediaWiki.html.element() generating style elements with Cdata at ~17us per iteration. For comparison, $('body').append('
') takes 200us. --- resources/mediawiki.util/mediawiki.util.js | 24 ---- .../mediawiki.util/mediawiki.util.test.js | 6 +- resources/mediawiki/mediawiki.js | 113 ++++++++++++++++-- 3 files changed, 105 insertions(+), 38 deletions(-) diff --git a/resources/mediawiki.util/mediawiki.util.js b/resources/mediawiki.util/mediawiki.util.js index 397828c1ac..c09fc7021a 100644 --- a/resources/mediawiki.util/mediawiki.util.js +++ b/resources/mediawiki.util/mediawiki.util.js @@ -148,30 +148,6 @@ return null; }, - /** - * Convert special characters to their HTML entities - * - * @param str Text to escape - */ - 'htmlEscape': function( str ) { - return str.replace( /['"<>&]/g, this.htmlEscape_callback ); - }, - - 'htmlEscape_callback': function( str ) { - switch ( str ) { - case "'": - return '''; - case '"': - return '"'; - case '<': - return '<'; - case '>': - return '>'; - case '&': - return '&'; - } - }, - // Access key prefix. // Will be re-defined based on browser/operating system detection in // mw.util.init(). diff --git a/resources/mediawiki.util/mediawiki.util.test.js b/resources/mediawiki.util/mediawiki.util.test.js index f60ffc3b3d..389dc2d224 100644 --- a/resources/mediawiki.util/mediawiki.util.test.js +++ b/resources/mediawiki.util/mediawiki.util.test.js @@ -28,7 +28,7 @@ contain = result; } this.addedTests.push([code, result, contain]); - this.$table.append('' + mw.util.htmlEscape(code) + '' + mw.util.htmlEscape(result) + '?'); + this.$table.append('' + mw.html.escape(code) + '' + mw.html.escape(result) + '?'); }, /* Initialisation */ @@ -88,10 +88,6 @@ 'function (string)'); mw.test.addTest('mw.util.getParamValue( \'action\' )', 'mwutiltest (string)'); - mw.test.addTest('typeof mw.util.htmlEscape', - 'function (string)'); - mw.test.addTest('mw.util.htmlEscape( \'link\' )', - '<a href="http://mw.org/?a=b&c=d">link</a> (string)'); mw.test.addTest('mw.util.tooltipAccessKeyRegexp.constructor.name', 'RegExp (string)'); mw.test.addTest('typeof mw.util.updateTooltipAccessKeys', diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index d3f836716c..625cab1dff 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -468,16 +468,18 @@ window.mediaWiki = new ( function( $ ) { // Add style sheet to document if ( typeof registry[module].style === 'string' && registry[module].style.length ) { $( 'head' ) - .append( '' ); + .append( mediaWiki.html.element( 'style', + { type: "text/css" }, + new mediaWiki.html.Cdata( registry[module].style ) + ) ); } else if ( typeof registry[module].style === 'object' && !( registry[module].style instanceof Array ) ) { for ( var media in registry[module].style ) { - $( 'head' ).append( - '' - ); + $( 'head' ).append( mediaWiki.html.element( 'style', + { type: 'text/css', media: media }, + new mediaWiki.html.Cdata( registry[module].style[media] ) + ) ); } } // Add localizations to message system @@ -652,7 +654,8 @@ window.mediaWiki = new ( function( $ ) { requests[r] = sortQuery( requests[r] ); // Build out the HTML var src = mediaWiki.config.get( 'wgLoadScript' ) + '?' + $.param( requests[r] ); - html += ''; + html += mediaWiki.html.element( 'script', + { type: 'text/javascript', src: src }, '' ); } return html; } @@ -711,7 +714,7 @@ window.mediaWiki = new ( function( $ ) { * calls to this function. */ this.implement = function( module, script, style, localization ) { - // Automaically register module + // Automatically register module if ( typeof registry[module] === 'undefined' ) { mediaWiki.loader.register( module ); } @@ -825,7 +828,8 @@ window.mediaWiki = new ( function( $ ) { .attr( 'href', modules ) ); return true; } else if ( type === 'text/javascript' || typeof type === 'undefined' ) { - var script = ''; + var script = mediaWiki.html.element( 'script', + { type: 'text/javascript', src: modules }, '' ); if ( ready ) { $( 'body' ).append( script ); } else { @@ -900,6 +904,97 @@ window.mediaWiki = new ( function( $ ) { $(document).ready( function() { ready = true; } ); } )(); + /** HTML construction helper functions */ + this.html = new ( function () { + function escapeCallback( s ) { + switch ( s ) { + case "'": + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + } + } + + /** + * Escape a string for HTML. Converts special characters to HTML entities. + * @param s The string to escape + */ + this.escape = function( s ) { + return s.replace( /['"<>&]/g, escapeCallback ); + }; + + /** + * Wrapper object for raw HTML passed to mediaWiki.html.element(). + */ + this.Raw = function( value ) { + this.value = value; + }; + + /** + * Wrapper object for CDATA element contents passed to mediaWiki.html.element() + */ + this.Cdata = function( value ) { + this.value = value; + } + + /** + * Create an HTML element string, with safe escaping. + * + * @param name The tag name. + * @param attrs An object with members mapping element names to values + * @param contents The contents of the element. May be either: + * - string: The string is escaped. + * - null or undefined: The short closing form is used, e.g.
. + * - this.Raw: The value attribute is included without escaping. + * - this.Cdata: The value attribute is included, and an exception is + * thrown if it contains an illegal ETAGO delimiter. + * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2 + * + * Example: + * var h = mediaWiki.html; + * return h.element( 'div', {}, + * new h.Raw( h.element( 'img', {src: '<'} ) ) ); + * Returns
+ */ + this.element = function( name, attrs, contents ) { + var s = '<' + name; + for ( attrName in attrs ) { + s += ' ' + attrName + '="' + this.escape( attrs[attrName] ) + '"'; + } + if ( typeof contents == 'undefined' || contents === null ) { + // Short close tag + s += '/>'; + return s; + } + // Regular close tag + s += '>'; + if (typeof contents === 'string') { + // Escaped + s += this.escape( contents ); + } else if ( contents instanceof this.Raw ) { + // Raw HTML inclusion + s += contents.value; + } else if ( contents instanceof this.Cdata ) { + // CDATA + if ( /<\/[a-zA-z]/.test( contents.value ) ) { + throw new Error( 'mw.html.element: Illegal end tag found in CDATA' ); + } + s += contents.value; + } else { + throw new Error( 'mw.html.element: Invalid type of contents' ); + } + s += ''; + return s; + }; + } )(); + + /* Extension points */ this.legacy = {}; -- 2.20.1