Compare commits

..

13 Commits
4.6 ... 4.7

Author SHA1 Message Date
Alex Vandiver
4e724c1ec6 Release Zulip Server 4.7 2021-10-04 17:31:57 +00:00
Alex Vandiver
e2d303c1bb CVE-2021-41115: Use re2 for user-supplied linkifier patterns.
Zulip attempts to validate that the regular expressions that admins
enter for linkifiers are well-formatted, and only contain a specific
subset of regex grammar.  The process of checking these
properties (via a regex!) can cause denial-of-service via
backtracking.

Furthermore, this validation itself does not prevent the creation of
linkifiers which themselves cause denial-of-service when they are
executed.  As the validator accepts literally anything inside of a
`(?P<word>...)` block, any quadratic backtracking expression can be
hidden therein.

Switch user-provided linkifier patterns to be matched in the Markdown
processor by the `re2` library, which is guaranteed constant-time.
This somewhat limits the possible features of the regular
expression (notably, look-head and -behind, and back-references);
however, these features had never been advertised as working in the
context of linkifiers.

A migration removes any existing linkifiers which would not function
under re2, after printing them for posterity during the upgrade; they
are unlikely to be common, and are impossible to fix automatically.

The denial-of-service in the linkifier validator was discovered by
@erik-krogh and @yoff, as GHSL-2021-118.
2021-10-04 17:24:37 +00:00
Alex Vandiver
d3091a6096 requirements: Add google-re2, a drop-in replacement for re using re2.
re2[1] compiles (strictly) regular expressions to deterministic finite
automata, which guarantees linear-time behavior; `google-re2` is a
drop-in replacement for the `re` module which uses re2 under the hood.

[1]: https://github.com/google/re2/
2021-10-02 01:01:14 +00:00
Alex Vandiver
313bcfd02a github: Ignore CodeQL analysis in private repos.
CodeQL only runs in public repos; private forks will otherwise error
their CI runs.

(cherry picked from commit acbe7ae7a8)
2021-10-01 18:00:52 -07:00
Gaurav Pandey
09bfd485e9 ci: Remove unnecessary steps from production upgrade script.
This removes some steps which are no longer necessary to be run
in the production upgrade script. The steps were used due to
errors related to supervisor failing to restart which was resolved
in the commit 08c39a7388.

(cherry picked from commit dc2066c7e8)
2021-10-01 18:00:52 -07:00
Anders Kaseorg
576ae9cc9f ci: Use apt-get -y in production-upgrade test.
We currently configure ‘APT::Get::Assume-Yes’ in our custom Docker
image, but this is the only place we rely on it (outside of the
Dockerfile itself), and it’s better not to.

Also ‘apt-get remove && apt-get purge’ is the same as just ‘apt-get
purge’.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit db476bdc51)
2021-10-01 18:00:52 -07:00
Alex Vandiver
300447ddd9 ci: Use an init process to reap defunct processes.
When Github Actions run in Docker, the default pid 1 entrypoint is
`tail -f /dev/null`.  PID 1 is responsible for propagating signals to
its children, and calling `waitpid()` on defunct processes; `tail`
does not do these things.  This results in zombie processes piling up
inside the container, which is not an issue in most contexts.

However, it affects `start-stop-daemon`, which hangs when stopping
daemon processes, as they are never reaped.  This appears in CI as
`/etc/init.d/supervisor restart` never being able to succeed.

Run the docker container with `--init`, which spawns a
`/sbin/docker-init` PID 1 to handle the job of an init process.

(cherry picked from commit 2daad58afa)
2021-10-01 18:00:52 -07:00
Gaurav Pandey
f8149b0d5a ci: Add prod upgrade step to prod suite.
This adds a check in the current production suite of
CI that upgrades a previous release of zulip server
with a newer one.

Fixes #18346.

