Compare commits

...

7 Commits

Author SHA1 Message Date
Matt Panaro
61302c9f37
Merge 7fb47a4866 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
Matt Panaro
7fb47a4866 refactor handleChange to extract context from URLs
previously, the search box would only add new quickActions if raw `@` or `#` characters were found in the search string; but in the case of a drag and drop, or a copy-paste, of a URL into the search bar, no elements of the URL's path would be recognized for potential quickActions.
This change enables identification of possible hashtags or user accounts in a (presumed) mastodon URL; augmenting id by special input characters.
2025-01-03 11:51:11 -05:00
Matt Panaro
4b28d99f16 refactor handleChange to include helper-object for quickActions
parameterize and move all the objects being pushed to newQuickActions (inside of handleChange) into a helper-object called QuickActionGenerators, so that handleChange isn't so many lines; it also separates the concerns a bit between parsing search input text and generating quickActions based on it
2025-01-03 11:51:11 -05:00
Matt Panaro
da6a2d3b51 introduce drag & drop handlers to search box
without modification, default browser behavior for dropping new text on top of pre-existing text in an input field is to insert the new text in between whatever characters the mouse is pointing to at time of drop.  Instead, by adding handlers, the pre-existing content can be overwritten/replaced.  The primary use-case is for accessing quoted toots, or referenced hashtags or accounts, in the feed by searching for the linked URLs & showing the original/expanded version of them in the search results (as opposed to clicking on the links themselves and having them open in a new tab, on a potentially different mastodon server)
Since the search box is designed to actively process its input for context-clues, after the text has been dropped, an Input event is triggered to bubble up from the search-box's new handleDrop method, that will cause the handleChange method to be invoked.  This strategy was adapted from https://stackoverflow.com/a/47409362
2025-01-03 11:51:11 -05:00
9 changed files with 218 additions and 95 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

