diff --git a/frontend_tests/node_tests/reload.js b/frontend_tests/node_tests/reload.js new file mode 100644 index 0000000000..68d5c6077c --- /dev/null +++ b/frontend_tests/node_tests/reload.js @@ -0,0 +1,37 @@ +"use strict"; + +const {strict: assert} = require("assert"); + +const {zrequire} = require("../zjsunit/namespace"); +// override file-level function call in reload.js +window.addEventListener = () => {}; +const reload = zrequire("reload"); +const {run_test} = require("../zjsunit/test"); + +run_test("old_metadata_string_is_stale", () => { + assert.ok(reload.is_stale_refresh_token("1663886962834", "1663883954033"), true); +}); + +run_test("recent_token_is_not_stale ", () => { + assert.ok( + !reload.is_stale_refresh_token( + { + url: "#reload:234234235234", + timestamp: Date.parse("21 Jan 2022 00:00:00 GMT"), + }, + Date.parse("23 Jan 2022 00:00:00 GMT"), + ), + ); +}); + +run_test("old_token_is_stale ", () => { + assert.ok( + reload.is_stale_refresh_token( + { + url: "#reload:234234235234", + timestamp: Date.parse("13 Jan 2022 00:00:00 GMT"), + }, + Date.parse("23 Jan 2022 00:00:00 GMT"), + ), + ); +}); diff --git a/static/js/localstorage.js b/static/js/localstorage.js index 68dcdf5930..475dc8d010 100644 --- a/static/js/localstorage.js +++ b/static/js/localstorage.js @@ -63,10 +63,14 @@ const ls = { localStorage.removeItem(key); }, - // Remove keys which match a regex. - removeDataRegex(version, regex) { + // Remove keys which (1) map to a value that satisfies a + // property tested by `condition_checker` and (2) which match + // the pattern given by `name`. + removeDataRegexWithCondition(version, regex, condition_checker) { const key_regex = new RegExp(this.formGetter(version, regex)); - const keys = Object.keys(localStorage).filter((key) => key_regex.test(key)); + const keys = Object.keys(localStorage).filter( + (key) => key_regex.test(key) && condition_checker(localStorage.getItem(key)), + ); for (const key of keys) { localStorage.removeItem(key); @@ -141,9 +145,11 @@ export const localstorage = function () { ls.removeData(_data.VERSION, name); }, - // Remove keys which match the pattern given by name. - removeRegex(name) { - ls.removeDataRegex(_data.VERSION, name); + // Remove keys which (1) map to a value that satisfies a + // property tested by `condition_checker` AND (2) which + // match the pattern given by `name`. + removeDataRegexWithCondition(name, condition_checker) { + ls.removeDataRegexWithCondition(_data.VERSION, name, condition_checker); }, migrate(name, v1, v2, callback) { diff --git a/static/js/reload.js b/static/js/reload.js index 378371235b..8274bc3388 100644 --- a/static/js/reload.js +++ b/static/js/reload.js @@ -86,9 +86,9 @@ function preserve_state(send_after_reload, save_pointer, save_narrow, save_compo url += hash_util.build_reload_url(); + // Delete unused states that have been around for a while. const ls = localstorage(); - // Delete all the previous preserved states. - ls.removeRegex("reload:\\d+"); + delete_stale_tokens(ls); // To protect the browser against CSRF type attacks, the reload // logic uses a random token (to distinct this browser from @@ -99,11 +99,40 @@ function preserve_state(send_after_reload, save_pointer, save_narrow, save_compo // TODO: Remove the now-unnecessary URL-encoding logic above and // just pass the actual data structures through local storage. const token = util.random_int(0, 1024 * 1024 * 1024 * 1024); - - ls.set("reload:" + token, url); + const metadata = { + url, + timestamp: Date.now(), + }; + ls.set("reload:" + token, metadata); window.location.replace("#reload:" + token); } +export function is_stale_refresh_token(token_metadata, now) { + // TODO/compatibility: the metadata was changed from a string + // to a map containing the string and a timestamp. For now we'll + // delete all tokens that only contain the url. Remove this + // early return once you can no longer directly upgrade from + // Zulip 5.x to the current version. + if (!token_metadata.timestamp) { + return true; + } + + // The time between reload token generation and use should usually be + // fewer than 30 seconds, but we keep tokens around for a week just in case + // (e.g. a tab could fail to load and be refreshed a while later). + const milliseconds_in_a_day = 1000 * 60 * 60 * 24; + const timedelta = now - token_metadata.timestamp; + const days_since_token_creation = timedelta / milliseconds_in_a_day; + return days_since_token_creation > 7; +} + +function delete_stale_tokens(ls) { + const now = Date.now(); + ls.removeDataRegexWithCondition("reload:\\d+", (metadata) => + is_stale_refresh_token(metadata, now), + ); +} + // Check if we're doing a compose-preserving reload. This must be // done before the first call to get_events export function initialize() { @@ -129,7 +158,12 @@ export function initialize() { } ls.remove(hash_fragment); - [, fragment] = /^#reload:(.*)/.exec(fragment); + // TODO/compatibility: `fragment` was changed from a string + // to a map containing the string and a timestamp. For now we'll + // delete all tokens that only contain the url. Remove the + // `|| fragment` once you can no longer directly upgrade + // from Zulip 5.x to the current version. + [, fragment] = /^#reload:(.*)/.exec(fragment.url || fragment); const keyvals = fragment.split("+"); const vars = {};