(cherry picked from commit e648ad3477)
2021-10-01 18:00:52 -07:00
Priyank Patel
b579dad7d9 github-actions: Upgrade styfle/cancel-workflow-action.
(cherry picked from commit 05510a8c04)
2021-10-01 18:00:52 -07:00
Priyank Patel
fdfabb800d github-actions: Ensure cancel previous run job never fails.
(cherry picked from commit 607110ca33)
2021-10-01 18:00:52 -07:00
Tim Abbott
2c4156678c docs: Inline some upgrade instructions.
It feels like the "Same as" content was unnecessarily requiring the
user to bounce around in these cases.

(I've left the "Same as" text for the Ubuntu ones, where it's two
steps in a row to follow).
2021-10-01 11:10:13 -07:00
Gaurav Pandey
0a87276a27 docs: Document upgrade steps from buster to bullseye.
Fixes #17863.
2021-10-01 11:10:12 -07:00
Tim Abbott
19aed43817 version: Update version after 4.6 release. 2021-09-23 16:14:53 -07:00
17 changed files with 303 additions and 53 deletions

View File

@@ -22,6 +22,7 @@ jobs:
# so this is required.
- name: Get workflow IDs.
id: workflow_ids
continue-on-error: true # Don't fail this job on failure
env:
# This is in <owner>/<repo> format e.g. zulip/zulip
REPOSITORY: ${{ github.repository }}
@@ -35,7 +36,8 @@ jobs:
ids=$(node -e "$script")
echo "::set-output name=ids::$ids"
- uses: styfle/cancel-workflow-action@0.4.1
- uses: styfle/cancel-workflow-action@0.9.0
continue-on-error: true # Don't fail this job on failure
with:
workflow_id: ${{ steps.workflow_ids.outputs.ids }}
access_token: ${{ github.token }}

View File

@@ -4,6 +4,7 @@ on: [push, pull_request]
jobs:
CodeQL:
if: ${{!github.event.repository.private}}
runs-on: ubuntu-latest
steps:

View File

@@ -30,6 +30,8 @@ defaults:
jobs:
production_build:
# This job builds a release tarball from the current commit, which
# will be used for all of the following install/upgrade tests.
name: Bionic production build
runs-on: ubuntu-latest
@@ -106,6 +108,9 @@ jobs:
run: tools/ci/send-failure-message
production_install:
# This job installs the server release tarball built above on a
# range of platforms, and does some basic health checks on the
# resulting installer Zulip server.
strategy:
fail-fast: false
matrix:
@@ -208,3 +213,63 @@ jobs:
env:
ZULIP_BOT_KEY: ${{ secrets.ZULIP_BOT_KEY }}
run: /tmp/send-failure-message
production_upgrade:
# The production upgrade job starts with a container with a
# previous Zulip release installed, and attempts to upgrade it to
# the release tarball built for the current commit being tested.
#
# This is intended to catch bugs that result in the upgrade
# process failing.
strategy:
fail-fast: false
matrix:
include:
# Base images are built using `tools/ci/Dockerfile.prod.template`.
# The comments at the top explain how to build and upload these images.
- docker_image: zulip/ci:buster-3.4
name: 3.4 Version Upgrade
is_focal: true
os: buster
name: ${{ matrix.name }}
container:
image: ${{ matrix.docker_image }}
options: --init
runs-on: ubuntu-latest
needs: production_build
steps:
- name: Download built production tarball
uses: actions/download-artifact@v2
with:
name: production-tarball
path: /tmp
- name: Add required permissions and setup
run: |
# This is the GitHub Actions specific cache directory the
# the current github user must be able to access for the
# cache action to work. It is owned by root currently.
sudo chmod -R 0777 /__w/_temp/
# Since actions/download-artifact@v2 loses all the permissions
# of the tarball uploaded by the upload artifact fix those.
chmod +x /tmp/production-upgrade
chmod +x /tmp/production-verify
chmod +x /tmp/send-failure-message
- name: Upgrade production
run: sudo /tmp/production-upgrade
# TODO: We should be running production-verify here, but it
# doesn't pass yet.
#
# - name: Verify install
# run: sudo /tmp/production-verify
- name: Report status
if: failure()
env:
ZULIP_BOT_KEY: ${{ secrets.ZULIP_BOT_KEY }}
run: /tmp/send-failure-message

View File

