diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a2c48a1c..425c098505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,54 @@ All notable changes to this project will be documented in this file. +## [4.1.3] - 2023-07-06 + +### Added + +- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600)) + +### Changed + +- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058)) +- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868)) +- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852)) +- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614)) +- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510)) +- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216)) + +### Removed + +- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070)) + +### Fixed + +- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464)) +- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519)) +- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477)) +- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840)) +- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361)) +- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273)) +- Fix `tootctl accounts approve --number N` not aproving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605)) +- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988)) +- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015)) +- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016)) +- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060)) +- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713)) +- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499)) +- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431)) +- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637)) +- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342)) + +### Security + +- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463)) +- Update dependencies +- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756)) +- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462) +- Fix timeout handling of outbound HTTP requests (CVE-2023-36461) +- Fix arbitrary file creation through media processing (CVE-2023-36460) +- Fix possible XSS in preview cards (CVE-2023-36459) + ## [4.1.2] - 2023-04-04 ### Fixed diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index b084eae425..cc74db58e5 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController params[:q], current_account, limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) + search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following)) ) end def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id) + params.permit(:type, :offset, :min_id, :max_id, :account_id, :following) end end diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 8c9089d02c..e007417fbb 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -64,6 +64,10 @@ module FormattingHelper end def account_field_value_format(field, with_rel_me: true) - html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) + if field.verified? && !field.account.local? + TextFormatter.shortened_link(field.value_for_verification) + else + html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) + end end end diff --git a/app/javascript/flavours/glitch/components/poll.jsx b/app/javascript/flavours/glitch/components/poll.jsx index eca7b5c525..623d343806 100644 --- a/app/javascript/flavours/glitch/components/poll.jsx +++ b/app/javascript/flavours/glitch/components/poll.jsx @@ -131,6 +131,10 @@ class Poll extends ImmutablePureComponent { this.props.refresh(); }; + handleReveal = () => { + this.setState({ revealed: true }); + } + renderOption (option, optionIndex, showResults) { const { poll, lang, disabled, intl } = this.props; const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); @@ -206,14 +210,14 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; - const { expired } = this.state; + const { revealed, expired } = this.state; if (!poll) { return null; } const timeRemaining = expired ? intl.formatMessage(messages.closed) : ; - const showResults = poll.get('voted') || expired; + const showResults = poll.get('voted') || revealed || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); let votesCount = null; @@ -232,9 +236,10 @@ class Poll extends ImmutablePureComponent {
{!showResults && } - {showResults && !this.props.disabled && · } + {!showResults && <> · } + {showResults && !this.props.disabled && <> · } {votesCount} - {poll.get('expires_at') && · {timeRemaining}} + {poll.get('expires_at') && <> · {timeRemaining}}
); diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index fc3676dd1f..380e23d633 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -163,7 +163,7 @@ class StatusContent extends PureComponent { if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', mention.get('acct')); + link.setAttribute('title', `@${mention.get('acct')}`); if (rewriteMentions !== 'no') { while (link.firstChild) link.removeChild(link.firstChild); link.appendChild(document.createTextNode('@')); diff --git a/app/javascript/flavours/glitch/features/account/components/header.jsx b/app/javascript/flavours/glitch/features/account/components/header.jsx index ca2eb37eb1..0c440dc8a3 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account/components/header.jsx @@ -398,6 +398,7 @@ class Header extends ImmutablePureComponent { {titleFromAccount(account)} + ); diff --git a/app/javascript/flavours/glitch/features/firehose/index.jsx b/app/javascript/flavours/glitch/features/firehose/index.jsx index ae71ba1764..53a39eb63d 100644 --- a/app/javascript/flavours/glitch/features/firehose/index.jsx +++ b/app/javascript/flavours/glitch/features/firehose/index.jsx @@ -103,7 +103,7 @@ const Firehose = ({ feedType, multiColumn }) => { (maxId) => { switch(feedType) { case 'community': - dispatch(expandCommunityTimeline({ onlyMedia })); + dispatch(expandCommunityTimeline({ maxId, onlyMedia })); break; case 'public': dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly })); @@ -154,12 +154,13 @@ const Firehose = ({ feedType, multiColumn }) => { /> ) : ( - - - + + + ); const emptyMessage = feedType === 'community' ? ( @@ -168,10 +169,10 @@ const Firehose = ({ feedType, multiColumn }) => { defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' /> ) : ( - + ); return ( @@ -190,11 +191,11 @@ const Firehose = ({ feedType, multiColumn }) => {
- + - + diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.jsx b/app/javascript/flavours/glitch/features/public_timeline/index.jsx index 4e4b350f8b..c1e49e6ef3 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/public_timeline/index.jsx @@ -13,6 +13,7 @@ import { expandPublicTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import { domain } from 'flavours/glitch/initial_state'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import ColumnSettingsContainer from './containers/column_settings_container'; @@ -147,7 +148,7 @@ class PublicTimeline extends PureComponent { } + prepend={} timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} trackScroll={!pinned} diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index e58b696b56..393614947c 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -771,6 +771,7 @@ class Status extends ImmutablePureComponent { {titleFromStatus(intl, status)} + ); diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index a57e014e9e..7adeaeee01 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -128,7 +128,6 @@ $content-width: 840px; } &.selected { - background: darken($ui-base-color, 2%); border-radius: 4px 0 0; } } @@ -146,13 +145,9 @@ $content-width: 840px; .simple-navigation-active-leaf a { color: $primary-text-color; - background-color: darken($ui-highlight-color, 2%); + background-color: $ui-highlight-color; border-bottom: 0; border-radius: 0; - - &:hover { - background-color: $ui-highlight-color; - } } } @@ -246,12 +241,6 @@ $content-width: 840px; font-weight: 700; color: $primary-text-color; background: $ui-highlight-color; - - &:hover, - &:focus, - &:active { - background: lighten($ui-highlight-color, 4%); - } } } } diff --git a/app/javascript/flavours/glitch/styles/components/misc.scss b/app/javascript/flavours/glitch/styles/components/misc.scss index 208204021a..c8c227e0cb 100644 --- a/app/javascript/flavours/glitch/styles/components/misc.scss +++ b/app/javascript/flavours/glitch/styles/components/misc.scss @@ -38,11 +38,11 @@ } .button { - background-color: darken($ui-highlight-color, 3%); + background-color: $ui-button-background-color; border: 10px none; border-radius: 4px; box-sizing: border-box; - color: $primary-text-color; + color: $ui-button-color; cursor: pointer; display: inline-block; font-family: inherit; @@ -62,14 +62,14 @@ &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; } &--destructive { &:active, &:focus, &:hover { - background-color: $error-red; + background-color: $ui-button-destructive-focus-background-color; transition: none; } } @@ -79,43 +79,22 @@ cursor: default; } - &.button-alternative { - color: $inverted-text-color; - background: $ui-primary-color; - - &:active, - &:focus, - &:hover { - background-color: lighten($ui-primary-color, 4%); - } - } - - &.button-alternative-2 { - background: $ui-base-lighter-color; - - &:active, - &:focus, - &:hover { - background-color: lighten($ui-base-lighter-color, 4%); - } - } - &.button-secondary { font-size: 16px; line-height: 36px; height: auto; - color: $darker-text-color; + color: $ui-button-secondary-color; text-transform: none; background: transparent; padding: 6px 17px; - border: 1px solid lighten($ui-base-color, 12%); + border: 1px solid $ui-button-secondary-border-color; &:active, &:focus, &:hover { - background: lighten($ui-base-color, 4%); - border-color: lighten($ui-base-color, 16%); - color: lighten($darker-text-color, 4%); + border-color: $ui-button-secondary-focus-background-color; + color: $ui-button-secondary-focus-color; + background-color: $ui-button-secondary-focus-background-color; text-decoration: none; } @@ -127,14 +106,14 @@ &.button-tertiary { background: transparent; padding: 6px 17px; - color: $highlight-text-color; - border: 1px solid $highlight-text-color; + color: $ui-button-tertiary-color; + border: 1px solid $ui-button-tertiary-border-color; &:active, &:focus, &:hover { - background: $ui-highlight-color; - color: $primary-text-color; + background-color: $ui-button-tertiary-focus-background-color; + color: $ui-button-tertiary-focus-color; border: 0; padding: 7px 18px; } diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index c68c5fc53d..9c4149fb95 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -718,15 +718,15 @@ } .button.button-secondary { - border-color: $inverted-text-color; - color: $inverted-text-color; + border-color: $ui-button-secondary-border-color; + color: $ui-button-secondary-color; flex: 0 0 auto; &:hover, &:focus, &:active { - border-color: lighten($inverted-text-color, 15%); - color: lighten($inverted-text-color, 15%); + border-color: $ui-button-secondary-focus-background-color; + color: $ui-button-secondary-focus-color; } } diff --git a/app/javascript/flavours/glitch/styles/dashboard.scss b/app/javascript/flavours/glitch/styles/dashboard.scss index bc34c6ec0a..36a7f44253 100644 --- a/app/javascript/flavours/glitch/styles/dashboard.scss +++ b/app/javascript/flavours/glitch/styles/dashboard.scss @@ -81,7 +81,7 @@ display: flex; align-items: baseline; border-radius: 4px; - background: darken($ui-highlight-color, 2%); + background: $ui-button-background-color; color: $primary-text-color; transition: all 100ms ease-in; font-size: 14px; @@ -94,7 +94,7 @@ &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; transition: all 200ms ease-out; } diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 81f42af145..850374f613 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -512,8 +512,8 @@ code { width: 100%; border: 0; border-radius: 4px; - background: darken($ui-highlight-color, 2%); - color: $primary-text-color; + background: $ui-button-background-color; + color: $ui-button-color; font-size: 18px; line-height: inherit; height: auto; @@ -535,7 +535,7 @@ code { &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; } &:disabled:hover { @@ -543,15 +543,12 @@ code { } &.negative { - background: $error-value-color; - - &:hover { - background-color: lighten($error-value-color, 5%); - } + background: $ui-button-destructive-background-color; + &:hover, &:active, &:focus { - background-color: darken($error-value-color, 5%); + background-color: $ui-button-destructive-focus-background-color; } } } diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 74d8900095..d301a8ed3f 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -5,19 +5,6 @@ html { scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25); } -// Change the colors of button texts -.button { - color: $white; - - &.button-alternative-2 { - color: $white; - } - - &.button-tertiary { - color: $highlight-text-color; - } -} - .simple_form .button.button-tertiary { color: $highlight-text-color; } @@ -437,26 +424,6 @@ html { color: $white; } -.button.button-tertiary { - &:hover, - &:focus, - &:active { - color: $white; - } -} - -.button.button-secondary { - border-color: $darker-text-color; - color: $darker-text-color; - - &:hover, - &:focus, - &:active { - border-color: darken($darker-text-color, 8%); - color: darken($darker-text-color, 8%); - } -} - .flash-message.warning { color: lighten($gold-star, 16%); } diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss index cae065878c..250e200fc6 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss @@ -7,6 +7,12 @@ $classic-primary-color: #9baec8; $classic-secondary-color: #d9e1e8; $classic-highlight-color: #6364ff; +$blurple-600: #563acc; // Iris +$blurple-500: #6364ff; // Brand purple +$blurple-300: #858afa; // Faded Blue +$grey-600: #4e4c5a; // Trout +$grey-100: #dadaf3; // Topaz + // Differences $success-green: lighten(#3c754d, 8%); @@ -19,6 +25,13 @@ $ui-primary-color: #9bcbed; $ui-secondary-color: $classic-base-color !default; $ui-highlight-color: $classic-highlight-color !default; +$ui-button-secondary-color: $grey-600 !default; +$ui-button-secondary-border-color: $grey-600 !default; +$ui-button-secondary-focus-color: $white !default; + +$ui-button-tertiary-color: $blurple-500 !default; +$ui-button-tertiary-border-color: $blurple-500 !default; + $primary-text-color: $black !default; $darker-text-color: $classic-base-color !default; $highlight-text-color: darken($ui-highlight-color, 8%) !default; diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss index 8608fec723..8924e43113 100644 --- a/app/javascript/flavours/glitch/styles/variables.scss +++ b/app/javascript/flavours/glitch/styles/variables.scss @@ -1,10 +1,18 @@ // Commonly used web colors $black: #000000; // Black $white: #ffffff; // White -$success-green: #79bd9a; // Padua -$error-red: #df405a; // Cerise -$warning-red: #ff5050; // Sunset Orange -$gold-star: #ca8f04; // Dark Goldenrod +$red-600: #b7253d !default; // Deep Carmine +$red-500: #df405a !default; // Cerise +$blurple-600: #563acc; // Iris +$blurple-500: #6364ff; // Brand purple +$blurple-300: #858afa; // Faded Blue +$grey-600: #4e4c5a; // Trout +$grey-100: #dadaf3; // Topaz + +$success-green: #79bd9a !default; // Padua +$error-red: $red-500 !default; // Cerise +$warning-red: #ff5050 !default; // Sunset Orange +$gold-star: #ca8f04 !default; // Dark Goldenrod $red-bookmark: $warning-red; @@ -31,6 +39,22 @@ $ui-base-lighter-color: lighten( $ui-primary-color: $classic-primary-color !default; // Lighter $ui-secondary-color: $classic-secondary-color !default; // Lightest $ui-highlight-color: $classic-highlight-color !default; +$ui-button-color: $white !default; +$ui-button-background-color: $blurple-500 !default; +$ui-button-focus-background-color: $blurple-600 !default; + +$ui-button-secondary-color: $grey-100 !default; +$ui-button-secondary-border-color: $grey-100 !default; +$ui-button-secondary-focus-background-color: $grey-600 !default; +$ui-button-secondary-focus-color: $white !default; + +$ui-button-tertiary-color: $blurple-300 !default; +$ui-button-tertiary-border-color: $blurple-300 !default; +$ui-button-tertiary-focus-background-color: $blurple-600 !default; +$ui-button-tertiary-focus-color: $white !default; + +$ui-button-destructive-background-color: $red-500 !default; +$ui-button-destructive-focus-background-color: $red-600 !default; // Variables for texts $primary-text-color: $white !default; @@ -39,6 +63,7 @@ $dark-text-color: $ui-base-lighter-color !default; $secondary-text-color: $ui-secondary-color !default; $highlight-text-color: lighten($ui-highlight-color, 8%) !default; $action-button-color: $ui-base-lighter-color !default; +$action-button-focus-color: lighten($ui-base-lighter-color, 4%) !default; $passive-text-color: $gold-star !default; $active-passive-text-color: $success-green !default; diff --git a/app/javascript/mastodon/components/poll.jsx b/app/javascript/mastodon/components/poll.jsx index dfc4034fa3..4304f9acd4 100644 --- a/app/javascript/mastodon/components/poll.jsx +++ b/app/javascript/mastodon/components/poll.jsx @@ -130,6 +130,10 @@ class Poll extends ImmutablePureComponent { this.props.refresh(); }; + handleReveal = () => { + this.setState({ revealed: true }); + } + renderOption (option, optionIndex, showResults) { const { poll, lang, disabled, intl } = this.props; const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); @@ -205,14 +209,14 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; - const { expired } = this.state; + const { revealed, expired } = this.state; if (!poll) { return null; } const timeRemaining = expired ? intl.formatMessage(messages.closed) : ; - const showResults = poll.get('voted') || expired; + const showResults = poll.get('voted') || revealed || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); let votesCount = null; @@ -231,9 +235,10 @@ class Poll extends ImmutablePureComponent {
{!showResults && } - {showResults && !this.props.disabled && · } + {!showResults && <> · } + {showResults && !this.props.disabled && <> · } {votesCount} - {poll.get('expires_at') && · {timeRemaining}} + {poll.get('expires_at') && <> · {timeRemaining}}
); diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 3b3a191d6c..688a456319 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -104,7 +104,7 @@ class StatusContent extends PureComponent { if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', mention.get('acct')); + link.setAttribute('title', `@${mention.get('acct')}`); link.setAttribute('href', `/@${mention.get('acct')}`); } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index 5eea1abf04..b718e860d0 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -476,6 +476,7 @@ class Header extends ImmutablePureComponent { {titleFromAccount(account)} +
); diff --git a/app/javascript/mastodon/features/firehose/index.jsx b/app/javascript/mastodon/features/firehose/index.jsx index e8e399f787..9ba4fd5b2b 100644 --- a/app/javascript/mastodon/features/firehose/index.jsx +++ b/app/javascript/mastodon/features/firehose/index.jsx @@ -84,7 +84,7 @@ const Firehose = ({ feedType, multiColumn }) => { (maxId) => { switch(feedType) { case 'community': - dispatch(expandCommunityTimeline({ onlyMedia })); + dispatch(expandCommunityTimeline({ maxId, onlyMedia })); break; case 'public': dispatch(expandPublicTimeline({ maxId, onlyMedia })); @@ -135,12 +135,13 @@ const Firehose = ({ feedType, multiColumn }) => { /> ) : ( - - - + + + ); const emptyMessage = feedType === 'community' ? ( @@ -149,10 +150,10 @@ const Firehose = ({ feedType, multiColumn }) => { defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' /> ) : ( - + ); return ( @@ -171,11 +172,11 @@ const Firehose = ({ feedType, multiColumn }) => {
- + - + diff --git a/app/javascript/mastodon/features/public_timeline/index.jsx b/app/javascript/mastodon/features/public_timeline/index.jsx index d77b76a63e..352baa8336 100644 --- a/app/javascript/mastodon/features/public_timeline/index.jsx +++ b/app/javascript/mastodon/features/public_timeline/index.jsx @@ -8,6 +8,7 @@ import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { domain } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { connectPublicStream } from '../../actions/streaming'; @@ -143,7 +144,7 @@ class PublicTimeline extends PureComponent { } + prepend={} timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} trackScroll={!pinned} diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index cd918313ca..5718bebc25 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -716,6 +716,7 @@ class Status extends ImmutablePureComponent { {titleFromStatus(intl, status)} + ); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f1617a2040..2afac7e7e8 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -202,7 +202,7 @@ "dismissable_banner.explore_links": "These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.", "dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.", "dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.", - "dismissable_banner.public_timeline": "These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.", + "dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.", "embed.instructions": "Embed this post on your website by copying the code below.", "embed.preview": "Here is what it will look like:", "emoji_button.activity": "Activity", @@ -269,8 +269,8 @@ "filter_modal.select_filter.title": "Filter this post", "filter_modal.title.status": "Filter a post", "firehose.all": "All", - "firehose.local": "Local", - "firehose.remote": "Remote", + "firehose.local": "This server", + "firehose.remote": "Other servers", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", @@ -487,6 +487,7 @@ "picture_in_picture.restore": "Put it back", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.reveal": "See results", "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 91828d408a..9f33a5c9cc 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -5,19 +5,6 @@ html { scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25); } -// Change the colors of button texts -.button { - color: $white; - - &.button-alternative-2 { - color: $white; - } - - &.button-tertiary { - color: $highlight-text-color; - } -} - .simple_form .button.button-tertiary { color: $highlight-text-color; } @@ -436,26 +423,6 @@ html { color: $white; } -.button.button-tertiary { - &:hover, - &:focus, - &:active { - color: $white; - } -} - -.button.button-secondary { - border-color: $darker-text-color; - color: $darker-text-color; - - &:hover, - &:focus, - &:active { - border-color: darken($darker-text-color, 8%); - color: darken($darker-text-color, 8%); - } -} - .flash-message.warning { color: lighten($gold-star, 16%); } diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss index cae065878c..250e200fc6 100644 --- a/app/javascript/styles/mastodon-light/variables.scss +++ b/app/javascript/styles/mastodon-light/variables.scss @@ -7,6 +7,12 @@ $classic-primary-color: #9baec8; $classic-secondary-color: #d9e1e8; $classic-highlight-color: #6364ff; +$blurple-600: #563acc; // Iris +$blurple-500: #6364ff; // Brand purple +$blurple-300: #858afa; // Faded Blue +$grey-600: #4e4c5a; // Trout +$grey-100: #dadaf3; // Topaz + // Differences $success-green: lighten(#3c754d, 8%); @@ -19,6 +25,13 @@ $ui-primary-color: #9bcbed; $ui-secondary-color: $classic-base-color !default; $ui-highlight-color: $classic-highlight-color !default; +$ui-button-secondary-color: $grey-600 !default; +$ui-button-secondary-border-color: $grey-600 !default; +$ui-button-secondary-focus-color: $white !default; + +$ui-button-tertiary-color: $blurple-500 !default; +$ui-button-tertiary-border-color: $blurple-500 !default; + $primary-text-color: $black !default; $darker-text-color: $classic-base-color !default; $highlight-text-color: darken($ui-highlight-color, 8%) !default; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index f4dfe55607..6bfb23a46f 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -128,7 +128,6 @@ $content-width: 840px; } &.selected { - background: darken($ui-base-color, 2%); border-radius: 4px 0 0; } } @@ -146,13 +145,9 @@ $content-width: 840px; .simple-navigation-active-leaf a { color: $primary-text-color; - background-color: darken($ui-highlight-color, 2%); + background-color: $ui-highlight-color; border-bottom: 0; border-radius: 0; - - &:hover { - background-color: $ui-highlight-color; - } } } @@ -246,12 +241,6 @@ $content-width: 840px; font-weight: 700; color: $primary-text-color; background: $ui-highlight-color; - - &:hover, - &:focus, - &:active { - background: lighten($ui-highlight-color, 4%); - } } } } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 67be71d669..c6dd5ce145 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -47,11 +47,11 @@ } .button { - background-color: darken($ui-highlight-color, 2%); + background-color: $ui-button-background-color; border: 10px none; border-radius: 4px; box-sizing: border-box; - color: $primary-text-color; + color: $ui-button-color; cursor: pointer; display: inline-block; font-family: inherit; @@ -71,14 +71,14 @@ &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; } &--destructive { &:active, &:focus, &:hover { - background-color: $error-red; + background-color: $ui-button-destructive-focus-background-color; transition: none; } } @@ -108,39 +108,18 @@ outline: 0 !important; } - &.button-alternative { - color: $inverted-text-color; - background: $ui-primary-color; - - &:active, - &:focus, - &:hover { - background-color: lighten($ui-primary-color, 4%); - } - } - - &.button-alternative-2 { - background: $ui-base-lighter-color; - - &:active, - &:focus, - &:hover { - background-color: lighten($ui-base-lighter-color, 4%); - } - } - &.button-secondary { - color: $darker-text-color; + color: $ui-button-secondary-color; background: transparent; padding: 6px 17px; - border: 1px solid lighten($ui-base-color, 12%); + border: 1px solid $ui-button-secondary-border-color; &:active, &:focus, &:hover { - background: lighten($ui-base-color, 4%); - border-color: lighten($ui-base-color, 16%); - color: lighten($darker-text-color, 4%); + border-color: $ui-button-secondary-focus-background-color; + color: $ui-button-secondary-focus-color; + background-color: $ui-button-secondary-focus-background-color; text-decoration: none; } @@ -152,14 +131,14 @@ &.button-tertiary { background: transparent; padding: 6px 17px; - color: $highlight-text-color; - border: 1px solid $highlight-text-color; + color: $ui-button-tertiary-color; + border: 1px solid $ui-button-tertiary-border-color; &:active, &:focus, &:hover { - background: $ui-highlight-color; - color: $primary-text-color; + background-color: $ui-button-tertiary-focus-background-color; + color: $ui-button-tertiary-focus-color; border: 0; padding: 7px 18px; } @@ -1148,6 +1127,8 @@ body > [data-popper-placement] { } &--in-thread { + $thread-margin: 46px + 10px; + border-bottom: 0; .status__content, @@ -1158,8 +1139,12 @@ body > [data-popper-placement] { .attachment-list, .picture-in-picture-placeholder, .status-card { - margin-inline-start: 46px + 10px; - width: calc(100% - (46px + 10px)); + margin-inline-start: $thread-margin; + width: calc(100% - ($thread-margin)); + } + + .status__content__read-more-button { + margin-inline-start: $thread-margin; } } @@ -5810,15 +5795,15 @@ a.status-card.compact:hover { } .button.button-secondary { - border-color: $inverted-text-color; - color: $inverted-text-color; + border-color: $ui-button-secondary-border-color; + color: $ui-button-secondary-color; flex: 0 0 auto; &:hover, &:focus, &:active { - border-color: lighten($inverted-text-color, 15%); - color: lighten($inverted-text-color, 15%); + border-color: $ui-button-secondary-focus-background-color; + color: $ui-button-secondary-focus-color; } } diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss index bc34c6ec0a..36a7f44253 100644 --- a/app/javascript/styles/mastodon/dashboard.scss +++ b/app/javascript/styles/mastodon/dashboard.scss @@ -81,7 +81,7 @@ display: flex; align-items: baseline; border-radius: 4px; - background: darken($ui-highlight-color, 2%); + background: $ui-button-background-color; color: $primary-text-color; transition: all 100ms ease-in; font-size: 14px; @@ -94,7 +94,7 @@ &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; transition: all 200ms ease-out; } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 81a656a602..f69b699a0a 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -511,8 +511,8 @@ code { width: 100%; border: 0; border-radius: 4px; - background: darken($ui-highlight-color, 2%); - color: $primary-text-color; + background: $ui-button-background-color; + color: $ui-button-color; font-size: 18px; line-height: inherit; height: auto; @@ -534,7 +534,7 @@ code { &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; } &:disabled:hover { @@ -542,15 +542,12 @@ code { } &.negative { - background: $error-value-color; - - &:hover { - background-color: lighten($error-value-color, 5%); - } + background: $ui-button-destructive-background-color; + &:hover, &:active, &:focus { - background-color: darken($error-value-color, 5%); + background-color: $ui-button-destructive-focus-background-color; } } } diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index d6dda1b3c7..68db9d5fc0 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -1,8 +1,16 @@ // Commonly used web colors $black: #000000; // Black $white: #ffffff; // White +$red-600: #b7253d !default; // Deep Carmine +$red-500: #df405a !default; // Cerise +$blurple-600: #563acc; // Iris +$blurple-500: #6364ff; // Brand purple +$blurple-300: #858afa; // Faded Blue +$grey-600: #4e4c5a; // Trout +$grey-100: #dadaf3; // Topaz + $success-green: #79bd9a !default; // Padua -$error-red: #df405a !default; // Cerise +$error-red: $red-500 !default; // Cerise $warning-red: #ff5050 !default; // Sunset Orange $gold-star: #ca8f04 !default; // Dark Goldenrod @@ -31,6 +39,22 @@ $ui-base-lighter-color: lighten( $ui-primary-color: $classic-primary-color !default; // Lighter $ui-secondary-color: $classic-secondary-color !default; // Lightest $ui-highlight-color: $classic-highlight-color !default; +$ui-button-color: $white !default; +$ui-button-background-color: $blurple-500 !default; +$ui-button-focus-background-color: $blurple-600 !default; + +$ui-button-secondary-color: $grey-100 !default; +$ui-button-secondary-border-color: $grey-100 !default; +$ui-button-secondary-focus-background-color: $grey-600 !default; +$ui-button-secondary-focus-color: $white !default; + +$ui-button-tertiary-color: $blurple-300 !default; +$ui-button-tertiary-border-color: $blurple-300 !default; +$ui-button-tertiary-focus-background-color: $blurple-600 !default; +$ui-button-tertiary-focus-color: $white !default; + +$ui-button-destructive-background-color: $red-500 !default; +$ui-button-destructive-focus-background-color: $red-600 !default; // Variables for texts $primary-text-color: $white !default; @@ -39,6 +63,7 @@ $dark-text-color: $ui-base-lighter-color !default; $secondary-text-color: $ui-secondary-color !default; $highlight-text-color: lighten($ui-highlight-color, 8%) !default; $action-button-color: $ui-base-lighter-color !default; +$action-button-focus-color: lighten($ui-base-lighter-color, 4%) !default; $passive-text-color: $gold-star !default; $active-passive-text-color: $success-green !default; diff --git a/app/lib/request.rb b/app/lib/request.rb index 4bde6fc911..425effa1ac 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -7,11 +7,48 @@ require 'resolv' # Monkey-patch the HTTP.rb timeout class to avoid using a timeout block # around the Socket#open method, since we use our own timeout blocks inside # that method +# +# Also changes how the read timeout behaves so that it is cumulative (closer +# to HTTP::Timeout::Global, but still having distinct timeouts for other +# operation types) class HTTP::Timeout::PerOperation def connect(socket_class, host, port, nodelay = false) @socket = socket_class.open(host, port) @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay end + + # Reset deadline when the connection is re-used for different requests + def reset_counter + @deadline = nil + end + + # Read data from the socket + def readpartial(size, buffer = nil) + @deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @read_timeout + + timeout = false + loop do + result = @socket.read_nonblock(size, buffer, exception: false) + + return :eof if result.nil? + + remaining_time = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) + raise HTTP::TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout || remaining_time <= 0 + return result if result != :wait_readable + + # marking the socket for timeout. Why is this not being raised immediately? + # it seems there is some race-condition on the network level between calling + # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting + # for reads, and when waiting for x seconds, it returns nil suddenly without completing + # the x seconds. In a normal case this would be a timeout on wait/read, but it can + # also mean that the socket has been closed by the server. Therefore we "mark" the + # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no + # timeout. Else, the first timeout was a proper timeout. + # This hack has to be done because io/wait#wait_readable doesn't provide a value for when + # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks. + timeout = true unless @socket.to_io.wait_readable(remaining_time) + end + end end class Request diff --git a/app/lib/scope_parser.rb b/app/lib/scope_parser.rb index d268688c83..45eb3c7b93 100644 --- a/app/lib/scope_parser.rb +++ b/app/lib/scope_parser.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ScopeParser < Parslet::Parser - rule(:term) { match('[a-z]').repeat(1).as(:term) } + rule(:term) { match('[a-z_]').repeat(1).as(:term) } rule(:colon) { str(':') } rule(:access) { (str('write') | str('read')).as(:access) } rule(:namespace) { str('admin').as(:namespace) } diff --git a/app/lib/text_formatter.rb b/app/lib/text_formatter.rb index 0404cbaced..3570632dd9 100644 --- a/app/lib/text_formatter.rb +++ b/app/lib/text_formatter.rb @@ -48,6 +48,26 @@ class TextFormatter html.html_safe # rubocop:disable Rails/OutputSafety end + class << self + include ERB::Util + + def shortened_link(url, rel_me: false) + url = Addressable::URI.parse(url).to_s + rel = rel_me ? (DEFAULT_REL + %w(me)) : DEFAULT_REL + + prefix = url.match(URL_PREFIX_REGEX).to_s + display_url = url[prefix.length, 30] + suffix = url[prefix.length + 30..-1] + cutoff = url[prefix.length..-1].length > 30 + + <<~HTML.squish + #{h(display_url)} + HTML + rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError + h(url) + end + end + private def rewrite @@ -70,19 +90,7 @@ class TextFormatter end def link_to_url(entity) - url = Addressable::URI.parse(entity[:url]).to_s - rel = with_rel_me? ? (DEFAULT_REL + %w(me)) : DEFAULT_REL - - prefix = url.match(URL_PREFIX_REGEX).to_s - display_url = url[prefix.length, 30] - suffix = url[prefix.length + 30..-1] - cutoff = url[prefix.length..-1].length > 30 - - <<~HTML.squish - #{h(display_url)} - HTML - rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError - h(entity[:url]) + TextFormatter.shortened_link(entity[:url], rel_me: with_rel_me?) end def link_to_hashtag(entity) diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb index 9cafedc209..f93ee4c919 100644 --- a/app/models/concerns/attachmentable.rb +++ b/app/models/concerns/attachmentable.rb @@ -22,15 +22,14 @@ module Attachmentable included do def self.has_attached_file(name, options = {}) # rubocop:disable Naming/PredicateName - options = { validate_media_type: false }.merge(options) super(name, options) - send(:"before_#{name}_post_process") do + + send(:"before_#{name}_validate") do attachment = send(name) check_image_dimension(attachment) set_file_content_type(attachment) obfuscate_file_name(attachment) set_file_extension(attachment) - Paperclip::Validators::MediaTypeSpoofDetectionValidator.new(attributes: [name]).validate(self) end end end diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb index 8413b23d85..08bc07edd4 100644 --- a/app/serializers/rest/preview_card_serializer.rb +++ b/app/serializers/rest/preview_card_serializer.rb @@ -11,4 +11,8 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer def image object.image? ? full_asset_url(object.image.url(:original)) : nil end + + def html + Sanitize.fragment(object.html, Sanitize::Config::MASTODON_OEMBED) + end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index dad8c0b28f..05d2d0e7ce 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -2,12 +2,13 @@ class SearchService < BaseService def call(query, account, limit, options = {}) - @query = query&.strip - @account = account - @options = options - @limit = limit.to_i - @offset = options[:type].blank? ? 0 : options[:offset].to_i - @resolve = options[:resolve] || false + @query = query&.strip + @account = account + @options = options + @limit = limit.to_i + @offset = options[:type].blank? ? 0 : options[:offset].to_i + @resolve = options[:resolve] || false + @following = options[:following] || false default_results.tap do |results| next if @query.blank? || @limit.zero? @@ -31,7 +32,8 @@ class SearchService < BaseService limit: @limit, resolve: @resolve, offset: @offset, - use_searchable_text: true + use_searchable_text: true, + following: @following ) end diff --git a/app/workers/account_deletion_worker.rb b/app/workers/account_deletion_worker.rb index fdf013e010..b501511728 100644 --- a/app/workers/account_deletion_worker.rb +++ b/app/workers/account_deletion_worker.rb @@ -6,9 +6,12 @@ class AccountDeletionWorker sidekiq_options queue: 'pull', lock: :until_executed def perform(account_id, options = {}) + account = Account.find(account_id) + return unless account.suspended? + reserve_username = options.with_indifferent_access.fetch(:reserve_username, true) skip_activitypub = options.with_indifferent_access.fetch(:skip_activitypub, false) - DeleteAccountService.new.call(Account.find(account_id), reserve_username: reserve_username, skip_activitypub: skip_activitypub, reserve_email: false) + DeleteAccountService.new.call(account, reserve_username: reserve_username, skip_activitypub: skip_activitypub, reserve_email: false) rescue ActiveRecord::RecordNotFound true end diff --git a/config/application.rb b/config/application.rb index d3c99baa12..8c4ec27e7f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,6 +28,7 @@ require_relative '../lib/paperclip/url_generator_extensions' require_relative '../lib/paperclip/attachment_extensions' require_relative '../lib/paperclip/lazy_thumbnail' require_relative '../lib/paperclip/gif_transcoder' +require_relative '../lib/paperclip/media_type_spoof_detector_extensions' require_relative '../lib/paperclip/transcoder' require_relative '../lib/paperclip/type_corrector' require_relative '../lib/paperclip/response_with_limit_adapter' diff --git a/config/imagemagick/policy.xml b/config/imagemagick/policy.xml new file mode 100644 index 0000000000..1052476b31 --- /dev/null +++ b/config/imagemagick/policy.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 093d2ba9ae..f2da410dbe 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -153,3 +153,10 @@ unless defined?(Seahorse) end end end + +# Set our ImageMagick security policy, but allow admins to override it +ENV['MAGICK_CONFIGURE_PATH'] = begin + imagemagick_config_paths = ENV.fetch('MAGICK_CONFIGURE_PATH', '').split(File::PATH_SEPARATOR) + imagemagick_config_paths << Rails.root.join('config', 'imagemagick').expand_path.to_s + imagemagick_config_paths.join(File::PATH_SEPARATOR) +end diff --git a/config/routes.rb b/config/routes.rb index f2bfbeb22b..2f15c4fc02 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,6 +13,7 @@ Rails.application.routes.draw do /home /public /public/local + /public/remote /conversations /lists/(*any) /notifications diff --git a/db/migrate/20230702131023_add_superapp_index_to_applications.rb b/db/migrate/20230702131023_add_superapp_index_to_applications.rb new file mode 100644 index 0000000000..f301127a3e --- /dev/null +++ b/db/migrate/20230702131023_add_superapp_index_to_applications.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddSuperappIndexToApplications < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :oauth_applications, :superapp, where: 'superapp = true', algorithm: :concurrently + end +end diff --git a/db/migrate/20230702151753_add_index_user_on_unconfirmed_email.rb b/db/migrate/20230702151753_add_index_user_on_unconfirmed_email.rb new file mode 100644 index 0000000000..a935463eaa --- /dev/null +++ b/db/migrate/20230702151753_add_index_user_on_unconfirmed_email.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddIndexUserOnUnconfirmedEmail < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :users, :unconfirmed_email, where: 'unconfirmed_email IS NOT NULL', algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 14644543a0..e6a0c5f1a1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_06_30_145300) do +ActiveRecord::Schema.define(version: 2023_07_02_151753) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -700,6 +700,7 @@ ActiveRecord::Schema.define(version: 2023_06_30_145300) do t.bigint "owner_id" t.boolean "confidential", default: true, null: false t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" + t.index ["superapp"], name: "index_oauth_applications_on_superapp", where: "(superapp = true)" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end @@ -1101,6 +1102,7 @@ ActiveRecord::Schema.define(version: 2023_06_30_145300) do t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, opclass: :text_pattern_ops, where: "(reset_password_token IS NOT NULL)" t.index ["role_id"], name: "index_users_on_role_id", where: "(role_id IS NOT NULL)" + t.index ["unconfirmed_email"], name: "index_users_on_unconfirmed_email", where: "(unconfirmed_email IS NOT NULL)" end create_table "web_push_subscriptions", force: :cascade do |t| diff --git a/dist/nginx.conf b/dist/nginx.conf index bed4bd3db9..fc68e9a6d1 100644 --- a/dist/nginx.conf +++ b/dist/nginx.conf @@ -109,6 +109,8 @@ server { location ~ ^/system/ { add_header Cache-Control "public, max-age=2419200, immutable"; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; + add_header X-Content-Type-Options nosniff; + add_header Content-Security-Policy "default-src 'none'; form-action 'none'"; try_files $uri =404; } diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 9d3fa1a577..b938cd857f 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 2 + 3 end def flags diff --git a/lib/paperclip/media_type_spoof_detector_extensions.rb b/lib/paperclip/media_type_spoof_detector_extensions.rb new file mode 100644 index 0000000000..a406ef312f --- /dev/null +++ b/lib/paperclip/media_type_spoof_detector_extensions.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Paperclip + module MediaTypeSpoofDetectorExtensions + def calculated_content_type + return @calculated_content_type if defined?(@calculated_content_type) + + @calculated_content_type = type_from_file_command.chomp + + # The `file` command fails to recognize some MP3 files as such + @calculated_content_type = type_from_marcel if @calculated_content_type == 'application/octet-stream' && type_from_marcel == 'audio/mpeg' + @calculated_content_type + end + + def type_from_marcel + @type_from_marcel ||= Marcel::MimeType.for Pathname.new(@file.path), + name: @file.path + end + end +end + +Paperclip::MediaTypeSpoofDetector.prepend(Paperclip::MediaTypeSpoofDetectorExtensions) diff --git a/lib/paperclip/transcoder.rb b/lib/paperclip/transcoder.rb index b3b55f82fb..f4768aa602 100644 --- a/lib/paperclip/transcoder.rb +++ b/lib/paperclip/transcoder.rb @@ -19,10 +19,7 @@ module Paperclip def make metadata = VideoMetadataExtractor.new(@file.path) - unless metadata.valid? - Paperclip.log("Unsupported file #{@file.path}") - return File.open(@file.path) - end + raise Paperclip::Error, "Error while transcoding #{@file.path}: unsupported file" unless metadata.valid? update_attachment_type(metadata) update_options_from_metadata(metadata) diff --git a/lib/public_file_server_middleware.rb b/lib/public_file_server_middleware.rb index 3799230a22..7e02e37a08 100644 --- a/lib/public_file_server_middleware.rb +++ b/lib/public_file_server_middleware.rb @@ -32,6 +32,11 @@ class PublicFileServerMiddleware end end + # Override the default CSP header set by the CSP middleware + headers['Content-Security-Policy'] = "default-src 'none'; form-action 'none'" if request_path.start_with?(paperclip_root_url) + + headers['X-Content-Type-Options'] = 'nosniff' + [status, headers, response] end diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index a64d33bd99..830e6c20ae 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -107,26 +107,26 @@ class Sanitize ] ) - MASTODON_OEMBED ||= freeze_config merge( - RELAXED, - elements: RELAXED[:elements] + %w(audio embed iframe source video), + MASTODON_OEMBED ||= freeze_config( + elements: %w(audio embed iframe source video), - attributes: merge( - RELAXED[:attributes], + attributes: { 'audio' => %w(controls), 'embed' => %w(height src type width), 'iframe' => %w(allowfullscreen frameborder height scrolling src width), 'source' => %w(src type), 'video' => %w(controls height loop width), - 'div' => [:data] - ), + }, - protocols: merge( - RELAXED[:protocols], + protocols: { 'embed' => { 'src' => HTTP_PROTOCOLS }, 'iframe' => { 'src' => HTTP_PROTOCOLS }, - 'source' => { 'src' => HTTP_PROTOCOLS } - ) + 'source' => { 'src' => HTTP_PROTOCOLS }, + }, + + add_attributes: { + 'iframe' => { 'sandbox' => 'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms' }, + } ) LINK_REL_TRANSFORMER = lambda do |env| diff --git a/spec/controllers/api/v2/search_controller_spec.rb b/spec/controllers/api/v2/search_controller_spec.rb index bfabe8cc17..a3b92fc37a 100644 --- a/spec/controllers/api/v2/search_controller_spec.rb +++ b/spec/controllers/api/v2/search_controller_spec.rb @@ -14,13 +14,40 @@ RSpec.describe Api::V2::SearchController do end describe 'GET #index' do - before do - get :index, params: { q: 'test' } - end + let!(:bob) { Fabricate(:account, username: 'bob_test') } + let!(:ana) { Fabricate(:account, username: 'ana_test') } + let!(:tom) { Fabricate(:account, username: 'tom_test') } + let(:params) { { q: 'test' } } it 'returns http success' do + get :index, params: params + expect(response).to have_http_status(200) end + + context 'when searching accounts' do + let(:params) { { q: 'test', type: 'accounts' } } + + it 'returns all matching accounts' do + get :index, params: params + + expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s) + end + + context 'with following=true' do + let(:params) { { q: 'test', type: 'accounts', following: 'true' } } + + before do + user.account.follow!(ana) + end + + it 'returns only the followed accounts' do + get :index, params: params + + expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s) + end + end + end end end diff --git a/spec/fixtures/files/boop.mp3 b/spec/fixtures/files/boop.mp3 new file mode 100644 index 0000000000..ba106a3a32 Binary files /dev/null and b/spec/fixtures/files/boop.mp3 differ diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 2dfc6cf925..90e4f2f47b 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -152,6 +152,26 @@ RSpec.describe MediaAttachment, paperclip_processing: true do end end + describe 'mp3 with large cover art' do + let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('boop.mp3')) } + + it 'detects it as an audio file' do + expect(media.type).to eq 'audio' + end + + it 'sets meta for the duration' do + expect(media.file.meta['original']['duration']).to be_within(0.05).of(0.235102) + end + + it 'extracts thumbnail' do + expect(media.thumbnail.present?).to be true + end + + it 'gives the file a random name' do + expect(media.file_file_name).to_not eq 'boop.mp3' + end + end + describe 'jpeg' do let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) } diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 3bf7f8ce9f..497ec74474 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -68,7 +68,7 @@ describe SearchService, type: :service do allow(AccountSearchService).to receive(:new).and_return(service) results = subject.call(query, nil, 10) - expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, use_searchable_text: true) + expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, use_searchable_text: true, following: false) expect(results).to eq empty_results.merge(accounts: [account]) end end