From 2cff744cdf1dad10f81ebd2e35ad038927ca3651 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Wed, 31 Jan 2018 20:52:58 +0000 Subject: [PATCH 001/257] fix ruby 2.5 rvm install in vagrant (#6396) RVM has a known issue with installing Ruby 2.5 on the version of Ubuntu the Vagrant box is using: https://github.com/rvm/rvm/issues/4291 This bug was preventing any gem installs in the vagrant box --- Vagrantfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index 0c21bed68..bbe3b7f3b 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -48,7 +48,7 @@ curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-instal source /home/vagrant/.rvm/scripts/rvm # Install Ruby -rvm install ruby-$RUBY_VERSION +rvm reinstall ruby-$RUBY_VERSION --disable-binary # Configure database sudo -u postgres createuser -U postgres vagrant -s From 3ed194b67ddbd0f92c16edd3b7f933f3c73665bc Mon Sep 17 00:00:00 2001 From: Evgeny Petrov Date: Thu, 1 Feb 2018 01:33:54 +0300 Subject: [PATCH 002/257] Russian language updated (#6397) --- app/javascript/mastodon/locales/ru.json | 96 ++++++++++++------------- config/locales/devise.ru.yml | 25 ++++++- config/locales/doorkeeper.ru.yml | 6 ++ config/locales/simple_form.ru.yml | 5 +- 4 files changed, 81 insertions(+), 51 deletions(-) diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 29dbe84c5..58ffa8d55 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -7,22 +7,22 @@ "account.followers": "Подписаны", "account.follows": "Подписки", "account.follows_you": "Подписан(а) на Вас", - "account.hide_reblogs": "Hide boosts from @{name}", + "account.hide_reblogs": "Скрыть продвижения от @{name}", "account.media": "Медиаконтент", "account.mention": "Упомянуть", - "account.moved_to": "{name} has moved to:", + "account.moved_to": "Ищите {name} здесь:", "account.mute": "Заглушить", - "account.mute_notifications": "Mute notifications from @{name}", + "account.mute_notifications": "Скрыть уведомления от @{name}", "account.posts": "Посты", "account.report": "Пожаловаться", "account.requested": "Ожидает подтверждения", "account.share": "Поделиться профилем @{name}", - "account.show_reblogs": "Show boosts from @{name}", + "account.show_reblogs": "Показывать продвижения от @{name}", "account.unblock": "Разблокировать", "account.unblock_domain": "Разблокировать {domain}", "account.unfollow": "Отписаться", "account.unmute": "Снять глушение", - "account.unmute_notifications": "Unmute notifications from @{name}", + "account.unmute_notifications": "Показывать уведомления от @{name}", "account.view_full_profile": "Показать полный профиль", "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз", "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.", @@ -36,10 +36,10 @@ "column.favourites": "Понравившееся", "column.follow_requests": "Запросы на подписку", "column.home": "Главная", - "column.lists": "Lists", + "column.lists": "Списки", "column.mutes": "Список глушения", "column.notifications": "Уведомления", - "column.pins": "Pinned toot", + "column.pins": "Закреплённый пост", "column.public": "Глобальная лента", "column_back_button.label": "Назад", "column_header.hide_settings": "Скрыть настройки", @@ -50,7 +50,7 @@ "column_header.unpin": "Открепить", "column_subheading.navigation": "Навигация", "column_subheading.settings": "Настройки", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Этот пост не будет показывается в поиске по хэштегу, т.к. он непубличный. Только публичные посты можно найти в поиске по хэштегу.", "compose_form.lock_disclaimer": "Ваш аккаунт не {locked}. Любой человек может подписаться на Вас и просматривать посты для подписчиков.", "compose_form.lock_disclaimer.lock": "закрыт", "compose_form.placeholder": "О чем Вы думаете?", @@ -64,8 +64,8 @@ "confirmations.block.message": "Вы уверены, что хотите заблокировать {name}?", "confirmations.delete.confirm": "Удалить", "confirmations.delete.message": "Вы уверены, что хотите удалить этот статус?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.delete_list.confirm": "Удалить", + "confirmations.delete_list.message": "Вы действительно хотите навсегда удалить этот список?", "confirmations.domain_block.confirm": "Блокировать весь домен", "confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.", "confirmations.mute.confirm": "Заглушить", @@ -92,7 +92,7 @@ "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.", "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", "empty_column.home.public_timeline": "публичные ленты", - "empty_column.list": "There is nothing in this list yet.", + "empty_column.list": "В этом списке пока ничего нет.", "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.", "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.", "follow_request.authorize": "Авторизовать", @@ -108,50 +108,50 @@ "home.column_settings.show_reblogs": "Показывать продвижения", "home.column_settings.show_replies": "Показывать ответы", "home.settings": "Настройки колонки", - "keyboard_shortcuts.back": "to navigate back", - "keyboard_shortcuts.boost": "to boost", - "keyboard_shortcuts.column": "to focus a status in one of the columns", - "keyboard_shortcuts.compose": "to focus the compose textarea", - "keyboard_shortcuts.description": "Description", - "keyboard_shortcuts.down": "to move down in the list", - "keyboard_shortcuts.enter": "to open status", - "keyboard_shortcuts.favourite": "to favourite", - "keyboard_shortcuts.heading": "Keyboard Shortcuts", - "keyboard_shortcuts.hotkey": "Hotkey", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.search": "to focus search", - "keyboard_shortcuts.toot": "to start a brand new toot", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", - "keyboard_shortcuts.up": "to move up in the list", + "keyboard_shortcuts.back": "перейти назад", + "keyboard_shortcuts.boost": "продвинуть пост", + "keyboard_shortcuts.column": "фокус на одном из столбцов", + "keyboard_shortcuts.compose": "фокус на поле ввода", + "keyboard_shortcuts.description": "Описание", + "keyboard_shortcuts.down": "вниз по списку", + "keyboard_shortcuts.enter": "развернуть пост", + "keyboard_shortcuts.favourite": "в избранное", + "keyboard_shortcuts.heading": "Сочетания клавиш", + "keyboard_shortcuts.hotkey": "Гор. клавиша", + "keyboard_shortcuts.legend": "показать это окно", + "keyboard_shortcuts.mention": "упомянуть автора поста", + "keyboard_shortcuts.reply": "ответить", + "keyboard_shortcuts.search": "перейти к поиску", + "keyboard_shortcuts.toot": "начать писать новый пост", + "keyboard_shortcuts.unfocus": "убрать фокус с поля ввода/поиска", + "keyboard_shortcuts.up": "вверх по списку", "lightbox.close": "Закрыть", "lightbox.next": "Далее", "lightbox.previous": "Назад", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lists.account.add": "Добавить в список", + "lists.account.remove": "Убрать из списка", + "lists.delete": "Удалить список", + "lists.edit": "Изменить список", + "lists.new.create": "Новый список", + "lists.new.title_placeholder": "Заголовок списка", + "lists.search": "Искать из ваших подписок", + "lists.subheading": "Ваши списки", "loading_indicator.label": "Загрузка...", "media_gallery.toggle_visible": "Показать/скрыть", "missing_indicator.label": "Не найдено", - "missing_indicator.sublabel": "This resource could not be found", - "mute_modal.hide_notifications": "Hide notifications from this user?", + "missing_indicator.sublabel": "Запрашиваемый ресурс не найден", + "mute_modal.hide_notifications": "Убрать уведомления от этого пользователя?", "navigation_bar.blocks": "Список блокировки", "navigation_bar.community_timeline": "Локальная лента", "navigation_bar.edit_profile": "Изменить профиль", "navigation_bar.favourites": "Понравившееся", "navigation_bar.follow_requests": "Запросы на подписку", "navigation_bar.info": "Об узле", - "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", - "navigation_bar.lists": "Lists", + "navigation_bar.keyboard_shortcuts": "Сочетания клавиш", + "navigation_bar.lists": "Списки", "navigation_bar.logout": "Выйти", "navigation_bar.mutes": "Список глушения", - "navigation_bar.pins": "Pinned toots", + "navigation_bar.pins": "Закреплённые посты", "navigation_bar.preferences": "Опции", "navigation_bar.public_timeline": "Глобальная лента", "notification.favourite": "{name} понравился Ваш статус", @@ -175,8 +175,8 @@ "onboarding.page_four.home": "Домашняя лента показывает посты от тех, на кого Вы подписаны.", "onboarding.page_four.notifications": "Колонка уведомлений сообщает о взаимодействии с Вами других людей.", "onboarding.page_one.federation": "Mastodon - это сеть независимых серверов, которые вместе образуют единую социальную сеть. Мы называем эти сервера узлами.", - "onboarding.page_one.full_handle": "Your full handle", - "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", + "onboarding.page_one.full_handle": "Всё в ваших руках", + "onboarding.page_one.handle_hint": "Это то, что вы посоветуете искать своим друзьям.", "onboarding.page_one.welcome": "Добро пожаловать в Mastodon!", "onboarding.page_six.admin": "Админ Вашего узла - {admin}.", "onboarding.page_six.almost_done": "Почти готово...", @@ -199,8 +199,8 @@ "privacy.public.short": "Публичный", "privacy.unlisted.long": "Не показывать в лентах", "privacy.unlisted.short": "Скрытый", - "regeneration_indicator.label": "Loading…", - "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "regeneration_indicator.label": "Загрузка…", + "regeneration_indicator.sublabel": "Ваша домашняя лента готовится!", "relative_time.days": "{number}д", "relative_time.hours": "{number}ч", "relative_time.just_now": "только что", @@ -218,7 +218,7 @@ "search_popout.tips.user": "пользователь", "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", "standalone.public_title": "Прямо сейчас", - "status.block": "Block @{name}", + "status.block": "Заблокировать @{name}", "status.cannot_reblog": "Этот статус не может быть продвинут", "status.delete": "Удалить", "status.embed": "Встроить", @@ -227,7 +227,7 @@ "status.media_hidden": "Медиаконтент скрыт", "status.mention": "Упомянуть @{name}", "status.more": "Больше", - "status.mute": "Mute @{name}", + "status.mute": "Заглушить @{name}", "status.mute_conversation": "Заглушить тред", "status.open": "Развернуть статус", "status.pin": "Закрепить в профиле", @@ -248,7 +248,7 @@ "tabs_bar.home": "Главная", "tabs_bar.local_timeline": "Локальная", "tabs_bar.notifications": "Уведомления", - "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.", "upload_area.title": "Перетащите сюда, чтобы загрузить", "upload_button.label": "Добавить медиаконтент", "upload_form.description": "Описать для людей с нарушениями зрения", diff --git a/config/locales/devise.ru.yml b/config/locales/devise.ru.yml index b5b0321bd..f80f7ad05 100644 --- a/config/locales/devise.ru.yml +++ b/config/locales/devise.ru.yml @@ -17,11 +17,32 @@ ru: unconfirmed: Вам необходимо подтвердить ваш адрес e-mail для продолжения. mailer: confirmation_instructions: - subject: 'Mastodon: Инструкция по подтверждению' + action: Подтвердите e-mail адрес + explanation: Вы создали учётную запись на сайте %{host}, используя этот e-mail адрес. Остался лишь один шаг для активации. Если это были не вы, просто игнорируйте письмо. + extra_html: Пожалуйста, ознакомьтесь правилами узла and условиями пользования Сервисом. + subject: 'Mastodon: Инструкция по подтверждению на узле %{instance}' + title: Подтвердите e-mail адрес + email_changed: + explanation: 'E-mail адрес Вашей учётной записи будет изменён на:' + extra: Если Вы не меняли адрес e-mail, возможно кто-то получил доступ к Вашей учётной записи. Пожалуйста, срочно смените пароль или свяжитесь с администратором узла, если у Вас нет доступа к учётной записи + subject: 'Mastodon: Адрес e-mail изменён' + title: Новый адрес e-mail password_change: + explanation: Пароль Вашей учётной записи был изменён. + extra: Если Вы не меняли пароль, возможно кто-то получил доступ к Вашей учётной записи. Пожалуйста, срочно смените пароль или свяжитесь с администратором узла, если у Вас нет доступа к учётной записи. subject: 'Mastodon: Пароль изменен' + title: Пароль изменён + reconfirmation_instructions: + explanation: Подтвердите новый адрес для смены e-mail. + extra: Если смену e-mail инициировали не Вы, пожалуйста, игнорируйте это письмо. Адрес e-mail для учётной записи Mastodon не будет изменён, пока Вы не перейдёте по ссылке выше. + subject: 'Mastodon: Подтверждение e-mail для узла %{instance}' + title: Подтвердите e-mail адрес reset_password_instructions: - subject: 'Mastodon: Инструкция по сбросу пароля' + action: Смена пароля + explanation: Вы запросили новый пароль для Вашей учётной записи. + extra: Если это сделали не Вы, пожалуйста, игнорируйте письмо. Ваш пароль не будет изменён, пока Вы не перейдёте по ссылке выше и не создадите новый пароль. + subject: 'Mastodon: инструкция по смене пароля' + title: Сброс пароля unlock_instructions: subject: 'Mastodon: Инструкция по разблокировке' omniauth_callbacks: diff --git a/config/locales/doorkeeper.ru.yml b/config/locales/doorkeeper.ru.yml index 2234a9bbe..05c3d971c 100644 --- a/config/locales/doorkeeper.ru.yml +++ b/config/locales/doorkeeper.ru.yml @@ -5,6 +5,8 @@ ru: doorkeeper/application: name: Название redirect_uri: URI перенаправления + scopes: Права + website: Веб-сайт приложения errors: models: doorkeeper/application: @@ -33,9 +35,13 @@ ru: redirect_uri: Используйте по одной строке на URI scopes: Разделяйте список разрешений пробелами. Оставьте незаполненным для использования разрешений по умолчанию. index: + application: Приложение callback_url: Callback URL + delete: Удалить name: Название new: Новое Приложение + scopes: Права + show: Показывать title: Ваши приложения new: title: Новое Приложение diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index 1b780ac26..e34140b7c 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -34,10 +34,12 @@ ru: data: Данные display_name: Показываемое имя email: Адрес e-mail + expires_in: Срок действия filtered_languages: Фильтруемые языки header: Заголовок locale: Язык locked: Сделать аккаунт приватным + max_uses: Макс. число использований new_password: Новый пароль note: О Вас otp_attempt: Двухфакторный код @@ -49,8 +51,8 @@ ru: setting_delete_modal: Показывать диалог подтверждения перед удалением setting_noindex: Отказаться от индексации в поисковых машинах setting_reduce_motion: Уменьшить движение в анимации - setting_site_theme: Тема сайта setting_system_font_ui: Использовать шрифт системы по умолчанию + setting_theme: Тема сайта setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта severity: Строгость type: Тип импорта @@ -58,6 +60,7 @@ ru: interactions: must_be_follower: Заблокировать уведомления не от подписчиков must_be_following: Заблокировать уведомления от людей, на которых Вы не подписаны + must_be_following_dm: Заблокировать личные сообщения от людей, на которых Вы не подписаны notification_emails: digest: Присылать дайджест по e-mail favourite: Уведомлять по e-mail, когда кому-то нравится Ваш статус From ffb2b8ef8c3c7cd6f57860240378fac8d5964105 Mon Sep 17 00:00:00 2001 From: abcang Date: Fri, 2 Feb 2018 01:17:17 +0900 Subject: [PATCH 003/257] Fix button hiding when header title is too long (#6406) --- .../mastodon/components/column_header.js | 4 +--- .../styles/mastodon/components.scss | 19 ++++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index c300db89b..6b79ec02d 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -133,9 +133,7 @@ export default class ColumnHeader extends React.PureComponent {

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index bfca34f4d..2beb19aff 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1913,7 +1913,7 @@ font-family: inherit; color: $ui-highlight-color; cursor: pointer; - flex: 0 0 auto; + white-space: nowrap; font-size: 16px; padding: 0 5px 0 0; z-index: 3; @@ -2403,15 +2403,16 @@ overflow: hidden; & > button { - display: flex; - flex: auto; margin: 0; border: none; - padding: 15px; + padding: 15px 0 15px 15px; color: inherit; background: transparent; font: inherit; text-align: left; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } &.active { @@ -2432,7 +2433,7 @@ .column-header__buttons { height: 48px; display: flex; - margin-left: 0; + margin-left: auto; } .column-header__links .text-btn { @@ -2512,14 +2513,6 @@ } } -.column-header__title { - display: inline-block; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - flex: 1; -} - .text-btn { display: inline-block; padding: 0; From f4bd51da1e4236fce5d46d76136bb2ef4a0e51ed Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Thu, 1 Feb 2018 16:54:22 +0000 Subject: [PATCH 004/257] Upgrade Paperclip > 5.2.1 (#6404) Mitigation for CVE-2017-0889. https://www.cvedetails.com/cve/CVE-2017-0889/ https://medium.com/in-the-weeds/all-about-paperclips-cve-2017-0889-server-side-request-forgery-ssrf-vulnerability-8cb2b1c96fe8 --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b116318a7..b3bd6fcb0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -215,7 +215,7 @@ GEM httplog (0.99.7) colorize rack - i18n (0.9.1) + i18n (0.9.3) concurrent-ruby (~> 1.0) i18n-tasks (0.9.19) activesupport (>= 4.0.2) @@ -284,7 +284,7 @@ GEM mimemagic (0.3.2) mini_mime (1.0.0) mini_portile2 (2.3.0) - minitest (5.10.3) + minitest (5.11.3) msgpack (1.1.0) multi_json (1.12.2) net-scp (1.2.1) @@ -307,7 +307,7 @@ GEM http (~> 3.0) nokogiri (~> 1.8) ox (2.8.2) - paperclip (5.1.0) + paperclip (5.2.1) activemodel (>= 4.2.0) activesupport (>= 4.2.0) cocaine (~> 0.5.5) From 1afc70c990d4d23e5fac57de9cb579c396a82b5c Mon Sep 17 00:00:00 2001 From: abcang Date: Fri, 2 Feb 2018 18:10:18 +0900 Subject: [PATCH 005/257] Fix mistake in cache deletion (#6408) --- spec/models/setting_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index e99dfc0d7..bbba5f98d 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -52,7 +52,7 @@ RSpec.describe Setting, type: :model do allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object) allow(described_class).to receive(:default_settings).and_return(default_settings) allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records) - Rails.cache.clear(cache_key) + Rails.cache.delete(cache_key) end let(:object) { nil } From 04fef7b8886bb78f3473e143894a521ca578f1db Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 2 Feb 2018 10:18:55 +0100 Subject: [PATCH 006/257] pam authentication (#5303) * add pam support, without extra column * bugfixes for pam login * document options * fix code style * fix codestyle * fix tests * don't call remember_me without password * fix codestyle * improve checks for pam usage (should fix tests) * fix remember_me part 1 * add remember_token column because :rememberable requires either a password or this column. * migrate db for remember_token * move pam_authentication to the right place, fix logic bug in edit.html.haml * fix tests * fix pam authentication, improve username lookup, add comment * valid? is sometimes not honored, return nil instead trying to authenticate with pam * update devise_pam_authenticatable2 and adjust code. Fixes sideeffects observed in tests * update devise_pam_authenticatable gem, fixes for codeconventions, fix finding user * codeconvention fixes * code convention fixes * fix idention * update dependency, explicit conflict check * fix disabled password updates if in pam mode * fix check password if password is present, fix templates * block registration if account is maintained by pam * Revert "block registration if account is maintained by pam" This reverts commit 8e7a083d650240b6fac414926744b4b90b435f20. * fix identation error introduced by rebase * block usernames maintained by pam * document pam settings better * fix code style --- Gemfile | 3 + Gemfile.lock | 5 ++ app/controllers/application_controller.rb | 5 ++ .../auth/registrations_controller.rb | 5 ++ app/controllers/auth/sessions_controller.rb | 6 +- app/models/user.rb | 69 +++++++++++++++++++ .../unreserved_username_validator.rb | 6 ++ app/views/auth/passwords/edit.html.haml | 18 +++-- app/views/auth/registrations/edit.html.haml | 15 ++-- app/views/auth/sessions/new.html.haml | 5 +- config/initializers/devise.rb | 34 ++++++++- config/locales/simple_form.de.yml | 1 + config/locales/simple_form.en.yml | 1 + ...80109143959_add_remember_token_to_users.rb | 5 ++ db/schema.rb | 3 +- 15 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20180109143959_add_remember_token_to_users.rb diff --git a/Gemfile b/Gemfile index eaa1d29de..f3844aca6 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,9 @@ gem 'iso-639' gem 'cld3', '~> 3.2.0' gem 'devise', '~> 4.4' gem 'devise-two-factor', '~> 3.0' + +gem 'devise_pam_authenticatable2', '~> 8.0' + gem 'doorkeeper', '~> 4.2' gem 'fast_blank', '~> 1.0' gem 'goldfinger', '~> 2.1' diff --git a/Gemfile.lock b/Gemfile.lock index b3bd6fcb0..7da9bfe39 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -137,6 +137,9 @@ GEM devise (~> 4.0) railties (< 5.2) rotp (~> 2.0) + devise_pam_authenticatable2 (8.0.1) + devise (>= 4.0.0) + rpam2 (~> 3.0) diff-lcs (1.3) docile (1.1.5) domain_name (0.5.20170404) @@ -420,6 +423,7 @@ GEM actionpack (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3) rotp (2.1.2) + rpam2 (3.1.0) rqrcode (0.10.1) chunky_png (~> 1.0) rspec-core (3.7.0) @@ -570,6 +574,7 @@ DEPENDENCIES climate_control (~> 0.2) devise (~> 4.4) devise-two-factor (~> 3.0) + devise_pam_authenticatable2 (~> 8.0) doorkeeper (~> 4.2) dotenv-rails (~> 2.2) fabrication (~> 2.18) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e1aae0b67..b38a68467 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base helper_method :current_session helper_method :current_theme helper_method :single_user_mode? + helper_method :use_pam? rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found @@ -75,6 +76,10 @@ class ApplicationController < ActionController::Base @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? end + def use_pam? + Devise.pam_authentication + end + def current_account @current_account ||= current_user.try(:account) end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index b8ff4e54f..417e2b63b 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -14,6 +14,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController protected + def update_resource(resource, params) + params[:password] = nil if Devise.pam_authentication && resource.encrypted_password.blank? + super + end + def build_resource(hash = nil) super(hash) diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index a5acb6c36..4fc41b378 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -28,7 +28,11 @@ class Auth::SessionsController < Devise::SessionsController if session[:otp_user_id] User.find(session[:otp_user_id]) elsif user_params[:email] - User.find_for_authentication(email: user_params[:email]) + if use_pam? && Devise.check_at_sign && user_params[:email].index('@').nil? + User.joins(:account).find_by(accounts: { username: user_params[:email] }) + else + User.find_for_authentication(email: user_params[:email]) + end end end diff --git a/app/models/user.rb b/app/models/user.rb index 40c298b1a..fa4ebfc71 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -34,6 +34,7 @@ # disabled :boolean default(FALSE), not null # moderator :boolean default(FALSE), not null # invite_id :integer +# remember_token :string # class User < ApplicationRecord @@ -50,6 +51,8 @@ class User < ApplicationRecord devise :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable + devise :pam_authenticatable + belongs_to :account, inverse_of: :user belongs_to :invite, counter_cache: :uses, optional: true accepts_nested_attributes_for :account @@ -84,6 +87,33 @@ class User < ApplicationRecord attr_accessor :invite_code + def pam_conflict(_) + # block pam login tries on traditional account + nil + end + + def pam_conflict? + return false unless Devise.pam_authentication + encrypted_password.present? && is_pam_account? + end + + def pam_get_name + return account.username if account.present? + super + end + + def pam_setup(_attributes) + acc = Account.new(username: pam_get_name) + acc.save!(validate: false) + + self.email = "#{acc.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix + self.confirmed_at = Time.now.utc + self.admin = false + self.account = acc + + acc.destroy! unless save + end + def confirmed? confirmed_at.present? end @@ -213,6 +243,45 @@ class User < ApplicationRecord @invite_code = code end + def password_required? + return false if Devise.pam_authentication + super + end + + def send_reset_password_instructions + return false if encrypted_password.blank? && Devise.pam_authentication + super + end + + def reset_password!(new_password, new_password_confirmation) + return false if encrypted_password.blank? && Devise.pam_authentication + super + end + + def self.pam_get_user(attributes = {}) + if attributes[:email] + resource = + if Devise.check_at_sign && !attributes[:email].index('@') + joins(:account).find_by(accounts: { username: attributes[:email] }) + else + find_by(email: attributes[:email]) + end + + if resource.blank? + resource = new(email: attributes[:email]) + if Devise.check_at_sign && !resource[:email].index('@') + resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" + end + end + resource + end + end + + def self.authenticate_with_pam(attributes = {}) + return nil unless Devise.pam_authentication + super + end + protected def send_devise_notification(notification, *args) diff --git a/app/validators/unreserved_username_validator.rb b/app/validators/unreserved_username_validator.rb index 44ea4359b..c2311a89a 100644 --- a/app/validators/unreserved_username_validator.rb +++ b/app/validators/unreserved_username_validator.rb @@ -8,7 +8,13 @@ class UnreservedUsernameValidator < ActiveModel::Validator private + def pam_controlled?(value) + return false unless Devise.pam_authentication && Devise.pam_controlled_service + Rpam2.account(Devise.pam_controlled_service, value).present? + end + def reserved_username?(value) + return true if pam_controlled?(value) return false unless Setting.reserved_usernames Setting.reserved_usernames.include?(value.downcase) end diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml index 5ef3de976..d8fed9e77 100644 --- a/app/views/auth/passwords/edit.html.haml +++ b/app/views/auth/passwords/edit.html.haml @@ -1,14 +1,18 @@ - content_for :page_title do = t('auth.set_new_password') -= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| - = render 'shared/error_messages', object: resource - = f.input :reset_password_token, as: :hidden + = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| + = render 'shared/error_messages', object: resource - = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } - = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } + - if use_pam? || current_user.encrypted_password.present? + = f.input :reset_password_token, as: :hidden - .actions - = f.button :button, t('auth.set_new_password'), type: :submit + = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } + + .actions + = f.button :button, t('auth.set_new_password'), type: :submit + - else + = t('simple_form.labels.defaults.pam_account') .form-footer= render 'auth/shared/links' diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 145f5cd9e..102199f81 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -4,13 +4,16 @@ = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f| = render 'shared/error_messages', object: resource - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } - = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } - = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' } + - if !use_pam? || current_user.encrypted_password.present? + = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } + = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' } - .actions - = f.button :button, t('generic.save_changes'), type: :submit + .actions + = f.button :button, t('generic.save_changes'), type: :submit + - else + = t('simple_form.labels.defaults.pam_account') %hr/ diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index a52b0053b..3edb0d2d4 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -5,7 +5,10 @@ = render partial: 'shared/og' = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + - if use_pam? + = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.username_or_email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') } + - else + = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } .actions diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 07912c28b..f2f7f1ba3 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -30,6 +30,19 @@ Warden::Manager.before_logout do |_, warden| warden.cookies.delete('_session_id') end +module Devise + mattr_accessor :pam_authentication + @@pam_authentication = false + mattr_accessor :pam_controlled_service + @@pam_controlled_service = nil + + class Strategies::PamAuthenticatable + def valid? + super && ::Devise.pam_authentication + end + end +end + Devise.setup do |config| config.warden do |manager| manager.default_strategies(scope: :user).unshift :two_factor_authenticatable @@ -96,7 +109,7 @@ Devise.setup do |config| # given strategies, for example, `config.http_authenticatable = [:database]` will # enable it only for database authentication. The supported strategies are: # :database = Support basic authentication with authentication key + password - config.http_authenticatable = [:database] + config.http_authenticatable = [:pam, :database] # If 401 status code should be returned for AJAX requests. True by default. # config.http_authenticatable_on_xhr = true @@ -301,4 +314,23 @@ Devise.setup do |config| # When using OmniAuth, Devise cannot automatically set OmniAuth path, # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' + + # PAM: only look for email field + config.usernamefield = nil + config.emailfield = "email" + + # authentication with pam possible + # if not enabled, all pam settings are ignored + #config.pam_authentication = true + # check if email is actually a username + config.check_at_sign = true + # suffix for email address generation (warning: without pam must provide email in the pam environment) + config.pam_default_suffix = "pam" + # name of the pam service + # pam "auth" section is evaluated + config.pam_default_service = "rpam" + # name of the pam service used for checking if an user can register + # pam "account" section is evaluated + # nil for allowing registration of pam names (not recommended) + config.pam_controlled_service = "rpam" end diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 3c5e467a2..bb78ae21a 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -53,6 +53,7 @@ de: severity: Gewichtung type: Importtyp username: Profilname + username_or_email: Profilname oder Email interactions: must_be_follower: Benachrichtigungen von Nicht-Folgenden blockieren must_be_following: Benachrichtigungen von Profilen blockieren, denen ich nicht folge diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 143daaa29..c56334d56 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -53,6 +53,7 @@ en: severity: Severity type: Import type username: Username + username_or_email: Username or Email interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow diff --git a/db/migrate/20180109143959_add_remember_token_to_users.rb b/db/migrate/20180109143959_add_remember_token_to_users.rb new file mode 100644 index 000000000..662905bcb --- /dev/null +++ b/db/migrate/20180109143959_add_remember_token_to_users.rb @@ -0,0 +1,5 @@ +class AddRememberTokenToUsers < ActiveRecord::Migration[5.1] + def change + add_column :users, :remember_token, :string, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index d1722fa29..a411de20f 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: 20180106000232) do +ActiveRecord::Schema.define(version: 20180109143959) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -486,6 +486,7 @@ ActiveRecord::Schema.define(version: 20180106000232) do t.boolean "disabled", default: false, null: false t.boolean "moderator", default: false, null: false t.bigint "invite_id" + t.string "remember_token" t.index ["account_id"], name: "index_users_on_account_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true From 8e4cf6282b8a3bcb100506b27ecaed3e88832681 Mon Sep 17 00:00:00 2001 From: puckipedia Date: Fri, 2 Feb 2018 10:19:59 +0100 Subject: [PATCH 007/257] Allow retrieval of private statuses (single or in outbox) using HTTP signatures (#6225) --- app/controllers/activitypub/outboxes_controller.rb | 4 +++- app/controllers/concerns/signature_authentication.rb | 11 +++++++++++ app/controllers/statuses_controller.rb | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 app/controllers/concerns/signature_authentication.rb diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 9f97ff622..a431e3557 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true class ActivityPub::OutboxesController < Api::BaseController + include SignatureVerification + before_action :set_account def show - @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) + @statuses = @account.statuses.permitted_for(@account, signed_request_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = cache_collection(@statuses, Status) render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' diff --git a/app/controllers/concerns/signature_authentication.rb b/app/controllers/concerns/signature_authentication.rb new file mode 100644 index 000000000..beec93223 --- /dev/null +++ b/app/controllers/concerns/signature_authentication.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module SignatureAuthentication + extend ActiveSupport::Concern + + include SignatureVerification + + def current_account + super || signed_request_account + end +end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 367ea34e7..45226c8d2 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class StatusesController < ApplicationController + include SignatureAuthentication include Authorization layout 'public' From 0be9a1e3212b0b9918abe1536e51efe2fefa49f1 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Fri, 2 Feb 2018 18:22:15 +0900 Subject: [PATCH 008/257] Accept ActivityPub announce from the author of the original note (#6236) --- app/lib/activitypub/activity/announce.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index abf2b9b80..c8a358195 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -15,7 +15,8 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity account: @account, reblog: original_status, uri: @json['id'], - created_at: @options[:override_timestamps] ? nil : @json['published'] + created_at: @options[:override_timestamps] ? nil : @json['published'], + visibility: original_status.visibility ) distribute(status) @@ -35,6 +36,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity end def announceable?(status) - status.public_visibility? || status.unlisted_visibility? + status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? end end From 5da5c65db8557abd7c6be15842189b9d83e85079 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Fri, 2 Feb 2018 18:32:21 +0900 Subject: [PATCH 009/257] Unify links container implementation in about pages (#6382) They were redundant, and also had a inconsistency; the button for "other instances" had an icon for the external link in "more" page, but it didn't in the other pages. This unifies the implementation, and the external link icon is now shown in all the about pages. --- app/views/about/_links.html.haml | 16 ++++++++++++++++ app/views/about/more.html.haml | 17 +---------------- app/views/about/show.html.haml | 17 +---------------- app/views/about/terms.html.haml | 14 +------------- 4 files changed, 19 insertions(+), 45 deletions(-) create mode 100644 app/views/about/_links.html.haml diff --git a/app/views/about/_links.html.haml b/app/views/about/_links.html.haml new file mode 100644 index 000000000..ccf4f08b9 --- /dev/null +++ b/app/views/about/_links.html.haml @@ -0,0 +1,16 @@ +.container.links + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + + %ul.nav + %li + - if user_signed_in? + = link_to t('settings.back'), root_url, class: 'webapp-btn' + - else + = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' + %li= link_to t('about.about_this'), about_more_path + %li + = link_to 'https://joinmastodon.org/' do + = "#{t('about.other_instances')}" + %i.fa.fa-external-link{ style: 'padding-left: 5px;' } diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index b012606ce..9c9580eac 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -8,22 +8,7 @@ .landing-page .header-wrapper.compact .header - .container.links - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - - %ul.nav - %li - - if user_signed_in? - = link_to t('settings.back'), root_url, class: 'webapp-btn' - - else - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - %li= link_to t('about.about_this'), about_more_path - %li - = link_to 'https://joinmastodon.org/' do - = "#{t('about.other_instances')}" - %i.fa.fa-external-link{ style: 'padding-left: 5px;' } + = render 'links' .container.hero .heading diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index f8f90ce24..b7c08479d 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -12,22 +12,7 @@ = image_tag asset_pack_path('elephant-fren.png'), alt: '', role: 'presentation', class: 'mascot' .header - .container.links - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - - %ul.nav - %li - - if user_signed_in? - = link_to t('settings.back'), root_url, class: 'webapp-btn' - - else - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - %li= link_to t('about.about_this'), about_more_path - %li - = link_to 'https://joinmastodon.org/' do - = "#{t('about.other_instances')}" - %i.fa.fa-external-link{ style: 'padding-left: 5px;' } + = render 'links' .container.hero .floats diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml index 7004cb0b1..ba780759c 100644 --- a/app/views/about/terms.html.haml +++ b/app/views/about/terms.html.haml @@ -4,19 +4,7 @@ .landing-page .header-wrapper.compact .header - .container.links - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - - %ul.nav - %li - - if user_signed_in? - = link_to t('settings.back'), root_url, class: 'webapp-btn' - - else - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - %li= link_to t('about.about_this'), about_more_path - %li= link_to t('about.other_instances'), 'https://joinmastodon.org/' + = render 'links' .extended-description .container From 77dd9e7d2728fb0f0e52718c3544ef6898af4fff Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Fri, 2 Feb 2018 18:32:41 +0900 Subject: [PATCH 010/257] Remove wave from list drawer (#6381) --- app/javascript/mastodon/features/compose/index.js | 2 +- app/javascript/styles/mastodon/components.scss | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 84e3a2338..f809bafcf 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -91,7 +91,7 @@ export default class Compose extends React.PureComponent {
-
+
{multiColumn &&
} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 2beb19aff..2b38f7ae4 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1766,7 +1766,7 @@ position: absolute; top: 0; left: 0; - background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; + background: lighten($ui-base-color, 13%); box-sizing: border-box; padding: 0; display: flex; @@ -1779,6 +1779,10 @@ &.darker { background: $ui-base-color; } +} + +.drawer__inner--with-mastodon { + background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; > .mastodon { background: url('../images/elephant_ui_plane.svg') no-repeat left bottom / contain; From af4082499873f54047050655ee63a2fdc3b53b99 Mon Sep 17 00:00:00 2001 From: Charlotte Fields Date: Fri, 2 Feb 2018 20:45:43 +1100 Subject: [PATCH 011/257] moved save button (#3792) * moved save button * added save back to the bottom * Update show.html.haml --- app/views/settings/preferences/show.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 69e26a7be..441e27a68 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -4,6 +4,9 @@ = simple_form_for current_user, url: settings_preferences_path, html: { method: :put } do |f| = render 'shared/error_messages', object: current_user + .actions + = f.button :button, t('generic.save_changes'), type: :submit + %h4= t 'preferences.languages' .fields-group From ac1093256c500f55a6578836c3364d4a8a67ee58 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 2 Feb 2018 10:54:04 +0100 Subject: [PATCH 012/257] Allow HTTP caching of atom-rendered public toots (OStatus compatibility) (#6207) --- app/controllers/stream_entries_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index cc579dbc8..f81856cc6 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -10,6 +10,7 @@ class StreamEntriesController < ApplicationController before_action :set_stream_entry before_action :set_link_headers before_action :check_account_suspension + before_action :set_cache_headers def show respond_to do |format| @@ -19,6 +20,10 @@ class StreamEntriesController < ApplicationController end format.atom do + unless @stream_entry.hidden? + skip_session! + expires_in 3.minutes, public: true + end render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) end end From c1efe0aa1d7ab43aa74387df6f1d56a56ec268de Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Fri, 2 Feb 2018 19:56:50 +0900 Subject: [PATCH 013/257] Set minimum height for mastodon on drawer (#6142) --- app/javascript/mastodon/features/compose/index.js | 9 +++++++-- app/javascript/styles/mastodon/components.scss | 13 +++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index f809bafcf..138bc4e2e 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -12,6 +12,7 @@ import Motion from '../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import SearchResultsContainer from './containers/search_results_container'; import { changeComposing } from '../../actions/compose'; +import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; const messages = defineMessages({ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -91,10 +92,14 @@ export default class Compose extends React.PureComponent {
-
+
- {multiColumn &&
} + {multiColumn && ( +
+ +
+ )}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 2b38f7ae4..6359e9d0d 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1781,12 +1781,17 @@ } } -.drawer__inner--with-mastodon { +.drawer__inner__mastodon { background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; + flex: 1; + min-height: 47px; - > .mastodon { - background: url('../images/elephant_ui_plane.svg') no-repeat left bottom / contain; - flex: 1; + > img { + display: block; + object-fit: contain; + object-position: bottom left; + width: 100%; + height: 100%; } } From 7e5c433dfce68dea0af09784753f2f4f3003f2a3 Mon Sep 17 00:00:00 2001 From: abcang Date: Fri, 2 Feb 2018 19:57:59 +0900 Subject: [PATCH 014/257] Fix saving of oEmbed image (#6409) --- app/services/fetch_link_card_service.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index d0472a1d7..3e31a4145 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -91,13 +91,13 @@ class FetchLinkCardService < BaseService case @card.type when 'link' - @card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url) + @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url) when 'photo' return false unless embed.respond_to?(:url) - @card.embed_url = embed.url - @card.image = URI.parse(embed.url) - @card.width = embed.width.presence || 0 - @card.height = embed.height.presence || 0 + @card.embed_url = embed.url + @card.image_remote_url = embed.url + @card.width = embed.width.presence || 0 + @card.height = embed.height.presence || 0 when 'video' @card.width = embed.width.presence || 0 @card.height = embed.height.presence || 0 From 33f56811e38bc330de9dcfa6794c29a176a30311 Mon Sep 17 00:00:00 2001 From: abcang Date: Fri, 2 Feb 2018 21:31:28 +0900 Subject: [PATCH 015/257] Fix column header button (#6411) --- app/javascript/styles/mastodon/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 6359e9d0d..c2c9a040f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2422,6 +2422,7 @@ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; + flex: 1; } &.active { @@ -2442,7 +2443,6 @@ .column-header__buttons { height: 48px; display: flex; - margin-left: auto; } .column-header__links .text-btn { From f7bf36d8fc5f8a7638a2cee215513c1dcd4f4a96 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sun, 4 Feb 2018 02:41:01 +0900 Subject: [PATCH 016/257] Require environment for generate_static_pages (#6420) It is required for ApplicationController. --- lib/tasks/assets.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index f60c1b9f2..0826f0186 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -9,7 +9,7 @@ end namespace :assets do desc 'Generate static pages' - task :generate_static_pages do + task generate_static_pages: :environment do render_static_page 'errors/500', layout: 'error', dest: Rails.root.join('public', 'assets', '500.html') end end From d75d2a9f9960f08bbcacd4f5acb86243dbdb3179 Mon Sep 17 00:00:00 2001 From: takayamaki Date: Sun, 4 Feb 2018 02:41:51 +0900 Subject: [PATCH 017/257] fix ColumnBackButtonSlim should extended from ColumnBackButton (#6417) --- .../mastodon/components/column_back_button_slim.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app/javascript/mastodon/components/column_back_button_slim.js b/app/javascript/mastodon/components/column_back_button_slim.js index 3b4f46d99..964c100be 100644 --- a/app/javascript/mastodon/components/column_back_button_slim.js +++ b/app/javascript/mastodon/components/column_back_button_slim.js @@ -1,17 +1,8 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; +import ColumnBackButton from './column_back_button'; -export default class ColumnBackButtonSlim extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - handleClick = () => { - if (window.history && window.history.length === 1) this.context.router.history.push('/'); - else this.context.router.history.goBack(); - } +export default class ColumnBackButtonSlim extends ColumnBackButton { render () { return ( From 9da81a16391edfcbda9c748dcd519fb3ebd765e5 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sun, 4 Feb 2018 02:44:22 +0900 Subject: [PATCH 018/257] Isolate internal services from external networks in Docker configuration (#6369) The database and Redis do not need external connections, so isolate them and prevent unauthorized access. --- docker-compose.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index cfe70c5e8..aaa3a4478 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ services: db: restart: always image: postgres:9.6-alpine + networks: + - internal_network ### Uncomment to enable DB persistance # volumes: # - ./postgres:/var/lib/postgresql/data @@ -11,6 +13,8 @@ services: redis: restart: always image: redis:4.0-alpine + networks: + - internal_network ### Uncomment to enable REDIS persistance # volumes: # - ./redis:/data @@ -21,6 +25,9 @@ services: restart: always env_file: .env.production command: bundle exec rails s -p 3000 -b '0.0.0.0' + networks: + - external_network + - internal_network ports: - "3000:3000" depends_on: @@ -37,6 +44,9 @@ services: restart: always env_file: .env.production command: npm run start + networks: + - external_network + - internal_network ports: - "4000:4000" depends_on: @@ -52,6 +62,14 @@ services: depends_on: - db - redis + networks: + - external_network + - internal_network volumes: - ./public/packs:/mastodon/public/packs - ./public/system:/mastodon/public/system + +networks: + external_network: + internal_network: + internal: true From 26f21fd5a03b1c6407cd81c58481288d06958ad3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 4 Feb 2018 05:42:13 +0100 Subject: [PATCH 019/257] CAS + SAML authentication feature (#6425) * Cas authentication feature * Config * Remove class_eval + Omniauth initializer * Codeclimate review * Codeclimate review 2 * Codeclimate review 3 * Remove uid/email reconciliation * SAML authentication * Clean up code * Improve login form * Fix code style issues * Add locales --- .env.production.sample | 44 +++++++++- Gemfile | 3 + Gemfile.lock | 16 ++++ .../auth/confirmations_controller.rb | 24 ++++++ .../auth/omniauth_callbacks_controller.rb | 33 ++++++++ app/javascript/styles/mastodon/forms.scss | 18 +++++ app/models/concerns/omniauthable.rb | 81 +++++++++++++++++++ app/models/identity.rb | 22 +++++ app/models/user.rb | 2 + .../confirmations/finish_signup.html.haml | 14 ++++ app/views/auth/sessions/new.html.haml | 9 +++ config/i18n-tasks.yml | 1 + config/initializers/omniauth.rb | 59 ++++++++++++++ config/locales/en.yml | 5 ++ config/locales/fr.yml | 2 + config/routes.rb | 2 + .../20180204034416_create_identities.rb | 11 +++ db/schema.rb | 12 ++- spec/fabricators/identity_fabricator.rb | 5 ++ spec/models/identity_spec.rb | 5 ++ 20 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 app/controllers/auth/omniauth_callbacks_controller.rb create mode 100644 app/models/concerns/omniauthable.rb create mode 100644 app/models/identity.rb create mode 100644 app/views/auth/confirmations/finish_signup.html.haml create mode 100644 config/initializers/omniauth.rb create mode 100644 db/migrate/20180204034416_create_identities.rb create mode 100644 spec/fabricators/identity_fabricator.rb create mode 100644 spec/models/identity_spec.rb diff --git a/.env.production.sample b/.env.production.sample index 3f0edd72f..777336de1 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -13,7 +13,7 @@ DB_PORT=5432 # Federation # Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation. # LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com. -LOCAL_DOMAIN=example.com +LOCAL_DOMAIN=example.com # Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links) @@ -58,7 +58,7 @@ VAPID_PUBLIC_KEY= # E-mail configuration # Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers # If you want to use an SMTP server without authentication (e.g local Postfix relay) -# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and +# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and # *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough). SMTP_SERVER=smtp.mailgun.org SMTP_PORT=587 @@ -135,3 +135,43 @@ STREAMING_CLUSTER_NUM=1 # If you use Docker, you may want to assign UID/GID manually. # UID=1000 # GID=1000 + +# Optional CAS authentication (cf. omniauth-cas) : +# CAS_ENABLED=true +# CAS_URL=https://sso.myserver.com/ +# CAS_HOST=sso.myserver.com/ +# CAS_PORT=443 +# CAS_SSL=true +# CAS_VALIDATE_URL= +# CAS_CALLBACK_URL= +# CAS_LOGOUT_URL= +# CAS_LOGIN_URL= +# CAS_UID_FIELD='user' +# CAS_CA_PATH= +# CAS_DISABLE_SSL_VERIFICATION=false +# CAS_UID_KEY='user' +# CAS_NAME_KEY='name' +# CAS_EMAIL_KEY='email' +# CAS_NICKNAME_KEY='nickname' +# CAS_FIRST_NAME_KEY='firstname' +# CAS_LAST_NAME_KEY='lastname' +# CAS_LOCATION_KEY='location' +# CAS_IMAGE_KEY='image' +# CAS_PHONE_KEY='phone' + +# Optional SAML authentication (cf. omniauth-saml) +# SAML_ENABLED=true +# SAML_ACS_URL= +# SAML_ISSUER=http://localhost:3000/auth/auth/saml/metadata +# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO +# SAML_IDP_CERT= +# SAML_IDP_CERT_FINGERPRINT= +# SAML_NAME_IDENTIFIER_FORMAT= +# SAML_CERT= +# SAML_PRIVATE_KEY= +# SAML_SECURITY_WANT_ASSERTION_SIGNED=true +# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true +# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1" +# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" +# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42" +# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1" diff --git a/Gemfile b/Gemfile index f3844aca6..5b6ae707d 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,9 @@ gem 'devise', '~> 4.4' gem 'devise-two-factor', '~> 3.0' gem 'devise_pam_authenticatable2', '~> 8.0' +gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' } +gem 'omniauth-saml', '~> 1.8', install_if: -> { ENV['SAML_ENABLED'] == 'true' } +gem 'omniauth', '~> 1.2' gem 'doorkeeper', '~> 4.2' gem 'fast_blank', '~> 1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 7da9bfe39..c357bfbd1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -201,6 +201,7 @@ GEM hamster (3.0.0) concurrent-ruby (~> 1.0) hashdiff (0.3.7) + hashie (3.5.7) highline (1.7.10) hiredis (0.6.1) hkdf (0.3.0) @@ -304,6 +305,16 @@ GEM sidekiq (>= 3.5.0) statsd-ruby (~> 1.2.0) oj (3.3.10) + omniauth (1.8.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-cas (1.1.1) + addressable (~> 2.3) + nokogiri (~> 1.5) + omniauth (~> 1.2) + omniauth-saml (1.9.0) + omniauth (~> 1.3, >= 1.3.2) + ruby-saml (~> 1.4, >= 1.4.3) orm_adapter (0.5.0) ostatus2 (2.0.3) addressable (~> 2.5) @@ -455,6 +466,8 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-oembed (0.12.0) ruby-progressbar (1.9.0) + ruby-saml (1.6.1) + nokogiri (>= 1.5.10) rufus-scheduler (3.4.2) et-orbi (~> 1.0) safe_yaml (1.0.4) @@ -606,6 +619,9 @@ DEPENDENCIES nokogiri (~> 1.8) nsa (~> 0.2) oj (~> 3.3) + omniauth (~> 1.2) + omniauth-cas (~> 1.1) + omniauth-saml (~> 1.8) ostatus2 (~> 2.0) ox (~> 2.8) paperclip (~> 5.1) diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 2fdb281f4..a240425cd 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -2,4 +2,28 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController layout 'auth' + + before_action :set_user, only: [:finish_signup] + + # GET/PATCH /users/:id/finish_signup + def finish_signup + return unless request.patch? && params[:user] + if @user.update(user_params) + @user.skip_reconfirmation! + sign_in(@user, bypass: true) + redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions') + else + @show_errors = true + end + end + + private + + def set_user + @user = current_user + end + + def user_params + params.require(:user).permit(:email) + end end diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..bbf63bed3 --- /dev/null +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :verify_authenticity_token + + def self.provides_callback_for(provider) + provider_id = provider.to_s.chomp '_oauth2' + + define_method provider do + @user = User.find_for_oauth(request.env['omniauth.auth'], current_user) + + if @user.persisted? + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format? + else + session["devise.#{provider}_data"] = request.env['omniauth.auth'] + redirect_to new_user_registration_url + end + end + end + + Devise.omniauth_configs.each_key do |provider| + provides_callback_for provider + end + + def after_sign_in_path_for(resource) + if resource.email_verified? + root_path + else + finish_signup_path + end + end +end diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 2bef53cff..dec7d2284 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -568,3 +568,21 @@ code { margin-bottom: 4px; } } + +.alternative-login { + margin-top: 20px; + margin-bottom: 20px; + + h4 { + font-size: 16px; + color: $ui-base-lighter-color; + text-align: center; + margin-bottom: 20px; + border: 0; + padding: 0; + } + + .button { + display: block; + } +} diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb new file mode 100644 index 000000000..a3d55108d --- /dev/null +++ b/app/models/concerns/omniauthable.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Omniauthable + extend ActiveSupport::Concern + + TEMP_EMAIL_PREFIX = 'change@me' + TEMP_EMAIL_REGEX = /\Achange@me/ + + included do + def omniauth_providers + Devise.omniauth_configs.keys + end + + def email_verified? + email && email !~ TEMP_EMAIL_REGEX + end + end + + class_methods do + def find_for_oauth(auth, signed_in_resource = nil) + # EOLE-SSO Patch + auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array + identity = Identity.find_for_oauth(auth) + + # If a signed_in_resource is provided it always overrides the existing user + # to prevent the identity being locked with accidentally created accounts. + # Note that this may leave zombie accounts (with no associated identity) which + # can be cleaned up at a later date. + user = signed_in_resource ? signed_in_resource : identity.user + user = create_for_oauth(auth) if user.nil? + + if identity.user.nil? + identity.user = user + identity.save! + end + + user + end + + def create_for_oauth(auth) + # Check if the user exists with provided email if the provider gives us a + # verified email. If no verified email was provided or the user already + # exists, we assign a temporary email and ask the user to verify it on + # the next step via Auth::ConfirmationsController.finish_signup + + user = User.new(user_params_from_auth(auth)) + user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/ + user.skip_confirmation! + user.save! + user + end + + private + + def user_params_from_auth(auth) + email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email) + email = auth.info.email if email_is_verified && !User.exists?(email: auth.info.email) + + { + email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", + password: Devise.friendly_token[0, 20], + account_attributes: { + username: ensure_unique_username(auth.uid), + display_name: [auth.info.first_name, auth.info.last_name].join(' '), + }, + } + end + + def ensure_unique_username(starting_username) + username = starting_username + i = 0 + + while Account.exists?(username: username) + i += 1 + username = "#{starting_username}_#{i}" + end + + username + end + end +end diff --git a/app/models/identity.rb b/app/models/identity.rb new file mode 100644 index 000000000..a5e0c09ec --- /dev/null +++ b/app/models/identity.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: identities +# +# id :integer not null, primary key +# user_id :integer +# provider :string default(""), not null +# uid :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Identity < ApplicationRecord + belongs_to :user, dependent: :destroy + validates :uid, presence: true, uniqueness: { scope: :provider } + validates :provider, presence: true + + def self.find_for_oauth(auth) + find_or_create_by(uid: auth.uid, provider: auth.provider) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index fa4ebfc71..fba478453 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -39,6 +39,7 @@ class User < ApplicationRecord include Settings::Extend + include Omniauthable ACTIVE_DURATION = 14.days @@ -52,6 +53,7 @@ class User < ApplicationRecord :confirmable devise :pam_authenticatable + devise :omniauthable belongs_to :account, inverse_of: :user belongs_to :invite, counter_cache: :uses, optional: true diff --git a/app/views/auth/confirmations/finish_signup.html.haml b/app/views/auth/confirmations/finish_signup.html.haml new file mode 100644 index 000000000..4b5161d6b --- /dev/null +++ b/app/views/auth/confirmations/finish_signup.html.haml @@ -0,0 +1,14 @@ +- content_for :page_title do + = t('auth.confirm_email') + += simple_form_for(current_user, as: 'user', url: finish_signup_path, html: { role: 'form'}) do |f| + - if @show_errors && current_user.errors.any? + #error_explanation + - current_user.errors.full_messages.each do |msg| + = msg + %br + + = f.input :email + + .actions + = f.submit t('auth.confirm_email'), class: 'button' diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index 3edb0d2d4..1c3a0b6b4 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -14,4 +14,13 @@ .actions = f.button :button, t('auth.login'), type: :submit +- if devise_mapping.omniauthable? and resource_class.omniauth_providers.any? + .simple_form.alternative-login + %h4= t('auth.or_log_in_with') + + .actions + - resource_class.omniauth_providers.each do |provider| + = link_to omniauth_authorize_path(resource_name, provider), class: "button button-#{provider}" do + = t("auth.providers.#{provider}", default: provider.to_s.chomp("_oauth2").capitalize) + .form-footer= render 'auth/shared/links' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 014055804..bcd816d30 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -46,6 +46,7 @@ ignore_missing: - 'terms.body_html' - 'application_mailer.salutation' - 'errors.500' + - 'auth.providers.*' ignore_unused: - 'activemodel.errors.*' diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 000000000..97f32c0a4 --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,59 @@ +Rails.application.config.middleware.use OmniAuth::Builder do + # Vanilla omniauth stategies +end + +Devise.setup do |config| + # Devise omniauth strategies + + # CAS strategy + if ENV['CAS_ENABLED'] == 'true' + cas_options = {} + cas_options[:url] = ENV['CAS_URL'] if ENV['CAS_URL'] + cas_options[:host] = ENV['CAS_HOST'] if ENV['CAS_HOST'] + cas_options[:port] = ENV['CAS_PORT'] if ENV['CAS_PORT'] + cas_options[:ssl] = ENV['CAS_SSL'] == 'true' if ENV['CAS_SSL'] + cas_options[:validate_url] = ENV['CAS_VALIDATE_URL'] if ENV['CAS_VALIDATE_URL'] + cas_options[:callback_url] = ENV['CAS_CALLBACK_URL'] if ENV['CAS_CALLBACK_URL'] + cas_options[:logout_url] = ENV['CAS_LOGOUT_URL'] if ENV['CAS_LOGOUT_URL'] + cas_options[:login_url] = ENV['CAS_LOGIN_URL'] if ENV['CAS_LOGIN_URL'] + cas_options[:uid_field] = ENV['CAS_UID_FIELD'] || 'user' if ENV['CAS_UID_FIELD'] + cas_options[:ca_path] = ENV['CAS_CA_PATH'] if ENV['CAS_CA_PATH'] + cas_options[:disable_ssl_verification] = ENV['CAS_DISABLE_SSL_VERIFICATION'] == 'true' if ENV['CAS_DISABLE_SSL_VERIFICATION'] + cas_options[:uid_key] = ENV['CAS_UID_KEY'] || 'user' + cas_options[:name_key] = ENV['CAS_NAME_KEY'] || 'name' + cas_options[:email_key] = ENV['CAS_EMAIL_KEY'] || 'email' + cas_options[:nickname_key] = ENV['CAS_NICKNAME_KEY'] || 'nickname' + cas_options[:first_name_key] = ENV['CAS_FIRST_NAME_KEY'] || 'firstname' + cas_options[:last_name_key] = ENV['CAS_LAST_NAME_KEY'] || 'lastname' + cas_options[:location_key] = ENV['CAS_LOCATION_KEY'] || 'location' + cas_options[:image_key] = ENV['CAS_IMAGE_KEY'] || 'image' + cas_options[:phone_key] = ENV['CAS_PHONE_KEY'] || 'phone' + config.omniauth :cas, cas_options + end + + # SAML strategy + if ENV['SAML_ENABLED'] == 'true' + saml_options = {} + saml_options[:assertion_consumer_service_url] = ENV['SAML_ACS_URL'] if ENV['SAML_ACS_URL'] + saml_options[:issuer] = ENV['SAML_ISSUER'] if ENV['SAML_ISSUER'] + saml_options[:idp_sso_target_url] = ENV['SAML_IDP_SSO_TARGET_URL'] if ENV['SAML_IDP_SSO_TARGET_URL'] + saml_options[:idp_sso_target_url_runtime_params] = ENV['SAML_IDP_SSO_TARGET_PARAMS'] if ENV['SAML_IDP_SSO_TARGET_PARAMS'] # FIXME: Should be parsable Hash + saml_options[:idp_cert] = ENV['SAML_IDP_CERT'] if ENV['SAML_IDP_CERT'] + saml_options[:idp_cert_fingerprint] = ENV['SAML_IDP_CERT_FINGERPRINT'] if ENV['SAML_IDP_CERT_FINGERPRINT'] + saml_options[:idp_cert_fingerprint_validator] = ENV['SAML_IDP_CERT_FINGERPRINT_VALIDATOR'] if ENV['SAML_IDP_CERT_FINGERPRINT_VALIDATOR'] # FIXME: Should be Lambda { |fingerprint| } + saml_options[:name_identifier_format] = ENV['SAML_NAME_IDENTIFIER_FORMAT'] if ENV['SAML_NAME_IDENTIFIER_FORMAT'] + saml_options[:request_attributes] = {} + saml_options[:certificate] = ENV['SAML_CERT'] if ENV['SAML_CERT'] + saml_options[:private_key] = ENV['SAML_PRIVATE_KEY'] if ENV['SAML_PRIVATE_KEY'] + saml_options[:security] = {} + saml_options[:security][:want_assertions_signed] = ENV['SAML_SECURITY_WANT_ASSERTION_SIGNED'] == 'true' + saml_options[:security][:want_assertions_encrypted] = ENV['SAML_SECURITY_WANT_ASSERTION_ENCRYPTED'] == 'true' + saml_options[:attribute_statements] = {} + saml_options[:attribute_statements][:uid] = [ENV['SAML_ATTRIBUTES_STATEMENTS_UID']] if ENV['SAML_ATTRIBUTES_STATEMENTS_UID'] + saml_options[:attribute_statements][:email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL'] + saml_options[:attribute_statements][:full_name] = [ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']] if ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME'] + saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE'] + config.omniauth :saml, saml_options + end + +end diff --git a/config/locales/en.yml b/config/locales/en.yml index cd6138ff2..6805a6e87 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -355,6 +355,7 @@ en: auth: agreement_html: By signing up you agree to follow the rules of the instance and our terms of service. change_password: Security + confirm_email: Confirm email delete_account: Delete account delete_account_html: If you wish to delete your account, you can proceed here. You will be asked for confirmation. didnt_get_confirmation: Didn't receive confirmation instructions? @@ -364,6 +365,10 @@ en: logout: Logout migrate_account: Move to a different account migrate_account_html: If you wish to redirect this account to a different one, you can configure it here. + or_log_in_with: Or log in with + providers: + cas: CAS + saml: SAML register: Sign up resend_confirmation: Resend confirmation instructions reset_password: Reset password diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 3ad535f28..f0fc07f7a 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -355,6 +355,7 @@ fr: auth: agreement_html: En vous inscrivant, vous souscrivez aux règles de l’instance et à nos conditions d’utilisation. change_password: Sécurité + confirm_email: Confirmer mon adresse mail delete_account: Supprimer le compte delete_account_html: Si vous désirez supprimer votre compte, vous pouvez cliquer ici. Il vous sera demandé de confirmer cette action. didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? @@ -364,6 +365,7 @@ fr: logout: Se déconnecter migrate_account: Déplacer vers un compte différent migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le configurer ici. + or_log_in_with: Ou authentifiez-vous avec register: S’inscrire resend_confirmation: Envoyer à nouveau les consignes de confirmation reset_password: Réinitialiser le mot de passe diff --git a/config/routes.rb b/config/routes.rb index 80a2c6d13..34f33fa95 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,9 +24,11 @@ Rails.application.routes.draw do devise_scope :user do get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite + match '/auth/finish_signup' => 'auth/confirmations#finish_signup', via: [:get, :patch], as: :finish_signup end devise_for :users, path: 'auth', controllers: { + omniauth_callbacks: 'auth/omniauth_callbacks', sessions: 'auth/sessions', registrations: 'auth/registrations', passwords: 'auth/passwords', diff --git a/db/migrate/20180204034416_create_identities.rb b/db/migrate/20180204034416_create_identities.rb new file mode 100644 index 000000000..f6f5da910 --- /dev/null +++ b/db/migrate/20180204034416_create_identities.rb @@ -0,0 +1,11 @@ +class CreateIdentities < ActiveRecord::Migration[5.0] + def change + create_table :identities do |t| + t.references :user, foreign_key: { on_delete: :cascade } + t.string :provider, null: false, default: '' + t.string :uid, null: false, default: '' + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index a411de20f..02e84cbd1 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: 20180109143959) do +ActiveRecord::Schema.define(version: 20180204034416) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -173,6 +173,15 @@ ActiveRecord::Schema.define(version: 20180109143959) do t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true end + create_table "identities", id: :serial, force: :cascade do |t| + t.integer "user_id" + t.string "provider", default: "", null: false + t.string "uid", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_identities_on_user_id" + end + create_table "imports", force: :cascade do |t| t.integer "type", null: false t.boolean "approved", default: false, null: false @@ -526,6 +535,7 @@ ActiveRecord::Schema.define(version: 20180109143959) do add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade + add_foreign_key "identities", "users", on_delete: :cascade add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade add_foreign_key "invites", "users", on_delete: :cascade add_foreign_key "list_accounts", "accounts", on_delete: :cascade diff --git a/spec/fabricators/identity_fabricator.rb b/spec/fabricators/identity_fabricator.rb new file mode 100644 index 000000000..bc832df9f --- /dev/null +++ b/spec/fabricators/identity_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:identity) do + user nil + provider "MyString" + uid "MyString" +end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb new file mode 100644 index 000000000..53f355410 --- /dev/null +++ b/spec/models/identity_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Identity, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 4e4f1b0dcb386464d653fcce765ca775e566a03c Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Sun, 4 Feb 2018 06:00:10 +0100 Subject: [PATCH 020/257] Add option to show only local toots in timeline preview (#6292) * Add option to show only local toots in timeline preview Right know, toots from all the known fediverse are shown in the main page of an instance. That however doesn't reflect the instance itself. With this option the admin may choose to display only local toots so that users checking the instance get a better idea of internal conversations. * Fix issues pointed by codeclimate and eslint * Add default message for community timeline * Update pl.yml --- app/controllers/about_controller.rb | 2 +- app/controllers/admin/settings_controller.rb | 2 + .../mastodon/containers/timeline_container.js | 12 ++- .../standalone/community_timeline/index.js | 74 +++++++++++++++++++ .../mastodon/locales/defaultMessages.json | 9 +++ app/models/form/admin_settings.rb | 2 + app/views/admin/settings/edit.html.haml | 3 + config/locales/en.yml | 3 + config/locales/pl.yml | 3 + config/locales/pt-BR.yml | 3 + config/settings.yml | 1 + 11 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 app/javascript/mastodon/features/standalone/community_timeline/index.js diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 47690e81e..4ffdfb685 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -31,7 +31,7 @@ class AboutController < ApplicationController def initial_state_params { - settings: {}, + settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token, } end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 487282dc3..a6214dc3f 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -19,6 +19,7 @@ module Admin min_invite_role activity_api_enabled peers_api_enabled + show_known_fediverse_at_about_page ).freeze BOOLEAN_SETTINGS = %w( @@ -28,6 +29,7 @@ module Admin show_staff_badge activity_api_enabled peers_api_enabled + show_known_fediverse_at_about_page ).freeze UPLOAD_SETTINGS = %w( diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js index e84c921ee..8719bb5c9 100644 --- a/app/javascript/mastodon/containers/timeline_container.js +++ b/app/javascript/mastodon/containers/timeline_container.js @@ -6,6 +6,7 @@ import { hydrateStore } from '../actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; import PublicTimeline from '../features/standalone/public_timeline'; +import CommunityTimeline from '../features/standalone/community_timeline'; import HashtagTimeline from '../features/standalone/hashtag_timeline'; import initialState from '../initial_state'; @@ -23,17 +24,24 @@ export default class TimelineContainer extends React.PureComponent { static propTypes = { locale: PropTypes.string.isRequired, hashtag: PropTypes.string, + showPublicTimeline: PropTypes.bool.isRequired, + }; + + static defaultProps = { + showPublicTimeline: initialState.settings.known_fediverse, }; render () { - const { locale, hashtag } = this.props; + const { locale, hashtag, showPublicTimeline } = this.props; let timeline; if (hashtag) { timeline = ; - } else { + } else if (showPublicTimeline) { timeline = ; + } else { + timeline = ; } return ( diff --git a/app/javascript/mastodon/features/standalone/community_timeline/index.js b/app/javascript/mastodon/features/standalone/community_timeline/index.js new file mode 100644 index 000000000..51e50e1f5 --- /dev/null +++ b/app/javascript/mastodon/features/standalone/community_timeline/index.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../../ui/containers/status_list_container'; +import { + refreshCommunityTimeline, + expandCommunityTimeline, +} from '../../../actions/timelines'; +import Column from '../../../components/column'; +import ColumnHeader from '../../../components/column_header'; +import { defineMessages, injectIntl } from 'react-intl'; +import { connectCommunityStream } from '../../../actions/streaming'; + +const messages = defineMessages({ + title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, +}); + +@connect() +@injectIntl +export default class CommunityTimeline extends React.PureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + setRef = c => { + this.column = c; + } + + componentDidMount () { + const { dispatch } = this.props; + + dispatch(refreshCommunityTimeline()); + this.disconnect = dispatch(connectCommunityStream()); + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + handleLoadMore = () => { + this.props.dispatch(expandCommunityTimeline()); + } + + render () { + const { intl } = this.props; + + return ( + + + + + + ); + } + +} diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 9a46927c1..2788a7a14 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -1230,6 +1230,15 @@ ], "path": "app/javascript/mastodon/features/public_timeline/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "A look inside...", + "id": "standalone.public_title" + } + ], + "path": "app/javascript/mastodon/features/standalone/community_timeline/index.json" + }, { "descriptors": [ { diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index dd629279c..32922e7f1 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -34,6 +34,8 @@ class Form::AdminSettings :activity_api_enabled=, :peers_api_enabled, :peers_api_enabled=, + :show_known_fediverse_at_about_page, + :show_known_fediverse_at_about_page=, to: Setting ) end diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 4f9115ed2..73fd5642e 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -18,6 +18,9 @@ .fields-group = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') + .fields-group + = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html') + .fields-group = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html') diff --git a/config/locales/en.yml b/config/locales/en.yml index 6805a6e87..5cd3b08cf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -290,6 +290,9 @@ en: open: desc_html: Allow anyone to create an account title: Open registration + show_known_fediverse_at_about_page: + desc_html: When toggled, it will show toots from all the known fediverse on preview. Otherwise it will only show local toots. + title: Show known fediverse on timeline preview show_staff_badge: desc_html: Show a staff badge on a user page title: Show staff badge diff --git a/config/locales/pl.yml b/config/locales/pl.yml index a66710800..633850b28 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -291,6 +291,9 @@ pl: open: desc_html: Pozwól każdemu na założenie konta title: Otwarta rejestracja + show_known_fediverse_at_about_page: + desc_html: Jeśli włączone, podgląd instancji będzie wyświetlał wpisy z całego Fediwersum. W innym przypadku, będą wyświetlane tylko lokalne wpisy. + title: Pokazuj wszystkie znane wpisy na podglądzie instancji show_staff_badge: desc_html: Pokazuj odznakę uprawnień na stronie profilu użytkownika title: Pokazuj odznakę administracji diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 82c96c92b..31481ced4 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -290,6 +290,9 @@ pt-BR: open: desc_html: Permitir que qualquer um crie uma conta title: Cadastro aberto + show_known_fediverse_at_about_page: + desc_html: Quando ligado, vai mostrar toots de todo o fediverso conhecido na prévia da timeline. Senão, mostra somente toots locais. + title: Mostrar fediverso conhecido na prévia da timeline show_staff_badge: desc_html: Mostrar uma insígnia de Equipe na página de usuário title: Mostrar insígnia de equipe diff --git a/config/settings.yml b/config/settings.yml index 4a2519464..32d0687ce 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -49,6 +49,7 @@ defaults: &defaults bootstrap_timeline_accounts: '' activity_api_enabled: true peers_api_enabled: true + show_known_fediverse_at_about_page: true development: <<: *defaults From 258dcb849f2146069a2b2914cea3a28619f55c90 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Sun, 4 Feb 2018 05:03:01 +0000 Subject: [PATCH 021/257] Upgrade Vagrant box to Xenial (#6421) * upgrade vagrant box to xenial this allows the redis version to be upgraded to support the new redis features used in the activity tracker * add libpam0g package to vagrant box this is required for native extensions of gems to build after the addition of PAM support was added in #5303 --- Vagrantfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index bbe3b7f3b..ddcdf3510 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -39,6 +39,7 @@ sudo apt-get install \ libidn11-dev \ libprotobuf-dev \ libreadline-dev \ + libpam0g-dev \ -y # Install rvm @@ -79,7 +80,7 @@ VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - config.vm.box = "ubuntu/trusty64" + config.vm.box = "ubuntu/xenial64" config.vm.provider :virtualbox do |vb| vb.name = "mastodon" From c156a83e7d4458355e7ab60ee118ca8c09b80ece Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 4 Feb 2018 20:31:46 +0900 Subject: [PATCH 022/257] Make sure status is not nil (#6428) --- app/mailers/notification_mailer.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 9fed4a636..b45844296 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -9,7 +9,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -33,7 +33,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -46,7 +46,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) From 3f35d4322266ee6f1bfab73a1161af2b0848573a Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 4 Feb 2018 20:32:10 +0900 Subject: [PATCH 023/257] Exclude nil from relationships array (#6427) --- app/controllers/api/v1/accounts/relationships_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 91a942d75..6cc3da498 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController accounts = Account.where(id: account_ids).select('id') # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. - @accounts = accounts.index_by(&:id).values_at(*account_ids) + @accounts = accounts.index_by(&:id).values_at(*account_ids).compact render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end From 9b6223f5e26ed53f285a95921e9c660e831a7f6d Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 4 Feb 2018 20:32:41 +0900 Subject: [PATCH 024/257] Validation of count works even when text of status is nil (#6429) --- app/validators/status_length_validator.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb index 77be3f1f5..ed5563f64 100644 --- a/app/validators/status_length_validator.rb +++ b/app/validators/status_length_validator.rb @@ -23,6 +23,8 @@ class StatusLengthValidator < ActiveModel::Validator end def countable_text(status) + return '' if status.text.nil? + status.text.dup.tap do |new_text| new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23) new_text.gsub!(Account::MENTION_RE, '@\2') From 38e0133e1b01c21a710111097102a6eb205b9b9b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 4 Feb 2018 15:05:53 +0100 Subject: [PATCH 025/257] Make PAM gem optional, allow configuration over environment (#6415) --- .env.production.sample | 9 +++++++++ Gemfile | 2 +- app/models/user.rb | 2 +- config/initializers/devise.rb | 27 +++++++++------------------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.env.production.sample b/.env.production.sample index 777336de1..a4b689a31 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -136,6 +136,15 @@ STREAMING_CLUSTER_NUM=1 # UID=1000 # GID=1000 +# PAM authentication (optional) +# PAM_ENABLED=true +# Suffix for email address generation (nil by default) +# PAM_DEFAULT_SUFFIX=pam +# Name of the pam service (pam "auth" section is evaluated) +# PAM_DEFAULT_SERVICE=rpam +# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) +# PAM_CONTROLLED_SERVICE=rpam + # Optional CAS authentication (cf. omniauth-cas) : # CAS_ENABLED=true # CAS_URL=https://sso.myserver.com/ diff --git a/Gemfile b/Gemfile index 5b6ae707d..3b39f3946 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,7 @@ gem 'cld3', '~> 3.2.0' gem 'devise', '~> 4.4' gem 'devise-two-factor', '~> 3.0' -gem 'devise_pam_authenticatable2', '~> 8.0' +gem 'devise_pam_authenticatable2', '~> 8.0', install_if: -> { ENV['PAM_ENABLED'] == 'true' } gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' } gem 'omniauth-saml', '~> 1.8', install_if: -> { ENV['SAML_ENABLED'] == 'true' } gem 'omniauth', '~> 1.2' diff --git a/app/models/user.rb b/app/models/user.rb index fba478453..feaf8b26c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -52,7 +52,7 @@ class User < ApplicationRecord devise :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable - devise :pam_authenticatable + devise :pam_authenticatable if Devise.pam_authentication devise :omniauthable belongs_to :account, inverse_of: :user diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f2f7f1ba3..ba7ad9e6c 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -315,22 +315,13 @@ Devise.setup do |config| # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' - # PAM: only look for email field - config.usernamefield = nil - config.emailfield = "email" - - # authentication with pam possible - # if not enabled, all pam settings are ignored - #config.pam_authentication = true - # check if email is actually a username - config.check_at_sign = true - # suffix for email address generation (warning: without pam must provide email in the pam environment) - config.pam_default_suffix = "pam" - # name of the pam service - # pam "auth" section is evaluated - config.pam_default_service = "rpam" - # name of the pam service used for checking if an user can register - # pam "account" section is evaluated - # nil for allowing registration of pam names (not recommended) - config.pam_controlled_service = "rpam" + if ENV['PAM_ENABLED'] == 'true' + config.pam_authentication = true + config.usernamefield = nil + config.emailfield = 'email' + config.check_at_sign = true + config.pam_default_suffix = ENV.fetch('PAM_DEFAULT_SUFFIX') { nil } + config.pam_default_service = ENV.fetch('PAM_DEFAULT_SERVICE') { 'rpam' } + config.pam_controlled_service = ENV.fetch('PAM_CONTROLLED_SERVICE') { 'rpam' } + end end From 95c8232109f5e59273de48858ede7fbe7ce63d58 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Mon, 5 Feb 2018 01:44:13 +0000 Subject: [PATCH 026/257] match hashtag regex in js client with server (#6431) the slight mismatch in hashtag regex between js and ruby was causing hashtag warning to be displayed for unlisted tweets when an invalid hashtag was entered exact version of ruby regex not possible in js as POSIX bracket expressions are not supported, this version approximates and doesn't give same unicode support --- .../mastodon/features/compose/containers/warning_container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js index b9f280958..dbb80dfb0 100644 --- a/app/javascript/mastodon/features/compose/containers/warning_container.js +++ b/app/javascript/mastodon/features/compose/containers/warning_container.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { me } from '../../../initial_state'; -const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/i; +const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z]\w*)/i; const mapStateToProps = state => ({ needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), From 67f7ffa79270e3591478a95a1a083bfffa1fd218 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Thu, 8 Feb 2018 00:35:44 +0900 Subject: [PATCH 027/257] Change user_id column non-nullable (#6435) --- app/models/invite.rb | 2 +- app/models/web/setting.rb | 2 +- db/migrate/20180206000000_change_user_id_nonnullable.rb | 6 ++++++ db/schema.rb | 6 +++--- 4 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20180206000000_change_user_id_nonnullable.rb diff --git a/app/models/invite.rb b/app/models/invite.rb index b87a3b722..4ba5432d2 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -4,7 +4,7 @@ # Table name: invites # # id :integer not null, primary key -# user_id :integer +# user_id :integer not null # code :string default(""), not null # expires_at :datetime # max_uses :integer diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb index 12b9d1226..0a5129d17 100644 --- a/app/models/web/setting.rb +++ b/app/models/web/setting.rb @@ -7,7 +7,7 @@ # data :json # created_at :datetime not null # updated_at :datetime not null -# user_id :integer +# user_id :integer not null # class Web::Setting < ApplicationRecord diff --git a/db/migrate/20180206000000_change_user_id_nonnullable.rb b/db/migrate/20180206000000_change_user_id_nonnullable.rb new file mode 100644 index 000000000..4eecb6154 --- /dev/null +++ b/db/migrate/20180206000000_change_user_id_nonnullable.rb @@ -0,0 +1,6 @@ +class ChangeUserIdNonnullable < ActiveRecord::Migration[5.1] + def change + change_column_null :invites, :user_id, false + change_column_null :web_settings, :user_id, false + end +end diff --git a/db/schema.rb b/db/schema.rb index 02e84cbd1..281110124 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: 20180204034416) do +ActiveRecord::Schema.define(version: 20180206000000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -195,7 +195,7 @@ ActiveRecord::Schema.define(version: 20180204034416) do end create_table "invites", force: :cascade do |t| - t.bigint "user_id" + t.bigint "user_id", null: false t.string "code", default: "", null: false t.datetime "expires_at" t.integer "max_uses" @@ -516,7 +516,7 @@ ActiveRecord::Schema.define(version: 20180204034416) do t.json "data" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.bigint "user_id" + t.bigint "user_id", null: false t.index ["user_id"], name: "index_web_settings_on_user_id", unique: true end From 2bb393684baf5da1112923db7bcda920d606be78 Mon Sep 17 00:00:00 2001 From: Kazushige Tominaga Date: Thu, 8 Feb 2018 08:17:53 +0900 Subject: [PATCH 028/257] Added #link_header spec (#6439) --- spec/services/fetch_atom_service_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/services/fetch_atom_service_spec.rb b/spec/services/fetch_atom_service_spec.rb index 5491fd027..fcba55e8d 100644 --- a/spec/services/fetch_atom_service_spec.rb +++ b/spec/services/fetch_atom_service_spec.rb @@ -1,4 +1,24 @@ require 'rails_helper' RSpec.describe FetchAtomService do + describe '#link_header' do + context 'Link is Array' do + target = FetchAtomService.new + target.instance_variable_set('@response', 'Link' => [ + '; rel="up"; meta="bar"', + '; rel="self"', + ]) + + it 'set first link as link_header' do + expect(target.send(:link_header).links[0].href).to eq 'http://example.com/' + end + end + + context 'Link is not Array' do + target = FetchAtomService.new + target.instance_variable_set('@response', 'Link' => '; rel="self", ; rel = "up"') + + it { expect(target.send(:link_header).links[0].href).to eq 'http://example.com/foo' } + end + end end From cf32f7da5c5af7c86af3cab89d18cdbe7b35f4a2 Mon Sep 17 00:00:00 2001 From: abcang Date: Thu, 8 Feb 2018 13:00:45 +0900 Subject: [PATCH 029/257] Fix response of signature_verification_failure_reason (#6441) --- .../activitypub/inboxes_controller.rb | 2 +- app/controllers/api/salmon_controller.rb | 4 +++- spec/controllers/api/salmon_controller_spec.rb | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 7d0bc74d3..af51e32d5 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -11,7 +11,7 @@ class ActivityPub::InboxesController < Api::BaseController process_payload head 202 else - [signature_verification_failure_reason, 401] + render plain: signature_verification_failure_reason, status: 401 end end diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb index 143e9d3cd..ac5f3268d 100644 --- a/app/controllers/api/salmon_controller.rb +++ b/app/controllers/api/salmon_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::SalmonController < Api::BaseController + include SignatureVerification + before_action :set_account respond_to :txt @@ -9,7 +11,7 @@ class Api::SalmonController < Api::BaseController process_salmon head 202 elsif payload.present? - [signature_verification_failure_reason, 401] + render plain: signature_verification_failure_reason, status: 401 else head 400 end diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb index 323d85b61..8af8b83a8 100644 --- a/spec/controllers/api/salmon_controller_spec.rb +++ b/spec/controllers/api/salmon_controller_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Api::SalmonController, type: :controller do end end - context 'with invalid post data' do + context 'with empty post data' do before do request.env['RAW_POST_DATA'] = '' post :update, params: { id: account.id } @@ -50,5 +50,19 @@ RSpec.describe Api::SalmonController, type: :controller do expect(response).to have_http_status(400) end end + + context 'with invalid post data' do + before do + service = double(call: false) + allow(VerifySalmonService).to receive(:new).and_return(service) + + request.env['RAW_POST_DATA'] = File.read(File.join(Rails.root, 'spec', 'fixtures', 'salmon', 'mention.xml')) + post :update, params: { id: account.id } + end + + it 'returns http client error' do + expect(response).to have_http_status(401) + end + end end end From 298c81c00f951241f026b0b3f711ead405b78fbe Mon Sep 17 00:00:00 2001 From: abcang Date: Thu, 8 Feb 2018 23:33:23 +0900 Subject: [PATCH 030/257] Clear account cache of notification target_status (#6442) --- app/models/notification.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/notification.rb b/app/models/notification.rb index 733f89cf7..7f8dae5ec 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -69,7 +69,7 @@ class Notification < ApplicationRecord class << self def reload_stale_associations!(cached_items) - account_ids = cached_items.map(&:from_account_id).uniq + account_ids = (cached_items.map(&:from_account_id) + cached_items.map { |item| item.target_status&.account_id }.compact).uniq return if account_ids.empty? @@ -77,6 +77,7 @@ class Notification < ApplicationRecord cached_items.each do |item| item.from_account = accounts[item.from_account_id] + item.target_status.account = accounts[item.target_status.account_id] if item.target_status end end From 1167c6dbf8d5b8411d9924350b2b9735da6d9d26 Mon Sep 17 00:00:00 2001 From: Kazushige Tominaga Date: Fri, 9 Feb 2018 08:12:35 +0900 Subject: [PATCH 031/257] Perform request spec (#6446) * Added #link_header spec * Added #perform_request spec --- spec/services/fetch_atom_service_spec.rb | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/spec/services/fetch_atom_service_spec.rb b/spec/services/fetch_atom_service_spec.rb index fcba55e8d..d7f162b85 100644 --- a/spec/services/fetch_atom_service_spec.rb +++ b/spec/services/fetch_atom_service_spec.rb @@ -21,4 +21,43 @@ RSpec.describe FetchAtomService do it { expect(target.send(:link_header).links[0].href).to eq 'http://example.com/foo' } end end + + describe '#perform_request' do + let(:url) { 'http://example.com' } + context 'Check method result' do + before do + WebMock.stub_request(:get, url).to_return(status: 200, body: '', headers: {}) + @target = FetchAtomService.new + @target.instance_variable_set('@url', url) + end + + it 'HTTP::Response instance is returned and set to @response' do + expect(@target.send(:perform_request).status.to_s).to eq '200 OK' + expect(@target.instance_variable_get('@response')).to be_instance_of HTTP::Response + end + end + + context 'check passed parameters to Request' do + before do + @target = FetchAtomService.new + @target.instance_variable_set('@url', url) + @target.instance_variable_set('@unsupported_activity', unsupported_activity) + allow(Request).to receive(:new).with(:get, url) + expect(Request).to receive_message_chain(:new, :add_headers).with('Accept' => accept) + allow(Request).to receive_message_chain(:new, :add_headers, :perform).with(no_args) + end + + context '@unsupported_activity is true' do + let(:unsupported_activity) { true } + let(:accept) { 'text/html' } + it { @target.send(:perform_request) } + end + + context '@unsupported_activity is false' do + let(:unsupported_activity) { false } + let(:accept) { 'application/activity+json, application/ld+json, application/atom+xml, text/html' } + it { @target.send(:perform_request) } + end + end + end end From 76f3d5d16be2fb97d2252909589510165ec05e12 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 9 Feb 2018 00:26:57 +0100 Subject: [PATCH 032/257] Add preference to always display sensitive media (#6448) --- .../settings/preferences_controller.rb | 1 + .../mastodon/components/media_gallery.js | 4 +-- .../mastodon/features/video/index.js | 3 +- app/javascript/mastodon/initial_state.js | 1 + app/lib/user_settings_decorator.rb | 29 +++++++++++-------- app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 13 +++++---- app/views/settings/preferences/show.html.haml | 1 + .../stream_entries/_detailed_status.html.haml | 4 +-- .../stream_entries/_simple_status.html.haml | 4 +-- config/locales/simple_form.en.yml | 1 + config/settings.yml | 1 + 12 files changed, 38 insertions(+), 26 deletions(-) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 069026715..839763138 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -39,6 +39,7 @@ class Settings::PreferencesController < ApplicationController :setting_boost_modal, :setting_delete_modal, :setting_auto_play_gif, + :setting_display_sensitive_media, :setting_reduce_motion, :setting_system_font_ui, :setting_noindex, diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 20febdb16..a276b17d5 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -6,7 +6,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from '../is_mobile'; import classNames from 'classnames'; -import { autoPlayGif } from '../initial_state'; +import { autoPlayGif, displaySensitiveMedia } from '../initial_state'; const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, @@ -187,7 +187,7 @@ export default class MediaGallery extends React.PureComponent { }; state = { - visible: !this.props.sensitive, + visible: !this.props.sensitive || displaySensitiveMedia, }; componentWillReceiveProps (nextProps) { diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 0ee8bb6c8..7752fc057 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; +import { displaySensitiveMedia } from '../../initial_state'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -107,7 +108,7 @@ export default class Video extends React.PureComponent { fullscreen: false, hovered: false, muted: false, - revealed: !this.props.sensitive, + revealed: !this.props.sensitive || displaySensitiveMedia, }; setPlayerRef = c => { diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 3fc45077d..6f1356324 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -5,6 +5,7 @@ const getMeta = (prop) => initialState && initialState.meta && initialState.meta export const reduceMotion = getMeta('reduce_motion'); export const autoPlayGif = getMeta('auto_play_gif'); +export const displaySensitiveMedia = getMeta('display_sensitive_media'); export const unfollowModal = getMeta('unfollow_modal'); export const boostModal = getMeta('boost_modal'); export const deleteModal = getMeta('delete_modal'); diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index d48e1da65..4d6f19467 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -15,18 +15,19 @@ class UserSettingsDecorator private def process_update - user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') - user.settings['interactions'] = merged_interactions if change?('interactions') - user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') - user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') - user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') - user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') - user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') - user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') - user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') - user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') - user.settings['noindex'] = noindex_preference if change?('setting_noindex') - user.settings['theme'] = theme_preference if change?('setting_theme') + user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') + user.settings['interactions'] = merged_interactions if change?('interactions') + user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') + user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') + user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') + user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') + user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') + user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') + user.settings['display_sensitive_media'] = display_sensitive_media_preference if change?('setting_display_sensitive_media') + user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') + user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') + user.settings['noindex'] = noindex_preference if change?('setting_noindex') + user.settings['theme'] = theme_preference if change?('setting_theme') end def merged_notification_emails @@ -65,6 +66,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_auto_play_gif' end + def display_sensitive_media_preference + boolean_cast_setting 'setting_display_sensitive_media' + end + def reduce_motion_preference boolean_cast_setting 'setting_reduce_motion' end diff --git a/app/models/user.rb b/app/models/user.rb index feaf8b26c..fd153912e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -84,7 +84,7 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, - :reduce_motion, :system_font_ui, :noindex, :theme, + :reduce_motion, :system_font_ui, :noindex, :theme, :display_sensitive_media, to: :settings, prefix: :setting, allow_nil: false attr_accessor :invite_code diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 4fa1981ed..152e10f37 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -20,12 +20,13 @@ class InitialStateSerializer < ActiveModel::Serializer } if object.current_account - store[:me] = object.current_account.id.to_s - store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal - store[:boost_modal] = object.current_account.user.setting_boost_modal - store[:delete_modal] = object.current_account.user.setting_delete_modal - store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif - store[:reduce_motion] = object.current_account.user.setting_reduce_motion + store[:me] = object.current_account.id.to_s + store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal + store[:boost_modal] = object.current_account.user.setting_boost_modal + store[:delete_modal] = object.current_account.user.setting_delete_modal + store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif + store[:display_sensitive_media] = object.current_account.user.setting_display_sensitive_media + store[:reduce_motion] = object.current_account.user.setting_reduce_motion end store diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 441e27a68..030719201 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -38,6 +38,7 @@ .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label + = f.input :setting_display_sensitive_media, as: :boolean, wrapper: :with_label = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 94e081c84..470bff218 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -22,9 +22,9 @@ - if !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first - %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380, detailed: true) }} + %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 670, height: 380, detailed: true) }} - else - %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }} + %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }} - elsif status.preview_cards.first %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }} diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index b594c9da6..ce723f1cc 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -23,6 +23,6 @@ - unless status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first - %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 610, height: 343) }} + %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343) }} - else - %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 343, sensitive: status.sensitive?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }} + %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }} diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index c56334d56..90b97ce0e 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -45,6 +45,7 @@ en: setting_default_privacy: Post privacy setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a toot + setting_display_sensitive_media: Always show media marked as sensitive setting_noindex: Opt-out of search engine indexing setting_reduce_motion: Reduce motion in animations setting_system_font_ui: Use system's default font diff --git a/config/settings.yml b/config/settings.yml index 32d0687ce..68579ad0f 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -24,6 +24,7 @@ defaults: &defaults boost_modal: false delete_modal: true auto_play_gif: false + display_sensitive_media: false reduce_motion: false system_font_ui: false noindex: false From 2ef9d0e1011b586099d9f9992607b03d96c8a7f8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 9 Feb 2018 00:27:18 +0100 Subject: [PATCH 033/257] Change web UI "posts" to "toots" on profile for consistency (#6447) --- .../mastodon/features/account/components/action_bar.js | 2 +- app/javascript/mastodon/locales/defaultMessages.json | 4 ++-- app/javascript/mastodon/locales/en.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index cb849fa5d..b4f812c9a 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -122,7 +122,7 @@ export default class ActionBar extends React.PureComponent {
- + diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 2788a7a14..e46da4cb3 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -433,7 +433,7 @@ "id": "account.view_full_profile" }, { - "defaultMessage": "Posts", + "defaultMessage": "Toots", "id": "account.posts" }, { @@ -1659,4 +1659,4 @@ ], "path": "app/javascript/mastodon/features/video/index.json" } -] \ No newline at end of file +] diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 1e952b7b7..03e63a04a 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -13,7 +13,7 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", "account.mute_notifications": "Mute notifications from @{name}", - "account.posts": "Posts", + "account.posts": "Toots", "account.report": "Report @{name}", "account.requested": "Awaiting approval. Click to cancel follow request", "account.share": "Share @{name}'s profile", From 235c14c79d620d47012a08425324df222a136457 Mon Sep 17 00:00:00 2001 From: masarakki Date: Fri, 9 Feb 2018 23:29:48 +0900 Subject: [PATCH 034/257] fix-indent (#6453) --- .travis.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 35fc49dde..2fba133c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,15 @@ cache: bundler: true yarn: true directories: - - node_modules - - public/assets - - public/packs-test - - tmp/cache/babel-loader + - node_modules + - public/assets + - public/packs-test + - tmp/cache/babel-loader dist: trusty sudo: false branches: only: - - master + - master notifications: email: false @@ -29,15 +29,15 @@ addons: postgresql: 9.4 apt: sources: - - trusty-media - - sourceline: deb https://dl.yarnpkg.com/debian/ stable main - key_url: https://dl.yarnpkg.com/debian/pubkey.gpg + - trusty-media + - sourceline: deb https://dl.yarnpkg.com/debian/ stable main + key_url: https://dl.yarnpkg.com/debian/pubkey.gpg packages: - - ffmpeg - - libicu-dev - - libprotobuf-dev - - protobuf-compiler - - yarn + - ffmpeg + - libicu-dev + - libprotobuf-dev + - protobuf-compiler + - yarn rvm: - 2.4.2 From 3ebc0ad4d3c2fe0b0951a334642b769bd521a799 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 9 Feb 2018 23:04:47 +0100 Subject: [PATCH 035/257] Full-text search for authorized statuses (#6423) * Add full-text search for authorized statuses - Search API will return statuses that match the query - Only for logged in users - Only if you are author of the status, - Or you were mentioned in it - Or you favourited or reblogged it - Configuration over `ES_ENABLED`, `ES_HOST`, `ES_PORT`, `ES_PREFIX` - Run `rails chewy:deploy` to create & populate index Fix #5880 Fix #4293 Fix #1152 * Add commented out docker-compose configuration for ES container * Optimize index import, filter search results * Add basic normalization to the index * Add better stemming and normalization to the index * Skip webfinger request if search query includes both @ and a space * Fix code style * Visually separate search result sections * Fix code style issues --- .env.production.sample | 4 ++ Gemfile | 1 + Gemfile.lock | 22 +++++++ app/chewy/statuses_index.rb | 61 +++++++++++++++++++ .../compose/components/search_results.js | 6 ++ .../styles/mastodon/components.scss | 39 +++++++++++- app/lib/status_filter.rb | 1 + app/models/favourite.rb | 2 + app/models/status.rb | 18 ++++++ app/services/search_service.rb | 43 +++++++++++-- config/initializers/chewy.rb | 22 +++++++ docker-compose.yml | 12 ++++ spec/spec_helper.rb | 4 ++ 13 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 app/chewy/statuses_index.rb create mode 100644 config/initializers/chewy.rb diff --git a/.env.production.sample b/.env.production.sample index a4b689a31..38f7326f0 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -9,6 +9,10 @@ DB_USER=postgres DB_NAME=postgres DB_PASS= DB_PORT=5432 +# Optional ElasticSearch configuration +# ES_ENABLED=true +# ES_HOST=localhost +# ES_PORT=9200 # Federation # Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation. diff --git a/Gemfile b/Gemfile index 3b39f3946..d1c00b498 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem 'bootsnap' gem 'browser' gem 'charlock_holmes', '~> 0.7.5' gem 'iso-639' +gem 'chewy', '~> 0.10', git: 'https://github.com/toptal/chewy.git' gem 'cld3', '~> 3.2.0' gem 'devise', '~> 4.4' gem 'devise-two-factor', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index c357bfbd1..b82fc49a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/toptal/chewy.git + revision: a7d21eb4b0bd7415533ef134bb6d31b2df309701 + specs: + chewy (0.10.1) + activesupport (>= 4.0) + elasticsearch (>= 2.0.0) + elasticsearch-dsl + GEM remote: https://rubygems.org/ specs: @@ -154,6 +163,15 @@ GEM json thread thread_safe + elasticsearch (6.0.1) + elasticsearch-api (= 6.0.1) + elasticsearch-transport (= 6.0.1) + elasticsearch-api (6.0.1) + multi_json + elasticsearch-dsl (0.1.5) + elasticsearch-transport (6.0.1) + faraday + multi_json encryptor (3.0.0) erubi (1.7.0) et-orbi (1.0.8) @@ -163,6 +181,8 @@ GEM fabrication (2.18.0) faker (1.8.4) i18n (~> 0.5) + faraday (0.14.0) + multipart-post (>= 1.2, < 3) fast_blank (1.0.0) ffi (1.9.18) fog-core (1.45.0) @@ -291,6 +311,7 @@ GEM minitest (5.11.3) msgpack (1.1.0) multi_json (1.12.2) + multipart-post (2.0.0) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.2.0) @@ -583,6 +604,7 @@ DEPENDENCIES capistrano-yarn (~> 2.0) capybara (~> 2.15) charlock_holmes (~> 0.7.5) + chewy (~> 0.10)! cld3 (~> 3.2.0) climate_control (~> 0.2) devise (~> 4.4) diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb new file mode 100644 index 000000000..8bf5b4af7 --- /dev/null +++ b/app/chewy/statuses_index.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class StatusesIndex < Chewy::Index + settings index: { refresh_interval: '15m' }, analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { + content: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + }, + } + + define_type ::Status.without_reblogs do + crutch :mentions do |collection| + data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) + data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } + end + + crutch :favourites do |collection| + data = ::Favourite.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) + data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } + end + + crutch :reblogs do |collection| + data = ::Status.where(reblog_of_id: collection.map(&:id)).pluck(:reblog_of_id, :account_id) + data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } + end + + root date_detection: false do + field :account_id, type: 'long' + + field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do + field :stemmed, type: 'text', analyzer: 'content' + end + + field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } + field :created_at, type: 'date' + end + end +end diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index d16f7fce7..84455563c 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -22,6 +22,8 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('accounts').size; accounts = (
+
+ {results.get('accounts').map(accountId => )}
); @@ -31,6 +33,8 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('statuses').size; statuses = (
+
+ {results.get('statuses').map(statusId => )}
); @@ -40,6 +44,8 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('hashtags').size; hashtags = (
+
+ {results.get('hashtags').map(hashtag => ( #{hashtag} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index c2c9a040f..fe895809a 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1786,7 +1786,7 @@ flex: 1; min-height: 47px; - > img { + > img { display: block; object-fit: contain; object-position: bottom left; @@ -3229,6 +3229,43 @@ font-weight: 500; } +.search-results__section { + margin-bottom: 20px; + + h5 { + position: relative; + + &::before { + content: ""; + display: block; + position: absolute; + left: 0; + right: 0; + top: 50%; + width: 100%; + height: 0; + border-top: 1px solid lighten($ui-base-color, 8%); + } + + span { + display: inline-block; + background: $ui-base-color; + color: $ui-primary-color; + font-size: 14px; + font-weight: 500; + padding: 10px; + position: relative; + z-index: 1; + cursor: default; + } + } + + .account:last-child, + & > div:last-child .status { + border-bottom: 0; + } +} + .search-results__hashtag { display: block; padding: 10px; diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb index a6a050ce1..41d4381e5 100644 --- a/app/lib/status_filter.rb +++ b/app/lib/status_filter.rb @@ -9,6 +9,7 @@ class StatusFilter end def filtered? + return false if !account.nil? && account.id == status.account_id blocked_by_policy? || (account_present? && filtered_status?) || silenced_account? end diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 2b1271f31..fa1884b86 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -13,6 +13,8 @@ class Favourite < ApplicationRecord include Paginable + update_index('statuses#status', :status) if Chewy.enabled? + belongs_to :account, inverse_of: :favourites belongs_to :status, inverse_of: :favourites, counter_cache: true diff --git a/app/models/status.rb b/app/models/status.rb index 26ff40bf7..0de89ad4e 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -31,6 +31,8 @@ class Status < ApplicationRecord include Cacheable include StatusThreadingConcern + update_index('statuses#status', :proper) if Chewy.enabled? + enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility belongs_to :application, class_name: 'Doorkeeper::Application', optional: true @@ -78,6 +80,22 @@ class Status < ApplicationRecord delegate :domain, to: :account, prefix: true + def searchable_by(preloaded = nil) + ids = [account_id] + + if preloaded.nil? + ids += mentions.pluck(:account_id) + ids += favourites.pluck(:account_id) + ids += reblogs.pluck(:account_id) + else + ids += preloaded.mentions[id] || [] + ids += preloaded.favourites[id] || [] + ids += preloaded.reblogs[id] || [] + end + + ids.uniq + end + def reply? !in_reply_to_id.nil? || attributes['reply'] end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 5f763b8f7..fe9856686 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -1,21 +1,43 @@ # frozen_string_literal: true class SearchService < BaseService - attr_accessor :query + attr_accessor :query, :account, :limit, :resolve def call(query, limit, resolve = false, account = nil) - @query = query + @query = query + @account = account + @limit = limit + @resolve = resolve default_results.tap do |results| if url_query? results.merge!(url_resource_results) unless url_resource.nil? elsif query.present? - results[:accounts] = AccountSearchService.new.call(query, limit, account, resolve: resolve) - results[:hashtags] = Tag.search_for(query.gsub(/\A#/, ''), limit) unless query.start_with?('@') + results[:accounts] = perform_accounts_search! if account_searchable? + results[:statuses] = perform_statuses_search! if full_text_searchable? + results[:hashtags] = perform_hashtags_search! if hashtag_searchable? end end end + private + + def perform_accounts_search! + AccountSearchService.new.call(query, limit, account, resolve: resolve) + end + + def perform_statuses_search! + statuses = StatusesIndex.filter(term: { searchable_by: account.id }) + .query(multi_match: { type: 'most_fields', query: query, operator: 'and', fields: %w(text text.stemmed) }) + .limit(limit).objects + + statuses.reject { |status| StatusFilter.new(status, account).filtered? } + end + + def perform_hashtags_search! + Tag.search_for(query.gsub(/\A#/, ''), limit) + end + def default_results { accounts: [], hashtags: [], statuses: [] } end @@ -35,4 +57,17 @@ class SearchService < BaseService def url_resource_symbol url_resource.class.name.downcase.pluralize.to_sym end + + def full_text_searchable? + return false unless Chewy.enabled? + !account.nil? && !((query.start_with?('#') || query.include?('@')) && !query.include?(' ')) + end + + def account_searchable? + !(query.include?('@') && query.include?(' ')) + end + + def hashtag_searchable? + !query.include?('@') + end end diff --git a/config/initializers/chewy.rb b/config/initializers/chewy.rb new file mode 100644 index 000000000..bef2746ec --- /dev/null +++ b/config/initializers/chewy.rb @@ -0,0 +1,22 @@ +enabled = ENV['ES_ENABLED'] == 'true' +host = ENV.fetch('ES_HOST') { 'localhost' } +port = ENV.fetch('ES_PORT') { 9200 } +fallback_prefix = ENV.fetch('REDIS_NAMESPACE') { nil } +prefix = ENV.fetch('ES_PREFIX') { fallback_prefix } + +Chewy.settings = { + host: "#{host}:#{port}", + prefix: prefix, + enabled: enabled, + journal: false, +} + +Chewy.root_strategy = enabled ? :sidekiq : :bypass + +module Chewy + class << self + def enabled? + settings[:enabled] + end + end +end diff --git a/docker-compose.yml b/docker-compose.yml index aaa3a4478..55b419e98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,17 @@ services: # volumes: # - ./redis:/data +# es: +# restart: always +# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.1.3 +# environment: +# - "ES_JAVA_OPTS=-Xms512m -Xmx512m" +# networks: +# - internal_network +#### Uncomment to enable ES persistance +## volumes: +## - ./elasticsearch:/usr/share/elasticsearch/data + web: build: . image: gargron/mastodon @@ -33,6 +44,7 @@ services: depends_on: - db - redis +# - es volumes: - ./public/assets:/mastodon/public/assets - ./public/packs:/mastodon/public/packs diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index eecaec4ac..a0466dd4b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,10 @@ RSpec.configure do |config| end end + config.before :suite do + Chewy.strategy(:bypass) + end + config.after :suite do gc_counter = 0 FileUtils.rm_rf(Dir["#{Rails.root}/spec/test_files/"]) From cbe8743e476ce66fca29a64b09f41c71fb2820c9 Mon Sep 17 00:00:00 2001 From: Kazushige Tominaga Date: Sat, 10 Feb 2018 11:31:38 +0900 Subject: [PATCH 036/257] Added #call spec (#6455) * Added #link_header spec * Added #call spec * Delete spec of private methods --- spec/services/fetch_atom_service_spec.rb | 114 ++++++++++++++--------- 1 file changed, 70 insertions(+), 44 deletions(-) diff --git a/spec/services/fetch_atom_service_spec.rb b/spec/services/fetch_atom_service_spec.rb index d7f162b85..2bd127e92 100644 --- a/spec/services/fetch_atom_service_spec.rb +++ b/spec/services/fetch_atom_service_spec.rb @@ -1,62 +1,88 @@ require 'rails_helper' RSpec.describe FetchAtomService do - describe '#link_header' do - context 'Link is Array' do - target = FetchAtomService.new - target.instance_variable_set('@response', 'Link' => [ - '; rel="up"; meta="bar"', - '; rel="self"', - ]) - - it 'set first link as link_header' do - expect(target.send(:link_header).links[0].href).to eq 'http://example.com/' - end - end - - context 'Link is not Array' do - target = FetchAtomService.new - target.instance_variable_set('@response', 'Link' => '; rel="self", ; rel = "up"') - - it { expect(target.send(:link_header).links[0].href).to eq 'http://example.com/foo' } - end - end - - describe '#perform_request' do + describe '#call' do let(:url) { 'http://example.com' } - context 'Check method result' do + subject { FetchAtomService.new.call(url) } + + context 'url is blank' do + let(:url) { '' } + it { is_expected.to be_nil } + end + + context 'request failed' do before do - WebMock.stub_request(:get, url).to_return(status: 200, body: '', headers: {}) - @target = FetchAtomService.new - @target.instance_variable_set('@url', url) + WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {}) end - it 'HTTP::Response instance is returned and set to @response' do - expect(@target.send(:perform_request).status.to_s).to eq '200 OK' - expect(@target.instance_variable_get('@response')).to be_instance_of HTTP::Response + it { is_expected.to be_nil } + end + + context 'raise OpenSSL::SSL::SSLError' do + before do + allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError) + end + + it 'output log and return nil' do + expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('SSL error: OpenSSL::SSL::SSLError') + is_expected.to be_nil end end - context 'check passed parameters to Request' do + context 'raise HTTP::ConnectionError' do before do - @target = FetchAtomService.new - @target.instance_variable_set('@url', url) - @target.instance_variable_set('@unsupported_activity', unsupported_activity) - allow(Request).to receive(:new).with(:get, url) - expect(Request).to receive_message_chain(:new, :add_headers).with('Accept' => accept) - allow(Request).to receive_message_chain(:new, :add_headers, :perform).with(no_args) + allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError) end - context '@unsupported_activity is true' do - let(:unsupported_activity) { true } - let(:accept) { 'text/html' } - it { @target.send(:perform_request) } + it 'output log and return nil' do + expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('HTTP ConnectionError: HTTP::ConnectionError') + is_expected.to be_nil + end + end + + context 'response success' do + let(:body) { '' } + let(:headers) { { 'Content-Type' => content_type } } + let(:json) { + { id: 1, + '@context': ActivityPub::TagManager::CONTEXT, + type: 'Note', + }.to_json + } + + before do + WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) end - context '@unsupported_activity is false' do - let(:unsupported_activity) { false } - let(:accept) { 'application/activity+json, application/ld+json, application/atom+xml, text/html' } - it { @target.send(:perform_request) } + context 'content type is application/atom+xml' do + let(:content_type) { 'application/atom+xml' } + + it { is_expected.to eq [url, {:prefetched_body=>""}, :ostatus] } + end + + context 'content_type is json' do + let(:content_type) { 'application/activity+json' } + let(:body) { json } + + it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } + end + + before do + WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + WebMock.stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) + end + + context 'has link header' do + let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"', } } + + it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } + end + + context 'content type is text/html' do + let(:content_type) { 'text/html' } + let(:body) { '' } + + it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } end end end From 411c9ecb4b7396afe95bbf6f191616dcd3fc970c Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 10 Feb 2018 17:09:44 +0100 Subject: [PATCH 037/257] Fix password recovery (#6459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix password recovery * Use “resource” instead of “current_user” --- app/views/auth/passwords/edit.html.haml | 20 ++++++++++---------- app/views/auth/registrations/edit.html.haml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml index d8fed9e77..703c821c0 100644 --- a/app/views/auth/passwords/edit.html.haml +++ b/app/views/auth/passwords/edit.html.haml @@ -1,18 +1,18 @@ - content_for :page_title do = t('auth.set_new_password') - = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| - = render 'shared/error_messages', object: resource += simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| + = render 'shared/error_messages', object: resource - - if use_pam? || current_user.encrypted_password.present? - = f.input :reset_password_token, as: :hidden + - if !use_pam? || resource.encrypted_password.present? + = f.input :reset_password_token, as: :hidden - = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } - = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } + = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } - .actions - = f.button :button, t('auth.set_new_password'), type: :submit - - else - = t('simple_form.labels.defaults.pam_account') + .actions + = f.button :button, t('auth.set_new_password'), type: :submit + - else + = t('simple_form.labels.defaults.pam_account') .form-footer= render 'auth/shared/links' diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 102199f81..ca18caa56 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -4,7 +4,7 @@ = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f| = render 'shared/error_messages', object: resource - - if !use_pam? || current_user.encrypted_password.present? + - if !use_pam? || resource.encrypted_password.present? = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } From 718802a05dfb3211d758513daf6070ffa22751dd Mon Sep 17 00:00:00 2001 From: Kazushige Tominaga Date: Sun, 11 Feb 2018 01:10:58 +0900 Subject: [PATCH 038/257] Added FetchRemoteAccountService spec (#6456) * Added #link_header spec * Added #call spec * Delete spec of private methods * Added #call spec --- spec/fixtures/requests/webfinger-hacker3.txt | 11 +++ .../fetch_remote_account_service_spec.rb | 67 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 spec/fixtures/requests/webfinger-hacker3.txt diff --git a/spec/fixtures/requests/webfinger-hacker3.txt b/spec/fixtures/requests/webfinger-hacker3.txt new file mode 100644 index 000000000..c59f518bd --- /dev/null +++ b/spec/fixtures/requests/webfinger-hacker3.txt @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Server: nginx/1.6.2 +Date: Sun, 20 Mar 2016 11:13:16 GMT +Content-Type: application/jrd+json +Transfer-Encoding: chunked +Connection: keep-alive +Access-Control-Allow-Origin: * +Vary: Accept-Encoding,Cookie +Strict-Transport-Security: max-age=31536000; includeSubdomains; + +{"subject":"acct:localhost@kickass.zone","aliases":["https:\/\/kickass.zone\/user\/7477","https:\/\/kickass.zone\/gargron","https:\/\/kickass.zone\/index.php\/user\/7477","https:\/\/kickass.zone\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/kickass.zone\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/kickass.zone\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/kickass.zone\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/kickass.zone\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/kickass.zone\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/kickass.zone\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/kickass.zone\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/kickass.zone\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/kickass.zone\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/kickass.zone\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/kickass.zone\/main\/ostatussub?profile={uri}"}]} diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index bb1877c7a..4388d4cf4 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -1,4 +1,71 @@ require 'rails_helper' RSpec.describe FetchRemoteAccountService do + let(:url) { 'https://example.com' } + let(:prefetched_body) { nil } + let(:protocol) { :ostatus } + subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) } + + let(:actor) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/alice', + type: 'Person', + preferredUsername: 'alice', + name: 'Alice', + summary: 'Foo bar', + inbox: 'http://example.com/alice/inbox', + } + end + + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + let(:xml) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'xml', 'mastodon.atom')) } + + shared_examples 'return Account' do + it { is_expected.to be_an Account } + end + + context 'protocol is :activitypub' do + let(:prefetched_body) { Oj.dump(actor) } + let(:protocol) { :activitypub } + + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + end + + include_examples 'return Account' + end + + context 'protocol is :ostatus' do + let(:prefetched_body) { xml } + let(:protocol) { :ostatus } + + before do + stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) + stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) + end + + include_examples 'return Account' + end + + context 'when prefetched_body is nil' do + context 'protocol is :activitypub' do + before do + stub_request(:get, url).to_return(status: 200, body: Oj.dump(actor), headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + end + + include_examples 'return Account' + end + + context 'protocol is :ostatus' do + before do + stub_request(:get, url).to_return(status: 200, body: xml, headers: { 'Content-Type' => 'application/atom+xml' }) + stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) + stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) + end + + include_examples 'return Account' + end + end end From cf36d184f41b5bfc0c63d6c8409b05cca2eb67ee Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 11 Feb 2018 18:40:57 +0100 Subject: [PATCH 039/257] Interactive `rake mastodon:setup` task (#6451) * Add better CLI prompt * Add rake mastodon:setup interactive wizard * Test db/redis/smtp configurations and add admin user at the end * Test database connection even when database does not exist yet --- Gemfile | 2 + Gemfile.lock | 26 +++ lib/tasks/mastodon.rake | 408 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 414 insertions(+), 22 deletions(-) diff --git a/Gemfile b/Gemfile index d1c00b498..e219f5159 100644 --- a/Gemfile +++ b/Gemfile @@ -76,6 +76,8 @@ gem 'simple-navigation', '~> 4.0' gem 'simple_form', '~> 3.4' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'strong_migrations' +gem 'tty-command' +gem 'tty-prompt' gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2017' gem 'webpacker', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index b82fc49a6..2131afa65 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -173,6 +173,7 @@ GEM faraday multi_json encryptor (3.0.0) + equatable (0.5.0) erubi (1.7.0) et-orbi (1.0.8) tzinfo @@ -224,6 +225,7 @@ GEM hashie (3.5.7) highline (1.7.10) hiredis (0.6.1) + hitimes (1.2.6) hkdf (0.3.0) htmlentities (4.3.4) http (3.0.0) @@ -312,6 +314,7 @@ GEM msgpack (1.1.0) multi_json (1.12.2) multipart-post (2.0.0) + necromancer (0.4.0) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.2.0) @@ -356,6 +359,9 @@ GEM parallel parser (2.4.0.2) ast (~> 2.3) + pastel (0.7.2) + equatable (~> 0.5.0) + tty-color (~> 0.4.0) pg (0.21.0) pghero (1.7.0) activerecord @@ -550,6 +556,23 @@ GEM thread (0.2.2) thread_safe (0.3.6) tilt (2.0.8) + timers (4.1.2) + hitimes + tty-color (0.4.2) + tty-command (0.7.0) + pastel (~> 0.7.0) + tty-cursor (0.5.0) + tty-prompt (0.15.0) + necromancer (~> 0.4.0) + pastel (~> 0.7.0) + timers (~> 4.0) + tty-cursor (~> 0.5.0) + tty-reader (~> 0.2.0) + tty-reader (0.2.0) + tty-cursor (~> 0.5.0) + tty-screen (~> 0.6.4) + wisper (~> 2.0.0) + tty-screen (0.6.4) twitter-text (1.14.7) unf (~> 0.1.0) tzinfo (1.2.4) @@ -579,6 +602,7 @@ GEM websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) + wisper (2.0.0) xpath (2.1.0) nokogiri (~> 1.3) @@ -684,6 +708,8 @@ DEPENDENCIES simplecov (~> 0.14) sprockets-rails (~> 3.2) strong_migrations + tty-command + tty-prompt twitter-text (~> 1.14) tzinfo-data (~> 1.2017) uglifier (~> 3.2) diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 486c035de..e144621e5 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -4,6 +4,362 @@ require 'optparse' require 'colorize' namespace :mastodon do + desc 'Configure the instance for production use' + task :setup do + prompt = TTY::Prompt.new + env = {} + + begin + prompt.say('Your instance is identified by its domain name. Changing it afterward will break things.') + env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q| + q.required true + q.modify :strip + q.validate(/\A[a-z0-9\.\-]+\z/i) + q.messages[:valid?] = 'Invalid domain. If you intend to use unicode characters, enter punycode here' + end + + prompt.say "\n" + + prompt.say('Single user mode disables registrations and redirects the landing page to your public profile.') + env['SINGLE_USER_MODE'] = prompt.yes?('Do you want to enable single user mode?', default: false) + + %w(SECRET_KEY_BASE PAPERCLIP_SECRET OTP_SECRET).each do |key| + env[key] = SecureRandom.hex(64) + end + + vapid_key = Webpush.generate_key + + env['VAPID_PRIVATE_KEY'] = vapid_key.private_key + env['VAPID_PUBLIC_KEY'] = vapid_key.public_key + + prompt.say "\n" + + using_docker = prompt.yes?('Are you using Docker to run Mastodon?') + db_connection_works = false + + prompt.say "\n" + + loop do + env['DB_HOST'] = prompt.ask('PostgreSQL host:') do |q| + q.required true + q.default using_docker ? 'db' : '/var/run/postgresql' + q.modify :strip + end + + env['DB_PORT'] = prompt.ask('PostgreSQL port:') do |q| + q.required true + q.default 5432 + q.convert :int + end + + env['DB_NAME'] = prompt.ask('Name of PostgreSQL database:') do |q| + q.required true + q.default using_docker ? 'postgres' : 'mastodon_production' + q.modify :strip + end + + env['DB_USER'] = prompt.ask('Name of PostgreSQL user:') do |q| + q.required true + q.default using_docker ? 'postgres' : 'mastodon' + q.modify :strip + end + + env['DB_PASS'] = prompt.ask('Password of PostgreSQL user:') do |q| + q.echo false + end + + # The chosen database may not exist yet. Connect to default database + # to avoid "database does not exist" error. + db_options = { + adapter: :postgresql, + database: 'postgres', + host: env['DB_HOST'], + port: env['DB_PORT'], + user: env['DB_USER'], + password: env['DB_PASS'], + } + + begin + ActiveRecord::Base.establish_connection(db_options) + ActiveRecord::Base.connection + prompt.ok 'Database configuration works! 🎆' + db_connection_works = true + break + rescue StandardError => e + prompt.error 'Database connection could not be established with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + + loop do + env['REDIS_HOST'] = prompt.ask('Redis host:') do |q| + q.required true + q.default using_docker ? 'redis' : 'localhost' + q.modify :strip + end + + env['REDIS_PORT'] = prompt.ask('Redis port:') do |q| + q.required true + q.default 6379 + q.convert :int + end + + redis_options = { + host: env['REDIS_HOST'], + port: env['REDIS_PORT'], + driver: :hiredis, + } + + begin + redis = Redis.new(redis_options) + redis.ping + prompt.ok 'Redis configuration works! 🎆' + break + rescue StandardError => e + prompt.error 'Redis connection could not be established with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + + if prompt.yes?('Do you want to store uploaded files on the cloud?', default: false) + case prompt.select('Provider', ['Amazon S3', 'Wasabi', 'Minio']) + when 'Amazon S3' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + + env['S3_BUCKET'] = prompt.ask('S3 bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['S3_REGION'] = prompt.ask('S3 region:') do |q| + q.required true + q.default 'us-east-1' + q.modify :strip + end + + env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q| + q.required true + q.default 's3-us-east-1.amazonaws.com' + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('S3 access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('S3 secret key:') do |q| + q.required true + q.modify :strip + end + when 'Wasabi' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + env['S3_REGION'] = 'us-east-1' + env['S3_HOSTNAME'] = 's3.wasabisys.com' + env['S3_ENDPOINT'] = 'https://s3.wasabisys.com/' + + env['S3_BUCKET'] = prompt.ask('Wasabi bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('Wasabi access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Wasabi secret key:') do |q| + q.required true + q.modify :strip + end + when 'Minio' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + env['S3_REGION'] = 'us-east-1' + + env['S3_ENDPOINT'] = prompt.ask('Minio endpoint URL:') do |q| + q.required true + q.modify :strip + end + + env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http' + env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '') + + env['S3_BUCKET'] = prompt.ask('Minio bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('Minio access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Minio secret key:') do |q| + q.required true + q.modify :strip + end + end + + if prompt.yes?('Do you want to access the uploaded files from your own domain?') + env['S3_CLOUDFRONT_HOST'] = prompt.ask('Domain for uploaded files:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + end + end + + prompt.say "\n" + + loop do + env['SMTP_SERVER'] = prompt.ask('SMTP server:') do |q| + q.required true + q.default 'smtp.mailgun.org' + q.modify :strip + end + + env['SMTP_PORT'] = prompt.ask('SMTP port:') do |q| + q.required true + q.default 587 + q.convert :int + end + + env['SMTP_LOGIN'] = prompt.ask('SMTP username:') do |q| + q.modify :strip + end + + env['SMTP_PASSWORD'] = prompt.ask('SMTP password:') do |q| + q.echo false + end + + env['SMTP_FROM_ADDRESS'] = prompt.ask('E-mail address to send e-mails "from":') do |q| + q.required true + q.default "Mastodon " + q.modify :strip + end + + break unless prompt.yes?('Send a test e-mail with this configuration right now?') + + send_to = prompt.ask('Send test e-mail to:', required: true) + + begin + ActionMailer::Base.smtp_settings = { + :port => env['SMTP_PORT'], + :address => env['SMTP_SERVER'], + :user_name => env['SMTP_LOGIN'].presence, + :password => env['SMTP_PASSWORD'].presence, + :domain => env['LOCAL_DOMAIN'], + :authentication => :plain, + :enable_starttls_auto => true, + } + + ActionMailer::Base.default_options = { + from: env['SMTP_FROM_ADDRESS'], + } + + mail = ActionMailer::Base.new.mail to: send_to, subject: 'Test', body: 'Mastodon SMTP configuration works!' + mail.deliver + rescue StandardError => e + prompt.error 'E-mail could not be sent with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + prompt.say 'This configuration will be written to .env.production' + + if prompt.yes?('Save configuration?') + cmd = TTY::Command.new(printer: :quiet) + + File.write(Rails.root.join('.env.production'), "# Generated with mastodon:setup on #{Time.now.utc}\n\n" + env.each_pair.map { |key, value| "#{key}=#{value}" }.join("\n") + "\n") + + prompt.say "\n" + prompt.say 'Now that configuration is saved, the database schema must be loaded.' + prompt.warn 'If the database already exists, this will erase its contents.' + + if prompt.yes?('Prepare the database now?') + prompt.say 'Running `RAILS_ENV=production rails db:setup` ...' + prompt.say "\n" + + if cmd.run!({ RAILS_ENV: 'production' }, :rails, 'db:setup').failure? + prompt.say "\n" + prompt.error 'That failed! Perhaps your configuration is not right' + else + prompt.say "\n" + prompt.ok 'Done!' + end + end + + prompt.say "\n" + prompt.say 'The final step is compiling CSS/JS assets.' + prompt.say 'This may take a while and consume a lot of RAM.' + + if prompt.yes?('Compile the assets now?') + prompt.say 'Running `RAILS_ENV=production rails assets:precompile` ...' + prompt.say "\n" + + if cmd.run!({ RAILS_ENV: 'production' }, :rails, 'assets:precompile').failure? + prompt.say "\n" + prompt.error 'That failed! Maybe you need swap space?' + else + prompt.say "\n" + prompt.say 'Done!' + end + end + + prompt.say "\n" + prompt.ok 'All done! You can now power on the Mastodon server 🐘' + prompt.say "\n" + + if db_connection_works && prompt.yes?('Do you want to create an admin user straight away?') + env.each_pair do |key, value| + ENV[key] = value.to_s + end + + require_relative '../../config/environment' + disable_log_stdout! + + username = prompt.ask('Username:') do |q| + q.required true + q.default 'admin' + q.validate(/\A[a-z0-9_]+\z/i) + q.modify :strip + end + + email = prompt.ask('E-mail:') do |q| + q.required true + q.modify :strip + end + + password = SecureRandom.hex(16) + + user = User.new(admin: true, email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }) + user.save(validate: false) + + prompt.ok "You can login with the password: #{password}" + prompt.warn 'You can change your password once you login.' + end + else + prompt.warn 'Nothing saved. Bye!' + end + rescue TTY::Reader::InputInterrupt + prompt.ok 'Aborting. Bye!' + end + end + desc 'Execute daily tasks (deprecated)' task :daily do # No-op @@ -67,32 +423,40 @@ namespace :mastodon do desc 'Add a user by providing their email, username and initial password.' \ 'The user will receive a confirmation email, then they must reset their password before logging in.' task add_user: :environment do - print 'Enter email: ' - email = STDIN.gets.chomp + disable_log_stdout! - print 'Enter username: ' - username = STDIN.gets.chomp + prompt = TTY::Prompt.new - print 'Create user and send them confirmation mail [y/N]: ' - confirm = STDIN.gets.chomp - puts - - if confirm.casecmp('y').zero? - password = SecureRandom.hex - user = User.new(email: email, password: password, account_attributes: { username: username }) - if user.save - puts 'User added and confirmation mail sent to user\'s email address.' - puts "Here is the random password generated for the user: #{password}" - else - puts 'Following errors occured while creating new user:' - user.errors.each do |key, val| - puts "#{key}: #{val}" - end + begin + email = prompt.ask('E-mail:', required: true) do |q| + q.modify :strip end - else - puts 'Aborted by user.' + + username = prompt.ask('Username:', required: true) do |q| + q.modify :strip + end + + role = prompt.select('Role:', %w(user moderator admin)) + + if prompt.yes?('Proceed to create the user?') + user = User.new(email: email, password: SecureRandom.hex, admin: role == 'admin', moderator: role == 'moderator', account_attributes: { username: username }) + + if user.save + prompt.ok 'User created and confirmation mail sent to the user\'s email address.' + prompt.ok "Here is the random password generated for the user: #{password}" + else + prompt.warn 'User was not created because of the following errors:' + + user.errors.each do |key, val| + prompt.error "#{key}: #{val}" + end + end + else + prompt.ok 'Aborting. Bye!' + end + rescue TTY::Reader::InputInterrupt + prompt.ok 'Aborting. Bye!' end - puts end namespace :media do From e20700fe8fa22562d3df10ad0361e65676a01231 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 11 Feb 2018 22:59:44 +0100 Subject: [PATCH 040/257] Fix Chewy trying to update index with the wrong strategy (#6464) --- config/initializers/chewy.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/initializers/chewy.rb b/config/initializers/chewy.rb index bef2746ec..702f7516c 100644 --- a/config/initializers/chewy.rb +++ b/config/initializers/chewy.rb @@ -11,7 +11,8 @@ Chewy.settings = { journal: false, } -Chewy.root_strategy = enabled ? :sidekiq : :bypass +Chewy.root_strategy = enabled ? :sidekiq : :bypass +Chewy.request_strategy = enabled ? :sidekiq : :bypass module Chewy class << self From 6ef3874b2eac79cc2b602de609793254b8d6c611 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Sun, 11 Feb 2018 22:49:18 +0000 Subject: [PATCH 041/257] Fix URLs incorrectly having trailing hyphen removed (#6465) In cases where a URL has a trailing hyphen the FetchLinkCardService incorrectly removes the hyphen when it is parsed The hyphen is not a reserved character in the URI spec https://tools.ietf.org/html/rfc3986#section-2.2 --- config/initializers/twitter_regex.rb | 2 +- spec/services/fetch_link_card_service_spec.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb index e924fac22..7fa828300 100644 --- a/config/initializers/twitter_regex.rb +++ b/config/initializers/twitter_regex.rb @@ -2,7 +2,7 @@ module Twitter class Regex REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}\(\)\?]/iou - REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*';:=\,\.\$%\[\]\p{Pd}~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou + REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*';:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou REGEXEN[:valid_url_balanced_parens] = / \( (?: diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index ba61d22c3..edacc4425 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -15,6 +15,8 @@ RSpec.describe FetchLinkCardService do stub_request(:head, 'http://example.com/日本語').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt')) stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404) + stub_request(:head, 'http://example.com/test-').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt')) subject.call(status) end @@ -63,6 +65,14 @@ RSpec.describe FetchLinkCardService do expect(status.preview_cards.first.title).to eq("SJISのページ") end end + + context do + let(:status) { Fabricate(:status, text: 'test http://example.com/test-') } + + it 'works with a URL ending with a hyphen' do + expect(a_request(:get, 'http://example.com/test-')).to have_been_made.at_least_once + end + end end context 'in a remote status' do From ba8ec4eed67cdd72c56a42a0bd093bcab73f7e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= Date: Tue, 13 Feb 2018 23:55:45 +0100 Subject: [PATCH 042/257] i18n: Update Polish translation (#6470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- app/javascript/mastodon/locales/pl.json | 13 ++++++++----- config/locales/pl.yml | 5 +++++ config/locales/simple_form.pl.yml | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 81ec79776..1a3efdce7 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -8,7 +8,7 @@ "account.follows": "Śledzeni", "account.follows_you": "Śledzi Cię", "account.hide_reblogs": "Ukryj podbicia od @{name}", - "account.media": "Media", + "account.media": "Zawartość multimedialna", "account.mention": "Wspomnij o @{name}", "account.moved_to": "{name} przeniósł się do:", "account.mute": "Wycisz @{name}", @@ -129,7 +129,7 @@ "lightbox.next": "Następne", "lightbox.previous": "Poprzednie", "lists.account.add": "Dodaj do listy", - "lists.account.remove": "Remove from list", + "lists.account.remove": "Usuń z listy", "lists.delete": "Usuń listę", "lists.edit": "Edytuj listę", "lists.new.create": "Utwórz listę", @@ -139,7 +139,7 @@ "loading_indicator.label": "Ładowanie…", "media_gallery.toggle_visible": "Przełącz widoczność", "missing_indicator.label": "Nie znaleziono", - "missing_indicator.sublabel": "This resource could not be found", + "missing_indicator.sublabel": "Nie można odnaleźć tego zasobu", "mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?", "navigation_bar.blocks": "Zablokowani użytkownicy", "navigation_bar.community_timeline": "Lokalna oś czasu", @@ -199,8 +199,8 @@ "privacy.public.short": "Publiczny", "privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu", "privacy.unlisted.short": "Niewidoczny", - "regeneration_indicator.label": "Loading…", - "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "regeneration_indicator.label": "Ładowanie…", + "regeneration_indicator.sublabel": "Twoja oś czasu jest przygotowywana!", "relative_time.days": "{number} dni", "relative_time.hours": "{number} godz.", "relative_time.just_now": "teraz", @@ -216,6 +216,9 @@ "search_popout.tips.status": "wpis", "search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów", "search_popout.tips.user": "użytkownik", + "search_results.accounts": "Ludzie", + "search_results.hashtags": "Hashtagi", + "search_results.statuses": "Wpisy", "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", "standalone.public_title": "Spojrzenie w głąb…", "status.block": "Zablokuj @{name}", diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 633850b28..010b03ed2 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -359,6 +359,7 @@ pl: auth: agreement_html: Rejestrując się, oświadczasz, że zapoznałeś się z informacjami o instancji i zasadami korzystania z usługi. change_password: Bezpieczeństwo + confirm_email: Potwierdź adres e-mail delete_account: Usunięcie konta delete_account_html: Jeżeli chcesz usunąć konto, przejdź tutaj. Otrzymasz prośbę o potwierdzenie. didnt_get_confirmation: Nie otrzymałeś instrukcji weryfikacji? @@ -368,6 +369,10 @@ pl: logout: Wyloguj się migrate_account: Przenieś konto migrate_account_html: Jeżeli chcesz skonfigurować przekierowanie z obecnego konta na inne, możesz skonfigurować to tutaj. + or_log_in_with: Lub zaloguj się używając + providers: + cas: CAS + saml: SAML register: Rejestracja resend_confirmation: Ponownie prześlij instrukcje weryfikacji reset_password: Zresetuj hasło diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 23eb8e83b..e078dea1f 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -49,6 +49,7 @@ pl: setting_default_privacy: Widoczność wpisów setting_default_sensitive: Zawsze oznaczaj zawartość multimedialną jako wrażliwą setting_delete_modal: Pytaj o potwierdzenie przed usunięciem wpisu + setting_display_sensitive_media: Zawsze oznaczaj zawartość multimedialną jako wrażliwą setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych setting_reduce_motion: Ogranicz ruch w animacjach setting_system_font_ui: Używaj domyślnej czcionki systemu @@ -57,6 +58,7 @@ pl: severity: Priorytet type: Typ importu username: Nazwa użytkownika + username_or_email: Nazwa użytkownika lub adres e-mail interactions: must_be_follower: Nie wyświetlaj powiadomień od osób, które Cię nie śledzą must_be_following: Nie wyświetlaj powiadomień od osób, których nie śledzisz From ecdac9017efceb77da155bf85d5e7d6084382da2 Mon Sep 17 00:00:00 2001 From: abcang Date: Thu, 15 Feb 2018 12:40:42 +0900 Subject: [PATCH 043/257] Fix media button type (#6478) --- .../mastodon/components/media_gallery.js | 2 +- app/javascript/mastodon/features/video/index.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index a276b17d5..00943e205 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -249,7 +249,7 @@ export default class MediaGallery extends React.PureComponent { } children = ( - diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 7752fc057..6335d84b6 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -271,7 +271,7 @@ export default class Video extends React.PureComponent { onProgress={this.handleProgress} /> - @@ -290,10 +290,10 @@ export default class Video extends React.PureComponent {
- - + + - {!onCloseVideo && } + {!onCloseVideo && } {(detailed || fullscreen) && @@ -305,9 +305,9 @@ export default class Video extends React.PureComponent {
- {(!fullscreen && onOpenVideo) && } - {onCloseVideo && } - + {(!fullscreen && onOpenVideo) && } + {onCloseVideo && } +
From f7765acf9d92951a616f41b738d5d23ede58c162 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 15 Feb 2018 07:04:28 +0100 Subject: [PATCH 044/257] Fix #5173: Click card to embed external content (#6471) --- .../features/status/components/card.js | 150 +++++++++++------- .../styles/mastodon/components.scss | 63 ++++++-- app/services/fetch_link_card_service.rb | 16 +- 3 files changed, 150 insertions(+), 79 deletions(-) diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 2f6a7831e..8a6383471 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -20,6 +20,16 @@ const getHostname = url => { return parser.hostname; }; +const trim = (text, len) => { + const cut = text.indexOf(' ', len); + + if (cut === -1) { + return text; + } + + return text.substring(0, cut) + (text.length > len ? '…' : ''); +}; + export default class Card extends React.PureComponent { static propTypes = { @@ -33,9 +43,16 @@ export default class Card extends React.PureComponent { }; state = { - width: 0, + width: 280, + embedded: false, }; + componentWillReceiveProps (nextProps) { + if (this.props.card !== nextProps.card) { + this.setState({ embedded: false }); + } + } + handlePhotoClick = () => { const { card, onOpenMedia } = this.props; @@ -57,56 +74,14 @@ export default class Card extends React.PureComponent { ); }; - renderLink () { - const { card, maxDescription } = this.props; - const { width } = this.state; - const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width); - - let image = ''; - let provider = card.get('provider_name'); - - if (card.get('image')) { - image = ( -
- {card.get('title')} -
- ); - } - - if (provider.length < 1) { - provider = decodeIDNA(getHostname(card.get('url'))); - } - - const className = classnames('status-card', { horizontal }); - - return ( - - {image} - -
- {card.get('title')} - {!horizontal &&

{(card.get('description') || '').substring(0, maxDescription)}

} - {provider} -
-
- ); - } - - renderPhoto () { + handleEmbedClick = () => { const { card } = this.props; - return ( - {card.get('title')} - ); + if (card.get('type') === 'photo') { + this.handlePhotoClick(); + } else { + this.setState({ embedded: true }); + } } setRef = c => { @@ -125,7 +100,7 @@ export default class Card extends React.PureComponent { return (
@@ -133,23 +108,76 @@ export default class Card extends React.PureComponent { } render () { - const { card } = this.props; + const { card, maxDescription } = this.props; + const { width, embedded } = this.state; if (card === null) { return null; } - switch(card.get('type')) { - case 'link': - return this.renderLink(); - case 'photo': - return this.renderPhoto(); - case 'video': - return this.renderVideo(); - case 'rich': - default: - return null; + const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); + const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link'; + const className = classnames('status-card', { horizontal }); + const interactive = card.get('type') !== 'link'; + const title = interactive ? {card.get('title')} : {card.get('title')}; + const ratio = card.get('width') / card.get('height'); + const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); + + const description = ( +
+ {title} + {!horizontal &&

{trim(card.get('description') || '', maxDescription)}

} + {provider} +
+ ); + + let embed = ''; + let thumbnail =
; + + if (interactive) { + if (embedded) { + embed = this.renderVideo(); + } else { + let iconVariant = 'play'; + + if (card.get('type') === 'photo') { + iconVariant = 'search-plus'; + } + + embed = ( +
+ {thumbnail} + +
+
+ + +
+
+
+ ); + } + + return ( +
+ {embed} + {description} +
+ ); + } else if (card.get('image')) { + embed = ( +
+ {thumbnail} +
+ ); } + + return ( + + {embed} + {description} + + ); } } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index fe895809a..d1fbabfc5 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2208,7 +2208,6 @@ .status-card { display: flex; - cursor: pointer; font-size: 14px; border: 1px solid lighten($ui-base-color, 8%); border-radius: 4px; @@ -2217,20 +2216,58 @@ text-decoration: none; overflow: hidden; - &:hover { - background: lighten($ui-base-color, 8%); + &__actions { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + + & > div { + background: rgba($base-shadow-color, 0.6); + border-radius: 4px; + padding: 12px 9px; + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + } + + button, + a { + display: inline; + color: $primary-text-color; + background: transparent; + border: 0; + padding: 0 5px; + text-decoration: none; + opacity: 0.6; + font-size: 18px; + line-height: 18px; + + &:hover, + &:active, + &:focus { + opacity: 1; + } + } + + a { + font-size: 19px; + position: relative; + bottom: -1px; + } } } -.status-card-video, -.status-card-rich, -.status-card-photo { - margin-top: 14px; - overflow: hidden; +a.status-card { + cursor: pointer; - iframe { - width: 100%; - height: auto; + &:hover { + background: lighten($ui-base-color, 8%); } } @@ -2258,6 +2295,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + text-decoration: none; } .status-card__content { @@ -2279,6 +2317,7 @@ .status-card__image { flex: 0 0 100px; background: lighten($ui-base-color, 8%); + position: relative; } .status-card.horizontal { @@ -2304,6 +2343,8 @@ width: 100%; height: 100%; object-fit: cover; + background-size: cover; + background-position: center center; } .load-more { diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 3e31a4145..8f252e64c 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -94,14 +94,16 @@ class FetchLinkCardService < BaseService @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url) when 'photo' return false unless embed.respond_to?(:url) + @card.embed_url = embed.url @card.image_remote_url = embed.url @card.width = embed.width.presence || 0 @card.height = embed.height.presence || 0 when 'video' - @card.width = embed.width.presence || 0 - @card.height = embed.height.presence || 0 - @card.html = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED) + @card.width = embed.width.presence || 0 + @card.height = embed.height.presence || 0 + @card.html = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED) + @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url) when 'rich' # Most providers rely on