r76283 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r76282‎ | r76283 | r76284 >
Date:23:46, 7 November 2010
Author:tstarling
Status:ok (Comments)
Tags:
Comment:
* 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('<div/>') takes 200us.
Modified paths:
  • /trunk/phase3/resources/mediawiki.util/mediawiki.util.js (modified) (history)
  • /trunk/phase3/resources/mediawiki.util/mediawiki.util.test.js (modified) (history)
  • /trunk/phase3/resources/mediawiki/mediawiki.js (modified) (history)

Diff [purge]

Index: trunk/phase3/resources/mediawiki.util/mediawiki.util.test.js
@@ -28,7 +28,7 @@
2929 contain = result;
3030 }
3131 this.addedTests.push([code, result, contain]);
32 - this.$table.append('<tr><td>' + mw.util.htmlEscape(code) + '</td><td>' + mw.util.htmlEscape(result) + '<td></td></td><td>?</td></tr>');
 32+ this.$table.append('<tr><td>' + mw.html.escape(code) + '</td><td>' + mw.html.escape(result) + '<td></td></td><td>?</td></tr>');
3333 },
3434
3535 /* Initialisation */
@@ -88,10 +88,6 @@
8989 'function (string)');
9090 mw.test.addTest('mw.util.getParamValue( \'action\' )',
9191 'mwutiltest (string)');
92 - mw.test.addTest('typeof mw.util.htmlEscape',
93 - 'function (string)');
94 - mw.test.addTest('mw.util.htmlEscape( \'<a href="http://mw.org/?a=b&c=d">link</a>\' )',
95 - '&lt;a href=&quot;http://mw.org/?a=b&amp;c=d&quot;&gt;link&lt;/a&gt; (string)');
9692 mw.test.addTest('mw.util.tooltipAccessKeyRegexp.constructor.name',
9793 'RegExp (string)');
9894 mw.test.addTest('typeof mw.util.updateTooltipAccessKeys',
Index: trunk/phase3/resources/mediawiki.util/mediawiki.util.js
@@ -148,30 +148,6 @@
149149 return null;
150150 },
151151
152 - /**
153 - * Convert special characters to their HTML entities
154 - *
155 - * @param str Text to escape
156 - */
157 - 'htmlEscape': function( str ) {
158 - return str.replace( /['"<>&]/g, this.htmlEscape_callback );
159 - },
160 -
161 - 'htmlEscape_callback': function( str ) {
162 - switch ( str ) {
163 - case "'":
164 - return '&#039;';
165 - case '"':
166 - return '&quot;';
167 - case '<':
168 - return '&lt;';
169 - case '>':
170 - return '&gt;';
171 - case '&':
172 - return '&amp;';
173 - }
174 - },
175 -
176152 // Access key prefix.
177153 // Will be re-defined based on browser/operating system detection in
178154 // mw.util.init().
Index: trunk/phase3/resources/mediawiki/mediawiki.js
@@ -468,16 +468,18 @@
469469 // Add style sheet to document
470470 if ( typeof registry[module].style === 'string' && registry[module].style.length ) {
471471 $( 'head' )
472 - .append( '<style type="text/css">' + registry[module].style + '</style>' );
 472+ .append( mediaWiki.html.element( 'style',
 473+ { type: "text/css" },
 474+ new mediaWiki.html.Cdata( registry[module].style )
 475+ ) );
473476 } else if ( typeof registry[module].style === 'object'
474477 && !( registry[module].style instanceof Array ) )
475478 {
476479 for ( var media in registry[module].style ) {
477 - $( 'head' ).append(
478 - '<style type="text/css" media="' + media + '">' +
479 - registry[module].style[media] +
480 - '</style>'
481 - );
 480+ $( 'head' ).append( mediaWiki.html.element( 'style',
 481+ { type: 'text/css', media: media },
 482+ new mediaWiki.html.Cdata( registry[module].style[media] )
 483+ ) );
482484 }
483485 }
484486 // Add localizations to message system
@@ -652,7 +654,8 @@
653655 requests[r] = sortQuery( requests[r] );
654656 // Build out the HTML
655657 var src = mediaWiki.config.get( 'wgLoadScript' ) + '?' + $.param( requests[r] );
656 - html += '<script type="text/javascript" src="' + src + '"></script>';
 658+ html += mediaWiki.html.element( 'script',
 659+ { type: 'text/javascript', src: src }, '' );
657660 }
658661 return html;
659662 }
@@ -711,7 +714,7 @@
712715 * calls to this function.
713716 */
714717 this.implement = function( module, script, style, localization ) {
715 - // Automaically register module
 718+ // Automatically register module
716719 if ( typeof registry[module] === 'undefined' ) {
717720 mediaWiki.loader.register( module );
718721 }
@@ -825,7 +828,8 @@
826829 .attr( 'href', modules ) );
827830 return true;
828831 } else if ( type === 'text/javascript' || typeof type === 'undefined' ) {
829 - var script = '<script type="text/javascript" src="' + modules + '"></script>';
 832+ var script = mediaWiki.html.element( 'script',
 833+ { type: 'text/javascript', src: modules }, '' );
830834 if ( ready ) {
831835 $( 'body' ).append( script );
832836 } else {
@@ -900,6 +904,97 @@
901905 $(document).ready( function() { ready = true; } );
902906 } )();
903907
 908+ /** HTML construction helper functions */
 909+ this.html = new ( function () {
 910+ function escapeCallback( s ) {
 911+ switch ( s ) {
 912+ case "'":
 913+ return '&#039;';
 914+ case '"':
 915+ return '&quot;';
 916+ case '<':
 917+ return '&lt;';
 918+ case '>':
 919+ return '&gt;';
 920+ case '&':
 921+ return '&amp;';
 922+ }
 923+ }
 924+
 925+ /**
 926+ * Escape a string for HTML. Converts special characters to HTML entities.
 927+ * @param s The string to escape
 928+ */
 929+ this.escape = function( s ) {
 930+ return s.replace( /['"<>&]/g, escapeCallback );
 931+ };
 932+
 933+ /**
 934+ * Wrapper object for raw HTML passed to mediaWiki.html.element().
 935+ */
 936+ this.Raw = function( value ) {
 937+ this.value = value;
 938+ };
 939+
 940+ /**
 941+ * Wrapper object for CDATA element contents passed to mediaWiki.html.element()
 942+ */
 943+ this.Cdata = function( value ) {
 944+ this.value = value;
 945+ }
 946+
 947+ /**
 948+ * Create an HTML element string, with safe escaping.
 949+ *
 950+ * @param name The tag name.
 951+ * @param attrs An object with members mapping element names to values
 952+ * @param contents The contents of the element. May be either:
 953+ * - string: The string is escaped.
 954+ * - null or undefined: The short closing form is used, e.g. <br/>.
 955+ * - this.Raw: The value attribute is included without escaping.
 956+ * - this.Cdata: The value attribute is included, and an exception is
 957+ * thrown if it contains an illegal ETAGO delimiter.
 958+ * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
 959+ *
 960+ * Example:
 961+ * var h = mediaWiki.html;
 962+ * return h.element( 'div', {},
 963+ * new h.Raw( h.element( 'img', {src: '<'} ) ) );
 964+ * Returns <div><img src="&lt;"/></div>
 965+ */
 966+ this.element = function( name, attrs, contents ) {
 967+ var s = '<' + name;
 968+ for ( attrName in attrs ) {
 969+ s += ' ' + attrName + '="' + this.escape( attrs[attrName] ) + '"';
 970+ }
 971+ if ( typeof contents == 'undefined' || contents === null ) {
 972+ // Short close tag
 973+ s += '/>';
 974+ return s;
 975+ }
 976+ // Regular close tag
 977+ s += '>';
 978+ if (typeof contents === 'string') {
 979+ // Escaped
 980+ s += this.escape( contents );
 981+ } else if ( contents instanceof this.Raw ) {
 982+ // Raw HTML inclusion
 983+ s += contents.value;
 984+ } else if ( contents instanceof this.Cdata ) {
 985+ // CDATA
 986+ if ( /<\/[a-zA-z]/.test( contents.value ) ) {
 987+ throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
 988+ }
 989+ s += contents.value;
 990+ } else {
 991+ throw new Error( 'mw.html.element: Invalid type of contents' );
 992+ }
 993+ s += '</' + name + '>';
 994+ return s;
 995+ };
 996+ } )();
 997+
 998+
904999 /* Extension points */
9051000
9061001 this.legacy = {};

Follow-up revisions

RevisionCommit summaryAuthorDate
r76316Making comments added in r76283 more clear + added mw.html to test suitekrinkle17:01, 8 November 2010

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r75170ResourceLoaderFileModule now uses mediaWiki.loader.load to load scripts in de...tparscal21:25, 21 October 2010

Comments

#Comment by Trevor Parscal (WMF) (talk | contribs)   18:27, 8 November 2010

"Hopefully that's fast enough to convince Trevor that escaping is unlikely to be a significant component of page render time" .... I'm not so sure I ever claimed that escaping was being avoided because of performance concerns.

In any event, this is a cool utility, awesome work.

Status & tagging log