Compare commits

...

17 Commits

Author SHA1 Message Date
Karl Stolley
90054890f3 api_docs: Add section on transcoded images. 2025-10-14 15:37:17 -07:00
Karl Stolley
19e5c8b8c9 api_docs: Clean up intro text on image placeholders. 2025-10-14 15:35:27 -07:00
Karl Stolley
35aac76176 api_docs: Reference example images as example.png. 2025-10-14 15:35:27 -07:00
Karl Stolley
85e6cec1db api_docs: Better structure Image previews section. 2025-10-14 15:35:27 -07:00
Alex Vandiver
bdb2c921ba docs: Document wal-g restore process. 2025-10-14 15:07:33 -07:00
Alex Vandiver
dd92036550 docs: Trim PostgreSQL support table.
Remove versions which we have also removed from ReadTheDocs.
2025-10-14 15:06:01 -07:00
Alex Vandiver
9815db9811 upload: Use normpath when comparing to LOCAL_UPLOADS_DIR.
This prevents a development-mode-only directory traversal attack,
where the Django development server could be made to respond to
requests for `/user_avatars/../../../../../../etc/passwd`.

The production server is not affected by this vulnerability, as
nginx's configuration sets `PATH_INFO` to `$document_uri`, which is
normalized[^1] -- that is, by the time uwsgi and Django see it, the path
has been percent-decoded once, and all `../` path components have been
applied[^2].

Close this by explicitly normalizing the paths before comparing; the
`LOCAL_UPLOADS_DIR` side is unlikely to require normalization as well,
but is also normalized for consistency.  The failure here is left as
an assertion failure, and not a JsonableError, because it only affects
the development server.

[^1]: https://nginx.org/en/docs/http/ngx_http_core_module.html#var_uri
[^2]: https://nginx.org/en/docs/http/ngx_http_core_module.html#location
2025-10-14 12:56:10 -07:00
Lauryn Menard
5d7adcbc00 typing: Use people.emails_string_to_user_ids for "dm" narrow term.
Updates the logic for getting the typists for a specific direct
message conversation to mirror narrow_state.set_compose_defaults
for the current filter's "dm" narrow term.
2025-10-14 12:19:53 -07:00
Lauryn Menard
c2d008aadb test-signup: Confirm all invalid email field error messages.
In Django, when cleaning a form field, all validators are run on
the field and all validation error messages are all collected.
Updates our test for invalid email addresses when creating a new
realm to confirm all expected error messages from the various
validators that are run on that field.
2025-10-14 12:19:07 -07:00
Lauryn Menard
f55c89a87f forms: Call superclass clean method for CaptchaRealmCreationForm.
Ensures that the form fields set in the superclass are validated.
2025-10-14 12:19:07 -07:00
Lauryn Menard
7185f2c236 forms: Set realm_creation field one time when initializing form.
In commit c7a08f3b77, we started setting the realm_creation field
in both the RegistrationForm and its superclass, RealmDetailsForm,
which was likely a copy and paste error.

Since we only need to set the realm_creation field once when
initializing the form fields, we set it in the RealmDetailsForm,
which also removes any confusion related to the comment about
removing extra kwargs in the RegistrationForm initialization.
2025-10-14 12:19:07 -07:00
PieterCK
b36f09c67f slack_importer_doc: Fix outdated links.
Existing links redirects to irrelevant documentation pages.
2025-10-14 12:17:45 -07:00
Niloth P
ad122af6f8 integrations: Use dir_name instead of name for default view fn.
Use the directory name as the template literal in the default view
function path.
2025-10-14 12:17:05 -07:00
Niloth P
23740c97a4 integrations: Remove redundant arguments. 2025-10-14 12:17:05 -07:00
Niloth P
f33ef8f206 integrations: Rename IFTTT view function to match conventions. 2025-10-14 12:17:05 -07:00
Tim Abbott
e4ba536eae migrations: Add merge migration for backport of 0753.
Systems upgrading from 11.x will have 0753 and not 0752, while systems
upgrading from main may have 0752 and not 0753, so a merge migration
is required to smoothly handle upgrades from both states.
2025-10-14 11:56:57 -07:00
Aman Agrawal
9ffe31e352 message_list_view: Fix missing bookend when prepending messages.
The logic for inserting bookend when prepending messages was
missing.

