From aa650a4c88e1b577549f19683bf833d47a745292 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 3 Feb 2021 14:23:32 -0800 Subject: [PATCH] js: Escape strings interpolated into CSS selectors with CSS.escape. Signed-off-by: Anders Kaseorg --- .eslintrc.json | 1 + frontend_tests/node_tests/activity.js | 2 +- frontend_tests/node_tests/billing_helpers.js | 4 +- .../node_tests/dropdown_list_widget.js | 4 +- frontend_tests/node_tests/reactions.js | 6 +-- frontend_tests/node_tests/settings_org.js | 22 ++++----- .../node_tests/settings_user_groups.js | 18 +++---- frontend_tests/node_tests/stream_edit.js | 5 +- frontend_tests/node_tests/stream_events.js | 2 +- frontend_tests/node_tests/subs.js | 4 +- frontend_tests/node_tests/upload.js | 22 +++++---- frontend_tests/puppeteer_lib/common.js | 7 +-- .../puppeteer_tests/02-message-basics.js | 17 ++++--- frontend_tests/puppeteer_tests/03-compose.js | 2 +- .../puppeteer_tests/04-subscriptions.js | 4 +- frontend_tests/puppeteer_tests/05-stars.js | 2 +- .../puppeteer_tests/07-navigation.js | 4 +- frontend_tests/puppeteer_tests/08-admin.js | 4 +- .../puppeteer_tests/11-user-deactivation.js | 2 +- .../puppeteer_tests/14-copy-and-paste.js | 2 +- frontend_tests/puppeteer_tests/16-settings.js | 18 ++++--- frontend_tests/zjsunit/index.js | 1 + frontend_tests/zjsunit/zjquery.js | 3 -- package.json | 1 + static/js/billing/helpers.js | 22 ++++----- static/js/billing/upgrade.js | 4 +- static/js/buddy_list.js | 2 +- static/js/bundles/common.js | 1 + static/js/click_handlers.js | 18 +++---- static/js/compose.js | 2 +- static/js/compose_state.js | 2 +- static/js/composebox_typeahead.js | 2 +- static/js/dropdown_list_widget.js | 30 +++++++----- static/js/echo.js | 8 ++-- static/js/emoji_picker.js | 8 ++-- static/js/hotspots.js | 14 +++--- static/js/info_overlay.js | 6 +-- static/js/lightbox.js | 4 +- static/js/message_edit.js | 10 ++-- static/js/message_list_view.js | 2 +- static/js/notifications.js | 2 +- static/js/overlays.js | 2 +- static/js/popovers.js | 4 +- static/js/portico/help.js | 4 +- static/js/portico/integrations.js | 16 ++++--- static/js/portico/landing-page.js | 2 +- static/js/portico/team.js | 6 +-- static/js/reactions.js | 4 +- static/js/recent_topics.js | 10 ++-- static/js/reminder.js | 2 +- static/js/rows.js | 2 +- static/js/settings.js | 4 +- static/js/settings_account.js | 4 +- static/js/settings_bots.js | 10 ++-- static/js/settings_display.js | 4 +- static/js/settings_notifications.js | 2 +- static/js/settings_org.js | 6 +-- static/js/settings_panel_menu.js | 4 +- static/js/settings_profile_fields.js | 4 +- static/js/settings_ui.js | 8 ++-- static/js/settings_user_groups.js | 48 ++++++++++--------- static/js/settings_users.js | 6 +-- static/js/stream_color.js | 21 ++++---- static/js/stream_edit.js | 26 ++++++---- static/js/stream_events.js | 2 +- static/js/stream_muting.js | 6 +-- static/js/stream_popover.js | 2 +- static/js/stream_ui_updates.js | 4 +- static/js/subs.js | 12 +++-- static/js/timerender.js | 2 +- static/js/ui_util.js | 2 +- static/js/upload.js | 18 +++---- tools/message-screenshot.js | 7 +-- version.py | 2 +- yarn.lock | 5 ++ 75 files changed, 309 insertions(+), 246 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index e263c13450..392804daad 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -348,6 +348,7 @@ { "files": ["frontend_tests/**"], "globals": { + "CSS": false, "document": false, "navigator": false, "window": false diff --git a/frontend_tests/node_tests/activity.js b/frontend_tests/node_tests/activity.js index 9becf0a9ae..a4936847b5 100644 --- a/frontend_tests/node_tests/activity.js +++ b/frontend_tests/node_tests/activity.js @@ -286,7 +286,7 @@ function buddy_list_add(user_id, stub) { stub.attr("data-user-id", user_id); } stub.length = 1; - const sel = `li.user_sidebar_entry[data-user-id='${user_id}']`; + const sel = `li.user_sidebar_entry[data-user-id='${CSS.escape(user_id)}']`; $("#user_presences").set_find_results(sel, stub); } diff --git a/frontend_tests/node_tests/billing_helpers.js b/frontend_tests/node_tests/billing_helpers.js index 59e17dc961..e2c644260d 100644 --- a/frontend_tests/node_tests/billing_helpers.js +++ b/frontend_tests/node_tests/billing_helpers.js @@ -254,12 +254,12 @@ run_test("set_tab", () => { scrollTop: 0, }; - $('#upgrade-tabs.nav a[href="#billing"]').tab = (action) => { + $('#upgrade-tabs.nav a[href="\\#billing"]').tab = (action) => { state.show_tab_billing += 1; assert.equal(action, "show"); }; - $('#upgrade-tabs.nav a[href="#payment-method"]').tab = (action) => { + $('#upgrade-tabs.nav a[href="\\#payment-method"]').tab = (action) => { state.show_tab_payment_method += 1; assert.equal(action, "show"); }; diff --git a/frontend_tests/node_tests/dropdown_list_widget.js b/frontend_tests/node_tests/dropdown_list_widget.js index 5d5601301b..680b61a652 100644 --- a/frontend_tests/node_tests/dropdown_list_widget.js +++ b/frontend_tests/node_tests/dropdown_list_widget.js @@ -21,8 +21,8 @@ const setup_zjquery_data = (name) => { const input_group = $(".input_group"); const reset_button = $(".dropdown_list_reset_button"); input_group.set_find_results(".dropdown_list_reset_button:enabled", reset_button); - $(`#${name}_widget #${name}_name`).closest = () => input_group; - const $widget = $(`#${name}_widget #${name}_name`); + $(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`).closest = () => input_group; + const $widget = $(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`); return {reset_button, $widget}; }; diff --git a/frontend_tests/node_tests/reactions.js b/frontend_tests/node_tests/reactions.js index 08204ce424..9809fda2cd 100644 --- a/frontend_tests/node_tests/reactions.js +++ b/frontend_tests/node_tests/reactions.js @@ -349,7 +349,7 @@ run_test("get_reaction_section", () => { const message_row = $.create("some-message-row"); const message_reactions = $.create("our-reactions-section"); - message_table.set_find_results("[zid='555']", message_row); + message_table.set_find_results(`[zid='${CSS.escape(555)}']`, message_row); message_row.set_find_results(".message_reactions", message_reactions); const section = reactions.get_reaction_section(555); @@ -439,7 +439,7 @@ run_test("add_and_remove_reaction", () => { reaction_element.set_find_results(".message_reaction_count", count_element); message_reactions.find = function (selector) { - assert.equal(selector, "[data-reaction-id='unicode_emoji,1f3b1']"); + assert.equal(selector, "[data-reaction-id='unicode_emoji\\,1f3b1']"); return reaction_element; }; @@ -506,7 +506,7 @@ run_test("add_and_remove_reaction", () => { }; message_reactions.find = function (selector) { - assert.equal(selector, "[data-reaction-id='realm_emoji,991']"); + assert.equal(selector, "[data-reaction-id='realm_emoji\\,991']"); return reaction_element; }; reaction_element.prop = function () {}; diff --git a/frontend_tests/node_tests/settings_org.js b/frontend_tests/node_tests/settings_org.js index eacfddc248..051f2525c6 100644 --- a/frontend_tests/node_tests/settings_org.js +++ b/frontend_tests/node_tests/settings_org.js @@ -154,10 +154,10 @@ function test_realms_domain_modal(add_realm_domain) { } function createSaveButtons(subsection) { - const stub_save_button_header = $(`#org-${subsection}`); + const stub_save_button_header = $(`#org-${CSS.escape(subsection)}`); const save_button_controls = $(".save-button-controls"); - const stub_save_button = $(`#org-submit-${subsection}`); - const stub_discard_button = $(`#org-discard-${subsection}`); + const stub_save_button = $(`#org-submit-${CSS.escape(subsection)}`); + const stub_discard_button = $(`#org-discard-${CSS.escape(subsection)}`); const stub_save_button_text = $(".save-discard-widget-button-text"); stub_save_button_header.set_find_results( ".subsection-failed-status p", @@ -169,7 +169,7 @@ function createSaveButtons(subsection) { stub_save_button_header.set_find_results(".save-button-controls", save_button_controls); stub_save_button_header.set_find_results( ".subsection-changes-discard .button", - $(`#org-discard-${subsection}`), + $(`#org-discard-${CSS.escape(subsection)}`), ); save_button_controls.set_find_results(".discard-button", stub_discard_button); const props = {}; @@ -222,7 +222,7 @@ function test_submit_settings_form(submit_form) { }; let subsection = "other-permissions"; - ev.currentTarget = `#org-submit-${subsection}`; + ev.currentTarget = `#org-submit-${CSS.escape(subsection)}`; let stubs = createSaveButtons(subsection); let save_button = stubs.save_button; save_button.attr("id", `org-submit-${subsection}`); @@ -253,7 +253,7 @@ function test_submit_settings_form(submit_form) { email_address_visibility_elem.attr("id", "id_realm_email_address_visibility"); email_address_visibility_elem.data = () => "number"; - let subsection_elem = $(`#org-${subsection}`); + let subsection_elem = $(`#org-${CSS.escape(subsection)}`); subsection_elem.closest = () => subsection_elem; subsection_elem.set_find_results(".prop-element", [ bot_creation_policy_elem, @@ -277,7 +277,7 @@ function test_submit_settings_form(submit_form) { assert.deepEqual(data, expected_value); subsection = "user-defaults"; - ev.currentTarget = `#org-submit-${subsection}`; + ev.currentTarget = `#org-submit-${CSS.escape(subsection)}`; stubs = createSaveButtons(subsection); save_button = stubs.save_button; save_button.attr("id", `org-submit-${subsection}`); @@ -291,7 +291,7 @@ function test_submit_settings_form(submit_form) { realm_default_twenty_four_hour_time_elem.attr("id", "id_realm_default_twenty_four_hour_time"); realm_default_twenty_four_hour_time_elem.data = () => "boolean"; - subsection_elem = $(`#org-${subsection}`); + subsection_elem = $(`#org-${CSS.escape(subsection)}`); subsection_elem.closest = () => subsection_elem; subsection_elem.set_find_results(".prop-element", [ realm_default_language_elem, @@ -1017,7 +1017,7 @@ run_test("misc", () => { $.create(""), ); for (const name of widget_settings) { - const elem = $.create(`#${name}_widget #${name}_name`); + const elem = $.create(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`); elem.closest = () => dropdown_list_parent; } @@ -1030,7 +1030,7 @@ run_test("misc", () => { settings_org.init_dropdown_widgets(); let setting_name = "realm_notifications_stream_id"; - let elem = $(`#${setting_name}_widget #${setting_name}_name`); + let elem = $(`#${CSS.escape(setting_name)}_widget #${CSS.escape(setting_name)}_name`); elem.closest = function () { return stub_notification_disable_parent; }; @@ -1047,7 +1047,7 @@ run_test("misc", () => { assert(elem.hasClass("text-warning")); setting_name = "realm_signup_notifications_stream_id"; - elem = $(`#${setting_name}_widget #${setting_name}_name`); + elem = $(`#${CSS.escape(setting_name)}_widget #${CSS.escape(setting_name)}_name`); elem.closest = function () { return stub_notification_disable_parent; }; diff --git a/frontend_tests/node_tests/settings_user_groups.js b/frontend_tests/node_tests/settings_user_groups.js index 47b25736a0..17db804d95 100644 --- a/frontend_tests/node_tests/settings_user_groups.js +++ b/frontend_tests/node_tests/settings_user_groups.js @@ -90,12 +90,12 @@ run_test("can_edit", () => { assert(settings_user_groups.can_edit(1)); }); -const user_group_selector = "#user-groups #1"; -const cancel_selector = "#user-groups #1 .save-status.btn-danger"; -const saved_selector = "#user-groups #1 .save-status.sea-green"; -const name_selector = "#user-groups #1 .name"; -const description_selector = "#user-groups #1 .description"; -const instructions_selector = "#user-groups #1 .save-instructions"; +const user_group_selector = `#user-groups #${CSS.escape(1)}`; +const cancel_selector = `#user-groups #${CSS.escape(1)} .save-status.btn-danger`; +const saved_selector = `#user-groups #${CSS.escape(1)} .save-status.sea-green`; +const name_selector = `#user-groups #${CSS.escape(1)} .name`; +const description_selector = `#user-groups #${CSS.escape(1)} .description`; +const instructions_selector = `#user-groups #${CSS.escape(1)} .save-instructions`; run_test("populate_user_groups", () => { const realm_user_group = { @@ -169,7 +169,7 @@ run_test("populate_user_groups", () => { const all_pills = new Map(); - const pill_container_stub = $('.pill-container[data-group-pills="1"]'); + const pill_container_stub = $(`.pill-container[data-group-pills="${CSS.escape(1)}"]`); pills.appendValidatedData = function (item) { const id = item.user_id; assert(!all_pills.has(id)); @@ -397,7 +397,7 @@ run_test("with_external_user", () => { set_global("$", make_zjquery()); let user_group_find_called = 0; - const user_group_stub = $('div.user-group[id="1"]'); + const user_group_stub = $(`div.user-group[id="${CSS.escape(1)}"]`); const name_field_stub = $.create("fake-name-field"); const description_field_stub = $.create("fake-description-field"); const input_stub = $.create("fake-input"); @@ -413,7 +413,7 @@ run_test("with_external_user", () => { throw new Error(`Unknown element ${elem}`); }; - const pill_container_stub = $('.pill-container[data-group-pills="1"]'); + const pill_container_stub = $(`.pill-container[data-group-pills="${CSS.escape(1)}"]`); const pill_stub = $.create("fake-pill"); let pill_container_find_called = 0; pill_container_stub.find = function (elem) { diff --git a/frontend_tests/node_tests/stream_edit.js b/frontend_tests/node_tests/stream_edit.js index ddfc0cba37..bf6119c341 100644 --- a/frontend_tests/node_tests/stream_edit.js +++ b/frontend_tests/node_tests/stream_edit.js @@ -98,8 +98,9 @@ for (const sub of subs) { const subscriptions_table_selector = "#subscriptions_table"; const input_field_stub = $.create(".input"); -const sub_settings_selector = - "#subscription_overlay .subscription_settings[data-stream-id='" + denmark.stream_id + "']"; +const sub_settings_selector = `#subscription_overlay .subscription_settings[data-stream-id='${CSS.escape( + denmark.stream_id, +)}']`; const $sub_settings_container = $.create(sub_settings_selector); $sub_settings_container.find = noop; $sub_settings_container.find = function () { diff --git a/frontend_tests/node_tests/stream_events.js b/frontend_tests/node_tests/stream_events.js index 3d9eebac3a..cd028d3a22 100644 --- a/frontend_tests/node_tests/stream_events.js +++ b/frontend_tests/node_tests/stream_events.js @@ -95,7 +95,7 @@ run_test("update_property", (override) => { } function checkbox_for(property) { - return $(`#${property}_${stream_id}`); + return $(`#${CSS.escape(property)}_${CSS.escape(stream_id)}`); } // Test desktop notifications diff --git a/frontend_tests/node_tests/subs.js b/frontend_tests/node_tests/subs.js index 8f34d950b7..0185078146 100644 --- a/frontend_tests/node_tests/subs.js +++ b/frontend_tests/node_tests/subs.js @@ -111,7 +111,7 @@ run_test("filter_table", () => { const sub_stubs = []; for (const data of populated_subs) { - const sub_row = ".stream-row-" + data.elem; + const sub_row = `.stream-row-${CSS.escape(data.elem)}`; sub_stubs.push(sub_row); $(sub_row).attr("data-stream-id", data.stream_id); @@ -149,7 +149,7 @@ run_test("filter_table", () => { // Filtering has the side effect of setting the "active" class // on our current stream, even if it doesn't match the filter. - const denmark_row = $(`.stream-row[data-stream-id='${denmark_stream_id}']`); + const denmark_row = $(`.stream-row[data-stream-id='${CSS.escape(denmark_stream_id)}']`); // sanity check it's not set to active assert(!denmark_row.hasClass("active")); diff --git a/frontend_tests/node_tests/upload.js b/frontend_tests/node_tests/upload.js index c227bc9005..23a8a1c2d3 100644 --- a/frontend_tests/node_tests/upload.js +++ b/frontend_tests/node_tests/upload.js @@ -62,9 +62,12 @@ run_test("get_item", () => { $("#undo_markdown_preview"), ); - assert.equal(upload.get_item("textarea", {mode: "edit", row: 1}), $("#message_edit_content_1")); + assert.equal( + upload.get_item("textarea", {mode: "edit", row: 1}), + $(`#message_edit_content_${CSS.escape(1)}`), + ); - $("#message_edit_content_2").closest = () => { + $(`#message_edit_content_${CSS.escape(2)}`).closest = () => { $("#message_edit_form").set_find_results(".message_edit_save", $(".message_edit_save")); return $("#message_edit_form"); }; @@ -72,14 +75,14 @@ run_test("get_item", () => { assert.equal( upload.get_item("send_status_identifier", {mode: "edit", row: 11}), - "#message-edit-send-status-11", + `#message-edit-send-status-${CSS.escape(11)}`, ); assert.equal( upload.get_item("send_status", {mode: "edit", row: 75}), - $("#message-edit-send-status-75"), + $(`#message-edit-send-status-${CSS.escape(75)}`), ); - $("#message-edit-send-status-2").set_find_results( + $(`#message-edit-send-status-${CSS.escape(2)}`).set_find_results( ".send-status-close", $(".send-status-close"), ); @@ -88,12 +91,15 @@ run_test("get_item", () => { $(".send-status-close"), ); - $("#message-edit-send-status-22").set_find_results(".error-msg", $(".error-msg")); + $(`#message-edit-send-status-${CSS.escape(22)}`).set_find_results( + ".error-msg", + $(".error-msg"), + ); assert.equal(upload.get_item("send_status_message", {mode: "edit", row: 22}), $(".error-msg")); assert.equal( upload.get_item("file_input_identifier", {mode: "edit", row: 123}), - "#message_edit_file_input_123", + `#message_edit_file_input_${CSS.escape(123)}`, ); assert.equal(upload.get_item("source", {mode: "edit", row: 123}), "message-edit-file-input"); assert.equal( @@ -102,7 +108,7 @@ run_test("get_item", () => { ); assert.equal( upload.get_item("markdown_preview_hide_button", {mode: "edit", row: 65}), - $("#undo_markdown_preview_65"), + $(`#undo_markdown_preview_${CSS.escape(65)}`), ); assert.throws( diff --git a/frontend_tests/puppeteer_lib/common.js b/frontend_tests/puppeteer_lib/common.js index c680286060..8eba08788f 100644 --- a/frontend_tests/puppeteer_lib/common.js +++ b/frontend_tests/puppeteer_lib/common.js @@ -3,6 +3,7 @@ const {strict: assert} = require("assert"); const path = require("path"); +require("css.escape"); const puppeteer = require("puppeteer"); const {test_credentials} = require("../../var/puppeteer/test_credentials"); @@ -381,7 +382,7 @@ class CommonUtils { */ async get_rendered_messages(page, table = "zhome") { return await page.evaluate((table) => { - const $recipient_rows = $(`#${table}`).find(".recipient_row"); + const $recipient_rows = $(`#${CSS.escape(table)}`).find(".recipient_row"); return $recipient_rows.toArray().map((element) => { const $el = $(element); const stream_name = $el.find(".stream_label").text().trim(); @@ -410,7 +411,7 @@ class CommonUtils { // The method will only check that all the messages in the // messages array passed exist in the order they are passed. async check_messages_sent(page, table, messages) { - await page.waitForSelector("#" + table, {visible: true}); + await page.waitForSelector(`#${CSS.escape(table)}`, {visible: true}); const rendered_messages = await this.get_rendered_messages(page, table); // We only check the last n messages because if we run @@ -452,7 +453,7 @@ class CommonUtils { const tah = $(field_selector).data().typeahead; tah.mouseenter({ - currentTarget: $('.typeahead:visible li:contains("' + item + '")')[0], + currentTarget: $(`.typeahead:visible li:contains("${CSS.escape(item)}")`)[0], }); tah.select(); }, diff --git a/frontend_tests/puppeteer_tests/02-message-basics.js b/frontend_tests/puppeteer_tests/02-message-basics.js index c456aa7376..789b9ba0bd 100644 --- a/frontend_tests/puppeteer_tests/02-message-basics.js +++ b/frontend_tests/puppeteer_tests/02-message-basics.js @@ -6,7 +6,7 @@ const common = require("../puppeteer_lib/common"); async function get_stream_li(page, stream_name) { const stream_id = await common.get_stream_id(page, stream_name); - return `#stream_filters [data-stream-id="${stream_id}"]`; + return `#stream_filters [data-stream-id="${CSS.escape(stream_id)}"]`; } async function expect_home(page) { @@ -343,19 +343,24 @@ async function test_stream_search_filters_stream_list(page) { async function test_users_search(page) { console.log("Search users using right sidebar"); async function assert_in_list(page, name) { - await page.waitForSelector(`#user_presences li [data-name="${name}"]`, {visible: true}); + await page.waitForSelector(`#user_presences li [data-name="${CSS.escape(name)}"]`, { + visible: true, + }); } async function assert_selected(page, name) { - await page.waitForSelector(`#user_presences li.highlighted_user [data-name="${name}"]`, { - visible: true, - }); + await page.waitForSelector( + `#user_presences li.highlighted_user [data-name="${CSS.escape(name)}"]`, + { + visible: true, + }, + ); } async function assert_not_selected(page, name) { await common.assert_selector_doesnt_exist( page, - `#user_presences li.highlighted_user [data-name="${name}"]`, + `#user_presences li.highlighted_user [data-name="${CSS.escape(name)}"]`, ); } diff --git a/frontend_tests/puppeteer_tests/03-compose.js b/frontend_tests/puppeteer_tests/03-compose.js index f98726f6d5..1fee3504cd 100644 --- a/frontend_tests/puppeteer_tests/03-compose.js +++ b/frontend_tests/puppeteer_tests/03-compose.js @@ -132,7 +132,7 @@ async function test_narrow_to_private_messages_with_cordelia(page) { async function test_send_multirecipient_pm_from_cordelia_pm_narrow(page) { const recipients = ["cordelia@zulip.com", "othello@zulip.com"]; const multiple_recipients_pm = "A huddle to check spaces"; - const pm_selector = `.messagebox:contains('${multiple_recipients_pm}')`; + const pm_selector = `.messagebox:contains('${CSS.escape(multiple_recipients_pm)}')`; await common.send_message(page, "private", { recipient: recipients.join(", "), outside_view: true, diff --git a/frontend_tests/puppeteer_tests/04-subscriptions.js b/frontend_tests/puppeteer_tests/04-subscriptions.js index ade26b39a9..18d94b21bd 100644 --- a/frontend_tests/puppeteer_tests/04-subscriptions.js +++ b/frontend_tests/puppeteer_tests/04-subscriptions.js @@ -6,7 +6,7 @@ const common = require("../puppeteer_lib/common"); async function user_checkbox(page, name) { const user_id = await common.get_user_id_from_name(page, name); - return `#user-checkboxes [data-user-id="${user_id}"]`; + return `#user-checkboxes [data-user-id="${CSS.escape(user_id)}"]`; } async function user_span(page, name) { @@ -15,7 +15,7 @@ async function user_span(page, name) { async function stream_checkbox(page, stream_name) { const stream_id = await common.get_stream_id(page, stream_name); - return `#stream-checkboxes [data-stream-id="${stream_id}"]`; + return `#stream-checkboxes [data-stream-id="${CSS.escape(stream_id)}"]`; } async function stream_span(page, stream_name) { diff --git a/frontend_tests/puppeteer_tests/05-stars.js b/frontend_tests/puppeteer_tests/05-stars.js index e1b873bac5..3ff498e6f5 100644 --- a/frontend_tests/puppeteer_tests/05-stars.js +++ b/frontend_tests/puppeteer_tests/05-stars.js @@ -12,7 +12,7 @@ async function stars_count(page) { async function toggle_test_star_message(page) { await page.evaluate((message) => { - const msg = $(`.message_content:contains(${message}):visible`).last(); + const msg = $(`.message_content:contains("${CSS.escape(message)}"):visible`).last(); if (msg.length !== 1) { throw new Error("cannot find test star message"); } diff --git a/frontend_tests/puppeteer_tests/07-navigation.js b/frontend_tests/puppeteer_tests/07-navigation.js index 339af97146..cad967a15b 100644 --- a/frontend_tests/puppeteer_tests/07-navigation.js +++ b/frontend_tests/puppeteer_tests/07-navigation.js @@ -5,13 +5,13 @@ const {strict: assert} = require("assert"); const common = require("../puppeteer_lib/common"); async function wait_for_tab(page, tab) { - const tab_slector = `#${tab}.tab-pane.active`; + const tab_slector = `#${CSS.escape(tab)}.tab-pane.active`; await page.waitForSelector(tab_slector, {visible: true}); } async function navigate_to(page, click_target, tab) { console.log("Visiting #" + click_target); - await page.click(`a[href='#${click_target}']`); + await page.click(`a[href='#${CSS.escape(click_target)}']`); await wait_for_tab(page, tab); } diff --git a/frontend_tests/puppeteer_tests/08-admin.js b/frontend_tests/puppeteer_tests/08-admin.js index 0a615cb9b4..188e953b3c 100644 --- a/frontend_tests/puppeteer_tests/08-admin.js +++ b/frontend_tests/puppeteer_tests/08-admin.js @@ -247,7 +247,7 @@ async function select_from_suggestions(page, item) { await page.evaluate((item) => { const tah = $(".create_default_stream").data().typeahead; tah.mouseenter({ - currentTarget: $('.typeahead:visible li:contains("' + item + '")')[0], + currentTarget: $(`.typeahead:visible li:contains("${CSS.escape(item)}")`)[0], }); tah.select(); }, item); @@ -278,7 +278,7 @@ async function test_default_streams(page) { const stream_name = "Scotland"; const stream_id = await common.get_stream_id(page, stream_name); - const row = `.default_stream_row[data-stream-id='${stream_id}']`; + const row = `.default_stream_row[data-stream-id='${CSS.escape(stream_id)}']`; await test_add_default_stream(page, stream_name, row); await test_remove_default_stream(page, row); diff --git a/frontend_tests/puppeteer_tests/11-user-deactivation.js b/frontend_tests/puppeteer_tests/11-user-deactivation.js index 6155657bff..3504a064a5 100644 --- a/frontend_tests/puppeteer_tests/11-user-deactivation.js +++ b/frontend_tests/puppeteer_tests/11-user-deactivation.js @@ -15,7 +15,7 @@ async function navigate_to_user_list(page) { async function user_row(page, name) { const user_id = await common.get_user_id_from_name(page, name); - return `.user_row[data-user-id="${user_id}"]`; + return `.user_row[data-user-id="${CSS.escape(user_id)}"]`; } async function test_deactivate_user(page) { diff --git a/frontend_tests/puppeteer_tests/14-copy-and-paste.js b/frontend_tests/puppeteer_tests/14-copy-and-paste.js index 1134c0be11..511ed550b5 100644 --- a/frontend_tests/puppeteer_tests/14-copy-and-paste.js +++ b/frontend_tests/puppeteer_tests/14-copy-and-paste.js @@ -8,7 +8,7 @@ async function copy_messages(page, start_message, end_message) { return await page.evaluate( (start_message, end_message) => { function get_message_node(message) { - return $('.message_row .message_content:contains("' + message + '")').get(0); + return $(`.message_row .message_content:contains("${CSS.escape(message)}")`).get(0); } // select messages from start_message to end_message diff --git a/frontend_tests/puppeteer_tests/16-settings.js b/frontend_tests/puppeteer_tests/16-settings.js index e5f0979695..f0406c7f6b 100644 --- a/frontend_tests/puppeteer_tests/16-settings.js +++ b/frontend_tests/puppeteer_tests/16-settings.js @@ -96,7 +96,9 @@ async function test_webhook_bot_creation(page) { await page.click("#create_bot_button"); const bot_email = "1-bot@zulip.testserver"; - const download_zuliprc_selector = '.download_bot_zuliprc[data-email="' + bot_email + '"]'; + const download_zuliprc_selector = `.download_bot_zuliprc[data-email="${CSS.escape( + bot_email, + )}"]`; const outgoing_webhook_zuliprc_regex = /^data:application\/octet-stream;charset=utf-8,\[api]\nemail=.+\nkey=.+\nsite=.+\ntoken=.+\n$/; await page.waitForSelector(download_zuliprc_selector, {visible: true}); @@ -122,7 +124,9 @@ async function test_normal_bot_creation(page) { await page.click("#create_bot_button"); const bot_email = "2-bot@zulip.testserver"; - const download_zuliprc_selector = '.download_bot_zuliprc[data-email="' + bot_email + '"]'; + const download_zuliprc_selector = `.download_bot_zuliprc[data-email="${CSS.escape( + bot_email, + )}"]`; await page.waitForSelector(download_zuliprc_selector, {visible: true}); await page.click(download_zuliprc_selector); @@ -143,10 +147,10 @@ async function test_botserverrc(page) { async function test_edit_bot_form(page) { const bot1_email = "1-bot@zulip.testserver"; - const bot1_edit_btn = '.open_edit_bot_form[data-email="' + bot1_email + '"]'; + const bot1_edit_btn = `.open_edit_bot_form[data-email="${CSS.escape(bot1_email)}"]`; await page.click(bot1_edit_btn); - const edit_form_selector = '.edit_bot_form[data-email="' + bot1_email + '"]'; + const edit_form_selector = `.edit_bot_form[data-email="${CSS.escape(bot1_email)}"]`; await page.waitForSelector(edit_form_selector, {visible: true}); const name_field_selector = edit_form_selector + " [name=bot_name]"; assert(common.get_text_from_selector(page, name_field_selector), "Bot 1"); @@ -183,7 +187,7 @@ async function add_alert_word(page, word) { } async function check_alert_word_added(page, word) { - const added_alert_word_selector = `.alert-word-item[data-word='${word}']`; + const added_alert_word_selector = `.alert-word-item[data-word='${CSS.escape(word)}']`; await page.waitForSelector(added_alert_word_selector, {visible: true}); } @@ -213,7 +217,7 @@ async function test_duplicate_alert_words_cannot_be_added(page, duplicate_word) } async function delete_alert_word(page, word) { - const delete_btn_selector = `.remove-alert-word[data-word="${word}"]`; + const delete_btn_selector = `.remove-alert-word[data-word="${CSS.escape(word)}"]`; await page.click(delete_btn_selector); await common.assert_selector_doesnt_exist(page, delete_btn_selector); } @@ -239,7 +243,7 @@ async function change_language(page, language_data_code) { await page.waitForSelector("#default_language", {visible: true}); await page.click("#default_language"); await page.waitForSelector("#default_language_modal", {visible: true}); - const language_selector = `a[data-code="${language_data_code}"]`; + const language_selector = `a[data-code="${CSS.escape(language_data_code)}"]`; await page.click(language_selector); } diff --git a/frontend_tests/zjsunit/index.js b/frontend_tests/zjsunit/index.js index 12dcdb1f05..2c840d0718 100644 --- a/frontend_tests/zjsunit/index.js +++ b/frontend_tests/zjsunit/index.js @@ -3,6 +3,7 @@ const Module = require("module"); const path = require("path"); +require("css.escape"); const Handlebars = require("handlebars/runtime"); const _ = require("lodash"); diff --git a/frontend_tests/zjsunit/zjquery.js b/frontend_tests/zjsunit/zjquery.js index cf4ce0e8bc..b45f3f493d 100644 --- a/frontend_tests/zjsunit/zjquery.js +++ b/frontend_tests/zjsunit/zjquery.js @@ -535,9 +535,6 @@ exports.make_zjquery = function (opts) { zjquery.clear_all_elements = function () { elems.clear(); }; - zjquery.escapeSelector = function (s) { - return s; - }; return zjquery; }; diff --git a/package.json b/package.json index eda6cf80ec..5ab27a9eea 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "clipboard": "^2.0.4", "core-js": "^3.6.5", "css-loader": "^5.0.0", + "css.escape": "^1.5.1", "emoji-datasource-google": "^6.0.0", "emoji-datasource-google-blob": "npm:emoji-datasource-google@^3.0.0", "emoji-datasource-twitter": "^6.0.0", diff --git a/static/js/billing/helpers.js b/static/js/billing/helpers.js index 06e63a873c..81372d6d44 100644 --- a/static/js/billing/helpers.js +++ b/static/js/billing/helpers.js @@ -7,12 +7,12 @@ exports.create_ajax_request = function ( numeric_inputs = [], redirect_to = "/billing", ) { - const form = $("#" + form_name + "-form"); - const form_loading_indicator = "#" + form_name + "_loading_indicator"; - const form_input_section = "#" + form_name + "-input-section"; - const form_success = "#" + form_name + "-success"; - const form_error = "#" + form_name + "-error"; - const form_loading = "#" + form_name + "-loading"; + const form = $(`#${CSS.escape(form_name)}-form`); + const form_loading_indicator = `#${CSS.escape(form_name)}_loading_indicator`; + const form_input_section = `#${CSS.escape(form_name)}-input-section`; + const form_success = `#${CSS.escape(form_name)}-success`; + const form_error = `#${CSS.escape(form_name)}-error`; + const form_loading = `#${CSS.escape(form_name)}-loading`; const zulip_limited_section = "#zulip-limited-section"; const free_trial_alert_message = "#free-trial-alert-message"; @@ -103,27 +103,27 @@ exports.show_license_section = function (license) { $("#automatic_license_count").prop("disabled", true); $("#manual_license_count").prop("disabled", true); - const section_id = "#license-" + license + "-section"; + const section_id = `#license-${CSS.escape(license)}-section`; $(section_id).show(); - const input_id = "#" + license + "_license_count"; + const input_id = `#${CSS.escape(license)}_license_count`; $(input_id).prop("disabled", false); }; let current_page; function handle_hashchange() { - $("#" + current_page + '-tabs.nav a[href="' + location.hash + '"]').tab("show"); + $(`#${CSS.escape(current_page)}-tabs.nav a[href="${CSS.escape(location.hash)}"]`).tab("show"); $("html").scrollTop(0); } exports.set_tab = function (page) { const hash = location.hash; if (hash) { - $("#" + page + '-tabs.nav a[href="' + hash + '"]').tab("show"); + $(`#${CSS.escape(page)}-tabs.nav a[href="${CSS.escape(hash)}"]`).tab("show"); $("html").scrollTop(0); } - $("#" + page + "-tabs.nav-tabs a").on("click", function () { + $(`#${CSS.escape(page)}-tabs.nav-tabs a`).on("click", function () { location.hash = this.hash; }); diff --git a/static/js/billing/upgrade.js b/static/js/billing/upgrade.js index 9bd127bdd1..f185351561 100644 --- a/static/js/billing/upgrade.js +++ b/static/js/billing/upgrade.js @@ -16,7 +16,9 @@ exports.initialize = () => { $("#add-card-button").on("click", (e) => { const license_management = $("input[type=radio][name=license_management]:checked").val(); - if (helpers.is_valid_input($("#" + license_management + "_license_count")) === false) { + if ( + helpers.is_valid_input($(`#${CSS.escape(license_management)}_license_count`)) === false + ) { return; } add_card_handler.open({ diff --git a/static/js/buddy_list.js b/static/js/buddy_list.js index 5900293af5..66427de46a 100644 --- a/static/js/buddy_list.js +++ b/static/js/buddy_list.js @@ -23,7 +23,7 @@ class BuddyListConf { get_li_from_key(opts) { const user_id = opts.key; const container = $(this.container_sel); - return container.find(this.item_sel + "[data-user-id='" + user_id + "']"); + return container.find(`${this.item_sel}[data-user-id='${CSS.escape(user_id)}']`); } get_key_from_li(opts) { diff --git a/static/js/bundles/common.js b/static/js/bundles/common.js index f78f9d0e29..1209829185 100644 --- a/static/js/bundles/common.js +++ b/static/js/bundles/common.js @@ -1,5 +1,6 @@ import "core-js/features/promise"; import "core-js/features/symbol"; +import "css.escape"; import "../../../tools/debug-require"; import "jquery"; import "../page_params"; diff --git a/static/js/click_handlers.js b/static/js/click_handlers.js index c571b70bfa..36b9b6f94c 100644 --- a/static/js/click_handlers.js +++ b/static/js/click_handlers.js @@ -340,14 +340,14 @@ exports.initialize = function () { }); $("#message_edit_form .send-status-close").on("click", function () { const row_id = rows.id($(this).closest(".message_row")); - const send_status = $("#message-edit-send-status-" + row_id); + const send_status = $(`#message-edit-send-status-${CSS.escape(row_id)}`); $(send_status).stop(true).fadeOut(200); }); $("body").on("click", "#message_edit_form [id^='attach_files_']", function (e) { e.preventDefault(); const row_id = rows.id($(this).closest(".message_row")); - $("#message_edit_file_input_" + row_id).trigger("click"); + $(`#message_edit_file_input_${CSS.escape(row_id)}`).trigger("click"); }); $("body").on("click", "#message_edit_form [id^='markdown_preview_']", function (e) { @@ -355,7 +355,7 @@ exports.initialize = function () { const row_id = rows.id($(this).closest(".message_row")); function $_(selector) { - return $(selector + "_" + row_id); + return $(`${selector}_${CSS.escape(row_id)}`); } const content = $_("#message_edit_content").val(); @@ -376,7 +376,7 @@ exports.initialize = function () { const row_id = rows.id($(this).closest(".message_row")); function $_(selector) { - return $(selector + "_" + row_id); + return $(`${selector}_${CSS.escape(row_id)}`); } $_("#message_edit_content").show(); @@ -829,12 +829,12 @@ exports.initialize = function () { const edit_area = $(this).parent().find(`${selector}`); $(selector).removeClass("stream-name-edit-box"); if (edit_area.attr("contenteditable") === "true") { - $("[data-finish-editing='" + selector + "']").hide(); + $(`[data-finish-editing='${CSS.escape(selector)}']`).hide(); edit_area.attr("contenteditable", false); edit_area.text(edit_area.attr("data-prev-text")); $(this).html(""); } else { - $("[data-finish-editing='" + selector + "']").show(); + $(`[data-finish-editing='${CSS.escape(selector)}']`).show(); $(selector).addClass("stream-name-edit-box"); edit_area @@ -858,7 +858,7 @@ exports.initialize = function () { map[selector].on_save(e); $(this).hide(); $(this).parent().find(`${selector}`).attr("contenteditable", false); - $("[data-make-editable='" + selector + "']").html(""); + $(`[data-make-editable='${CSS.escape(selector)}']`).html(""); } }); })(); @@ -880,7 +880,7 @@ exports.initialize = function () { overlays.open_overlay({ name: overlay_name, - overlay: $("#" + overlay_name), + overlay: $(`#${CSS.escape(overlay_name)}`), on_close: function () { // close popover $(this).css({display: "block"}); @@ -910,7 +910,7 @@ exports.initialize = function () { hotspots.post_hotspot_as_read(hotspot_name); overlays.close_overlay(overlay_name); - $("#hotspot_" + hotspot_name + "_icon").remove(); + $(`#hotspot_${CSS.escape(hotspot_name)}_icon`).remove(); }); $("body").on("click", ".hotspot-button", (e) => { diff --git a/static/js/compose.js b/static/js/compose.js index 5a3d25b6dd..5b025205fa 100644 --- a/static/js/compose.js +++ b/static/js/compose.js @@ -1191,7 +1191,7 @@ exports.initialize = function () { // compose box. const edit_message_id = $(e.target).attr("data-message-id"); if (edit_message_id !== undefined) { - target_textarea = $("#message_edit_content_" + edit_message_id); + target_textarea = $(`#message_edit_content_${CSS.escape(edit_message_id)}`); } let video_call_link; diff --git a/static/js/compose_state.js b/static/js/compose_state.js index 3483efae47..39a60b6b01 100644 --- a/static/js/compose_state.js +++ b/static/js/compose_state.js @@ -29,7 +29,7 @@ function get_or_set(fieldname, keep_leading_whitespace) { // because the DOM element might not exist yet when get_or_set // is called. return function (newval) { - const elem = $("#" + fieldname); + const elem = $(`#${CSS.escape(fieldname)}`); const oldval = elem.val(); if (newval !== undefined) { elem.val(newval); diff --git a/static/js/composebox_typeahead.js b/static/js/composebox_typeahead.js index 6d9e7f5c41..74b6ef364b 100644 --- a/static/js/composebox_typeahead.js +++ b/static/js/composebox_typeahead.js @@ -178,7 +178,7 @@ function handle_keydown(e) { let target_sel; if (e.target.id) { - target_sel = "#" + e.target.id; + target_sel = `#${CSS.escape(e.target.id)}`; } const on_stream = target_sel === "#stream_message_recipient_stream"; diff --git a/static/js/dropdown_list_widget.js b/static/js/dropdown_list_widget.js index 107ce8cb5d..758ce79224 100644 --- a/static/js/dropdown_list_widget.js +++ b/static/js/dropdown_list_widget.js @@ -21,9 +21,9 @@ const DropdownListWidget = function (opts) { init(); const render = (value) => { - $(`#${opts.container_id} #${opts.value_id}`).data("value", value); + $(`#${CSS.escape(opts.container_id)} #${CSS.escape(opts.value_id)}`).data("value", value); - const elem = $(`#${opts.container_id} #${opts.widget_name}_name`); + const elem = $(`#${CSS.escape(opts.container_id)} #${CSS.escape(opts.widget_name)}_name`); if (!value || value === opts.null_value) { elem.text(opts.default_text); @@ -46,11 +46,11 @@ const DropdownListWidget = function (opts) { }; const register_event_handlers = () => { - $(`#${opts.container_id} .dropdown-list-body`).on( + $(`#${CSS.escape(opts.container_id)} .dropdown-list-body`).on( "click keypress", ".list_item", function (e) { - const setting_elem = $(this).closest(`.${opts.widget_name}_setting`); + const setting_elem = $(this).closest(`.${CSS.escape(opts.widget_name)}_setting`); if (e.type === "keypress") { if (e.which === 13) { setting_elem.find(".dropdown-menu").dropdown("toggle"); @@ -62,7 +62,7 @@ const DropdownListWidget = function (opts) { update(value); }, ); - $(`#${opts.container_id} .dropdown_list_reset_button`).on("click", (e) => { + $(`#${CSS.escape(opts.container_id)} .dropdown_list_reset_button`).on("click", (e) => { update(opts.null_value); e.preventDefault(); }); @@ -70,12 +70,16 @@ const DropdownListWidget = function (opts) { const setup = () => { // populate the dropdown - const dropdown_list_body = $(`#${opts.container_id} .dropdown-list-body`).expectOne(); - const search_input = $(`#${opts.container_id} .dropdown-search > input[type=text]`); - const dropdown_toggle = $(`#${opts.container_id} .dropdown-toggle`); + const dropdown_list_body = $( + `#${CSS.escape(opts.container_id)} .dropdown-list-body`, + ).expectOne(); + const search_input = $( + `#${CSS.escape(opts.container_id)} .dropdown-search > input[type=text]`, + ); + const dropdown_toggle = $(`#${CSS.escape(opts.container_id)} .dropdown-toggle`); ListWidget.create(dropdown_list_body, opts.data, { - name: `${opts.widget_name}_list`, + name: `${CSS.escape(opts.widget_name)}_list`, modifier(item) { return render_dropdown_list({item}); }, @@ -85,9 +89,9 @@ const DropdownListWidget = function (opts) { return item.name.toLowerCase().includes(value); }, }, - simplebar_container: $(`#${opts.container_id} .dropdown-list-wrapper`), + simplebar_container: $(`#${CSS.escape(opts.container_id)} .dropdown-list-wrapper`), }); - $(`#${opts.container_id} .dropdown-search`).on("click", (e) => { + $(`#${CSS.escape(opts.container_id)} .dropdown-search`).on("click", (e) => { e.stopPropagation(); }); @@ -119,7 +123,9 @@ const DropdownListWidget = function (opts) { }; const value = () => { - let val = $(`#${opts.container_id} #${opts.value_id}`).data("value"); + let val = $(`#${CSS.escape(opts.container_id)} #${CSS.escape(opts.value_id)}`).data( + "value", + ); if (val === null) { val = ""; } diff --git a/static/js/echo.js b/static/js/echo.js index 2d970fcaba..79b4abc87b 100644 --- a/static/js/echo.js +++ b/static/js/echo.js @@ -367,8 +367,8 @@ function abort_message(message) { } exports.initialize = function () { - function on_failed_action(action, callback) { - $("#main_div").on("click", "." + action + "-failed-message", function (e) { + function on_failed_action(selector, callback) { + $("#main_div").on("click", selector, function (e) { e.stopPropagation(); popovers.hide_all(); const row = $(this).closest(".message_row"); @@ -387,8 +387,8 @@ exports.initialize = function () { }); } - on_failed_action("remove", abort_message); - on_failed_action("refresh", resend_message); + on_failed_action(".remove-failed-message", abort_message); + on_failed_action(".refresh-failed-message", resend_message); }; window.echo = exports; diff --git a/static/js/emoji_picker.js b/static/js/emoji_picker.js index 215a791661..97518403ac 100644 --- a/static/js/emoji_picker.js +++ b/static/js/emoji_picker.js @@ -194,7 +194,7 @@ function get_selected_emoji() { function get_rendered_emoji(section, index) { const emoji_id = get_emoji_id(section, index); - const emoji = $(".emoji-popover-emoji[data-emoji-id='" + emoji_id + "']"); + const emoji = $(`.emoji-popover-emoji[data-emoji-id='${CSS.escape(emoji_id)}']`); if (emoji.length > 0) { return emoji; } @@ -562,7 +562,9 @@ exports.emoji_select_tab = function (elt) { } if (currently_selected) { $(".emoji-popover-tab-item.active").removeClass("active"); - $('.emoji-popover-tab-item[data-tab-name="' + currently_selected + '"]').addClass("active"); + $(`.emoji-popover-tab-item[data-tab-name="${CSS.escape(currently_selected)}"]`).addClass( + "active", + ); } }; @@ -674,7 +676,7 @@ exports.register_click_handlers = function () { // The following check will return false if emoji was not selected in // message edit form. if (edit_message_id !== null) { - const edit_message_textarea = $("#message_edit_content_" + edit_message_id); + const edit_message_textarea = $(`#message_edit_content_${CSS.escape(edit_message_id)}`); // Assign null to edit_message_id so that the selection of emoji in new // message composition form works correctly. edit_message_id = null; diff --git a/static/js/hotspots.js b/static/js/hotspots.js index 92c5901d95..ad248cae50 100644 --- a/static/js/hotspots.js +++ b/static/js/hotspots.js @@ -75,7 +75,7 @@ exports.post_hotspot_as_read = function (hotspot_name) { function place_icon(hotspot) { const element = $(hotspot.location.element); - const icon = $("#hotspot_" + hotspot.name + "_icon"); + const icon = $(`#hotspot_${CSS.escape(hotspot.name)}_icon`); if ( element.length === 0 || @@ -106,9 +106,11 @@ function place_popover(hotspot) { return; } - const popover_width = $("#hotspot_" + hotspot.name + "_overlay .hotspot-popover").outerWidth(); + const popover_width = $( + `#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`, + ).outerWidth(); const popover_height = $( - "#hotspot_" + hotspot.name + "_overlay .hotspot-popover", + `#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`, ).outerHeight(); const el_width = $(hotspot.location.element).outerWidth(); const el_height = $(hotspot.location.element).outerHeight(); @@ -181,7 +183,7 @@ function place_popover(hotspot) { // position arrow arrow_placement = "arrow-" + arrow_placement; - $("#hotspot_" + hotspot.name + "_overlay .hotspot-popover") + $(`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`) .removeClass("arrow-top arrow-left arrow-bottom arrow-right") .addClass(arrow_placement); @@ -202,7 +204,7 @@ function place_popover(hotspot) { }; } - $("#hotspot_" + hotspot.name + "_overlay .hotspot-popover").css(popover_placement); + $(`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`).css(popover_placement); } function insert_hotspot_into_DOM(hotspot) { @@ -272,7 +274,7 @@ function close_read_hotspots(new_hotspots) { ); for (const hotspot_name of unwanted_hotspots) { - exports.close_hotspot_icon($("#hotspot_" + hotspot_name + "_icon")); + exports.close_hotspot_icon($(`#hotspot_${CSS.escape(hotspot_name)}_icon`)); } } diff --git a/static/js/info_overlay.js b/static/js/info_overlay.js index 8f47f080da..7c797d102e 100644 --- a/static/js/info_overlay.js +++ b/static/js/info_overlay.js @@ -15,8 +15,8 @@ exports.set_up_toggler = function () { ], callback(name, key) { $(".overlay-modal").hide(); - $("#" + key).show(); - ui.get_scroll_element($("#" + key).find(".modal-body")).trigger("focus"); + $(`#${CSS.escape(key)}`).show(); + ui.get_scroll_element($(`#${CSS.escape(key)}`).find(".modal-body")).trigger("focus"); }, }; @@ -26,7 +26,7 @@ exports.set_up_toggler = function () { const modals = opts.values.map((item) => { const key = item.key; // e.g. message-formatting - const modal = $("#" + key).find(".modal-body"); + const modal = $(`#${CSS.escape(key)}`).find(".modal-body"); return modal; }); diff --git a/static/js/lightbox.js b/static/js/lightbox.js index 2070eb51d7..9374cd7251 100644 --- a/static/js/lightbox.js +++ b/static/js/lightbox.js @@ -281,7 +281,9 @@ exports.initialize = function () { $("#lightbox_overlay").on("click", ".image-list .image", function () { const $image_list = $(this).parent(); - const $original_image = $(".message_row img[src='" + $(this).attr("data-src") + "']"); + const $original_image = $( + `.message_row img[src='${CSS.escape($(this).attr("data-src"))}']`, + ); exports.open($original_image); diff --git a/static/js/message_edit.js b/static/js/message_edit.js index c73a545da4..ba6401c641 100644 --- a/static/js/message_edit.js +++ b/static/js/message_edit.js @@ -345,9 +345,9 @@ function edit_message(row, raw_content) { form.on("keydown", handle_message_row_edit_keydown); - upload.feature_check($("#attach_files_" + rows.id(row))); + upload.feature_check($(`#attach_files_${CSS.escape(rows.id(row))}`)); - const message_edit_stream = row.find("#select_stream_id_" + message.id); + const message_edit_stream = row.find(`#select_stream_id_${CSS.escape(message.id)}`); const stream_header_colorblock = row.find(".stream_header_colorblock"); const message_edit_content = row.find("textarea.message_edit_content"); const message_edit_topic = row.find("input.message_edit_topic"); @@ -381,7 +381,7 @@ function edit_message(row, raw_content) { new ClipboardJS(copy_message[0]); } else if (editability === editability_types.FULL) { copy_message.remove(); - const edit_id = "#message_edit_content_" + rows.id(row); + const edit_id = `#message_edit_content_${CSS.escape(rows.id(row))}`; const listeners = resize.watch_manual_resize(edit_id); if (listeners) { currently_editing_messages.get(rows.id(row)).listeners = listeners; @@ -576,7 +576,7 @@ exports.end_message_row_edit = function (row) { // Clean up resize event listeners const listeners = currently_editing_messages.get(message.id).listeners; - const edit_box = document.querySelector("#message_edit_content_" + message.id); + const edit_box = document.querySelector(`#message_edit_content_${CSS.escape(message.id)}`); if (listeners !== undefined) { // Event listeners to cleanup are only set in some edit types edit_box.removeEventListener("mousedown", listeners[0]); @@ -670,7 +670,7 @@ exports.save_message_row_edit = function (row) { new_topic = row.find(".message_edit_topic").val(); topic_changed = new_topic !== old_topic && new_topic.trim() !== ""; - new_stream_id = Number.parseInt($("#select_stream_id_" + message_id).val(), 10); + new_stream_id = Number.parseInt($(`#select_stream_id_${CSS.escape(message_id)}`).val(), 10); stream_changed = new_stream_id !== old_stream_id; } // Editing a not-yet-acked message (because the original send attempt failed) diff --git a/static/js/message_list_view.js b/static/js/message_list_view.js index 4384675a96..4e39770ad4 100644 --- a/static/js/message_list_view.js +++ b/static/js/message_list_view.js @@ -659,7 +659,7 @@ class MessageListView { save_scroll_position(); for (const message_group of message_actions.rerender_groups) { - const old_message_group = $("#" + message_group.message_group_id); + const old_message_group = $(`#${CSS.escape(message_group.message_group_id)}`); // Remove the top date_row, we'll re-add it after rendering old_message_group.prev(".date_row").remove(); diff --git a/static/js/notifications.js b/static/js/notifications.js index 929dbc9baf..d1265078bd 100644 --- a/static/js/notifications.js +++ b/static/js/notifications.js @@ -190,7 +190,7 @@ function in_browser_notify(message, title, content, raw_operators, opts) { }) .show(); - $(".notification[data-message-id='" + message.id + "']") + $(`.notification[data-message-id='${CSS.escape(message.id)}']`) .expectOne() .data("narrow", { raw_operators, diff --git a/static/js/overlays.js b/static/js/overlays.js index b15b6e4d5f..d6764b9b5d 100644 --- a/static/js/overlays.js +++ b/static/js/overlays.js @@ -65,7 +65,7 @@ exports.active_modal = function () { blueslip.error("Programming error — Called active_modal when there is no modal open"); return undefined; } - return "#" + $(".modal.in").attr("id"); + return `#${CSS.escape($(".modal.in").attr("id"))}`; }; exports.open_overlay = function (opts) { diff --git a/static/js/popovers.js b/static/js/popovers.js index b1858fc70e..b817989343 100644 --- a/static/js/popovers.js +++ b/static/js/popovers.js @@ -580,7 +580,7 @@ exports.render_actions_remind_popover = function (element, id) { }); elt.popover("show"); current_flatpickr_instance = $( - '.remind.custom[data-message-id="' + message.id + '"]', + `.remind.custom[data-message-id="${CSS.escape(message.id)}"]`, ).flatpickr({ enableTime: true, clickOpens: false, @@ -1226,7 +1226,7 @@ exports.register_click_handlers = function () { $("body").on("click", ".copy_link", function (e) { exports.hide_actions_popover(); const message_id = $(this).attr("data-message-id"); - const row = $("[zid='" + message_id + "']"); + const row = $(`[zid='${CSS.escape(message_id)}']`); row.find(".alert-msg") .text(i18n.t("Copied!")) .css("display", "block") diff --git a/static/js/portico/help.js b/static/js/portico/help.js index 7951e9c781..1cd1f7bf67 100644 --- a/static/js/portico/help.js +++ b/static/js/portico/help.js @@ -34,12 +34,12 @@ function highlight_current_article() { } const hash = window.location.hash; - let article = $('.help .sidebar a[href="' + path + hash + '"]'); + let article = $(`.help .sidebar a[href="${CSS.escape(path + hash)}"]`); if (!article.length) { // If there isn't an entry in the left sidebar that matches // the full URL+hash pair, instead highlight an entry in the // left sidebar that just matches the URL part. - article = $('.help .sidebar a[href="' + path + '"]'); + article = $(`.help .sidebar a[href="${CSS.escape(path)}"]`); } // Highlight current article link and the heading of the same article.closest("ul").css("display", "block"); diff --git a/static/js/portico/integrations.js b/static/js/portico/integrations.js index ca84e7bd6d..ef7e802dbb 100644 --- a/static/js/portico/integrations.js +++ b/static/js/portico/integrations.js @@ -63,11 +63,11 @@ function adjust_font_sizing() { function update_path() { let next_path; if (state.integration) { - next_path = $('.integration-lozenge[data-name="' + state.integration + '"]') + next_path = $(`.integration-lozenge[data-name="${CSS.escape(state.integration)}"]`) .closest("a") .attr("href"); } else if (state.category) { - next_path = $('.integration-category[data-category="' + state.category + '"]') + next_path = $(`.integration-category[data-category="${CSS.escape(state.category)}"]`) .closest("a") .attr("href"); } else { @@ -82,7 +82,7 @@ function update_categories() { $(".integration-lozenges").css("opacity", 0); $(".integration-category").removeClass("selected"); - $('[data-category="' + state.category + '"]').addClass("selected"); + $(`[data-category="${CSS.escape(state.category)}"]`).addClass("selected"); const $dropdown_label = $(".integration-categories-dropdown .dropdown-category-label"); if (state.category === INITIAL_STATE.category) { @@ -132,10 +132,12 @@ const update_integrations = _.debounce(() => { }, 50); function hide_catalog_show_integration() { - const $lozenge_icon = $(".integration-lozenge.integration-" + state.integration).clone(false); + const $lozenge_icon = $( + `.integration-lozenge.integration-${CSS.escape(state.integration)}`, + ).clone(false); $lozenge_icon.removeClass("legacy"); - const categories = $(".integration-" + state.integration) + const categories = $(`.integration-${CSS.escape(state.integration)}`) .data("categories") .slice(1, -1) .split(",") @@ -162,10 +164,10 @@ function hide_catalog_show_integration() { display: "flex", }); $(".integration-instructions").css("display", "none"); - $("#" + state.integration + ".integration-instructions .help-content").html(doc); + $(`#${CSS.escape(state.integration)}.integration-instructions .help-content`).html(doc); $("#integration-instruction-block .integration-lozenge").remove(); $("#integration-instruction-block").append($lozenge_icon).css("display", "flex"); - $(".integration-instructions#" + state.integration).css("display", "block"); + $(`.integration-instructions#${CSS.escape(state.integration)}`).css("display", "block"); $("html, body").animate({scrollTop: 0}, {duration: 200}); $("#integration-instructions-group").animate({opacity: 1}, {duration: 300}); diff --git a/static/js/portico/landing-page.js b/static/js/portico/landing-page.js index bd0eef25fb..9b50413def 100644 --- a/static/js/portico/landing-page.js +++ b/static/js/portico/landing-page.js @@ -156,7 +156,7 @@ const events = function () { // pop the last element to get the current section (eg. `features`). const location = window.location.pathname.replace(/\/#*$/, "").split(/\//).pop(); - $("[data-on-page='" + location + "']").addClass("active"); + $(`[data-on-page='${CSS.escape(location)}']`).addClass("active"); $("body").on("click", (e) => { const $e = $(e.target); diff --git a/static/js/portico/team.js b/static/js/portico/team.js index e142cb0607..291bca7256 100644 --- a/static/js/portico/team.js +++ b/static/js/portico/team.js @@ -80,9 +80,9 @@ export default function render_tabs() { continue; } // Set as the loading template for now, and load when clicked. - $("#tab-" + tab_name).html($("#loading-template").html()); + $(`#tab-${CSS.escape(tab_name)}`).html($("#loading-template").html()); - $("#" + tab_name).on("click", () => { + $(`#${CSS.escape(tab_name)}`).on("click", () => { if (!loaded_repos.includes(repo_name)) { const html = contributors_list .filter((c) => c[repo_name]) @@ -100,7 +100,7 @@ export default function render_tabs() { ) .join(""); - $("#tab-" + tab_name).html(html); + $(`#tab-${CSS.escape(tab_name)}`).html(html); loaded_repos.push(repo_name); } diff --git a/static/js/reactions.js b/static/js/reactions.js index f5bfd6ec72..d766b47ad7 100644 --- a/static/js/reactions.js +++ b/static/js/reactions.js @@ -195,14 +195,14 @@ exports.get_reaction_title_data = function (message_id, local_id) { }; exports.get_reaction_section = function (message_id) { - const message_element = $(".message_table").find("[zid='" + message_id + "']"); + const message_element = $(".message_table").find(`[zid='${CSS.escape(message_id)}']`); const section = message_element.find(".message_reactions"); return section; }; exports.find_reaction = function (message_id, local_id) { const reaction_section = exports.get_reaction_section(message_id); - const reaction = reaction_section.find("[data-reaction-id='" + local_id + "']"); + const reaction = reaction_section.find(`[data-reaction-id='${CSS.escape(local_id)}']`); return reaction; }; diff --git a/static/js/recent_topics.js b/static/js/recent_topics.js index 8609b421cc..6a6cf5860d 100644 --- a/static/js/recent_topics.js +++ b/static/js/recent_topics.js @@ -77,7 +77,7 @@ function revive_current_focus() { set_default_focus(); } else { current_focus_elem = $("#recent_topics_filter_buttons").find( - "[data-filter='" + filter_button + "']", + `[data-filter='${CSS.escape(filter_button)}']`, ); current_focus_elem.trigger("focus"); } @@ -211,7 +211,7 @@ function format_topic(topic_data) { function get_topic_row(topic_data) { const msg = message_store.get(topic_data.last_msg_id); const topic_key = get_topic_key(msg.stream_id, msg.topic); - return $("#" + $.escapeSelector("recent_topic:" + topic_key)); + return $(`#${CSS.escape("recent_topic:" + topic_key)}`); } exports.process_topic_edit = function (old_stream_id, old_topic, new_topic, new_stream_id) { @@ -326,7 +326,9 @@ exports.set_filter = function (filter) { // set `filters`. // Get the button which was clicked. - const filter_elem = $("#recent_topics_filter_buttons").find('[data-filter="' + filter + '"]'); + const filter_elem = $("#recent_topics_filter_buttons").find( + `[data-filter="${CSS.escape(filter)}"]`, + ); // If user clicks `All`, we clear all filters. if (filter === "all" && filters.size !== 0) { @@ -350,7 +352,7 @@ function show_selected_filters() { } else { for (const filter of filters) { $("#recent_topics_filter_buttons") - .find('[data-filter="' + filter + '"]') + .find(`[data-filter="${CSS.escape(filter)}"]`) .addClass("btn-recent-selected"); } } diff --git a/static/js/reminder.js b/static/js/reminder.js index 5655ac990e..cf578e1c6f 100644 --- a/static/js/reminder.js +++ b/static/js/reminder.js @@ -106,7 +106,7 @@ exports.schedule_message = function (request) { }; exports.do_set_reminder_for_message = function (message_id, timestamp) { - const row = $("[zid='" + message_id + "']"); + const row = $(`[zid='${CSS.escape(message_id)}']`); function error() { row.find(".alert-msg") .text(i18n.t("Reminder not set!")) diff --git a/static/js/rows.js b/static/js/rows.js index 33808aac14..512c43b9d8 100644 --- a/static/js/rows.js +++ b/static/js/rows.js @@ -120,7 +120,7 @@ exports.get_table = function (table_name) { return $(); } - return $("#" + table_name); + return $(`#${CSS.escape(table_name)}`); }; exports.get_message_id = function (elem) { diff --git a/static/js/settings.js b/static/js/settings.js index 6d55cf2eda..ac1a653bfb 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -101,7 +101,9 @@ exports.launch = function (section) { }; exports.set_settings_header = function (key) { - const header_text = $(`#settings_page .sidebar-list [data-section='${key}'] .text`).text(); + const header_text = $( + `#settings_page .sidebar-list [data-section='${CSS.escape(key)}'] .text`, + ).text(); if (header_text) { $(".settings-header h1 .section").text(" / " + header_text); } else { diff --git a/static/js/settings_account.js b/static/js/settings_account.js index 17c8a93791..d70a3cb361 100644 --- a/static/js/settings_account.js +++ b/static/js/settings_account.js @@ -108,7 +108,7 @@ function update_custom_profile_field(field, method) { } const spinner_element = $( - '.custom_user_field[data-field-id="' + field_id + '"] .custom-field-status', + `.custom_user_field[data-field-id="${CSS.escape(field_id)}"] .custom-field-status`, ).expectOne(); settings_ui.do_settings_change( method, @@ -230,7 +230,7 @@ exports.initialize_custom_user_type_fields = function ( // pill container for that field and proceed further if (field.type === field_types.USER.id && (field_value_raw || is_editable)) { const pill_container = $(element_id) - .find('.custom_user_field[data-field-id="' + field.id + '"] .pill-container') + .find(`.custom_user_field[data-field-id="${CSS.escape(field.id)}"] .pill-container`) .expectOne(); const pills = user_pill.create_pills(pill_container); diff --git a/static/js/settings_bots.js b/static/js/settings_bots.js index 99f8ae4a37..4cf031be6e 100644 --- a/static/js/settings_bots.js +++ b/static/js/settings_bots.js @@ -42,7 +42,7 @@ const focus_tab = { }; exports.get_bot_info_div = function (bot_id) { - const sel = '.bot_info[data-user-id="' + bot_id + '"]'; + const sel = `.bot_info[data-user-id="${CSS.escape(bot_id)}"]`; return $(sel).expectOne(); }; @@ -229,7 +229,7 @@ exports.set_up = function () { const selected_embedded_bot = "converter"; $("#select_service_name").val(selected_embedded_bot); // TODO: Use 'select a bot'. $("#config_inputbox").children().hide(); - $("[name*='" + selected_embedded_bot + "']").show(); + $(`[name*='${CSS.escape(selected_embedded_bot)}']`).show(); $("#download_botserverrc").on("click", function () { const OUTGOING_WEBHOOK_BOT_TYPE_INT = 3; @@ -293,7 +293,7 @@ exports.set_up = function () { } else if (bot_type === EMBEDDED_BOT_TYPE) { formData.append("service_name", service_name); const config_data = {}; - $("#config_inputbox [name*='" + service_name + "'] input").each(function () { + $(`#config_inputbox [name*='${CSS.escape(service_name)}'] input`).each(function () { config_data[$(this).attr("name")] = $(this).val(); }); formData.append("config_data", JSON.stringify(config_data)); @@ -317,7 +317,7 @@ exports.set_up = function () { $("#create_payload_url").val(""); $("#payload_url_inputbox").hide(); $("#config_inputbox").hide(); - $("[name*='" + service_name + "'] input").each(function () { + $(`[name*='${CSS.escape(service_name)}'] input`).each(function () { $(this).val(""); }); $("#create_bot_type").val(GENERIC_BOT_TYPE); @@ -362,7 +362,7 @@ exports.set_up = function () { $("#select_service_name").on("change", () => { $("#config_inputbox").children().hide(); const selected_bot = $("#select_service_name :selected").val(); - $("[name*='" + selected_bot + "']").show(); + $(`[name*='${CSS.escape(selected_bot)}']`).show(); }); $("#active_bots_list").on("click", "button.delete_bot", (e) => { diff --git a/static/js/settings_display.js b/static/js/settings_display.js index fa3066a6e8..946dc9e9ca 100644 --- a/static/js/settings_display.js +++ b/static/js/settings_display.js @@ -41,7 +41,7 @@ exports.set_up = function () { $("#twenty_four_hour_time").val(JSON.stringify(page_params.twenty_four_hour_time)); - $(".emojiset_choice[value=" + page_params.emojiset + "]").prop("checked", true); + $(`.emojiset_choice[value="${CSS.escape(page_params.emojiset)}"]`).prop("checked", true); $("#default_language_modal [data-dismiss]").on("click", () => { overlays.close_modal("#default_language_modal"); @@ -49,7 +49,7 @@ exports.set_up = function () { const all_display_settings = settings_config.get_all_display_settings(); for (const setting of all_display_settings.settings.user_display_settings) { - $("#" + setting).on("change", function () { + $(`#${CSS.escape(setting)}`).on("change", function () { const data = {}; data[setting] = JSON.stringify($(this).prop("checked")); diff --git a/static/js/settings_notifications.js b/static/js/settings_notifications.js index 11bdd68346..22d3779d9b 100644 --- a/static/js/settings_notifications.js +++ b/static/js/settings_notifications.js @@ -156,7 +156,7 @@ exports.update_page = function () { continue; } - $("#" + setting).prop("checked", page_params[setting]); + $(`#${CSS.escape(setting)}`).prop("checked", page_params[setting]); } rerender_ui(); }; diff --git a/static/js/settings_org.js b/static/js/settings_org.js index b0ab076dd3..8c8dca3cab 100644 --- a/static/js/settings_org.js +++ b/static/js/settings_org.js @@ -210,11 +210,11 @@ const simple_dropdown_properties = [ ]; function set_property_dropdown_value(property_name) { - $("#id_" + property_name).val(get_property_value(property_name)); + $(`#id_${CSS.escape(property_name)}`).val(get_property_value(property_name)); } function change_element_block_display_property(elem_id, show_element) { - const elem = $("#" + elem_id); + const elem = $(`#${CSS.escape(elem_id)}`); if (show_element) { elem.parent().show(); } else { @@ -433,7 +433,7 @@ exports.sync_realm_settings = function (property) { } else if (property === "invite_required" || property === "invite_by_admins_only") { property = "user_invite_restriction"; } - const element = $("#id_realm_" + property); + const element = $(`#id_realm_${CSS.escape(property)}`); if (element.length) { discard_property_element_changes(element); } diff --git a/static/js/settings_panel_menu.js b/static/js/settings_panel_menu.js index ebcd1b3759..8d2bdcdc95 100644 --- a/static/js/settings_panel_menu.js +++ b/static/js/settings_panel_menu.js @@ -47,7 +47,7 @@ class SettingsPanelMenu { } li_for_section(section) { - const li = $("#settings_overlay_container li[data-section='" + section + "']"); + const li = $(`#settings_overlay_container li[data-section='${CSS.escape(section)}']`); return li; } @@ -123,7 +123,7 @@ class SettingsPanelMenu { get_panel() { const section = this.curr_li.data("section"); - const sel = "[data-name='" + section + "']"; + const sel = `[data-name='${CSS.escape(section)}']`; const panel = $(".settings-section" + sel); return panel; } diff --git a/static/js/settings_profile_fields.js b/static/js/settings_profile_fields.js index d1c1a1bbc7..46a6ffdecc 100644 --- a/static/js/settings_profile_fields.js +++ b/static/js/settings_profile_fields.js @@ -209,8 +209,8 @@ function delete_choice_row(e) { function get_profile_field_info(id) { const info = {}; - info.row = $("tr.profile-field-row[data-profile-field-id='" + id + "']"); - info.form = $("tr.profile-field-form[data-profile-field-id='" + id + "']"); + info.row = $(`tr.profile-field-row[data-profile-field-id='${CSS.escape(id)}']`); + info.form = $(`tr.profile-field-form[data-profile-field-id='${CSS.escape(id)}']`); return info; } diff --git a/static/js/settings_ui.js b/static/js/settings_ui.js index d789e33d22..9c37ac5f8b 100644 --- a/static/js/settings_ui.js +++ b/static/js/settings_ui.js @@ -77,13 +77,13 @@ exports.do_settings_change = function (request_method, url, data, status_element // when main setting unchecked. exports.disable_sub_setting_onchange = function (is_checked, sub_setting_id, disable_on_uncheck) { if ((is_checked && disable_on_uncheck) || (!is_checked && !disable_on_uncheck)) { - $("#" + sub_setting_id).prop("disabled", false); - $("#" + sub_setting_id + "_label") + $(`#${CSS.escape(sub_setting_id)}`).prop("disabled", false); + $(`#${CSS.escape(sub_setting_id)}_label`) .parent() .removeClass("control-label-disabled"); } else if ((is_checked && !disable_on_uncheck) || (!is_checked && disable_on_uncheck)) { - $("#" + sub_setting_id).prop("disabled", true); - $("#" + sub_setting_id + "_label") + $(`#${CSS.escape(sub_setting_id)}`).prop("disabled", true); + $(`#${CSS.escape(sub_setting_id)}_label`) .parent() .addClass("control-label-disabled"); } diff --git a/static/js/settings_user_groups.js b/static/js/settings_user_groups.js index 69dedf5493..9b038b7993 100644 --- a/static/js/settings_user_groups.js +++ b/static/js/settings_user_groups.js @@ -57,14 +57,14 @@ exports.populate_user_groups = function () { }, }), ); - const pill_container = $('.pill-container[data-group-pills="' + data.id + '"]'); + const pill_container = $(`.pill-container[data-group-pills="${CSS.escape(data.id)}"]`); const pills = user_pill.create_pills(pill_container); function get_pill_user_ids() { return user_pill.get_user_ids(pills); } - const userg = $('div.user-group[id="' + data.id + '"]'); + const userg = $(`div.user-group[id="${CSS.escape(data.id)}"]`); for (const user_id of data.members) { const user = people.get_by_user_id(user_id); user_pill.append_user(user, pills); @@ -97,13 +97,13 @@ exports.populate_user_groups = function () { const group_data = user_groups.get_user_group_from_id(data.id); const original_group = Array.from(group_data.members); const same_groups = _.isEqual(_.sortBy(draft_group), _.sortBy(original_group)); - const description = $("#user-groups #" + data.id + " .description") + const description = $(`#user-groups #${CSS.escape(data.id)} .description`) .text() .trim(); - const name = $("#user-groups #" + data.id + " .name") + const name = $(`#user-groups #${CSS.escape(data.id)} .name`) .text() .trim(); - const user_group_status = $("#user-groups #" + data.id + " .user-group-status"); + const user_group_status = $(`#user-groups #${CSS.escape(data.id)} .user-group-status`); if (user_group_status.is(":visible")) { return false; @@ -123,9 +123,9 @@ exports.populate_user_groups = function () { if (!exports.can_edit(data.id)) { return; } - const cancel_button = $("#user-groups #" + data.id + " .save-status.btn-danger"); - const saved_button = $("#user-groups #" + data.id + " .save-status.sea-green"); - const save_instructions = $("#user-groups #" + data.id + " .save-instructions"); + const cancel_button = $(`#user-groups #${CSS.escape(data.id)} .save-status.btn-danger`); + const saved_button = $(`#user-groups #${CSS.escape(data.id)} .save-status.sea-green`); + const save_instructions = $(`#user-groups #${CSS.escape(data.id)} .save-instructions`); if (is_user_group_changed() && !cancel_button.is(":visible")) { saved_button.fadeOut(0); @@ -138,9 +138,9 @@ exports.populate_user_groups = function () { } function show_saved_button() { - const cancel_button = $("#user-groups #" + data.id + " .save-status.btn-danger"); - const saved_button = $("#user-groups #" + data.id + " .save-status.sea-green"); - const save_instructions = $("#user-groups #" + data.id + " .save-instructions"); + const cancel_button = $(`#user-groups #${CSS.escape(data.id)} .save-status.btn-danger`); + const saved_button = $(`#user-groups #${CSS.escape(data.id)} .save-status.sea-green`); + const save_instructions = $(`#user-groups #${CSS.escape(data.id)} .save-instructions`); if (!saved_button.is(":visible")) { cancel_button.fadeOut(0); save_instructions.fadeOut(0); @@ -175,12 +175,12 @@ exports.populate_user_groups = function () { } function save_name_desc() { - const user_group_status = $("#user-groups #" + data.id + " .user-group-status"); + const user_group_status = $(`#user-groups #${CSS.escape(data.id)} .user-group-status`); const group_data = user_groups.get_user_group_from_id(data.id); - const description = $("#user-groups #" + data.id + " .description") + const description = $(`#user-groups #${CSS.escape(data.id)} .description`) .text() .trim(); - const name = $("#user-groups #" + data.id + " .name") + const name = $(`#user-groups #${CSS.escape(data.id)} .name`) .text() .trim(); @@ -203,8 +203,10 @@ exports.populate_user_groups = function () { xhr.responseText = JSON.stringify({msg: errors}); ui_report.error(i18n.t("Failed"), xhr, user_group_status); update_cancel_button(); - $("#user-groups #" + data.id + " .name").text(group_data.name); - $("#user-groups #" + data.id + " .description").text(group_data.description); + $(`#user-groups #${CSS.escape(data.id)} .name`).text(group_data.name); + $(`#user-groups #${CSS.escape(data.id)} .description`).text( + group_data.description, + ); }, }); } @@ -219,7 +221,7 @@ exports.populate_user_groups = function () { [".pill-container", ".name", ".description", ".input", ".delete"], except_class, ); - if ($(event.relatedTarget).closest("#user-groups #" + data.id).length) { + if ($(event.relatedTarget).closest(`#user-groups #${CSS.escape(data.id)}`).length) { return blur_exceptions.some( (class_name) => $(event.relatedTarget).closest(class_name).length, ); @@ -236,7 +238,7 @@ exports.populate_user_groups = function () { return; } if ( - $(event.relatedTarget).closest("#user-groups #" + data.id) && + $(event.relatedTarget).closest(`#user-groups #${CSS.escape(data.id)}`) && $(event.relatedTarget).closest(".save-status.btn-danger").length ) { exports.reload(); @@ -246,21 +248,21 @@ exports.populate_user_groups = function () { save_members(); } - $("#user-groups #" + data.id).on("blur", ".input", (event) => { + $(`#user-groups #${CSS.escape(data.id)}`).on("blur", ".input", (event) => { auto_save(".input", event); }); - $("#user-groups #" + data.id).on("blur", ".name", (event) => { + $(`#user-groups #${CSS.escape(data.id)}`).on("blur", ".name", (event) => { auto_save(".name", event); }); - $("#user-groups #" + data.id).on("input", ".name", () => { + $(`#user-groups #${CSS.escape(data.id)}`).on("input", ".name", () => { update_cancel_button(); }); - $("#user-groups #" + data.id).on("blur", ".description", (event) => { + $(`#user-groups #${CSS.escape(data.id)}`).on("blur", ".description", (event) => { auto_save(".description", event); }); - $("#user-groups #" + data.id).on("input", ".description", () => { + $(`#user-groups #${CSS.escape(data.id)}`).on("input", ".description", () => { update_cancel_button(); }); diff --git a/static/js/settings_users.js b/static/js/settings_users.js index 3d5ef199ff..375e10afd5 100644 --- a/static/js/settings_users.js +++ b/static/js/settings_users.js @@ -66,7 +66,7 @@ function sort_last_active(a, b) { } function get_user_info_row(user_id) { - return $("tr.user_row[data-user-id='" + user_id + "']"); + return $(`tr.user_row[data-user-id='${CSS.escape(user_id)}']`); } function set_user_role_dropdown(person) { @@ -258,7 +258,7 @@ section.bots.create_table = () => { name: "admin_bot_list", get_item: bot_info, modifier: render_admin_user_list, - html_selector: (item) => `tr[data-user-id='${item}']`, + html_selector: (item) => `tr[data-user-id='${CSS.escape(item)}']`, filter: { element: $bots_table.closest(".settings-section").find(".search"), predicate(item, value) { @@ -403,7 +403,7 @@ function open_human_form(person) { set_user_role_dropdown(person); if (!page_params.is_owner) { $("#user-role-select") - .find("option[value=" + settings_config.user_role_values.owner.code + "]") + .find(`option[value="${CSS.escape(settings_config.user_role_values.owner.code)}"]`) .hide(); } diff --git a/static/js/stream_color.js b/static/js/stream_color.js index 932ff1d1e7..7d400cc424 100644 --- a/static/js/stream_color.js +++ b/static/js/stream_color.js @@ -39,8 +39,8 @@ function update_table_stream_color(table, stream_name, color) { } function update_stream_sidebar_swatch_color(id, color) { - $("#stream_sidebar_swatch_" + id).css("background-color", color); - $("#stream_sidebar_privacy_swatch_" + id).css("color", color); + $(`#stream_sidebar_swatch_${CSS.escape(id)}`).css("background-color", color); + $(`#stream_sidebar_privacy_swatch_${CSS.escape(id)}`).css("color", color); } function update_historical_message_color(stream_name, color) { @@ -77,20 +77,23 @@ exports.update_stream_color = function (sub, color, opts) { sub.color = color; const stream_id = sub.stream_id; // The swatch in the subscription row header. - $(".stream-row[data-stream-id='" + stream_id + "'] .icon").css("background-color", color); + $(`.stream-row[data-stream-id='${CSS.escape(stream_id)}'] .icon`).css( + "background-color", + color, + ); // The swatch in the color picker. exports.set_colorpicker_color( $( - "#subscription_overlay .subscription_settings[data-stream-id='" + - stream_id + - "'] .colorpicker", + `#subscription_overlay .subscription_settings[data-stream-id='${CSS.escape( + stream_id, + )}'] .colorpicker`, ), color, ); $( - "#subscription_overlay .subscription_settings[data-stream-id='" + - stream_id + - "'] .large-icon", + `#subscription_overlay .subscription_settings[data-stream-id='${CSS.escape( + stream_id, + )}'] .large-icon`, ).css("color", color); if (opts.update_historical) { diff --git a/static/js/stream_edit.js b/static/js/stream_edit.js index 46545faa51..b196f1bc41 100644 --- a/static/js/stream_edit.js +++ b/static/js/stream_edit.js @@ -40,7 +40,9 @@ exports.setup_subscriptions_tab_hash = function (tab_key_value) { exports.settings_for_sub = function (sub) { return $( - "#subscription_overlay .subscription_settings[data-stream-id='" + sub.stream_id + "']", + `#subscription_overlay .subscription_settings[data-stream-id='${CSS.escape( + sub.stream_id, + )}']`, ); }; @@ -168,7 +170,9 @@ function format_member_list_elem(person) { function get_subscriber_list(sub_row) { const stream_id_str = sub_row.data("stream-id"); - return $('.subscription_settings[data-stream-id="' + stream_id_str + '"] .subscriber-list'); + return $( + `.subscription_settings[data-stream-id="${CSS.escape(stream_id_str)}"] .subscriber-list`, + ); } exports.update_stream_name = function (sub, new_name) { @@ -294,9 +298,9 @@ function show_subscription_settings(sub) { stream_ui_updates.update_add_subscriptions_elements(sub); const container = $( - "#subscription_overlay .subscription_settings[data-stream-id='" + - stream_id + - "'] .pill-container", + `#subscription_overlay .subscription_settings[data-stream-id='${CSS.escape( + stream_id, + )}'] .pill-container`, ); function create_item_from_text(text, current_items) { @@ -345,7 +349,7 @@ function show_subscription_settings(sub) { return format_member_list_elem(item); }, filter: { - element: $("[data-stream-id='" + stream_id + "'] .search"), + element: $(`[data-stream-id='${CSS.escape(stream_id)}'] .search`), predicate(item, value) { const person = item; @@ -433,7 +437,11 @@ function stream_is_muted_changed(e) { const sub_settings = exports.settings_for_sub(sub); const notification_checkboxes = sub_settings.find(".sub_notification_setting"); - subs.set_muted(sub, e.target.checked, `#stream_change_property_status${sub.stream_id}`); + subs.set_muted( + sub, + e.target.checked, + `#stream_change_property_status${CSS.escape(sub.stream_id)}`, + ); sub_settings.find(".mute-note").toggleClass("hide-mute-note", !sub.is_muted); notification_checkboxes.toggleClass("muted-sub", sub.is_muted); notification_checkboxes.find("input[type='checkbox']").prop("disabled", sub.is_muted); @@ -447,7 +455,7 @@ exports.stream_setting_changed = function (e, from_notification_settings) { const sub = get_sub_for_target(e.target); const status_element = from_notification_settings ? $(e.target).closest(".subsection-parent").find(".alert-notification") - : $("#stream_change_property_status" + sub.stream_id); + : $(`#stream_change_property_status${CSS.escape(sub.stream_id)}`); const setting = e.target.name; if (!sub) { blueslip.error("undefined sub in stream_setting_changed()"); @@ -796,7 +804,7 @@ exports.initialize = function () { const sub = get_sub_for_target(e.target); // Makes sure we take the correct stream_row. const stream_row = $( - "#subscriptions_table div.stream-row[data-stream-id='" + sub.stream_id + "']", + `#subscriptions_table div.stream-row[data-stream-id='${CSS.escape(sub.stream_id)}']`, ); subs.sub_or_unsub(sub, stream_row); diff --git a/static/js/stream_events.js b/static/js/stream_events.js index 42d842aea0..43be47c16a 100644 --- a/static/js/stream_events.js +++ b/static/js/stream_events.js @@ -7,7 +7,7 @@ const peer_data = require("./peer_data"); // doing so is unnecessary with the current code. Ideally, we'd do a // refactor to address that, however. function update_stream_setting(sub, value, setting) { - const setting_checkbox = $("#" + setting + "_" + sub.stream_id); + const setting_checkbox = $(`#${CSS.escape(setting)}_${CSS.escape(sub.stream_id)}`); setting_checkbox.prop("checked", value); sub[setting] = value; } diff --git a/static/js/stream_muting.js b/static/js/stream_muting.js index 6bf1680a8b..5c55240d2b 100644 --- a/static/js/stream_muting.js +++ b/static/js/stream_muting.js @@ -53,9 +53,9 @@ exports.update_is_muted = function (sub, value) { stream_list.set_in_home_view(sub.stream_id, !sub.is_muted); const is_muted_checkbox = $( - ".subscription_settings[data-stream-id='" + - sub.stream_id + - "'] #sub_is_muted_setting .sub_setting_control", + `.subscription_settings[data-stream-id='${CSS.escape( + sub.stream_id, + )}'] #sub_is_muted_setting .sub_setting_control`, ); is_muted_checkbox.prop("checked", value); }; diff --git a/static/js/stream_popover.js b/static/js/stream_popover.js index 2ff6c96f2b..a3288e35f6 100644 --- a/static/js/stream_popover.js +++ b/static/js/stream_popover.js @@ -186,7 +186,7 @@ function build_stream_popover(opts) { }); $(elt).popover("show"); - const popover = $(".streams_popover[data-stream-id=" + stream_id + "]"); + const popover = $(`.streams_popover[data-stream-id="${CSS.escape(stream_id)}"]`); update_spectrum(popover, (colorpicker) => { colorpicker.spectrum(stream_color.sidebar_popover_colorpicker_options); diff --git a/static/js/stream_ui_updates.js b/static/js/stream_ui_updates.js index 022bb71508..ea55b79c97 100644 --- a/static/js/stream_ui_updates.js +++ b/static/js/stream_ui_updates.js @@ -82,7 +82,7 @@ exports.update_regular_sub_settings = function (sub) { if (!stream_edit.is_sub_settings_active(sub)) { return; } - const $settings = $(".subscription_settings[data-stream-id='" + sub.stream_id + "']"); + const $settings = $(`.subscription_settings[data-stream-id='${CSS.escape(sub.stream_id)}']`); if (sub.subscribed) { if ($settings.find(".email-address").val().length === 0) { // Rerender stream email address, if not. @@ -113,7 +113,7 @@ exports.update_notification_setting_checkbox = function (notification_name) { return; } const stream_id = stream_row.data("stream-id"); - $(`#${notification_name}_${stream_id}`).prop( + $(`#${CSS.escape(notification_name)}_${CSS.escape(stream_id)}`).prop( "checked", stream_data.receives_notifications(stream_id, notification_name), ); diff --git a/static/js/subs.js b/static/js/subs.js index 2c6b9e368b..2cb8e5615e 100644 --- a/static/js/subs.js +++ b/static/js/subs.js @@ -26,17 +26,19 @@ exports.show_subs_pane = { }; exports.check_button_for_sub = function (sub) { - return $(".stream-row[data-stream-id='" + sub.stream_id + "'] .check"); + return $(`.stream-row[data-stream-id='${CSS.escape(sub.stream_id)}'] .check`); }; exports.row_for_stream_id = function (stream_id) { - return $(".stream-row[data-stream-id='" + stream_id + "']"); + return $(`.stream-row[data-stream-id='${CSS.escape(stream_id)}']`); }; exports.settings_button_for_sub = function (sub) { // We don't do expectOne() here, because this button is only // visible if the user has that stream selected in the streams UI. - return $(".subscription_settings[data-stream-id='" + sub.stream_id + "'] .subscribe-button"); + return $( + `.subscription_settings[data-stream-id='${CSS.escape(sub.stream_id)}'] .subscribe-button`, + ); }; function get_row_data(row) { @@ -284,7 +286,9 @@ exports.remove_stream = function (stream_id) { exports.update_settings_for_subscribed = function (sub) { stream_ui_updates.update_add_subscriptions_elements(sub); $( - ".subscription_settings[data-stream-id='" + sub.stream_id + "'] #preview-stream-button", + `.subscription_settings[data-stream-id='${CSS.escape( + sub.stream_id, + )}'] #preview-stream-button`, ).show(); if (exports.is_sub_already_present(sub)) { diff --git a/static/js/timerender.js b/static/js/timerender.js index 9b928c748c..7447bfb718 100644 --- a/static/js/timerender.js +++ b/static/js/timerender.js @@ -193,7 +193,7 @@ exports.update_timestamps = function () { for (const entry of to_process) { const className = entry.className; - const elements = $("." + className); + const elements = $(`.${CSS.escape(className)}`); // The element might not exist any more (because it // was in the zfilt table, or because we added // messages above it and re-collapsed). diff --git a/static/js/ui_util.js b/static/js/ui_util.js index 0f8e2cef58..784d5a6410 100644 --- a/static/js/ui_util.js +++ b/static/js/ui_util.js @@ -4,7 +4,7 @@ // dependencies other than jQuery. exports.change_tab_to = function (tabname) { - $('#gear-menu a[href="' + tabname + '"]').tab("show"); + $(`#gear-menu a[href="${CSS.escape(tabname)}"]`).tab("show"); }; // https://stackoverflow.com/questions/4233265/contenteditable-set-caret-at-the-end-of-the-text-cross-browser diff --git a/static/js/upload.js b/static/js/upload.js index 219e95c3b6..8da345d4a5 100644 --- a/static/js/upload.js +++ b/static/js/upload.js @@ -58,27 +58,29 @@ exports.get_item = function (key, config) { } switch (key) { case "textarea": - return $("#message_edit_content_" + config.row); + return $(`#message_edit_content_${CSS.escape(config.row)}`); case "send_button": - return $("#message_edit_content_" + config.row) + return $(`#message_edit_content_${CSS.escape(config.row)}`) .closest("#message_edit_form") .find(".message_edit_save"); case "send_status_identifier": - return "#message-edit-send-status-" + config.row; + return `#message-edit-send-status-${CSS.escape(config.row)}`; case "send_status": - return $("#message-edit-send-status-" + config.row); + return $(`#message-edit-send-status-${CSS.escape(config.row)}`); case "send_status_close_button": - return $("#message-edit-send-status-" + config.row).find(".send-status-close"); + return $(`#message-edit-send-status-${CSS.escape(config.row)}`).find( + ".send-status-close", + ); case "send_status_message": - return $("#message-edit-send-status-" + config.row).find(".error-msg"); + return $(`#message-edit-send-status-${CSS.escape(config.row)}`).find(".error-msg"); case "file_input_identifier": - return "#message_edit_file_input_" + config.row; + return `#message_edit_file_input_${CSS.escape(config.row)}`; case "source": return "message-edit-file-input"; case "drag_drop_container": return $("#message_edit_form"); case "markdown_preview_hide_button": - return $("#undo_markdown_preview_" + config.row); + return $(`#undo_markdown_preview_${CSS.escape(config.row)}`); default: throw new Error(`Invalid key name for mode "${config.mode}"`); } diff --git a/tools/message-screenshot.js b/tools/message-screenshot.js index 9960d8c169..b5939329fc 100644 --- a/tools/message-screenshot.js +++ b/tools/message-screenshot.js @@ -1,10 +1,11 @@ "use strict"; -/* global $, navigate */ +/* global $, CSS, navigate */ const path = require("path"); const commander = require("commander"); +require("css.escape"); const mkdirp = require("mkdirp"); const puppeteer = require("puppeteer"); @@ -54,10 +55,10 @@ async function run() { // Navigate to message and capture screenshot await page.goto(`http://${host}/#narrow/near/${options.messageId}`); - const messageSelector = `#zfilt${options.messageId}`; + const messageSelector = `#zfilt${CSS.escape(options.messageId)}`; await page.waitForSelector(messageSelector); // remove unread marker and don't select message - const marker = `#zfilt${options.messageId} .unread_marker`; + const marker = `#zfilt${CSS.escape(options.messageId)} .unread_marker`; await page.evaluate((sel) => $(sel).remove(), marker); await page.evaluate(() => navigate.up()); const messageBox = await page.$(messageSelector); diff --git a/version.py b/version.py index a0750341b2..7ef81e9a40 100644 --- a/version.py +++ b/version.py @@ -43,4 +43,4 @@ API_FEATURE_LEVEL = 38 # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = '124.1' +PROVISION_VERSION = '124.2' diff --git a/yarn.lock b/yarn.lock index 61d8529c80..769cbe5644 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3619,6 +3619,11 @@ css-what@^3.2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + csscolorparser@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b"