Commits

Remy Blank  committed ae83aff

util: Fixed `to_json()` and `javascript_quote()` to escape special HTML characters (`&<>"`) so that they aren't escaped by Genshi, and fixed escaping of those same characters when generating elements in !JavaScript through `jQuery()`.

Initial patch by jomae. Closes #9396.

  • Participants
  • Parent commits 0a51064
  • Branches trunk

Comments (0)

Files changed (8)

File trac/htdocs/js/expand_dir.js

   
       tr.addClass("expanded");
       // insert "Loading ..." row
-      var loading_row = $($.template(
+      var loading_row = $($.htmlFormat(
         '<tr>'+
-        ' <td class="$td_class" colspan="$colspan" '+
-        '     style="padding-left: ${depth}px">'+
-        '  <span class="loading">${loading}</span>'+
-        ' </td>'+
+        ' <td class="$td_class" colspan="$colspan" ' +
+        '     style="padding-left: ${depth}px">' +
+        '  <span class="loading">${loading}</span>' +
+        ' </td>' +
         '</tr>', {
         td_class: td_class, 
         colspan: tr.children("td").length, 
         depth: depth, 
-        loading: babel.format(_("Loading %(entry)s..."), {entry: a.text()})
+        loading: babel.format(_("Loading %(entry)s..."), {entry: a.text()}),
       }));
       tr.after(loading_row);
   

File trac/htdocs/js/query.js

         idx = this.name.length;
       var propertyName = this.name.substring(10, idx);
       $(this).replaceWith(
-        $($.template('<input type="button" value="$1">', this.value))
+        $($.htmlFormat('<input type="button" value="$1">', this.value))
           .click(function() { 
                    removeRow(this, propertyName);
                    return false;
     
     // Convenience function for creating a <label>
     function createLabel(text, htmlFor) {
-      var label = $("<label>" + text + "</label>");
+      var label = $($.htmlFormat("<label>$1</label>", text));
       if (htmlFor)
         label.attr("for", htmlFor).addClass("control");
       return label;
     
     // Convenience function for creating an <input type="text">
     function createText(name, size) {
-      return $($.template('<input type="text" name="$1" size="$2">', 
-                          name, size));
+      return $($.htmlFormat('<input type="text" name="$1" size="$2">', 
+                            name, size));
     }
     
     // Convenience function for creating an <input type="checkbox">
     function createCheckbox(name, value, id) {
-      return $($.template('<input type="checkbox"'+
-                          '  id="$1" name="$2" value="$3">',
-                          id, name, value));
+      return $($.htmlFormat('<input type="checkbox" id="$1" name="$2"' +
+                            ' value="$3">', id, name, value));
     }
     
     // Convenience function for creating an <input type="radio">
     function createRadio(name, value, id) {
       // Workaround for IE, otherwise the radio buttons are not selectable
-      return $($.template('<input type="radio" id="$1" name="$2" value="$3">',
-                          id, name, value));
+      return $($.htmlFormat('<input type="radio" id="$1" name="$2"' +
+                            ' value="$3">', id, name, value));
     }
     
     // Convenience function for creating a <select>
     function createSelect(name, options, optional) {
-      var e = $($.template('<select name="$1">', name));
+      var e = $($.htmlFormat('<select name="$1">', name));
       if (optional)
         $("<option>").appendTo(e);
       for (var i = 0; i < options.length; i++) {
         var opt = options[i], v = opt, t = opt;
         if (typeof opt == "object") 
           v = opt.value, t = opt.text;
-        $($.template('<option value="$1">$2</option>', v, t)).appendTo(e);
+        $($.htmlFormat('<option value="$1">$2</option>', v, t)).appendTo(e);
       }
       return e;
     }

File trac/htdocs/js/trac.js

     });
   }
   
-  $.template = function(str) { 
-    var args = arguments, kwargs = arguments[arguments.length-1];
+  // Escape special HTML characters (&<>")
+  var quote = {"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;"};
+
+  $.htmlEscape = function(value) {
+    if (typeof value != "string")
+      return value;
+    return value.replace(/[&<>"]/g, function(c) { return quote[c]; });
+  }
+  
+  function format(str, args, escape) {
+    var kwargs = args[args.length - 1];
     return str.replace(/\${?(\w+)}?/g, function(_, k) {
+      var result;
       if (k.length == 1 && k >= '0' && k <= '9')
-        return args[k-'0'];
+        result = args[k - '0'];
       else
-        return kwargs[k];
+        result = kwargs[k];
+      return escape ? escape(result) : result;
     }); 
   }
-  
+
+  // Expand positional ($1 .. $9) and keyword ($name) arguments in a string.
+  // The htmlFormat() version HTML-escapes arguments prior to substitution.
+  $.format = function(str) {
+    return format(str, arguments);
+  }
+
+  $.htmlFormat = function(str) {
+    return format(str, arguments, $.htmlEscape);
+  }
+
+  $.template = $.format;    // For backward compatibility
+
   // Used for dynamically updating the height of a textarea
   window.resizeTextArea = function (id, rows) {
     var textarea = $("#" + id).get(0);

File trac/ticket/templates/query.html

   <head>
     <title>$title</title>
     <script type="text/javascript" src="${chrome.htdocs_location}js/folding.js"></script>
-    <script type="text/javascript">
+    <script type="text/javascript">/*<![CDATA[*/
       jQuery(document).ready(function($) {
         $("#group").change(function() {
           $("#groupdesc").enable(this.selectedIndex != 0)
         }).change();
         $("fieldset legend.foldable").enableFolding(false);
         /* Hide the filters for saved queries. */
-        if ( window.location.href.search(/[?&amp;]report=[0-9]+/) != -1 )
+        if (window.location.href.search(/[?&]report=[0-9]+/) != -1)
           $("#filters").toggleClass("collapsed");
         /* Hide the columns by default. */
         $("#columns").toggleClass("collapsed");
       });
-    </script>
+    /*]]>*/</script>
   </head>
 
   <body>

File trac/util/presentation.py

 """
 
 from math import ceil
+import re
 
 __all__ = ['classes', 'first_last', 'group', 'istext', 'prepared_paginate', 
            'paginate', 'Paginator']
 
 try:
     from json import dumps
+
+    _js_quote = dict((c, '\\u%04x' % ord(c)) for c in '&<>')
+    _js_quote_re = re.compile('[' + ''.join(_js_quote) + ']')
     
     def to_json(value):
         """Encode `value` to JSON."""
-        return dumps(value, sort_keys=True, separators=(',', ':'))
+        def replace(match):
+            return _js_quote[match.group(0)]
+        text = dumps(value, sort_keys=True, separators=(',', ':'))
+        return _js_quote_re.sub(replace, text)
 
 except ImportError:
     from trac.util.text import javascript_quote

File trac/util/tests/presentation.py

         self.assertEqual('null', presentation.to_json(None))
         self.assertEqual('"String"', presentation.to_json('String'))
         self.assertEqual(r'"a \" quote"', presentation.to_json('a " quote'))
+        self.assertEqual(r'"\u003cb\u003e\u0026\u003c/b\u003e"',
+                         presentation.to_json('<b>&</b>'))
 
     def test_compound_types(self):
         self.assertEqual('[1,2,[true,false]]',
                          presentation.to_json([1, 2, [True, False]]))
-        self.assertEqual('{"one":1,"other":[null,0],"two":2}',
+        self.assertEqual(r'{"one":1,"other":[null,0],'
+                         r'"three":[3,"\u0026\u003c\u003e"],'
+                         r'"two":2}',
                          presentation.to_json({"one": 1, "two": 2,
-                                               "other": [None, 0]}))
+                                               "other": [None, 0],
+                                               "three": [3, "&<>"]}))
 
 
 def suite():

File trac/util/tests/text.py

                          javascript_quote('\\"\b\f\n\r\t\''))
         self.assertEqual(r'\u0002\u001e',
                          javascript_quote('\x02\x1e'))
+        self.assertEqual(r'\u0026\u003c\u003e',
+                         javascript_quote('&<>'))
 
 
 class WhitespaceTestCase(unittest.TestCase):

File trac/util/text.py

 
 _js_quote = {'\\': '\\\\', '"': '\\"', '\b': '\\b', '\f': '\\f',
              '\n': '\\n', '\r': '\\r', '\t': '\\t', "'": "\\'"}
-for i in range(0x20):
+for i in range(0x20) + [ord(c) for c in '&<>']:
     _js_quote.setdefault(chr(i), '\\u%04x' % i)
-_js_quote_re = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t\']')
+_js_quote_re = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t\'&<>]')
 
 def javascript_quote(text):
     """Quote strings for inclusion in javascript"""