A device's snooze is clamped to the server's range, 1 minute to 30 days
The server clamped; the core took any i64, so 0 or less set a reminder in the past and a huge value overflowed Duration::minutes. Every caller passes 60 or 1440 today, so this was latent. Both sides now name the range, SNOOZE_MAX_MINUTES, and point at each other. Fixes #5386. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -711,8 +711,14 @@ pub fn complete_reminder(conn: &Connection, id: &str) -> rusqlite::Result<Note>
|
||||
load_note(conn, id)
|
||||
}
|
||||
|
||||
/// The longest snooze, in minutes: 30 days. The server's snooze route clamps to the
|
||||
/// same range (`notes.SNOOZE_MAX_MINUTES`), so a device and the web agree (#5386).
|
||||
pub const SNOOZE_MAX_MINUTES: i64 = 60 * 24 * 30;
|
||||
|
||||
pub fn snooze_reminder(conn: &Connection, id: &str, minutes: i64) -> rusqlite::Result<Note> {
|
||||
require_owner(conn, id)?;
|
||||
// 0 or less would set a reminder in the past; a huge value overflows `Duration`.
|
||||
let minutes = minutes.clamp(1, SNOOZE_MAX_MINUTES);
|
||||
let t = (Utc::now() + Duration::minutes(minutes)).to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
conn.execute(
|
||||
"UPDATE notes SET remind_at = ?1 WHERE id = ?2",
|
||||
@@ -1229,6 +1235,25 @@ mod tests {
|
||||
assert!(own.shared_by.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_snooze_lasts_between_a_minute_and_thirty_days() {
|
||||
let conn = db();
|
||||
let id = note(&conn, "call back").id;
|
||||
// Minutes from just before the call, rounded: the stored time is cut to the
|
||||
// millisecond, so it can land a hair under the whole minute.
|
||||
let lands = |minutes: i64| -> i64 {
|
||||
let before = Utc::now();
|
||||
let snoozed = snooze_reminder(&conn, &id, minutes).expect("snooze");
|
||||
let stamp = snoozed.remind_at.expect("set");
|
||||
let at = DateTime::parse_from_rfc3339(&stamp).expect("rfc3339");
|
||||
((at.with_timezone(&Utc) - before).num_seconds() + 30) / 60
|
||||
};
|
||||
assert_eq!(lands(0), 1, "never in the past");
|
||||
assert_eq!(lands(-5), 1);
|
||||
assert_eq!(lands(i64::MAX), SNOOZE_MAX_MINUTES, "no overflow");
|
||||
assert_eq!(lands(60), 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_view_share_changes_nothing_here() {
|
||||
let conn = db();
|
||||
|
||||
@@ -206,6 +206,11 @@ async def complete_reminder(note_id: str):
|
||||
return jsonify(await _serialize_note(db, note, g.user_id))
|
||||
|
||||
|
||||
# The longest snooze: 30 days. The core clamps to the same range
|
||||
# (`store::SNOOZE_MAX_MINUTES`), so a device and the web agree (#5386).
|
||||
SNOOZE_MAX_MINUTES = 60 * 24 * 30
|
||||
|
||||
|
||||
@bp.post("/<note_id>/reminder/snooze")
|
||||
@login_required
|
||||
async def snooze_reminder(note_id: str):
|
||||
@@ -215,7 +220,7 @@ async def snooze_reminder(note_id: str):
|
||||
minutes = int(data.get("minutes", 10))
|
||||
except (ValueError, TypeError):
|
||||
minutes = 10
|
||||
minutes = max(1, min(minutes, 60 * 24 * 30)) # 1 minute .. 30 days
|
||||
minutes = max(1, min(minutes, SNOOZE_MAX_MINUTES))
|
||||
async with session_scope() as db:
|
||||
note = await _get_owned(db, note_id)
|
||||
if note is None:
|
||||
|
||||
Reference in New Issue
Block a user