mirror of
https://github.com/zulip/zulip.git
synced 2025-11-07 07:23:22 +00:00
reload: Preserve unused reload tokens for a week.
Previously, we deleted all reload tokens on each reload, which created a race condition if there were multiple tabs open. Now, we continue to delete tokens after using them, but if a token is not used it is preserved for a week before being deleted. Fixes #22832.
This commit is contained in:
37
frontend_tests/node_tests/reload.js
Normal file
37
frontend_tests/node_tests/reload.js
Normal file
@@ -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"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -63,10 +63,14 @@ const ls = {
|
|||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Remove keys which match a regex.
|
// Remove keys which (1) map to a value that satisfies a
|
||||||
removeDataRegex(version, regex) {
|
// 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 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) {
|
for (const key of keys) {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
@@ -141,9 +145,11 @@ export const localstorage = function () {
|
|||||||
ls.removeData(_data.VERSION, name);
|
ls.removeData(_data.VERSION, name);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Remove keys which match the pattern given by name.
|
// Remove keys which (1) map to a value that satisfies a
|
||||||
removeRegex(name) {
|
// property tested by `condition_checker` AND (2) which
|
||||||
ls.removeDataRegex(_data.VERSION, name);
|
// match the pattern given by `name`.
|
||||||
|
removeDataRegexWithCondition(name, condition_checker) {
|
||||||
|
ls.removeDataRegexWithCondition(_data.VERSION, name, condition_checker);
|
||||||
},
|
},
|
||||||
|
|
||||||
migrate(name, v1, v2, callback) {
|
migrate(name, v1, v2, callback) {
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ function preserve_state(send_after_reload, save_pointer, save_narrow, save_compo
|
|||||||
|
|
||||||
url += hash_util.build_reload_url();
|
url += hash_util.build_reload_url();
|
||||||
|
|
||||||
|
// Delete unused states that have been around for a while.
|
||||||
const ls = localstorage();
|
const ls = localstorage();
|
||||||
// Delete all the previous preserved states.
|
delete_stale_tokens(ls);
|
||||||
ls.removeRegex("reload:\\d+");
|
|
||||||
|
|
||||||
// To protect the browser against CSRF type attacks, the reload
|
// To protect the browser against CSRF type attacks, the reload
|
||||||
// logic uses a random token (to distinct this browser from
|
// 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
|
// TODO: Remove the now-unnecessary URL-encoding logic above and
|
||||||
// just pass the actual data structures through local storage.
|
// just pass the actual data structures through local storage.
|
||||||
const token = util.random_int(0, 1024 * 1024 * 1024 * 1024);
|
const token = util.random_int(0, 1024 * 1024 * 1024 * 1024);
|
||||||
|
const metadata = {
|
||||||
ls.set("reload:" + token, url);
|
url,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
ls.set("reload:" + token, metadata);
|
||||||
window.location.replace("#reload:" + token);
|
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
|
// Check if we're doing a compose-preserving reload. This must be
|
||||||
// done before the first call to get_events
|
// done before the first call to get_events
|
||||||
export function initialize() {
|
export function initialize() {
|
||||||
@@ -129,7 +158,12 @@ export function initialize() {
|
|||||||
}
|
}
|
||||||
ls.remove(hash_fragment);
|
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 keyvals = fragment.split("+");
|
||||||
const vars = {};
|
const vars = {};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user