@@ -7,6 +7,12 @@ up-to-date list of raw changes.
## Zulip 4.x series
### 4.7 -- 2021-10-04
- CVE-2021-41115: Prevent organization administrators from affecting
the server with a regular expression denial-of-service attack
through linkifier patterns.
### 4.6 -- 2021-09-23
- Documented official support for Debian 11 Bullseye, now that it is

View File

@@ -412,11 +412,64 @@ instructions for other supported platforms.
### Upgrading from Debian Buster to Debian Bullseye
We expect to have tested documentation for upgrading from Buster to
Bullseye available soon. See [this chat.zulip.org
thread][bullseye-discussion-thread]) for the current status of that work.
1. Upgrade your server to the latest Zulip `4.x` release.
[bullseye-discussion-thread]: https://chat.zulip.org/#narrow/stream/3-backend/topic/Upgrade.20to.20bullseye
2. As the Zulip user, stop the Zulip server and run the following
to back up the system:
```bash
supervisorctl stop all
/home/zulip/deployments/current/manage.py backup --output=/home/zulip/release-upgrade.backup.tar.gz
```
3. Follow [Debian's instructions to upgrade the OS][bullseye-upgrade].
[bullseye-upgrade]: https://www.debian.org/releases/bullseye/amd64/release-notes/ch-upgrading.html
When prompted for you how to upgrade configuration
files for services that Zulip manages like Redis, PostgreSQL,
Nginx, and memcached, the best choice is `N` to keep the
currently installed version. But it's not important; the next
step will re-install Zulip's configuration in any case.
4. As root, run the following steps to regenerate configurations
for services used by Zulip:
```bash
apt remove upstart -y
/home/zulip/deployments/current/scripts/zulip-puppet-apply -f
```
5. Reinstall the current version of Zulip, which among other things
will recompile Zulip's Python module dependencies for your new
version of Python:
```bash
rm -rf /srv/zulip-venv-cache/*
/home/zulip/deployments/current/scripts/lib/upgrade-zulip-stage-2 \
/home/zulip/deployments/current/ --ignore-static-assets
```
This will finish by restarting your Zulip server; you should now
be able to navigate to its URL and confirm everything is working
correctly.
6. Debian Bullseye has a different version of the low-level glibc
library, which affects how PostgreSQL orders text data (known as
"collations"); this corrupts database indexes that rely on
collations. Regenerate the affected indexes by running:
```bash
/home/zulip/deployments/current/scripts/setup/reindex-textual-data --force
```
7. As root, finish by verifying the contents of the full-text indexes:
```bash
/home/zulip/deployments/current/manage.py audit_fts_indexes
```
8. As an additional step, you can also [upgrade the postgresql version](#upgrading-postgresql).
### Upgrading from Debian Stretch to Debian Buster
@@ -424,7 +477,13 @@ thread][bullseye-discussion-thread]) for the current status of that work.
only upgrade to Zulip 3.0 and newer after completing this process,
since newer releases don't support Ubuntu Debian Stretch.
2. Same as for Bionic to Focal.
2. As the Zulip user, stop the Zulip server and run the following
to back up the system:
```bash
supervisorctl stop all
/home/zulip/deployments/current/manage.py backup --output=/home/zulip/release-upgrade.backup.tar.gz
```
3. Follow [Debian's instructions to upgrade the OS][debian-upgrade-os].

View File

@@ -42,7 +42,7 @@ async function test_delete_linkifier(page: Page): Promise<void> {
async function test_add_invalid_linkifier_pattern(page: Page): Promise<void> {
await page.waitForSelector(".admin-linkifier-form", {visible: true});
await common.fill_form(page, "form.admin-linkifier-form", {
pattern: "a$",
pattern: "(foo",
url_format_string: "https://trac.example.com/ticket/%(id)s",
});
await page.click("form.admin-linkifier-form button.button");
@@ -50,7 +50,7 @@ async function test_add_invalid_linkifier_pattern(page: Page): Promise<void> {
await page.waitForSelector("div#admin-linkifier-pattern-status", {visible: true});
assert.strictEqual(
await common.get_text_from_selector(page, "div#admin-linkifier-pattern-status"),
"Failed: Invalid linkifier pattern. Valid characters are [ a-zA-Z_#=/:+!-].",
"Failed: Bad regular expression: missing ): (foo",
);
}
@@ -83,8 +83,8 @@ async function test_edit_invalid_linkifier(page: Page): Promise<void> {
await page.click(".linkifier_row .edit");
await page.waitForFunction(() => document.activeElement === $("#linkifier-edit-form-modal")[0]);
await common.fill_form(page, "form.linkifier-edit-form", {
pattern: "####",
url_format_string: "####",
pattern: "#(?P<id>d????)",
url_format_string: "????",
});
await page.click(".submit-linkifier-info-change");
@@ -96,7 +96,7 @@ async function test_edit_invalid_linkifier(page: Page): Promise<void> {
);
assert.strictEqual(
edit_linkifier_pattern_status,
"Failed: Invalid linkifier pattern. Valid characters are [ a-zA-Z_#=/:+!-].",
"Failed: Bad regular expression: bad repetition operator: ????",
);
const edit_linkifier_format_status_selector = "div#edit-linkifier-format-status";

View File

@@ -184,3 +184,6 @@ pyuca
# Handle connection retries with exponential backoff
backoff
# Non-backtracking regular expressions
google-re2

View File

@@ -408,6 +408,13 @@ gitlint==0.15.1 \
--hash=sha256:4b22916dcbdca381244aee6cb8d8743756cfd98f27e7d1f02e78733f07c3c21c \
--hash=sha256:7ebdb8e7d333e577e956225cbc3ad8e0e96d05e638e6d461c9b66b784f9d2ac4
# via -r requirements/dev.in
google-re2==0.2.20210901 \
--hash=sha256:028bb86b1612cbd7947e7493a92f1dc33c8b95b31a57017c392811650b62bf5d \
--hash=sha256:10020ad49a6217534c5b82543ca4ee56c3317981afb68a8c737c212f222564f5 \
--hash=sha256:542b9b18f01d06375ffab6a0e2ed809e3de7ddedde3382b6d24a98aced57c7a5 \
--hash=sha256:676fa9ee54e3fb70f290526fc0f4d78d1e5a4add701b5547494eaf7c68c72247 \
--hash=sha256:a1175c7e5b5ad12462279184099b7b95f72b25fab4d141289224c7a3ffdc4a06
# via -r requirements/common.in
h2==2.6.2 \
--hash=sha256:93cbd1013a2218539af05cdf9fc37b786655b93bbc94f5296b7dabd1c5cadf41 \
--hash=sha256:af35878673c83a44afbc12b13ac91a489da2819b5dc1e11768f3c2406f740fe9

View File

@@ -259,6 +259,13 @@ django[argon2]==3.2.2 \
future==0.18.2 \
--hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d
# via python-twitter
google-re2==0.2.20210901 \
--hash=sha256:028bb86b1612cbd7947e7493a92f1dc33c8b95b31a57017c392811650b62bf5d \
--hash=sha256:10020ad49a6217534c5b82543ca4ee56c3317981afb68a8c737c212f222564f5 \
--hash=sha256:542b9b18f01d06375ffab6a0e2ed809e3de7ddedde3382b6d24a98aced57c7a5 \
--hash=sha256:676fa9ee54e3fb70f290526fc0f4d78d1e5a4add701b5547494eaf7c68c72247 \
--hash=sha256:a1175c7e5b5ad12462279184099b7b95f72b25fab4d141289224c7a3ffdc4a06
# via -r requirements/common.in
h2==2.6.2 \
--hash=sha256:93cbd1013a2218539af05cdf9fc37b786655b93bbc94f5296b7dabd1c5cadf41 \
--hash=sha256:af35878673c83a44afbc12b13ac91a489da2819b5dc1e11768f3c2406f740fe9

View File

@@ -37,6 +37,7 @@ cp -a \
tools/ci/success-http-headers.template.debian.txt \
tools/ci/production-install \
tools/ci/production-verify \
tools/ci/production-upgrade \
tools/ci/production-upgrade-pg \
tools/ci/production-extract-tarball \
tools/ci/send-failure-message \

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Given a Zulip production environment that had been installed with a
# previous version of Zulip, upgrade it to the commit being tested.
# This takes as input the tarball generated by production-build.
set -e
set -x
# Structurally, this script should just call upgrade-zulip. However,
# because of a set of issues that result in the previously installed
# GitHub Actions Docker containers not actually working on boot, we
# need to do some preparatory steps. It is a goal to delete these
# steps.
# Reinstall rabbitmq-server.
#
# * For rabbitmq-server, we likely need to do this to work around the
# hostname changing on reboot causing RabbitMQ to not boot.
sudo apt-get -y purge rabbitmq-server
sudo apt-get -y install rabbitmq-server
# Start the postgresql service.
sudo service postgresql start
# Starting the rabbitmq-server
if ! sudo service rabbitmq-server start; then
echo
echo "Starting rabbitmq-server failed. Trying again:"
sudo service rabbitmq-server start
fi
# Start the supervisor
sudo service supervisor start
# Zulip releases before 2.1.8/3.5/4.4 have a bug in their
# `upgrade-zulip` scripts, resulting in them exiting with status 0
# unconditionally. We work around that by running
# scripts/lib/upgrade-zulip instead.
UPGRADE_SCRIPT=/home/zulip/deployments/current/scripts/lib/upgrade-zulip
# Execute the upgrade.
sudo "$UPGRADE_SCRIPT" /tmp/zulip-server-test.tar.gz

View File

@@ -1,6 +1,6 @@
import os
ZULIP_VERSION = "4.6"
ZULIP_VERSION = "4.7"
# Add information on number of commits and commit hash to version, if available
zulip_git_version_file = os.path.join(
@@ -14,7 +14,7 @@ ZULIP_VERSION = lines.pop(0).strip()
ZULIP_MERGE_BASE = lines.pop(0).strip()
LATEST_MAJOR_VERSION = "4.0"
LATEST_RELEASE_VERSION = "4.6"
LATEST_RELEASE_VERSION = "4.7"
LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.com/2021/05/13/zulip-4-0-released/"
# Versions of the desktop app below DESKTOP_MINIMUM_VERSION will be
@@ -47,4 +47,4 @@ API_FEATURE_LEVEL = 65
# historical commits sharing the same major version, in which case a
# minor version bump suffices.
PROVISION_VERSION = "146.0"
PROVISION_VERSION = "146.1"

View File

@@ -37,6 +37,7 @@ import markdown.inlinepatterns
import markdown.postprocessors
import markdown.treeprocessors
import markdown.util
import re2
import requests
from django.conf import settings
from django.db.models import Q
@@ -1779,7 +1780,9 @@ class MarkdownListPreprocessor(markdown.preprocessors.Preprocessor):
# Name for the outer capture group we use to separate whitespace and
# other delimiters from the actual content. This value won't be an
# option in user-entered capture groups.
BEFORE_CAPTURE_GROUP = "linkifier_before_match"
OUTER_CAPTURE_GROUP = "linkifier_actual_match"
AFTER_CAPTURE_GROUP = "linkifier_after_match"
def prepare_linkifier_pattern(source: str) -> str:
@@ -1787,31 +1790,45 @@ def prepare_linkifier_pattern(source: str) -> str:
whitespace, or opening delimiters, won't match if there are word
characters directly after, and saves what was matched as
OUTER_CAPTURE_GROUP."""
return fr"""(?<![^\s'"\(,:<])(?P<{OUTER_CAPTURE_GROUP}>{source})(?!\w)"""
return fr"""(?P<{BEFORE_CAPTURE_GROUP}>^|\s|['"\(,:<])(?P<{OUTER_CAPTURE_GROUP}>{source})(?P<{AFTER_CAPTURE_GROUP}>$|[^\pL\pN])"""
# Given a regular expression pattern, linkifies groups that match it
# using the provided format string to construct the URL.
class LinkifierPattern(markdown.inlinepatterns.Pattern):
class LinkifierPattern(markdown.inlinepatterns.InlineProcessor):
"""Applied a given linkifier to the input"""
def __init__(
self,
source_pattern: str,
format_string: str,
markdown_instance: Optional[markdown.Markdown] = None,
md: markdown.Markdown,
) -> None:
self.pattern = prepare_linkifier_pattern(source_pattern)
self.format_string = format_string
markdown.inlinepatterns.Pattern.__init__(self, self.pattern, markdown_instance)
# Do not write errors to stderr (this still raises exceptions)
options = re2.Options()
options.log_errors = False
def handleMatch(self, m: Match[str]) -> Union[Element, str]:
self.md = md
self.compiled_re = re2.compile(prepare_linkifier_pattern(source_pattern), options=options)
self.format_string = format_string
def handleMatch( # type: ignore[override] # supertype incompatible with supersupertype
self, m: Match[str], data: str
) -> Union[Tuple[Element, int, int], Tuple[None, None, None]]:
db_data = self.md.zulip_db_data
return url_to_a(
url = url_to_a(
db_data,
self.format_string % m.groupdict(),
markdown.util.AtomicString(m.group(OUTER_CAPTURE_GROUP)),
)
if isinstance(url, str):
return None, None, None
return (
url,
m.start(2),
m.end(2),
)
class UserMentionPattern(markdown.inlinepatterns.InlineProcessor):
@@ -2336,11 +2353,19 @@ def topic_links(linkifiers_key: int, topic_name: str) -> List[Dict[str, str]]:
matches: List[Dict[str, Union[str, int]]] = []
linkifiers = linkifiers_for_realm(linkifiers_key)
options = re2.Options()
options.log_errors = False
for linkifier in linkifiers:
raw_pattern = linkifier["pattern"]
url_format_string = linkifier["url_format"]
pattern = prepare_linkifier_pattern(raw_pattern)
for m in re.finditer(pattern, topic_name):
try:
pattern = re2.compile(prepare_linkifier_pattern(raw_pattern), options=options)
except re2.error:
# An invalid regex shouldn't be possible here, and logging
# here on an invalid regex would spam the logs with every
# message sent; simply move on.
continue
for m in pattern.finditer(topic_name):
match_details = m.groupdict()
match_text = match_details["linkifier_actual_match"]
# We format the linkifier's url string using the matched text.

View File

@@ -0,0 +1,39 @@
import re2
from django.db import migrations
from django.db.backends.postgresql.schema import DatabaseSchemaEditor
from django.db.migrations.state import StateApps
def delete_re2_invalid(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
options = re2.Options()
options.log_errors = False
RealmFilter = apps.get_model("zerver", "RealmFilter")
found_errors = False
for linkifier in RealmFilter.objects.all():
try:
re2.compile(linkifier.pattern, options=options)
except re2.error:
if not found_errors:
print()
found_errors = True
print(
f"Deleting linkifier {linkifier.id} in realm {linkifier.realm.string_id} which is not compatible with new re2 engine:"
)
print(f" {linkifier.pattern} -> {linkifier.url_format_string}")
linkifier.delete()
class Migration(migrations.Migration):
dependencies = [
("zerver", "0325_alter_realmplayground_unique_together"),
]
operations = [
migrations.RunPython(
delete_re2_invalid,
reverse_code=migrations.RunPython.noop,
elidable=True,
)
]

View File

@@ -19,6 +19,7 @@ from typing import (
)
import django.contrib.auth
import re2
from bitfield import BitField
from bitfield.types import BitHandler
from django.conf import settings
@@ -906,19 +907,19 @@ post_delete.connect(flush_realm_emoji, sender=RealmEmoji)
def filter_pattern_validator(value: str) -> None:
regex = re.compile(r"^(?:(?:[\w\-#_= /:]*|[+]|[!])(\(\?P<\w+>.+\)))+$")
error_msg = _("Invalid linkifier pattern. Valid characters are {}.").format(
"[ a-zA-Z_#=/:+!-]",
)
if not regex.match(str(value)):
raise ValidationError(error_msg)
try:
re.compile(value)
except re.error:
# Regex is invalid
raise ValidationError(error_msg)
# Do not write errors to stderr (this still raises exceptions)
options = re2.Options()
options.log_errors = False
re2.compile(value, options=options)
except re2.error as e:
if len(e.args) >= 1:
if isinstance(e.args[0], str): # nocoverage
raise ValidationError(_("Bad regular expression: {}").format(e.args[0]))
if isinstance(e.args[0], bytes):
raise ValidationError(_("Bad regular expression: {}").format(e.args[0].decode()))
raise ValidationError(_("Unknown regular expression error")) # nocoverage
def filter_format_validator(value: str) -> None:

View File

@@ -1401,26 +1401,25 @@ class MarkdownTest(ZulipTestCase):
url_format_string = r"https://trac.example.com/ticket/%(id)s"
linkifier_1 = RealmFilter(
realm=realm,
pattern=r"(?P<id>ABC\-[0-9]+)(?![A-Z0-9-])",
pattern=r"(?P<id>ABC\-[0-9]+)",
url_format_string=url_format_string,
)
linkifier_1.save()
self.assertEqual(
linkifier_1.__str__(),
r"<RealmFilter(zulip): (?P<id>ABC\-[0-9]+)(?![A-Z0-9-])"
" https://trac.example.com/ticket/%(id)s>",
r"<RealmFilter(zulip): (?P<id>ABC\-[0-9]+) https://trac.example.com/ticket/%(id)s>",
)
url_format_string = r"https://other-trac.example.com/ticket/%(id)s"
linkifier_2 = RealmFilter(
realm=realm,
pattern=r"(?P<id>[A-Z][A-Z0-9]*\-[0-9]+)(?![A-Z0-9-])",
pattern=r"(?P<id>[A-Z][A-Z0-9]*\-[0-9]+)",
url_format_string=url_format_string,
)
linkifier_2.save()
self.assertEqual(
linkifier_2.__str__(),
r"<RealmFilter(zulip): (?P<id>[A-Z][A-Z0-9]*\-[0-9]+)(?![A-Z0-9-])"
r"<RealmFilter(zulip): (?P<id>[A-Z][A-Z0-9]*\-[0-9]+)"
" https://other-trac.example.com/ticket/%(id)s>",
)

View File

@@ -27,17 +27,13 @@ class RealmFilterTest(ZulipTestCase):
result = self.client_post("/json/realm/filters", info=data)
self.assert_json_error(result, "This field cannot be blank.")
data["pattern"] = "$a"
data["pattern"] = "(foo"
result = self.client_post("/json/realm/filters", info=data)
self.assert_json_error(
result, "Invalid linkifier pattern. Valid characters are [ a-zA-Z_#=/:+!-]."
)
self.assert_json_error(result, "Bad regular expression: missing ): (foo")
data["pattern"] = r"ZUL-(?P<id>\d++)"
data["pattern"] = r"ZUL-(?P<id>\d????)"
result = self.client_post("/json/realm/filters", info=data)
self.assert_json_error(
result, "Invalid linkifier pattern. Valid characters are [ a-zA-Z_#=/:+!-]."
)
self.assert_json_error(result, "Bad regular expression: bad repetition operator: ????")
data["pattern"] = r"ZUL-(?P<id>\d+)"
data["url_format_string"] = "$fgfg"
@@ -154,13 +150,11 @@ class RealmFilterTest(ZulipTestCase):
)
data = {
"pattern": r"ZUL-(?P<id>\d++)",
"pattern": r"ZUL-(?P<id>\d????)",
"url_format_string": "https://realm.com/my_realm_filter/%(id)s",
}
result = self.client_patch(f"/json/realm/filters/{linkifier_id}", info=data)
self.assert_json_error(
result, "Invalid linkifier pattern. Valid characters are [ a-zA-Z_#=/:+!-]."
)
self.assert_json_error(result, "Bad regular expression: bad repetition operator: ????")
data["pattern"] = r"ZUL-(?P<id>\d+)"
data["url_format_string"] = "$fgfg"