2021-03-11 05:43:45 +01:00
|
|
|
import $ from "jquery";
|
|
|
|
|
2021-02-28 01:13:42 +01:00
|
|
|
import * as activity from "./activity";
|
2021-03-16 23:38:59 +01:00
|
|
|
import * as blueslip from "./blueslip";
|
2021-02-28 00:51:57 +01:00
|
|
|
import * as compose from "./compose";
|
2021-02-28 21:32:22 +01:00
|
|
|
import * as compose_actions from "./compose_actions";
|
2021-02-28 00:50:52 +01:00
|
|
|
import * as compose_state from "./compose_state";
|
2021-03-25 23:20:18 +01:00
|
|
|
import {csrf_token} from "./csrf";
|
reload: Manually save draft and preserve id when triggering reload.
We received a complaint about the generation of multiple duplicate
drafts for a single message. It was discovered that the likely cause
of this was how we were handling clients that were frequently
suspending/unsuspending, we would initiate a reload when we discovered
this, and expect the `beforeunload` handler to save the draft. This
behaved correctly, however, we would also save the compose state and
fill it in via `preserve_state` in reload.js. The important detail
here is that `preserve_state` would not encode and preserve the
`draft_id` for the current message, partly because it had no way of
knowing the `draft_id` of the draft... since we have not saved it yet,
the `beforeunload` event happens after `preserve_state`. As such,
performing any action that would trigger a draft to be saved, eg
pressing Esc to close the compose box, would save a duplicate draft of
the same message.
To resolve the above bug, we (1) ensure that we call
`drafts.update_draft()` in `preserve_state`, this returns a draft_id
to us, which we (2) ensure that we encode as part of the url and (3)
set on the `#composebox-textarea` as a `draft-id` data attribute,
which we check the next time we try to save the draft, post reload.
Note that this causes us to save the draft twice, once from
preserve_state and then again from the `beforeunload` handler, but we
do not add two drafts since the second update_draft call just edits
the timestamp because it finds the `draft-id` data attribute on the
`#composebox-textarea` set by the first call.
2021-11-30 17:57:42 +01:00
|
|
|
import * as drafts from "./drafts";
|
2021-03-04 13:36:30 +01:00
|
|
|
import * as hash_util from "./hash_util";
|
2021-02-28 01:07:47 +01:00
|
|
|
import * as hashchange from "./hashchange";
|
2021-02-28 00:48:40 +01:00
|
|
|
import {localstorage} from "./localstorage";
|
2021-03-30 02:21:21 +02:00
|
|
|
import * as message_lists from "./message_lists";
|
2021-02-28 00:48:40 +01:00
|
|
|
import * as narrow_state from "./narrow_state";
|
2021-03-25 22:35:45 +01:00
|
|
|
import {page_params} from "./page_params";
|
2021-02-28 00:48:40 +01:00
|
|
|
import * as reload_state from "./reload_state";
|
2021-02-28 01:11:47 +01:00
|
|
|
import * as server_events from "./server_events";
|
2021-02-28 00:58:55 +01:00
|
|
|
import * as ui_report from "./ui_report";
|
2021-02-28 00:48:40 +01:00
|
|
|
import * as util from "./util";
|
2021-02-24 05:00:36 +01:00
|
|
|
|
2017-11-16 19:51:44 +01:00
|
|
|
// Read https://zulip.readthedocs.io/en/latest/subsystems/hashchange-system.html
|
2015-11-29 03:00:00 +01:00
|
|
|
function preserve_state(send_after_reload, save_pointer, save_narrow, save_compose) {
|
2017-05-05 01:21:58 +02:00
|
|
|
if (!localstorage.supported()) {
|
|
|
|
// If local storage is not supported by the browser, we can't
|
|
|
|
// save the browser's position across reloads (since there's
|
|
|
|
// no secure way to pass that state in a signed fashion to the
|
|
|
|
// next instance of the browser client).
|
|
|
|
//
|
2022-02-08 00:13:33 +01:00
|
|
|
// So we just return here and let the reload proceed without
|
2017-05-05 01:21:58 +02:00
|
|
|
// having preserved state. We keep the hash the same so we'll
|
|
|
|
// at least save their narrow state.
|
|
|
|
blueslip.log("Can't preserve state; no local storage.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let url = "#reload:send_after_reload=" + Number(send_after_reload);
|
2012-11-14 19:24:01 +01:00
|
|
|
url += "+csrf_token=" + encodeURIComponent(csrf_token);
|
2013-07-17 18:07:06 +02:00
|
|
|
|
2015-11-29 03:00:00 +01:00
|
|
|
if (save_compose) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const msg_type = compose_state.get_message_type();
|
2020-07-15 01:29:15 +02:00
|
|
|
if (msg_type === "stream") {
|
2015-11-29 03:00:00 +01:00
|
|
|
url += "+msg_type=stream";
|
2017-04-15 01:15:59 +02:00
|
|
|
url += "+stream=" + encodeURIComponent(compose_state.stream_name());
|
2018-12-16 18:02:16 +01:00
|
|
|
url += "+topic=" + encodeURIComponent(compose_state.topic());
|
2020-07-15 01:29:15 +02:00
|
|
|
} else if (msg_type === "private") {
|
2015-11-29 03:00:00 +01:00
|
|
|
url += "+msg_type=private";
|
2019-12-02 17:53:55 +01:00
|
|
|
url += "+recipient=" + encodeURIComponent(compose_state.private_message_recipient());
|
2015-11-29 03:00:00 +01:00
|
|
|
}
|
2013-07-17 18:07:06 +02:00
|
|
|
|
2017-04-24 20:35:26 +02:00
|
|
|
if (msg_type) {
|
2017-04-15 01:15:59 +02:00
|
|
|
url += "+msg=" + encodeURIComponent(compose_state.message_content());
|
reload: Manually save draft and preserve id when triggering reload.
We received a complaint about the generation of multiple duplicate
drafts for a single message. It was discovered that the likely cause
of this was how we were handling clients that were frequently
suspending/unsuspending, we would initiate a reload when we discovered
this, and expect the `beforeunload` handler to save the draft. This
behaved correctly, however, we would also save the compose state and
fill it in via `preserve_state` in reload.js. The important detail
here is that `preserve_state` would not encode and preserve the
`draft_id` for the current message, partly because it had no way of
knowing the `draft_id` of the draft... since we have not saved it yet,
the `beforeunload` event happens after `preserve_state`. As such,
performing any action that would trigger a draft to be saved, eg
pressing Esc to close the compose box, would save a duplicate draft of
the same message.
To resolve the above bug, we (1) ensure that we call
`drafts.update_draft()` in `preserve_state`, this returns a draft_id
to us, which we (2) ensure that we encode as part of the url and (3)
set on the `#composebox-textarea` as a `draft-id` data attribute,
which we check the next time we try to save the draft, post reload.
Note that this causes us to save the draft twice, once from
preserve_state and then again from the `beforeunload` handler, but we
do not add two drafts since the second update_draft call just edits
the timestamp because it finds the `draft-id` data attribute on the
`#composebox-textarea` set by the first call.
2021-11-30 17:57:42 +01:00
|
|
|
const draft_id = drafts.update_draft();
|
|
|
|
if (draft_id) {
|
|
|
|
url += "+draft_id=" + encodeURIComponent(draft_id);
|
|
|
|
}
|
2015-11-29 03:00:00 +01:00
|
|
|
}
|
2013-07-17 18:07:06 +02:00
|
|
|
}
|
|
|
|
|
2015-11-29 03:00:00 +01:00
|
|
|
if (save_pointer) {
|
2021-03-30 02:21:21 +02:00
|
|
|
const pointer = message_lists.home.selected_id();
|
2015-11-29 03:00:00 +01:00
|
|
|
if (pointer !== -1) {
|
|
|
|
url += "+pointer=" + pointer;
|
2014-02-12 20:03:05 +01:00
|
|
|
}
|
2015-11-29 03:00:00 +01:00
|
|
|
}
|
2014-02-12 20:03:05 +01:00
|
|
|
|
2015-11-29 03:00:00 +01:00
|
|
|
if (save_narrow) {
|
2022-01-25 11:36:19 +01:00
|
|
|
const $row = message_lists.home.selected_row();
|
2017-04-25 15:25:31 +02:00
|
|
|
if (!narrow_state.active()) {
|
2022-01-25 11:36:19 +01:00
|
|
|
if ($row.length > 0) {
|
|
|
|
url += "+offset=" + $row.offset().top;
|
2015-11-29 03:00:00 +01:00
|
|
|
}
|
|
|
|
} else {
|
2021-03-30 02:21:21 +02:00
|
|
|
url += "+offset=" + message_lists.home.pre_narrow_offset;
|
2015-11-29 03:00:00 +01:00
|
|
|
|
2022-09-07 09:06:25 +02:00
|
|
|
// narrow_state.active() is true, so this is the current
|
|
|
|
// narrowed message list.
|
|
|
|
const narrow_pointer = message_lists.current.selected_id();
|
2015-11-29 03:00:00 +01:00
|
|
|
if (narrow_pointer !== -1) {
|
|
|
|
url += "+narrow_pointer=" + narrow_pointer;
|
|
|
|
}
|
2022-09-07 09:06:25 +02:00
|
|
|
const $narrow_row = message_lists.current.selected_row();
|
2022-01-25 11:36:19 +01:00
|
|
|
if ($narrow_row.length > 0) {
|
|
|
|
url += "+narrow_offset=" + $narrow_row.offset().top;
|
2015-11-29 03:00:00 +01:00
|
|
|
}
|
2014-02-12 20:03:05 +01:00
|
|
|
}
|
2014-02-04 22:02:26 +01:00
|
|
|
}
|
2013-06-18 23:04:39 +02:00
|
|
|
|
2021-03-04 13:36:30 +01:00
|
|
|
url += hash_util.build_reload_url();
|
2012-10-29 21:02:46 +01:00
|
|
|
|
2022-09-22 22:01:57 +02:00
|
|
|
// Delete unused states that have been around for a while.
|
2019-11-02 00:06:25 +01:00
|
|
|
const ls = localstorage();
|
2022-09-22 22:01:57 +02:00
|
|
|
delete_stale_tokens(ls);
|
2017-04-26 12:37:28 +02:00
|
|
|
|
2017-03-23 06:21:13 +01:00
|
|
|
// To protect the browser against CSRF type attacks, the reload
|
|
|
|
// logic uses a random token (to distinct this browser from
|
|
|
|
// others) which is passed via the URL to the browser (post
|
|
|
|
// reloading). The token is a key into local storage, where we
|
|
|
|
// marshall and store the URL.
|
|
|
|
//
|
|
|
|
// TODO: Remove the now-unnecessary URL-encoding logic above and
|
|
|
|
// just pass the actual data structures through local storage.
|
2019-11-02 00:06:25 +01:00
|
|
|
const token = util.random_int(0, 1024 * 1024 * 1024 * 1024);
|
2022-09-22 22:01:57 +02:00
|
|
|
const metadata = {
|
|
|
|
url,
|
|
|
|
timestamp: Date.now(),
|
|
|
|
};
|
|
|
|
ls.set("reload:" + token, metadata);
|
2017-03-23 06:21:13 +01:00
|
|
|
window.location.replace("#reload:" + token);
|
2012-10-29 21:02:46 +01:00
|
|
|
}
|
|
|
|
|
2022-09-22 22:01:57 +02:00
|
|
|
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),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2012-10-29 21:02:46 +01:00
|
|
|
// Check if we're doing a compose-preserving reload. This must be
|
2014-01-30 20:29:00 +01:00
|
|
|
// done before the first call to get_events
|
2021-02-28 00:48:40 +01:00
|
|
|
export function initialize() {
|
2022-03-02 03:45:35 +01:00
|
|
|
// location.hash should be e.g. `#reload:12345123412312`
|
|
|
|
if (!location.hash.startsWith("#reload:")) {
|
2017-03-23 06:21:13 +01:00
|
|
|
return;
|
|
|
|
}
|
2022-03-02 03:45:35 +01:00
|
|
|
const hash_fragment = location.hash.slice("#".length);
|
2017-03-23 06:21:13 +01:00
|
|
|
|
|
|
|
// Using the token, recover the saved pre-reload data from local
|
|
|
|
// storage. Afterwards, we clear the reload entry from local
|
|
|
|
// storage to avoid a local storage space leak.
|
2019-11-02 00:06:25 +01:00
|
|
|
const ls = localstorage();
|
|
|
|
let fragment = ls.get(hash_fragment);
|
2017-03-23 06:21:13 +01:00
|
|
|
if (fragment === undefined) {
|
2017-05-05 01:16:02 +02:00
|
|
|
// Since this can happen sometimes with hand-reloading, it's
|
|
|
|
// not really worth throwing an exception if these don't
|
|
|
|
// exist, but be log it so that it's available for future
|
|
|
|
// debugging if an exception happens later.
|
|
|
|
blueslip.info("Invalid hash change reload token");
|
|
|
|
hashchange.changehash("");
|
2012-10-29 21:02:46 +01:00
|
|
|
return;
|
|
|
|
}
|
2017-03-23 06:21:13 +01:00
|
|
|
ls.remove(hash_fragment);
|
|
|
|
|
2022-09-22 22:01:57 +02:00
|
|
|
// 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);
|
2019-11-02 00:06:25 +01:00
|
|
|
const keyvals = fragment.split("+");
|
|
|
|
const vars = {};
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const str of keyvals) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const pair = str.split("=");
|
2012-10-29 21:02:46 +01:00
|
|
|
vars[pair[0]] = decodeURIComponent(pair[1]);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2012-10-29 21:02:46 +01:00
|
|
|
|
2015-11-10 19:01:34 +01:00
|
|
|
if (vars.msg !== undefined) {
|
2020-10-07 09:17:30 +02:00
|
|
|
const send_now = Number.parseInt(vars.send_after_reload, 10);
|
2013-07-17 18:07:06 +02:00
|
|
|
|
2018-03-07 14:16:34 +01:00
|
|
|
try {
|
2020-07-15 00:34:28 +02:00
|
|
|
compose_actions.start(vars.msg_type, {
|
|
|
|
stream: vars.stream || "",
|
2022-09-28 21:39:19 +02:00
|
|
|
topic: vars.topic || "",
|
2020-07-15 00:34:28 +02:00
|
|
|
private_message_recipient: vars.recipient || "",
|
|
|
|
content: vars.msg || "",
|
reload: Manually save draft and preserve id when triggering reload.
We received a complaint about the generation of multiple duplicate
drafts for a single message. It was discovered that the likely cause
of this was how we were handling clients that were frequently
suspending/unsuspending, we would initiate a reload when we discovered
this, and expect the `beforeunload` handler to save the draft. This
behaved correctly, however, we would also save the compose state and
fill it in via `preserve_state` in reload.js. The important detail
here is that `preserve_state` would not encode and preserve the
`draft_id` for the current message, partly because it had no way of
knowing the `draft_id` of the draft... since we have not saved it yet,
the `beforeunload` event happens after `preserve_state`. As such,
performing any action that would trigger a draft to be saved, eg
pressing Esc to close the compose box, would save a duplicate draft of
the same message.
To resolve the above bug, we (1) ensure that we call
`drafts.update_draft()` in `preserve_state`, this returns a draft_id
to us, which we (2) ensure that we encode as part of the url and (3)
set on the `#composebox-textarea` as a `draft-id` data attribute,
which we check the next time we try to save the draft, post reload.
Note that this causes us to save the draft twice, once from
preserve_state and then again from the `beforeunload` handler, but we
do not add two drafts since the second update_draft call just edits
the timestamp because it finds the `draft-id` data attribute on the
`#composebox-textarea` set by the first call.
2021-11-30 17:57:42 +01:00
|
|
|
draft_id: vars.draft_id || "",
|
2020-07-15 00:34:28 +02:00
|
|
|
});
|
2018-03-07 14:16:34 +01:00
|
|
|
if (send_now) {
|
|
|
|
compose.finish();
|
|
|
|
}
|
2020-10-07 10:20:41 +02:00
|
|
|
} catch (error) {
|
2018-03-07 14:16:34 +01:00
|
|
|
// We log an error if we can't open the compose box, but otherwise
|
|
|
|
// we continue, since this is not critical.
|
2020-10-07 10:20:41 +02:00
|
|
|
blueslip.warn(error.toString());
|
2013-07-17 18:07:06 +02:00
|
|
|
}
|
|
|
|
}
|
2012-10-29 21:02:46 +01:00
|
|
|
|
2020-10-07 09:17:30 +02:00
|
|
|
const pointer = Number.parseInt(vars.pointer, 10);
|
2013-06-18 23:04:39 +02:00
|
|
|
|
2013-07-17 18:07:06 +02:00
|
|
|
if (pointer) {
|
2020-02-19 22:24:00 +01:00
|
|
|
page_params.initial_pointer = pointer;
|
2012-10-29 21:02:46 +01:00
|
|
|
}
|
2020-10-07 09:17:30 +02:00
|
|
|
const offset = Number.parseInt(vars.offset, 10);
|
2014-02-04 22:02:26 +01:00
|
|
|
if (offset) {
|
|
|
|
page_params.initial_offset = offset;
|
|
|
|
}
|
2013-07-17 18:07:06 +02:00
|
|
|
|
2020-10-07 09:17:30 +02:00
|
|
|
const narrow_pointer = Number.parseInt(vars.narrow_pointer, 10);
|
2014-02-12 20:03:05 +01:00
|
|
|
if (narrow_pointer) {
|
|
|
|
page_params.initial_narrow_pointer = narrow_pointer;
|
|
|
|
}
|
2020-10-07 09:17:30 +02:00
|
|
|
const narrow_offset = Number.parseInt(vars.narrow_offset, 10);
|
2014-02-12 20:03:05 +01:00
|
|
|
if (narrow_offset) {
|
|
|
|
page_params.initial_narrow_offset = narrow_offset;
|
|
|
|
}
|
|
|
|
|
2018-08-04 08:22:44 +02:00
|
|
|
activity.set_new_user_input(false);
|
2013-07-17 18:07:06 +02:00
|
|
|
hashchange.changehash(vars.oldhash);
|
2021-02-28 00:48:40 +01:00
|
|
|
}
|
2012-10-29 21:02:46 +01:00
|
|
|
|
2021-04-13 05:18:25 +02:00
|
|
|
function do_reload_app(send_after_reload, save_pointer, save_narrow, save_compose, message_html) {
|
2018-08-04 15:40:25 +02:00
|
|
|
if (reload_state.is_in_progress()) {
|
2017-10-12 05:31:32 +02:00
|
|
|
blueslip.log("do_reload_app: Doing nothing since reload_in_progress");
|
|
|
|
return;
|
|
|
|
}
|
2014-02-19 18:41:01 +01:00
|
|
|
|
2014-02-10 21:26:25 +01:00
|
|
|
// TODO: we should completely disable the UI here
|
2015-11-29 03:00:00 +01:00
|
|
|
if (save_pointer || save_narrow || save_compose) {
|
2017-03-27 22:32:10 +02:00
|
|
|
try {
|
|
|
|
preserve_state(send_after_reload, save_pointer, save_narrow, save_compose);
|
2020-10-07 10:20:41 +02:00
|
|
|
} catch (error) {
|
|
|
|
blueslip.error("Failed to preserve state", undefined, error.stack);
|
2017-03-27 22:32:10 +02:00
|
|
|
}
|
2014-02-10 21:26:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: We need a better API for showing messages.
|
2021-04-13 05:18:25 +02:00
|
|
|
ui_report.message(message_html, $("#reloading-application"));
|
2020-07-15 01:29:15 +02:00
|
|
|
blueslip.log("Starting server requested page reload");
|
2018-08-04 15:40:25 +02:00
|
|
|
reload_state.set_state_to_in_progress();
|
2014-02-10 21:26:25 +01:00
|
|
|
|
2017-10-04 19:32:43 +02:00
|
|
|
// Sometimes the window.location.reload that we attempt has no
|
|
|
|
// immediate effect (likely by browsers trying to save power by
|
|
|
|
// skipping requested reloads), which can leave the Zulip app in a
|
|
|
|
// broken state and cause lots of confusing tracebacks. So, we
|
|
|
|
// set ourselves to try reloading a bit later, both periodically
|
|
|
|
// and when the user focuses the window.
|
2022-07-19 23:09:12 +02:00
|
|
|
$(window).one("focus", () => {
|
2017-10-04 19:32:43 +02:00
|
|
|
blueslip.log("Retrying on-focus page reload");
|
|
|
|
window.location.reload(true);
|
|
|
|
});
|
2022-11-15 12:28:24 +01:00
|
|
|
|
|
|
|
function retry_reload() {
|
2017-10-04 19:32:43 +02:00
|
|
|
blueslip.log("Retrying page reload due to 30s timer");
|
|
|
|
window.location.reload(true);
|
2022-11-15 12:28:24 +01:00
|
|
|
}
|
2022-11-15 15:41:56 +01:00
|
|
|
util.call_function_periodically(retry_reload, 30000);
|
2017-10-04 19:32:43 +02:00
|
|
|
|
2017-03-27 22:18:55 +02:00
|
|
|
try {
|
|
|
|
server_events.cleanup_event_queue();
|
2020-10-07 10:20:41 +02:00
|
|
|
} catch (error) {
|
docs: Add missing space to compound verbs “log in”, “set up”, etc.
Noun: backup, checkout, cleanup, login, logout, setup, shutdown, signup,
timeout.
Verb: back up, check out, clean up, log in, log out, set up, shut
down, sign up, time out.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 23:05:38 +02:00
|
|
|
blueslip.error("Failed to clean up before reloading", undefined, error.stack);
|
2014-02-10 21:26:25 +01:00
|
|
|
}
|
|
|
|
|
2012-10-29 21:02:46 +01:00
|
|
|
window.location.reload(true);
|
|
|
|
}
|
|
|
|
|
2021-03-24 21:44:43 +01:00
|
|
|
export function initiate({
|
|
|
|
immediate = false,
|
|
|
|
save_pointer = true,
|
|
|
|
save_narrow = true,
|
|
|
|
save_compose = true,
|
|
|
|
send_after_reload = false,
|
2021-04-13 05:18:25 +02:00
|
|
|
message_html = "Reloading ...",
|
2021-03-24 21:44:43 +01:00
|
|
|
}) {
|
|
|
|
if (immediate) {
|
2021-04-13 05:18:25 +02:00
|
|
|
do_reload_app(send_after_reload, save_pointer, save_narrow, save_compose, message_html);
|
2012-10-29 21:02:46 +01:00
|
|
|
}
|
|
|
|
|
2022-07-19 23:08:50 +02:00
|
|
|
if (reload_state.is_pending() || reload_state.is_in_progress()) {
|
2012-10-29 21:02:46 +01:00
|
|
|
return;
|
|
|
|
}
|
2018-08-04 15:40:25 +02:00
|
|
|
reload_state.set_state_to_pending();
|
2012-10-29 21:02:46 +01:00
|
|
|
|
2020-06-23 20:52:04 +02:00
|
|
|
// We're now planning to execute a reload of the browser, usually
|
2021-05-14 00:16:30 +02:00
|
|
|
// to get an updated version of the Zulip web app code. Because in
|
2020-06-23 20:52:04 +02:00
|
|
|
// most cases all browsers will be receiving this notice at the
|
|
|
|
// same or similar times, we need to randomize the time that we do
|
|
|
|
// this in order to avoid a thundering herd overloading the server.
|
|
|
|
//
|
|
|
|
// Additionally, we try to do this reload at a time the user will
|
|
|
|
// not notice. So completely idle clients will reload first;
|
|
|
|
// those will an open compose box will wait until the message has
|
|
|
|
// been sent (or until it's clear the user isn't likely to send it).
|
|
|
|
//
|
|
|
|
// And then we unconditionally reload sometime after 30 minutes
|
|
|
|
// even if there is continued activity, because we don't support
|
|
|
|
// old JavaScript versions against newer servers and eventually
|
|
|
|
// letting that situation continue will lead to users seeing bugs.
|
|
|
|
//
|
|
|
|
// It's a little odd that how this timeout logic works with
|
|
|
|
// compose box resets including the random variance, but that
|
|
|
|
// makes it simple to reason about: We know that reloads will be
|
|
|
|
// spread over at least 5 minutes in all cases.
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let idle_control;
|
2020-06-23 20:52:04 +02:00
|
|
|
const random_variance = util.random_int(0, 1000 * 60 * 5);
|
|
|
|
const unconditional_timeout = 1000 * 60 * 30 + random_variance;
|
2020-07-16 23:29:01 +02:00
|
|
|
const composing_idle_timeout = 1000 * 60 * 7 + random_variance;
|
|
|
|
const basic_idle_timeout = 1000 * 60 * 1 + random_variance;
|
2019-11-02 00:06:25 +01:00
|
|
|
let compose_started_handler;
|
2012-10-29 21:02:46 +01:00
|
|
|
|
2016-12-05 07:02:18 +01:00
|
|
|
function reload_from_idle() {
|
2021-04-13 05:18:25 +02:00
|
|
|
do_reload_app(false, save_pointer, save_narrow, save_compose, message_html);
|
2013-06-18 21:03:20 +02:00
|
|
|
}
|
2012-11-30 17:35:19 +01:00
|
|
|
|
2020-06-23 20:52:04 +02:00
|
|
|
// Make sure we always do a reload eventually after
|
|
|
|
// unconditional_timeout. Because we save cursor location and
|
|
|
|
// compose state when reloading, we expect this to not be
|
|
|
|
// particularly disruptive.
|
2013-07-22 23:27:32 +02:00
|
|
|
setTimeout(reload_from_idle, unconditional_timeout);
|
|
|
|
|
2019-10-26 00:11:05 +02:00
|
|
|
const compose_done_handler = function () {
|
2020-06-23 20:52:04 +02:00
|
|
|
// If the user sends their message or otherwise closes
|
|
|
|
// compose, we return them to the not-composing timeouts.
|
2012-10-29 21:02:46 +01:00
|
|
|
idle_control.cancel();
|
2020-07-15 00:34:28 +02:00
|
|
|
idle_control = $(document).idle({idle: basic_idle_timeout, onIdle: reload_from_idle});
|
|
|
|
$(document).off("compose_canceled.zulip compose_finished.zulip", compose_done_handler);
|
2020-07-15 01:29:15 +02:00
|
|
|
$(document).on("compose_started.zulip", compose_started_handler);
|
2012-10-29 21:02:46 +01:00
|
|
|
};
|
|
|
|
compose_started_handler = function () {
|
2020-06-23 20:52:04 +02:00
|
|
|
// If the user stops being idle and starts composing a
|
|
|
|
// message, switch to the compose-open timeouts.
|
2012-10-29 21:02:46 +01:00
|
|
|
idle_control.cancel();
|
2020-07-15 00:34:28 +02:00
|
|
|
idle_control = $(document).idle({idle: composing_idle_timeout, onIdle: reload_from_idle});
|
2020-07-15 01:29:15 +02:00
|
|
|
$(document).off("compose_started.zulip", compose_started_handler);
|
2020-07-15 00:34:28 +02:00
|
|
|
$(document).on("compose_canceled.zulip compose_finished.zulip", compose_done_handler);
|
2012-10-29 21:02:46 +01:00
|
|
|
};
|
|
|
|
|
2017-03-18 18:48:43 +01:00
|
|
|
if (compose_state.composing()) {
|
2020-07-15 00:34:28 +02:00
|
|
|
idle_control = $(document).idle({idle: composing_idle_timeout, onIdle: reload_from_idle});
|
|
|
|
$(document).on("compose_canceled.zulip compose_finished.zulip", compose_done_handler);
|
2012-10-29 21:02:46 +01:00
|
|
|
} else {
|
2020-07-15 00:34:28 +02:00
|
|
|
idle_control = $(document).idle({idle: basic_idle_timeout, onIdle: reload_from_idle});
|
2020-07-15 01:29:15 +02:00
|
|
|
$(document).on("compose_started.zulip", compose_started_handler);
|
2012-10-29 21:02:46 +01:00
|
|
|
}
|
2021-02-28 00:48:40 +01:00
|
|
|
}
|
2012-10-29 21:02:46 +01:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
window.addEventListener("beforeunload", () => {
|
2014-02-19 18:41:01 +01:00
|
|
|
// When navigating away from the page do not try to reload.
|
|
|
|
// The polling get_events call will fail after we delete the event queue.
|
|
|
|
// When that happens we reload the page to correct the problem. If this
|
|
|
|
// happens before the navigation is complete the user is kept captive at
|
|
|
|
// zulip.
|
2017-10-12 05:31:32 +02:00
|
|
|
blueslip.log("Setting reload_in_progress in beforeunload handler");
|
2018-08-04 15:40:25 +02:00
|
|
|
reload_state.set_state_to_in_progress();
|
2014-02-19 18:41:01 +01:00
|
|
|
});
|
2021-03-10 02:39:50 +01:00
|
|
|
|
|
|
|
reload_state.set_csrf_failed_handler(() => {
|
|
|
|
initiate({
|
|
|
|
immediate: true,
|
|
|
|
save_pointer: true,
|
|
|
|
save_narrow: true,
|
|
|
|
save_compose: true,
|
|
|
|
});
|
|
|
|
});
|