From c172f2068dbc4800a5ad02005c30d27f837765a4 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 27 Aug 2020 14:07:51 -0400 Subject: [PATCH] REFACTOR: Remove $.cookie in favor of a local library based on it (#10548) This helps us out in a few ways: 1. It lessens our reliance on jQuery 2. It's slightly less code because it omits options we don't use 3. It is one less library to import and put into ES6 modules --- .../app/components/create-account.js | 6 +- .../discourse/app/components/global-notice.js | 5 +- .../app/components/group-membership-button.js | 3 +- .../discourse/app/components/login-modal.js | 6 +- .../app/controllers/create-account.js | 7 +- .../app/controllers/forgot-password.js | 5 +- .../discourse/app/controllers/login.js | 13 +- .../javascripts/discourse/app/lib/cookie.js | 99 +++++++++++++++ .../discourse/app/lib/theme-selector.js | 5 +- .../javascripts/discourse/app/models/user.js | 9 +- .../discourse/app/routes/new-message.js | 3 +- .../discourse/app/routes/new-topic.js | 3 +- app/assets/javascripts/vendor.js | 1 - .../acceptance/preferences-test.js | 13 +- vendor/assets/javascripts/jquery.cookie.js | 117 ------------------ 15 files changed, 145 insertions(+), 150 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/lib/cookie.js delete mode 100644 vendor/assets/javascripts/jquery.cookie.js diff --git a/app/assets/javascripts/discourse/app/components/create-account.js b/app/assets/javascripts/discourse/app/components/create-account.js index e382244dc9e..f1d8c8f06d2 100644 --- a/app/assets/javascripts/discourse/app/components/create-account.js +++ b/app/assets/javascripts/discourse/app/components/create-account.js @@ -1,12 +1,14 @@ import Component from "@ember/component"; +import cookie from "discourse/lib/cookie"; + export default Component.extend({ classNames: ["create-account"], didInsertElement() { this._super(...arguments); - if ($.cookie("email")) { - this.set("email", $.cookie("email")); + if (cookie("email")) { + this.set("email", cookie("email")); } $(this.element).on("keydown.discourse-create-account", e => { diff --git a/app/assets/javascripts/discourse/app/components/global-notice.js b/app/assets/javascripts/discourse/app/components/global-notice.js index e76a3687202..01e3636af46 100644 --- a/app/assets/javascripts/discourse/app/components/global-notice.js +++ b/app/assets/javascripts/discourse/app/components/global-notice.js @@ -4,6 +4,7 @@ import I18n from "I18n"; import Component from "@ember/component"; import LogsNotice from "discourse/services/logs-notice"; import EmberObject, { computed } from "@ember/object"; +import cookie, { removeCookie } from "discourse/lib/cookie"; const _pluginNotices = []; @@ -67,8 +68,8 @@ export default Component.extend({ function() { let notices = []; - if ($.cookie("dosp") === "1") { - $.removeCookie("dosp", { path: "/" }); + if (cookie("dosp") === "1") { + removeCookie("dosp", { path: "/" }); notices.push( Notice.create({ text: I18n.t("forced_anonymous"), diff --git a/app/assets/javascripts/discourse/app/components/group-membership-button.js b/app/assets/javascripts/discourse/app/components/group-membership-button.js index e0237764aea..f35b6549f1b 100644 --- a/app/assets/javascripts/discourse/app/components/group-membership-button.js +++ b/app/assets/javascripts/discourse/app/components/group-membership-button.js @@ -4,6 +4,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; import showModal from "discourse/lib/show-modal"; import bootbox from "bootbox"; +import cookie from "discourse/lib/cookie"; export default Component.extend({ classNames: ["group-membership-button"], @@ -30,7 +31,7 @@ export default Component.extend({ _showLoginModal() { this.showLogin(); - $.cookie("destination_url", window.location.href); + cookie("destination_url", window.location.href); }, removeFromGroup() { diff --git a/app/assets/javascripts/discourse/app/components/login-modal.js b/app/assets/javascripts/discourse/app/components/login-modal.js index 2ee72f87225..42b55809c0b 100644 --- a/app/assets/javascripts/discourse/app/components/login-modal.js +++ b/app/assets/javascripts/discourse/app/components/login-modal.js @@ -1,5 +1,7 @@ import { schedule } from "@ember/runloop"; import Component from "@ember/component"; +import cookie from "discourse/lib/cookie"; + export default Component.extend({ didInsertElement() { this._super(...arguments); @@ -11,8 +13,8 @@ export default Component.extend({ "loginPassword", $("#hidden-login-form input[name=password]").val() ); - } else if ($.cookie("email")) { - this.set("loginName", $.cookie("email")); + } else if (cookie("email")) { + this.set("loginName", cookie("email")); } schedule("afterRender", () => { diff --git a/app/assets/javascripts/discourse/app/controllers/create-account.js b/app/assets/javascripts/discourse/app/controllers/create-account.js index 3c9c7fe3411..1b971287c96 100644 --- a/app/assets/javascripts/discourse/app/controllers/create-account.js +++ b/app/assets/javascripts/discourse/app/controllers/create-account.js @@ -21,6 +21,7 @@ import { findAll } from "discourse/models/login-method"; import EmberObject from "@ember/object"; import User from "discourse/models/user"; import { Promise } from "rsvp"; +import cookie, { removeCookie } from "discourse/lib/cookie"; export default Controller.extend( ModalFunctionality, @@ -270,7 +271,7 @@ export default Controller.extend( const destinationUrl = this.get("authOptions.destination_url"); if (!isEmpty(destinationUrl)) { - $.cookie("destination_url", destinationUrl, { path: "/" }); + cookie("destination_url", destinationUrl, { path: "/" }); } // Add the userfields to the data @@ -326,12 +327,12 @@ export default Controller.extend( this.rejectedPasswords.pushObject(attrs.accountPassword); } this.set("formSubmitted", false); - $.removeCookie("destination_url"); + removeCookie("destination_url"); } }, () => { this.set("formSubmitted", false); - $.removeCookie("destination_url"); + removeCookie("destination_url"); return this.flash(I18n.t("create_account.failed"), "error"); } ); diff --git a/app/assets/javascripts/discourse/app/controllers/forgot-password.js b/app/assets/javascripts/discourse/app/controllers/forgot-password.js index 1adaa5a8b05..ad838d5541f 100644 --- a/app/assets/javascripts/discourse/app/controllers/forgot-password.js +++ b/app/assets/javascripts/discourse/app/controllers/forgot-password.js @@ -7,6 +7,7 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; import { escapeExpression } from "discourse/lib/utilities"; import { extractError } from "discourse/lib/ajax-error"; import getURL from "discourse-common/lib/get-url"; +import cookie from "discourse/lib/cookie"; export default Controller.extend(ModalFunctionality, { offerHelp: null, @@ -18,8 +19,8 @@ export default Controller.extend(ModalFunctionality, { }, onShow() { - if ($.cookie("email")) { - this.set("accountEmailOrUsername", $.cookie("email")); + if (cookie("email")) { + this.set("accountEmailOrUsername", cookie("email")); } }, diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index f43886ab8c8..357bfde5308 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -17,6 +17,7 @@ import { extractError } from "discourse/lib/ajax-error"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; import { getWebauthnCredential } from "discourse/lib/webauthn"; import bootbox from "bootbox"; +import cookie, { removeCookie } from "discourse/lib/cookie"; // This is happening outside of the app via popup const AuthErrors = [ @@ -187,19 +188,19 @@ export default Controller.extend(ModalFunctionality, { hiddenLoginForm.querySelector(`input[name=${key}]`).value = value; }; - const destinationUrl = $.cookie("destination_url"); - const ssoDestinationUrl = $.cookie("sso_destination_url"); + const destinationUrl = cookie("destination_url"); + const ssoDestinationUrl = cookie("sso_destination_url"); applyHiddenFormInputValue(this.loginName, "username"); applyHiddenFormInputValue(this.loginPassword, "password"); if (ssoDestinationUrl) { - $.removeCookie("sso_destination_url"); + removeCookie("sso_destination_url"); window.location.assign(ssoDestinationUrl); return; } else if (destinationUrl) { // redirect client to the original URL - $.removeCookie("destination_url"); + removeCookie("destination_url"); applyHiddenFormInputValue(destinationUrl, "redirect"); } else { @@ -369,10 +370,10 @@ export default Controller.extend(ModalFunctionality, { // Reload the page if we're authenticated if (options.authenticated) { const destinationUrl = - $.cookie("destination_url") || options.destination_url; + cookie("destination_url") || options.destination_url; if (destinationUrl) { // redirect client to the original URL - $.removeCookie("destination_url"); + removeCookie("destination_url"); window.location.href = destinationUrl; } else if (window.location.pathname === getURL("/login")) { window.location = getURL("/"); diff --git a/app/assets/javascripts/discourse/app/lib/cookie.js b/app/assets/javascripts/discourse/app/lib/cookie.js new file mode 100644 index 00000000000..cace18f2878 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/cookie.js @@ -0,0 +1,99 @@ +import deprecated from "discourse-common/lib/deprecated"; + +const pluses = /\+/g; + +function parseCookieValue(s) { + if (s.indexOf('"') === 0) { + // This is a quoted cookie as according to RFC2068, unescape... + s = s + .slice(1, -1) + .replace(/\\"/g, '"') + .replace(/\\\\/g, "\\"); + } + + try { + // Replace server-side written pluses with spaces. + // If we can't decode the cookie, ignore it, it's unusable. + // If we can't parse the cookie, ignore it, it's unusable. + s = decodeURIComponent(s.replace(pluses, " ")); + return s; + } catch (e) {} +} + +function cookie(key, value, options) { + // Write + if (value !== undefined) { + options = Object.assign({}, options || {}); + + if (typeof options.expires === "number") { + let days = options.expires, + t = (options.expires = new Date()); + t.setTime(+t + days * 864e5); + } + + return (document.cookie = [ + encodeURIComponent(key), + "=", + encodeURIComponent(String(value)), + options.expires ? "; expires=" + options.expires.toUTCString() : "", // use expires attribute, max-age is not supported by IE + options.path ? "; path=" + options.path : "", + options.domain ? "; domain=" + options.domain : "", + options.secure ? "; secure" : "" + ].join("")); + } + + // Read + let result = key ? undefined : {}; + + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling cookie(). + let cookies = document.cookie ? document.cookie.split("; ") : []; + + for (let i = 0, l = cookies.length; i < l; i++) { + let parts = cookies[i].split("="); + let name = decodeURIComponent(parts.shift()); + let c = parts.join("="); + + if (key && key === name) { + result = parseCookieValue(c); + break; + } + + // Prevent storing a cookie that we couldn't decode. + if (!key && (c = parseCookieValue(c)) !== undefined) { + result[name] = c; + } + } + return result; +} + +export function removeCookie(key, options) { + if (cookie(key) === undefined) { + return false; + } + + // Must not alter options, thus extending a fresh object... + cookie(key, "", Object.assign({}, options || {}, { expires: -1 })); + return !cookie(key); +} + +if (window && window.$) { + const depOpts = { since: "2.6.0", dropFrom: "2.7.0" }; + window.$.cookie = function() { + deprecated( + "$.cookie is being removed from Discourse. Please import our cookie module and use that instead.", + depOpts + ); + return cookie(...arguments); + }; + window.$.removeCookie = function() { + deprecated( + "$.removeCookie is being removed from Discourse. Please import our cookie module and use that instead.", + depOpts + ); + return removeCookie(...arguments); + }; +} + +export default cookie; diff --git a/app/assets/javascripts/discourse/app/lib/theme-selector.js b/app/assets/javascripts/discourse/app/lib/theme-selector.js index 48f63c31fbf..a5b46c8ac1e 100644 --- a/app/assets/javascripts/discourse/app/lib/theme-selector.js +++ b/app/assets/javascripts/discourse/app/lib/theme-selector.js @@ -1,5 +1,6 @@ import I18n from "I18n"; import deprecated from "discourse-common/lib/deprecated"; +import cookie, { removeCookie } from "discourse/lib/cookie"; const keySelector = "meta[name=discourse_theme_ids]"; @@ -34,12 +35,12 @@ export function currentThemeId() { export function setLocalTheme(ids, themeSeq) { ids = ids.reject(id => !id); if (ids && ids.length > 0) { - $.cookie("theme_ids", `${ids.join(",")}|${themeSeq}`, { + cookie("theme_ids", `${ids.join(",")}|${themeSeq}`, { path: "/", expires: 9999 }); } else { - $.removeCookie("theme_ids", { path: "/", expires: 1 }); + removeCookie("theme_ids", { path: "/", expires: 1 }); } } diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index 8751b58a39d..940c5678194 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -31,6 +31,7 @@ import Site from "discourse/models/site"; import { NotificationLevels } from "discourse/lib/notification-levels"; import { escapeExpression } from "discourse/lib/utilities"; import { getOwner } from "discourse-common/lib/get-owner"; +import cookie, { removeCookie } from "discourse/lib/cookie"; export const SECOND_FACTOR_METHODS = { TOTP: 1, @@ -874,8 +875,8 @@ const User = RestModel.extend({ @discourseComputed("user_option.text_size_seq", "user_option.text_size") currentTextSize(serverSeq, serverSize) { - if ($.cookie("text_size")) { - const [cookieSize, cookieSeq] = $.cookie("text_size").split("|"); + if (cookie("text_size")) { + const [cookieSize, cookieSeq] = cookie("text_size").split("|"); if (cookieSeq >= serverSeq) { return cookieSize; } @@ -886,12 +887,12 @@ const User = RestModel.extend({ updateTextSizeCookie(newSize) { if (newSize) { const seq = this.get("user_option.text_size_seq"); - $.cookie("text_size", `${newSize}|${seq}`, { + cookie("text_size", `${newSize}|${seq}`, { path: "/", expires: 9999 }); } else { - $.removeCookie("text_size", { path: "/", expires: 1 }); + removeCookie("text_size", { path: "/", expires: 1 }); } }, diff --git a/app/assets/javascripts/discourse/app/routes/new-message.js b/app/assets/javascripts/discourse/app/routes/new-message.js index 5c8545b0371..245d92ea917 100644 --- a/app/assets/javascripts/discourse/app/routes/new-message.js +++ b/app/assets/javascripts/discourse/app/routes/new-message.js @@ -4,6 +4,7 @@ import DiscourseRoute from "discourse/routes/discourse"; import User from "discourse/models/user"; import Group from "discourse/models/group"; import bootbox from "bootbox"; +import cookie from "discourse/lib/cookie"; export default DiscourseRoute.extend({ beforeModel(transition) { @@ -58,7 +59,7 @@ export default DiscourseRoute.extend({ } }); } else { - $.cookie("destination_url", window.location.href); + cookie("destination_url", window.location.href); this.replaceWith("login"); } } diff --git a/app/assets/javascripts/discourse/app/routes/new-topic.js b/app/assets/javascripts/discourse/app/routes/new-topic.js index 550b4b5341c..a36be92484b 100644 --- a/app/assets/javascripts/discourse/app/routes/new-topic.js +++ b/app/assets/javascripts/discourse/app/routes/new-topic.js @@ -1,6 +1,7 @@ import { next } from "@ember/runloop"; import DiscourseRoute from "discourse/routes/discourse"; import Category from "discourse/models/category"; +import cookie from "discourse/lib/cookie"; export default DiscourseRoute.extend({ beforeModel(transition) { @@ -58,7 +59,7 @@ export default DiscourseRoute.extend({ } } else { // User is not logged in - $.cookie("destination_url", window.location.href); + cookie("destination_url", window.location.href); this.replaceWith("login"); } }, diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index 41b3c268bac..cf116f3f4c7 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -13,7 +13,6 @@ //= require favcount.js //= require jquery.ba-resize.js //= require jquery.color.js -//= require jquery.cookie.js //= require jquery.fileupload.js //= require jquery.iframe-transport.js //= require jquery.tagsinput.js diff --git a/test/javascripts/acceptance/preferences-test.js b/test/javascripts/acceptance/preferences-test.js index eb9aed022d4..e0e3ceaf0c8 100644 --- a/test/javascripts/acceptance/preferences-test.js +++ b/test/javascripts/acceptance/preferences-test.js @@ -2,6 +2,7 @@ import I18n from "I18n"; import { acceptance, updateCurrentUser } from "helpers/qunit-helpers"; import selectKit from "helpers/select-kit-helper"; import User from "discourse/models/user"; +import cookie, { removeCookie } from "discourse/lib/cookie"; function preferencesPretender(server, helper) { server.post("/u/second_factors.json", () => { @@ -122,7 +123,7 @@ QUnit.test("update some fields", async assert => { }); QUnit.test("font size change", async assert => { - $.removeCookie("text_size"); + removeCookie("text_size"); const savePreferences = async () => { assert.ok(!exists(".saved"), "it hasn't been saved yet"); @@ -142,12 +143,12 @@ QUnit.test("font size change", async assert => { await selectKit(".text-size .combobox").selectRowByValue("largest"); assert.ok(document.documentElement.classList.contains("text-size-largest")); - assert.equal($.cookie("text_size"), null, "cookie is not set"); + assert.equal(cookie("text_size"), null, "cookie is not set"); // Click save (by default this sets for all browsers, no cookie) await savePreferences(); - assert.equal($.cookie("text_size"), null, "cookie is not set"); + assert.equal(cookie("text_size"), null, "cookie is not set"); await selectKit(".text-size .combobox").expand(); await selectKit(".text-size .combobox").selectRowByValue("larger"); @@ -155,15 +156,15 @@ QUnit.test("font size change", async assert => { await savePreferences(); - assert.equal($.cookie("text_size"), "larger|1", "cookie is set"); + assert.equal(cookie("text_size"), "larger|1", "cookie is set"); await click(".text-size input[type=checkbox]"); await selectKit(".text-size .combobox").expand(); await selectKit(".text-size .combobox").selectRowByValue("largest"); await savePreferences(); - assert.equal($.cookie("text_size"), null, "cookie is removed"); + assert.equal(cookie("text_size"), null, "cookie is removed"); - $.removeCookie("text_size"); + removeCookie("text_size"); }); QUnit.test("username", async assert => { diff --git a/vendor/assets/javascripts/jquery.cookie.js b/vendor/assets/javascripts/jquery.cookie.js deleted file mode 100644 index c7f3a59b512..00000000000 --- a/vendor/assets/javascripts/jquery.cookie.js +++ /dev/null @@ -1,117 +0,0 @@ -/*! - * jQuery Cookie Plugin v1.4.1 - * https://github.com/carhartl/jquery-cookie - * - * Copyright 2013 Klaus Hartl - * Released under the MIT license - */ -(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD - define(['jquery'], factory); - } else if (typeof exports === 'object') { - // CommonJS - factory(require('jquery')); - } else { - // Browser globals - factory(jQuery); - } -}(function ($) { - - var pluses = /\+/g; - - function encode(s) { - return config.raw ? s : encodeURIComponent(s); - } - - function decode(s) { - return config.raw ? s : decodeURIComponent(s); - } - - function stringifyCookieValue(value) { - return encode(config.json ? JSON.stringify(value) : String(value)); - } - - function parseCookieValue(s) { - if (s.indexOf('"') === 0) { - // This is a quoted cookie as according to RFC2068, unescape... - s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); - } - - try { - // Replace server-side written pluses with spaces. - // If we can't decode the cookie, ignore it, it's unusable. - // If we can't parse the cookie, ignore it, it's unusable. - s = decodeURIComponent(s.replace(pluses, ' ')); - return config.json ? JSON.parse(s) : s; - } catch(e) {} - } - - function read(s, converter) { - var value = config.raw ? s : parseCookieValue(s); - return $.isFunction(converter) ? converter(value) : value; - } - - var config = $.cookie = function (key, value, options) { - - // Write - - if (value !== undefined && !$.isFunction(value)) { - options = $.extend({}, config.defaults, options); - - if (typeof options.expires === 'number') { - var days = options.expires, t = options.expires = new Date(); - t.setTime(+t + days * 864e+5); - } - - return (document.cookie = [ - encode(key), '=', stringifyCookieValue(value), - options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE - options.path ? '; path=' + options.path : '', - options.domain ? '; domain=' + options.domain : '', - options.secure ? '; secure' : '' - ].join('')); - } - - // Read - - var result = key ? undefined : {}; - - // To prevent the for loop in the first place assign an empty array - // in case there are no cookies at all. Also prevents odd result when - // calling $.cookie(). - var cookies = document.cookie ? document.cookie.split('; ') : []; - - for (var i = 0, l = cookies.length; i < l; i++) { - var parts = cookies[i].split('='); - var name = decode(parts.shift()); - var cookie = parts.join('='); - - if (key && key === name) { - // If second argument (value) is a function it's a converter... - result = read(cookie, value); - break; - } - - // Prevent storing a cookie that we couldn't decode. - if (!key && (cookie = read(cookie)) !== undefined) { - result[name] = cookie; - } - } - - return result; - }; - - config.defaults = {}; - - $.removeCookie = function (key, options) { - if ($.cookie(key) === undefined) { - return false; - } - - // Must not alter options, thus extending a fresh object... - $.cookie(key, '', $.extend({}, options, { expires: -1 })); - return !$.cookie(key); - }; - -}));