Compare commits

...

15 Commits

Author SHA1 Message Date
Alex Vandiver
c6a60fd533 exceptions: Add link to rate-limiting docs in 429 response. 2025-10-27 16:22:13 -07:00
Alex Vandiver
5c2b0d91d5 rate_limit: Additionally limit to 2000 authenticated requests per hour. 2025-10-27 16:22:13 -07:00
apoorvapendse
421ba8afcf paste: Prevent insertion of extraneous newlines in Firefox.
Firefox preserves the newlines when copying a visually
line-wrapped paragraph that originally contains zero
"intentional" newlines.

This fix removes those newlines from the text content before
pasting into the compose box.

This probably won't cause removall of intentional newlines,
as they are represented with <br/> tags in the `paste_html`.

Original report: https://rust-lang.zulipchat.com/#narrow/channel/122653-zulip/topic/Copy-paste.20includes.20hard.20newlines.20when.20message.20didn't/with/544174740

Discussion: https://chat.zulip.org/#narrow/channel/9-issues/topic/extraneous.20newlines.20when.20pasting.20in.20Firefox/with/2275319

Signed-off-by: apoorvapendse <apoorvavpendse@gmail.com>

Co-authored-by: Alex Vandiver <alexmv@zulip.com>
2025-10-27 16:19:22 -07:00
Alex Vandiver
3ca5a49557 i18n: Properly handle when a locale is removed from Weblate. 2025-10-27 09:05:06 -07:00
Sahil Batra
c31a23d589 user-profile: Fix textarea being too small in "Manage user" modal.
This commit fixes textarea width being too small for "Long text"
custom profile fields in "Manage user" modal. This commit removes
the CSS which set the incorrect width and also restricts the
maximum width such that it does not overflows on narrow screens.
2025-10-24 07:15:26 -07:00
Sahil Batra
2ea21b00f7 modals: Restrict dropdown widget button widths to visible space.
This commit adds CSS to make sure dropdowns widgets remain inside
the visible area of the modals and are not cut off in narrow width
screens.
2025-10-24 07:15:26 -07:00
Sahil Batra
ad19c16dca modals: Restrict dropdown and input widths in narrow screens.
This commit adds CSS to make sure dropdowns and inputs, including
date type and pill inputs, remain inside the visible area of the
modals and are not cut off in narrow width screens.

Apart from setting max-width, this commit also changes the grid
CSS for datepicker input so the layout is arranged in such a way
that the "x" button is inside the input element and we can set
the max-width in a simpler way.
2025-10-24 07:15:26 -07:00
Sahil Batra
cce328a38f custom-profile-fields: Fix class name in datepicker input.
We previously added "settings_text_input" class to the datepickr
input in "Manage user" form as well, while other inputs have
"modal_text_input" class. This commit fixes that.
2025-10-24 07:15:26 -07:00
Anders Kaseorg
1714bfa173 requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-10-23 15:52:47 -07:00
Anders Kaseorg
69f2e95e49 install-uv: Upgrade uv from 0.8.22 to 0.9.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-10-23 15:52:47 -07:00
Anders Kaseorg
7e29b35fa0 saml: Update RelayState format.
The format was changed in social-core 4.5.0-5-gb6317968 and the old
format is removed in 4.7.0-49-g5d98d92a.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-10-23 15:52:47 -07:00
Alex Vandiver
c17564ac27 puppet: Include Zulip version and external host in camo User-Agent. 2025-10-23 15:40:21 -07:00
Alex Vandiver
5319b767a1 puppet: Add a get_django_setting_slow function.
The `_slow` is a hint that this function is not for general use.
2025-10-23 15:40:21 -07:00
Alex Vandiver
3f2aca5481 puppet: Add a zulip_version fact.
We use the version without the exact commit-id because Puppet likely
does not want to have a file that updates on literally every deploy.
2025-10-23 15:40:21 -07:00
Alex Vandiver
efa28c3a65 version: Add a version which does not change on every commit. 2025-10-23 15:40:21 -07:00
19 changed files with 1804 additions and 1435 deletions

View File

@@ -70,7 +70,8 @@ HTTP headers in all API responses:
and can vary by server and over time. The default configuration
currently limits:
* Every user is limited to 200 total API requests per minute.
* Every user is limited to 200 total API requests per minute, and 2000
total API requests per hour.
* Separate, much lower limits for authentication/login attempts.
When the Zulip server has configured multiple rate limits that apply

