-
-
Notifications
You must be signed in to change notification settings - Fork 619
Implement MSC3873 to handle escaped dots in push rule keys #3134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1facb89
0600523
0ac33c8
9da576c
f680ad8
a23ed41
704ed20
0b9d896
08144fa
b724cf1
7e11af8
97fe8e9
322913e
7016e10
d7fcd36
c8a8f09
256cc92
bdde4ef
d04d2a6
e673f36
d85806e
3c445f6
ab9f212
fc66cec
cdb8760
9e7c3c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -123,6 +123,12 @@ export class PushProcessor { | |
*/ | ||
public constructor(private readonly client: MatrixClient) {} | ||
|
||
/** | ||
* Maps the original key from the push rules to a list of property names | ||
* after unescaping. | ||
*/ | ||
private readonly parsedKeys = new Map<string, string[]>(); | ||
clokep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Convert a list of actions into a object with the actions as keys and their values | ||
* @example | ||
|
@@ -162,7 +168,7 @@ export class PushProcessor { | |
if (!newRules) newRules = {} as IPushRules; | ||
if (!newRules.global) newRules.global = {} as PushRuleSet; | ||
if (!newRules.global.override) newRules.global.override = []; | ||
if (!newRules.global.override) newRules.global.underride = []; | ||
if (!newRules.global.underride) newRules.global.underride = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to have been a separate, long-standing bug? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, how exciting. What would the symptoms have been? Maybe we can find an issue to close.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was added in #2873, but that also added (on line 191) a |
||
|
||
// Merge the client-level defaults with the ones from the server | ||
const globalOverrides = newRules.global.override; | ||
|
@@ -202,6 +208,53 @@ export class PushProcessor { | |
return newRules; | ||
} | ||
|
||
/** | ||
* Pre-caches the parsed keys for push rules and cleans out any obsolete cache | ||
* entries. Should be called after push rules are updated. | ||
* @param newRules - The new push rules. | ||
*/ | ||
public updateCachedPushRuleKeys(newRules: IPushRules): void { | ||
// These lines are mostly to make the tests happy. We shouldn't run into these | ||
// properties missing in practice. | ||
if (!newRules) newRules = {} as IPushRules; | ||
if (!newRules.global) newRules.global = {} as PushRuleSet; | ||
if (!newRules.global.override) newRules.global.override = []; | ||
if (!newRules.global.room) newRules.global.room = []; | ||
if (!newRules.global.sender) newRules.global.sender = []; | ||
if (!newRules.global.underride) newRules.global.underride = []; | ||
Comment on lines
+219
to
+224
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was copied from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems fine like this, if it's more consistent. |
||
|
||
// Process the 'key' property on event_match conditions pre-cache the | ||
// values and clean-out any unused values. | ||
const toRemoveKeys = new Set(this.parsedKeys.keys()); | ||
for (const ruleset of [ | ||
newRules.global.override, | ||
newRules.global.room, | ||
newRules.global.sender, | ||
newRules.global.underride, | ||
]) { | ||
for (const rule of ruleset) { | ||
if (!rule.conditions) { | ||
continue; | ||
} | ||
|
||
for (const condition of rule.conditions) { | ||
if (condition.kind !== ConditionKind.EventMatch) { | ||
continue; | ||
} | ||
|
||
// Ensure we keep this key. | ||
toRemoveKeys.delete(condition.key); | ||
|
||
// Pre-process the key. | ||
this.parsedKeys.set(condition.key, PushProcessor.partsForDottedKey(condition.key)); | ||
} | ||
} | ||
} | ||
// Any keys that were previously cached, but are no longer needed should | ||
// be removed. | ||
toRemoveKeys.forEach((k) => this.parsedKeys.delete(k)); | ||
} | ||
|
||
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp | ||
|
||
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null { | ||
|
@@ -433,25 +486,99 @@ export class PushProcessor { | |
return PushProcessor.cachedGlobToRegex[glob]; | ||
} | ||
|
||
/** | ||
* Parse the key into the separate fields to search by splitting on | ||
* unescaped ".", and then removing any escape characters. | ||
* | ||
* @param str - The key of the push rule condition: a dotted field. | ||
* @returns The unescaped parts to fetch. | ||
* @internal | ||
*/ | ||
public static partsForDottedKey(str: string): string[] { | ||
const result = []; | ||
|
||
// The current field and whether the previous character was the escape | ||
// character (a backslash). | ||
let part = ""; | ||
let escaped = false; | ||
|
||
// Iterate over each character, and decide whether to append to the current | ||
// part (following the escape rules) or to start a new part (based on the | ||
// field separator). | ||
clokep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (const c of str) { | ||
// If the previous character was the escape character (a backslash) | ||
// then decide what to append to the current part. | ||
if (escaped) { | ||
if (c === "\\" || c === ".") { | ||
// An escaped backslash or dot just gets added. | ||
part += c; | ||
} else { | ||
// A character that shouldn't be escaped gets the backslash prepended. | ||
part += "\\" + c; | ||
} | ||
// This always resets being escaped. | ||
escaped = false; | ||
continue; | ||
} | ||
|
||
if (c == ".") { | ||
// The field separator creates a new part. | ||
result.push(part); | ||
part = ""; | ||
} else if (c == "\\") { | ||
// A backslash adds no characters, but starts an escape sequence. | ||
escaped = true; | ||
} else { | ||
// Otherwise, just add the current character. | ||
part += c; | ||
} | ||
} | ||
|
||
// Ensure the final part is included. If there's an open escape sequence | ||
// it should be included. | ||
if (escaped) { | ||
part += "\\"; | ||
} | ||
result.push(part); | ||
|
||
return result; | ||
} | ||
|
||
/** | ||
* For a dotted field and event, fetch the value at that position, if one | ||
* exists. | ||
* | ||
* @param key - The key of the push rule condition: a dotted field to fetch. | ||
* @param ev - The matrix event to fetch the field from. | ||
* @returns The value at the dotted path given by key. | ||
*/ | ||
private valueForDottedKey(key: string, ev: MatrixEvent): any { | ||
const parts = key.split("."); | ||
// The key should already have been parsed via updateCachedPushRuleKeys, | ||
// but if it hasn't (maybe via an old consumer of the SDK which hasn't | ||
// been updated?) then lazily calculate it here. | ||
let parts = this.parsedKeys.get(key); | ||
if (parts === undefined) { | ||
parts = PushProcessor.partsForDottedKey(key); | ||
this.parsedKeys.set(key, parts); | ||
} | ||
let val: any; | ||
|
||
// special-case the first component to deal with encrypted messages | ||
const firstPart = parts[0]; | ||
let currentIndex = 0; | ||
if (firstPart === "content") { | ||
val = ev.getContent(); | ||
parts.shift(); | ||
++currentIndex; | ||
} else if (firstPart === "type") { | ||
val = ev.getType(); | ||
parts.shift(); | ||
++currentIndex; | ||
} else { | ||
// use the raw event for any other fields | ||
val = ev.event; | ||
} | ||
|
||
while (parts.length > 0) { | ||
const thisPart = parts.shift()!; | ||
for (; currentIndex < parts.length; ++currentIndex) { | ||
const thisPart = parts[currentIndex]; | ||
if (isNullOrUndefined(val[thisPart])) { | ||
return null; | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.