diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dd4783597..a9819a6c79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,34 @@
All notable changes to this project will be documented in this file.
+## [4.3.8] - 2025-05-06
+
+### Security
+
+- Update dependencies
+- Check scheme on account, profile, and media URLs ([GHSA-x2rc-v5wx-g3m5](https://github.com/mastodon/mastodon/security/advisories/GHSA-x2rc-v5wx-g3m5))
+
+### Added
+
+- Add warning for REDIS_NAMESPACE deprecation at startup (#34581 by @ClearlyClaire)
+- Add built-in context for interaction policies (#34574 by @ClearlyClaire)
+
+### Changed
+
+- Change activity distribution error handling to skip retrying for deleted accounts (#33617 by @ClearlyClaire)
+
+### Removed
+
+- Remove double-query for signed query strings (#34610 by @ClearlyClaire)
+
+### Fixed
+
+- Fix incorrect redirect in response to unauthenticated API requests in limited federation mode (#34549 by @ClearlyClaire)
+- Fix sign-up e-mail confirmation page reloading on error or redirect (#34548 by @ClearlyClaire)
+
## [4.3.7] - 2025-04-02
-### Add
+### Added
- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)
diff --git a/Gemfile b/Gemfile
index b55ec1d730..44c4c9a54d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -212,7 +212,7 @@ group :development, :test do
gem 'test-prof', require: false
# RSpec runner for rails
- gem 'rspec-rails', '~> 7.0'
+ gem 'rspec-rails', '~> 8.0'
end
group :production do
diff --git a/Gemfile.lock b/Gemfile.lock
index e9cdfa2e7d..1df8c3ed3d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -435,7 +435,7 @@ GEM
mutex_m (0.3.0)
net-http (0.6.0)
uri
- net-imap (0.5.6)
+ net-imap (0.5.8)
date
net-protocol
net-ldap (0.19.0)
@@ -620,7 +620,7 @@ GEM
psych (5.2.3)
date
stringio
- public_suffix (6.0.1)
+ public_suffix (6.0.2)
puma (6.6.0)
nio4r (~> 2.0)
pundit (2.5.0)
@@ -721,18 +721,18 @@ GEM
rspec-mocks (~> 3.13.0)
rspec-core (3.13.3)
rspec-support (~> 3.13.0)
- rspec-expectations (3.13.3)
+ rspec-expectations (3.13.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-github (3.0.0)
rspec-core (~> 3.0)
- rspec-mocks (3.13.2)
+ rspec-mocks (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-rails (7.1.1)
- actionpack (>= 7.0)
- activesupport (>= 7.0)
- railties (>= 7.0)
+ rspec-rails (8.0.0)
+ actionpack (>= 7.2)
+ activesupport (>= 7.2)
+ railties (>= 7.2)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
@@ -742,8 +742,8 @@ GEM
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
- rspec-support (3.13.2)
- rubocop (1.75.4)
+ rspec-support (3.13.3)
+ rubocop (1.75.5)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -800,7 +800,7 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.4.1)
- selenium-webdriver (4.31.0)
+ selenium-webdriver (4.32.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -842,7 +842,7 @@ GEM
base64
stoplight (4.1.1)
redlock (~> 1.0)
- stringio (3.1.6)
+ stringio (3.1.7)
strong_migrations (2.3.0)
activerecord (>= 7)
swd (2.0.3)
@@ -1045,7 +1045,7 @@ DEPENDENCIES
redis-namespace (~> 1.10)
rqrcode (~> 3.0)
rspec-github (~> 3.0)
- rspec-rails (~> 7.0)
+ rspec-rails (~> 8.0)
rspec-sidekiq (~> 5.0)
rubocop
rubocop-capybara
diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/json_ld_helper.rb
index 693cdf730f..7b7680661f 100644
--- a/app/helpers/json_ld_helper.rb
+++ b/app/helpers/json_ld_helper.rb
@@ -80,6 +80,18 @@ module JsonLdHelper
!haystack.casecmp(needle).zero?
end
+ def safe_prefetched_embed(account, object, context)
+ return unless object.is_a?(Hash)
+
+ # NOTE: Replacing the object's context by that of the parent activity is
+ # not sound, but it's consistent with the rest of the codebase
+ object = object.merge({ '@context' => context })
+
+ return if value_or_id(first_of_value(object['attributedTo'])) != account.uri || non_matching_uri_hosts?(account.uri, object['id'])
+
+ object
+ end
+
def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize)
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 93fc3b9fef..24dffe3121 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -89,6 +89,17 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
+
+ if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
+ normalStatus.url = null;
+ }
+
+ normalStatus.url ||= normalStatus.uri;
+
+ normalStatus.media_attachments.forEach(item => {
+ if (item.remote_url && !(item.remote_url.startsWith('http://') || item.remote_url.startsWith('https://')))
+ item.remote_url = null;
+ });
}
if (normalOldStatus) {
diff --git a/app/javascript/mastodon/blurhash.ts b/app/javascript/mastodon/blurhash.ts
index cafe7b12dc..a1d1a0f4e1 100644
--- a/app/javascript/mastodon/blurhash.ts
+++ b/app/javascript/mastodon/blurhash.ts
@@ -96,13 +96,19 @@ export const decode83 = (str: string) => {
return value;
};
-export const intToRGB = (int: number) => ({
+export interface RGB {
+ r: number;
+ g: number;
+ b: number;
+}
+
+export const intToRGB = (int: number): RGB => ({
r: Math.max(0, int >> 16),
g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, int & 255),
});
-export const getAverageFromBlurhash = (blurhash: string) => {
+export const getAverageFromBlurhash = (blurhash: string | null) => {
if (!blurhash) {
return null;
}
diff --git a/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx b/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx
index 351f1c949e..3c33688b0c 100644
--- a/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx
+++ b/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx
@@ -105,7 +105,7 @@ class ReportReasonSelector extends PureComponent {
};
componentDidMount() {
- api(false).get('/api/v1/instance').then(res => {
+ api(false).get('/api/v2/instance').then(res => {
this.setState({
rules: res.data.rules,
});
diff --git a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx
deleted file mode 100644
index 50f91a9275..0000000000
--- a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { FormattedMessage } from 'react-intl';
-
-import { connect } from 'react-redux';
-
-import CancelPresentationIcon from '@/material-icons/400-24px/cancel_presentation.svg?react';
-import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
-import { Icon } from 'mastodon/components/icon';
-
-class PictureInPicturePlaceholder extends PureComponent {
-
- static propTypes = {
- dispatch: PropTypes.func.isRequired,
- aspectRatio: PropTypes.string,
- };
-
- handleClick = () => {
- const { dispatch } = this.props;
- dispatch(removePictureInPicture());
- };
-
- render () {
- const { aspectRatio } = this.props;
-
- return (
-
-
-
-
- );
- }
-
-}
-
-export default connect()(PictureInPicturePlaceholder);
diff --git a/app/javascript/mastodon/components/picture_in_picture_placeholder.tsx b/app/javascript/mastodon/components/picture_in_picture_placeholder.tsx
new file mode 100644
index 0000000000..829ab5febd
--- /dev/null
+++ b/app/javascript/mastodon/components/picture_in_picture_placeholder.tsx
@@ -0,0 +1,46 @@
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import PipExitIcon from '@/material-icons/400-24px/pip_exit.svg?react';
+import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
+import { Icon } from 'mastodon/components/icon';
+import { useAppDispatch } from 'mastodon/store';
+
+export const PictureInPicturePlaceholder: React.FC<{ aspectRatio: string }> = ({
+ aspectRatio,
+}) => {
+ const dispatch = useAppDispatch();
+
+ const handleClick = useCallback(() => {
+ dispatch(removePictureInPicture());
+ }, [dispatch]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ e.stopPropagation();
+ handleClick();
+ }
+ },
+ [handleClick],
+ );
+
+ return (
+
+
+
+
+ );
+};
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index c778666ceb..130aa83090 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -21,7 +21,7 @@ import AttachmentList from 'mastodon/components/attachment_list';
import { ContentWarning } from 'mastodon/components/content_warning';
import { FilterWarning } from 'mastodon/components/filter_warning';
import { Icon } from 'mastodon/components/icon';
-import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
+import { PictureInPicturePlaceholder } from 'mastodon/components/picture_in_picture_placeholder';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router';
import Card from '../features/status/components/card';
@@ -507,9 +507,6 @@ class Status extends ImmutablePureComponent {
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
- width={this.props.cachedMediaWidth}
- height={110}
- cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')}
diff --git a/app/javascript/mastodon/containers/media_container.jsx b/app/javascript/mastodon/containers/media_container.jsx
index 9c07341faa..e826dbfa96 100644
--- a/app/javascript/mastodon/containers/media_container.jsx
+++ b/app/javascript/mastodon/containers/media_container.jsx
@@ -8,7 +8,7 @@ import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import MediaGallery from 'mastodon/components/media_gallery';
import ModalRoot from 'mastodon/components/modal_root';
import { Poll } from 'mastodon/components/poll';
-import Audio from 'mastodon/features/audio';
+import { Audio } from 'mastodon/features/audio';
import Card from 'mastodon/features/status/components/card';
import MediaModal from 'mastodon/features/ui/components/media_modal';
import { Video } from 'mastodon/features/video';
diff --git a/app/javascript/mastodon/features/alt_text_modal/index.tsx b/app/javascript/mastodon/features/alt_text_modal/index.tsx
index e2d05a99ca..08e4a8917c 100644
--- a/app/javascript/mastodon/features/alt_text_modal/index.tsx
+++ b/app/javascript/mastodon/features/alt_text_modal/index.tsx
@@ -27,7 +27,7 @@ import { Button } from 'mastodon/components/button';
import { GIFV } from 'mastodon/components/gifv';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { Skeleton } from 'mastodon/components/skeleton';
-import Audio from 'mastodon/features/audio';
+import { Audio } from 'mastodon/features/audio';
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import { Video, getPointerPosition } from 'mastodon/features/video';
@@ -212,11 +212,11 @@ const Preview: React.FC<{
return (