Fixed by inserting the bookend at the correct position.

Reproducer:

Modify `message_fetch` parameters to only fetch
one message per fetch to ensure that each message is prepended.

Subscribe to a channel and send a message.

Reload.

Bookend is absent before the latest message without this commit.
2025-10-14 11:52:36 -07:00
15 changed files with 135 additions and 55 deletions

View File

@@ -866,7 +866,7 @@ deactivated groups.
**Feature level 336**
* [Markdown message formatting](/api/message-formatting#image-previews): Added
* [Markdown message formatting](/api/message-formatting#images): Added
`data-original-content-type` attribute to convey the type of the original
image, and optional `data-transcoded-image` attribute for images with formats
which are not widely supported by browsers.
@@ -1435,7 +1435,7 @@ deactivated groups.
**Feature level 287**
* [Markdown message
formatting](/api/message-formatting#image-previews): Added
formatting](/api/message-formatting#images): Added
`data-original-dimensions` attributes to placeholder images
(`image-loading-placeholder`), containing the dimensions of the
original image. This change was also backported to the Zulip 9.x
@@ -1511,7 +1511,7 @@ releases.
**Feature level 278**
* [Markdown message
formatting](/api/message-formatting#image-previews): Added
formatting](/api/message-formatting#images): Added
`data-original-dimensions` attributes to placeholder images
(`image-loading-placeholder`), containing the dimensions of the
original image. Backported change from feature level 287.
@@ -1524,7 +1524,7 @@ No changes; feature level used for Zulip 9.0 release.
**Feature level 276**
* [Markdown message formatting](/api/message-formatting#image-previews):
* [Markdown message formatting](/api/message-formatting#images):
Image preview elements not contain a `data-original-dimensions`
attribute containing the dimensions of the original image.

View File

@@ -121,31 +121,35 @@ the href for those is the default behavior of the link that also
encodes the channel alongside the data-stream-id field, but clients
can override that default based on `web_channel_default_view` setting.
## Image previews
## Images
When a Zulip message is sent linking to an uploaded image, Zulip will
generate an image preview element with the following format.
generate an image preview element with the following format:
``` html
<div class="message_inline_image">
<a href="/user_uploads/path/to/image.png" title="image.png">
<a href="/user_uploads/path/to/example.png" title="example.png">
<img data-original-dimensions="1920x1080"
data-original-content-type="image/png"
src="/user_uploads/thumbnail/path/to/image.png/840x560.webp">
src="/user_uploads/thumbnail/path/to/example.png/840x560.webp">
</a>
</div>
```
If the server has not yet generated thumbnails for the image yet at
the time the message is sent, the `img` element will be a temporary
loading indicator image and have the `image-loading-placeholder`
**Changes**: See [Changes to image formatting](#changes-to-image-formatting).
### Image-loading placeholders
If the server has yet to generate thumbnails for the image by
the time the message is sent, the `img` element will temporarily
reference a loading indicator image and have the `image-loading-placeholder`
class, which clients can use to identify loading indicators and
replace them with a more native loading indicator element if
desired. For example:
``` html
<div class="message_inline_image">
<a href="/user_uploads/path/to/image.png" title="image.png">
<a href="/user_uploads/path/to/example.png" title="example.png">
<img class="image-loading-placeholder"
data-original-dimensions="1920x1080"
data-original-content-type="image/png"
@@ -168,6 +172,31 @@ backlogged, an individual message containing multiple image previews
may be re-rendered multiple times as each image finishes thumbnailing
and triggers a message update.
### Transcoded images
Image elements whose formats are not widely supported by web browsers
(e.g., HEIC and TIFF) may contain a `data-transcoded-image` attribute,
which specifies a high-resolution thumbnail format that clients may
opt to present instead of the original image. If the
`data-transcoded-image` attribute is present, clients should use the
`data-original-content-type` attribute to decide whether to display the
original image or use the transcoded version.
Transcoded images are presented with this structure in image previews:
``` html
<div class="message_inline_image">
<a href="/user_uploads/path/to/example.heic" title="example.heic">
<img data-original-dimensions="1920x1080"
data-original-content-type="image/heic"
data-transcoded-image="1920x1080.webp"
src="/user_uploads/thumbnail/path/to/example.heic/840x560.webp">
</a>
</div>
```
### Recommended client processing of image previews
Clients are recommended to do the following when processing image
previews:
@@ -217,16 +246,18 @@ previews:
format match what they requested.
- No other processing of the URLs is recommended.
**Changes**: In Zulip 10.0 (feature level 336), added
### Changes to image formatting
**In Zulip 10.0** (feature level 336), added
`data-original-content-type` attribute to convey the type of the
original image, and optional `data-transcoded-image` attribute for
images with formats which are not widely supported by browsers.
**Changes**: In Zulip 9.2 (feature levels 278-279, and 287+), added
**In Zulip 9.2** (feature levels 278-279, and 287+), added
`data-original-dimensions` to the `image-loading-placeholder` spinner
images, containing the dimensions of the original image.
In Zulip 9.0 (feature level 276), added `data-original-dimensions`
**In Zulip 9.0** (feature level 276), added `data-original-dimensions`
attribute to images that have been thumbnailed, containing the
dimensions of the full-size version of the image. Thumbnailing itself
was reintroduced at feature level 275.

View File

@@ -587,6 +587,53 @@ analysis of recent application-level data changes.
You may also want to adjust the [incremental backups][incremental]
configuration.
### Restoring from wal-g backups
The following steps will restore the database from the latest
backup. Note that this process will _delete your current database_.
1. As `root` on your database host, check your list of backups; the
most recent will be listed at the bottom, and is what will be
restored by the commands below.
```shell
env-wal-g backup-list --pretty
```
1. Stop Zulip, if it is running. On your application host (which may
or may not be different from your database host, depending on your
configuration):
```shell
/home/zulip/deployments/current/scripts/stop-server
```
1. As `root` on your database host, stop PostgreSQL:
```shell
service postgresql stop
```
1. As `root` on your database host, delete the current database, and
restore from the backup. This may take some time, depending on the
size of your database and your connection to your backup storage.
```shell
pg_version=$(crudini --get /etc/zulip/zulip.conf postgresql version)
rm -rf "/var/lib/postgresql/$pg_version/main"
env-wal-g backup-fetch "/var/lib/postgresql/$pg_version/main" LATEST
chown -R postgres.postgres "/var/lib/postgresql/$pg_version/main"
touch "/var/lib/postgresql/$pg_version/main/recovery.signal"
service postgresql start
```
1. As `root` on your application host, flush caches and start Zulip:
```shell
/home/zulip/deployments/current/scripts/setup/flush-memcached
/home/zulip/deployments/current/scripts/start-server
```
[wal]: https://www.postgresql.org/docs/current/wal-intro.html
[archive-timeout]: https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-ARCHIVE-TIMEOUT
[mobile-push]: ../production/mobile-push-notifications.md

View File

@@ -1,9 +1,5 @@
| Zulip Server version | Supported versions of PostgreSQL |
| -------------------- | -------------------------------- |
| 3.x | 9.3, 9.5, 9.6, 10, 11, 12 |
| 4.x | 9.3, 9.5, 9.6, 10, 11, 12, 13 |
| 5.x | 10, 11, 12, 13, 14 |
| 6.x | 11, 12, 13, 14 |
| 7.x | 12, 13, 14, 15 |
| 8.x | 12, 13, 14, 15, 16 |
| 9.x | 12, 13, 14, 15, 16 |

View File

@@ -75,13 +75,13 @@ in order to export direct message data.
1. [Create a new Slack app](https://api.slack.com/apps). Choose the "From
scratch" creation option.
1. [Create a
bot user](https://api.slack.com/authentication/basics#scopes),
bot user](https://docs.slack.dev/app-management/quickstart-app-settings/#creating),
following the instructions to add the following OAuth scopes to your bot:
* `emoji:read`
* `users:read`
* `users:read.email`
* `team:read`
1. [Install your new app](https://api.slack.com/authentication/basics#installing)
1. [Install your new app](https://docs.slack.dev/app-management/quickstart-app-settings/#installing)
to your Slack workspace.
1. You will immediately see a **Bot User OAuth Token**, which is a long
string of numbers and characters starting with `xoxb-`. Copy this token. It

View File

@@ -983,6 +983,11 @@ export class MessageListView {
update_group_date(second_group, curr_msg_container.msg, prev_msg_container?.msg);
// We could add an action to update the date row, but for now rerender the group.
message_actions.rerender_groups.push(second_group);
} else if (second_group.bookend_top) {
// We know there was no bookend_top before since we
// are adding messages to the top.
const rendered_bookend_html = render_bookend(second_group);
this.$list.prepend($(rendered_bookend_html));
}
message_actions.prepend_groups = new_message_groups;
this._message_groups = [...new_message_groups, ...this._message_groups];

View File

@@ -90,23 +90,16 @@ function get_users_typing_for_narrow(): number[] {
return [];
}
const terms = narrow_state.search_terms();
if (terms[0] === undefined) {
return [];
}
const first_term = terms[0];
if (first_term.operator === "dm") {
// Narrow has a filter with either "dm:" or "is:dm".
const current_filter = narrow_state.filter()!;
if (current_filter.has_operator("dm")) {
// Get list of users typing in this conversation
const narrow_emails_string = first_term.operand;
// TODO: Create people.emails_strings_to_user_ids.
const narrow_user_ids_string = people.reply_to_to_user_ids_string(narrow_emails_string);
if (!narrow_user_ids_string) {
const narrow_emails_string = current_filter.operands("dm")[0]!;
if (!people.is_valid_bulk_emails_for_compose(narrow_emails_string.split(","))) {
// Narrowed to an invalid direct message recipient.
return [];
}
const narrow_user_ids = narrow_user_ids_string
.split(",")
.map((user_id_string) => Number.parseInt(user_id_string, 10));
const narrow_user_ids = people.emails_string_to_user_ids(narrow_emails_string);
const group = [...narrow_user_ids, current_user.user_id];
return typing_data.get_group_typists(group);
}

View File

@@ -174,7 +174,6 @@ class RegistrationForm(RealmDetailsForm):
def __init__(self, *args: Any, **kwargs: Any) -> None:
# Since the superclass doesn't except random extra kwargs, we
# remove it from the kwargs dict before initializing.
self.realm_creation = kwargs["realm_creation"]
self.realm = kwargs.pop("realm", None)
super().__init__(*args, **kwargs)
@@ -390,6 +389,7 @@ class CaptchaRealmCreationForm(RealmCreationForm):
@override
def clean(self) -> None:
super().clean()
if not self.data.get("captcha"):
self.add_error("captcha", _("Validation failed, please try again."))

View File

@@ -259,7 +259,7 @@ class PythonAPIIntegration(Integration):
class WebhookIntegration(Integration):
DEFAULT_FUNCTION_PATH = "zerver.webhooks.{name}.view.api_{name}_webhook"
DEFAULT_FUNCTION_PATH = "zerver.webhooks.{dir_name}.view.api_{dir_name}_webhook"
DEFAULT_URL = "api/v1/external/{name}"
DEFAULT_CLIENT_NAME = "Zulip{name}Webhook"
DEFAULT_DOC_PATH = "{name}/doc.md"
@@ -296,8 +296,12 @@ class WebhookIntegration(Integration):
url_options=url_options,
)
if dir_name is None:
dir_name = self.name
self.dir_name = dir_name
if function is None:
function = self.DEFAULT_FUNCTION_PATH.format(name=name)
function = self.DEFAULT_FUNCTION_PATH.format(dir_name=dir_name)
self.function_name = function
if url is None:
@@ -308,10 +312,6 @@ class WebhookIntegration(Integration):
doc = self.DEFAULT_DOC_PATH.format(name=name)
self.doc = doc
if dir_name is None:
dir_name = self.name
self.dir_name = dir_name
def get_function(self) -> Callable[[HttpRequest], HttpResponseBase]:
return import_string(self.function_name)
@@ -497,8 +497,6 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
"github",
["version-control"],
display_name="GitHub",
function="zerver.webhooks.github.view.api_github_webhook",
stream_name="github",
url_options=[
WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES),
WebhookUrlOption.build_preset_config(PresetUrlOption.IGNORE_PRIVATE_REPOSITORIES),
@@ -510,7 +508,6 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
display_name="GitHub Sponsors",
logo="images/integrations/logos/github.svg",
dir_name="github",
function="zerver.webhooks.github.view.api_github_webhook",
doc="github/githubsponsors.md",
stream_name="github",
),
@@ -536,12 +533,7 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
WebhookIntegration("helloworld", ["misc"], display_name="Hello World"),
WebhookIntegration("heroku", ["deployment"]),
WebhookIntegration("homeassistant", ["misc"], display_name="Home Assistant"),
WebhookIntegration(
"ifttt",
["meta-integration"],
function="zerver.webhooks.ifttt.view.api_iftt_app_webhook",
display_name="IFTTT",
),
WebhookIntegration("ifttt", ["meta-integration"], display_name="IFTTT"),
WebhookIntegration("insping", ["monitoring"]),
WebhookIntegration("intercom", ["customer-support"]),
# Avoid collision with jira-plugin's doc "jira/doc.md".

View File

@@ -27,7 +27,8 @@ def assert_is_local_storage_path(type: Literal["avatars", "files"], full_path: s
defense in depth.
"""
assert settings.LOCAL_UPLOADS_DIR is not None
type_path = os.path.join(settings.LOCAL_UPLOADS_DIR, type)
type_path = os.path.normpath(os.path.join(settings.LOCAL_UPLOADS_DIR, type))
full_path = os.path.normpath(full_path)
assert os.path.commonpath([type_path, full_path]) == type_path

View File

@@ -15,7 +15,7 @@ def remove_google_blob(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor)
class Migration(migrations.Migration):
dependencies = [
("zerver", "0752_remove_stream_is_in_zephyr_realm"),
("zerver", "0751_externalauthid_zerver_user_externalauth_uniq"),
]
operations = [

View File

@@ -0,0 +1,12 @@
# Generated by Django 5.2.6 on 2025-10-14 18:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("zerver", "0752_remove_stream_is_in_zephyr_realm"),
("zerver", "0753_remove_google_blob_emojiset"),
]
operations = []

View File

@@ -2073,11 +2073,14 @@ class RealmCreationTest(ZulipTestCase):
email="<foo", realm_subdomain="custom-test", realm_name="Zulip test"
)
self.assert_in_response("Please use your real email address.", result)
self.assert_in_response("Enter a valid email address.", result)
result = self.submit_realm_creation_form(
email="foo\x00bar", realm_subdomain="custom-test", realm_name="Zulip test"
)
self.assert_in_response("Please use your real email address.", result)
self.assert_in_response("Null characters are not allowed.", result)
self.assert_in_response("Enter a valid email address.", result)
@override_settings(OPEN_REALM_CREATION=True)
def test_mailinator_signup(self) -> None:

View File

@@ -5,7 +5,7 @@ class IFTTTHookTests(WebhookTestCase):
CHANNEL_NAME = "ifttt"
URL_TEMPLATE = "/api/v1/external/ifttt?stream={stream}&api_key={api_key}"
WEBHOOK_DIR_NAME = "ifttt"
VIEW_FUNCTION_NAME = "api_iftt_app_webhook"
VIEW_FUNCTION_NAME = "api_ifttt_webhook"
def test_ifttt_when_subject_and_body_are_correct(self) -> None:
expected_topic_name = "Email sent from email@email.com"

View File

@@ -13,7 +13,7 @@ from zerver.models import UserProfile
@webhook_view("IFTTT")
@typed_endpoint
def api_iftt_app_webhook(
def api_ifttt_webhook(
request: HttpRequest,
user_profile: UserProfile,
*,