mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ResponseOps] [Alerting] Handle invalid RRule params and prevent infinite looping (#205650)
## Summary Closes https://github.com/elastic/kibana/issues/205558 Updates the RRule library to correctly handle some scenarios with invalid parameters that would either cause it to return strange recurrence data or to infinitely loop. Specifically: - On `RRule` object creation, removes and ignores any `bymonth`, `bymonthday`, `byweekday`, or `byyearday` value that's out of bounds, e.g. less than 0 or greater than the number of possible months, days, weekdays, etc. - Successfully ignores cases of `BYMONTH=2, BYMONTHDAY=30` (February 30th), an input that's complicated to invalidate but still won't ever occur Allowing these values to go unhandled led to unpredictable behavior. The RRule library uses Moment.js to compare dates, but Moment.js months, days, and other values generally start at `0` while RRule values start at `1`. That led to several circumstances where we passed Moment.js a value of `-1`, which Moment.js interpreted as moving to the ***previous*** year, month, or other period of time. At worst, this could cause an infinite loop because the RRule library was constantly iterating through the wrong year, never reaching the date it was supposed to end on. In addition to making the RRule library more able to handle these cases, this PR also gives it a hard 100,000 iteration limit to prevent any possible infinite loops we've missed. Lastly, the Snooze Schedule APIs also come with additional validation to hopefully prevent out of bounds dates from ever being set. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Co-authored-by: Janki Salvi <jankigaurav.salvi@elastic.co> Co-authored-by: adcoelho <antonio.coelho@elastic.co>
This commit is contained in:
parent
ca42d93bd4
commit
b30210929b
9 changed files with 615 additions and 68 deletions
|
@ -7,6 +7,6 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { RRule, Frequency, Weekday } from './rrule';
|
||||
export type { Options } from './rrule';
|
||||
export declare type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
||||
export { RRule } from './rrule';
|
||||
export type { Options, WeekdayStr } from './types';
|
||||
export { Frequency, Weekday } from './types';
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
*/
|
||||
|
||||
import sinon from 'sinon';
|
||||
import { RRule, Frequency, Weekday } from './rrule';
|
||||
import { RRule } from './rrule';
|
||||
import { Frequency, Weekday } from './types';
|
||||
|
||||
const DATE_2019 = '2019-01-01T00:00:00.000Z';
|
||||
const DATE_2019_DECEMBER_19 = '2019-12-19T00:00:00.000Z';
|
||||
|
@ -730,6 +731,228 @@ describe('RRule', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
it('ignores invalid byweekday values', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.WEEKLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
byweekday: [Weekday.TH, 0, -2],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2019-12-19T00:00:00.000Z,
|
||||
2019-12-26T00:00:00.000Z,
|
||||
2020-01-02T00:00:00.000Z,
|
||||
2020-01-09T00:00:00.000Z,
|
||||
2020-01-16T00:00:00.000Z,
|
||||
2020-01-23T00:00:00.000Z,
|
||||
2020-01-30T00:00:00.000Z,
|
||||
2020-02-06T00:00:00.000Z,
|
||||
2020-02-13T00:00:00.000Z,
|
||||
2020-02-20T00:00:00.000Z,
|
||||
2020-02-27T00:00:00.000Z,
|
||||
2020-03-05T00:00:00.000Z,
|
||||
2020-03-12T00:00:00.000Z,
|
||||
2020-03-19T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
|
||||
const rule2 = new RRule({
|
||||
dtstart: new Date(DATE_2019),
|
||||
freq: Frequency.WEEKLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
byweekday: [Weekday.SA, Weekday.SU, Weekday.MO, 0],
|
||||
});
|
||||
|
||||
expect(rule2.all(9)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2019-01-05T00:00:00.000Z,
|
||||
2019-01-06T00:00:00.000Z,
|
||||
2019-01-07T00:00:00.000Z,
|
||||
2019-01-12T00:00:00.000Z,
|
||||
2019-01-13T00:00:00.000Z,
|
||||
2019-01-14T00:00:00.000Z,
|
||||
2019-01-19T00:00:00.000Z,
|
||||
2019-01-20T00:00:00.000Z,
|
||||
2019-01-21T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonth', () => {
|
||||
it('works with yearly frequency', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonth: [2, 5],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-02-19T00:00:00.000Z,
|
||||
2020-05-19T00:00:00.000Z,
|
||||
2021-02-19T00:00:00.000Z,
|
||||
2021-05-19T00:00:00.000Z,
|
||||
2022-02-19T00:00:00.000Z,
|
||||
2022-05-19T00:00:00.000Z,
|
||||
2023-02-19T00:00:00.000Z,
|
||||
2023-05-19T00:00:00.000Z,
|
||||
2024-02-19T00:00:00.000Z,
|
||||
2024-05-19T00:00:00.000Z,
|
||||
2025-02-19T00:00:00.000Z,
|
||||
2025-05-19T00:00:00.000Z,
|
||||
2026-02-19T00:00:00.000Z,
|
||||
2026-05-19T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('ignores invalid bymonth values', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonth: [0],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2019-12-19T00:00:00.000Z,
|
||||
2020-12-19T00:00:00.000Z,
|
||||
2021-12-19T00:00:00.000Z,
|
||||
2022-12-19T00:00:00.000Z,
|
||||
2023-12-19T00:00:00.000Z,
|
||||
2024-12-19T00:00:00.000Z,
|
||||
2025-12-19T00:00:00.000Z,
|
||||
2026-12-19T00:00:00.000Z,
|
||||
2027-12-19T00:00:00.000Z,
|
||||
2028-12-19T00:00:00.000Z,
|
||||
2029-12-19T00:00:00.000Z,
|
||||
2030-12-19T00:00:00.000Z,
|
||||
2031-12-19T00:00:00.000Z,
|
||||
2032-12-19T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonthday', () => {
|
||||
it('works with monthly frequency', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.MONTHLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonthday: [1, 15],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-01-01T00:00:00.000Z,
|
||||
2020-01-15T00:00:00.000Z,
|
||||
2020-02-01T00:00:00.000Z,
|
||||
2020-02-15T00:00:00.000Z,
|
||||
2020-03-01T00:00:00.000Z,
|
||||
2020-03-15T00:00:00.000Z,
|
||||
2020-04-01T00:00:00.000Z,
|
||||
2020-04-15T00:00:00.000Z,
|
||||
2020-05-01T00:00:00.000Z,
|
||||
2020-05-15T00:00:00.000Z,
|
||||
2020-06-01T00:00:00.000Z,
|
||||
2020-06-15T00:00:00.000Z,
|
||||
2020-07-01T00:00:00.000Z,
|
||||
2020-07-15T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('ignores invalid bymonthday values', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.MONTHLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonthday: [0, -1, 32],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2019-12-19T00:00:00.000Z,
|
||||
2020-01-19T00:00:00.000Z,
|
||||
2020-02-19T00:00:00.000Z,
|
||||
2020-03-19T00:00:00.000Z,
|
||||
2020-04-19T00:00:00.000Z,
|
||||
2020-05-19T00:00:00.000Z,
|
||||
2020-06-19T00:00:00.000Z,
|
||||
2020-07-19T00:00:00.000Z,
|
||||
2020-08-19T00:00:00.000Z,
|
||||
2020-09-19T00:00:00.000Z,
|
||||
2020-10-19T00:00:00.000Z,
|
||||
2020-11-19T00:00:00.000Z,
|
||||
2020-12-19T00:00:00.000Z,
|
||||
2021-01-19T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bymonth, bymonthday', () => {
|
||||
it('works with yearly frequency', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonth: [2, 5],
|
||||
bymonthday: [8],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-02-08T00:00:00.000Z,
|
||||
2020-05-08T00:00:00.000Z,
|
||||
2021-02-08T00:00:00.000Z,
|
||||
2021-05-08T00:00:00.000Z,
|
||||
2022-02-08T00:00:00.000Z,
|
||||
2022-05-08T00:00:00.000Z,
|
||||
2023-02-08T00:00:00.000Z,
|
||||
2023-05-08T00:00:00.000Z,
|
||||
2024-02-08T00:00:00.000Z,
|
||||
2024-05-08T00:00:00.000Z,
|
||||
2025-02-08T00:00:00.000Z,
|
||||
2025-05-08T00:00:00.000Z,
|
||||
2026-02-08T00:00:00.000Z,
|
||||
2026-05-08T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('ignores valid dates that do not exist e.g. February 30th', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2019_DECEMBER_19),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
bymonth: [2, 5],
|
||||
bymonthday: [30],
|
||||
});
|
||||
expect(rule.all(14)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-05-30T00:00:00.000Z,
|
||||
2021-05-30T00:00:00.000Z,
|
||||
2022-05-30T00:00:00.000Z,
|
||||
2023-05-30T00:00:00.000Z,
|
||||
2024-05-30T00:00:00.000Z,
|
||||
2025-05-30T00:00:00.000Z,
|
||||
2026-05-30T00:00:00.000Z,
|
||||
2027-05-30T00:00:00.000Z,
|
||||
2028-05-30T00:00:00.000Z,
|
||||
2029-05-30T00:00:00.000Z,
|
||||
2030-05-30T00:00:00.000Z,
|
||||
2031-05-30T00:00:00.000Z,
|
||||
2032-05-30T00:00:00.000Z,
|
||||
2033-05-30T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('byhour, byminute, bysecond', () => {
|
||||
|
@ -844,6 +1067,30 @@ describe('RRule', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
it('ignores invalid byyearday values', () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2020),
|
||||
freq: Frequency.YEARLY,
|
||||
byyearday: [0, -1],
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
});
|
||||
|
||||
expect(rule.all(10)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2020-01-01T00:00:00.000Z,
|
||||
2021-01-01T00:00:00.000Z,
|
||||
2022-01-01T00:00:00.000Z,
|
||||
2023-01-01T00:00:00.000Z,
|
||||
2024-01-01T00:00:00.000Z,
|
||||
2025-01-01T00:00:00.000Z,
|
||||
2026-01-01T00:00:00.000Z,
|
||||
2027-01-01T00:00:00.000Z,
|
||||
2028-01-01T00:00:00.000Z,
|
||||
2029-01-01T00:00:00.000Z,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
|
@ -872,5 +1119,33 @@ describe('RRule', () => {
|
|||
`"Cannot create RRule: until is an invalid date"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error on an interval of 0', () => {
|
||||
const testFn = () =>
|
||||
new RRule({
|
||||
dtstart: new Date(DATE_2020),
|
||||
freq: Frequency.HOURLY,
|
||||
interval: 0,
|
||||
tzid: 'UTC',
|
||||
});
|
||||
expect(testFn).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot create RRule: interval must be greater than 0"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when exceeding the iteration limit', () => {
|
||||
const testFn = () => {
|
||||
const rule = new RRule({
|
||||
dtstart: new Date(DATE_2020),
|
||||
freq: Frequency.YEARLY,
|
||||
byyearday: [1],
|
||||
interval: 1,
|
||||
tzid: 'UTC',
|
||||
});
|
||||
rule.all(100001);
|
||||
};
|
||||
|
||||
expect(testFn).toThrowErrorMatchingInlineSnapshot(`"RRule iteration limit exceeded"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,58 +7,16 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import moment, { Moment } from 'moment-timezone';
|
||||
import moment, { type Moment } from 'moment-timezone';
|
||||
|
||||
export enum Frequency {
|
||||
YEARLY = 0,
|
||||
MONTHLY = 1,
|
||||
WEEKLY = 2,
|
||||
DAILY = 3,
|
||||
HOURLY = 4,
|
||||
MINUTELY = 5,
|
||||
SECONDLY = 6,
|
||||
}
|
||||
|
||||
export enum Weekday {
|
||||
MO = 1,
|
||||
TU = 2,
|
||||
WE = 3,
|
||||
TH = 4,
|
||||
FR = 5,
|
||||
SA = 6,
|
||||
SU = 7,
|
||||
}
|
||||
|
||||
export type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
||||
interface IterOptions {
|
||||
refDT: Moment;
|
||||
wkst?: Weekday | number | null;
|
||||
byyearday?: number[] | null;
|
||||
bymonth?: number[] | null;
|
||||
bysetpos?: number[] | null;
|
||||
bymonthday?: number[] | null;
|
||||
byweekday?: Weekday[] | null;
|
||||
byhour?: number[] | null;
|
||||
byminute?: number[] | null;
|
||||
bysecond?: number[] | null;
|
||||
}
|
||||
|
||||
type Options = Omit<IterOptions, 'refDT'> & {
|
||||
dtstart: Date;
|
||||
freq?: Frequency;
|
||||
interval?: number;
|
||||
until?: Date | null;
|
||||
count?: number;
|
||||
tzid: string;
|
||||
};
|
||||
import { Frequency, Weekday, type WeekdayStr, type Options, type IterOptions } from './types';
|
||||
import { sanitizeOptions } from './sanitize';
|
||||
|
||||
type ConstructorOptions = Omit<Options, 'byweekday' | 'wkst'> & {
|
||||
byweekday?: Array<string | number> | null;
|
||||
wkst?: Weekday | WeekdayStr | number | null;
|
||||
};
|
||||
|
||||
export type { ConstructorOptions as Options };
|
||||
|
||||
const ISO_WEEKDAYS = [
|
||||
Weekday.MO,
|
||||
Weekday.TU,
|
||||
|
@ -74,19 +32,15 @@ type AllResult = Date[] & {
|
|||
};
|
||||
|
||||
const ALL_LIMIT = 10000;
|
||||
const TIMEOUT_LIMIT = 100000;
|
||||
|
||||
export class RRule {
|
||||
private options: Options;
|
||||
constructor(options: ConstructorOptions) {
|
||||
this.options = options as Options;
|
||||
if (isNaN(options.dtstart.getTime())) {
|
||||
throw new Error('Cannot create RRule: dtstart is an invalid date');
|
||||
}
|
||||
if (options.until && isNaN(options.until.getTime())) {
|
||||
throw new Error('Cannot create RRule: until is an invalid date');
|
||||
}
|
||||
this.options = sanitizeOptions(options as Options);
|
||||
if (typeof options.wkst === 'string') {
|
||||
this.options.wkst = Weekday[options.wkst];
|
||||
if (!this.options.wkst) delete this.options.wkst;
|
||||
}
|
||||
const weekdayParseResult = parseByWeekdayPos(options.byweekday);
|
||||
if (weekdayParseResult) {
|
||||
|
@ -112,12 +66,17 @@ export class RRule {
|
|||
.toDate();
|
||||
|
||||
const nextRecurrences: Moment[] = [];
|
||||
let iters = 0;
|
||||
|
||||
while (
|
||||
(!count && !until) ||
|
||||
(count && yieldedRecurrenceCount < count) ||
|
||||
(until && current.getTime() < new Date(until).getTime())
|
||||
(until && current.getTime() < until.getTime())
|
||||
) {
|
||||
iters++;
|
||||
if (iters > TIMEOUT_LIMIT) {
|
||||
throw new Error('RRule iteration limit exceeded');
|
||||
}
|
||||
const next = nextRecurrences.shift()?.toDate();
|
||||
if (next) {
|
||||
current = next;
|
||||
|
@ -313,6 +272,7 @@ const getYearOfRecurrences = function ({
|
|||
return getMonthOfRecurrences({
|
||||
refDT: currentMonth,
|
||||
wkst,
|
||||
bymonth,
|
||||
bymonthday,
|
||||
byweekday,
|
||||
byhour,
|
||||
|
@ -327,6 +287,7 @@ const getYearOfRecurrences = function ({
|
|||
|
||||
return derivedByyearday.flatMap((dayOfYear) => {
|
||||
const currentDate = moment(refDT).dayOfYear(dayOfYear);
|
||||
if (currentDate.year() !== refDT.year()) return [];
|
||||
if (!derivedByweekday.includes(currentDate.isoWeekday())) return [];
|
||||
return getDayOfRecurrences({ refDT: currentDate, byhour, byminute, bysecond });
|
||||
});
|
||||
|
@ -345,7 +306,7 @@ const getMonthOfRecurrences = function ({
|
|||
}: IterOptions) {
|
||||
const derivedByweekday = byweekday ?? ISO_WEEKDAYS;
|
||||
const currentMonth = refDT.month();
|
||||
if (bymonth && !bymonth.includes(currentMonth)) return [];
|
||||
if (bymonth && !bymonth.includes(currentMonth + 1)) return [];
|
||||
|
||||
let derivedBymonthday = bymonthday ?? [refDT.date()];
|
||||
if (bysetpos) {
|
||||
|
@ -392,6 +353,7 @@ const getMonthOfRecurrences = function ({
|
|||
|
||||
return derivedBymonthday.flatMap((date) => {
|
||||
const currentDate = moment(refDT).date(date);
|
||||
if (bymonth && !bymonth.includes(currentDate.month() + 1)) return [];
|
||||
if (!derivedByweekday.includes(currentDate.isoWeekday())) return [];
|
||||
return getDayOfRecurrences({ refDT: currentDate, byhour, byminute, bysecond });
|
||||
});
|
||||
|
|
131
src/platform/packages/shared/kbn-rrule/sanitize.test.ts
Normal file
131
src/platform/packages/shared/kbn-rrule/sanitize.test.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { sanitizeOptions } from './sanitize';
|
||||
import { Weekday, Frequency, type Options } from './types';
|
||||
|
||||
describe('sanitizeOptions', () => {
|
||||
const options: Options = {
|
||||
wkst: Weekday.MO,
|
||||
byyearday: [1, 2, 3],
|
||||
bymonth: [1],
|
||||
bysetpos: [1],
|
||||
bymonthday: [1],
|
||||
byweekday: [Weekday.MO],
|
||||
byhour: [1],
|
||||
byminute: [1],
|
||||
bysecond: [1],
|
||||
dtstart: new Date('September 3, 1998 03:24:00'),
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
until: new Date('February 25, 2022 03:24:00'),
|
||||
count: 3,
|
||||
tzid: 'foobar',
|
||||
};
|
||||
|
||||
it('happy path', () => {
|
||||
expect(sanitizeOptions(options)).toEqual(options);
|
||||
});
|
||||
|
||||
it('throws an error when dtstart is missing', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => sanitizeOptions({ ...options, dtstart: null })).toThrowError(
|
||||
'Cannot create RRule: dtstart is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when tzid is missing', () => {
|
||||
expect(() => sanitizeOptions({ ...options, tzid: '' })).toThrowError(
|
||||
'Cannot create RRule: tzid is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when until field is invalid', () => {
|
||||
expect(() =>
|
||||
sanitizeOptions({
|
||||
...options,
|
||||
// @ts-expect-error
|
||||
until: {
|
||||
getTime: () => NaN,
|
||||
},
|
||||
})
|
||||
).toThrowError('Cannot create RRule: until is an invalid date');
|
||||
});
|
||||
|
||||
it('throws an error when interval is less than 0', () => {
|
||||
expect(() => sanitizeOptions({ ...options, interval: -3 })).toThrowError(
|
||||
'Cannot create RRule: interval must be greater than 0'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when interval is not a number', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => sanitizeOptions({ ...options, interval: 'foobar' })).toThrowError(
|
||||
'Cannot create RRule: interval must be a number'
|
||||
);
|
||||
});
|
||||
|
||||
it('filters out invalid bymonth values', () => {
|
||||
expect(sanitizeOptions({ ...options, bymonth: [0, 6, 13] })).toEqual({
|
||||
...options,
|
||||
bymonth: [6],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes bymonth when it is empty', () => {
|
||||
expect(sanitizeOptions({ ...options, bymonth: [0] })).toEqual({
|
||||
...options,
|
||||
bymonth: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out invalid bymonthday values', () => {
|
||||
expect(sanitizeOptions({ ...options, bymonthday: [0, 15, 32] })).toEqual({
|
||||
...options,
|
||||
bymonthday: [15],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes bymonthday when it is empty', () => {
|
||||
expect(sanitizeOptions({ ...options, bymonthday: [0] })).toEqual({
|
||||
...options,
|
||||
bymonthday: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out invalid byweekday values', () => {
|
||||
// @ts-expect-error
|
||||
expect(sanitizeOptions({ ...options, byweekday: [0, 4, 8] })).toEqual({
|
||||
...options,
|
||||
byweekday: [4],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes byweekday when it is empty', () => {
|
||||
// @ts-expect-error
|
||||
expect(sanitizeOptions({ ...options, byweekday: [0] })).toEqual({
|
||||
...options,
|
||||
byweekday: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out invalid byyearday values', () => {
|
||||
expect(sanitizeOptions({ ...options, byyearday: [0, 150, 367] })).toEqual({
|
||||
...options,
|
||||
byyearday: [150],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes byyearday when it is empty', () => {
|
||||
expect(sanitizeOptions({ ...options, byyearday: [0] })).toEqual({
|
||||
...options,
|
||||
byyearday: undefined,
|
||||
});
|
||||
});
|
||||
});
|
82
src/platform/packages/shared/kbn-rrule/sanitize.ts
Normal file
82
src/platform/packages/shared/kbn-rrule/sanitize.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { Options } from './types';
|
||||
|
||||
export function sanitizeOptions(opts: Options) {
|
||||
const options = { ...opts };
|
||||
|
||||
// Guard against invalid options that can't be omitted
|
||||
if (!options.dtstart) {
|
||||
throw new Error('Cannot create RRule: dtstart is required');
|
||||
}
|
||||
|
||||
if (!options.tzid) {
|
||||
throw new Error('Cannot create RRule: tzid is required');
|
||||
}
|
||||
|
||||
if (isNaN(options.dtstart.getTime())) {
|
||||
throw new Error('Cannot create RRule: dtstart is an invalid date');
|
||||
}
|
||||
|
||||
if (options.until && isNaN(options.until.getTime())) {
|
||||
throw new Error('Cannot create RRule: until is an invalid date');
|
||||
}
|
||||
|
||||
if (options.interval != null) {
|
||||
if (typeof options.interval !== 'number') {
|
||||
throw new Error('Cannot create RRule: interval must be a number');
|
||||
}
|
||||
|
||||
if (options.interval < 1) {
|
||||
throw new Error('Cannot create RRule: interval must be greater than 0');
|
||||
}
|
||||
}
|
||||
|
||||
// Omit invalid options
|
||||
if (options.bymonth) {
|
||||
// Only months between 1 and 12 are valid
|
||||
options.bymonth = options.bymonth.filter(
|
||||
(month) => typeof month === 'number' && month >= 1 && month <= 12
|
||||
);
|
||||
if (!options.bymonth.length) {
|
||||
delete options.bymonth;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.bymonthday) {
|
||||
// Only days between 1 and 31 are valid
|
||||
options.bymonthday = options.bymonthday.filter(
|
||||
(day) => typeof day === 'number' && day >= 1 && day <= 31
|
||||
);
|
||||
if (!options.bymonthday.length) {
|
||||
delete options.bymonthday;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.byweekday) {
|
||||
// Only weekdays between 1 and 7 are valid
|
||||
options.byweekday = options.byweekday.filter(
|
||||
(weekday) => typeof weekday === 'number' && weekday >= 1 && weekday <= 7
|
||||
);
|
||||
if (!options.byweekday.length) {
|
||||
delete options.byweekday;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.byyearday) {
|
||||
// Only days between 1 and 366 are valid
|
||||
options.byyearday = options.byyearday.filter((day) => day >= 1 && day <= 366);
|
||||
if (!options.byyearday.length) {
|
||||
delete options.byyearday;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
54
src/platform/packages/shared/kbn-rrule/types.ts
Normal file
54
src/platform/packages/shared/kbn-rrule/types.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { Moment } from 'moment';
|
||||
|
||||
export enum Frequency {
|
||||
YEARLY = 0,
|
||||
MONTHLY = 1,
|
||||
WEEKLY = 2,
|
||||
DAILY = 3,
|
||||
HOURLY = 4,
|
||||
MINUTELY = 5,
|
||||
SECONDLY = 6,
|
||||
}
|
||||
|
||||
export enum Weekday {
|
||||
MO = 1,
|
||||
TU = 2,
|
||||
WE = 3,
|
||||
TH = 4,
|
||||
FR = 5,
|
||||
SA = 6,
|
||||
SU = 7,
|
||||
}
|
||||
|
||||
export type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
||||
|
||||
export interface IterOptions {
|
||||
refDT: Moment;
|
||||
wkst?: Weekday | number | null;
|
||||
byyearday?: number[] | null;
|
||||
bymonth?: number[] | null;
|
||||
bysetpos?: number[] | null;
|
||||
bymonthday?: number[] | null;
|
||||
byweekday?: Weekday[] | null;
|
||||
byhour?: number[] | null;
|
||||
byminute?: number[] | null;
|
||||
bysecond?: number[] | null;
|
||||
}
|
||||
|
||||
export type Options = Omit<IterOptions, 'refDT'> & {
|
||||
dtstart: Date;
|
||||
freq?: Frequency;
|
||||
interval?: number;
|
||||
until?: Date | null;
|
||||
count?: number;
|
||||
tzid: string;
|
||||
};
|
|
@ -40,17 +40,28 @@ export const rRuleRequestSchema = schema.object({
|
|||
})
|
||||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
validate: createValidateRecurrenceByV1('byweekday'),
|
||||
})
|
||||
schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.literal('MO'),
|
||||
schema.literal('TU'),
|
||||
schema.literal('WE'),
|
||||
schema.literal('TH'),
|
||||
schema.literal('FR'),
|
||||
schema.literal('SA'),
|
||||
schema.literal('SU'),
|
||||
]),
|
||||
{
|
||||
validate: createValidateRecurrenceByV1('byweekday'),
|
||||
}
|
||||
)
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
schema.arrayOf(schema.number({ min: 1, max: 31 }), {
|
||||
validate: createValidateRecurrenceByV1('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
schema.arrayOf(schema.number({ min: 1, max: 12 }), {
|
||||
validate: createValidateRecurrenceByV1('bymonth'),
|
||||
})
|
||||
),
|
||||
|
|
|
@ -35,17 +35,28 @@ export const rRuleRequestSchema = schema.object({
|
|||
})
|
||||
),
|
||||
byweekday: schema.maybe(
|
||||
schema.arrayOf(schema.string(), {
|
||||
validate: createValidateRecurrenceBy('byweekday'),
|
||||
})
|
||||
schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.literal('MO'),
|
||||
schema.literal('TU'),
|
||||
schema.literal('WE'),
|
||||
schema.literal('TH'),
|
||||
schema.literal('FR'),
|
||||
schema.literal('SA'),
|
||||
schema.literal('SU'),
|
||||
]),
|
||||
{
|
||||
validate: createValidateRecurrenceBy('byweekday'),
|
||||
}
|
||||
)
|
||||
),
|
||||
bymonthday: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
schema.arrayOf(schema.number({ min: 1, max: 31 }), {
|
||||
validate: createValidateRecurrenceBy('bymonthday'),
|
||||
})
|
||||
),
|
||||
bymonth: schema.maybe(
|
||||
schema.arrayOf(schema.number(), {
|
||||
schema.arrayOf(schema.number({ min: 1, max: 12 }), {
|
||||
validate: createValidateRecurrenceBy('bymonth'),
|
||||
})
|
||||
),
|
||||
|
|
|
@ -230,4 +230,25 @@ describe('isSnoozeActive', () => {
|
|||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
|
||||
test('snooze still works with invalid bymonth value', () => {
|
||||
// Set the current time as:
|
||||
// - Feb 27 2023 08:15:00 GMT+0000 - Monday
|
||||
fakeTimer = sinon.useFakeTimers(new Date('2023-02-09T08:15:00.000Z'));
|
||||
|
||||
const snoozeA = {
|
||||
duration: moment('2023-01', 'YYYY-MM').daysInMonth() * 24 * 60 * 60 * 1000, // 1 month
|
||||
rRule: {
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
bymonthday: [1],
|
||||
bymonth: [0],
|
||||
tzid: 'Europe/Madrid',
|
||||
dtstart: '2023-01-01T00:00:00.000Z',
|
||||
} as RRuleRecord,
|
||||
id: '9141dc1f-ed85-4656-91e4-119173105432',
|
||||
};
|
||||
expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`);
|
||||
fakeTimer.restore();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue