Skip to content

Commit 494b46e

Browse files
clokepphil-flex
authored andcommitted
Do not treat display names as globs for push rules. (matrix-org#7271)
1 parent c6cf87e commit 494b46e

File tree

4 files changed

+106
-30
lines changed

4 files changed

+106
-30
lines changed

changelog.d/7271.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Do not treat display names as globs in push rules.

synapse/push/push_rule_evaluator.py

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
import logging
1818
import re
19+
from typing import Pattern
1920

2021
from six import string_types
2122

23+
from synapse.events import EventBase
2224
from synapse.types import UserID
2325
from synapse.util.caches import CACHE_SIZE_FACTOR, register_cache
2426
from synapse.util.caches.lrucache import LruCache
@@ -56,18 +58,18 @@ def _test_ineq_condition(condition, number):
5658
rhs = m.group(2)
5759
if not rhs.isdigit():
5860
return False
59-
rhs = int(rhs)
61+
rhs_int = int(rhs)
6062

6163
if ineq == "" or ineq == "==":
62-
return number == rhs
64+
return number == rhs_int
6365
elif ineq == "<":
64-
return number < rhs
66+
return number < rhs_int
6567
elif ineq == ">":
66-
return number > rhs
68+
return number > rhs_int
6769
elif ineq == ">=":
68-
return number >= rhs
70+
return number >= rhs_int
6971
elif ineq == "<=":
70-
return number <= rhs
72+
return number <= rhs_int
7173
else:
7274
return False
7375

@@ -83,7 +85,13 @@ def tweaks_for_actions(actions):
8385

8486

8587
class PushRuleEvaluatorForEvent(object):
86-
def __init__(self, event, room_member_count, sender_power_level, power_levels):
88+
def __init__(
89+
self,
90+
event: EventBase,
91+
room_member_count: int,
92+
sender_power_level: int,
93+
power_levels: dict,
94+
):
8795
self._event = event
8896
self._room_member_count = room_member_count
8997
self._sender_power_level = sender_power_level
@@ -92,7 +100,7 @@ def __init__(self, event, room_member_count, sender_power_level, power_levels):
92100
# Maps strings of e.g. 'content.body' -> event["content"]["body"]
93101
self._value_cache = _flatten_dict(event)
94102

95-
def matches(self, condition, user_id, display_name):
103+
def matches(self, condition: dict, user_id: str, display_name: str) -> bool:
96104
if condition["kind"] == "event_match":
97105
return self._event_match(condition, user_id)
98106
elif condition["kind"] == "contains_display_name":
@@ -106,7 +114,7 @@ def matches(self, condition, user_id, display_name):
106114
else:
107115
return True
108116

109-
def _event_match(self, condition, user_id):
117+
def _event_match(self, condition: dict, user_id: str) -> bool:
110118
pattern = condition.get("pattern", None)
111119

112120
if not pattern:
@@ -134,59 +142,60 @@ def _event_match(self, condition, user_id):
134142

135143
return _glob_matches(pattern, haystack)
136144

137-
def _contains_display_name(self, display_name):
145+
def _contains_display_name(self, display_name: str) -> bool:
138146
if not display_name:
139147
return False
140148

141149
body = self._event.content.get("body", None)
142150
if not body:
143151
return False
144152

145-
return _glob_matches(display_name, body, word_boundary=True)
153+
# Similar to _glob_matches, but do not treat display_name as a glob.
154+
r = regex_cache.get((display_name, False, True), None)
155+
if not r:
156+
r = re.escape(display_name)
157+
r = _re_word_boundary(r)
158+
r = re.compile(r, flags=re.IGNORECASE)
159+
regex_cache[(display_name, False, True)] = r
160+
161+
return r.search(body)
146162

147-
def _get_value(self, dotted_key):
163+
def _get_value(self, dotted_key: str) -> str:
148164
return self._value_cache.get(dotted_key, None)
149165

150166

151-
# Caches (glob, word_boundary) -> regex for push. See _glob_matches
167+
# Caches (string, is_glob, word_boundary) -> regex for push. See _glob_matches
152168
regex_cache = LruCache(50000 * CACHE_SIZE_FACTOR)
153169
register_cache("cache", "regex_push_cache", regex_cache)
154170

155171

156-
def _glob_matches(glob, value, word_boundary=False):
172+
def _glob_matches(glob: str, value: str, word_boundary: bool = False) -> bool:
157173
"""Tests if value matches glob.
158174
159175
Args:
160-
glob (string)
161-
value (string): String to test against glob.
162-
word_boundary (bool): Whether to match against word boundaries or entire
176+
glob
177+
value: String to test against glob.
178+
word_boundary: Whether to match against word boundaries or entire
163179
string. Defaults to False.
164-
165-
Returns:
166-
bool
167180
"""
168181

169182
try:
170-
r = regex_cache.get((glob, word_boundary), None)
183+
r = regex_cache.get((glob, True, word_boundary), None)
171184
if not r:
172185
r = _glob_to_re(glob, word_boundary)
173-
regex_cache[(glob, word_boundary)] = r
186+
regex_cache[(glob, True, word_boundary)] = r
174187
return r.search(value)
175188
except re.error:
176189
logger.warning("Failed to parse glob to regex: %r", glob)
177190
return False
178191

179192

180-
def _glob_to_re(glob, word_boundary):
193+
def _glob_to_re(glob: str, word_boundary: bool) -> Pattern:
181194
"""Generates regex for a given glob.
182195
183196
Args:
184-
glob (string)
185-
word_boundary (bool): Whether to match against word boundaries or entire
186-
string. Defaults to False.
187-
188-
Returns:
189-
regex object
197+
glob
198+
word_boundary: Whether to match against word boundaries or entire string.
190199
"""
191200
if IS_GLOB.search(glob):
192201
r = re.escape(glob)
@@ -219,7 +228,7 @@ def _glob_to_re(glob, word_boundary):
219228
return re.compile(r, flags=re.IGNORECASE)
220229

221230

222-
def _re_word_boundary(r):
231+
def _re_word_boundary(r: str) -> str:
223232
"""
224233
Adds word boundary characters to the start and end of an
225234
expression to require that the match occur as a whole word,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2020 The Matrix.org Foundation C.I.C.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from synapse.api.room_versions import RoomVersions
17+
from synapse.events import FrozenEvent
18+
from synapse.push.push_rule_evaluator import PushRuleEvaluatorForEvent
19+
20+
from tests import unittest
21+
22+
23+
class PushRuleEvaluatorTestCase(unittest.TestCase):
24+
def setUp(self):
25+
event = FrozenEvent(
26+
{
27+
"event_id": "$event_id",
28+
"type": "m.room.history_visibility",
29+
"sender": "@user:test",
30+
"state_key": "",
31+
"room_id": "@room:test",
32+
"content": {"body": "foo bar baz"},
33+
},
34+
RoomVersions.V1,
35+
)
36+
room_member_count = 0
37+
sender_power_level = 0
38+
power_levels = {}
39+
self.evaluator = PushRuleEvaluatorForEvent(
40+
event, room_member_count, sender_power_level, power_levels
41+
)
42+
43+
def test_display_name(self):
44+
"""Check for a matching display name in the body of the event."""
45+
condition = {
46+
"kind": "contains_display_name",
47+
}
48+
49+
# Blank names are skipped.
50+
self.assertFalse(self.evaluator.matches(condition, "@user:test", ""))
51+
52+
# Check a display name that doesn't match.
53+
self.assertFalse(self.evaluator.matches(condition, "@user:test", "not found"))
54+
55+
# Check a display name which matches.
56+
self.assertTrue(self.evaluator.matches(condition, "@user:test", "foo"))
57+
58+
# A display name that matches, but not a full word does not result in a match.
59+
self.assertFalse(self.evaluator.matches(condition, "@user:test", "ba"))
60+
61+
# A display name should not be interpreted as a regular expression.
62+
self.assertFalse(self.evaluator.matches(condition, "@user:test", "ba[rz]"))
63+
64+
# A display name with spaces should work fine.
65+
self.assertTrue(self.evaluator.matches(condition, "@user:test", "foo bar"))

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ commands = mypy \
194194
synapse/metrics \
195195
synapse/module_api \
196196
synapse/push/pusherpool.py \
197+
synapse/push/push_rule_evaluator.py \
197198
synapse/replication \
198199
synapse/rest \
199200
synapse/spam_checker_api \

0 commit comments

Comments
 (0)