From 9c4b458d2af8f9200be3dfb900da74a8187f2b8e Mon Sep 17 00:00:00 2001 From: Samuel Walladge Date: Fri, 7 Feb 2020 14:56:29 +1030 Subject: [PATCH] Fix elem not selected if id contains special chars If the id of a `.formulaequationinput input` element contains a special character, then the selector for $preview was silently failing to match the element, because no escaping was happening. This fixes the issue by escaping the id before passing to the jQuery selector function. CSS.escape is the ideal method, but this isn't present in IE or Edge, so we use a fallback borrowed from the new jQuery.escapeSelector method. --- .../spec/formula_equation_preview_spec.js | 26 ++++++++++++++ .../js/capa/src/formula_equation_preview.js | 36 ++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/common/static/js/capa/spec/formula_equation_preview_spec.js b/common/static/js/capa/spec/formula_equation_preview_spec.js index ad0a903e03..5e9cf7a487 100644 --- a/common/static/js/capa/spec/formula_equation_preview_spec.js +++ b/common/static/js/capa/spec/formula_equation_preview_spec.js @@ -1,3 +1,29 @@ +describe('escapeSelector', function() { + 'use strict'; + var escapeSelector = window.escapeSelector; + + it('correctly escapes css', function() { + // tests borrowed from https://github.com/jquery/jquery/blob/3edfa1bcdc50bca41ac58b2642b12f3feee03a3b/test/unit/selector.js#L2030 + expect(escapeSelector('-')).toEqual('\\-'); + expect(escapeSelector('-a')).toEqual('-a'); + expect(escapeSelector('--')).toEqual('--'); + expect(escapeSelector('--a')).toEqual('--a'); + expect(escapeSelector('\uFFFD')).toEqual('\uFFFD'); + expect(escapeSelector('\uFFFDb')).toEqual('\uFFFDb'); + expect(escapeSelector('a\uFFFDb')).toEqual('a\uFFFDb'); + expect(escapeSelector('1a')).toEqual('\\31 a'); + expect(escapeSelector('a\0b')).toEqual('a\uFFFDb'); + expect(escapeSelector('a3b')).toEqual('a3b'); + expect(escapeSelector('-4a')).toEqual('-\\34 a'); + expect(escapeSelector('\x01\x02\x1E\x1F')).toEqual('\\1 \\2 \\1e \\1f '); + + // This is the important one; xblocks and course ids often contain invalid characters, so if these aren't + // escaped when embedding/searching xblock IDs using css selectors, bad things happen. + expect(escapeSelector('course-v1:edX+DemoX+Demo_Course')).toEqual('course-v1\\:edX\\+DemoX\\+Demo_Course'); + expect(escapeSelector('block-v1:edX+DemoX+Demo_Course+type@sequential+block')).toEqual('block-v1\\:edX\\+DemoX\\+Demo_Course\\+type\\@sequential\\+block'); + }); +}); + describe('Formula Equation Preview', function() { 'use strict'; var formulaEquationPreview = window.formulaEquationPreview; diff --git a/common/static/js/capa/src/formula_equation_preview.js b/common/static/js/capa/src/formula_equation_preview.js index 646ba0809c..1838bf498b 100644 --- a/common/static/js/capa/src/formula_equation_preview.js +++ b/common/static/js/capa/src/formula_equation_preview.js @@ -1,3 +1,37 @@ +function escapeSelector(id) { + // Wrapper around window.CSS.escape that uses a fallback method if CSS.escape is not available. + // This is designed to serialize a string to be used as a valid css selector. See https://drafts.csswg.org/cssom/#the-css.escape()-method + // For example, can be used with xblock and course ids, which often contain invalid characters that must be escaped + // to function properly in css selectors. + + // TODO: if this escaping is also required elsewhere, it may be useful to add a global CSS.escape polyfill and + // use that directly. + if (window.CSS && window.CSS.escape) { + return CSS.escape(id); + } else { + // CSS escape alternative borrowed from https://api.jquery.com/jQuery.escapeSelector/ source. When we upgrade to jQuery 3.0, we can use $.escapeSelector() instead of this shim escapeSelector function. + // source: https://github.com/jquery/jquery/blob/3edfa1bc/src/selector/escapeSelector.js + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + function fcssescape( ch, asCodePoint ) { + if ( asCodePoint ) { + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + } + // ensure string and then run the replacements + return (id + "").replace(rcssescape, fcssescape); + } +} + var formulaEquationPreview = { minDelay: 300, // Minimum time between requests sent out. errorDelay: 1500 // Wait time before showing error (prevent frustration). @@ -13,7 +47,7 @@ formulaEquationPreview.enable = function() { function setupInput() { var $this = $(this); // cache the jQuery object - var $preview = $('#' + this.id + '_preview'); + var $preview = $('#' + escapeSelector(this.id) + '_preview'); var inputData = { // These are the mutable values