Compare commits

...

5 Commits

Author SHA1 Message Date
Claire
afcf779b94
Merge b21c226396 into fbe9728f36 2025-05-06 15:05:46 +00:00
Claire
fbe9728f36
Bump version to v4.3.8 (#34626)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-05-06 14:17:07 +00:00
Claire
3bbf3e9709
Fix code style issue (#34624) 2025-05-06 13:35:54 +00:00
Claire
79931bf3ae
Merge commit from fork
* Check scheme in account and post links

* Harden media attachments

* Client-side mitigation

* Client-side mitigation for media attachments
2025-05-06 15:02:13 +02:00
Claire
b21c226396 Change how the modal stack is handled 2025-01-21 15:13:37 +01:00
13 changed files with 79 additions and 72 deletions

View File

@ -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)

View File

@ -77,6 +77,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) {

View File

@ -9,7 +9,6 @@ export type ModalType = keyof typeof MODAL_COMPONENTS;
interface OpenModalPayload {
modalType: ModalType;
modalProps: ModalProps;
previousModalProps?: ModalProps;
}
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');

View File

@ -232,11 +232,6 @@ const Preview: React.FC<{
}
};
interface RestoreProps {
previousDescription: string;
previousPosition: FocalPoint;
}
interface Props {
mediaId: string;
onClose: () => void;
@ -245,15 +240,14 @@ interface Props {
interface ConfirmationMessage {
message: string;
confirm: string;
props?: RestoreProps;
}
export interface ModalRef {
getCloseConfirmationMessage: () => null | ConfirmationMessage;
}
export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
({ mediaId, previousDescription, previousPosition, onClose }, ref) => {
export const AltTextModal = forwardRef<ModalRef, Props>(
({ mediaId, onClose }, ref) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const media = useAppSelector((state) =>
@ -272,18 +266,15 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
const focusY =
(media?.getIn(['meta', 'focus', 'y'], 0) as number | undefined) ?? 0;
const [description, setDescription] = useState(
previousDescription ??
(media?.get('description') as string | undefined) ??
'',
);
const [position, setPosition] = useState<FocalPoint>(
previousPosition ?? [focusX / 2 + 0.5, focusY / -2 + 0.5],
[focusX / 2 + 0.5, focusY / -2 + 0.5],
);
const [isDetecting, setIsDetecting] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const dirtyRef = useRef(
previousDescription || previousPosition ? true : false,
);
const dirtyRef = useRef(false);
const type = media?.get('type') as string;
const valid = length(description) <= MAX_LENGTH;

View File

@ -3,6 +3,8 @@ import { PureComponent } from 'react';
import { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Base from 'mastodon/components/modal_root';
import { AltTextModal } from 'mastodon/features/alt_text_modal';
import {
@ -80,8 +82,7 @@ export const MODAL_COMPONENTS = {
export default class ModalRoot extends PureComponent {
static propTypes = {
type: PropTypes.string,
props: PropTypes.object,
modals: ImmutablePropTypes.list.isRequired,
onClose: PropTypes.func.isRequired,
ignoreFocus: PropTypes.bool,
};
@ -91,7 +92,7 @@ export default class ModalRoot extends PureComponent {
};
getSnapshotBeforeUpdate () {
return { visible: !!this.props.type };
return { visible: !!this.props.modals.get([0, 'type']) };
}
componentDidUpdate (prevProps, prevState, { visible }) {
@ -131,25 +132,19 @@ export default class ModalRoot extends PureComponent {
};
render () {
const { type, props, ignoreFocus } = this.props;
const { modals, ignoreFocus } = this.props;
const { backgroundColor } = this.state;
const visible = !!type;
const visible = !modals.isEmpty();
return (
<Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}>
{visible && (
<>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => {
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />;
}}
</BundleContainer>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</>
)}
{visible && modals.toArray().map((modal, index) => (
<BundleContainer key={modal.modalKey} fetchComponent={MODAL_COMPONENTS[modal.modalType]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => {
return <SpecificComponent {...modal.modalProps} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={index === 0 ? this.setModalRef : null} />;
}}
</BundleContainer>
))}
</Base>
);
}

View File

@ -1,14 +1,12 @@
import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux';
import { openModal, closeModal } from '../../../actions/modal';
import ModalRoot from '../components/modal_root';
const defaultProps = {};
const mapStateToProps = state => ({
ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
props: state.getIn(['modal', 'stack', 0, 'modalProps'], defaultProps),
modals: state.getIn(['modal', 'stack'], ImmutableList()),
});
const mapDispatchToProps = dispatch => ({
@ -16,7 +14,6 @@ const mapDispatchToProps = dispatch => ({
if (confirmationMessage) {
dispatch(
openModal({
previousModalProps: confirmationMessage.props,
modalType: 'CONFIRM',
modalProps: {
message: confirmationMessage.message,

View File

@ -144,5 +144,10 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
),
note_emojified: emojify(accountJSON.note, emojiMap),
note_plain: unescapeHTML(accountJSON.note),
url:
accountJSON.url.startsWith('http://') ||
accountJSON.url.startsWith('https://')
? accountJSON.url
: accountJSON.uri,
});
}

View File

@ -5,16 +5,19 @@ import { timelineDelete } from 'mastodon/actions/timelines_typed';
import type { ModalType } from '../actions/modal';
import { openModal, closeModal } from '../actions/modal';
import { uuid } from '../uuid';
export type ModalProps = Record<string, unknown>;
interface Modal {
modalType: ModalType;
modalProps: ModalProps;
modalKey: string;
}
const Modal = ImmutableRecord<Modal>({
modalType: 'ACTIONS',
modalProps: ImmutableRecord({})(),
modalKey: '',
});
interface ModalState {
@ -52,35 +55,11 @@ const pushModal = (
state: State,
modalType: ModalType,
modalProps: ModalProps,
previousModalProps?: ModalProps,
): State => {
return state.withMutations((record) => {
record.set('ignoreFocus', false);
record.update('stack', (stack) => {
let tmp = stack;
// With this option, we update the previously opened modal, so that when the
// current (new) modal is closed, the previous modal is re-opened with different
// props. Specifically, this is useful for the confirmation modal.
if (previousModalProps) {
const previousModal = tmp.first() as Modal | undefined;
if (previousModal) {
tmp = tmp.shift().unshift(
Modal({
modalType: previousModal.modalType,
modalProps: {
...previousModal.modalProps,
...previousModalProps,
},
}),
);
}
}
tmp = tmp.unshift(Modal({ modalType, modalProps }));
return tmp;
return stack.unshift(Modal({ modalType, modalProps, modalKey: uuid() }));
});
});
};
@ -91,7 +70,6 @@ export const modalReducer: Reducer<State> = (state = initialState, action) => {
state,
action.payload.modalType,
action.payload.modalProps,
action.payload.previousModalProps,
);
else if (closeModal.match(action)) return popModal(state, action.payload);
// TODO: type those actions

View File

@ -15,13 +15,15 @@ class ActivityPub::Parser::MediaAttachmentParser
end
def remote_url
Addressable::URI.parse(@json['url'])&.normalize&.to_s
url = Addressable::URI.parse(@json['url'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError
nil
end
def thumbnail_remote_url
Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
url = Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError
nil
end

View File

@ -29,7 +29,10 @@ class ActivityPub::Parser::StatusParser
end
def url
url_to_href(@object['url'], 'text/html') if @object['url'].present?
return if @object['url'].blank?
url = url_to_href(@object['url'], 'text/html')
url unless unsupported_uri_scheme?(url)
end
def text

View File

@ -4,6 +4,7 @@ require 'singleton'
class ActivityPub::TagManager
include Singleton
include JsonLdHelper
include RoutingHelper
CONTEXT = 'https://www.w3.org/ns/activitystreams'
@ -17,7 +18,7 @@ class ActivityPub::TagManager
end
def url_for(target)
return target.url if target.respond_to?(:local?) && !target.local?
return unsupported_uri_scheme?(target.url) ? nil : target.url if target.respond_to?(:local?) && !target.local?
return unless target.respond_to?(:object_type)

View File

@ -59,7 +59,7 @@ services:
web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/mastodon/mastodon:v4.3.7
image: ghcr.io/mastodon/mastodon:v4.3.8
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
@ -83,7 +83,7 @@ services:
# build:
# dockerfile: ./streaming/Dockerfile
# context: .
image: ghcr.io/mastodon/mastodon-streaming:v4.3.7
image: ghcr.io/mastodon/mastodon-streaming:v4.3.8
restart: always
env_file: .env.production
command: node ./streaming/index.js
@ -102,7 +102,7 @@ services:
sidekiq:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/mastodon/mastodon:v4.3.7
image: ghcr.io/mastodon/mastodon:v4.3.8
restart: always
env_file: .env.production
command: bundle exec sidekiq

View File

@ -17,7 +17,7 @@ module Mastodon
end
def default_prerelease
'alpha.4'
'alpha.5'
end
def prerelease