@ -1,4 +1,4 @@
import { useCallback, useState, useRef } from 'react';
import { useCallback, useState, useRef, useMemo } from 'react';
import {
defineMessages,
@ -252,6 +252,103 @@ export const Search: React.FC<{
[dispatch, history],
);
const QuickActionGenerators: {
couldBeURL: (url: string) => SearchOption;
couldBeHashtag: (tag: string) => SearchOption;
couldBeUsername: (username: string) => SearchOption;
couldBeStatusSearch: (status: string) => SearchOption;
accountSearch: (account: string) => SearchOption;
} = useMemo(
() => ({
couldBeURL: (url: string): SearchOption => ({
key: 'open-url',
label: (
<FormattedMessage
id='search.quick_action.open_url'
defaultMessage='Open URL in Mastodon'
/>
),
action: () => {
dispatch(openURL({ url: url }))
.then((result) => {
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
);
}
}
return void 0;
})
.catch((e: unknown) => {
console.error(e);
})
.finally(() => {
unfocus();
});
},
}),
couldBeHashtag: (tag: string): SearchOption => ({
key: 'go-to-hashtag',
label: (
<FormattedMessage
id='search.quick_action.go_to_hashtag'
defaultMessage='Go to hashtag {x}'
values={{ x: <mark>#{tag}</mark> }}
/>
),
action: () => {
history.push(`/tags/${tag}`);
void dispatch(clickSearchResult({ q: tag, type: 'hashtag' }));
unfocus();
},
}),
couldBeUsername: (username: string): SearchOption => ({
key: 'go-to-account',
label: (
<FormattedMessage
id='search.quick_action.go_to_account'
defaultMessage='Go to profile {x}'
values={{ x: <mark>@{username}</mark> }}
/>
),
action: () => {
history.push(`/@${username}`);
void dispatch(clickSearchResult({ q: username, type: 'account' }));
unfocus();
},
}),
couldBeStatusSearch: (status: string): SearchOption => ({
key: 'status-search',
label: (
<FormattedMessage
id='search.quick_action.status_search'
defaultMessage='Posts matching {x}'
values={{ x: <mark>{status}</mark> }}
/>
),
action: () => {
submit(status, 'statuses');
},
}),
accountSearch: (account: string): SearchOption => ({
key: 'account-search',
label: (
<FormattedMessage
id='search.quick_action.account_search'
defaultMessage='Profiles matching {x}'
values={{ x: <mark>{account}</mark> }}
/>
),
action: () => {
submit(account, 'accounts');
},
}),
}),
[dispatch, history, submit],
);
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setValue(value);
@ -263,113 +360,63 @@ export const Search: React.FC<{
const couldBeURL =
trimmedValue.startsWith('https://') && !trimmedValue.includes(' ');
let mastoPath;
if (couldBeURL) {
newQuickActions.push({
key: 'open-url',
label: (
<FormattedMessage
id='search.quick_action.open_url'
defaultMessage='Open URL in Mastodon'
/>
),
action: async () => {
const result = await dispatch(openURL({ url: trimmedValue }));
newQuickActions.push(QuickActionGenerators.couldBeURL(trimmedValue));
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
);
}
}
unfocus();
},
});
// presume URL is from a mastodon server; not just some random web URL
mastoPath = new URL(trimmedValue).pathname.replace(/^\//, '');
} else {
mastoPath = '';
}
const couldBeHashtag =
(trimmedValue.startsWith('#') && trimmedValue.length > 1) ||
trimmedValue.match(HASHTAG_REGEX);
trimmedValue.match(HASHTAG_REGEX) ||
(couldBeURL && mastoPath.startsWith('tags/'));
if (couldBeHashtag) {
newQuickActions.push({
key: 'go-to-hashtag',
label: (
<FormattedMessage
id='search.quick_action.go_to_hashtag'
defaultMessage='Go to hashtag {x}'
values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }}
/>
),
action: () => {
const query = trimmedValue.replace(/^#/, '');
history.push(`/tags/${query}`);
void dispatch(clickSearchResult({ q: query, type: 'hashtag' }));
unfocus();
},
});
const hashtag = couldBeURL
? mastoPath.replace(/^tags\//, '')
: trimmedValue.replace(/^#/, '');
newQuickActions.push(QuickActionGenerators.couldBeHashtag(hashtag));
}
const couldBeUsername = /^@?[a-z0-9_-]+(@[^\s]+)?$/i.exec(trimmedValue);
const userRegexp = /^@?[a-z0-9_-]+(@[^\s]+)?$/i;
const couldBeUsername =
userRegexp.test(trimmedValue) ||
(couldBeURL && userRegexp.test(mastoPath));
if (couldBeUsername) {
newQuickActions.push({
key: 'go-to-account',
label: (
<FormattedMessage
id='search.quick_action.go_to_account'
defaultMessage='Go to profile {x}'
values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }}
/>
),
action: () => {
const query = trimmedValue.replace(/^@/, '');
history.push(`/@${query}`);
void dispatch(clickSearchResult({ q: query, type: 'account' }));
unfocus();
},
});
const mastoUser = mastoPath.replace(/^@/, '');
const username = !couldBeURL
? trimmedValue.replace(/^@/, '')
: // @ts-expect-error : mastoPath was tested against userRegexp above, meaning there must be at least one `@`:
// so match can't return null here
mastoPath.match(/@/g).length === 1
? // if there's only 1 `@` in mastoPath, obtain domain from URL's FQDN & append to mastoUser
`${mastoUser}@${new URL(trimmedValue).hostname}`
: // otherwise there were at least (hopefully, only) 2, and it's a full masto identifier
mastoUser;
newQuickActions.push(QuickActionGenerators.couldBeUsername(username));
}
const couldBeStatusSearch = searchEnabled;
if (couldBeStatusSearch && signedIn) {
newQuickActions.push({
key: 'status-search',
label: (
<FormattedMessage
id='search.quick_action.status_search'
defaultMessage='Posts matching {x}'
values={{ x: <mark>{trimmedValue}</mark> }}
/>
),
action: () => {
submit(trimmedValue, 'statuses');
},
});
newQuickActions.push(
QuickActionGenerators.couldBeStatusSearch(trimmedValue),
);
}
newQuickActions.push({
key: 'account-search',
label: (
<FormattedMessage
id='search.quick_action.account_search'
defaultMessage='Profiles matching {x}'
values={{ x: <mark>{trimmedValue}</mark> }}
/>
),
action: () => {
submit(trimmedValue, 'accounts');
},
});
newQuickActions.push(QuickActionGenerators.accountSearch(trimmedValue));
}
setQuickActions(newQuickActions);
},
[dispatch, history, signedIn, setValue, setQuickActions, submit],
[signedIn, setValue, setQuickActions, QuickActionGenerators],
);
const handleClear = useCallback(() => {
@ -451,6 +498,33 @@ export const Search: React.FC<{
setSelectedOption(-1);
}, [setExpanded, setSelectedOption]);
const handleDragOver = useCallback(
(event: React.DragEvent<HTMLInputElement>) => {
event.preventDefault();
},
[],
);
const handleDrop = useCallback(
(event: React.DragEvent<HTMLInputElement>) => {
event.preventDefault();
handleClear();
const query =
event.dataTransfer.getData('URL') ||
event.dataTransfer.getData('text/plain');
Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value',
)?.set?.call(event.target, query);
event.currentTarget.focus();
event.target.dispatchEvent(new Event('change', { bubbles: true }));
},
[handleClear],
);
return (
<form className={classNames('search', { active: expanded })}>
<input
@ -468,6 +542,8 @@ export const Search: React.FC<{
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
onDragOver={handleDragOver}
onDrop={handleDrop}
/>
<button type='button' className='search__icon' onClick={handleClear}>

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

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