View File

@@ -0,0 +1,13 @@
Facter.add(:zulip_version) do
setcode do
Dir.chdir("/home/zulip/deployments/current") do
output = `python3 -c 'import version; print(version.ZULIP_VERSION_WITHOUT_COMMIT' 2>&1`
if not $?.success?
Facter.debug("zulip_version error: #{output}")
nil
else
output.strip
end
end
end
end

View File

@@ -0,0 +1,22 @@
require "shellwords"
# Note that this is very slow (~350ms) and may get values which will
# rapidly go out of date, since settings are changed much more
# frequently than deploys -- in addition to potentially just not
# working if we're not on the application server. We should generally
# avoid using this if at all possible.
Puppet::Functions.create_function(:get_django_setting_slow) do
def get_django_setting_slow(name)
if File.exist?("/etc/zulip/settings.py")
output = `/home/zulip/deployments/current/scripts/get-django-setting #{name.shellescape} 2>&1`
if $?.success?
output.strip
else
nil
end
else
nil
end
end
end

View File

@@ -35,6 +35,8 @@ class zulip::camo (String $listen_address = '0.0.0.0') {
$proxy = ''
}
$zulip_version = $facts['zulip_version']
$external_uri = pick(get_django_setting_slow('ROOT_DOMAIN_URI'), 'https://zulip.com')
file { "${zulip::common::supervisor_conf_dir}/go-camo.conf":
ensure => file,
require => [

View File

@@ -1,5 +1,5 @@
[program:go-camo]
command=/usr/local/bin/secret-env-wrapper GOCAMO_HMAC=camo_key <%= @bin %> --listen=<%= @listen_address %>:9292 -H "Strict-Transport-Security: max-age=15768000" -H "X-Frame-Options: DENY" --metrics --verbose --allow-content-video
command=/usr/local/bin/secret-env-wrapper GOCAMO_HMAC=camo_key <%= @bin %> --listen=<%= @listen_address %>:9292 -H "Strict-Transport-Security: max-age=15768000" -H "X-Frame-Options: DENY" --metrics --verbose --allow-content-video --user-agent "Zulip-Server/<%= @zulip_version %> (<%= @external_uri %>/) go-camo/<%= @version %>" --server-name "go-camo/<%= @version %>"
environment=HTTP_PROXY="<%= @proxy %>",HTTPS_PROXY="<%= @proxy %>"
priority=15
autostart=true

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env bash
set -eu
version=0.8.22
version=0.9.5
arch="$(uname -m)"
tarball="uv-$arch-unknown-linux-gnu.tar.gz"
declare -A sha256=(
[aarch64]=726b72a137fda33565143325f7d31c42cd30ff9ccdf067e00d124d37b4081cb2
[x86_64]=741ff1f5742c5a4a25d2f829e8395355e43f7a5ae2ebc6368e9ae2df0efb69cf
[aarch64]=9db0c2f6683099f86bfeea47f4134e915f382512278de95b2a0e625957594ff3
[x86_64]=2cf10babba653310606f8b49876cfb679928669e7ddaa1fb41fb00ce73e64f66
)
check_version() {

View File

@@ -26,6 +26,11 @@ if git rev-parse --verify --quiet "origin/$local_branch" >/dev/null; then
fi
git checkout -b "$local_branch" "upstream/$branch"
# Clear out local `.mo` files which cause locale/*/LC_MESSAGES/
# directories to not be empty when their .po files vanish, so git
# doesn't remove the directory.
rm locale/*/LC_MESSAGES/*.mo
wlc lock "zulip/frontend$suffix"
wlc lock "zulip/django$suffix"
trap 'wlc unlock "zulip/frontend$suffix" && wlc unlock "zulip/django$suffix"' EXIT
@@ -59,6 +64,10 @@ else
# Update locale/*/legacy_stream_translations.json
./tools/i18n/update-for-legacy-translations
# Trim out any now-empty locale directories
find locale/ -type d -empty -delete
git add locale/
# Double-check that they all compile

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import glob
import os
import re
from subprocess import check_output
@@ -239,11 +240,19 @@ def update_for_legacy_stream_translations(
print(f"Updated {number_of_updates} strings in: {path}")
expected_legacy_filenames = set()
for locale in get_locales():
current = get_json_filename(locale)
legacy = get_legacy_filename(locale)
expected_legacy_filenames.add(legacy)
if os.path.exists(current) and os.path.exists(legacy):
print(f"Checking legacy translations for: {current}")
current_translations = get_translations(current)
legacy_translations = get_translations(legacy)
update_for_legacy_stream_translations(current_translations, legacy_translations, current)
for extra_file in (
set(glob.glob("locale/*/legacy_stream_translations.json")) - expected_legacy_filenames
):
print(f"Removing dangling legacy translation file {extra_file}")
os.unlink(extra_file)

3087
uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import os
ZULIP_VERSION = "12.0-dev+git"
# Add information on number of commits and commit hash to version, if available
ZULIP_VERSION_WITHOUT_COMMIT = ZULIP_VERSION
zulip_git_version_file = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "zulip-git-version"
)
@@ -49,4 +50,4 @@ API_FEATURE_LEVEL = 427
# historical commits sharing the same major version, in which case a
# minor version bump suffices.
PROVISION_VERSION = (353, 0) # bumped 2025-10-07 to rebuild emoji_names
PROVISION_VERSION = (354, 0) # bumped 2025-10-23 to upgrade Python requirements

View File

@@ -232,7 +232,12 @@ export function paste_handler_converter(
// that such a text node exists in `has_single_textful_child_node`.
const text_content = get_the_only_textful_child_content([...copied_html_fragment.children]);
if (text_content) {
return text_content;
// Firefox preserves the wrapped newline characters in the textContent of <p>
// tags in `paste_html`, even when the original content does not contain newlines.
// This results in unexpected bad wrapping of paragraphs when copy-pasting
// text back into the compose box.
// This replace logic is used to handle that edge case.
return text_content.replaceAll(/\s*\n\s*/g, " ");
}
// Ideally, this should never happen.
// Just for fallback in case it does.

View File

@@ -277,12 +277,17 @@ export function initialize_custom_date_type_fields(
}
}
let common_class_name = "modal_text_input";
if (for_profile_settings_panel) {
common_class_name = "settings_text_input";
}
flatpickr($date_picker_elements, {
altInput: true,
// We would need to handle the altInput separately
// than ".custom_user_field_value" elements to handle
// invalid values typed in the input.
altInputClass: "date-field-alt-input settings_text_input",
altInputClass: "date-field-alt-input " + common_class_name,
altFormat: "F j, Y",
allowInput: true,
static: true,

View File

@@ -429,6 +429,7 @@
border: 1px solid hsl(0deg 0% 80%);
cursor: pointer;
background-color: hsl(0deg 0% 100%);
max-width: 100%;
&:disabled {
cursor: not-allowed;
@@ -456,6 +457,14 @@
}
}
.modal__body,
.modal__content {
.dropdown-widget-button,
.dropdown_widget_with_label_wrapper {
max-width: 100%;
}
}
.modal_password_input,
.modal_url_input,
.modal_text_input {
@@ -470,6 +479,7 @@
margin-bottom: 10px;
/* subtract padding (6px each side) and border (1px each side) */
width: calc(var(--modal-input-width) - 14px);
max-width: calc(100% - 14px);
&:focus {
border-color: hsl(206deg 80% 62% / 80%);

View File

@@ -607,6 +607,8 @@ ul.popover-group-menu-member-list {
/* Override default modal input width, since that overflows.
This is 185px (the default "width: unset" width) at 14px/em */
width: 13.2142em;
/* subtract padding (6px on left and 28px on right) and border (1px each side) */
max-width: calc(100% - 36px);
}
.group-search:placeholder-shown + #clear_groups_search,
@@ -905,6 +907,9 @@ ul.popover-group-menu-member-list {
box-sizing: border-box;
width: 100%;
height: auto;
/* Unset max-width set using modal_text_input selector as box-sizing
is set to border-box here and we already set width to 100%. */
max-width: unset;
&.empty-topic-display::placeholder {
color: inherit;

View File

@@ -813,6 +813,11 @@ input[type="checkbox"] {
grid-template-columns: minmax(0, 1fr) 1.4285em; /* 20px at 14px em */
align-items: center;
width: var(--modal-input-width);
.flatpickr-wrapper {
grid-column: datepicker-start / close-button-end;
grid-row: close-button;
}
}
.control-label-disabled {
@@ -1713,12 +1718,14 @@ label.preferences-radio-choice-label {
#edit-user-form {
.person_picker {
/* Subtract (1px border and 2px of padding) on each side */
min-width: calc(var(--modal-input-width) - 6px);
width: calc(var(--modal-input-width) - 6px);
max-width: calc(100% - 6px);
}
& textarea {
/* Subtract (1px border and 6px padding) on each side */
width: calc(var(--modal-input-width) - 14px);
max-width: calc(100% - 14px);
}
}
@@ -2109,12 +2116,6 @@ label.preferences-radio-choice-label {
margin-top: 5px;
}
#edit-user-form {
.custom_user_field textarea {
width: calc(100% - 25px);
}
}
.topic_date_updated {
display: none;
}

View File

@@ -279,6 +279,12 @@ run_test("paste_handler_converter", () => {
'<meta http-equiv="content-type" content="text/html; charset=utf-8"><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><span style="font-size:10pt;font-family:Arial;font-style:normal;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:123}" data-sheets-userformat="{&quot;2&quot;:769,&quot;3&quot;:{&quot;1&quot;:0},&quot;11&quot;:3,&quot;12&quot;:0}">123</span>';
assert.equal(compose_paste.paste_handler_converter(input), "123");
// Pasting a long, visually line-wrapped single-line message from Firefox should not insert extraneous newlines.
input = `<html><body>\n<!--StartFragment--><div class="message_content rendered_markdown">\n<p>At some point recently, Zulip changed such that copying a \nlong message includes hard newlines, rather than putting things all on \none line when they were on one line in the original message.</p>\n</div><!--EndFragment-->\n</body>\n</html>`;
assert.equal(
compose_paste.paste_handler_converter(input),
"At some point recently, Zulip changed such that copying a long message includes hard newlines, rather than putting things all on one line when they were on one line in the original message.",
);
// Pasting from Excel
input = `<html xmlns:v="urn:schemas-microsoft-com:vml"\nxmlns:o="urn:schemas-microsoft-com:office:office"\nxmlns:x="urn:schemas-microsoft-com:office:excel"\nxmlns="http://www.w3.org/TR/REC-html40">\n<head>\n<meta http-equiv=Content-Type content="text/html; charset=utf-8">\n<meta name=ProgId content=Excel.Sheet>\n<meta name=Generator content="Microsoft Excel 15">\n<link id=Main-File rel=Main-File\nhref="file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip.htm">\n<link rel=File-List\nhref="file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_filelist.xml">\n<style>\n<!--table\n {mso-displayed-decimal-separator:"\\.";\n mso-displayed-thousand-separator:"\\,";}\n@page\n {margin:.75in .7in .75in .7in;\n mso-header-margin:.3in;\n mso-footer-margin:.3in;}\ntr\n {mso-height-source:auto;}\ncol\n {mso-width-source:auto;}\nbr\n {mso-data-placement:same-cell;}\ntd\n {padding-top:1px;\n padding-right:1px;\n padding-left:1px;\n mso-ignore:padding;\n color:black;\n font-size:11.0pt;\n font-weight:400;\n font-style:normal;\n text-decoration:none;\n font-family:Calibri, sans-serif;\n mso-font-charset:0;\n mso-number-format:General;\n text-align:general;\n vertical-align:bottom;\n border:none;\n mso-background-source:auto;\n mso-pattern:auto;\n mso-protection:locked visible;\n white-space:nowrap;\n mso-rotate:0;}\n.xl65\n {mso-number-format:"_\\(\\0022$\\0022* \\#\\,\\#\\#0\\.00_\\)\\;_\\(\\0022$\\0022* \\\\\\(\\#\\,\\#\\#0\\.00\\\\\\)\\;_\\(\\0022$\\0022* \\0022-\\0022??_\\)\\;_\\(\\@_\\)";}\n-->\n</style>\n</head>\n<body link="#0563C1" vlink="#954F72">\n<table border=0 cellpadding=0 cellspacing=0 width=88 style='border-collapse:\n collapse;width:66pt'>\n<!--StartFragment-->\n <col width=88 style='mso-width-source:userset;mso-width-alt:3218;width:66pt'>\n <tr height=20 style='height:15.0pt'>\n <td height=20 class=xl65 width=88 style='height:15.0pt;width:66pt;font-size:\n 11.0pt;color:black;font-weight:400;text-decoration:none;text-underline-style:\n none;text-line-through:none;font-family:Calibri, sans-serif;border-top:.5pt solid #5B9BD5;\n border-right:none;border-bottom:none;border-left:none'><span\n style='mso-spacerun:yes'> </span>$<span style='mso-spacerun:yes'>\n </span>20.00 </td>\n </tr>\n <tr height=20 style='height:15.0pt'>\n <td height=20 class=xl65 style='height:15.0pt;font-size:11.0pt;color:black;\n font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:\n none;font-family:Calibri, sans-serif;border-top:.5pt solid #5B9BD5;\n border-right:none;border-bottom:none;border-left:none'><span\n style='mso-spacerun:yes'> </span>$<span\n style='mso-spacerun:yes'> </span>7.00 </td>\n </tr>\n<!--EndFragment-->\n</table>\n</body>\n</html>`;

View File

@@ -268,7 +268,9 @@ class RateLimitedError(JsonableError):
@staticmethod
@override
def msg_format() -> str:
return _("API usage exceeded rate limit")
return _(
"API usage exceeded rate limit; see https://zulip.com/api/http-headers#rate-limiting-response-headers"
)
@property
@override

View File

@@ -2861,17 +2861,17 @@ class ZulipSAMLIdentityProvider(SAMLIdentityProvider):
result = super().get_user_details(attributes)
extra_attr_names = self.conf.get("extra_attrs", [])
result["extra_attrs"] = {}
extra_attrs = {}
if (groups_list := attributes.get("zulip_groups")) is not None:
result["extra_attrs"]["zulip_groups"] = groups_list
extra_attrs["zulip_groups"] = groups_list
for extra_attr_name in extra_attr_names:
result["extra_attrs"][extra_attr_name] = self.get_attr(
attributes=attributes, conf_key=None, default_attribute=extra_attr_name
extra_attrs[extra_attr_name] = self.get_attr(
attributes=attributes, conf_key="<extra>", default_attributes=(extra_attr_name,)
)
return result
return {**result, "extra_attrs": extra_attrs}
class SAMLDocument:
@@ -3062,11 +3062,18 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
super().__init__(*args, **kwargs)
@override
def get_idp(self, idp_name: str) -> ZulipSAMLIdentityProvider:
"""Given the name of an IdP, get a SAMLIdentityProvider instance.
def get_idp(self, idp_name: str | None) -> ZulipSAMLIdentityProvider:
"""Given the name of an IdP, get a SAMLIdentityProvider instance
Forked to use our subclass of SAMLIdentityProvider for more flexibility."""
idp_config = self.setting("ENABLED_IDPS")[idp_name]
return ZulipSAMLIdentityProvider(idp_name, **idp_config)
enabled_idps: dict[str, dict[str, str]] = self.setting("ENABLED_IDPS")
if idp_name is None: # nocoverage
# RelayState was missing, perhaps an IdP initiated flow
if len(enabled_idps) != 1:
raise AuthMissingParameter(self, "RelayState.idp")
# Use the only configured IDP
idp_name = next(iter(enabled_idps))
idp_config = enabled_idps[idp_name]
return ZulipSAMLIdentityProvider(self, idp_name, **idp_config)
@override
def auth_url(self) -> str:
@@ -3374,7 +3381,7 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
# super().auth_complete expects to have RelayState set to the idp_name,
# so we need to replace this param.
post_params = self.strategy.request.POST.copy()
post_params["RelayState"] = idp_name
post_params["RelayState"] = orjson.dumps({"idp": idp_name}).decode()
self.strategy.request.POST = post_params
# Call the auth_complete method of SocialAuthMixIn

View File

@@ -266,6 +266,8 @@ DEFAULT_RATE_LIMITING_RULES = {
"api_by_user": [
# 200 requests per minute
(60, 200),
# 2000 requests per hour
(3600, 2000),
],
# Limits total number of unauthenticated API requests (primarily
# used by the public access option). Since these are