From 64171730827c58df26a7ad75f0e58f17c2add118 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 9 Jan 2023 12:10:19 +0000 Subject: [PATCH] DEV: Apply syntax_tree formatting to `lib/*` --- .streerc | 1 - .../session/discourse_cookie_store.rb | 4 +- lib/admin_confirmation.rb | 11 +- lib/admin_constraint.rb | 2 - lib/admin_user_index_query.rb | 86 +- lib/age_words.rb | 2 - lib/archetype.rb | 18 +- lib/auth.rb | 21 +- lib/auth/auth_provider.rb | 45 +- lib/auth/current_user_provider.rb | 4 +- lib/auth/default_current_user_provider.rb | 227 ++- lib/auth/discord_authenticator.rb | 58 +- lib/auth/facebook_authenticator.rb | 23 +- lib/auth/github_authenticator.rb | 16 +- lib/auth/google_oauth2_authenticator.rb | 88 +- lib/auth/managed_authenticator.rb | 51 +- lib/auth/result.rb | 121 +- lib/auth/twitter_authenticator.rb | 11 +- lib/autospec/base_runner.rb | 4 - lib/autospec/formatter.rb | 17 +- lib/autospec/manager.rb | 74 +- lib/autospec/reload_css.rb | 9 +- lib/autospec/rspec_runner.rb | 30 +- lib/autospec/simple_runner.rb | 58 +- lib/backup_restore.rb | 20 +- lib/backup_restore/backup_file_handler.rb | 44 +- lib/backup_restore/backup_store.rb | 6 +- lib/backup_restore/backuper.rb | 125 +- lib/backup_restore/database_restorer.rb | 73 +- lib/backup_restore/local_backup_store.rb | 11 +- lib/backup_restore/logger.rb | 7 +- lib/backup_restore/meta_data_handler.rb | 6 +- lib/backup_restore/restorer.rb | 10 +- lib/backup_restore/s3_backup_store.rb | 53 +- lib/backup_restore/system_interface.rb | 4 +- lib/backup_restore/uploads_restorer.rb | 61 +- lib/badge_queries.rb | 3 +- lib/bookmark_query.rb | 50 +- lib/bookmark_reminder_notification_handler.rb | 6 +- lib/browser_detection.rb | 2 - lib/cache.rb | 17 +- lib/canonical_url.rb | 19 +- lib/category_badge.rb | 112 +- lib/chrome_installed_checker.rb | 22 +- lib/comment_migration.rb | 27 +- lib/common_passwords.rb | 10 +- lib/composer_messages_finder.rb | 230 +-- lib/compression/engine.rb | 5 +- lib/compression/gzip.rb | 21 +- lib/compression/pipeline.rb | 18 +- lib/compression/strategy.rb | 10 +- lib/compression/tar.rb | 16 +- lib/compression/zip.rb | 15 +- lib/configurable_urls.rb | 8 +- lib/content_buffer.rb | 8 +- lib/content_security_policy.rb | 4 +- lib/content_security_policy/builder.rb | 8 +- lib/content_security_policy/default.rb | 94 +- lib/content_security_policy/extension.rb | 62 +- lib/content_security_policy/middleware.rb | 16 +- lib/cooked_post_processor.rb | 233 +-- lib/cooked_processor_mixin.rb | 94 +- lib/crawler_detection.rb | 47 +- lib/csrf_token_verifier.rb | 7 +- lib/current_user.rb | 2 - lib/db_helper.rb | 108 +- lib/demon/base.rb | 81 +- lib/demon/email_sync.rb | 49 +- lib/demon/rails_autospec.rb | 8 +- lib/demon/sidekiq.rb | 10 +- lib/directory_helper.rb | 7 +- lib/discourse.rb | 452 +++--- lib/discourse_connect_base.rb | 35 +- lib/discourse_connect_provider.rb | 41 +- lib/discourse_dev/category.rb | 9 +- lib/discourse_dev/config.rb | 39 +- lib/discourse_dev/group.rb | 7 +- lib/discourse_dev/post.rb | 42 +- lib/discourse_dev/post_revision.rb | 5 +- lib/discourse_dev/record.rb | 27 +- lib/discourse_dev/tag.rb | 11 +- lib/discourse_dev/topic.rb | 42 +- lib/discourse_diff.rb | 62 +- lib/discourse_event.rb | 12 +- lib/discourse_hub.rb | 64 +- lib/discourse_ip_info.rb | 79 +- lib/discourse_js_processor.rb | 187 ++- lib/discourse_logstash_logger.rb | 29 +- lib/discourse_plugin_registry.rb | 26 +- lib/discourse_redis.rb | 137 +- lib/discourse_sourcemapping_url_processor.rb | 3 +- lib/discourse_tagging.rb | 300 ++-- lib/discourse_updates.rb | 73 +- lib/disk_space.rb | 4 +- lib/distributed_cache.rb | 13 +- lib/distributed_mutex.rb | 10 +- lib/edit_rate_limiter.rb | 2 +- lib/email.rb | 10 +- lib/email/authentication_results.rb | 56 +- lib/email/build_email_helper.rb | 4 +- lib/email/cleaner.rb | 9 +- lib/email/message_builder.rb | 196 ++- lib/email/message_id_service.rb | 35 +- lib/email/processor.rb | 107 +- lib/email/receiver.rb | 782 +++++---- lib/email/renderer.rb | 29 +- lib/email/sender.rb | 291 ++-- lib/email/styles.rb | 512 +++--- lib/email/validator.rb | 5 +- lib/email_backup_token.rb | 1 - .../base_email_unsubscriber.rb | 12 +- .../digest_email_unsubscriber.rb | 40 +- .../topic_email_unsubscriber.rb | 37 +- lib/email_cook.rb | 22 +- lib/email_updater.rb | 43 +- lib/ember_cli.rb | 68 +- lib/encodings.rb | 15 +- lib/enum.rb | 8 +- lib/excerpt_parser.rb | 93 +- lib/external_upload_helpers.rb | 248 +-- lib/faker/discourse.rb | 11 +- lib/faker/discourse_markdown.rb | 28 +- lib/feed_element_installer.rb | 41 +- lib/file_helper.rb | 68 +- lib/file_store/base_store.rb | 54 +- lib/file_store/local_store.rb | 25 +- lib/file_store/s3_store.rb | 160 +- lib/file_store/to_s3_migration.rb | 135 +- lib/filter_best_posts.rb | 31 +- lib/final_destination.rb | 154 +- lib/final_destination/resolver.rb | 21 +- lib/final_destination/ssrf_detector.rb | 6 +- lib/flag_settings.rb | 4 +- .../better_handlebars_errors.rb | 3 +- lib/freedom_patches/cose_rsapkcs1.rb | 8 +- lib/freedom_patches/fast_pluck.rb | 14 +- lib/freedom_patches/inflector_backport.rb | 33 +- lib/freedom_patches/ip_addr.rb | 18 +- lib/freedom_patches/mail_disable_starttls.rb | 4 +- lib/freedom_patches/rails4.rb | 109 +- lib/freedom_patches/rails_multisite.rb | 14 +- lib/freedom_patches/safe_buffer.rb | 11 +- lib/freedom_patches/safe_migrations.rb | 4 +- .../schema_migration_details.rb | 31 +- lib/freedom_patches/sprockets_patches.rb | 5 +- lib/freedom_patches/translate_accelerator.rb | 53 +- lib/gaps.rb | 5 +- lib/git_url.rb | 2 +- lib/global_path.rb | 6 +- lib/group_email_credentials_check.rb | 10 +- lib/guardian.rb | 173 +- lib/guardian/category_guardian.rb | 34 +- lib/guardian/ensure_magic.rb | 6 +- lib/guardian/group_guardian.rb | 19 +- lib/guardian/post_guardian.rb | 195 +-- lib/guardian/post_revision_guardian.rb | 2 - lib/guardian/tag_guardian.rb | 22 +- lib/guardian/topic_guardian.rb | 167 +- lib/guardian/user_guardian.rb | 58 +- lib/has_errors.rb | 5 +- lib/highlight_js.rb | 48 +- lib/hijack.rb | 93 +- lib/homepage_constraint.rb | 2 +- lib/html_prettify.rb | 120 +- lib/html_to_markdown.rb | 145 +- lib/http_language_parser.rb | 6 +- lib/i18n/backend/discourse_i18n.rb | 24 +- lib/i18n/duplicate_key_finder.rb | 3 +- lib/i18n/locale_file_checker.rb | 54 +- lib/i18n/locale_file_walker.rb | 6 +- lib/image_sizer.rb | 6 +- lib/imap/providers/detector.rb | 2 +- lib/imap/providers/generic.rb | 143 +- lib/imap/providers/gmail.rb | 78 +- lib/imap/sync.rb | 211 ++- lib/import/normalize.rb | 9 +- lib/import_export.rb | 6 +- lib/import_export/base_exporter.rb | 143 +- lib/import_export/category_exporter.rb | 19 +- .../category_structure_exporter.rb | 7 +- lib/import_export/group_exporter.rb | 6 +- lib/import_export/importer.rb | 101 +- lib/import_export/topic_exporter.rb | 7 +- .../translation_overrides_exporter.rb | 6 +- lib/inline_oneboxer.rb | 55 +- lib/js_locale_helper.rb | 146 +- lib/json_error.rb | 6 +- lib/letter_avatar.rb | 481 +++--- lib/markdown_linker.rb | 6 +- lib/mem_info.rb | 4 +- lib/message_bus_diags.rb | 2 - lib/method_profiler.rb | 84 +- lib/middleware/anonymous_cache.rb | 140 +- lib/middleware/discourse_public_exceptions.rb | 38 +- lib/middleware/enforce_hostname.rb | 3 +- lib/middleware/missing_avatars.rb | 8 +- lib/middleware/omniauth_bypass_middleware.rb | 20 +- lib/middleware/request_tracker.rb | 155 +- lib/middleware/turbo_dev.rb | 14 +- lib/migration/base_dropper.rb | 24 +- lib/migration/column_dropper.rb | 6 +- lib/migration/safe_migrate.rb | 39 +- lib/migration/table_dropper.rb | 2 +- lib/mini_sql_multisite_connection.rb | 20 +- lib/mobile_detection.rb | 9 +- lib/new_post_manager.rb | 137 +- lib/new_post_result.rb | 3 +- lib/notification_levels.rb | 28 +- lib/onebox.rb | 4 +- lib/onebox/domain_checker.rb | 7 +- lib/onebox/engine.rb | 8 +- .../engine/allowlisted_generic_onebox.rb | 208 +-- lib/onebox/engine/amazon_onebox.rb | 116 +- lib/onebox/engine/animated_image_onebox.rb | 2 +- lib/onebox/engine/audio_onebox.rb | 2 +- lib/onebox/engine/audioboom_onebox.rb | 2 +- lib/onebox/engine/band_camp_onebox.rb | 2 +- lib/onebox/engine/cloud_app_onebox.rb | 2 +- lib/onebox/engine/coub_onebox.rb | 2 +- lib/onebox/engine/facebook_media_onebox.rb | 2 +- lib/onebox/engine/five_hundred_px_onebox.rb | 2 +- lib/onebox/engine/flickr_onebox.rb | 6 +- lib/onebox/engine/flickr_shortened_onebox.rb | 4 +- lib/onebox/engine/gfycat_onebox.rb | 19 +- lib/onebox/engine/github_actions_onebox.rb | 38 +- lib/onebox/engine/github_blob_onebox.rb | 8 +- lib/onebox/engine/github_commit_onebox.rb | 30 +- lib/onebox/engine/github_folder_onebox.rb | 6 +- lib/onebox/engine/github_gist_onebox.rb | 10 +- lib/onebox/engine/github_issue_onebox.rb | 43 +- .../engine/github_pull_request_onebox.rb | 50 +- lib/onebox/engine/gitlab_blob_onebox.rb | 8 +- lib/onebox/engine/google_calendar_onebox.rb | 2 +- lib/onebox/engine/google_docs_onebox.rb | 26 +- lib/onebox/engine/google_drive_onebox.rb | 6 +- lib/onebox/engine/google_maps_onebox.rb | 75 +- lib/onebox/engine/google_photos_onebox.rb | 2 +- lib/onebox/engine/google_play_app_onebox.rb | 31 +- lib/onebox/engine/hackernews_onebox.rb | 22 +- lib/onebox/engine/image_onebox.rb | 6 +- lib/onebox/engine/imgur_onebox.rb | 15 +- lib/onebox/engine/instagram_onebox.rb | 40 +- lib/onebox/engine/kaltura_onebox.rb | 2 +- lib/onebox/engine/mixcloud_onebox.rb | 2 +- lib/onebox/engine/motoko_onebox.rb | 2 +- lib/onebox/engine/opengraph_image.rb | 1 - lib/onebox/engine/pastebin_onebox.rb | 40 +- lib/onebox/engine/pdf_onebox.rb | 4 +- lib/onebox/engine/pubmed_onebox.rb | 13 +- lib/onebox/engine/reddit_media_onebox.rb | 4 +- lib/onebox/engine/replit_onebox.rb | 2 +- lib/onebox/engine/simplecast_onebox.rb | 2 +- lib/onebox/engine/sketch_fab_onebox.rb | 4 +- lib/onebox/engine/sound_cloud_onebox.rb | 4 +- lib/onebox/engine/stack_exchange_onebox.rb | 30 +- lib/onebox/engine/standard_embed.rb | 60 +- lib/onebox/engine/steam_store_onebox.rb | 4 +- lib/onebox/engine/trello_onebox.rb | 4 +- lib/onebox/engine/twitch_clips_onebox.rb | 4 +- lib/onebox/engine/twitch_stream_onebox.rb | 2 +- lib/onebox/engine/twitch_video_onebox.rb | 4 +- lib/onebox/engine/twitter_status_onebox.rb | 16 +- lib/onebox/engine/typeform_onebox.rb | 12 +- lib/onebox/engine/video_onebox.rb | 4 +- lib/onebox/engine/vimeo_onebox.rb | 8 +- lib/onebox/engine/wikimedia_onebox.rb | 14 +- lib/onebox/engine/wikipedia_onebox.rb | 27 +- lib/onebox/engine/wistia_onebox.rb | 2 +- lib/onebox/engine/xkcd_onebox.rb | 9 +- lib/onebox/engine/youku_onebox.rb | 6 +- lib/onebox/engine/youtube_onebox.rb | 122 +- lib/onebox/file_type_finder.rb | 12 +- lib/onebox/helpers.rb | 154 +- lib/onebox/json_ld.rb | 26 +- lib/onebox/layout.rb | 6 +- lib/onebox/layout_support.rb | 1 - lib/onebox/matcher.rb | 9 +- lib/onebox/mixins/git_blob_onebox.rb | 95 +- lib/onebox/mixins/github_body.rb | 2 +- lib/onebox/movie.rb | 16 +- lib/onebox/normalizer.rb | 12 +- lib/onebox/open_graph.rb | 20 +- lib/onebox/preview.rb | 36 +- lib/onebox/sanitize_config.rb | 157 +- lib/oneboxer.rb | 350 ++-- lib/onpdiff.rb | 13 +- lib/pbkdf2.rb | 6 +- lib/permalink_constraint.rb | 2 - lib/pinned_check.rb | 11 +- lib/plain_text_to_markdown.rb | 24 +- lib/plugin.rb | 38 +- lib/plugin/filter.rb | 1 - lib/plugin/filter_manager.rb | 5 +- lib/plugin/instance.rb | 364 +++-- lib/plugin/metadata.rb | 199 ++- lib/plugin_gem.rb | 13 +- lib/post_action_creator.rb | 190 +-- lib/post_action_destroyer.rb | 31 +- lib/post_action_result.rb | 2 +- lib/post_creator.rb | 273 ++-- lib/post_destroyer.rb | 200 ++- lib/post_jobs_enqueuer.rb | 10 +- lib/post_merger.rb | 20 +- lib/post_revisor.rb | 183 +-- lib/presence_channel.rb | 143 +- lib/pretty_text.rb | 461 +++--- lib/pretty_text/helpers.rb | 52 +- lib/promotion.rb | 41 +- lib/quote_comparer.rb | 3 +- lib/rake_helpers.rb | 7 +- lib/rate_limiter.rb | 35 +- lib/rate_limiter/limit_exceeded.rb | 2 - lib/rate_limiter/on_create_record.rb | 17 +- lib/read_only_mixin.rb | 2 +- lib/redis_snapshot.rb | 11 +- ...quire_dependency_backward_compatibility.rb | 4 +- lib/retrieve_title.rb | 43 +- lib/reviewable/actions.rb | 18 +- lib/reviewable/perform_result.rb | 2 +- lib/route_format.rb | 2 - lib/route_matcher.rb | 19 +- lib/rtl.rb | 6 +- lib/s3_cors_rulesets.rb | 50 +- lib/s3_helper.rb | 218 ++- lib/s3_inventory.rb | 169 +- lib/scheduler/defer.rb | 27 +- lib/score_calculator.rb | 23 +- lib/screening_model.rb | 8 +- lib/search.rb | 744 ++++----- lib/search/grouped_search_results.rb | 40 +- .../actions/discourse_connect_provider.rb | 37 +- lib/second_factor/actions/grant_admin.rb | 12 +- lib/second_factor/auth_manager.rb | 39 +- lib/second_factor/auth_manager_result.rb | 4 +- lib/seed_data/categories.rb | 149 +- lib/seed_data/topics.rb | 112 +- lib/shrink_uploaded_image.rb | 52 +- lib/sidekiq/pausable.rb | 45 +- lib/sidekiq_logster_reporter.rb | 13 +- lib/site_icon_manager.rb | 63 +- lib/site_setting_extension.rb | 210 +-- lib/site_settings/db_provider.rb | 12 +- lib/site_settings/defaults_provider.rb | 10 +- lib/site_settings/deprecated_settings.rb | 45 +- lib/site_settings/local_process_provider.rb | 3 +- lib/site_settings/type_supervisor.rb | 116 +- lib/site_settings/validations.rb | 102 +- lib/site_settings/yaml_loader.rb | 15 +- lib/slug.rb | 32 +- lib/socket_server.rb | 16 +- lib/spam_handler.rb | 18 +- lib/staff_constraint.rb | 2 - lib/stylesheet/compiler.rb | 30 +- lib/stylesheet/importer.rb | 97 +- lib/stylesheet/manager.rb | 188 ++- lib/stylesheet/manager/builder.rb | 151 +- lib/stylesheet/manager/scss_checker.rb | 36 +- lib/stylesheet/watcher.rb | 98 +- lib/suggested_topics_builder.rb | 14 +- lib/svg_sprite.rb | 674 ++++---- lib/system_message.rb | 42 +- lib/tasks/add_topic_to_quotes.rake | 12 +- lib/tasks/admin.rake | 34 +- lib/tasks/annotate.rake | 5 +- lib/tasks/assets.rake | 102 +- lib/tasks/auto_annotate_models.rake | 51 +- lib/tasks/autospec.rake | 21 +- lib/tasks/avatars.rake | 7 +- lib/tasks/categories.rake | 10 +- lib/tasks/cdn.rake | 23 +- lib/tasks/db.rake | 229 +-- lib/tasks/destroy.rake | 6 +- lib/tasks/dev.rake | 51 +- lib/tasks/docker.rake | 121 +- lib/tasks/emails.rake | 32 +- lib/tasks/emoji.rake | 575 ++++--- lib/tasks/export.rake | 16 +- lib/tasks/groups.rake | 2 +- lib/tasks/i18n.rake | 10 +- lib/tasks/import.rake | 73 +- lib/tasks/incoming_emails.rake | 6 +- lib/tasks/integration.rake | 24 +- lib/tasks/javascript.rake | 195 +-- lib/tasks/maxminddb.rake | 6 +- lib/tasks/plugin.rake | 168 +- lib/tasks/populate.rake | 42 +- lib/tasks/posts.rake | 473 +++--- lib/tasks/profile.rake | 19 +- lib/tasks/qunit.rake | 40 +- lib/tasks/redis.rake | 6 +- lib/tasks/release_note.rake | 36 +- lib/tasks/revisions.rake | 4 +- lib/tasks/rspec.rake | 2 +- lib/tasks/s3.rake | 63 +- lib/tasks/scheduler.rake | 4 +- lib/tasks/search.rake | 22 +- lib/tasks/site.rake | 365 +++-- lib/tasks/site_settings.rake | 10 +- lib/tasks/smoke_test.rake | 55 +- lib/tasks/svg_icons.rake | 10 +- lib/tasks/tags.rake | 2 +- lib/tasks/themes.rake | 106 +- lib/tasks/topics.rake | 26 +- lib/tasks/turbo.rake | 9 +- lib/tasks/uploads.rake | 580 ++++--- lib/tasks/users.rake | 61 +- lib/temporary_db.rb | 44 +- lib/temporary_redis.rb | 54 +- lib/text_cleaner.rb | 31 +- lib/text_sentinel.rb | 47 +- lib/theme_javascript_compiler.rb | 51 +- lib/theme_modifier_helper.rb | 4 +- lib/theme_settings_manager.rb | 16 +- lib/theme_settings_parser.rb | 7 +- lib/theme_store/git_importer.rb | 55 +- lib/theme_store/zip_exporter.rb | 18 +- lib/theme_store/zip_importer.rb | 17 +- lib/theme_translation_manager.rb | 29 +- lib/theme_translation_parser.rb | 15 +- lib/timeline_lookup.rb | 3 - lib/tiny_japanese_segmenter.rb | 1412 ++++++++++++++++- lib/topic_creator.rb | 115 +- lib/topic_list_responder.rb | 13 +- lib/topic_publisher.rb | 64 +- lib/topic_query.rb | 654 ++++---- lib/topic_query/private_message_lists.rb | 96 +- lib/topic_query_params.rb | 8 +- lib/topic_retriever.rb | 6 +- lib/topic_subtype.rb | 25 +- lib/topic_upload_security_manager.rb | 53 +- lib/topic_view.rb | 401 ++--- lib/topics_bulk_action.rb | 78 +- lib/trust_level.rb | 3 +- lib/turbo_tests.rb | 74 +- lib/turbo_tests/json_rows_formatter.rb | 46 +- lib/turbo_tests/reporter.rb | 35 +- lib/turbo_tests/runner.rb | 124 +- lib/twitter_api.rb | 100 +- lib/unread.rb | 7 +- lib/upload_creator.rb | 325 ++-- lib/upload_fixer.rb | 31 +- lib/upload_markdown.rb | 2 +- lib/upload_recovery.rb | 117 +- lib/upload_security.rb | 15 +- lib/url_helper.rb | 45 +- lib/user_comm_screener.rb | 130 +- lib/user_lookup.rb | 76 +- lib/user_name_suggester.rb | 43 +- .../allow_user_locale_enabled_validator.rb | 2 - .../allowed_ip_address_validator.rb | 9 +- ...tive_reply_by_email_addresses_validator.rb | 2 +- lib/validators/categories_topics_validator.rb | 2 +- lib/validators/censored_words_validator.rb | 2 +- lib/validators/color_list_validator.rb | 2 +- lib/validators/css_color_validator.rb | 172 +- .../default_composer_category_validator.rb | 2 +- ...ete_rejected_email_after_days_validator.rb | 2 +- lib/validators/email_setting_validator.rb | 2 +- lib/validators/email_validator.rb | 16 +- .../enable_invite_only_validator.rb | 4 +- ...enable_local_logins_via_email_validator.rb | 4 +- ...enable_private_email_messages_validator.rb | 4 +- lib/validators/enable_sso_validator.rb | 18 +- .../google_oauth2_hd_groups_validator.rb | 8 +- lib/validators/group_setting_validator.rb | 3 +- lib/validators/host_list_setting_validator.rb | 2 +- lib/validators/integer_setting_validator.rb | 8 +- lib/validators/ip_address_format_validator.rb | 6 +- .../markdown_linkify_tlds_validator.rb | 1 - lib/validators/max_emojis_validator.rb | 12 +- .../max_username_length_validator.rb | 8 +- .../min_username_length_validator.rb | 8 +- lib/validators/not_username_validator.rb | 2 +- lib/validators/password_validator.rb | 8 +- ...rsonal_message_enabled_groups_validator.rb | 1 - .../pop3_polling_enabled_setting_validator.rb | 36 +- lib/validators/post_validator.rb | 108 +- lib/validators/quality_title_validator.rb | 4 +- lib/validators/regex_setting_validation.rb | 4 +- lib/validators/regex_setting_validator.rb | 8 +- lib/validators/regexp_list_validator.rb | 24 +- .../reply_by_email_address_validator.rb | 4 +- .../reply_by_email_enabled_validator.rb | 5 +- .../selectable_avatars_mode_validator.rb | 2 +- .../sso_overrides_email_validator.rb | 6 +- lib/validators/string_setting_validator.rb | 14 +- lib/validators/stripped_length_validator.rb | 17 +- lib/validators/timezone_validator.rb | 6 +- .../topic_title_length_validator.rb | 8 +- .../unicode_username_allowlist_validator.rb | 8 +- lib/validators/unique_among_validator.rb | 17 +- lib/validators/upload_validator.rb | 70 +- lib/validators/url_validator.rb | 2 +- lib/validators/user_full_name_validator.rb | 5 +- lib/validators/username_setting_validator.rb | 3 +- lib/validators/watched_words_validator.rb | 6 +- lib/vary_header.rb | 2 +- lib/version.rb | 21 +- lib/webauthn.rb | 57 +- lib/webauthn/challenge_generator.rb | 2 +- .../security_key_authentication_service.rb | 29 +- .../security_key_base_validation_service.rb | 23 +- .../security_key_registration_service.rb | 49 +- lib/wizard.rb | 25 +- lib/wizard/builder.rb | 216 +-- lib/wizard/field.rb | 2 - lib/wizard/step_updater.rb | 14 +- 507 files changed, 16550 insertions(+), 12627 deletions(-) diff --git a/.streerc b/.streerc index b43ee5bb6ee..fd138211ec6 100644 --- a/.streerc +++ b/.streerc @@ -1,4 +1,3 @@ --print-width=100 --plugins=plugin/trailing_comma,disable_ternary --ignore-files=app/* ---ignore-files=lib/* diff --git a/lib/action_dispatch/session/discourse_cookie_store.rb b/lib/action_dispatch/session/discourse_cookie_store.rb index 91a32f4545a..58da5c8f5d3 100644 --- a/lib/action_dispatch/session/discourse_cookie_store.rb +++ b/lib/action_dispatch/session/discourse_cookie_store.rb @@ -9,9 +9,7 @@ class ActionDispatch::Session::DiscourseCookieStore < ActionDispatch::Session::C def set_cookie(request, session_id, cookie) if Hash === cookie - if SiteSetting.force_https - cookie[:secure] = true - end + cookie[:secure] = true if SiteSetting.force_https unless SiteSetting.same_site_cookies == "Disabled" cookie[:same_site] = SiteSetting.same_site_cookies end diff --git a/lib/admin_confirmation.rb b/lib/admin_confirmation.rb index fa73f66de70..06b7bc775cf 100644 --- a/lib/admin_confirmation.rb +++ b/lib/admin_confirmation.rb @@ -17,10 +17,7 @@ class AdminConfirmation @token = SecureRandom.hex Discourse.redis.setex("admin-confirmation:#{@target_user.id}", 3.hours.to_i, @token) - payload = { - target_user_id: @target_user.id, - performed_by: @performed_by.id - } + payload = { target_user_id: @target_user.id, performed_by: @performed_by.id } Discourse.redis.setex("admin-confirmation-token:#{@token}", 3.hours.to_i, payload.to_json) Jobs.enqueue( @@ -28,7 +25,7 @@ class AdminConfirmation to_address: @performed_by.email, target_email: @target_user.email, target_username: @target_user.username, - token: @token + token: @token, ) end @@ -51,8 +48,8 @@ class AdminConfirmation return nil unless json parsed = JSON.parse(json) - target_user = User.find(parsed['target_user_id'].to_i) - performed_by = User.find(parsed['performed_by'].to_i) + target_user = User.find(parsed["target_user_id"].to_i) + performed_by = User.find(parsed["performed_by"].to_i) ac = AdminConfirmation.new(target_user, performed_by) ac.token = token diff --git a/lib/admin_constraint.rb b/lib/admin_constraint.rb index 7146eedfeeb..521cb0c2be9 100644 --- a/lib/admin_constraint.rb +++ b/lib/admin_constraint.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminConstraint - def initialize(options = {}) @require_master = options[:require_master] end @@ -19,5 +18,4 @@ class AdminConstraint def custom_admin_check(request) true end - end diff --git a/lib/admin_user_index_query.rb b/lib/admin_user_index_query.rb index 9ecbb8f19ad..49564110bfc 100644 --- a/lib/admin_user_index_query.rb +++ b/lib/admin_user_index_query.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminUserIndexQuery - def initialize(params = {}, klass = User, trust_levels = TrustLevel.levels) @params = params @query = initialize_query_with_order(klass) @@ -11,24 +10,22 @@ class AdminUserIndexQuery attr_reader :params, :trust_levels SORTABLE_MAPPING = { - 'created' => 'created_at', - 'last_emailed' => "COALESCE(last_emailed_at, to_date('1970-01-01', 'YYYY-MM-DD'))", - 'seen' => "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD'))", - 'username' => 'username', - 'email' => 'email', - 'trust_level' => 'trust_level', - 'days_visited' => 'user_stats.days_visited', - 'posts_read' => 'user_stats.posts_read_count', - 'topics_viewed' => 'user_stats.topics_entered', - 'posts' => 'user_stats.post_count', - 'read_time' => 'user_stats.time_read' + "created" => "created_at", + "last_emailed" => "COALESCE(last_emailed_at, to_date('1970-01-01', 'YYYY-MM-DD'))", + "seen" => "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD'))", + "username" => "username", + "email" => "email", + "trust_level" => "trust_level", + "days_visited" => "user_stats.days_visited", + "posts_read" => "user_stats.posts_read_count", + "topics_viewed" => "user_stats.topics_entered", + "posts" => "user_stats.post_count", + "read_time" => "user_stats.time_read", } def find_users(limit = 100) page = params[:page].to_i - 1 - if page < 0 - page = 0 - end + page = 0 if page < 0 find_users_query.limit(limit).offset(page * limit) end @@ -37,7 +34,13 @@ class AdminUserIndexQuery end def custom_direction - Discourse.deprecate(":ascending is deprecated please use :asc instead", output_in_test: true, drop_from: '2.9.0') if params[:ascending] + if params[:ascending] + Discourse.deprecate( + ":ascending is deprecated please use :asc instead", + output_in_test: true, + drop_from: "2.9.0", + ) + end asc = params[:asc] || params[:ascending] asc.present? && asc ? "ASC" : "DESC" end @@ -47,7 +50,7 @@ class AdminUserIndexQuery custom_order = params[:order] if custom_order.present? && - without_dir = SORTABLE_MAPPING[custom_order.downcase.sub(/ (asc|desc)$/, '')] + without_dir = SORTABLE_MAPPING[custom_order.downcase.sub(/ (asc|desc)$/, "")] order << "#{without_dir} #{custom_direction}" end @@ -61,13 +64,9 @@ class AdminUserIndexQuery order << "users.username" end - query = klass - .includes(:totps) - .order(order.reject(&:blank?).join(",")) + query = klass.includes(:totps).order(order.reject(&:blank?).join(",")) - unless params[:stats].present? && params[:stats] == false - query = query.includes(:user_stat) - end + query = query.includes(:user_stat) unless params[:stats].present? && params[:stats] == false query = query.joins(:primary_email) if params[:show_emails] == "true" @@ -77,32 +76,44 @@ class AdminUserIndexQuery def filter_by_trust levels = trust_levels.map { |key, _| key.to_s } if levels.include?(params[:query]) - @query.where('trust_level = ?', trust_levels[params[:query].to_sym]) + @query.where("trust_level = ?", trust_levels[params[:query].to_sym]) end end def filter_by_query_classification case params[:query] - when 'staff' then @query.where("admin or moderator") - when 'admins' then @query.where(admin: true) - when 'moderators' then @query.where(moderator: true) - when 'silenced' then @query.silenced - when 'suspended' then @query.suspended - when 'pending' then @query.not_suspended.where(approved: false, active: true) - when 'staged' then @query.where(staged: true) + when "staff" + @query.where("admin or moderator") + when "admins" + @query.where(admin: true) + when "moderators" + @query.where(moderator: true) + when "silenced" + @query.silenced + when "suspended" + @query.suspended + when "pending" + @query.not_suspended.where(approved: false, active: true) + when "staged" + @query.where(staged: true) end end def filter_by_search if params[:email].present? - return @query.joins(:primary_email).where('user_emails.email = ?', params[:email].downcase) + return @query.joins(:primary_email).where("user_emails.email = ?", params[:email].downcase) end filter = params[:filter] if filter.present? filter = filter.strip - if ip = IPAddr.new(filter) rescue nil - @query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s) + if ip = + begin + IPAddr.new(filter) + rescue StandardError + nil + end + @query.where("ip_address <<= :ip OR registration_ip_address <<= :ip", ip: ip.to_cidr_s) else @query.filter_by_username_or_email(filter) end @@ -111,14 +122,12 @@ class AdminUserIndexQuery def filter_by_ip if params[:ip].present? - @query.where('ip_address = :ip OR registration_ip_address = :ip', ip: params[:ip].strip) + @query.where("ip_address = :ip OR registration_ip_address = :ip", ip: params[:ip].strip) end end def filter_exclude - if params[:exclude].present? - @query.where('users.id != ?', params[:exclude]) - end + @query.where("users.id != ?", params[:exclude]) if params[:exclude].present? end # this might not be needed in rails 4 ? @@ -134,5 +143,4 @@ class AdminUserIndexQuery append filter_by_search @query end - end diff --git a/lib/age_words.rb b/lib/age_words.rb index 814cc219e56..c9bfbfec281 100644 --- a/lib/age_words.rb +++ b/lib/age_words.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module AgeWords - def self.age_words(secs) if secs.blank? "—" @@ -10,5 +9,4 @@ module AgeWords FreedomPatches::Rails4.distance_of_time_in_words(now, now + secs) end end - end diff --git a/lib/archetype.rb b/lib/archetype.rb index e3fc8e53540..a9380d59d98 100644 --- a/lib/archetype.rb +++ b/lib/archetype.rb @@ -11,22 +11,19 @@ class Archetype end def attributes - { - id: @id, - options: @options - } + { id: @id, options: @options } end def self.default - 'regular' + "regular" end def self.private_message - 'private_message' + "private_message" end def self.banner - 'banner' + "banner" end def self.list @@ -40,8 +37,7 @@ class Archetype end # default archetypes - register 'regular' - register 'private_message' - register 'banner' - + register "regular" + register "private_message" + register "banner" end diff --git a/lib/auth.rb b/lib/auth.rb index f501d901574..5380c826d0f 100644 --- a/lib/auth.rb +++ b/lib/auth.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -module Auth; end +module Auth +end -require 'auth/auth_provider' -require 'auth/result' -require 'auth/authenticator' -require 'auth/managed_authenticator' -require 'auth/facebook_authenticator' -require 'auth/github_authenticator' -require 'auth/twitter_authenticator' -require 'auth/google_oauth2_authenticator' -require 'auth/discord_authenticator' +require "auth/auth_provider" +require "auth/result" +require "auth/authenticator" +require "auth/managed_authenticator" +require "auth/facebook_authenticator" +require "auth/github_authenticator" +require "auth/twitter_authenticator" +require "auth/google_oauth2_authenticator" +require "auth/discord_authenticator" diff --git a/lib/auth/auth_provider.rb b/lib/auth/auth_provider.rb index 09f0f5f39a4..20e0e6cfbcd 100644 --- a/lib/auth/auth_provider.rb +++ b/lib/auth/auth_provider.rb @@ -8,32 +8,60 @@ class Auth::AuthProvider end def self.auth_attributes - [:authenticator, :pretty_name, :title, :message, :frame_width, :frame_height, - :pretty_name_setting, :title_setting, :enabled_setting, :full_screen_login, :full_screen_login_setting, - :custom_url, :background_color, :icon] + %i[ + authenticator + pretty_name + title + message + frame_width + frame_height + pretty_name_setting + title_setting + enabled_setting + full_screen_login + full_screen_login_setting + custom_url + background_color + icon + ] end attr_accessor(*auth_attributes) def enabled_setting=(val) - Discourse.deprecate("(#{authenticator.name}) enabled_setting is deprecated. Please define authenticator.enabled? instead", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) enabled_setting is deprecated. Please define authenticator.enabled? instead", + drop_from: "2.9.0", + ) @enabled_setting = val end def background_color=(val) - Discourse.deprecate("(#{authenticator.name}) background_color is no longer functional. Please use CSS instead", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) background_color is no longer functional. Please use CSS instead", + drop_from: "2.9.0", + ) end def full_screen_login=(val) - Discourse.deprecate("(#{authenticator.name}) full_screen_login is now forced. The full_screen_login parameter can be removed from the auth_provider.", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) full_screen_login is now forced. The full_screen_login parameter can be removed from the auth_provider.", + drop_from: "2.9.0", + ) end def full_screen_login_setting=(val) - Discourse.deprecate("(#{authenticator.name}) full_screen_login is now forced. The full_screen_login_setting parameter can be removed from the auth_provider.", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) full_screen_login is now forced. The full_screen_login_setting parameter can be removed from the auth_provider.", + drop_from: "2.9.0", + ) end def message=(val) - Discourse.deprecate("(#{authenticator.name}) message is no longer used because all logins are full screen. It should be removed from the auth_provider", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) message is no longer used because all logins are full screen. It should be removed from the auth_provider", + drop_from: "2.9.0", + ) end def name @@ -47,5 +75,4 @@ class Auth::AuthProvider def can_revoke authenticator.can_revoke? end - end diff --git a/lib/auth/current_user_provider.rb b/lib/auth/current_user_provider.rb index 31880b6dc63..f12e992de3d 100644 --- a/lib/auth/current_user_provider.rb +++ b/lib/auth/current_user_provider.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -module Auth; end +module Auth +end class Auth::CurrentUserProvider - # do all current user initialization here def initialize(env) raise NotImplementedError diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index 3318b062da3..91bacb78a77 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require_relative '../route_matcher' +require_relative "../route_matcher" # You may have seen references to v0 and v1 of our auth cookie in the codebase # and you're not sure how they differ, so here is an explanation: @@ -23,7 +23,6 @@ require_relative '../route_matcher' # We'll drop support for v0 after Discourse 2.9 is released. class Auth::DefaultCurrentUserProvider - CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER" USER_TOKEN_KEY ||= "_DISCOURSE_USER_TOKEN" API_KEY ||= "api_key" @@ -37,7 +36,7 @@ class Auth::DefaultCurrentUserProvider USER_API_CLIENT_ID ||= "HTTP_USER_API_CLIENT_ID" API_KEY_ENV ||= "_DISCOURSE_API" USER_API_KEY_ENV ||= "_DISCOURSE_USER_API" - TOKEN_COOKIE ||= ENV['DISCOURSE_TOKEN_COOKIE'] || "_t" + TOKEN_COOKIE ||= ENV["DISCOURSE_TOKEN_COOKIE"] || "_t" PATH_INFO ||= "PATH_INFO" COOKIE_ATTEMPTS_PER_MIN ||= 10 BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN" @@ -59,30 +58,20 @@ class Auth::DefaultCurrentUserProvider "badges#show", "tags#tag_feed", "tags#show", - *[:latest, :unread, :new, :read, :posted, :bookmarks].map { |f| "list##{f}_feed" }, - *[:all, :yearly, :quarterly, :monthly, :weekly, :daily].map { |p| "list#top_#{p}_feed" }, - *[:latest, :unread, :new, :read, :posted, :bookmarks].map { |f| "tags#show_#{f}" } + *%i[latest unread new read posted bookmarks].map { |f| "list##{f}_feed" }, + *%i[all yearly quarterly monthly weekly daily].map { |p| "list#top_#{p}_feed" }, + *%i[latest unread new read posted bookmarks].map { |f| "tags#show_#{f}" }, ], - formats: :rss - ), - RouteMatcher.new( - methods: :get, - actions: "users#bookmarks", - formats: :ics - ), - RouteMatcher.new( - methods: :post, - actions: "admin/email#handle_mail", - formats: nil + formats: :rss, ), + RouteMatcher.new(methods: :get, actions: "users#bookmarks", formats: :ics), + RouteMatcher.new(methods: :post, actions: "admin/email#handle_mail", formats: nil), ] def self.find_v0_auth_cookie(request) cookie = request.cookies[TOKEN_COOKIE] - if cookie&.valid_encoding? && cookie.present? && cookie.size == TOKEN_SIZE - cookie - end + cookie if cookie&.valid_encoding? && cookie.present? && cookie.size == TOKEN_SIZE end def self.find_v1_auth_cookie(env) @@ -111,12 +100,10 @@ class Auth::DefaultCurrentUserProvider return @env[CURRENT_USER_KEY] if @env.key?(CURRENT_USER_KEY) # bypass if we have the shared session header - if shared_key = @env['HTTP_X_SHARED_SESSION_KEY'] + if shared_key = @env["HTTP_X_SHARED_SESSION_KEY"] uid = Discourse.redis.get("shared_session_key_#{shared_key}") user = nil - if uid - user = User.find_by(id: uid.to_i) - end + user = User.find_by(id: uid.to_i) if uid @env[CURRENT_USER_KEY] = user return user end @@ -130,28 +117,27 @@ class Auth::DefaultCurrentUserProvider user_api_key ||= request[PARAMETER_USER_API_KEY] end - if !@env.blank? && request[API_KEY] && api_parameter_allowed? - api_key ||= request[API_KEY] - end + api_key ||= request[API_KEY] if !@env.blank? && request[API_KEY] && api_parameter_allowed? auth_token = find_auth_token current_user = nil if auth_token - limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN , 60) + limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN, 60) if limiter.can_perform? - @env[USER_TOKEN_KEY] = @user_token = begin - UserAuthToken.lookup( - auth_token, - seen: true, - user_agent: @env['HTTP_USER_AGENT'], - path: @env['REQUEST_PATH'], - client_ip: @request.ip - ) - rescue ActiveRecord::ReadOnlyError - nil - end + @env[USER_TOKEN_KEY] = @user_token = + begin + UserAuthToken.lookup( + auth_token, + seen: true, + user_agent: @env["HTTP_USER_AGENT"], + path: @env["REQUEST_PATH"], + client_ip: @request.ip, + ) + rescue ActiveRecord::ReadOnlyError + nil + end current_user = @user_token.try(:user) end @@ -161,14 +147,10 @@ class Auth::DefaultCurrentUserProvider begin limiter.performed! rescue RateLimiter::LimitExceeded - raise Discourse::InvalidAccess.new( - 'Invalid Access', - nil, - delete_cookie: TOKEN_COOKIE - ) + raise Discourse::InvalidAccess.new("Invalid Access", nil, delete_cookie: TOKEN_COOKIE) end end - elsif @env['HTTP_DISCOURSE_LOGGED_IN'] + elsif @env["HTTP_DISCOURSE_LOGGED_IN"] @env[BAD_TOKEN] = true end @@ -177,10 +159,10 @@ class Auth::DefaultCurrentUserProvider current_user = lookup_api_user(api_key, request) if !current_user raise Discourse::InvalidAccess.new( - I18n.t('invalid_api_credentials'), - nil, - custom_message: "invalid_api_credentials" - ) + I18n.t("invalid_api_credentials"), + nil, + custom_message: "invalid_api_credentials", + ) end raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active admin_api_key_limiter.performed! if !Rails.env.profile? @@ -191,12 +173,13 @@ class Auth::DefaultCurrentUserProvider if user_api_key @hashed_user_api_key = ApiKey.hash_key(user_api_key) - user_api_key_obj = UserApiKey - .active - .joins(:user) - .where(key_hash: @hashed_user_api_key) - .includes(:user, :scopes) - .first + user_api_key_obj = + UserApiKey + .active + .joins(:user) + .where(key_hash: @hashed_user_api_key) + .includes(:user, :scopes) + .first raise Discourse::InvalidAccess unless user_api_key_obj @@ -208,18 +191,14 @@ class Auth::DefaultCurrentUserProvider current_user = user_api_key_obj.user raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active - if can_write? - user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID]) - end + user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID]) if can_write? @env[USER_API_KEY_ENV] = true end # keep this rule here as a safeguard # under no conditions to suspended or inactive accounts get current_user - if current_user && (current_user.suspended? || !current_user.active) - current_user = nil - end + current_user = nil if current_user && (current_user.suspended? || !current_user.active) if current_user && should_update_last_seen? ip = request.ip @@ -247,31 +226,40 @@ class Auth::DefaultCurrentUserProvider if !is_user_api? && @user_token && @user_token.user == user rotated_at = @user_token.rotated_at - needs_rotation = @user_token.auth_token_seen ? rotated_at < UserAuthToken::ROTATE_TIME.ago : rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago + needs_rotation = + ( + if @user_token.auth_token_seen + rotated_at < UserAuthToken::ROTATE_TIME.ago + else + rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago + end + ) if needs_rotation - if @user_token.rotate!(user_agent: @env['HTTP_USER_AGENT'], - client_ip: @request.ip, - path: @env['REQUEST_PATH']) + if @user_token.rotate!( + user_agent: @env["HTTP_USER_AGENT"], + client_ip: @request.ip, + path: @env["REQUEST_PATH"], + ) set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar) DiscourseEvent.trigger(:user_session_refreshed, user) end end end - if !user && cookie_jar.key?(TOKEN_COOKIE) - cookie_jar.delete(TOKEN_COOKIE) - end + cookie_jar.delete(TOKEN_COOKIE) if !user && cookie_jar.key?(TOKEN_COOKIE) end def log_on_user(user, session, cookie_jar, opts = {}) - @env[USER_TOKEN_KEY] = @user_token = UserAuthToken.generate!( - user_id: user.id, - user_agent: @env['HTTP_USER_AGENT'], - path: @env['REQUEST_PATH'], - client_ip: @request.ip, - staff: user.staff?, - impersonate: opts[:impersonate]) + @env[USER_TOKEN_KEY] = @user_token = + UserAuthToken.generate!( + user_id: user.id, + user_agent: @env["HTTP_USER_AGENT"], + path: @env["REQUEST_PATH"], + client_ip: @request.ip, + staff: user.staff?, + impersonate: opts[:impersonate], + ) set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar) user.unstage! @@ -288,23 +276,19 @@ class Auth::DefaultCurrentUserProvider token: unhashed_auth_token, user_id: user.id, trust_level: user.trust_level, - issued_at: Time.zone.now.to_i + issued_at: Time.zone.now.to_i, } - if SiteSetting.persistent_sessions - expires = SiteSetting.maximum_session_age.hours.from_now - end + expires = SiteSetting.maximum_session_age.hours.from_now if SiteSetting.persistent_sessions - if SiteSetting.same_site_cookies != "Disabled" - same_site = SiteSetting.same_site_cookies - end + same_site = SiteSetting.same_site_cookies if SiteSetting.same_site_cookies != "Disabled" cookie_jar.encrypted[TOKEN_COOKIE] = { value: data, httponly: true, secure: SiteSetting.force_https, expires: expires, - same_site: same_site + same_site: same_site, } end @@ -313,10 +297,8 @@ class Auth::DefaultCurrentUserProvider # for signup flow, since all admin emails are stored in # DISCOURSE_DEVELOPER_EMAILS for self-hosters. def make_developer_admin(user) - if user.active? && - !user.admin && - Rails.configuration.respond_to?(:developer_emails) && - Rails.configuration.developer_emails.include?(user.email) + if user.active? && !user.admin && Rails.configuration.respond_to?(:developer_emails) && + Rails.configuration.developer_emails.include?(user.email) user.admin = true user.save Group.refresh_automatic_groups!(:staff, :admins) @@ -347,7 +329,7 @@ class Auth::DefaultCurrentUserProvider @user_token.destroy end - cookie_jar.delete('authentication_data') + cookie_jar.delete("authentication_data") cookie_jar.delete(TOKEN_COOKIE) end @@ -384,9 +366,7 @@ class Auth::DefaultCurrentUserProvider if api_key = ApiKey.active.with_key(api_key_value).includes(:user).first api_username = header_api_key? ? @env[HEADER_API_USERNAME] : request[API_USERNAME] - if !api_key.request_allowed?(@env) - return nil - end + return nil if !api_key.request_allowed?(@env) user = if api_key.user @@ -395,7 +375,8 @@ class Auth::DefaultCurrentUserProvider User.find_by(username_lower: api_username.downcase) elsif user_id = header_api_key? ? @env[HEADER_API_USER_ID] : request["api_user_id"] User.find_by(id: user_id.to_i) - elsif external_id = header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"] + elsif external_id = + header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"] SingleSignOnRecord.find_by(external_id: external_id.to_s).try(:user) end @@ -435,52 +416,48 @@ class Auth::DefaultCurrentUserProvider limit = GlobalSetting.max_admin_api_reqs_per_minute.to_i if GlobalSetting.respond_to?(:max_admin_api_reqs_per_key_per_minute) - Discourse.deprecate("DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE", drop_from: '2.9.0') - limit = [ - GlobalSetting.max_admin_api_reqs_per_key_per_minute.to_i, - limit - ].max + Discourse.deprecate( + "DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE", + drop_from: "2.9.0", + ) + limit = [GlobalSetting.max_admin_api_reqs_per_key_per_minute.to_i, limit].max end - @admin_api_key_limiter = RateLimiter.new( - nil, - "admin_api_min", - limit, - 60, - error_code: "admin_api_key_rate_limit" - ) + @admin_api_key_limiter = + RateLimiter.new(nil, "admin_api_min", limit, 60, error_code: "admin_api_key_rate_limit") end def user_api_key_limiter_60_secs - @user_api_key_limiter_60_secs ||= RateLimiter.new( - nil, - "user_api_min_#{@hashed_user_api_key}", - GlobalSetting.max_user_api_reqs_per_minute, - 60, - error_code: "user_api_key_limiter_60_secs" - ) + @user_api_key_limiter_60_secs ||= + RateLimiter.new( + nil, + "user_api_min_#{@hashed_user_api_key}", + GlobalSetting.max_user_api_reqs_per_minute, + 60, + error_code: "user_api_key_limiter_60_secs", + ) end def user_api_key_limiter_1_day - @user_api_key_limiter_1_day ||= RateLimiter.new( - nil, - "user_api_day_#{@hashed_user_api_key}", - GlobalSetting.max_user_api_reqs_per_day, - 86400, - error_code: "user_api_key_limiter_1_day" - ) + @user_api_key_limiter_1_day ||= + RateLimiter.new( + nil, + "user_api_day_#{@hashed_user_api_key}", + GlobalSetting.max_user_api_reqs_per_day, + 86_400, + error_code: "user_api_key_limiter_1_day", + ) end def find_auth_token return @auth_token if defined?(@auth_token) - @auth_token = begin - if v0 = self.class.find_v0_auth_cookie(@request) - v0 - elsif v1 = self.class.find_v1_auth_cookie(@env) - if v1[:issued_at] >= SiteSetting.maximum_session_age.hours.ago.to_i - v1[:token] + @auth_token = + begin + if v0 = self.class.find_v0_auth_cookie(@request) + v0 + elsif v1 = self.class.find_v1_auth_cookie(@env) + v1[:token] if v1[:issued_at] >= SiteSetting.maximum_session_age.hours.ago.to_i end end - end end end diff --git a/lib/auth/discord_authenticator.rb b/lib/auth/discord_authenticator.rb index 2629e6d3690..56d8923cd4c 100644 --- a/lib/auth/discord_authenticator.rb +++ b/lib/auth/discord_authenticator.rb @@ -2,35 +2,34 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator class DiscordStrategy < OmniAuth::Strategies::OAuth2 - option :name, 'discord' - option :scope, 'identify email guilds' + option :name, "discord" + option :scope, "identify email guilds" option :client_options, - site: 'https://discord.com/api', - authorize_url: 'oauth2/authorize', - token_url: 'oauth2/token' + site: "https://discord.com/api", + authorize_url: "oauth2/authorize", + token_url: "oauth2/token" option :authorize_options, %i[scope permissions] - uid { raw_info['id'] } + uid { raw_info["id"] } info do { - name: raw_info['username'], - email: raw_info['verified'] ? raw_info['email'] : nil, - image: "https://cdn.discordapp.com/avatars/#{raw_info['id']}/#{raw_info['avatar']}" + name: raw_info["username"], + email: raw_info["verified"] ? raw_info["email"] : nil, + image: "https://cdn.discordapp.com/avatars/#{raw_info["id"]}/#{raw_info["avatar"]}", } end - extra do - { - 'raw_info' => raw_info - } - end + extra { { "raw_info" => raw_info } } def raw_info - @raw_info ||= access_token.get('users/@me').parsed. - merge(guilds: access_token.get('users/@me/guilds').parsed) + @raw_info ||= + access_token + .get("users/@me") + .parsed + .merge(guilds: access_token.get("users/@me/guilds").parsed) end def callback_url @@ -39,7 +38,7 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator end def name - 'discord' + "discord" end def enabled? @@ -48,23 +47,26 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider DiscordStrategy, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.discord_client_id - strategy.options[:client_secret] = SiteSetting.discord_secret - } - end + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.discord_client_id + strategy.options[:client_secret] = SiteSetting.discord_secret + } + end def after_authenticate(auth_token, existing_account: nil) allowed_guild_ids = SiteSetting.discord_trusted_guilds.split("|") if allowed_guild_ids.length > 0 - user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g['id'] } + user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g["id"] } if (user_guild_ids & allowed_guild_ids).empty? # User is not in any allowed guilds - return Auth::Result.new.tap do |auth_result| - auth_result.failed = true - auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild") - end + return( + Auth::Result.new.tap do |auth_result| + auth_result.failed = true + auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild") + end + ) end end diff --git a/lib/auth/facebook_authenticator.rb b/lib/auth/facebook_authenticator.rb index 48a57cd9aec..18924c5fc00 100644 --- a/lib/auth/facebook_authenticator.rb +++ b/lib/auth/facebook_authenticator.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator - AVATAR_SIZE ||= 480 def name @@ -14,15 +13,19 @@ class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :facebook, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.facebook_app_id - strategy.options[:client_secret] = SiteSetting.facebook_app_secret - strategy.options[:info_fields] = 'name,first_name,last_name,email' - strategy.options[:image_size] = { width: AVATAR_SIZE, height: AVATAR_SIZE } - strategy.options[:secure_image_url] = true - }, - scope: "email" + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.facebook_app_id + strategy.options[:client_secret] = SiteSetting.facebook_app_secret + strategy.options[:info_fields] = "name,first_name,last_name,email" + strategy.options[:image_size] = { + width: AVATAR_SIZE, + height: AVATAR_SIZE, + } + strategy.options[:secure_image_url] = true + }, + scope: "email" end # facebook doesn't return unverified email addresses so it's safe to assume diff --git a/lib/auth/github_authenticator.rb b/lib/auth/github_authenticator.rb index 865cb21e0c2..03f2913f277 100644 --- a/lib/auth/github_authenticator.rb +++ b/lib/auth/github_authenticator.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'has_errors' +require "has_errors" class Auth::GithubAuthenticator < Auth::ManagedAuthenticator - def name "github" end @@ -50,12 +49,13 @@ class Auth::GithubAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :github, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.github_client_id - strategy.options[:client_secret] = SiteSetting.github_client_secret - }, - scope: "user:email" + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.github_client_id + strategy.options[:client_secret] = SiteSetting.github_client_secret + }, + scope: "user:email" end # the omniauth-github gem only picks up the primary email if it's verified: diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index ba35b1fecd3..3e7c4c4afb6 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -22,47 +22,46 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) options = { - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.google_oauth2_client_id - strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret + setup: + lambda do |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.google_oauth2_client_id + strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret - if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present? - strategy.options[:hd] = google_oauth2_hd - end + if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present? + strategy.options[:hd] = google_oauth2_hd + end - if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? - strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") - end + if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? + strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") + end - # All the data we need for the `info` and `credentials` auth hash - # are obtained via the user info API, not the JWT. Using and verifying - # the JWT can fail due to clock skew, so let's skip it completely. - # https://github.com/zquestz/omniauth-google-oauth2/pull/392 - strategy.options[:skip_jwt] = true - } + # All the data we need for the `info` and `credentials` auth hash + # are obtained via the user info API, not the JWT. Using and verifying + # the JWT can fail due to clock skew, so let's skip it completely. + # https://github.com/zquestz/omniauth-google-oauth2/pull/392 + strategy.options[:skip_jwt] = true + end, } omniauth.provider :google_oauth2, options end def after_authenticate(auth_token, existing_account: nil) groups = provides_groups? ? raw_groups(auth_token.uid) : nil - if groups - auth_token.extra[:raw_groups] = groups - end + auth_token.extra[:raw_groups] = groups if groups result = super if groups - result.associated_groups = groups.map { |group| group.with_indifferent_access.slice(:id, :name) } + result.associated_groups = + groups.map { |group| group.with_indifferent_access.slice(:id, :name) } end result end def provides_groups? - SiteSetting.google_oauth2_hd.present? && - SiteSetting.google_oauth2_hd_groups && + SiteSetting.google_oauth2_hd.present? && SiteSetting.google_oauth2_hd_groups && SiteSetting.google_oauth2_hd_groups_service_account_admin_email.present? && SiteSetting.google_oauth2_hd_groups_service_account_json.present? end @@ -77,20 +76,20 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator return if client.nil? loop do - params = { - userKey: uid - } + params = { userKey: uid } params[:pageToken] = page_token if page_token response = client.get(groups_url, params: params, raise_errors: false) if response.status == 200 response = response.parsed - groups.push(*response['groups']) - page_token = response['nextPageToken'] + groups.push(*response["groups"]) + page_token = response["nextPageToken"] break if page_token.nil? else - Rails.logger.error("[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}") + Rails.logger.error( + "[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}", + ) break end end @@ -107,26 +106,35 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator scope: GROUPS_SCOPE, iat: Time.now.to_i, exp: Time.now.to_i + 60, - sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email + sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email, } headers = { "alg" => "RS256", "typ" => "JWT" } key = OpenSSL::PKey::RSA.new(service_account_info["private_key"]) - encoded_jwt = ::JWT.encode(payload, key, 'RS256', headers) + encoded_jwt = ::JWT.encode(payload, key, "RS256", headers) - client = OAuth2::Client.new( - SiteSetting.google_oauth2_client_id, - SiteSetting.google_oauth2_client_secret, - site: OAUTH2_BASE_URL - ) + client = + OAuth2::Client.new( + SiteSetting.google_oauth2_client_id, + SiteSetting.google_oauth2_client_secret, + site: OAUTH2_BASE_URL, + ) - token_response = client.request(:post, '/token', body: { - grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", - assertion: encoded_jwt - }, raise_errors: false) + token_response = + client.request( + :post, + "/token", + body: { + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: encoded_jwt, + }, + raise_errors: false, + ) if token_response.status != 200 - Rails.logger.error("[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}") + Rails.logger.error( + "[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}", + ) return end diff --git a/lib/auth/managed_authenticator.rb b/lib/auth/managed_authenticator.rb index 41f36a611fc..bb163d1a715 100644 --- a/lib/auth/managed_authenticator.rb +++ b/lib/auth/managed_authenticator.rb @@ -56,28 +56,27 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def after_authenticate(auth_token, existing_account: nil) # Try and find an association for this account - association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid]) + association = + UserAssociatedAccount.find_or_initialize_by( + provider_name: auth_token[:provider], + provider_uid: auth_token[:uid], + ) # Reconnecting to existing account - if can_connect_existing_user? && existing_account && (association.user.nil? || existing_account.id != association.user_id) + if can_connect_existing_user? && existing_account && + (association.user.nil? || existing_account.id != association.user_id) association.user = existing_account end # Matching an account by email - if match_by_email && - association.user.nil? && - (user = find_user_by_email(auth_token)) - + if match_by_email && association.user.nil? && (user = find_user_by_email(auth_token)) UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end # Matching an account by username - if match_by_username && - association.user.nil? && - SiteSetting.username_change_period.zero? && - (user = find_user_by_username(auth_token)) - + if match_by_username && association.user.nil? && SiteSetting.username_change_period.zero? && + (user = find_user_by_username(auth_token)) UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end @@ -100,7 +99,14 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result = Auth::Result.new info = auth_token[:info] result.email = info[:email] - result.name = (info[:first_name] && info[:last_name]) ? "#{info[:first_name]} #{info[:last_name]}" : info[:name] + result.name = + ( + if (info[:first_name] && info[:last_name]) + "#{info[:first_name]} #{info[:last_name]}" + else + info[:name] + end + ) if result.name.present? && result.name == result.email # Some IDPs send the email address in the name parameter (e.g. Auth0 with default configuration) # We add some generic protection here, so that users don't accidently make their email addresses public @@ -109,10 +115,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result.username = info[:nickname] result.email_valid = primary_email_verified?(auth_token) if result.email.present? result.overrides_email = always_update_user_email? - result.extra_data = { - provider: auth_token[:provider], - uid: auth_token[:uid] - } + result.extra_data = { provider: auth_token[:provider], uid: auth_token[:uid] } result.user = association.user result @@ -120,7 +123,11 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def after_create_account(user, auth_result) auth_token = auth_result[:extra_data] - association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid]) + association = + UserAssociatedAccount.find_or_initialize_by( + provider_name: auth_token[:provider], + provider_uid: auth_token[:uid], + ) association.user = user association.save! @@ -132,16 +139,12 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def find_user_by_email(auth_token) email = auth_token.dig(:info, :email) - if email && primary_email_verified?(auth_token) - User.find_by_email(email) - end + User.find_by_email(email) if email && primary_email_verified?(auth_token) end def find_user_by_username(auth_token) username = auth_token.dig(:info, :nickname) - if username - User.find_by_username(username) - end + User.find_by_username(username) if username end def retrieve_avatar(user, url) @@ -158,7 +161,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator if bio || location profile = user.user_profile - profile.bio_raw = bio unless profile.bio_raw.present? + profile.bio_raw = bio unless profile.bio_raw.present? profile.location = location unless profile.location.present? profile.save end diff --git a/lib/auth/result.rb b/lib/auth/result.rb index 397a373f80e..8e83fea8ba2 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -1,48 +1,48 @@ # frozen_string_literal: true class Auth::Result - ATTRIBUTES = [ - :user, - :name, - :username, - :email, - :email_valid, - :extra_data, - :awaiting_activation, - :awaiting_approval, - :authenticated, - :authenticator_name, - :requires_invite, - :not_allowed_from_ip_address, - :admin_not_allowed_from_ip_address, - :skip_email_validation, - :destination_url, - :omniauth_disallow_totp, - :failed, - :failed_reason, - :failed_code, - :associated_groups, - :overrides_email, - :overrides_username, - :overrides_name, + ATTRIBUTES = %i[ + user + name + username + email + email_valid + extra_data + awaiting_activation + awaiting_approval + authenticated + authenticator_name + requires_invite + not_allowed_from_ip_address + admin_not_allowed_from_ip_address + skip_email_validation + destination_url + omniauth_disallow_totp + failed + failed_reason + failed_code + associated_groups + overrides_email + overrides_username + overrides_name ] attr_accessor *ATTRIBUTES # These are stored in the session during # account creation. The user cannot read or modify them - SESSION_ATTRIBUTES = [ - :email, - :username, - :email_valid, - :name, - :authenticator_name, - :extra_data, - :skip_email_validation, - :associated_groups, - :overrides_email, - :overrides_username, - :overrides_name, + SESSION_ATTRIBUTES = %i[ + email + username + email_valid + name + authenticator_name + extra_data + skip_email_validation + associated_groups + overrides_email + overrides_username + overrides_name ] def [](key) @@ -59,9 +59,7 @@ class Auth::Result end def email_valid=(val) - if !val.in? [true, false, nil] - raise ArgumentError, "email_valid should be boolean or nil" - end + raise ArgumentError, "email_valid should be boolean or nil" if !val.in? [true, false, nil] @email_valid = !!val end @@ -83,14 +81,14 @@ class Auth::Result def apply_user_attributes! change_made = false - if (SiteSetting.auth_overrides_username? || overrides_username) && (resolved_username = resolve_username).present? + if (SiteSetting.auth_overrides_username? || overrides_username) && + (resolved_username = resolve_username).present? change_made = UsernameChanger.override(user, resolved_username) end - if (SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid")) && - email_valid && - email.present? && - user.email != Email.downcase(email) + if ( + SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid") + ) && email_valid && email.present? && user.email != Email.downcase(email) user.email = email change_made = true end @@ -109,11 +107,12 @@ class Auth::Result associated_groups.uniq.each do |associated_group| begin - associated_group = AssociatedGroup.find_or_create_by( - name: associated_group[:name], - provider_id: associated_group[:id], - provider_name: extra_data[:provider] - ) + associated_group = + AssociatedGroup.find_or_create_by( + name: associated_group[:name], + provider_id: associated_group[:id], + provider_name: extra_data[:provider], + ) rescue ActiveRecord::RecordNotUnique retry end @@ -135,22 +134,12 @@ class Auth::Result end def to_client_hash - if requires_invite - return { requires_invite: true } - end + return { requires_invite: true } if requires_invite - if user&.suspended? - return { - suspended: true, - suspended_message: user.suspended_message - } - end + return { suspended: true, suspended_message: user.suspended_message } if user&.suspended? if omniauth_disallow_totp - return { - omniauth_disallow_totp: !!omniauth_disallow_totp, - email: email - } + return { omniauth_disallow_totp: !!omniauth_disallow_totp, email: email } end if user @@ -159,7 +148,7 @@ class Auth::Result awaiting_activation: !!awaiting_activation, awaiting_approval: !!awaiting_approval, not_allowed_from_ip_address: !!not_allowed_from_ip_address, - admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address + admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address, } result[:destination_url] = destination_url if authenticated && destination_url.present? @@ -173,7 +162,7 @@ class Auth::Result auth_provider: authenticator_name, email_valid: !!email_valid, can_edit_username: can_edit_username, - can_edit_name: can_edit_name + can_edit_name: can_edit_name, } result[:destination_url] = destination_url if destination_url.present? @@ -190,9 +179,7 @@ class Auth::Result def staged_user return @staged_user if defined?(@staged_user) - if email.present? && email_valid - @staged_user = User.where(staged: true).find_by_email(email) - end + @staged_user = User.where(staged: true).find_by_email(email) if email.present? && email_valid end def username_suggester_attributes diff --git a/lib/auth/twitter_authenticator.rb b/lib/auth/twitter_authenticator.rb index 35f990bb667..5817f617e35 100644 --- a/lib/auth/twitter_authenticator.rb +++ b/lib/auth/twitter_authenticator.rb @@ -17,11 +17,12 @@ class Auth::TwitterAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :twitter, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key - strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret - } + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key + strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret + } end # twitter doesn't return unverfied email addresses in the API diff --git a/lib/autospec/base_runner.rb b/lib/autospec/base_runner.rb index bc78371f5c3..4a0128f4ccf 100644 --- a/lib/autospec/base_runner.rb +++ b/lib/autospec/base_runner.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Autospec - class BaseRunner - # used when starting the runner - preloading happens here def start(opts = {}) end @@ -32,7 +30,5 @@ module Autospec # used to stop the runner def stop end - end - end diff --git a/lib/autospec/formatter.rb b/lib/autospec/formatter.rb index a6b26aff7ff..5dee7dd42cf 100644 --- a/lib/autospec/formatter.rb +++ b/lib/autospec/formatter.rb @@ -3,11 +3,15 @@ require "rspec/core/formatters/base_text_formatter" require "parallel_tests/rspec/logger_base" -module Autospec; end +module Autospec +end class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter - - RSpec::Core::Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump + RSpec::Core::Formatters.register self, + :example_passed, + :example_pending, + :example_failed, + :start_dump RSPEC_RESULT = "./tmp/rspec_result" @@ -19,15 +23,15 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter end def example_passed(_notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('.', :success) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap(".", :success) end def example_pending(_notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('*', :pending) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap("*", :pending) end def example_failed(notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('F', :failure) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap("F", :failure) @fail_file.puts(notification.example.location + " ") @fail_file.flush end @@ -40,5 +44,4 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter @fail_file.close super(filename) end - end diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb index e912887d2e2..87dded1963c 100644 --- a/lib/autospec/manager.rb +++ b/lib/autospec/manager.rb @@ -7,7 +7,8 @@ require "autospec/reload_css" require "autospec/base_runner" require "socket_server" -module Autospec; end +module Autospec +end class Autospec::Manager def self.run(opts = {}) @@ -25,7 +26,10 @@ class Autospec::Manager end def run - Signal.trap("HUP") { stop_runners; exit } + Signal.trap("HUP") do + stop_runners + exit + end Signal.trap("INT") do begin @@ -47,7 +51,6 @@ class Autospec::Manager STDIN.gets process_queue end - rescue => e fail(e, "failed in run") ensure @@ -71,16 +74,16 @@ class Autospec::Manager @queue.reject! { |_, s, _| s == "spec" } - if current_runner - @queue.concat [['spec', 'spec', current_runner]] - end + @queue.concat [["spec", "spec", current_runner]] if current_runner @runners.each do |runner| - @queue.concat [['spec', 'spec', runner]] unless @queue.any? { |_, s, r| s == "spec" && r == runner } + unless @queue.any? { |_, s, r| s == "spec" && r == runner } + @queue.concat [["spec", "spec", runner]] + end end end - [:start, :stop, :abort].each do |verb| + %i[start stop abort].each do |verb| define_method("#{verb}_runners") do puts "@@@@@@@@@@@@ #{verb}_runners" if @debug @runners.each(&verb) @@ -89,11 +92,7 @@ class Autospec::Manager def start_service_queue puts "@@@@@@@@@@@@ start_service_queue" if @debug - Thread.new do - while true - thread_loop - end - end + Thread.new { thread_loop while true } end # the main loop, will run the specs in the queue till one fails or the queue is empty @@ -176,9 +175,7 @@ class Autospec::Manager Dir[root_path + "/plugins/*"].each do |f| next if !File.directory? f resolved = File.realpath(f) - if resolved != f - map[resolved] = f - end + map[resolved] = f if resolved != f end map end @@ -188,9 +185,7 @@ class Autospec::Manager resolved = file @reverse_map ||= reverse_symlink_map @reverse_map.each do |location, discourse_location| - if file.start_with?(location) - resolved = discourse_location + file[location.length..-1] - end + resolved = discourse_location + file[location.length..-1] if file.start_with?(location) end resolved @@ -199,9 +194,7 @@ class Autospec::Manager def listen_for_changes puts "@@@@@@@@@@@@ listen_for_changes" if @debug - options = { - ignore: /^lib\/autospec/, - } + options = { ignore: %r{^lib/autospec} } if @opts[:force_polling] options[:force_polling] = true @@ -210,14 +203,14 @@ class Autospec::Manager path = root_path - if ENV['VIM_AUTOSPEC'] + if ENV["VIM_AUTOSPEC"] STDERR.puts "Using VIM file listener" socket_path = (Rails.root + "tmp/file_change.sock").to_s FileUtils.rm_f(socket_path) server = SocketServer.new(socket_path) server.start do |line| - file, line = line.split(' ') + file, line = line.split(" ") file = reverse_symlink(file) file = file.sub(Rails.root.to_s + "/", "") # process_change can acquire a mutex and block @@ -235,20 +228,20 @@ class Autospec::Manager end # to speed up boot we use a thread - ["spec", "lib", "app", "config", "test", "vendor", "plugins"].each do |watch| - + %w[spec lib app config test vendor plugins].each do |watch| puts "@@@@@@@@@ Listen to #{path}/#{watch} #{options}" if @debug Thread.new do begin - listener = Listen.to("#{path}/#{watch}", options) do |modified, added, _| - paths = [modified, added].flatten - paths.compact! - paths.map! do |long| - long = reverse_symlink(long) - long[(path.length + 1)..-1] + listener = + Listen.to("#{path}/#{watch}", options) do |modified, added, _| + paths = [modified, added].flatten + paths.compact! + paths.map! do |long| + long = reverse_symlink(long) + long[(path.length + 1)..-1] + end + process_change(paths) end - process_change(paths) - end listener.start sleep rescue => e @@ -257,7 +250,6 @@ class Autospec::Manager end end end - end def process_change(files) @@ -285,13 +277,9 @@ class Autospec::Manager hit = true spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file with_line = spec - if spec == file && line - with_line = spec + ":" << line.to_s - end + with_line = spec + ":" << line.to_s if spec == file && line if File.exist?(spec) || Dir.exist?(spec) - if with_line != spec - specs << [file, spec, runner] - end + specs << [file, spec, runner] if with_line != spec specs << [file, with_line, runner] end end @@ -329,9 +317,7 @@ class Autospec::Manager focus = @queue.shift @queue.unshift([file, spec, runner]) unless spec.include?(":") && focus[1].include?(spec.split(":")[0]) - if focus[1].include?(spec) || file != spec - @queue.unshift(focus) - end + @queue.unshift(focus) if focus[1].include?(spec) || file != spec end else @queue.unshift([file, spec, runner]) diff --git a/lib/autospec/reload_css.rb b/lib/autospec/reload_css.rb index 8f24cdec984..78258bed17f 100644 --- a/lib/autospec/reload_css.rb +++ b/lib/autospec/reload_css.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -module Autospec; end +module Autospec +end class Autospec::ReloadCss - WATCHERS = {} def self.watch(pattern, &blk) WATCHERS[pattern] = blk @@ -30,7 +30,7 @@ class Autospec::ReloadCss if paths.any? { |p| p =~ /\.(css|s[ac]ss)/ } # todo connect to dev instead? ActiveRecord::Base.establish_connection - [:desktop, :mobile].each do |style| + %i[desktop mobile].each do |style| s = DiscourseStylesheets.new(style) s.compile paths << "public" + s.stylesheet_relpath_no_digest @@ -44,10 +44,9 @@ class Autospec::ReloadCss p = p.sub(/\.sass\.erb/, "") p = p.sub(/\.sass/, "") p = p.sub(/\.scss/, "") - p = p.sub(/^app\/assets\/stylesheets/, "assets") + p = p.sub(%r{^app/assets/stylesheets}, "assets") { name: p, hash: hash || SecureRandom.hex } end message_bus.publish "/file-change", paths end - end diff --git a/lib/autospec/rspec_runner.rb b/lib/autospec/rspec_runner.rb index b4ebbf3ca2f..408b6fe79e0 100644 --- a/lib/autospec/rspec_runner.rb +++ b/lib/autospec/rspec_runner.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Autospec - class RspecRunner < BaseRunner - WATCHERS = {} def self.watch(pattern, &blk) WATCHERS[pattern] = blk @@ -13,26 +11,28 @@ module Autospec end # Discourse specific - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } + watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^spec/support/.+\.rb$}) { "spec" } - watch("app/controllers/application_controller.rb") { "spec/requests" } + watch(%r{^spec/support/.+\.rb$}) { "spec" } + watch("app/controllers/application_controller.rb") { "spec/requests" } - watch(%r{app/controllers/(.+).rb}) { |m| "spec/requests/#{m[1]}_spec.rb" } + watch(%r{app/controllers/(.+).rb}) { |m| "spec/requests/#{m[1]}_spec.rb" } - watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } + watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } - watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" } + watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" } - watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" } + watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) do + "spec/components/pretty_text_spec.rb" + end watch(%r{^plugins/.*/discourse-markdown/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" } watch(%r{^plugins/.*/spec/.*\.rb}) - watch(%r{^(plugins/.*/)plugin\.rb}) { |m| "#{m[1]}spec" } - watch(%r{^(plugins/.*)/(lib|app)}) { |m| "#{m[1]}/spec/integration" } + watch(%r{^(plugins/.*/)plugin\.rb}) { |m| "#{m[1]}spec" } + watch(%r{^(plugins/.*)/(lib|app)}) { |m| "#{m[1]}/spec/integration" } watch(%r{^(plugins/.*)/lib/(.*)\.rb}) { |m| "#{m[1]}/spec/lib/#{m[2]}_spec.rb" } RELOADERS = Set.new @@ -50,11 +50,9 @@ module Autospec def failed_specs specs = [] - path = './tmp/rspec_result' + path = "./tmp/rspec_result" specs = File.readlines(path) if File.exist?(path) specs end - end - end diff --git a/lib/autospec/simple_runner.rb b/lib/autospec/simple_runner.rb index 62fcbc21a0a..dcf88e44434 100644 --- a/lib/autospec/simple_runner.rb +++ b/lib/autospec/simple_runner.rb @@ -3,7 +3,6 @@ require "autospec/rspec_runner" module Autospec - class SimpleRunner < RspecRunner def initialize @mutex = Mutex.new @@ -12,36 +11,29 @@ module Autospec def run(specs) puts "Running Rspec: #{specs}" # kill previous rspec instance - @mutex.synchronize do - self.abort - end + @mutex.synchronize { self.abort } # we use our custom rspec formatter - args = [ - "-r", "#{File.dirname(__FILE__)}/formatter.rb", - "-f", "Autospec::Formatter" - ] + args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb", "-f", "Autospec::Formatter"] - command = begin - line_specified = specs.split.any? { |s| s =~ /\:/ } # Parallel spec can't run specific line - multiple_files = specs.split.count > 1 || specs == "spec" # Only parallelize multiple files - if ENV["PARALLEL_SPEC"] == '1' && multiple_files && !line_specified - "bin/turbo_rspec #{args.join(" ")} #{specs.split.join(" ")}" - else - "bin/rspec #{args.join(" ")} #{specs.split.join(" ")}" + command = + begin + line_specified = specs.split.any? { |s| s =~ /\:/ } # Parallel spec can't run specific line + multiple_files = specs.split.count > 1 || specs == "spec" # Only parallelize multiple files + if ENV["PARALLEL_SPEC"] == "1" && multiple_files && !line_specified + "bin/turbo_rspec #{args.join(" ")} #{specs.split.join(" ")}" + else + "bin/rspec #{args.join(" ")} #{specs.split.join(" ")}" + end end - end # launch rspec Dir.chdir(Rails.root) do # rubocop:disable Discourse/NoChdir because this is not part of the app env = { "RAILS_ENV" => "test" } - if specs.split(' ').any? { |s| s =~ /^(.\/)?plugins/ } + if specs.split(" ").any? { |s| s =~ %r{^(./)?plugins} } env["LOAD_PLUGINS"] = "1" puts "Loading plugins while running specs" end - pid = - @mutex.synchronize do - @pid = Process.spawn(env, command) - end + pid = @mutex.synchronize { @pid = Process.spawn(env, command) } _, status = Process.wait2(pid) @@ -51,7 +43,11 @@ module Autospec def abort if pid = @pid - Process.kill("TERM", pid) rescue nil + begin + Process.kill("TERM", pid) + rescue StandardError + nil + end wait_for_done(pid) pid = nil end @@ -66,16 +62,26 @@ module Autospec def wait_for_done(pid) i = 3000 - while (i > 0 && Process.getpgid(pid) rescue nil) + while ( + begin + i > 0 && Process.getpgid(pid) + rescue StandardError + nil + end + ) sleep 0.001 i -= 1 end - if (Process.getpgid(pid) rescue nil) + if ( + begin + Process.getpgid(pid) + rescue StandardError + nil + end + ) STDERR.puts "Terminating rspec #{pid} by force cause it refused graceful termination" Process.kill("KILL", pid) end end - end - end diff --git a/lib/backup_restore.rb b/lib/backup_restore.rb index 291567a6f0a..b8c7fa77843 100644 --- a/lib/backup_restore.rb +++ b/lib/backup_restore.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module BackupRestore - - class OperationRunningError < RuntimeError; end + class OperationRunningError < RuntimeError + end VERSION_PREFIX = "v" DUMP_FILE = "dump.sql.gz" @@ -22,9 +22,7 @@ module BackupRestore def self.rollback! raise BackupRestore::OperationRunningError if BackupRestore.is_operation_running? - if can_rollback? - move_tables_between_schemas("backup", "public") - end + move_tables_between_schemas("backup", "public") if can_rollback? end def self.cancel! @@ -58,7 +56,7 @@ module BackupRestore { is_operation_running: is_operation_running?, can_rollback: can_rollback?, - allow_restore: Rails.env.development? || SiteSetting.allow_restore + allow_restore: Rails.env.development? || SiteSetting.allow_restore, } end @@ -133,7 +131,7 @@ module BackupRestore config["backup_port"] || config["port"], config["username"] || username || ENV["USER"] || "postgres", config["password"] || password, - config["database"] + config["database"], ) end @@ -194,7 +192,11 @@ module BackupRestore end def self.backup_tables_count - DB.query_single("SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'").first.to_i + DB + .query_single( + "SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'", + ) + .first + .to_i end - end diff --git a/lib/backup_restore/backup_file_handler.rb b/lib/backup_restore/backup_file_handler.rb index 483d6f6473a..645fad9f64d 100644 --- a/lib/backup_restore/backup_file_handler.rb +++ b/lib/backup_restore/backup_file_handler.rb @@ -69,15 +69,22 @@ module BackupRestore path_transformation = case tar_implementation when :gnu - ['--transform', 's|var/www/discourse/public/uploads/|uploads/|'] + %w[--transform s|var/www/discourse/public/uploads/|uploads/|] when :bsd - ['-s', '|var/www/discourse/public/uploads/|uploads/|'] + %w[-s |var/www/discourse/public/uploads/|uploads/|] end log "Unzipping archive, this may take a while..." Discourse::Utils.execute_command( - 'tar', '--extract', '--gzip', '--file', @archive_path, '--directory', @tmp_directory, - *path_transformation, failure_message: "Failed to decompress archive." + "tar", + "--extract", + "--gzip", + "--file", + @archive_path, + "--directory", + @tmp_directory, + *path_transformation, + failure_message: "Failed to decompress archive.", ) end @@ -86,15 +93,19 @@ module BackupRestore if @is_archive # for compatibility with backups from Discourse v1.5 and below old_dump_path = File.join(@tmp_directory, OLD_DUMP_FILENAME) - File.exist?(old_dump_path) ? old_dump_path : File.join(@tmp_directory, BackupRestore::DUMP_FILE) + if File.exist?(old_dump_path) + old_dump_path + else + File.join(@tmp_directory, BackupRestore::DUMP_FILE) + end else File.join(@tmp_directory, @filename) end - if File.extname(@db_dump_path) == '.gz' + if File.extname(@db_dump_path) == ".gz" log "Extracting dump file..." Compression::Gzip.new.decompress(@tmp_directory, @db_dump_path, available_size) - @db_dump_path.delete_suffix!('.gz') + @db_dump_path.delete_suffix!(".gz") end @db_dump_path @@ -105,17 +116,18 @@ module BackupRestore end def tar_implementation - @tar_version ||= begin - tar_version = Discourse::Utils.execute_command('tar', '--version') + @tar_version ||= + begin + tar_version = Discourse::Utils.execute_command("tar", "--version") - if tar_version.include?("GNU tar") - :gnu - elsif tar_version.include?("bsdtar") - :bsd - else - raise "Unknown tar implementation: #{tar_version}" + if tar_version.include?("GNU tar") + :gnu + elsif tar_version.include?("bsdtar") + :bsd + else + raise "Unknown tar implementation: #{tar_version}" + end end - end end end end diff --git a/lib/backup_restore/backup_store.rb b/lib/backup_restore/backup_store.rb index 65a3acaa951..5afcec27bfc 100644 --- a/lib/backup_restore/backup_store.rb +++ b/lib/backup_restore/backup_store.rb @@ -37,9 +37,7 @@ module BackupRestore return unless cleanup_allowed? return if (backup_files = files).size <= SiteSetting.maximum_backups - backup_files[SiteSetting.maximum_backups..-1].each do |file| - delete_file(file.filename) - end + backup_files[SiteSetting.maximum_backups..-1].each { |file| delete_file(file.filename) } reset_cache end @@ -74,7 +72,7 @@ module BackupRestore used_bytes: used_bytes, free_bytes: free_bytes, count: files.size, - last_backup_taken_at: latest_file&.last_modified + last_backup_taken_at: latest_file&.last_modified, } end diff --git a/lib/backup_restore/backuper.rb b/lib/backup_restore/backuper.rb index 83c10911b39..2483f5bb7b9 100644 --- a/lib/backup_restore/backuper.rb +++ b/lib/backup_restore/backuper.rb @@ -4,7 +4,6 @@ require "mini_mime" require "file_store/s3_store" module BackupRestore - class Backuper attr_reader :success @@ -84,7 +83,11 @@ module BackupRestore @dump_filename = File.join(@tmp_directory, BackupRestore::DUMP_FILE) @archive_directory = BackupRestore::LocalBackupStore.base_directory(db: @current_db) filename = @filename_override || "#{get_parameterized_title}-#{@timestamp}" - @archive_basename = File.join(@archive_directory, "#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}") + @archive_basename = + File.join( + @archive_directory, + "#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}", + ) @backup_filename = if @with_uploads @@ -119,9 +122,18 @@ module BackupRestore BackupMetadata.delete_all BackupMetadata.create!(name: "base_url", value: Discourse.base_url) BackupMetadata.create!(name: "cdn_url", value: Discourse.asset_host) - BackupMetadata.create!(name: "s3_base_url", value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_base_url : nil) - BackupMetadata.create!(name: "s3_cdn_url", value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_cdn_url : nil) - BackupMetadata.create!(name: "db_name", value: RailsMultisite::ConnectionManagement.current_db) + BackupMetadata.create!( + name: "s3_base_url", + value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_base_url : nil, + ) + BackupMetadata.create!( + name: "s3_cdn_url", + value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_cdn_url : nil, + ) + BackupMetadata.create!( + name: "db_name", + value: RailsMultisite::ConnectionManagement.current_db, + ) BackupMetadata.create!(name: "multisite", value: Rails.configuration.multisite) end @@ -132,7 +144,7 @@ module BackupRestore pg_dump_running = true Thread.new do - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) while pg_dump_running message = logs.pop.strip log(message) unless message.blank? @@ -159,23 +171,24 @@ module BackupRestore db_conf = BackupRestore.database_configuration password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present? - host_argument = "--host=#{db_conf.host}" if db_conf.host.present? - port_argument = "--port=#{db_conf.port}" if db_conf.port.present? + host_argument = "--host=#{db_conf.host}" if db_conf.host.present? + port_argument = "--port=#{db_conf.port}" if db_conf.port.present? username_argument = "--username=#{db_conf.username}" if db_conf.username.present? - [ password_argument, # pass the password to pg_dump (if any) - "pg_dump", # the pg_dump command - "--schema=public", # only public schema - "-T public.pg_*", # exclude tables and views whose name starts with "pg_" + [ + password_argument, # pass the password to pg_dump (if any) + "pg_dump", # the pg_dump command + "--schema=public", # only public schema + "-T public.pg_*", # exclude tables and views whose name starts with "pg_" "--file='#{@dump_filename}'", # output to the dump.sql file - "--no-owner", # do not output commands to set ownership of objects - "--no-privileges", # prevent dumping of access privileges - "--verbose", # specifies verbose mode - "--compress=4", # Compression level of 4 - host_argument, # the hostname to connect to (if any) - port_argument, # the port to connect to (if any) - username_argument, # the username to connect as (if any) - db_conf.database # the name of the database to dump + "--no-owner", # do not output commands to set ownership of objects + "--no-privileges", # prevent dumping of access privileges + "--verbose", # specifies verbose mode + "--compress=4", # Compression level of 4 + host_argument, # the hostname to connect to (if any) + port_argument, # the port to connect to (if any) + username_argument, # the username to connect as (if any) + db_conf.database, # the name of the database to dump ].join(" ") end @@ -185,8 +198,10 @@ module BackupRestore archive_filename = File.join(@archive_directory, @backup_filename) Discourse::Utils.execute_command( - 'mv', @dump_filename, archive_filename, - failure_message: "Failed to move database dump file." + "mv", + @dump_filename, + archive_filename, + failure_message: "Failed to move database dump file.", ) remove_tmp_directory @@ -198,17 +213,29 @@ module BackupRestore tar_filename = "#{@archive_basename}.tar" log "Making sure archive does not already exist..." - Discourse::Utils.execute_command('rm', '-f', tar_filename) - Discourse::Utils.execute_command('rm', '-f', "#{tar_filename}.gz") + Discourse::Utils.execute_command("rm", "-f", tar_filename) + Discourse::Utils.execute_command("rm", "-f", "#{tar_filename}.gz") log "Creating empty archive..." - Discourse::Utils.execute_command('tar', '--create', '--file', tar_filename, '--files-from', '/dev/null') + Discourse::Utils.execute_command( + "tar", + "--create", + "--file", + tar_filename, + "--files-from", + "/dev/null", + ) log "Archiving data dump..." Discourse::Utils.execute_command( - 'tar', '--append', '--dereference', '--file', tar_filename, File.basename(@dump_filename), + "tar", + "--append", + "--dereference", + "--file", + tar_filename, + File.basename(@dump_filename), failure_message: "Failed to archive data dump.", - chdir: File.dirname(@dump_filename) + chdir: File.dirname(@dump_filename), ) add_local_uploads_to_archive(tar_filename) @@ -218,8 +245,10 @@ module BackupRestore log "Gzipping archive, this may take a while..." Discourse::Utils.execute_command( - 'gzip', "-#{SiteSetting.backup_gzip_compression_level_for_uploads}", tar_filename, - failure_message: "Failed to gzip archive." + "gzip", + "-#{SiteSetting.backup_gzip_compression_level_for_uploads}", + tar_filename, + failure_message: "Failed to gzip archive.", ) end @@ -244,14 +273,21 @@ module BackupRestore if SiteSetting.include_thumbnails_in_backups exclude_optimized = "" else - optimized_path = File.join(upload_directory, 'optimized') + optimized_path = File.join(upload_directory, "optimized") exclude_optimized = "--exclude=#{optimized_path}" end Discourse::Utils.execute_command( - 'tar', '--append', '--dereference', exclude_optimized, '--file', tar_filename, upload_directory, - failure_message: "Failed to archive uploads.", success_status_codes: [0, 1], - chdir: File.join(Rails.root, "public") + "tar", + "--append", + "--dereference", + exclude_optimized, + "--file", + tar_filename, + upload_directory, + failure_message: "Failed to archive uploads.", + success_status_codes: [0, 1], + chdir: File.join(Rails.root, "public"), ) else log "No local uploads found. Skipping archiving of local uploads..." @@ -287,9 +323,14 @@ module BackupRestore log "Appending uploads to archive..." Discourse::Utils.execute_command( - 'tar', '--append', '--file', tar_filename, upload_directory, - failure_message: "Failed to append uploads to archive.", success_status_codes: [0, 1], - chdir: @tmp_directory + "tar", + "--append", + "--file", + tar_filename, + upload_directory, + failure_message: "Failed to append uploads to archive.", + success_status_codes: [0, 1], + chdir: @tmp_directory, ) log "No uploads found on S3. Skipping archiving of uploads stored on S3..." if count == 0 @@ -327,9 +368,7 @@ module BackupRestore logs = Discourse::Utils.logs_markdown(@logs, user: @user) post = SystemMessage.create_from_system_user(@user, status, logs: logs) - if @user.id == Discourse::SYSTEM_USER_ID - post.topic.invite_group(@user, Group[:admins]) - end + post.topic.invite_group(@user, Group[:admins]) if @user.id == Discourse::SYSTEM_USER_ID rescue => ex log "Something went wrong while notifying user.", ex end @@ -399,7 +438,12 @@ module BackupRestore def publish_log(message, timestamp) return unless @publish_to_message_bus data = { timestamp: timestamp, operation: "backup", message: message } - MessageBus.publish(BackupRestore::LOGS_CHANNEL, data, user_ids: [@user_id], client_ids: [@client_id]) + MessageBus.publish( + BackupRestore::LOGS_CHANNEL, + data, + user_ids: [@user_id], + client_ids: [@client_id], + ) end def save_log(message, timestamp) @@ -416,5 +460,4 @@ module BackupRestore end end end - end diff --git a/lib/backup_restore/database_restorer.rb b/lib/backup_restore/database_restorer.rb index ae3ac7deaa4..2a3bc6985c7 100644 --- a/lib/backup_restore/database_restorer.rb +++ b/lib/backup_restore/database_restorer.rb @@ -46,9 +46,7 @@ module BackupRestore end def self.drop_backup_schema - if backup_schema_dropable? - ActiveRecord::Base.connection.drop_schema(BACKUP_SCHEMA) - end + ActiveRecord::Base.connection.drop_schema(BACKUP_SCHEMA) if backup_schema_dropable? end def self.core_migration_files @@ -65,13 +63,14 @@ module BackupRestore last_line = nil psql_running = true - log_thread = Thread.new do - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) - while psql_running || !logs.empty? - message = logs.pop.strip - log(message) if message.present? + log_thread = + Thread.new do + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) + while psql_running || !logs.empty? + message = logs.pop.strip + log(message) if message.present? + end end - end IO.popen(restore_dump_command) do |pipe| begin @@ -89,7 +88,9 @@ module BackupRestore logs << "" log_thread.join - raise DatabaseRestoreError.new("psql failed: #{last_line}") if Process.last_status&.exitstatus != 0 + if Process.last_status&.exitstatus != 0 + raise DatabaseRestoreError.new("psql failed: #{last_line}") + end end # Removes unwanted SQL added by certain versions of pg_dump and modifies @@ -99,7 +100,7 @@ module BackupRestore "DROP SCHEMA", # Discourse <= v1.5 "CREATE SCHEMA", # PostgreSQL 11+ "COMMENT ON SCHEMA", # PostgreSQL 11+ - "SET default_table_access_method" # PostgreSQL 12 + "SET default_table_access_method", # PostgreSQL 12 ].join("|") command = "sed -E '/^(#{unwanted_sql})/d' #{@db_dump_path}" @@ -117,18 +118,19 @@ module BackupRestore db_conf = BackupRestore.database_configuration password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present? - host_argument = "--host=#{db_conf.host}" if db_conf.host.present? - port_argument = "--port=#{db_conf.port}" if db_conf.port.present? - username_argument = "--username=#{db_conf.username}" if db_conf.username.present? + host_argument = "--host=#{db_conf.host}" if db_conf.host.present? + port_argument = "--port=#{db_conf.port}" if db_conf.port.present? + username_argument = "--username=#{db_conf.username}" if db_conf.username.present? - [ password_argument, # pass the password to psql (if any) - "psql", # the psql command + [ + password_argument, # pass the password to psql (if any) + "psql", # the psql command "--dbname='#{db_conf.database}'", # connect to database *dbname* - "--single-transaction", # all or nothing (also runs COPY commands faster) - "--variable=ON_ERROR_STOP=1", # stop on first error - host_argument, # the hostname to connect to (if any) - port_argument, # the port to connect to (if any) - username_argument # the username to connect as (if any) + "--single-transaction", # all or nothing (also runs COPY commands faster) + "--variable=ON_ERROR_STOP=1", # stop on first error + host_argument, # the hostname to connect to (if any) + port_argument, # the port to connect to (if any) + username_argument, # the username to connect as (if any) ].compact.join(" ") end @@ -136,21 +138,22 @@ module BackupRestore log "Migrating the database..." log Discourse::Utils.execute_command( - { - "SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0", - "SKIP_OPTIMIZE_ICONS" => "1", - "DISABLE_TRANSLATION_OVERRIDES" => "1" - }, - "rake", "db:migrate", - failure_message: "Failed to migrate database.", - chdir: Rails.root - ) + { + "SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0", + "SKIP_OPTIMIZE_ICONS" => "1", + "DISABLE_TRANSLATION_OVERRIDES" => "1", + }, + "rake", + "db:migrate", + failure_message: "Failed to migrate database.", + chdir: Rails.root, + ) end def reconnect_database log "Reconnecting to the database..." - RailsMultisite::ConnectionManagement::reload if RailsMultisite::ConnectionManagement::instance - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) + RailsMultisite::ConnectionManagement.reload if RailsMultisite::ConnectionManagement.instance + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) end def create_missing_discourse_functions @@ -179,10 +182,12 @@ module BackupRestore end end - existing_function_names = Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" } + existing_function_names = + Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" } all_readonly_table_columns.each do |table_name, column_name| - function_name = Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false) + function_name = + Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false) if !existing_function_names.include?(function_name) Migration::BaseDropper.create_readonly_function(table_name, column_name) diff --git a/lib/backup_restore/local_backup_store.rb b/lib/backup_restore/local_backup_store.rb index fd7f16b9f63..77ccfcce097 100644 --- a/lib/backup_restore/local_backup_store.rb +++ b/lib/backup_restore/local_backup_store.rb @@ -12,7 +12,12 @@ module BackupRestore end def self.chunk_path(identifier, filename, chunk_number) - File.join(LocalBackupStore.base_directory, "tmp", identifier, "#{filename}.part#{chunk_number}") + File.join( + LocalBackupStore.base_directory, + "tmp", + identifier, + "#{filename}.part#{chunk_number}", + ) end def initialize(opts = {}) @@ -39,7 +44,7 @@ module BackupRestore def download_file(filename, destination, failure_message = "") path = path_from_filename(filename) - Discourse::Utils.execute_command('cp', path, destination, failure_message: failure_message) + Discourse::Utils.execute_command("cp", path, destination, failure_message: failure_message) end private @@ -59,7 +64,7 @@ module BackupRestore filename: File.basename(path), size: File.size(path), last_modified: File.mtime(path).utc, - source: include_download_source ? path : nil + source: include_download_source ? path : nil, ) end diff --git a/lib/backup_restore/logger.rb b/lib/backup_restore/logger.rb index fe777d389bc..f9c0eab642e 100644 --- a/lib/backup_restore/logger.rb +++ b/lib/backup_restore/logger.rb @@ -32,7 +32,12 @@ module BackupRestore def publish_log(message, timestamp) return unless @publish_to_message_bus data = { timestamp: timestamp, operation: "restore", message: message } - MessageBus.publish(BackupRestore::LOGS_CHANNEL, data, user_ids: [@user_id], client_ids: [@client_id]) + MessageBus.publish( + BackupRestore::LOGS_CHANNEL, + data, + user_ids: [@user_id], + client_ids: [@client_id], + ) end def save_log(message, timestamp) diff --git a/lib/backup_restore/meta_data_handler.rb b/lib/backup_restore/meta_data_handler.rb index 02bc0e98bf0..0996d8866bc 100644 --- a/lib/backup_restore/meta_data_handler.rb +++ b/lib/backup_restore/meta_data_handler.rb @@ -28,8 +28,10 @@ module BackupRestore log " Restored version: #{metadata[:version]}" if metadata[:version] > @current_version - raise MigrationRequiredError.new("You're trying to restore a more recent version of the schema. " \ - "You should migrate first!") + raise MigrationRequiredError.new( + "You're trying to restore a more recent version of the schema. " \ + "You should migrate first!", + ) end metadata diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index 00bedab2410..8caf5dafecd 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -65,8 +65,8 @@ module BackupRestore after_restore_hook rescue Compression::Strategy::ExtractFailed - log 'ERROR: The uncompressed file is too big. Consider increasing the hidden ' \ - '"decompressed_backup_max_file_size_mb" setting.' + log "ERROR: The uncompressed file is too big. Consider increasing the hidden " \ + '"decompressed_backup_max_file_size_mb" setting.' @database_restorer.rollback rescue SystemExit log "Restore process was cancelled!" @@ -118,10 +118,10 @@ module BackupRestore DiscourseEvent.trigger(:site_settings_restored) - if @disable_emails && SiteSetting.disable_emails == 'no' + if @disable_emails && SiteSetting.disable_emails == "no" log "Disabling outgoing emails for non-staff users..." user = User.find_by_email(@user_info[:email]) || Discourse.system_user - SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + SiteSetting.set_and_log(:disable_emails, "non-staff", user) end end @@ -152,7 +152,7 @@ module BackupRestore post = SystemMessage.create_from_system_user(user, status, logs: logs) else log "Could not send notification to '#{@user_info[:username]}' " \ - "(#{@user_info[:email]}), because the user does not exist." + "(#{@user_info[:email]}), because the user does not exist." end rescue => ex log "Something went wrong while notifying user.", ex diff --git a/lib/backup_restore/s3_backup_store.rb b/lib/backup_restore/s3_backup_store.rb index 591eb2ed067..10e2798642a 100644 --- a/lib/backup_restore/s3_backup_store.rb +++ b/lib/backup_restore/s3_backup_store.rb @@ -4,8 +4,11 @@ module BackupRestore class S3BackupStore < BackupStore UPLOAD_URL_EXPIRES_AFTER_SECONDS ||= 6.hours.to_i - delegate :abort_multipart, :presign_multipart_part, :list_multipart_parts, - :complete_multipart, to: :s3_helper + delegate :abort_multipart, + :presign_multipart_part, + :list_multipart_parts, + :complete_multipart, + to: :s3_helper def initialize(opts = {}) @s3_options = S3Helper.s3_options(SiteSetting) @@ -13,7 +16,7 @@ module BackupRestore end def s3_helper - @s3_helper ||= S3Helper.new(s3_bucket_name_with_prefix, '', @s3_options.clone) + @s3_helper ||= S3Helper.new(s3_bucket_name_with_prefix, "", @s3_options.clone) end def remote? @@ -57,11 +60,17 @@ module BackupRestore presigned_url(obj, :put, UPLOAD_URL_EXPIRES_AFTER_SECONDS) rescue Aws::Errors::ServiceError => e - Rails.logger.warn("Failed to generate upload URL for S3: #{e.message.presence || e.class.name}") + Rails.logger.warn( + "Failed to generate upload URL for S3: #{e.message.presence || e.class.name}", + ) raise StorageError.new(e.message.presence || e.class.name) end - def signed_url_for_temporary_upload(file_name, expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, metadata: {}) + def signed_url_for_temporary_upload( + file_name, + expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, + metadata: {} + ) obj = object_from_path(file_name) raise BackupFileExists.new if obj.exists? key = temporary_upload_path(file_name) @@ -71,8 +80,8 @@ module BackupRestore expires_in: expires_in, opts: { metadata: metadata, - acl: "private" - } + acl: "private", + }, ) end @@ -84,7 +93,7 @@ module BackupRestore folder_prefix = s3_helper.s3_bucket_folder_path.nil? ? "" : s3_helper.s3_bucket_folder_path if Rails.env.test? - folder_prefix = File.join(folder_prefix, "test_#{ENV['TEST_ENV_NUMBER'].presence || '0'}") + folder_prefix = File.join(folder_prefix, "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}") end folder_prefix @@ -105,7 +114,10 @@ module BackupRestore s3_helper.copy( existing_external_upload_key, File.join(s3_helper.s3_bucket_folder_path, original_filename), - options: { acl: "private", apply_metadata_to_destination: true } + options: { + acl: "private", + apply_metadata_to_destination: true, + }, ) s3_helper.delete_object(existing_external_upload_key) end @@ -120,9 +132,7 @@ module BackupRestore objects = [] s3_helper.list.each do |obj| - if obj.key.match?(file_regex) - objects << create_file_from_object(obj) - end + objects << create_file_from_object(obj) if obj.key.match?(file_regex) end objects @@ -137,7 +147,7 @@ module BackupRestore filename: File.basename(obj.key), size: obj.size, last_modified: obj.last_modified, - source: include_download_source ? presigned_url(obj, :get, expires) : nil + source: include_download_source ? presigned_url(obj, :get, expires) : nil, ) end @@ -154,16 +164,17 @@ module BackupRestore end def file_regex - @file_regex ||= begin - path = s3_helper.s3_bucket_folder_path || "" + @file_regex ||= + begin + path = s3_helper.s3_bucket_folder_path || "" - if path.present? - path = "#{path}/" unless path.end_with?("/") - path = Regexp.quote(path) + if path.present? + path = "#{path}/" unless path.end_with?("/") + path = Regexp.quote(path) + end + + %r{^#{path}[^/]*\.t?gz$}i end - - /^#{path}[^\/]*\.t?gz$/i - end end def free_bytes diff --git a/lib/backup_restore/system_interface.rb b/lib/backup_restore/system_interface.rb index c0d209e5380..31bd4eb944e 100644 --- a/lib/backup_restore/system_interface.rb +++ b/lib/backup_restore/system_interface.rb @@ -98,9 +98,7 @@ module BackupRestore def flush_redis redis = Discourse.redis - redis.scan_each(match: "*") do |key| - redis.del(key) unless key == SidekiqPauser::PAUSED_KEY - end + redis.scan_each(match: "*") { |key| redis.del(key) unless key == SidekiqPauser::PAUSED_KEY } end def clear_sidekiq_queues diff --git a/lib/backup_restore/uploads_restorer.rb b/lib/backup_restore/uploads_restorer.rb index 7409c38f56c..8b6c13e17d1 100644 --- a/lib/backup_restore/uploads_restorer.rb +++ b/lib/backup_restore/uploads_restorer.rb @@ -11,11 +11,12 @@ module BackupRestore def self.s3_regex_string(s3_base_url) clean_url = s3_base_url.sub(S3_ENDPOINT_REGEX, ".s3.amazonaws.com") - regex_string = clean_url - .split(".s3.amazonaws.com") - .map { |s| Regexp.escape(s) } - .insert(1, S3_ENDPOINT_REGEX.source) - .join("") + regex_string = + clean_url + .split(".s3.amazonaws.com") + .map { |s| Regexp.escape(s) } + .insert(1, S3_ENDPOINT_REGEX.source) + .join("") [regex_string, clean_url] end @@ -25,12 +26,16 @@ module BackupRestore end def restore(tmp_directory) - upload_directories = Dir.glob(File.join(tmp_directory, "uploads", "*")) - .reject { |path| File.basename(path).start_with?("PaxHeaders") } + upload_directories = + Dir + .glob(File.join(tmp_directory, "uploads", "*")) + .reject { |path| File.basename(path).start_with?("PaxHeaders") } if upload_directories.count > 1 - raise UploadsRestoreError.new("Could not find uploads, because the uploads " \ - "directory contains multiple folders.") + raise UploadsRestoreError.new( + "Could not find uploads, because the uploads " \ + "directory contains multiple folders.", + ) end @tmp_uploads_path = upload_directories.first @@ -55,7 +60,9 @@ module BackupRestore if !store.respond_to?(:copy_from) # a FileStore implementation from a plugin might not support this method, so raise a helpful error store_name = Discourse.store.class.name - raise UploadsRestoreError.new("The current file store (#{store_name}) does not support restoring uploads.") + raise UploadsRestoreError.new( + "The current file store (#{store_name}) does not support restoring uploads.", + ) end log "Restoring uploads, this may take a while..." @@ -89,13 +96,17 @@ module BackupRestore remap(old_base_url, Discourse.base_url) end - current_s3_base_url = SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_base_url : nil - if (old_s3_base_url = BackupMetadata.value_for("s3_base_url")) && old_s3_base_url != current_s3_base_url + current_s3_base_url = + SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_base_url : nil + if (old_s3_base_url = BackupMetadata.value_for("s3_base_url")) && + old_s3_base_url != current_s3_base_url remap_s3("#{old_s3_base_url}/", uploads_folder) end - current_s3_cdn_url = SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_cdn_url : nil - if (old_s3_cdn_url = BackupMetadata.value_for("s3_cdn_url")) && old_s3_cdn_url != current_s3_cdn_url + current_s3_cdn_url = + SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_cdn_url : nil + if (old_s3_cdn_url = BackupMetadata.value_for("s3_cdn_url")) && + old_s3_cdn_url != current_s3_cdn_url base_url = current_s3_cdn_url || Discourse.base_url remap("#{old_s3_cdn_url}/", UrlHelper.schemaless("#{base_url}#{uploads_folder}")) @@ -113,10 +124,7 @@ module BackupRestore remap(old_host, new_host) if old_host != new_host end - if @previous_db_name != @current_db_name - remap("/uploads/#{@previous_db_name}/", upload_path) - end - + remap("/uploads/#{@previous_db_name}/", upload_path) if @previous_db_name != @current_db_name rescue => ex log "Something went wrong while remapping uploads.", ex end @@ -130,7 +138,12 @@ module BackupRestore if old_s3_base_url.include?("amazonaws.com") from_regex, from_clean_url = self.class.s3_regex_string(old_s3_base_url) log "Remapping with regex from '#{from_clean_url}' to '#{uploads_folder}'" - DbHelper.regexp_replace(from_regex, uploads_folder, verbose: true, excluded_tables: ["backup_metadata"]) + DbHelper.regexp_replace( + from_regex, + uploads_folder, + verbose: true, + excluded_tables: ["backup_metadata"], + ) else remap(old_s3_base_url, uploads_folder) end @@ -141,13 +154,15 @@ module BackupRestore DB.exec("TRUNCATE TABLE optimized_images") SiteIconManager.ensure_optimized! - User.where("uploaded_avatar_id IS NOT NULL").find_each do |user| - Jobs.enqueue(:create_avatar_thumbnails, upload_id: user.uploaded_avatar_id) - end + User + .where("uploaded_avatar_id IS NOT NULL") + .find_each do |user| + Jobs.enqueue(:create_avatar_thumbnails, upload_id: user.uploaded_avatar_id) + end end def rebake_posts_with_uploads - log 'Posts will be rebaked by a background job in sidekiq. You will see missing images until that has completed.' + log "Posts will be rebaked by a background job in sidekiq. You will see missing images until that has completed." log 'You can expedite the process by manually running "rake posts:rebake_uncooked_posts"' DB.exec(<<~SQL) diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index 0f8a4c48329..4eda8098739 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -173,7 +173,7 @@ module BadgeQueries <<~SQL SELECT p.user_id, p.id post_id, current_timestamp granted_at FROM badge_posts p - WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1" } AND p.like_count >= #{count.to_i} AND + WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1"} AND p.like_count >= #{count.to_i} AND (:backfill OR p.id IN (:post_ids) ) SQL end @@ -271,5 +271,4 @@ module BadgeQueries WHERE "rank" = 1 SQL end - end diff --git a/lib/bookmark_query.rb b/lib/bookmark_query.rb index f62ce591fac..3a54a195807 100644 --- a/lib/bookmark_query.rb +++ b/lib/bookmark_query.rb @@ -11,9 +11,7 @@ class BookmarkQuery def self.preload(bookmarks, object) preload_polymorphic_associations(bookmarks, object.guardian) - if @preload - @preload.each { |preload| preload.call(bookmarks, object) } - end + @preload.each { |preload| preload.call(bookmarks, object) } if @preload end # These polymorphic associations are loaded to make the UserBookmarkListSerializer's @@ -42,24 +40,27 @@ class BookmarkQuery ts_query = search_term.present? ? Search.ts_query(term: search_term) : nil search_term_wildcard = search_term.present? ? "%#{search_term}%" : nil - queries = Bookmark.registered_bookmarkables.map do |bookmarkable| - interim_results = bookmarkable.perform_list_query(@user, @guardian) + queries = + Bookmark + .registered_bookmarkables + .map do |bookmarkable| + interim_results = bookmarkable.perform_list_query(@user, @guardian) - # this could occur if there is some security reason that the user cannot - # access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark - # on a topic and that topic was moved into a private category - next if interim_results.blank? + # this could occur if there is some security reason that the user cannot + # access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark + # on a topic and that topic was moved into a private category + next if interim_results.blank? - if search_term.present? - interim_results = bookmarkable.perform_search_query( - interim_results, search_term_wildcard, ts_query - ) - end + if search_term.present? + interim_results = + bookmarkable.perform_search_query(interim_results, search_term_wildcard, ts_query) + end - # this is purely to make the query easy to read and debug, otherwise it's - # all mashed up into a massive ball in MiniProfiler :) - "---- #{bookmarkable.model.to_s} bookmarkable ---\n\n #{interim_results.to_sql}" - end.compact + # this is purely to make the query easy to read and debug, otherwise it's + # all mashed up into a massive ball in MiniProfiler :) + "---- #{bookmarkable.model.to_s} bookmarkable ---\n\n #{interim_results.to_sql}" + end + .compact # same for interim results being blank, the user might have been locked out # from all their various bookmarks, in which case they will see nothing and @@ -68,17 +69,16 @@ class BookmarkQuery union_sql = queries.join("\n\nUNION\n\n") results = Bookmark.select("bookmarks.*").from("(\n\n#{union_sql}\n\n) as bookmarks") - results = results.order( - "(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END), + results = + results.order( + "(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END), bookmarks.reminder_at ASC, - bookmarks.updated_at DESC" - ) + bookmarks.updated_at DESC", + ) @count = results.count - if @page.positive? - results = results.offset(@page * @params[:per_page]) - end + results = results.offset(@page * @params[:per_page]) if @page.positive? if updated_results = blk&.call(results) results = updated_results diff --git a/lib/bookmark_reminder_notification_handler.rb b/lib/bookmark_reminder_notification_handler.rb index 13f5b2966e1..247cc4637db 100644 --- a/lib/bookmark_reminder_notification_handler.rb +++ b/lib/bookmark_reminder_notification_handler.rb @@ -28,12 +28,10 @@ class BookmarkReminderNotificationHandler def clear_reminder Rails.logger.debug( - "Clearing bookmark reminder for bookmark_id #{bookmark.id}. reminder at: #{bookmark.reminder_at}" + "Clearing bookmark reminder for bookmark_id #{bookmark.id}. reminder at: #{bookmark.reminder_at}", ) - if bookmark.auto_clear_reminder_when_reminder_sent? - bookmark.reminder_at = nil - end + bookmark.reminder_at = nil if bookmark.auto_clear_reminder_when_reminder_sent? bookmark.clear_reminder! end diff --git a/lib/browser_detection.rb b/lib/browser_detection.rb index e2f8d46a88a..7cfbaffe3be 100644 --- a/lib/browser_detection.rb +++ b/lib/browser_detection.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module BrowserDetection - def self.browser(user_agent) case user_agent when /Edg/i @@ -66,5 +65,4 @@ module BrowserDetection :unknown end end - end diff --git a/lib/cache.rb b/lib/cache.rb index ca814c9e2bb..296519b17ed 100644 --- a/lib/cache.rb +++ b/lib/cache.rb @@ -15,12 +15,11 @@ # this makes it harder to reason about the API class Cache - # nothing is cached for longer than 1 day EVER # there is no reason to have data older than this clogging redis # it is dangerous cause if we rename keys we will be stuck with # pointless data - MAX_CACHE_AGE = 1.day unless defined? MAX_CACHE_AGE + MAX_CACHE_AGE = 1.day unless defined?(MAX_CACHE_AGE) attr_reader :namespace @@ -47,9 +46,7 @@ class Cache end def clear - keys.each do |k| - redis.del(k) - end + keys.each { |k| redis.del(k) } end def normalize_key(key) @@ -80,9 +77,7 @@ class Cache key = normalize_key(name) raw = nil - if !force - raw = redis.get(key) - end + raw = redis.get(key) if !force if raw begin @@ -96,7 +91,8 @@ class Cache val end elsif force - raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block." + raise ArgumentError, + "Missing block: Calling `Cache#fetch` with `force: true` requires a block." else read(name) end @@ -105,7 +101,7 @@ class Cache protected def log_first_exception(e) - if !defined? @logged_a_warning + if !defined?(@logged_a_warning) @logged_a_warning = true Discourse.warn_exception(e, "Corrupt cache... skipping entry for key #{key}") end @@ -129,5 +125,4 @@ class Cache redis.setex(key, expiry, dumped) true end - end diff --git a/lib/canonical_url.rb b/lib/canonical_url.rb index df66c37a6c7..bc9b6e2d108 100644 --- a/lib/canonical_url.rb +++ b/lib/canonical_url.rb @@ -2,7 +2,7 @@ module CanonicalURL module ControllerExtensions - ALLOWED_CANONICAL_PARAMS = %w(page) + ALLOWED_CANONICAL_PARAMS = %w[page] def canonical_url(url_for_options = {}) case url_for_options @@ -14,14 +14,15 @@ module CanonicalURL end def default_canonical - @default_canonical ||= begin - canonical = +"#{Discourse.base_url_no_prefix}#{request.path}" - allowed_params = params.select { |key| ALLOWED_CANONICAL_PARAMS.include?(key) } - if allowed_params.present? - canonical << "?#{allowed_params.keys.zip(allowed_params.values).map { |key, value| "#{key}=#{value}" }.join("&")}" + @default_canonical ||= + begin + canonical = +"#{Discourse.base_url_no_prefix}#{request.path}" + allowed_params = params.select { |key| ALLOWED_CANONICAL_PARAMS.include?(key) } + if allowed_params.present? + canonical << "?#{allowed_params.keys.zip(allowed_params.values).map { |key, value| "#{key}=#{value}" }.join("&")}" + end + canonical end - canonical - end end def self.included(base) @@ -31,7 +32,7 @@ module CanonicalURL module Helpers def canonical_link_tag(url = nil) - tag('link', rel: 'canonical', href: url || @canonical_url || default_canonical) + tag("link", rel: "canonical", href: url || @canonical_url || default_canonical) end end end diff --git a/lib/category_badge.rb b/lib/category_badge.rb index fdfd8035c51..9cf422e5880 100644 --- a/lib/category_badge.rb +++ b/lib/category_badge.rb @@ -1,23 +1,26 @@ # frozen_string_literal: true module CategoryBadge - def self.category_stripe(color, classes) - style = color ? "style='background-color: ##{color};'" : '' + style = color ? "style='background-color: ##{color};'" : "" "" end - def self.inline_category_stripe(color, styles = '', insert_blank = false) - "#{insert_blank ? ' ' : ''}" + def self.inline_category_stripe(color, styles = "", insert_blank = false) + "#{insert_blank ? " " : ""}" end def self.inline_badge_wrapper_style(category) style = case (SiteSetting.category_style || :box).to_sym - when :bar then 'line-height: 1.25; margin-right: 5px;' - when :box then "background-color:##{category.color}; line-height: 1.5; margin-top: 5px; margin-right: 5px;" - when :bullet then 'line-height: 1; margin-right: 10px;' - when :none then '' + when :bar + "line-height: 1.25; margin-right: 5px;" + when :box + "background-color:##{category.color}; line-height: 1.5; margin-top: 5px; margin-right: 5px;" + when :bullet + "line-height: 1; margin-right: 10px;" + when :none + "" end " style='font-size: 0.857em; white-space: nowrap; display: inline-block; position: relative; #{style}'" @@ -34,73 +37,88 @@ module CategoryBadge extra_classes = "#{opts[:extra_classes]} #{SiteSetting.category_style}" - result = +'' + result = +"" # parent span unless category.parent_category_id.nil? || opts[:hide_parent] parent_category = Category.find_by(id: category.parent_category_id) - result << - if opts[:inline_style] - case (SiteSetting.category_style || :box).to_sym - when :bar - inline_category_stripe(parent_category.color, 'display: inline-block; padding: 1px;', true) - when :box - inline_category_stripe(parent_category.color, 'display: inline-block; padding: 0 1px;', true) - when :bullet - inline_category_stripe(parent_category.color, 'display: inline-block; width: 5px; height: 10px; line-height: 1;') - when :none - '' - end - else - category_stripe(parent_category.color, 'badge-category-parent-bg') + result << if opts[:inline_style] + case (SiteSetting.category_style || :box).to_sym + when :bar + inline_category_stripe( + parent_category.color, + "display: inline-block; padding: 1px;", + true, + ) + when :box + inline_category_stripe( + parent_category.color, + "display: inline-block; padding: 0 1px;", + true, + ) + when :bullet + inline_category_stripe( + parent_category.color, + "display: inline-block; width: 5px; height: 10px; line-height: 1;", + ) + when :none + "" end + else + category_stripe(parent_category.color, "badge-category-parent-bg") + end end # sub parent or main category span - result << - if opts[:inline_style] - case (SiteSetting.category_style || :box).to_sym - when :bar - inline_category_stripe(category.color, 'display: inline-block; padding: 1px;', true) - when :box - '' - when :bullet - inline_category_stripe(category.color, "display: inline-block; width: #{category.parent_category_id.nil? ? 10 : 5}px; height: 10px;") - when :none - '' - end - else - category_stripe(category.color, 'badge-category-bg') + result << if opts[:inline_style] + case (SiteSetting.category_style || :box).to_sym + when :bar + inline_category_stripe(category.color, "display: inline-block; padding: 1px;", true) + when :box + "" + when :bullet + inline_category_stripe( + category.color, + "display: inline-block; width: #{category.parent_category_id.nil? ? 10 : 5}px; height: 10px;", + ) + when :none + "" end + else + category_stripe(category.color, "badge-category-bg") + end # category name - class_names = 'badge-category clear-badge' - description = category.description_text ? "title='#{category.description_text}'" : '' - category_url = opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url + class_names = "badge-category clear-badge" + description = category.description_text ? "title='#{category.description_text}'" : "" + category_url = + opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url extra_span_classes = if opts[:inline_style] case (SiteSetting.category_style || :box).to_sym when :bar - 'color: #222222; padding: 3px; vertical-align: text-top; margin-top: -3px; display: inline-block;' + "color: #222222; padding: 3px; vertical-align: text-top; margin-top: -3px; display: inline-block;" when :box "color: ##{category.text_color}; padding: 0 5px;" when :bullet - 'color: #222222; vertical-align: text-top; line-height: 1; margin-left: 4px; padding-left: 2px; display: inline;' + "color: #222222; vertical-align: text-top; line-height: 1; margin-left: 4px; padding-left: 2px; display: inline;" when :none - '' - end + 'max-width: 150px; overflow: hidden; text-overflow: ellipsis;' + "" + end + "max-width: 150px; overflow: hidden; text-overflow: ellipsis;" elsif (SiteSetting.category_style).to_sym == :box "color: ##{category.text_color}" else - '' + "" end result << "" - result << ERB::Util.html_escape(category.name) << '' + result << ERB::Util.html_escape(category.name) << "" - result = "#{result}" + result = + "#{result}" result.html_safe end diff --git a/lib/chrome_installed_checker.rb b/lib/chrome_installed_checker.rb index 0576eb74016..2f8c2824cc9 100644 --- a/lib/chrome_installed_checker.rb +++ b/lib/chrome_installed_checker.rb @@ -3,13 +3,17 @@ require "rbconfig" class ChromeInstalledChecker - class ChromeError < StandardError; end - class ChromeVersionError < ChromeError; end - class ChromeNotInstalled < ChromeError; end - class ChromeVersionTooLow < ChromeError; end + class ChromeError < StandardError + end + class ChromeVersionError < ChromeError + end + class ChromeNotInstalled < ChromeError + end + class ChromeVersionTooLow < ChromeError + end def self.run - if RbConfig::CONFIG['host_os'][/darwin|mac os/] + if RbConfig::CONFIG["host_os"][/darwin|mac os/] binary = "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" elsif system("command -v google-chrome-stable >/dev/null;") binary = "google-chrome-stable" @@ -18,15 +22,15 @@ class ChromeInstalledChecker binary ||= "chromium" if system("command -v chromium >/dev/null;") if !binary - raise ChromeNotInstalled.new("Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html") + raise ChromeNotInstalled.new( + "Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html", + ) end version = `\"#{binary}\" --version` version_match = version.match(/[\d\.]+/) - if !version_match - raise ChromeError.new("Can't get the #{binary} version") - end + raise ChromeError.new("Can't get the #{binary} version") if !version_match if Gem::Version.new(version_match[0]) < Gem::Version.new("59") raise ChromeVersionTooLow.new("Chrome 59 or higher is required") diff --git a/lib/comment_migration.rb b/lib/comment_migration.rb index bcfcdfd10ec..94062378c23 100644 --- a/lib/comment_migration.rb +++ b/lib/comment_migration.rb @@ -28,24 +28,27 @@ class CommentMigration < ActiveRecord::Migration[4.2] end def down - replace_nils(comments_up).deep_merge(comments_down).each do |table| - table[1].each do |column| - table_name = table[0] - column_name = column[0] - comment = column[1] + replace_nils(comments_up) + .deep_merge(comments_down) + .each do |table| + table[1].each do |column| + table_name = table[0] + column_name = column[0] + comment = column[1] - if column_name == :_table - DB.exec "COMMENT ON TABLE #{table_name} IS ?", comment - puts " COMMENT ON TABLE #{table_name}" - else - DB.exec "COMMENT ON COLUMN #{table_name}.#{column_name} IS ?", comment - puts " COMMENT ON COLUMN #{table_name}.#{column_name}" + if column_name == :_table + DB.exec "COMMENT ON TABLE #{table_name} IS ?", comment + puts " COMMENT ON TABLE #{table_name}" + else + DB.exec "COMMENT ON COLUMN #{table_name}.#{column_name} IS ?", comment + puts " COMMENT ON COLUMN #{table_name}.#{column_name}" + end end end - end end private + def replace_nils(hash) hash.each do |key, value| if Hash === value diff --git a/lib/common_passwords.rb b/lib/common_passwords.rb index 901a007b01b..5b4aec28e85 100644 --- a/lib/common_passwords.rb +++ b/lib/common_passwords.rb @@ -12,9 +12,8 @@ # Discourse.redis.without_namespace.del CommonPasswords::LIST_KEY class CommonPasswords - - PASSWORD_FILE = File.join(Rails.root, 'lib', 'common_passwords', '10-char-common-passwords.txt') - LIST_KEY = 'discourse-common-passwords' + PASSWORD_FILE = File.join(Rails.root, "lib", "common_passwords", "10-char-common-passwords.txt") + LIST_KEY = "discourse-common-passwords" @mutex = Mutex.new @@ -32,9 +31,7 @@ class CommonPasswords end def self.password_list - @mutex.synchronize do - load_passwords unless redis.scard(LIST_KEY) > 0 - end + @mutex.synchronize { load_passwords unless redis.scard(LIST_KEY) > 0 } RedisPasswordList.new end @@ -49,5 +46,4 @@ class CommonPasswords # tolerate this so we don't block signups Rails.logger.error "Common passwords file #{PASSWORD_FILE} is not found! Common password checking is skipped." end - end diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index 510048d1db4..878727f20c9 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ComposerMessagesFinder - def initialize(user, details) @user = user @details = details @@ -29,26 +28,30 @@ class ComposerMessagesFinder if creating_topic? count = @user.created_topic_count - education_key = 'education.new-topic' + education_key = "education.new-topic" else count = @user.post_count - education_key = 'education.new-reply' + education_key = "education.new-reply" end if count < SiteSetting.educate_until_posts - return { - id: 'education', - templateName: 'education', - wait_for_typing: true, - body: PrettyText.cook( - I18n.t( - education_key, - education_posts_text: I18n.t('education.until_posts', count: SiteSetting.educate_until_posts), - site_name: SiteSetting.title, - base_path: Discourse.base_path - ) - ) - } + return( + { + id: "education", + templateName: "education", + wait_for_typing: true, + body: + PrettyText.cook( + I18n.t( + education_key, + education_posts_text: + I18n.t("education.until_posts", count: SiteSetting.educate_until_posts), + site_name: SiteSetting.title, + base_path: Discourse.base_path, + ), + ), + } + ) end nil @@ -59,35 +62,55 @@ class ComposerMessagesFinder return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id]) { - id: 'too_many_replies', - templateName: 'education', - body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic)) + id: "too_many_replies", + templateName: "education", + body: + PrettyText.cook( + I18n.t( + "education.too_many_replies", + newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic, + ), + ), } end # Should a user be contacted to update their avatar? def check_avatar_notification - # A user has to be basic at least to be considered for an avatar notification return unless @user.has_trust_level?(TrustLevel[1]) # We don't notify users who have avatars or who have been notified already. - return if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar) + if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar) + return + end # Do not notify user if any of the following is true: # - "disable avatar education message" is enabled # - "sso overrides avatar" is enabled # - "allow uploaded avatars" is disabled - return if SiteSetting.disable_avatar_education_message || SiteSetting.discourse_connect_overrides_avatar || !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, @user) + if SiteSetting.disable_avatar_education_message || + SiteSetting.discourse_connect_overrides_avatar || + !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, @user) + return + end # If we got this far, log that we've nagged them about the avatar - UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id) + UserHistory.create!( + action: UserHistory.actions[:notified_about_avatar], + target_user_id: @user.id, + ) # Return the message { - id: 'avatar', - templateName: 'education', - body: PrettyText.cook(I18n.t('education.avatar', profile_path: "/u/#{@user.username_lower}/preferences/account#profile-picture")) + id: "avatar", + templateName: "education", + body: + PrettyText.cook( + I18n.t( + "education.avatar", + profile_path: "/u/#{@user.username_lower}/preferences/account#profile-picture", + ), + ), } end @@ -96,39 +119,45 @@ class ComposerMessagesFinder return unless educate_reply?(:notified_about_sequential_replies) # Count the posts made by this user in the last day - recent_posts_user_ids = Post.where(topic_id: @details[:topic_id]) - .where("created_at > ?", 1.day.ago) - .where(post_type: Post.types[:regular]) - .order('created_at desc') - .limit(SiteSetting.sequential_replies_threshold) - .pluck(:user_id) + recent_posts_user_ids = + Post + .where(topic_id: @details[:topic_id]) + .where("created_at > ?", 1.day.ago) + .where(post_type: Post.types[:regular]) + .order("created_at desc") + .limit(SiteSetting.sequential_replies_threshold) + .pluck(:user_id) # Did we get back as many posts as we asked for, and are they all by the current user? - return if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold || - recent_posts_user_ids.detect { |u| u != @user.id } + if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold || + recent_posts_user_ids.detect { |u| u != @user.id } + return + end # If we got this far, log that we've nagged them about the sequential replies - UserHistory.create!(action: UserHistory.actions[:notified_about_sequential_replies], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_sequential_replies], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) { - id: 'sequential_replies', - templateName: 'education', + id: "sequential_replies", + templateName: "education", wait_for_typing: true, - extraClass: 'education-message', + extraClass: "education-message", hide_if_whisper: true, - body: PrettyText.cook(I18n.t('education.sequential_replies')) + body: PrettyText.cook(I18n.t("education.sequential_replies")), } end def check_dominating_topic return unless educate_reply?(:notified_about_dominating_topic) - return if @topic.blank? || - @topic.user_id == @user.id || - @topic.posts_count < SiteSetting.summary_posts_required || - @topic.private_message? + if @topic.blank? || @topic.user_id == @user.id || + @topic.posts_count < SiteSetting.summary_posts_required || @topic.private_message? + return + end posts_by_user = @user.posts.where(topic_id: @topic.id).count @@ -136,16 +165,18 @@ class ComposerMessagesFinder return if ratio < (SiteSetting.dominating_topic_minimum_percent.to_f / 100.0) # Log the topic notification - UserHistory.create!(action: UserHistory.actions[:notified_about_dominating_topic], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_dominating_topic], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) { - id: 'dominating_topic', - templateName: 'dominating-topic', + id: "dominating_topic", + templateName: "dominating-topic", wait_for_typing: true, - extraClass: 'education-message dominating-topic-message', - body: PrettyText.cook(I18n.t('education.dominating_topic')) + extraClass: "education-message dominating-topic-message", + body: PrettyText.cook(I18n.t("education.dominating_topic")), } end @@ -157,73 +188,85 @@ class ComposerMessagesFinder reply_to_user_id = Post.where(id: @details[:post_id]).pluck(:user_id)[0] # Users's last x posts in the topic - last_x_replies = @topic. - posts. - where(user_id: @user.id). - order('created_at desc'). - limit(SiteSetting.get_a_room_threshold). - pluck(:reply_to_user_id). - find_all { |uid| uid != @user.id && uid == reply_to_user_id } + last_x_replies = + @topic + .posts + .where(user_id: @user.id) + .order("created_at desc") + .limit(SiteSetting.get_a_room_threshold) + .pluck(:reply_to_user_id) + .find_all { |uid| uid != @user.id && uid == reply_to_user_id } return unless last_x_replies.size == SiteSetting.get_a_room_threshold - return unless @topic.posts.count('distinct user_id') >= min_users_posted + return unless @topic.posts.count("distinct user_id") >= min_users_posted - UserHistory.create!(action: UserHistory.actions[:notified_about_get_a_room], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_get_a_room], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) reply_username = User.where(id: last_x_replies[0]).pluck_first(:username) { - id: 'get_a_room', - templateName: 'get-a-room', + id: "get_a_room", + templateName: "get-a-room", wait_for_typing: true, reply_username: reply_username, - extraClass: 'education-message get-a-room', - body: PrettyText.cook( - I18n.t( - 'education.get_a_room', - count: SiteSetting.get_a_room_threshold, - reply_username: reply_username, - base_path: Discourse.base_path - ) - ) + extraClass: "education-message get-a-room", + body: + PrettyText.cook( + I18n.t( + "education.get_a_room", + count: SiteSetting.get_a_room_threshold, + reply_username: reply_username, + base_path: Discourse.base_path, + ), + ), } end def check_reviving_old_topic return unless replying? - return if @topic.nil? || - SiteSetting.warn_reviving_old_topic_age < 1 || - @topic.last_posted_at.nil? || - @topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago + if @topic.nil? || SiteSetting.warn_reviving_old_topic_age < 1 || @topic.last_posted_at.nil? || + @topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago + return + end { - id: 'reviving_old', - templateName: 'education', + id: "reviving_old", + templateName: "education", wait_for_typing: false, - extraClass: 'education-message', - body: PrettyText.cook( - I18n.t( - 'education.reviving_old_topic', - time_ago: FreedomPatches::Rails4.time_ago_in_words(@topic.last_posted_at, false, scope: :'datetime.distance_in_words_verbose') - ) - ) + extraClass: "education-message", + body: + PrettyText.cook( + I18n.t( + "education.reviving_old_topic", + time_ago: + FreedomPatches::Rails4.time_ago_in_words( + @topic.last_posted_at, + false, + scope: :"datetime.distance_in_words_verbose", + ), + ), + ), } end def self.user_not_seen_in_a_while(usernames) - User.where(username_lower: usernames).where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago).pluck(:username).sort + User + .where(username_lower: usernames) + .where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago) + .pluck(:username) + .sort end private def educate_reply?(type) - replying? && - @details[:topic_id] && - (@topic.present? && !@topic.private_message?) && - (@user.post_count >= SiteSetting.educate_until_posts) && - !UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id]) + replying? && @details[:topic_id] && (@topic.present? && !@topic.private_message?) && + (@user.post_count >= SiteSetting.educate_until_posts) && + !UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id]) end def creating_topic? @@ -237,5 +280,4 @@ class ComposerMessagesFinder def editing_post? @details[:composer_action] == "edit" end - end diff --git a/lib/compression/engine.rb b/lib/compression/engine.rb index c098c0b6687..732164639dc 100644 --- a/lib/compression/engine.rb +++ b/lib/compression/engine.rb @@ -9,12 +9,13 @@ module Compression Compression::Zip.new, Compression::Pipeline.new([Compression::Tar.new, Compression::Gzip.new]), Compression::Gzip.new, - Compression::Tar.new + Compression::Tar.new, ] end def self.engine_for(filename, strategies: default_strategies) - strategy = strategies.detect(-> { raise UnsupportedFileExtension }) { |e| e.can_handle?(filename) } + strategy = + strategies.detect(-> { raise UnsupportedFileExtension }) { |e| e.can_handle?(filename) } new(strategy) end diff --git a/lib/compression/gzip.rb b/lib/compression/gzip.rb index 4d0b7c51e15..c668b088f72 100644 --- a/lib/compression/gzip.rb +++ b/lib/compression/gzip.rb @@ -3,12 +3,17 @@ module Compression class Gzip < Strategy def extension - '.gz' + ".gz" end def compress(path, target_name) gzip_target = sanitize_path("#{path}/#{target_name}") - Discourse::Utils.execute_command('gzip', '-5', gzip_target, failure_message: "Failed to gzip file.") + Discourse::Utils.execute_command( + "gzip", + "-5", + gzip_target, + failure_message: "Failed to gzip file.", + ) "#{gzip_target}.gz" end @@ -23,7 +28,8 @@ module Compression true end - def extract_folder(_entry, _entry_path); end + def extract_folder(_entry, _entry_path) + end def get_compressed_file_stream(compressed_file_path) gzip = Zlib::GzipReader.open(compressed_file_path) @@ -32,7 +38,7 @@ module Compression def build_entry_path(dest_path, _, compressed_file_path) basename = File.basename(compressed_file_path) - basename.gsub!(/#{Regexp.escape(extension)}$/, '') + basename.gsub!(/#{Regexp.escape(extension)}$/, "") File.join(dest_path, basename) end @@ -44,12 +50,11 @@ module Compression remaining_size = available_size if ::File.exist?(entry_path) - raise ::Zip::DestinationFileExistsError, - "Destination '#{entry_path}' already exists" + raise ::Zip::DestinationFileExistsError, "Destination '#{entry_path}' already exists" end # Change this later. - ::File.open(entry_path, 'wb') do |os| - buf = ''.dup + ::File.open(entry_path, "wb") do |os| + buf = "".dup while (buf = entry.read(chunk_size)) remaining_size -= chunk_size raise ExtractFailed if remaining_size.negative? diff --git a/lib/compression/pipeline.rb b/lib/compression/pipeline.rb index 1ea9f17ddb2..39a03f9cdae 100644 --- a/lib/compression/pipeline.rb +++ b/lib/compression/pipeline.rb @@ -7,25 +7,27 @@ module Compression end def extension - @strategies.reduce('') { |ext, strategy| ext += strategy.extension } + @strategies.reduce("") { |ext, strategy| ext += strategy.extension } end def compress(path, target_name) current_target = target_name - @strategies.reduce('') do |compressed_path, strategy| + @strategies.reduce("") do |compressed_path, strategy| compressed_path = strategy.compress(path, current_target) - current_target = compressed_path.split('/').last + current_target = compressed_path.split("/").last compressed_path end end def decompress(dest_path, compressed_file_path, max_size) - @strategies.reverse.reduce(compressed_file_path) do |to_decompress, strategy| - next_compressed_file = strategy.decompress(dest_path, to_decompress, max_size) - FileUtils.rm_rf(to_decompress) - next_compressed_file - end + @strategies + .reverse + .reduce(compressed_file_path) do |to_decompress, strategy| + next_compressed_file = strategy.decompress(dest_path, to_decompress, max_size) + FileUtils.rm_rf(to_decompress) + next_compressed_file + end end end end diff --git a/lib/compression/strategy.rb b/lib/compression/strategy.rb index 8bb54f87d4f..d4a05a578c9 100644 --- a/lib/compression/strategy.rb +++ b/lib/compression/strategy.rb @@ -18,9 +18,7 @@ module Compression entries_of(compressed_file).each do |entry| entry_path = build_entry_path(sanitized_dest_path, entry, sanitized_compressed_file_path) - if !is_safe_path_for_extraction?(entry_path, sanitized_dest_path) - next - end + next if !is_safe_path_for_extraction?(entry_path, sanitized_dest_path) FileUtils.mkdir_p(File.dirname(entry_path)) if is_file?(entry) @@ -45,10 +43,10 @@ module Compression filename.strip.tap do |name| # NOTE: File.basename doesn't work right with Windows paths on Unix # get only the filename, not the whole path - name.sub! /\A.*(\\|\/)/, '' + name.sub! %r{\A.*(\\|/)}, "" # Finally, replace all non alphanumeric, underscore # or periods with underscore - name.gsub! /[^\w\.\-]/, '_' + name.gsub! /[^\w\.\-]/, "_" end end @@ -75,7 +73,7 @@ module Compression raise DestinationFileExistsError, "Destination '#{entry_path}' already exists" end - ::File.open(entry_path, 'wb') do |os| + ::File.open(entry_path, "wb") do |os| while (buf = entry.read(chunk_size)) remaining_size -= buf.size raise ExtractFailed if remaining_size.negative? diff --git a/lib/compression/tar.rb b/lib/compression/tar.rb index 580a8ebdd5d..3346d1e764a 100644 --- a/lib/compression/tar.rb +++ b/lib/compression/tar.rb @@ -1,23 +1,31 @@ # frozen_string_literal: true -require 'rubygems/package' +require "rubygems/package" module Compression class Tar < Strategy def extension - '.tar' + ".tar" end def compress(path, target_name) tar_filename = sanitize_filename("#{target_name}.tar") - Discourse::Utils.execute_command('tar', '--create', '--file', tar_filename, target_name, failure_message: "Failed to tar file.") + Discourse::Utils.execute_command( + "tar", + "--create", + "--file", + tar_filename, + target_name, + failure_message: "Failed to tar file.", + ) sanitize_path("#{path}/#{tar_filename}") end private - def extract_folder(_entry, _entry_path); end + def extract_folder(_entry, _entry_path) + end def get_compressed_file_stream(compressed_file_path) file_stream = IO.new(IO.sysopen(compressed_file_path)) diff --git a/lib/compression/zip.rb b/lib/compression/zip.rb index 63c6b927299..dbf132bf880 100644 --- a/lib/compression/zip.rb +++ b/lib/compression/zip.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require 'zip' +require "zip" module Compression class Zip < Strategy def extension - '.zip' + ".zip" end def compress(path, target_name) @@ -15,7 +15,7 @@ module Compression ::Zip::File.open(zip_filename, ::Zip::File::CREATE) do |zipfile| if File.directory?(absolute_path) entries = Dir.entries(absolute_path) - %w[. ..] - write_entries(entries, absolute_path, '', zipfile) + write_entries(entries, absolute_path, "", zipfile) else put_into_archive(absolute_path, zipfile, target_name) end @@ -47,15 +47,14 @@ module Compression remaining_size = available_size if ::File.exist?(entry_path) - raise ::Zip::DestinationFileExistsError, - "Destination '#{entry_path}' already exists" + raise ::Zip::DestinationFileExistsError, "Destination '#{entry_path}' already exists" end - ::File.open(entry_path, 'wb') do |os| + ::File.open(entry_path, "wb") do |os| entry.get_input_stream do |is| entry.set_extra_attributes_on_path(entry_path) - buf = ''.dup + buf = "".dup while (buf = is.sysread(chunk_size, buf)) remaining_size -= chunk_size raise ExtractFailed if remaining_size.negative? @@ -70,7 +69,7 @@ module Compression # A helper method to make the recursion work. def write_entries(entries, base_path, path, zipfile) entries.each do |e| - zipfile_path = path == '' ? e : File.join(path, e) + zipfile_path = path == "" ? e : File.join(path, e) disk_file_path = File.join(base_path, zipfile_path) if File.directory? disk_file_path diff --git a/lib/configurable_urls.rb b/lib/configurable_urls.rb index f20321ff6dc..c196b74b12f 100644 --- a/lib/configurable_urls.rb +++ b/lib/configurable_urls.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module ConfigurableUrls - def faq_path SiteSetting.faq_url.blank? ? "#{Discourse.base_path}/faq" : SiteSetting.faq_url end @@ -11,7 +10,10 @@ module ConfigurableUrls end def privacy_path - SiteSetting.privacy_policy_url.blank? ? "#{Discourse.base_path}/privacy" : SiteSetting.privacy_policy_url + if SiteSetting.privacy_policy_url.blank? + "#{Discourse.base_path}/privacy" + else + SiteSetting.privacy_policy_url + end end - end diff --git a/lib/content_buffer.rb b/lib/content_buffer.rb index 997b57d795e..6d6895811f0 100644 --- a/lib/content_buffer.rb +++ b/lib/content_buffer.rb @@ -3,7 +3,6 @@ # this class is used to track changes to an arbitrary buffer class ContentBuffer - def initialize(initial_content) @initial_content = initial_content @lines = @initial_content.split("\n") @@ -17,7 +16,6 @@ class ContentBuffer text = transform[:text] if transform[:operation] == :delete - # fix first line l = @lines[start_row] @@ -32,16 +30,13 @@ class ContentBuffer @lines[start_row] = l # remove middle lines - (finish_row - start_row).times do - l = @lines.delete_at start_row + 1 - end + (finish_row - start_row).times { l = @lines.delete_at start_row + 1 } # fix last line @lines[start_row] << @lines[finish_row][finish_col - 1..-1] end if transform[:operation] == :insert - @lines[start_row].insert(start_col, text) split = @lines[start_row].split("\n") @@ -56,7 +51,6 @@ class ContentBuffer @lines.insert(i, "") unless @lines.length > i @lines[i] = split[-1] + @lines[i] end - end end diff --git a/lib/content_security_policy.rb b/lib/content_security_policy.rb index 0cfd309a4bc..107dc0437df 100644 --- a/lib/content_security_policy.rb +++ b/lib/content_security_policy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'content_security_policy/builder' -require 'content_security_policy/extension' +require "content_security_policy/builder" +require "content_security_policy/extension" class ContentSecurityPolicy class << self diff --git a/lib/content_security_policy/builder.rb b/lib/content_security_policy/builder.rb index 4f5dbfb9133..e23f55111e2 100644 --- a/lib/content_security_policy/builder.rb +++ b/lib/content_security_policy/builder.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy/default' +require "content_security_policy/default" class ContentSecurityPolicy class Builder @@ -33,7 +33,9 @@ class ContentSecurityPolicy def <<(extension) return unless valid_extension?(extension) - extension.each { |directive, sources| extend_directive(normalize_directive(directive), sources) } + extension.each do |directive, sources| + extend_directive(normalize_directive(directive), sources) + end end def build @@ -53,7 +55,7 @@ class ContentSecurityPolicy private def normalize_directive(directive) - directive.to_s.gsub('-', '_').to_sym + directive.to_s.gsub("-", "_").to_sym end def normalize_source(source) diff --git a/lib/content_security_policy/default.rb b/lib/content_security_policy/default.rb index ef67672b059..6b147079c70 100644 --- a/lib/content_security_policy/default.rb +++ b/lib/content_security_policy/default.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy' +require "content_security_policy" class ContentSecurityPolicy class Default @@ -7,16 +7,19 @@ class ContentSecurityPolicy def initialize(base_url:) @base_url = base_url - @directives = {}.tap do |directives| - directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https - directives[:base_uri] = [:self] - directives[:object_src] = [:none] - directives[:script_src] = script_src - directives[:worker_src] = worker_src - directives[:report_uri] = report_uri if SiteSetting.content_security_policy_collect_reports - directives[:frame_ancestors] = frame_ancestors if restrict_embed? - directives[:manifest_src] = ["'self'"] - end + @directives = + {}.tap do |directives| + directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https + directives[:base_uri] = [:self] + directives[:object_src] = [:none] + directives[:script_src] = script_src + directives[:worker_src] = worker_src + directives[ + :report_uri + ] = report_uri if SiteSetting.content_security_policy_collect_reports + directives[:frame_ancestors] = frame_ancestors if restrict_embed? + directives[:manifest_src] = ["'self'"] + end end private @@ -27,27 +30,34 @@ class ContentSecurityPolicy SCRIPT_ASSET_DIRECTORIES = [ # [dir, can_use_s3_cdn, can_use_cdn, for_worker] - ['/assets/', true, true, true], - ['/brotli_asset/', true, true, true], - ['/extra-locales/', false, false, false], - ['/highlight-js/', false, true, false], - ['/javascripts/', false, true, true], - ['/plugins/', false, true, true], - ['/theme-javascripts/', false, true, false], - ['/svg-sprite/', false, true, false], + ["/assets/", true, true, true], + ["/brotli_asset/", true, true, true], + ["/extra-locales/", false, false, false], + ["/highlight-js/", false, true, false], + ["/javascripts/", false, true, true], + ["/plugins/", false, true, true], + ["/theme-javascripts/", false, true, false], + ["/svg-sprite/", false, true, false], ] - def script_assets(base = base_url, s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url, cdn = GlobalSetting.cdn_url, worker: false) - SCRIPT_ASSET_DIRECTORIES.map do |dir, can_use_s3_cdn, can_use_cdn, for_worker| - next if worker && !for_worker - if can_use_s3_cdn && s3_cdn - s3_cdn + dir - elsif can_use_cdn && cdn - cdn + Discourse.base_path + dir - else - base + dir + def script_assets( + base = base_url, + s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url, + cdn = GlobalSetting.cdn_url, + worker: false + ) + SCRIPT_ASSET_DIRECTORIES + .map do |dir, can_use_s3_cdn, can_use_cdn, for_worker| + next if worker && !for_worker + if can_use_s3_cdn && s3_cdn + s3_cdn + dir + elsif can_use_cdn && cdn + cdn + Discourse.base_path + dir + else + base + dir + end end - end.compact + .compact end def script_src @@ -55,7 +65,7 @@ class ContentSecurityPolicy "#{base_url}/logs/", "#{base_url}/sidekiq/", "#{base_url}/mini-profiler-resources/", - *script_assets + *script_assets, ].tap do |sources| sources << :report_sample if SiteSetting.content_security_policy_collect_reports sources << :unsafe_eval if Rails.env.development? # TODO remove this once we have proper source maps in dev @@ -67,23 +77,25 @@ class ContentSecurityPolicy end # we need analytics.js still as gtag/js is a script wrapper for it - sources << 'https://www.google-analytics.com/analytics.js' if SiteSetting.ga_universal_tracking_code.present? - sources << 'https://www.googletagmanager.com/gtag/js' if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag" + if SiteSetting.ga_universal_tracking_code.present? + sources << "https://www.google-analytics.com/analytics.js" + end + if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag" + sources << "https://www.googletagmanager.com/gtag/js" + end if SiteSetting.gtm_container_id.present? - sources << 'https://www.googletagmanager.com/gtm.js' + sources << "https://www.googletagmanager.com/gtm.js" sources << "'nonce-#{ApplicationHelper.google_tag_manager_nonce}'" end - if SiteSetting.splash_screen - sources << "'#{SplashScreenHelper.fingerprint}'" - end + sources << "'#{SplashScreenHelper.fingerprint}'" if SiteSetting.splash_screen end end def worker_src [ "'self'", # For service worker - *script_assets(worker: true) + *script_assets(worker: true), ] end @@ -92,15 +104,11 @@ class ContentSecurityPolicy end def frame_ancestors - [ - "'self'", - *EmbeddableHost.pluck(:host).map { |host| "https://#{host}" } - ] + ["'self'", *EmbeddableHost.pluck(:host).map { |host| "https://#{host}" }] end def restrict_embed? - SiteSetting.content_security_policy_frame_ancestors && - !SiteSetting.embed_any_origin + SiteSetting.content_security_policy_frame_ancestors && !SiteSetting.embed_any_origin end end end diff --git a/lib/content_security_policy/extension.rb b/lib/content_security_policy/extension.rb index 51b59acda31..150e0048622 100644 --- a/lib/content_security_policy/extension.rb +++ b/lib/content_security_policy/extension.rb @@ -4,12 +4,12 @@ class ContentSecurityPolicy extend self def site_setting_extension - { script_src: SiteSetting.content_security_policy_script_src.split('|') } + { script_src: SiteSetting.content_security_policy_script_src.split("|") } end def path_specific_extension(path_info) {}.tap do |obj| - for_qunit_route = !Rails.env.production? && ["/qunit", "/wizard/qunit"].include?(path_info) + for_qunit_route = !Rails.env.production? && %w[/qunit /wizard/qunit].include?(path_info) for_qunit_route ||= "/theme-qunit" == path_info obj[:script_src] = :unsafe_eval if for_qunit_route end @@ -23,7 +23,7 @@ class ContentSecurityPolicy end end - THEME_SETTING = 'extend_content_security_policy' + THEME_SETTING = "extend_content_security_policy" def theme_extensions(theme_id) key = "theme_extensions_#{theme_id}" @@ -37,47 +37,55 @@ class ContentSecurityPolicy private def cache - @cache ||= DistributedCache.new('csp_extensions') + @cache ||= DistributedCache.new("csp_extensions") end def find_theme_extensions(theme_id) extensions = [] theme_ids = Theme.transform_ids(theme_id) - Theme.where(id: theme_ids).find_each do |theme| - theme.cached_settings.each do |setting, value| - extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING + Theme + .where(id: theme_ids) + .find_each do |theme| + theme.cached_settings.each do |setting, value| + extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING + end end - end - extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions) - - html_fields = ThemeField.where( - theme_id: theme_ids, - target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, - name: ThemeField.html_fields + extensions << build_theme_extension( + ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions, ) + html_fields = + ThemeField.where( + theme_id: theme_ids, + target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, + name: ThemeField.html_fields, + ) + auto_script_src_extension = { script_src: [] } html_fields.each(&:ensure_baked!) doc = html_fields.map(&:value_baked).join("\n") - Nokogiri::HTML5.fragment(doc).css('script[src]').each do |node| - src = node['src'] - uri = URI(src) + Nokogiri::HTML5 + .fragment(doc) + .css("script[src]") + .each do |node| + src = node["src"] + uri = URI(src) - next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts) - next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts) - next if uri.path.nil? # Ignore raw hosts + next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts) + next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts) + next if uri.path.nil? # Ignore raw hosts - uri.query = nil # CSP should not include query part of url + uri.query = nil # CSP should not include query part of url - uri_string = uri.to_s.sub(/^\/\//, '') # Protocol-less CSP should not have // at beginning of URL + uri_string = uri.to_s.sub(%r{^//}, "") # Protocol-less CSP should not have // at beginning of URL - auto_script_src_extension[:script_src] << uri_string - rescue URI::Error - # Ignore invalid URI - end + auto_script_src_extension[:script_src] << uri_string + rescue URI::Error + # Ignore invalid URI + end extensions << auto_script_src_extension @@ -87,7 +95,7 @@ class ContentSecurityPolicy def build_theme_extension(entries) {}.tap do |extension| entries.each do |entry| - directive, source = entry.split(':', 2).map(&:strip) + directive, source = entry.split(":", 2).map(&:strip) extension[directive] ||= [] extension[directive] << source diff --git a/lib/content_security_policy/middleware.rb b/lib/content_security_policy/middleware.rb index 54ec0f5a656..79ec0834275 100644 --- a/lib/content_security_policy/middleware.rb +++ b/lib/content_security_policy/middleware.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy' +require "content_security_policy" class ContentSecurityPolicy class Middleware @@ -19,8 +19,16 @@ class ContentSecurityPolicy theme_id = env[:resolved_theme_id] - headers['Content-Security-Policy'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy - headers['Content-Security-Policy-Report-Only'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy_report_only + headers["Content-Security-Policy"] = policy( + theme_id, + base_url: base_url, + path_info: env["PATH_INFO"], + ) if SiteSetting.content_security_policy + headers["Content-Security-Policy-Report-Only"] = policy( + theme_id, + base_url: base_url, + path_info: env["PATH_INFO"], + ) if SiteSetting.content_security_policy_report_only response end @@ -30,7 +38,7 @@ class ContentSecurityPolicy delegate :policy, to: :ContentSecurityPolicy def html_response?(headers) - headers['Content-Type'] && headers['Content-Type'] =~ /html/ + headers["Content-Type"] && headers["Content-Type"] =~ /html/ end end end diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 4d8df118e6a..01463bb7761 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -7,7 +7,7 @@ class CookedPostProcessor include CookedProcessorMixin LIGHTBOX_WRAPPER_CSS_CLASS = "lightbox-wrapper" - GIF_SOURCES_REGEXP = /(giphy|tenor)\.com\// + GIF_SOURCES_REGEXP = %r{(giphy|tenor)\.com/} attr_reader :cooking_options, :doc @@ -61,25 +61,27 @@ class CookedPostProcessor return if @post.user.blank? || !Guardian.new.can_see?(@post) BadgeGranter.grant(Badge.find(Badge::FirstEmoji), @post.user, post_id: @post.id) if has_emoji? - BadgeGranter.grant(Badge.find(Badge::FirstOnebox), @post.user, post_id: @post.id) if @has_oneboxes - BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id) if @post.is_reply_by_email? + if @has_oneboxes + BadgeGranter.grant(Badge.find(Badge::FirstOnebox), @post.user, post_id: @post.id) + end + if @post.is_reply_by_email? + BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id) + end end def post_process_quotes - @doc.css("aside.quote").each do |q| - post_number = q['data-post'] - topic_id = q['data-topic'] - if topic_id && post_number - comparer = QuoteComparer.new( - topic_id.to_i, - post_number.to_i, - q.css('blockquote').text - ) + @doc + .css("aside.quote") + .each do |q| + post_number = q["data-post"] + topic_id = q["data-topic"] + if topic_id && post_number + comparer = QuoteComparer.new(topic_id.to_i, post_number.to_i, q.css("blockquote").text) - q['class'] = ((q['class'] || '') + " quote-post-not-found").strip if comparer.missing? - q['class'] = ((q['class'] || '') + " quote-modified").strip if comparer.modified? + q["class"] = ((q["class"] || "") + " quote-post-not-found").strip if comparer.missing? + q["class"] = ((q["class"] || "") + " quote-modified").strip if comparer.modified? + end end - end end def remove_full_quote_on_direct_reply @@ -87,66 +89,68 @@ class CookedPostProcessor return if @post.post_number == 1 return if @doc.xpath("aside[contains(@class, 'quote')]").size != 1 - previous = Post - .where("post_number < ? AND topic_id = ? AND post_type = ? AND NOT hidden", @post.post_number, @post.topic_id, Post.types[:regular]) - .order("post_number DESC") - .limit(1) - .pluck(:cooked) - .first + previous = + Post + .where( + "post_number < ? AND topic_id = ? AND post_type = ? AND NOT hidden", + @post.post_number, + @post.topic_id, + Post.types[:regular], + ) + .order("post_number DESC") + .limit(1) + .pluck(:cooked) + .first return if previous.blank? - previous_text = Nokogiri::HTML5::fragment(previous).text.strip + previous_text = Nokogiri::HTML5.fragment(previous).text.strip quoted_text = @doc.css("aside.quote:first-child blockquote").first&.text&.strip || "" return if previous_text.gsub(/(\s){2,}/, '\1') != quoted_text.gsub(/(\s){2,}/, '\1') - quote_regexp = /\A\s*\[quote.+\[\/quote\]/im + quote_regexp = %r{\A\s*\[quote.+\[/quote\]}im quoteless_raw = @post.raw.sub(quote_regexp, "").strip return if @post.raw.strip == quoteless_raw PostRevisor.new(@post).revise!( Discourse.system_user, - { - raw: quoteless_raw, - edit_reason: I18n.t(:removed_direct_reply_full_quotes) - }, + { raw: quoteless_raw, edit_reason: I18n.t(:removed_direct_reply_full_quotes) }, skip_validations: true, - bypass_bump: true + bypass_bump: true, ) end def extract_images # all images with a src attribute @doc.css("img[src], img[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]") - - # minus data images - @doc.css("img[src^='data']") - - # minus emojis - @doc.css("img.emoji") + # minus data images + @doc.css("img[src^='data']") - + # minus emojis + @doc.css("img.emoji") end def extract_images_for_post # all images with a src attribute @doc.css("img[src]") - - # minus emojis - @doc.css("img.emoji") - - # minus images inside quotes - @doc.css(".quote img") - - # minus onebox site icons - @doc.css("img.site-icon") - - # minus onebox avatars - @doc.css("img.onebox-avatar") - - @doc.css("img.onebox-avatar-inline") - - # minus github onebox profile images - @doc.css(".onebox.githubfolder img") + # minus emojis + @doc.css("img.emoji") - + # minus images inside quotes + @doc.css(".quote img") - + # minus onebox site icons + @doc.css("img.site-icon") - + # minus onebox avatars + @doc.css("img.onebox-avatar") - @doc.css("img.onebox-avatar-inline") - + # minus github onebox profile images + @doc.css(".onebox.githubfolder img") end def convert_to_link!(img) w, h = img["width"].to_i, img["height"].to_i - user_width, user_height = (w > 0 && h > 0 && [w, h]) || - get_size_from_attributes(img) || - get_size_from_image_sizes(img["src"], @opts[:image_sizes]) + user_width, user_height = + (w > 0 && h > 0 && [w, h]) || get_size_from_attributes(img) || + get_size_from_image_sizes(img["src"], @opts[:image_sizes]) limit_size!(img) @@ -155,7 +159,7 @@ class CookedPostProcessor upload = Upload.get_from_url(src) - original_width, original_height = nil + original_width, original_height = nil if (upload.present?) original_width = upload.width || 0 @@ -172,12 +176,17 @@ class CookedPostProcessor img.add_class("animated") end - return if original_width <= SiteSetting.max_image_width && original_height <= SiteSetting.max_image_height + if original_width <= SiteSetting.max_image_width && + original_height <= SiteSetting.max_image_height + return + end - user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && user_height.to_i <= 0 + user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && + user_height.to_i <= 0 width, height = user_width, user_height - crop = SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop + crop = + SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop if crop width, height = ImageSizer.crop(width, height) @@ -200,7 +209,7 @@ class CookedPostProcessor return if upload.animated? - if img.ancestors('.onebox, .onebox-body, .quote').blank? && !img.classes.include?("onebox") + if img.ancestors(".onebox, .onebox-body, .quote").blank? && !img.classes.include?("onebox") add_lightbox!(img, original_width, original_height, upload, cropped: crop) end @@ -211,7 +220,7 @@ class CookedPostProcessor def each_responsive_ratio SiteSetting .responsive_post_image_sizes - .split('|') + .split("|") .map(&:to_f) .sort .each { |r| yield r if r > 1 } @@ -239,13 +248,16 @@ class CookedPostProcessor srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x" end - img["srcset"] = "#{UrlHelper.cook_url(img["src"], secure: @post.with_secure_uploads?)}#{srcset}" if srcset.present? + img[ + "srcset" + ] = "#{UrlHelper.cook_url(img["src"], secure: @post.with_secure_uploads?)}#{srcset}" if srcset.present? end else img["src"] = upload.url end - if !@disable_dominant_color && (color = upload.dominant_color(calculate_if_missing: true).presence) + if !@disable_dominant_color && + (color = upload.dominant_color(calculate_if_missing: true).presence) img["data-dominant-color"] = color end end @@ -261,9 +273,7 @@ class CookedPostProcessor a = create_link_node("lightbox", src) img.add_next_sibling(a) - if upload - a["data-download-href"] = Discourse.store.download_url(upload) - end + a["data-download-href"] = Discourse.store.download_url(upload) if upload a.add_child(img) @@ -309,48 +319,55 @@ class CookedPostProcessor @post.update_column(:image_upload_id, upload.id) # post if @post.is_first_post? # topic @post.topic.update_column(:image_upload_id, upload.id) - extra_sizes = ThemeModifierHelper.new(theme_ids: Theme.user_selectable.pluck(:id)).topic_thumbnail_sizes + extra_sizes = + ThemeModifierHelper.new(theme_ids: Theme.user_selectable.pluck(:id)).topic_thumbnail_sizes @post.topic.generate_thumbnails!(extra_sizes: extra_sizes) end else @post.update_column(:image_upload_id, nil) if @post.image_upload_id - @post.topic.update_column(:image_upload_id, nil) if @post.topic.image_upload_id && @post.is_first_post? + if @post.topic.image_upload_id && @post.is_first_post? + @post.topic.update_column(:image_upload_id, nil) + end nil end end def optimize_urls - %w{href data-download-href}.each do |selector| - @doc.css("a[#{selector}]").each do |a| - a[selector] = UrlHelper.cook_url(a[selector].to_s) - end + %w[href data-download-href].each do |selector| + @doc.css("a[#{selector}]").each { |a| a[selector] = UrlHelper.cook_url(a[selector].to_s) } end - %w{src}.each do |selector| - @doc.css("img[#{selector}]").each do |img| - custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"]) - img[selector] = UrlHelper.cook_url( - img[selector].to_s, secure: @post.with_secure_uploads? && !custom_emoji - ) - end + %w[src].each do |selector| + @doc + .css("img[#{selector}]") + .each do |img| + custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"]) + img[selector] = UrlHelper.cook_url( + img[selector].to_s, + secure: @post.with_secure_uploads? && !custom_emoji, + ) + end end end def remove_user_ids - @doc.css("a[href]").each do |a| - uri = begin - URI(a["href"]) - rescue URI::Error - next + @doc + .css("a[href]") + .each do |a| + uri = + begin + URI(a["href"]) + rescue URI::Error + next + end + next if uri.hostname != Discourse.current_hostname + + query = Rack::Utils.parse_nested_query(uri.query) + next if !query.delete("u") + + uri.query = query.map { |k, v| "#{k}=#{v}" }.join("&").presence + a["href"] = uri.to_s end - next if uri.hostname != Discourse.current_hostname - - query = Rack::Utils.parse_nested_query(uri.query) - next if !query.delete("u") - - uri.query = query.map { |k, v| "#{k}=#{v}" }.join("&").presence - a["href"] = uri.to_s - end end def enforce_nofollow @@ -369,13 +386,14 @@ class CookedPostProcessor def process_hotlinked_image(img) @hotlinked_map ||= @post.post_hotlinked_media.preload(:upload).map { |r| [r.url, r] }.to_h - normalized_src = PostHotlinkedMedia.normalize_src(img["src"] || img[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR]) + normalized_src = + PostHotlinkedMedia.normalize_src(img["src"] || img[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR]) info = @hotlinked_map[normalized_src] still_an_image = true if info&.too_large? - if img.ancestors('.onebox, .onebox-body').blank? + if img.ancestors(".onebox, .onebox-body").blank? add_large_image_placeholder!(img) else img.remove @@ -383,7 +401,7 @@ class CookedPostProcessor still_an_image = false elsif info&.download_failed? - if img.ancestors('.onebox, .onebox-body').blank? + if img.ancestors(".onebox, .onebox-body").blank? add_broken_image_placeholder!(img) else img.remove @@ -399,28 +417,29 @@ class CookedPostProcessor end def add_blocked_hotlinked_media_placeholders - @doc.css([ - "[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]", - "[#{PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR}]", - ].join(',')).each do |el| - src = el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] || - el[PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR]&.split(',')&.first&.split(' ')&.first + @doc + .css( + [ + "[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]", + "[#{PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR}]", + ].join(","), + ) + .each do |el| + src = + el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] || + el[PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR]&.split(",")&.first&.split(" ")&.first - if el.name == "img" - add_blocked_hotlinked_image_placeholder!(el) - next + if el.name == "img" + add_blocked_hotlinked_image_placeholder!(el) + next + end + + el = el.parent if %w[video audio].include?(el.parent.name) + + el = el.parent if el.parent.classes.include?("video-container") + + add_blocked_hotlinked_media_placeholder!(el, src) end - - if ["video", "audio"].include?(el.parent.name) - el = el.parent - end - - if el.parent.classes.include?("video-container") - el = el.parent - end - - add_blocked_hotlinked_media_placeholder!(el, src) - end end def is_svg?(img) @@ -431,6 +450,6 @@ class CookedPostProcessor nil end - File.extname(path) == '.svg' if path + File.extname(path) == ".svg" if path end end diff --git a/lib/cooked_processor_mixin.rb b/lib/cooked_processor_mixin.rb index 68add8893fa..4c2fba4c17a 100644 --- a/lib/cooked_processor_mixin.rb +++ b/lib/cooked_processor_mixin.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module CookedProcessorMixin - def post_process_oneboxes limit = SiteSetting.max_oneboxes_per_post - @doc.css("aside.onebox, a.inline-onebox").size oneboxes = {} @@ -14,7 +13,7 @@ module CookedProcessorMixin if skip_onebox if is_onebox - element.remove_class('onebox') + element.remove_class("onebox") else remove_inline_onebox_loading_class(element) end @@ -26,11 +25,13 @@ module CookedProcessorMixin map[url] = true if is_onebox - onebox = Oneboxer.onebox(url, - invalidate_oneboxes: !!@opts[:invalidate_oneboxes], - user_id: @model&.user_id, - category_id: @category_id - ) + onebox = + Oneboxer.onebox( + url, + invalidate_oneboxes: !!@opts[:invalidate_oneboxes], + user_id: @model&.user_id, + category_id: @category_id, + ) @has_oneboxes = true if onebox.present? onebox @@ -56,7 +57,7 @@ module CookedProcessorMixin # and wrap in a div limit_size!(img) - next if img["class"]&.include?('onebox-avatar') + next if img["class"]&.include?("onebox-avatar") parent = parent&.parent if parent&.name == "a" parent_class = parent && parent["class"] @@ -84,12 +85,18 @@ module CookedProcessorMixin if width < 64 && height < 64 img["class"] = img["class"].to_s + " onebox-full-image" else - img.delete('width') - img.delete('height') - new_parent = img.add_next_sibling("
") + img.delete("width") + img.delete("height") + new_parent = + img.add_next_sibling( + "
", + ) new_parent.first.add_child(img) end - elsif (parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") || parent_class&.include?("scale-images")) && width > 0 && height > 0 + elsif ( + parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") || + parent_class&.include?("scale-images") + ) && width > 0 && height > 0 img.remove_attribute("width") img.remove_attribute("height") parent["class"] = "aspect-image-full-size" @@ -98,16 +105,18 @@ module CookedProcessorMixin end if @omit_nofollow || !SiteSetting.add_rel_nofollow_to_user_content - @doc.css(".onebox-body a[rel], .onebox a[rel]").each do |a| - rel_values = a['rel'].split(' ').map(&:downcase) - rel_values.delete('nofollow') - rel_values.delete('ugc') - if rel_values.blank? - a.remove_attribute("rel") - else - a["rel"] = rel_values.join(' ') + @doc + .css(".onebox-body a[rel], .onebox a[rel]") + .each do |a| + rel_values = a["rel"].split(" ").map(&:downcase) + rel_values.delete("nofollow") + rel_values.delete("ugc") + if rel_values.blank? + a.remove_attribute("rel") + else + a["rel"] = rel_values.join(" ") + end end - end end end @@ -116,9 +125,9 @@ module CookedProcessorMixin # 1) the width/height attributes # 2) the dimension from the preview (image_sizes) # 3) the dimension of the original image (HTTP request) - w, h = get_size_from_attributes(img) || - get_size_from_image_sizes(img["src"], @opts[:image_sizes]) || - get_size(img["src"]) + w, h = + get_size_from_attributes(img) || get_size_from_image_sizes(img["src"], @opts[:image_sizes]) || + get_size(img["src"]) # limit the size of the thumbnail img["width"], img["height"] = ImageSizer.resize(w, h) @@ -126,7 +135,7 @@ module CookedProcessorMixin def get_size_from_attributes(img) w, h = img["width"].to_i, img["height"].to_i - return [w, h] unless w <= 0 || h <= 0 + return w, h unless w <= 0 || h <= 0 # if only width or height are specified attempt to scale image if w > 0 || h > 0 w = w.to_f @@ -149,9 +158,9 @@ module CookedProcessorMixin return unless image_sizes.present? image_sizes.each do |image_size| url, size = image_size[0], image_size[1] - if url && src && url.include?(src) && - size && size["width"].to_i > 0 && size["height"].to_i > 0 - return [size["width"], size["height"]] + if url && src && url.include?(src) && size && size["width"].to_i > 0 && + size["height"].to_i > 0 + return size["width"], size["height"] end end nil @@ -165,7 +174,7 @@ module CookedProcessorMixin return @size_cache[url] if @size_cache.has_key?(url) absolute_url = url - absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ /^\/[^\/]/ + absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ %r{^/[^/]} return unless absolute_url @@ -186,14 +195,13 @@ module CookedProcessorMixin else @size_cache[url] = FastImage.size(absolute_url) end - rescue Zlib::BufError, URI::Error, OpenSSL::SSL::SSLError # FastImage.size raises BufError for some gifs, leave it. end def is_valid_image_url?(url) uri = URI.parse(url) - %w(http https).include? uri.scheme + %w[http https].include? uri.scheme rescue URI::Error end @@ -217,9 +225,12 @@ module CookedProcessorMixin "help", I18n.t( "upload.placeholders.too_large_humanized", - max_size: ActiveSupport::NumberHelper.number_to_human_size(SiteSetting.max_image_size_kb.kilobytes) - ) - ) + max_size: + ActiveSupport::NumberHelper.number_to_human_size( + SiteSetting.max_image_size_kb.kilobytes, + ), + ), + ), ) # Only if the image is already linked @@ -227,7 +238,7 @@ module CookedProcessorMixin parent = placeholder.parent parent.add_next_sibling(placeholder) - if parent.name == 'a' && parent["href"].present? + if parent.name == "a" && parent["href"].present? if url == parent["href"] parent.remove else @@ -295,12 +306,13 @@ module CookedProcessorMixin end def process_inline_onebox(element) - inline_onebox = InlineOneboxer.lookup( - element.attributes["href"].value, - invalidate: !!@opts[:invalidate_oneboxes], - user_id: @model&.user_id, - category_id: @category_id - ) + inline_onebox = + InlineOneboxer.lookup( + element.attributes["href"].value, + invalidate: !!@opts[:invalidate_oneboxes], + user_id: @model&.user_id, + category_id: @category_id, + ) if title = inline_onebox&.dig(:title) element.children = CGI.escapeHTML(title) diff --git a/lib/crawler_detection.rb b/lib/crawler_detection.rb index 0b90dc0acb6..f926d3455df 100644 --- a/lib/crawler_detection.rb +++ b/lib/crawler_detection.rb @@ -4,7 +4,7 @@ module CrawlerDetection WAYBACK_MACHINE_URL = "archive.org" def self.to_matcher(string, type: nil) - escaped = string.split('|').map { |agent| Regexp.escape(agent) }.join('|') + escaped = string.split("|").map { |agent| Regexp.escape(agent) }.join("|") if type == :real && Rails.env == "test" # we need this bypass so we properly render views @@ -15,18 +15,33 @@ module CrawlerDetection end def self.crawler?(user_agent, via_header = nil) - return true if user_agent.nil? || user_agent&.include?(WAYBACK_MACHINE_URL) || via_header&.include?(WAYBACK_MACHINE_URL) + if user_agent.nil? || user_agent&.include?(WAYBACK_MACHINE_URL) || + via_header&.include?(WAYBACK_MACHINE_URL) + return true + end # this is done to avoid regenerating regexes @non_crawler_matchers ||= {} @matchers ||= {} - possibly_real = (@non_crawler_matchers[SiteSetting.non_crawler_user_agents] ||= to_matcher(SiteSetting.non_crawler_user_agents, type: :real)) + possibly_real = + ( + @non_crawler_matchers[SiteSetting.non_crawler_user_agents] ||= to_matcher( + SiteSetting.non_crawler_user_agents, + type: :real, + ) + ) if user_agent.match?(possibly_real) - known_bots = (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents)) + known_bots = + (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents)) if user_agent.match?(known_bots) - bypass = (@matchers[SiteSetting.crawler_check_bypass_agents] ||= to_matcher(SiteSetting.crawler_check_bypass_agents)) + bypass = + ( + @matchers[SiteSetting.crawler_check_bypass_agents] ||= to_matcher( + SiteSetting.crawler_check_bypass_agents, + ) + ) !user_agent.match?(bypass) else false @@ -34,30 +49,40 @@ module CrawlerDetection else true end - end def self.show_browser_update?(user_agent) return false if SiteSetting.browser_update_user_agents.blank? @browser_update_matchers ||= {} - matcher = @browser_update_matchers[SiteSetting.browser_update_user_agents] ||= to_matcher(SiteSetting.browser_update_user_agents) + matcher = + @browser_update_matchers[SiteSetting.browser_update_user_agents] ||= to_matcher( + SiteSetting.browser_update_user_agents, + ) user_agent.match?(matcher) end # Given a user_agent that returns true from crawler?, should its request be allowed? def self.allow_crawler?(user_agent) - return true if SiteSetting.allowed_crawler_user_agents.blank? && - SiteSetting.blocked_crawler_user_agents.blank? + if SiteSetting.allowed_crawler_user_agents.blank? && + SiteSetting.blocked_crawler_user_agents.blank? + return true + end @allowlisted_matchers ||= {} @blocklisted_matchers ||= {} if SiteSetting.allowed_crawler_user_agents.present? - allowlisted = @allowlisted_matchers[SiteSetting.allowed_crawler_user_agents] ||= to_matcher(SiteSetting.allowed_crawler_user_agents) + allowlisted = + @allowlisted_matchers[SiteSetting.allowed_crawler_user_agents] ||= to_matcher( + SiteSetting.allowed_crawler_user_agents, + ) !user_agent.nil? && user_agent.match?(allowlisted) else - blocklisted = @blocklisted_matchers[SiteSetting.blocked_crawler_user_agents] ||= to_matcher(SiteSetting.blocked_crawler_user_agents) + blocklisted = + @blocklisted_matchers[SiteSetting.blocked_crawler_user_agents] ||= to_matcher( + SiteSetting.blocked_crawler_user_agents, + ) user_agent.nil? || !user_agent.match?(blocklisted) end end diff --git a/lib/csrf_token_verifier.rb b/lib/csrf_token_verifier.rb index 56ed911a7f0..8736b749df1 100644 --- a/lib/csrf_token_verifier.rb +++ b/lib/csrf_token_verifier.rb @@ -2,7 +2,8 @@ # Provides a way to check a CSRF token outside of a controller class CSRFTokenVerifier - class InvalidCSRFToken < StandardError; end + class InvalidCSRFToken < StandardError + end include ActiveSupport::Configurable include ActionController::RequestForgeryProtection @@ -18,9 +19,7 @@ class CSRFTokenVerifier def call(env) @request = ActionDispatch::Request.new(env.dup) - unless verified_request? - raise InvalidCSRFToken - end + raise InvalidCSRFToken unless verified_request? end public :form_authenticity_token diff --git a/lib/current_user.rb b/lib/current_user.rb index cf84adfb278..fdf78431981 100644 --- a/lib/current_user.rb +++ b/lib/current_user.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module CurrentUser - def self.has_auth_cookie?(env) Discourse.current_user_provider.new(env).has_auth_cookie? end @@ -45,5 +44,4 @@ module CurrentUser def current_user_provider @current_user_provider ||= Discourse.current_user_provider.new(request.env) end - end diff --git a/lib/db_helper.rb b/lib/db_helper.rb index 6bc79b044ef..29117934d86 100644 --- a/lib/db_helper.rb +++ b/lib/db_helper.rb @@ -3,7 +3,6 @@ require "migration/base_dropper" class DbHelper - REMAP_SQL ||= <<~SQL SELECT table_name::text, column_name::text, character_maximum_length FROM information_schema.columns @@ -19,24 +18,33 @@ class DbHelper WHERE trigger_name LIKE '%_readonly' SQL - TRUNCATABLE_COLUMNS ||= [ - 'topic_links.url' - ] + TRUNCATABLE_COLUMNS ||= ["topic_links.url"] - def self.remap(from, to, anchor_left: false, anchor_right: false, excluded_tables: [], verbose: false) - like = "#{anchor_left ? '' : "%"}#{from}#{anchor_right ? '' : "%"}" + def self.remap( + from, + to, + anchor_left: false, + anchor_right: false, + excluded_tables: [], + verbose: false + ) + like = "#{anchor_left ? "" : "%"}#{from}#{anchor_right ? "" : "%"}" text_columns = find_text_columns(excluded_tables) text_columns.each do |table, columns| - set = columns.map do |column| - replace = "REPLACE(\"#{column[:name]}\", :from, :to)" - replace = truncate(replace, table, column) - "\"#{column[:name]}\" = #{replace}" - end.join(", ") + set = + columns + .map do |column| + replace = "REPLACE(\"#{column[:name]}\", :from, :to)" + replace = truncate(replace, table, column) + "\"#{column[:name]}\" = #{replace}" + end + .join(", ") - where = columns.map do |column| - "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like" - end.join(" OR ") + where = + columns + .map { |column| "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like" } + .join(" OR ") rows = DB.exec(<<~SQL, from: from, to: to, like: like) UPDATE \"#{table}\" @@ -50,19 +58,32 @@ class DbHelper finish! end - def self.regexp_replace(pattern, replacement, flags: "gi", match: "~*", excluded_tables: [], verbose: false) + def self.regexp_replace( + pattern, + replacement, + flags: "gi", + match: "~*", + excluded_tables: [], + verbose: false + ) text_columns = find_text_columns(excluded_tables) text_columns.each do |table, columns| - set = columns.map do |column| - replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)" - replace = truncate(replace, table, column) - "\"#{column[:name]}\" = #{replace}" - end.join(", ") + set = + columns + .map do |column| + replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)" + replace = truncate(replace, table, column) + "\"#{column[:name]}\" = #{replace}" + end + .join(", ") - where = columns.map do |column| - "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern" - end.join(" OR ") + where = + columns + .map do |column| + "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern" + end + .join(" OR ") rows = DB.exec(<<~SQL, pattern: pattern, replacement: replacement, flags: flags, match: match) UPDATE \"#{table}\" @@ -78,23 +99,25 @@ class DbHelper def self.find(needle, anchor_left: false, anchor_right: false, excluded_tables: []) found = {} - like = "#{anchor_left ? '' : "%"}#{needle}#{anchor_right ? '' : "%"}" + like = "#{anchor_left ? "" : "%"}#{needle}#{anchor_right ? "" : "%"}" - DB.query(REMAP_SQL).each do |r| - next if excluded_tables.include?(r.table_name) + DB + .query(REMAP_SQL) + .each do |r| + next if excluded_tables.include?(r.table_name) - rows = DB.query(<<~SQL, like: like) + rows = DB.query(<<~SQL, like: like) SELECT \"#{r.column_name}\" FROM \"#{r.table_name}\" WHERE \""#{r.column_name}"\" LIKE :like SQL - if rows.size > 0 - found["#{r.table_name}.#{r.column_name}"] = rows.map do |row| - row.public_send(r.column_name) + if rows.size > 0 + found["#{r.table_name}.#{r.column_name}"] = rows.map do |row| + row.public_send(r.column_name) + end end end - end found end @@ -112,16 +135,21 @@ class DbHelper triggers = DB.query(TRIGGERS_SQL).map(&:trigger_name).to_set text_columns = Hash.new { |h, k| h[k] = [] } - DB.query(REMAP_SQL).each do |r| - next if excluded_tables.include?(r.table_name) || - triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name, r.column_name)) || - triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name)) + DB + .query(REMAP_SQL) + .each do |r| + if excluded_tables.include?(r.table_name) || + triggers.include?( + Migration::BaseDropper.readonly_trigger_name(r.table_name, r.column_name), + ) || triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name)) + next + end - text_columns[r.table_name] << { - name: r.column_name, - max_length: r.character_maximum_length - } - end + text_columns[r.table_name] << { + name: r.column_name, + max_length: r.character_maximum_length, + } + end text_columns end diff --git a/lib/demon/base.rb b/lib/demon/base.rb index 397fb5d0b08..c49265a4cfb 100644 --- a/lib/demon/base.rb +++ b/lib/demon/base.rb @@ -1,26 +1,22 @@ # frozen_string_literal: true -module Demon; end +module Demon +end # intelligent fork based demonizer class Demon::Base - def self.demons @demons end def self.start(count = 1, verbose: false) @demons ||= {} - count.times do |i| - (@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start - end + count.times { |i| (@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start } end def self.stop return unless @demons - @demons.values.each do |demon| - demon.stop - end + @demons.values.each { |demon| demon.stop } end def self.restart @@ -32,16 +28,12 @@ class Demon::Base end def self.ensure_running - @demons.values.each do |demon| - demon.ensure_running - end + @demons.values.each { |demon| demon.ensure_running } end def self.kill(signal) return unless @demons - @demons.values.each do |demon| - demon.kill(signal) - end + @demons.values.each { |demon| demon.kill(signal) } end attr_reader :pid, :parent_pid, :started, :index @@ -83,18 +75,27 @@ class Demon::Base if @pid Process.kill(stop_signal, @pid) - wait_for_stop = lambda { - timeout = @stop_timeout + wait_for_stop = + lambda do + timeout = @stop_timeout - while alive? && timeout > 0 - timeout -= (@stop_timeout / 10.0) - sleep(@stop_timeout / 10.0) - Process.waitpid(@pid, Process::WNOHANG) rescue -1 + while alive? && timeout > 0 + timeout -= (@stop_timeout / 10.0) + sleep(@stop_timeout / 10.0) + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end + end + + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end end - Process.waitpid(@pid, Process::WNOHANG) rescue -1 - } - wait_for_stop.call if alive? @@ -118,7 +119,12 @@ class Demon::Base return end - dead = Process.waitpid(@pid, Process::WNOHANG) rescue -1 + dead = + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end if dead STDERR.puts "Detected dead worker #{@pid}, restarting..." @pid = nil @@ -141,21 +147,20 @@ class Demon::Base end def run - @pid = fork do - Process.setproctitle("discourse #{self.class.prefix}") - monitor_parent - establish_app - after_fork - end + @pid = + fork do + Process.setproctitle("discourse #{self.class.prefix}") + monitor_parent + establish_app + after_fork + end write_pid_file end def already_running? if File.exist? pid_file pid = File.read(pid_file).to_i - if Demon::Base.alive?(pid) - return pid - end + return pid if Demon::Base.alive?(pid) end nil @@ -164,24 +169,20 @@ class Demon::Base def self.alive?(pid) Process.kill(0, pid) true - rescue + rescue StandardError false end private def verbose(msg) - if @verbose - puts msg - end + puts msg if @verbose end def write_pid_file verbose("writing pid file #{pid_file} for #{@pid}") FileUtils.mkdir_p(@rails_root + "tmp/pids") - File.open(pid_file, 'w') do |f| - f.write(@pid) - end + File.open(pid_file, "w") { |f| f.write(@pid) } end def delete_pid_file diff --git a/lib/demon/email_sync.rb b/lib/demon/email_sync.rb index 12fedc5724d..93a627c0330 100644 --- a/lib/demon/email_sync.rb +++ b/lib/demon/email_sync.rb @@ -36,15 +36,20 @@ class Demon::EmailSync < ::Demon::Base status = nil idle = false - while @running && group.reload.imap_mailbox_name.present? do + while @running && group.reload.imap_mailbox_name.present? ImapSyncLog.debug("Processing mailbox for group #{group.name} in db #{db}", group) - status = syncer.process( - idle: syncer.can_idle? && status && status[:remaining] == 0, - old_emails_limit: status && status[:remaining] > 0 ? 0 : nil, - ) + status = + syncer.process( + idle: syncer.can_idle? && status && status[:remaining] == 0, + old_emails_limit: status && status[:remaining] > 0 ? 0 : nil, + ) if !syncer.can_idle? && status[:remaining] == 0 - ImapSyncLog.debug("Going to sleep for group #{group.name} in db #{db} to wait for new emails", group, db: false) + ImapSyncLog.debug( + "Going to sleep for group #{group.name} in db #{db} to wait for new emails", + group, + db: false, + ) # Thread goes into sleep for a bit so it is better to return any # connection back to the pool. @@ -66,11 +71,7 @@ class Demon::EmailSync < ::Demon::Base # synchronization primitives available anyway). @running = false - @sync_data.each do |db, sync_data| - sync_data.each do |_, data| - kill_and_disconnect!(data) - end - end + @sync_data.each { |db, sync_data| sync_data.each { |_, data| kill_and_disconnect!(data) } } exit 0 end @@ -89,9 +90,9 @@ class Demon::EmailSync < ::Demon::Base @sync_data = {} @sync_lock = Mutex.new - trap('INT') { kill_threads } - trap('TERM') { kill_threads } - trap('HUP') { kill_threads } + trap("INT") { kill_threads } + trap("TERM") { kill_threads } + trap("HUP") { kill_threads } while @running Discourse.redis.set(HEARTBEAT_KEY, Time.now.to_i, ex: HEARTBEAT_INTERVAL) @@ -101,9 +102,7 @@ class Demon::EmailSync < ::Demon::Base @sync_data.filter! do |db, sync_data| next true if all_dbs.include?(db) - sync_data.each do |_, data| - kill_and_disconnect!(data) - end + sync_data.each { |_, data| kill_and_disconnect!(data) } false end @@ -121,7 +120,10 @@ class Demon::EmailSync < ::Demon::Base next true if groups[group_id] && data[:thread]&.alive? && !data[:syncer]&.disconnected? if !groups[group_id] - ImapSyncLog.warn("Killing thread for group because mailbox is no longer synced", group_id) + ImapSyncLog.warn( + "Killing thread for group because mailbox is no longer synced", + group_id, + ) else ImapSyncLog.warn("Thread for group is dead", group_id) end @@ -133,12 +135,13 @@ class Demon::EmailSync < ::Demon::Base # Spawn new threads for groups that are now synchronized. groups.each do |group_id, group| if !@sync_data[db][group_id] - ImapSyncLog.debug("Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}", group, db: false) + ImapSyncLog.debug( + "Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}", + group, + db: false, + ) - @sync_data[db][group_id] = { - thread: start_thread(db, group), - syncer: nil - } + @sync_data[db][group_id] = { thread: start_thread(db, group), syncer: nil } end end end diff --git a/lib/demon/rails_autospec.rb b/lib/demon/rails_autospec.rb index babf5a5e7e1..78f0782046a 100644 --- a/lib/demon/rails_autospec.rb +++ b/lib/demon/rails_autospec.rb @@ -3,7 +3,6 @@ require "demon/base" class Demon::RailsAutospec < Demon::Base - def self.prefix "rails-autospec" end @@ -17,15 +16,10 @@ class Demon::RailsAutospec < Demon::Base def after_fork require "rack" ENV["RAILS_ENV"] = "test" - Rack::Server.start( - config: "config.ru", - AccessLog: [], - Port: ENV["TEST_SERVER_PORT"] || 60099, - ) + Rack::Server.start(config: "config.ru", AccessLog: [], Port: ENV["TEST_SERVER_PORT"] || 60_099) rescue => e STDERR.puts e.message STDERR.puts e.backtrace.join("\n") exit 1 end - end diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index ed7556d299d..1c1776d80ca 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -3,7 +3,6 @@ require "demon/base" class Demon::Sidekiq < ::Demon::Base - def self.prefix "sidekiq" end @@ -26,7 +25,7 @@ class Demon::Sidekiq < ::Demon::Base Demon::Sidekiq.after_fork&.call puts "Loading Sidekiq in process id #{Process.pid}" - require 'sidekiq/cli' + require "sidekiq/cli" cli = Sidekiq::CLI.instance # Unicorn uses USR1 to indicate that log files have been rotated @@ -38,10 +37,10 @@ class Demon::Sidekiq < ::Demon::Base options = ["-c", GlobalSetting.sidekiq_workers.to_s] - [['critical', 8], ['default', 4], ['low', 2], ['ultra_low', 1]].each do |queue_name, weight| + [["critical", 8], ["default", 4], ["low", 2], ["ultra_low", 1]].each do |queue_name, weight| custom_queue_hostname = ENV["UNICORN_SIDEKIQ_#{queue_name.upcase}_QUEUE_HOSTNAME"] - if !custom_queue_hostname || custom_queue_hostname.split(',').include?(Discourse.os_hostname) + if !custom_queue_hostname || custom_queue_hostname.split(",").include?(Discourse.os_hostname) options << "-q" options << "#{queue_name},#{weight}" end @@ -49,7 +48,7 @@ class Demon::Sidekiq < ::Demon::Base # Sidekiq not as high priority as web, in this environment it is forked so a web is very # likely running - Discourse::Utils.execute_command('renice', '-n', '5', '-p', Process.pid.to_s) + Discourse::Utils.execute_command("renice", "-n", "5", "-p", Process.pid.to_s) cli.parse(options) load Rails.root + "config/initializers/100-sidekiq.rb" @@ -59,5 +58,4 @@ class Demon::Sidekiq < ::Demon::Base STDERR.puts e.backtrace.join("\n") exit 1 end - end diff --git a/lib/directory_helper.rb b/lib/directory_helper.rb index a41b117a397..80b2865a85a 100644 --- a/lib/directory_helper.rb +++ b/lib/directory_helper.rb @@ -1,24 +1,23 @@ # frozen_string_literal: true module DirectoryHelper - def tmp_directory(prefix) directory_cache[prefix] ||= begin - f = File.join(Rails.root, 'tmp', Time.now.strftime("#{prefix}%Y%m%d%H%M%S")) + f = File.join(Rails.root, "tmp", Time.now.strftime("#{prefix}%Y%m%d%H%M%S")) FileUtils.mkdir_p(f) unless Dir[f].present? f end end def remove_tmp_directory(prefix) - tmp_directory_name = directory_cache[prefix] || '' + tmp_directory_name = directory_cache[prefix] || "" directory_cache.delete(prefix) FileUtils.rm_rf(tmp_directory_name) if Dir[tmp_directory_name].present? end private + def directory_cache @directory_cache ||= {} end - end diff --git a/lib/discourse.rb b/lib/discourse.rb index 68c2e6f2104..caba3ba4cef 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require 'cache' -require 'open3' -require 'plugin/instance' -require 'version' +require "cache" +require "open3" +require "plugin/instance" +require "version" module Discourse DB_POST_MIGRATE_PATH ||= "db/post_migrate" REQUESTED_HOSTNAME ||= "REQUESTED_HOSTNAME" class Utils - URI_REGEXP ||= URI.regexp(%w{http https}) + URI_REGEXP ||= URI.regexp(%w[http https]) # Usage: # Discourse::Utils.execute_command("pwd", chdir: 'mydirectory') @@ -22,7 +22,9 @@ module Discourse runner = CommandRunner.new(**args) if block_given? - raise RuntimeError.new("Cannot pass command and block to execute_command") if command.present? + if command.present? + raise RuntimeError.new("Cannot pass command and block to execute_command") + end yield runner else runner.exec(*command) @@ -33,33 +35,32 @@ module Discourse logs.join("\n") end - def self.logs_markdown(logs, user:, filename: 'log.txt') + def self.logs_markdown(logs, user:, filename: "log.txt") # Reserve 250 characters for the rest of the text max_logs_length = SiteSetting.max_post_length - 250 pretty_logs = Discourse::Utils.pretty_logs(logs) # If logs are short, try to inline them - if pretty_logs.size < max_logs_length - return <<~TEXT + return <<~TEXT if pretty_logs.size < max_logs_length ```text #{pretty_logs} ``` TEXT - end # Try to create an upload for the logs - upload = Dir.mktmpdir do |dir| - File.write(File.join(dir, filename), pretty_logs) - zipfile = Compression::Zip.new.compress(dir, filename) - File.open(zipfile) do |file| - UploadCreator.new( - file, - File.basename(zipfile), - type: 'backup_logs', - for_export: 'true' - ).create_for(user.id) + upload = + Dir.mktmpdir do |dir| + File.write(File.join(dir, filename), pretty_logs) + zipfile = Compression::Zip.new.compress(dir, filename) + File.open(zipfile) do |file| + UploadCreator.new( + file, + File.basename(zipfile), + type: "backup_logs", + for_export: "true", + ).create_for(user.id) + end end - end if upload.persisted? return UploadMarkdown.new(upload).attachment_markdown @@ -82,8 +83,8 @@ module Discourse rescue Errno::ENOENT end - FileUtils.mkdir_p(File.join(Rails.root, 'tmp')) - temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex) + FileUtils.mkdir_p(File.join(Rails.root, "tmp")) + temp_destination = File.join(Rails.root, "tmp", SecureRandom.hex) File.open(temp_destination, "w") do |fd| fd.write(contents) @@ -101,9 +102,9 @@ module Discourse rescue Errno::ENOENT, Errno::EINVAL end - FileUtils.mkdir_p(File.join(Rails.root, 'tmp')) - temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex) - execute_command('ln', '-s', source, temp_destination) + FileUtils.mkdir_p(File.join(Rails.root, "tmp")) + temp_destination = File.join(Rails.root, "tmp", SecureRandom.hex) + execute_command("ln", "-s", source, temp_destination) FileUtils.mv(temp_destination, destination) nil @@ -127,13 +128,22 @@ module Discourse end def exec(*command, **exec_params) - raise RuntimeError.new("Cannot specify same parameters at block and command level") if (@init_params.keys & exec_params.keys).present? + if (@init_params.keys & exec_params.keys).present? + raise RuntimeError.new("Cannot specify same parameters at block and command level") + end execute_command(*command, **@init_params.merge(exec_params)) end private - def execute_command(*command, timeout: nil, failure_message: "", success_status_codes: [0], chdir: ".", unsafe_shell: false) + def execute_command( + *command, + timeout: nil, + failure_message: "", + success_status_codes: [0], + chdir: ".", + unsafe_shell: false + ) env = nil env = command.shift if command[0].is_a?(Hash) @@ -156,11 +166,11 @@ module Discourse if !status.exited? || !success_status_codes.include?(status.exitstatus) failure_message = "#{failure_message}\n" if !failure_message.blank? raise CommandError.new( - "#{caller[0]}: #{failure_message}#{stderr}", - stdout: stdout, - stderr: stderr, - status: status - ) + "#{caller[0]}: #{failure_message}#{stderr}", + stdout: stdout, + stderr: stderr, + status: status, + ) end stdout @@ -195,33 +205,32 @@ module Discourse # mini_scheduler direct reporting if Hash === job job_class = job["class"] - if job_class - job_exception_stats[job_class] += 1 - end + job_exception_stats[job_class] += 1 if job_class end # internal reporting - if job.class == Class && ::Jobs::Base > job - job_exception_stats[job] += 1 - end + job_exception_stats[job] += 1 if job.class == Class && ::Jobs::Base > job cm = RailsMultisite::ConnectionManagement - parent_logger.handle_exception(ex, { - current_db: cm.current_db, - current_hostname: cm.current_hostname - }.merge(context)) + parent_logger.handle_exception( + ex, + { current_db: cm.current_db, current_hostname: cm.current_hostname }.merge(context), + ) raise ex if Rails.env.test? end # Expected less matches than what we got in a find - class TooManyMatches < StandardError; end + class TooManyMatches < StandardError + end # When they try to do something they should be logged in for - class NotLoggedIn < StandardError; end + class NotLoggedIn < StandardError + end # When the input is somehow bad - class InvalidParameters < StandardError; end + class InvalidParameters < StandardError + end # When they don't have permission to do something class InvalidAccess < StandardError @@ -249,7 +258,13 @@ module Discourse attr_reader :original_path attr_reader :custom_message - def initialize(msg = nil, status: 404, check_permalinks: false, original_path: nil, custom_message: nil) + def initialize( + msg = nil, + status: 404, + check_permalinks: false, + original_path: nil, + custom_message: nil + ) super(msg) @status = status @@ -260,27 +275,33 @@ module Discourse end # When a setting is missing - class SiteSettingMissing < StandardError; end + class SiteSettingMissing < StandardError + end # When ImageMagick is missing - class ImageMagickMissing < StandardError; end + class ImageMagickMissing < StandardError + end # When read-only mode is enabled - class ReadOnly < StandardError; end + class ReadOnly < StandardError + end # Cross site request forgery - class CSRF < StandardError; end + class CSRF < StandardError + end - class Deprecation < StandardError; end + class Deprecation < StandardError + end - class ScssError < StandardError; end + class ScssError < StandardError + end def self.filters - @filters ||= [:latest, :unread, :new, :unseen, :top, :read, :posted, :bookmarks] + @filters ||= %i[latest unread new unseen top read posted bookmarks] end def self.anonymous_filters - @anonymous_filters ||= [:latest, :top, :categories] + @anonymous_filters ||= %i[latest top categories] end def self.top_menu_items @@ -288,7 +309,7 @@ module Discourse end def self.anonymous_top_menu_items - @anonymous_top_menu_items ||= Discourse.anonymous_filters + [:categories, :top] + @anonymous_top_menu_items ||= Discourse.anonymous_filters + %i[categories top] end PIXEL_RATIOS ||= [1, 1.5, 2, 3] @@ -297,26 +318,28 @@ module Discourse # TODO: should cache these when we get a notification system for site settings set = Set.new - SiteSetting.avatar_sizes.split("|").map(&:to_i).each do |size| - PIXEL_RATIOS.each do |pixel_ratio| - set << (size * pixel_ratio).to_i - end - end + SiteSetting + .avatar_sizes + .split("|") + .map(&:to_i) + .each { |size| PIXEL_RATIOS.each { |pixel_ratio| set << (size * pixel_ratio).to_i } } set end def self.activate_plugins! @plugins = [] - Plugin::Instance.find_all("#{Rails.root}/plugins").each do |p| - v = p.metadata.required_version || Discourse::VERSION::STRING - if Discourse.has_needed_version?(Discourse::VERSION::STRING, v) - p.activate! - @plugins << p - else - STDERR.puts "Could not activate #{p.metadata.name}, discourse does not meet required version (#{v})" + Plugin::Instance + .find_all("#{Rails.root}/plugins") + .each do |p| + v = p.metadata.required_version || Discourse::VERSION::STRING + if Discourse.has_needed_version?(Discourse::VERSION::STRING, v) + p.activate! + @plugins << p + else + STDERR.puts "Could not activate #{p.metadata.name}, discourse does not meet required version (#{v})" + end end - end DiscourseEvent.trigger(:after_plugin_activation) end @@ -360,9 +383,7 @@ module Discourse def self.apply_asset_filters(plugins, type, request) filter_opts = asset_filter_options(type, request) - plugins.select do |plugin| - plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) } - end + plugins.select { |plugin| plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) } } end def self.asset_filter_options(type, request) @@ -385,20 +406,24 @@ module Discourse targets << :desktop if args[:desktop_view] targets.each do |target| - assets += plugins.find_all do |plugin| - plugin.css_asset_exists?(target) - end.map do |plugin| - target.nil? ? plugin.directory_name : "#{plugin.directory_name}_#{target}" - end + assets += + plugins + .find_all { |plugin| plugin.css_asset_exists?(target) } + .map do |plugin| + target.nil? ? plugin.directory_name : "#{plugin.directory_name}_#{target}" + end end assets end def self.find_plugin_js_assets(args) - plugins = self.find_plugins(args).select do |plugin| - plugin.js_asset_exists? || plugin.extra_js_asset_exists? || plugin.admin_js_asset_exists? - end + plugins = + self + .find_plugins(args) + .select do |plugin| + plugin.js_asset_exists? || plugin.extra_js_asset_exists? || plugin.admin_js_asset_exists? + end plugins = apply_asset_filters(plugins, :js, args[:request]) @@ -413,25 +438,33 @@ module Discourse end def self.assets_digest - @assets_digest ||= begin - digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join) + @assets_digest ||= + begin + digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join) - channel = "/global/asset-version" - message = MessageBus.last_message(channel) + channel = "/global/asset-version" + message = MessageBus.last_message(channel) - unless message && message.data == digest - MessageBus.publish channel, digest + MessageBus.publish channel, digest unless message && message.data == digest + digest end - digest - end end BUILTIN_AUTH ||= [ - Auth::AuthProvider.new(authenticator: Auth::FacebookAuthenticator.new, frame_width: 580, frame_height: 400, icon: "fab-facebook"), - Auth::AuthProvider.new(authenticator: Auth::GoogleOAuth2Authenticator.new, frame_width: 850, frame_height: 500), # Custom icon implemented in client + Auth::AuthProvider.new( + authenticator: Auth::FacebookAuthenticator.new, + frame_width: 580, + frame_height: 400, + icon: "fab-facebook", + ), + Auth::AuthProvider.new( + authenticator: Auth::GoogleOAuth2Authenticator.new, + frame_width: 850, + frame_height: 500, + ), # Custom icon implemented in client Auth::AuthProvider.new(authenticator: Auth::GithubAuthenticator.new, icon: "fab-github"), Auth::AuthProvider.new(authenticator: Auth::TwitterAuthenticator.new, icon: "fab-twitter"), - Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord") + Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord"), ] def self.auth_providers @@ -439,7 +472,7 @@ module Discourse end def self.enabled_auth_providers - auth_providers.select { |provider| provider.authenticator.enabled? } + auth_providers.select { |provider| provider.authenticator.enabled? } end def self.authenticators @@ -449,17 +482,18 @@ module Discourse end def self.enabled_authenticators - authenticators.select { |authenticator| authenticator.enabled? } + authenticators.select { |authenticator| authenticator.enabled? } end def self.cache - @cache ||= begin - if GlobalSetting.skip_redis? - ActiveSupport::Cache::MemoryStore.new - else - Cache.new + @cache ||= + begin + if GlobalSetting.skip_redis? + ActiveSupport::Cache::MemoryStore.new + else + Cache.new + end end - end end # hostname of the server, operating system level @@ -467,15 +501,15 @@ module Discourse def self.os_hostname @os_hostname ||= begin - require 'socket' + require "socket" Socket.gethostname rescue => e - warn_exception(e, message: 'Socket.gethostname is not working') + warn_exception(e, message: "Socket.gethostname is not working") begin `hostname`.strip rescue => e - warn_exception(e, message: 'hostname command is not working') - 'unknown_host' + warn_exception(e, message: "hostname command is not working") + "unknown_host" end end end @@ -501,12 +535,12 @@ module Discourse def self.current_hostname_with_port default_port = SiteSetting.force_https? ? 443 : 80 result = +"#{current_hostname}" - result << ":#{SiteSetting.port}" if SiteSetting.port.to_i > 0 && SiteSetting.port.to_i != default_port - - if Rails.env.development? && SiteSetting.port.blank? - result << ":#{ENV["UNICORN_PORT"] || 3000}" + if SiteSetting.port.to_i > 0 && SiteSetting.port.to_i != default_port + result << ":#{SiteSetting.port}" end + result << ":#{ENV["UNICORN_PORT"] || 3000}" if Rails.env.development? && SiteSetting.port.blank? + result end @@ -520,16 +554,18 @@ module Discourse def self.route_for(uri) unless uri.is_a?(URI) - uri = begin - URI(uri) - rescue ArgumentError, URI::Error - end + uri = + begin + URI(uri) + rescue ArgumentError, URI::Error + end end return unless uri path = +(uri.path || "") - if !uri.host || (uri.host == Discourse.current_hostname && path.start_with?(Discourse.base_path)) + if !uri.host || + (uri.host == Discourse.current_hostname && path.start_with?(Discourse.base_path)) path.slice!(Discourse.base_path) return Rails.application.routes.recognize_path(path) end @@ -543,21 +579,21 @@ module Discourse alias_method :base_url_no_path, :base_url_no_prefix end - READONLY_MODE_KEY_TTL ||= 60 - READONLY_MODE_KEY ||= 'readonly_mode' - PG_READONLY_MODE_KEY ||= 'readonly_mode:postgres' - PG_READONLY_MODE_KEY_TTL ||= 300 - USER_READONLY_MODE_KEY ||= 'readonly_mode:user' - PG_FORCE_READONLY_MODE_KEY ||= 'readonly_mode:postgres_force' + READONLY_MODE_KEY_TTL ||= 60 + READONLY_MODE_KEY ||= "readonly_mode" + PG_READONLY_MODE_KEY ||= "readonly_mode:postgres" + PG_READONLY_MODE_KEY_TTL ||= 300 + USER_READONLY_MODE_KEY ||= "readonly_mode:user" + PG_FORCE_READONLY_MODE_KEY ||= "readonly_mode:postgres_force" # Psuedo readonly mode, where staff can still write - STAFF_WRITES_ONLY_MODE_KEY ||= 'readonly_mode:staff_writes_only' + STAFF_WRITES_ONLY_MODE_KEY ||= "readonly_mode:staff_writes_only" READONLY_KEYS ||= [ READONLY_MODE_KEY, PG_READONLY_MODE_KEY, USER_READONLY_MODE_KEY, - PG_FORCE_READONLY_MODE_KEY + PG_FORCE_READONLY_MODE_KEY, ] def self.enable_readonly_mode(key = READONLY_MODE_KEY) @@ -565,7 +601,9 @@ module Discourse Sidekiq.pause!("pg_failover") if !Sidekiq.paused? end - if [USER_READONLY_MODE_KEY, PG_FORCE_READONLY_MODE_KEY, STAFF_WRITES_ONLY_MODE_KEY].include?(key) + if [USER_READONLY_MODE_KEY, PG_FORCE_READONLY_MODE_KEY, STAFF_WRITES_ONLY_MODE_KEY].include?( + key, + ) Discourse.redis.set(key, 1) else ttl = @@ -595,15 +633,13 @@ module Discourse unless @threads[key]&.alive? @threads[key] = Thread.new do - while @dbs.size > 0 do + while @dbs.size > 0 sleep ttl / 2 @mutex.synchronize do @dbs.each do |db| RailsMultisite::ConnectionManagement.with_connection(db) do - if !Discourse.redis.expire(key, ttl) - @dbs.delete(db) - end + @dbs.delete(db) if !Discourse.redis.expire(key, ttl) end end end @@ -653,7 +689,7 @@ module Discourse # Shared between processes def self.postgres_last_read_only - @postgres_last_read_only ||= DistributedCache.new('postgres_last_read_only', namespace: false) + @postgres_last_read_only ||= DistributedCache.new("postgres_last_read_only", namespace: false) end # Per-process @@ -698,39 +734,43 @@ module Discourse # This is better than `MessageBus.publish "/file-change", ["refresh"]` because # it spreads the refreshes out over a time period if user_ids - MessageBus.publish("/refresh_client", 'clobber', user_ids: user_ids) + MessageBus.publish("/refresh_client", "clobber", user_ids: user_ids) else - MessageBus.publish('/global/asset-version', 'clobber') + MessageBus.publish("/global/asset-version", "clobber") end end def self.git_version - @git_version ||= begin - git_cmd = 'git rev-parse HEAD' - self.try_git(git_cmd, Discourse::VERSION::STRING) - end + @git_version ||= + begin + git_cmd = "git rev-parse HEAD" + self.try_git(git_cmd, Discourse::VERSION::STRING) + end end def self.git_branch - @git_branch ||= begin - git_cmd = 'git rev-parse --abbrev-ref HEAD' - self.try_git(git_cmd, 'unknown') - end + @git_branch ||= + begin + git_cmd = "git rev-parse --abbrev-ref HEAD" + self.try_git(git_cmd, "unknown") + end end def self.full_version - @full_version ||= begin - git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null' - self.try_git(git_cmd, 'unknown') - end + @full_version ||= + begin + git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null' + self.try_git(git_cmd, "unknown") + end end def self.last_commit_date - @last_commit_date ||= begin - git_cmd = 'git log -1 --format="%ct"' - seconds = self.try_git(git_cmd, nil) - seconds.nil? ? nil : DateTime.strptime(seconds, '%s') - end + @last_commit_date ||= + begin + git_cmd = 'git log -1 --format="%ct"' + seconds = self.try_git(git_cmd, nil) + seconds.nil? ? nil : DateTime.strptime(seconds, "%s") + end end def self.try_git(git_cmd, default_value) @@ -738,20 +778,21 @@ module Discourse begin version_value = `#{git_cmd}`.strip - rescue + rescue StandardError version_value = default_value end - if version_value.empty? - version_value = default_value - end + version_value = default_value if version_value.empty? version_value end # Either returns the site_contact_username user or the first admin. def self.site_contact_user - user = User.find_by(username_lower: SiteSetting.site_contact_username.downcase) if SiteSetting.site_contact_username.present? + user = + User.find_by( + username_lower: SiteSetting.site_contact_username.downcase, + ) if SiteSetting.site_contact_username.present? user ||= (system_user || User.admins.real.order(:id).first) end @@ -765,10 +806,10 @@ module Discourse def self.store if SiteSetting.Upload.enable_s3_uploads - @s3_store_loaded ||= require 'file_store/s3_store' + @s3_store_loaded ||= require "file_store/s3_store" FileStore::S3Store.new else - @local_store_loaded ||= require 'file_store/local_store' + @local_store_loaded ||= require "file_store/local_store" FileStore::LocalStore.new end end @@ -805,15 +846,15 @@ module Discourse Discourse.cache.reconnect Logster.store.redis.reconnect # shuts down all connections in the pool - Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! } + Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! } # re-establish Sidekiq.redis = sidekiq_redis_config # in case v8 was initialized we want to make sure it is nil PrettyText.reset_context - DiscourseJsProcessor::Transpiler.reset_context if defined? DiscourseJsProcessor::Transpiler - JsLocaleHelper.reset_context if defined? JsLocaleHelper + DiscourseJsProcessor::Transpiler.reset_context if defined?(DiscourseJsProcessor::Transpiler) + JsLocaleHelper.reset_context if defined?(JsLocaleHelper) # warm up v8 after fork, that way we do not fork a v8 context # it may cause issues if bg threads in a v8 isolate randomly stop @@ -831,7 +872,7 @@ module Discourse # you can use Discourse.warn when you want to report custom environment # with the error, this helps with grouping def self.warn(message, env = nil) - append = env ? (+" ") << env.map { |k, v|"#{k}: #{v}" }.join(" ") : "" + append = env ? (+" ") << env.map { |k, v| "#{k}: #{v}" }.join(" ") : "" if !(Logster::Logger === Rails.logger) Rails.logger.warn("#{message}#{append}") @@ -839,9 +880,7 @@ module Discourse end loggers = [Rails.logger] - if Rails.logger.chained - loggers.concat(Rails.logger.chained) - end + loggers.concat(Rails.logger.chained) if Rails.logger.chained logster_env = env @@ -849,9 +888,7 @@ module Discourse logster_env = Logster::Message.populate_from_env(old_env) # a bit awkward by try to keep the new params - env.each do |k, v| - logster_env[k] = v - end + env.each { |k, v| logster_env[k] = v } end loggers.each do |logger| @@ -860,12 +897,7 @@ module Discourse next end - logger.store.report( - ::Logger::Severity::WARN, - "discourse", - message, - env: logster_env - ) + logger.store.report(::Logger::Severity::WARN, "discourse", message, env: logster_env) end if old_env @@ -881,7 +913,6 @@ module Discourse # report a warning maintaining backtrack for logster def self.warn_exception(e, message: "", env: nil) if Rails.logger.respond_to? :add_with_opts - env ||= {} env[:current_db] ||= RailsMultisite::ConnectionManagement.current_db @@ -891,13 +922,13 @@ module Discourse "#{message} : #{e.class.name} : #{e}", "discourse-exception", backtrace: e.backtrace.join("\n"), - env: env + env: env, ) else # no logster ... fallback Rails.logger.warn("#{message} #{e}\n#{e.backtrace.join("\n")}") end - rescue + rescue StandardError STDERR.puts "Failed to report exception #{e} #{message}" end @@ -909,17 +940,11 @@ module Discourse warning << "\nAt #{location}" warning = warning.join(" ") - if raise_error - raise Deprecation.new(warning) - end + raise Deprecation.new(warning) if raise_error - if Rails.env == "development" - STDERR.puts(warning) - end + STDERR.puts(warning) if Rails.env == "development" - if output_in_test && Rails.env == "test" - STDERR.puts(warning) - end + STDERR.puts(warning) if output_in_test && Rails.env == "test" digest = Digest::MD5.hexdigest(warning) redis_key = "deprecate-notice-#{digest}" @@ -935,7 +960,7 @@ module Discourse warning end - SIDEKIQ_NAMESPACE ||= 'sidekiq' + SIDEKIQ_NAMESPACE ||= "sidekiq" def self.sidekiq_redis_config conf = GlobalSetting.redis_config.dup @@ -951,7 +976,8 @@ module Discourse def self.reset_active_record_cache_if_needed(e) last_cache_reset = Discourse.last_ar_cache_reset - if e && e.message =~ /UndefinedColumn/ && (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago) + if e && e.message =~ /UndefinedColumn/ && + (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago) Rails.logger.warn "Clearing Active Record cache, this can happen if schema changed while site is running or in a multisite various databases are running different schemas. Consider running rake multisite:migrate." Discourse.last_ar_cache_reset = Time.zone.now Discourse.reset_active_record_cache @@ -961,7 +987,11 @@ module Discourse def self.reset_active_record_cache ActiveRecord::Base.connection.query_cache.clear (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.reset_column_information rescue nil + begin + table.classify.constantize.reset_column_information + rescue StandardError + nil + end end nil end @@ -971,7 +1001,7 @@ module Discourse end def self.skip_post_deployment_migrations? - ['1', 'true'].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s) + %w[1 true].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s) end # this is used to preload as much stuff as possible prior to forking @@ -985,7 +1015,11 @@ module Discourse # load up all models and schema (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.first rescue nil + begin + table.classify.constantize.first + rescue StandardError + nil + end end # ensure we have a full schema cache in case we missed something above @@ -1024,29 +1058,27 @@ module Discourse end [ - Thread.new { + Thread.new do # router warm up - Rails.application.routes.recognize_path('abc') rescue nil - }, - Thread.new { + begin + Rails.application.routes.recognize_path("abc") + rescue StandardError + nil + end + end, + Thread.new do # preload discourse version Discourse.git_version Discourse.git_branch Discourse.full_version - }, - Thread.new { - require 'actionview_precompiler' + end, + Thread.new do + require "actionview_precompiler" ActionviewPrecompiler.precompile - }, - Thread.new { - LetterAvatar.image_magick_version - }, - Thread.new { - SvgSprite.core_svgs - }, - Thread.new { - EmberCli.script_chunks - } + end, + Thread.new { LetterAvatar.image_magick_version }, + Thread.new { SvgSprite.core_svgs }, + Thread.new { EmberCli.script_chunks }, ].each(&:join) ensure @preloaded_rails = true @@ -1055,10 +1087,10 @@ module Discourse mattr_accessor :redis def self.is_parallel_test? - ENV['RAILS_ENV'] == "test" && ENV['TEST_ENV_NUMBER'] + ENV["RAILS_ENV"] == "test" && ENV["TEST_ENV_NUMBER"] end - CDN_REQUEST_METHODS ||= ["GET", "HEAD", "OPTIONS"] + CDN_REQUEST_METHODS ||= %w[GET HEAD OPTIONS] def self.is_cdn_request?(env, request_method) return unless CDN_REQUEST_METHODS.include?(request_method) @@ -1071,8 +1103,8 @@ module Discourse end def self.apply_cdn_headers(headers) - headers['Access-Control-Allow-Origin'] = '*' - headers['Access-Control-Allow-Methods'] = CDN_REQUEST_METHODS.join(", ") + headers["Access-Control-Allow-Origin"] = "*" + headers["Access-Control-Allow-Methods"] = CDN_REQUEST_METHODS.join(", ") headers end @@ -1091,8 +1123,12 @@ module Discourse end def self.anonymous_locale(request) - locale = HttpLanguageParser.parse(request.cookies["locale"]) if SiteSetting.set_locale_from_cookie - locale ||= HttpLanguageParser.parse(request.env["HTTP_ACCEPT_LANGUAGE"]) if SiteSetting.set_locale_from_accept_language_header + locale = + HttpLanguageParser.parse(request.cookies["locale"]) if SiteSetting.set_locale_from_cookie + locale ||= + HttpLanguageParser.parse( + request.env["HTTP_ACCEPT_LANGUAGE"], + ) if SiteSetting.set_locale_from_accept_language_header locale end end diff --git a/lib/discourse_connect_base.rb b/lib/discourse_connect_base.rb index 3a503ddcbe3..b5e04d8f212 100644 --- a/lib/discourse_connect_base.rb +++ b/lib/discourse_connect_base.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true class DiscourseConnectBase + class ParseError < RuntimeError + end - class ParseError < RuntimeError; end - - ACCESSORS = %i{ + ACCESSORS = %i[ add_groups - admin moderator + admin + moderator avatar_force_update avatar_url bio @@ -31,11 +32,11 @@ class DiscourseConnectBase title username website - } + ] FIXNUMS = [] - BOOLS = %i{ + BOOLS = %i[ admin avatar_force_update confirmed_2fa @@ -46,7 +47,7 @@ class DiscourseConnectBase require_2fa require_activation suppress_welcome_message - } + ] def self.nonce_expiry_time @nonce_expiry_time ||= 10.minutes @@ -80,9 +81,11 @@ class DiscourseConnectBase decoded_hash = Rack::Utils.parse_query(decoded) if sso.sign(parsed["sso"]) != parsed["sig"] - diags = "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" - if parsed["sso"] =~ /[^a-zA-Z0-9=\r\n\/+]/m - raise ParseError, "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}" + diags = + "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" + if parsed["sso"] =~ %r{[^a-zA-Z0-9=\r\n/+]}m + raise ParseError, + "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}" else raise ParseError, "Bad signature for payload #{diags}" end @@ -91,9 +94,7 @@ class DiscourseConnectBase ACCESSORS.each do |k| val = decoded_hash[k.to_s] val = val.to_i if FIXNUMS.include? k - if BOOLS.include? k - val = ["true", "false"].include?(val) ? val == "true" : nil - end + val = %w[true false].include?(val) ? val == "true" : nil if BOOLS.include? k sso.public_send("#{k}=", val) end @@ -137,12 +138,12 @@ class DiscourseConnectBase def to_url(base_url = nil) base = "#{base_url || sso_url}" - "#{base}#{base.include?('?') ? '&' : '?'}#{payload}" + "#{base}#{base.include?("?") ? "&" : "?"}#{payload}" end def payload(secret = nil) payload = Base64.strict_encode64(unsigned_payload) - "sso=#{CGI::escape(payload)}&sig=#{sign(payload, secret)}" + "sso=#{CGI.escape(payload)}&sig=#{sign(payload, secret)}" end def unsigned_payload @@ -157,9 +158,7 @@ class DiscourseConnectBase payload[k] = val end - @custom_fields&.each do |k, v| - payload["custom.#{k}"] = v.to_s - end + @custom_fields&.each { |k, v| payload["custom.#{k}"] = v.to_s } payload end diff --git a/lib/discourse_connect_provider.rb b/lib/discourse_connect_provider.rb index 218d081b10a..76f46989df7 100644 --- a/lib/discourse_connect_provider.rb +++ b/lib/discourse_connect_provider.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class DiscourseConnectProvider < DiscourseConnectBase - class BlankSecret < RuntimeError; end - class BlankReturnUrl < RuntimeError; end + class BlankSecret < RuntimeError + end + class BlankReturnUrl < RuntimeError + end def self.parse(payload, sso_secret = nil, **init_kwargs) parsed_payload = Rack::Utils.parse_query(payload) @@ -15,11 +17,16 @@ class DiscourseConnectProvider < DiscourseConnectBase if sso_secret.blank? begin host = URI.parse(return_sso_url).host - Rails.logger.warn("SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings") + Rails.logger.warn( + "SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings", + ) rescue StandardError => e # going for StandardError cause URI::Error may not be enough, eg it parses to something not # responding to host - Discourse.warn_exception(e, message: "SSO failed; invalid or missing return_sso_url in SSO payload") + Discourse.warn_exception( + e, + message: "SSO failed; invalid or missing return_sso_url in SSO payload", + ) end raise BlankSecret @@ -31,7 +38,7 @@ class DiscourseConnectProvider < DiscourseConnectBase def self.lookup_return_sso_url(parsed_payload) decoded = Base64.decode64(parsed_payload["sso"]) decoded_hash = Rack::Utils.parse_query(decoded) - decoded_hash['return_sso_url'] + decoded_hash["return_sso_url"] end def self.lookup_sso_secret(return_sso_url, parsed_payload) @@ -39,21 +46,23 @@ class DiscourseConnectProvider < DiscourseConnectBase return_url_host = URI.parse(return_sso_url).host - provider_secrets = SiteSetting - .discourse_connect_provider_secrets - .split("\n") - .map { |row| row.split("|", 2) } - .sort_by { |k, _| k } - .reverse + provider_secrets = + SiteSetting + .discourse_connect_provider_secrets + .split("\n") + .map { |row| row.split("|", 2) } + .sort_by { |k, _| k } + .reverse first_domain_match = nil - pair = provider_secrets.find do |domain, configured_secret| - if WildcardDomainChecker.check_domain(domain, return_url_host) - first_domain_match ||= configured_secret - sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"] + pair = + provider_secrets.find do |domain, configured_secret| + if WildcardDomainChecker.check_domain(domain, return_url_host) + first_domain_match ||= configured_secret + sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"] + end end - end # falls back to a secret which will fail to validate in DiscourseConnectBase # this ensures error flow is correct diff --git a/lib/discourse_dev/category.rb b/lib/discourse_dev/category.rb index b13c43d7587..7c1fe2500c6 100644 --- a/lib/discourse_dev/category.rb +++ b/lib/discourse_dev/category.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Category < Record - def initialize super(::Category, DiscourseDev.config.category[:count]) @parent_category_ids = ::Category.where(parent_category_id: nil).pluck(:id) @@ -29,7 +28,7 @@ module DiscourseDev description: Faker::Lorem.paragraph, user_id: ::Discourse::SYSTEM_USER_ID, color: Faker::Color.hex_color.last(6), - parent_category_id: parent_category_id + parent_category_id: parent_category_id, } end diff --git a/lib/discourse_dev/config.rb b/lib/discourse_dev/config.rb index eb4a1d160bd..f83eebe880e 100644 --- a/lib/discourse_dev/config.rb +++ b/lib/discourse_dev/config.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'rails' -require 'highline/import' +require "rails" +require "highline/import" module DiscourseDev class Config @@ -63,10 +63,11 @@ module DiscourseDev if settings.present? email = settings[:email] || "new_user@example.com" - new_user = ::User.create!( - email: email, - username: settings[:username] || UserNameSuggester.suggest(email) - ) + new_user = + ::User.create!( + email: email, + username: settings[:username] || UserNameSuggester.suggest(email), + ) new_user.email_tokens.update_all confirmed: true new_user.activate end @@ -88,15 +89,14 @@ module DiscourseDev def create_admin_user_from_settings(settings) email = settings[:email] - admin = ::User.with_email(email).first_or_create!( - email: email, - username: settings[:username] || UserNameSuggester.suggest(email), - password: settings[:password] - ) + admin = + ::User.with_email(email).first_or_create!( + email: email, + username: settings[:username] || UserNameSuggester.suggest(email), + password: settings[:password], + ) admin.grant_admin! - if admin.trust_level < 1 - admin.change_trust_level!(1) - end + admin.change_trust_level!(1) if admin.trust_level < 1 admin.email_tokens.update_all confirmed: true admin.activate end @@ -107,10 +107,7 @@ module DiscourseDev password = ask("Password (optional, press ENTER to skip): ") username = UserNameSuggester.suggest(email) - admin = ::User.new( - email: email, - username: username - ) + admin = ::User.new(email: email, username: username) if password.present? admin.password = password @@ -122,7 +119,7 @@ module DiscourseDev saved = admin.save if saved - File.open(file_path, 'a') do | file| + File.open(file_path, "a") do |file| file.puts("admin:") file.puts(" username: #{admin.username}") file.puts(" email: #{admin.email}") @@ -137,9 +134,7 @@ module DiscourseDev admin.save admin.grant_admin! - if admin.trust_level < 1 - admin.change_trust_level!(1) - end + admin.change_trust_level!(1) if admin.trust_level < 1 admin.email_tokens.update_all confirmed: true admin.activate diff --git a/lib/discourse_dev/group.rb b/lib/discourse_dev/group.rb index c346b0c5faa..91a0ffcb7e1 100644 --- a/lib/discourse_dev/group.rb +++ b/lib/discourse_dev/group.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Group < Record - def initialize super(::Group, DiscourseDev.config.group[:count]) end diff --git a/lib/discourse_dev/post.rb b/lib/discourse_dev/post.rb index ef0e072daa4..350696a7d8d 100644 --- a/lib/discourse_dev/post.rb +++ b/lib/discourse_dev/post.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class Post < Record - attr_reader :topic def initialize(topic, count) @@ -28,7 +27,7 @@ module DiscourseDev raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), created_at: Faker::Time.between(from: topic.last_posted_at, to: DateTime.now), skip_validations: true, - skip_guardian: true + skip_guardian: true, } end @@ -44,13 +43,20 @@ module DiscourseDev def generate_likes(post) user_ids = [post.user_id] - Faker::Number.between(from: 0, to: @max_likes_count).times do - user = self.user - next if user_ids.include?(user.id) + Faker::Number + .between(from: 0, to: @max_likes_count) + .times do + user = self.user + next if user_ids.include?(user.id) - PostActionCreator.new(user, post, PostActionType.types[:like], created_at: Faker::Time.between(from: post.created_at, to: DateTime.now)).perform - user_ids << user.id - end + PostActionCreator.new( + user, + post, + PostActionType.types[:like], + created_at: Faker::Time.between(from: post.created_at, to: DateTime.now), + ).perform + user_ids << user.id + end end def user @@ -90,13 +96,14 @@ module DiscourseDev count.times do |i| begin user = User.random - reply = Faker::DiscourseMarkdown.with_user(user.id) do - { - topic_id: topic.id, - raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), - skip_validations: true - } - end + reply = + Faker::DiscourseMarkdown.with_user(user.id) do + { + topic_id: topic.id, + raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), + skip_validations: true, + } + end PostCreator.new(user, reply).create! rescue ActiveRecord::RecordNotSaved => e puts e @@ -109,6 +116,5 @@ module DiscourseDev def self.random super(::Post) end - end end diff --git a/lib/discourse_dev/post_revision.rb b/lib/discourse_dev/post_revision.rb index 7c1f1da644c..0c282e9540c 100644 --- a/lib/discourse_dev/post_revision.rb +++ b/lib/discourse_dev/post_revision.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class PostRevision < Record - def initialize super(::PostRevision, DiscourseDev.config.post_revisions[:count]) end diff --git a/lib/discourse_dev/record.rb b/lib/discourse_dev/record.rb index 3e2767ef62c..b1b84097df2 100644 --- a/lib/discourse_dev/record.rb +++ b/lib/discourse_dev/record.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'discourse_dev' -require 'rails' -require 'faker' +require "discourse_dev" +require "rails" +require "faker" module DiscourseDev class Record @@ -12,11 +12,12 @@ module DiscourseDev attr_reader :model, :type def initialize(model, count = DEFAULT_COUNT) - @@initialized ||= begin - Faker::Discourse.unique.clear - RateLimiter.disable - true - end + @@initialized ||= + begin + Faker::Discourse.unique.clear + RateLimiter.disable + true + end @model = model @type = model.to_s.downcase.to_sym @@ -40,11 +41,9 @@ module DiscourseDev if current_count >= @count puts "Already have #{current_count} #{type} records" - Rake.application.top_level_tasks.each do |task_name| - Rake::Task[task_name].reenable - end + Rake.application.top_level_tasks.each { |task_name| Rake::Task[task_name].reenable } - Rake::Task['dev:repopulate'].invoke + Rake::Task["dev:repopulate"].invoke return elsif current_count > 0 @count -= current_count @@ -74,7 +73,9 @@ module DiscourseDev end def self.random(model, use_existing_records: true) - model.joins(:_custom_fields).where("#{:type}_custom_fields.name = '#{AUTO_POPULATED}'") if !use_existing_records && model.new.respond_to?(:custom_fields) + if !use_existing_records && model.new.respond_to?(:custom_fields) + model.joins(:_custom_fields).where("#{:type}_custom_fields.name = '#{AUTO_POPULATED}'") + end count = model.count raise "#{:type} records are not yet populated" if count == 0 diff --git a/lib/discourse_dev/tag.rb b/lib/discourse_dev/tag.rb index 987d8656e9c..96c06ad6e1d 100644 --- a/lib/discourse_dev/tag.rb +++ b/lib/discourse_dev/tag.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Tag < Record - def initialize super(::Tag, DiscourseDev.config.tag[:count]) end @@ -24,9 +23,7 @@ module DiscourseDev end def data - { - name: Faker::Discourse.unique.tag, - } + { name: Faker::Discourse.unique.tag } end end end diff --git a/lib/discourse_dev/topic.rb b/lib/discourse_dev/topic.rb index c08af628ebd..9c6baaafd50 100644 --- a/lib/discourse_dev/topic.rb +++ b/lib/discourse_dev/topic.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class Topic < Record - def initialize(private_messages: false, recipient: nil, ignore_current_count: false) @settings = DiscourseDev.config.topic @private_messages = private_messages @@ -33,15 +32,9 @@ module DiscourseDev end if @category - merge_attributes = { - category: @category.id, - tags: tags - } + merge_attributes = { category: @category.id, tags: tags } else - merge_attributes = { - archetype: "private_message", - target_usernames: [@recipient] - } + merge_attributes = { archetype: "private_message", target_usernames: [@recipient] } end { @@ -51,9 +44,11 @@ module DiscourseDev topic_opts: { import_mode: true, views: Faker::Number.between(from: 1, to: max_views), - custom_fields: { dev_sample: true } + custom_fields: { + dev_sample: true, + }, }, - skip_validations: true + skip_validations: true, }.merge(merge_attributes) end @@ -61,7 +56,10 @@ module DiscourseDev if current_count < I18n.t("faker.discourse.topics").count Faker::Discourse.unique.topic else - Faker::Lorem.unique.sentence(word_count: 5, supplemental: true, random_words_to_add: 4).chomp(".") + Faker::Lorem + .unique + .sentence(word_count: 5, supplemental: true, random_words_to_add: 4) + .chomp(".") end end @@ -70,9 +68,9 @@ module DiscourseDev @tags = [] - Faker::Number.between(from: @settings.dig(:tags, :min), to: @settings.dig(:tags, :max)).times do - @tags << Faker::Discourse.tag - end + Faker::Number + .between(from: @settings.dig(:tags, :min), to: @settings.dig(:tags, :max)) + .times { @tags << Faker::Discourse.tag } @tags.uniq end @@ -92,7 +90,11 @@ module DiscourseDev if override = @settings.dig(:replies, :overrides).find { |o| o[:title] == topic_data[:title] } reply_count = override[:count] else - reply_count = Faker::Number.between(from: @settings.dig(:replies, :min), to: @settings.dig(:replies, :max)) + reply_count = + Faker::Number.between( + from: @settings.dig(:replies, :min), + to: @settings.dig(:replies, :max), + ) end topic = post.topic @@ -123,9 +125,7 @@ module DiscourseDev end def delete_unwanted_sidekiq_jobs - Sidekiq::ScheduledSet.new.each do |job| - job.delete if job.item["class"] == "Jobs::UserEmail" - end + Sidekiq::ScheduledSet.new.each { |job| job.delete if job.item["class"] == "Jobs::UserEmail" } end end end diff --git a/lib/discourse_diff.rb b/lib/discourse_diff.rb index 3887abe134c..e31a699f85a 100644 --- a/lib/discourse_diff.rb +++ b/lib/discourse_diff.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DiscourseDiff - MAX_DIFFERENCE = 200 def initialize(before, after) @@ -9,8 +8,8 @@ class DiscourseDiff @after = after before_html = tokenize_html_blocks(@before) after_html = tokenize_html_blocks(@after) - before_markdown = tokenize_line(CGI::escapeHTML(@before)) - after_markdown = tokenize_line(CGI::escapeHTML(@after)) + before_markdown = tokenize_line(CGI.escapeHTML(@before)) + after_markdown = tokenize_line(CGI.escapeHTML(@after)) @block_by_block_diff = ONPDiff.new(before_html, after_html).paragraph_diff @line_by_line_diff = ONPDiff.new(before_markdown, after_markdown).short_diff @@ -21,7 +20,8 @@ class DiscourseDiff inline = [] while i < @block_by_block_diff.size op_code = @block_by_block_diff[i][1] - if op_code == :common then inline << @block_by_block_diff[i][0] + if op_code == :common + inline << @block_by_block_diff[i][0] else if op_code == :delete opposite_op_code = :add @@ -36,7 +36,11 @@ class DiscourseDiff end if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code - diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff + diff = + ONPDiff.new( + tokenize_html(@block_by_block_diff[first][0]), + tokenize_html(@block_by_block_diff[second][0]), + ).diff inline << generate_inline_html(diff) i += 1 else @@ -73,7 +77,11 @@ class DiscourseDiff end if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code - diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff + diff = + ONPDiff.new( + tokenize_html(@block_by_block_diff[first][0]), + tokenize_html(@block_by_block_diff[second][0]), + ).diff deleted, inserted = generate_side_by_side_html(diff) left << deleted right << inserted @@ -109,9 +117,13 @@ class DiscourseDiff end if i + 1 < @line_by_line_diff.size && @line_by_line_diff[i + 1][1] == opposite_op_code - before_tokens, after_tokens = tokenize_markdown(@line_by_line_diff[first][0]), tokenize_markdown(@line_by_line_diff[second][0]) + before_tokens, after_tokens = + tokenize_markdown(@line_by_line_diff[first][0]), + tokenize_markdown(@line_by_line_diff[second][0]) if (before_tokens.size - after_tokens.size).abs > MAX_DIFFERENCE - before_tokens, after_tokens = tokenize_line(@line_by_line_diff[first][0]), tokenize_line(@line_by_line_diff[second][0]) + before_tokens, after_tokens = + tokenize_line(@line_by_line_diff[first][0]), + tokenize_line(@line_by_line_diff[second][0]) end diff = ONPDiff.new(before_tokens, after_tokens).short_diff deleted, inserted = generate_side_by_side_markdown(diff) @@ -178,7 +190,7 @@ class DiscourseDiff def add_class_or_wrap_in_tags(html_or_text, klass) result = html_or_text.dup index_of_next_chevron = result.index(">") - if result.size > 0 && result[0] == '<' && index_of_next_chevron + if result.size > 0 && result[0] == "<" && index_of_next_chevron index_of_class = result.index("class=") if index_of_class.nil? || index_of_class > index_of_next_chevron # we do not have a class for the current tag @@ -202,9 +214,12 @@ class DiscourseDiff inline = [] diff.each do |d| case d[1] - when :common then inline << d[0] - when :delete then inline << add_class_or_wrap_in_tags(d[0], "del") - when :add then inline << add_class_or_wrap_in_tags(d[0], "ins") + when :common + inline << d[0] + when :delete + inline << add_class_or_wrap_in_tags(d[0], "del") + when :add + inline << add_class_or_wrap_in_tags(d[0], "ins") end end inline @@ -217,8 +232,10 @@ class DiscourseDiff when :common deleted << d[0] inserted << d[0] - when :delete then deleted << add_class_or_wrap_in_tags(d[0], "del") - when :add then inserted << add_class_or_wrap_in_tags(d[0], "ins") + when :delete + deleted << add_class_or_wrap_in_tags(d[0], "del") + when :add + inserted << add_class_or_wrap_in_tags(d[0], "ins") end end [deleted, inserted] @@ -231,15 +248,16 @@ class DiscourseDiff when :common deleted << d[0] inserted << d[0] - when :delete then deleted << "#{d[0]}" - when :add then inserted << "#{d[0]}" + when :delete + deleted << "#{d[0]}" + when :add + inserted << "#{d[0]}" end end [deleted, inserted] end class HtmlTokenizer < Nokogiri::XML::SAX::Document - attr_accessor :tokens def initialize @@ -253,23 +271,21 @@ class DiscourseDiff me.tokens end - USELESS_TAGS = %w{html body} + USELESS_TAGS = %w[html body] def start_element(name, attributes = []) return if USELESS_TAGS.include?(name) - attrs = attributes.map { |a| " #{a[0]}=\"#{CGI::escapeHTML(a[1])}\"" }.join + attrs = attributes.map { |a| " #{a[0]}=\"#{CGI.escapeHTML(a[1])}\"" }.join @tokens << "<#{name}#{attrs}>" end - AUTOCLOSING_TAGS = %w{area base br col embed hr img input meta} + AUTOCLOSING_TAGS = %w[area base br col embed hr img input meta] def end_element(name) return if USELESS_TAGS.include?(name) || AUTOCLOSING_TAGS.include?(name) @tokens << "" end def characters(string) - @tokens.concat string.scan(/\W|\w+[ \t]*/).map { |x| CGI::escapeHTML(x) } + @tokens.concat string.scan(/\W|\w+[ \t]*/).map { |x| CGI.escapeHTML(x) } end - end - end diff --git a/lib/discourse_event.rb b/lib/discourse_event.rb index 4d7f30e27a1..43c85487651 100644 --- a/lib/discourse_event.rb +++ b/lib/discourse_event.rb @@ -3,21 +3,23 @@ # This is meant to be used by plugins to trigger and listen to events # So we can execute code when things happen. class DiscourseEvent - # Defaults to a hash where default values are empty sets. def self.events @events ||= Hash.new { |hash, key| hash[key] = Set.new } end def self.trigger(event_name, *args, **kwargs) - events[event_name].each do |event| - event.call(*args, **kwargs) - end + events[event_name].each { |event| event.call(*args, **kwargs) } end def self.on(event_name, &block) if event_name == :site_setting_saved - Discourse.deprecate("The :site_setting_saved event is deprecated. Please use :site_setting_changed instead", since: "2.3.0beta8", drop_from: "2.4", raise_error: true) + Discourse.deprecate( + "The :site_setting_saved event is deprecated. Please use :site_setting_changed instead", + since: "2.3.0beta8", + drop_from: "2.4", + raise_error: true, + ) end events[event_name] << block end diff --git a/lib/discourse_hub.rb b/lib/discourse_hub.rb index 31d4e8a2f0a..4b5b36d4811 100644 --- a/lib/discourse_hub.rb +++ b/lib/discourse_hub.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true module DiscourseHub - STATS_FETCHED_AT_KEY = "stats_fetched_at" def self.version_check_payload - default_payload = { installed_version: Discourse::VERSION::STRING }.merge!(Discourse.git_branch == "unknown" ? {} : { branch: Discourse.git_branch }) + default_payload = { installed_version: Discourse::VERSION::STRING }.merge!( + Discourse.git_branch == "unknown" ? {} : { branch: Discourse.git_branch }, + ) default_payload.merge!(get_payload) end def self.discourse_version_check - get('/version_check', version_check_payload) + get("/version_check", version_check_payload) end def self.stats_fetched_at=(time_with_zone) @@ -18,7 +19,11 @@ module DiscourseHub end def self.get_payload - SiteSetting.share_anonymized_statistics && stats_fetched_at < 7.days.ago ? About.fetch_cached_stats.symbolize_keys : {} + if SiteSetting.share_anonymized_statistics && stats_fetched_at < 7.days.ago + About.fetch_cached_stats.symbolize_keys + else + {} + end end def self.get(rel_url, params = {}) @@ -40,27 +45,39 @@ module DiscourseHub def self.singular_action(action, rel_url, params = {}) connect_opts = connect_opts(params) - JSON.parse(Excon.public_send(action, - "#{hub_base_url}#{rel_url}", - { - headers: { 'Referer' => referer, 'Accept' => accepts.join(', ') }, - query: params, - omit_default_port: true - }.merge(connect_opts) - ).body) + JSON.parse( + Excon.public_send( + action, + "#{hub_base_url}#{rel_url}", + { + headers: { + "Referer" => referer, + "Accept" => accepts.join(", "), + }, + query: params, + omit_default_port: true, + }.merge(connect_opts), + ).body, + ) end def self.collection_action(action, rel_url, params = {}) connect_opts = connect_opts(params) - response = Excon.public_send(action, - "#{hub_base_url}#{rel_url}", - { - body: JSON[params], - headers: { 'Referer' => referer, 'Accept' => accepts.join(', '), "Content-Type" => "application/json" }, - omit_default_port: true - }.merge(connect_opts) - ) + response = + Excon.public_send( + action, + "#{hub_base_url}#{rel_url}", + { + body: JSON[params], + headers: { + "Referer" => referer, + "Accept" => accepts.join(", "), + "Content-Type" => "application/json", + }, + omit_default_port: true, + }.merge(connect_opts), + ) if (status = response.status) != 200 Rails.logger.warn(response_status_log_message(rel_url, status)) @@ -87,14 +104,14 @@ module DiscourseHub def self.hub_base_url if Rails.env.production? - ENV['HUB_BASE_URL'] || 'https://api.discourse.org/api' + ENV["HUB_BASE_URL"] || "https://api.discourse.org/api" else - ENV['HUB_BASE_URL'] || 'http://local.hub:3000/api' + ENV["HUB_BASE_URL"] || "http://local.hub:3000/api" end end def self.accepts - ['application/json', 'application/vnd.discoursehub.v1'] + %w[application/json application/vnd.discoursehub.v1] end def self.referer @@ -105,5 +122,4 @@ module DiscourseHub t = Discourse.redis.get(STATS_FETCHED_AT_KEY) t ? Time.zone.at(t.to_i) : 1.year.ago end - end diff --git a/lib/discourse_ip_info.rb b/lib/discourse_ip_info.rb index 7f7116c47da..2949f5858cb 100644 --- a/lib/discourse_ip_info.rb +++ b/lib/discourse_ip_info.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'maxminddb' -require 'resolv' +require "maxminddb" +require "resolv" class DiscourseIpInfo include Singleton @@ -11,13 +11,13 @@ class DiscourseIpInfo end def open_db(path) - @loc_mmdb = mmdb_load(File.join(path, 'GeoLite2-City.mmdb')) - @asn_mmdb = mmdb_load(File.join(path, 'GeoLite2-ASN.mmdb')) + @loc_mmdb = mmdb_load(File.join(path, "GeoLite2-City.mmdb")) + @asn_mmdb = mmdb_load(File.join(path, "GeoLite2-ASN.mmdb")) @cache = LruRedux::ThreadSafeCache.new(2000) end def self.path - @path ||= File.join(Rails.root, 'vendor', 'data') + @path ||= File.join(Rails.root, "vendor", "data") end def self.mmdb_path(name) @@ -25,7 +25,6 @@ class DiscourseIpInfo end def self.mmdb_download(name) - if GlobalSetting.maxmind_license_key.blank? STDERR.puts "MaxMind IP database updates require a license" STDERR.puts "Please set DISCOURSE_MAXMIND_LICENSE_KEY to one you generated at https://www.maxmind.com" @@ -34,41 +33,29 @@ class DiscourseIpInfo FileUtils.mkdir_p(path) - url = "https://download.maxmind.com/app/geoip_download?license_key=#{GlobalSetting.maxmind_license_key}&edition_id=#{name}&suffix=tar.gz" + url = + "https://download.maxmind.com/app/geoip_download?license_key=#{GlobalSetting.maxmind_license_key}&edition_id=#{name}&suffix=tar.gz" - gz_file = FileHelper.download( - url, - max_file_size: 100.megabytes, - tmp_file_name: "#{name}.gz", - validate_uri: false, - follow_redirect: false - ) + gz_file = + FileHelper.download( + url, + max_file_size: 100.megabytes, + tmp_file_name: "#{name}.gz", + validate_uri: false, + follow_redirect: false, + ) filename = File.basename(gz_file.path) dir = "#{Dir.tmpdir}/#{SecureRandom.hex}" - Discourse::Utils.execute_command( - "mkdir", "-p", dir - ) + Discourse::Utils.execute_command("mkdir", "-p", dir) - Discourse::Utils.execute_command( - "cp", - gz_file.path, - "#{dir}/#{filename}" - ) + Discourse::Utils.execute_command("cp", gz_file.path, "#{dir}/#{filename}") - Discourse::Utils.execute_command( - "tar", - "-xzvf", - "#{dir}/#{filename}", - chdir: dir - ) - - Dir["#{dir}/**/*.mmdb"].each do |f| - FileUtils.mv(f, mmdb_path(name)) - end + Discourse::Utils.execute_command("tar", "-xzvf", "#{dir}/#{filename}", chdir: dir) + Dir["#{dir}/**/*.mmdb"].each { |f| FileUtils.mv(f, mmdb_path(name)) } ensure FileUtils.rm_r(dir, force: true) if dir gz_file&.close! @@ -96,7 +83,8 @@ class DiscourseIpInfo if result&.found? ret[:country] = result.country.name(locale) || result.country.name ret[:country_code] = result.country.iso_code - ret[:region] = result.subdivisions.most_specific.name(locale) || result.subdivisions.most_specific.name + ret[:region] = result.subdivisions.most_specific.name(locale) || + result.subdivisions.most_specific.name ret[:city] = result.city.name(locale) || result.city.name ret[:latitude] = result.location.latitude ret[:longitude] = result.location.longitude @@ -104,13 +92,18 @@ class DiscourseIpInfo # used by plugins or API to locate users more accurately ret[:geoname_ids] = [ - result.continent.geoname_id, result.country.geoname_id, result.city.geoname_id, - *result.subdivisions.map(&:geoname_id) + result.continent.geoname_id, + result.country.geoname_id, + result.city.geoname_id, + *result.subdivisions.map(&:geoname_id), ] ret[:geoname_ids].compact! end rescue => e - Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.") + Discourse.warn_exception( + e, + message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.", + ) end end @@ -123,7 +116,10 @@ class DiscourseIpInfo ret[:organization] = result["autonomous_system_organization"] end rescue => e - Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-ASN database.") + Discourse.warn_exception( + e, + message: "IP #{ip} could not be looked up in MaxMind GeoLite2-ASN database.", + ) end end @@ -142,10 +138,13 @@ class DiscourseIpInfo def get(ip, locale: :en, resolve_hostname: false) ip = ip.to_s - locale = locale.to_s.sub('_', '-') + locale = locale.to_s.sub("_", "-") - @cache["#{ip}-#{locale}-#{resolve_hostname}"] ||= - lookup(ip, locale: locale, resolve_hostname: resolve_hostname) + @cache["#{ip}-#{locale}-#{resolve_hostname}"] ||= lookup( + ip, + locale: locale, + resolve_hostname: resolve_hostname, + ) end def self.open_db(path) diff --git a/lib/discourse_js_processor.rb b/lib/discourse_js_processor.rb index 11f1ee27672..ae167ffe57f 100644 --- a/lib/discourse_js_processor.rb +++ b/lib/discourse_js_processor.rb @@ -1,27 +1,28 @@ # frozen_string_literal: true -require 'execjs' -require 'mini_racer' +require "execjs" +require "mini_racer" class DiscourseJsProcessor - class TranspileError < StandardError; end + class TranspileError < StandardError + end DISCOURSE_COMMON_BABEL_PLUGINS = [ - 'proposal-optional-chaining', - ['proposal-decorators', { legacy: true } ], - 'transform-template-literals', - 'proposal-class-properties', - 'proposal-class-static-block', - 'proposal-private-property-in-object', - 'proposal-private-methods', - 'proposal-numeric-separator', - 'proposal-logical-assignment-operators', - 'proposal-nullish-coalescing-operator', - 'proposal-json-strings', - 'proposal-optional-catch-binding', - 'transform-parameters', - 'proposal-async-generator-functions', - 'proposal-object-rest-spread', - 'proposal-export-namespace-from', + "proposal-optional-chaining", + ["proposal-decorators", { legacy: true }], + "transform-template-literals", + "proposal-class-properties", + "proposal-class-static-block", + "proposal-private-property-in-object", + "proposal-private-methods", + "proposal-numeric-separator", + "proposal-logical-assignment-operators", + "proposal-nullish-coalescing-operator", + "proposal-json-strings", + "proposal-optional-catch-binding", + "transform-parameters", + "proposal-async-generator-functions", + "proposal-object-rest-spread", + "proposal-export-namespace-from", ] def self.plugin_transpile_paths @@ -33,22 +34,22 @@ class DiscourseJsProcessor end def self.call(input) - root_path = input[:load_path] || '' - logical_path = (input[:filename] || '').sub(root_path, '').gsub(/\.(js|es6).*$/, '').sub(/^\//, '') + root_path = input[:load_path] || "" + logical_path = + (input[:filename] || "").sub(root_path, "").gsub(/\.(js|es6).*$/, "").sub(%r{^/}, "") data = input[:data] - if should_transpile?(input[:filename]) - data = transpile(data, root_path, logical_path) - end + data = transpile(data, root_path, logical_path) if should_transpile?(input[:filename]) # add sourceURL until we can do proper source maps if !Rails.env.production? && !ember_cli?(input[:filename]) - plugin_name = root_path[/\/plugins\/([\w-]+)\/assets/, 1] - source_url = if plugin_name - "plugins/#{plugin_name}/assets/javascripts/#{logical_path}" - else - logical_path - end + plugin_name = root_path[%r{/plugins/([\w-]+)/assets}, 1] + source_url = + if plugin_name + "plugins/#{plugin_name}/assets/javascripts/#{logical_path}" + else + logical_path + end data = "eval(#{data.inspect} + \"\\n//# sourceURL=#{source_url}\");\n" end @@ -62,7 +63,7 @@ class DiscourseJsProcessor end def self.should_transpile?(filename) - filename ||= '' + filename ||= "" # skip ember cli return false if ember_cli?(filename) @@ -73,7 +74,7 @@ class DiscourseJsProcessor # For .js check the path... return false unless filename.end_with?(".js") || filename.end_with?(".js.erb") - relative_path = filename.sub(Rails.root.to_s, '').sub(/^\/*/, '') + relative_path = filename.sub(Rails.root.to_s, "").sub(%r{^/*}, "") js_root = "app/assets/javascripts" test_root = "test/javascripts" @@ -81,26 +82,27 @@ class DiscourseJsProcessor return false if relative_path.start_with?("#{js_root}/locales/") return false if relative_path.start_with?("#{js_root}/plugins/") - return true if %w( - start-discourse - onpopstate-handler - google-tag-manager - google-universal-analytics-v3 - google-universal-analytics-v4 - activate-account - auto-redirect - embed-application - app-boot - ).any? { |f| relative_path == "#{js_root}/#{f}.js" } + if %w[ + start-discourse + onpopstate-handler + google-tag-manager + google-universal-analytics-v3 + google-universal-analytics-v4 + activate-account + auto-redirect + embed-application + app-boot + ].any? { |f| relative_path == "#{js_root}/#{f}.js" } + return true + end return true if plugin_transpile_paths.any? { |prefix| relative_path.start_with?(prefix) } - !!(relative_path =~ /^#{js_root}\/[^\/]+\// || - relative_path =~ /^#{test_root}\/[^\/]+\//) + !!(relative_path =~ %r{^#{js_root}/[^/]+/} || relative_path =~ %r{^#{test_root}/[^/]+/}) end def self.skip_module?(data) - !!(data.present? && data =~ /^\/\/ discourse-skip-module$/) + !!(data.present? && data =~ %r{^// discourse-skip-module$}) end class Transpiler @@ -113,19 +115,17 @@ class DiscourseJsProcessor def self.load_file_in_context(ctx, path, wrap_in_module: nil) contents = File.read("#{Rails.root}/app/assets/javascripts/#{path}") - if wrap_in_module - contents = <<~JS + contents = <<~JS if wrap_in_module define(#{wrap_in_module.to_json}, ["exports", "require", "module"], function(exports, require, module){ #{contents} }); JS - end ctx.eval(contents, filename: path) end def self.create_new_context # timeout any eval that takes longer than 15 seconds - ctx = MiniRacer::Context.new(timeout: 15000, ensure_gc_after_idle: 2000) + ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000) # General shims ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) }) @@ -158,10 +158,26 @@ class DiscourseJsProcessor # Template Compiler load_file_in_context(ctx, "node_modules/ember-source/dist/ember-template-compiler.js") - load_file_in_context(ctx, "node_modules/babel-plugin-ember-template-compilation/src/plugin.js", wrap_in_module: "babel-plugin-ember-template-compilation/index") - load_file_in_context(ctx, "node_modules/babel-plugin-ember-template-compilation/src/expression-parser.js", wrap_in_module: "babel-plugin-ember-template-compilation/expression-parser") - load_file_in_context(ctx, "node_modules/babel-import-util/src/index.js", wrap_in_module: "babel-import-util") - load_file_in_context(ctx, "node_modules/ember-cli-htmlbars/lib/colocated-babel-plugin.js", wrap_in_module: "colocated-babel-plugin") + load_file_in_context( + ctx, + "node_modules/babel-plugin-ember-template-compilation/src/plugin.js", + wrap_in_module: "babel-plugin-ember-template-compilation/index", + ) + load_file_in_context( + ctx, + "node_modules/babel-plugin-ember-template-compilation/src/expression-parser.js", + wrap_in_module: "babel-plugin-ember-template-compilation/expression-parser", + ) + load_file_in_context( + ctx, + "node_modules/babel-import-util/src/index.js", + wrap_in_module: "babel-import-util", + ) + load_file_in_context( + ctx, + "node_modules/ember-cli-htmlbars/lib/colocated-babel-plugin.js", + wrap_in_module: "colocated-babel-plugin", + ) # Widget HBS compiler widget_hbs_compiler_source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js") @@ -170,32 +186,44 @@ class DiscourseJsProcessor #{widget_hbs_compiler_source} }); JS - widget_hbs_compiler_transpiled = ctx.call("rawBabelTransform", widget_hbs_compiler_source, { - ast: false, - moduleId: 'widget-hbs-compiler', - plugins: DISCOURSE_COMMON_BABEL_PLUGINS - }) + widget_hbs_compiler_transpiled = + ctx.call( + "rawBabelTransform", + widget_hbs_compiler_source, + { ast: false, moduleId: "widget-hbs-compiler", plugins: DISCOURSE_COMMON_BABEL_PLUGINS }, + ) ctx.eval(widget_hbs_compiler_transpiled, filename: "widget-hbs-compiler.js") # Raw HBS compiler - load_file_in_context(ctx, "node_modules/handlebars/dist/handlebars.js", wrap_in_module: "handlebars") - - raw_hbs_transpiled = ctx.call( - "rawBabelTransform", - File.read("#{Rails.root}/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js"), - { - ast: false, - moduleId: "raw-handlebars", - plugins: [ - ['transform-modules-amd', { noInterop: true }], - *DISCOURSE_COMMON_BABEL_PLUGINS - ] - } + load_file_in_context( + ctx, + "node_modules/handlebars/dist/handlebars.js", + wrap_in_module: "handlebars", ) + + raw_hbs_transpiled = + ctx.call( + "rawBabelTransform", + File.read( + "#{Rails.root}/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js", + ), + { + ast: false, + moduleId: "raw-handlebars", + plugins: [ + ["transform-modules-amd", { noInterop: true }], + *DISCOURSE_COMMON_BABEL_PLUGINS, + ], + }, + ) ctx.eval(raw_hbs_transpiled, filename: "raw-handlebars.js") # Theme template AST transformation plugins - load_file_in_context(ctx, "discourse-js-processor.js", wrap_in_module: "discourse-js-processor") + load_file_in_context( + ctx, + "discourse-js-processor.js", + wrap_in_module: "discourse-js-processor", + ) # Make interfaces available via `v8.call` ctx.eval <<~JS @@ -262,10 +290,10 @@ class DiscourseJsProcessor { skip_module: @skip_module, moduleId: module_name(root_path, logical_path), - filename: logical_path || 'unknown', + filename: logical_path || "unknown", themeId: theme_id, - commonPlugins: DISCOURSE_COMMON_BABEL_PLUGINS - } + commonPlugins: DISCOURSE_COMMON_BABEL_PLUGINS, + }, ) end @@ -274,15 +302,16 @@ class DiscourseJsProcessor root_base = File.basename(Rails.root) # If the resource is a plugin, use the plugin name as a prefix - if root_path =~ /(.*\/#{root_base}\/plugins\/[^\/]+)\// + if root_path =~ %r{(.*/#{root_base}/plugins/[^/]+)/} plugin_path = "#{Regexp.last_match[1]}/plugin.rb" plugin = Discourse.plugins.find { |p| p.path == plugin_path } - path = "discourse/plugins/#{plugin.name}/#{logical_path.sub(/javascripts\//, '')}" if plugin + path = + "discourse/plugins/#{plugin.name}/#{logical_path.sub(%r{javascripts/}, "")}" if plugin end # We need to strip the app subdirectory to replicate how ember-cli works. - path || logical_path&.gsub('app/', '')&.gsub('addon/', '')&.gsub('admin/addon', 'admin') + path || logical_path&.gsub("app/", "")&.gsub("addon/", "")&.gsub("admin/addon", "admin") end def compile_raw_template(source, theme_id: nil) diff --git a/lib/discourse_logstash_logger.rb b/lib/discourse_logstash_logger.rb index ee1b739133d..15225d33849 100644 --- a/lib/discourse_logstash_logger.rb +++ b/lib/discourse_logstash_logger.rb @@ -1,27 +1,28 @@ # frozen_string_literal: true -require 'logstash-logger' +require "logstash-logger" class DiscourseLogstashLogger def self.logger(uri:, type:) # See Discourse.os_hostname - hostname = begin - require 'socket' - Socket.gethostname - rescue => e - `hostname`.chomp - end + hostname = + begin + require "socket" + Socket.gethostname + rescue => e + `hostname`.chomp + end LogStashLogger.new( uri: uri, sync: true, - customize_event: ->(event) { - event['hostname'] = hostname - event['severity_name'] = event['severity'] - event['severity'] = Object.const_get("Logger::Severity::#{event['severity']}") - event['type'] = type - event['pid'] = Process.pid - }, + customize_event: ->(event) do + event["hostname"] = hostname + event["severity_name"] = event["severity"] + event["severity"] = Object.const_get("Logger::Severity::#{event["severity"]}") + event["type"] = type + event["pid"] = Process.pid + end, ) end end diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index b6bcbb98211..9f0fd9e6082 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -4,7 +4,6 @@ # A class that handles interaction between a plugin and the Discourse App. # class DiscoursePluginRegistry - # Plugins often need to be able to register additional handlers, data, or # classes that will be used by core classes. This should be used if you # need to control which type the registry is, and if it doesn't need to @@ -24,9 +23,7 @@ class DiscoursePluginRegistry instance_variable_set(:"@#{register_name}", type.new) end - define_method(register_name) do - self.class.public_send(register_name) - end + define_method(register_name) { self.class.public_send(register_name) } end # Plugins often need to add values to a list, and we need to filter those @@ -45,10 +42,7 @@ class DiscoursePluginRegistry define_singleton_method(register_name) do unfiltered = public_send(:"_raw_#{register_name}") - unfiltered - .filter { |v| v[:plugin].enabled? } - .map { |v| v[:value] } - .uniq + unfiltered.filter { |v| v[:plugin].enabled? }.map { |v| v[:value] }.uniq end define_singleton_method("register_#{register_name.to_s.singularize}") do |value, plugin| @@ -158,9 +152,7 @@ class DiscoursePluginRegistry next if each_options[:admin] end - Dir.glob("#{root}/**/*.#{ext}") do |f| - yield f - end + Dir.glob("#{root}/**/*.#{ext}") { |f| yield f } end end @@ -227,7 +219,7 @@ class DiscoursePluginRegistry def self.seed_paths result = SeedFu.fixture_paths.dup - unless Rails.env.test? && ENV['LOAD_PLUGINS'] != "1" + unless Rails.env.test? && ENV["LOAD_PLUGINS"] != "1" seed_path_builders.each { |b| result += b.call } end result.uniq @@ -239,7 +231,7 @@ class DiscoursePluginRegistry VENDORED_CORE_PRETTY_TEXT_MAP = { "moment.js" => "vendor/assets/javascripts/moment.js", - "moment-timezone.js" => "vendor/assets/javascripts/moment-timezone-with-data.js" + "moment-timezone.js" => "vendor/assets/javascripts/moment-timezone-with-data.js", } def self.core_asset_for_name(name) asset = VENDORED_CORE_PRETTY_TEXT_MAP[name] @@ -248,16 +240,12 @@ class DiscoursePluginRegistry end def self.reset! - @@register_names.each do |name| - instance_variable_set(:"@#{name}", nil) - end + @@register_names.each { |name| instance_variable_set(:"@#{name}", nil) } end def self.reset_register!(register_name) found_register = @@register_names.detect { |name| name == register_name } - if found_register - instance_variable_set(:"@#{found_register}", nil) - end + instance_variable_set(:"@#{found_register}", nil) if found_register end end diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index 4c661cc9afe..e47e9734c56 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -46,15 +46,103 @@ class DiscourseRedis end # Proxy key methods through, but prefix the keys with the namespace - [:append, :blpop, :brpop, :brpoplpush, :decr, :decrby, :expire, :expireat, :get, :getbit, :getrange, :getset, - :hdel, :hexists, :hget, :hgetall, :hincrby, :hincrbyfloat, :hkeys, :hlen, :hmget, :hmset, :hset, :hsetnx, :hvals, :incr, - :incrby, :incrbyfloat, :lindex, :linsert, :llen, :lpop, :lpush, :lpushx, :lrange, :lrem, :lset, :ltrim, - :mapped_hmset, :mapped_hmget, :mapped_mget, :mapped_mset, :mapped_msetnx, :move, :mset, - :msetnx, :persist, :pexpire, :pexpireat, :psetex, :pttl, :rename, :renamenx, :rpop, :rpoplpush, :rpush, :rpushx, :sadd, :sadd?, :scard, - :sdiff, :set, :setbit, :setex, :setnx, :setrange, :sinter, :sismember, :smembers, :sort, :spop, :srandmember, :srem, :srem?, :strlen, - :sunion, :ttl, :type, :watch, :zadd, :zcard, :zcount, :zincrby, :zrange, :zrangebyscore, :zrank, :zrem, :zremrangebyrank, - :zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore, - :dump, :restore].each do |m| + %i[ + append + blpop + brpop + brpoplpush + decr + decrby + expire + expireat + get + getbit + getrange + getset + hdel + hexists + hget + hgetall + hincrby + hincrbyfloat + hkeys + hlen + hmget + hmset + hset + hsetnx + hvals + incr + incrby + incrbyfloat + lindex + linsert + llen + lpop + lpush + lpushx + lrange + lrem + lset + ltrim + mapped_hmset + mapped_hmget + mapped_mget + mapped_mset + mapped_msetnx + move + mset + msetnx + persist + pexpire + pexpireat + psetex + pttl + rename + renamenx + rpop + rpoplpush + rpush + rpushx + sadd + sadd? + scard + sdiff + set + setbit + setex + setnx + setrange + sinter + sismember + smembers + sort + spop + srandmember + srem + srem? + strlen + sunion + ttl + type + watch + zadd + zcard + zcount + zincrby + zrange + zrangebyscore + zrank + zrem + zremrangebyrank + zremrangebyscore + zrevrange + zrevrangebyscore + zrevrank + zrangebyscore + dump + restore + ].each do |m| define_method m do |*args, **kwargs| args[0] = "#{namespace}:#{args[0]}" if @namespace DiscourseRedis.ignore_readonly { @redis.public_send(m, *args, **kwargs) } @@ -72,7 +160,7 @@ class DiscourseRedis end def mget(*args) - args.map! { |a| "#{namespace}:#{a}" } if @namespace + args.map! { |a| "#{namespace}:#{a}" } if @namespace DiscourseRedis.ignore_readonly { @redis.mget(*args) } end @@ -86,14 +174,13 @@ class DiscourseRedis def scan_each(options = {}, &block) DiscourseRedis.ignore_readonly do - match = options[:match].presence || '*' + match = options[:match].presence || "*" - options[:match] = - if @namespace - "#{namespace}:#{match}" - else - match - end + options[:match] = if @namespace + "#{namespace}:#{match}" + else + match + end if block @redis.scan_each(**options) do |key| @@ -101,17 +188,19 @@ class DiscourseRedis block.call(key) end else - @redis.scan_each(**options).map do |key| - key = remove_namespace(key) if @namespace - key - end + @redis + .scan_each(**options) + .map do |key| + key = remove_namespace(key) if @namespace + key + end end end end def keys(pattern = nil) DiscourseRedis.ignore_readonly do - pattern = pattern || '*' + pattern = pattern || "*" pattern = "#{namespace}:#{pattern}" if @namespace keys = @redis.keys(pattern) @@ -125,9 +214,7 @@ class DiscourseRedis end def delete_prefixed(prefix) - DiscourseRedis.ignore_readonly do - keys("#{prefix}*").each { |k| Discourse.redis.del(k) } - end + DiscourseRedis.ignore_readonly { keys("#{prefix}*").each { |k| Discourse.redis.del(k) } } end def reconnect diff --git a/lib/discourse_sourcemapping_url_processor.rb b/lib/discourse_sourcemapping_url_processor.rb index 7b6ae82d563..29095863c50 100644 --- a/lib/discourse_sourcemapping_url_processor.rb +++ b/lib/discourse_sourcemapping_url_processor.rb @@ -6,7 +6,8 @@ class DiscourseSourcemappingUrlProcessor < Sprockets::Rails::SourcemappingUrlProcessor def self.sourcemap_asset_path(sourcemap_logical_path, context:) result = super(sourcemap_logical_path, context: context) - if (File.basename(sourcemap_logical_path) === sourcemap_logical_path) || sourcemap_logical_path.start_with?("plugins/") + if (File.basename(sourcemap_logical_path) === sourcemap_logical_path) || + sourcemap_logical_path.start_with?("plugins/") # If the original sourcemap reference is relative, keep it relative result = File.basename(result) end diff --git a/lib/discourse_tagging.rb b/lib/discourse_tagging.rb index e68b9032cdf..8a8c2d7f878 100644 --- a/lib/discourse_tagging.rb +++ b/lib/discourse_tagging.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module DiscourseTagging - TAGS_FIELD_NAME ||= "tags" TAGS_FILTER_REGEXP ||= /[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/ # /?#[]@!$&'()*+,;=.%\`^|{}"<> TAGS_STAFF_CACHE_KEY ||= "staff_tag_names" @@ -22,9 +21,11 @@ module DiscourseTagging tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, guardian) || [] if !tag_names.empty? - Tag.where_name(tag_names).joins(:target_tag).includes(:target_tag).each do |tag| - tag_names[tag_names.index(tag.name)] = tag.target_tag.name - end + Tag + .where_name(tag_names) + .joins(:target_tag) + .includes(:target_tag) + .each { |tag| tag_names[tag_names.index(tag.name)] = tag.target_tag.name } end # tags currently on the topic @@ -45,9 +46,7 @@ module DiscourseTagging # If this user has explicit permission to use certain tags, # we need to ensure those tags are removed from the list of # restricted tags - if permitted_tags.present? - readonly_tags = readonly_tags - permitted_tags - end + readonly_tags = readonly_tags - permitted_tags if permitted_tags.present? # visible, but not usable, tags this user is trying to use disallowed_tags = new_tag_names & readonly_tags @@ -55,13 +54,19 @@ module DiscourseTagging disallowed_tags += new_tag_names & hidden_tags if disallowed_tags.present? - topic.errors.add(:base, I18n.t("tags.restricted_tag_disallowed", tag: disallowed_tags.join(" "))) + topic.errors.add( + :base, + I18n.t("tags.restricted_tag_disallowed", tag: disallowed_tags.join(" ")), + ) return false end removed_readonly_tags = removed_tag_names & readonly_tags if removed_readonly_tags.present? - topic.errors.add(:base, I18n.t("tags.restricted_tag_remove_disallowed", tag: removed_readonly_tags.join(" "))) + topic.errors.add( + :base, + I18n.t("tags.restricted_tag_remove_disallowed", tag: removed_readonly_tags.join(" ")), + ) return false end @@ -73,50 +78,61 @@ module DiscourseTagging if tag_names.present? # guardian is explicitly nil cause we don't want to strip all # staff tags that already passed validation - tags = filter_allowed_tags( - nil, # guardian - for_topic: true, - category: category, - selected_tags: tag_names, - only_tag_names: tag_names - ) + tags = + filter_allowed_tags( + nil, # guardian + for_topic: true, + category: category, + selected_tags: tag_names, + only_tag_names: tag_names, + ) # keep existent tags that current user cannot use tags += Tag.where(name: old_tag_names & tag_names) tags = Tag.where(id: tags.map(&:id)).all.to_a if tags.size > 0 - if tags.size < tag_names.size && (category.nil? || category.allow_global_tags || (category.tags.count == 0 && category.tag_groups.count == 0)) + if tags.size < tag_names.size && + ( + category.nil? || category.allow_global_tags || + (category.tags.count == 0 && category.tag_groups.count == 0) + ) tag_names.each do |name| - unless Tag.where_name(name).exists? - tags << Tag.create(name: name) - end + tags << Tag.create(name: name) unless Tag.where_name(name).exists? end end # add missing mandatory parent tags tag_ids = tags.map(&:id) - parent_tags_map = DB.query(" + parent_tags_map = + DB + .query( + " SELECT tgm.tag_id, tg.parent_tag_id FROM tag_groups tg INNER JOIN tag_group_memberships tgm ON tgm.tag_group_id = tg.id WHERE tg.parent_tag_id IS NOT NULL AND tgm.tag_id IN (?) - ", tag_ids).inject({}) do |h, v| - h[v.tag_id] ||= [] - h[v.tag_id] << v.parent_tag_id - h - end + ", + tag_ids, + ) + .inject({}) do |h, v| + h[v.tag_id] ||= [] + h[v.tag_id] << v.parent_tag_id + h + end - missing_parent_tag_ids = parent_tags_map.map do |_, parent_tag_ids| - (tag_ids & parent_tag_ids).size == 0 ? parent_tag_ids.first : nil - end.compact.uniq + missing_parent_tag_ids = + parent_tags_map + .map do |_, parent_tag_ids| + (tag_ids & parent_tag_ids).size == 0 ? parent_tag_ids.first : nil + end + .compact + .uniq - unless missing_parent_tag_ids.empty? - tags = tags + Tag.where(id: missing_parent_tag_ids).all - end + tags = tags + Tag.where(id: missing_parent_tag_ids).all unless missing_parent_tag_ids.empty? return false unless validate_min_required_tags_for_category(guardian, topic, category, tags) return false unless validate_required_tags_from_group(guardian, topic, category, tags) @@ -137,7 +153,9 @@ module DiscourseTagging DiscourseEvent.trigger( :topic_tags_changed, - topic, old_tag_names: old_tag_names, new_tag_names: topic.tags.map(&:name) + topic, + old_tag_names: old_tag_names, + new_tag_names: topic.tags.map(&:name), ) return true @@ -146,12 +164,12 @@ module DiscourseTagging end def self.validate_min_required_tags_for_category(guardian, model, category, tags = []) - if !guardian.is_staff? && - category && - category.minimum_required_tags > 0 && - tags.length < category.minimum_required_tags - - model.errors.add(:base, I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags)) + if !guardian.is_staff? && category && category.minimum_required_tags > 0 && + tags.length < category.minimum_required_tags + model.errors.add( + :base, + I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags), + ) false else true @@ -164,17 +182,17 @@ module DiscourseTagging success = true category.category_required_tag_groups.each do |crtg| if tags.length < crtg.min_count || - crtg.tag_group.tags.where("tags.id in (?)", tags.map(&:id)).count < crtg.min_count - + crtg.tag_group.tags.where("tags.id in (?)", tags.map(&:id)).count < crtg.min_count success = false - model.errors.add(:base, + model.errors.add( + :base, I18n.t( "tags.required_tags_from_group", count: crtg.min_count, tag_group_name: crtg.tag_group.name, - tags: crtg.tag_group.tags.order(:id).pluck(:name).join(", ") - ) + tags: crtg.tag_group.tags.order(:id).pluck(:name).join(", "), + ), ) end end @@ -189,24 +207,28 @@ module DiscourseTagging tags_restricted_to_categories = Hash.new { |h, k| h[k] = Set.new } query = Tag.where(name: tags) - query.joins(tag_groups: :categories).pluck(:name, 'categories.id').each do |(tag, cat_id)| - tags_restricted_to_categories[tag] << cat_id - end - query.joins(:categories).pluck(:name, 'categories.id').each do |(tag, cat_id)| - tags_restricted_to_categories[tag] << cat_id - end + query + .joins(tag_groups: :categories) + .pluck(:name, "categories.id") + .each { |(tag, cat_id)| tags_restricted_to_categories[tag] << cat_id } + query + .joins(:categories) + .pluck(:name, "categories.id") + .each { |(tag, cat_id)| tags_restricted_to_categories[tag] << cat_id } - unallowed_tags = tags_restricted_to_categories.keys.select do |tag| - !tags_restricted_to_categories[tag].include?(category.id) - end + unallowed_tags = + tags_restricted_to_categories.keys.select do |tag| + !tags_restricted_to_categories[tag].include?(category.id) + end if unallowed_tags.present? - msg = I18n.t( - "tags.forbidden.restricted_tags_cannot_be_used_in_category", - count: unallowed_tags.size, - tags: unallowed_tags.sort.join(", "), - category: category.name - ) + msg = + I18n.t( + "tags.forbidden.restricted_tags_cannot_be_used_in_category", + count: unallowed_tags.size, + tags: unallowed_tags.sort.join(", "), + category: category.name, + ) model.errors.add(:base, msg) return false end @@ -214,12 +236,13 @@ module DiscourseTagging if !category.allow_global_tags && category.has_restricted_tags? unrestricted_tags = tags - tags_restricted_to_categories.keys if unrestricted_tags.present? - msg = I18n.t( - "tags.forbidden.category_does_not_allow_tags", - count: unrestricted_tags.size, - tags: unrestricted_tags.sort.join(", "), - category: category.name - ) + msg = + I18n.t( + "tags.forbidden.category_does_not_allow_tags", + count: unrestricted_tags.size, + tags: unrestricted_tags.sort.join(", "), + category: category.name, + ) model.errors.add(:base, msg) return false end @@ -280,7 +303,8 @@ module DiscourseTagging def self.filter_allowed_tags(guardian, opts = {}) selected_tag_ids = opts[:selected_tags] ? Tag.where_name(opts[:selected_tags]).pluck(:id) : [] category = opts[:category] - category_has_restricted_tags = category ? (category.tags.count > 0 || category.tag_groups.count > 0) : false + category_has_restricted_tags = + category ? (category.tags.count > 0 || category.tag_groups.count > 0) : false # If guardian is nil, it means the caller doesn't want tags to be filtered # based on guardian rules. Use the same rules as for staff users. @@ -288,9 +312,7 @@ module DiscourseTagging builder_params = {} - unless selected_tag_ids.empty? - builder_params[:selected_tag_ids] = selected_tag_ids - end + builder_params[:selected_tag_ids] = selected_tag_ids unless selected_tag_ids.empty? sql = +"WITH #{TAG_GROUP_RESTRICTIONS_SQL}, #{CATEGORY_RESTRICTIONS_SQL}" if (opts[:for_input] || opts[:for_topic]) && filter_for_non_staff @@ -301,13 +323,14 @@ module DiscourseTagging outer_join = category.nil? || category.allow_global_tags || !category_has_restricted_tags - distinct_clause = if opts[:order_popularity] - "DISTINCT ON (topic_count, name)" - elsif opts[:order_search_results] && opts[:term].present? - "DISTINCT ON (lower(name) = lower(:cleaned_term), topic_count, name)" - else - "" - end + distinct_clause = + if opts[:order_popularity] + "DISTINCT ON (topic_count, name)" + elsif opts[:order_search_results] && opts[:term].present? + "DISTINCT ON (lower(name) = lower(:cleaned_term), topic_count, name)" + else + "" + end sql << <<~SQL SELECT #{distinct_clause} t.id, t.name, t.topic_count, t.pm_topic_count, t.description, @@ -336,16 +359,20 @@ module DiscourseTagging # parent tag requirements if opts[:for_input] builder.where( - builder_params[:selected_tag_ids] ? - "tgm_id IS NULL OR parent_tag_id IS NULL OR parent_tag_id IN (:selected_tag_ids)" : - "tgm_id IS NULL OR parent_tag_id IS NULL" + ( + if builder_params[:selected_tag_ids] + "tgm_id IS NULL OR parent_tag_id IS NULL OR parent_tag_id IN (:selected_tag_ids)" + else + "tgm_id IS NULL OR parent_tag_id IS NULL" + end + ), ) end if category && category_has_restricted_tags builder.where( category.allow_global_tags ? "category_id = ? OR category_id IS NULL" : "category_id = ?", - category.id + category.id, ) elsif category || opts[:for_input] || opts[:for_topic] # tags not restricted to any categories @@ -354,7 +381,9 @@ module DiscourseTagging if filter_for_non_staff && (opts[:for_input] || opts[:for_topic]) # exclude staff-only tag groups - builder.where("tag_group_id IS NULL OR tag_group_id IN (SELECT tag_group_id FROM permitted_tag_groups)") + builder.where( + "tag_group_id IS NULL OR tag_group_id IN (SELECT tag_group_id FROM permitted_tag_groups)", + ) end term = opts[:term] @@ -380,7 +409,8 @@ module DiscourseTagging # - and no search term has been included required_tag_ids = nil required_category_tag_group = nil - if opts[:for_input] && category&.category_required_tag_groups.present? && (filter_for_non_staff || term.blank?) + if opts[:for_input] && category&.category_required_tag_groups.present? && + (filter_for_non_staff || term.blank?) category.category_required_tag_groups.each do |crtg| group_tags = crtg.tag_group.tags.pluck(:id) next if (group_tags & selected_tag_ids).size >= crtg.min_count @@ -426,22 +456,18 @@ module DiscourseTagging if !one_tag_per_group_ids.empty? builder.where( "tag_group_id IS NULL OR tag_group_id NOT IN (?) OR id IN (:selected_tag_ids)", - one_tag_per_group_ids + one_tag_per_group_ids, ) end end - if opts[:exclude_synonyms] - builder.where("target_tag_id IS NULL") - end + builder.where("target_tag_id IS NULL") if opts[:exclude_synonyms] if opts[:exclude_has_synonyms] builder.where("id NOT IN (SELECT target_tag_id FROM tags WHERE target_tag_id IS NOT NULL)") end - if opts[:excluded_tag_names]&.any? - builder.where("name NOT IN (?)", opts[:excluded_tag_names]) - end + builder.where("name NOT IN (?)", opts[:excluded_tag_names]) if opts[:excluded_tag_names]&.any? if opts[:limit] if required_tag_ids && term.blank? @@ -465,7 +491,7 @@ module DiscourseTagging if required_category_tag_group context[:required_tag_group] = { name: required_category_tag_group.tag_group.name, - min_count: required_category_tag_group.min_count + min_count: required_category_tag_group.min_count, } end [result, context] @@ -480,21 +506,15 @@ module DiscourseTagging else # Visible tags either have no permissions or have allowable permissions Tag - .where.not( - id: - TagGroupMembership - .joins(tag_group: :tag_group_permissions) - .select(:tag_id) - ) + .where.not(id: TagGroupMembership.joins(tag_group: :tag_group_permissions).select(:tag_id)) .or( - Tag - .where( - id: - TagGroupPermission - .joins(tag_group: :tag_group_memberships) - .where(group_id: permitted_group_ids_query(guardian)) - .select('tag_group_memberships.tag_id'), - ) + Tag.where( + id: + TagGroupPermission + .joins(tag_group: :tag_group_memberships) + .where(group_id: permitted_group_ids_query(guardian)) + .select("tag_group_memberships.tag_id"), + ), ) end end @@ -509,21 +529,18 @@ module DiscourseTagging def self.permitted_group_ids_query(guardian = nil) if guardian&.authenticated? - Group - .from( - Group.sanitize_sql( - ["(SELECT ? AS id UNION #{guardian.user.groups.select(:id).to_sql}) as groups", Group::AUTO_GROUPS[:everyone]] - ) - ) - .select(:id) + Group.from( + Group.sanitize_sql( + [ + "(SELECT ? AS id UNION #{guardian.user.groups.select(:id).to_sql}) as groups", + Group::AUTO_GROUPS[:everyone], + ], + ), + ).select(:id) else - Group - .from( - Group.sanitize_sql( - ["(SELECT ? AS id) AS groups", Group::AUTO_GROUPS[:everyone]] - ) - ) - .select(:id) + Group.from( + Group.sanitize_sql(["(SELECT ? AS id) AS groups", Group::AUTO_GROUPS[:everyone]]), + ).select(:id) end end @@ -535,9 +552,11 @@ module DiscourseTagging def self.readonly_tag_names(guardian = nil) return [] if guardian&.is_staff? - query = Tag.joins(tag_groups: :tag_group_permissions) - .where('tag_group_permissions.permission_type = ?', - TagGroupPermission.permission_types[:readonly]) + query = + Tag.joins(tag_groups: :tag_group_permissions).where( + "tag_group_permissions.permission_type = ?", + TagGroupPermission.permission_types[:readonly], + ) query.pluck(:name) end @@ -545,14 +564,12 @@ module DiscourseTagging # explicit permissions to use these tags def self.permitted_tag_names(guardian = nil) query = - Tag - .joins(tag_groups: :tag_group_permissions) - .where( - tag_group_permissions: { - group_id: permitted_group_ids(guardian), - permission_type: TagGroupPermission.permission_types[:full], - }, - ) + Tag.joins(tag_groups: :tag_group_permissions).where( + tag_group_permissions: { + group_id: permitted_group_ids(guardian), + permission_type: TagGroupPermission.permission_types[:full], + }, + ) query.pluck(:name).uniq end @@ -586,15 +603,14 @@ module DiscourseTagging tag = tag.dup tag.downcase! if SiteSetting.force_lowercase_tags tag.strip! - tag.gsub!(/[[:space:]]+/, '-') - tag.gsub!(/[^[:word:][:punct:]]+/, '') - tag.squeeze!('-') - tag.gsub!(TAGS_FILTER_REGEXP, '') + tag.gsub!(/[[:space:]]+/, "-") + tag.gsub!(/[^[:word:][:punct:]]+/, "") + tag.squeeze!("-") + tag.gsub!(TAGS_FILTER_REGEXP, "") tag[0...SiteSetting.max_tag_length] end def self.tags_for_saving(tags_arg, guardian, opts = {}) - return [] unless guardian.can_tag_topics? && tags_arg.present? tag_names = Tag.where_name(tags_arg).pluck(:name) @@ -609,21 +625,23 @@ module DiscourseTagging end def self.add_or_create_tags_by_name(taggable, tag_names_arg, opts = {}) - tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user), opts) || [] + tag_names = + DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user), opts) || + [] if taggable.tags.pluck(:name).sort != tag_names.sort taggable.tags = Tag.where_name(tag_names).all - new_tag_names = taggable.tags.size < tag_names.size ? tag_names - taggable.tags.map(&:name) : [] + new_tag_names = + taggable.tags.size < tag_names.size ? tag_names - taggable.tags.map(&:name) : [] taggable.tags << Tag.where(target_tag_id: taggable.tags.map(&:id)).all - new_tag_names.each do |name| - taggable.tags << Tag.create(name: name) - end + new_tag_names.each { |name| taggable.tags << Tag.create(name: name) } end end # Returns true if all were added successfully, or an Array of the # tags that failed to be added, with errors on each Tag. def self.add_or_create_synonyms_by_name(target_tag, synonym_names) - tag_names = DiscourseTagging.tags_for_saving(synonym_names, Guardian.new(Discourse.system_user)) || [] + tag_names = + DiscourseTagging.tags_for_saving(synonym_names, Guardian.new(Discourse.system_user)) || [] tag_names -= [target_tag.name] existing = Tag.where_name(tag_names).all target_tag.synonyms << existing @@ -642,6 +660,6 @@ module DiscourseTagging def self.muted_tags(user) return [] unless user - TagUser.lookup(user, :muted).joins(:tag).pluck('tags.name') + TagUser.lookup(user, :muted).joins(:tag).pluck("tags.name") end end diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb index 5fea6c0724f..d20870b9668 100644 --- a/lib/discourse_updates.rb +++ b/lib/discourse_updates.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true module DiscourseUpdates - class << self - def check_version attrs = { installed_version: Discourse::VERSION::STRING, - installed_sha: (Discourse.git_version == 'unknown' ? nil : Discourse.git_version), + installed_sha: (Discourse.git_version == "unknown" ? nil : Discourse.git_version), installed_describe: Discourse.full_version, git_branch: Discourse.git_branch, updated_at: updated_at, @@ -17,7 +15,7 @@ module DiscourseUpdates attrs.merge!( latest_version: latest_version, critical_updates: critical_updates_available?, - missing_versions_count: missing_versions_count + missing_versions_count: missing_versions_count, ) end @@ -25,19 +23,24 @@ module DiscourseUpdates # replace -commit_count with +commit_count if version_info.installed_describe =~ /-(\d+)-/ - version_info.installed_describe = version_info.installed_describe.gsub(/-(\d+)-.*/, " +#{$1}") + version_info.installed_describe = + version_info.installed_describe.gsub(/-(\d+)-.*/, " +#{$1}") end if SiteSetting.version_checks? is_stale_data = - (version_info.missing_versions_count == 0 && version_info.latest_version != version_info.installed_version) || - (version_info.missing_versions_count != 0 && version_info.latest_version == version_info.installed_version) + ( + version_info.missing_versions_count == 0 && + version_info.latest_version != version_info.installed_version + ) || + ( + version_info.missing_versions_count != 0 && + version_info.latest_version == version_info.installed_version + ) # Handle cases when version check data is old so we report something that makes sense - if version_info.updated_at.nil? || # never performed a version check - last_installed_version != Discourse::VERSION::STRING || # upgraded since the last version check - is_stale_data - + if version_info.updated_at.nil? || last_installed_version != Discourse::VERSION::STRING || # never performed a version check # upgraded since the last version check + is_stale_data Jobs.enqueue(:version_check, all_sites: true) version_info.version_check_pending = true @@ -48,9 +51,8 @@ module DiscourseUpdates end version_info.stale_data = - version_info.version_check_pending || - (updated_at && updated_at < 48.hours.ago) || - is_stale_data + version_info.version_check_pending || (updated_at && updated_at < 48.hours.ago) || + is_stale_data end version_info @@ -82,7 +84,7 @@ module DiscourseUpdates end def critical_updates_available? - (Discourse.redis.get(critical_updates_available_key) || false) == 'true' + (Discourse.redis.get(critical_updates_available_key) || false) == "true" end def critical_updates_available=(arg) @@ -110,7 +112,7 @@ module DiscourseUpdates # store the list in redis version_keys = [] versions[0, 5].each do |v| - key = "#{missing_versions_key_prefix}:#{v['version']}" + key = "#{missing_versions_key_prefix}:#{v["version"]}" Discourse.redis.mapped_hmset key, v version_keys << key end @@ -140,11 +142,21 @@ module DiscourseUpdates end def new_features - entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil + entries = + begin + JSON.parse(Discourse.redis.get(new_features_key)) + rescue StandardError + nil + end return nil if entries.nil? entries.select! do |item| - item["discourse_version"].nil? || Discourse.has_needed_version?(current_version, item["discourse_version"]) rescue nil + begin + item["discourse_version"].nil? || + Discourse.has_needed_version?(current_version, item["discourse_version"]) + rescue StandardError + nil + end end entries.sort_by { |item| Time.zone.parse(item["created_at"]).to_i }.reverse @@ -170,7 +182,12 @@ module DiscourseUpdates end def mark_new_features_as_seen(user_id) - entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil + entries = + begin + JSON.parse(Discourse.redis.get(new_features_key)) + rescue StandardError + nil + end return nil if entries.nil? last_seen = entries.max_by { |x| x["created_at"] } Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"]) @@ -204,39 +221,39 @@ module DiscourseUpdates private def last_installed_version_key - 'last_installed_version' + "last_installed_version" end def latest_version_key - 'discourse_latest_version' + "discourse_latest_version" end def critical_updates_available_key - 'critical_updates_available' + "critical_updates_available" end def missing_versions_count_key - 'missing_versions_count' + "missing_versions_count" end def updated_at_key - 'last_version_check_at' + "last_version_check_at" end def missing_versions_list_key - 'missing_versions' + "missing_versions" end def missing_versions_key_prefix - 'missing_version' + "missing_version" end def new_features_endpoint - 'https://meta.discourse.org/new-features.json' + "https://meta.discourse.org/new-features.json" end def new_features_key - 'new_features' + "new_features" end def new_features_last_seen_key(user_id) diff --git a/lib/disk_space.rb b/lib/disk_space.rb index 9903164e3c1..c7ee672cc0b 100644 --- a/lib/disk_space.rb +++ b/lib/disk_space.rb @@ -18,13 +18,13 @@ class DiskSpace end def self.free(path) - output = Discourse::Utils.execute_command('df', '-Pk', path) + output = Discourse::Utils.execute_command("df", "-Pk", path) size_line = output.split("\n")[1] size_line.split(/\s+/)[3].to_i * 1024 end def self.percent_free(path) - output = Discourse::Utils.execute_command('df', '-P', path) + output = Discourse::Utils.execute_command("df", "-P", path) size_line = output.split("\n")[1] size_line.split(/\s+/)[4].to_i end diff --git a/lib/distributed_cache.rb b/lib/distributed_cache.rb index 89e6058d11c..80fa7bf1f82 100644 --- a/lib/distributed_cache.rb +++ b/lib/distributed_cache.rb @@ -1,23 +1,16 @@ # frozen_string_literal: true -require 'message_bus/distributed_cache' +require "message_bus/distributed_cache" class DistributedCache < MessageBus::DistributedCache def initialize(key, manager: nil, namespace: true) - super( - key, - manager: manager, - namespace: namespace, - app_version: Discourse.git_version - ) + super(key, manager: manager, namespace: namespace, app_version: Discourse.git_version) end # Defer setting of the key in the cache for performance critical path to avoid # waiting on MessageBus to publish the message which involves writing to Redis. def defer_set(k, v) - Scheduler::Defer.later("#{@key}_set") do - self[k] = v - end + Scheduler::Defer.later("#{@key}_set") { self[k] = v } end def defer_get_set(k, &block) diff --git a/lib/distributed_mutex.rb b/lib/distributed_mutex.rb index b456244a6d7..f7eb3b4e512 100644 --- a/lib/distributed_mutex.rb +++ b/lib/distributed_mutex.rb @@ -31,11 +31,7 @@ class DistributedMutex LUA def self.synchronize(key, redis: nil, validity: DEFAULT_VALIDITY, &blk) - self.new( - key, - redis: redis, - validity: validity - ).synchronize(&blk) + self.new(key, redis: redis, validity: validity).synchronize(&blk) end def initialize(key, redis: nil, validity: DEFAULT_VALIDITY) @@ -58,7 +54,9 @@ class DistributedMutex ensure current_time = redis.time[0] if current_time > expire_time - warn("held for too long, expected max: #{@validity} secs, took an extra #{current_time - expire_time} secs") + warn( + "held for too long, expected max: #{@validity} secs, took an extra #{current_time - expire_time} secs", + ) end unlocked = UNLOCK_SCRIPT.eval(redis, [prefixed_key], [expire_time.to_s]) diff --git a/lib/edit_rate_limiter.rb b/lib/edit_rate_limiter.rb index d9769f52621..b39bcb5a8cd 100644 --- a/lib/edit_rate_limiter.rb +++ b/lib/edit_rate_limiter.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rate_limiter' +require "rate_limiter" class EditRateLimiter < RateLimiter def initialize(user) limit = SiteSetting.max_edits_per_day diff --git a/lib/email.rb b/lib/email.rb index 8993c2a4163..ec2061c29a6 100644 --- a/lib/email.rb +++ b/lib/email.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'mail' +require "mail" module Email # See https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml#smtp-enhanced-status-codes-1 @@ -21,19 +21,19 @@ module Email def self.obfuscate(email) return email if !Email.is_valid?(email) - first, _, last = email.rpartition('@') + first, _, last = email.rpartition("@") # Obfuscate each last part, except tld - last = last.split('.') + last = last.split(".") tld = last.pop last.map! { |part| obfuscate_part(part) } last << tld - "#{obfuscate_part(first)}@#{last.join('.')}" + "#{obfuscate_part(first)}@#{last.join(".")}" end def self.cleanup_alias(name) - name ? name.gsub(/[:<>,"]/, '') : name + name ? name.gsub(/[:<>,"]/, "") : name end def self.extract_parts(raw) diff --git a/lib/email/authentication_results.rb b/lib/email/authentication_results.rb index aa8f5ad2ef9..05bea8df78d 100644 --- a/lib/email/authentication_results.rb +++ b/lib/email/authentication_results.rb @@ -2,12 +2,7 @@ module Email class AuthenticationResults - VERDICT = Enum.new( - :gray, - :pass, - :fail, - start: 0, - ) + VERDICT = Enum.new(:gray, :pass, :fail, start: 0) def initialize(headers) @authserv_id = SiteSetting.email_in_authserv_id @@ -16,11 +11,10 @@ module Email end def results - @results ||= Array(@headers).map do |header| - parse_header(header.to_s) - end.filter do |result| - @authserv_id.blank? || @authserv_id == result[:authserv_id] - end + @results ||= + Array(@headers) + .map { |header| parse_header(header.to_s) } + .filter { |result| @authserv_id.blank? || @authserv_id == result[:authserv_id] } end def action @@ -55,7 +49,8 @@ module Email end end end - verdict = VERDICT[:gray] if SiteSetting.email_in_authserv_id.blank? && verdict == VERDICT[:pass] + verdict = VERDICT[:gray] if SiteSetting.email_in_authserv_id.blank? && + verdict == VERDICT[:pass] verdict end @@ -67,10 +62,11 @@ module Email authres_version = /\d+#{cfws}?/ no_result = /#{cfws}?;#{cfws}?none/ keyword = /([a-zA-Z0-9-]*[a-zA-Z0-9])/ - authres_payload = /\A#{cfws}?#{authserv_id}(?:#{cfws}#{authres_version})?(?:#{no_result}|([\S\s]*))/ + authres_payload = + /\A#{cfws}?#{authserv_id}(?:#{cfws}#{authres_version})?(?:#{no_result}|([\S\s]*))/ method_version = authres_version - method = /#{keyword}\s*(?:#{cfws}?\/#{cfws}?#{method_version})?/ + method = %r{#{keyword}\s*(?:#{cfws}?/#{cfws}?#{method_version})?} result = keyword methodspec = /#{cfws}?#{method}#{cfws}?=#{cfws}?#{result}/ reasonspec = /reason#{cfws}?=#{cfws}?#{value}/ @@ -87,27 +83,21 @@ module Email if resinfo_val resinfo_scan = resinfo_val.scan(resinfo) - parsed_resinfo = resinfo_scan.map do |x| - { - method: x[2], - result: x[8], - reason: x[12] || x[13], - props: x[-1].scan(propspec).map do |y| - { - ptype: y[0], - property: y[4], - pvalue: y[8] || y[9] - } - end - } - end + parsed_resinfo = + resinfo_scan.map do |x| + { + method: x[2], + result: x[8], + reason: x[12] || x[13], + props: + x[-1] + .scan(propspec) + .map { |y| { ptype: y[0], property: y[4], pvalue: y[8] || y[9] } }, + } + end end - { - authserv_id: parsed_authserv_id, - resinfo: parsed_resinfo - } + { authserv_id: parsed_authserv_id, resinfo: parsed_resinfo } end - end end diff --git a/lib/email/build_email_helper.rb b/lib/email/build_email_helper.rb index 80677942a08..51ddd1f97a0 100644 --- a/lib/email/build_email_helper.rb +++ b/lib/email/build_email_helper.rb @@ -5,11 +5,11 @@ module Email def build_email(*builder_args) builder = Email::MessageBuilder.new(*builder_args) headers(builder.header_args) if builder.header_args.present? - mail(builder.build_args).tap { |message| + mail(builder.build_args).tap do |message| if message && h = builder.html_part message.html_part = h end - } + end end end end diff --git a/lib/email/cleaner.rb b/lib/email/cleaner.rb index 52170a96598..16668dd3fb7 100644 --- a/lib/email/cleaner.rb +++ b/lib/email/cleaner.rb @@ -4,7 +4,7 @@ module Email class Cleaner def initialize(mail, remove_attachments: true, truncate: true, rejected: false) @mail = Mail.new(mail) - @mail.charset = 'UTF-8' + @mail.charset = "UTF-8" @remove_attachments = remove_attachments @truncate = truncate @rejected = rejected @@ -17,13 +17,16 @@ module Email end def self.delete_rejected! - IncomingEmail.delete_by('rejection_message IS NOT NULL AND created_at < ?', SiteSetting.delete_rejected_email_after_days.days.ago) + IncomingEmail.delete_by( + "rejection_message IS NOT NULL AND created_at < ?", + SiteSetting.delete_rejected_email_after_days.days.ago, + ) end private def truncate! - parts.each { |part| part.body = part.body.decoded.truncate(truncate_limit, omission: '') } + parts.each { |part| part.body = part.body.decoded.truncate(truncate_limit, omission: "") } end def parts diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb index 42aefe600a3..396085955a4 100644 --- a/lib/email/message_builder.rb +++ b/lib/email/message_builder.rb @@ -6,7 +6,7 @@ module Email class MessageBuilder attr_reader :template_args - ALLOW_REPLY_BY_EMAIL_HEADER = 'X-Discourse-Allow-Reply-By-Email' + ALLOW_REPLY_BY_EMAIL_HEADER = "X-Discourse-Allow-Reply-By-Email" def initialize(to, opts = nil) @to = to @@ -21,30 +21,44 @@ module Email }.merge!(@opts) if @template_args[:url].present? - @template_args[:header_instructions] ||= I18n.t('user_notifications.header_instructions', @template_args) + @template_args[:header_instructions] ||= I18n.t( + "user_notifications.header_instructions", + @template_args, + ) if @opts[:include_respond_instructions] == false - @template_args[:respond_instructions] = '' - @template_args[:respond_instructions] = I18n.t('user_notifications.pm_participants', @template_args) if @opts[:private_reply] + @template_args[:respond_instructions] = "" + @template_args[:respond_instructions] = I18n.t( + "user_notifications.pm_participants", + @template_args, + ) if @opts[:private_reply] else if @opts[:only_reply_by_email] string = +"user_notifications.only_reply_by_email" string << "_pm" if @opts[:private_reply] else - string = allow_reply_by_email? ? +"user_notifications.reply_by_email" : +"user_notifications.visit_link_to_respond" + string = + ( + if allow_reply_by_email? + +"user_notifications.reply_by_email" + else + +"user_notifications.visit_link_to_respond" + end + ) string << "_pm" if @opts[:private_reply] end @template_args[:respond_instructions] = "---\n" + I18n.t(string, @template_args) end if @opts[:add_unsubscribe_link] - unsubscribe_string = if @opts[:mailing_list_mode] - "unsubscribe_mailing_list" - elsif SiteSetting.unsubscribe_via_email_footer - "unsubscribe_link_and_mail" - else - "unsubscribe_link" - end + unsubscribe_string = + if @opts[:mailing_list_mode] + "unsubscribe_mailing_list" + elsif SiteSetting.unsubscribe_via_email_footer + "unsubscribe_link_and_mail" + else + "unsubscribe_link" + end @template_args[:unsubscribe_instructions] = I18n.t(unsubscribe_string, @template_args) end end @@ -52,26 +66,60 @@ module Email def subject if @opts[:template] && - TranslationOverride.exists?(locale: I18n.locale, translation_key: "#{@opts[:template]}.subject_template") - augmented_template_args = @template_args.merge({ - site_name: @template_args[:email_prefix], - optional_re: @opts[:add_re_to_subject] ? I18n.t('subject_re') : '', - optional_pm: @opts[:private_reply] ? @template_args[:subject_pm] : '', - optional_cat: @template_args[:show_category_in_subject] ? "[#{@template_args[:show_category_in_subject]}] " : '', - optional_tags: @template_args[:show_tags_in_subject] ? "#{@template_args[:show_tags_in_subject]} " : '', - topic_title: @template_args[:topic_title] ? @template_args[:topic_title] : '', - }) + TranslationOverride.exists?( + locale: I18n.locale, + translation_key: "#{@opts[:template]}.subject_template", + ) + augmented_template_args = + @template_args.merge( + { + site_name: @template_args[:email_prefix], + optional_re: @opts[:add_re_to_subject] ? I18n.t("subject_re") : "", + optional_pm: @opts[:private_reply] ? @template_args[:subject_pm] : "", + optional_cat: + ( + if @template_args[:show_category_in_subject] + "[#{@template_args[:show_category_in_subject]}] " + else + "" + end + ), + optional_tags: + ( + if @template_args[:show_tags_in_subject] + "#{@template_args[:show_tags_in_subject]} " + else + "" + end + ), + topic_title: @template_args[:topic_title] ? @template_args[:topic_title] : "", + }, + ) subject = I18n.t("#{@opts[:template]}.subject_template", augmented_template_args) elsif @opts[:use_site_subject] subject = String.new(SiteSetting.email_subject) subject.gsub!("%{site_name}", @template_args[:email_prefix]) - subject.gsub!("%{optional_re}", @opts[:add_re_to_subject] ? I18n.t('subject_re') : '') - subject.gsub!("%{optional_pm}", @opts[:private_reply] ? @template_args[:subject_pm] : '') - subject.gsub!("%{optional_cat}", @template_args[:show_category_in_subject] ? "[#{@template_args[:show_category_in_subject]}] " : '') - subject.gsub!("%{optional_tags}", @template_args[:show_tags_in_subject] ? "#{@template_args[:show_tags_in_subject]} " : '') - subject.gsub!("%{topic_title}", @template_args[:topic_title]) if @template_args[:topic_title] # must be last for safety + subject.gsub!("%{optional_re}", @opts[:add_re_to_subject] ? I18n.t("subject_re") : "") + subject.gsub!("%{optional_pm}", @opts[:private_reply] ? @template_args[:subject_pm] : "") + subject.gsub!( + "%{optional_cat}", + ( + if @template_args[:show_category_in_subject] + "[#{@template_args[:show_category_in_subject]}] " + else + "" + end + ), + ) + subject.gsub!( + "%{optional_tags}", + @template_args[:show_tags_in_subject] ? "#{@template_args[:show_tags_in_subject]} " : "", + ) + if @template_args[:topic_title] + subject.gsub!("%{topic_title}", @template_args[:topic_title]) + end # must be last for safety elsif @opts[:use_topic_title_subject] - subject = @opts[:add_re_to_subject] ? I18n.t('subject_re') : '' + subject = @opts[:add_re_to_subject] ? I18n.t("subject_re") : "" subject = "#{subject}#{@template_args[:topic_title]}" elsif @opts[:template] subject = I18n.t("#{@opts[:template]}.subject_template", @template_args) @@ -85,34 +133,40 @@ module Email return unless html_override = @opts[:html_override] if @template_args[:unsubscribe_instructions].present? - unsubscribe_instructions = PrettyText.cook(@template_args[:unsubscribe_instructions], sanitize: false).html_safe + unsubscribe_instructions = + PrettyText.cook(@template_args[:unsubscribe_instructions], sanitize: false).html_safe html_override.gsub!("%{unsubscribe_instructions}", unsubscribe_instructions) else html_override.gsub!("%{unsubscribe_instructions}", "") end if @template_args[:header_instructions].present? - header_instructions = PrettyText.cook(@template_args[:header_instructions], sanitize: false).html_safe + header_instructions = + PrettyText.cook(@template_args[:header_instructions], sanitize: false).html_safe html_override.gsub!("%{header_instructions}", header_instructions) else html_override.gsub!("%{header_instructions}", "") end if @template_args[:respond_instructions].present? - respond_instructions = PrettyText.cook(@template_args[:respond_instructions], sanitize: false).html_safe + respond_instructions = + PrettyText.cook(@template_args[:respond_instructions], sanitize: false).html_safe html_override.gsub!("%{respond_instructions}", respond_instructions) else html_override.gsub!("%{respond_instructions}", "") end - html = UserNotificationRenderer.render( - template: 'layouts/email_template', - format: :html, - locals: { html_body: html_override.html_safe } - ) + html = + UserNotificationRenderer.render( + template: "layouts/email_template", + format: :html, + locals: { + html_body: html_override.html_safe, + }, + ) Mail::Part.new do - content_type 'text/html; charset=UTF-8' + content_type "text/html; charset=UTF-8" body html end end @@ -139,14 +193,18 @@ module Email to: @to, subject: subject, body: body, - charset: 'UTF-8', + charset: "UTF-8", from: from_value, cc: @opts[:cc], - bcc: @opts[:bcc] + bcc: @opts[:bcc], } - args[:delivery_method_options] = @opts[:delivery_method_options] if @opts[:delivery_method_options] - args[:delivery_method_options] = (args[:delivery_method_options] || {}).merge(return_response: true) + args[:delivery_method_options] = @opts[:delivery_method_options] if @opts[ + :delivery_method_options + ] + args[:delivery_method_options] = (args[:delivery_method_options] || {}).merge( + return_response: true, + ) args end @@ -154,38 +212,42 @@ module Email def header_args result = {} if @opts[:add_unsubscribe_link] - unsubscribe_url = @template_args[:unsubscribe_url].presence || @template_args[:user_preferences_url] - result['List-Unsubscribe'] = "<#{unsubscribe_url}>" + unsubscribe_url = + @template_args[:unsubscribe_url].presence || @template_args[:user_preferences_url] + result["List-Unsubscribe"] = "<#{unsubscribe_url}>" end - result['X-Discourse-Post-Id'] = @opts[:post_id].to_s if @opts[:post_id] - result['X-Discourse-Topic-Id'] = @opts[:topic_id].to_s if @opts[:topic_id] + result["X-Discourse-Post-Id"] = @opts[:post_id].to_s if @opts[:post_id] + result["X-Discourse-Topic-Id"] = @opts[:topic_id].to_s if @opts[:topic_id] # at this point these have been filtered by the recipient's guardian for visibility, # see UserNotifications#send_notification_email - result['X-Discourse-Tags'] = @template_args[:show_tags_in_subject] if @opts[:show_tags_in_subject] - result['X-Discourse-Category'] = @template_args[:show_category_in_subject] if @opts[:show_category_in_subject] + result["X-Discourse-Tags"] = @template_args[:show_tags_in_subject] if @opts[ + :show_tags_in_subject + ] + result["X-Discourse-Category"] = @template_args[:show_category_in_subject] if @opts[ + :show_category_in_subject + ] # please, don't send us automatic responses... - result['X-Auto-Response-Suppress'] = 'All' + result["X-Auto-Response-Suppress"] = "All" if !allow_reply_by_email? # This will end up being the notification_email, which is a # noreply address. - result['Reply-To'] = from_value + result["Reply-To"] = from_value else - # The only reason we use from address for reply to is for group # SMTP emails, where the person will be replying to the group's # email_username. if !@opts[:use_from_address_for_reply_to] result[ALLOW_REPLY_BY_EMAIL_HEADER] = true - result['Reply-To'] = reply_by_email_address + result["Reply-To"] = reply_by_email_address else # No point in adding a reply-to header if it is going to be identical # to the from address/alias. If the from option is not present, then # the default reply-to address is used. - result['Reply-To'] = from_value if from_value != alias_email(@opts[:from]) + result["Reply-To"] = from_value if from_value != alias_email(@opts[:from]) end end @@ -194,23 +256,24 @@ module Email def self.custom_headers(string) result = {} - string.split('|').each { |item| - header = item.split(':', 2) - if header.length == 2 - name = header[0].strip - value = header[1].strip - result[name] = value if name.length > 0 && value.length > 0 - end - } unless string.nil? + string + .split("|") + .each do |item| + header = item.split(":", 2) + if header.length == 2 + name = header[0].strip + value = header[1].strip + result[name] = value if name.length > 0 && value.length > 0 + end + end unless string.nil? result end protected def allow_reply_by_email? - SiteSetting.reply_by_email_enabled? && - reply_by_email_address.present? && - @opts[:allow_reply_by_email] + SiteSetting.reply_by_email_enabled? && reply_by_email_address.present? && + @opts[:allow_reply_by_email] end def private_reply? @@ -238,9 +301,10 @@ module Email end def alias_email(source) - return source if @opts[:from_alias].blank? && - SiteSetting.email_site_title.blank? && - SiteSetting.title.blank? + if @opts[:from_alias].blank? && SiteSetting.email_site_title.blank? && + SiteSetting.title.blank? + return source + end if @opts[:from_alias].present? %Q|"#{Email.cleanup_alias(@opts[:from_alias])}" <#{source}>| @@ -255,7 +319,5 @@ module Email from_alias = Email.site_title %Q|"#{Email.cleanup_alias(from_alias)}" <#{source}>| end - end - end diff --git a/lib/email/message_id_service.rb b/lib/email/message_id_service.rb index fe2af543433..4ba6b0d7375 100644 --- a/lib/email/message_id_service.rb +++ b/lib/email/message_id_service.rb @@ -49,9 +49,8 @@ module Email # inbound email sent to Discourse and handled by Email::Receiver, # this is the only case where we want to use the original Message-ID # because we want to maintain threading in the original mail client. - if use_incoming_email_if_present && - incoming_email&.message_id.present? && - incoming_email&.created_via == IncomingEmail.created_via_types[:handle_mail] + if use_incoming_email_if_present && incoming_email&.message_id.present? && + incoming_email&.created_via == IncomingEmail.created_via_types[:handle_mail] return "<#{first_post.incoming_email.message_id}>" end @@ -100,12 +99,26 @@ module Email # TODO (martin) 2023-01-01 We should remove these backwards-compatible # formats for the Message-ID and solely use the discourse/post/999@host # format. - topic_ids = message_ids.map { |message_id| message_id[message_id_topic_id_regexp, 1] }.compact.map(&:to_i) - post_ids = message_ids.map { |message_id| message_id[message_id_post_id_regexp, 1] }.compact.map(&:to_i) + topic_ids = + message_ids + .map { |message_id| message_id[message_id_topic_id_regexp, 1] } + .compact + .map(&:to_i) + post_ids = + message_ids + .map { |message_id| message_id[message_id_post_id_regexp, 1] } + .compact + .map(&:to_i) - post_ids << message_ids.map { |message_id| message_id[message_id_discourse_regexp, 1] }.compact.map(&:to_i) + post_ids << message_ids + .map { |message_id| message_id[message_id_discourse_regexp, 1] } + .compact + .map(&:to_i) - post_ids << Post.where(outbound_message_id: message_ids).or(Post.where(topic_id: topic_ids, post_number: 1)).pluck(:id) + post_ids << Post + .where(outbound_message_id: message_ids) + .or(Post.where(topic_id: topic_ids, post_number: 1)) + .pluck(:id) post_ids << EmailLog.where(message_id: message_ids).pluck(:post_id) post_ids << IncomingEmail.where(message_id: message_ids).pluck(:post_id) @@ -151,11 +164,15 @@ module Email end def message_id_clean(message_id) - message_id.present? && is_message_id_rfc?(message_id) ? message_id.gsub(/^<|>$/, "") : message_id + if message_id.present? && is_message_id_rfc?(message_id) + message_id.gsub(/^<|>$/, "") + else + message_id + end end def is_message_id_rfc?(message_id) - message_id.start_with?('<') && message_id.include?('@') && message_id.end_with?('>') + message_id.start_with?("<") && message_id.include?("@") && message_id.end_with?(">") end def host diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 4d9f5085d40..9c807fc2b8e 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Email - class Processor attr_reader :receiver @@ -19,7 +18,11 @@ module Email @receiver = Email::Receiver.new(@mail, @opts) @receiver.process! rescue RateLimiter::LimitExceeded - @opts[:retry_on_rate_limit] ? Jobs.enqueue(:process_email, mail: @mail, source: @opts[:source]) : raise + if @opts[:retry_on_rate_limit] + Jobs.enqueue(:process_email, mail: @mail, source: @opts[:source]) + else + raise + end rescue => e return handle_bounce(e) if @receiver.is_bounce? @@ -37,39 +40,70 @@ module Email def handle_bounce(e) # never reply to bounced emails log_email_process_failure(@mail, e) - set_incoming_email_rejection_message(@receiver.incoming_email, I18n.t("emails.incoming.errors.bounced_email_error")) + set_incoming_email_rejection_message( + @receiver.incoming_email, + I18n.t("emails.incoming.errors.bounced_email_error"), + ) end def handle_failure(mail_string, e) - message_template = case e - when Email::Receiver::NoSenderDetectedError then return nil - when Email::Receiver::FromReplyByAddressError then return nil - when Email::Receiver::EmptyEmailError then :email_reject_empty - when Email::Receiver::NoBodyDetectedError then :email_reject_empty - when Email::Receiver::UserNotFoundError then :email_reject_user_not_found - when Email::Receiver::ScreenedEmailError then :email_reject_screened_email - when Email::Receiver::EmailNotAllowed then :email_reject_not_allowed_email - when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated - when Email::Receiver::InactiveUserError then :email_reject_inactive_user - when Email::Receiver::SilencedUserError then :email_reject_silenced_user - when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address - when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed - when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level - when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching - when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found - when Email::Receiver::TopicClosedError then :email_reject_topic_closed - when Email::Receiver::InvalidPost then :email_reject_invalid_post - when Email::Receiver::TooShortPost then :email_reject_post_too_short - when Email::Receiver::UnsubscribeNotAllowed then :email_reject_invalid_post - when ActiveRecord::Rollback then :email_reject_invalid_post - when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action - when Discourse::InvalidAccess then :email_reject_invalid_access - when Email::Receiver::OldDestinationError then :email_reject_old_destination - when Email::Receiver::ReplyNotAllowedError then :email_reject_reply_not_allowed - when Email::Receiver::ReplyToDigestError then :email_reject_reply_to_digest - when Email::Receiver::TooManyRecipientsError then :email_reject_too_many_recipients - else :email_reject_unrecognized_error - end + message_template = + case e + when Email::Receiver::NoSenderDetectedError + return nil + when Email::Receiver::FromReplyByAddressError + return nil + when Email::Receiver::EmptyEmailError + :email_reject_empty + when Email::Receiver::NoBodyDetectedError + :email_reject_empty + when Email::Receiver::UserNotFoundError + :email_reject_user_not_found + when Email::Receiver::ScreenedEmailError + :email_reject_screened_email + when Email::Receiver::EmailNotAllowed + :email_reject_not_allowed_email + when Email::Receiver::AutoGeneratedEmailError + :email_reject_auto_generated + when Email::Receiver::InactiveUserError + :email_reject_inactive_user + when Email::Receiver::SilencedUserError + :email_reject_silenced_user + when Email::Receiver::BadDestinationAddress + :email_reject_bad_destination_address + when Email::Receiver::StrangersNotAllowedError + :email_reject_strangers_not_allowed + when Email::Receiver::InsufficientTrustLevelError + :email_reject_insufficient_trust_level + when Email::Receiver::ReplyUserNotMatchingError + :email_reject_reply_user_not_matching + when Email::Receiver::TopicNotFoundError + :email_reject_topic_not_found + when Email::Receiver::TopicClosedError + :email_reject_topic_closed + when Email::Receiver::InvalidPost + :email_reject_invalid_post + when Email::Receiver::TooShortPost + :email_reject_post_too_short + when Email::Receiver::UnsubscribeNotAllowed + :email_reject_invalid_post + when ActiveRecord::Rollback + :email_reject_invalid_post + when Email::Receiver::InvalidPostAction + :email_reject_invalid_post_action + when Discourse::InvalidAccess + :email_reject_invalid_access + when Email::Receiver::OldDestinationError + :email_reject_old_destination + when Email::Receiver::ReplyNotAllowedError + :email_reject_reply_not_allowed + when Email::Receiver::ReplyToDigestError + :email_reject_reply_to_digest + when Email::Receiver::TooManyRecipientsError + :email_reject_too_many_recipients + else + :email_reject_unrecognized_error + end template_args = {} client_message = nil @@ -85,7 +119,7 @@ module Email end if message_template == :email_reject_unrecognized_error - msg = "Unrecognized error type (#{e.class}: #{e.message}) when processing incoming email" + msg = "Unrecognized error type (#{e.class}: #{e.message}) when processing incoming email" msg += "\n\nBacktrace:\n#{e.backtrace.map { |l| " #{l}" }.join("\n")}" msg += "\n\nMail:\n#{mail_string}" @@ -109,7 +143,8 @@ module Email template_args[:destination] = message.to template_args[:site_name] = SiteSetting.title - client_message = RejectionMailer.send_rejection(message_template, message.from, template_args) + client_message = + RejectionMailer.send_rejection(message_template, message.from, template_args) # only send one rejection email per day to the same email address if can_send_rejection_email?(message.from, message_template) @@ -138,7 +173,7 @@ module Email if incoming_email incoming_email.update!( rejection_message: message, - raw: Email::Cleaner.new(incoming_email.raw, rejected: true).execute + raw: Email::Cleaner.new(incoming_email.raw, rejected: true).execute, ) end end @@ -148,7 +183,5 @@ module Email Rails.logger.warn("Email can not be processed: #{exception}\n\n#{mail_string}") end end - end - end diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index ab318439ba8..05b51c7eb00 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -7,31 +7,56 @@ module Email # If you add a new error, you need to # * add it to Email::Processor#handle_failure() # * add text to server.en.yml (parent key: "emails.incoming.errors") - class ProcessingError < StandardError; end - class EmptyEmailError < ProcessingError; end - class ScreenedEmailError < ProcessingError; end - class UserNotFoundError < ProcessingError; end - class AutoGeneratedEmailError < ProcessingError; end - class BouncedEmailError < ProcessingError; end - class NoBodyDetectedError < ProcessingError; end - class NoSenderDetectedError < ProcessingError; end - class FromReplyByAddressError < ProcessingError; end - class InactiveUserError < ProcessingError; end - class SilencedUserError < ProcessingError; end - class BadDestinationAddress < ProcessingError; end - class StrangersNotAllowedError < ProcessingError; end - class ReplyNotAllowedError < ProcessingError; end - class InsufficientTrustLevelError < ProcessingError; end - class ReplyUserNotMatchingError < ProcessingError; end - class TopicNotFoundError < ProcessingError; end - class TopicClosedError < ProcessingError; end - class InvalidPost < ProcessingError; end - class TooShortPost < ProcessingError; end - class InvalidPostAction < ProcessingError; end - class UnsubscribeNotAllowed < ProcessingError; end - class EmailNotAllowed < ProcessingError; end - class OldDestinationError < ProcessingError; end - class ReplyToDigestError < ProcessingError; end + class ProcessingError < StandardError + end + class EmptyEmailError < ProcessingError + end + class ScreenedEmailError < ProcessingError + end + class UserNotFoundError < ProcessingError + end + class AutoGeneratedEmailError < ProcessingError + end + class BouncedEmailError < ProcessingError + end + class NoBodyDetectedError < ProcessingError + end + class NoSenderDetectedError < ProcessingError + end + class FromReplyByAddressError < ProcessingError + end + class InactiveUserError < ProcessingError + end + class SilencedUserError < ProcessingError + end + class BadDestinationAddress < ProcessingError + end + class StrangersNotAllowedError < ProcessingError + end + class ReplyNotAllowedError < ProcessingError + end + class InsufficientTrustLevelError < ProcessingError + end + class ReplyUserNotMatchingError < ProcessingError + end + class TopicNotFoundError < ProcessingError + end + class TopicClosedError < ProcessingError + end + class InvalidPost < ProcessingError + end + class TooShortPost < ProcessingError + end + class InvalidPostAction < ProcessingError + end + class UnsubscribeNotAllowed < ProcessingError + end + class EmailNotAllowed < ProcessingError + end + class OldDestinationError < ProcessingError + end + class ReplyToDigestError < ProcessingError + end class TooManyRecipientsError < ProcessingError attr_reader :recipients_count @@ -120,7 +145,7 @@ module Email imap_uid_validity: @opts[:imap_uid_validity], imap_uid: @opts[:imap_uid], imap_group_id: @opts[:imap_group_id], - imap_sync: false + imap_sync: false, ) incoming_email @@ -133,9 +158,7 @@ module Email def create_incoming_email cc_addresses = Array.wrap(@mail.cc) - if has_been_forwarded? && embedded_email&.cc - cc_addresses.concat(embedded_email.cc) - end + cc_addresses.concat(embedded_email.cc) if has_been_forwarded? && embedded_email&.cc IncomingEmail.create( message_id: @message_id, raw: Email::Cleaner.new(@raw_email).execute, @@ -147,7 +170,7 @@ module Email imap_uid: @opts[:imap_uid], imap_group_id: @opts[:imap_group_id], imap_sync: false, - created_via: IncomingEmail.created_via_types[@opts[:source] || :unknown] + created_via: IncomingEmail.created_via_types[@opts[:source] || :unknown], ) end @@ -199,13 +222,15 @@ module Email end end - create_reply(user: user, - raw: body, - elided: elided, - post: post, - topic: post.topic, - skip_validations: user.staged?, - bounce: is_bounce?) + create_reply( + user: user, + raw: body, + elided: elided, + post: post, + topic: post.topic, + skip_validations: user.staged?, + bounce: is_bounce?, + ) else first_exception = nil @@ -232,7 +257,9 @@ module Email end end - raise ReplyToDigestError if EmailLog.where(email_type: "digest", message_id: @mail.in_reply_to).exists? + if EmailLog.where(email_type: "digest", message_id: @mail.in_reply_to).exists? + raise ReplyToDigestError + end raise BadDestinationAddress end end @@ -247,7 +274,7 @@ module Email def get_all_recipients(mail) recipients = Set.new - %i(to cc bcc).each do |field| + %i[to cc bcc].each do |field| next if mail[field].blank? mail[field].each do |address_field| @@ -270,10 +297,7 @@ module Email mail_error_statuses = Array.wrap(@mail.error_status) if email_log.present? - email_log.update_columns( - bounced: true, - bounce_error_code: mail_error_statuses.first - ) + email_log.update_columns(bounced: true, bounce_error_code: mail_error_statuses.first) post = email_log.post topic = email_log.topic end @@ -293,13 +317,15 @@ module Email body, elided = select_body body ||= "" - create_reply(user: @from_user, - raw: body, - elided: elided, - post: post, - topic: topic, - skip_validations: true, - bounce: true) + create_reply( + user: @from_user, + raw: body, + elided: elided, + post: post, + topic: topic, + skip_validations: true, + bounce: true, + ) end end @@ -311,10 +337,11 @@ module Email end def bounce_key - @bounce_key ||= begin - verp = all_destinations.select { |to| to[/\+verp-\h{32}@/] }.first - verp && verp[/\+verp-(\h{32})@/, 1] - end + @bounce_key ||= + begin + verp = all_destinations.select { |to| to[/\+verp-\h{32}@/] }.first + verp && verp[/\+verp-(\h{32})@/, 1] + end end def email_log @@ -329,13 +356,19 @@ module Email range = (old_bounce_score + 1..new_bounce_score) user.user_stat.bounce_score = new_bounce_score - user.user_stat.reset_bounce_score_after = SiteSetting.reset_bounce_score_after_days.days.from_now + user.user_stat.reset_bounce_score_after = + SiteSetting.reset_bounce_score_after_days.days.from_now user.user_stat.save! if range === SiteSetting.bounce_score_threshold # NOTE: we check bounce_score before sending emails # So log we revoked the email... - reason = I18n.t("user.email.revoked", email: user.email, date: user.user_stat.reset_bounce_score_after) + reason = + I18n.t( + "user.email.revoked", + email: user.email, + date: user.user_stat.reset_bounce_score_after, + ) StaffActionLogger.new(Discourse.system_user).log_revoke_email(user, reason) # ... and PM the user SystemMessage.create_from_system_user(user, :email_revoked) @@ -344,20 +377,24 @@ module Email end def is_auto_generated? - return false if SiteSetting.auto_generated_allowlist.split('|').include?(@from_email) + return false if SiteSetting.auto_generated_allowlist.split("|").include?(@from_email) @mail[:precedence].to_s[/list|junk|bulk|auto_reply/i] || - @mail[:from].to_s[/(mailer[\-_]?daemon|post[\-_]?master|no[\-_]?reply)@/i] || - @mail[:subject].to_s[/^\s*(Auto:|Automatic reply|Autosvar|Automatisk svar|Automatisch antwoord|Abwesenheitsnotiz|Risposta Non al computer|Automatisch antwoord|Auto Response|Respuesta automática|Fuori sede|Out of Office|Frånvaro|Réponse automatique)/i] || - @mail.header.to_s[/auto[\-_]?(response|submitted|replied|reply|generated|respond)|holidayreply|machinegenerated/i] + @mail[:from].to_s[/(mailer[\-_]?daemon|post[\-_]?master|no[\-_]?reply)@/i] || + @mail[:subject].to_s[ + /^\s*(Auto:|Automatic reply|Autosvar|Automatisk svar|Automatisch antwoord|Abwesenheitsnotiz|Risposta Non al computer|Automatisch antwoord|Auto Response|Respuesta automática|Fuori sede|Out of Office|Frånvaro|Réponse automatique)/i + ] || + @mail.header.to_s[ + /auto[\-_]?(response|submitted|replied|reply|generated|respond)|holidayreply|machinegenerated/i + ] end def is_spam? case SiteSetting.email_in_spam_header - when 'X-Spam-Flag' + when "X-Spam-Flag" @mail[:x_spam_flag].to_s[/YES/i] - when 'X-Spam-Status' + when "X-Spam-Status" @mail[:x_spam_status].to_s[/^Yes, /i] - when 'X-SES-Spam-Verdict' + when "X-SES-Spam-Verdict" @mail[:x_ses_spam_verdict].to_s[/FAIL/i] else false @@ -394,53 +431,62 @@ module Email text_content_type ||= "" converter_opts = { format_flowed: !!(text_content_type =~ /format\s*=\s*["']?flowed["']?/i), - delete_flowed_space: !!(text_content_type =~ /DelSp\s*=\s*["']?yes["']?/i) + delete_flowed_space: !!(text_content_type =~ /DelSp\s*=\s*["']?yes["']?/i), } text = PlainTextToMarkdown.new(text, converter_opts).to_markdown elided_text = PlainTextToMarkdown.new(elided_text, converter_opts).to_markdown end end - markdown, elided_markdown = if html.present? - # use the first html extracter that matches - if html_extracter = HTML_EXTRACTERS.select { |_, r| html[r] }.min_by { |_, r| html =~ r } - doc = Nokogiri::HTML5.fragment(html) - self.public_send(:"extract_from_#{html_extracter[0]}", doc) - else - markdown = HtmlToMarkdown.new(html, keep_img_tags: true, keep_cid_imgs: true).to_markdown - markdown = trim_discourse_markers(markdown) - trim_reply_and_extract_elided(markdown) + markdown, elided_markdown = + if html.present? + # use the first html extracter that matches + if html_extracter = HTML_EXTRACTERS.select { |_, r| html[r] }.min_by { |_, r| html =~ r } + doc = Nokogiri::HTML5.fragment(html) + self.public_send(:"extract_from_#{html_extracter[0]}", doc) + else + markdown = + HtmlToMarkdown.new(html, keep_img_tags: true, keep_cid_imgs: true).to_markdown + markdown = trim_discourse_markers(markdown) + trim_reply_and_extract_elided(markdown) + end end - end - text_format = Receiver::formats[:plaintext] + text_format = Receiver.formats[:plaintext] if text.blank? || (SiteSetting.incoming_email_prefer_html && markdown.present?) - text, elided_text, text_format = markdown, elided_markdown, Receiver::formats[:markdown] + text, elided_text, text_format = markdown, elided_markdown, Receiver.formats[:markdown] end if SiteSetting.strip_incoming_email_lines && text.present? in_code = nil - text = text.lines.map! do |line| - stripped = line.strip << "\n" + text = + text + .lines + .map! do |line| + stripped = line.strip << "\n" - # Do not strip list items. - next line if (stripped[0] == '*' || stripped[0] == '-' || stripped[0] == '+') && stripped[1] == ' ' + # Do not strip list items. + if (stripped[0] == "*" || stripped[0] == "-" || stripped[0] == "+") && + stripped[1] == " " + next line + end - # Match beginning and ending of code blocks. - if !in_code && stripped[0..2] == '```' - in_code = '```' - elsif in_code == '```' && stripped[0..2] == '```' - in_code = nil - elsif !in_code && stripped[0..4] == '[code' - in_code = '[code]' - elsif in_code == '[code]' && stripped[0..6] == '[/code]' - in_code = nil - end + # Match beginning and ending of code blocks. + if !in_code && stripped[0..2] == "```" + in_code = "```" + elsif in_code == "```" && stripped[0..2] == "```" + in_code = nil + elsif !in_code && stripped[0..4] == "[code" + in_code = "[code]" + elsif in_code == "[code]" && stripped[0..6] == "[/code]" + in_code = nil + end - # Strip only lines outside code blocks. - in_code ? line : stripped - end.join + # Strip only lines outside code blocks. + in_code ? line : stripped + end + .join end [text, elided_text, text_format] @@ -448,7 +494,8 @@ module Email def to_markdown(html, elided_html) markdown = HtmlToMarkdown.new(html, keep_img_tags: true, keep_cid_imgs: true).to_markdown - elided_markdown = HtmlToMarkdown.new(elided_html, keep_img_tags: true, keep_cid_imgs: true).to_markdown + elided_markdown = + HtmlToMarkdown.new(elided_html, keep_img_tags: true, keep_cid_imgs: true).to_markdown [EmailReplyTrimmer.trim(markdown), elided_markdown] end @@ -481,7 +528,10 @@ module Email def extract_from_word(doc) # Word (?) keeps the content in the 'WordSection1' class and uses

tags # When there's something else (,
, etc..) there's high chance it's a signature or forwarded email - elided = doc.css(".WordSection1 > :not(p):not(ul):first-of-type, .WordSection1 > :not(p):not(ul):first-of-type ~ *").remove + elided = + doc.css( + ".WordSection1 > :not(p):not(ul):first-of-type, .WordSection1 > :not(p):not(ul):first-of-type ~ *", + ).remove to_markdown(doc.at(".WordSection1").to_html, elided.to_html) end @@ -502,9 +552,12 @@ module Email def extract_from_mozilla(doc) # Mozilla (Thunderbird ?) properly identifies signature and forwarded emails # Remove them and anything that comes after - elided = doc.css("*[class^='moz-cite'], *[class^='moz-cite'] ~ *, " \ - "*[class^='moz-signature'], *[class^='moz-signature'] ~ *, " \ - "*[class^='moz-forward'], *[class^='moz-forward'] ~ *").remove + elided = + doc.css( + "*[class^='moz-cite'], *[class^='moz-cite'] ~ *, " \ + "*[class^='moz-signature'], *[class^='moz-signature'] ~ *, " \ + "*[class^='moz-forward'], *[class^='moz-forward'] ~ *", + ).remove to_markdown(doc.to_html, elided.to_html) end @@ -533,14 +586,19 @@ module Email end def trim_reply_and_extract_elided(text) - return [text, ""] if @opts[:skip_trimming] || !SiteSetting.trim_incoming_emails + return text, "" if @opts[:skip_trimming] || !SiteSetting.trim_incoming_emails EmailReplyTrimmer.trim(text, true) end def fix_charset(mail_part) return nil if mail_part.blank? || mail_part.body.blank? - string = mail_part.body.decoded rescue nil + string = + begin + mail_part.body.decoded + rescue StandardError + nil + end return nil if string.blank? @@ -572,23 +630,32 @@ module Email end def previous_replies_regex - strings = I18n.available_locales.map do |locale| - I18n.with_locale(locale) { I18n.t("user_notifications.previous_discussion") } - end.uniq + strings = + I18n + .available_locales + .map do |locale| + I18n.with_locale(locale) { I18n.t("user_notifications.previous_discussion") } + end + .uniq - @previous_replies_regex ||= /^--[- ]\n\*(?:#{strings.map { |x| Regexp.escape(x) }.join("|")})\*\n/im + @previous_replies_regex ||= + /^--[- ]\n\*(?:#{strings.map { |x| Regexp.escape(x) }.join("|")})\*\n/im end def reply_above_line_regex - strings = I18n.available_locales.map do |locale| - I18n.with_locale(locale) { I18n.t("user_notifications.reply_above_line") } - end.uniq + strings = + I18n + .available_locales + .map do |locale| + I18n.with_locale(locale) { I18n.t("user_notifications.reply_above_line") } + end + .uniq @reply_above_line_regex ||= /\n(?:#{strings.map { |x| Regexp.escape(x) }.join("|")})\n/im end def trim_discourse_markers(reply) - return '' if reply.blank? + return "" if reply.blank? reply = reply.split(previous_replies_regex)[0] reply.split(reply_above_line_regex)[0] end @@ -598,11 +665,9 @@ module Email if email_log.present? email = email_log.to_address || email_log.user&.email - return [email, email_log.user&.username] + return email, email_log.user&.username elsif mail.bounced? - Array.wrap(mail.final_recipient).each do |from| - return extract_from_address_and_name(from) - end + Array.wrap(mail.final_recipient).each { |from| return extract_from_address_and_name(from) } end return unless mail[:from] @@ -613,7 +678,7 @@ module Email if has_been_forwarded? if mail[:from].to_s =~ group_incoming_emails_regex && embedded_email[:from].errors.blank? from_address, from_display_name = extract_from_fields_from_header(embedded_email, :from) - return [from_address, from_display_name] if from_address + return from_address, from_display_name if from_address end end @@ -621,18 +686,21 @@ module Email # been forwarded via Google Groups, which is why we are checking the # X-Original-From header too. In future we may want to use the Reply-To # header in more cases. - if mail['X-Original-From'].present? + if mail["X-Original-From"].present? if mail[:reply_to] && mail[:reply_to].errors.blank? - from_address, from_display_name = extract_from_fields_from_header( - mail, :reply_to, comparison_headers: ['X-Original-From'] - ) - return [from_address, from_display_name] if from_address + from_address, from_display_name = + extract_from_fields_from_header( + mail, + :reply_to, + comparison_headers: ["X-Original-From"], + ) + return from_address, from_display_name if from_address end end if mail[:from].errors.blank? from_address, from_display_name = extract_from_fields_from_header(mail, :from) - return [from_address, from_display_name] if from_address + return from_address, from_display_name if from_address end return extract_from_address_and_name(mail.from) if mail.from.is_a? String @@ -640,7 +708,7 @@ module Email if mail.from.is_a? Mail::AddressContainer mail.from.each do |from| from_address, from_display_name = extract_from_address_and_name(from) - return [from_address, from_display_name] if from_address + return from_address, from_display_name if from_address end end @@ -665,7 +733,7 @@ module Email next if comparison_failed next if !from_address&.include?("@") - return [from_address&.downcase, from_display_name&.strip] + return from_address&.downcase, from_display_name&.strip end [nil, nil] @@ -674,7 +742,7 @@ module Email def extract_from_address_and_name(value) if value[";"] from_display_name, from_address = value.split(";") - return [from_address&.strip&.downcase, from_display_name&.strip] + return from_address&.strip&.downcase, from_display_name&.strip end if value[/<[^>]+>/] @@ -708,12 +776,13 @@ module Email username = UserNameSuggester.sanitize_username(display_name) if display_name.present? begin - user = User.create!( - email: email, - username: UserNameSuggester.suggest(username.presence || email), - name: display_name.presence || User.suggest_name(email), - staged: true - ) + user = + User.create!( + email: email, + username: UserNameSuggester.suggest(username.presence || email), + name: display_name.presence || User.suggest_name(email), + staged: true, + ) @created_staged_users << user rescue PG::UniqueViolation, ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid raise if raise_on_failed_create @@ -740,19 +809,19 @@ module Email end def destinations - @destinations ||= all_destinations - .map { |d| Email::Receiver.check_address(d, is_bounce?) } - .reject(&:blank?) + @destinations ||= + all_destinations.map { |d| Email::Receiver.check_address(d, is_bounce?) }.reject(&:blank?) end def sent_to_mailinglist_mirror? - @sent_to_mailinglist_mirror ||= begin - destinations.each do |destination| - return true if destination.is_a?(Category) && destination.mailinglist_mirror? - end + @sent_to_mailinglist_mirror ||= + begin + destinations.each do |destination| + return true if destination.is_a?(Category) && destination.mailinglist_mirror? + end - false - end + false + end end def self.check_address(address, include_verp = false) @@ -778,9 +847,10 @@ module Email end def process_destination(destination, user, body, elided) - return if SiteSetting.forwarded_emails_behaviour != "hide" && - has_been_forwarded? && - process_forwarded_email(destination, user) + if SiteSetting.forwarded_emails_behaviour != "hide" && has_been_forwarded? && + process_forwarded_email(destination, user) + return + end return if is_bounce? && !destination.is_a?(PostReplyKey) @@ -788,19 +858,24 @@ module Email user ||= stage_from_user create_group_post(destination, user, body, elided) elsif destination.is_a?(Category) - raise StrangersNotAllowedError if (user.nil? || user.staged?) && !destination.email_in_allow_strangers + if (user.nil? || user.staged?) && !destination.email_in_allow_strangers + raise StrangersNotAllowedError + end user ||= stage_from_user - raise InsufficientTrustLevelError if !user.has_trust_level?(SiteSetting.email_in_min_trust) && !sent_to_mailinglist_mirror? - - create_topic(user: user, - raw: body, - elided: elided, - title: subject, - category: destination.id, - skip_validations: user.staged?) + if !user.has_trust_level?(SiteSetting.email_in_min_trust) && !sent_to_mailinglist_mirror? + raise InsufficientTrustLevelError + end + create_topic( + user: user, + raw: body, + elided: elided, + title: subject, + category: destination.id, + skip_validations: user.staged?, + ) elsif destination.is_a?(PostReplyKey) # We don't stage new users for emails to reply addresses, exit if user is nil raise BadDestinationAddress if user.blank? @@ -809,16 +884,19 @@ module Email raise ReplyNotAllowedError if !Guardian.new(user).can_create_post?(post&.topic) if destination.user_id != user.id && !forwarded_reply_key?(destination, user) - raise ReplyUserNotMatchingError, "post_reply_key.user_id => #{destination.user_id.inspect}, user.id => #{user.id.inspect}" + raise ReplyUserNotMatchingError, + "post_reply_key.user_id => #{destination.user_id.inspect}, user.id => #{user.id.inspect}" end - create_reply(user: user, - raw: body, - elided: elided, - post: post, - topic: post&.topic, - skip_validations: user.staged?, - bounce: is_bounce?) + create_reply( + user: user, + raw: body, + elided: elided, + post: post, + topic: post&.topic, + skip_validations: user.staged?, + bounce: is_bounce?, + ) end end @@ -839,11 +917,14 @@ module Email # there will be a corresponding EmailLog record, so we can use that as the # reply post if it exists if Email::MessageIdService.discourse_generated_message_id?(mail.in_reply_to) - post_id_from_email_log = EmailLog.where(message_id: mail.in_reply_to) - .addressed_to_user(user) - .order(created_at: :desc) - .limit(1) - .pluck(:post_id).last + post_id_from_email_log = + EmailLog + .where(message_id: mail.in_reply_to) + .addressed_to_user(user) + .order(created_at: :desc) + .limit(1) + .pluck(:post_id) + .last post_ids << post_id_from_email_log if post_id_from_email_log end @@ -851,14 +932,16 @@ module Email too_old_for_group_smtp = (destination_too_old?(target_post) && group.smtp_enabled) if target_post.blank? || too_old_for_group_smtp - create_topic(user: user, - raw: new_group_topic_body(body, target_post, too_old_for_group_smtp), - elided: elided, - title: subject, - archetype: Archetype.private_message, - target_group_names: [group.name], - is_group_message: true, - skip_validations: true) + create_topic( + user: user, + raw: new_group_topic_body(body, target_post, too_old_for_group_smtp), + elided: elided, + title: subject, + archetype: Archetype.private_message, + target_group_names: [group.name], + is_group_message: true, + skip_validations: true, + ) else # This must be done for the unknown user (who is staged) to # be allowed to post a reply in the topic. @@ -866,39 +949,47 @@ module Email target_post.topic.topic_allowed_users.find_or_create_by!(user_id: user.id) end - create_reply(user: user, - raw: body, - elided: elided, - post: target_post, - topic: target_post.topic, - skip_validations: true) + create_reply( + user: user, + raw: body, + elided: elided, + post: target_post, + topic: target_post.topic, + skip_validations: true, + ) end end def new_group_topic_body(body, target_post, too_old_for_group_smtp) return body if !too_old_for_group_smtp - body + "\n\n----\n\n" + I18n.t( - "emails.incoming.continuing_old_discussion", - url: target_post.topic.url, - title: target_post.topic.title, - count: SiteSetting.disallow_reply_by_email_after_days - ) + body + "\n\n----\n\n" + + I18n.t( + "emails.incoming.continuing_old_discussion", + url: target_post.topic.url, + title: target_post.topic.title, + count: SiteSetting.disallow_reply_by_email_after_days, + ) end def forwarded_reply_key?(post_reply_key, user) - incoming_emails = IncomingEmail - .joins(:post) - .where('posts.topic_id = ?', post_reply_key.post.topic_id) - .addressed_to(post_reply_key.reply_key) - .addressed_to_user(user) - .pluck(:to_addresses, :cc_addresses) + incoming_emails = + IncomingEmail + .joins(:post) + .where("posts.topic_id = ?", post_reply_key.post.topic_id) + .addressed_to(post_reply_key.reply_key) + .addressed_to_user(user) + .pluck(:to_addresses, :cc_addresses) incoming_emails.each do |to_addresses, cc_addresses| - next unless contains_email_address_of_user?(to_addresses, user) || - contains_email_address_of_user?(cc_addresses, user) + unless contains_email_address_of_user?(to_addresses, user) || + contains_email_address_of_user?(cc_addresses, user) + next + end - return true if contains_reply_by_email_address(to_addresses, post_reply_key.reply_key) || - contains_reply_by_email_address(cc_addresses, post_reply_key.reply_key) + if contains_reply_by_email_address(to_addresses, post_reply_key.reply_key) || + contains_reply_by_email_address(cc_addresses, post_reply_key.reply_key) + return true + end end false @@ -914,10 +1005,12 @@ module Email def contains_reply_by_email_address(addresses, reply_key) return false if addresses.blank? - addresses.split(";").each do |address| - match = Email::Receiver.reply_by_email_address_regex.match(address) - return true if match && match.captures&.include?(reply_key) - end + addresses + .split(";") + .each do |address| + match = Email::Receiver.reply_by_email_address_regex.match(address) + return true if match && match.captures&.include?(reply_key) + end false end @@ -934,13 +1027,14 @@ module Email end def embedded_email - @embedded_email ||= if embedded_email_raw.present? - mail = Mail.new(embedded_email_raw) - Email::Validator.ensure_valid_address_lists!(mail) - mail - else - nil - end + @embedded_email ||= + if embedded_email_raw.present? + mail = Mail.new(embedded_email_raw) + Email::Validator.ensure_valid_address_lists!(mail) + mail + else + nil + end end def process_forwarded_email(destination, user) @@ -955,30 +1049,40 @@ module Email end end - def forwarded_email_create_topic(destination: , user: , raw: , title: , date: nil, embedded_user: nil) + def forwarded_email_create_topic( + destination:, + user:, + raw:, + title:, + date: nil, + embedded_user: nil + ) if destination.is_a?(Group) topic_user = embedded_user&.call || user - create_topic(user: topic_user, - raw: raw, - title: title, - archetype: Archetype.private_message, - target_usernames: [user.username], - target_group_names: [destination.name], - is_group_message: true, - skip_validations: true, - created_at: date) - + create_topic( + user: topic_user, + raw: raw, + title: title, + archetype: Archetype.private_message, + target_usernames: [user.username], + target_group_names: [destination.name], + is_group_message: true, + skip_validations: true, + created_at: date, + ) elsif destination.is_a?(Category) return false if user.staged? && !destination.email_in_allow_strangers return false if !user.has_trust_level?(SiteSetting.email_in_min_trust) topic_user = embedded_user&.call || user - create_topic(user: topic_user, - raw: raw, - title: title, - category: destination.id, - skip_validations: topic_user.staged?, - created_at: date) + create_topic( + user: topic_user, + raw: raw, + title: title, + category: destination.id, + skip_validations: topic_user.staged?, + created_at: date, + ) else false end @@ -994,29 +1098,40 @@ module Email return false if email.blank? || !email["@"] - post = forwarded_email_create_topic(destination: destination, - user: user, - raw: try_to_encode(embedded.decoded, "UTF-8").presence || embedded.to_s, - title: embedded.subject.presence || subject, - date: embedded.date, - embedded_user: lambda { find_or_create_user(email, display_name) }) + post = + forwarded_email_create_topic( + destination: destination, + user: user, + raw: try_to_encode(embedded.decoded, "UTF-8").presence || embedded.to_s, + title: embedded.subject.presence || subject, + date: embedded.date, + embedded_user: lambda { find_or_create_user(email, display_name) }, + ) return false unless post if post.topic # mark post as seen for the forwarder - PostTiming.record_timing(user_id: user.id, topic_id: post.topic_id, post_number: post.post_number, msecs: 5000) + PostTiming.record_timing( + user_id: user.id, + topic_id: post.topic_id, + post_number: post.post_number, + msecs: 5000, + ) # create reply when available if @before_embedded.present? post_type = Post.types[:regular] - post_type = Post.types[:whisper] if post.topic.private_message? && destination.usernames[user.username] + post_type = Post.types[:whisper] if post.topic.private_message? && + destination.usernames[user.username] - create_reply(user: user, - raw: @before_embedded, - post: post, - topic: post.topic, - post_type: post_type, - skip_validations: user.staged?) + create_reply( + user: user, + raw: @before_embedded, + post: post, + topic: post.topic, + post_type: post_type, + skip_validations: user.staged?, + ) else if @forwarded_by_user post.topic.topic_allowed_users.find_or_create_by!(user_id: @forwarded_by_user.id) @@ -1050,15 +1165,28 @@ module Email [/quote] MD - return true if forwarded_email_create_topic(destination: destination, user: user, raw: raw, title: subject) + if forwarded_email_create_topic( + destination: destination, + user: user, + raw: raw, + title: subject, + ) + true + end end def self.reply_by_email_address_regex(extract_reply_key = true, include_verp = false) reply_addresses = [SiteSetting.reply_by_email_address] - reply_addresses << (SiteSetting.alternative_reply_by_email_addresses.presence || "").split("|") + reply_addresses << (SiteSetting.alternative_reply_by_email_addresses.presence || "").split( + "|", + ) - if include_verp && SiteSetting.reply_by_email_address.present? && SiteSetting.reply_by_email_address["+"] - reply_addresses << SiteSetting.reply_by_email_address.sub("%{reply_key}", "verp-%{reply_key}") + if include_verp && SiteSetting.reply_by_email_address.present? && + SiteSetting.reply_by_email_address["+"] + reply_addresses << SiteSetting.reply_by_email_address.sub( + "%{reply_key}", + "verp-%{reply_key}", + ) end reply_addresses.flatten! @@ -1074,17 +1202,22 @@ module Email end def group_incoming_emails_regex - @group_incoming_emails_regex = Regexp.union( - DB.query_single(<<~SQL).map { |e| e.split("|") }.flatten.compact_blank.uniq + @group_incoming_emails_regex = + Regexp.union(DB.query_single(<<~SQL).map { |e| e.split("|") }.flatten.compact_blank.uniq) SELECT CONCAT(incoming_email, '|', email_username) FROM groups WHERE incoming_email IS NOT NULL OR email_username IS NOT NULL SQL - ) end def category_email_in_regex - @category_email_in_regex ||= Regexp.union Category.pluck(:email_in).select(&:present?).map { |e| e.split("|") }.flatten.uniq + @category_email_in_regex ||= + Regexp.union Category + .pluck(:email_in) + .select(&:present?) + .map { |e| e.split("|") } + .flatten + .uniq end def find_related_post(force: false) @@ -1108,21 +1241,19 @@ module Email if Array === references references elsif references.present? - references.split(/[\s,]/).map do |r| - Email::MessageIdService.message_id_clean(r) - end + references.split(/[\s,]/).map { |r| Email::MessageIdService.message_id_clean(r) } end end def likes - @likes ||= Set.new ["+1", "<3", "❤", I18n.t('post_action_types.like.title').downcase] + @likes ||= Set.new ["+1", "<3", "❤", I18n.t("post_action_types.like.title").downcase] end def subscription_action_for(body, subject) return unless SiteSetting.unsubscribe_via_email return if sent_to_mailinglist_mirror? - if ([subject, body].compact.map(&:to_s).map(&:downcase) & ['unsubscribe']).any? + if ([subject, body].compact.map(&:to_s).map(&:downcase) & ["unsubscribe"]).any? :confirm_unsubscribe end end @@ -1132,9 +1263,7 @@ module Email end def create_topic(options = {}) - if options[:archetype] == Archetype.private_message - enable_email_pm_setting(options[:user]) - end + enable_email_pm_setting(options[:user]) if options[:archetype] == Archetype.private_message create_post_with_attachments(options) end @@ -1151,26 +1280,36 @@ module Email NotificationLevels.topic_levels[:tracking] when "watch" NotificationLevels.topic_levels[:watching] - else nil + else + nil end end def create_reply(options = {}) raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed? - raise BouncedEmailError if options[:bounce] && options[:topic].archetype != Archetype.private_message + if options[:bounce] && options[:topic].archetype != Archetype.private_message + raise BouncedEmailError + end options[:post] = nil if options[:post]&.trashed? - enable_email_pm_setting(options[:user]) if options[:topic].archetype == Archetype.private_message + if options[:topic].archetype == Archetype.private_message + enable_email_pm_setting(options[:user]) + end if post_action_type = post_action_for(options[:raw]) create_post_action(options[:user], options[:post], post_action_type) elsif notification_level = notification_level_for(options[:raw]) - TopicUser.change(options[:user].id, options[:post].topic_id, notification_level: notification_level) + TopicUser.change( + options[:user].id, + options[:post].topic_id, + notification_level: notification_level, + ) else raise TopicClosedError if options[:topic].closed? options[:topic_id] = options[:topic].id options[:reply_to_post_number] = options[:post]&.post_number - options[:is_group_message] = options[:topic].private_message? && options[:topic].allowed_groups.exists? + options[:is_group_message] = options[:topic].private_message? && + options[:topic].allowed_groups.exists? create_post_with_attachments(options) end end @@ -1182,21 +1321,20 @@ module Email def is_allowed?(attachment) attachment.content_type !~ SiteSetting.blocked_attachment_content_types_regex && - attachment.filename !~ SiteSetting.blocked_attachment_filenames_regex + attachment.filename !~ SiteSetting.blocked_attachment_filenames_regex end def attachments - @attachments ||= begin - attachments = @mail.attachments.select { |attachment| is_allowed?(attachment) } - attachments << @mail if @mail.attachment? && is_allowed?(@mail) + @attachments ||= + begin + attachments = @mail.attachments.select { |attachment| is_allowed?(attachment) } + attachments << @mail if @mail.attachment? && is_allowed?(@mail) - @mail.parts.each do |part| - attachments << part if part.attachment? && is_allowed?(part) + @mail.parts.each { |part| attachments << part if part.attachment? && is_allowed?(part) } + + attachments.uniq! + attachments end - - attachments.uniq! - attachments - end end def create_post_with_attachments(options = {}) @@ -1223,10 +1361,13 @@ module Email if raw[attachment.url] raw.sub!(attachment.url, upload.url) - InlineUploads.match_img(raw, uploads: { upload.url => upload }) do |match, src, replacement, _| - if src == upload.url - raw = raw.sub(match, replacement) - end + InlineUploads.match_img( + raw, + uploads: { + upload.url => upload, + }, + ) do |match, src, replacement, _| + raw = raw.sub(match, replacement) if src == upload.url end elsif raw[/\[image:[^\]]*\]/i] raw.sub!(/\[image:[^\]]*\]/i, UploadMarkdown.new(upload).to_markdown) @@ -1238,13 +1379,15 @@ module Email end else rejected_attachments << upload - raw << "\n\n#{I18n.t('emails.incoming.missing_attachment', filename: upload.original_filename)}\n\n" + raw << "\n\n#{I18n.t("emails.incoming.missing_attachment", filename: upload.original_filename)}\n\n" end ensure tmp&.close! end end - notify_about_rejected_attachment(rejected_attachments) if rejected_attachments.present? && !user.staged? + if rejected_attachments.present? && !user.staged? + notify_about_rejected_attachment(rejected_attachments) + end raw end @@ -1262,19 +1405,21 @@ module Email former_title: message.subject, destination: message.to, site_name: SiteSetting.title, - rejected_errors: errors.join("\n") + rejected_errors: errors.join("\n"), } - client_message = RejectionMailer.send_rejection(:email_reject_attachment, message.from, template_args) + client_message = + RejectionMailer.send_rejection(:email_reject_attachment, message.from, template_args) Email::Sender.new(client_message, :email_reject_attachment).send end def add_elided_to_raw!(options) - is_private_message = options[:archetype] == Archetype.private_message || - options[:topic].try(:private_message?) + is_private_message = + options[:archetype] == Archetype.private_message || options[:topic].try(:private_message?) # only add elided part in messages - if options[:elided].present? && (SiteSetting.always_show_trimmed_content || is_private_message) + if options[:elided].present? && + (SiteSetting.always_show_trimmed_content || is_private_message) options[:raw] << Email::Receiver.elided_html(options[:elided]) options[:elided] = "" end @@ -1303,7 +1448,11 @@ module Email user = options.delete(:user) if options[:bounce] - options[:raw] = I18n.t("system_messages.email_bounced", email: user.email, raw: options[:raw]) + options[:raw] = I18n.t( + "system_messages.email_bounced", + email: user.email, + raw: options[:raw], + ) user = Discourse.system_user options[:post_type] = Post.types[:whisper] end @@ -1316,16 +1465,16 @@ module Email result = NewPostManager.new(user, options).perform errors = result.errors.full_messages - if errors.any? do |message| + if errors.any? { |message| message.include?(I18n.t("activerecord.attributes.post.raw").strip) && - message.include?(I18n.t("errors.messages.too_short", count: SiteSetting.min_post_length).strip) - end + message.include?( + I18n.t("errors.messages.too_short", count: SiteSetting.min_post_length).strip, + ) + } raise TooShortPost end - if result.errors.present? - raise InvalidPost, errors.join("\n") - end + raise InvalidPost, errors.join("\n") if result.errors.present? if result.post IncomingEmail.transaction do @@ -1343,11 +1492,16 @@ module Email # Alert the people involved in the topic now that the incoming email # has been linked to the post. - PostJobsEnqueuer.new(result.post, result.post.topic, options[:topic_id].blank?, - import_mode: options[:import_mode], - post_alert_options: options[:post_alert_options] - ).enqueue_jobs - DiscourseEvent.trigger(:topic_created, result.post.topic, options, user) if result.post.is_first_post? + PostJobsEnqueuer.new( + result.post, + result.post.topic, + options[:topic_id].blank?, + import_mode: options[:import_mode], + post_alert_options: options[:post_alert_options], + ).enqueue_jobs + if result.post.is_first_post? + DiscourseEvent.trigger(:topic_created, result.post.topic, options, user) + end DiscourseEvent.trigger(:post_created, result.post, options, user) end @@ -1355,8 +1509,8 @@ module Email end def self.elided_html(elided) - html = +"\n\n" << "
" << "\n" - html << "···" << "\n\n" + html = +"\n\n" << "
" << "\n" + html << "···" << "\n\n" html << elided << "\n\n" html << "
" << "\n" html @@ -1365,7 +1519,7 @@ module Email def add_other_addresses(post, sender, mail_object) max_staged_users_post = nil - %i(to cc bcc).each do |d| + %i[to cc bcc].each do |d| next if mail_object[d].blank? mail_object[d].each do |address_field| @@ -1379,16 +1533,31 @@ module Email user = User.find_by_email(email) # cap number of staged users created per email - if (!user || user.staged) && @staged_users.count >= SiteSetting.maximum_staged_users_per_email - max_staged_users_post ||= post.topic.add_moderator_post(sender, I18n.t("emails.incoming.maximum_staged_user_per_email_reached"), import_mode: @opts[:import_mode]) + if (!user || user.staged) && + @staged_users.count >= SiteSetting.maximum_staged_users_per_email + max_staged_users_post ||= + post.topic.add_moderator_post( + sender, + I18n.t("emails.incoming.maximum_staged_user_per_email_reached"), + import_mode: @opts[:import_mode], + ) next end user = find_or_create_user(email, display_name, user: user) if user && can_invite?(post.topic, user) post.topic.topic_allowed_users.create!(user_id: user.id) - TopicUser.auto_notification_for_staging(user.id, post.topic_id, TopicUser.notification_reasons[:auto_watch]) - post.topic.add_small_action(sender, "invited_user", user.username, import_mode: @opts[:import_mode]) + TopicUser.auto_notification_for_staging( + user.id, + post.topic_id, + TopicUser.notification_reasons[:auto_watch], + ) + post.topic.add_small_action( + sender, + "invited_user", + user.username, + import_mode: @opts[:import_mode], + ) end end rescue ActiveRecord::RecordInvalid, EmailNotAllowed @@ -1400,13 +1569,15 @@ module Email def should_invite?(email) email !~ Email::Receiver.reply_by_email_address_regex && - email !~ group_incoming_emails_regex && - email !~ category_email_in_regex + email !~ group_incoming_emails_regex && email !~ category_email_in_regex end def can_invite?(topic, user) !topic.topic_allowed_users.where(user_id: user.id).exists? && - !topic.topic_allowed_groups.where("group_id IN (SELECT group_id FROM group_users WHERE user_id = ?)", user.id).exists? + !topic + .topic_allowed_groups + .where("group_id IN (SELECT group_id FROM group_users WHERE user_id = ?)", user.id) + .exists? end def send_subscription_mail(action, user) @@ -1419,26 +1590,21 @@ module Email end def stage_sender_user(email, display_name) - find_or_create_user!(email, display_name).tap do |u| - log_and_validate_user(u) - end + find_or_create_user!(email, display_name).tap { |u| log_and_validate_user(u) } end def delete_created_staged_users @created_staged_users.each do |user| - if @incoming_email.user&.id == user.id - @incoming_email.update_columns(user_id: nil) - end + @incoming_email.update_columns(user_id: nil) if @incoming_email.user&.id == user.id - if user.posts.count == 0 - UserDestroyer.new(Discourse.system_user).destroy(user, quiet: true) - end + UserDestroyer.new(Discourse.system_user).destroy(user, quiet: true) if user.posts.count == 0 end end def enable_email_pm_setting(user) # ensure user PM emails are enabled (since user is posting via email) - if !user.staged && user.user_option.email_messages_level == UserOption.email_level_types[:never] + if !user.staged && + user.user_option.email_messages_level == UserOption.email_level_types[:never] user.user_option.update!(email_messages_level: UserOption.email_level_types[:always]) end end diff --git a/lib/email/renderer.rb b/lib/email/renderer.rb index feeb63c4112..a375176e2c8 100644 --- a/lib/email/renderer.rb +++ b/lib/email/renderer.rb @@ -2,7 +2,6 @@ module Email class Renderer - def initialize(message, opts = nil) @message = message @opts = opts || {} @@ -10,26 +9,30 @@ module Email def text return @text if @text - @text = (+(@message.text_part ? @message.text_part : @message).body.to_s).force_encoding('UTF-8') + @text = + (+(@message.text_part ? @message.text_part : @message).body.to_s).force_encoding("UTF-8") @text = CGI.unescapeHTML(@text) end def html - style = if @message.html_part - Email::Styles.new(@message.html_part.body.to_s, @opts) - else - unstyled = UserNotificationRenderer.render( - template: 'layouts/email_template', - format: :html, - locals: { html_body: PrettyText.cook(text).html_safe } - ) - Email::Styles.new(unstyled, @opts) - end + style = + if @message.html_part + Email::Styles.new(@message.html_part.body.to_s, @opts) + else + unstyled = + UserNotificationRenderer.render( + template: "layouts/email_template", + format: :html, + locals: { + html_body: PrettyText.cook(text).html_safe, + }, + ) + Email::Styles.new(unstyled, @opts) + end style.format_basic style.format_html style.to_html end - end end diff --git a/lib/email/sender.rb b/lib/email/sender.rb index c31df8b76ac..1f3c59efd04 100644 --- a/lib/email/sender.rb +++ b/lib/email/sender.rb @@ -8,11 +8,11 @@ # # It also adds an HTML part for the plain text body # -require 'uri' -require 'net/smtp' +require "uri" +require "net/smtp" SMTP_CLIENT_ERRORS = [Net::SMTPFatalError, Net::SMTPSyntaxError] -BYPASS_DISABLE_TYPES = %w( +BYPASS_DISABLE_TYPES = %w[ admin_login test_message new_version @@ -20,11 +20,10 @@ BYPASS_DISABLE_TYPES = %w( invite_password_instructions download_backup_message admin_confirmation_message -) +] module Email class Sender - def initialize(message, email_type, user = nil) @message = message @message_attachments_index = {} @@ -35,33 +34,40 @@ module Email def send bypass_disable = BYPASS_DISABLE_TYPES.include?(@email_type.to_s) - if SiteSetting.disable_emails == "yes" && !bypass_disable + return if SiteSetting.disable_emails == "yes" && !bypass_disable + + return if ActionMailer::Base::NullMail === @message + if ActionMailer::Base::NullMail === + ( + begin + @message.message + rescue StandardError + nil + end + ) return end - return if ActionMailer::Base::NullMail === @message - return if ActionMailer::Base::NullMail === (@message.message rescue nil) - - return skip(SkippedEmailLog.reason_types[:sender_message_blank]) if @message.blank? + return skip(SkippedEmailLog.reason_types[:sender_message_blank]) if @message.blank? return skip(SkippedEmailLog.reason_types[:sender_message_to_blank]) if @message.to.blank? if SiteSetting.disable_emails == "non-staff" && !bypass_disable return unless find_user&.staff? end - return skip(SkippedEmailLog.reason_types[:sender_message_to_invalid]) if to_address.end_with?(".invalid") + if to_address.end_with?(".invalid") + return skip(SkippedEmailLog.reason_types[:sender_message_to_invalid]) + end if @message.text_part if @message.text_part.body.to_s.blank? return skip(SkippedEmailLog.reason_types[:sender_text_part_body_blank]) end else - if @message.body.to_s.blank? - return skip(SkippedEmailLog.reason_types[:sender_body_blank]) - end + return skip(SkippedEmailLog.reason_types[:sender_body_blank]) if @message.body.to_s.blank? end - @message.charset = 'UTF-8' + @message.charset = "UTF-8" opts = {} @@ -70,50 +76,58 @@ module Email if @message.html_part @message.html_part.body = renderer.html else - @message.html_part = Mail::Part.new do - content_type 'text/html; charset=UTF-8' - body renderer.html - end + @message.html_part = + Mail::Part.new do + content_type "text/html; charset=UTF-8" + body renderer.html + end end # Fix relative (ie upload) HTML links in markdown which do not work well in plain text emails. # These are the links we add when a user uploads a file or image. # Ideally we would parse general markdown into plain text, but that is almost an intractable problem. url_prefix = Discourse.base_url - @message.parts[0].body = @message.parts[0].body.to_s.gsub(/([^<]*)<\/a>/, '[\2|attachment](' + url_prefix + '\1)') - @message.parts[0].body = @message.parts[0].body.to_s.gsub(/]*)>/, '![](' + url_prefix + '\1)') + @message.parts[0].body = + @message.parts[0].body.to_s.gsub( + %r{([^<]*)}, + '[\2|attachment](' + url_prefix + '\1)', + ) + @message.parts[0].body = + @message.parts[0].body.to_s.gsub( + %r{]*)>}, + "![](" + url_prefix + '\1)', + ) - @message.text_part.content_type = 'text/plain; charset=UTF-8' + @message.text_part.content_type = "text/plain; charset=UTF-8" user_id = @user&.id # Set up the email log - email_log = EmailLog.new( - email_type: @email_type, - to_address: to_address, - user_id: user_id - ) + email_log = EmailLog.new(email_type: @email_type, to_address: to_address, user_id: user_id) if cc_addresses.any? email_log.cc_addresses = cc_addresses.join(";") email_log.cc_user_ids = User.with_email(cc_addresses).pluck(:id) end - if bcc_addresses.any? - email_log.bcc_addresses = bcc_addresses.join(";") - end + email_log.bcc_addresses = bcc_addresses.join(";") if bcc_addresses.any? host = Email::Sender.host_for(Discourse.base_url) - post_id = header_value('X-Discourse-Post-Id') - topic_id = header_value('X-Discourse-Topic-Id') + post_id = header_value("X-Discourse-Post-Id") + topic_id = header_value("X-Discourse-Topic-Id") reply_key = get_reply_key(post_id, user_id) from_address = @message.from&.first - smtp_group_id = from_address.blank? ? nil : Group.where( - email_username: from_address, smtp_enabled: true - ).pluck_first(:id) + smtp_group_id = + ( + if from_address.blank? + nil + else + Group.where(email_username: from_address, smtp_enabled: true).pluck_first(:id) + end + ) # always set a default Message ID from the host - @message.header['Message-ID'] = Email::MessageIdService.generate_default + @message.header["Message-ID"] = Email::MessageIdService.generate_default if topic_id.present? && post_id.present? post = Post.find_by(id: post_id, topic_id: topic_id) @@ -130,12 +144,14 @@ module Email # See https://www.ietf.org/rfc/rfc2919.txt for the List-ID # specification. if topic&.category && !topic.category.uncategorized? - list_id = "#{SiteSetting.title} | #{topic.category.name} <#{topic.category.name.downcase.tr(' ', '-')}.#{host}>" + list_id = + "#{SiteSetting.title} | #{topic.category.name} <#{topic.category.name.downcase.tr(" ", "-")}.#{host}>" # subcategory case if !topic.category.parent_category_id.nil? parent_category_name = Category.find_by(id: topic.category.parent_category_id).name - list_id = "#{SiteSetting.title} | #{parent_category_name} #{topic.category.name} <#{topic.category.name.downcase.tr(' ', '-')}.#{parent_category_name.downcase.tr(' ', '-')}.#{host}>" + list_id = + "#{SiteSetting.title} | #{parent_category_name} #{topic.category.name} <#{topic.category.name.downcase.tr(" ", "-")}.#{parent_category_name.downcase.tr(" ", "-")}.#{host}>" end else list_id = "#{SiteSetting.title} <#{host}>" @@ -148,16 +164,15 @@ module Email # conversation between the group and a small handful of people # directly contacting the group, often just one person. if !smtp_group_id - # https://www.ietf.org/rfc/rfc3834.txt - @message.header['Precedence'] = 'list' - @message.header['List-ID'] = list_id + @message.header["Precedence"] = "list" + @message.header["List-ID"] = list_id if topic if SiteSetting.private_email? - @message.header['List-Archive'] = "#{Discourse.base_url}#{topic.slugless_url}" + @message.header["List-Archive"] = "#{Discourse.base_url}#{topic.slugless_url}" else - @message.header['List-Archive'] = topic.url + @message.header["List-Archive"] = topic.url end end end @@ -176,61 +191,59 @@ module Email email_log.topic_id = topic_id if topic_id.present? if reply_key.present? - @message.header['Reply-To'] = header_value('Reply-To').gsub!("%{reply_key}", reply_key) + @message.header["Reply-To"] = header_value("Reply-To").gsub!("%{reply_key}", reply_key) @message.header[Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER] = nil end - MessageBuilder.custom_headers(SiteSetting.email_custom_headers).each do |key, _| - # Any custom headers added via MessageBuilder that are doubled up here - # with values that we determine should be set to the last value, which is - # the one we determined. Our header values should always override the email_custom_headers. - # - # While it is valid via RFC5322 to have more than one value for certain headers, - # we just want to keep it to one, especially in cases where the custom value - # would conflict with our own. - # - # See https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 and - # https://github.com/mikel/mail/blob/8ef377d6a2ca78aa5bd7f739813f5a0648482087/lib/mail/header.rb#L109-L132 - custom_header = @message.header[key] - if custom_header.is_a?(Array) - our_value = custom_header.last.value + MessageBuilder + .custom_headers(SiteSetting.email_custom_headers) + .each do |key, _| + # Any custom headers added via MessageBuilder that are doubled up here + # with values that we determine should be set to the last value, which is + # the one we determined. Our header values should always override the email_custom_headers. + # + # While it is valid via RFC5322 to have more than one value for certain headers, + # we just want to keep it to one, especially in cases where the custom value + # would conflict with our own. + # + # See https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 and + # https://github.com/mikel/mail/blob/8ef377d6a2ca78aa5bd7f739813f5a0648482087/lib/mail/header.rb#L109-L132 + custom_header = @message.header[key] + if custom_header.is_a?(Array) + our_value = custom_header.last.value - # Must be set to nil first otherwise another value is just added - # to the array of values for the header. - @message.header[key] = nil - @message.header[key] = our_value - end + # Must be set to nil first otherwise another value is just added + # to the array of values for the header. + @message.header[key] = nil + @message.header[key] = our_value + end - value = header_value(key) + value = header_value(key) - # Remove Auto-Submitted header for group private message emails, it does - # not make sense there and may hurt deliverability. - # - # From https://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml: - # - # > Indicates that a message was generated by an automatic process, and is not a direct response to another message. - if key.downcase == "auto-submitted" && smtp_group_id - @message.header[key] = nil - end + # Remove Auto-Submitted header for group private message emails, it does + # not make sense there and may hurt deliverability. + # + # From https://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml: + # + # > Indicates that a message was generated by an automatic process, and is not a direct response to another message. + @message.header[key] = nil if key.downcase == "auto-submitted" && smtp_group_id - # Replace reply_key in custom headers or remove - if value&.include?('%{reply_key}') - # Delete old header first or else the same header will be added twice - @message.header[key] = nil - if reply_key.present? - @message.header[key] = value.gsub!('%{reply_key}', reply_key) + # Replace reply_key in custom headers or remove + if value&.include?("%{reply_key}") + # Delete old header first or else the same header will be added twice + @message.header[key] = nil + @message.header[key] = value.gsub!("%{reply_key}", reply_key) if reply_key.present? end end - end # pass the original message_id when using mailjet/mandrill/sparkpost case ActionMailer::Base.smtp_settings[:address] when /\.mailjet\.com/ - @message.header['X-MJ-CustomID'] = @message.message_id + @message.header["X-MJ-CustomID"] = @message.message_id when "smtp.mandrillapp.com" - merge_json_x_header('X-MC-Metadata', message_id: @message.message_id) + merge_json_x_header("X-MC-Metadata", message_id: @message.message_id) when "smtp.sparkpostmail.com" - merge_json_x_header('X-MSYS-API', metadata: { message_id: @message.message_id }) + merge_json_x_header("X-MSYS-API", metadata: { message_id: @message.message_id }) end # Parse the HTML again so we can make any final changes before @@ -239,8 +252,8 @@ module Email # Suppress images from short emails if SiteSetting.strip_images_from_short_emails && - @message.html_part.body.to_s.bytesize <= SiteSetting.short_email_length && - @message.html_part.body =~ /]+>/ + @message.html_part.body.to_s.bytesize <= SiteSetting.short_email_length && + @message.html_part.body =~ /]+>/ style.strip_avatars_and_emojis end @@ -291,23 +304,26 @@ module Email end def to_address - @to_address ||= begin - to = @message.try(:to) - to = to.first if Array === to - to.presence || "no_email_found" - end + @to_address ||= + begin + to = @message.try(:to) + to = to.first if Array === to + to.presence || "no_email_found" + end end def cc_addresses - @cc_addresses ||= begin - @message.try(:cc) || [] - end + @cc_addresses ||= + begin + @message.try(:cc) || [] + end end def bcc_addresses - @bcc_addresses ||= begin - @message.try(:bcc) || [] - end + @bcc_addresses ||= + begin + @message.try(:bcc) || [] + end end def self.host_for(base_url) @@ -333,7 +349,7 @@ module Email optimized_1X = original_upload.optimized_images.first if FileHelper.is_supported_image?(original_upload.original_filename) && - !should_attach_image?(original_upload, optimized_1X) + !should_attach_image?(original_upload, optimized_1X) next end @@ -341,11 +357,12 @@ module Email next if email_size + attached_upload.filesize > max_email_size begin - path = if attached_upload.local? - Discourse.store.path_for(attached_upload) - else - Discourse.store.download(attached_upload).path - end + path = + if attached_upload.local? + Discourse.store.path_for(attached_upload) + else + Discourse.store.download(attached_upload).path + end @message_attachments_index[original_upload.sha1] = @message.attachments.size @message.attachments[original_upload.original_filename] = File.read(path) @@ -357,8 +374,8 @@ module Email env: { post_id: post.id, upload_id: original_upload.id, - filename: original_upload.original_filename - } + filename: original_upload.original_filename, + }, ) end end @@ -368,7 +385,10 @@ module Email def should_attach_image?(upload, optimized_1X = nil) return if !SiteSetting.secure_uploads_allow_embed_images_in_emails || !upload.secure? - return if (optimized_1X&.filesize || upload.filesize) > SiteSetting.secure_uploads_max_email_embed_image_size_kb.kilobytes + if (optimized_1X&.filesize || upload.filesize) > + SiteSetting.secure_uploads_max_email_embed_image_size_kb.kilobytes + return + end true end @@ -391,8 +411,7 @@ module Email # def fix_parts_after_attachments! has_attachments = @message.attachments.present? - has_alternative_renderings = - @message.html_part.present? && @message.text_part.present? + has_alternative_renderings = @message.html_part.present? && @message.text_part.present? if has_attachments && has_alternative_renderings @message.content_type = "multipart/mixed" @@ -403,15 +422,16 @@ module Email text_part = @message.text_part @message.text_part = nil - content = Mail::Part.new do - content_type "multipart/alternative" + content = + Mail::Part.new do + content_type "multipart/alternative" - # we have to re-specify the charset and give the part the decoded body - # here otherwise the parts will get encoded with US-ASCII which makes - # a bunch of characters not render correctly in the email - part content_type: "text/html; charset=utf-8", body: html_part.body.decoded - part content_type: "text/plain; charset=utf-8", body: text_part.body.decoded - end + # we have to re-specify the charset and give the part the decoded body + # here otherwise the parts will get encoded with US-ASCII which makes + # a bunch of characters not render correctly in the email + part content_type: "text/html; charset=utf-8", body: html_part.body.decoded + part content_type: "text/plain; charset=utf-8", body: text_part.body.decoded + end @message.parts.unshift(content) end @@ -437,7 +457,7 @@ module Email email_type: @email_type, to_address: to_address, user_id: @user&.id, - reason_type: reason_type + reason_type: reason_type, } attributes[:custom_reason] = custom_reason if custom_reason @@ -445,7 +465,12 @@ module Email end def merge_json_x_header(name, value) - data = JSON.parse(@message.header[name].to_s) rescue nil + data = + begin + JSON.parse(@message.header[name].to_s) + rescue StandardError + nil + end data ||= {} data.merge!(value) # /!\ @message.header is not a standard ruby hash. @@ -460,12 +485,12 @@ module Email def get_reply_key(post_id, user_id) # ALLOW_REPLY_BY_EMAIL_HEADER is only added if we are _not_ sending # via group SMTP and if reply by email site settings are configured - return if !user_id || !post_id || !header_value(Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER).present? + if !user_id || !post_id || + !header_value(Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER).present? + return + end - PostReplyKey.create_or_find_by!( - post_id: post_id, - user_id: user_id - ).reply_key + PostReplyKey.create_or_find_by!(post_id: post_id, user_id: user_id).reply_key end def self.bounceable_reply_address? @@ -514,7 +539,9 @@ module Email # # https://meta.discourse.org/t/discourse-email-messages-are-incorrectly-threaded/233499 def add_identification_field_headers(topic, post) - @message.header["Message-ID"] = Email::MessageIdService.generate_or_use_existing(post.id).first + @message.header["Message-ID"] = Email::MessageIdService.generate_or_use_existing( + post.id, + ).first if post.post_number > 1 op_message_id = Email::MessageIdService.generate_or_use_existing(topic.first_post.id).first @@ -523,11 +550,12 @@ module Email # Whenever we reply to a post directly _or_ quote a post, a PostReply # record is made, with the reply_post_id referencing the newly created # post, and the post_id referencing the post that was quoted or replied to. - referenced_posts = Post - .joins("INNER JOIN post_replies ON post_replies.post_id = posts.id ") - .where("post_replies.reply_post_id = ?", post.id) - .order(id: :desc) - .to_a + referenced_posts = + Post + .joins("INNER JOIN post_replies ON post_replies.post_id = posts.id ") + .where("post_replies.reply_post_id = ?", post.id) + .order(id: :desc) + .to_a ## # No referenced posts means that we are just creating a new post not @@ -543,7 +571,8 @@ module Email # every directly replied to post can go into In-Reply-To. # # We want to make sure all of the outbound_message_ids are already filled here. - in_reply_to_message_ids = MessageIdService.generate_or_use_existing(referenced_posts.map(&:id)) + in_reply_to_message_ids = + MessageIdService.generate_or_use_existing(referenced_posts.map(&:id)) @message.header["In-Reply-To"] = in_reply_to_message_ids most_recent_post_message_id = in_reply_to_message_ids.last @@ -559,7 +588,9 @@ module Email parent_message_ids = MessageIdService.generate_or_use_existing(reply_tree.values.flatten) @message.header["References"] = [ - op_message_id, parent_message_ids, most_recent_post_message_id + op_message_id, + parent_message_ids, + most_recent_post_message_id, ].flatten.uniq end end diff --git a/lib/email/styles.rb b/lib/email/styles.rb index cd561eb8b58..ee240b981ef 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -7,7 +7,8 @@ module Email class Styles MAX_IMAGE_DIMENSION = 400 - ONEBOX_IMAGE_BASE_STYLE = "max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;" + ONEBOX_IMAGE_BASE_STYLE = + "max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;" ONEBOX_IMAGE_THUMBNAIL_STYLE = "width: 60px;" ONEBOX_INLINE_AVATAR_STYLE = "width: 20px; height: 20px; float: none; vertical-align: middle;" @@ -29,12 +30,12 @@ module Email end def add_styles(node, new_styles) - existing = node['style'] + existing = node["style"] if existing.present? # merge styles - node['style'] = "#{new_styles}; #{existing}" + node["style"] = "#{new_styles}; #{existing}" else - node['style'] = new_styles + node["style"] = new_styles end end @@ -47,12 +48,12 @@ module Email if !css.blank? # there is a minor race condition here, CssParser could be # loaded by ::CssParser::Parser not loaded - require 'css_parser' unless defined?(::CssParser::Parser) + require "css_parser" unless defined?(::CssParser::Parser) parser = ::CssParser::Parser.new(import: false) parser.load_string!(css) parser.each_selector do |selector, value| - @custom_styles[selector] ||= +'' + @custom_styles[selector] ||= +"" @custom_styles[selector] << value end end @@ -67,118 +68,148 @@ module Email @fragment.css('svg, img[src$=".svg"]').remove # images - @fragment.css('img').each do |img| - next if img['class'] == 'site-logo' + @fragment + .css("img") + .each do |img| + next if img["class"] == "site-logo" - if (img['class'] && img['class']['emoji']) || (img['src'] && img['src'][/\/_?emoji\//]) - img['width'] = img['height'] = 20 - else - # use dimensions of original iPhone screen for 'too big, let device rescale' - if img['width'].to_i > (320) || img['height'].to_i > (480) - img['width'] = img['height'] = 'auto' + if (img["class"] && img["class"]["emoji"]) || (img["src"] && img["src"][%r{/_?emoji/}]) + img["width"] = img["height"] = 20 + else + # use dimensions of original iPhone screen for 'too big, let device rescale' + if img["width"].to_i > (320) || img["height"].to_i > (480) + img["width"] = img["height"] = "auto" + end + end + + if img["src"] + # ensure all urls are absolute + img["src"] = "#{Discourse.base_url}#{img["src"]}" if img["src"][%r{^/[^/]}] + # ensure no schemaless urls + img["src"] = "#{uri.scheme}:#{img["src"]}" if img["src"][%r{^//}] end end - if img['src'] - # ensure all urls are absolute - img['src'] = "#{Discourse.base_url}#{img['src']}" if img['src'][/^\/[^\/]/] - # ensure no schemaless urls - img['src'] = "#{uri.scheme}:#{img['src']}" if img['src'][/^\/\//] - end - end - # add max-width to big images - big_images = @fragment.css('img[width="auto"][height="auto"]') - - @fragment.css('aside.onebox img') - - @fragment.css('img.site-logo, img.emoji') - big_images.each do |img| - add_styles(img, 'max-width: 100%;') if img['style'] !~ /max-width/ - end + big_images = + @fragment.css('img[width="auto"][height="auto"]') - @fragment.css("aside.onebox img") - + @fragment.css("img.site-logo, img.emoji") + big_images.each { |img| add_styles(img, "max-width: 100%;") if img["style"] !~ /max-width/ } # topic featured link - @fragment.css('a.topic-featured-link').each do |e| - e['style'] = "color:#858585;padding:2px 8px;border:1px solid #e6e6e6;border-radius:2px;box-shadow:0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);" - end + @fragment + .css("a.topic-featured-link") + .each do |e| + e[ + "style" + ] = "color:#858585;padding:2px 8px;border:1px solid #e6e6e6;border-radius:2px;box-shadow:0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);" + end # attachments - @fragment.css('a.attachment').each do |a| - # ensure all urls are absolute - if a['href'] =~ /^\/[^\/]/ - a['href'] = "#{Discourse.base_url}#{a['href']}" - end + @fragment + .css("a.attachment") + .each do |a| + # ensure all urls are absolute + a["href"] = "#{Discourse.base_url}#{a["href"]}" if a["href"] =~ %r{^/[^/]} - # ensure no schemaless urls - if a['href'] && a['href'].starts_with?("//") - a['href'] = "#{uri.scheme}:#{a['href']}" + # ensure no schemaless urls + a["href"] = "#{uri.scheme}:#{a["href"]}" if a["href"] && a["href"].starts_with?("//") end - end end def onebox_styles # Links to other topics - style('aside.quote', 'padding: 12px 25px 2px 12px; margin-bottom: 10px;') - style('aside.quote div.info-line', 'color: #666; margin: 10px 0') - style('aside.quote .avatar', 'margin-right: 5px; width:20px; height:20px; vertical-align:middle;') - style('aside.quote', 'border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin: 0;') + style("aside.quote", "padding: 12px 25px 2px 12px; margin-bottom: 10px;") + style("aside.quote div.info-line", "color: #666; margin: 10px 0") + style( + "aside.quote .avatar", + "margin-right: 5px; width:20px; height:20px; vertical-align:middle;", + ) + style("aside.quote", "border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin: 0;") - style('blockquote', 'border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin-left: 0; padding: 12px;') + style( + "blockquote", + "border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin-left: 0; padding: 12px;", + ) # Oneboxes - style('aside.onebox', "border: 5px solid #e9e9e9; padding: 12px 25px 12px 12px; margin-bottom: 10px;") - style('aside.onebox header img.site-icon', "width: 16px; height: 16px; margin-right: 3px;") - style('aside.onebox header a[href]', "color: #222222; text-decoration: none;") - style('aside.onebox .onebox-body', "clear: both") - style('aside.onebox .onebox-body img:not(.onebox-avatar-inline)', ONEBOX_IMAGE_BASE_STYLE) - style('aside.onebox .onebox-body img.thumbnail', ONEBOX_IMAGE_THUMBNAIL_STYLE) - style('aside.onebox .onebox-body h3, aside.onebox .onebox-body h4', "font-size: 1.17em; margin: 10px 0;") - style('.onebox-metadata', "color: #919191") - style('.github-info', "margin-top: 10px;") - style('.github-info .added', "color: #090;") - style('.github-info .removed', "color: #e45735;") - style('.github-info div', "display: inline; margin-right: 10px;") - style('.github-icon-container', 'float: left;') - style('.github-icon-container *', 'fill: #646464; width: 40px; height: 40px;') - style('.github-body-container', 'font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; margin-top: 1em !important;') - style('.onebox-avatar-inline', ONEBOX_INLINE_AVATAR_STYLE) + style( + "aside.onebox", + "border: 5px solid #e9e9e9; padding: 12px 25px 12px 12px; margin-bottom: 10px;", + ) + style("aside.onebox header img.site-icon", "width: 16px; height: 16px; margin-right: 3px;") + style("aside.onebox header a[href]", "color: #222222; text-decoration: none;") + style("aside.onebox .onebox-body", "clear: both") + style("aside.onebox .onebox-body img:not(.onebox-avatar-inline)", ONEBOX_IMAGE_BASE_STYLE) + style("aside.onebox .onebox-body img.thumbnail", ONEBOX_IMAGE_THUMBNAIL_STYLE) + style( + "aside.onebox .onebox-body h3, aside.onebox .onebox-body h4", + "font-size: 1.17em; margin: 10px 0;", + ) + style(".onebox-metadata", "color: #919191") + style(".github-info", "margin-top: 10px;") + style(".github-info .added", "color: #090;") + style(".github-info .removed", "color: #e45735;") + style(".github-info div", "display: inline; margin-right: 10px;") + style(".github-icon-container", "float: left;") + style(".github-icon-container *", "fill: #646464; width: 40px; height: 40px;") + style( + ".github-body-container", + 'font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; margin-top: 1em !important;', + ) + style(".onebox-avatar-inline", ONEBOX_INLINE_AVATAR_STYLE) - @fragment.css('.github-body-container .excerpt').remove + @fragment.css(".github-body-container .excerpt").remove - @fragment.css('aside.quote blockquote > p').each do |p| - p['style'] = 'padding: 0;' - end + @fragment.css("aside.quote blockquote > p").each { |p| p["style"] = "padding: 0;" } # Convert all `aside.quote` tags to `blockquote`s - @fragment.css('aside.quote').each do |n| - original_node = n.dup - original_node.search('div.quote-controls').remove - blockquote = original_node.css('blockquote').inner_html.strip.start_with?("#{original_node.css('blockquote').inner_html}

" - n.inner_html = original_node.css('div.title').inner_html + blockquote - n.name = "blockquote" - end + @fragment + .css("aside.quote") + .each do |n| + original_node = n.dup + original_node.search("div.quote-controls").remove + blockquote = + ( + if original_node.css("blockquote").inner_html.strip.start_with?("#{original_node.css("blockquote").inner_html}

" + end + ) + n.inner_html = original_node.css("div.title").inner_html + blockquote + n.name = "blockquote" + end # Finally, convert all `aside` tags to `div`s - @fragment.css('aside, article, header').each do |n| - n.name = "div" - end + @fragment.css("aside, article, header").each { |n| n.name = "div" } # iframes can't go in emails, so replace them with clickable links - @fragment.css('iframe').each do |i| - begin - # sometimes, iframes are blocklisted... - if i["src"].blank? - i.remove - next - end + @fragment + .css("iframe") + .each do |i| + begin + # sometimes, iframes are blocklisted... + if i["src"].blank? + i.remove + next + end - src_uri = i["data-original-href"].present? ? URI(i["data-original-href"]) : URI(i['src']) - # If an iframe is protocol relative, use SSL when displaying it - display_src = "#{src_uri.scheme || 'https'}://#{src_uri.host}#{src_uri.path}#{src_uri.query.nil? ? '' : '?' + src_uri.query}#{src_uri.fragment.nil? ? '' : '#' + src_uri.fragment}" - i.replace(Nokogiri::HTML5.fragment("

#{CGI.escapeHTML(display_src)}

")) - rescue URI::Error - # If the URL is weird, remove the iframe - i.remove + src_uri = + i["data-original-href"].present? ? URI(i["data-original-href"]) : URI(i["src"]) + # If an iframe is protocol relative, use SSL when displaying it + display_src = + "#{src_uri.scheme || "https"}://#{src_uri.host}#{src_uri.path}#{src_uri.query.nil? ? "" : "?" + src_uri.query}#{src_uri.fragment.nil? ? "" : "#" + src_uri.fragment}" + i.replace( + Nokogiri::HTML5.fragment( + "

#{CGI.escapeHTML(display_src)}

", + ), + ) + rescue URI::Error + # If the URL is weird, remove the iframe + i.remove + end end - end end def format_html @@ -189,67 +220,93 @@ module Email reset_tables html_lang = SiteSetting.default_locale.sub("_", "-") - style('html', nil, lang: html_lang, 'xml:lang' => html_lang) - style('body', "line-height: 1.4; text-align:#{ Rtl.new(nil).enabled? ? 'right' : 'left' };") - style('body', nil, dir: Rtl.new(nil).enabled? ? 'rtl' : 'ltr') + style("html", nil, :lang => html_lang, "xml:lang" => html_lang) + style("body", "line-height: 1.4; text-align:#{Rtl.new(nil).enabled? ? "right" : "left"};") + style("body", nil, dir: Rtl.new(nil).enabled? ? "rtl" : "ltr") - style('.with-dir', - "text-align:#{ Rtl.new(nil).enabled? ? 'right' : 'left' };", - dir: Rtl.new(nil).enabled? ? 'rtl' : 'ltr' + style( + ".with-dir", + "text-align:#{Rtl.new(nil).enabled? ? "right" : "left"};", + dir: Rtl.new(nil).enabled? ? "rtl" : "ltr", ) - style('blockquote > :first-child', 'margin-top: 0;') - style('blockquote > :last-child', 'margin-bottom: 0;') - style('blockquote > p', 'padding: 0;') + style("blockquote > :first-child", "margin-top: 0;") + style("blockquote > :last-child", "margin-bottom: 0;") + style("blockquote > p", "padding: 0;") - style('.with-accent-colors', "background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};") - style('h4', 'color: #222;') - style('h3', 'margin: 30px 0 10px;') - style('hr', 'background-color: #ddd; height: 1px; border: 1px;') - style('a', "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color};") - style('ul', 'margin: 0 0 0 10px; padding: 0 0 0 20px;') - style('li', 'padding-bottom: 10px') - style('div.summary-footer', 'color:#666; font-size:95%; text-align:center; padding-top:15px;') - style('span.post-count', 'margin: 0 5px; color: #777;') - style('pre', 'word-wrap: break-word; max-width: 694px;') - style('code', 'background-color: #f9f9f9; padding: 2px 5px;') - style('pre code', 'display: block; background-color: #f9f9f9; overflow: auto; padding: 5px;') - style('pre.onebox code', 'white-space: normal;') - style('pre code li', 'white-space: pre;') - style('.featured-topic a', "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;") - style('.summary-email', "-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%") + style( + ".with-accent-colors", + "background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};", + ) + style("h4", "color: #222;") + style("h3", "margin: 30px 0 10px;") + style("hr", "background-color: #ddd; height: 1px; border: 1px;") + style( + "a", + "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color};", + ) + style("ul", "margin: 0 0 0 10px; padding: 0 0 0 20px;") + style("li", "padding-bottom: 10px") + style("div.summary-footer", "color:#666; font-size:95%; text-align:center; padding-top:15px;") + style("span.post-count", "margin: 0 5px; color: #777;") + style("pre", "word-wrap: break-word; max-width: 694px;") + style("code", "background-color: #f9f9f9; padding: 2px 5px;") + style("pre code", "display: block; background-color: #f9f9f9; overflow: auto; padding: 5px;") + style("pre.onebox code", "white-space: normal;") + style("pre code li", "white-space: pre;") + style( + ".featured-topic a", + "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;", + ) + style( + ".summary-email", + "-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%", + ) - style('.previous-discussion', 'font-size: 17px; color: #444; margin-bottom:10px;') - style('.notification-date', "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px") - style('.username', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;font-weight:bold") - style('.username-link', "color:#{SiteSetting.email_link_color};") - style('.username-title', "color:#777;margin-left:5px;") - style('.user-title', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:5px;color: #999;") - style('.post-wrapper', "margin-bottom:25px;") - style('.user-avatar', 'vertical-align:top;width:55px;') - style('.user-avatar img', nil, width: '45', height: '45') - style('hr', 'background-color: #ddd; height: 1px; border: 1px;') - style('.rtl', 'direction: rtl;') - style('div.body', 'padding-top:5px;') - style('.whisper div.body', 'font-style: italic; color: #9c9c9c;') - style('.lightbox-wrapper .meta', 'display: none') - style('div.undecorated-link-footer a', "font-weight: normal;") - style('.mso-accent-link', "mso-border-alt: 6px solid #{SiteSetting.email_accent_bg_color}; background-color: #{SiteSetting.email_accent_bg_color};") - style('.reply-above-line', "font-size: 10px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color: #b5b5b5;padding: 5px 0px 20px;border-top: 1px dotted #ddd;") + style(".previous-discussion", "font-size: 17px; color: #444; margin-bottom:10px;") + style( + ".notification-date", + "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px", + ) + style( + ".username", + "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;font-weight:bold", + ) + style(".username-link", "color:#{SiteSetting.email_link_color};") + style(".username-title", "color:#777;margin-left:5px;") + style( + ".user-title", + "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:5px;color: #999;", + ) + style(".post-wrapper", "margin-bottom:25px;") + style(".user-avatar", "vertical-align:top;width:55px;") + style(".user-avatar img", nil, width: "45", height: "45") + style("hr", "background-color: #ddd; height: 1px; border: 1px;") + style(".rtl", "direction: rtl;") + style("div.body", "padding-top:5px;") + style(".whisper div.body", "font-style: italic; color: #9c9c9c;") + style(".lightbox-wrapper .meta", "display: none") + style("div.undecorated-link-footer a", "font-weight: normal;") + style( + ".mso-accent-link", + "mso-border-alt: 6px solid #{SiteSetting.email_accent_bg_color}; background-color: #{SiteSetting.email_accent_bg_color};", + ) + style( + ".reply-above-line", + "font-size: 10px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color: #b5b5b5;padding: 5px 0px 20px;border-top: 1px dotted #ddd;", + ) onebox_styles plugin_styles dark_mode_styles - style('.post-excerpt img', "max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;") + style(".post-excerpt img", "max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;") format_custom end def format_custom - custom_styles.each do |selector, value| - style(selector, value) - end + custom_styles.each { |selector, value| style(selector, value) } end # this method is reserved for styles specific to plugin @@ -258,33 +315,46 @@ module Email end def inline_secure_images(attachments, attachments_index) - stripped_media = @fragment.css('[data-stripped-secure-media], [data-stripped-secure-upload]') + stripped_media = @fragment.css("[data-stripped-secure-media], [data-stripped-secure-upload]") upload_shas = {} stripped_media.each do |div| - url = div['data-stripped-secure-media'] || div['data-stripped-secure-upload'] + url = div["data-stripped-secure-media"] || div["data-stripped-secure-upload"] filename = File.basename(url) filename_bare = filename.gsub(File.extname(filename), "") - sha1 = filename_bare.partition('_').first + sha1 = filename_bare.partition("_").first upload_shas[url] = sha1 end uploads = Upload.select(:original_filename, :sha1).where(sha1: upload_shas.values) stripped_media.each do |div| - upload = uploads.find do |upl| - upl.sha1 == (upload_shas[div['data-stripped-secure-media']] || upload_shas[div['data-stripped-secure-upload']]) - end + upload = + uploads.find do |upl| + upl.sha1 == + ( + upload_shas[div["data-stripped-secure-media"]] || + upload_shas[div["data-stripped-secure-upload"]] + ) + end next if !upload if attachments[attachments_index[upload.sha1]] url = attachments[attachments_index[upload.sha1]].url - onebox_type = div['data-onebox-type'] - style = if onebox_type - onebox_style = onebox_type == "avatar-inline" ? ONEBOX_INLINE_AVATAR_STYLE : ONEBOX_IMAGE_THUMBNAIL_STYLE - "#{onebox_style} #{ONEBOX_IMAGE_BASE_STYLE}" - else - calculate_width_and_height_style(div) - end + onebox_type = div["data-onebox-type"] + style = + if onebox_type + onebox_style = + ( + if onebox_type == "avatar-inline" + ONEBOX_INLINE_AVATAR_STYLE + else + ONEBOX_IMAGE_THUMBNAIL_STYLE + end + ) + "#{onebox_style} #{ONEBOX_IMAGE_BASE_STYLE}" + else + calculate_width_and_height_style(div) + end div.add_next_sibling(<<~HTML) @@ -309,39 +379,45 @@ module Email end def strip_avatars_and_emojis - @fragment.search('img').each do |img| - next unless img['src'] + @fragment + .search("img") + .each do |img| + next unless img["src"] - if img['src'][/_avatar/] - img.parent['style'] = "vertical-align: top;" if img.parent&.name == 'td' - img.remove - end + if img["src"][/_avatar/] + img.parent["style"] = "vertical-align: top;" if img.parent&.name == "td" + img.remove + end - if img['title'] && img['src'][/\/_?emoji\//] - img.add_previous_sibling(img['title'] || "emoji") - img.remove + if img["title"] && img["src"][%r{/_?emoji/}] + img.add_previous_sibling(img["title"] || "emoji") + img.remove + end end - end end def decorate_hashtags - @fragment.search(".hashtag-cooked").each do |hashtag| - hashtag.children.each(&:remove) - hashtag.add_child(<<~HTML) + @fragment + .search(".hashtag-cooked") + .each do |hashtag| + hashtag.children.each(&:remove) + hashtag.add_child(<<~HTML) ##{hashtag["data-slug"]} HTML - end + end end def make_all_links_absolute site_uri = URI(Discourse.base_url) - @fragment.css("a").each do |link| - begin - link["href"] = "#{site_uri}#{link['href']}" unless URI(link["href"].to_s).host.present? - rescue URI::Error - # leave it + @fragment + .css("a") + .each do |link| + begin + link["href"] = "#{site_uri}#{link["href"]}" unless URI(link["href"].to_s).host.present? + rescue URI::Error + # leave it + end end - end end private @@ -350,8 +426,16 @@ module Email # When we ship the email template and its styles we strip all css classes so to give our # dark mode styles we are including in the template a selector we add a data-attr of 'dm=value' to # the appropriate place - style(".digest-header, .digest-topic, .digest-topic-title-wrapper, .digest-topic-stats, .popular-post-excerpt", nil, dm: "header") - style(".digest-content, .header-popular-posts, .spacer, .popular-post-spacer, .popular-post-meta, .digest-new-header, .digest-new-topic, .body", nil, dm: "body") + style( + ".digest-header, .digest-topic, .digest-topic-title-wrapper, .digest-topic-stats, .popular-post-excerpt", + nil, + dm: "header", + ) + style( + ".digest-content, .header-popular-posts, .spacer, .popular-post-spacer, .popular-post-meta, .digest-new-header, .digest-new-topic, .body", + nil, + dm: "body", + ) style(".with-accent-colors, .digest-content-header", nil, dm: "body_primary") style(".digest-topic-body", nil, dm: "topic-body") style(".summary-footer", nil, dm: "text-color") @@ -363,18 +447,19 @@ module Email host = forum_uri.host scheme = forum_uri.scheme - @fragment.css('[href]').each do |element| - href = element['href'] - if href.start_with?("\/\/#{host}") - element['href'] = "#{scheme}:#{href}" + @fragment + .css("[href]") + .each do |element| + href = element["href"] + element["href"] = "#{scheme}:#{href}" if href.start_with?("\/\/#{host}") end - end end def calculate_width_and_height_style(div) - width = div['data-width'] - height = div['data-height'] - if width.present? && height.present? && height.to_i < MAX_IMAGE_DIMENSION && width.to_i < MAX_IMAGE_DIMENSION + width = div["data-width"] + height = div["data-height"] + if width.present? && height.present? && height.to_i < MAX_IMAGE_DIMENSION && + width.to_i < MAX_IMAGE_DIMENSION "width: #{width}px; height: #{height}px;" else "max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;" @@ -386,59 +471,68 @@ module Email # notification template but that may not catch everything PrettyText.strip_secure_uploads(@fragment) - style('div.secure-upload-notice', 'border: 5px solid #e9e9e9; padding: 5px; display: inline-block;') - style('div.secure-upload-notice a', "color: #{SiteSetting.email_link_color}") + style( + "div.secure-upload-notice", + "border: 5px solid #e9e9e9; padding: 5px; display: inline-block;", + ) + style("div.secure-upload-notice a", "color: #{SiteSetting.email_link_color}") end def correct_first_body_margin - @fragment.css('div.body p').each do |element| - element['style'] = "margin-top:0; border: 0;" - end + @fragment.css("div.body p").each { |element| element["style"] = "margin-top:0; border: 0;" } end def correct_footer_style - @fragment.css('.footer').each do |element| - element['style'] = "color:#666;" - element.css('a').each do |inner| - inner['style'] = "color:#666;" + @fragment + .css(".footer") + .each do |element| + element["style"] = "color:#666;" + element.css("a").each { |inner| inner["style"] = "color:#666;" } end - end end def correct_footer_style_highlight_first footernum = 0 - @fragment.css('.footer.highlight').each do |element| - linknum = 0 - element.css('a').each do |inner| - # we want the first footer link to be specially highlighted as IMPORTANT - if footernum == (0) && linknum == (0) - bg_color = SiteSetting.email_accent_bg_color - inner['style'] = "background-color: #{bg_color}; color: #{SiteSetting.email_accent_fg_color}; border-top: 4px solid #{bg_color}; border-right: 6px solid #{bg_color}; border-bottom: 4px solid #{bg_color}; border-left: 6px solid #{bg_color}; display: inline-block; font-weight: bold;" - end + @fragment + .css(".footer.highlight") + .each do |element| + linknum = 0 + element + .css("a") + .each do |inner| + # we want the first footer link to be specially highlighted as IMPORTANT + if footernum == (0) && linknum == (0) + bg_color = SiteSetting.email_accent_bg_color + inner[ + "style" + ] = "background-color: #{bg_color}; color: #{SiteSetting.email_accent_fg_color}; border-top: 4px solid #{bg_color}; border-right: 6px solid #{bg_color}; border-bottom: 4px solid #{bg_color}; border-left: 6px solid #{bg_color}; display: inline-block; font-weight: bold;" + end + return + end return end - return - end end def strip_classes_and_ids - @fragment.css('*').each do |element| - element.delete('class') - element.delete('id') - end + @fragment + .css("*") + .each do |element| + element.delete("class") + element.delete("id") + end end def reset_tables - style('table', nil, cellspacing: '0', cellpadding: '0', border: '0') + style("table", nil, cellspacing: "0", cellpadding: "0", border: "0") end def style(selector, style, attribs = {}) - @fragment.css(selector).each do |element| - add_styles(element, style) if style - attribs.each do |k, v| - element[k] = v + @fragment + .css(selector) + .each do |element| + add_styles(element, style) if style + attribs.each { |k, v| element[k] = v } end - end end end end diff --git a/lib/email/validator.rb b/lib/email/validator.rb index 2795055a93e..764bb7e13c2 100644 --- a/lib/email/validator.rb +++ b/lib/email/validator.rb @@ -10,7 +10,7 @@ module Email end def self.ensure_valid_address_lists!(mail) - [:to, :cc, :bcc].each do |field| + %i[to cc bcc].each do |field| addresses = mail[field] if addresses&.errors.present? @@ -21,7 +21,8 @@ module Email def self.ensure_valid_date!(mail) if mail.date.nil? - raise Email::Receiver::InvalidPost, I18n.t("system_messages.email_reject_invalid_post_specified.date_invalid") + raise Email::Receiver::InvalidPost, + I18n.t("system_messages.email_reject_invalid_post_specified.date_invalid") end end end diff --git a/lib/email_backup_token.rb b/lib/email_backup_token.rb index 098f7c7e073..0aef08ca542 100644 --- a/lib/email_backup_token.rb +++ b/lib/email_backup_token.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class EmailBackupToken - def self.key(user_id) "email-backup-token:#{user_id}" end diff --git a/lib/email_controller_helper/base_email_unsubscriber.rb b/lib/email_controller_helper/base_email_unsubscriber.rb index b04560040ca..46267cb36e7 100644 --- a/lib/email_controller_helper/base_email_unsubscriber.rb +++ b/lib/email_controller_helper/base_email_unsubscriber.rb @@ -20,7 +20,7 @@ module EmailControllerHelper controller.instance_variable_set( :@unsubscribed_from_all, - key_owner.user_option.unsubscribed_from_all? + key_owner.user_option.unsubscribed_from_all?, ) end @@ -38,10 +38,12 @@ module EmailControllerHelper end if params[:unsubscribe_all] - key_owner.user_option.update_columns(email_digests: false, - email_level: UserOption.email_level_types[:never], - email_messages_level: UserOption.email_level_types[:never], - mailing_list_mode: false) + key_owner.user_option.update_columns( + email_digests: false, + email_level: UserOption.email_level_types[:never], + email_messages_level: UserOption.email_level_types[:never], + mailing_list_mode: false, + ) updated = true end diff --git a/lib/email_controller_helper/digest_email_unsubscriber.rb b/lib/email_controller_helper/digest_email_unsubscriber.rb index 7291d01258b..b96e77402c5 100644 --- a/lib/email_controller_helper/digest_email_unsubscriber.rb +++ b/lib/email_controller_helper/digest_email_unsubscriber.rb @@ -12,22 +12,34 @@ module EmailControllerHelper never = frequencies.delete_at(0) allowed_frequencies = %w[never weekly every_month every_six_months] - result = frequencies.reduce(frequencies: [], current: nil, selected: nil, take_next: false) do |memo, v| - memo[:current] = v[:name] if v[:value] == frequency_in_minutes && email_digests - next(memo) unless allowed_frequencies.include?(v[:name]) + result = + frequencies.reduce( + frequencies: [], + current: nil, + selected: nil, + take_next: false, + ) do |memo, v| + memo[:current] = v[:name] if v[:value] == frequency_in_minutes && email_digests + next(memo) unless allowed_frequencies.include?(v[:name]) - memo.tap do |m| - m[:selected] = v[:value] if m[:take_next] && email_digests - m[:frequencies] << [I18n.t("unsubscribe.digest_frequency.#{v[:name]}"), v[:value]] - m[:take_next] = !m[:take_next] && m[:current] + memo.tap do |m| + m[:selected] = v[:value] if m[:take_next] && email_digests + m[:frequencies] << [I18n.t("unsubscribe.digest_frequency.#{v[:name]}"), v[:value]] + m[:take_next] = !m[:take_next] && m[:current] + end end - end - digest_frequencies = result.slice(:frequencies, :current, :selected).tap do |r| - r[:frequencies] << [I18n.t("unsubscribe.digest_frequency.#{never[:name]}"), never[:value]] - r[:selected] ||= never[:value] - r[:current] ||= never[:name] - end + digest_frequencies = + result + .slice(:frequencies, :current, :selected) + .tap do |r| + r[:frequencies] << [ + I18n.t("unsubscribe.digest_frequency.#{never[:name]}"), + never[:value], + ] + r[:selected] ||= never[:value] + r[:current] ||= never[:name] + end controller.instance_variable_set(:@digest_frequencies, digest_frequencies) end @@ -40,7 +52,7 @@ module EmailControllerHelper unsubscribe_key.user.user_option.update_columns( digest_after_minutes: digest_frequency, - email_digests: digest_frequency.positive? + email_digests: digest_frequency.positive?, ) updated = true end diff --git a/lib/email_controller_helper/topic_email_unsubscriber.rb b/lib/email_controller_helper/topic_email_unsubscriber.rb index 6265853f541..eda37b7d667 100644 --- a/lib/email_controller_helper/topic_email_unsubscriber.rb +++ b/lib/email_controller_helper/topic_email_unsubscriber.rb @@ -11,16 +11,25 @@ module EmailControllerHelper controller.instance_variable_set(:@topic, topic) controller.instance_variable_set( :@watching_topic, - TopicUser.exists?(user: key_owner, notification_level: watching, topic_id: topic.id) + TopicUser.exists?(user: key_owner, notification_level: watching, topic_id: topic.id), ) return if topic.category_id.blank? - return if !CategoryUser.exists?(user: key_owner, notification_level: CategoryUser.watching_levels, category_id: topic.category_id) + if !CategoryUser.exists?( + user: key_owner, + notification_level: CategoryUser.watching_levels, + category_id: topic.category_id, + ) + return + end controller.instance_variable_set( :@watched_count, - TopicUser.joins(:topic) - .where(user: key_owner, notification_level: watching).where(topics: { category_id: topic.category_id }).count + TopicUser + .joins(:topic) + .where(user: key_owner, notification_level: watching) + .where(topics: { category_id: topic.category_id }) + .count, ) end @@ -31,27 +40,33 @@ module EmailControllerHelper return updated if topic.nil? if params[:unwatch_topic] - TopicUser.where(topic_id: topic.id, user_id: key_owner.id) - .update_all(notification_level: TopicUser.notification_levels[:tracking]) + TopicUser.where(topic_id: topic.id, user_id: key_owner.id).update_all( + notification_level: TopicUser.notification_levels[:tracking], + ) updated = true end if params[:unwatch_category] && topic.category_id - TopicUser.joins(:topic) + TopicUser + .joins(:topic) .where(user: key_owner, notification_level: TopicUser.notification_levels[:watching]) .where(topics: { category_id: topic.category_id }) .update_all(notification_level: TopicUser.notification_levels[:tracking]) - CategoryUser - .where(user_id: key_owner.id, category_id: topic.category_id, notification_level: CategoryUser.watching_levels) - .destroy_all + CategoryUser.where( + user_id: key_owner.id, + category_id: topic.category_id, + notification_level: CategoryUser.watching_levels, + ).destroy_all updated = true end if params[:mute_topic] - TopicUser.where(topic_id: topic.id, user_id: key_owner.id).update_all(notification_level: TopicUser.notification_levels[:muted]) + TopicUser.where(topic_id: topic.id, user_id: key_owner.id).update_all( + notification_level: TopicUser.notification_levels[:muted], + ) updated = true end diff --git a/lib/email_cook.rb b/lib/email_cook.rb index 89b59891f12..2c76e1f2ff2 100644 --- a/lib/email_cook.rb +++ b/lib/email_cook.rb @@ -2,9 +2,9 @@ # A very simple formatter for imported emails class EmailCook - def self.raw_regexp - @raw_regexp ||= /^\[plaintext\]$\n(.*)\n^\[\/plaintext\]$(?:\s^\[attachments\]$\n(.*)\n^\[\/attachments\]$)?(?:\s^\[elided\]$\n(.*)\n^\[\/elided\]$)?/m + @raw_regexp ||= + %r{^\[plaintext\]$\n(.*)\n^\[/plaintext\]$(?:\s^\[attachments\]$\n(.*)\n^\[/attachments\]$)?(?:\s^\[elided\]$\n(.*)\n^\[/elided\]$)?}m end def initialize(raw) @@ -22,7 +22,7 @@ class EmailCook def link_string!(line, unescaped_line) unescaped_line = unescaped_line.strip line.gsub!(/\S+/) do |str| - if str.match?(/^(https?:\/\/)[\S]+$/i) + if str.match?(%r{^(https?://)[\S]+$}i) begin url = URI.parse(str).to_s if unescaped_line == url @@ -52,7 +52,7 @@ class EmailCook if line =~ /^\s*>/ in_quote = true - line.sub!(/^[\s>]*/, '') + line.sub!(/^[\s>]*/, "") unescaped_line = line line = CGI.escapeHTML(line) @@ -64,7 +64,6 @@ class EmailCook quote_buffer = "" in_quote = false else - sz = line.size unescaped_line = line @@ -72,9 +71,7 @@ class EmailCook link_string!(line, unescaped_line) if sz < 60 - if in_text && line == "\n" - result << "
" - end + result << "
" if in_text && line == "\n" result << line result << "
" @@ -86,11 +83,9 @@ class EmailCook end end - if in_quote && quote_buffer.present? - add_quote(result, quote_buffer) - end + add_quote(result, quote_buffer) if in_quote && quote_buffer.present? - result.gsub!(/(
\n*){3,10}/, '

') + result.gsub!(/(
\n*){3,10}/, "

") result end @@ -98,10 +93,9 @@ class EmailCook # fallback to PrettyText if we failed to detect a body return PrettyText.cook(@raw, opts) if @body.nil? - result = htmlify(@body) + result = htmlify(@body) result << "\n
" << @attachment_html if @attachment_html.present? result << "\n

" << Email::Receiver.elided_html(htmlify(@elided)) if @elided.present? result end - end diff --git a/lib/email_updater.rb b/lib/email_updater.rb index 8f5fb292e2b..c25c2382e86 100644 --- a/lib/email_updater.rb +++ b/lib/email_updater.rb @@ -26,8 +26,8 @@ class EmailUpdater if SiteSetting.hide_email_address_taken Jobs.enqueue(:critical_user_email, type: "account_exists", user_id: existing_user.id) else - error_message = +'change_email.error' - error_message << '_staged' if existing_user.staged? + error_message = +"change_email.error" + error_message << "_staged" if existing_user.staged? errors.add(:base, I18n.t(error_message)) end end @@ -57,19 +57,23 @@ class EmailUpdater @change_req.new_email = email end - if @change_req.change_state.blank? || @change_req.change_state == EmailChangeRequest.states[:complete] - @change_req.change_state = if SiteSetting.require_change_email_confirmation || @user.staff? - EmailChangeRequest.states[:authorizing_old] - else - EmailChangeRequest.states[:authorizing_new] - end + if @change_req.change_state.blank? || + @change_req.change_state == EmailChangeRequest.states[:complete] + @change_req.change_state = + if SiteSetting.require_change_email_confirmation || @user.staff? + EmailChangeRequest.states[:authorizing_old] + else + EmailChangeRequest.states[:authorizing_new] + end end if @change_req.change_state == EmailChangeRequest.states[:authorizing_old] - @change_req.old_email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:email_update]) + @change_req.old_email_token = + @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:email_update]) send_email(add ? "confirm_old_email_add" : "confirm_old_email", @change_req.old_email_token) elsif @change_req.change_state == EmailChangeRequest.states[:authorizing_new] - @change_req.new_email_token = @user.email_tokens.create!(email: email, scope: EmailToken.scopes[:email_update]) + @change_req.new_email_token = + @user.email_tokens.create!(email: email, scope: EmailToken.scopes[:email_update]) send_email("confirm_new_email", @change_req.new_email_token) end @@ -83,7 +87,7 @@ class EmailUpdater User.transaction do email_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_update]) if email_token.blank? - errors.add(:base, I18n.t('change_email.already_done')) + errors.add(:base, I18n.t("change_email.already_done")) confirm_result = :error next end @@ -91,15 +95,24 @@ class EmailUpdater email_token.update!(confirmed: true) @user = email_token.user - @change_req = @user.email_change_requests - .where('old_email_token_id = :token_id OR new_email_token_id = :token_id', token_id: email_token.id) - .first + @change_req = + @user + .email_change_requests + .where( + "old_email_token_id = :token_id OR new_email_token_id = :token_id", + token_id: email_token.id, + ) + .first case @change_req.try(:change_state) when EmailChangeRequest.states[:authorizing_old] @change_req.update!( change_state: EmailChangeRequest.states[:authorizing_new], - new_email_token: @user.email_tokens.create!(email: @change_req.new_email, scope: EmailToken.scopes[:email_update]) + new_email_token: + @user.email_tokens.create!( + email: @change_req.new_email, + scope: EmailToken.scopes[:email_update], + ), ) send_email("confirm_new_email", @change_req.new_email_token) confirm_result = :authorizing_new diff --git a/lib/ember_cli.rb b/lib/ember_cli.rb index fd3892918b5..00c65610bad 100644 --- a/lib/ember_cli.rb +++ b/lib/ember_cli.rb @@ -2,37 +2,47 @@ module EmberCli def self.assets - @assets ||= begin - assets = %w( - discourse.js - admin.js - wizard.js - ember_jquery.js - markdown-it-bundle.js - start-discourse.js - vendor.js - ) - assets += Dir.glob("app/assets/javascripts/discourse/scripts/*.js").map { |f| File.basename(f) } + @assets ||= + begin + assets = %w[ + discourse.js + admin.js + wizard.js + ember_jquery.js + markdown-it-bundle.js + start-discourse.js + vendor.js + ] + assets += + Dir.glob("app/assets/javascripts/discourse/scripts/*.js").map { |f| File.basename(f) } - Discourse.find_plugin_js_assets(include_disabled: true).each do |file| - next if file.ends_with?("_extra") # these are still handled by sprockets - assets << "#{file}.js" + Discourse + .find_plugin_js_assets(include_disabled: true) + .each do |file| + next if file.ends_with?("_extra") # these are still handled by sprockets + assets << "#{file}.js" + end + + assets end - - assets - end end def self.script_chunks - return @@chunk_infos if defined? @@chunk_infos + return @@chunk_infos if defined?(@@chunk_infos) - raw_chunk_infos = JSON.parse(File.read("#{Rails.configuration.root}/app/assets/javascripts/discourse/dist/chunks.json")) + raw_chunk_infos = + JSON.parse( + File.read("#{Rails.configuration.root}/app/assets/javascripts/discourse/dist/chunks.json"), + ) - chunk_infos = raw_chunk_infos["scripts"].map do |info| - logical_name = info["afterFile"][/\Aassets\/(.*)\.js\z/, 1] - chunks = info["scriptChunks"].map { |filename| filename[/\Aassets\/(.*)\.js\z/, 1] } - [logical_name, chunks] - end.to_h + chunk_infos = + raw_chunk_infos["scripts"] + .map do |info| + logical_name = info["afterFile"][%r{\Aassets/(.*)\.js\z}, 1] + chunks = info["scriptChunks"].map { |filename| filename[%r{\Aassets/(.*)\.js\z}, 1] } + [logical_name, chunks] + end + .to_h @@chunk_infos = chunk_infos if Rails.env.production? chunk_infos @@ -45,9 +55,11 @@ module EmberCli end def self.ember_version - @version ||= begin - ember_source_package_raw = File.read("#{Rails.root}/app/assets/javascripts/node_modules/ember-source/package.json") - JSON.parse(ember_source_package_raw)["version"] - end + @version ||= + begin + ember_source_package_raw = + File.read("#{Rails.root}/app/assets/javascripts/node_modules/ember-source/package.json") + JSON.parse(ember_source_package_raw)["version"] + end end end diff --git a/lib/encodings.rb b/lib/encodings.rb index 8bf0c7c72be..b8e68f24e57 100644 --- a/lib/encodings.rb +++ b/lib/encodings.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'rchardet' +require "rchardet" module Encodings def self.to_utf8(string) result = CharDet.detect(string) - encoded_string = try_utf8(string, result['encoding']) if result && result['encoding'] + encoded_string = try_utf8(string, result["encoding"]) if result && result["encoding"] encoded_string = force_utf8(string) if encoded_string.nil? encoded_string end @@ -15,21 +15,18 @@ module Encodings encoded = string.encode(Encoding::UTF_8, source_encoding) encoded&.valid_encoding? ? delete_bom!(encoded) : nil rescue Encoding::InvalidByteSequenceError, - Encoding::UndefinedConversionError, - Encoding::ConverterNotFoundError + Encoding::UndefinedConversionError, + Encoding::ConverterNotFoundError nil end def self.force_utf8(string) - encoded_string = string.encode(Encoding::UTF_8, - undef: :replace, - invalid: :replace, - replace: '') + encoded_string = string.encode(Encoding::UTF_8, undef: :replace, invalid: :replace, replace: "") delete_bom!(encoded_string) end def self.delete_bom!(string) - string.sub!(/\A\xEF\xBB\xBF/, '') unless string.blank? + string.sub!(/\A\xEF\xBB\xBF/, "") unless string.blank? string end end diff --git a/lib/enum.rb b/lib/enum.rb index d4401219002..9c87135c723 100644 --- a/lib/enum.rb +++ b/lib/enum.rb @@ -43,15 +43,11 @@ class Enum < Hash # Public: Create a subset of enum, only include specified keys. def only(*keys) - dup.tap do |d| - d.keep_if { |k| keys.include?(k) } - end + dup.tap { |d| d.keep_if { |k| keys.include?(k) } } end # Public: Create a subset of enum, preserve all items but specified ones. def except(*keys) - dup.tap do |d| - d.delete_if { |k| keys.include?(k) } - end + dup.tap { |d| d.delete_if { |k| keys.include?(k) } } end end diff --git a/lib/excerpt_parser.rb b/lib/excerpt_parser.rb index 2a4fbc8e3b1..f01499e9343 100644 --- a/lib/excerpt_parser.rb +++ b/lib/excerpt_parser.rb @@ -28,15 +28,14 @@ class ExcerptParser < Nokogiri::XML::SAX::Document end def self.get_excerpt(html, length, options) - html ||= '' - length = html.length if html.include?('excerpt') && CUSTOM_EXCERPT_REGEX === html + html ||= "" + length = html.length if html.include?("excerpt") && CUSTOM_EXCERPT_REGEX === html me = self.new(length, options) parser = Nokogiri::HTML::SAX::Parser.new(me) - catch(:done) do - parser.parse(html) - end + catch(:done) { parser.parse(html) } excerpt = me.excerpt.strip - excerpt = excerpt.gsub(/\s*\n+\s*/, "\n\n") if options[:keep_onebox_source] || options[:keep_onebox_body] + excerpt = excerpt.gsub(/\s*\n+\s*/, "\n\n") if options[:keep_onebox_source] || + options[:keep_onebox_body] excerpt = CGI.unescapeHTML(excerpt) if options[:text_entities] == true excerpt end @@ -53,8 +52,12 @@ class ExcerptParser < Nokogiri::XML::SAX::Document end def include_tag(name, attributes) - characters("<#{name} #{attributes.map { |k, v| "#{k}=\"#{escape_attribute(v)}\"" }.join(' ')}>", - truncate: false, count_it: false, encode: false) + characters( + "<#{name} #{attributes.map { |k, v| "#{k}=\"#{escape_attribute(v)}\"" }.join(" ")}>", + truncate: false, + count_it: false, + encode: false, + ) end def start_element(name, attributes = []) @@ -62,7 +65,7 @@ class ExcerptParser < Nokogiri::XML::SAX::Document when "img" attributes = Hash[*attributes.flatten] - if attributes["class"]&.include?('emoji') + if attributes["class"]&.include?("emoji") if @remap_emoji title = (attributes["alt"] || "").gsub(":", "") title = Emoji.lookup_unicode(title) || attributes["alt"] @@ -83,68 +86,53 @@ class ExcerptParser < Nokogiri::XML::SAX::Document elsif !attributes["title"].blank? characters("[#{attributes["title"]}]") else - characters("[#{I18n.t 'excerpt_image'}]") + characters("[#{I18n.t "excerpt_image"}]") end - characters("(#{attributes['src']})") if @markdown_images + characters("(#{attributes["src"]})") if @markdown_images end - when "a" unless @strip_links include_tag(name, attributes) @in_a = true end - when "aside" attributes = Hash[*attributes.flatten] - unless (@keep_onebox_source || @keep_onebox_body) && attributes['class']&.include?('onebox') + unless (@keep_onebox_source || @keep_onebox_body) && attributes["class"]&.include?("onebox") @in_quote = true end - if attributes['class']&.include?('quote') - if @keep_quotes || (@keep_onebox_body && attributes['data-topic'].present?) + if attributes["class"]&.include?("quote") + if @keep_quotes || (@keep_onebox_body && attributes["data-topic"].present?) @in_quote = false end end - - when 'article' - if attributes.include?(['class', 'onebox-body']) - @in_quote = !@keep_onebox_body - end - - when 'header' - if attributes.include?(['class', 'source']) - @in_quote = !@keep_onebox_source - end - + when "article" + @in_quote = !@keep_onebox_body if attributes.include?(%w[class onebox-body]) + when "header" + @in_quote = !@keep_onebox_source if attributes.include?(%w[class source]) when "div", "span" - if attributes.include?(["class", "excerpt"]) + if attributes.include?(%w[class excerpt]) @excerpt = +"" @current_length = 0 @start_excerpt = true end - when "details" @detail_contents = +"" if @in_details_depth == 0 @in_details_depth += 1 - when "summary" if @in_details_depth == 1 && !@in_summary @summary_contents = +"" @in_summary = true end - when "svg" attributes = Hash[*attributes.flatten] if attributes["class"]&.include?("d-icon") && @keep_svg include_tag(name, attributes) @in_svg = true end - when "use" - if @in_svg && @keep_svg - include_tag(name, attributes) - end + include_tag(name, attributes) if @in_svg && @keep_svg end end @@ -170,20 +158,22 @@ class ExcerptParser < Nokogiri::XML::SAX::Document @detail_contents = clean(@detail_contents) if @current_length + @summary_contents.length >= @length - characters(@summary_contents, - encode: false, - before_string: "

", - after_string: "
") + characters( + @summary_contents, + encode: false, + before_string: "
", + after_string: "
", + ) else - characters(@summary_contents, - truncate: false, - encode: false, - before_string: "
", - after_string: "") + characters( + @summary_contents, + truncate: false, + encode: false, + before_string: "
", + after_string: "", + ) - characters(@detail_contents, - encode: false, - after_string: "
") + characters(@detail_contents, encode: false, after_string: "
") end end when "summary" @@ -202,7 +192,14 @@ class ExcerptParser < Nokogiri::XML::SAX::Document ERB::Util.html_escape(str.strip) end - def characters(string, truncate: true, count_it: true, encode: true, before_string: nil, after_string: nil) + def characters( + string, + truncate: true, + count_it: true, + encode: true, + before_string: nil, + after_string: nil + ) return if @in_quote # we call length on this so might as well ensure we have a string diff --git a/lib/external_upload_helpers.rb b/lib/external_upload_helpers.rb index 5b0e43f5ab3..3ac4cea5bcc 100644 --- a/lib/external_upload_helpers.rb +++ b/lib/external_upload_helpers.rb @@ -5,35 +5,41 @@ module ExternalUploadHelpers extend ActiveSupport::Concern - class ExternalUploadValidationError < StandardError; end + class ExternalUploadValidationError < StandardError + end PRESIGNED_PUT_RATE_LIMIT_PER_MINUTE = 10 CREATE_MULTIPART_RATE_LIMIT_PER_MINUTE = 10 COMPLETE_MULTIPART_RATE_LIMIT_PER_MINUTE = 10 included do - before_action :external_store_check, only: [ - :generate_presigned_put, - :complete_external_upload, - :create_multipart, - :batch_presign_multipart_parts, - :abort_multipart, - :complete_multipart - ] - before_action :direct_s3_uploads_check, only: [ - :generate_presigned_put, - :complete_external_upload, - :create_multipart, - :batch_presign_multipart_parts, - :abort_multipart, - :complete_multipart - ] - before_action :can_upload_external?, only: [:create_multipart, :generate_presigned_put] + before_action :external_store_check, + only: %i[ + generate_presigned_put + complete_external_upload + create_multipart + batch_presign_multipart_parts + abort_multipart + complete_multipart + ] + before_action :direct_s3_uploads_check, + only: %i[ + generate_presigned_put + complete_external_upload + create_multipart + batch_presign_multipart_parts + abort_multipart + complete_multipart + ] + before_action :can_upload_external?, only: %i[create_multipart generate_presigned_put] end def generate_presigned_put RateLimiter.new( - current_user, "generate-presigned-put-upload-stub", ExternalUploadHelpers::PRESIGNED_PUT_RATE_LIMIT_PER_MINUTE, 1.minute + current_user, + "generate-presigned-put-upload-stub", + ExternalUploadHelpers::PRESIGNED_PUT_RATE_LIMIT_PER_MINUTE, + 1.minute, ).performed! file_name = params.require(:file_name) @@ -44,28 +50,28 @@ module ExternalUploadHelpers validate_before_create_direct_upload( file_name: file_name, file_size: file_size, - upload_type: type + upload_type: type, ) rescue ExternalUploadValidationError => err return render_json_error(err.message, status: 422) end - external_upload_data = ExternalUploadManager.create_direct_upload( - current_user: current_user, - file_name: file_name, - file_size: file_size, - upload_type: type, - metadata: parse_allowed_metadata(params[:metadata]) - ) + external_upload_data = + ExternalUploadManager.create_direct_upload( + current_user: current_user, + file_name: file_name, + file_size: file_size, + upload_type: type, + metadata: parse_allowed_metadata(params[:metadata]), + ) render json: external_upload_data end def complete_external_upload unique_identifier = params.require(:unique_identifier) - external_upload_stub = ExternalUploadStub.find_by( - unique_identifier: unique_identifier, created_by: current_user - ) + external_upload_stub = + ExternalUploadStub.find_by(unique_identifier: unique_identifier, created_by: current_user) return render_404 if external_upload_stub.blank? complete_external_upload_via_manager(external_upload_stub) @@ -73,7 +79,10 @@ module ExternalUploadHelpers def create_multipart RateLimiter.new( - current_user, "create-multipart-upload", ExternalUploadHelpers::CREATE_MULTIPART_RATE_LIMIT_PER_MINUTE, 1.minute + current_user, + "create-multipart-upload", + ExternalUploadHelpers::CREATE_MULTIPART_RATE_LIMIT_PER_MINUTE, + 1.minute, ).performed! file_name = params.require(:file_name) @@ -84,22 +93,23 @@ module ExternalUploadHelpers validate_before_create_multipart( file_name: file_name, file_size: file_size, - upload_type: upload_type + upload_type: upload_type, ) rescue ExternalUploadValidationError => err return render_json_error(err.message, status: 422) end begin - external_upload_data = create_direct_multipart_upload do - ExternalUploadManager.create_direct_multipart_upload( - current_user: current_user, - file_name: file_name, - file_size: file_size, - upload_type: upload_type, - metadata: parse_allowed_metadata(params[:metadata]) - ) - end + external_upload_data = + create_direct_multipart_upload do + ExternalUploadManager.create_direct_multipart_upload( + current_user: current_user, + file_name: file_name, + file_size: file_size, + upload_type: upload_type, + metadata: parse_allowed_metadata(params[:metadata]), + ) + end rescue ExternalUploadHelpers::ExternalUploadValidationError => err return render_json_error(err.message, status: 422) end @@ -121,21 +131,19 @@ module ExternalUploadHelpers # The other external upload endpoints are not hit as often, so they can stay as constant # values for now. RateLimiter.new( - current_user, "batch-presign", SiteSetting.max_batch_presign_multipart_per_minute, 1.minute + current_user, + "batch-presign", + SiteSetting.max_batch_presign_multipart_per_minute, + 1.minute, ).performed! - part_numbers = part_numbers.map do |part_number| - validate_part_number(part_number) - end + part_numbers = part_numbers.map { |part_number| validate_part_number(part_number) } - external_upload_stub = ExternalUploadStub.find_by( - unique_identifier: unique_identifier, created_by: current_user - ) + external_upload_stub = + ExternalUploadStub.find_by(unique_identifier: unique_identifier, created_by: current_user) return render_404 if external_upload_stub.blank? - if !multipart_upload_exists?(external_upload_stub) - return render_404 - end + return render_404 if !multipart_upload_exists?(external_upload_stub) store = multipart_store(external_upload_stub.upload_type) @@ -144,7 +152,7 @@ module ExternalUploadHelpers presigned_urls[part_number] = store.presign_multipart_part( upload_id: external_upload_stub.external_upload_identifier, key: external_upload_stub.key, - part_number: part_number + part_number: part_number, ) end @@ -157,10 +165,16 @@ module ExternalUploadHelpers store.list_multipart_parts( upload_id: external_upload_stub.external_upload_identifier, key: external_upload_stub.key, - max_parts: 1 + max_parts: 1, ) rescue Aws::S3::Errors::NoSuchUpload => err - debug_upload_error(err, I18n.t("upload.external_upload_not_found", additional_detail: "path: #{external_upload_stub.key}")) + debug_upload_error( + err, + I18n.t( + "upload.external_upload_not_found", + additional_detail: "path: #{external_upload_stub.key}", + ), + ) return false end true @@ -168,9 +182,8 @@ module ExternalUploadHelpers def abort_multipart external_upload_identifier = params.require(:external_upload_identifier) - external_upload_stub = ExternalUploadStub.find_by( - external_upload_identifier: external_upload_identifier - ) + external_upload_stub = + ExternalUploadStub.find_by(external_upload_identifier: external_upload_identifier) # The stub could have already been deleted by an earlier error via # ExternalUploadManager, so we consider this a great success if the @@ -183,12 +196,20 @@ module ExternalUploadHelpers begin store.abort_multipart( upload_id: external_upload_stub.external_upload_identifier, - key: external_upload_stub.key + key: external_upload_stub.key, ) rescue Aws::S3::Errors::ServiceError => err - return render_json_error( - debug_upload_error(err, I18n.t("upload.abort_multipart_failure", additional_detail: "external upload stub id: #{external_upload_stub.id}")), - status: 422 + return( + render_json_error( + debug_upload_error( + err, + I18n.t( + "upload.abort_multipart_failure", + additional_detail: "external upload stub id: #{external_upload_stub.id}", + ), + ), + status: 422, + ) ) end @@ -202,45 +223,57 @@ module ExternalUploadHelpers parts = params.require(:parts) RateLimiter.new( - current_user, "complete-multipart-upload", ExternalUploadHelpers::COMPLETE_MULTIPART_RATE_LIMIT_PER_MINUTE, 1.minute + current_user, + "complete-multipart-upload", + ExternalUploadHelpers::COMPLETE_MULTIPART_RATE_LIMIT_PER_MINUTE, + 1.minute, ).performed! - external_upload_stub = ExternalUploadStub.find_by( - unique_identifier: unique_identifier, created_by: current_user - ) + external_upload_stub = + ExternalUploadStub.find_by(unique_identifier: unique_identifier, created_by: current_user) return render_404 if external_upload_stub.blank? - if !multipart_upload_exists?(external_upload_stub) - return render_404 - end + return render_404 if !multipart_upload_exists?(external_upload_stub) store = multipart_store(external_upload_stub.upload_type) - parts = parts.map do |part| - part_number = part[:part_number] - etag = part[:etag] - part_number = validate_part_number(part_number) + parts = + parts + .map do |part| + part_number = part[:part_number] + etag = part[:etag] + part_number = validate_part_number(part_number) - if etag.blank? - raise Discourse::InvalidParameters.new("All parts must have an etag and a valid part number") - end + if etag.blank? + raise Discourse::InvalidParameters.new( + "All parts must have an etag and a valid part number", + ) + end - # this is done so it's an array of hashes rather than an array of - # ActionController::Parameters - { part_number: part_number, etag: etag } - end.sort_by do |part| - part[:part_number] - end + # this is done so it's an array of hashes rather than an array of + # ActionController::Parameters + { part_number: part_number, etag: etag } + end + .sort_by { |part| part[:part_number] } begin - complete_response = store.complete_multipart( - upload_id: external_upload_stub.external_upload_identifier, - key: external_upload_stub.key, - parts: parts - ) + complete_response = + store.complete_multipart( + upload_id: external_upload_stub.external_upload_identifier, + key: external_upload_stub.key, + parts: parts, + ) rescue Aws::S3::Errors::ServiceError => err - return render_json_error( - debug_upload_error(err, I18n.t("upload.complete_multipart_failure", additional_detail: "external upload stub id: #{external_upload_stub.id}")), - status: 422 + return( + render_json_error( + debug_upload_error( + err, + I18n.t( + "upload.complete_multipart_failure", + additional_detail: "external upload stub id: #{external_upload_stub.id}", + ), + ), + status: 422, + ) ) end @@ -270,27 +303,40 @@ module ExternalUploadHelpers end rescue ExternalUploadManager::SizeMismatchError => err render_json_error( - debug_upload_error(err, I18n.t("upload.size_mismatch_failure", additional_detail: err.message)), - status: 422 + debug_upload_error( + err, + I18n.t("upload.size_mismatch_failure", additional_detail: err.message), + ), + status: 422, ) rescue ExternalUploadManager::ChecksumMismatchError => err render_json_error( - debug_upload_error(err, I18n.t("upload.checksum_mismatch_failure", additional_detail: err.message)), - status: 422 + debug_upload_error( + err, + I18n.t("upload.checksum_mismatch_failure", additional_detail: err.message), + ), + status: 422, ) rescue ExternalUploadManager::CannotPromoteError => err render_json_error( - debug_upload_error(err, I18n.t("upload.cannot_promote_failure", additional_detail: err.message)), - status: 422 + debug_upload_error( + err, + I18n.t("upload.cannot_promote_failure", additional_detail: err.message), + ), + status: 422, ) rescue ExternalUploadManager::DownloadFailedError, Aws::S3::Errors::NotFound => err render_json_error( - debug_upload_error(err, I18n.t("upload.download_failure", additional_detail: err.message)), - status: 422 + debug_upload_error( + err, + I18n.t("upload.download_failure", additional_detail: err.message), + ), + status: 422, ) rescue => err Discourse.warn_exception( - err, message: "Complete external upload failed unexpectedly for user #{current_user.id}" + err, + message: "Complete external upload failed unexpectedly for user #{current_user.id}", ) render_json_error(I18n.t("upload.failed"), status: 422) @@ -308,10 +354,8 @@ module ExternalUploadHelpers def validate_part_number(part_number) part_number = part_number.to_i - if !part_number.between?(1, 10000) - raise Discourse::InvalidParameters.new( - "Each part number should be between 1 and 10000" - ) + if !part_number.between?(1, 10_000) + raise Discourse::InvalidParameters.new("Each part number should be between 1 and 10000") end part_number end diff --git a/lib/faker/discourse.rb b/lib/faker/discourse.rb index a2c4720026d..ed527841dcb 100644 --- a/lib/faker/discourse.rb +++ b/lib/faker/discourse.rb @@ -1,25 +1,24 @@ # frozen_string_literal: true -require 'faker' +require "faker" module Faker class Discourse < Base class << self - def tag - fetch('discourse.tags') + fetch("discourse.tags") end def category - fetch('discourse.categories') + fetch("discourse.categories") end def group - fetch('discourse.groups') + fetch("discourse.groups") end def topic - fetch('discourse.topics') + fetch("discourse.topics") end end end diff --git a/lib/faker/discourse_markdown.rb b/lib/faker/discourse_markdown.rb index 8f5266a1914..f893f051894 100644 --- a/lib/faker/discourse_markdown.rb +++ b/lib/faker/discourse_markdown.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'faker' -require 'net/http' -require 'json' +require "faker" +require "net/http" +require "json" module Faker class DiscourseMarkdown < Markdown @@ -27,11 +27,8 @@ module Faker image = next_image image_file = load_image(image) - upload = ::UploadCreator.new( - image_file, - image[:filename], - origin: image[:url] - ).create_for(user_id) + upload = + ::UploadCreator.new(image_file, image[:filename], origin: image[:url]).create_for(user_id) ::UploadMarkdown.new(upload).to_markdown if upload.present? && upload.persisted? rescue => e @@ -62,7 +59,7 @@ module Faker end image = @images.pop - { filename: "#{image['id']}.jpg", url: "#{image['download_url']}.jpg" } + { filename: "#{image["id"]}.jpg", url: "#{image["download_url"]}.jpg" } end def image_cache_dir @@ -74,12 +71,13 @@ module Faker if !::File.exist?(cache_path) FileUtils.mkdir_p(image_cache_dir) - temp_file = ::FileHelper.download( - image[:url], - max_file_size: [SiteSetting.max_image_size_kb.kilobytes, 10.megabytes].max, - tmp_file_name: "image", - follow_redirect: true - ) + temp_file = + ::FileHelper.download( + image[:url], + max_file_size: [SiteSetting.max_image_size_kb.kilobytes, 10.megabytes].max, + tmp_file_name: "image", + follow_redirect: true, + ) FileUtils.cp(temp_file, cache_path) end diff --git a/lib/feed_element_installer.rb b/lib/feed_element_installer.rb index 2e0ecd40ec3..c96e89d1446 100644 --- a/lib/feed_element_installer.rb +++ b/lib/feed_element_installer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'rexml/document' -require 'rss' +require "rexml/document" +require "rss" class FeedElementInstaller private_class_method :new @@ -10,7 +10,7 @@ class FeedElementInstaller # RSS Specification at http://cyber.harvard.edu/rss/rss.html#extendingRss # > A RSS feed may contain [non-standard elements], only if those elements are *defined in a namespace* - new(element_name, feed).install if element_name.include?(':') + new(element_name, feed).install if element_name.include?(":") end attr_reader :feed, :original_name, :element_namespace, :element_name, :element_accessor @@ -18,12 +18,13 @@ class FeedElementInstaller def initialize(element_name, feed) @feed = feed @original_name = element_name - @element_namespace, @element_name = *element_name.split(':') + @element_namespace, @element_name = *element_name.split(":") @element_accessor = "#{@element_namespace}_#{@element_name}" end def element_uri - @element_uri ||= REXML::Document.new(feed).root&.attributes&.namespaces&.fetch(@element_namespace, '') || '' + @element_uri ||= + REXML::Document.new(feed).root&.attributes&.namespaces&.fetch(@element_namespace, "") || "" end def install @@ -34,13 +35,34 @@ class FeedElementInstaller private def install_in_rss - RSS::Rss::Channel::Item.install_text_element(element_name, element_uri, '?', element_accessor, nil, original_name) + RSS::Rss::Channel::Item.install_text_element( + element_name, + element_uri, + "?", + element_accessor, + nil, + original_name, + ) RSS::BaseListener.install_get_text_element(element_uri, element_name, element_accessor) end def install_in_atom - RSS::Atom::Entry.install_text_element(element_name, element_uri, '?', element_accessor, nil, original_name) - RSS::Atom::Feed::Entry.install_text_element(element_name, element_uri, '?', element_accessor, nil, original_name) + RSS::Atom::Entry.install_text_element( + element_name, + element_uri, + "?", + element_accessor, + nil, + original_name, + ) + RSS::Atom::Feed::Entry.install_text_element( + element_name, + element_uri, + "?", + element_accessor, + nil, + original_name, + ) RSS::BaseListener.install_get_text_element(element_uri, element_name, element_accessor) end @@ -49,6 +71,7 @@ class FeedElementInstaller end def installed_in_atom? - RSS::Atom::Entry.method_defined?(element_accessor) || RSS::Atom::Feed::Entry.method_defined?(element_accessor) + RSS::Atom::Entry.method_defined?(element_accessor) || + RSS::Atom::Feed::Entry.method_defined?(element_accessor) end end diff --git a/lib/file_helper.rb b/lib/file_helper.rb index 31530a12e96..9b57251f83c 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -5,11 +5,10 @@ require "mini_mime" require "open-uri" class FileHelper - def self.log(log_level, message) Rails.logger.public_send( log_level, - "#{RailsMultisite::ConnectionManagement.current_db}: #{message}" + "#{RailsMultisite::ConnectionManagement.current_db}: #{message}", ) end @@ -41,29 +40,31 @@ class FileHelper attr_accessor :status end - def self.download(url, - max_file_size:, - tmp_file_name:, - follow_redirect: false, - read_timeout: 5, - skip_rate_limit: false, - verbose: false, - validate_uri: true, - retain_on_max_file_size_exceeded: false) - + def self.download( + url, + max_file_size:, + tmp_file_name:, + follow_redirect: false, + read_timeout: 5, + skip_rate_limit: false, + verbose: false, + validate_uri: true, + retain_on_max_file_size_exceeded: false + ) url = "https:" + url if url.start_with?("//") - raise Discourse::InvalidParameters.new(:url) unless url =~ /^https?:\/\// + raise Discourse::InvalidParameters.new(:url) unless url =~ %r{^https?://} tmp = nil - fd = FinalDestination.new( - url, - max_redirects: follow_redirect ? 5 : 0, - skip_rate_limit: skip_rate_limit, - verbose: verbose, - validate_uri: validate_uri, - timeout: read_timeout - ) + fd = + FinalDestination.new( + url, + max_redirects: follow_redirect ? 5 : 0, + skip_rate_limit: skip_rate_limit, + verbose: verbose, + validate_uri: validate_uri, + timeout: read_timeout, + ) fd.get do |response, chunk, uri| if tmp.nil? @@ -110,7 +111,7 @@ class FileHelper def self.optimize_image!(filename, allow_pngquant: false) image_optim( allow_pngquant: allow_pngquant, - strip_image_metadata: SiteSetting.strip_image_metadata + strip_image_metadata: SiteSetting.strip_image_metadata, ).optimize_image!(filename) end @@ -119,23 +120,26 @@ class FileHelper # sometimes up to 200ms searching for binaries and looking at versions memoize("image_optim", allow_pngquant, strip_image_metadata) do pngquant_options = false - if allow_pngquant - pngquant_options = { allow_lossy: true } - end + pngquant_options = { allow_lossy: true } if allow_pngquant ImageOptim.new( # GLOBAL timeout: 15, skip_missing_workers: true, # PNG - oxipng: { level: 3, strip: strip_image_metadata }, + oxipng: { + level: 3, + strip: strip_image_metadata, + }, optipng: false, advpng: false, pngcrush: false, pngout: false, pngquant: pngquant_options, # JPG - jpegoptim: { strip: strip_image_metadata ? "all" : "none" }, + jpegoptim: { + strip: strip_image_metadata ? "all" : "none", + }, jpegtran: false, jpegrecompress: false, # Skip looking for gifsicle, svgo binaries @@ -150,24 +154,24 @@ class FileHelper end def self.supported_gravatar_extensions - @@supported_gravatar_images ||= Set.new(%w{jpg jpeg png gif}) + @@supported_gravatar_images ||= Set.new(%w[jpg jpeg png gif]) end def self.supported_images - @@supported_images ||= Set.new %w{jpg jpeg png gif svg ico webp} + @@supported_images ||= Set.new %w[jpg jpeg png gif svg ico webp] end def self.inline_images # SVG cannot safely be shown as a document - @@inline_images ||= supported_images - %w{svg} + @@inline_images ||= supported_images - %w[svg] end def self.supported_audio - @@supported_audio ||= Set.new %w{mp3 ogg oga opus wav m4a m4b m4p m4r aac flac} + @@supported_audio ||= Set.new %w[mp3 ogg oga opus wav m4a m4b m4p m4r aac flac] end def self.supported_video - @@supported_video ||= Set.new %w{mov mp4 webm ogv m4v 3gp avi mpeg} + @@supported_video ||= Set.new %w[mov mp4 webm ogv m4v 3gp avi mpeg] end def self.supported_video_regexp diff --git a/lib/file_store/base_store.rb b/lib/file_store/base_store.rb index 8b4c41cf6f4..f73114ca897 100644 --- a/lib/file_store/base_store.rb +++ b/lib/file_store/base_store.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true module FileStore - class BaseStore - UPLOAD_PATH_REGEX ||= %r|/(original/\d+X/.*)| - OPTIMIZED_IMAGE_PATH_REGEX ||= %r|/(optimized/\d+X/.*)| + UPLOAD_PATH_REGEX ||= %r{/(original/\d+X/.*)} + OPTIMIZED_IMAGE_PATH_REGEX ||= %r{/(optimized/\d+X/.*)} TEMPORARY_UPLOAD_PREFIX ||= "temp/" def store_upload(file, upload, content_type = nil) @@ -38,7 +37,7 @@ module FileStore def upload_path path = File.join("uploads", RailsMultisite::ConnectionManagement.current_db) return path if !Rails.env.test? - File.join(path, "test_#{ENV['TEST_ENV_NUMBER'].presence || '0'}") + File.join(path, "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}") end def self.temporary_upload_path(file_name, folder_prefix: "") @@ -46,12 +45,7 @@ module FileStore # characters, which can interfere with external providers operations and # introduce other unexpected behaviour. file_name_random = "#{SecureRandom.hex}#{File.extname(file_name)}" - File.join( - TEMPORARY_UPLOAD_PREFIX, - folder_prefix, - SecureRandom.hex, - file_name_random - ) + File.join(TEMPORARY_UPLOAD_PREFIX, folder_prefix, SecureRandom.hex, file_name_random) end def has_been_uploaded?(url) @@ -96,25 +90,37 @@ module FileStore def download(object, max_file_size_kb: nil) DistributedMutex.synchronize("download_#{object.sha1}", validity: 3.minutes) do - extension = File.extname(object.respond_to?(:original_filename) ? object.original_filename : object.url) + extension = + File.extname( + object.respond_to?(:original_filename) ? object.original_filename : object.url, + ) filename = "#{object.sha1}#{extension}" file = get_from_cache(filename) if !file - max_file_size_kb ||= [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes + max_file_size_kb ||= [ + SiteSetting.max_image_size_kb, + SiteSetting.max_attachment_size_kb, + ].max.kilobytes secure = object.respond_to?(:secure) ? object.secure? : object.upload.secure? - url = secure ? - Discourse.store.signed_url_for_path(object.url) : - Discourse.store.cdn_url(object.url) + url = + ( + if secure + Discourse.store.signed_url_for_path(object.url) + else + Discourse.store.cdn_url(object.url) + end + ) - url = SiteSetting.scheme + ":" + url if url =~ /^\/\// - file = FileHelper.download( - url, - max_file_size: max_file_size_kb, - tmp_file_name: "discourse-download", - follow_redirect: true - ) + url = SiteSetting.scheme + ":" + url if url =~ %r{^//} + file = + FileHelper.download( + url, + max_file_size: max_file_size_kb, + tmp_file_name: "discourse-download", + follow_redirect: true, + ) return nil if file.nil? @@ -162,7 +168,8 @@ module FileStore upload = optimized_image.upload version = optimized_image.version || 1 - extension = "_#{version}_#{optimized_image.width}x#{optimized_image.height}#{optimized_image.extension}" + extension = + "_#{version}_#{optimized_image.width}x#{optimized_image.height}#{optimized_image.extension}" get_path_for("optimized", upload.id, upload.sha1, extension) end @@ -214,5 +221,4 @@ module FileStore path end end - end diff --git a/lib/file_store/local_store.rb b/lib/file_store/local_store.rb index 4922eca7077..c0461aa50a7 100644 --- a/lib/file_store/local_store.rb +++ b/lib/file_store/local_store.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require 'file_store/base_store' +require "file_store/base_store" module FileStore - class LocalStore < BaseStore - def store_file(file, path) copy_file(file, "#{public_dir}#{path}") "#{Discourse.base_path}#{path}" @@ -64,7 +62,13 @@ module FileStore def purge_tombstone(grace_period) if Dir.exist?(Discourse.store.tombstone_dir) Discourse::Utils.execute_command( - 'find', tombstone_dir, '-mtime', "+#{grace_period}", '-type', 'f', '-delete' + "find", + tombstone_dir, + "-mtime", + "+#{grace_period}", + "-type", + "f", + "-delete", ) end end @@ -108,9 +112,13 @@ module FileStore FileUtils.mkdir_p(File.join(public_dir, upload_path)) Discourse::Utils.execute_command( - 'rsync', '-a', '--safe-links', "#{source_path}/", "#{upload_path}/", + "rsync", + "-a", + "--safe-links", + "#{source_path}/", + "#{upload_path}/", failure_message: "Failed to copy uploads.", - chdir: public_dir + chdir: public_dir, ) end @@ -119,15 +127,14 @@ module FileStore def list_missing(model) count = 0 model.find_each do |upload| - # could be a remote image - next unless upload.url =~ /^\/[^\/]/ + next unless upload.url =~ %r{^/[^/]} path = "#{public_dir}#{upload.url}" bad = true begin bad = false if File.size(path) != 0 - rescue + rescue StandardError # something is messed up end if bad diff --git a/lib/file_store/s3_store.rb b/lib/file_store/s3_store.rb index 074bf924468..8fdbd2d9fab 100644 --- a/lib/file_store/s3_store.rb +++ b/lib/file_store/s3_store.rb @@ -7,61 +7,64 @@ require "s3_helper" require "file_helper" module FileStore - class S3Store < BaseStore TOMBSTONE_PREFIX ||= "tombstone/" - delegate :abort_multipart, :presign_multipart_part, :list_multipart_parts, - :complete_multipart, to: :s3_helper + delegate :abort_multipart, + :presign_multipart_part, + :list_multipart_parts, + :complete_multipart, + to: :s3_helper def initialize(s3_helper = nil) @s3_helper = s3_helper end def s3_helper - @s3_helper ||= S3Helper.new(s3_bucket, - Rails.configuration.multisite ? multisite_tombstone_prefix : TOMBSTONE_PREFIX - ) + @s3_helper ||= + S3Helper.new( + s3_bucket, + Rails.configuration.multisite ? multisite_tombstone_prefix : TOMBSTONE_PREFIX, + ) end def store_upload(file, upload, content_type = nil) upload.url = nil path = get_path_for_upload(upload) - url, upload.etag = store_file( - file, - path, - filename: upload.original_filename, - content_type: content_type, - cache_locally: true, - private_acl: upload.secure? - ) + url, upload.etag = + store_file( + file, + path, + filename: upload.original_filename, + content_type: content_type, + cache_locally: true, + private_acl: upload.secure?, + ) url end - def move_existing_stored_upload( - existing_external_upload_key:, - upload: nil, - content_type: nil - ) + def move_existing_stored_upload(existing_external_upload_key:, upload: nil, content_type: nil) upload.url = nil path = get_path_for_upload(upload) - url, upload.etag = store_file( - nil, - path, - filename: upload.original_filename, - content_type: content_type, - cache_locally: false, - private_acl: upload.secure?, - move_existing: true, - existing_external_upload_key: existing_external_upload_key - ) + url, upload.etag = + store_file( + nil, + path, + filename: upload.original_filename, + content_type: content_type, + cache_locally: false, + private_acl: upload.secure?, + move_existing: true, + existing_external_upload_key: existing_external_upload_key, + ) url end def store_optimized_image(file, optimized_image, content_type = nil, secure: false) optimized_image.url = nil path = get_path_for_optimized_image(optimized_image) - url, optimized_image.etag = store_file(file, path, content_type: content_type, private_acl: secure) + url, optimized_image.etag = + store_file(file, path, content_type: content_type, private_acl: secure) url end @@ -85,8 +88,9 @@ module FileStore cache_file(file, File.basename(path)) if opts[:cache_locally] options = { acl: opts[:private_acl] ? "private" : "public-read", - cache_control: 'max-age=31556952, public, immutable', - content_type: opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type + cache_control: "max-age=31556952, public, immutable", + content_type: + opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type, } # add a "content disposition: attachment" header with the original @@ -96,7 +100,8 @@ module FileStore # browser. if !FileHelper.is_inline_image?(filename) options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format( - disposition: "attachment", filename: filename + disposition: "attachment", + filename: filename, ) end @@ -106,11 +111,7 @@ module FileStore if opts[:move_existing] && opts[:existing_external_upload_key] original_path = opts[:existing_external_upload_key] options[:apply_metadata_to_destination] = true - path, etag = s3_helper.copy( - original_path, - path, - options: options - ) + path, etag = s3_helper.copy(original_path, path, options: options) delete_file(original_path) else path, etag = s3_helper.upload(file, path, options) @@ -142,7 +143,7 @@ module FileStore begin parsed_url = URI.parse(UrlHelper.encode(url)) - rescue + rescue StandardError # There are many exceptions possible here including Addressable::URI:: exceptions # and URI:: exceptions, catch all may seem wide, but it makes no sense to raise ever # on an invalid url here @@ -169,7 +170,10 @@ module FileStore s3_cdn_url = URI.parse(SiteSetting.Upload.s3_cdn_url || "") cdn_hostname = s3_cdn_url.hostname - return true if cdn_hostname.presence && url[cdn_hostname] && (s3_cdn_url.path.blank? || parsed_url.path.starts_with?(s3_cdn_url.path)) + if cdn_hostname.presence && url[cdn_hostname] && + (s3_cdn_url.path.blank? || parsed_url.path.starts_with?(s3_cdn_url.path)) + return true + end false end @@ -186,7 +190,11 @@ module FileStore end def s3_upload_host - SiteSetting.Upload.s3_cdn_url.present? ? SiteSetting.Upload.s3_cdn_url : "https:#{absolute_base_url}" + if SiteSetting.Upload.s3_cdn_url.present? + SiteSetting.Upload.s3_cdn_url + else + "https:#{absolute_base_url}" + end end def external? @@ -208,28 +216,45 @@ module FileStore def path_for(upload) url = upload&.url - FileStore::LocalStore.new.path_for(upload) if url && url[/^\/[^\/]/] + FileStore::LocalStore.new.path_for(upload) if url && url[%r{^/[^/]}] end def url_for(upload, force_download: false) - upload.secure? || force_download ? - presigned_get_url(get_upload_key(upload), force_download: force_download, filename: upload.original_filename) : + if upload.secure? || force_download + presigned_get_url( + get_upload_key(upload), + force_download: force_download, + filename: upload.original_filename, + ) + else upload.url + end end def cdn_url(url) return url if SiteSetting.Upload.s3_cdn_url.blank? - schema = url[/^(https?:)?\/\//, 1] + schema = url[%r{^(https?:)?//}, 1] folder = s3_bucket_folder_path.nil? ? "" : "#{s3_bucket_folder_path}/" - url.sub(File.join("#{schema}#{absolute_base_url}", folder), File.join(SiteSetting.Upload.s3_cdn_url, "/")) + url.sub( + File.join("#{schema}#{absolute_base_url}", folder), + File.join(SiteSetting.Upload.s3_cdn_url, "/"), + ) end - def signed_url_for_path(path, expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds, force_download: false) + def signed_url_for_path( + path, + expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds, + force_download: false + ) key = path.sub(absolute_base_url + "/", "") presigned_get_url(key, expires_in: expires_in, force_download: force_download) end - def signed_url_for_temporary_upload(file_name, expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, metadata: {}) + def signed_url_for_temporary_upload( + file_name, + expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, + metadata: {} + ) key = temporary_upload_path(file_name) s3_helper.presigned_url( key, @@ -237,16 +262,15 @@ module FileStore expires_in: expires_in, opts: { metadata: metadata, - acl: "private" - } + acl: "private", + }, ) end def temporary_upload_path(file_name) - folder_prefix = s3_bucket_folder_path.nil? ? upload_path : File.join(s3_bucket_folder_path, upload_path) - FileStore::BaseStore.temporary_upload_path( - file_name, folder_prefix: folder_prefix - ) + folder_prefix = + s3_bucket_folder_path.nil? ? upload_path : File.join(s3_bucket_folder_path, upload_path) + FileStore::BaseStore.temporary_upload_path(file_name, folder_prefix: folder_prefix) end def object_from_path(path) @@ -264,13 +288,15 @@ module FileStore end def s3_bucket - raise Discourse::SiteSettingMissing.new("s3_upload_bucket") if SiteSetting.Upload.s3_upload_bucket.blank? + if SiteSetting.Upload.s3_upload_bucket.blank? + raise Discourse::SiteSettingMissing.new("s3_upload_bucket") + end SiteSetting.Upload.s3_upload_bucket.downcase end def list_missing_uploads(skip_optimized: false) if SiteSetting.enable_s3_inventory - require 's3_inventory' + require "s3_inventory" S3Inventory.new(s3_helper, :upload).backfill_etags_and_list_missing S3Inventory.new(s3_helper, :optimized).backfill_etags_and_list_missing unless skip_optimized else @@ -326,7 +352,6 @@ module FileStore s3_options: FileStore::ToS3Migration.s3_options_from_site_settings, migrate_to_multisite: Rails.configuration.multisite, ).migrate - ensure FileUtils.rm(public_upload_path) if File.symlink?(public_upload_path) FileUtils.mv(old_upload_path, public_upload_path) if old_upload_path @@ -349,7 +374,8 @@ module FileStore if force_download && filename opts[:response_content_disposition] = ActionDispatch::Http::ContentDisposition.format( - disposition: "attachment", filename: filename + disposition: "attachment", + filename: filename, ) end @@ -375,11 +401,11 @@ module FileStore def list_missing(model, prefix) connection = ActiveRecord::Base.connection.raw_connection - connection.exec('CREATE TEMP TABLE verified_ids(val integer PRIMARY KEY)') + connection.exec("CREATE TEMP TABLE verified_ids(val integer PRIMARY KEY)") marker = nil files = s3_helper.list(prefix, marker) - while files.count > 0 do + while files.count > 0 verified_ids = [] files.each do |f| @@ -388,23 +414,25 @@ module FileStore marker = f.key end - verified_id_clause = verified_ids.map { |id| "('#{PG::Connection.escape_string(id.to_s)}')" }.join(",") + verified_id_clause = + verified_ids.map { |id| "('#{PG::Connection.escape_string(id.to_s)}')" }.join(",") connection.exec("INSERT INTO verified_ids VALUES #{verified_id_clause}") files = s3_helper.list(prefix, marker) end - missing_uploads = model.joins('LEFT JOIN verified_ids ON verified_ids.val = id').where("verified_ids.val IS NULL") + missing_uploads = + model.joins("LEFT JOIN verified_ids ON verified_ids.val = id").where( + "verified_ids.val IS NULL", + ) missing_count = missing_uploads.count if missing_count > 0 - missing_uploads.find_each do |upload| - puts upload.url - end + missing_uploads.find_each { |upload| puts upload.url } puts "#{missing_count} of #{model.count} #{model.name.underscore.pluralize} are missing" end ensure - connection.exec('DROP TABLE verified_ids') unless connection.nil? + connection.exec("DROP TABLE verified_ids") unless connection.nil? end end end diff --git a/lib/file_store/to_s3_migration.rb b/lib/file_store/to_s3_migration.rb index 0d6c098327f..b99c911a6b9 100644 --- a/lib/file_store/to_s3_migration.rb +++ b/lib/file_store/to_s3_migration.rb @@ -1,16 +1,20 @@ # frozen_string_literal: true -require 'aws-sdk-s3' +require "aws-sdk-s3" module FileStore ToS3MigrationError = Class.new(RuntimeError) class ToS3Migration - MISSING_UPLOADS_RAKE_TASK_NAME ||= 'posts:missing_uploads' + MISSING_UPLOADS_RAKE_TASK_NAME ||= "posts:missing_uploads" UPLOAD_CONCURRENCY ||= 20 - def initialize(s3_options:, dry_run: false, migrate_to_multisite: false, skip_etag_verify: false) - + def initialize( + s3_options:, + dry_run: false, + migrate_to_multisite: false, + skip_etag_verify: false + ) @s3_bucket = s3_options[:bucket] @s3_client_options = s3_options[:client_options] @dry_run = dry_run @@ -22,20 +26,18 @@ module FileStore def self.s3_options_from_site_settings { client_options: S3Helper.s3_options(SiteSetting), - bucket: SiteSetting.Upload.s3_upload_bucket + bucket: SiteSetting.Upload.s3_upload_bucket, } end def self.s3_options_from_env - unless ENV["DISCOURSE_S3_BUCKET"].present? && - ENV["DISCOURSE_S3_REGION"].present? && - ( - ( - ENV["DISCOURSE_S3_ACCESS_KEY_ID"].present? && - ENV["DISCOURSE_S3_SECRET_ACCESS_KEY"].present? - ) || ENV["DISCOURSE_S3_USE_IAM_PROFILE"].present? - ) - + unless ENV["DISCOURSE_S3_BUCKET"].present? && ENV["DISCOURSE_S3_REGION"].present? && + ( + ( + ENV["DISCOURSE_S3_ACCESS_KEY_ID"].present? && + ENV["DISCOURSE_S3_SECRET_ACCESS_KEY"].present? + ) || ENV["DISCOURSE_S3_USE_IAM_PROFILE"].present? + ) raise ToS3MigrationError.new(<<~TEXT) Please provide the following environment variables: - DISCOURSE_S3_BUCKET @@ -53,13 +55,10 @@ module FileStore if ENV["DISCOURSE_S3_USE_IAM_PROFILE"].blank? opts[:access_key_id] = ENV["DISCOURSE_S3_ACCESS_KEY_ID"] - opts[:secret_access_key] = ENV["DISCOURSE_S3_SECRET_ACCESS_KEY"] + opts[:secret_access_key] = ENV["DISCOURSE_S3_SECRET_ACCESS_KEY"] end - { - client_options: opts, - bucket: ENV["DISCOURSE_S3_BUCKET"] - } + { client_options: opts, bucket: ENV["DISCOURSE_S3_BUCKET"] } end def migrate @@ -75,7 +74,8 @@ module FileStore base_url = File.join(SiteSetting.Upload.s3_base_url, prefix) count = Upload.by_users.where("url NOT LIKE '#{base_url}%'").count if count > 0 - error_message = "#{count} of #{Upload.count} uploads are not migrated to S3. #{failure_message}" + error_message = + "#{count} of #{Upload.count} uploads are not migrated to S3. #{failure_message}" raise_or_log(error_message, should_raise) success = false end @@ -88,7 +88,9 @@ module FileStore success = false end - Discourse::Application.load_tasks unless Rake::Task.task_defined?(MISSING_UPLOADS_RAKE_TASK_NAME) + unless Rake::Task.task_defined?(MISSING_UPLOADS_RAKE_TASK_NAME) + Discourse::Application.load_tasks + end Rake::Task[MISSING_UPLOADS_RAKE_TASK_NAME] count = DB.query_single(<<~SQL, Post::MISSING_UPLOADS, Post::MISSING_UPLOADS_IGNORED).first SELECT COUNT(1) @@ -109,10 +111,14 @@ module FileStore success = false end - count = Post.where('baked_version <> ? OR baked_version IS NULL', Post::BAKED_VERSION).count + count = Post.where("baked_version <> ? OR baked_version IS NULL", Post::BAKED_VERSION).count if count > 0 log("#{count} posts still require rebaking and will be rebaked during regular job") - log("To speed up migrations of posts we recommend you run 'rake posts:rebake_uncooked_posts'") if count > 100 + if count > 100 + log( + "To speed up migrations of posts we recommend you run 'rake posts:rebake_uncooked_posts'", + ) + end success = false else log("No posts require rebaking") @@ -153,8 +159,10 @@ module FileStore Upload.migrate_to_new_scheme if !uploads_migrated_to_new_scheme? - raise ToS3MigrationError.new("Some uploads could not be migrated to the new scheme. " \ - "You need to fix this manually.") + raise ToS3MigrationError.new( + "Some uploads could not be migrated to the new scheme. " \ + "You need to fix this manually.", + ) end end @@ -174,10 +182,12 @@ module FileStore log " - Listing local files" local_files = [] - IO.popen("cd #{public_directory} && find uploads/#{@current_db}/original -type f").each do |file| - local_files << file.chomp - putc "." if local_files.size % 1000 == 0 - end + IO + .popen("cd #{public_directory} && find uploads/#{@current_db}/original -type f") + .each do |file| + local_files << file.chomp + putc "." if local_files.size % 1000 == 0 + end log " => #{local_files.size} files" log " - Listing S3 files" @@ -203,19 +213,20 @@ module FileStore failed = [] lock = Mutex.new - upload_threads = UPLOAD_CONCURRENCY.times.map do - Thread.new do - while obj = queue.pop - if s3.put_object(obj[:options]).etag[obj[:etag]] - putc "." - lock.synchronize { synced += 1 } - else - putc "X" - lock.synchronize { failed << obj[:path] } + upload_threads = + UPLOAD_CONCURRENCY.times.map do + Thread.new do + while obj = queue.pop + if s3.put_object(obj[:options]).etag[obj[:etag]] + putc "." + lock.synchronize { synced += 1 } + else + putc "X" + lock.synchronize { failed << obj[:path] } + end end end end - end local_files.each do |file| path = File.join(public_directory, file) @@ -242,17 +253,17 @@ module FileStore if upload&.original_filename options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format( - disposition: "attachment", filename: upload.original_filename + disposition: "attachment", + filename: upload.original_filename, ) end - if upload&.secure - options[:acl] = "private" - end + options[:acl] = "private" if upload&.secure elsif !FileHelper.is_inline_image?(name) upload = Upload.find_by(url: "/#{file}") options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format( - disposition: "attachment", filename: upload&.original_filename || name + disposition: "attachment", + filename: upload&.original_filename || name, ) end @@ -292,26 +303,25 @@ module FileStore [ [ "src=\"/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)", - "src=\"#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1" + "src=\"#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1", ], [ "src='/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)", - "src='#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1" + "src='#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1", ], [ "href=\"/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)", - "href=\"#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1" + "href=\"#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1", ], [ "href='/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)", - "href='#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1" + "href='#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1", ], [ "\\[img\\]/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)\\[/img\\]", - "[img]#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1[/img]" - ] + "[img]#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1[/img]", + ], ].each do |from_url, to_url| - if @dry_run log "REPLACING '#{from_url}' WITH '#{to_url}'" else @@ -321,16 +331,22 @@ module FileStore unless @dry_run # Legacy inline image format - Post.where("raw LIKE '%![](/uploads/default/original/%)%'").each do |post| - regexp = /!\[\](\/uploads\/#{@current_db}\/original\/(\dX\/(?:[a-f0-9]\/)*[a-f0-9]{40}[a-z0-9\.]*))/ + Post + .where("raw LIKE '%![](/uploads/default/original/%)%'") + .each do |post| + regexp = + /!\[\](\/uploads\/#{@current_db}\/original\/(\dX\/(?:[a-f0-9]\/)*[a-f0-9]{40}[a-z0-9\.]*))/ - post.raw.scan(regexp).each do |upload_url, _| - upload = Upload.get_from_url(upload_url) - post.raw = post.raw.gsub("![](#{upload_url})", "![](#{upload.short_url})") + post + .raw + .scan(regexp) + .each do |upload_url, _| + upload = Upload.get_from_url(upload_url) + post.raw = post.raw.gsub("![](#{upload_url})", "![](#{upload.short_url})") + end + + post.save!(validate: false) end - - post.save!(validate: false) - end end if Discourse.asset_host.present? @@ -373,7 +389,6 @@ module FileStore migration_successful?(should_raise: true) log "Done!" - ensure Jobs.run_later! end diff --git a/lib/filter_best_posts.rb b/lib/filter_best_posts.rb index a243156b231..2ee63d35b55 100644 --- a/lib/filter_best_posts.rb +++ b/lib/filter_best_posts.rb @@ -1,16 +1,13 @@ # frozen_string_literal: true class FilterBestPosts - attr_accessor :filtered_posts, :posts def initialize(topic, filtered_posts, limit, options = {}) @filtered_posts = filtered_posts @topic = topic @limit = limit - options.each do |key, value| - self.instance_variable_set("@#{key}".to_sym, value) - end + options.each { |key, value| self.instance_variable_set("@#{key}".to_sym, value) } filter end @@ -31,37 +28,41 @@ class FilterBestPosts def filter_posts_liked_by_moderators return unless @only_moderator_liked - liked_by_moderators = PostAction.where(post_id: @filtered_posts.pluck(:id), post_action_type_id: PostActionType.types[:like]) - liked_by_moderators = liked_by_moderators.joins(:user).where('users.moderator').pluck(:post_id) + liked_by_moderators = + PostAction.where( + post_id: @filtered_posts.pluck(:id), + post_action_type_id: PostActionType.types[:like], + ) + liked_by_moderators = liked_by_moderators.joins(:user).where("users.moderator").pluck(:post_id) @filtered_posts = @filtered_posts.where(id: liked_by_moderators) end def setup_posts - @posts = @filtered_posts.order('percent_rank asc, sort_order asc').where("post_number > 1") + @posts = @filtered_posts.order("percent_rank asc, sort_order asc").where("post_number > 1") @posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(@limit) end def filter_posts_based_on_trust_level - return unless @min_trust_level.try('>', 0) + return unless @min_trust_level.try(">", 0) @posts = - if @bypass_trust_level_score.try('>', 0) - @posts.where('COALESCE(users.trust_level,0) >= ? OR posts.score >= ?', + if @bypass_trust_level_score.try(">", 0) + @posts.where( + "COALESCE(users.trust_level,0) >= ? OR posts.score >= ?", @min_trust_level, - @bypass_trust_level_score + @bypass_trust_level_score, ) else - @posts.where('COALESCE(users.trust_level,0) >= ?', @min_trust_level) + @posts.where("COALESCE(users.trust_level,0) >= ?", @min_trust_level) end end def filter_posts_based_on_score - return unless @min_score.try('>', 0) - @posts = @posts.where('posts.score >= ?', @min_score) + return unless @min_score.try(">", 0) + @posts = @posts.where("posts.score >= ?", @min_score) end def sort_posts @posts = Post.from(@posts, :posts).order(post_number: :asc) end - end diff --git a/lib/final_destination.rb b/lib/final_destination.rb index fa5cbcc06d0..893b38e9c58 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'socket' -require 'ipaddr' -require 'excon' -require 'rate_limiter' -require 'url_helper' +require "socket" +require "ipaddr" +require "excon" +require "rate_limiter" +require "url_helper" # Determine the final endpoint for a Web URI, following redirects class FinalDestination @@ -30,7 +30,8 @@ class FinalDestination "HTTPS_DOMAIN_#{domain}" end - DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15" + DEFAULT_USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15" attr_reader :status, :cookie, :status_code, :content_type, :ignored @@ -53,15 +54,11 @@ class FinalDestination if @limit > 0 ignore_redirects = [Discourse.base_url_no_prefix] - if @opts[:ignore_redirects] - ignore_redirects.concat(@opts[:ignore_redirects]) - end + ignore_redirects.concat(@opts[:ignore_redirects]) if @opts[:ignore_redirects] ignore_redirects.each do |ignore_redirect| ignore_redirect = uri(ignore_redirect) - if ignore_redirect.present? && ignore_redirect.hostname - @ignored << ignore_redirect.hostname - end + @ignored << ignore_redirect.hostname if ignore_redirect.present? && ignore_redirect.hostname end end @@ -74,7 +71,14 @@ class FinalDestination @timeout = @opts[:timeout] || nil @preserve_fragment_url = @preserve_fragment_url_hosts.any? { |host| hostname_matches?(host) } @validate_uri = @opts.fetch(:validate_uri) { true } - @user_agent = @force_custom_user_agent_hosts.any? { |host| hostname_matches?(host) } ? Onebox.options.user_agent : @default_user_agent + @user_agent = + ( + if @force_custom_user_agent_hosts.any? { |host| hostname_matches?(host) } + Onebox.options.user_agent + else + @default_user_agent + end + ) @stop_at_blocked_pages = @opts[:stop_at_blocked_pages] end @@ -107,10 +111,10 @@ class FinalDestination "User-Agent" => @user_agent, "Accept" => "*/*", "Accept-Language" => "*", - "Host" => @uri.hostname + "Host" => @uri.hostname, } - result['Cookie'] = @cookie if @cookie + result["Cookie"] = @cookie if @cookie result end @@ -119,7 +123,12 @@ class FinalDestination status_code, response_headers = nil catch(:done) do - FinalDestination::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.is_a?(URI::HTTPS), open_timeout: timeout) do |http| + FinalDestination::HTTP.start( + @uri.host, + @uri.port, + use_ssl: @uri.is_a?(URI::HTTPS), + open_timeout: timeout, + ) do |http| http.read_timeout = timeout http.request_get(@uri.request_uri, request_headers) do |resp| status_code = resp.code.to_i @@ -162,7 +171,8 @@ class FinalDestination location = "#{@uri.scheme}://#{@uri.host}#{location}" if location[0] == "/" @uri = uri(location) - if @uri && redirects == @max_redirects && @https_redirect_ignore_limit && same_uri_but_https?(old_uri, @uri) + if @uri && redirects == @max_redirects && @https_redirect_ignore_limit && + same_uri_but_https?(old_uri, @uri) redirects += 1 @https_redirect_ignore_limit = false end @@ -177,7 +187,7 @@ class FinalDestination return if !@uri extra = nil - extra = { 'Cookie' => cookie } if cookie + extra = { "Cookie" => cookie } if cookie get(redirects - 1, extra_headers: extra, &blk) elsif result == :ok @@ -223,11 +233,16 @@ class FinalDestination request_start_time = Time.now response_body = +"" - request_validator = lambda do |chunk, _remaining_bytes, _total_bytes| - response_body << chunk - raise Excon::Errors::ExpectationFailed.new("response size too big: #{@uri.to_s}") if response_body.bytesize > MAX_REQUEST_SIZE_BYTES - raise Excon::Errors::ExpectationFailed.new("connect timeout reached: #{@uri.to_s}") if Time.now - request_start_time > MAX_REQUEST_TIME_SECONDS - end + request_validator = + lambda do |chunk, _remaining_bytes, _total_bytes| + response_body << chunk + if response_body.bytesize > MAX_REQUEST_SIZE_BYTES + raise Excon::Errors::ExpectationFailed.new("response size too big: #{@uri.to_s}") + end + if Time.now - request_start_time > MAX_REQUEST_TIME_SECONDS + raise Excon::Errors::ExpectationFailed.new("connect timeout reached: #{@uri.to_s}") + end + end # This technique will only use the first resolved IP # TODO: Can we standardise this by using FinalDestination::HTTP? @@ -240,18 +255,20 @@ class FinalDestination request_uri = @uri.dup request_uri.hostname = resolved_ip unless Rails.env.test? # WebMock doesn't understand the IP-based requests - response = Excon.public_send(@http_verb, - request_uri.to_s, - read_timeout: timeout, - connect_timeout: timeout, - headers: { "Host" => @uri.hostname }.merge(headers), - middlewares: middlewares, - response_block: request_validator, - ssl_verify_peer_host: @uri.hostname - ) + response = + Excon.public_send( + @http_verb, + request_uri.to_s, + read_timeout: timeout, + connect_timeout: timeout, + headers: { "Host" => @uri.hostname }.merge(headers), + middlewares: middlewares, + response_block: request_validator, + ssl_verify_peer_host: @uri.hostname, + ) if @stop_at_blocked_pages - if blocked_domain?(@uri) || response.headers['Discourse-No-Onebox'] == "1" + if blocked_domain?(@uri) || response.headers["Discourse-No-Onebox"] == "1" @status = :blocked_page return end @@ -282,7 +299,7 @@ class FinalDestination end end - @content_type = response.headers['Content-Type'] if response.headers.has_key?('Content-Type') + @content_type = response.headers["Content-Type"] if response.headers.has_key?("Content-Type") @status = :resolved return @uri when 103, 400, 405, 406, 409, 500, 501 @@ -306,11 +323,11 @@ class FinalDestination end response_headers = {} - if cookie_val = small_headers['set-cookie'] + if cookie_val = small_headers["set-cookie"] response_headers[:cookies] = cookie_val end - if location_val = small_headers['location'] + if location_val = small_headers["location"] response_headers[:location] = location_val.join end end @@ -318,21 +335,20 @@ class FinalDestination unless response_headers response_headers = { cookies: response.data[:cookies] || response.headers[:"set-cookie"], - location: response.headers[:location] + location: response.headers[:location], } end - if (300..399).include?(response_status) - location = response_headers[:location] - end + location = response_headers[:location] if (300..399).include?(response_status) if cookies = response_headers[:cookies] - @cookie = Array.wrap(cookies).map { |c| c.split(';').first.strip }.join('; ') + @cookie = Array.wrap(cookies).map { |c| c.split(";").first.strip }.join("; ") end if location redirect_uri = uri(location) - if @uri.host == redirect_uri.host && (redirect_uri.path =~ /\/login/ || redirect_uri.path =~ /\/session/) + if @uri.host == redirect_uri.host && + (redirect_uri.path =~ %r{/login} || redirect_uri.path =~ %r{/session}) @status = :resolved return @uri end @@ -342,7 +358,8 @@ class FinalDestination location = "#{@uri.scheme}://#{@uri.host}#{location}" if location[0] == "/" @uri = uri(location) - if @uri && @limit == @max_redirects && @https_redirect_ignore_limit && same_uri_but_https?(old_uri, @uri) + if @uri && @limit == @max_redirects && @https_redirect_ignore_limit && + same_uri_but_https?(old_uri, @uri) @limit += 1 @https_redirect_ignore_limit = false end @@ -376,12 +393,18 @@ class FinalDestination def validate_uri_format return false unless @uri && @uri.host - return false unless ['https', 'http'].include?(@uri.scheme) - return false if @uri.scheme == 'http' && @uri.port != 80 - return false if @uri.scheme == 'https' && @uri.port != 443 + return false unless %w[https http].include?(@uri.scheme) + return false if @uri.scheme == "http" && @uri.port != 80 + return false if @uri.scheme == "https" && @uri.port != 443 # Disallow IP based crawling - (IPAddr.new(@uri.hostname) rescue nil).nil? + ( + begin + IPAddr.new(@uri.hostname) + rescue StandardError + nil + end + ).nil? end def hostname @@ -392,11 +415,11 @@ class FinalDestination url = uri(url) if @uri&.hostname.present? && url&.hostname.present? - hostname_parts = url.hostname.split('.') - has_wildcard = hostname_parts.first == '*' + hostname_parts = url.hostname.split(".") + has_wildcard = hostname_parts.first == "*" if has_wildcard - @uri.hostname.end_with?(hostname_parts[1..-1].join('.')) + @uri.hostname.end_with?(hostname_parts[1..-1].join(".")) else @uri.hostname == url.hostname end @@ -413,7 +436,7 @@ class FinalDestination Rails.logger.public_send( log_level, - "#{RailsMultisite::ConnectionManagement.current_db}: #{message}" + "#{RailsMultisite::ConnectionManagement.current_db}: #{message}", ) end @@ -425,15 +448,12 @@ class FinalDestination headers_subset = Struct.new(:location, :set_cookie).new safe_session(uri) do |http| - headers = request_headers.merge( - 'Accept-Encoding' => 'gzip', - 'Host' => uri.host - ) + headers = request_headers.merge("Accept-Encoding" => "gzip", "Host" => uri.host) req = FinalDestination::HTTP::Get.new(uri.request_uri, headers) http.request(req) do |resp| - headers_subset.set_cookie = resp['Set-Cookie'] + headers_subset.set_cookie = resp["Set-Cookie"] if @stop_at_blocked_pages dont_onebox = resp["Discourse-No-Onebox"] == "1" @@ -444,7 +464,7 @@ class FinalDestination end if Net::HTTPRedirection === resp - headers_subset.location = resp['location'] + headers_subset.location = resp["location"] result = :redirect, headers_subset end @@ -471,9 +491,7 @@ class FinalDestination end result = :ok, headers_subset else - catch(:done) do - yield resp, nil, nil - end + catch(:done) { yield resp, nil, nil } end end end @@ -490,7 +508,12 @@ class FinalDestination end def safe_session(uri) - FinalDestination::HTTP.start(uri.host, uri.port, use_ssl: (uri.scheme == "https"), open_timeout: timeout) do |http| + FinalDestination::HTTP.start( + uri.host, + uri.port, + use_ssl: (uri.scheme == "https"), + open_timeout: timeout, + ) do |http| http.read_timeout = timeout yield http end @@ -508,14 +531,14 @@ class FinalDestination def fetch_canonical_url(body) return if body.blank? - canonical_element = Nokogiri::HTML5(body).at("link[rel='canonical']") + canonical_element = Nokogiri.HTML5(body).at("link[rel='canonical']") return if canonical_element.nil? - canonical_uri = uri(canonical_element['href']) + canonical_uri = uri(canonical_element["href"]) return if canonical_uri.blank? return canonical_uri if canonical_uri.host.present? parts = [@uri.host, canonical_uri.to_s] - complete_url = canonical_uri.to_s.starts_with?('/') ? parts.join('') : parts.join('/') + complete_url = canonical_uri.to_s.starts_with?("/") ? parts.join("") : parts.join("/") complete_url = "#{@uri.scheme}://#{complete_url}" if @uri.scheme uri(complete_url) @@ -528,8 +551,7 @@ class FinalDestination def same_uri_but_https?(before, after) before = before.to_s after = after.to_s - before.start_with?("http://") && - after.start_with?("https://") && + before.start_with?("http://") && after.start_with?("https://") && before.sub("http://", "") == after.sub("https://", "") end end diff --git a/lib/final_destination/resolver.rb b/lib/final_destination/resolver.rb index f809099d4d2..843a6a313bc 100644 --- a/lib/final_destination/resolver.rb +++ b/lib/final_destination/resolver.rb @@ -39,18 +39,19 @@ class FinalDestination::Resolver def self.ensure_lookup_thread return if @thread&.alive? - @thread = Thread.new do - while true - @queue.deq - @error = nil - begin - @result = Addrinfo.getaddrinfo(@lookup, 80, nil, :STREAM).map(&:ip_address) - rescue => e - @error = e + @thread = + Thread.new do + while true + @queue.deq + @error = nil + begin + @result = Addrinfo.getaddrinfo(@lookup, 80, nil, :STREAM).map(&:ip_address) + rescue => e + @error = e + end + @parent.wakeup end - @parent.wakeup end - end @thread.name = "final-destination_resolver_thread" end end diff --git a/lib/final_destination/ssrf_detector.rb b/lib/final_destination/ssrf_detector.rb index aeb01d9ec90..0d06306ce8f 100644 --- a/lib/final_destination/ssrf_detector.rb +++ b/lib/final_destination/ssrf_detector.rb @@ -2,8 +2,10 @@ class FinalDestination module SSRFDetector - class DisallowedIpError < SocketError; end - class LookupFailedError < SocketError; end + class DisallowedIpError < SocketError + end + class LookupFailedError < SocketError + end def self.standard_private_ranges @private_ranges ||= [ diff --git a/lib/flag_settings.rb b/lib/flag_settings.rb index da86d2c1b43..d88c01e35af 100644 --- a/lib/flag_settings.rb +++ b/lib/flag_settings.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true class FlagSettings - attr_reader( :without_custom_types, :notify_types, :topic_flag_types, :auto_action_types, - :custom_types + :custom_types, ) def initialize @@ -39,5 +38,4 @@ class FlagSettings def flag_types @all_flag_types end - end diff --git a/lib/freedom_patches/better_handlebars_errors.rb b/lib/freedom_patches/better_handlebars_errors.rb index c308f744106..a11eb271200 100644 --- a/lib/freedom_patches/better_handlebars_errors.rb +++ b/lib/freedom_patches/better_handlebars_errors.rb @@ -3,9 +3,8 @@ module Ember module Handlebars class Template - # Wrap in an IIFE in development mode to get the correct filename - def compile_ember_handlebars(string, ember_template = 'Handlebars', options = nil) + def compile_ember_handlebars(string, ember_template = "Handlebars", options = nil) if ::Rails.env.development? "(function() { try { return Ember.#{ember_template}.compile(#{indent(string).inspect}); } catch(err) { throw err; } })()" else diff --git a/lib/freedom_patches/cose_rsapkcs1.rb b/lib/freedom_patches/cose_rsapkcs1.rb index f6964e1835f..55b639b5a5b 100644 --- a/lib/freedom_patches/cose_rsapkcs1.rb +++ b/lib/freedom_patches/cose_rsapkcs1.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'cose' -require 'openssl/signature_algorithm/rsapkcs1' +require "cose" +require "openssl/signature_algorithm/rsapkcs1" # 'cose' gem does not implement all algorithms from the Web Authentication # (WebAuthn) standard specification. This patch implements one of the missing @@ -38,11 +38,11 @@ module COSE when OpenSSL::PKey::RSA key else - raise(COSE::Error, 'Incompatible key for algorithm') + raise(COSE::Error, "Incompatible key for algorithm") end end end - register(RSAPKCS1.new(-257, 'RS256', hash_function: 'SHA256')) + register(RSAPKCS1.new(-257, "RS256", hash_function: "SHA256")) end end diff --git a/lib/freedom_patches/fast_pluck.rb b/lib/freedom_patches/fast_pluck.rb index ef467c56664..b33e7a2723d 100644 --- a/lib/freedom_patches/fast_pluck.rb +++ b/lib/freedom_patches/fast_pluck.rb @@ -5,7 +5,6 @@ # # class ActiveRecord::Relation - # Note: In discourse, the following code is included in lib/sql_builder.rb # # class RailsDateTimeDecoder < PG::SimpleDecoder @@ -43,7 +42,8 @@ class ActiveRecord::Relation end def pluck(*column_names) - if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? + if loaded? && + (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? return records.pluck(*column_names) end @@ -55,10 +55,12 @@ class ActiveRecord::Relation relation.select_values = column_names - klass.connection.select_raw(relation.arel) do |result, _| - result.type_map = DB.type_map - result.nfields == 1 ? result.column_values(0) : result.values - end + klass + .connection + .select_raw(relation.arel) do |result, _| + result.type_map = DB.type_map + result.nfields == 1 ? result.column_values(0) : result.values + end end end end diff --git a/lib/freedom_patches/inflector_backport.rb b/lib/freedom_patches/inflector_backport.rb index 67547cddadb..7b1d02d4048 100644 --- a/lib/freedom_patches/inflector_backport.rb +++ b/lib/freedom_patches/inflector_backport.rb @@ -6,7 +6,6 @@ module ActiveSupport module Inflector - LRU_CACHE_SIZE = 200 LRU_CACHES = [] @@ -22,26 +21,30 @@ module ActiveSupport uncached = "#{method_name}_without_cache" alias_method uncached, method_name - m = define_method(method_name) do |*arguments| - # this avoids recursive locks - found = true - data = cache.fetch(arguments) { found = false } - unless found - cache[arguments] = data = public_send(uncached, *arguments) + m = + define_method(method_name) do |*arguments| + # this avoids recursive locks + found = true + data = cache.fetch(arguments) { found = false } + cache[arguments] = data = public_send(uncached, *arguments) unless found + # so cache is never corrupted + data.dup end - # so cache is never corrupted - data.dup - end # https://bugs.ruby-lang.org/issues/16897 - if Module.respond_to?(:ruby2_keywords, true) - ruby2_keywords(m) - end + ruby2_keywords(m) if Module.respond_to?(:ruby2_keywords, true) end end - memoize :pluralize, :singularize, :camelize, :underscore, :humanize, - :titleize, :tableize, :classify, :foreign_key + memoize :pluralize, + :singularize, + :camelize, + :underscore, + :humanize, + :titleize, + :tableize, + :classify, + :foreign_key end end diff --git a/lib/freedom_patches/ip_addr.rb b/lib/freedom_patches/ip_addr.rb index e9b118b8d06..9c7053d66c2 100644 --- a/lib/freedom_patches/ip_addr.rb +++ b/lib/freedom_patches/ip_addr.rb @@ -1,29 +1,28 @@ # frozen_string_literal: true class IPAddr - def self.handle_wildcards(val) return if val.blank? - num_wildcards = val.count('*') + num_wildcards = val.count("*") return val if num_wildcards == 0 # strip ranges like "/16" from the end if present - v = val.gsub(/\/.*/, '') + v = val.gsub(%r{/.*}, "") - return if v[v.index('*')..-1] =~ /[^\.\*]/ + return if v[v.index("*")..-1] =~ /[^\.\*]/ - parts = v.split('.') - (4 - parts.size).times { parts << '*' } # support strings like 192.* - v = parts.join('.') + parts = v.split(".") + (4 - parts.size).times { parts << "*" } # support strings like 192.* + v = parts.join(".") - "#{v.tr('*', '0')}/#{32 - (v.count('*') * 8)}" + "#{v.tr("*", "0")}/#{32 - (v.count("*") * 8)}" end def to_cidr_s if @addr - mask = @mask_addr.to_s(2).count('1') + mask = @mask_addr.to_s(2).count("1") if mask == 32 to_s else @@ -33,5 +32,4 @@ class IPAddr nil end end - end diff --git a/lib/freedom_patches/mail_disable_starttls.rb b/lib/freedom_patches/mail_disable_starttls.rb index 45daba893a6..a8a60bda86e 100644 --- a/lib/freedom_patches/mail_disable_starttls.rb +++ b/lib/freedom_patches/mail_disable_starttls.rb @@ -11,9 +11,7 @@ module FreedomPatches def build_smtp_session super.tap do |smtp| unless settings[:enable_starttls_auto] - if smtp.respond_to?(:disable_starttls) - smtp.disable_starttls - end + smtp.disable_starttls if smtp.respond_to?(:disable_starttls) end end end diff --git a/lib/freedom_patches/rails4.rb b/lib/freedom_patches/rails4.rb index 894c3dad904..806813b7c87 100644 --- a/lib/freedom_patches/rails4.rb +++ b/lib/freedom_patches/rails4.rb @@ -5,11 +5,13 @@ # Backporting a fix to rails itself may get too complex module FreedomPatches module Rails4 - - def self.distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) - options = { - scope: :'datetime.distance_in_words', - }.merge!(options) + def self.distance_of_time_in_words( + from_time, + to_time = 0, + include_seconds = false, + options = {} + ) + options = { scope: :"datetime.distance_in_words" }.merge!(options) from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) @@ -20,49 +22,68 @@ module FreedomPatches I18n.with_options locale: options[:locale], scope: options[:scope] do |locale| case distance_in_minutes when 0..1 - return distance_in_minutes == 0 ? - locale.t(:less_than_x_minutes, count: 1) : - locale.t(:x_minutes, count: distance_in_minutes) unless include_seconds + unless include_seconds + return( + ( + if distance_in_minutes == 0 + locale.t(:less_than_x_minutes, count: 1) + else + locale.t(:x_minutes, count: distance_in_minutes) + end + ) + ) + end - case distance_in_seconds - when 0..4 then locale.t :less_than_x_seconds, count: 5 - when 5..9 then locale.t :less_than_x_seconds, count: 10 - when 10..19 then locale.t :less_than_x_seconds, count: 20 - when 20..39 then locale.t :half_a_minute - when 40..59 then locale.t :less_than_x_minutes, count: 1 - else locale.t :x_minutes, count: 1 - end - - when 2..44 then locale.t :x_minutes, count: distance_in_minutes - when 45..89 then locale.t :about_x_hours, count: 1 - when 90..1439 then locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round - when 1440..2519 then locale.t :x_days, count: 1 + case distance_in_seconds + when 0..4 + locale.t :less_than_x_seconds, count: 5 + when 5..9 + locale.t :less_than_x_seconds, count: 10 + when 10..19 + locale.t :less_than_x_seconds, count: 20 + when 20..39 + locale.t :half_a_minute + when 40..59 + locale.t :less_than_x_minutes, count: 1 + else + locale.t :x_minutes, count: 1 + end + when 2..44 + locale.t :x_minutes, count: distance_in_minutes + when 45..89 + locale.t :about_x_hours, count: 1 + when 90..1439 + locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round + when 1440..2519 + locale.t :x_days, count: 1 # this is were we diverge from Rails - when 2520..129599 then locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round - when 129600..525599 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round - else + when 2520..129_599 + locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round + when 129_600..525_599 + locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round + else fyear = from_time.year - fyear += 1 if from_time.month >= 3 - tyear = to_time.year - tyear -= 1 if to_time.month < 3 - leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count { |x| Date.leap?(x) } - minute_offset_for_leap_year = leap_years * 1440 - # Discount the leap year days when calculating year distance. - # e.g. if there are 20 leap year days between 2 dates having the same day - # and month then the based on 365 days calculation - # the distance in years will come out to over 80 years when in written - # english it would read better as about 80 years. - minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year - remainder = (minutes_with_offset % 525600) - distance_in_years = (minutes_with_offset / 525600) - if remainder < 131400 - locale.t(:about_x_years, count: distance_in_years) - elsif remainder < 394200 - locale.t(:over_x_years, count: distance_in_years) - else - locale.t(:almost_x_years, count: distance_in_years + 1) - end + fyear += 1 if from_time.month >= 3 + tyear = to_time.year + tyear -= 1 if to_time.month < 3 + leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count { |x| Date.leap?(x) } + minute_offset_for_leap_year = leap_years * 1440 + # Discount the leap year days when calculating year distance. + # e.g. if there are 20 leap year days between 2 dates having the same day + # and month then the based on 365 days calculation + # the distance in years will come out to over 80 years when in written + # english it would read better as about 80 years. + minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year + remainder = (minutes_with_offset % 525_600) + distance_in_years = (minutes_with_offset / 525_600) + if remainder < 131_400 + locale.t(:about_x_years, count: distance_in_years) + elsif remainder < 394_200 + locale.t(:over_x_years, count: distance_in_years) + else + locale.t(:almost_x_years, count: distance_in_years + 1) + end end end end diff --git a/lib/freedom_patches/rails_multisite.rb b/lib/freedom_patches/rails_multisite.rb index 99244669d7e..0a78baaf320 100644 --- a/lib/freedom_patches/rails_multisite.rb +++ b/lib/freedom_patches/rails_multisite.rb @@ -19,12 +19,10 @@ module RailsMultisite handler end - ActiveRecord::Base.connected_to(role: reading_role) do - yield(db) if block_given? - end + ActiveRecord::Base.connected_to(role: reading_role) { yield(db) if block_given? } rescue => e - STDERR.puts "URGENT: Failed to initialize site #{db}: "\ - "#{e.class} #{e.message}\n#{e.backtrace.join("\n")}" + STDERR.puts "URGENT: Failed to initialize site #{db}: " \ + "#{e.class} #{e.message}\n#{e.backtrace.join("\n")}" # the show must go on, don't stop startup if multisite fails end @@ -34,11 +32,7 @@ module RailsMultisite class DiscoursePatches def self.config - { - db_lookup: lambda do |env| - env["PATH_INFO"] == "/srv/status" ? "default" : nil - end - } + { db_lookup: lambda { |env| env["PATH_INFO"] == "/srv/status" ? "default" : nil } } end end end diff --git a/lib/freedom_patches/safe_buffer.rb b/lib/freedom_patches/safe_buffer.rb index 5bc2feb01e4..efc6880b6d9 100644 --- a/lib/freedom_patches/safe_buffer.rb +++ b/lib/freedom_patches/safe_buffer.rb @@ -12,7 +12,8 @@ module FreedomPatches rescue Encoding::CompatibilityError raise if raise_encoding_err - encoding_diags = +"internal encoding #{Encoding.default_internal}, external encoding #{Encoding.default_external}" + encoding_diags = + +"internal encoding #{Encoding.default_internal}, external encoding #{Encoding.default_external}" if encoding != Encoding::UTF_8 encoding_diags << " my encoding is #{encoding} " force_encoding("UTF-8") @@ -20,12 +21,16 @@ module FreedomPatches encode!("utf-16", "utf-8", invalid: :replace) encode!("utf-8", "utf-16") end - Rails.logger.warn("Encountered a non UTF-8 string in SafeBuffer - #{self} - #{encoding_diags}") + Rails.logger.warn( + "Encountered a non UTF-8 string in SafeBuffer - #{self} - #{encoding_diags}", + ) end if value.encoding != Encoding::UTF_8 encoding_diags << " attempted to append encoding #{value.encoding} " value = value.dup.force_encoding("UTF-8").scrub - Rails.logger.warn("Attempted to concat a non UTF-8 string in SafeBuffer - #{value} - #{encoding_diags}") + Rails.logger.warn( + "Attempted to concat a non UTF-8 string in SafeBuffer - #{value} - #{encoding_diags}", + ) end concat(value, _raise = true) end diff --git a/lib/freedom_patches/safe_migrations.rb b/lib/freedom_patches/safe_migrations.rb index cdf989fdd5b..61f9fe47540 100644 --- a/lib/freedom_patches/safe_migrations.rb +++ b/lib/freedom_patches/safe_migrations.rb @@ -5,7 +5,7 @@ # which rake:multisite_migrate uses # # The protection is only needed in Dev and Test -if ENV['RAILS_ENV'] != "production" - require_dependency 'migration/safe_migrate' +if ENV["RAILS_ENV"] != "production" + require_dependency "migration/safe_migrate" Migration::SafeMigrate.patch_active_record! end diff --git a/lib/freedom_patches/schema_migration_details.rb b/lib/freedom_patches/schema_migration_details.rb index 6b72cc8fdcf..c02d9c51ace 100644 --- a/lib/freedom_patches/schema_migration_details.rb +++ b/lib/freedom_patches/schema_migration_details.rb @@ -5,9 +5,7 @@ module FreedomPatches def exec_migration(conn, direction) rval = nil - time = Benchmark.measure do - rval = super - end + time = Benchmark.measure { rval = super } sql = < err Discourse.warn_exception( - err, message: "Unexpected error when checking SMTP credentials for group #{group.id} (#{group.name})." + err, + message: + "Unexpected error when checking SMTP credentials for group #{group.id} (#{group.name}).", ) nil end diff --git a/lib/guardian.rb b/lib/guardian.rb index e6a4ad76ac1..1a9b2dd52f3 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -require 'guardian/category_guardian' -require 'guardian/ensure_magic' -require 'guardian/post_guardian' -require 'guardian/bookmark_guardian' -require 'guardian/topic_guardian' -require 'guardian/user_guardian' -require 'guardian/post_revision_guardian' -require 'guardian/group_guardian' -require 'guardian/tag_guardian' +require "guardian/category_guardian" +require "guardian/ensure_magic" +require "guardian/post_guardian" +require "guardian/bookmark_guardian" +require "guardian/topic_guardian" +require "guardian/user_guardian" +require "guardian/post_revision_guardian" +require "guardian/group_guardian" +require "guardian/tag_guardian" # The guardian is responsible for confirming access to various site resources and operations class Guardian @@ -89,7 +89,7 @@ class Guardian def user @user.presence end - alias :current_user :user + alias current_user user def anonymous? !authenticated? @@ -127,7 +127,9 @@ class Guardian if @category_group_moderator_groups.key?(reviewable_by_group_id) @category_group_moderator_groups[reviewable_by_group_id] else - @category_group_moderator_groups[reviewable_by_group_id] = category_group_moderator_scope.exists?("categories.id": category.id) + @category_group_moderator_groups[ + reviewable_by_group_id + ] = category_group_moderator_scope.exists?("categories.id": category.id) end end @@ -136,16 +138,14 @@ class Guardian end def is_developer? - @user && - is_admin? && - ( - Rails.env.development? || - Developer.user_ids.include?(@user.id) || + @user && is_admin? && ( - Rails.configuration.respond_to?(:developer_emails) && - Rails.configuration.developer_emails.include?(@user.email) + Rails.env.development? || Developer.user_ids.include?(@user.id) || + ( + Rails.configuration.respond_to?(:developer_emails) && + Rails.configuration.developer_emails.include?(@user.email) + ) ) - ) end def is_staged? @@ -203,12 +203,13 @@ class Guardian end def can_moderate?(obj) - obj && authenticated? && !is_silenced? && ( - is_staff? || - (obj.is_a?(Topic) && @user.has_trust_level?(TrustLevel[4]) && can_see_topic?(obj)) - ) + obj && authenticated? && !is_silenced? && + ( + is_staff? || + (obj.is_a?(Topic) && @user.has_trust_level?(TrustLevel[4]) && can_see_topic?(obj)) + ) end - alias :can_see_flags? :can_moderate? + alias can_see_flags? can_moderate? def can_tag?(topic) return false if topic.blank? @@ -229,9 +230,7 @@ class Guardian end def can_delete_reviewable_queued_post?(reviewable) - reviewable.present? && - authenticated? && - reviewable.created_by_id == @user.id + reviewable.present? && authenticated? && reviewable.created_by_id == @user.id end def can_see_group?(group) @@ -243,7 +242,9 @@ class Guardian return true if is_admin? || group.members_visibility_level == Group.visibility_levels[:public] return true if is_staff? && group.members_visibility_level == Group.visibility_levels[:staff] return true if is_staff? && group.members_visibility_level == Group.visibility_levels[:members] - return true if authenticated? && group.members_visibility_level == Group.visibility_levels[:logged_on_users] + if authenticated? && group.members_visibility_level == Group.visibility_levels[:logged_on_users] + return true + end return false if user.blank? return false unless membership = GroupUser.find_by(group_id: group.id, user_id: user.id) @@ -257,10 +258,19 @@ class Guardian def can_see_groups?(groups) return false if groups.blank? - return true if is_admin? || groups.all? { |g| g.visibility_level == Group.visibility_levels[:public] } - return true if is_staff? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:staff] } - return true if is_staff? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:members] } - return true if authenticated? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:logged_on_users] } + if is_admin? || groups.all? { |g| g.visibility_level == Group.visibility_levels[:public] } + return true + end + if is_staff? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:staff] } + return true + end + if is_staff? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:members] } + return true + end + if authenticated? && + groups.all? { |g| g.visibility_level == Group.visibility_levels[:logged_on_users] } + return true + end return false if user.blank? memberships = GroupUser.where(group: groups, user_id: user.id).pluck(:owner) @@ -277,7 +287,8 @@ class Guardian return false if groups.blank? requested_group_ids = groups.map(&:id) # Can't use pluck, groups could be a regular array - matching_group_ids = Group.where(id: requested_group_ids).members_visible_groups(user).pluck(:id) + matching_group_ids = + Group.where(id: requested_group_ids).members_visible_groups(user).pluck(:id) matching_group_ids.sort == requested_group_ids.sort end @@ -285,12 +296,10 @@ class Guardian # Can we impersonate this user? def can_impersonate?(target) target && - - # You must be an admin to impersonate - is_admin? && - - # You may not impersonate other admins unless you are a dev - (!target.admin? || is_developer?) + # You must be an admin to impersonate + is_admin? && + # You may not impersonate other admins unless you are a dev + (!target.admin? || is_developer?) # Additionally, you may not impersonate yourself; # but the two tests for different admin statuses @@ -313,7 +322,7 @@ class Guardian def can_suspend?(user) user && is_staff? && user.regular? end - alias :can_deactivate? :can_suspend? + alias can_deactivate? can_suspend? def can_revoke_admin?(admin) can_administer_user?(admin) && admin.admin? @@ -337,10 +346,13 @@ class Guardian return true if title.empty? # A title set to '(none)' in the UI is an empty string return false if user != @user - return true if user.badges - .where(allow_title: true) - .pluck(:name) - .any? { |name| Badge.display_name(name) == title } + if user + .badges + .where(allow_title: true) + .pluck(:name) + .any? { |name| Badge.display_name(name) == title } + return true + end user.groups.where(title: title).exists? end @@ -349,13 +361,13 @@ class Guardian return false if !user || !group_id group = Group.find_by(id: group_id.to_i) - user.group_ids.include?(group_id.to_i) && - (group ? !group.automatic : false) + user.group_ids.include?(group_id.to_i) && (group ? !group.automatic : false) end def can_use_flair_group?(user, group_id = nil) return false if !user || !group_id || !user.group_ids.include?(group_id.to_i) - flair_icon, flair_upload_id = Group.where(id: group_id.to_i).pluck_first(:flair_icon, :flair_upload_id) + flair_icon, flair_upload_id = + Group.where(id: group_id.to_i).pluck_first(:flair_icon, :flair_upload_id) flair_icon.present? || flair_upload_id.present? end @@ -387,10 +399,9 @@ class Guardian end def can_invite_to_forum?(groups = nil) - authenticated? && - (is_staff? || SiteSetting.max_invites_per_day.to_i.positive?) && - (is_staff? || @user.has_trust_level?(SiteSetting.min_trust_level_to_allow_invite.to_i)) && - (is_admin? || groups.blank? || groups.all? { |g| can_edit_group?(g) }) + authenticated? && (is_staff? || SiteSetting.max_invites_per_day.to_i.positive?) && + (is_staff? || @user.has_trust_level?(SiteSetting.min_trust_level_to_allow_invite.to_i)) && + (is_admin? || groups.blank? || groups.all? { |g| can_edit_group?(g) }) end def can_invite_to?(object, groups = nil) @@ -402,9 +413,7 @@ class Guardian if object.private_message? return true if is_admin? - if !@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) - return false - end + return false if !@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) return false if object.reached_recipients_limit? && !is_staff? end @@ -441,8 +450,7 @@ class Guardian end def can_invite_group_to_private_message?(group, topic) - can_see_topic?(topic) && - can_send_private_message?(group) + can_see_topic?(topic) && can_send_private_message?(group) end ## @@ -459,8 +467,11 @@ class Guardian # User is authenticated authenticated? && # User can send PMs, this can be covered by trust levels as well via AUTO_GROUPS - (is_staff? || from_bot || from_system || \ - (@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map)) || notify_moderators) + ( + is_staff? || from_bot || from_system || + (@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map)) || + notify_moderators + ) end ## @@ -480,14 +491,14 @@ class Guardian # User is authenticated and can send PMs, this can be covered by trust levels as well via AUTO_GROUPS (can_send_private_messages?(notify_moderators: notify_moderators) || group_is_messageable) && - # User disabled private message - (is_staff? || target_is_group || target.user_option.allow_private_messages) && - # Can't send PMs to suspended users - (is_staff? || target_is_group || !target.suspended?) && - # Check group messageable level - (from_system || target_is_user || group_is_messageable || notify_moderators) && - # Silenced users can only send PM to staff - (!is_silenced? || target.staff?) + # User disabled private message + (is_staff? || target_is_group || target.user_option.allow_private_messages) && + # Can't send PMs to suspended users + (is_staff? || target_is_group || !target.suspended?) && + # Check group messageable level + (from_system || target_is_user || group_is_messageable || notify_moderators) && + # Silenced users can only send PM to staff + (!is_silenced? || target.staff?) end def can_send_private_messages_to_email? @@ -503,17 +514,18 @@ class Guardian def can_export_entity?(entity) return false if anonymous? return true if is_admin? - return entity != 'user_list' if is_moderator? + return entity != "user_list" if is_moderator? # Regular users can only export their archives return false unless entity == "user_archive" - UserExport.where(user_id: @user.id, created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day)).count == 0 + UserExport.where( + user_id: @user.id, + created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day), + ).count == 0 end def can_mute_user?(target_user) - can_mute_users? && - @user.id != target_user.id && - !target_user.staff? + can_mute_users? && @user.id != target_user.id && !target_user.staff? end def can_mute_users? @@ -546,20 +558,15 @@ class Guardian return true if theme_ids.blank? if allowed_theme_ids = Theme.allowed_remote_theme_ids - if (theme_ids - allowed_theme_ids).present? - return false - end + return false if (theme_ids - allowed_theme_ids).present? end - if include_preview && is_staff? && (theme_ids - Theme.theme_ids).blank? - return true - end + return true if include_preview && is_staff? && (theme_ids - Theme.theme_ids).blank? parent = theme_ids.first components = theme_ids[1..-1] || [] - Theme.user_theme_ids.include?(parent) && - (components - Theme.components_for(parent)).empty? + Theme.user_theme_ids.include?(parent) && (components - Theme.components_for(parent)).empty? end def can_publish_page?(topic) @@ -608,7 +615,6 @@ class Guardian private def is_my_own?(obj) - unless anonymous? return obj.user_id == @user.id if obj.respond_to?(:user_id) && obj.user_id && @user.id return obj.user == @user if obj.respond_to?(:user) @@ -650,9 +656,8 @@ class Guardian end def category_group_moderator_scope - Category - .joins("INNER JOIN group_users ON group_users.group_id = categories.reviewable_by_group_id") - .where("group_users.user_id = ?", user.id) + Category.joins( + "INNER JOIN group_users ON group_users.group_id = categories.reviewable_by_group_id", + ).where("group_users.user_id = ?", user.id) end - end diff --git a/lib/guardian/category_guardian.rb b/lib/guardian/category_guardian.rb index 35bf8d30afe..e437455dfc5 100644 --- a/lib/guardian/category_guardian.rb +++ b/lib/guardian/category_guardian.rb @@ -2,40 +2,31 @@ #mixin for all guardian methods dealing with category permissions module CategoryGuardian - # Creating Method def can_create_category?(parent = nil) - is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? - ) + is_admin? || (SiteSetting.moderators_manage_categories_and_groups && is_moderator?) end # Editing Method def can_edit_category?(category) is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? && - can_see_category?(category) - ) + ( + SiteSetting.moderators_manage_categories_and_groups && is_moderator? && + can_see_category?(category) + ) end def can_edit_serialized_category?(category_id:, read_restricted:) is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? && - can_see_serialized_category?(category_id: category_id, read_restricted: read_restricted) - ) + ( + SiteSetting.moderators_manage_categories_and_groups && is_moderator? && + can_see_serialized_category?(category_id: category_id, read_restricted: read_restricted) + ) end def can_delete_category?(category) - can_edit_category?(category) && - category.topic_count <= 0 && - !category.uncategorized? && - !category.has_children? + can_edit_category?(category) && category.topic_count <= 0 && !category.uncategorized? && + !category.has_children? end def can_see_serialized_category?(category_id:, read_restricted: true) @@ -84,6 +75,7 @@ module CategoryGuardian end def topic_featured_link_allowed_category_ids - @topic_featured_link_allowed_category_ids = Category.where(topic_featured_link_allowed: true).pluck(:id) + @topic_featured_link_allowed_category_ids = + Category.where(topic_featured_link_allowed: true).pluck(:id) end end diff --git a/lib/guardian/ensure_magic.rb b/lib/guardian/ensure_magic.rb index bff9f402dca..62cece83b61 100644 --- a/lib/guardian/ensure_magic.rb +++ b/lib/guardian/ensure_magic.rb @@ -2,13 +2,14 @@ # Support for ensure_{blah}! methods. module EnsureMagic - def method_missing(method, *args, &block) if method.to_s =~ /^ensure_(.*)\!$/ can_method = :"#{Regexp.last_match[1]}?" if respond_to?(can_method) - raise Discourse::InvalidAccess.new("#{can_method} failed") unless send(can_method, *args, &block) + unless send(can_method, *args, &block) + raise Discourse::InvalidAccess.new("#{can_method} failed") + end return end end @@ -20,5 +21,4 @@ module EnsureMagic def ensure_can_see!(obj) raise Discourse::InvalidAccess.new("Can't see #{obj}") unless can_see?(obj) end - end diff --git a/lib/guardian/group_guardian.rb b/lib/guardian/group_guardian.rb index b3e571776c9..7b153615a1b 100644 --- a/lib/guardian/group_guardian.rb +++ b/lib/guardian/group_guardian.rb @@ -2,14 +2,9 @@ #mixin for all guardian methods dealing with group permissions module GroupGuardian - # Creating Method def can_create_group? - is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? - ) + is_admin? || (SiteSetting.moderators_manage_categories_and_groups && is_moderator?) end # Edit authority for groups means membership changes only. @@ -17,17 +12,15 @@ module GroupGuardian # table and thus do not allow membership changes. def can_edit_group?(group) !group.automatic && - (can_admin_group?(group) || group.users.where('group_users.owner').include?(user)) + (can_admin_group?(group) || group.users.where("group_users.owner").include?(user)) end def can_admin_group?(group) is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? && - can_see?(group) && - group.id != Group::AUTO_GROUPS[:admins] - ) + ( + SiteSetting.moderators_manage_categories_and_groups && is_moderator? && can_see?(group) && + group.id != Group::AUTO_GROUPS[:admins] + ) end def can_see_group_messages?(group) diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 35e70305d25..6ca1256896d 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -2,26 +2,24 @@ # mixin for all guardian methods dealing with post permissions module PostGuardian - def unrestricted_link_posting? authenticated? && @user.has_trust_level?(TrustLevel[SiteSetting.min_trust_to_post_links]) end def link_posting_access if unrestricted_link_posting? - 'full' + "full" elsif SiteSetting.allowed_link_domains.present? - 'limited' + "limited" else - 'none' + "none" end end def can_post_link?(host: nil) return false if host.blank? - unrestricted_link_posting? || - SiteSetting.allowed_link_domains.split('|').include?(host) + unrestricted_link_posting? || SiteSetting.allowed_link_domains.split("|").include?(host) end # Can the user act on the post in a particular way. @@ -30,47 +28,55 @@ module PostGuardian return false unless (can_see_post.nil? && can_see_post?(post)) || can_see_post # no warnings except for staff - return false if action_key == :notify_user && (post.user.blank? || (!is_staff? && opts[:is_warning].present? && opts[:is_warning] == 'true')) + if action_key == :notify_user && + ( + post.user.blank? || + (!is_staff? && opts[:is_warning].present? && opts[:is_warning] == "true") + ) + return false + end taken = opts[:taken_actions].try(:keys).to_a - is_flag = PostActionType.notify_flag_types[action_key] || PostActionType.custom_types[action_key] + is_flag = + PostActionType.notify_flag_types[action_key] || PostActionType.custom_types[action_key] already_taken_this_action = taken.any? && taken.include?(PostActionType.types[action_key]) - already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any? + already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any? - result = if authenticated? && post && !@user.anonymous? + result = + if authenticated? && post && !@user.anonymous? + # Silenced users can't flag + return false if is_flag && @user.silenced? - # Silenced users can't flag - return false if is_flag && @user.silenced? + # Hidden posts can't be flagged + return false if is_flag && post.hidden? - # Hidden posts can't be flagged - return false if is_flag && post.hidden? + # post made by staff, but we don't allow staff flags + return false if is_flag && (!SiteSetting.allow_flagging_staff?) && post&.user&.staff? - # post made by staff, but we don't allow staff flags - return false if is_flag && - (!SiteSetting.allow_flagging_staff?) && - post&.user&.staff? + if action_key == :notify_user && + !@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) + return false + end - if action_key == :notify_user && !@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) - return false + # we allow flagging for trust level 1 and higher + # always allowed for private messages + ( + is_flag && not(already_did_flagging) && + ( + @user.has_trust_level?(TrustLevel[SiteSetting.min_trust_to_flag_posts]) || + post.topic.private_message? + ) + ) || + # not a flagging action, and haven't done it already + not(is_flag || already_taken_this_action) && + # nothing except flagging on archived topics + not(post.topic&.archived?) && + # nothing except flagging on deleted posts + not(post.trashed?) && + # don't like your own stuff + not(action_key == :like && (post.user.blank? || is_my_own?(post))) end - # we allow flagging for trust level 1 and higher - # always allowed for private messages - (is_flag && not(already_did_flagging) && (@user.has_trust_level?(TrustLevel[SiteSetting.min_trust_to_flag_posts]) || post.topic.private_message?)) || - - # not a flagging action, and haven't done it already - not(is_flag || already_taken_this_action) && - - # nothing except flagging on archived topics - not(post.topic&.archived?) && - - # nothing except flagging on deleted posts - not(post.trashed?) && - - # don't like your own stuff - not(action_key == :like && (post.user.blank? || is_my_own?(post))) - end - !!result end @@ -94,12 +100,16 @@ module PostGuardian end def can_delete_all_posts?(user) - is_staff? && - user && - !user.admin? && - (is_admin? || - ((user.first_post_created_at.nil? || user.first_post_created_at >= SiteSetting.delete_user_max_post_age.days.ago) && - user.post_count <= SiteSetting.delete_all_posts_max.to_i)) + is_staff? && user && !user.admin? && + ( + is_admin? || + ( + ( + user.first_post_created_at.nil? || + user.first_post_created_at >= SiteSetting.delete_user_max_post_age.days.ago + ) && user.post_count <= SiteSetting.delete_all_posts_max.to_i + ) + ) end def can_create_post?(topic) @@ -108,53 +118,43 @@ module PostGuardian key = topic_memoize_key(topic) @can_create_post ||= {} - @can_create_post.fetch(key) do - @can_create_post[key] = can_create_post_in_topic?(topic) - end + @can_create_post.fetch(key) { @can_create_post[key] = can_create_post_in_topic?(topic) } end def can_edit_post?(post) - if Discourse.static_doc_topic_ids.include?(post.topic_id) && !is_admin? - return false - end + return false if Discourse.static_doc_topic_ids.include?(post.topic_id) && !is_admin? return true if is_admin? # Must be staff to edit a locked post return false if post.locked? && !is_staff? - return can_create_post?(post.topic) if ( - is_staff? || - ( - SiteSetting.trusted_users_can_edit_others? && - @user.has_trust_level?(TrustLevel[4]) - ) || - is_category_group_moderator?(post.topic&.category) - ) - - if post.topic&.archived? || post.user_deleted || post.deleted_at - return false + if ( + is_staff? || + (SiteSetting.trusted_users_can_edit_others? && @user.has_trust_level?(TrustLevel[4])) || + is_category_group_moderator?(post.topic&.category) + ) + return can_create_post?(post.topic) end + return false if post.topic&.archived? || post.user_deleted || post.deleted_at + # Editing a shared draft. - return true if ( - can_see_post?(post) && - can_create_post?(post.topic) && - post.topic.category_id == SiteSetting.shared_drafts_category.to_i && - can_see_category?(post.topic.category) && - can_see_shared_draft? - ) + if ( + can_see_post?(post) && can_create_post?(post.topic) && + post.topic.category_id == SiteSetting.shared_drafts_category.to_i && + can_see_category?(post.topic.category) && can_see_shared_draft? + ) + return true + end if post.wiki && (@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i) return can_create_post?(post.topic) end - if @user.trust_level < SiteSetting.min_trust_to_edit_post - return false - end + return false if @user.trust_level < SiteSetting.min_trust_to_edit_post if is_my_own?(post) - return false if @user.silenced? return can_edit_hidden_post?(post) if post.hidden? @@ -175,7 +175,8 @@ module PostGuardian def can_edit_hidden_post?(post) return false if post.nil? - post.hidden_at.nil? || post.hidden_at < SiteSetting.cooldown_minutes_after_hiding_posts.minutes.ago + post.hidden_at.nil? || + post.hidden_at < SiteSetting.cooldown_minutes_after_hiding_posts.minutes.ago end def can_delete_post_or_topic?(post) @@ -195,7 +196,12 @@ module PostGuardian # You can delete your own posts if is_my_own?(post) - return false if (SiteSetting.max_post_deletions_per_minute < 1 || SiteSetting.max_post_deletions_per_day < 1) + if ( + SiteSetting.max_post_deletions_per_minute < 1 || + SiteSetting.max_post_deletions_per_day < 1 + ) + return false + end return true if !post.user_deleted? end @@ -208,7 +214,9 @@ module PostGuardian return false if post.is_first_post? return false if !is_admin? || !can_edit_post?(post) return false if !post.deleted_at - return false if post.deleted_by_id == @user.id && post.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago + if post.deleted_by_id == @user.id && post.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago + return false + end true end @@ -220,7 +228,12 @@ module PostGuardian return true if can_moderate_topic?(topic) && !!post.deleted_at if is_my_own?(post) - return false if (SiteSetting.max_post_deletions_per_minute < 1 || SiteSetting.max_post_deletions_per_day < 1) + if ( + SiteSetting.max_post_deletions_per_minute < 1 || + SiteSetting.max_post_deletions_per_day < 1 + ) + return false + end return true if post.user_deleted && !post.deleted_at end @@ -230,14 +243,17 @@ module PostGuardian def can_delete_post_action?(post_action) return false unless is_my_own?(post_action) && !post_action.is_private_message? - post_action.created_at > SiteSetting.post_undo_action_window_mins.minutes.ago && !post_action.post&.topic&.archived? + post_action.created_at > SiteSetting.post_undo_action_window_mins.minutes.ago && + !post_action.post&.topic&.archived? end def can_see_post?(post) return false if post.blank? return true if is_admin? return false unless can_see_post_topic?(post) - return false unless post.user == @user || Topic.visible_post_types(@user).include?(post.post_type) + unless post.user == @user || Topic.visible_post_types(@user).include?(post.post_type) + return false + end return true if is_moderator? || is_category_group_moderator?(post.topic.category) return true if !post.trashed? || can_see_deleted_post?(post) false @@ -257,9 +273,7 @@ module PostGuardian return true if post.wiki || SiteSetting.edit_history_visible_to_public end - authenticated? && - (is_staff? || @user.id == post.user_id) && - can_see_post?(post) + authenticated? && (is_staff? || @user.id == post.user_id) && can_see_post?(post) end def can_change_post_owner? @@ -315,13 +329,18 @@ module PostGuardian private def can_create_post_in_topic?(topic) - return false if !SiteSetting.enable_system_message_replies? && topic.try(:subtype) == "system_message" + if !SiteSetting.enable_system_message_replies? && topic.try(:subtype) == "system_message" + return false + end - (!SpamRule::AutoSilence.prevent_posting?(@user) || (!!topic.try(:private_message?) && topic.allowed_users.include?(@user))) && ( - !topic || - !topic.category || - Category.post_create_allowed(self).where(id: topic.category.id).count == 1 - ) + ( + !SpamRule::AutoSilence.prevent_posting?(@user) || + (!!topic.try(:private_message?) && topic.allowed_users.include?(@user)) + ) && + ( + !topic || !topic.category || + Category.post_create_allowed(self).where(id: topic.category.id).count == 1 + ) end def topic_memoize_key(topic) @@ -336,8 +355,6 @@ module PostGuardian key = topic_memoize_key(topic) @can_see_post_topic ||= {} - @can_see_post_topic.fetch(key) do - @can_see_post_topic[key] = can_see_topic?(topic) - end + @can_see_post_topic.fetch(key) { @can_see_post_topic[key] = can_see_topic?(topic) } end end diff --git a/lib/guardian/post_revision_guardian.rb b/lib/guardian/post_revision_guardian.rb index 4372728b955..1e61b19e746 100644 --- a/lib/guardian/post_revision_guardian.rb +++ b/lib/guardian/post_revision_guardian.rb @@ -2,7 +2,6 @@ # mixin for all Guardian methods dealing with post_revisions permissions module PostRevisionGuardian - def can_see_post_revision?(post_revision) return false unless post_revision return false if post_revision.hidden && !can_view_hidden_post_revisions? @@ -21,5 +20,4 @@ module PostRevisionGuardian def can_view_hidden_post_revisions? is_staff? end - end diff --git a/lib/guardian/tag_guardian.rb b/lib/guardian/tag_guardian.rb index 5a4be92ab43..db1ec7688ce 100644 --- a/lib/guardian/tag_guardian.rb +++ b/lib/guardian/tag_guardian.rb @@ -3,11 +3,13 @@ #mixin for all guardian methods dealing with tagging permissions module TagGuardian def can_create_tag? - SiteSetting.tagging_enabled && @user.has_trust_level_or_staff?(SiteSetting.min_trust_to_create_tag) + SiteSetting.tagging_enabled && + @user.has_trust_level_or_staff?(SiteSetting.min_trust_to_create_tag) end def can_tag_topics? - SiteSetting.tagging_enabled && @user.has_trust_level_or_staff?(SiteSetting.min_trust_level_to_tag_topics) + SiteSetting.tagging_enabled && + @user.has_trust_level_or_staff?(SiteSetting.min_trust_level_to_tag_topics) end def can_tag_pms? @@ -16,7 +18,8 @@ module TagGuardian return true if @user == Discourse.system_user group_ids = SiteSetting.pm_tags_allowed_for_groups_map - group_ids.include?(Group::AUTO_GROUPS[:everyone]) || @user.group_users.exists?(group_id: group_ids) + group_ids.include?(Group::AUTO_GROUPS[:everyone]) || + @user.group_users.exists?(group_id: group_ids) end def can_admin_tags? @@ -28,12 +31,13 @@ module TagGuardian end def hidden_tag_names - @hidden_tag_names ||= begin - if SiteSetting.tagging_enabled && !is_staff? - DiscourseTagging.hidden_tag_names(self) - else - [] + @hidden_tag_names ||= + begin + if SiteSetting.tagging_enabled && !is_staff? + DiscourseTagging.hidden_tag_names(self) + else + [] + end end - end end end diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb index c487de68978..05fb6ad041d 100644 --- a/lib/guardian/topic_guardian.rb +++ b/lib/guardian/topic_guardian.rb @@ -3,13 +3,11 @@ #mixin for all guardian methods dealing with topic permissions module TopicGuardian def can_remove_allowed_users?(topic, target_user = nil) - is_staff? || - (topic.user == @user && @user.has_trust_level?(TrustLevel[2])) || - ( - topic.allowed_users.count > 1 && - topic.user != target_user && - !!(target_user && user == target_user) - ) + is_staff? || (topic.user == @user && @user.has_trust_level?(TrustLevel[2])) || + ( + topic.allowed_users.count > 1 && topic.user != target_user && + !!(target_user && user == target_user) + ) end def can_review_topic?(topic) @@ -49,10 +47,10 @@ module TopicGuardian # Creating Methods def can_create_topic?(parent) is_staff? || - (user && - user.trust_level >= SiteSetting.min_trust_to_create_topic.to_i && - can_create_post?(parent) && - Category.topic_create_allowed(self).limit(1).count == 1) + ( + user && user.trust_level >= SiteSetting.min_trust_to_create_topic.to_i && + can_create_post?(parent) && Category.topic_create_allowed(self).limit(1).count == 1 + ) end def can_create_topic_on_category?(category) @@ -60,11 +58,18 @@ module TopicGuardian category_id = Category === category ? category.id : category can_create_topic?(nil) && - (!category || Category.topic_create_allowed(self).where(id: category_id).count == 1) + (!category || Category.topic_create_allowed(self).where(id: category_id).count == 1) end def can_move_topic_to_category?(category) - category = Category === category ? category : Category.find(category || SiteSetting.uncategorized_category_id) + category = + ( + if Category === category + category + else + Category.find(category || SiteSetting.uncategorized_category_id) + end + ) is_staff? || (can_create_topic_on_category?(category) && !category.require_topic_approval?) end @@ -75,7 +80,9 @@ module TopicGuardian return false if topic.trashed? return true if is_admin? - trusted = (authenticated? && user.has_trust_level?(TrustLevel[4])) || is_moderator? || can_perform_action_available_to_group_moderators?(topic) + trusted = + (authenticated? && user.has_trust_level?(TrustLevel[4])) || is_moderator? || + can_perform_action_available_to_group_moderators?(topic) (!(topic.closed? || topic.archived?) || trusted) && can_create_post?(topic) end @@ -97,45 +104,40 @@ module TopicGuardian # except for a tiny edge case where the topic is uncategorized and you are trying # to fix it but uncategorized is disabled if ( - SiteSetting.allow_uncategorized_topics || - topic.category_id != SiteSetting.uncategorized_category_id - ) + SiteSetting.allow_uncategorized_topics || + topic.category_id != SiteSetting.uncategorized_category_id + ) return false if !can_create_topic_on_category?(topic.category) end # Editing a shared draft. - return true if ( - !topic.archived && - !topic.private_message? && - topic.category_id == SiteSetting.shared_drafts_category.to_i && - can_see_category?(topic.category) && - can_see_shared_draft? && - can_create_post?(topic) - ) + if ( + !topic.archived && !topic.private_message? && + topic.category_id == SiteSetting.shared_drafts_category.to_i && + can_see_category?(topic.category) && can_see_shared_draft? && can_create_post?(topic) + ) + return true + end # TL4 users can edit archived topics, but can not edit private messages - return true if ( - SiteSetting.trusted_users_can_edit_others? && - topic.archived && - !topic.private_message? && - user.has_trust_level?(TrustLevel[4]) && - can_create_post?(topic) - ) + if ( + SiteSetting.trusted_users_can_edit_others? && topic.archived && !topic.private_message? && + user.has_trust_level?(TrustLevel[4]) && can_create_post?(topic) + ) + return true + end # TL3 users can not edit archived topics and private messages - return true if ( - SiteSetting.trusted_users_can_edit_others? && - !topic.archived && - !topic.private_message? && - user.has_trust_level?(TrustLevel[3]) && - can_create_post?(topic) - ) + if ( + SiteSetting.trusted_users_can_edit_others? && !topic.archived && !topic.private_message? && + user.has_trust_level?(TrustLevel[3]) && can_create_post?(topic) + ) + return true + end return false if topic.archived - is_my_own?(topic) && - !topic.edit_time_limit_expired?(user) && - !first_post&.locked? && + is_my_own?(topic) && !topic.edit_time_limit_expired?(user) && !first_post&.locked? && (!first_post&.hidden? || can_edit_hidden_post?(first_post)) end @@ -149,9 +151,13 @@ module TopicGuardian def can_delete_topic?(topic) !topic.trashed? && - (is_staff? || (is_my_own?(topic) && topic.posts_count <= 1 && topic.created_at && topic.created_at > 24.hours.ago) || is_category_group_moderator?(topic.category)) && - !topic.is_category_topic? && - !Discourse.static_doc_topic_ids.include?(topic.id) + ( + is_staff? || + ( + is_my_own?(topic) && topic.posts_count <= 1 && topic.created_at && + topic.created_at > 24.hours.ago + ) || is_category_group_moderator?(topic.category) + ) && !topic.is_category_topic? && !Discourse.static_doc_topic_ids.include?(topic.id) end def can_permanently_delete_topic?(topic) @@ -165,15 +171,21 @@ module TopicGuardian # All other posts that were deleted still must be permanently deleted # before the topic can be deleted with the exception of small action # posts that will be deleted right before the topic is. - all_posts_count = Post.with_deleted - .where(topic_id: topic.id) - .where(post_type: [Post.types[:regular], Post.types[:moderator_action], Post.types[:whisper]]) - .count + all_posts_count = + Post + .with_deleted + .where(topic_id: topic.id) + .where( + post_type: [Post.types[:regular], Post.types[:moderator_action], Post.types[:whisper]], + ) + .count return false if all_posts_count > 1 return false if !is_admin? || !can_see_topic?(topic) return false if !topic.deleted_at - return false if topic.deleted_by_id == @user.id && topic.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago + if topic.deleted_by_id == @user.id && topic.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago + return false + end true end @@ -181,7 +193,7 @@ module TopicGuardian can_moderate?(topic) || can_perform_action_available_to_group_moderators?(topic) end - alias :can_create_unlisted_topic? :can_toggle_topic_visibility? + alias can_create_unlisted_topic? can_toggle_topic_visibility? def can_convert_topic?(topic) return false unless @user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) @@ -228,13 +240,16 @@ module TopicGuardian # Filter out topics with shared drafts if user cannot see shared drafts if !can_see_shared_draft? - default_scope = default_scope.left_outer_joins(:shared_draft).where("shared_drafts.id IS NULL") + default_scope = + default_scope.left_outer_joins(:shared_draft).where("shared_drafts.id IS NULL") end all_topics_scope = if authenticated? Topic.unscoped.merge( - secured_regular_topic_scope(default_scope, topic_ids: topic_ids).or(private_message_topic_scope(default_scope)) + secured_regular_topic_scope(default_scope, topic_ids: topic_ids).or( + private_message_topic_scope(default_scope), + ), ) else Topic.unscoped.merge(secured_regular_topic_scope(default_scope, topic_ids: topic_ids)) @@ -256,7 +271,10 @@ module TopicGuardian category = topic.category can_see_category?(category) && - (!category.read_restricted || !is_staged? || secure_category_ids.include?(category.id) || topic.user == user) + ( + !category.read_restricted || !is_staged? || secure_category_ids.include?(category.id) || + topic.user == user + ) end def can_get_access_to_topic?(topic) @@ -266,9 +284,17 @@ module TopicGuardian def filter_allowed_categories(records) return records if is_admin? && !SiteSetting.suppress_secured_categories_from_admin - records = allowed_category_ids.size == 0 ? - records.where('topics.category_id IS NULL') : - records.where('topics.category_id IS NULL or topics.category_id IN (?)', allowed_category_ids) + records = + ( + if allowed_category_ids.size == 0 + records.where("topics.category_id IS NULL") + else + records.where( + "topics.category_id IS NULL or topics.category_id IN (?)", + allowed_category_ids, + ) + end + ) records.references(:categories) end @@ -276,7 +302,10 @@ module TopicGuardian def can_edit_featured_link?(category_id) return false unless SiteSetting.topic_featured_link_enabled return false unless @user.trust_level >= TrustLevel.levels[:basic] - Category.where(id: category_id || SiteSetting.uncategorized_category_id, topic_featured_link_allowed: true).exists? + Category.where( + id: category_id || SiteSetting.uncategorized_category_id, + topic_featured_link_allowed: true, + ).exists? end def can_update_bumped_at? @@ -292,7 +321,8 @@ module TopicGuardian return false if topic.private_message? && !can_tag_pms? return true if can_edit_topic?(topic) - if topic&.first_post&.wiki && (@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i) + if topic&.first_post&.wiki && + (@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i) return can_create_post?(topic) end @@ -306,12 +336,12 @@ module TopicGuardian is_category_group_moderator?(topic.category) end - alias :can_archive_topic? :can_perform_action_available_to_group_moderators? - alias :can_close_topic? :can_perform_action_available_to_group_moderators? - alias :can_open_topic? :can_perform_action_available_to_group_moderators? - alias :can_split_merge_topic? :can_perform_action_available_to_group_moderators? - alias :can_edit_staff_notes? :can_perform_action_available_to_group_moderators? - alias :can_pin_unpin_topic? :can_perform_action_available_to_group_moderators? + alias can_archive_topic? can_perform_action_available_to_group_moderators? + alias can_close_topic? can_perform_action_available_to_group_moderators? + alias can_open_topic? can_perform_action_available_to_group_moderators? + alias can_split_merge_topic? can_perform_action_available_to_group_moderators? + alias can_edit_staff_notes? can_perform_action_available_to_group_moderators? + alias can_pin_unpin_topic? can_perform_action_available_to_group_moderators? def can_move_posts?(topic) return false if is_silenced? @@ -327,12 +357,10 @@ module TopicGuardian def private_message_topic_scope(scope) pm_scope = scope.private_messages_for_user(user) - if is_moderator? - pm_scope = pm_scope.or(scope.where(<<~SQL)) + pm_scope = pm_scope.or(scope.where(<<~SQL)) if is_moderator? topics.subtype = '#{TopicSubtype.moderator_warning}' OR topics.id IN (#{Topic.has_flag_scope.select(:topic_id).to_sql}) SQL - end pm_scope end @@ -357,7 +385,8 @@ module TopicGuardian ) SQL - secured_scope = secured_scope.or(Topic.unscoped.where(sql, user_id: user.id, topic_ids: topic_ids)) + secured_scope = + secured_scope.or(Topic.unscoped.where(sql, user_id: user.id, topic_ids: topic_ids)) end scope.listable_topics.merge(secured_scope) diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index 675e3431ef7..2879ad036f9 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -2,9 +2,8 @@ # mixin for all Guardian methods dealing with user permissions module UserGuardian - def can_claim_reviewable_topic?(topic) - SiteSetting.reviewable_claiming != 'disabled' && can_review_topic?(topic) + SiteSetting.reviewable_claiming != "disabled" && can_review_topic?(topic) end def can_pick_avatar?(user_avatar, upload) @@ -63,13 +62,14 @@ module UserGuardian if is_me?(user) !SiteSetting.enable_discourse_connect && - !user.has_more_posts_than?(SiteSetting.delete_user_self_max_post_count) + !user.has_more_posts_than?(SiteSetting.delete_user_self_max_post_count) else - is_staff? && ( - user.first_post_created_at.nil? || - !user.has_more_posts_than?(User::MAX_STAFF_DELETE_POST_COUNT) || - user.first_post_created_at > SiteSetting.delete_user_max_post_age.to_i.days.ago - ) + is_staff? && + ( + user.first_post_created_at.nil? || + !user.has_more_posts_than?(User::MAX_STAFF_DELETE_POST_COUNT) || + user.first_post_created_at > SiteSetting.delete_user_max_post_age.to_i.days.ago + ) end end @@ -123,9 +123,7 @@ module UserGuardian return true if !SiteSetting.allow_users_to_hide_profile? # If a user has hidden their profile, restrict it to them and staff - if user.user_option.try(:hide_profile_and_presence?) - return is_me?(user) || is_staff? - end + return is_me?(user) || is_staff? if user.user_option.try(:hide_profile_and_presence?) true end @@ -141,14 +139,13 @@ module UserGuardian is_staff_or_is_me = is_staff? || is_me?(user) cache_key = is_staff_or_is_me ? :staff_or_me : :other - @allowed_user_field_ids[cache_key] ||= - begin - if is_staff_or_is_me - UserField.pluck(:id) - else - UserField.where("show_on_profile OR show_on_user_card").pluck(:id) - end + @allowed_user_field_ids[cache_key] ||= begin + if is_staff_or_is_me + UserField.pluck(:id) + else + UserField.where("show_on_profile OR show_on_user_card").pluck(:id) end + end end def can_feature_topic?(user, topic) @@ -161,13 +158,14 @@ module UserGuardian end def can_see_review_queue? - is_staff? || ( - SiteSetting.enable_category_group_moderation && - Reviewable - .where(reviewable_by_group_id: @user.group_users.pluck(:group_id)) - .where('category_id IS NULL or category_id IN (?)', allowed_category_ids) - .exists? - ) + is_staff? || + ( + SiteSetting.enable_category_group_moderation && + Reviewable + .where(reviewable_by_group_id: @user.group_users.pluck(:group_id)) + .where("category_id IS NULL or category_id IN (?)", allowed_category_ids) + .exists? + ) end def can_see_summary_stats?(target_user) @@ -175,11 +173,17 @@ module UserGuardian end def can_upload_profile_header?(user) - (is_me?(user) && user.has_trust_level?(SiteSetting.min_trust_level_to_allow_profile_background.to_i)) || is_staff? + ( + is_me?(user) && + user.has_trust_level?(SiteSetting.min_trust_level_to_allow_profile_background.to_i) + ) || is_staff? end def can_upload_user_card_background?(user) - (is_me?(user) && user.has_trust_level?(SiteSetting.min_trust_level_to_allow_user_card_background.to_i)) || is_staff? + ( + is_me?(user) && + user.has_trust_level?(SiteSetting.min_trust_level_to_allow_user_card_background.to_i) + ) || is_staff? end def can_upload_external? diff --git a/lib/has_errors.rb b/lib/has_errors.rb index 907e537d1c0..daa8bb8e8d3 100644 --- a/lib/has_errors.rb +++ b/lib/has_errors.rb @@ -33,11 +33,8 @@ module HasErrors def add_errors_from(obj) return if obj.blank? - if obj.is_a?(StandardError) - return add_error(obj.message) - end + return add_error(obj.message) if obj.is_a?(StandardError) obj.errors.full_messages.each { |msg| add_error(msg) } end - end diff --git a/lib/highlight_js.rb b/lib/highlight_js.rb index 8ad4f26264e..48bd77baf39 100644 --- a/lib/highlight_js.rb +++ b/lib/highlight_js.rb @@ -2,12 +2,47 @@ module HighlightJs HIGHLIGHTJS_DIR ||= "#{Rails.root}/vendor/assets/javascripts/highlightjs/" - BUNDLED_LANGS = %w(bash c cpp csharp css diff go graphql ini java javascript json kotlin less lua makefile xml markdown objectivec perl php php-template plaintext python python-repl r ruby rust scss shell sql swift typescript vbnet wasm yaml) + BUNDLED_LANGS = %w[ + bash + c + cpp + csharp + css + diff + go + graphql + ini + java + javascript + json + kotlin + less + lua + makefile + xml + markdown + objectivec + perl + php + php-template + plaintext + python + python-repl + r + ruby + rust + scss + shell + sql + swift + typescript + vbnet + wasm + yaml + ] def self.languages - langs = Dir.glob(HIGHLIGHTJS_DIR + "languages/*.js").map do |path| - File.basename(path)[0..-8] - end + langs = Dir.glob(HIGHLIGHTJS_DIR + "languages/*.js").map { |path| File.basename(path)[0..-8] } langs.sort end @@ -26,8 +61,9 @@ module HighlightJs end def self.version(lang_string) - (@lang_string_cache ||= {})[lang_string] ||= - Digest::SHA1.hexdigest(bundle lang_string.split("|")) + (@lang_string_cache ||= {})[lang_string] ||= Digest::SHA1.hexdigest( + bundle lang_string.split("|") + ) end def self.path diff --git a/lib/hijack.rb b/lib/hijack.rb index 0b39abb7103..eb9b5ce2163 100644 --- a/lib/hijack.rb +++ b/lib/hijack.rb @@ -1,19 +1,17 @@ # frozen_string_literal: true -require 'method_profiler' +require "method_profiler" # This module allows us to hijack a request and send it to the client in the deferred job queue # For cases where we are making remote calls like onebox or proxying files and so on this helps # free up a unicorn worker while the remote IO is happening module Hijack - def hijack(info: nil, &blk) controller_class = self.class - if hijack = request.env['rack.hijack'] - - request.env['discourse.request_tracker.skip'] = true - request_tracker = request.env['discourse.request_tracker'] + if hijack = request.env["rack.hijack"] + request.env["discourse.request_tracker.skip"] = true + request_tracker = request.env["discourse.request_tracker"] # in the past unicorn would recycle env, this is not longer the case env = request.env @@ -32,7 +30,6 @@ module Hijack original_headers = response.headers.dup Scheduler::Defer.later("hijack #{params["controller"]} #{params["action"]} #{info}") do - MethodProfiler.start(transfer_timings) begin Thread.current[Logster::Logger::LOGSTER_ENV] = env @@ -47,22 +44,22 @@ module Hijack instance.response = response instance.request = request_copy - original_headers&.each do |k, v| - instance.response.headers[k] = v - end + original_headers&.each { |k, v| instance.response.headers[k] = v } view_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin instance.instance_eval(&blk) rescue => e # TODO we need to reuse our exception handling in ApplicationController - Discourse.warn_exception(e, message: "Failed to process hijacked response correctly", env: env) + Discourse.warn_exception( + e, + message: "Failed to process hijacked response correctly", + env: env, + ) end view_runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - view_start - unless instance.response_body || response.committed? - instance.status = 500 - end + instance.status = 500 unless instance.response_body || response.committed? response.commit! @@ -74,13 +71,11 @@ module Hijack Discourse::Cors.apply_headers(cors_origins, env, headers) end - headers['Content-Type'] ||= response.content_type || "text/plain" - headers['Content-Length'] = body.bytesize - headers['Connection'] = "close" + headers["Content-Type"] ||= response.content_type || "text/plain" + headers["Content-Length"] = body.bytesize + headers["Connection"] = "close" - if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] - headers['Discourse-Logged-Out'] = '1' - end + headers["Discourse-Logged-Out"] = "1" if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] status_string = Rack::Utils::HTTP_STATUS_CODES[response.status.to_i] || "Unknown" io.write "#{response.status} #{status_string}\r\n" @@ -90,9 +85,7 @@ module Hijack headers["X-Runtime"] = "#{"%0.6f" % duration}" end - headers.each do |name, val| - io.write "#{name}: #{val}\r\n" - end + headers.each { |name, val| io.write "#{name}: #{val}\r\n" } io.write "\r\n" io.write body @@ -100,30 +93,35 @@ module Hijack # happens if client terminated before we responded, ignore io = nil ensure - if Rails.configuration.try(:lograge).try(:enabled) if timings db_runtime = 0 - if timings[:sql] - db_runtime = timings[:sql][:duration] - end + db_runtime = timings[:sql][:duration] if timings[:sql] subscriber = Lograge::LogSubscribers::ActionController.new - payload = ActiveSupport::HashWithIndifferentAccess.new( - controller: self.class.name, - action: action_name, - params: request.filtered_parameters, - headers: request.headers, - format: request.format.ref, - method: request.request_method, - path: request.fullpath, - view_runtime: view_runtime * 1000.0, - db_runtime: db_runtime * 1000.0, - timings: timings, - status: response.status - ) + payload = + ActiveSupport::HashWithIndifferentAccess.new( + controller: self.class.name, + action: action_name, + params: request.filtered_parameters, + headers: request.headers, + format: request.format.ref, + method: request.request_method, + path: request.fullpath, + view_runtime: view_runtime * 1000.0, + db_runtime: db_runtime * 1000.0, + timings: timings, + status: response.status, + ) - event = ActiveSupport::Notifications::Event.new("hijack", Time.now, Time.now + timings[:total_duration], "", payload) + event = + ActiveSupport::Notifications::Event.new( + "hijack", + Time.now, + Time.now + timings[:total_duration], + "", + payload, + ) subscriber.process_action(event) end end @@ -131,10 +129,19 @@ module Hijack MethodProfiler.clear Thread.current[Logster::Logger::LOGSTER_ENV] = nil - io.close if io rescue nil + begin + io.close if io + rescue StandardError + nil + end if request_tracker - status = response.status rescue 500 + status = + begin + response.status + rescue StandardError + 500 + end request_tracker.log_request_info(env, [status, headers || {}, []], timings) end diff --git a/lib/homepage_constraint.rb b/lib/homepage_constraint.rb index ef6686d5fdb..78492e4f018 100644 --- a/lib/homepage_constraint.rb +++ b/lib/homepage_constraint.rb @@ -6,7 +6,7 @@ class HomePageConstraint end def matches?(request) - return @filter == 'finish_installation' if SiteSetting.has_login_hint? + return @filter == "finish_installation" if SiteSetting.has_login_hint? current_user = CurrentUser.lookup_from_env(request.env) homepage = current_user&.user_option&.homepage || SiteSetting.anonymous_homepage diff --git a/lib/html_prettify.rb b/lib/html_prettify.rb index 06a00099b2b..074a3c8a2a5 100644 --- a/lib/html_prettify.rb +++ b/lib/html_prettify.rb @@ -82,14 +82,14 @@ class HtmlPrettify < String elsif @options.include?(-1) do_stupefy = true else - do_quotes = @options.include?(:quotes) - do_backticks = @options.include?(:backticks) - do_backticks = :both if @options.include?(:allbackticks) - do_dashes = :normal if @options.include?(:dashes) - do_dashes = :oldschool if @options.include?(:oldschool) - do_dashes = :inverted if @options.include?(:inverted) - do_ellipses = @options.include?(:ellipses) - do_stupefy = @options.include?(:stupefy) + do_quotes = @options.include?(:quotes) + do_backticks = @options.include?(:backticks) + do_backticks = :both if @options.include?(:allbackticks) + do_dashes = :normal if @options.include?(:dashes) + do_dashes = :oldschool if @options.include?(:oldschool) + do_dashes = :inverted if @options.include?(:inverted) + do_ellipses = @options.include?(:ellipses) + do_stupefy = @options.include?(:stupefy) end # Parse the HTML @@ -110,8 +110,8 @@ class HtmlPrettify < String tokens.each do |token| if token.first == :tag result << token[1] - if token[1] =~ %r!<(/?)(?:pre|code|kbd|script|math)[\s>]! - in_pre = ($1 != "/") # Opening or closing tag? + if token[1] =~ %r{<(/?)(?:pre|code|kbd|script|math)[\s>]} + in_pre = ($1 != "/") # Opening or closing tag? end else t = token[1] @@ -120,24 +120,23 @@ class HtmlPrettify < String last_char = t[-1].chr unless in_pre - t.gsub!("'", "'") t.gsub!(""", '"') if do_dashes - t = educate_dashes t if do_dashes == :normal - t = educate_dashes_oldschool t if do_dashes == :oldschool - t = educate_dashes_inverted t if do_dashes == :inverted + t = educate_dashes t if do_dashes == :normal + t = educate_dashes_oldschool t if do_dashes == :oldschool + t = educate_dashes_inverted t if do_dashes == :inverted end - t = educate_ellipses t if do_ellipses + t = educate_ellipses t if do_ellipses t = educate_fractions t # Note: backticks need to be processed before quotes. if do_backticks t = educate_backticks t - t = educate_single_backticks t if do_backticks == :both + t = educate_single_backticks t if do_backticks == :both end if do_quotes @@ -161,7 +160,7 @@ class HtmlPrettify < String end end - t = stupefy_entities t if do_stupefy + t = stupefy_entities t if do_stupefy end prev_token_last_char = last_char @@ -179,8 +178,7 @@ class HtmlPrettify < String # em-dash HTML entity. # def educate_dashes(str) - str. - gsub(/--/, entity(:em_dash)) + str.gsub(/--/, entity(:em_dash)) end # The string, with each instance of "--" translated to an @@ -188,9 +186,7 @@ class HtmlPrettify < String # em-dash HTML entity. # def educate_dashes_oldschool(str) - str. - gsub(/---/, entity(:em_dash)). - gsub(/--/, entity(:en_dash)) + str.gsub(/---/, entity(:em_dash)).gsub(/--/, entity(:en_dash)) end # Return the string, with each instance of "--" translated @@ -204,9 +200,7 @@ class HtmlPrettify < String # Aaron Swartz for the idea.) # def educate_dashes_inverted(str) - str. - gsub(/---/, entity(:en_dash)). - gsub(/--/, entity(:em_dash)) + str.gsub(/---/, entity(:en_dash)).gsub(/--/, entity(:em_dash)) end # Return the string, with each instance of "..." translated @@ -214,31 +208,25 @@ class HtmlPrettify < String # spaces between the dots. # def educate_ellipses(str) - str. - gsub('...', entity(:ellipsis)). - gsub('. . .', entity(:ellipsis)) + str.gsub("...", entity(:ellipsis)).gsub(". . .", entity(:ellipsis)) end # Return the string, with "``backticks''"-style single quotes # translated into HTML curly quote entities. # def educate_backticks(str) - str. - gsub("``", entity(:double_left_quote)). - gsub("''", entity(:double_right_quote)) + str.gsub("``", entity(:double_left_quote)).gsub("''", entity(:double_right_quote)) end # Return the string, with "`backticks'"-style single quotes # translated into HTML curly quote entities. # def educate_single_backticks(str) - str. - gsub("`", entity(:single_left_quote)). - gsub("'", entity(:single_right_quote)) + str.gsub("`", entity(:single_left_quote)).gsub("'", entity(:single_right_quote)) end def educate_fractions(str) - str.gsub(/(\s+|^)(1\/4|1\/2|3\/4)([,.;\s]|$)/) do + str.gsub(%r{(\s+|^)(1/4|1/2|3/4)([,.;\s]|$)}) do frac = if $2 == "1/2" entity(:frac12) @@ -261,52 +249,45 @@ class HtmlPrettify < String # Special case if the very first character is a quote followed by # punctuation at a non-word-break. Close the quotes by brute # force: - str.gsub!(/^'(?=#{punct_class}\B)/, - entity(:single_right_quote)) - str.gsub!(/^"(?=#{punct_class}\B)/, - entity(:double_right_quote)) + str.gsub!(/^'(?=#{punct_class}\B)/, entity(:single_right_quote)) + str.gsub!(/^"(?=#{punct_class}\B)/, entity(:double_right_quote)) # Special case for double sets of quotes, e.g.: #

He said, "'Quoted' words in a larger quote."

- str.gsub!(/"'(?=\w)/, - "#{entity(:double_left_quote)}#{entity(:single_left_quote)}") - str.gsub!(/'"(?=\w)/, - "#{entity(:single_left_quote)}#{entity(:double_left_quote)}") + str.gsub!(/"'(?=\w)/, "#{entity(:double_left_quote)}#{entity(:single_left_quote)}") + str.gsub!(/'"(?=\w)/, "#{entity(:single_left_quote)}#{entity(:double_left_quote)}") # Special case for decade abbreviations (the '80s): - str.gsub!(/'(?=\d\ds)/, - entity(:single_right_quote)) + str.gsub!(/'(?=\d\ds)/, entity(:single_right_quote)) close_class = %![^\ \t\r\n\\[\{\(\-]! dec_dashes = "#{entity(:en_dash)}|#{entity(:em_dash)}" # Get most opening single quotes: - str.gsub!(/(\s| |=|--|&[mn]dash;|#{dec_dashes}|ȁ[34];)'(?=\w)/, - '\1' + entity(:single_left_quote)) + str.gsub!( + /(\s| |=|--|&[mn]dash;|#{dec_dashes}|ȁ[34];)'(?=\w)/, + '\1' + entity(:single_left_quote), + ) # Single closing quotes: - str.gsub!(/(#{close_class})'/, - '\1' + entity(:single_right_quote)) - str.gsub!(/'(\s|s\b|$)/, - entity(:single_right_quote) + '\1') + str.gsub!(/(#{close_class})'/, '\1' + entity(:single_right_quote)) + str.gsub!(/'(\s|s\b|$)/, entity(:single_right_quote) + '\1') # Any remaining single quotes should be opening ones: - str.gsub!(/'/, - entity(:single_left_quote)) + str.gsub!(/'/, entity(:single_left_quote)) # Get most opening double quotes: - str.gsub!(/(\s| |=|--|&[mn]dash;|#{dec_dashes}|ȁ[34];)"(?=\w)/, - '\1' + entity(:double_left_quote)) + str.gsub!( + /(\s| |=|--|&[mn]dash;|#{dec_dashes}|ȁ[34];)"(?=\w)/, + '\1' + entity(:double_left_quote), + ) # Double closing quotes: - str.gsub!(/(#{close_class})"/, - '\1' + entity(:double_right_quote)) - str.gsub!(/"(\s|s\b|$)/, - entity(:double_right_quote) + '\1') + str.gsub!(/(#{close_class})"/, '\1' + entity(:double_right_quote)) + str.gsub!(/"(\s|s\b|$)/, entity(:double_right_quote) + '\1') # Any remaining quotes should be opening ones: - str.gsub!(/"/, - entity(:double_left_quote)) + str.gsub!(/"/, entity(:double_left_quote)) str end @@ -320,16 +301,14 @@ class HtmlPrettify < String new_str = str.dup { - en_dash: '-', - em_dash: '--', + en_dash: "-", + em_dash: "--", single_left_quote: "'", single_right_quote: "'", double_left_quote: '"', double_right_quote: '"', - ellipsis: '...' - }.each do |k, v| - new_str.gsub!(/#{entity(k)}/, v) - end + ellipsis: "...", + }.each { |k, v| new_str.gsub!(/#{entity(k)}/, v) } new_str end @@ -354,14 +333,12 @@ class HtmlPrettify < String prev_end = 0 scan(tag_soup) do - tokens << [:text, $1] if $1 != "" + tokens << [:text, $1] if $1 != "" tokens << [:tag, $2] prev_end = $~.end(0) end - if prev_end < size - tokens << [:text, self[prev_end..-1]] - end + tokens << [:text, self[prev_end..-1]] if prev_end < size tokens end @@ -385,5 +362,4 @@ class HtmlPrettify < String def entity(key) @entities[key] end - end diff --git a/lib/html_to_markdown.rb b/lib/html_to_markdown.rb index 2d2783d4671..67626fd76e7 100644 --- a/lib/html_to_markdown.rb +++ b/lib/html_to_markdown.rb @@ -3,12 +3,11 @@ require "securerandom" class HtmlToMarkdown - def initialize(html, opts = {}) @opts = opts # we're only interested in - @doc = Nokogiri::HTML5(html).at("body") + @doc = Nokogiri.HTML5(html).at("body") remove_not_allowed!(@doc) remove_hidden!(@doc) @@ -17,9 +16,7 @@ class HtmlToMarkdown end def to_markdown - traverse(@doc) - .gsub(/\n{2,}/, "\n\n") - .strip + traverse(@doc).gsub(/\n{2,}/, "\n\n").strip end private @@ -50,31 +47,33 @@ class HtmlToMarkdown loop do changed = false - doc.css("br.#{klass}").each do |br| - parent = br.parent + doc + .css("br.#{klass}") + .each do |br| + parent = br.parent - if block?(parent) - br.remove_class(klass) - else - before, after = parent.children.slice_when { |n| n == br }.to_a + if block?(parent) + br.remove_class(klass) + else + before, after = parent.children.slice_when { |n| n == br }.to_a - if before.size > 1 - b = doc.document.create_element(parent.name) - before[0...-1].each { |c| b.add_child(c) } - parent.previous = b if b.inner_html.present? + if before.size > 1 + b = doc.document.create_element(parent.name) + before[0...-1].each { |c| b.add_child(c) } + parent.previous = b if b.inner_html.present? + end + + if after.present? + a = doc.document.create_element(parent.name) + after.each { |c| a.add_child(c) } + parent.next = a if a.inner_html.present? + end + + parent.replace(br) + + changed = true end - - if after.present? - a = doc.document.create_element(parent.name) - after.each { |c| a.add_child(c) } - parent.next = a if a.inner_html.present? - end - - parent.replace(br) - - changed = true end - end break if !changed end @@ -85,17 +84,21 @@ class HtmlToMarkdown def remove_whitespaces!(node) return true if "pre" == node.name - node.children.chunk { |n| is_inline?(n) }.each do |inline, nodes| - if inline - collapse_spaces!(nodes) && remove_trailing_space!(nodes) - else - nodes.each { |n| remove_whitespaces!(n) } + node + .children + .chunk { |n| is_inline?(n) } + .each do |inline, nodes| + if inline + collapse_spaces!(nodes) && remove_trailing_space!(nodes) + else + nodes.each { |n| remove_whitespaces!(n) } + end end - end end def is_inline?(node) - node.text? || ("br" != node.name && node.description&.inline? && node.children.all? { |n| is_inline?(n) }) + node.text? || + ("br" != node.name && node.description&.inline? && node.children.all? { |n| is_inline?(n) }) end def collapse_spaces!(nodes, was_space = true) @@ -141,15 +144,16 @@ class HtmlToMarkdown send(visitor, node) if respond_to?(visitor, true) end - ALLOWED_IMG_SRCS ||= %w{http:// https:// www.} + ALLOWED_IMG_SRCS ||= %w[http:// https:// www.] def allowed_hrefs - @allowed_hrefs ||= begin - hrefs = SiteSetting.allowed_href_schemes.split("|").map { |scheme| "#{scheme}:" }.to_set - ALLOWED_IMG_SRCS.each { |src| hrefs << src } - hrefs << "mailto:" - hrefs.to_a - end + @allowed_hrefs ||= + begin + hrefs = SiteSetting.allowed_href_schemes.split("|").map { |scheme| "#{scheme}:" }.to_set + ALLOWED_IMG_SRCS.each { |src| hrefs << src } + hrefs << "mailto:" + hrefs.to_a + end end def visit_a(node) @@ -176,11 +180,9 @@ class HtmlToMarkdown end end - ALLOWED ||= %w{kbd del ins small big sub sup dl dd dt mark} + ALLOWED ||= %w[kbd del ins small big sub sup dl dd dt mark] ALLOWED.each do |tag| - define_method("visit_#{tag}") do |node| - "<#{tag}>#{traverse(node)}" - end + define_method("visit_#{tag}") { |node| "<#{tag}>#{traverse(node)}" } end def visit_blockquote(node) @@ -191,7 +193,7 @@ class HtmlToMarkdown "\n\n#{text}\n\n" end - BLOCKS ||= %w{div tr} + BLOCKS ||= %w[div tr] BLOCKS.each do |tag| define_method("visit_#{tag}") do |node| prefix = block?(node.previous_element) ? "" : "\n" @@ -203,12 +205,8 @@ class HtmlToMarkdown "\n\n#{traverse(node)}\n\n" end - TRAVERSABLES ||= %w{aside font span thead tbody tfooter u} - TRAVERSABLES.each do |tag| - define_method("visit_#{tag}") do |node| - traverse(node) - end - end + TRAVERSABLES ||= %w[aside font span thead tbody tfooter u] + TRAVERSABLES.each { |tag| define_method("visit_#{tag}") { |node| traverse(node) } } def visit_tt(node) "`#{traverse(node)}`" @@ -245,18 +243,10 @@ class HtmlToMarkdown visit_abbr(node) end - (1..6).each do |n| - define_method("visit_h#{n}") do |node| - "#{"#" * n} #{traverse(node)}" - end - end + (1..6).each { |n| define_method("visit_h#{n}") { |node| "#{"#" * n} #{traverse(node)}" } } - CELLS ||= %w{th td} - CELLS.each do |tag| - define_method("visit_#{tag}") do |node| - "#{traverse(node)} " - end - end + CELLS ||= %w[th td] + CELLS.each { |tag| define_method("visit_#{tag}") { |node| "#{traverse(node)} " } } def visit_table(node) if rows = extract_rows(node) @@ -264,7 +254,8 @@ class HtmlToMarkdown text = "| " + headers.map { |td| traverse(td).gsub(/\n/, "
") }.join(" | ") + " |\n" text << "| " + (["-"] * headers.size).join(" | ") + " |\n" rows[1..-1].each do |row| - text << "| " + row.css("td").map { |td| traverse(td).gsub(/\n/, "
") }.join(" | ") + " |\n" + text << "| " + row.css("td").map { |td| traverse(td).gsub(/\n/, "
") }.join(" | ") + + " |\n" end "\n\n#{text}\n\n" else @@ -280,7 +271,7 @@ class HtmlToMarkdown rows end - LISTS ||= %w{ul ol} + LISTS ||= %w[ul ol] LISTS.each do |tag| define_method("visit_#{tag}") do |node| prefix = block?(node.previous_element) ? "" : "\n" @@ -304,12 +295,12 @@ class HtmlToMarkdown "#{marker}#{text}#{suffix}" end - EMPHASES ||= %w{i em} + EMPHASES ||= %w[i em] EMPHASES.each do |tag| define_method("visit_#{tag}") do |node| text = traverse(node) - return "" if text.empty? + return "" if text.empty? return " " if text.blank? return "<#{tag}>#{text}" if text["\n"] || (text["*"] && text["_"]) @@ -321,12 +312,12 @@ class HtmlToMarkdown end end - STRONGS ||= %w{b strong} + STRONGS ||= %w[b strong] STRONGS.each do |tag| define_method("visit_#{tag}") do |node| text = traverse(node) - return "" if text.empty? + return "" if text.empty? return " " if text.blank? return "<#{tag}>#{text}" if text["\n"] || (text["*"] && text["_"]) @@ -338,12 +329,12 @@ class HtmlToMarkdown end end - STRIKES ||= %w{s strike} + STRIKES ||= %w[s strike] STRIKES.each do |tag| define_method("visit_#{tag}") do |node| text = traverse(node) - return "" if text.empty? + return "" if text.empty? return " " if text.blank? return "<#{tag}>#{text}" if text["\n"] || text["~~"] @@ -358,7 +349,19 @@ class HtmlToMarkdown node.text end - HTML5_BLOCK_ELEMENTS ||= %w[article aside details dialog figcaption figure footer header main nav section] + HTML5_BLOCK_ELEMENTS ||= %w[ + article + aside + details + dialog + figcaption + figure + footer + header + main + nav + section + ] def block?(node) return false if !node node.description&.block? || HTML5_BLOCK_ELEMENTS.include?(node.name) diff --git a/lib/http_language_parser.rb b/lib/http_language_parser.rb index debfddc604d..a2e24ca0aae 100644 --- a/lib/http_language_parser.rb +++ b/lib/http_language_parser.rb @@ -4,10 +4,10 @@ module HttpLanguageParser def self.parse(header) # Rails I18n uses underscores between the locale and the region; the request # headers use hyphens. - require 'http_accept_language' unless defined? HttpAcceptLanguage - available_locales = I18n.available_locales.map { |locale| locale.to_s.tr('_', '-') } + require "http_accept_language" unless defined?(HttpAcceptLanguage) + available_locales = I18n.available_locales.map { |locale| locale.to_s.tr("_", "-") } parser = HttpAcceptLanguage::Parser.new(header) - matched = parser.language_region_compatible_from(available_locales)&.tr('-', '_') + matched = parser.language_region_compatible_from(available_locales)&.tr("-", "_") matched || SiteSetting.default_locale end end diff --git a/lib/i18n/backend/discourse_i18n.rb b/lib/i18n/backend/discourse_i18n.rb index 1511e67e961..4b1e24e9ec6 100644 --- a/lib/i18n/backend/discourse_i18n.rb +++ b/lib/i18n/backend/discourse_i18n.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'i18n/backend/pluralization' +require "i18n/backend/pluralization" module I18n module Backend @@ -22,9 +22,7 @@ module I18n # force explicit loading def load_translations(*filenames) unless filenames.empty? - self.class.sort_locale_files(filenames.flatten).each do |filename| - load_file(filename) - end + self.class.sort_locale_files(filenames.flatten).each { |filename| load_file(filename) } end end @@ -90,10 +88,12 @@ module I18n if overrides if options[:count] if !existing_translations - I18n.fallbacks[locale].drop(1).each do |fallback| - existing_translations = super(fallback, key, scope, options) - break if existing_translations.present? - end + I18n.fallbacks[locale] + .drop(1) + .each do |fallback| + existing_translations = super(fallback, key, scope, options) + break if existing_translations.present? + end end if existing_translations @@ -106,9 +106,11 @@ module I18n result = {} - remapped_translations.merge(overrides).each do |k, v| - result[k.split('.').last.to_sym] = v if k != key && k.start_with?(key) - end + remapped_translations + .merge(overrides) + .each do |k, v| + result[k.split(".").last.to_sym] = v if k != key && k.start_with?(key) + end return result if result.size > 0 end end diff --git a/lib/i18n/duplicate_key_finder.rb b/lib/i18n/duplicate_key_finder.rb index 65b0f33d0e9..4abd6300ff9 100644 --- a/lib/i18n/duplicate_key_finder.rb +++ b/lib/i18n/duplicate_key_finder.rb @@ -3,7 +3,6 @@ require "locale_file_walker" class DuplicateKeyFinder < LocaleFileWalker - def find_duplicates(path) @keys_with_count = Hash.new { 0 } handle_document(Psych.parse_file(path)) @@ -14,6 +13,6 @@ class DuplicateKeyFinder < LocaleFileWalker def handle_scalar(node, depth, parents) super - @keys_with_count[parents.join('.')] += 1 + @keys_with_count[parents.join(".")] += 1 end end diff --git a/lib/i18n/locale_file_checker.rb b/lib/i18n/locale_file_checker.rb index 4d0088ab8c6..de9eae3056c 100644 --- a/lib/i18n/locale_file_checker.rb +++ b/lib/i18n/locale_file_checker.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'i18n/i18n_interpolation_keys_finder' -require 'yaml' +require "i18n/i18n_interpolation_keys_finder" +require "yaml" class LocaleFileChecker TYPE_MISSING_INTERPOLATION_KEYS = 1 @@ -17,7 +17,8 @@ class LocaleFileChecker locale_files.each do |locale_path| next unless reference_path = reference_file(locale_path) - @relative_locale_path = Pathname.new(locale_path).relative_path_from(Pathname.new(Rails.root)).to_s + @relative_locale_path = + Pathname.new(locale_path).relative_path_from(Pathname.new(Rails.root)).to_s @locale_yaml = YAML.load_file(locale_path) @reference_yaml = YAML.load_file(reference_path) @@ -34,14 +35,14 @@ class LocaleFileChecker private - YML_DIRS = ["config/locales", "plugins/**/locales"] + YML_DIRS = %w[config/locales plugins/**/locales] PLURALS_FILE = "config/locales/plurals.rb" REFERENCE_LOCALE = "en" - REFERENCE_PLURAL_KEYS = ["one", "other"] + REFERENCE_PLURAL_KEYS = %w[one other] # Some languages should always use %{count} in pluralized strings. # https://meta.discourse.org/t/always-use-count-variable-when-translating-pluralized-strings/83969 - FORCE_PLURAL_COUNT_LOCALES = ["bs", "fr", "lt", "lv", "ru", "sl", "sr", "uk"] + FORCE_PLURAL_COUNT_LOCALES = %w[bs fr lt lv ru sl sr uk] def locale_files YML_DIRS.map { |dir| Dir["#{Rails.root}/#{dir}/{client,server}.#{@locale}.yml"] }.flatten @@ -92,8 +93,17 @@ class LocaleFileChecker missing_keys.delete("count") end - add_error(keys, TYPE_MISSING_INTERPOLATION_KEYS, missing_keys, pluralized: pluralized) unless missing_keys.empty? - add_error(keys, TYPE_UNSUPPORTED_INTERPOLATION_KEYS, unsupported_keys, pluralized: pluralized) unless unsupported_keys.empty? + unless missing_keys.empty? + add_error(keys, TYPE_MISSING_INTERPOLATION_KEYS, missing_keys, pluralized: pluralized) + end + unless unsupported_keys.empty? + add_error( + keys, + TYPE_UNSUPPORTED_INTERPOLATION_KEYS, + unsupported_keys, + pluralized: pluralized, + ) + end end end @@ -123,12 +133,15 @@ class LocaleFileChecker actual_plural_keys = parent.is_a?(Hash) ? parent.keys : [] missing_plural_keys = expected_plural_keys - actual_plural_keys - add_error(keys, TYPE_MISSING_PLURAL_KEYS, missing_plural_keys, pluralized: true) unless missing_plural_keys.empty? + unless missing_plural_keys.empty? + add_error(keys, TYPE_MISSING_PLURAL_KEYS, missing_plural_keys, pluralized: true) + end end end def check_message_format - mf_locale, mf_filename = JsLocaleHelper.find_message_format_locale([@locale], fallback_to_english: true) + mf_locale, mf_filename = + JsLocaleHelper.find_message_format_locale([@locale], fallback_to_english: true) traverse_hash(@locale_yaml, []) do |keys, value| next unless keys.last.ends_with?("_MF") @@ -158,17 +171,18 @@ class LocaleFileChecker end def reference_value_pluralized?(value) - value.is_a?(Hash) && - value.keys.sort == REFERENCE_PLURAL_KEYS && + value.is_a?(Hash) && value.keys.sort == REFERENCE_PLURAL_KEYS && value.keys.all? { |k| value[k].is_a?(String) } end def plural_keys - @plural_keys ||= begin - eval(File.read("#{Rails.root}/#{PLURALS_FILE}")).map do |locale, value| # rubocop:disable Security/Eval - [locale.to_s, value[:i18n][:plural][:keys].map(&:to_s)] - end.to_h - end + @plural_keys ||= + begin + # rubocop:disable Security/Eval + eval(File.read("#{Rails.root}/#{PLURALS_FILE}")) + .map { |locale, value| [locale.to_s, value[:i18n][:plural][:keys].map(&:to_s)] } + .to_h + end end def add_error(keys, type, details, pluralized:) @@ -180,10 +194,6 @@ class LocaleFileChecker joined_key = keys[1..-1].join(".") end - @errors[@relative_locale_path] << { - key: joined_key, - type: type, - details: details.to_s - } + @errors[@relative_locale_path] << { key: joined_key, type: type, details: details.to_s } end end diff --git a/lib/i18n/locale_file_walker.rb b/lib/i18n/locale_file_walker.rb index facc7235c02..94c27dc3368 100644 --- a/lib/i18n/locale_file_walker.rb +++ b/lib/i18n/locale_file_walker.rb @@ -22,7 +22,11 @@ class LocaleFileWalker def handle_node(node, depth, parents, consecutive_scalars) if node_is_scalar = node.is_a?(Psych::Nodes::Scalar) - valid_scalar?(depth, consecutive_scalars) ? handle_scalar(node, depth, parents) : handle_value(node.value, parents) + if valid_scalar?(depth, consecutive_scalars) + handle_scalar(node, depth, parents) + else + handle_value(node.value, parents) + end elsif node.is_a?(Psych::Nodes::Alias) handle_alias(node, depth, parents) elsif node.is_a?(Psych::Nodes::Mapping) diff --git a/lib/image_sizer.rb b/lib/image_sizer.rb index ddb86396d58..29118b39402 100644 --- a/lib/image_sizer.rb +++ b/lib/image_sizer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module ImageSizer - # Resize an image to the aspect ratio we want def self.resize(width, height, opts = {}) return if width.blank? || height.blank? @@ -12,7 +11,7 @@ module ImageSizer w = width.to_f h = height.to_f - return [w.floor, h.floor] if w <= max_width && h <= max_height + return w.floor, h.floor if w <= max_width && h <= max_height ratio = [max_width / w, max_height / h].min [(w * ratio).floor, (h * ratio).floor] @@ -27,11 +26,10 @@ module ImageSizer w = width.to_f h = height.to_f - return [w.floor, h.floor] if w <= max_width && h <= max_height + return w.floor, h.floor if w <= max_width && h <= max_height ratio = max_width / w [[max_width, w].min.floor, [max_height, (h * ratio)].min.floor] end - end diff --git a/lib/imap/providers/detector.rb b/lib/imap/providers/detector.rb index 41f356517e3..7ad50c4ea28 100644 --- a/lib/imap/providers/detector.rb +++ b/lib/imap/providers/detector.rb @@ -4,7 +4,7 @@ module Imap module Providers class Detector def self.init_with_detected_provider(config) - if config[:server] == 'imap.gmail.com' + if config[:server] == "imap.gmail.com" return Imap::Providers::Gmail.new(config[:server], config) end Imap::Providers::Generic.new(config[:server], config) diff --git a/lib/imap/providers/generic.rb b/lib/imap/providers/generic.rb index 53ec57459d0..59f43d35f2d 100644 --- a/lib/imap/providers/generic.rb +++ b/lib/imap/providers/generic.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true -require 'net/imap' +require "net/imap" module Imap module Providers - class WriteDisabledError < StandardError; end + class WriteDisabledError < StandardError + end class TrashedMailResponse attr_accessor :trashed_emails, :trash_uid_validity @@ -50,12 +51,16 @@ module Imap end def disconnect! - imap.logout rescue nil + begin + imap.logout + rescue StandardError + nil + end imap.disconnect end def can?(capability) - @capabilities ||= imap.responses['CAPABILITY'][-1] || imap.capability + @capabilities ||= imap.responses["CAPABILITY"][-1] || imap.capability @capabilities.include?(capability) end @@ -67,22 +72,23 @@ module Imap elsif opts[:to] imap.uid_search("UID 1:#{opts[:to]}") else - imap.uid_search('ALL') + imap.uid_search("ALL") end end def labels - @labels ||= begin - labels = {} + @labels ||= + begin + labels = {} - list_mailboxes.each do |name| - if tag = to_tag(name) - labels[tag] = name + list_mailboxes.each do |name| + if tag = to_tag(name) + labels[tag] = name + end end - end - labels - end + labels + end end def open_mailbox(mailbox_name, write: false) @@ -98,9 +104,7 @@ module Imap @open_mailbox_name = mailbox_name @open_mailbox_write = write - { - uid_validity: imap.responses['UIDVALIDITY'][-1] - } + { uid_validity: imap.responses["UIDVALIDITY"][-1] } end def emails(uids, fields, opts = {}) @@ -114,9 +118,7 @@ module Imap fetched.map do |email| attributes = {} - fields.each do |field| - attributes[field] = email.attr[field] - end + fields.each { |field| attributes[field] = email.attr[field] } attributes end @@ -131,11 +133,11 @@ module Imap def to_tag(label) label = DiscourseTagging.clean_tag(label.to_s) - label if label != 'inbox' && label != 'sent' + label if label != "inbox" && label != "sent" end def tag_to_flag(tag) - :Seen if tag == 'seen' + :Seen if tag == "seen" end def tag_to_label(tag) @@ -150,24 +152,25 @@ module Imap def list_mailboxes_with_attributes(attr_filter = nil) # Basically, list all mailboxes in the root of the server. # ref: https://tools.ietf.org/html/rfc3501#section-6.3.8 - imap.list('', '*').reject do |m| - - # Noselect cannot be selected with the SELECT command. - # technically we could use this for readonly mode when - # SiteSetting.imap_write is disabled...maybe a later TODO - # ref: https://tools.ietf.org/html/rfc3501#section-7.2.2 - m.attr.include?(:Noselect) - end.select do |m| - - # There are Special-Use mailboxes denoted by an attribute. For - # example, some common ones are \Trash or \Sent. - # ref: https://tools.ietf.org/html/rfc6154 - if attr_filter - m.attr.include? attr_filter - else - true + imap + .list("", "*") + .reject do |m| + # Noselect cannot be selected with the SELECT command. + # technically we could use this for readonly mode when + # SiteSetting.imap_write is disabled...maybe a later TODO + # ref: https://tools.ietf.org/html/rfc3501#section-7.2.2 + m.attr.include?(:Noselect) + end + .select do |m| + # There are Special-Use mailboxes denoted by an attribute. For + # example, some common ones are \Trash or \Sent. + # ref: https://tools.ietf.org/html/rfc6154 + if attr_filter + m.attr.include? attr_filter + else + true + end end - end end def filter_mailboxes(mailboxes) @@ -186,16 +189,20 @@ module Imap # Look for the special Trash XLIST attribute. def trash_mailbox - Discourse.cache.fetch("imap_trash_mailbox_#{account_digest}", expires_in: 30.minutes) do - list_mailboxes(:Trash).first - end + Discourse + .cache + .fetch("imap_trash_mailbox_#{account_digest}", expires_in: 30.minutes) do + list_mailboxes(:Trash).first + end end # Look for the special Junk XLIST attribute. def spam_mailbox - Discourse.cache.fetch("imap_spam_mailbox_#{account_digest}", expires_in: 30.minutes) do - list_mailboxes(:Junk).first - end + Discourse + .cache + .fetch("imap_spam_mailbox_#{account_digest}", expires_in: 30.minutes) do + list_mailboxes(:Junk).first + end end # open the trash mailbox for inspection or writing. after the yield we @@ -232,14 +239,19 @@ module Imap def find_trashed_by_message_ids(message_ids) trashed_emails = [] - trash_uid_validity = open_trash_mailbox do - trashed_email_uids = find_uids_by_message_ids(message_ids) - if trashed_email_uids.any? - trashed_emails = emails(trashed_email_uids, ["UID", "ENVELOPE"]).map do |e| - BasicMail.new(message_id: Email::MessageIdService.message_id_clean(e['ENVELOPE'].message_id), uid: e['UID']) + trash_uid_validity = + open_trash_mailbox do + trashed_email_uids = find_uids_by_message_ids(message_ids) + if trashed_email_uids.any? + trashed_emails = + emails(trashed_email_uids, %w[UID ENVELOPE]).map do |e| + BasicMail.new( + message_id: Email::MessageIdService.message_id_clean(e["ENVELOPE"].message_id), + uid: e["UID"], + ) + end end end - end TrashedMailResponse.new.tap do |resp| resp.trashed_emails = trashed_emails @@ -249,14 +261,19 @@ module Imap def find_spam_by_message_ids(message_ids) spam_emails = [] - spam_uid_validity = open_spam_mailbox do - spam_email_uids = find_uids_by_message_ids(message_ids) - if spam_email_uids.any? - spam_emails = emails(spam_email_uids, ["UID", "ENVELOPE"]).map do |e| - BasicMail.new(message_id: Email::MessageIdService.message_id_clean(e['ENVELOPE'].message_id), uid: e['UID']) + spam_uid_validity = + open_spam_mailbox do + spam_email_uids = find_uids_by_message_ids(message_ids) + if spam_email_uids.any? + spam_emails = + emails(spam_email_uids, %w[UID ENVELOPE]).map do |e| + BasicMail.new( + message_id: Email::MessageIdService.message_id_clean(e["ENVELOPE"].message_id), + uid: e["UID"], + ) + end end end - end SpamMailResponse.new.tap do |resp| resp.spam_emails = spam_emails @@ -265,13 +282,14 @@ module Imap end def find_uids_by_message_ids(message_ids) - header_message_id_terms = message_ids.map do |msgid| - "HEADER Message-ID '#{Email::MessageIdService.message_id_rfc_format(msgid)}'" - end + header_message_id_terms = + message_ids.map do |msgid| + "HEADER Message-ID '#{Email::MessageIdService.message_id_rfc_format(msgid)}'" + end # OR clauses are written in Polish notation...so the query looks like this: # OR OR HEADER Message-ID XXXX HEADER Message-ID XXXX HEADER Message-ID XXXX - or_clauses = 'OR ' * (header_message_id_terms.length - 1) + or_clauses = "OR " * (header_message_id_terms.length - 1) query = "#{or_clauses}#{header_message_id_terms.join(" ")}" imap.uid_search(query) @@ -280,17 +298,16 @@ module Imap def trash(uid) # MOVE is way easier than doing the COPY \Deleted EXPUNGE dance ourselves. # It is supported by Gmail and Outlook. - if can?('MOVE') + if can?("MOVE") trash_move(uid) else - # default behaviour for IMAP servers is to add the \Deleted flag # then EXPUNGE the mailbox which permanently deletes these messages # https://tools.ietf.org/html/rfc3501#section-6.4.3 # # TODO: We may want to add the option at some point to copy to some # other mailbox first before doing this (e.g. Trash) - store(uid, 'FLAGS', [], ["\\Deleted"]) + store(uid, "FLAGS", [], ["\\Deleted"]) imap.expunge end end diff --git a/lib/imap/providers/gmail.rb b/lib/imap/providers/gmail.rb index 7ac51f73042..fc888d66eda 100644 --- a/lib/imap/providers/gmail.rb +++ b/lib/imap/providers/gmail.rb @@ -8,61 +8,58 @@ module Imap # all UIDs in a thread must have the \\Inbox label removed. # class Gmail < Generic - X_GM_LABELS = 'X-GM-LABELS' - X_GM_THRID = 'X-GM-THRID' + X_GM_LABELS = "X-GM-LABELS" + X_GM_THRID = "X-GM-THRID" def imap @imap ||= super.tap { |imap| apply_gmail_patch(imap) } end def emails(uids, fields, opts = {}) - # gmail has a special header for labels - if fields.include?('LABELS') - fields[fields.index('LABELS')] = X_GM_LABELS - end + fields[fields.index("LABELS")] = X_GM_LABELS if fields.include?("LABELS") emails = super(uids, fields, opts) emails.each do |email| - email['LABELS'] = Array(email['LABELS']) + email["LABELS"] = Array(email["LABELS"]) if email[X_GM_LABELS] - email['LABELS'] << Array(email.delete(X_GM_LABELS)) - email['LABELS'].flatten! + email["LABELS"] << Array(email.delete(X_GM_LABELS)) + email["LABELS"].flatten! end - email['LABELS'] << '\\Inbox' if @open_mailbox_name == 'INBOX' + email["LABELS"] << '\\Inbox' if @open_mailbox_name == "INBOX" - email['LABELS'].uniq! + email["LABELS"].uniq! end emails end def store(uid, attribute, old_set, new_set) - attribute = X_GM_LABELS if attribute == 'LABELS' + attribute = X_GM_LABELS if attribute == "LABELS" super(uid, attribute, old_set, new_set) end def to_tag(label) # Label `\\Starred` is Gmail equivalent of :Flagged (both present) - return 'starred' if label == :Flagged - return if label == '[Gmail]/All Mail' + return "starred" if label == :Flagged + return if label == "[Gmail]/All Mail" - label = label.to_s.gsub('[Gmail]/', '') + label = label.to_s.gsub("[Gmail]/", "") super(label) end def tag_to_flag(tag) - return :Flagged if tag == 'starred' + return :Flagged if tag == "starred" super(tag) end def tag_to_label(tag) - return '\\Important' if tag == 'important' - return '\\Starred' if tag == 'starred' + return '\\Important' if tag == "important" + return '\\Starred' if tag == "starred" super(tag) end @@ -73,11 +70,14 @@ module Imap thread_id = thread_id_from_uid(uid) emails_to_archive = emails_in_thread(thread_id) emails_to_archive.each do |email| - labels = email['LABELS'] + labels = email["LABELS"] new_labels = labels.reject { |l| l == "\\Inbox" } store(email["UID"], "LABELS", labels, new_labels) end - ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) archived in Gmail mailbox for #{@username}", :debug) + ImapSyncLog.log( + "Thread ID #{thread_id} (UID #{uid}) archived in Gmail mailbox for #{@username}", + :debug, + ) end # Though Gmail considers the email thread unarchived if the first email @@ -87,36 +87,38 @@ module Imap thread_id = thread_id_from_uid(uid) emails_to_unarchive = emails_in_thread(thread_id) emails_to_unarchive.each do |email| - labels = email['LABELS'] + labels = email["LABELS"] new_labels = labels.dup - if !new_labels.include?("\\Inbox") - new_labels << "\\Inbox" - end + new_labels << "\\Inbox" if !new_labels.include?("\\Inbox") store(email["UID"], "LABELS", labels, new_labels) end - ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) unarchived in Gmail mailbox for #{@username}", :debug) + ImapSyncLog.log( + "Thread ID #{thread_id} (UID #{uid}) unarchived in Gmail mailbox for #{@username}", + :debug, + ) end def thread_id_from_uid(uid) fetched = imap.uid_fetch(uid, [X_GM_THRID]) - if !fetched - raise "Thread not found for UID #{uid}!" - end + raise "Thread not found for UID #{uid}!" if !fetched fetched.last.attr[X_GM_THRID] end def emails_in_thread(thread_id) uids_to_fetch = imap.uid_search("#{X_GM_THRID} #{thread_id}") - emails(uids_to_fetch, ["UID", "LABELS"]) + emails(uids_to_fetch, %w[UID LABELS]) end def trash_move(uid) thread_id = thread_id_from_uid(uid) - email_uids_to_trash = emails_in_thread(thread_id).map { |e| e['UID'] } + email_uids_to_trash = emails_in_thread(thread_id).map { |e| e["UID"] } imap.uid_move(email_uids_to_trash, trash_mailbox) - ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) trashed in Gmail mailbox for #{@username}", :debug) + ImapSyncLog.log( + "Thread ID #{thread_id} (UID #{uid}) trashed in Gmail mailbox for #{@username}", + :debug, + ) { trash_uid_validity: open_trash_mailbox, email_uids_to_trash: email_uids_to_trash } end @@ -124,16 +126,15 @@ module Imap # used for the dropdown in the UI where we allow the user to select the # IMAP mailbox to sync with. def filter_mailboxes(mailboxes_with_attributes) - mailboxes_with_attributes.reject do |mb| - (mb.attr & [:Drafts, :Sent, :Junk, :Flagged, :Trash]).any? - end.map(&:name) + mailboxes_with_attributes + .reject { |mb| (mb.attr & %i[Drafts Sent Junk Flagged Trash]).any? } + .map(&:name) end private def apply_gmail_patch(imap) - class << imap.instance_variable_get('@parser') - + class << imap.instance_variable_get("@parser") # Modified version of the original `msg_att` from here: # https://github.com/ruby/ruby/blob/1cc8ff001da217d0e98d13fe61fbc9f5547ef722/lib/net/imap.rb#L2346 # @@ -172,15 +173,14 @@ module Imap when /\A(?:MODSEQ)\z/ni name, val = modseq_data - # Adding support for GMail extended attributes. + # Adding support for GMail extended attributes. when /\A(?:X-GM-LABELS)\z/ni name, val = label_data when /\A(?:X-GM-MSGID)\z/ni name, val = uid_data when /\A(?:X-GM-THRID)\z/ni name, val = uid_data - # End custom support for Gmail. - + # End custom support for Gmail. else parse_error("unknown attribute `%s' for {%d}", token.value, n) end diff --git a/lib/imap/sync.rb b/lib/imap/sync.rb index be5b32fb0d4..0d9220131ea 100644 --- a/lib/imap/sync.rb +++ b/lib/imap/sync.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'net/imap' +require "net/imap" module Imap class Sync @@ -23,13 +23,13 @@ module Imap end def can_idle? - SiteSetting.enable_imap_idle && @provider.can?('IDLE') + SiteSetting.enable_imap_idle && @provider.can?("IDLE") end def process(idle: false, import_limit: nil, old_emails_limit: nil, new_emails_limit: nil) - raise 'disconnected' if disconnected? + raise "disconnected" if disconnected? - import_limit ||= SiteSetting.imap_batch_import_email + import_limit ||= SiteSetting.imap_batch_import_email old_emails_limit ||= SiteSetting.imap_polling_old_emails new_emails_limit ||= SiteSetting.imap_polling_new_emails @@ -43,30 +43,42 @@ module Imap # If UID validity changes, the whole mailbox must be synchronized (all # emails are considered new and will be associated to existent topics # in Email::Receiver by matching Message-Ids). - ImapSyncLog.warn("UIDVALIDITY = #{@status[:uid_validity]} does not match expected #{@group.imap_uid_validity}, invalidating IMAP cache and resyncing emails for mailbox #{@group.imap_mailbox_name}", @group) + ImapSyncLog.warn( + "UIDVALIDITY = #{@status[:uid_validity]} does not match expected #{@group.imap_uid_validity}, invalidating IMAP cache and resyncing emails for mailbox #{@group.imap_mailbox_name}", + @group, + ) @group.imap_last_uid = 0 end if idle && !can_idle? - ImapSyncLog.warn("IMAP server for group cannot IDLE or imap idle site setting is disabled", @group) + ImapSyncLog.warn( + "IMAP server for group cannot IDLE or imap idle site setting is disabled", + @group, + ) idle = false end if idle - raise 'IMAP IDLE is disabled' if !SiteSetting.enable_imap_idle + raise "IMAP IDLE is disabled" if !SiteSetting.enable_imap_idle # Thread goes into sleep and it is better to return any connection # back to the pool. ActiveRecord::Base.connection_handler.clear_active_connections! idle_polling_mins = SiteSetting.imap_polling_period_mins.minutes.to_i - ImapSyncLog.debug("Going IDLE for #{idle_polling_mins} seconds to wait for more work", @group, db: false) + ImapSyncLog.debug( + "Going IDLE for #{idle_polling_mins} seconds to wait for more work", + @group, + db: false, + ) - @provider.imap.idle(idle_polling_mins) do |resp| - if resp.kind_of?(Net::IMAP::UntaggedResponse) && resp.name == 'EXISTS' - @provider.imap.idle_done + @provider + .imap + .idle(idle_polling_mins) do |resp| + if resp.kind_of?(Net::IMAP::UntaggedResponse) && resp.name == "EXISTS" + @provider.imap.idle_done + end end - end end # Fetching UIDs of old (already imported into Discourse, but might need @@ -82,7 +94,10 @@ module Imap # Sometimes, new_uids contains elements from old_uids. new_uids = new_uids - old_uids - ImapSyncLog.debug("Remote email server has #{old_uids.size} old emails and #{new_uids.size} new emails", @group) + ImapSyncLog.debug( + "Remote email server has #{old_uids.size} old emails and #{new_uids.size} new emails", + @group, + ) all_old_uids_size = old_uids.size all_new_uids_size = new_uids.size @@ -90,7 +105,7 @@ module Imap @group.update_columns( imap_last_error: nil, imap_old_emails: all_old_uids_size, - imap_new_emails: all_new_uids_size + imap_new_emails: all_new_uids_size, ) import_mode = import_limit > -1 && new_uids.size > import_limit @@ -112,10 +127,10 @@ module Imap end def update_topic(email, incoming_email, opts = {}) - return if !incoming_email || - incoming_email.imap_sync || - !incoming_email.topic || - incoming_email.post&.post_number != 1 + if !incoming_email || incoming_email.imap_sync || !incoming_email.topic || + incoming_email.post&.post_number != 1 + return + end update_topic_archived_state(email, incoming_email, opts) update_topic_tags(email, incoming_email, opts) @@ -125,33 +140,41 @@ module Imap def process_old_uids(old_uids) ImapSyncLog.debug("Syncing #{old_uids.size} randomly-selected old emails", @group) - emails = old_uids.empty? ? [] : @provider.emails(old_uids, ['UID', 'FLAGS', 'LABELS', 'ENVELOPE']) + emails = old_uids.empty? ? [] : @provider.emails(old_uids, %w[UID FLAGS LABELS ENVELOPE]) emails.each do |email| - incoming_email = IncomingEmail.find_by( - imap_uid_validity: @status[:uid_validity], - imap_uid: email['UID'], - imap_group_id: @group.id - ) + incoming_email = + IncomingEmail.find_by( + imap_uid_validity: @status[:uid_validity], + imap_uid: email["UID"], + imap_group_id: @group.id, + ) if incoming_email.present? update_topic(email, incoming_email, mailbox_name: @group.imap_mailbox_name) else # try finding email by message-id instead, we may be able to set the uid etc. - incoming_email = IncomingEmail.where( - message_id: Email::MessageIdService.message_id_clean(email['ENVELOPE'].message_id), - imap_uid: nil, - imap_uid_validity: nil - ).where("to_addresses LIKE ?", "%#{@group.email_username}%").first + incoming_email = + IncomingEmail + .where( + message_id: Email::MessageIdService.message_id_clean(email["ENVELOPE"].message_id), + imap_uid: nil, + imap_uid_validity: nil, + ) + .where("to_addresses LIKE ?", "%#{@group.email_username}%") + .first if incoming_email incoming_email.update( imap_uid_validity: @status[:uid_validity], - imap_uid: email['UID'], - imap_group_id: @group.id + imap_uid: email["UID"], + imap_group_id: @group.id, ) update_topic(email, incoming_email, mailbox_name: @group.imap_mailbox_name) else - ImapSyncLog.warn("Could not find old email (UIDVALIDITY = #{@status[:uid_validity]}, UID = #{email['UID']})", @group) + ImapSyncLog.warn( + "Could not find old email (UIDVALIDITY = #{@status[:uid_validity]}, UID = #{email["UID"]})", + @group, + ) end end end @@ -165,15 +188,18 @@ module Imap # if they have been deleted and if so delete the associated post/topic. then the remaining we # can just remove the imap details from the IncomingEmail table and if they end up back in the # original mailbox then they will be picked up in a future resync. - existing_incoming = IncomingEmail.includes(:post).where( - imap_group_id: @group.id, imap_uid_validity: @status[:uid_validity] - ).where.not(imap_uid: nil) + existing_incoming = + IncomingEmail + .includes(:post) + .where(imap_group_id: @group.id, imap_uid_validity: @status[:uid_validity]) + .where.not(imap_uid: nil) existing_uids = existing_incoming.map(&:imap_uid) missing_uids = existing_uids - old_uids - missing_message_ids = existing_incoming.select do |incoming| - missing_uids.include?(incoming.imap_uid) - end.map(&:message_id) + missing_message_ids = + existing_incoming + .select { |incoming| missing_uids.include?(incoming.imap_uid) } + .map(&:message_id) return if missing_message_ids.empty? @@ -183,7 +209,8 @@ module Imap potential_spam = [] response = @provider.find_trashed_by_message_ids(missing_message_ids) existing_incoming.each do |incoming| - matching_trashed = response.trashed_emails.find { |email| email.message_id == incoming.message_id } + matching_trashed = + response.trashed_emails.find { |email| email.message_id == incoming.message_id } if !matching_trashed potential_spam << incoming @@ -194,13 +221,22 @@ module Imap # not exist, and this sync is just updating the old UIDs to the new ones # in the trash, and we don't need to re-destroy the post if incoming.post - ImapSyncLog.debug("Deleting post ID #{incoming.post_id}, topic id #{incoming.topic_id}; email has been deleted on the IMAP server.", @group) + ImapSyncLog.debug( + "Deleting post ID #{incoming.post_id}, topic id #{incoming.topic_id}; email has been deleted on the IMAP server.", + @group, + ) PostDestroyer.new(Discourse.system_user, incoming.post).destroy end # the email has moved mailboxes, we don't want to try trashing again next time - ImapSyncLog.debug("Updating incoming ID #{incoming.id} uid data FROM [UID #{incoming.imap_uid} | UIDVALIDITY #{incoming.imap_uid_validity}] TO [UID #{matching_trashed.uid} | UIDVALIDITY #{response.trash_uid_validity}] (TRASHED)", @group) - incoming.update(imap_uid_validity: response.trash_uid_validity, imap_uid: matching_trashed.uid) + ImapSyncLog.debug( + "Updating incoming ID #{incoming.id} uid data FROM [UID #{incoming.imap_uid} | UIDVALIDITY #{incoming.imap_uid_validity}] TO [UID #{matching_trashed.uid} | UIDVALIDITY #{response.trash_uid_validity}] (TRASHED)", + @group, + ) + incoming.update( + imap_uid_validity: response.trash_uid_validity, + imap_uid: matching_trashed.uid, + ) end # This can be done because Message-ID is unique on a mail server between mailboxes, @@ -208,12 +244,16 @@ module Imap # the new UID from the spam. response = @provider.find_spam_by_message_ids(missing_message_ids) potential_spam.each do |incoming| - matching_spam = response.spam_emails.find { |email| email.message_id == incoming.message_id } + matching_spam = + response.spam_emails.find { |email| email.message_id == incoming.message_id } # if the email is not in the trash or spam then we don't know where it is... could # be in any mailbox on the server or could be permanently deleted. if !matching_spam - ImapSyncLog.debug("Email for incoming ID #{incoming.id} (#{incoming.message_id}) could not be found in the group mailbox, trash, or spam. It could be in another mailbox or permanently deleted.", @group) + ImapSyncLog.debug( + "Email for incoming ID #{incoming.id} (#{incoming.message_id}) could not be found in the group mailbox, trash, or spam. It could be in another mailbox or permanently deleted.", + @group, + ) incoming.update(imap_missing: true) next end @@ -222,12 +262,18 @@ module Imap # not exist, and this sync is just updating the old UIDs to the new ones # in the spam, and we don't need to re-destroy the post if incoming.post - ImapSyncLog.debug("Deleting post ID #{incoming.post_id}, topic id #{incoming.topic_id}; email has been moved to spam on the IMAP server.", @group) + ImapSyncLog.debug( + "Deleting post ID #{incoming.post_id}, topic id #{incoming.topic_id}; email has been moved to spam on the IMAP server.", + @group, + ) PostDestroyer.new(Discourse.system_user, incoming.post).destroy end # the email has moved mailboxes, we don't want to try marking as spam again next time - ImapSyncLog.debug("Updating incoming ID #{incoming.id} uid data FROM [UID #{incoming.imap_uid} | UIDVALIDITY #{incoming.imap_uid_validity}] TO [UID #{matching_spam.uid} | UIDVALIDITY #{response.spam_uid_validity}] (SPAM)", @group) + ImapSyncLog.debug( + "Updating incoming ID #{incoming.id} uid data FROM [UID #{incoming.imap_uid} | UIDVALIDITY #{incoming.imap_uid_validity}] TO [UID #{matching_spam.uid} | UIDVALIDITY #{response.spam_uid_validity}] (SPAM)", + @group, + ) incoming.update(imap_uid_validity: response.spam_uid_validity, imap_uid: matching_spam.uid) end end @@ -235,7 +281,7 @@ module Imap def process_new_uids(new_uids, import_mode, all_old_uids_size, all_new_uids_size) ImapSyncLog.debug("Syncing #{new_uids.size} new emails (oldest first)", @group) - emails = @provider.emails(new_uids, ['UID', 'FLAGS', 'LABELS', 'RFC822']) + emails = @provider.emails(new_uids, %w[UID FLAGS LABELS RFC822]) processed = 0 # TODO (maybe): We might need something here to exclusively handle @@ -247,29 +293,33 @@ module Imap # (for example replies must be processed after the original email # to have a topic where the reply can be posted). begin - receiver = Email::Receiver.new( - email['RFC822'], - allow_auto_generated: true, - import_mode: import_mode, - destinations: [@group], - imap_uid_validity: @status[:uid_validity], - imap_uid: email['UID'], - imap_group_id: @group.id, - source: :imap - ) + receiver = + Email::Receiver.new( + email["RFC822"], + allow_auto_generated: true, + import_mode: import_mode, + destinations: [@group], + imap_uid_validity: @status[:uid_validity], + imap_uid: email["UID"], + imap_group_id: @group.id, + source: :imap, + ) receiver.process! update_topic(email, receiver.incoming_email, mailbox_name: @group.imap_mailbox_name) rescue Email::Receiver::ProcessingError => e - ImapSyncLog.warn("Could not process (UIDVALIDITY = #{@status[:uid_validity]}, UID = #{email['UID']}): #{e.message}", @group) + ImapSyncLog.warn( + "Could not process (UIDVALIDITY = #{@status[:uid_validity]}, UID = #{email["UID"]}): #{e.message}", + @group, + ) end processed += 1 @group.update_columns( imap_uid_validity: @status[:uid_validity], - imap_last_uid: email['UID'], + imap_last_uid: email["UID"], imap_old_emails: all_old_uids_size + processed, - imap_new_emails: all_new_uids_size - processed + imap_new_emails: all_new_uids_size - processed, ) end end @@ -281,7 +331,10 @@ module Imap if to_sync.size > 0 @provider.open_mailbox(@group.imap_mailbox_name, write: true) to_sync.each do |incoming_email| - ImapSyncLog.debug("Updating email on IMAP server for incoming email ID = #{incoming_email.id}, UID = #{incoming_email.imap_uid}", @group) + ImapSyncLog.debug( + "Updating email on IMAP server for incoming email ID = #{incoming_email.id}, UID = #{incoming_email.imap_uid}", + @group, + ) update_email(incoming_email) incoming_email.update(imap_sync: false) end @@ -292,7 +345,7 @@ module Imap topic = incoming_email.topic topic_is_archived = topic.group_archived_messages.size > 0 - email_is_archived = !email['LABELS'].include?('\\Inbox') && !email['LABELS'].include?('INBOX') + email_is_archived = !email["LABELS"].include?('\\Inbox') && !email["LABELS"].include?("INBOX") if topic_is_archived && !email_is_archived ImapSyncLog.debug("Unarchiving topic ID #{topic.id}, email was unarchived", @group) @@ -322,10 +375,10 @@ module Imap tags.add(@provider.to_tag(opts[:mailbox_name])) if opts[:mailbox_name] # Flags and labels - email['FLAGS'].each { |flag| tags.add(@provider.to_tag(flag)) } - email['LABELS'].each { |label| tags.add(@provider.to_tag(label)) } + email["FLAGS"].each { |flag| tags.add(@provider.to_tag(flag)) } + email["LABELS"].each { |label| tags.add(@provider.to_tag(label)) } - tags.subtract([nil, '']) + tags.subtract([nil, ""]) return if !tagging_enabled? @@ -354,11 +407,11 @@ module Imap # # A) the email has been deleted/moved to a different mailbox in the provider # B) the UID does not belong to the provider - email = @provider.emails(incoming_email.imap_uid, ['FLAGS', 'LABELS']).first + email = @provider.emails(incoming_email.imap_uid, %w[FLAGS LABELS]).first return if !email.present? - labels = email['LABELS'] - flags = email['FLAGS'] + labels = email["LABELS"] + flags = email["FLAGS"] new_labels = [] new_flags = [] @@ -367,7 +420,10 @@ module Imap if !topic # no need to do anything further here, we will recognize the UIDs in the # mail server email thread have been trashed on next sync - ImapSyncLog.debug("Trashing UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", @group) + ImapSyncLog.debug( + "Trashing UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", + @group, + ) return @provider.trash(incoming_email.imap_uid) end @@ -380,12 +436,18 @@ module Imap # at the same time. new_labels << "\\Inbox" - ImapSyncLog.debug("Unarchiving UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", @group) + ImapSyncLog.debug( + "Unarchiving UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", + @group, + ) # some providers need special handling for unarchiving too @provider.unarchive(incoming_email.imap_uid) else - ImapSyncLog.debug("Archiving UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", @group) + ImapSyncLog.debug( + "Archiving UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", + @group, + ) # some providers need special handling for archiving. this way we preserve # any new tag-labels, and archive, even though it may cause extra requests @@ -397,13 +459,14 @@ module Imap if tagging_enabled? tags = topic.tags.pluck(:name) new_flags = tags.map { |tag| @provider.tag_to_flag(tag) }.reject(&:blank?) - new_labels = new_labels.concat(tags.map { |tag| @provider.tag_to_label(tag) }.reject(&:blank?)) + new_labels = + new_labels.concat(tags.map { |tag| @provider.tag_to_label(tag) }.reject(&:blank?)) end # regardless of whether the topic needs to be archived we still update # the flags and the labels - @provider.store(incoming_email.imap_uid, 'FLAGS', flags, new_flags) - @provider.store(incoming_email.imap_uid, 'LABELS', labels, new_labels) + @provider.store(incoming_email.imap_uid, "FLAGS", flags, new_flags) + @provider.store(incoming_email.imap_uid, "LABELS", labels, new_labels) end def tagging_enabled? diff --git a/lib/import/normalize.rb b/lib/import/normalize.rb index c9c6dc74998..8b9b98b8e06 100644 --- a/lib/import/normalize.rb +++ b/lib/import/normalize.rb @@ -3,13 +3,14 @@ # markdown normalizer to be used by importers # # -require 'htmlentities' -module Import; end +require "htmlentities" +module Import +end module Import::Normalize def self.normalize_code_blocks(code, lang = nil) coder = HTMLEntities.new - code.gsub(/
\s*\n?(.*?)\n?<\/code>\s*<\/pre>/m) {
+    code.gsub(%r{
\s*\n?(.*?)\n?\s*
}m) do "\n```#{lang}\n#{coder.decode($1)}\n```\n" - } + end end end diff --git a/lib/import_export.rb b/lib/import_export.rb index 13d2b5eddf3..feb4d936015 100644 --- a/lib/import_export.rb +++ b/lib/import_export.rb @@ -10,9 +10,11 @@ require "import_export/translation_overrides_exporter" require "json" module ImportExport - def self.import(filename) - data = ActiveSupport::HashWithIndifferentAccess.new(File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) }) + data = + ActiveSupport::HashWithIndifferentAccess.new( + File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) }, + ) ImportExport::Importer.new(data).perform end diff --git a/lib/import_export/base_exporter.rb b/lib/import_export/base_exporter.rb index 989b7220e4d..2effbd2d9fe 100644 --- a/lib/import_export/base_exporter.rb +++ b/lib/import_export/base_exporter.rb @@ -4,22 +4,72 @@ module ImportExport class BaseExporter attr_reader :export_data, :categories - CATEGORY_ATTRS = [:id, :name, :color, :created_at, :user_id, :slug, :description, :text_color, - :auto_close_hours, :position, :parent_category_id, :auto_close_based_on_last_post, - :topic_template, :all_topics_wiki, :permissions_params] + CATEGORY_ATTRS = %i[ + id + name + color + created_at + user_id + slug + description + text_color + auto_close_hours + position + parent_category_id + auto_close_based_on_last_post + topic_template + all_topics_wiki + permissions_params + ] - GROUP_ATTRS = [:id, :name, :created_at, :automatic_membership_email_domains, :primary_group, - :title, :grant_trust_level, :incoming_email, :bio_raw, :allow_membership_requests, - :full_name, :default_notification_level, :visibility_level, :public_exit, - :public_admission, :membership_request_template, :messageable_level, :mentionable_level, - :members_visibility_level, :publish_read_state] + GROUP_ATTRS = %i[ + id + name + created_at + automatic_membership_email_domains + primary_group + title + grant_trust_level + incoming_email + bio_raw + allow_membership_requests + full_name + default_notification_level + visibility_level + public_exit + public_admission + membership_request_template + messageable_level + mentionable_level + members_visibility_level + publish_read_state + ] - USER_ATTRS = [:id, :email, :username, :name, :created_at, :trust_level, :active, :last_emailed_at, :custom_fields] + USER_ATTRS = %i[ + id + email + username + name + created_at + trust_level + active + last_emailed_at + custom_fields + ] - TOPIC_ATTRS = [:id, :title, :created_at, :views, :category_id, :closed, :archived, :archetype] + TOPIC_ATTRS = %i[id title created_at views category_id closed archived archetype] - POST_ATTRS = [:id, :user_id, :post_number, :raw, :created_at, :reply_to_post_number, :hidden, - :hidden_reason_id, :wiki] + POST_ATTRS = %i[ + id + user_id + post_number + raw + created_at + reply_to_post_number + hidden + hidden_reason_id + wiki + ] def categories @categories ||= Category.all.to_a @@ -29,7 +79,10 @@ module ImportExport data = [] categories.each do |cat| - data << CATEGORY_ATTRS.inject({}) { |h, a| h[a] = cat.public_send(a); h } + data << CATEGORY_ATTRS.inject({}) do |h, a| + h[a] = cat.public_send(a) + h + end end data @@ -47,7 +100,11 @@ module ImportExport groups = groups.where(name: group_names) if group_names.present? groups.find_each do |group| - attrs = GROUP_ATTRS.inject({}) { |h, a| h[a] = group.public_send(a); h } + attrs = + GROUP_ATTRS.inject({}) do |h, a| + h[a] = group.public_send(a) + h + end attrs[:user_ids] = group.users.pluck(:id) data << attrs end @@ -87,9 +144,7 @@ module ImportExport def export_group_users user_ids = [] - @export_data[:groups].each do |g| - user_ids += g[:user_ids] - end + @export_data[:groups].each { |g| user_ids += g[:user_ids] } user_ids.uniq! return User.none if user_ids.empty? @@ -110,22 +165,24 @@ module ImportExport @topics.each do |topic| puts topic.title - topic_data = TOPIC_ATTRS.inject({}) do |h, a| - h[a] = topic.public_send(a) - h - end + topic_data = + TOPIC_ATTRS.inject({}) do |h, a| + h[a] = topic.public_send(a) + h + end topic_data[:posts] = [] topic.ordered_posts.find_each do |post| - attributes = POST_ATTRS.inject({}) do |h, a| - h[a] = post.public_send(a) - h - end + attributes = + POST_ATTRS.inject({}) do |h, a| + h[a] = post.public_send(a) + h + end attributes[:raw] = attributes[:raw].gsub( 'src="/uploads', - "src=\"#{Discourse.base_url_no_prefix}/uploads" + "src=\"#{Discourse.base_url_no_prefix}/uploads", ) topic_data[:posts] << attributes @@ -147,7 +204,7 @@ module ImportExport return if @export_data[:topics].blank? topic_ids = @export_data[:topics].pluck(:id) - users = User.joins(:posts).where('posts.topic_id IN (?)', topic_ids).distinct + users = User.joins(:posts).where("posts.topic_id IN (?)", topic_ids).distinct export_users(users) end @@ -164,14 +221,17 @@ module ImportExport users.find_each do |u| next if u.id == Discourse::SYSTEM_USER_ID - x = USER_ATTRS.inject({}) do |h, a| - h[a] = u.public_send(a) - h - end + x = + USER_ATTRS.inject({}) do |h, a| + h[a] = u.public_send(a) + h + end - x.merge(bio_raw: u.user_profile.bio_raw, - website: u.user_profile.website, - location: u.user_profile.location) + x.merge( + bio_raw: u.user_profile.bio_raw, + website: u.user_profile.website, + location: u.user_profile.location, + ) data << x end @@ -179,7 +239,11 @@ module ImportExport end def export_translation_overrides - @export_data[:translation_overrides] = TranslationOverride.all.select(:locale, :translation_key, :value) + @export_data[:translation_overrides] = TranslationOverride.all.select( + :locale, + :translation_key, + :value, + ) self end @@ -189,13 +253,12 @@ module ImportExport end def save_to_file(filename = nil) - output_basename = filename || File.join("#{default_filename_prefix}-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json") - File.open(output_basename, "w:UTF-8") do |f| - f.write(@export_data.to_json) - end + output_basename = + filename || + File.join("#{default_filename_prefix}-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json") + File.open(output_basename, "w:UTF-8") { |f| f.write(@export_data.to_json) } puts "Export saved to #{output_basename}" output_basename end - end end diff --git a/lib/import_export/category_exporter.rb b/lib/import_export/category_exporter.rb index 85f857a6aa5..843c417d5e3 100644 --- a/lib/import_export/category_exporter.rb +++ b/lib/import_export/category_exporter.rb @@ -5,15 +5,10 @@ require "import_export/topic_exporter" module ImportExport class CategoryExporter < BaseExporter - def initialize(category_ids) - @categories = Category.where(id: category_ids).or(Category.where(parent_category_id: category_ids)).to_a - @export_data = { - categories: [], - groups: [], - topics: [], - users: [] - } + @categories = + Category.where(id: category_ids).or(Category.where(parent_category_id: category_ids)).to_a + @export_data = { categories: [], groups: [], topics: [], users: [] } end def perform @@ -26,9 +21,12 @@ module ImportExport def export_topics_and_users all_category_ids = @categories.pluck(:id) description_topic_ids = @categories.pluck(:topic_id) - topic_exporter = ImportExport::TopicExporter.new(Topic.where(category_id: all_category_ids).pluck(:id) - description_topic_ids) + topic_exporter = + ImportExport::TopicExporter.new( + Topic.where(category_id: all_category_ids).pluck(:id) - description_topic_ids, + ) topic_exporter.perform - @export_data[:users] = topic_exporter.export_data[:users] + @export_data[:users] = topic_exporter.export_data[:users] @export_data[:topics] = topic_exporter.export_data[:topics] self end @@ -36,6 +34,5 @@ module ImportExport def default_filename_prefix "category-export" end - end end diff --git a/lib/import_export/category_structure_exporter.rb b/lib/import_export/category_structure_exporter.rb index ce33de39d39..edaf89b3793 100644 --- a/lib/import_export/category_structure_exporter.rb +++ b/lib/import_export/category_structure_exporter.rb @@ -2,14 +2,10 @@ module ImportExport class CategoryStructureExporter < BaseExporter - def initialize(include_group_users = false) @include_group_users = include_group_users - @export_data = { - groups: [], - categories: [] - } + @export_data = { groups: [], categories: [] } @export_data[:users] = [] if @include_group_users end @@ -25,6 +21,5 @@ module ImportExport def default_filename_prefix "category-structure-export" end - end end diff --git a/lib/import_export/group_exporter.rb b/lib/import_export/group_exporter.rb index 893320cfbbb..a0ed870641d 100644 --- a/lib/import_export/group_exporter.rb +++ b/lib/import_export/group_exporter.rb @@ -2,13 +2,10 @@ module ImportExport class GroupExporter < BaseExporter - def initialize(include_group_users = false) @include_group_users = include_group_users - @export_data = { - groups: [] - } + @export_data = { groups: [] } @export_data[:users] = [] if @include_group_users end @@ -23,6 +20,5 @@ module ImportExport def default_filename_prefix "groups-export" end - end end diff --git a/lib/import_export/importer.rb b/lib/import_export/importer.rb index c9d03030a15..1498831e46e 100644 --- a/lib/import_export/importer.rb +++ b/lib/import_export/importer.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require File.join(Rails.root, 'script', 'import_scripts', 'base.rb') +require File.join(Rails.root, "script", "import_scripts", "base.rb") module ImportExport class Importer < ImportScripts::Base - def initialize(data) @users = data[:users] @groups = data[:groups] @@ -66,7 +65,11 @@ module ImportExport external_id = g.delete(:id) new_group = Group.find_by_name(g[:name]) || Group.create!(g) user_ids.each do |external_user_id| - new_group.add(User.find(new_user_id(external_user_id))) rescue ActiveRecord::RecordNotUnique + begin + new_group.add(User.find(new_user_id(external_user_id))) + rescue StandardError + ActiveRecord::RecordNotUnique + end end end @@ -79,7 +82,11 @@ module ImportExport puts "Importing categories..." import_ids = @categories.collect { |c| "#{c[:id]}#{import_source}" } - existing_categories = CategoryCustomField.where("name = 'import_id' AND value IN (?)", import_ids).select(:category_id, :value).to_a + existing_categories = + CategoryCustomField + .where("name = 'import_id' AND value IN (?)", import_ids) + .select(:category_id, :value) + .to_a existing_category_ids = existing_categories.pluck(:value) levels = category_levels @@ -100,7 +107,10 @@ module ImportExport permissions = cat_attrs.delete(:permissions_params) category = Category.new(cat_attrs) - category.parent_category_id = new_category_id(cat_attrs[:parent_category_id]) if cat_attrs[:parent_category_id].present? + category.parent_category_id = + new_category_id(cat_attrs[:parent_category_id]) if cat_attrs[ + :parent_category_id + ].present? category.user_id = new_user_id(cat_attrs[:user_id]) import_id = "#{id}#{import_source}" category.custom_fields["import_id"] = import_id @@ -126,13 +136,14 @@ module ImportExport def import_topics return if @topics.blank? - puts "Importing topics...", '' + puts "Importing topics...", "" @topics.each do |t| puts "" print t[:title] - first_post_attrs = t[:posts].first.merge(t.slice(*(TopicExporter::TOPIC_ATTRS - [:id, :category_id]))) + first_post_attrs = + t[:posts].first.merge(t.slice(*(TopicExporter::TOPIC_ATTRS - %i[id category_id]))) first_post_attrs[:user_id] = new_user_id(first_post_attrs[:user_id]) first_post_attrs[:category] = new_category_id(t[:category_id]) @@ -140,9 +151,7 @@ module ImportExport import_id = "#{first_post_attrs[:id]}#{import_source}" first_post = PostCustomField.where(name: "import_id", value: import_id).first&.post - unless first_post - first_post = create_post(first_post_attrs, import_id) - end + first_post = create_post(first_post_attrs, import_id) unless first_post topic_id = first_post.topic_id @@ -154,11 +163,8 @@ module ImportExport unless existing # see ImportScripts::Base create_post( - post_data.merge( - topic_id: topic_id, - user_id: new_user_id(post_data[:user_id]) - ), - post_import_id + post_data.merge(topic_id: topic_id, user_id: new_user_id(post_data[:user_id])), + post_import_id, ) end end @@ -182,51 +188,53 @@ module ImportExport end def new_user_id(external_user_id) - ucf = UserCustomField.where(name: "import_id", value: "#{external_user_id}#{import_source}").first + ucf = + UserCustomField.where(name: "import_id", value: "#{external_user_id}#{import_source}").first ucf ? ucf.user_id : Discourse::SYSTEM_USER_ID end def new_category_id(external_category_id) - CategoryCustomField.where( - name: "import_id", - value: "#{external_category_id}#{import_source}" - ).first&.category_id + CategoryCustomField + .where(name: "import_id", value: "#{external_category_id}#{import_source}") + .first + &.category_id end def import_source - @_import_source ||= "#{ENV['IMPORT_SOURCE'] || ''}" + @_import_source ||= "#{ENV["IMPORT_SOURCE"] || ""}" end def category_levels - @levels ||= begin - levels = {} + @levels ||= + begin + levels = {} - # Incomplete backups may lack definitions for some parent categories - # which would cause an infinite loop below. - parent_ids = @categories.map { |category| category[:parent_category_id] }.uniq - category_ids = @categories.map { |category| category[:id] }.uniq - (parent_ids - category_ids).each { |id| levels[id] = 0 } + # Incomplete backups may lack definitions for some parent categories + # which would cause an infinite loop below. + parent_ids = @categories.map { |category| category[:parent_category_id] }.uniq + category_ids = @categories.map { |category| category[:id] }.uniq + (parent_ids - category_ids).each { |id| levels[id] = 0 } - loop do - changed = false + loop do + changed = false - @categories.each do |category| - if !levels[category[:id]] - if !category[:parent_category_id] - levels[category[:id]] = 1 - elsif levels[category[:parent_category_id]] - levels[category[:id]] = levels[category[:parent_category_id]] + 1 + @categories.each do |category| + if !levels[category[:id]] + if !category[:parent_category_id] + levels[category[:id]] = 1 + elsif levels[category[:parent_category_id]] + levels[category[:id]] = levels[category[:parent_category_id]] + 1 + end + + changed = true end - - changed = true end + + break if !changed end - break if !changed + levels end - - levels - end end def fix_permissions @@ -242,14 +250,19 @@ module ImportExport max_level.times do @categories.each do |category| parent_category = categories_by_id[category[:parent_category_id]] - next if !parent_category || !parent_category[:permissions_params] || parent_category[:permissions_params][:everyone] + if !parent_category || !parent_category[:permissions_params] || + parent_category[:permissions_params][:everyone] + next + end parent_groups = parent_category[:permissions_params].map(&:first) child_groups = category[:permissions_params].map(&:first) only_subcategory_groups = child_groups - parent_groups if only_subcategory_groups.present? - parent_category[:permissions_params].merge!(category[:permissions_params].slice(*only_subcategory_groups)) + parent_category[:permissions_params].merge!( + category[:permissions_params].slice(*only_subcategory_groups), + ) end end end diff --git a/lib/import_export/topic_exporter.rb b/lib/import_export/topic_exporter.rb index 64ab80aaf0f..e7a4efd909c 100644 --- a/lib/import_export/topic_exporter.rb +++ b/lib/import_export/topic_exporter.rb @@ -4,13 +4,9 @@ require "import_export/base_exporter" module ImportExport class TopicExporter < ImportExport::BaseExporter - def initialize(topic_ids) @topics = Topic.where(id: topic_ids).to_a - @export_data = { - topics: [], - users: [] - } + @export_data = { topics: [], users: [] } end def perform @@ -24,6 +20,5 @@ module ImportExport def default_filename_prefix "topic-export" end - end end diff --git a/lib/import_export/translation_overrides_exporter.rb b/lib/import_export/translation_overrides_exporter.rb index 7094248a99f..19feec10985 100644 --- a/lib/import_export/translation_overrides_exporter.rb +++ b/lib/import_export/translation_overrides_exporter.rb @@ -2,11 +2,8 @@ module ImportExport class TranslationOverridesExporter < BaseExporter - def initialize() - @export_data = { - translation_overrides: [] - } + @export_data = { translation_overrides: [] } end def perform @@ -19,6 +16,5 @@ module ImportExport def default_filename_prefix "translation-overrides" end - end end diff --git a/lib/inline_oneboxer.rb b/lib/inline_oneboxer.rb index 6383bbddff4..e3ef92ee849 100644 --- a/lib/inline_oneboxer.rb +++ b/lib/inline_oneboxer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class InlineOneboxer - MIN_TITLE_LENGTH = 2 def initialize(urls, opts = nil) @@ -60,26 +59,25 @@ class InlineOneboxer end always_allow = SiteSetting.enable_inline_onebox_on_all_domains - allowed_domains = SiteSetting.allowed_inline_onebox_domains&.split('|') unless always_allow + allowed_domains = SiteSetting.allowed_inline_onebox_domains&.split("|") unless always_allow if always_allow || allowed_domains - uri = begin - URI(url) - rescue URI::Error - end - - if uri.present? && - uri.hostname.present? && - (always_allow || allowed_domains.include?(uri.hostname)) && - !Onebox::DomainChecker.is_blocked?(uri.hostname) - if SiteSetting.block_onebox_on_redirect - max_redirects = 0 + uri = + begin + URI(url) + rescue URI::Error end - title = RetrieveTitle.crawl( - url, - max_redirects: max_redirects, - initial_https_redirect_ignore_limit: SiteSetting.block_onebox_on_redirect - ) + + if uri.present? && uri.hostname.present? && + (always_allow || allowed_domains.include?(uri.hostname)) && + !Onebox::DomainChecker.is_blocked?(uri.hostname) + max_redirects = 0 if SiteSetting.block_onebox_on_redirect + title = + RetrieveTitle.crawl( + url, + max_redirects: max_redirects, + initial_https_redirect_ignore_limit: SiteSetting.block_onebox_on_redirect, + ) title = nil if title && title.length < MIN_TITLE_LENGTH return onebox_for(url, title, opts) end @@ -95,23 +93,20 @@ class InlineOneboxer if title && opts[:post_number] title += " - " if opts[:post_author] - title += I18n.t( - "inline_oneboxer.topic_page_title_post_number_by_user", - post_number: opts[:post_number], - username: opts[:post_author] - ) + title += + I18n.t( + "inline_oneboxer.topic_page_title_post_number_by_user", + post_number: opts[:post_number], + username: opts[:post_author], + ) else - title += I18n.t( - "inline_oneboxer.topic_page_title_post_number", - post_number: opts[:post_number] - ) + title += + I18n.t("inline_oneboxer.topic_page_title_post_number", post_number: opts[:post_number]) end end title = title && Emoji.gsub_emoji_to_unicode(title) - if title.present? - title = WordWatcher.censor_text(title) - end + title = WordWatcher.censor_text(title) if title.present? onebox = { url: url, title: title } diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 009b75698f4..2b8db855754 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module JsLocaleHelper - def self.plugin_client_files(locale_str) files = Dir["#{Rails.root}/plugins/*/config/locales/client*.#{locale_str}.yml"] I18n::Backend::DiscourseI18n.sort_locale_files(files) @@ -10,9 +9,7 @@ module JsLocaleHelper def self.reloadable_plugins(locale_sym, ctx) return unless Rails.env.development? I18n.fallbacks[locale_sym].each do |locale| - plugin_client_files(locale.to_s).each do |file| - ctx.depend_on(file) - end + plugin_client_files(locale.to_s).each { |file| ctx.depend_on(file) } end end @@ -44,24 +41,28 @@ module JsLocaleHelper else # If we can't find a base file in Discourse, it might only exist in a plugin # so let's start with a basic object we can merge into - translations = { - locale_str => { - 'js' => {}, - 'admin_js' => {}, - 'wizard_js' => {} - } - } + translations = { locale_str => { "js" => {}, "admin_js" => {}, "wizard_js" => {} } } end # merge translations (plugin translations overwrite default translations) if translations[locale_str] && plugin_translations(locale_str) - translations[locale_str]['js'] ||= {} - translations[locale_str]['admin_js'] ||= {} - translations[locale_str]['wizard_js'] ||= {} + translations[locale_str]["js"] ||= {} + translations[locale_str]["admin_js"] ||= {} + translations[locale_str]["wizard_js"] ||= {} - translations[locale_str]['js'].deep_merge!(plugin_translations(locale_str)['js']) if plugin_translations(locale_str)['js'] - translations[locale_str]['admin_js'].deep_merge!(plugin_translations(locale_str)['admin_js']) if plugin_translations(locale_str)['admin_js'] - translations[locale_str]['wizard_js'].deep_merge!(plugin_translations(locale_str)['wizard_js']) if plugin_translations(locale_str)['wizard_js'] + if plugin_translations(locale_str)["js"] + translations[locale_str]["js"].deep_merge!(plugin_translations(locale_str)["js"]) + end + if plugin_translations(locale_str)["admin_js"] + translations[locale_str]["admin_js"].deep_merge!( + plugin_translations(locale_str)["admin_js"], + ) + end + if plugin_translations(locale_str)["wizard_js"] + translations[locale_str]["wizard_js"].deep_merge!( + plugin_translations(locale_str)["wizard_js"], + ) + end end translations @@ -82,9 +83,7 @@ module JsLocaleHelper new_hash[key] = new_at_key end else - if checking_hashes.any? { |h| h.include?(key) } - new_hash.delete(key) - end + new_hash.delete(key) if checking_hashes.any? { |h| h.include?(key) } end end new_hash @@ -93,16 +92,21 @@ module JsLocaleHelper def self.load_translations_merged(*locales) locales = locales.uniq.compact @loaded_merges ||= {} - @loaded_merges[locales.join('-')] ||= begin + @loaded_merges[locales.join("-")] ||= begin all_translations = {} merged_translations = {} loaded_locales = [] - locales.map(&:to_s).each do |locale| - all_translations[locale] = load_translations(locale) - merged_translations[locale] = deep_delete_matches(all_translations[locale][locale], loaded_locales.map { |l| merged_translations[l] }) - loaded_locales << locale - end + locales + .map(&:to_s) + .each do |locale| + all_translations[locale] = load_translations(locale) + merged_translations[locale] = deep_delete_matches( + all_translations[locale][locale], + loaded_locales.map { |l| merged_translations[l] }, + ) + loaded_locales << locale + end merged_translations end end @@ -118,13 +122,14 @@ module JsLocaleHelper locale_sym = locale_str.to_sym - translations = I18n.with_locale(locale_sym) do - if locale_sym == :en - load_translations(locale_sym) - else - load_translations_merged(*I18n.fallbacks[locale_sym]) + translations = + I18n.with_locale(locale_sym) do + if locale_sym == :en + load_translations(locale_sym) + else + load_translations_merged(*I18n.fallbacks[locale_sym]) + end end - end Marshal.load(Marshal.dump(translations)) end @@ -139,16 +144,18 @@ module JsLocaleHelper result = generate_message_format(message_formats, mf_locale, mf_filename) translations.keys.each do |l| - translations[l].keys.each do |k| - translations[l].delete(k) unless k == "js" - end + translations[l].keys.each { |k| translations[l].delete(k) unless k == "js" } end # I18n result << "I18n.translations = #{translations.to_json};\n" result << "I18n.locale = '#{locale_str}';\n" - result << "I18n.fallbackLocale = '#{fallback_locale_str}';\n" if fallback_locale_str && fallback_locale_str != "en" - result << "I18n.pluralizationRules.#{locale_str} = MessageFormat.locale.#{mf_locale};\n" if mf_locale != "en" + if fallback_locale_str && fallback_locale_str != "en" + result << "I18n.fallbackLocale = '#{fallback_locale_str}';\n" + end + if mf_locale != "en" + result << "I18n.pluralizationRules.#{locale_str} = MessageFormat.locale.#{mf_locale};\n" + end # moment result << File.read("#{Rails.root}/vendor/assets/javascripts/moment.js") @@ -165,10 +172,11 @@ module JsLocaleHelper has_overrides = false I18n.fallbacks[main_locale].each do |locale| - overrides = all_overrides[locale] = TranslationOverride - .where(locale: locale) - .where("translation_key LIKE 'js.%' OR translation_key LIKE 'admin_js.%'") - .pluck(:translation_key, :value, :compiled_js) + overrides = + all_overrides[locale] = TranslationOverride + .where(locale: locale) + .where("translation_key LIKE 'js.%' OR translation_key LIKE 'admin_js.%'") + .pluck(:translation_key, :value, :compiled_js) has_overrides ||= overrides.present? end @@ -214,19 +222,15 @@ module JsLocaleHelper return "" if translations.blank? output = +"if (!I18n.extras) { I18n.extras = {}; }" - locales.each do |l| - output << <<~JS + locales.each { |l| output << <<~JS } if (!I18n.extras["#{l}"]) { I18n.extras["#{l}"] = {}; } Object.assign(I18n.extras["#{l}"], #{translations[l].to_json}); JS - end output end - MOMENT_LOCALE_MAPPING ||= { - "hy" => "hy-am" - } + MOMENT_LOCALE_MAPPING ||= { "hy" => "hy-am" } def self.find_moment_locale(locale_chain, timezone_names: false) if timezone_names @@ -240,7 +244,7 @@ module JsLocaleHelper find_locale(locale_chain, path, type, fallback_to_english: false) do |locale| locale = MOMENT_LOCALE_MAPPING[locale] if MOMENT_LOCALE_MAPPING.key?(locale) # moment.js uses a different naming scheme for locale files - locale.tr('_', '-').downcase + locale.tr("_", "-").downcase end end @@ -258,14 +262,14 @@ module JsLocaleHelper locale = yield(locale) if block_given? filename = File.join(path, "#{locale}.js") - return [locale, filename] if File.exist?(filename) + return locale, filename if File.exist?(filename) end locale_chain.map! { |locale| yield(locale) } if block_given? # try again, but this time only with the language itself - locale_chain = locale_chain.map { |l| l.split(/[-_]/)[0] } - .uniq.reject { |l| locale_chain.include?(l) } + locale_chain = + locale_chain.map { |l| l.split(/[-_]/)[0] }.uniq.reject { |l| locale_chain.include?(l) } if locale_chain.any? locale_data = find_locale(locale_chain, path, type, fallback_to_english: false) @@ -278,9 +282,9 @@ module JsLocaleHelper def self.moment_formats result = +"" - result << moment_format_function('short_date_no_year') - result << moment_format_function('short_date') - result << moment_format_function('long_date') + result << moment_format_function("short_date_no_year") + result << moment_format_function("short_date") + result << moment_format_function("long_date") result << "moment.fn.relativeAge = function(opts){ return Discourse.Formatter.relativeAge(this.toDate(), opts)};\n" end @@ -295,7 +299,10 @@ module JsLocaleHelper end def self.generate_message_format(message_formats, locale, filename) - formats = message_formats.map { |k, v| k.inspect << " : " << compile_message_format(filename, locale, v) }.join(", ") + formats = + message_formats + .map { |k, v| k.inspect << " : " << compile_message_format(filename, locale, v) } + .join(", ") result = +"MessageFormat = {locale: {}};\n" result << "I18n._compiledMFs = {#{formats}};\n" @@ -318,11 +325,14 @@ module JsLocaleHelper @mutex = Mutex.new def self.with_context @mutex.synchronize do - yield @ctx ||= begin - ctx = MiniRacer::Context.new(timeout: 15000, ensure_gc_after_idle: 2000) - ctx.load("#{Rails.root}/lib/javascripts/messageformat.js") - ctx - end + yield( + @ctx ||= + begin + ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000) + ctx.load("#{Rails.root}/lib/javascripts/messageformat.js") + ctx + end + ) end end @@ -339,13 +349,15 @@ module JsLocaleHelper def self.remove_message_formats!(translations, locale) message_formats = {} - I18n.fallbacks[locale].map(&:to_s).each do |l| - next unless translations.key?(l) + I18n.fallbacks[locale] + .map(&:to_s) + .each do |l| + next unless translations.key?(l) - %w{js admin_js}.each do |k| - message_formats.merge!(strip_out_message_formats!(translations[l][k])) + %w[js admin_js].each do |k| + message_formats.merge!(strip_out_message_formats!(translations[l][k])) + end end - end message_formats end @@ -353,7 +365,9 @@ module JsLocaleHelper if hash.is_a?(Hash) hash.each do |key, value| if value.is_a?(Hash) - message_formats.merge!(strip_out_message_formats!(value, join_key(prefix, key), message_formats)) + message_formats.merge!( + strip_out_message_formats!(value, join_key(prefix, key), message_formats), + ) elsif key.to_s.end_with?("_MF") message_formats[join_key(prefix, key)] = value hash.delete(key) diff --git a/lib/json_error.rb b/lib/json_error.rb index ac6ff76edc5..d8867684a06 100644 --- a/lib/json_error.rb +++ b/lib/json_error.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module JsonError - def create_errors_json(obj, opts = nil) opts ||= {} @@ -32,7 +31,7 @@ module JsonError return { errors: [message] } if message.present? end - return { errors: [I18n.t('not_found')] } if obj.is_a?(HasErrors) && obj.not_found + return { errors: [I18n.t("not_found")] } if obj.is_a?(HasErrors) && obj.not_found # Log a warning (unless obj is nil) Rails.logger.warn("create_errors_json called with unrecognized type: #{obj.inspect}") if obj @@ -42,7 +41,6 @@ module JsonError end def self.generic_error - { errors: [I18n.t('js.generic_error')] } + { errors: [I18n.t("js.generic_error")] } end - end diff --git a/lib/letter_avatar.rb b/lib/letter_avatar.rb index 1a8524f338a..88cea034a0a 100644 --- a/lib/letter_avatar.rb +++ b/lib/letter_avatar.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true class LetterAvatar - class Identity attr_accessor :color, :letter def self.from_username(username) identity = new - identity.color = LetterAvatar::COLORS[ - Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length - ] + identity.color = + LetterAvatar::COLORS[ + Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length + ] identity.letter = username[0].upcase identity end @@ -19,11 +19,10 @@ class LetterAvatar VERSION = 5 # CHANGE these values to support more pixel ratios - FULLSIZE = 120 * 3 + FULLSIZE = 120 * 3 POINTSIZE = 280 class << self - def version "#{VERSION}_#{image_magick_version}" end @@ -72,19 +71,27 @@ class LetterAvatar filename = fullsize_path(identity) - instructions = %W{ - -size #{FULLSIZE}x#{FULLSIZE} + instructions = %W[ + -size + #{FULLSIZE}x#{FULLSIZE} xc:#{to_rgb(color)} - -pointsize #{POINTSIZE} - -fill #FFFFFFCC - -font Helvetica - -gravity Center - -annotate -0+26 #{letter} - -depth 8 + -pointsize + #{POINTSIZE} + -fill + #FFFFFFCC + -font + Helvetica + -gravity + Center + -annotate + -0+26 + #{letter} + -depth + 8 #{filename} - } + ] - Discourse::Utils.execute_command('convert', *instructions) + Discourse::Utils.execute_command("convert", *instructions) ## do not optimize image, it will end up larger than original filename @@ -110,11 +117,11 @@ class LetterAvatar begin skip = File.basename(cache_path) parent_path = File.dirname(cache_path) - Dir.entries(parent_path).each do |path| - unless ['.', '..'].include?(path) || path == skip - FileUtils.rm_rf(parent_path + "/" + path) + Dir + .entries(parent_path) + .each do |path| + FileUtils.rm_rf(parent_path + "/" + path) unless %w[. ..].include?(path) || path == skip end - end rescue Errno::ENOENT # no worries, folder doesn't exists end @@ -127,220 +134,222 @@ class LetterAvatar # - H: 0 - 360 # - C: 0 - 2 # - L: 0.75 - 1.5 - COLORS = [[198, 125, 40], - [61, 155, 243], - [74, 243, 75], - [238, 89, 166], - [52, 240, 224], - [177, 156, 155], - [240, 120, 145], - [111, 154, 78], - [237, 179, 245], - [237, 101, 95], - [89, 239, 155], - [43, 254, 70], - [163, 212, 245], - [65, 152, 142], - [165, 135, 246], - [181, 166, 38], - [187, 229, 206], - [77, 164, 25], - [179, 246, 101], - [234, 93, 37], - [225, 155, 115], - [142, 140, 188], - [223, 120, 140], - [249, 174, 27], - [244, 117, 225], - [137, 141, 102], - [75, 191, 146], - [188, 239, 142], - [164, 199, 145], - [173, 120, 149], - [59, 195, 89], - [222, 198, 220], - [68, 145, 187], - [236, 204, 179], - [159, 195, 72], - [188, 121, 189], - [166, 160, 85], - [181, 233, 37], - [236, 177, 85], - [121, 147, 160], - [234, 218, 110], - [241, 157, 191], - [62, 200, 234], - [133, 243, 34], - [88, 149, 110], - [59, 228, 248], - [183, 119, 118], - [251, 195, 45], - [113, 196, 122], - [197, 115, 70], - [80, 175, 187], - [103, 231, 238], - [240, 72, 133], - [228, 149, 241], - [180, 188, 159], - [172, 132, 85], - [180, 135, 251], - [236, 194, 58], - [217, 176, 109], - [88, 244, 199], - [186, 157, 239], - [113, 230, 96], - [206, 115, 165], - [244, 178, 163], - [230, 139, 26], - [241, 125, 89], - [83, 160, 66], - [107, 190, 166], - [197, 161, 210], - [198, 203, 245], - [238, 117, 19], - [228, 119, 116], - [131, 156, 41], - [145, 178, 168], - [139, 170, 220], - [233, 95, 125], - [87, 178, 230], - [157, 200, 119], - [237, 140, 76], - [229, 185, 186], - [144, 206, 212], - [236, 209, 158], - [185, 189, 79], - [34, 208, 66], - [84, 238, 129], - [133, 140, 134], - [67, 157, 94], - [168, 179, 25], - [140, 145, 240], - [151, 241, 125], - [67, 162, 107], - [200, 156, 21], - [169, 173, 189], - [226, 116, 189], - [133, 231, 191], - [194, 161, 63], - [241, 77, 99], - [241, 217, 53], - [123, 204, 105], - [210, 201, 119], - [229, 108, 155], - [240, 91, 72], - [187, 115, 210], - [240, 163, 100], - [178, 217, 57], - [179, 135, 116], - [204, 211, 24], - [186, 135, 57], - [223, 176, 135], - [204, 148, 151], - [116, 223, 50], - [95, 195, 46], - [123, 160, 236], - [181, 172, 131], - [142, 220, 202], - [240, 140, 112], - [172, 145, 164], - [228, 124, 45], - [135, 151, 243], - [42, 205, 125], - [192, 233, 116], - [119, 170, 114], - [158, 138, 26], - [73, 190, 183], - [185, 229, 243], - [227, 107, 55], - [196, 205, 202], - [132, 143, 60], - [233, 192, 237], - [62, 150, 220], - [205, 201, 141], - [106, 140, 190], - [161, 131, 205], - [135, 134, 158], - [198, 139, 81], - [115, 171, 32], - [101, 181, 67], - [149, 137, 119], - [37, 142, 183], - [183, 130, 175], - [168, 125, 133], - [124, 142, 87], - [236, 156, 171], - [232, 194, 91], - [219, 200, 69], - [144, 219, 34], - [219, 95, 187], - [145, 154, 217], - [165, 185, 100], - [127, 238, 163], - [224, 178, 198], - [119, 153, 120], - [124, 212, 92], - [172, 161, 105], - [231, 155, 135], - [157, 132, 101], - [122, 185, 146], - [53, 166, 51], - [70, 163, 90], - [150, 190, 213], - [210, 107, 60], - [166, 152, 185], - [159, 194, 159], - [39, 141, 222], - [202, 176, 161], - [95, 140, 229], - [168, 142, 87], - [93, 170, 203], - [159, 142, 54], - [14, 168, 39], - [94, 150, 149], - [187, 206, 136], - [157, 224, 166], - [235, 158, 208], - [109, 232, 216], - [141, 201, 87], - [208, 124, 118], - [142, 125, 214], - [19, 237, 174], - [72, 219, 41], - [234, 102, 111], - [168, 142, 79], - [188, 135, 35], - [95, 155, 143], - [148, 173, 116], - [223, 112, 95], - [228, 128, 236], - [206, 114, 54], - [195, 119, 88], - [235, 140, 94], - [235, 202, 125], - [233, 155, 153], - [214, 214, 238], - [246, 200, 35], - [151, 125, 171], - [132, 145, 172], - [131, 142, 118], - [199, 126, 150], - [61, 162, 123], - [58, 176, 151], - [215, 141, 69], - [225, 154, 220], - [220, 77, 167], - [233, 161, 64], - [130, 221, 137], - [81, 191, 129], - [169, 162, 140], - [174, 177, 222], - [236, 174, 47], - [233, 188, 180], - [69, 222, 172], - [71, 232, 93], - [118, 211, 238], - [157, 224, 83], - [218, 105, 73], - [126, 169, 36]] + COLORS = [ + [198, 125, 40], + [61, 155, 243], + [74, 243, 75], + [238, 89, 166], + [52, 240, 224], + [177, 156, 155], + [240, 120, 145], + [111, 154, 78], + [237, 179, 245], + [237, 101, 95], + [89, 239, 155], + [43, 254, 70], + [163, 212, 245], + [65, 152, 142], + [165, 135, 246], + [181, 166, 38], + [187, 229, 206], + [77, 164, 25], + [179, 246, 101], + [234, 93, 37], + [225, 155, 115], + [142, 140, 188], + [223, 120, 140], + [249, 174, 27], + [244, 117, 225], + [137, 141, 102], + [75, 191, 146], + [188, 239, 142], + [164, 199, 145], + [173, 120, 149], + [59, 195, 89], + [222, 198, 220], + [68, 145, 187], + [236, 204, 179], + [159, 195, 72], + [188, 121, 189], + [166, 160, 85], + [181, 233, 37], + [236, 177, 85], + [121, 147, 160], + [234, 218, 110], + [241, 157, 191], + [62, 200, 234], + [133, 243, 34], + [88, 149, 110], + [59, 228, 248], + [183, 119, 118], + [251, 195, 45], + [113, 196, 122], + [197, 115, 70], + [80, 175, 187], + [103, 231, 238], + [240, 72, 133], + [228, 149, 241], + [180, 188, 159], + [172, 132, 85], + [180, 135, 251], + [236, 194, 58], + [217, 176, 109], + [88, 244, 199], + [186, 157, 239], + [113, 230, 96], + [206, 115, 165], + [244, 178, 163], + [230, 139, 26], + [241, 125, 89], + [83, 160, 66], + [107, 190, 166], + [197, 161, 210], + [198, 203, 245], + [238, 117, 19], + [228, 119, 116], + [131, 156, 41], + [145, 178, 168], + [139, 170, 220], + [233, 95, 125], + [87, 178, 230], + [157, 200, 119], + [237, 140, 76], + [229, 185, 186], + [144, 206, 212], + [236, 209, 158], + [185, 189, 79], + [34, 208, 66], + [84, 238, 129], + [133, 140, 134], + [67, 157, 94], + [168, 179, 25], + [140, 145, 240], + [151, 241, 125], + [67, 162, 107], + [200, 156, 21], + [169, 173, 189], + [226, 116, 189], + [133, 231, 191], + [194, 161, 63], + [241, 77, 99], + [241, 217, 53], + [123, 204, 105], + [210, 201, 119], + [229, 108, 155], + [240, 91, 72], + [187, 115, 210], + [240, 163, 100], + [178, 217, 57], + [179, 135, 116], + [204, 211, 24], + [186, 135, 57], + [223, 176, 135], + [204, 148, 151], + [116, 223, 50], + [95, 195, 46], + [123, 160, 236], + [181, 172, 131], + [142, 220, 202], + [240, 140, 112], + [172, 145, 164], + [228, 124, 45], + [135, 151, 243], + [42, 205, 125], + [192, 233, 116], + [119, 170, 114], + [158, 138, 26], + [73, 190, 183], + [185, 229, 243], + [227, 107, 55], + [196, 205, 202], + [132, 143, 60], + [233, 192, 237], + [62, 150, 220], + [205, 201, 141], + [106, 140, 190], + [161, 131, 205], + [135, 134, 158], + [198, 139, 81], + [115, 171, 32], + [101, 181, 67], + [149, 137, 119], + [37, 142, 183], + [183, 130, 175], + [168, 125, 133], + [124, 142, 87], + [236, 156, 171], + [232, 194, 91], + [219, 200, 69], + [144, 219, 34], + [219, 95, 187], + [145, 154, 217], + [165, 185, 100], + [127, 238, 163], + [224, 178, 198], + [119, 153, 120], + [124, 212, 92], + [172, 161, 105], + [231, 155, 135], + [157, 132, 101], + [122, 185, 146], + [53, 166, 51], + [70, 163, 90], + [150, 190, 213], + [210, 107, 60], + [166, 152, 185], + [159, 194, 159], + [39, 141, 222], + [202, 176, 161], + [95, 140, 229], + [168, 142, 87], + [93, 170, 203], + [159, 142, 54], + [14, 168, 39], + [94, 150, 149], + [187, 206, 136], + [157, 224, 166], + [235, 158, 208], + [109, 232, 216], + [141, 201, 87], + [208, 124, 118], + [142, 125, 214], + [19, 237, 174], + [72, 219, 41], + [234, 102, 111], + [168, 142, 79], + [188, 135, 35], + [95, 155, 143], + [148, 173, 116], + [223, 112, 95], + [228, 128, 236], + [206, 114, 54], + [195, 119, 88], + [235, 140, 94], + [235, 202, 125], + [233, 155, 153], + [214, 214, 238], + [246, 200, 35], + [151, 125, 171], + [132, 145, 172], + [131, 142, 118], + [199, 126, 150], + [61, 162, 123], + [58, 176, 151], + [215, 141, 69], + [225, 154, 220], + [220, 77, 167], + [233, 161, 64], + [130, 221, 137], + [81, 191, 129], + [169, 162, 140], + [174, 177, 222], + [236, 174, 47], + [233, 188, 180], + [69, 222, 172], + [71, 232, 93], + [118, 211, 238], + [157, 224, 83], + [218, 105, 73], + [126, 169, 36], + ] end diff --git a/lib/markdown_linker.rb b/lib/markdown_linker.rb index fe48d4f5f2b..bc5bbdd0126 100644 --- a/lib/markdown_linker.rb +++ b/lib/markdown_linker.rb @@ -2,7 +2,6 @@ # Helps create links using markdown (where references are at the bottom) class MarkdownLinker - def initialize(base_url) @base_url = base_url @index = 1 @@ -19,11 +18,8 @@ class MarkdownLinker def references result = +"" - (@rendered..@index - 1).each do |i| - result << "[#{i}]: #{@markdown_links[i]}\n" - end + (@rendered..@index - 1).each { |i| result << "[#{i}]: #{@markdown_links[i]}\n" } @rendered = @index result end - end diff --git a/lib/mem_info.rb b/lib/mem_info.rb index 6404fa8733d..c8ea0d0d6c3 100644 --- a/lib/mem_info.rb +++ b/lib/mem_info.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class MemInfo - # Total memory in kb. On Mac OS uses "sysctl", elsewhere expects the system has /proc/meminfo. # Returns nil if it cannot be determined. def mem_total @@ -15,9 +14,8 @@ class MemInfo s = `grep MemTotal /proc/meminfo` /(\d+)/.match(s)[0].try(:to_i) end - rescue + rescue StandardError nil end end - end diff --git a/lib/message_bus_diags.rb b/lib/message_bus_diags.rb index 16bde3a9a14..b2de1ad80ef 100644 --- a/lib/message_bus_diags.rb +++ b/lib/message_bus_diags.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class MessageBusDiags - @host_info = {} def self.my_id @@ -21,7 +20,6 @@ class MessageBusDiags end unless @subscribed - MessageBus.subscribe "/server-name-reply/#{my_id}" do |msg| MessageBusDiags.seen_host(msg.data) end diff --git a/lib/method_profiler.rb b/lib/method_profiler.rb index fd6588c6bc7..aa0836278dc 100644 --- a/lib/method_profiler.rb +++ b/lib/method_profiler.rb @@ -3,17 +3,16 @@ # see https://samsaffron.com/archive/2017/10/18/fastest-way-to-profile-a-method-in-ruby class MethodProfiler def self.patch(klass, methods, name, no_recurse: false) - patches = methods.map do |method_name| - - recurse_protection = "" - if no_recurse - recurse_protection = <<~RUBY + patches = + methods + .map do |method_name| + recurse_protection = "" + recurse_protection = <<~RUBY if no_recurse return #{method_name}__mp_unpatched(*args, &blk) if @mp_recurse_protect_#{method_name} @mp_recurse_protect_#{method_name} = true RUBY - end - <<~RUBY + <<~RUBY unless defined?(#{method_name}__mp_unpatched) alias_method :#{method_name}__mp_unpatched, :#{method_name} def #{method_name}(*args, &blk) @@ -33,23 +32,23 @@ class MethodProfiler end end RUBY - end.join("\n") + end + .join("\n") klass.class_eval patches end def self.patch_with_debug_sql(klass, methods, name, no_recurse: false) - patches = methods.map do |method_name| - - recurse_protection = "" - if no_recurse - recurse_protection = <<~RUBY + patches = + methods + .map do |method_name| + recurse_protection = "" + recurse_protection = <<~RUBY if no_recurse return #{method_name}__mp_unpatched_debug_sql(*args, &blk) if @mp_recurse_protect_#{method_name} @mp_recurse_protect_#{method_name} = true RUBY - end - <<~RUBY + <<~RUBY unless defined?(#{method_name}__mp_unpatched_debug_sql) alias_method :#{method_name}__mp_unpatched_debug_sql, :#{method_name} def #{method_name}(*args, &blk) @@ -77,7 +76,8 @@ class MethodProfiler end end RUBY - end.join("\n") + end + .join("\n") klass.class_eval patches end @@ -89,9 +89,8 @@ class MethodProfiler end def self.start(transfer = nil) - Thread.current[:_method_profiler] = transfer || { - __start: Process.clock_gettime(Process::CLOCK_MONOTONIC) - } + Thread.current[:_method_profiler] = transfer || + { __start: Process.clock_gettime(Process::CLOCK_MONOTONIC) } end def self.clear @@ -116,35 +115,36 @@ class MethodProfiler # filter_transactions - When true, we do not record timings of transaction # related commits (BEGIN, COMMIT, ROLLBACK) def self.output_sql_to_stderr!(filter_transactions: false) - Rails.logger.warn("Stop! This instrumentation is not intended for use in production outside of debugging scenarios. Please be sure you know what you are doing when enabling this instrumentation.") + Rails.logger.warn( + "Stop! This instrumentation is not intended for use in production outside of debugging scenarios. Please be sure you know what you are doing when enabling this instrumentation.", + ) @@instrumentation_debug_sql_filter_transactions = filter_transactions - @@instrumentation_setup_debug_sql ||= begin - MethodProfiler.patch_with_debug_sql(PG::Connection, [ - :exec, :async_exec, :exec_prepared, :send_query_prepared, :query, :exec_params - ], :sql) - true - end + @@instrumentation_setup_debug_sql ||= + begin + MethodProfiler.patch_with_debug_sql( + PG::Connection, + %i[exec async_exec exec_prepared send_query_prepared query exec_params], + :sql, + ) + true + end end def self.ensure_discourse_instrumentation! - @@instrumentation_setup ||= begin - MethodProfiler.patch(PG::Connection, [ - :exec, :async_exec, :exec_prepared, :send_query_prepared, :query, :exec_params - ], :sql) + @@instrumentation_setup ||= + begin + MethodProfiler.patch( + PG::Connection, + %i[exec async_exec exec_prepared send_query_prepared query exec_params], + :sql, + ) - MethodProfiler.patch(Redis::Client, [ - :call, :call_pipeline - ], :redis) + MethodProfiler.patch(Redis::Client, %i[call call_pipeline], :redis) - MethodProfiler.patch(Net::HTTP, [ - :request - ], :net, no_recurse: true) + MethodProfiler.patch(Net::HTTP, [:request], :net, no_recurse: true) - MethodProfiler.patch(Excon::Connection, [ - :request - ], :net) - true - end + MethodProfiler.patch(Excon::Connection, [:request], :net) + true + end end - end diff --git a/lib/middleware/anonymous_cache.rb b/lib/middleware/anonymous_cache.rb index ef6bc7e1dd2..553113e7899 100644 --- a/lib/middleware/anonymous_cache.rb +++ b/lib/middleware/anonymous_cache.rb @@ -7,17 +7,16 @@ require "http_language_parser" module Middleware class AnonymousCache - def self.cache_key_segments @@cache_key_segments ||= { - m: 'key_is_mobile?', - c: 'key_is_crawler?', - o: 'key_is_old_browser?', - d: 'key_is_modern_mobile_device?', - b: 'key_has_brotli?', - t: 'key_cache_theme_ids', - ca: 'key_compress_anon', - l: 'key_locale' + m: "key_is_mobile?", + c: "key_is_crawler?", + o: "key_is_old_browser?", + d: "key_is_modern_mobile_device?", + b: "key_has_brotli?", + t: "key_cache_theme_ids", + ca: "key_compress_anon", + l: "key_locale", } end @@ -46,9 +45,9 @@ module Middleware # This gives us an API to insert anonymous cache segments class Helper - RACK_SESSION = "rack.session" - USER_AGENT = "HTTP_USER_AGENT" - ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING" + RACK_SESSION = "rack.session" + USER_AGENT = "HTTP_USER_AGENT" + ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING" DISCOURSE_RENDER = "HTTP_DISCOURSE_RENDER" REDIS_STORE_SCRIPT = DiscourseRedis::EvalHelper.new <<~LUA @@ -63,13 +62,11 @@ module Middleware end def blocked_crawler? - @request.get? && - !@request.xhr? && - !@request.path.ends_with?('robots.txt') && - !@request.path.ends_with?('srv/status') && - @request[Auth::DefaultCurrentUserProvider::API_KEY].nil? && - @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? && - CrawlerDetection.is_blocked_crawler?(@env[USER_AGENT]) + @request.get? && !@request.xhr? && !@request.path.ends_with?("robots.txt") && + !@request.path.ends_with?("srv/status") && + @request[Auth::DefaultCurrentUserProvider::API_KEY].nil? && + @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? && + CrawlerDetection.is_blocked_crawler?(@env[USER_AGENT]) end def is_mobile=(val) @@ -112,10 +109,16 @@ module Middleware begin user_agent = @env[USER_AGENT] - if @env[DISCOURSE_RENDER] == "crawler" || CrawlerDetection.crawler?(user_agent, @env["HTTP_VIA"]) + if @env[DISCOURSE_RENDER] == "crawler" || + CrawlerDetection.crawler?(user_agent, @env["HTTP_VIA"]) :true else - user_agent.downcase.include?("discourse") && !user_agent.downcase.include?("mobile") ? :true : :false + if user_agent.downcase.include?("discourse") && + !user_agent.downcase.include?("mobile") + :true + else + :false + end end end @is_crawler == :true @@ -133,13 +136,14 @@ module Middleware def cache_key return @cache_key if defined?(@cache_key) - @cache_key = +"ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env[Rack::RACK_URL_SCHEME]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}" + @cache_key = + +"ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env[Rack::RACK_URL_SCHEME]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}" @cache_key << AnonymousCache.build_cache_key(self) @cache_key end def key_cache_theme_ids - theme_ids.join(',') + theme_ids.join(",") end def key_compress_anon @@ -147,7 +151,7 @@ module Middleware end def theme_ids - ids, _ = @request.cookies['theme_ids']&.split('|') + ids, _ = @request.cookies["theme_ids"]&.split("|") id = ids&.split(",")&.map(&:to_i)&.first if id && Guardian.new.allow_themes?([id]) Theme.transform_ids(id) @@ -178,31 +182,31 @@ module Middleware def no_cache_bypass request = Rack::Request.new(@env) - request.cookies['_bypass_cache'].nil? && - (request.path != '/srv/status') && + request.cookies["_bypass_cache"].nil? && (request.path != "/srv/status") && request[Auth::DefaultCurrentUserProvider::API_KEY].nil? && @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? end def force_anonymous! @env[Auth::DefaultCurrentUserProvider::USER_API_KEY] = nil - @env['HTTP_COOKIE'] = nil - @env['HTTP_DISCOURSE_LOGGED_IN'] = nil - @env['rack.request.cookie.hash'] = {} - @env['rack.request.cookie.string'] = '' - @env['_bypass_cache'] = nil + @env["HTTP_COOKIE"] = nil + @env["HTTP_DISCOURSE_LOGGED_IN"] = nil + @env["rack.request.cookie.hash"] = {} + @env["rack.request.cookie.string"] = "" + @env["_bypass_cache"] = nil request = Rack::Request.new(@env) - request.delete_param('api_username') - request.delete_param('api_key') + request.delete_param("api_username") + request.delete_param("api_key") end def logged_in_anon_limiter - @logged_in_anon_limiter ||= RateLimiter.new( - nil, - "logged_in_anon_cache_#{@env["HTTP_HOST"]}/#{@env["REQUEST_URI"]}", - GlobalSetting.force_anonymous_min_per_10_seconds, - 10 - ) + @logged_in_anon_limiter ||= + RateLimiter.new( + nil, + "logged_in_anon_cache_#{@env["HTTP_HOST"]}/#{@env["REQUEST_URI"]}", + GlobalSetting.force_anonymous_min_per_10_seconds, + 10, + ) end def check_logged_in_rate_limit! @@ -213,13 +217,11 @@ module Middleware ADP = "action_dispatch.request.parameters" def should_force_anonymous? - if (queue_time = @env['REQUEST_QUEUE_SECONDS']) && get? + if (queue_time = @env["REQUEST_QUEUE_SECONDS"]) && get? if queue_time > GlobalSetting.force_anonymous_min_queue_seconds return check_logged_in_rate_limit! elsif queue_time >= MIN_TIME_TO_CHECK - if !logged_in_anon_limiter.can_perform? - return check_logged_in_rate_limit! - end + return check_logged_in_rate_limit! if !logged_in_anon_limiter.can_perform? end end @@ -233,7 +235,7 @@ module Middleware def compress(val) if val && GlobalSetting.compress_anon_cache require "lz4-ruby" if !defined?(LZ4) - LZ4::compress(val) + LZ4.compress(val) else val end @@ -242,7 +244,7 @@ module Middleware def decompress(val) if val && GlobalSetting.compress_anon_cache require "lz4-ruby" if !defined?(LZ4) - LZ4::uncompress(val) + LZ4.uncompress(val) else val end @@ -273,7 +275,6 @@ module Middleware status, headers, response = result if status == 200 && cache_duration - if GlobalSetting.anon_cache_store_threshold > 1 count = REDIS_STORE_SCRIPT.eval(Discourse.redis, [cache_key_count], [cache_duration]) @@ -281,25 +282,24 @@ module Middleware # prudent here, hence the to_i if count.to_i < GlobalSetting.anon_cache_store_threshold headers["X-Discourse-Cached"] = "skip" - return [status, headers, response] + return status, headers, response end end - headers_stripped = headers.dup.delete_if { |k, _| ["Set-Cookie", "X-MiniProfiler-Ids"].include? k } + headers_stripped = + headers.dup.delete_if { |k, _| %w[Set-Cookie X-MiniProfiler-Ids].include? k } headers_stripped["X-Discourse-Cached"] = "true" parts = [] - response.each do |part| - parts << part - end + response.each { |part| parts << part } if req_params = env[ADP] headers_stripped[ADP] = { "action" => req_params["action"], - "controller" => req_params["controller"] + "controller" => req_params["controller"], } end - Discourse.redis.setex(cache_key_body, cache_duration, compress(parts.join)) + Discourse.redis.setex(cache_key_body, cache_duration, compress(parts.join)) Discourse.redis.setex(cache_key_other, cache_duration, [status, headers_stripped].to_json) headers["X-Discourse-Cached"] = "store" @@ -314,20 +314,18 @@ module Middleware Discourse.redis.del(cache_key_body) Discourse.redis.del(cache_key_other) end - end def initialize(app, settings = {}) @app = app end - PAYLOAD_INVALID_REQUEST_METHODS = ["GET", "HEAD"] + PAYLOAD_INVALID_REQUEST_METHODS = %w[GET HEAD] def call(env) if PAYLOAD_INVALID_REQUEST_METHODS.include?(env[Rack::REQUEST_METHOD]) && - env[Rack::RACK_INPUT].size > 0 - - return [413, { "Cache-Control" => "private, max-age=0, must-revalidate" }, []] + env[Rack::RACK_INPUT].size > 0 + return 413, { "Cache-Control" => "private, max-age=0, must-revalidate" }, [] end helper = Helper.new(env) @@ -335,7 +333,7 @@ module Middleware if helper.blocked_crawler? env["discourse.request_tracker.skip"] = true - return [403, {}, ["Crawler is not allowed!"]] + return 403, {}, ["Crawler is not allowed!"] end if helper.should_force_anonymous? @@ -348,15 +346,15 @@ module Middleware if max_time > 0 && queue_time.to_f > max_time return [ 429, - { - "content-type" => "application/json; charset=utf-8" - }, - [{ - errors: I18n.t("rate_limiter.slow_down"), - extras: { - wait_seconds: 5 + (5 * rand).round(2) - } - }.to_json] + { "content-type" => "application/json; charset=utf-8" }, + [ + { + errors: I18n.t("rate_limiter.slow_down"), + extras: { + wait_seconds: 5 + (5 * rand).round(2), + }, + }.to_json, + ] ] end end @@ -368,13 +366,9 @@ module Middleware @app.call(env) end - if force_anon - result[1]["Set-Cookie"] = "dosp=1; Path=/" - end + result[1]["Set-Cookie"] = "dosp=1; Path=/" if force_anon result end - end - end diff --git a/lib/middleware/discourse_public_exceptions.rb b/lib/middleware/discourse_public_exceptions.rb index 9a9ea11571c..b507bc867a3 100644 --- a/lib/middleware/discourse_public_exceptions.rb +++ b/lib/middleware/discourse_public_exceptions.rb @@ -4,11 +4,14 @@ # we need to handle certain exceptions here module Middleware class DiscoursePublicExceptions < ::ActionDispatch::PublicExceptions - INVALID_REQUEST_ERRORS = Set.new([ - Rack::QueryParser::InvalidParameterError, - ActionController::BadRequest, - ActionDispatch::Http::Parameters::ParseError, - ]) + INVALID_REQUEST_ERRORS = + Set.new( + [ + Rack::QueryParser::InvalidParameterError, + ActionController::BadRequest, + ActionDispatch::Http::Parameters::ParseError, + ], + ) def initialize(path) super @@ -35,31 +38,38 @@ module Middleware begin request.format rescue Mime::Type::InvalidMimeType - return [400, { "Cache-Control" => "private, max-age=0, must-revalidate" }, ["Invalid MIME type"]] + return [ + 400, + { "Cache-Control" => "private, max-age=0, must-revalidate" }, + ["Invalid MIME type"] + ] end # Or badly formatted multipart requests begin request.POST rescue EOFError - return [400, { "Cache-Control" => "private, max-age=0, must-revalidate" }, ["Invalid request"]] + return [ + 400, + { "Cache-Control" => "private, max-age=0, must-revalidate" }, + ["Invalid request"] + ] end if ApplicationController.rescue_with_handler(exception, object: fake_controller) body = response.body - if String === body - body = [body] - end - return [response.status, response.headers, body] + body = [body] if String === body + return response.status, response.headers, body end rescue => e return super if INVALID_REQUEST_ERRORS.include?(e.class) - Discourse.warn_exception(e, message: "Failed to handle exception in exception app middleware") + Discourse.warn_exception( + e, + message: "Failed to handle exception in exception app middleware", + ) end - end super end - end end diff --git a/lib/middleware/enforce_hostname.rb b/lib/middleware/enforce_hostname.rb index 44ac8d7d4a1..f0f604b145b 100644 --- a/lib/middleware/enforce_hostname.rb +++ b/lib/middleware/enforce_hostname.rb @@ -18,7 +18,8 @@ module Middleware requested_hostname = env[Rack::HTTP_HOST] env[Discourse::REQUESTED_HOSTNAME] = requested_hostname - env[Rack::HTTP_HOST] = allowed_hostnames.find { |h| h == requested_hostname } || Discourse.current_hostname_with_port + env[Rack::HTTP_HOST] = allowed_hostnames.find { |h| h == requested_hostname } || + Discourse.current_hostname_with_port @app.call(env) end diff --git a/lib/middleware/missing_avatars.rb b/lib/middleware/missing_avatars.rb index f0f2da5d5c1..958aecaa3ee 100644 --- a/lib/middleware/missing_avatars.rb +++ b/lib/middleware/missing_avatars.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Middleware - # In development mode, it is common to use a database from a production site for testing # with their data. Unfortunately, you can end up with dozens of missing avatar requests # due to the files not being present locally. This middleware, only enabled in development @@ -12,11 +11,11 @@ module Middleware end def call(env) - if (env['REQUEST_PATH'] =~ /^\/uploads\/default\/avatars/) - path = "#{Rails.root}/public#{env['REQUEST_PATH']}" + if (env["REQUEST_PATH"] =~ %r{^/uploads/default/avatars}) + path = "#{Rails.root}/public#{env["REQUEST_PATH"]}" unless File.exist?(path) default_image = "#{Rails.root}/public/images/d-logo-sketch-small.png" - return [ 200, { 'Content-Type' => 'image/png' }, [ File.read(default_image)] ] + return 200, { "Content-Type" => "image/png" }, [File.read(default_image)] end end @@ -24,5 +23,4 @@ module Middleware [status, headers, response] end end - end diff --git a/lib/middleware/omniauth_bypass_middleware.rb b/lib/middleware/omniauth_bypass_middleware.rb index c794b11aabb..b8c65caac88 100644 --- a/lib/middleware/omniauth_bypass_middleware.rb +++ b/lib/middleware/omniauth_bypass_middleware.rb @@ -5,7 +5,8 @@ require "csrf_token_verifier" # omniauth loves spending lots cycles in its magic middleware stack # this middleware bypasses omniauth middleware and only hits it when needed class Middleware::OmniauthBypassMiddleware - class AuthenticatorDisabled < StandardError; end + class AuthenticatorDisabled < StandardError + end def initialize(app, options = {}) @app = app @@ -15,11 +16,10 @@ class Middleware::OmniauthBypassMiddleware # if you need to test this and are having ssl issues see: # http://stackoverflow.com/questions/6756460/openssl-error-using-omniauth-specified-ssl-path-but-didnt-work # OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE if Rails.env.development? - @omniauth = OmniAuth::Builder.new(app) do - Discourse.authenticators.each do |authenticator| - authenticator.register_middleware(self) + @omniauth = + OmniAuth::Builder.new(app) do + Discourse.authenticators.each { |authenticator| authenticator.register_middleware(self) } end - end @omniauth.before_request_phase do |env| request = ActionDispatch::Request.new(env) @@ -28,7 +28,9 @@ class Middleware::OmniauthBypassMiddleware CSRFTokenVerifier.new.call(env) if request.request_method.downcase.to_sym != :get # Check whether the authenticator is enabled - if !Discourse.enabled_authenticators.any? { |a| a.name.to_sym == env['omniauth.strategy'].name.to_sym } + if !Discourse.enabled_authenticators.any? { |a| + a.name.to_sym == env["omniauth.strategy"].name.to_sym + } raise AuthenticatorDisabled end @@ -44,8 +46,9 @@ class Middleware::OmniauthBypassMiddleware if env["PATH_INFO"].start_with?("/auth") begin # When only one provider is enabled, assume it can be completely trusted, and allow GET requests - only_one_provider = !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 - OmniAuth.config.allowed_request_methods = only_one_provider ? [:get, :post] : [:post] + only_one_provider = + !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 + OmniAuth.config.allowed_request_methods = only_one_provider ? %i[get post] : [:post] @omniauth.call(env) rescue AuthenticatorDisabled => e @@ -71,5 +74,4 @@ class Middleware::OmniauthBypassMiddleware @app.call(env) end end - end diff --git a/lib/middleware/request_tracker.rb b/lib/middleware/request_tracker.rb index 4decb0983c8..23acf64d127 100644 --- a/lib/middleware/request_tracker.rb +++ b/lib/middleware/request_tracker.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'method_profiler' -require 'middleware/anonymous_cache' +require "method_profiler" +require "middleware/anonymous_cache" class Middleware::RequestTracker @@detailed_request_loggers = nil @@ -15,7 +15,8 @@ class Middleware::RequestTracker # 14.15.16.32/27 # 216.148.1.2 # - STATIC_IP_SKIPPER = ENV['DISCOURSE_MAX_REQS_PER_IP_EXCEPTIONS']&.split&.map { |ip| IPAddr.new(ip) } + STATIC_IP_SKIPPER = + ENV["DISCOURSE_MAX_REQS_PER_IP_EXCEPTIONS"]&.split&.map { |ip| IPAddr.new(ip) } # register callbacks for detailed request loggers called on every request # example: @@ -30,9 +31,7 @@ class Middleware::RequestTracker def self.unregister_detailed_request_logger(callback) @@detailed_request_loggers.delete(callback) - if @@detailed_request_loggers.length == 0 - @detailed_request_loggers = nil - end + @detailed_request_loggers = nil if @@detailed_request_loggers.length == 0 end # used for testing @@ -107,7 +106,8 @@ class Middleware::RequestTracker env_track_view = env["HTTP_DISCOURSE_TRACK_VIEW"] track_view = status == 200 track_view &&= env_track_view != "0" && env_track_view != "false" - track_view &&= env_track_view || (request.get? && !request.xhr? && headers["Content-Type"] =~ /text\/html/) + track_view &&= + env_track_view || (request.get? && !request.xhr? && headers["Content-Type"] =~ %r{text/html}) track_view = !!track_view has_auth_cookie = Auth::DefaultCurrentUserProvider.find_v0_auth_cookie(request).present? has_auth_cookie ||= Auth::DefaultCurrentUserProvider.find_v1_auth_cookie(env).present? @@ -128,7 +128,7 @@ class Middleware::RequestTracker is_mobile: helper.is_mobile?, track_view: track_view, timing: timing, - queue_seconds: env['REQUEST_QUEUE_SECONDS'] + queue_seconds: env["REQUEST_QUEUE_SECONDS"], } if h[:is_background] @@ -146,7 +146,7 @@ class Middleware::RequestTracker end if h[:is_crawler] - user_agent = env['HTTP_USER_AGENT'] + user_agent = env["HTTP_USER_AGENT"] if user_agent && (user_agent.encoding != Encoding::UTF_8) user_agent = user_agent.encode("utf-8") user_agent.scrub! @@ -163,7 +163,12 @@ class Middleware::RequestTracker def log_request_info(env, result, info, request = nil) # we got to skip this on error ... its just logging - data = self.class.get_data(env, result, info, request) rescue nil + data = + begin + self.class.get_data(env, result, info, request) + rescue StandardError + nil + end if data if result && (headers = result[1]) @@ -179,15 +184,16 @@ class Middleware::RequestTracker end def self.populate_request_queue_seconds!(env) - if !env['REQUEST_QUEUE_SECONDS'] - if queue_start = env['HTTP_X_REQUEST_START'] - queue_start = if queue_start.start_with?("t=") - queue_start.split("t=")[1].to_f - else - queue_start.to_f / 1000.0 - end + if !env["REQUEST_QUEUE_SECONDS"] + if queue_start = env["HTTP_X_REQUEST_START"] + queue_start = + if queue_start.start_with?("t=") + queue_start.split("t=")[1].to_f + else + queue_start.to_f / 1000.0 + end queue_time = (Time.now.to_f - queue_start) - env['REQUEST_QUEUE_SECONDS'] = queue_time + env["REQUEST_QUEUE_SECONDS"] = queue_time end end end @@ -212,9 +218,9 @@ class Middleware::RequestTracker TEXT headers = { "Retry-After" => available_in.to_s, - "Discourse-Rate-Limit-Error-Code" => error_code + "Discourse-Rate-Limit-Error-Code" => error_code, } - return [429, headers, [message]] + return 429, headers, [message] end env["discourse.request_tracker"] = self @@ -235,21 +241,21 @@ class Middleware::RequestTracker headers["X-Sql-Calls"] = sql[:calls].to_s headers["X-Sql-Time"] = "%0.6f" % sql[:duration] end - if queue = env['REQUEST_QUEUE_SECONDS'] + if queue = env["REQUEST_QUEUE_SECONDS"] headers["X-Queue-Time"] = "%0.6f" % queue end end end if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] && (headers = result[1]) - headers['Discourse-Logged-Out'] = '1' + headers["Discourse-Logged-Out"] = "1" end result ensure - if (limiters = env['DISCOURSE_RATE_LIMITERS']) && env['DISCOURSE_IS_ASSET_PATH'] + if (limiters = env["DISCOURSE_RATE_LIMITERS"]) && env["DISCOURSE_IS_ASSET_PATH"] limiters.each(&:rollback!) - env['DISCOURSE_ASSET_RATE_LIMITERS'].each do |limiter| + env["DISCOURSE_ASSET_RATE_LIMITERS"].each do |limiter| begin limiter.performed! rescue RateLimiter::LimitExceeded @@ -257,25 +263,19 @@ class Middleware::RequestTracker end end end - if !env["discourse.request_tracker.skip"] - log_request_info(env, result, info, request) - end + log_request_info(env, result, info, request) if !env["discourse.request_tracker.skip"] end def log_later(data) Scheduler::Defer.later("Track view") do - unless Discourse.pg_readonly_mode? - self.class.log_request(data) - end + self.class.log_request(data) unless Discourse.pg_readonly_mode? end end def find_auth_cookie(env) min_allowed_timestamp = Time.now.to_i - (UserAuthToken::ROTATE_TIME_MINS + 1) * 60 cookie = Auth::DefaultCurrentUserProvider.find_v1_auth_cookie(env) - if cookie && cookie[:issued_at] >= min_allowed_timestamp - cookie - end + cookie if cookie && cookie[:issued_at] >= min_allowed_timestamp end def is_private_ip?(ip) @@ -286,10 +286,12 @@ class Middleware::RequestTracker end def rate_limit(request, cookie) - warn = GlobalSetting.max_reqs_per_ip_mode == "warn" || - GlobalSetting.max_reqs_per_ip_mode == "warn+block" - block = GlobalSetting.max_reqs_per_ip_mode == "block" || - GlobalSetting.max_reqs_per_ip_mode == "warn+block" + warn = + GlobalSetting.max_reqs_per_ip_mode == "warn" || + GlobalSetting.max_reqs_per_ip_mode == "warn+block" + block = + GlobalSetting.max_reqs_per_ip_mode == "block" || + GlobalSetting.max_reqs_per_ip_mode == "warn+block" return if !block && !warn @@ -304,54 +306,56 @@ class Middleware::RequestTracker ip_or_id = ip limit_on_id = false - if cookie && cookie[:user_id] && cookie[:trust_level] && cookie[:trust_level] >= GlobalSetting.skip_per_ip_rate_limit_trust_level + if cookie && cookie[:user_id] && cookie[:trust_level] && + cookie[:trust_level] >= GlobalSetting.skip_per_ip_rate_limit_trust_level ip_or_id = cookie[:user_id] limit_on_id = true end - limiter10 = RateLimiter.new( - nil, - "global_ip_limit_10_#{ip_or_id}", - GlobalSetting.max_reqs_per_ip_per_10_seconds, - 10, - global: !limit_on_id, - aggressive: true, - error_code: limit_on_id ? "id_10_secs_limit" : "ip_10_secs_limit" - ) + limiter10 = + RateLimiter.new( + nil, + "global_ip_limit_10_#{ip_or_id}", + GlobalSetting.max_reqs_per_ip_per_10_seconds, + 10, + global: !limit_on_id, + aggressive: true, + error_code: limit_on_id ? "id_10_secs_limit" : "ip_10_secs_limit", + ) - limiter60 = RateLimiter.new( - nil, - "global_ip_limit_60_#{ip_or_id}", - GlobalSetting.max_reqs_per_ip_per_minute, - 60, - global: !limit_on_id, - error_code: limit_on_id ? "id_60_secs_limit" : "ip_60_secs_limit", - aggressive: true - ) + limiter60 = + RateLimiter.new( + nil, + "global_ip_limit_60_#{ip_or_id}", + GlobalSetting.max_reqs_per_ip_per_minute, + 60, + global: !limit_on_id, + error_code: limit_on_id ? "id_60_secs_limit" : "ip_60_secs_limit", + aggressive: true, + ) - limiter_assets10 = RateLimiter.new( - nil, - "global_ip_limit_10_assets_#{ip_or_id}", - GlobalSetting.max_asset_reqs_per_ip_per_10_seconds, - 10, - error_code: limit_on_id ? "id_assets_10_secs_limit" : "ip_assets_10_secs_limit", - global: !limit_on_id - ) + limiter_assets10 = + RateLimiter.new( + nil, + "global_ip_limit_10_assets_#{ip_or_id}", + GlobalSetting.max_asset_reqs_per_ip_per_10_seconds, + 10, + error_code: limit_on_id ? "id_assets_10_secs_limit" : "ip_assets_10_secs_limit", + global: !limit_on_id, + ) - request.env['DISCOURSE_RATE_LIMITERS'] = [limiter10, limiter60] - request.env['DISCOURSE_ASSET_RATE_LIMITERS'] = [limiter_assets10] + request.env["DISCOURSE_RATE_LIMITERS"] = [limiter10, limiter60] + request.env["DISCOURSE_ASSET_RATE_LIMITERS"] = [limiter_assets10] if !limiter_assets10.can_perform? if warn - Discourse.warn("Global asset IP rate limit exceeded for #{ip}: 10 second rate limit", uri: request.env["REQUEST_URI"]) + Discourse.warn( + "Global asset IP rate limit exceeded for #{ip}: 10 second rate limit", + uri: request.env["REQUEST_URI"], + ) end - if block - return [ - limiter_assets10.seconds_to_wait(Time.now.to_i), - limiter_assets10.error_code - ] - end + return limiter_assets10.seconds_to_wait(Time.now.to_i), limiter_assets10.error_code if block end begin @@ -364,7 +368,10 @@ class Middleware::RequestTracker nil rescue RateLimiter::LimitExceeded => e if warn - Discourse.warn("Global IP rate limit exceeded for #{ip}: #{type} second rate limit", uri: request.env["REQUEST_URI"]) + Discourse.warn( + "Global IP rate limit exceeded for #{ip}: #{type} second rate limit", + uri: request.env["REQUEST_URI"], + ) end if block [e.available_in, e.error_code] diff --git a/lib/middleware/turbo_dev.rb b/lib/middleware/turbo_dev.rb index 53e81cb50df..8754508caf6 100644 --- a/lib/middleware/turbo_dev.rb +++ b/lib/middleware/turbo_dev.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true module Middleware - # Cheat and bypass Rails in development mode if the client attempts to download a static asset # that's already been downloaded. # @@ -19,22 +18,19 @@ module Middleware def call(env) root = "#{GlobalSetting.relative_url_root}/assets/" - is_asset = env['REQUEST_PATH'] && env['REQUEST_PATH'].starts_with?(root) + is_asset = env["REQUEST_PATH"] && env["REQUEST_PATH"].starts_with?(root) # hack to bypass all middleware if serving assets, a lot faster 4.5 seconds -> 1.5 seconds - if (etag = env['HTTP_IF_NONE_MATCH']) && is_asset - name = env['REQUEST_PATH'][(root.length)..-1] + if (etag = env["HTTP_IF_NONE_MATCH"]) && is_asset + name = env["REQUEST_PATH"][(root.length)..-1] etag = etag.gsub "\"", "" asset = Rails.application.assets.find_asset(name) - if asset && asset.digest == etag - return [304, {}, []] - end + return 304, {}, [] if asset && asset.digest == etag end status, headers, response = @app.call(env) - headers['Cache-Control'] = 'no-cache' if is_asset + headers["Cache-Control"] = "no-cache" if is_asset [status, headers, response] end end - end diff --git a/lib/migration/base_dropper.rb b/lib/migration/base_dropper.rb index 9aaf77e138a..abf7a39a734 100644 --- a/lib/migration/base_dropper.rb +++ b/lib/migration/base_dropper.rb @@ -9,9 +9,14 @@ module Migration CREATE SCHEMA IF NOT EXISTS #{FUNCTION_SCHEMA_NAME}; SQL - message = column_name ? - "Discourse: #{column_name} in #{table_name} is readonly" : - "Discourse: #{table_name} is read only" + message = + ( + if column_name + "Discourse: #{column_name} in #{table_name} is readonly" + else + "Discourse: #{table_name} is read only" + end + ) DB.exec <<~SQL CREATE OR REPLACE FUNCTION #{readonly_function_name(table_name, column_name)} RETURNS trigger AS $rcr$ @@ -27,12 +32,7 @@ module Migration end def self.readonly_function_name(table_name, column_name = nil, with_schema: true) - function_name = [ - "raise", - table_name, - column_name, - "readonly()" - ].compact.join("_") + function_name = ["raise", table_name, column_name, "readonly()"].compact.join("_") if with_schema && function_schema_exists? "#{FUNCTION_SCHEMA_NAME}.#{function_name}" @@ -42,9 +42,7 @@ module Migration end def self.old_readonly_function_name(table_name, column_name = nil) - readonly_function_name(table_name, column_name).sub( - "#{FUNCTION_SCHEMA_NAME}.", '' - ) + readonly_function_name(table_name, column_name).sub("#{FUNCTION_SCHEMA_NAME}.", "") end def self.readonly_trigger_name(table_name, column_name = nil) @@ -52,7 +50,7 @@ module Migration end def self.function_schema_exists? - DB.exec(<<~SQL).to_s == '1' + DB.exec(<<~SQL).to_s == "1" SELECT schema_name FROM information_schema.schemata WHERE schema_name = '#{FUNCTION_SCHEMA_NAME}' diff --git a/lib/migration/column_dropper.rb b/lib/migration/column_dropper.rb index 71e472baa49..83d2feec640 100644 --- a/lib/migration/column_dropper.rb +++ b/lib/migration/column_dropper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'migration/base_dropper' +require "migration/base_dropper" module Migration class ColumnDropper @@ -32,7 +32,9 @@ module Migration BaseDropper.drop_readonly_function(table_name, column_name) # Backward compatibility for old functions created in the public schema - DB.exec("DROP FUNCTION IF EXISTS #{BaseDropper.old_readonly_function_name(table_name, column_name)} CASCADE") + DB.exec( + "DROP FUNCTION IF EXISTS #{BaseDropper.old_readonly_function_name(table_name, column_name)} CASCADE", + ) end end end diff --git a/lib/migration/safe_migrate.rb b/lib/migration/safe_migrate.rb index 0c8105e5593..ce3013300e0 100644 --- a/lib/migration/safe_migrate.rb +++ b/lib/migration/safe_migrate.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true -module Migration; end +module Migration +end -class Discourse::InvalidMigration < StandardError; end +class Discourse::InvalidMigration < StandardError +end class Migration::SafeMigrate module SafeMigration @@ -17,11 +19,9 @@ class Migration::SafeMigrate end def migrate(direction) - if direction == :up && - version && version > Migration::SafeMigrate.earliest_post_deploy_version && - @@enable_safe != false && - !is_post_deploy_migration? - + if direction == :up && version && + version > Migration::SafeMigrate.earliest_post_deploy_version && + @@enable_safe != false && !is_post_deploy_migration? Migration::SafeMigrate.enable! end @@ -44,9 +44,7 @@ class Migration::SafeMigrate return false if !method - self.method(method).source_location.first.include?( - Discourse::DB_POST_MIGRATE_PATH - ) + self.method(method).source_location.first.include?(Discourse::DB_POST_MIGRATE_PATH) end end @@ -76,7 +74,7 @@ class Migration::SafeMigrate def self.enable! return if PG::Connection.method_defined?(:exec_migrator_unpatched) - return if ENV['RAILS_ENV'] == "production" + return if ENV["RAILS_ENV"] == "production" PG::Connection.class_eval do alias_method :exec_migrator_unpatched, :exec @@ -96,7 +94,7 @@ class Migration::SafeMigrate def self.disable! return if !PG::Connection.method_defined?(:exec_migrator_unpatched) - return if ENV['RAILS_ENV'] == "production" + return if ENV["RAILS_ENV"] == "production" PG::Connection.class_eval do alias_method :exec, :exec_migrator_unpatched @@ -108,11 +106,9 @@ class Migration::SafeMigrate end def self.patch_active_record! - return if ENV['RAILS_ENV'] == "production" + return if ENV["RAILS_ENV"] == "production" - ActiveSupport.on_load(:active_record) do - ActiveRecord::Migration.prepend(SafeMigration) - end + ActiveSupport.on_load(:active_record) { ActiveRecord::Migration.prepend(SafeMigration) } if defined?(ActiveRecord::Tasks::DatabaseTasks) ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(NiceErrors) @@ -154,10 +150,11 @@ class Migration::SafeMigrate end def self.earliest_post_deploy_version - @@earliest_post_deploy_version ||= begin - first_file = Dir.glob("#{Discourse::DB_POST_MIGRATE_PATH}/*.rb").sort.first - file_name = File.basename(first_file, ".rb") - file_name.first(14).to_i - end + @@earliest_post_deploy_version ||= + begin + first_file = Dir.glob("#{Discourse::DB_POST_MIGRATE_PATH}/*.rb").sort.first + file_name = File.basename(first_file, ".rb") + file_name.first(14).to_i + end end end diff --git a/lib/migration/table_dropper.rb b/lib/migration/table_dropper.rb index bbd27f02e5b..f2f9849cb9b 100644 --- a/lib/migration/table_dropper.rb +++ b/lib/migration/table_dropper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'migration/base_dropper' +require "migration/base_dropper" module Migration class Migration::TableDropper diff --git a/lib/mini_sql_multisite_connection.rb b/lib/mini_sql_multisite_connection.rb index 26ad1d50263..2b8ef82066a 100644 --- a/lib/mini_sql_multisite_connection.rb +++ b/lib/mini_sql_multisite_connection.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection - class CustomBuilder < MiniSql::Builder - def initialize(connection, sql) super end - def secure_category(secure_category_ids, category_alias = 'c') + def secure_category(secure_category_ids, category_alias = "c") if secure_category_ids.present? - where("NOT COALESCE(#{category_alias}.read_restricted, false) OR #{category_alias}.id in (:secure_category_ids)", secure_category_ids: secure_category_ids) + where( + "NOT COALESCE(#{category_alias}.read_restricted, false) OR #{category_alias}.id in (:secure_category_ids)", + secure_category_ids: secure_category_ids, + ) else where("NOT COALESCE(#{category_alias}.read_restricted, false)") end @@ -40,8 +41,10 @@ class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection end end - def before_committed!(*); end - def rolledback!(*); end + def before_committed!(*) + end + def rolledback!(*) + end def trigger_transactional_callbacks? true end @@ -67,9 +70,7 @@ class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection def after_commit(&blk) return blk.call if !transaction_open? - ActiveRecord::Base.connection.add_transaction_record( - AfterCommitWrapper.new(&blk) - ) + ActiveRecord::Base.connection.add_transaction_record(AfterCommitWrapper.new(&blk)) end def self.instance @@ -107,5 +108,4 @@ class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection query end end - end diff --git a/lib/mobile_detection.rb b/lib/mobile_detection.rb index 0fb621dd377..1ff3361a15d 100644 --- a/lib/mobile_detection.rb +++ b/lib/mobile_detection.rb @@ -10,10 +10,11 @@ module MobileDetection return false unless SiteSetting.enable_mobile_theme session[:mobile_view] = params[:mobile_view] if params && params.has_key?(:mobile_view) - session[:mobile_view] = nil if params && params.has_key?(:mobile_view) && params[:mobile_view] == 'auto' + session[:mobile_view] = nil if params && params.has_key?(:mobile_view) && + params[:mobile_view] == "auto" if session && session[:mobile_view] - session[:mobile_view] == '1' + session[:mobile_view] == "1" else mobile_device?(user_agent) end @@ -23,7 +24,8 @@ module MobileDetection user_agent =~ /iPad|iPhone|iPod/ end - MODERN_MOBILE_REGEX = %r{ + MODERN_MOBILE_REGEX = + %r{ \(.*iPhone\ OS\ 1[3-9].*\)| \(.*iPad.*OS\ 1[3-9].*\)| Chrome\/8[89]| @@ -37,5 +39,4 @@ module MobileDetection def self.modern_mobile_device?(user_agent) user_agent.match?(MODERN_MOBILE_REGEX) end - end diff --git a/lib/new_post_manager.rb b/lib/new_post_manager.rb index 777d93bdb0d..3903c5dd53d 100644 --- a/lib/new_post_manager.rb +++ b/lib/new_post_manager.rb @@ -6,7 +6,6 @@ # with `NewPostManager.add_handler` to take other approaches depending # on the user or input. class NewPostManager - attr_reader :user, :args def self.sorted_handlers @@ -38,24 +37,21 @@ class NewPostManager user = manager.user args = manager.args - !!( - args[:first_post_checks] && - user.post_count == 0 && - user.topic_count == 0 - ) + !!(args[:first_post_checks] && user.post_count == 0 && user.topic_count == 0) end def self.is_fast_typer?(manager) args = manager.args is_first_post?(manager) && - args[:typing_duration_msecs].to_i < SiteSetting.min_first_post_typing_time && - SiteSetting.auto_silence_fast_typers_on_first_post && - manager.user.trust_level <= SiteSetting.auto_silence_fast_typers_max_trust_level + args[:typing_duration_msecs].to_i < SiteSetting.min_first_post_typing_time && + SiteSetting.auto_silence_fast_typers_on_first_post && + manager.user.trust_level <= SiteSetting.auto_silence_fast_typers_max_trust_level end def self.auto_silence?(manager) - is_first_post?(manager) && WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").should_silence? + is_first_post?(manager) && + WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").should_silence? end def self.matches_auto_silence_regex?(manager) @@ -74,7 +70,6 @@ class NewPostManager end "#{args[:title]} #{args[:raw]}" =~ regex - end def self.exempt_user?(user) @@ -90,19 +85,25 @@ class NewPostManager return :email_spam if manager.args[:email_spam] - return :post_count if ( - user.trust_level <= TrustLevel.levels[:basic] && - (user.post_count + user.topic_count) < SiteSetting.approve_post_count - ) + if ( + user.trust_level <= TrustLevel.levels[:basic] && + (user.post_count + user.topic_count) < SiteSetting.approve_post_count + ) + return :post_count + end return :trust_level if user.trust_level < SiteSetting.approve_unless_trust_level.to_i - return :new_topics_unless_trust_level if ( - manager.args[:title].present? && - user.trust_level < SiteSetting.approve_new_topics_unless_trust_level.to_i - ) + if ( + manager.args[:title].present? && + user.trust_level < SiteSetting.approve_new_topics_unless_trust_level.to_i + ) + return :new_topics_unless_trust_level + end - return :watched_word if WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").requires_approval? + if WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").requires_approval? + return :watched_word + end return :fast_typer if is_fast_typer?(manager) @@ -112,10 +113,12 @@ class NewPostManager return :category if post_needs_approval_in_its_category?(manager) - return :contains_media if ( - manager.args[:image_sizes].present? && - user.trust_level < SiteSetting.review_media_unless_trust_level.to_i - ) + if ( + manager.args[:image_sizes].present? && + user.trust_level < SiteSetting.review_media_unless_trust_level.to_i + ) + return :contains_media + end :skip end @@ -136,7 +139,6 @@ class NewPostManager end def self.default_handler(manager) - reason = post_needs_approval?(manager) return if reason == :skip @@ -171,11 +173,26 @@ class NewPostManager I18n.with_locale(SiteSetting.default_locale) do if is_fast_typer?(manager) - UserSilencer.silence(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.new_user_typed_too_fast")) + UserSilencer.silence( + manager.user, + Discourse.system_user, + keep_posts: true, + reason: I18n.t("user.new_user_typed_too_fast"), + ) elsif auto_silence?(manager) || matches_auto_silence_regex?(manager) - UserSilencer.silence(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.content_matches_auto_silence_regex")) + UserSilencer.silence( + manager.user, + Discourse.system_user, + keep_posts: true, + reason: I18n.t("user.content_matches_auto_silence_regex"), + ) elsif reason == :email_spam && is_first_post?(manager) - UserSilencer.silence(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.email_in_spam_header")) + UserSilencer.silence( + manager.user, + Discourse.system_user, + keep_posts: true, + reason: I18n.t("user.email_in_spam_header"), + ) end end @@ -183,12 +200,10 @@ class NewPostManager end def self.queue_enabled? - SiteSetting.approve_post_count > 0 || - SiteSetting.approve_unless_trust_level.to_i > 0 || - SiteSetting.approve_new_topics_unless_trust_level.to_i > 0 || - SiteSetting.approve_unless_staged || - WordWatcher.words_for_action_exists?(:require_approval) || - handlers.size > 1 + SiteSetting.approve_post_count > 0 || SiteSetting.approve_unless_trust_level.to_i > 0 || + SiteSetting.approve_new_topics_unless_trust_level.to_i > 0 || + SiteSetting.approve_unless_staged || + WordWatcher.words_for_action_exists?(:require_approval) || handlers.size > 1 end def initialize(user, args) @@ -197,14 +212,15 @@ class NewPostManager end def perform - if !self.class.exempt_user?(@user) && matches = WordWatcher.new("#{@args[:title]} #{@args[:raw]}").should_block?.presence + if !self.class.exempt_user?(@user) && + matches = WordWatcher.new("#{@args[:title]} #{@args[:raw]}").should_block?.presence result = NewPostResult.new(:created_post, false) if matches.size == 1 - key = 'contains_blocked_word' + key = "contains_blocked_word" translation_args = { word: CGI.escapeHTML(matches[0]) } else - key = 'contains_blocked_words' - translation_args = { words: CGI.escapeHTML(matches.join(', ')) } + key = "contains_blocked_words" + translation_args = { words: CGI.escapeHTML(matches.join(", ")) } end result.errors.add(:base, I18n.t(key, translation_args)) return result @@ -217,8 +233,13 @@ class NewPostManager end # We never queue private messages - return perform_create_post if @args[:archetype] == Archetype.private_message || - (args[:topic_id] && Topic.where(id: args[:topic_id], archetype: Archetype.private_message).exists?) + if @args[:archetype] == Archetype.private_message || + ( + args[:topic_id] && + Topic.where(id: args[:topic_id], archetype: Archetype.private_message).exists? + ) + return perform_create_post + end NewPostManager.default_handler(self) || perform_create_post end @@ -226,11 +247,8 @@ class NewPostManager # Enqueue this post def enqueue(reason = nil) result = NewPostResult.new(:enqueued) - payload = { - raw: @args[:raw], - tags: @args[:tags] - } - %w(typing_duration_msecs composer_open_duration_msecs reply_to_post_number).each do |a| + payload = { raw: @args[:raw], tags: @args[:tags] } + %w[typing_duration_msecs composer_open_duration_msecs reply_to_post_number].each do |a| payload[a] = @args[a].to_i if @args[a] end @@ -239,21 +257,27 @@ class NewPostManager payload[:via_email] = true if !!@args[:via_email] payload[:raw_email] = @args[:raw_email] if @args[:raw_email].present? - reviewable = ReviewableQueuedPost.new( - created_by: @user, - payload: payload, - topic_id: @args[:topic_id], - reviewable_by_moderator: true - ) - reviewable.payload['title'] = @args[:title] if @args[:title].present? + reviewable = + ReviewableQueuedPost.new( + created_by: @user, + payload: payload, + topic_id: @args[:topic_id], + reviewable_by_moderator: true, + ) + reviewable.payload["title"] = @args[:title] if @args[:title].present? reviewable.category_id = args[:category] if args[:category].present? reviewable.created_new! create_options = reviewable.create_options - creator = @args[:topic_id] ? - PostCreator.new(@user, create_options) : - TopicCreator.new(@user, Guardian.new(@user), create_options) + creator = + ( + if @args[:topic_id] + PostCreator.new(@user, create_options) + else + TopicCreator.new(@user, Guardian.new(@user), create_options) + end + ) errors = Set.new creator.valid? @@ -265,7 +289,7 @@ class NewPostManager Discourse.system_user, ReviewableScore.types[:needs_approval], reason: reason, - force_review: true + force_review: true, ) else reviewable.errors.full_messages.each { |msg| errors << msg } @@ -293,5 +317,4 @@ class NewPostManager result end - end diff --git a/lib/new_post_result.rb b/lib/new_post_result.rb index 4fa50b24863..861fcc5ff94 100644 --- a/lib/new_post_result.rb +++ b/lib/new_post_result.rb @@ -37,7 +37,7 @@ class NewPostResult Discourse.deprecate( "NewPostManager#queued_post is deprecated. Please use #reviewable instead.", output_in_test: true, - drop_from: '2.9.0', + drop_from: "2.9.0", ) reviewable @@ -50,5 +50,4 @@ class NewPostResult def failed? !@success end - end diff --git a/lib/notification_levels.rb b/lib/notification_levels.rb index a9499506640..9b43e6be706 100644 --- a/lib/notification_levels.rb +++ b/lib/notification_levels.rb @@ -2,19 +2,25 @@ module NotificationLevels def self.all - @all_levels ||= Enum.new(muted: 0, - regular: 1, - normal: 1, # alias for regular - tracking: 2, - watching: 3, - watching_first_post: 4) + @all_levels ||= + Enum.new( + muted: 0, + regular: 1, + normal: 1, # alias for regular + tracking: 2, + watching: 3, + watching_first_post: 4, + ) end def self.topic_levels - @topic_levels ||= Enum.new(muted: 0, - regular: 1, - normal: 1, # alias for regular - tracking: 2, - watching: 3) + @topic_levels ||= + Enum.new( + muted: 0, + regular: 1, + normal: 1, # alias for regular + tracking: 2, + watching: 3, + ) end end diff --git a/lib/onebox.rb b/lib/onebox.rb index e6e0eed187a..feee7c8e6da 100644 --- a/lib/onebox.rb +++ b/lib/onebox.rb @@ -19,9 +19,9 @@ module Onebox max_download_kb: (10 * 1024), # 10MB load_paths: [File.join(Rails.root, "lib/onebox/templates")], allowed_ports: [80, 443], - allowed_schemes: ["http", "https"], + allowed_schemes: %w[http https], sanitize_config: SanitizeConfig::ONEBOX, - redirect_limit: 5 + redirect_limit: 5, } @@options = DEFAULTS diff --git a/lib/onebox/domain_checker.rb b/lib/onebox/domain_checker.rb index 1dd810491e2..6e6c99c3e39 100644 --- a/lib/onebox/domain_checker.rb +++ b/lib/onebox/domain_checker.rb @@ -3,9 +3,10 @@ module Onebox class DomainChecker def self.is_blocked?(hostname) - SiteSetting.blocked_onebox_domains&.split('|').any? do |blocked| - hostname == blocked || hostname.end_with?(".#{blocked}") - end + SiteSetting + .blocked_onebox_domains + &.split("|") + .any? { |blocked| hostname == blocked || hostname.end_with?(".#{blocked}") } end end end diff --git a/lib/onebox/engine.rb b/lib/onebox/engine.rb index e28807c9889..838986e685b 100644 --- a/lib/onebox/engine.rb +++ b/lib/onebox/engine.rb @@ -7,9 +7,7 @@ module Onebox end def self.engines - constants.select do |constant| - constant.to_s =~ /Onebox$/ - end.sort.map(&method(:const_get)) + constants.select { |constant| constant.to_s =~ /Onebox$/ }.sort.map(&method(:const_get)) end def self.all_iframe_origins @@ -25,7 +23,7 @@ module Onebox escaped_origin = escaped_origin.sub("\\*", '\S*') end - Regexp.new("\\A#{escaped_origin}", 'i') + Regexp.new("\\A#{escaped_origin}", "i") end end @@ -50,7 +48,7 @@ module Onebox @url = url @uri = URI(url) if always_https? - @uri.scheme = 'https' + @uri.scheme = "https" @url = @uri.to_s end @timeout = timeout || Onebox.options.timeout diff --git a/lib/onebox/engine/allowlisted_generic_onebox.rb b/lib/onebox/engine/allowlisted_generic_onebox.rb index aa25dd54a6a..3c8a2359eba 100644 --- a/lib/onebox/engine/allowlisted_generic_onebox.rb +++ b/lib/onebox/engine/allowlisted_generic_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'htmlentities' +require "htmlentities" require "ipaddr" module Onebox @@ -18,7 +18,7 @@ module Onebox # include the entire page HTML. However for some providers like Flickr it allows us # to return gifv and galleries. def self.default_html_providers - ['Flickr', 'Meetup'] + %w[Flickr Meetup] end def self.html_providers @@ -39,23 +39,33 @@ module Onebox end def self.https_hosts - %w(slideshare.net dailymotion.com livestream.com imgur.com flickr.com) + %w[slideshare.net dailymotion.com livestream.com imgur.com flickr.com] end def self.article_html_hosts - %w(imdb.com) + %w[imdb.com] end def self.host_matches(uri, list) - !!list.find { |h| %r((^|\.)#{Regexp.escape(h)}$).match(uri.host) } + !!list.find { |h| /(^|\.)#{Regexp.escape(h)}$/.match(uri.host) } end def self.allowed_twitter_labels - ['brand', 'price', 'usd', 'cad', 'reading time', 'likes'] + ["brand", "price", "usd", "cad", "reading time", "likes"] end def self.===(other) - other.is_a?(URI) ? (IPAddr.new(other.hostname) rescue nil).nil? : true + if other.is_a?(URI) + ( + begin + IPAddr.new(other.hostname) + rescue StandardError + nil + end + ).nil? + else + true + end end def to_html @@ -65,8 +75,12 @@ module Onebox def placeholder_html return article_html if (is_article? || force_article_html?) return image_html if is_image? - return Onebox::Helpers.video_placeholder_html if !SiteSetting.enable_diffhtml_preview? && (is_video? || is_card?) - return Onebox::Helpers.generic_placeholder_html if !SiteSetting.enable_diffhtml_preview? && is_embedded? + if !SiteSetting.enable_diffhtml_preview? && (is_video? || is_card?) + return Onebox::Helpers.video_placeholder_html + end + if !SiteSetting.enable_diffhtml_preview? && is_embedded? + return Onebox::Helpers.generic_placeholder_html + end to_html end @@ -75,72 +89,90 @@ module Onebox end def data - @data ||= begin - html_entities = HTMLEntities.new - d = { link: link }.merge(raw) + @data ||= + begin + html_entities = HTMLEntities.new + d = { link: link }.merge(raw) - if !Onebox::Helpers.blank?(d[:title]) - d[:title] = html_entities.decode(Onebox::Helpers.truncate(d[:title], 80)) - end - - d[:description] ||= d[:summary] - if !Onebox::Helpers.blank?(d[:description]) - d[:description] = html_entities.decode(Onebox::Helpers.truncate(d[:description], 250)) - end - - if !Onebox::Helpers.blank?(d[:site_name]) - d[:domain] = html_entities.decode(Onebox::Helpers.truncate(d[:site_name], 80)) - elsif !Onebox::Helpers.blank?(d[:domain]) - d[:domain] = "http://#{d[:domain]}" unless d[:domain] =~ /^https?:\/\// - d[:domain] = URI(d[:domain]).host.to_s.sub(/^www\./, '') rescue nil - end - - # prefer secure URLs - d[:image] = d[:image_secure_url] || d[:image_url] || d[:thumbnail_url] || d[:image] - d[:image] = Onebox::Helpers::get_absolute_image_url(d[:image], @url) - d[:image] = Onebox::Helpers::normalize_url_for_output(html_entities.decode(d[:image])) - d[:image] = nil if Onebox::Helpers.blank?(d[:image]) - - d[:video] = d[:video_secure_url] || d[:video_url] || d[:video] - d[:video] = nil if Onebox::Helpers.blank?(d[:video]) - - d[:published_time] = d[:article_published_time] unless Onebox::Helpers.blank?(d[:article_published_time]) - if !Onebox::Helpers.blank?(d[:published_time]) - d[:article_published_time] = Time.parse(d[:published_time]).strftime("%-d %b %y") - d[:article_published_time_title] = Time.parse(d[:published_time]).strftime("%I:%M%p - %d %B %Y") - end - - # Twitter labels - if !Onebox::Helpers.blank?(d[:label1]) && !Onebox::Helpers.blank?(d[:data1]) && !!AllowlistedGenericOnebox.allowed_twitter_labels.find { |l| d[:label1] =~ /#{l}/i } - d[:label_1] = Onebox::Helpers.truncate(d[:label1]) - d[:data_1] = Onebox::Helpers.truncate(d[:data1]) - end - if !Onebox::Helpers.blank?(d[:label2]) && !Onebox::Helpers.blank?(d[:data2]) && !!AllowlistedGenericOnebox.allowed_twitter_labels.find { |l| d[:label2] =~ /#{l}/i } - unless Onebox::Helpers.blank?(d[:label_1]) - d[:label_2] = Onebox::Helpers.truncate(d[:label2]) - d[:data_2] = Onebox::Helpers.truncate(d[:data2]) - else - d[:label_1] = Onebox::Helpers.truncate(d[:label2]) - d[:data_1] = Onebox::Helpers.truncate(d[:data2]) + if !Onebox::Helpers.blank?(d[:title]) + d[:title] = html_entities.decode(Onebox::Helpers.truncate(d[:title], 80)) end - end - if Onebox::Helpers.blank?(d[:label_1]) && !Onebox::Helpers.blank?(d[:price_amount]) && !Onebox::Helpers.blank?(d[:price_currency]) - d[:label_1] = "Price" - d[:data_1] = Onebox::Helpers.truncate("#{d[:price_currency].strip} #{d[:price_amount].strip}") - end - - skip_missing_tags = [:video] - d.each do |k, v| - next if skip_missing_tags.include?(k) - if v == nil || v == '' - errors[k] ||= [] - errors[k] << 'is blank' + d[:description] ||= d[:summary] + if !Onebox::Helpers.blank?(d[:description]) + d[:description] = html_entities.decode(Onebox::Helpers.truncate(d[:description], 250)) end - end - d - end + if !Onebox::Helpers.blank?(d[:site_name]) + d[:domain] = html_entities.decode(Onebox::Helpers.truncate(d[:site_name], 80)) + elsif !Onebox::Helpers.blank?(d[:domain]) + d[:domain] = "http://#{d[:domain]}" unless d[:domain] =~ %r{^https?://} + d[:domain] = begin + URI(d[:domain]).host.to_s.sub(/^www\./, "") + rescue StandardError + nil + end + end + + # prefer secure URLs + d[:image] = d[:image_secure_url] || d[:image_url] || d[:thumbnail_url] || d[:image] + d[:image] = Onebox::Helpers.get_absolute_image_url(d[:image], @url) + d[:image] = Onebox::Helpers.normalize_url_for_output(html_entities.decode(d[:image])) + d[:image] = nil if Onebox::Helpers.blank?(d[:image]) + + d[:video] = d[:video_secure_url] || d[:video_url] || d[:video] + d[:video] = nil if Onebox::Helpers.blank?(d[:video]) + + d[:published_time] = d[:article_published_time] unless Onebox::Helpers.blank?( + d[:article_published_time], + ) + if !Onebox::Helpers.blank?(d[:published_time]) + d[:article_published_time] = Time.parse(d[:published_time]).strftime("%-d %b %y") + d[:article_published_time_title] = Time.parse(d[:published_time]).strftime( + "%I:%M%p - %d %B %Y", + ) + end + + # Twitter labels + if !Onebox::Helpers.blank?(d[:label1]) && !Onebox::Helpers.blank?(d[:data1]) && + !!AllowlistedGenericOnebox.allowed_twitter_labels.find { |l| + d[:label1] =~ /#{l}/i + } + d[:label_1] = Onebox::Helpers.truncate(d[:label1]) + d[:data_1] = Onebox::Helpers.truncate(d[:data1]) + end + if !Onebox::Helpers.blank?(d[:label2]) && !Onebox::Helpers.blank?(d[:data2]) && + !!AllowlistedGenericOnebox.allowed_twitter_labels.find { |l| + d[:label2] =~ /#{l}/i + } + unless Onebox::Helpers.blank?(d[:label_1]) + d[:label_2] = Onebox::Helpers.truncate(d[:label2]) + d[:data_2] = Onebox::Helpers.truncate(d[:data2]) + else + d[:label_1] = Onebox::Helpers.truncate(d[:label2]) + d[:data_1] = Onebox::Helpers.truncate(d[:data2]) + end + end + + if Onebox::Helpers.blank?(d[:label_1]) && !Onebox::Helpers.blank?(d[:price_amount]) && + !Onebox::Helpers.blank?(d[:price_currency]) + d[:label_1] = "Price" + d[:data_1] = Onebox::Helpers.truncate( + "#{d[:price_currency].strip} #{d[:price_amount].strip}", + ) + end + + skip_missing_tags = [:video] + d.each do |k, v| + next if skip_missing_tags.include?(k) + if v == nil || v == "" + errors[k] ||= [] + errors[k] << "is blank" + end + end + + d + end end private @@ -154,23 +186,21 @@ module Onebox end def generic_html - return article_html if (is_article? || force_article_html?) - return video_html if is_video? - return image_html if is_image? + return article_html if (is_article? || force_article_html?) + return video_html if is_video? + return image_html if is_image? return embedded_html if is_embedded? - return card_html if is_card? - return article_html if (has_text? || is_image_article?) + return card_html if is_card? + return article_html if (has_text? || is_image_article?) end def is_card? - data[:card] == 'player' && - data[:player] =~ URI::regexp && + data[:card] == "player" && data[:player] =~ URI.regexp && options[:allowed_iframe_regexes]&.any? { |r| data[:player] =~ r } end def is_article? - (data[:type] =~ /article/ || data[:asset_type] =~ /article/) && - has_text? + (data[:type] =~ /article/ || data[:asset_type] =~ /article/) && has_text? end def has_text? @@ -186,9 +216,7 @@ module Onebox end def is_image? - data[:type] =~ /photo|image/ && - data[:type] !~ /photostream/ && - has_image? + data[:type] =~ /photo|image/ && data[:type] !~ /photostream/ && has_image? end def has_image? @@ -196,8 +224,7 @@ module Onebox end def is_video? - data[:type] =~ /^video[\/\.]/ && - data[:video_type] == "video/mp4" && # Many sites include 'videos' with text/html types (i.e. iframes) + data[:type] =~ %r{^video[/\.]} && data[:video_type] == "video/mp4" && # Many sites include 'videos' with text/html types (i.e. iframes) !Onebox::Helpers.blank?(data[:video]) end @@ -206,13 +233,14 @@ module Onebox return true if AllowlistedGenericOnebox.html_providers.include?(data[:provider_name]) return false unless data[:html]["iframe"] - fragment = Nokogiri::HTML5::fragment(data[:html]) - src = fragment.at_css('iframe')&.[]("src") + fragment = Nokogiri::HTML5.fragment(data[:html]) + src = fragment.at_css("iframe")&.[]("src") options[:allowed_iframe_regexes]&.any? { |r| src =~ r } end def force_article_html? - AllowlistedGenericOnebox.host_matches(uri, AllowlistedGenericOnebox.article_html_hosts) && (has_text? || is_image_article?) + AllowlistedGenericOnebox.host_matches(uri, AllowlistedGenericOnebox.article_html_hosts) && + (has_text? || is_image_article?) end def card_html @@ -237,8 +265,8 @@ module Onebox escaped_src = ::Onebox::Helpers.normalize_url_for_output(data[:image]) - alt = data[:description] || data[:title] - width = data[:image_width] || data[:thumbnail_width] || data[:width] + alt = data[:description] || data[:title] + width = data[:image_width] || data[:thumbnail_width] || data[:width] height = data[:image_height] || data[:thumbnail_height] || data[:height] "#{alt}" @@ -263,7 +291,7 @@ module Onebox end def embedded_html - fragment = Nokogiri::HTML5::fragment(data[:html]) + fragment = Nokogiri::HTML5.fragment(data[:html]) fragment.css("img").each { |img| img["class"] = "thumbnail" } if iframe = fragment.at_css("iframe") iframe.remove_attribute("style") diff --git a/lib/onebox/engine/amazon_onebox.rb b/lib/onebox/engine/amazon_onebox.rb index 9f1aca9f833..ae3ae9e36f5 100644 --- a/lib/onebox/engine/amazon_onebox.rb +++ b/lib/onebox/engine/amazon_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'json' +require "json" require "onebox/open_graph" module Onebox @@ -11,7 +11,9 @@ module Onebox include HTML always_https - matches_regexp(/^https?:\/\/(?:www\.)?(?:smile\.)?(amazon|amzn)\.(?com|ca|de|it|es|fr|co\.jp|co\.uk|cn|in|com\.br|com\.mx|nl|pl|sa|sg|se|com\.tr|ae)\//) + matches_regexp( + %r{^https?://(?:www\.)?(?:smile\.)?(amazon|amzn)\.(?com|ca|de|it|es|fr|co\.jp|co\.uk|cn|in|com\.br|com\.mx|nl|pl|sa|sg|se|com\.tr|ae)/}, + ) def url @raw ||= nil @@ -29,7 +31,8 @@ module Onebox end if match && match[:id] - id = Addressable::URI.encode_component(match[:id], Addressable::URI::CharacterClasses::PATH) + id = + Addressable::URI.encode_component(match[:id], Addressable::URI::CharacterClasses::PATH) return "https://www.amazon.#{tld}/dp/#{id}" end @@ -41,15 +44,13 @@ module Onebox end def http_params - if @options && @options[:user_agent] - { 'User-Agent' => @options[:user_agent] } - end + { "User-Agent" => @options[:user_agent] } if @options && @options[:user_agent] end def to_html(ignore_errors = false) unless ignore_errors verified_data # forces a check for missing fields - return '' unless errors.empty? + return "" unless errors.empty? end super() @@ -60,19 +61,20 @@ module Onebox end def verified_data - @verified_data ||= begin - result = data + @verified_data ||= + begin + result = data - required_tags = [:title, :description] - required_tags.each do |tag| - if result[tag].blank? - errors[tag] ||= [] - errors[tag] << 'is blank' + required_tags = %i[title description] + required_tags.each do |tag| + if result[tag].blank? + errors[tag] ||= [] + errors[tag] << "is blank" + end end - end - result - end + result + end @verified_data end @@ -80,13 +82,13 @@ module Onebox private def has_cached_body - body_cacher&.respond_to?('cache_response_body?') && + body_cacher&.respond_to?("cache_response_body?") && body_cacher.cache_response_body?(uri.to_s) && body_cacher.cached_response_body_exists?(uri.to_s) end def match - @match ||= @url.match(/(?:d|g)p\/(?:product\/|video\/detail\/)?(?[A-Z0-9]+)(?:\/|\?|$)/mi) + @match ||= @url.match(%r{(?:d|g)p/(?:product/|video/detail/)?(?[A-Z0-9]+)(?:/|\?|$)}mi) end def image @@ -117,14 +119,16 @@ module Onebox def price # get item price (Amazon markup is inconsistent, deal with it) - if raw.css("#priceblock_ourprice .restOfPrice")[0] && raw.css("#priceblock_ourprice .restOfPrice")[0].inner_text + if raw.css("#priceblock_ourprice .restOfPrice")[0] && + raw.css("#priceblock_ourprice .restOfPrice")[0].inner_text "#{raw.css("#priceblock_ourprice .restOfPrice")[0].inner_text}#{raw.css("#priceblock_ourprice .buyingPrice")[0].inner_text}.#{raw.css("#priceblock_ourprice .restOfPrice")[1].inner_text}" - elsif raw.css("#priceblock_dealprice") && (dealprice = raw.css("#priceblock_dealprice span")[0]) + elsif raw.css("#priceblock_dealprice") && + (dealprice = raw.css("#priceblock_dealprice span")[0]) dealprice.inner_text elsif !raw.css("#priceblock_ourprice").inner_text.empty? raw.css("#priceblock_ourprice").inner_text else - result = raw.css('#corePrice_feature_div .a-price .a-offscreen').first&.inner_text + result = raw.css("#corePrice_feature_div .a-price .a-offscreen").first&.inner_text if result.blank? result = raw.css(".mediaMatrixListItem.a-active .a-color-price").inner_text end @@ -134,21 +138,30 @@ module Onebox end def multiple_authors(authors_xpath) - raw - .xpath(authors_xpath) - .map { |a| a.inner_text.strip } - .join(", ") + raw.xpath(authors_xpath).map { |a| a.inner_text.strip }.join(", ") end def data og = ::Onebox::OpenGraph.new(raw) - if raw.at_css('#dp.book_mobile') # printed books + if raw.at_css("#dp.book_mobile") # printed books title = raw.at("h1#title")&.inner_text - authors = raw.at_css('#byline_secondary_view_div') ? multiple_authors("//div[@id='byline_secondary_view_div']//span[@class='a-text-bold']") : raw.at("#byline")&.inner_text - rating = raw.at("#averageCustomerReviews_feature_div .a-icon")&.inner_text || raw.at("#cmrsArcLink .a-icon")&.inner_text + authors = + ( + if raw.at_css("#byline_secondary_view_div") + multiple_authors( + "//div[@id='byline_secondary_view_div']//span[@class='a-text-bold']", + ) + else + raw.at("#byline")&.inner_text + end + ) + rating = + raw.at("#averageCustomerReviews_feature_div .a-icon")&.inner_text || + raw.at("#cmrsArcLink .a-icon")&.inner_text - table_xpath = "//div[@id='productDetails_secondary_view_div']//table[@id='productDetails_techSpec_section_1']" + table_xpath = + "//div[@id='productDetails_secondary_view_div']//table[@id='productDetails_techSpec_section_1']" isbn = raw.xpath("#{table_xpath}//tr[8]//td").inner_text.strip # if ISBN is misplaced or absent it's hard to find out which data is @@ -167,18 +180,29 @@ module Onebox by_info: authors, image: og.image || image, description: raw.at("#productDescription")&.inner_text, - rating: "#{rating}#{', ' if rating && (!isbn&.empty? || !price&.empty?)}", + rating: "#{rating}#{", " if rating && (!isbn&.empty? || !price&.empty?)}", price: price, isbn_asin_text: "ISBN", isbn_asin: isbn, publisher: publisher, - published: "#{published}#{', ' if published && !price&.empty?}" + published: "#{published}#{", " if published && !price&.empty?}", } - - elsif raw.at_css('#dp.ebooks_mobile') # ebooks + elsif raw.at_css("#dp.ebooks_mobile") # ebooks title = raw.at("#ebooksTitle")&.inner_text - authors = raw.at_css('#a-popover-mobile-udp-contributor-popover-id') ? multiple_authors("//div[@id='a-popover-mobile-udp-contributor-popover-id']//span[contains(@class,'a-text-bold')]") : (raw.at("#byline")&.inner_text&.strip || raw.at("#bylineInfo")&.inner_text&.strip) - rating = raw.at("#averageCustomerReviews_feature_div .a-icon")&.inner_text || raw.at("#cmrsArcLink .a-icon")&.inner_text || raw.at("#acrCustomerReviewLink .a-icon")&.inner_text + authors = + ( + if raw.at_css("#a-popover-mobile-udp-contributor-popover-id") + multiple_authors( + "//div[@id='a-popover-mobile-udp-contributor-popover-id']//span[contains(@class,'a-text-bold')]", + ) + else + (raw.at("#byline")&.inner_text&.strip || raw.at("#bylineInfo")&.inner_text&.strip) + end + ) + rating = + raw.at("#averageCustomerReviews_feature_div .a-icon")&.inner_text || + raw.at("#cmrsArcLink .a-icon")&.inner_text || + raw.at("#acrCustomerReviewLink .a-icon")&.inner_text table_xpath = "//div[@id='detailBullets_secondary_view_div']//ul" asin = raw.xpath("#{table_xpath}//li[4]/span/span[2]").inner_text @@ -198,22 +222,16 @@ module Onebox by_info: authors, image: og.image || image, description: raw.at("#productDescription")&.inner_text, - rating: "#{rating}#{', ' if rating && (!asin&.empty? || !price&.empty?)}", + rating: "#{rating}#{", " if rating && (!asin&.empty? || !price&.empty?)}", price: price, isbn_asin_text: "ASIN", isbn_asin: asin, publisher: publisher, - published: "#{published}#{', ' if published && !price&.empty?}" + published: "#{published}#{", " if published && !price&.empty?}", } - else title = og.title || CGI.unescapeHTML(raw.css("title").inner_text) - result = { - link: url, - title: title, - image: og.image || image, - price: price - } + result = { link: url, title: title, image: og.image || image, price: price } result[:by_info] = raw.at("#by-line") result[:by_info] = Onebox::Helpers.clean(result[:by_info].inner_html) if result[:by_info] @@ -221,10 +239,10 @@ module Onebox summary = raw.at("#productDescription") description = og.description || summary&.inner_text&.strip - if description.blank? - description = raw.css("meta[name=description]").first&.[]("content") - end - result[:description] = CGI.unescapeHTML(Onebox::Helpers.truncate(description, 250)) if description + description = raw.css("meta[name=description]").first&.[]("content") if description.blank? + result[:description] = CGI.unescapeHTML( + Onebox::Helpers.truncate(description, 250), + ) if description end result[:price] = nil if result[:price].start_with?("$0") || result[:price] == 0 diff --git a/lib/onebox/engine/animated_image_onebox.rb b/lib/onebox/engine/animated_image_onebox.rb index 9dc8a5f4846..a960ac5e619 100644 --- a/lib/onebox/engine/animated_image_onebox.rb +++ b/lib/onebox/engine/animated_image_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/.*(giphy\.com|gph\.is|tenor\.com)\//) + matches_regexp(%r{^https?://.*(giphy\.com|gph\.is|tenor\.com)/}) always_https def to_html diff --git a/lib/onebox/engine/audio_onebox.rb b/lib/onebox/engine/audio_onebox.rb index 8a5f52c4b08..8a41b100dd7 100644 --- a/lib/onebox/engine/audio_onebox.rb +++ b/lib/onebox/engine/audio_onebox.rb @@ -5,7 +5,7 @@ module Onebox class AudioOnebox include Engine - matches_regexp(/^(https?:)?\/\/.*\.(mp3|ogg|opus|wav|m4a)(\?.*)?$/i) + matches_regexp(%r{^(https?:)?//.*\.(mp3|ogg|opus|wav|m4a)(\?.*)?$}i) def always_https? AllowlistedGenericOnebox.host_matches(uri, AllowlistedGenericOnebox.https_hosts) diff --git a/lib/onebox/engine/audioboom_onebox.rb b/lib/onebox/engine/audioboom_onebox.rb index 89986f46857..daf26f6a831 100644 --- a/lib/onebox/engine/audioboom_onebox.rb +++ b/lib/onebox/engine/audioboom_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/audioboom\.com\/posts\/\d+/) + matches_regexp(%r{^https?://audioboom\.com/posts/\d+}) always_https def placeholder_html diff --git a/lib/onebox/engine/band_camp_onebox.rb b/lib/onebox/engine/band_camp_onebox.rb index a31e5890322..937826ce9ea 100644 --- a/lib/onebox/engine/band_camp_onebox.rb +++ b/lib/onebox/engine/band_camp_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/.*\.bandcamp\.com\/(album|track)\//) + matches_regexp(%r{^https?://.*\.bandcamp\.com/(album|track)/}) always_https requires_iframe_origins "https://bandcamp.com" diff --git a/lib/onebox/engine/cloud_app_onebox.rb b/lib/onebox/engine/cloud_app_onebox.rb index f1b985edf7c..2c076381411 100644 --- a/lib/onebox/engine/cloud_app_onebox.rb +++ b/lib/onebox/engine/cloud_app_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/cl\.ly/) + matches_regexp(%r{^https?://cl\.ly}) always_https def to_html diff --git a/lib/onebox/engine/coub_onebox.rb b/lib/onebox/engine/coub_onebox.rb index 7e57e454296..5cac6c5dbac 100644 --- a/lib/onebox/engine/coub_onebox.rb +++ b/lib/onebox/engine/coub_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/coub\.com\/view\//) + matches_regexp(%r{^https?://coub\.com/view/}) always_https def placeholder_html diff --git a/lib/onebox/engine/facebook_media_onebox.rb b/lib/onebox/engine/facebook_media_onebox.rb index 903eccb131a..cdf4d699ff3 100644 --- a/lib/onebox/engine/facebook_media_onebox.rb +++ b/lib/onebox/engine/facebook_media_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/.*\.facebook\.com\/(\w+)\/(videos|\?).*/) + matches_regexp(%r{^https?://.*\.facebook\.com/(\w+)/(videos|\?).*}) always_https requires_iframe_origins "https://www.facebook.com" diff --git a/lib/onebox/engine/five_hundred_px_onebox.rb b/lib/onebox/engine/five_hundred_px_onebox.rb index 806b5f9e6af..d2aab48eaf9 100644 --- a/lib/onebox/engine/five_hundred_px_onebox.rb +++ b/lib/onebox/engine/five_hundred_px_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/500px\.com\/photo\/\d+\//) + matches_regexp(%r{^https?://500px\.com/photo/\d+/}) always_https def to_html diff --git a/lib/onebox/engine/flickr_onebox.rb b/lib/onebox/engine/flickr_onebox.rb index 3ed26684a76..435a53e027f 100644 --- a/lib/onebox/engine/flickr_onebox.rb +++ b/lib/onebox/engine/flickr_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative './opengraph_image' +require_relative "./opengraph_image" module Onebox module Engine @@ -8,12 +8,12 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/www\.flickr\.com\/photos\//) + matches_regexp(%r{^https?://www\.flickr\.com/photos/}) always_https def to_html og = get_opengraph - return album_html(og) if og.url =~ /\/sets\// + return album_html(og) if og.url =~ %r{/sets/} return image_html(og) if !og.image.nil? nil end diff --git a/lib/onebox/engine/flickr_shortened_onebox.rb b/lib/onebox/engine/flickr_shortened_onebox.rb index 1c1243050bc..0a6baf1360c 100644 --- a/lib/onebox/engine/flickr_shortened_onebox.rb +++ b/lib/onebox/engine/flickr_shortened_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative './opengraph_image' +require_relative "./opengraph_image" module Onebox module Engine @@ -9,7 +9,7 @@ module Onebox include StandardEmbed include OpengraphImage - matches_regexp(/^https?:\/\/flic\.kr\/p\//) + matches_regexp(%r{^https?://flic\.kr/p/}) always_https end end diff --git a/lib/onebox/engine/gfycat_onebox.rb b/lib/onebox/engine/gfycat_onebox.rb index 27fd4bf79e0..44ca947f66f 100644 --- a/lib/onebox/engine/gfycat_onebox.rb +++ b/lib/onebox/engine/gfycat_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include JSON - matches_regexp(/^https?:\/\/gfycat\.com\//) + matches_regexp(%r{^https?://gfycat\.com/}) always_https # This engine should have priority over AllowlistedGenericOnebox. @@ -60,14 +60,19 @@ module Onebox private def match - @match ||= @url.match(/^https?:\/\/gfycat\.com\/(gifs\/detail\/)?(?.+)/) + @match ||= @url.match(%r{^https?://gfycat\.com/(gifs/detail/)?(?.+)}) end def og_data return @og_data if defined?(@og_data) - response = Onebox::Helpers.fetch_response(url, redirect_limit: 10) rescue nil - page = Nokogiri::HTML(response) + response = + begin + Onebox::Helpers.fetch_response(url, redirect_limit: 10) + rescue StandardError + nil + end + page = Nokogiri.HTML(response) script = page.at_css('script[type="application/ld+json"]') if json_string = script&.text @@ -82,15 +87,15 @@ module Onebox @data = { name: match[:name], - title: og_data[:headline] || 'No Title', + title: og_data[:headline] || "No Title", author: og_data[:author], url: @url, } - if keywords = og_data[:keywords]&.split(',') + if keywords = og_data[:keywords]&.split(",") @data[:keywords] = keywords .map { |keyword| "##{keyword}" } - .join(' ') + .join(" ") end if og_data[:video] diff --git a/lib/onebox/engine/github_actions_onebox.rb b/lib/onebox/engine/github_actions_onebox.rb index 182fff49d58..6630fefa4df 100644 --- a/lib/onebox/engine/github_actions_onebox.rb +++ b/lib/onebox/engine/github_actions_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../mixins/github_body' +require_relative "../mixins/github_body" module Onebox module Engine @@ -9,7 +9,9 @@ module Onebox include LayoutSupport include JSON - matches_regexp(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/(actions\/runs\/[[:digit:]]+|pull\/[[:digit:]]*\/checks\?check_run_id=[[:digit:]]+)/) + matches_regexp( + %r{^https?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/(actions/runs/[[:digit:]]+|pull/[[:digit:]]*/checks\?check_run_id=[[:digit:]]+)}, + ) always_https def url @@ -29,12 +31,18 @@ module Onebox def match_url return if defined?(@match) && defined?(@type) - if match = @url.match(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/actions\/runs\/(?[[:digit:]]+)/) + if match = + @url.match( + %r{^https?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/actions/runs/(?[[:digit:]]+)}, + ) @match = match @type = :actions_run end - if match = @url.match(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/pull\/(?[[:digit:]]*)\/checks\?check_run_id=(?[[:digit:]]+)/) + if match = + @url.match( + %r{^https?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/pull/(?[[:digit:]]*)/checks\?check_run_id=(?[[:digit:]]+)}, + ) @match = match @type = :pr_run end @@ -67,18 +75,20 @@ module Onebox status = "pending" end - title = if type == :actions_run - raw["head_commit"]["message"].lines.first - elsif type == :pr_run - pr_url = "https://api.github.com/repos/#{match[:org]}/#{match[:repo]}/pulls/#{match[:pr_id]}" - ::MultiJson.load(URI.parse(pr_url).open(read_timeout: timeout))["title"] - end + title = + if type == :actions_run + raw["head_commit"]["message"].lines.first + elsif type == :pr_run + pr_url = + "https://api.github.com/repos/#{match[:org]}/#{match[:repo]}/pulls/#{match[:pr_id]}" + ::MultiJson.load(URI.parse(pr_url).open(read_timeout: timeout))["title"] + end { - link: @url, - title: title, - name: raw["name"], - run_number: raw["run_number"], + :link => @url, + :title => title, + :name => raw["name"], + :run_number => raw["run_number"], status => true, } end diff --git a/lib/onebox/engine/github_blob_onebox.rb b/lib/onebox/engine/github_blob_onebox.rb index fd70a4b2af1..48a7e6dec05 100644 --- a/lib/onebox/engine/github_blob_onebox.rb +++ b/lib/onebox/engine/github_blob_onebox.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require_relative '../mixins/git_blob_onebox' +require_relative "../mixins/git_blob_onebox" module Onebox module Engine class GithubBlobOnebox def self.git_regexp - /^https?:\/\/(www\.)?github\.com.*\/blob\// + %r{^https?://(www\.)?github\.com.*/blob/} end def self.onebox_name @@ -16,7 +16,7 @@ module Onebox include Onebox::Mixins::GitBlobOnebox def raw_regexp - /github\.com\/(?[^\/]+)\/(?[^\/]+)\/blob\/(?[^\/]+)\/(?[^#]+)(#(L(?[^-]*)(-L(?.*))?))?/mi + %r{github\.com/(?[^/]+)/(?[^/]+)/blob/(?[^/]+)/(?[^#]+)(#(L(?[^-]*)(-L(?.*))?))?}mi end def raw_template(m) @@ -24,7 +24,7 @@ module Onebox end def title - Sanitize.fragment(Onebox::Helpers.uri_unencode(link).sub(/^https?\:\/\/github\.com\//, '')) + Sanitize.fragment(Onebox::Helpers.uri_unencode(link).sub(%r{^https?\://github\.com/}, "")) end end end diff --git a/lib/onebox/engine/github_commit_onebox.rb b/lib/onebox/engine/github_commit_onebox.rb index d1820faa4bb..e1fa68547ed 100644 --- a/lib/onebox/engine/github_commit_onebox.rb +++ b/lib/onebox/engine/github_commit_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../mixins/github_body' +require_relative "../mixins/github_body" module Onebox module Engine @@ -10,7 +10,7 @@ module Onebox include JSON include Onebox::Mixins::GithubBody - matches_regexp(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?(github)\.com(?:\/)?(?:.)*\/commit\//) + matches_regexp(%r{^https?://(?:www\.)?(?:(?:\w)+\.)?(github)\.com(?:/)?(?:.)*/commit/}) always_https def url @@ -22,8 +22,12 @@ module Onebox def match return @match if defined?(@match) - @match = @url.match(%{github\.com/(?[^/]+)/(?[^/]+)/commit/(?[^/]+)}) - @match ||= @url.match(%{github\.com/(?[^/]+)/(?[^/]+)/pull/(?[^/]+)/commit/(?[^/]+)}) + @match = + @url.match(%{github\.com/(?[^/]+)/(?[^/]+)/commit/(?[^/]+)}) + @match ||= + @url.match( + %{github\.com/(?[^/]+)/(?[^/]+)/pull/(?[^/]+)/commit/(?[^/]+)}, + ) @match end @@ -31,18 +35,18 @@ module Onebox def data result = raw.clone - lines = result['commit']['message'].split("\n") - result['title'] = lines.first - result['body'], result['excerpt'] = compute_body(lines[1..lines.length].join("\n")) + lines = result["commit"]["message"].split("\n") + result["title"] = lines.first + result["body"], result["excerpt"] = compute_body(lines[1..lines.length].join("\n")) - committed_at = Time.parse(result['commit']['committer']['date']) - result['committed_at'] = committed_at.strftime("%I:%M%p - %d %b %y %Z") - result['committed_at_date'] = committed_at.strftime("%F") - result['committed_at_time'] = committed_at.strftime("%T") + committed_at = Time.parse(result["commit"]["committer"]["date"]) + result["committed_at"] = committed_at.strftime("%I:%M%p - %d %b %y %Z") + result["committed_at_date"] = committed_at.strftime("%F") + result["committed_at_time"] = committed_at.strftime("%T") - result['link'] = link + result["link"] = link ulink = URI(link) - result['domain'] = "#{ulink.host}/#{ulink.path.split('/')[1]}/#{ulink.path.split('/')[2]}" + result["domain"] = "#{ulink.host}/#{ulink.path.split("/")[1]}/#{ulink.path.split("/")[2]}" result end diff --git a/lib/onebox/engine/github_folder_onebox.rb b/lib/onebox/engine/github_folder_onebox.rb index aae75888be0..d77a27ca749 100644 --- a/lib/onebox/engine/github_folder_onebox.rb +++ b/lib/onebox/engine/github_folder_onebox.rb @@ -28,7 +28,7 @@ module Onebox # For links to markdown and rdoc if html_doc.css(".Box.md, .Box.rdoc").present? - node = html_doc.css('a.anchor').find { |n| n['href'] == "##{fragment}" } + node = html_doc.css("a.anchor").find { |n| n["href"] == "##{fragment}" } subtitle = node&.parent&.text end @@ -40,12 +40,12 @@ module Onebox title: Onebox::Helpers.truncate(title, 250), path: display_path, description: display_description, - favicon: get_favicon + favicon: get_favicon, } end def extract_path(root, max_length) - path = url.split('#')[0].split('?')[0] + path = url.split("#")[0].split("?")[0] path = path["#{root}/tree/".length..-1] return unless path diff --git a/lib/onebox/engine/github_gist_onebox.rb b/lib/onebox/engine/github_gist_onebox.rb index 21561d85ffb..ad579428d25 100644 --- a/lib/onebox/engine/github_gist_onebox.rb +++ b/lib/onebox/engine/github_gist_onebox.rb @@ -9,7 +9,7 @@ module Onebox MAX_FILES = 3 - matches_regexp(/^http(?:s)?:\/\/gist\.(?:(?:\w)+\.)?(github)\.com(?:\/)?/) + matches_regexp(%r{^http(?:s)?://gist\.(?:(?:\w)+\.)?(github)\.com(?:/)?}) always_https def url @@ -20,10 +20,10 @@ module Onebox def data @data ||= { - title: 'gist.github.com', + title: "gist.github.com", link: link, gist_files: gist_files.take(MAX_FILES), - truncated_files?: truncated_files? + truncated_files?: truncated_files?, } end @@ -34,9 +34,7 @@ module Onebox def gist_files return [] unless gist_api - @gist_files ||= gist_api["files"].values.map do |file_json| - GistFile.new(file_json) - end + @gist_files ||= gist_api["files"].values.map { |file_json| GistFile.new(file_json) } end def gist_api diff --git a/lib/onebox/engine/github_issue_onebox.rb b/lib/onebox/engine/github_issue_onebox.rb index 4d387f47995..510fc0ca3e2 100644 --- a/lib/onebox/engine/github_issue_onebox.rb +++ b/lib/onebox/engine/github_issue_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../mixins/github_body' +require_relative "../mixins/github_body" module Onebox module Engine @@ -11,7 +11,9 @@ module Onebox include JSON include Onebox::Mixins::GithubBody - matches_regexp(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/issues\/([[:digit:]]+)/) + matches_regexp( + %r{^https?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/issues/([[:digit:]]+)}, + ) always_https def url @@ -22,35 +24,36 @@ module Onebox private def match - @match ||= @url.match(/^http(?:s)?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/(?issues)\/(?[\d]+)/) + @match ||= + @url.match( + %r{^http(?:s)?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/(?issues)/(?[\d]+)}, + ) end def data - created_at = Time.parse(raw['created_at']) - closed_at = Time.parse(raw['closed_at']) if raw['closed_at'] - body, excerpt = compute_body(raw['body']) + created_at = Time.parse(raw["created_at"]) + closed_at = Time.parse(raw["closed_at"]) if raw["closed_at"] + body, excerpt = compute_body(raw["body"]) ulink = URI(link) - labels = raw['labels'].map do |l| - { name: Emoji.codes_to_img(l['name']) } - end + labels = raw["labels"].map { |l| { name: Emoji.codes_to_img(l["name"]) } } { link: @url, - title: raw['title'], + title: raw["title"], body: body, excerpt: excerpt, labels: labels, - user: raw['user'], - created_at: created_at.strftime('%I:%M%p - %d %b %y %Z'), - created_at_date: created_at.strftime('%F'), - created_at_time: created_at.strftime('%T'), - closed_at: closed_at&.strftime('%I:%M%p - %d %b %y %Z'), - closed_at_date: closed_at&.strftime('%F'), - closed_at_time: closed_at&.strftime('%T'), - closed_by: raw['closed_by'], - avatar: "https://avatars1.githubusercontent.com/u/#{raw['user']['id']}?v=2&s=96", - domain: "#{ulink.host}/#{ulink.path.split('/')[1]}/#{ulink.path.split('/')[2]}", + user: raw["user"], + created_at: created_at.strftime("%I:%M%p - %d %b %y %Z"), + created_at_date: created_at.strftime("%F"), + created_at_time: created_at.strftime("%T"), + closed_at: closed_at&.strftime("%I:%M%p - %d %b %y %Z"), + closed_at_date: closed_at&.strftime("%F"), + closed_at_time: closed_at&.strftime("%T"), + closed_by: raw["closed_by"], + avatar: "https://avatars1.githubusercontent.com/u/#{raw["user"]["id"]}?v=2&s=96", + domain: "#{ulink.host}/#{ulink.path.split("/")[1]}/#{ulink.path.split("/")[2]}", } end end diff --git a/lib/onebox/engine/github_pull_request_onebox.rb b/lib/onebox/engine/github_pull_request_onebox.rb index 1ef23815cc9..77310cc46f9 100644 --- a/lib/onebox/engine/github_pull_request_onebox.rb +++ b/lib/onebox/engine/github_pull_request_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../mixins/github_body' +require_relative "../mixins/github_body" module Onebox module Engine @@ -12,7 +12,7 @@ module Onebox GITHUB_COMMENT_REGEX = /(\r\n)/ - matches_regexp(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?(github)\.com(?:\/)?(?:.)*\/pull/) + matches_regexp(%r{^https?://(?:www\.)?(?:(?:\w)+\.)?(github)\.com(?:/)?(?:.)*/pull}) always_https def url @@ -22,51 +22,59 @@ module Onebox private def match - @match ||= @url.match(%r{github\.com/(?[^/]+)/(?[^/]+)/pull/(?[^/]+)}) + @match ||= + @url.match(%r{github\.com/(?[^/]+)/(?[^/]+)/pull/(?[^/]+)}) end def data result = raw.clone - result['link'] = link + result["link"] = link - created_at = Time.parse(result['created_at']) - result['created_at'] = created_at.strftime("%I:%M%p - %d %b %y %Z") - result['created_at_date'] = created_at.strftime("%F") - result['created_at_time'] = created_at.strftime("%T") + created_at = Time.parse(result["created_at"]) + result["created_at"] = created_at.strftime("%I:%M%p - %d %b %y %Z") + result["created_at_date"] = created_at.strftime("%F") + result["created_at_time"] = created_at.strftime("%T") ulink = URI(link) - result['domain'] = "#{ulink.host}/#{ulink.path.split('/')[1]}/#{ulink.path.split('/')[2]}" + result["domain"] = "#{ulink.host}/#{ulink.path.split("/")[1]}/#{ulink.path.split("/")[2]}" - result['body'], result['excerpt'] = compute_body(result['body']) + result["body"], result["excerpt"] = compute_body(result["body"]) - if result['commit'] = load_commit(link) - result['body'], result['excerpt'] = compute_body(result['commit']['commit']['message'].lines[1..].join) - elsif result['comment'] = load_comment(link) - result['body'], result['excerpt'] = compute_body(result['comment']['body']) - elsif result['discussion'] = load_review(link) - result['body'], result['excerpt'] = compute_body(result['discussion']['body']) + if result["commit"] = load_commit(link) + result["body"], result["excerpt"] = + compute_body(result["commit"]["commit"]["message"].lines[1..].join) + elsif result["comment"] = load_comment(link) + result["body"], result["excerpt"] = compute_body(result["comment"]["body"]) + elsif result["discussion"] = load_review(link) + result["body"], result["excerpt"] = compute_body(result["discussion"]["body"]) else - result['pr'] = true + result["pr"] = true end result end def load_commit(link) - if commit_match = link.match(/commits\/(\h+)/) - load_json("https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/commits/#{commit_match[1]}") + if commit_match = link.match(%r{commits/(\h+)}) + load_json( + "https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/commits/#{commit_match[1]}", + ) end end def load_comment(link) if comment_match = link.match(/#issuecomment-(\d+)/) - load_json("https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/issues/comments/#{comment_match[1]}") + load_json( + "https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/issues/comments/#{comment_match[1]}", + ) end end def load_review(link) if review_match = link.match(/#discussion_r(\d+)/) - load_json("https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/pulls/comments/#{review_match[1]}") + load_json( + "https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/pulls/comments/#{review_match[1]}", + ) end end diff --git a/lib/onebox/engine/gitlab_blob_onebox.rb b/lib/onebox/engine/gitlab_blob_onebox.rb index d8ba1973381..c948e5bf6df 100644 --- a/lib/onebox/engine/gitlab_blob_onebox.rb +++ b/lib/onebox/engine/gitlab_blob_onebox.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require_relative '../mixins/git_blob_onebox' +require_relative "../mixins/git_blob_onebox" module Onebox module Engine class GitlabBlobOnebox def self.git_regexp - /^https?:\/\/(www\.)?gitlab\.com.*\/blob\// + %r{^https?://(www\.)?gitlab\.com.*/blob/} end def self.onebox_name @@ -16,7 +16,7 @@ module Onebox include Onebox::Mixins::GitBlobOnebox def raw_regexp - /gitlab\.com\/(?[^\/]+)\/(?[^\/]+)\/blob\/(?[^\/]+)\/(?[^#]+)(#(L(?[^-]*)(-L(?.*))?))?/mi + %r{gitlab\.com/(?[^/]+)/(?[^/]+)/blob/(?[^/]+)/(?[^#]+)(#(L(?[^-]*)(-L(?.*))?))?}mi end def raw_template(m) @@ -24,7 +24,7 @@ module Onebox end def title - Sanitize.fragment(Onebox::Helpers.uri_unencode(link).sub(/^https?\:\/\/gitlab\.com\//, '')) + Sanitize.fragment(Onebox::Helpers.uri_unencode(link).sub(%r{^https?\://gitlab\.com/}, "")) end end end diff --git a/lib/onebox/engine/google_calendar_onebox.rb b/lib/onebox/engine/google_calendar_onebox.rb index b666df4d080..a7b57b220b6 100644 --- a/lib/onebox/engine/google_calendar_onebox.rb +++ b/lib/onebox/engine/google_calendar_onebox.rb @@ -10,7 +10,7 @@ module Onebox requires_iframe_origins "https://calendar.google.com" def to_html - url = @url.split('&').first + url = @url.split("&").first src = ::Onebox::Helpers.normalize_url_for_output(url) "" end diff --git a/lib/onebox/engine/google_docs_onebox.rb b/lib/onebox/engine/google_docs_onebox.rb index cc7872aebe4..ebb29a43bb5 100644 --- a/lib/onebox/engine/google_docs_onebox.rb +++ b/lib/onebox/engine/google_docs_onebox.rb @@ -7,15 +7,12 @@ module Onebox include StandardEmbed include LayoutSupport - SUPPORTED_ENDPOINTS = %w(spreadsheets document forms presentation) - SHORT_TYPES = { - spreadsheets: :sheets, - document: :docs, - presentation: :slides, - forms: :forms, - } + SUPPORTED_ENDPOINTS = %w[spreadsheets document forms presentation] + SHORT_TYPES = { spreadsheets: :sheets, document: :docs, presentation: :slides, forms: :forms } - matches_regexp(/^(https?:)?\/\/(docs\.google\.com)\/(?(#{SUPPORTED_ENDPOINTS.join('|')}))\/d\/((?[\w-]*)).+$/) + matches_regexp( + %r{^(https?:)?//(docs\.google\.com)/(?(#{SUPPORTED_ENDPOINTS.join("|")}))/d/((?[\w-]*)).+$}, + ) always_https private @@ -24,17 +21,18 @@ module Onebox og_data = get_opengraph short_type = SHORT_TYPES[match[:endpoint].to_sym] - description = if Onebox::Helpers.blank?(og_data.description) - "This #{short_type.to_s.chop.capitalize} is private" - else - Onebox::Helpers.truncate(og_data.description, 250) - end + description = + if Onebox::Helpers.blank?(og_data.description) + "This #{short_type.to_s.chop.capitalize} is private" + else + Onebox::Helpers.truncate(og_data.description, 250) + end { link: link, title: og_data.title || "Google #{short_type.to_s.capitalize}", description: description, - type: short_type + type: short_type, } end diff --git a/lib/onebox/engine/google_drive_onebox.rb b/lib/onebox/engine/google_drive_onebox.rb index 82628228ea9..cb8bbf797e1 100644 --- a/lib/onebox/engine/google_drive_onebox.rb +++ b/lib/onebox/engine/google_drive_onebox.rb @@ -7,7 +7,7 @@ module Onebox include StandardEmbed include LayoutSupport - matches_regexp(/^(https?:)?\/\/(drive\.google\.com)\/file\/d\/(?[\w-]*)\/.+$/) + matches_regexp(%r{^(https?:)?//(drive\.google\.com)/file/d/(?[\w-]*)/.+$}) always_https protected @@ -15,14 +15,14 @@ module Onebox def data og_data = get_opengraph title = og_data.title || "Google Drive" - title = "#{og_data.title} (video)" if og_data.type =~ /^video[\/\.]/ + title = "#{og_data.title} (video)" if og_data.type =~ %r{^video[/\.]} description = og_data.description || "Google Drive file." { link: link, title: title, description: Onebox::Helpers.truncate(description, 250), - image: og_data.image + image: og_data.image, } end end diff --git a/lib/onebox/engine/google_maps_onebox.rb b/lib/onebox/engine/google_maps_onebox.rb index 0c4fbcf16d9..828c7fd5dcb 100644 --- a/lib/onebox/engine/google_maps_onebox.rb +++ b/lib/onebox/engine/google_maps_onebox.rb @@ -25,24 +25,30 @@ module Onebox requires_iframe_origins("https://maps.google.com", "https://google.com") # Matches shortened Google Maps URLs - matches_regexp :short, %r"^(https?:)?//goo\.gl/maps/" + matches_regexp :short, %r{^(https?:)?//goo\.gl/maps/} # Matches URLs for custom-created maps - matches_regexp :custom, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps/d/(?:edit|viewer|embed)\?mid=.+$" + matches_regexp :custom, + %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps/d/(?:edit|viewer|embed)\?mid=.+$" # Matches URLs with streetview data - matches_regexp :streetview, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps[^@]+@(?-?[\d.]+),(?-?[\d.]+),(?:\d+)a,(?[\d.]+)y,(?[\d.]+)h,(?[\d.]+)t.+?data=.*?!1s(?[^!]{22})" + matches_regexp :streetview, + %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps[^@]+@(?-?[\d.]+),(?-?[\d.]+),(?:\d+)a,(?[\d.]+)y,(?[\d.]+)h,(?[\d.]+)t.+?data=.*?!1s(?[^!]{22})" # Matches "normal" Google Maps URLs with arbitrary data - matches_regexp :standard, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps" + matches_regexp :standard, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps" # Matches URLs for the old Google Maps domain which we occasionally get redirected to - matches_regexp :canonical, %r"^(?:https?:)?//maps\.google(?:\.(?:\w{2,}))+/maps\?" + matches_regexp :canonical, %r"^(?:https?:)?//maps\.google(?:\.(?:\w{2,}))+/maps\?" def initialize(url, timeout = nil) super resolve_url! - rescue Net::HTTPServerException, Timeout::Error, Net::HTTPError, Errno::ECONNREFUSED, RuntimeError => err + rescue Net::HTTPServerException, + Timeout::Error, + Net::HTTPError, + Errno::ECONNREFUSED, + RuntimeError => err raise ArgumentError, "malformed url or unresolveable: #{err.message}" end @@ -95,17 +101,16 @@ module Onebox zoom = match[:mz] == "z" ? match[:zoom] : Math.log2(57280048.0 / match[:zoom].to_f).round location = "#{match[:lon]},#{match[:lat]}" url = "https://maps.google.com/maps?ll=#{location}&z=#{zoom}&output=embed&dg=ntvb" - url += "&q=#{$1}" if match = @url.match(/\/place\/([^\/\?]+)/) + url += "&q=#{$1}" if match = @url.match(%r{/place/([^/\?]+)}) url += "&cid=#{($1 + $2).to_i(16)}" if @url.match(/!3m1!1s0x(\h{16}):0x(\h{16})/) @url = url - @placeholder = "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap¢er=#{location}&zoom=#{zoom}&size=690x400&sensor=false" - + @placeholder = + "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap¢er=#{location}&zoom=#{zoom}&size=690x400&sensor=false" when :custom url = @url.dup @url = rewrite_custom_url(url, "embed") @placeholder = rewrite_custom_url(url, "thumbnail") @placeholder_height = @placeholder_width = 120 - when :streetview @streetview = true panoid = match[:pano] @@ -115,19 +120,24 @@ module Onebox pitch = (match[:pitch].to_f / 10.0).round(4).to_s fov = (match[:zoom].to_f / 100.0).round(4).to_s zoom = match[:zoom].to_f.round - @url = "https://www.google.com/maps/embed?pb=!3m2!2sen!4v0!6m8!1m7!1s#{panoid}!2m2!1d#{lon}!2d#{lat}!3f#{heading}!4f#{pitch}!5f#{fov}" - @placeholder = "https://maps.googleapis.com/maps/api/streetview?size=690x400&location=#{lon},#{lat}&pano=#{panoid}&fov=#{zoom}&heading=#{heading}&pitch=#{pitch}&sensor=false" - + @url = + "https://www.google.com/maps/embed?pb=!3m2!2sen!4v0!6m8!1m7!1s#{panoid}!2m2!1d#{lon}!2d#{lat}!3f#{heading}!4f#{pitch}!5f#{fov}" + @placeholder = + "https://maps.googleapis.com/maps/api/streetview?size=690x400&location=#{lon},#{lat}&pano=#{panoid}&fov=#{zoom}&heading=#{heading}&pitch=#{pitch}&sensor=false" when :canonical - query = URI::decode_www_form(uri.query).to_h + query = URI.decode_www_form(uri.query).to_h if !query.has_key?("ll") - raise ArgumentError, "canonical url lacks location argument" unless query.has_key?("sll") + unless query.has_key?("sll") + raise ArgumentError, "canonical url lacks location argument" + end query["ll"] = query["sll"] @url += "&ll=#{query["sll"]}" end location = query["ll"] if !query.has_key?("z") - raise ArgumentError, "canonical url has incomplete query arguments" unless query.has_key?("spn") || query.has_key?("sspn") + unless query.has_key?("spn") || query.has_key?("sspn") + raise ArgumentError, "canonical url has incomplete query arguments" + end if !query.has_key?("spn") query["spn"] = query["sspn"] @url += "&spn=#{query["sspn"]}" @@ -137,9 +147,9 @@ module Onebox else zoom = query["z"] end - @url = @url.sub('output=classic', 'output=embed') - @placeholder = "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap&size=690x400&sensor=false¢er=#{location}&zoom=#{zoom}" - + @url = @url.sub("output=classic", "output=embed") + @placeholder = + "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap&size=690x400&sensor=false¢er=#{location}&zoom=#{zoom}" else raise "unexpected url type #{type.inspect}" end @@ -156,27 +166,34 @@ module Onebox def rewrite_custom_url(url, target) uri = URI(url) - uri.path = uri.path.sub(/(?<=^\/maps\/d\/)\w+$/, target) + uri.path = uri.path.sub(%r{(?<=^/maps/d/)\w+$}, target) uri.to_s end def follow_redirect! begin - http = FinalDestination::HTTP.start( - uri.host, - uri.port, - use_ssl: uri.scheme == 'https', - open_timeout: timeout, - read_timeout: timeout - ) + http = + FinalDestination::HTTP.start( + uri.host, + uri.port, + use_ssl: uri.scheme == "https", + open_timeout: timeout, + read_timeout: timeout, + ) response = http.head(uri.path) - raise "unexpected response code #{response.code}" unless %w(200 301 302).include?(response.code) + unless %w[200 301 302].include?(response.code) + raise "unexpected response code #{response.code}" + end @url = response.code == "200" ? uri.to_s : response["Location"] @uri = URI(@url) ensure - http.finish rescue nil + begin + http.finish + rescue StandardError + nil + end end end end diff --git a/lib/onebox/engine/google_photos_onebox.rb b/lib/onebox/engine/google_photos_onebox.rb index 6c930ce59cc..afb8aade192 100644 --- a/lib/onebox/engine/google_photos_onebox.rb +++ b/lib/onebox/engine/google_photos_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/(photos)\.(app\.goo\.gl|google\.com)/) + matches_regexp(%r{^https?://(photos)\.(app\.goo\.gl|google\.com)}) always_https def to_html diff --git a/lib/onebox/engine/google_play_app_onebox.rb b/lib/onebox/engine/google_play_app_onebox.rb index 1e2557d4422..f03fceee505 100644 --- a/lib/onebox/engine/google_play_app_onebox.rb +++ b/lib/onebox/engine/google_play_app_onebox.rb @@ -7,23 +7,36 @@ module Onebox include LayoutSupport include HTML - DEFAULTS = { - MAX_DESCRIPTION_CHARS: 500 - } + DEFAULTS = { MAX_DESCRIPTION_CHARS: 500 } - matches_regexp(/^https?:\/\/play\.(?:(?:\w)+\.)?(google)\.com(?:\/)?\/store\/apps\//) + matches_regexp(%r{^https?://play\.(?:(?:\w)+\.)?(google)\.com(?:/)?/store/apps/}) always_https private def data - price = raw.css("meta[itemprop=price]").first["content"] rescue "Free" + price = + begin + raw.css("meta[itemprop=price]").first["content"] + rescue StandardError + "Free" + end { link: link, - title: raw.css("meta[property='og:title']").first["content"].gsub(" - Apps on Google Play", ""), - image: ::Onebox::Helpers.normalize_url_for_output(raw.css("meta[property='og:image']").first["content"]), - description: raw.css("meta[name=description]").first["content"][0..DEFAULTS[:MAX_DESCRIPTION_CHARS]].chop + "...", - price: price == "0" ? "Free" : price + title: + raw.css("meta[property='og:title']").first["content"].gsub( + " - Apps on Google Play", + "", + ), + image: + ::Onebox::Helpers.normalize_url_for_output( + raw.css("meta[property='og:image']").first["content"], + ), + description: + raw.css("meta[name=description]").first["content"][ + 0..DEFAULTS[:MAX_DESCRIPTION_CHARS] + ].chop + "...", + price: price == "0" ? "Free" : price, } end end diff --git a/lib/onebox/engine/hackernews_onebox.rb b/lib/onebox/engine/hackernews_onebox.rb index 79d8e037a58..b4507f26e0c 100644 --- a/lib/onebox/engine/hackernews_onebox.rb +++ b/lib/onebox/engine/hackernews_onebox.rb @@ -7,7 +7,7 @@ module Onebox include LayoutSupport include JSON - REGEX = /^https?:\/\/news\.ycombinator\.com\/item\?id=(?\d+)/ + REGEX = %r{^https?://news\.ycombinator\.com/item\?id=(?\d+)} matches_regexp(REGEX) @@ -23,22 +23,24 @@ module Onebox end def data - return nil unless %w{story comment}.include?(raw['type']) + return nil unless %w[story comment].include?(raw["type"]) html_entities = HTMLEntities.new data = { link: @url, - title: Onebox::Helpers.truncate(raw['title'], 80), - favicon: 'https://news.ycombinator.com/y18.gif', - timestamp: Time.at(raw['time']).strftime("%-l:%M %p - %-d %b %Y"), - author: raw['by'] + title: Onebox::Helpers.truncate(raw["title"], 80), + favicon: "https://news.ycombinator.com/y18.gif", + timestamp: Time.at(raw["time"]).strftime("%-l:%M %p - %-d %b %Y"), + author: raw["by"], } - data['description'] = html_entities.decode(Onebox::Helpers.truncate(raw['text'], 400)) if raw['text'] + data["description"] = html_entities.decode( + Onebox::Helpers.truncate(raw["text"], 400), + ) if raw["text"] - if raw['type'] == 'story' - data['data_1'] = raw['score'] - data['data_2'] = raw['descendants'] + if raw["type"] == "story" + data["data_1"] = raw["score"] + data["data_2"] = raw["descendants"] end data diff --git a/lib/onebox/engine/image_onebox.rb b/lib/onebox/engine/image_onebox.rb index d37faff841b..2b6e08ac923 100644 --- a/lib/onebox/engine/image_onebox.rb +++ b/lib/onebox/engine/image_onebox.rb @@ -5,8 +5,8 @@ module Onebox class ImageOnebox include Engine - matches_content_type(/^image\/(png|jpg|jpeg|gif|bmp|tif|tiff)$/) - matches_regexp(/^(https?:)?\/\/.+\.(png|jpg|jpeg|gif|bmp|tif|tiff)(\?.*)?$/i) + matches_content_type(%r{^image/(png|jpg|jpeg|gif|bmp|tif|tiff)$}) + matches_regexp(%r{^(https?:)?//.+\.(png|jpg|jpeg|gif|bmp|tif|tiff)(\?.*)?$}i) def always_https? AllowlistedGenericOnebox.host_matches(uri, AllowlistedGenericOnebox.https_hosts) @@ -14,7 +14,7 @@ module Onebox def to_html # Fix Dropbox image links - if @url[/^https:\/\/www.dropbox.com\/s\//] + if @url[%r{^https://www.dropbox.com/s/}] @url.sub!("https://www.dropbox.com", "https://dl.dropboxusercontent.com") end diff --git a/lib/onebox/engine/imgur_onebox.rb b/lib/onebox/engine/imgur_onebox.rb index 26a90379dd1..127f73baee9 100644 --- a/lib/onebox/engine/imgur_onebox.rb +++ b/lib/onebox/engine/imgur_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/(www\.)?imgur\.com/) + matches_regexp(%r{^https?://(www\.)?imgur\.com}) always_https def to_html @@ -23,7 +23,7 @@ module Onebox <<-HTML HTML end @@ -47,10 +47,15 @@ module Onebox end def is_album? - response = Onebox::Helpers.fetch_response("https://api.imgur.com/oembed.json?url=#{url}") rescue "{}" + response = + begin + Onebox::Helpers.fetch_response("https://api.imgur.com/oembed.json?url=#{url}") + rescue StandardError + "{}" + end oembed_data = Onebox::Helpers.symbolize_keys(::MultiJson.load(response)) - imgur_data_id = Nokogiri::HTML(oembed_data[:html]).xpath("//blockquote").attr("data-id") - imgur_data_id.to_s[/a\//] + imgur_data_id = Nokogiri.HTML(oembed_data[:html]).xpath("//blockquote").attr("data-id") + imgur_data_id.to_s[%r{a/}] end def image_html(og) diff --git a/lib/onebox/engine/instagram_onebox.rb b/lib/onebox/engine/instagram_onebox.rb index 7cc96ad3d61..86fd55947f2 100644 --- a/lib/onebox/engine/instagram_onebox.rb +++ b/lib/onebox/engine/instagram_onebox.rb @@ -7,28 +7,38 @@ module Onebox include StandardEmbed include LayoutSupport - matches_regexp(/^https?:\/\/(?:www\.)?(?:instagram\.com|instagr\.am)\/?(?:.*)\/(?:p|tv)\/[a-zA-Z\d_-]+/) + matches_regexp( + %r{^https?://(?:www\.)?(?:instagram\.com|instagr\.am)/?(?:.*)/(?:p|tv)/[a-zA-Z\d_-]+}, + ) always_https requires_iframe_origins "https://www.instagram.com" def clean_url - url.scan(/^https?:\/\/(?:www\.)?(?:instagram\.com|instagr\.am)\/?(?:.*)\/(?:p|tv)\/[a-zA-Z\d_-]+/).flatten.first + url + .scan( + %r{^https?://(?:www\.)?(?:instagram\.com|instagr\.am)/?(?:.*)/(?:p|tv)/[a-zA-Z\d_-]+}, + ) + .flatten + .first end def data - @data ||= begin - oembed = get_oembed - raise "No oEmbed data found. Ensure 'facebook_app_access_token' is valid" if oembed.data.empty? + @data ||= + begin + oembed = get_oembed + if oembed.data.empty? + raise "No oEmbed data found. Ensure 'facebook_app_access_token' is valid" + end - { - link: clean_url.gsub("/#{oembed.author_name}/", "/") + '/embed', - title: "@#{oembed.author_name}", - image: oembed.thumbnail_url, - image_width: oembed.data[:thumbnail_width], - image_height: oembed.data[:thumbnail_height], - description: Onebox::Helpers.truncate(oembed.title, 250), - } - end + { + link: clean_url.gsub("/#{oembed.author_name}/", "/") + "/embed", + title: "@#{oembed.author_name}", + image: oembed.thumbnail_url, + image_width: oembed.data[:thumbnail_width], + image_height: oembed.data[:thumbnail_height], + description: Onebox::Helpers.truncate(oembed.title, 250), + } + end end def placeholder_html @@ -53,7 +63,7 @@ module Onebox end def get_oembed_url - if access_token != '' + if access_token != "" "https://graph.facebook.com/v9.0/instagram_oembed?url=#{clean_url}&access_token=#{access_token}" else # The following is officially deprecated by Instagram, but works in some limited circumstances. diff --git a/lib/onebox/engine/kaltura_onebox.rb b/lib/onebox/engine/kaltura_onebox.rb index d94091a4f0a..f9ace113638 100644 --- a/lib/onebox/engine/kaltura_onebox.rb +++ b/lib/onebox/engine/kaltura_onebox.rb @@ -7,7 +7,7 @@ module Onebox include StandardEmbed always_https - matches_regexp(/^https?:\/\/[a-z0-9]+\.kaltura\.com\/id\/[a-zA-Z0-9]+/) + matches_regexp(%r{^https?://[a-z0-9]+\.kaltura\.com/id/[a-zA-Z0-9]+}) requires_iframe_origins "https://*.kaltura.com" def preview_html diff --git a/lib/onebox/engine/mixcloud_onebox.rb b/lib/onebox/engine/mixcloud_onebox.rb index 3d4e92295fa..1a681d5ec0f 100644 --- a/lib/onebox/engine/mixcloud_onebox.rb +++ b/lib/onebox/engine/mixcloud_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/www\.mixcloud\.com\//) + matches_regexp(%r{^https?://www\.mixcloud\.com/}) always_https requires_iframe_origins "https://www.mixcloud.com" diff --git a/lib/onebox/engine/motoko_onebox.rb b/lib/onebox/engine/motoko_onebox.rb index ec6433d2e47..1656150fbfd 100644 --- a/lib/onebox/engine/motoko_onebox.rb +++ b/lib/onebox/engine/motoko_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/embed\.smartcontracts\.org\/?.*/) + matches_regexp(%r{^https?://embed\.smartcontracts\.org/?.*}) requires_iframe_origins "https://embed.smartcontracts.org" always_https diff --git a/lib/onebox/engine/opengraph_image.rb b/lib/onebox/engine/opengraph_image.rb index a104db36ffb..77bf8ee2103 100644 --- a/lib/onebox/engine/opengraph_image.rb +++ b/lib/onebox/engine/opengraph_image.rb @@ -3,7 +3,6 @@ module Onebox module Engine module OpengraphImage - def to_html og = get_opengraph "" diff --git a/lib/onebox/engine/pastebin_onebox.rb b/lib/onebox/engine/pastebin_onebox.rb index d9b26467f3c..c542457f8d2 100644 --- a/lib/onebox/engine/pastebin_onebox.rb +++ b/lib/onebox/engine/pastebin_onebox.rb @@ -8,17 +8,12 @@ module Onebox MAX_LINES = 10 - matches_regexp(/^http?:\/\/pastebin\.com/) + matches_regexp(%r{^http?://pastebin\.com}) private def data - @data ||= { - title: 'pastebin.com', - link: link, - content: content, - truncated?: truncated? - } + @data ||= { title: "pastebin.com", link: link, content: content, truncated?: truncated? } end def content @@ -31,21 +26,30 @@ module Onebox def lines return @lines if defined?(@lines) - response = Onebox::Helpers.fetch_response("http://pastebin.com/raw/#{paste_key}", redirect_limit: 1) rescue "" + response = + begin + Onebox::Helpers.fetch_response( + "http://pastebin.com/raw/#{paste_key}", + redirect_limit: 1, + ) + rescue StandardError + "" + end @lines = response.split("\n") end def paste_key - regex = case uri - when /\/raw\// - /\/raw\/([^\/]+)/ - when /\/download\// - /\/download\/([^\/]+)/ - when /\/embed\// - /\/embed\/([^\/]+)/ - else - /\/([^\/]+)/ - end + regex = + case uri + when %r{/raw/} + %r{/raw/([^/]+)} + when %r{/download/} + %r{/download/([^/]+)} + when %r{/embed/} + %r{/embed/([^/]+)} + else + %r{/([^/]+)} + end match = uri.path.match(regex) match[1] if match && match[1] diff --git a/lib/onebox/engine/pdf_onebox.rb b/lib/onebox/engine/pdf_onebox.rb index 2a8d46f0d4a..30af7975b03 100644 --- a/lib/onebox/engine/pdf_onebox.rb +++ b/lib/onebox/engine/pdf_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include LayoutSupport - matches_regexp(/^(https?:)?\/\/.*\.pdf(\?.*)?$/i) + matches_regexp(%r{^(https?:)?//.*\.pdf(\?.*)?$}i) always_https private @@ -14,7 +14,7 @@ module Onebox def data begin size = Onebox::Helpers.fetch_content_length(@url) - rescue + rescue StandardError raise "Unable to read pdf file: #{@url}" end diff --git a/lib/onebox/engine/pubmed_onebox.rb b/lib/onebox/engine/pubmed_onebox.rb index 366d5d50299..fe3a39da27c 100644 --- a/lib/onebox/engine/pubmed_onebox.rb +++ b/lib/onebox/engine/pubmed_onebox.rb @@ -6,22 +6,22 @@ module Onebox include Engine include LayoutSupport - matches_regexp(/^https?:\/\/(?:(?:\w)+\.)?(www.ncbi.nlm.nih)\.gov(?:\/)?\/pubmed\/\d+/) + matches_regexp(%r{^https?://(?:(?:\w)+\.)?(www.ncbi.nlm.nih)\.gov(?:/)?/pubmed/\d+}) private def xml return @xml if defined?(@xml) - doc = Nokogiri::XML(URI.join(@url, "?report=xml&format=text").open) + doc = Nokogiri.XML(URI.join(@url, "?report=xml&format=text").open) pre = doc.xpath("//pre") - @xml = Nokogiri::XML("" + pre.text + "") + @xml = Nokogiri.XML("" + pre.text + "") end def authors initials = xml.css("Initials").map { |x| x.content } last_names = xml.css("LastName").map { |x| x.content } author_list = (initials.zip(last_names)).map { |i, l| i + " " + l } - if author_list.length > 1 then + if author_list.length > 1 author_list[-2] = author_list[-2] + " and " + author_list[-1] author_list.pop end @@ -29,7 +29,8 @@ module Onebox end def date - xml.css("PubDate") + xml + .css("PubDate") .children .map { |x| x.content } .select { |s| !s.match(/^\s+$/) } @@ -48,7 +49,7 @@ module Onebox abstract: xml.css("AbstractText").text, date: date, link: @url, - pmid: match[:pmid] + pmid: match[:pmid], } end diff --git a/lib/onebox/engine/reddit_media_onebox.rb b/lib/onebox/engine/reddit_media_onebox.rb index d479a6b6496..876ed41fd56 100644 --- a/lib/onebox/engine/reddit_media_onebox.rb +++ b/lib/onebox/engine/reddit_media_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/(www\.)?reddit\.com/) + matches_regexp(%r{^https?://(www\.)?reddit\.com}) def to_html if raw[:type] == "image" @@ -25,7 +25,7 @@ module Onebox HTML - elsif raw[:type] =~ /^video[\/\.]/ + elsif raw[:type] =~ %r{^video[/\.]} <<-HTML