From b6963b8ffb274fb191df79149be2d7d412f19d8e Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Sun, 26 Aug 2018 16:31:02 +0200 Subject: [PATCH 001/124] FIX: Ignore OneBox blacklisted domains. --- lib/final_destination.rb | 26 +++++++++++++++-------- lib/oneboxer.rb | 6 ++++-- spec/components/final_destination_spec.rb | 8 +++++++ spec/components/oneboxer_spec.rb | 9 ++++++++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/final_destination.rb b/lib/final_destination.rb index 7e1ed92396..b69b2e0ebd 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -26,7 +26,7 @@ class FinalDestination "HTTPS_DOMAIN_#{domain}" end - attr_reader :status, :cookie, :status_code + attr_reader :status, :cookie, :status_code, :ignored def initialize(url, opts = nil) @url = url @@ -36,7 +36,15 @@ class FinalDestination @force_get_hosts = @opts[:force_get_hosts] || [] @opts[:max_redirects] ||= 5 @opts[:lookup_ip] ||= lambda { |host| FinalDestination.lookup_ip(host) } - @ignored = [Discourse.base_url_no_prefix] + (@opts[:ignore_redirects] || []) + + @ignored = @opts[:ignore_hostnames] || [] + [Discourse.base_url_no_prefix].concat(@opts[:ignore_redirects] || []).each do |url| + url = uri(url) + if url.present? && url.hostname + @ignored << url.hostname + end + end + @limit = @opts[:max_redirects] @status = :ready @http_verb = @force_get_hosts.any? { |host| hostname_matches?(host) } ? :get : :head @@ -131,18 +139,18 @@ class FinalDestination return nil end - @ignored.each do |host| - if hostname_matches?(host) - @status = :resolved - return @uri - end - end - unless validate_uri log(:warn, "FinalDestination could not resolve URL (invalid URI): #{@uri}") if @verbose return nil end + @ignored.each do |host| + if @uri&.hostname&.match?(host) + @status = :resolved + return @uri + end + end + headers = request_headers response = Excon.public_send(@http_verb, @uri.to_s, diff --git a/lib/oneboxer.rb b/lib/oneboxer.rb index 2f99c0f814..a0f48d43b3 100644 --- a/lib/oneboxer.rb +++ b/lib/oneboxer.rb @@ -250,9 +250,11 @@ module Oneboxer def self.external_onebox(url) Rails.cache.fetch(onebox_cache_key(url), expires_in: 1.day) do - fd = FinalDestination.new(url, ignore_redirects: ignore_redirects, force_get_hosts: force_get_hosts) + ignored = SiteSetting.onebox_domains_blacklist.split("|") + + fd = FinalDestination.new(url, ignore_redirects: ignore_redirects, ignore_hostnames: ignored, force_get_hosts: force_get_hosts) uri = fd.resolve - return blank_onebox if uri.blank? || SiteSetting.onebox_domains_blacklist.include?(uri.hostname) + return blank_onebox if uri.blank? || ignored.map { |hostname| uri.hostname.match?(hostname) }.any? options = { cache: {}, diff --git a/spec/components/final_destination_spec.rb b/spec/components/final_destination_spec.rb index 6a30757a7c..78105c7f0d 100644 --- a/spec/components/final_destination_spec.rb +++ b/spec/components/final_destination_spec.rb @@ -47,6 +47,14 @@ describe FinalDestination do FinalDestination.new(url, opts) end + it 'correctly parses ignored hostnames' do + fd = FinalDestination.new('https://meta.discourse.org', + ignore_redirects: ['http://google.com', 'youtube.com', 'https://meta.discourse.org', '://bing.com'] + ) + + expect(fd.ignored).to eq(['test.localhost', 'google.com', 'meta.discourse.org']) + end + describe '.resolve' do it "has a ready status code before anything happens" do diff --git a/spec/components/oneboxer_spec.rb b/spec/components/oneboxer_spec.rb index b5ebd305ce..79520340ab 100644 --- a/spec/components/oneboxer_spec.rb +++ b/spec/components/oneboxer_spec.rb @@ -107,4 +107,13 @@ describe Oneboxer do end end + it "does not crawl blacklisted URLs" do + SiteSetting.onebox_domains_blacklist = "git.*.com|bitbucket.com" + url = 'https://github.com/discourse/discourse/commit/21b562852885f883be43032e03c709241e8e6d4f' + stub_request(:head, 'https://discourse.org/').to_return(status: 302, body: "", headers: { location: url }) + + expect(Oneboxer.external_onebox(url)[:onebox]).to be_empty + expect(Oneboxer.external_onebox('https://discourse.org/')[:onebox]).to be_empty + end + end From 4b6381367e780399076f9342986e369ee8a80dc7 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 30 Aug 2018 15:57:11 -0400 Subject: [PATCH 002/124] add support for Excon connection options in hub requests --- lib/discourse_hub.rb | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/discourse_hub.rb b/lib/discourse_hub.rb index cf6df955ee..bedcaeb7ce 100644 --- a/lib/discourse_hub.rb +++ b/lib/discourse_hub.rb @@ -39,23 +39,33 @@ module DiscourseHub end def self.singular_action(action, rel_url, params = {}) + connect_opts = connect_opts(params) JSON.parse(Excon.send(action, "#{hub_base_url}#{rel_url}", - headers: { 'Referer' => referer, 'Accept' => accepts.join(', ') }, - query: params, - omit_default_port: true + { + 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) JSON.parse(Excon.send(action, "#{hub_base_url}#{rel_url}", - body: JSON[params], - headers: { 'Referer' => referer, 'Accept' => accepts.join(', '), "Content-Type" => "application/json" }, - omit_default_port: true + { + body: JSON[params], + headers: { 'Referer' => referer, 'Accept' => accepts.join(', '), "Content-Type" => "application/json" }, + omit_default_port: true + }.merge(connect_opts) ).body) end + def self.connect_opts(params = {}) + params.delete(:connect_opts)&.except(:body, :headers, :query) || {} + end + def self.hub_base_url if Rails.env.production? ENV['HUB_BASE_URL'] || 'https://api.discourse.org/api' From ae532f854865ee5dc0d3f81cc517d2450f67f1d4 Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Thu, 30 Aug 2018 14:28:40 -0600 Subject: [PATCH 003/124] FIX: return 422 for an invalid group name on category create --- app/controllers/categories_controller.rb | 2 ++ spec/requests/categories_controller_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index e709d0e865..3bef1bd62c 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -134,6 +134,8 @@ class CategoriesController < ApplicationController else return render_json_error(@category) unless @category.save end + rescue ArgumentError => e + render json: { errors: [e.message] }, status: 422 end def update diff --git a/spec/requests/categories_controller_spec.rb b/spec/requests/categories_controller_spec.rb index ddcd111f51..ff43580fcd 100644 --- a/spec/requests/categories_controller_spec.rb +++ b/spec/requests/categories_controller_spec.rb @@ -99,6 +99,18 @@ describe CategoriesController do expect(response.status).to eq(422) end + + it "returns errors with invalid group" do + category = Fabricate(:category, user: admin) + readonly = CategoryGroup.permission_types[:readonly] + + post "/categories.json", params: { + name: category.name, color: "ff0", text_color: "fff", permissions: {"invalid_group" => readonly} + } + + expect(response.status).to eq(422) + expect(JSON.parse(response.body)['errors']).to be_present + end end describe "success" do From c6f339a0b5ea550ac917f84c0db66e120a958f45 Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Thu, 30 Aug 2018 14:39:40 -0600 Subject: [PATCH 004/124] format json better with spaces in my test --- spec/requests/categories_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/categories_controller_spec.rb b/spec/requests/categories_controller_spec.rb index ff43580fcd..fa6145d649 100644 --- a/spec/requests/categories_controller_spec.rb +++ b/spec/requests/categories_controller_spec.rb @@ -105,7 +105,7 @@ describe CategoriesController do readonly = CategoryGroup.permission_types[:readonly] post "/categories.json", params: { - name: category.name, color: "ff0", text_color: "fff", permissions: {"invalid_group" => readonly} + name: category.name, color: "ff0", text_color: "fff", permissions: { "invalid_group" => readonly } } expect(response.status).to eq(422) From 297e8aaf2e0c3f0929d53d9dbb608d278f433ec0 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 31 Aug 2018 03:02:24 +0530 Subject: [PATCH 005/124] FIX: Escape regex pattern variable before using it --- lib/i18n/backend/discourse_i18n.rb | 2 +- spec/components/discourse_i18n_spec.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/i18n/backend/discourse_i18n.rb b/lib/i18n/backend/discourse_i18n.rb index fec8150c79..553de8eb50 100644 --- a/lib/i18n/backend/discourse_i18n.rb +++ b/lib/i18n/backend/discourse_i18n.rb @@ -44,7 +44,7 @@ module I18n results = {} fallbacks(locale).each do |fallback| - find_results(/#{query}/i, results, translations[fallback]) + find_results(/#{Regexp.escape(query)}/i, results, translations[fallback]) end results diff --git a/spec/components/discourse_i18n_spec.rb b/spec/components/discourse_i18n_spec.rb index 72dfc368ce..bf51e6e725 100644 --- a/spec/components/discourse_i18n_spec.rb +++ b/spec/components/discourse_i18n_spec.rb @@ -12,6 +12,7 @@ describe I18n::Backend::DiscourseI18n do backend.store_translations(:en, items: { one: 'one item', other: "%{count} items" }) backend.store_translations(:de, bar: 'Bar in :de') backend.store_translations(:ru, baz: 'Baz in :ru') + backend.store_translations(:en, link: '[text](url)') end after do @@ -32,6 +33,7 @@ describe I18n::Backend::DiscourseI18n do expect(backend.search(:en, 'Foo')).to eq('foo' => 'Foo in :en') expect(backend.search(:en, 'hello')).to eq('wat' => 'Hello %{count}') expect(backend.search(:en, 'items.one')).to eq('items.one' => 'one item') + expect(backend.search(:en, '](')).to eq('link' => '[text](url)') end it 'can return multiple results' do From f3afc0cf76197c502461cd26b0deeaf3c0818e4f Mon Sep 17 00:00:00 2001 From: Joshua Rosenfeld Date: Thu, 30 Aug 2018 21:01:10 -0400 Subject: [PATCH 006/124] Add raw date to title attribute on old flags page (#6349) --- app/assets/javascripts/admin/templates/components/flag-user.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/templates/components/flag-user.hbs b/app/assets/javascripts/admin/templates/components/flag-user.hbs index 92f6871847..26087789e5 100644 --- a/app/assets/javascripts/admin/templates/components/flag-user.hbs +++ b/app/assets/javascripts/admin/templates/components/flag-user.hbs @@ -7,7 +7,7 @@ {{#link-to 'adminUser' user.id user.username class="flag-user-username"}} {{user.username}} {{/link-to}} -
+
{{format-age date}}
From 81b99efc681c46252599c738e989d873bdc20eea Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 31 Aug 2018 09:26:28 +0800 Subject: [PATCH 007/124] DEV: Raise an error if thread doesn't return within expected time. --- .../postgresql_fallback_adapter_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index a5085b6c81..80d0cce5bd 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -40,9 +40,12 @@ describe ActiveRecord::ConnectionHandling do postgresql_fallback_handler.setup! Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) ActiveRecord::Base.unstub(:postgresql_connection) - ActiveRecord::Base.establish_connection - (Thread.list - @threads).each { |thread| thread.join(5) } + (Thread.list - @threads).each do |thread| + raise "Thread still running" unless thread.join(5) + end + + ActiveRecord::Base.establish_connection end describe "#postgresql_fallback_connection" do From 9b7cab589ac15a034d1c0e700230c1b3f63f8ba0 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 31 Aug 2018 11:46:55 +1000 Subject: [PATCH 008/124] FIX: revert diacritic stripping See more details in test case and at: https://meta.discourse.org/t/discourse-should-ignore-if-a-character-is-accented-when-doing-a-search/90198/16?u=sam --- app/services/search_indexer.rb | 11 +++++++---- lib/search.rb | 3 +-- spec/components/search_spec.rb | 8 ++++++-- spec/services/search_indexer_spec.rb | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index eca12e0578..04dc32b697 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -176,14 +176,16 @@ class SearchIndexer attr_reader :scrubbed - def initialize + def initialize(strip_diacritics: false) @scrubbed = +"" + # for now we are disabling this per: https://meta.discourse.org/t/discourse-should-ignore-if-a-character-is-accented-when-doing-a-search/90198/16?u=sam + @strip_diacritics = strip_diacritics end - def self.scrub(html) + def self.scrub(html, strip_diacritics: false) return +"" if html.blank? - me = new + me = new(strip_diacritics: strip_diacritics) Nokogiri::HTML::SAX::Parser.new(me).parse("
#{html}
") me.scrubbed end @@ -201,7 +203,8 @@ class SearchIndexer DIACRITICS ||= /([\u0300-\u036f]|[\u1AB0-\u1AFF]|[\u1DC0-\u1DFF]|[\u20D0-\u20FF])/ def characters(str) - scrubbed << " #{HtmlScrubber.strip_diacritics(str)} " + str = HtmlScrubber.strip_diacritics(str) if @strip_diacritics + scrubbed << " #{str} " end end end diff --git a/lib/search.rb b/lib/search.rb index a90f651518..3cd0a20a14 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -828,10 +828,9 @@ class Search end def ts_query(ts_config = nil, weight_filter: nil) - # we must strip diacritics otherwise we will get no matches @ts_query_cache ||= {} @ts_query_cache["#{ts_config || default_ts_config} #{@term} #{weight_filter}"] ||= - Search.ts_query(term: SearchIndexer::HtmlScrubber.strip_diacritics(@term), ts_config: ts_config, weight_filter: weight_filter) + Search.ts_query(term: @term, ts_config: ts_config, weight_filter: weight_filter) end def wrap_rows(query) diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index c5e98e9a5d..1b59d8a118 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -1006,11 +1006,15 @@ describe Search do results = Search.execute('hello', type_filter: 'topic') expect(results.posts.length).to eq(1) + # TODO when we add diacritic support we should return 1 here results = Search.execute('regis', type_filter: 'topic') + expect(results.posts.length).to eq(0) + + results = Search.execute('Régis', type_filter: 'topic', include_blurbs: true) expect(results.posts.length).to eq(1) - results = Search.execute('Régis', type_filter: 'topic') - expect(results.posts.length).to eq(1) + # this is a test we got to keep working + expect(results.blurb(results.posts.first)).to include('Régis') results = Search.execute('สวัสดี', type_filter: 'topic') expect(results.posts.length).to eq(1) diff --git a/spec/services/search_indexer_spec.rb b/spec/services/search_indexer_spec.rb index 42a61fa367..e98d52e365 100644 --- a/spec/services/search_indexer_spec.rb +++ b/spec/services/search_indexer_spec.rb @@ -32,7 +32,7 @@ describe SearchIndexer do it 'removes diacritics' do html = "

HELLO Hétérogénéité Здравствуйте هتاف للترحيب 你好

" - scrubbed = SearchIndexer::HtmlScrubber.scrub(html) + scrubbed = SearchIndexer::HtmlScrubber.scrub(html, strip_diacritics: true) expect(scrubbed).to eq(" HELLO Heterogeneite Здравствуите هتاف للترحيب 你好 ") end From 6b9aeeea737a7f3db08d9f16a5d049474b50a303 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 31 Aug 2018 08:40:36 +0530 Subject: [PATCH 009/124] bump onebox version --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/assets/stylesheets/common/base/onebox.scss | 13 +++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 2d077d6913..ce94177e4c 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.58' +gem 'onebox', '1.8.59' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index c6cb017879..a722939dca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -257,7 +257,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.58) + onebox (1.8.59) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -509,7 +509,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.58) + onebox (= 1.8.59) openid-redis-store pg pry-nav diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index dafc0c3ffc..548b23cc84 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -247,6 +247,19 @@ aside.onebox { // instagram fixes .instagram-images { clear: both; + position: relative; + + .instagram-video-icon { + &:before { + font-family: FontAwesome; + font-size: $font-up-5; + content: "\f144"; + } + bottom: 10px; + right: 10px; + position: absolute; + color: white; + } .instagram-image { padding: 5px 5px 5px 5px; From e1975e293f2625259e925b4a3c93d88d5acfcaa8 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 31 Aug 2018 14:46:22 +1000 Subject: [PATCH 010/124] FIX: when uploads are destroyed clear up avatar refs in user table This also auto corrects twice daily when we ensure consistency --- app/jobs/scheduled/ensure_db_consistency.rb | 2 ++ app/models/upload.rb | 6 ++++ app/models/user.rb | 14 +++++++++ app/models/user_avatar.rb | 25 +++++++++++++++ spec/models/user_avatar_spec.rb | 34 +++++++++++++++++++++ spec/models/user_spec.rb | 20 ++++++++++++ 6 files changed, 101 insertions(+) diff --git a/app/jobs/scheduled/ensure_db_consistency.rb b/app/jobs/scheduled/ensure_db_consistency.rb index 03060c5d3e..4f0b6d6ce0 100644 --- a/app/jobs/scheduled/ensure_db_consistency.rb +++ b/app/jobs/scheduled/ensure_db_consistency.rb @@ -17,6 +17,8 @@ module Jobs UserOption.ensure_consistency! Tag.ensure_consistency! CategoryTagStat.ensure_consistency! + User.ensure_consistency! + UserAvatar.ensure_consistency! end end end diff --git a/app/models/upload.rb b/app/models/upload.rb index 25d7ecc472..4ab07e4ce1 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -24,6 +24,12 @@ class Upload < ActiveRecord::Base validates_with ::Validators::UploadValidator + after_destroy do + User.where(uploaded_avatar_id: self.id).update_all(uploaded_avatar_id: nil) + UserAvatar.where(gravatar_upload_id: self.id).update_all(gravatar_upload_id: nil) + UserAvatar.where(custom_upload_id: self.id).update_all(custom_upload_id: nil) + end + def thumbnail(width = self.thumbnail_width, height = self.thumbnail_height) optimized_images.find_by(width: width, height: height) end diff --git a/app/models/user.rb b/app/models/user.rb index 08ec67d06c..94e7bbf7b1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1283,6 +1283,20 @@ class User < ActiveRecord::Base true end + def self.ensure_consistency! + DB.exec <<~SQL + UPDATE users + SET uploaded_avatar_id = NULL + WHERE uploaded_avatar_id IN ( + SELECT u1.uploaded_avatar_id FROM users u1 + LEFT JOIN uploads up + ON u1.uploaded_avatar_id = up.id + WHERE u1.uploaded_avatar_id IS NOT NULL AND + up.id IS NULL + ) + SQL + end + end # == Schema Information diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index afc0c1dc0e..69d0ff7764 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -123,6 +123,31 @@ class UserAvatar < ActiveRecord::Base tempfile.close! if tempfile && tempfile.respond_to?(:close!) end + def self.ensure_consistency! + DB.exec <<~SQL + UPDATE user_avatars + SET gravatar_upload_id = NULL + WHERE gravatar_upload_id IN ( + SELECT u1.gravatar_upload_id FROM user_avatars u1 + LEFT JOIN uploads up + ON u1.gravatar_upload_id = up.id + WHERE u1.gravatar_upload_id IS NOT NULL AND + up.id IS NULL + ) + SQL + + DB.exec <<~SQL + UPDATE user_avatars + SET custom_upload_id = NULL + WHERE custom_upload_id IN ( + SELECT u1.custom_upload_id FROM user_avatars u1 + LEFT JOIN uploads up + ON u1.custom_upload_id = up.id + WHERE u1.custom_upload_id IS NOT NULL AND + up.id IS NULL + ) + SQL + end end # == Schema Information diff --git a/spec/models/user_avatar_spec.rb b/spec/models/user_avatar_spec.rb index f67cd4896c..5e9af7a5ea 100644 --- a/spec/models/user_avatar_spec.rb +++ b/spec/models/user_avatar_spec.rb @@ -118,4 +118,38 @@ describe UserAvatar do end end end + + describe "ensure_consistency!" do + + it "will clean up dangling avatars" do + upload1 = Fabricate(:upload) + upload2 = Fabricate(:upload) + + user_avatar = Fabricate(:user).user_avatar + + user_avatar.update_columns( + gravatar_upload_id: upload1.id, + custom_upload_id: upload2.id + ) + + upload1.destroy! + upload2.destroy! + + user_avatar.reload + expect(user_avatar.gravatar_upload_id).to eq(nil) + expect(user_avatar.custom_upload_id).to eq(nil) + + user_avatar.update_columns( + gravatar_upload_id: upload1.id, + custom_upload_id: upload2.id + ) + + UserAvatar.ensure_consistency! + + user_avatar.reload + expect(user_avatar.gravatar_upload_id).to eq(nil) + expect(user_avatar.custom_upload_id).to eq(nil) + end + + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0fa4bd5126..948a315d52 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1794,4 +1794,24 @@ describe User do end end + describe "ensure_consistency!" do + + it "will clean up dangling avatars" do + upload = Fabricate(:upload) + user = Fabricate(:user, uploaded_avatar_id: upload.id) + + upload.destroy! + user.reload + expect(user.uploaded_avatar_id).to eq(nil) + + user.update_columns(uploaded_avatar_id: upload.id) + + User.ensure_consistency! + + user.reload + expect(user.uploaded_avatar_id).to eq(nil) + end + + end + end From 1866a8e8daef8ae88631e2322e201cd021b749bc Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 31 Aug 2018 15:06:30 +1000 Subject: [PATCH 011/124] correct invalid spec --- spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb b/spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb index a2323b3152..c32a84315a 100644 --- a/spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb +++ b/spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb @@ -16,8 +16,9 @@ RSpec.describe Jobs::FixOutOfSyncUserUploadedAvatar do custom_upload2 = Fabricate(:upload, user: user_out_of_sync) gravatar_upload2 = Fabricate(:upload, user: user_out_of_sync) prev_gravatar_upload = Fabricate(:upload, user: user_out_of_sync) - user_out_of_sync.update!(uploaded_avatar: prev_gravatar_upload) + prev_gravatar_upload.destroy! + user_out_of_sync.update!(uploaded_avatar_id: prev_gravatar_upload.id) user_out_of_sync.user_avatar.update!( custom_upload: custom_upload2, @@ -38,5 +39,6 @@ RSpec.describe Jobs::FixOutOfSyncUserUploadedAvatar do expect(user_without_uploaded_avatar.reload.uploaded_avatar) .to eq(nil) + end end From b3aab1770fc8ee994586296c8367b2cb32c6ec89 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 31 Aug 2018 17:07:31 +1000 Subject: [PATCH 012/124] FIX: set old last modified date for invalid avatars In some cases Akami was holding tight to these invalid avatars, to avoid this happening we explain the avatar image is ancient then when a new upload is added it automatically is older than this. --- app/controllers/user_avatars_controller.rb | 2 +- spec/requests/user_avatars_controller_spec.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index 271cd41257..39050be70b 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -173,7 +173,7 @@ class UserAvatarsController < ApplicationController def render_blank path = Rails.root + "public/images/avatar.png" expires_in 10.minutes, public: true - response.headers["Last-Modified"] = 10.minutes.ago.httpdate + response.headers["Last-Modified"] = Time.new('1990-01-01').httpdate response.headers["Content-Length"] = File.size(path).to_s send_file path, disposition: nil end diff --git a/spec/requests/user_avatars_controller_spec.rb b/spec/requests/user_avatars_controller_spec.rb index 73bbfe16cf..5e8d0e79b0 100644 --- a/spec/requests/user_avatars_controller_spec.rb +++ b/spec/requests/user_avatars_controller_spec.rb @@ -107,7 +107,9 @@ describe UserAvatarsController do get "/user_avatar/default/xxx/51/777.png" expect(response.status).to eq(200) - expect(response.headers["Last-Modified"]).to eq(10.minutes.ago.httpdate) + + # this image should be really old so when it is fixed various algorithms pick it up + expect(response.headers["Last-Modified"]).to eq(Time.new('1990-01-01').httpdate) end it 'serves image even if size missing and its in local mode' do From 931cffcebee10128a1e492a35990809d5da8e3ab Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 31 Aug 2018 10:18:06 +0200 Subject: [PATCH 013/124] FEATURE: Let users see their user auth tokens. (#6313) --- .../controllers/preferences/account.js.es6 | 13 +++ .../templates/preferences/account.hbs | 68 ++++++++++++++ .../stylesheets/common/base/discourse.scss | 54 ++++++++++++ app/controllers/users_controller.rb | 16 +++- app/models/user.rb | 1 + app/models/user_auth_token.rb | 11 +++ app/models/user_auth_token_log.rb | 1 + app/serializers/user_auth_token_serializer.rb | 88 +++++++++++++++++++ app/serializers/user_serializer.rb | 12 ++- config/locales/client.en.yml | 13 +++ config/locales/server.en.yml | 12 +++ config/routes.rb | 1 + config/site_settings.yml | 2 +- .../web_hook_user_serializer_spec.rb | 2 +- 14 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 app/serializers/user_auth_token_serializer.rb diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index 1dd6e3d949..1697b06c2d 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -6,6 +6,8 @@ import { setting } from "discourse/lib/computed"; import { popupAjaxError } from "discourse/lib/ajax-error"; import showModal from "discourse/lib/show-modal"; import { findAll } from "discourse/models/login-method"; +import { ajax } from "discourse/lib/ajax"; +import { userPath } from "discourse/lib/url"; export default Ember.Controller.extend( CanCheckEmails, @@ -194,6 +196,17 @@ export default Ember.Controller.extend( }); }, + toggleToken(token) { + Ember.set(token, 'visible', !token.visible); + }, + + revokeAuthToken() { + ajax( + userPath(`${this.get('model.username_lower')}/preferences/revoke-auth-token`), + { type: "POST" } + ); + }, + connectAccount(method) { method.doLogin(); } diff --git a/app/assets/javascripts/discourse/templates/preferences/account.hbs b/app/assets/javascripts/discourse/templates/preferences/account.hbs index 276caf9c0f..2d1d283337 100644 --- a/app/assets/javascripts/discourse/templates/preferences/account.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/account.hbs @@ -161,6 +161,74 @@ {{/if}} +{{#if canCheckEmails}} +
+ + {{d-icon "sign-out"}} {{i18n 'user.auth_tokens.logout'}} + {{#each model.user_auth_tokens as |token|}} +
+
+
+ {{d-icon token.icon}} {{token.device_name}} + {{#if token.visible}} + {{d-icon "angle-double-up"}} + {{else}} + {{d-icon "angle-double-down"}} + {{/if}} +
+
+ {{token.client_ip}} — {{format-date token.seen_at}} +
+
+ {{#if token.visible}} +
+
+
{{i18n 'user.auth_tokens.ip_address'}}
+
{{token.client_ip}}
+
+
+
{{i18n 'user.auth_tokens.first_seen'}}
+
{{format-date token.created_at}}
+
+
+
{{i18n 'user.auth_tokens.last_seen'}}
+
{{format-date token.seen_at}}
+
+
+
{{i18n 'user.auth_tokens.operating_system'}}
+
{{token.os}}
+
+
+ {{/if}} +
+ {{/each}} +
+{{/if}} + +{{#if canCheckEmails}} + {{#if model.staff}} +
+ + {{#if model.user_auth_token_logs}} + + + + + + + {{#each model.user_auth_token_logs as |token|}} + + + + + + {{/each}} +
{{i18n 'user.auth_tokens.ip_address'}}{{i18n 'user.auth_tokens.created'}}{{i18n 'user.auth_tokens.operating_system'}}
{{token.client_ip}}{{format-date token.created_at}}{{d-icon token.icon}} {{token.os}}
+ {{/if}} +
+ {{/if}} +{{/if}} + {{plugin-outlet name="user-preferences-account" args=(hash model=model save=(action "save"))}}
diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index a2f54c184e..44fadad1eb 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -475,6 +475,16 @@ select { .control-group { @include clearfix; + + .table { + width: 100%; + + th, + td { + padding: 10px; + text-align: center; + } + } } .control-label { @@ -544,3 +554,47 @@ select { .inline { display: inline; } + +.pref-auth-tokens { + .control-label { + display: inline-block; + } + + .row { + margin: 5px 0px; + } + + .muted { + color: #888; + } + + .perf-auth-token { + background: #f9f9f9; + padding: 5px; + margin-bottom: 10px; + } + + .auth-token-summary { + padding: 0px 10px; + + .auth-token-label, .auth-token-value { + font-size: 1.2em; + margin-top: 5px; + } + } + + .auth-token-details { + background: #fff; + padding: 5px 10px; + margin: 10px 5px 5px 5px; + + .auth-token-label { + color: #888; + } + } + + .auth-token-label, .auth-token-value { + float: left; + width: 50%; + } +} diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index ae253dd5ba..29dc694d26 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -13,7 +13,8 @@ class UsersController < ApplicationController :username, :update, :user_preferences_redirect, :upload_user_image, :pick_avatar, :destroy_user_image, :destroy, :check_emails, :topic_tracking_state, :preferences, :create_second_factor, - :update_second_factor, :create_second_factor_backup, :select_avatar + :update_second_factor, :create_second_factor_backup, :select_avatar, + :revoke_auth_token ] skip_before_action :check_xhr, only: [ @@ -1097,6 +1098,19 @@ class UsersController < ApplicationController end end + def revoke_auth_token + user = fetch_user_from_params + guardian.ensure_can_edit!(user) + + UserAuthToken.where(user_id: user.id).destroy_all + + MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id] + + render json: { + success: true + } + end + private def honeypot_value diff --git a/app/models/user.rb b/app/models/user.rb index 94e7bbf7b1..9fe43dc6fc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,6 +47,7 @@ class User < ActiveRecord::Base has_many :email_change_requests, dependent: :destroy has_many :directory_items, dependent: :delete_all has_many :user_auth_tokens, dependent: :destroy + has_many :user_auth_token_logs, dependent: :destroy has_many :group_users, dependent: :destroy has_many :groups, through: :group_users diff --git a/app/models/user_auth_token.rb b/app/models/user_auth_token.rb index b7549486ca..3f6ca49acd 100644 --- a/app/models/user_auth_token.rb +++ b/app/models/user_auth_token.rb @@ -11,8 +11,19 @@ class UserAuthToken < ActiveRecord::Base # used when token did not arrive at client URGENT_ROTATE_TIME = 1.minute + USER_ACTIONS = ['generate'] + attr_accessor :unhashed_auth_token + before_destroy do + UserAuthToken.log(action: 'destroy', + user_auth_token_id: self.id, + user_id: self.user_id, + user_agent: self.user_agent, + client_ip: self.client_ip, + auth_token: self.auth_token) + end + def self.log(info) if SiteSetting.verbose_auth_token_logging UserAuthTokenLog.create!(info) diff --git a/app/models/user_auth_token_log.rb b/app/models/user_auth_token_log.rb index 532d363d73..b2edaebe54 100644 --- a/app/models/user_auth_token_log.rb +++ b/app/models/user_auth_token_log.rb @@ -1,4 +1,5 @@ class UserAuthTokenLog < ActiveRecord::Base + belongs_to :user end # == Schema Information diff --git a/app/serializers/user_auth_token_serializer.rb b/app/serializers/user_auth_token_serializer.rb new file mode 100644 index 0000000000..1b59cfb3e2 --- /dev/null +++ b/app/serializers/user_auth_token_serializer.rb @@ -0,0 +1,88 @@ +class UserAuthTokenSerializer < ApplicationSerializer + attributes :id, + :action, + :client_ip, + :created_at, + :seen_at, + :os, + :device_name, + :icon + + def action + case object.action + when 'generate' + I18n.t('log_in') + when 'destroy' + I18n.t('unsubscribe.log_out') + else + I18n.t('staff_action_logs.unknown') + end + end + + def include_action? + object.has_attribute?(:action) + end + + def client_ip + object.client_ip.to_s + end + + def include_seen_at? + object.has_attribute?(:seen_at) + end + + def os + case object.user_agent + when /Android/i + 'Android' + when /Linux/i + 'Linux' + when /Windows/i + 'Windows' + when /macOS/i + 'macOS' + when /iPhone|iPad|iPod/i + 'iOS' + else + I18n.t('staff_action_logs.unknown') + end + end + + def device_name + case object.user_agent + when /Android/i + I18n.t('user_auth_tokens.devices.android') + when /Linux/i + I18n.t('user_auth_tokens.devices.linux') + when /Windows/i + I18n.t('user_auth_tokens.devices.windows') + when /macOS/i + I18n.t('user_auth_tokens.devices.mac') + when /iPhone/i + I18n.t('user_auth_tokens.devices.iphone') + when /iPad/i + I18n.t('user_auth_tokens.devices.ipad') + when /iPod/i + I18n.t('user_auth_tokens.devices.ipod') + when /Mobile/i + I18n.t('user_auth_tokens.devices.mobile') + else + I18n.t('user_auth_tokens.devices.unknown') + end + end + + def icon + case os + when 'Linux' + 'linux' + when 'Windows' + 'windows' + when 'macOS', 'iOS' + 'apple' + when 'Android' + 'android' + else + 'question' + end + end +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 6b0ead5050..b4bd2426d1 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -112,7 +112,9 @@ class UserSerializer < BasicUserSerializer :muted_usernames, :mailing_list_posts_per_day, :can_change_bio, - :user_api_keys + :user_api_keys, + :user_auth_tokens, + :user_auth_token_logs untrusted_attributes :bio_raw, :bio_cooked, @@ -193,6 +195,14 @@ class UserSerializer < BasicUserSerializer keys.length > 0 ? keys : nil end + def user_auth_tokens + ActiveModel::ArraySerializer.new(object.user_auth_tokens.order(:seen_at).reverse_order, each_serializer: UserAuthTokenSerializer) + end + + def user_auth_token_logs + ActiveModel::ArraySerializer.new(object.user_auth_token_logs.where(action: UserAuthToken::USER_ACTIONS).order(:created_at).reverse_order, each_serializer: UserAuthTokenSerializer) + end + def bio_raw object.user_profile.bio_raw end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 79cfbd5915..3aa3cbe34a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -863,6 +863,19 @@ en: password_confirmation: title: "Password Again" + auth_tokens: + title: "Recently Used Devices" + title_logs: "Authentication Logs" + ip_address: "IP Address" + created: "Created" + first_seen: "First Seen" + last_seen: "Last Seen" + operating_system: "Operating System" + location: "Location" + action: "Action" + login: "Log in" + logout: "Log out everywhere" + last_posted: "Last Post" last_emailed: "Last Emailed" last_seen: "Seen" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 784ed97e34..2226486931 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -698,6 +698,18 @@ en: invalid_token: "Sorry, that email login link is too old. Select the Log In button and use 'I forgot my password' to get a new link." title: "Email login" + user_auth_tokens: + devices: + android: 'Android Device' + linux: 'Linux Computer' + windows: 'Windows Computer' + mac: 'Mac' + iphone: 'iPhone' + ipad: 'iPad' + ipod: 'iPod' + mobile: 'Mobile Device' + unknown: 'Unknown device' + change_email: confirmed: "Your email has been updated." please_continue: "Continue to %{site_name}" diff --git a/config/routes.rb b/config/routes.rb index 6308048a14..c661b0a727 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -413,6 +413,7 @@ Discourse::Application.routes.draw do put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username } put "#{root_path}/:username/preferences/avatar/select" => "users#select_avatar", constraints: { username: RouteFormat.username } post "#{root_path}/:username/preferences/revoke-account" => "users#revoke_account", constraints: { username: RouteFormat.username } + post "#{root_path}/:username/preferences/revoke-auth-token" => "users#revoke_auth_token", constraints: { username: RouteFormat.username } get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username } get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username } get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username } diff --git a/config/site_settings.yml b/config/site_settings.yml index 005c9f081c..cdb0582c00 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -333,7 +333,7 @@ login: verbose_sso_logging: false verbose_auth_token_logging: hidden: true - default: false + default: true sso_url: default: '' regex: '^https?:\/\/.+[^\/]$' diff --git a/spec/serializers/web_hook_user_serializer_spec.rb b/spec/serializers/web_hook_user_serializer_spec.rb index 6b6d3b1a62..65b5c8d876 100644 --- a/spec/serializers/web_hook_user_serializer_spec.rb +++ b/spec/serializers/web_hook_user_serializer_spec.rb @@ -21,7 +21,7 @@ RSpec.describe WebHookUserSerializer do it 'should only include the required keys' do count = serializer.as_json.keys.count - difference = count - 43 + difference = count - 45 expect(difference).to eq(0), lambda { message = "" From 6ada825a4d6238af61b05bd16bef4ba3392e4d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 31 Aug 2018 10:49:44 +0200 Subject: [PATCH 014/124] fix linting --- .../discourse/controllers/preferences/account.js.es6 | 6 ++++-- app/assets/stylesheets/common/base/discourse.scss | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index 1697b06c2d..554fbf30c6 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -197,12 +197,14 @@ export default Ember.Controller.extend( }, toggleToken(token) { - Ember.set(token, 'visible', !token.visible); + Ember.set(token, "visible", !token.visible); }, revokeAuthToken() { ajax( - userPath(`${this.get('model.username_lower')}/preferences/revoke-auth-token`), + userPath( + `${this.get("model.username_lower")}/preferences/revoke-auth-token` + ), { type: "POST" } ); }, diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 44fadad1eb..49e5cbafda 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -577,7 +577,8 @@ select { .auth-token-summary { padding: 0px 10px; - .auth-token-label, .auth-token-value { + .auth-token-label, + .auth-token-value { font-size: 1.2em; margin-top: 5px; } @@ -593,7 +594,8 @@ select { } } - .auth-token-label, .auth-token-value { + .auth-token-label, + .auth-token-value { float: left; width: 50%; } From ae2f00ee732a97f07b4fad11c346c9ae73148ca3 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 31 Aug 2018 13:44:56 +0800 Subject: [PATCH 015/124] DEV: Include the thread in the error message. --- .../connection_adapters/postgresql_fallback_adapter_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index 80d0cce5bd..94ff62c6fd 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -42,7 +42,9 @@ describe ActiveRecord::ConnectionHandling do ActiveRecord::Base.unstub(:postgresql_connection) (Thread.list - @threads).each do |thread| - raise "Thread still running" unless thread.join(5) + unless thread.join(5) + raise "Thread still running: #{thread}" + end end ActiveRecord::Base.establish_connection From 5a214a687c794381dcbce5d07f00269b055f25a8 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 31 Aug 2018 17:25:56 +0800 Subject: [PATCH 016/124] FIX: Exclude `UserAuthToken` and `UserAuthTokenLog` in user webhook. --- app/serializers/web_hook_user_serializer.rb | 2 ++ spec/serializers/web_hook_user_serializer_spec.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/serializers/web_hook_user_serializer.rb b/app/serializers/web_hook_user_serializer.rb index d715e4b025..1e89fc34d2 100644 --- a/app/serializers/web_hook_user_serializer.rb +++ b/app/serializers/web_hook_user_serializer.rb @@ -24,6 +24,8 @@ class WebHookUserSerializer < UserSerializer can_change_bio user_api_keys group_users + user_auth_tokens + user_auth_token_logs }.each do |attr| define_method("include_#{attr}?") do false diff --git a/spec/serializers/web_hook_user_serializer_spec.rb b/spec/serializers/web_hook_user_serializer_spec.rb index 65b5c8d876..6b6d3b1a62 100644 --- a/spec/serializers/web_hook_user_serializer_spec.rb +++ b/spec/serializers/web_hook_user_serializer_spec.rb @@ -21,7 +21,7 @@ RSpec.describe WebHookUserSerializer do it 'should only include the required keys' do count = serializer.as_json.keys.count - difference = count - 45 + difference = count - 43 expect(difference).to eq(0), lambda { message = "" From 8ce8edaf40fb8ea64eb123519c80dc5d84109ab5 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 31 Aug 2018 15:06:22 +0530 Subject: [PATCH 017/124] bump onebox version --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index ce94177e4c..5de25c77d7 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.59' +gem 'onebox', '1.8.60' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index a722939dca..600cecb1bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -257,7 +257,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.59) + onebox (1.8.60) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -509,7 +509,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.59) + onebox (= 1.8.60) openid-redis-store pg pry-nav From 5310b4841d27a4bc21c686181a352bde1e73d5dd Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 31 Aug 2018 12:01:59 +0200 Subject: [PATCH 018/124] UX: Show Rollback and Backup buttons on same line --- .../javascripts/admin/templates/backups.hbs | 48 +++++++++---------- .../stylesheets/common/admin/backups.scss | 12 ----- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/admin/templates/backups.hbs b/app/assets/javascripts/admin/templates/backups.hbs index ac36550404..2dcd6e02ba 100644 --- a/app/assets/javascripts/admin/templates/backups.hbs +++ b/app/assets/javascripts/admin/templates/backups.hbs @@ -5,33 +5,31 @@ {{nav-item route='admin.backups.index' label='admin.backups.menu.backups'}} {{nav-item route='admin.backups.logs' label='admin.backups.menu.logs'}} {{plugin-outlet name="downloader" tagName=""}} -
- {{#if model.canRollback}} - {{d-button action="rollback" - class="btn-rollback" - label="admin.backups.operations.rollback.label" - title="admin.backups.operations.rollback.title" - icon="ambulance" - disabled=rollbackDisabled}} - {{/if}} - {{#if model.isOperationRunning}} - {{d-button action="cancelOperation" - class="btn-danger" - title="admin.backups.operations.cancel.title" - label="admin.backups.operations.cancel.label" - icon="times"}} - {{else}} - {{d-button action="startBackup" - class="btn-primary" - title="admin.backups.operations.backup.title" - label="admin.backups.operations.backup.label" - icon="rocket"}} - {{/if}} -
+
+ {{#if model.canRollback}} + {{d-button action="rollback" + class="btn-rollback" + label="admin.backups.operations.rollback.label" + title="admin.backups.operations.rollback.title" + icon="ambulance" + disabled=rollbackDisabled}} + {{/if}} + {{#if model.isOperationRunning}} + {{d-button action="cancelOperation" + class="btn-danger" + title="admin.backups.operations.cancel.title" + label="admin.backups.operations.cancel.label" + icon="times"}} + {{else}} + {{d-button action="startBackup" + class="btn-primary" + title="admin.backups.operations.backup.title" + label="admin.backups.operations.backup.label" + icon="rocket"}} + {{/if}} +
- -
diff --git a/app/assets/stylesheets/common/admin/backups.scss b/app/assets/stylesheets/common/admin/backups.scss index ee99986d98..48b3791d82 100644 --- a/app/assets/stylesheets/common/admin/backups.scss +++ b/app/assets/stylesheets/common/admin/backups.scss @@ -17,18 +17,6 @@ $rollback-darker: darken($rollback, 20%) !default; } } -.backups { - .admin-controls { - .backup-operations { - margin-left: auto; - button { - display: flex; - margin-right: 20px; - } - } - } -} - .admin-backups { table { @media screen and (min-width: 550px) { From 91b3f200f481cb6e8a277076ec96ee982785be21 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 31 Aug 2018 12:14:41 +0200 Subject: [PATCH 019/124] UX: Primary button didn't have hover effect anymore --- app/assets/stylesheets/common/components/buttons.scss | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index d8cbd89b58..d8b15095e7 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -88,13 +88,14 @@ } &:hover, &.btn-hover { - color: #fff; - background: dark-light-choose($tertiary, $tertiary); + background: scale-color($tertiary, $lightness: -20%); } &:active, &.btn-active { - @include linear-gradient($tertiary, $tertiary); - color: $secondary; + @include linear-gradient( + scale-color($tertiary, $lightness: -20%), + $tertiary + ); } &[disabled], &.disabled { From 60eff9421af3a724c7942342cf4e26d9bdc2d7c0 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Fri, 31 Aug 2018 14:23:55 +0300 Subject: [PATCH 020/124] FIX: precompile `desktop_theme` and `mobile_theme` stylesheets required for environments that pre stage docker images and keep old image running during the deploy --- lib/stylesheet/manager.rb | 3 +- spec/components/stylesheet/manager_spec.rb | 61 ++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb index 3b4b4400a7..7beded7f1b 100644 --- a/lib/stylesheet/manager.rb +++ b/lib/stylesheet/manager.rb @@ -86,8 +86,9 @@ class Stylesheet::Manager themes = Theme.where('user_selectable OR id = ?', SiteSetting.default_theme_id).pluck(:id, :name) themes << nil themes.each do |id, name| - [:desktop, :mobile, :desktop_rtl, :mobile_rtl].each do |target| + [:desktop, :mobile, :desktop_rtl, :mobile_rtl, :desktop_theme, :mobile_theme, :admin].each do |target| theme_id = id || SiteSetting.default_theme_id + next if target =~ THEME_REGEX && theme_id == -1 cache_key = "#{target}_#{theme_id}" STDERR.puts "precompile target: #{target} #{name}" diff --git a/spec/components/stylesheet/manager_spec.rb b/spec/components/stylesheet/manager_spec.rb index fb6f3fd5db..19426478fb 100644 --- a/spec/components/stylesheet/manager_spec.rb +++ b/spec/components/stylesheet/manager_spec.rb @@ -157,4 +157,65 @@ describe Stylesheet::Manager do expect(digest3).to_not eq(digest1) end end + + # this test takes too long, we don't run it by default + describe ".precompile_css", if: ENV["RUN_LONG_TESTS"] == "1" do + before do + class << STDERR + alias_method :orig_write, :write + def write(x) + end + end + end + + after do + class << STDERR + def write(x) + orig_write(x) + end + end + FileUtils.rm_rf("tmp/stylesheet-cache") + end + + it "correctly generates precompiled CSS" do + scheme1 = ColorScheme.create!(name: "scheme1") + scheme2 = ColorScheme.create!(name: "scheme2") + core_targets = [:desktop, :mobile, :desktop_rtl, :mobile_rtl, :admin] + theme_targets = [:desktop_theme, :mobile_theme] + + Theme.update_all(user_selectable: false) + user_theme = Fabricate(:theme, user_selectable: true, color_scheme: scheme1) + default_theme = Fabricate(:theme, user_selectable: true, color_scheme: scheme2) + default_theme.set_default! + + StylesheetCache.destroy_all + + Stylesheet::Manager.precompile_css + results = StylesheetCache.pluck(:target) + + expect(results.size).to eq(14) # 2 themes x 7 targets + core_targets.each do |tar| + expect(results.count { |target| target =~ /^#{tar}_(#{scheme1.id}|#{scheme2.id})$/ }).to eq(2) + end + + theme_targets.each do |tar| + expect(results.count { |target| target =~ /^#{tar}_(#{user_theme.id}|#{default_theme.id})$/ }).to eq(2) + end + + Theme.clear_default! + StylesheetCache.destroy_all + + Stylesheet::Manager.precompile_css + results = StylesheetCache.pluck(:target) + expect(results.size).to eq(19) # (2 themes x 7 targets) + (1 no/default/core theme x 5 core targets) + + core_targets.each do |tar| + expect(results.count { |target| target =~ /^(#{tar}_(#{scheme1.id}|#{scheme2.id})|#{tar})$/ }).to eq(3) + end + + theme_targets.each do |tar| + expect(results.count { |target| target =~ /^#{tar}_(#{user_theme.id}|#{default_theme.id})$/ }).to eq(2) + end + end + end end From ddfd02ad3695c456e420930ed0486820a0a924c0 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 31 Aug 2018 13:50:15 +0200 Subject: [PATCH 021/124] FIX: Deleting backup failed after uploading backup --- app/assets/javascripts/admin/routes/admin-backups-index.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 b/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 index 760a102e28..39ed982a78 100644 --- a/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 @@ -3,7 +3,7 @@ import Backup from "admin/models/backup"; export default Ember.Route.extend({ activate() { this.messageBus.subscribe("/admin/backups", backups => - this.controller.set("model", backups) + this.controller.set("model", backups.map(backup => Backup.create(backup))) ); }, From 39414068ff1faa1c0db118cf1e3b2c8aa1ebaebb Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 31 Aug 2018 14:49:38 +0200 Subject: [PATCH 022/124] FIX: User agent browser detection (#6352) --- app/serializers/user_auth_token_serializer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/serializers/user_auth_token_serializer.rb b/app/serializers/user_auth_token_serializer.rb index 1b59cfb3e2..7e1c21d446 100644 --- a/app/serializers/user_auth_token_serializer.rb +++ b/app/serializers/user_auth_token_serializer.rb @@ -39,7 +39,7 @@ class UserAuthTokenSerializer < ApplicationSerializer 'Linux' when /Windows/i 'Windows' - when /macOS/i + when /Macintosh|Mac OS X|macOS/i 'macOS' when /iPhone|iPad|iPod/i 'iOS' @@ -56,7 +56,7 @@ class UserAuthTokenSerializer < ApplicationSerializer I18n.t('user_auth_tokens.devices.linux') when /Windows/i I18n.t('user_auth_tokens.devices.windows') - when /macOS/i + when /Macintosh|Mac OS X|macOS/i I18n.t('user_auth_tokens.devices.mac') when /iPhone/i I18n.t('user_auth_tokens.devices.iphone') From f0b551b6847541df13531fbe05f2587b6edc9241 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 31 Aug 2018 10:38:39 -0400 Subject: [PATCH 023/124] UX: avatar on collapsed user profile was the wrong size --- app/assets/stylesheets/common/base/user.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index b0ef257e06..b53d309611 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -219,6 +219,9 @@ .avatar-flair { bottom: 8px; right: 2px; + .fa { + font-size: $font-0; + } } } } From 690908993fba8e70ffef6b340a90fc3bb38b82ab Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 31 Aug 2018 13:27:25 -0700 Subject: [PATCH 024/124] reduce default post deletions per day --- config/site_settings.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index cdb0582c00..8d1662b47f 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1247,10 +1247,10 @@ rate_limits: default: 6 max_post_deletions_per_minute: min: 1 - default: 3 + default: 2 max_post_deletions_per_day: min: 1 - default: 20 + default: 10 developer: force_hostname: From 16974df1e9ebd1f814f5429e4ac5a2fdc6ace8b4 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Sat, 1 Sep 2018 02:08:11 +0200 Subject: [PATCH 025/124] FIX: Resetting site setting didn't remove "overriden" state --- .../admin/mixins/setting-component.js.es6 | 4 +- .../templates/components/site-setting.hbs | 2 +- .../admin/templates/site-settings.hbs | 6 +- .../admin-site-settings-test.js.es6 | 45 +++++++++++++ .../javascripts/fixtures/site_settings.js.es6 | 63 +++++++++++++++++++ 5 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 test/javascripts/acceptance/admin-site-settings-test.js.es6 create mode 100644 test/javascripts/fixtures/site_settings.js.es6 diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6 index 9636c47732..532abe8f20 100644 --- a/app/assets/javascripts/admin/mixins/setting-component.js.es6 +++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6 @@ -84,7 +84,7 @@ export default Ember.Mixin.create({ this.$().on("keydown.setting-enter", ".input-setting-string", function(e) { if (e.keyCode === 13) { // enter key - self._save(); + self.send("save"); } }); }.on("didInsertElement"), @@ -122,7 +122,7 @@ export default Ember.Mixin.create({ resetDefault() { this.set("buffered.value", this.get("setting.default")); - this._save(); + this.send("save"); }, toggleSecret() { diff --git a/app/assets/javascripts/admin/templates/components/site-setting.hbs b/app/assets/javascripts/admin/templates/components/site-setting.hbs index 03e5433fd0..558781c33d 100644 --- a/app/assets/javascripts/admin/templates/components/site-setting.hbs +++ b/app/assets/javascripts/admin/templates/components/site-setting.hbs @@ -13,5 +13,5 @@ {{#if setting.secret}} {{d-button action="toggleSecret" icon="eye-slash"}} {{/if}} - {{d-button action="resetDefault" icon="undo" label="admin.settings.reset"}} + {{d-button class="undo" action="resetDefault" icon="undo" label="admin.settings.reset"}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/site-settings.hbs b/app/assets/javascripts/admin/templates/site-settings.hbs index 61e57209b7..8a1b8afcf3 100644 --- a/app/assets/javascripts/admin/templates/site-settings.hbs +++ b/app/assets/javascripts/admin/templates/site-settings.hbs @@ -1,9 +1,9 @@
- +
{{d-button action="toggleMenu" class="menu-toggle" icon="bars"}} - {{text-field value=filter placeholderKey="type_to_filter" class="no-blur"}} - {{d-button action="clearFilter" label="admin.site_settings.clear_filter"}} + {{text-field id="setting-filter" value=filter placeholderKey="type_to_filter" class="no-blur"}} + {{d-button id="clear-filter" action="clearFilter" label="admin.site_settings.clear_filter"}}
{{/if}} -{{#if canCheckEmails}} - -{{/if}} - -{{#if canCheckEmails}} - {{#if model.staff}} -
- - {{#if model.user_auth_token_logs}} - - - - - - - {{#each model.user_auth_token_logs as |token|}} - - - - - - {{/each}} -
{{i18n 'user.auth_tokens.ip_address'}}{{i18n 'user.auth_tokens.created'}}{{i18n 'user.auth_tokens.operating_system'}}
{{token.client_ip}}{{format-date token.created_at}}{{d-icon token.icon}} {{token.os}}
- {{/if}} -
- {{/if}} -{{/if}} - {{plugin-outlet name="user-preferences-account" args=(hash model=model save=(action "save"))}}
From 3748d3e281904787808d94e50577cbd4675f7fe2 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Sep 2018 10:42:39 +1000 Subject: [PATCH 040/124] UX: hide associate accounts if second factor is enabled Once second factor is enabled all login via associated accounts is banned showing this section just leads to confusion --- .../controllers/preferences/account.js.es6 | 8 +++++-- .../preferences-account-test.js.es6 | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 test/javascripts/controllers/preferences-account-test.js.es6 diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index 554fbf30c6..b91f0e66ff 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -87,8 +87,12 @@ export default Ember.Controller.extend( return userId !== this.get("currentUser.id"); }, - @computed() - canUpdateAssociatedAccounts() { + @computed("model.second_factor_enabled") + canUpdateAssociatedAccounts(secondFactorEnabled) { + if (secondFactorEnabled) { + return false; + } + return ( findAll(this.siteSettings, this.capabilities, this.site.isMobileDevice) .length > 0 diff --git a/test/javascripts/controllers/preferences-account-test.js.es6 b/test/javascripts/controllers/preferences-account-test.js.es6 new file mode 100644 index 0000000000..4272459d95 --- /dev/null +++ b/test/javascripts/controllers/preferences-account-test.js.es6 @@ -0,0 +1,21 @@ +moduleFor("controller:preferences/account"); + +QUnit.test("updating of associated accounts", function(assert) { + const controller = this.subject({ + siteSettings: { + enable_google_oauth2_logins: true + }, + model: Em.Object.create({ + second_factor_enabled: true + }), + site: Em.Object.create({ + isMobileDevice: false + }) + }); + + assert.equal(controller.get("canUpdateAssociatedAccounts"), false); + + controller.set("model.second_factor_enabled", false); + + assert.equal(controller.get("canUpdateAssociatedAccounts"), true); +}); From fcae21c4fc3bd75bc433c20b0d402649418a8789 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Sep 2018 10:52:12 +1000 Subject: [PATCH 041/124] remove test that is no longer relevant --- test/javascripts/acceptance/preferences-test.js.es6 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index 730cc638ed..66d3269b21 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -211,12 +211,6 @@ QUnit.test("default avatar selector", async assert => { ); }); -QUnit.test("email field always shows up", async assert => { - await visit("/u/eviltrout/preferences"); - - assert.ok(exists(".pref-auth-tokens"), "it shows the auth tokens"); -}); - acceptance("Avatar selector when selectable avatars is enabled", { loggedIn: true, settings: { selectable_avatars_enabled: true }, From 0a14e0a256dc5c44852204e5d88342b81a3d425c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 4 Sep 2018 09:22:54 +0800 Subject: [PATCH 042/124] Ensure `params[:files]` responds to `map` in Lograge. --- config/initializers/101-lograge.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/101-lograge.rb b/config/initializers/101-lograge.rb index a432242ed9..d24b84877f 100644 --- a/config/initializers/101-lograge.rb +++ b/config/initializers/101-lograge.rb @@ -53,7 +53,7 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" params[:file] = file.headers end - if (files = params[:files]) + if (files = params[:files]) && files.respond_to?(:map) params[:files] = files.map do |file| file.respond_to?(:headers) ? file.headers : file end From 2f5c21e28c4f8b8a441b611caefe206a178b6eef Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Sep 2018 12:11:42 +1000 Subject: [PATCH 043/124] FIX: return a 400 error instead of 500 for null injections Many security scanners like to inject NULL in inputs causing application to exception out and return a 500 We now handle this exception and render a 400 status back --- app/controllers/application_controller.rb | 8 ++++++++ spec/requests/search_controller_spec.rb | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6a30686ee6..b6076636cf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -156,6 +156,14 @@ class ApplicationController < ActionController::Base end end + rescue_from ArgumentError do |e| + if e.message == "string contains null byte" + raise Discourse::InvalidParameters, e.message + else + raise e + end + end + rescue_from Discourse::InvalidParameters do |e| message = I18n.t('invalid_params', message: e.message) if (request.format && request.format.json?) || request.xhr? || !request.get? diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb index 84346d1b8d..3f2446c5ef 100644 --- a/spec/requests/search_controller_spec.rb +++ b/spec/requests/search_controller_spec.rb @@ -16,6 +16,16 @@ describe SearchController do $redis.flushall end + it "returns a 400 error if you search for null bytes" do + term = "hello\0hello" + + get "/search/query.json", params: { + term: term, include_blurb: true + } + + expect(response.status).to eq(400) + end + it "can search correctly" do my_post = Fabricate(:post, raw: 'this is my really awesome post') From 8dc1463ab35519d7e3f160d322b63ff27751e13d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 4 Sep 2018 10:16:21 +0800 Subject: [PATCH 044/124] Enable `Lint/ShadowingOuterLocalVariable` for Rubocop. --- .rubocop.yml | 3 +++ app/models/topic_tracking_state.rb | 4 +-- config/initializers/101-lograge.rb | 4 +-- lib/email/sender.rb | 10 ++++---- lib/final_destination.rb | 15 ++++++++--- lib/freedom_patches/inflector_backport.rb | 11 +++++---- lib/freedom_patches/translate_accelerator.rb | 6 +++-- lib/import_export/base_exporter.rb | 11 ++++++--- lib/tasks/typepad.thor | 11 +++++---- script/discourse | 2 +- script/import_scripts/discuz_x.rb | 8 +++--- script/import_scripts/smf1.rb | 4 +-- script/import_scripts/smf2.rb | 8 +++--- script/import_scripts/telligent.rb | 4 +-- spec/models/topic_tracking_state_spec.rb | 26 ++++++-------------- spec/services/topic_status_updater_spec.rb | 2 +- 16 files changed, 70 insertions(+), 59 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 8472c89a91..e28a0876aa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -102,6 +102,9 @@ Layout/EndAlignment: Lint/RequireParentheses: Enabled: true +Lint/ShadowingOuterLocalVariable: + Enabled: true + Layout/MultilineMethodCallIndentation: Enabled: true EnforcedStyle: indented diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index fe71d76bad..27965ecf4f 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -292,11 +292,11 @@ SQL topic_id: topic.id } - channels.each do |channel, user_ids| + channels.each do |channel, ids| MessageBus.publish( channel, message.as_json, - user_ids: user_ids + user_ids: ids ) end end diff --git a/config/initializers/101-lograge.rb b/config/initializers/101-lograge.rb index d24b84877f..c9a4eb9275 100644 --- a/config/initializers/101-lograge.rb +++ b/config/initializers/101-lograge.rb @@ -54,8 +54,8 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" end if (files = params[:files]) && files.respond_to?(:map) - params[:files] = files.map do |file| - file.respond_to?(:headers) ? file.headers : file + params[:files] = files.map do |f| + f.respond_to?(:headers) ? f.headers : f end end diff --git a/lib/email/sender.rb b/lib/email/sender.rb index 46d0ca84e8..c4daf17ec4 100644 --- a/lib/email/sender.rb +++ b/lib/email/sender.rb @@ -106,14 +106,14 @@ module Email .where(id: PostReply.where(reply_id: post_id).select(:post_id)) .order(id: :desc) - referenced_post_message_ids = referenced_posts.map do |post| - if post.incoming_email&.message_id.present? - "<#{post.incoming_email.message_id}>" + referenced_post_message_ids = referenced_posts.map do |referenced_post| + if referenced_post.incoming_email&.message_id.present? + "<#{referenced_post.incoming_email.message_id}>" else - if post.post_number == 1 + if referenced_post.post_number == 1 "" else - "" + "" end end end diff --git a/lib/final_destination.rb b/lib/final_destination.rb index b69b2e0ebd..d863344af7 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -38,10 +38,17 @@ class FinalDestination @opts[:lookup_ip] ||= lambda { |host| FinalDestination.lookup_ip(host) } @ignored = @opts[:ignore_hostnames] || [] - [Discourse.base_url_no_prefix].concat(@opts[:ignore_redirects] || []).each do |url| - url = uri(url) - if url.present? && url.hostname - @ignored << url.hostname + + ignore_redirects = [Discourse.base_url_no_prefix] + + if @opts[:ignore_redirects] + ignore_redirects.concat(@opts[:ignore_redirects]) + end + + ignore_redirects.each do |ignore_redirect| + ignore_redirect = uri(ignore_redirect) + if ignore_redirect.present? && ignore_redirect.hostname + @ignored << ignore_redirect.hostname end end diff --git a/lib/freedom_patches/inflector_backport.rb b/lib/freedom_patches/inflector_backport.rb index 1e192b0033..cc97659155 100644 --- a/lib/freedom_patches/inflector_backport.rb +++ b/lib/freedom_patches/inflector_backport.rb @@ -20,12 +20,12 @@ module ActiveSupport uncached = "#{method_name}_without_cache" alias_method uncached, method_name - define_method(method_name) do |*args| + define_method(method_name) do |*arguments| # this avoids recursive locks found = true - data = cache.fetch(args) { found = false } + data = cache.fetch(arguments) { found = false } unless found - cache[args] = data = send(uncached, *args) + cache[arguments] = data = send(uncached, *arguments) end # so cache is never corrupted data.dup @@ -45,9 +45,10 @@ module ActiveSupport args.each do |method_name| orig = "#{method_name}_without_clear_memoize" alias_method orig, method_name - define_method(method_name) do |*args| + + define_method(method_name) do |*arguments| ActiveSupport::Inflector.clear_memoize! - send(orig, *args) + send(orig, *arguments) end end end diff --git a/lib/freedom_patches/translate_accelerator.rb b/lib/freedom_patches/translate_accelerator.rb index d2ca7a30c3..9d1c55d92c 100644 --- a/lib/freedom_patches/translate_accelerator.rb +++ b/lib/freedom_patches/translate_accelerator.rb @@ -41,9 +41,11 @@ module I18n I18n.backend.load_translations(I18n.load_path.grep(/\.rb$/)) # load plural rules from plugins - DiscoursePluginRegistry.locales.each do |locale, options| + DiscoursePluginRegistry.locales.each do |plugin_locale, options| if options[:plural] - I18n.backend.store_translations(locale, i18n: { plural: options[:plural] }) + I18n.backend.store_translations(plugin_locale, + i18n: { plural: options[:plural] } + ) end end end diff --git a/lib/import_export/base_exporter.rb b/lib/import_export/base_exporter.rb index 58d3b77e4e..77057406ce 100644 --- a/lib/import_export/base_exporter.rb +++ b/lib/import_export/base_exporter.rb @@ -97,9 +97,14 @@ module ImportExport topic_data[:posts] = [] topic.ordered_posts.find_each do |post| - h = POST_ATTRS.inject({}) { |h, a| h[a] = post.send(a); h; } - h[:raw] = h[:raw].gsub('src="/uploads', "src=\"#{Discourse.base_url_no_prefix}/uploads") - topic_data[:posts] << h + attributes = POST_ATTRS.inject({}) { |h, a| h[a] = post.send(a); h; } + + attributes[:raw] = attributes[:raw].gsub( + 'src="/uploads', + "src=\"#{Discourse.base_url_no_prefix}/uploads" + ) + + topic_data[:posts] << attributes end data << topic_data diff --git a/lib/tasks/typepad.thor b/lib/tasks/typepad.thor index 7db75d415e..8197f7f0f5 100644 --- a/lib/tasks/typepad.thor +++ b/lib/tasks/typepad.thor @@ -27,21 +27,21 @@ class Typepad < Thor end inside_block = true - entry = "" + input = "" entries = [] File.open(options[:file]).each_line do |l| l = l.scrub if l =~ /^--------$/ - parsed_entry = process_entry(entry) + parsed_entry = process_entry(input) if parsed_entry puts "Parsed #{parsed_entry[:title]}" entries << parsed_entry end - entry = "" + input = "" else - entry << l + input << l end end @@ -55,6 +55,7 @@ class Typepad < Thor SiteSetting.email_domains_blacklist = "" puts "Importing #{entries.size} entries" + entries.each_with_index do |entry, idx| puts "Importing (#{idx + 1}/#{entries.size})" next if entry[:body].blank? @@ -219,7 +220,7 @@ class Typepad < Thor current << c end end - segments.delete_if { |s| s.nil? || s.size < 2 } + segments.delete_if { |segment| segment.nil? || segment.size < 2 } segments << current comment[:author] = segments[0] diff --git a/script/discourse b/script/discourse index 3cf57078fe..d0fccded04 100755 --- a/script/discourse +++ b/script/discourse @@ -95,7 +95,7 @@ class DiscourseCLI < Thor if !filename puts "You must provide a filename to restore. Did you mean one of the following?\n\n" - Dir["public/backups/default/*"].sort_by { |filename| File.mtime(filename) }.reverse.each do |f| + Dir["public/backups/default/*"].sort_by { |path| File.mtime(path) }.reverse.each do |f| puts "#{discourse} restore #{File.basename(f)}" end diff --git a/script/import_scripts/discuz_x.rb b/script/import_scripts/discuz_x.rb index 8f99ae4911..54c5071917 100644 --- a/script/import_scripts/discuz_x.rb +++ b/script/import_scripts/discuz_x.rb @@ -393,12 +393,12 @@ class ImportScripts::DiscuzX < ImportScripts::Base end if m['status'] & 1 == 1 || mapped[:raw].blank? - mapped[:post_create_action] = lambda do |post| - PostDestroyer.new(Discourse.system_user, post).perform_delete + mapped[:post_create_action] = lambda do |action_post| + PostDestroyer.new(Discourse.system_user, action_post).perform_delete end elsif (m['status'] & 2) >> 1 == 1 # waiting for approve - mapped[:post_create_action] = lambda do |post| - PostAction.act(Discourse.system_user, post, 6, take_action: false) + mapped[:post_create_action] = lambda do |action_post| + PostAction.act(Discourse.system_user, action_post, 6, take_action: false) end end skip ? nil : mapped diff --git a/script/import_scripts/smf1.rb b/script/import_scripts/smf1.rb index 8b17ce6815..7230450e0c 100644 --- a/script/import_scripts/smf1.rb +++ b/script/import_scripts/smf1.rb @@ -355,10 +355,10 @@ class ImportScripts::Smf1 < ImportScripts::Base post[:archetype] = Archetype.private_message post[:title] = title post[:target_usernames] = User.where(id: recipients).pluck(:username) - post[:post_create_action] = proc do |p| + post[:post_create_action] = proc do |action_post| @pm_mapping[users] ||= {} @pm_mapping[users][title] ||= [] - @pm_mapping[users][title] << p.topic_id + @pm_mapping[users][title] << action_post.topic_id end end diff --git a/script/import_scripts/smf2.rb b/script/import_scripts/smf2.rb index 5dc36d4453..354a69ce48 100644 --- a/script/import_scripts/smf2.rb +++ b/script/import_scripts/smf2.rb @@ -201,15 +201,17 @@ class ImportScripts::Smf2 < ImportScripts::Base SQL skip = false ignore_quotes = false + post = { id: message[:id_msg], user_id: user_id_from_imported_user_id(message[:id_member]) || -1, created_at: Time.zone.at(message[:poster_time]), - post_create_action: ignore_quotes && proc do |post| - post.custom_fields['import_rebake'] = 't' - post.save + post_create_action: ignore_quotes && proc do |p| + p.custom_fields['import_rebake'] = 't' + p.save end } + if message[:id_msg] == message[:id_first_msg] post[:category] = category_id_from_imported_category_id(message[:id_board]) post[:title] = decode_entities(message[:subject]) diff --git a/script/import_scripts/telligent.rb b/script/import_scripts/telligent.rb index 7594affcec..7981e1d5ca 100644 --- a/script/import_scripts/telligent.rb +++ b/script/import_scripts/telligent.rb @@ -273,8 +273,8 @@ class ImportScripts::Telligent < ImportScripts::Base user_id: user_id, created_at: row["DateCreated"], closed: row["IsLocked"], - post_create_action: proc do |post| - topic = post.topic + post_create_action: proc do |action_post| + topic = action_post.topic Jobs.enqueue_at(topic.pinned_until, :unpin_topic, topic_id: topic.id) if topic.pinned_until url = "f/#{row['ForumId']}/t/#{row['ThreadId']}" Permalink.create(url: url, topic_id: topic.id) unless Permalink.exists?(url: url) diff --git a/spec/models/topic_tracking_state_spec.rb b/spec/models/topic_tracking_state_spec.rb index bbeefa368f..73a124814a 100644 --- a/spec/models/topic_tracking_state_spec.rb +++ b/spec/models/topic_tracking_state_spec.rb @@ -114,16 +114,16 @@ describe TopicTrackingState do "/private-messages/group/#{group2.name}" ) - message = messages.find do |message| - message.channel == '/private-messages/inbox' + message = messages.find do |m| + m.channel == '/private-messages/inbox' end expect(message.data["topic_id"]).to eq(private_message_topic.id) expect(message.user_ids).to eq(private_message_topic.allowed_users.map(&:id)) [group1, group2].each do |group| - message = messages.find do |message| - message.channel == "/private-messages/group/#{group.name}" + message = messages.find do |m| + m.channel == "/private-messages/group/#{group.name}" end expect(message.data["topic_id"]).to eq(private_message_topic.id) @@ -148,9 +148,7 @@ describe TopicTrackingState do "/private-messages/group/#{group2.name}/archive", ) - message = messages.find do |message| - message.channel == '/private-messages/inbox' - end + message = messages.find { |m| m.channel == '/private-messages/inbox' } expect(message.data["topic_id"]).to eq(private_message_topic.id) expect(message.user_ids).to eq(private_message_topic.allowed_users.map(&:id)) @@ -162,10 +160,7 @@ describe TopicTrackingState do group_channel, "#{group_channel}/archive" ].each do |channel| - message = messages.find do |message| - message.channel == channel - end - + message = messages.find { |m| m.channel == channel } expect(message.data["topic_id"]).to eq(private_message_topic.id) expect(message.user_ids).to eq(group.users.map(&:id)) end @@ -211,9 +206,7 @@ describe TopicTrackingState do [user.id], [group.users.first.id] ]).each do |channel, user_ids| - message = messages.find do |message| - message.channel == channel - end + message = messages.find { |m| m.channel == channel } expect(message.data["topic_id"]).to eq(private_message_topic.id) expect(message.user_ids).to eq(user_ids) @@ -239,10 +232,7 @@ describe TopicTrackingState do expect(messages.map(&:channel)).to eq(expected_channels) expected_channels.each do |channel| - message = messages.find do |message| - message.channel = channel - end - + message = messages.find { |m| m.channel = channel } expect(message.data["topic_id"]).to eq(private_message_topic.id) expect(message.user_ids).to eq([private_message_post.user_id]) end diff --git a/spec/services/topic_status_updater_spec.rb b/spec/services/topic_status_updater_spec.rb index 14106b10f1..70ce71a74d 100644 --- a/spec/services/topic_status_updater_spec.rb +++ b/spec/services/topic_status_updater_spec.rb @@ -44,7 +44,7 @@ describe TopicStatusUpdater do topic = create_topic called = false - updater = -> (topic) { called = true } + updater = -> (_) { called = true } DiscourseEvent.on(:topic_closed, &updater) TopicStatusUpdater.new(topic, admin).update!("closed", true) From ad70502ab82b0f88b1e51c8a1ce5b322ead1324e Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Sep 2018 12:28:22 +1000 Subject: [PATCH 045/124] FIX: ignore invalid usernames in incoming link tracker If an incoming link username has NULL in it simply ignore it --- app/models/incoming_link.rb | 5 ++++- spec/models/incoming_link_spec.rb | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/incoming_link.rb b/app/models/incoming_link.rb index 82e281ef0e..8a852dd1cd 100644 --- a/app/models/incoming_link.rb +++ b/app/models/incoming_link.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class IncomingLink < ActiveRecord::Base belongs_to :post belongs_to :user @@ -15,7 +17,8 @@ class IncomingLink < ActiveRecord::Base current_user = opts[:current_user] username = opts[:username] - username = nil unless String === username + username = nil if !(String === username) + username = nil if username&.include?("\0") if username u = User.select(:id).find_by(username_lower: username.downcase) user_id = u.id if u diff --git a/spec/models/incoming_link_spec.rb b/spec/models/incoming_link_spec.rb index c44fa14fcd..962396a9e3 100644 --- a/spec/models/incoming_link_spec.rb +++ b/spec/models/incoming_link_spec.rb @@ -49,6 +49,12 @@ describe IncomingLink do IncomingLink.add(req(opts)) end + it "does not explode with bad username" do + add( + username: "test\0test" + ) + end + it "does not explode with bad referer" do add( referer: 'file:///Applications/Install/75067ABC-C9D1-47B7-8ACE-76AEDE3911B2/Install/', From 08b268c5bcb6a27cd37de121774fe84b0f990979 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 4 Sep 2018 10:30:56 +0800 Subject: [PATCH 046/124] Be more forceful in disconnecting connections during failover. --- .../postgresql_fallback_adapter.rb | 2 +- .../postgresql_fallback_adapter_spec.rb | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index 812f8d2168..6016b162c0 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -99,7 +99,7 @@ class PostgreSQLFallbackHandler end def clear_connections - ActiveRecord::Base.connection_pool.disconnect + ActiveRecord::Base.connection_pool.disconnect! end private diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index c207a6ba14..2d8bdc6cfe 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -38,22 +38,18 @@ describe ActiveRecord::ConnectionHandling do after do Sidekiq.unpause! postgresql_fallback_handler.setup! - Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) ActiveRecord::Base.unstub(:postgresql_connection) (Thread.list - @threads).each(&:kill) + ActiveRecord::Base.connection_pool.disconnect! ActiveRecord::Base.establish_connection end describe "#postgresql_fallback_connection" do it 'should return a PostgreSQL adapter' do - begin - connection = ActiveRecord::Base.postgresql_fallback_connection(config) + connection = ActiveRecord::Base.postgresql_fallback_connection(config) - expect(connection) - .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) - ensure - connection.disconnect! - end + expect(connection) + .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) end context 'when master server is down' do @@ -127,8 +123,6 @@ describe ActiveRecord::ConnectionHandling do expect(Discourse.readonly_mode?).to eq(false) expect(Sidekiq.paused?).to eq(false) - - # fails sometimes on this line! expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) expect(postgresql_fallback_handler.master_down?).to eq(nil) From f896d6b0219f949e9890bddb43028328dc9860bf Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 4 Sep 2018 10:45:35 +0530 Subject: [PATCH 047/124] FIX: Skip keypress event if alt key is down --- vendor/assets/javascripts/mousetrap.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vendor/assets/javascripts/mousetrap.js b/vendor/assets/javascripts/mousetrap.js index 6e28146130..b2a79743e9 100644 --- a/vendor/assets/javascripts/mousetrap.js +++ b/vendor/assets/javascripts/mousetrap.js @@ -575,8 +575,8 @@ // // chrome will not fire a keypress if meta or control is down // safari will fire a keypress if meta or meta+shift is down - // firefox will fire a keypress if meta or control is down - if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { + // firefox will fire a keypress if meta, alt or control is down + if ((action == 'keypress' && !e.altKey && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { // when you bind a combination or sequence a second time it // should overwrite the first one. if a sequenceName or From edbcc992d44863c6da5444ee0a5479fbf80d5f2c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 4 Sep 2018 13:19:48 +0800 Subject: [PATCH 048/124] Allow unicorn timeout to be configurable via ENV. --- config/unicorn.conf.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index b4c9dbdd88..9944a17d0e 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -25,7 +25,7 @@ pid (ENV["UNICORN_PID_PATH"] || "#{discourse_path}/tmp/pids/unicorn.pid") if ENV["RAILS_ENV"] == "development" || !ENV["RAILS_ENV"] logger Logger.new($stdout) # we want a longer timeout in dev cause first request can be really slow - timeout 60 + timeout (ENV["UNICORN_TIMEOUT"] && ENV["UNICORN_TIMEOUT"].to_i || 60) else # By default, the Unicorn logger will write to stderr. # Additionally, some applications/frameworks log to stderr or stdout, From 651b50b1a159258588ebd716f678035db2239b5a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 4 Sep 2018 13:52:58 +0800 Subject: [PATCH 049/124] FIX: Don't rate limit admin and staff constraints when matching routes. * When an error is raised when checking route constraints, we can only return true/false which either lets the request through or return a 404 error. Therefore, we just skip rate limiting here and let the controller handle the rate limiting. --- lib/admin_constraint.rb | 3 ++- lib/auth/default_current_user_provider.rb | 15 ++++++----- lib/staff_constraint.rb | 3 ++- .../default_current_user_provider_spec.rb | 27 ++++++++++++++++--- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/lib/admin_constraint.rb b/lib/admin_constraint.rb index 1d0291cd7f..3b9e053057 100644 --- a/lib/admin_constraint.rb +++ b/lib/admin_constraint.rb @@ -8,7 +8,8 @@ class AdminConstraint def matches?(request) return false if @require_master && RailsMultisite::ConnectionManagement.current_db != "default" - provider = Discourse.current_user_provider.new(request.env) + provider = Discourse.current_user_provider.new(request.env, rate_limit: false) + provider.current_user && provider.current_user.admin? && custom_admin_check(request) diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index e91cd59630..fa795cd609 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -17,9 +17,10 @@ class Auth::DefaultCurrentUserProvider BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN" # do all current user initialization here - def initialize(env) + def initialize(env, rate_limit: true) @env = env @request = Rack::Request.new(env) + @rate_limit = rate_limit end # our current user, return nil if none is found @@ -62,7 +63,7 @@ class Auth::DefaultCurrentUserProvider if !current_user @env[BAD_TOKEN] = true begin - limiter.performed! + limiter.performed! if @rate_limit rescue RateLimiter::LimitExceeded raise Discourse::InvalidAccess.new( 'Invalid Access', @@ -85,7 +86,7 @@ class Auth::DefaultCurrentUserProvider # we do not run this rate limiter while profiling if Rails.env != "profile" limiter_min = RateLimiter.new(nil, "admin_api_min_#{api_key}", GlobalSetting.max_admin_api_reqs_per_key_per_minute, 60) - limiter_min.performed! + limiter_min.performed! if @rate_limit end end @@ -96,19 +97,19 @@ class Auth::DefaultCurrentUserProvider limiter_day = RateLimiter.new(nil, "user_api_day_#{user_api_key}", GlobalSetting.max_user_api_reqs_per_day, 86400) unless limiter_day.can_perform? - limiter_day.performed! + limiter_day.performed! if @rate_limit end unless limiter_min.can_perform? - limiter_min.performed! + limiter_min.performed! if @rate_limit end current_user = lookup_user_api_user_and_update_key(user_api_key, @env[USER_API_CLIENT_ID]) raise Discourse::InvalidAccess unless current_user raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active - limiter_min.performed! - limiter_day.performed! + limiter_min.performed! if @rate_limit + limiter_day.performed! if @rate_limit @env[USER_API_KEY_ENV] = true end diff --git a/lib/staff_constraint.rb b/lib/staff_constraint.rb index 7572ada16c..a833ed56d8 100644 --- a/lib/staff_constraint.rb +++ b/lib/staff_constraint.rb @@ -3,7 +3,8 @@ require_dependency 'current_user' class StaffConstraint def matches?(request) - provider = Discourse.current_user_provider.new(request.env) + provider = Discourse.current_user_provider.new(request.env, rate_limit: false) + provider.current_user && provider.current_user.staff? && custom_staff_check(request) diff --git a/spec/components/auth/default_current_user_provider_spec.rb b/spec/components/auth/default_current_user_provider_spec.rb index 20cf716d96..29934674db 100644 --- a/spec/components/auth/default_current_user_provider_spec.rb +++ b/spec/components/auth/default_current_user_provider_spec.rb @@ -2,18 +2,19 @@ require 'rails_helper' require_dependency 'auth/default_current_user_provider' describe Auth::DefaultCurrentUserProvider do + let(:rate_limit) { true } class TestProvider < Auth::DefaultCurrentUserProvider attr_reader :env - def initialize(env) - super(env) + def initialize(env, rate_limit: true) + super(env, rate_limit: rate_limit) end end def provider(url, opts = nil) opts ||= { method: "GET" } env = Rack::MockRequest.env_for(url, opts) - TestProvider.new(env) + TestProvider.new(env, rate_limit: rate_limit) end it "can be used to pretend that a user doesn't exist" do @@ -145,6 +146,26 @@ describe Auth::DefaultCurrentUserProvider do provider("/?api_key=#{key}&api_username=#{user.username.downcase}").current_user end + + describe 'when rate limit is disabled' do + let(:rate_limit) { false } + + it 'should not raise any rate limit errors' do + global_setting :max_admin_api_reqs_per_key_per_minute, 1 + + freeze_time + + key = SecureRandom.hex + api_key = ApiKey.create!(key: key, created_by_id: -1) + + 2.times do + provider( + "/?api_key=#{key}&api_username=system", + nil + ).current_user + end + end + end end end From 19182c0c8fa12e166d2bd2c079d97537525cd264 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 4 Sep 2018 13:58:09 +0800 Subject: [PATCH 050/124] DEV: Skip fragile tests for now. --- .../connection_adapters/postgresql_fallback_adapter_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index 2d8bdc6cfe..98a35defd5 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -27,6 +27,8 @@ describe ActiveRecord::ConnectionHandling do let(:postgresql_fallback_handler) { PostgreSQLFallbackHandler.instance } before do + # TODO: tgxworld will rewrite it without stubs + skip("Skip causes our build to be unstable") @threads = Thread.list postgresql_fallback_handler.initialized = true From e4498d2a8aae836ede1e2067ad1a977a7b673536 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Sep 2018 16:05:21 +1000 Subject: [PATCH 051/124] FIX: keep db and job correctly in multisite logs This ensures we report job and db correctly, previously we were only reporting this on default --- app/jobs/base.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index 3747969fbf..0bd5b6fe35 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -119,11 +119,6 @@ module Jobs RailsMultisite::ConnectionManagement.all_dbs end - logster_env = {} - Logster.add_to_env(logster_env, :current_db, 'default') - Logster.add_to_env(logster_env, :job, self.class.to_s) - Thread.current[Logster::Logger::LOGSTER_ENV] = logster_env - exceptions = [] dbs.each do |db| begin @@ -134,7 +129,11 @@ module Jobs I18n.locale = SiteSetting.default_locale || "en" I18n.ensure_all_loaded! begin + logster_env = {} + Logster.add_to_env(logster_env, :job, self.class.to_s) Logster.add_to_env(logster_env, :db, db) + Thread.current[Logster::Logger::LOGSTER_ENV] = logster_env + execute(opts) rescue => e exception[:ex] = e @@ -146,7 +145,6 @@ module Jobs exception[:other] = { problem_db: db } ensure total_db_time += Instrumenter.stats.duration_ms - Thread.current[Logster::Logger::LOGSTER_ENV] = nil end end From 3b337bfc6beb52efd27b7da352a2adf1601da2ba Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 4 Sep 2018 14:17:05 +0800 Subject: [PATCH 052/124] Revert "FIX: Don't rate limit admin and staff constraints when matching routes." This reverts commit 651b50b1a159258588ebd716f678035db2239b5a. --- lib/admin_constraint.rb | 3 +-- lib/auth/default_current_user_provider.rb | 15 +++++------ lib/staff_constraint.rb | 3 +-- .../default_current_user_provider_spec.rb | 27 +++---------------- 4 files changed, 12 insertions(+), 36 deletions(-) diff --git a/lib/admin_constraint.rb b/lib/admin_constraint.rb index 3b9e053057..1d0291cd7f 100644 --- a/lib/admin_constraint.rb +++ b/lib/admin_constraint.rb @@ -8,8 +8,7 @@ class AdminConstraint def matches?(request) return false if @require_master && RailsMultisite::ConnectionManagement.current_db != "default" - provider = Discourse.current_user_provider.new(request.env, rate_limit: false) - + provider = Discourse.current_user_provider.new(request.env) provider.current_user && provider.current_user.admin? && custom_admin_check(request) diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index fa795cd609..e91cd59630 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -17,10 +17,9 @@ class Auth::DefaultCurrentUserProvider BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN" # do all current user initialization here - def initialize(env, rate_limit: true) + def initialize(env) @env = env @request = Rack::Request.new(env) - @rate_limit = rate_limit end # our current user, return nil if none is found @@ -63,7 +62,7 @@ class Auth::DefaultCurrentUserProvider if !current_user @env[BAD_TOKEN] = true begin - limiter.performed! if @rate_limit + limiter.performed! rescue RateLimiter::LimitExceeded raise Discourse::InvalidAccess.new( 'Invalid Access', @@ -86,7 +85,7 @@ class Auth::DefaultCurrentUserProvider # we do not run this rate limiter while profiling if Rails.env != "profile" limiter_min = RateLimiter.new(nil, "admin_api_min_#{api_key}", GlobalSetting.max_admin_api_reqs_per_key_per_minute, 60) - limiter_min.performed! if @rate_limit + limiter_min.performed! end end @@ -97,19 +96,19 @@ class Auth::DefaultCurrentUserProvider limiter_day = RateLimiter.new(nil, "user_api_day_#{user_api_key}", GlobalSetting.max_user_api_reqs_per_day, 86400) unless limiter_day.can_perform? - limiter_day.performed! if @rate_limit + limiter_day.performed! end unless limiter_min.can_perform? - limiter_min.performed! if @rate_limit + limiter_min.performed! end current_user = lookup_user_api_user_and_update_key(user_api_key, @env[USER_API_CLIENT_ID]) raise Discourse::InvalidAccess unless current_user raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active - limiter_min.performed! if @rate_limit - limiter_day.performed! if @rate_limit + limiter_min.performed! + limiter_day.performed! @env[USER_API_KEY_ENV] = true end diff --git a/lib/staff_constraint.rb b/lib/staff_constraint.rb index a833ed56d8..7572ada16c 100644 --- a/lib/staff_constraint.rb +++ b/lib/staff_constraint.rb @@ -3,8 +3,7 @@ require_dependency 'current_user' class StaffConstraint def matches?(request) - provider = Discourse.current_user_provider.new(request.env, rate_limit: false) - + provider = Discourse.current_user_provider.new(request.env) provider.current_user && provider.current_user.staff? && custom_staff_check(request) diff --git a/spec/components/auth/default_current_user_provider_spec.rb b/spec/components/auth/default_current_user_provider_spec.rb index 29934674db..20cf716d96 100644 --- a/spec/components/auth/default_current_user_provider_spec.rb +++ b/spec/components/auth/default_current_user_provider_spec.rb @@ -2,19 +2,18 @@ require 'rails_helper' require_dependency 'auth/default_current_user_provider' describe Auth::DefaultCurrentUserProvider do - let(:rate_limit) { true } class TestProvider < Auth::DefaultCurrentUserProvider attr_reader :env - def initialize(env, rate_limit: true) - super(env, rate_limit: rate_limit) + def initialize(env) + super(env) end end def provider(url, opts = nil) opts ||= { method: "GET" } env = Rack::MockRequest.env_for(url, opts) - TestProvider.new(env, rate_limit: rate_limit) + TestProvider.new(env) end it "can be used to pretend that a user doesn't exist" do @@ -146,26 +145,6 @@ describe Auth::DefaultCurrentUserProvider do provider("/?api_key=#{key}&api_username=#{user.username.downcase}").current_user end - - describe 'when rate limit is disabled' do - let(:rate_limit) { false } - - it 'should not raise any rate limit errors' do - global_setting :max_admin_api_reqs_per_key_per_minute, 1 - - freeze_time - - key = SecureRandom.hex - api_key = ApiKey.create!(key: key, created_by_id: -1) - - 2.times do - provider( - "/?api_key=#{key}&api_username=system", - nil - ).current_user - end - end - end end end From 6e3f249aeaea36f8519a238b9aff2bb80bd8c090 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 4 Sep 2018 17:05:06 +1000 Subject: [PATCH 053/124] Disable auth token logging We have a work in progress feature that required the logging, This feature is not going to be shipped for a while so disabling this for now. --- config/site_settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index 8d1662b47f..153ecac96f 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -333,7 +333,7 @@ login: verbose_sso_logging: false verbose_auth_token_logging: hidden: true - default: true + default: false sso_url: default: '' regex: '^https?:\/\/.+[^\/]$' From d1af89e3b35d2fa16cace657bf91d49784eee193 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 4 Sep 2018 16:35:49 +0800 Subject: [PATCH 054/124] DEV: Extract global admin api rate limiting into a dedicated method. * We have a use case for overriding the rate limiting logic in a plugin. --- lib/auth/default_current_user_provider.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index e91cd59630..671022deeb 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -81,12 +81,7 @@ class Auth::DefaultCurrentUserProvider raise Discourse::InvalidAccess.new(I18n.t('invalid_api_credentials'), nil, custom_message: "invalid_api_credentials") unless current_user raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active @env[API_KEY_ENV] = true - - # we do not run this rate limiter while profiling - if Rails.env != "profile" - limiter_min = RateLimiter.new(nil, "admin_api_min_#{api_key}", GlobalSetting.max_admin_api_reqs_per_key_per_minute, 60) - limiter_min.performed! - end + rate_limit_admin_api_requests(api_key) end # user api key handling @@ -296,4 +291,17 @@ class Auth::DefaultCurrentUserProvider end end + private + + def rate_limit_admin_api_requests(api_key) + return if Rails.env == "profile" + + RateLimiter.new( + nil, + "admin_api_min_#{api_key}", + GlobalSetting.max_admin_api_reqs_per_key_per_minute, + 60 + ).performed! + end + end From 4382fb5facb035f5b414c6c7257dc828327a57c7 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 4 Sep 2018 11:45:36 +0100 Subject: [PATCH 055/124] DEV: Allow plugins to whitelist specific user custom_fields for editing (#6358) --- app/controllers/users_controller.rb | 3 ++- app/models/user.rb | 18 +++++++++++++ lib/plugin/instance.rb | 6 +++++ spec/requests/users_controller_spec.rb | 37 +++++++++++++++++++++++--- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a5bf263b94..065f89732a 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -104,7 +104,7 @@ class UsersController < ApplicationController attributes.delete(:username) if params[:user_fields].present? - attributes[:custom_fields] = {} + attributes[:custom_fields] ||= {} fields = UserField.all fields = fields.where(editable: true) unless current_user.staff? @@ -1167,6 +1167,7 @@ class UsersController < ApplicationController :card_background ] + permitted << { custom_fields: User.editable_user_custom_fields } unless User.editable_user_custom_fields.blank? permitted.concat UserUpdater::OPTION_ATTR permitted.concat UserUpdater::CATEGORY_IDS.keys.map { |k| { k => [] } } permitted.concat UserUpdater::TAG_NAMES.keys diff --git a/app/models/user.rb b/app/models/user.rb index 9fe43dc6fc..82c97f9c63 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -222,6 +222,24 @@ class User < ActiveRecord::Base end end + def self.plugin_editable_user_custom_fields + @plugin_editable_user_custom_fields ||= {} + end + + def self.register_plugin_editable_user_custom_field(custom_field_name, plugin) + plugin_editable_user_custom_fields[custom_field_name] = plugin + end + + def self.editable_user_custom_fields + fields = [] + + plugin_editable_user_custom_fields.each do |k, v| + fields << k if v.enabled? + end + + fields.uniq + end + def self.plugin_staff_user_custom_fields @plugin_staff_user_custom_fields ||= {} end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index b47539fc53..21c4b82d88 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -121,6 +121,12 @@ class Plugin::Instance end end + def register_editable_user_custom_field(field) + reloadable_patch do |plugin| + ::User.register_plugin_editable_user_custom_field(field, plugin) # plugin.enabled? is checked at runtime + end + end + def custom_avatar_column(column) reloadable_patch do |plugin| AvatarLookup.lookup_columns << column diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 7960bfd74e..9956eae29b 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -1517,12 +1517,43 @@ describe UsersController do end context "custom_field" do - it "does not update the custom field" do - put "/u/#{user.username}.json", params: { custom_fields: { test: :it } } + before do + plugin = Plugin::Instance.new + plugin.register_editable_user_custom_field :test2 + end + + after do + User.plugin_editable_user_custom_fields.clear + end + + it "only updates allowed user fields" do + put "/u/#{user.username}.json", params: { custom_fields: { test1: :hello1, test2: :hello2 } } expect(response.status).to eq(200) - expect(user.custom_fields["test"]).to be_blank + expect(user.custom_fields["test1"]).to be_blank + expect(user.custom_fields["test2"]).to eq("hello2") end + + it "works alongside a user field" do + user_field = Fabricate(:user_field, editable: true) + put "/u/#{user.username}.json", params: { custom_fields: { test1: :hello1, test2: :hello2 }, user_fields: { user_field.id.to_s => 'happy' } } + expect(response.status).to eq(200) + expect(user.custom_fields["test1"]).to be_blank + expect(user.custom_fields["test2"]).to eq("hello2") + expect(user.user_fields[user_field.id.to_s]).to eq('happy') + end + + it "is secure when there are no registered editable fields" do + User.plugin_editable_user_custom_fields.clear + put "/u/#{user.username}.json", params: { custom_fields: { test1: :hello1, test2: :hello2 } } + expect(response.status).to eq(200) + expect(user.custom_fields["test1"]).to be_blank + expect(user.custom_fields["test2"]).to be_blank + + put "/u/#{user.username}.json", params: { custom_fields: ["arrayitem1", "arrayitem2"] } + expect(response.status).to eq(200) + end + end end From d8b543bb6794e26bedca3047532daef8e7e29afd Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 5 Sep 2018 00:46:54 +0530 Subject: [PATCH 056/124] FIX: redirect to original URL after social signup --- .../discourse/controllers/create-account.js.es6 | 7 +++++++ app/controllers/users_controller.rb | 2 ++ lib/auth/result.rb | 2 ++ spec/requests/users_controller_spec.rb | 12 ++++++++++++ 4 files changed, 23 insertions(+) diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index a07deba915..581a2040ff 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -205,6 +205,11 @@ export default Ember.Controller.extend( "accountChallenge" ); const userFields = this.get("userFields"); + const destinationUrl = this.get("authOptions.destination_url"); + + if (!Ember.isEmpty(destinationUrl)) { + $.cookie("destination_url", destinationUrl, { path: '/' }); + } // Add the userfields to the data if (!Ember.isEmpty(userFields)) { @@ -255,10 +260,12 @@ export default Ember.Controller.extend( this.get("rejectedPasswords").pushObject(attrs.accountPassword); } this.set("formSubmitted", false); + $.cookie("destination_url", null); } }, () => { this.set("formSubmitted", false); + $.cookie("destination_url", null); return this.flash(I18n.t("create_account.failed"), "error"); } ); diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 065f89732a..92a9bd4796 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -675,6 +675,8 @@ class UsersController < ApplicationController if current_user.present? if SiteSetting.enable_sso_provider && payload = cookies.delete(:sso_payload) return redirect_to(session_sso_provider_url + "?" + payload) + elsif destination_url = cookies.delete(:destination_url) + return redirect_to(destination_url) else return redirect_to(path('/')) end diff --git a/lib/auth/result.rb b/lib/auth/result.rb index 9f34d2c62e..e36d4638bd 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -69,6 +69,8 @@ class Auth::Result email_valid: !!email_valid, omit_username: !!omit_username } + result[:destination_url] = destination_url if destination_url.present? + if SiteSetting.enable_names? result[:name] = User.suggest_name(name || username || email) end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 9956eae29b..717b8ccdd7 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -2571,6 +2571,18 @@ describe UsersController do expect(response).to redirect_to("/") end + context 'when cookies contains a destination URL' do + it 'should redirect to the URL' do + sign_in(Fabricate(:user)) + destination_url = 'http://thisisasite.com/somepath' + cookies[:destination_url] = destination_url + + get "/u/account-created" + + expect(response).to redirect_to(destination_url) + end + end + context "when the user account is created" do include ApplicationHelper From 5cf1a9a23a50c94b01a5635e3e3d7e870fffc381 Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 4 Sep 2018 16:17:06 -0400 Subject: [PATCH 057/124] UX: primary & danger buttons should lighten on hover in dark themes --- app/assets/stylesheets/common/components/buttons.scss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index d8b15095e7..e3a77bb605 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -88,7 +88,10 @@ } &:hover, &.btn-hover { - background: scale-color($tertiary, $lightness: -20%); + background: dark-light-choose( + scale-color($tertiary, $lightness: -20%), + scale-color($tertiary, $lightness: 20%) + ); } &:active, &.btn-active { @@ -115,7 +118,10 @@ } &:hover, &.btn-hover { - background: scale-color($danger, $lightness: -20%); + background: dark-light-choose( + scale-color($danger, $lightness: -20%), + scale-color($danger, $lightness: 20%) + ); } &:active, &.btn-active { From 8a952a2cc2411d94eb27b0ed456d4a6db1cb0edd Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 5 Sep 2018 02:00:13 +0530 Subject: [PATCH 058/124] Make prettier happy --- .../javascripts/discourse/controllers/create-account.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index 581a2040ff..22848d8a2f 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -208,7 +208,7 @@ export default Ember.Controller.extend( const destinationUrl = this.get("authOptions.destination_url"); if (!Ember.isEmpty(destinationUrl)) { - $.cookie("destination_url", destinationUrl, { path: '/' }); + $.cookie("destination_url", destinationUrl, { path: "/" }); } // Add the userfields to the data From d9be4f47e85ebb2d853620b6fb290f2d74221f62 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 5 Sep 2018 03:24:50 +0530 Subject: [PATCH 059/124] SPEC: redirect to original URL after social signup --- .../omniauth_callbacks_controller_spec.rb | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/spec/requests/omniauth_callbacks_controller_spec.rb b/spec/requests/omniauth_callbacks_controller_spec.rb index 566d724cf9..6547c8a0f1 100644 --- a/spec/requests/omniauth_callbacks_controller_spec.rb +++ b/spec/requests/omniauth_callbacks_controller_spec.rb @@ -88,6 +88,61 @@ RSpec.describe Users::OmniauthCallbacksController do end end + describe 'when user not found' do + let(:email) { "somename@gmail.com" } + before do + OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( + provider: 'google_oauth2', + uid: '123545', + info: OmniAuth::AuthHash::InfoHash.new( + email: email, + name: 'Some name' + ), + extra: { + raw_info: OmniAuth::AuthHash.new( + email_verified: true, + email: email, + family_name: 'Huh', + given_name: "Some name", + gender: 'male', + name: "Some name Huh", + ) + }, + ) + + Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] + end + + it 'should return the right response' do + destination_url = 'http://thisisasite.com/somepath' + Rails.application.env_config["omniauth.origin"] = destination_url + + get "/auth/google_oauth2/callback.json" + + expect(response.status).to eq(200) + + response_body = JSON.parse(response.body) + + expect(response_body["email"]).to eq(email) + expect(response_body["username"]).to eq("Some_name") + expect(response_body["auth_provider"]).to eq("Google_oauth2") + expect(response_body["email_valid"]).to eq(true) + expect(response_body["omit_username"]).to eq(false) + expect(response_body["name"]).to eq("Some Name") + expect(response_body["destination_url"]).to eq(destination_url) + end + + it 'should include destination url in response' do + destination_url = 'http://thisisasite.com/somepath' + cookies[:destination_url] = destination_url + + get "/auth/google_oauth2/callback.json" + + response_body = JSON.parse(response.body) + expect(response_body["destination_url"]).to eq(destination_url) + end + end + describe 'when user has been verified' do before do OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( From c788737eede57efc7e8a93536a099a87b63fb594 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 4 Sep 2018 11:47:05 +0200 Subject: [PATCH 060/124] FIX: Notifications shouldn't use user locale unless allow_user_locale is enabled --- app/mailers/user_notifications.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index cde42b3ee6..76be39f1de 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -20,10 +20,15 @@ class UserNotifications < ActionMailer::Base end def signup_after_approval(user, opts = {}) + locale = user_locale(user) + tips = I18n.t('system_messages.usage_tips.text_body_template', + base_url: Discourse.base_url, + locale: locale) + build_email(user.email, template: 'user_notifications.signup_after_approval', - locale: user_locale(user), - new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url, locale: locale)) + locale: locale, + new_user_tips: tips) end def notify_old_email(user, opts = {}) @@ -320,7 +325,7 @@ class UserNotifications < ActionMailer::Base protected def user_locale(user) - (user.locale.present? && I18n.available_locales.include?(user.locale.to_sym)) ? user.locale : nil + user.effective_locale end def email_post_markdown(post, add_posted_by = false) From b8fc6991646a431b81caaac2283b5c317cb1aa8f Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 4 Sep 2018 12:01:29 +0200 Subject: [PATCH 061/124] FIX: Detect {{foo}} as interpolation key --- lib/i18n/i18n_interpolation_keys_finder.rb | 2 +- spec/services/i18n_interpolation_keys_finder_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/i18n/i18n_interpolation_keys_finder.rb b/lib/i18n/i18n_interpolation_keys_finder.rb index cc91e806b9..7c5fe9288a 100644 --- a/lib/i18n/i18n_interpolation_keys_finder.rb +++ b/lib/i18n/i18n_interpolation_keys_finder.rb @@ -1,6 +1,6 @@ class I18nInterpolationKeysFinder def self.find(text) - keys = text.scan(I18n::INTERPOLATION_PATTERN) + keys = text.scan(Regexp.union(I18n::INTERPOLATION_PATTERN, /\{\{(\w+)\}\}/)) keys.flatten! keys.compact! keys.uniq! diff --git a/spec/services/i18n_interpolation_keys_finder_spec.rb b/spec/services/i18n_interpolation_keys_finder_spec.rb index dde36f4040..7440767652 100644 --- a/spec/services/i18n_interpolation_keys_finder_spec.rb +++ b/spec/services/i18n_interpolation_keys_finder_spec.rb @@ -4,8 +4,8 @@ require "i18n/i18n_interpolation_keys_finder" RSpec.describe I18nInterpolationKeysFinder do describe '#find' do it 'should return the right keys' do - expect(described_class.find('%{first} %{second}')) - .to eq(['first', 'second']) + expect(described_class.find('%{first} %{second} {{third}}')) + .to eq(['first', 'second', 'third']) end end end From 0d8c72d8c4179af7318ee3d1b570c5cc9a2a9f42 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 4 Sep 2018 23:57:20 +0200 Subject: [PATCH 062/124] DEV: Add rake task to check locale files for errors --- Gemfile | 1 + Gemfile.lock | 1 + lib/i18n/locale_file_checker.rb | 147 ++++++++++++++++++++++++++++++++ lib/tasks/i18n.rake | 52 +++++++++++ 4 files changed, 201 insertions(+) create mode 100644 lib/i18n/locale_file_checker.rb create mode 100644 lib/tasks/i18n.rake diff --git a/Gemfile b/Gemfile index 5de25c77d7..ca22ff21f3 100644 --- a/Gemfile +++ b/Gemfile @@ -193,3 +193,4 @@ if ENV["IMPORT"] == "1" end gem 'webpush', require: false +gem 'colored2', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 600cecb1bd..689fe3d92d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -458,6 +458,7 @@ DEPENDENCIES bullet byebug certified + colored2 cppjieba_rb danger discourse_image_optim diff --git a/lib/i18n/locale_file_checker.rb b/lib/i18n/locale_file_checker.rb new file mode 100644 index 0000000000..558d442c5c --- /dev/null +++ b/lib/i18n/locale_file_checker.rb @@ -0,0 +1,147 @@ +require 'i18n/i18n_interpolation_keys_finder' +require 'yaml' + +class LocaleFileChecker + TYPE_MISSING_INTERPOLATION_KEY = 1 + TYPE_UNSUPPORTED_INTERPOLATION_KEY = 2 + TYPE_MISSING_PLURAL_KEY = 3 + + def check(locale) + @errors = {} + @locale = locale.to_s + + 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 + @locale_yaml = YAML.load_file(locale_path) + @reference_yaml = YAML.load_file(reference_path) + + check_interpolation_keys + check_plural_keys + + # TODO check MessageFormat + end + + @errors + end + + private + + YML_DIRS = ["config/locales", "plugins/**/locales"] + PLURALS_FILE = "config/locales/plurals.rb" + REFERENCE_LOCALE = "en" + REFERENCE_PLURAL_KEYS = ["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", "lt", "lv", "ru", "sl", "sr", "uk"] + + def locale_files + YML_DIRS.map { |dir| Dir["#{Rails.root}/#{dir}/{client,server}.#{@locale}.yml"] }.flatten + end + + def reference_file(path) + path = path.gsub(/\.\w{2,}\.yml$/, ".#{REFERENCE_LOCALE}.yml") + path if File.exists?(path) + end + + def traverse_hash(hash, parent_keys, &block) + hash.each do |key, value| + keys = parent_keys.dup << key + + if value.is_a?(Hash) + traverse_hash(value, keys, &block) + else + yield(keys, value, hash) + end + end + end + + def check_interpolation_keys + traverse_hash(@locale_yaml, []) do |keys, value| + reference_value = reference_value(keys) + next if reference_value.nil? + + if pluralized = reference_value_pluralized?(reference_value) + if keys.last == "one" && !FORCE_PLURAL_COUNT_LOCALES.include?(@locale) + reference_value = reference_value["one"] + else + reference_value = reference_value["other"] + end + end + + reference_interpolation_keys = I18nInterpolationKeysFinder.find(reference_value.to_s) + locale_interpolation_keys = I18nInterpolationKeysFinder.find(value.to_s) + + missing_keys = reference_interpolation_keys - locale_interpolation_keys + unsupported_keys = locale_interpolation_keys - reference_interpolation_keys + + # English strings often don't use the %{count} variable within the "one" key, + # but it's perfectly fine for other locales to use it. + unsupported_keys.delete("count") if pluralized && keys.last == "one" + + # Not all locales need the %{count} variable within the "one" key. + if pluralized && keys.last == "one" && !FORCE_PLURAL_COUNT_LOCALES.include?(@locale) + missing_keys.delete("count") + end + + add_error(keys, TYPE_MISSING_INTERPOLATION_KEY, missing_keys) unless missing_keys.empty? + add_error(keys, TYPE_UNSUPPORTED_INTERPOLATION_KEY, unsupported_keys) unless unsupported_keys.empty? + end + end + + def check_plural_keys + known_parent_keys = Set.new + + traverse_hash(@locale_yaml, []) do |keys, _, parent| + keys = keys[0..-2] + parent_key = keys.join(".") + next if known_parent_keys.include?(parent_key) + known_parent_keys << parent_key + + reference_value = reference_value(keys) + next if reference_value.nil? || !reference_value_pluralized?(reference_value) + + expected_plural_keys = plural_keys[@locale] + actual_plural_keys = parent.is_a?(Hash) ? parent.keys : [] + missing_plural_keys = expected_plural_keys - actual_plural_keys + + add_error(keys, TYPE_MISSING_PLURAL_KEY, missing_plural_keys) unless missing_plural_keys.empty? + end + end + + def reference_value(keys) + value = @reference_yaml[REFERENCE_LOCALE] + + keys[1..-2].each do |key| + value = value[key] + return nil if value.nil? + end + + reference_value_pluralized?(value) ? value : value[keys.last] + end + + def reference_value_pluralized?(value) + 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| + [locale.to_s, value[:i18n][:plural][:keys].map(&:to_s)] + end.to_h + end + end + + def add_error(keys, type, details) + @errors[@relative_locale_path] ||= [] + @errors[@relative_locale_path] << { + key: keys[1..-1].join("."), + type: type, + details: details.to_s + } + end +end diff --git a/lib/tasks/i18n.rake b/lib/tasks/i18n.rake new file mode 100644 index 0000000000..278891afd2 --- /dev/null +++ b/lib/tasks/i18n.rake @@ -0,0 +1,52 @@ +require 'i18n/locale_file_checker' +require 'colored2' + +desc "Checks locale files for errors" +task "i18n:check", [:locale] => [:environment] do |_, args| + locale = args[:locale] + failed_locales = [] + + if locale.present? + if LocaleSiteSetting.valid_value?(locale) + locales = [locale] + else + puts "ERROR: #{locale} is not a valid locale" + exit 1 + end + else + locales = LocaleSiteSetting.supported_locales + end + + locales.each do |locale| + begin + all_errors = LocaleFileChecker.new.check(locale) + rescue + failed_locales << locale + next + end + + all_errors.each do |filename, errors| + puts "", "=" * 80 + puts filename.bold + puts "=" * 80 + + errors.each do |error| + message = case error[:type] + when LocaleFileChecker::TYPE_MISSING_INTERPOLATION_KEY + "Missing interpolation key".red + when LocaleFileChecker::TYPE_UNSUPPORTED_INTERPOLATION_KEY + "Unsupported interpolation key".red + when LocaleFileChecker::TYPE_MISSING_PLURAL_KEY + "Missing plural key".yellow + end + details = error[:details] ? ": #{error[:details]}" : "" + + puts error[:key] << " -- " << message << details + end + end + end + + failed_locales.each do |locale| + puts "", "Failed to check locale files for #{locale}".red + end +end From 6658a6601c7c5f1667bf7b37589d32d82a0491d9 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 00:24:44 +0200 Subject: [PATCH 063/124] Remove unused rake task --- lib/tasks/i18n_stats.rake | 134 -------------------------------------- 1 file changed, 134 deletions(-) delete mode 100644 lib/tasks/i18n_stats.rake diff --git a/lib/tasks/i18n_stats.rake b/lib/tasks/i18n_stats.rake deleted file mode 100644 index ae48b2e968..0000000000 --- a/lib/tasks/i18n_stats.rake +++ /dev/null @@ -1,134 +0,0 @@ -require 'yaml' - -desc "show the current translation status" -task "i18n:stats" => :environment do - - def to_dotted_hash(source, target = {}, namespace = nil) - prefix = "#{namespace}." if namespace - if source.kind_of?(Hash) - - # detect pluralizable string - if (source["other"] != nil) - target[namespace] = { pluralizable: true, content: source } - return - end - - source.each do |key, value| - to_dotted_hash(value, target, "#{prefix}#{key}") - end - else - target[namespace] = source - end - target - end - - def compare(a, b, plural_keys) - locale1 = /.*\.([^.]{2,})\.yml$/.match(a)[1] - locale2 = /.*\.([^.]{2,})\.yml$/.match(b)[1] - - a = YAML.load_file("#{Rails.root}/config/locales/#{a}")[locale1] - b = YAML.load_file("#{Rails.root}/config/locales/#{b}")[locale2] - a = to_dotted_hash(a) - b = to_dotted_hash(b) - - plus = [] - minus = [] - same = [] - total = a.count - - a.each do |key, value| - if b[key] == nil - minus << key - end - if a[key] == b[key] - same << key - end - end - - b.each do |key, value| - if a[key] == nil - plus << key - end - end - - a.each do |key, value| - if value.kind_of?(Hash) - if value[:pluralizable] - plural_keys.each do |pl| - if b[key] == nil || !b[key].kind_of?(Hash) || b[key][:content][pl] == nil - minus << "#{key}.#{pl}" - end - end - - if b[key] != nil && b[key].kind_of?(Hash) - b[key][:content].each do |pl, val| - if ! plural_keys.include?(pl) - if a[key][:content]["zero"] == nil - plus << "#{key}.#{pl}" - end - end - end - end - - # special handling for zero - if a[key][:content]["zero"] != nil - if b[key] == nil || !b[key].kind_of?(Hash) || b[key][:content]["zero"] == nil - minus << "#{key}.zero" - end - end - end - end - end - - return plus, minus, same, total - end - - def get_plurals(locale) - I18n.t("i18n.plural.keys", locale: locale).map { |x| x.to_s } - end - - puts "Discourse Translation Status Script" - puts "To show details about a specific locale (e.g. 'de'), run as:" - puts " rake i18n:stats locale=de" - puts "" - - filemask = "client.*.yml" - details = false - if ENV['locale'] != nil - filemask = "client.#{ENV['locale']}.yml" - details = true - end - - puts " locale | cli+ | cli- | cli= | cli tot| srv+ | srv- | srv= | srv tot" - puts "----------------------------------------------------------------------------------" - - Dir["#{Rails.root}/config/locales/#{filemask}"].each do |f| - locale = /.*\.([^.]{2,})\.yml$/.match(f)[1] - next if locale == "en" - next if !File.exists?("#{Rails.root}/config/locales/client.#{locale}.yml") - next if !File.exists?("#{Rails.root}/config/locales/server.#{locale}.yml") - - pluralization_keys = get_plurals(locale) - - plus1, minus1, same1, total1 = compare("client.en.yml", "client.#{locale}.yml", pluralization_keys) - plus2, minus2, same2, total2 = compare("server.en.yml", "server.#{locale}.yml", pluralization_keys) - puts "%10s %8s %8s %8s %8s %8s %8s %8s %8s" % [locale, plus1.count, minus1.count, same1.count, total1, - plus2.count, minus2.count, same2.count, total2] - - if details - puts "" - puts "Equal keys:" - same1.each { |k| puts "client: #{locale}.#{k}" } - same2.each { |k| puts "server: #{locale}.#{k}" } - puts "" - puts "Missing keys:" - minus1.each { |k| puts "client: #{locale}.#{k}" } - minus2.each { |k| puts "server: #{locale}.#{k}" } - puts "" - puts "Surplus keys:" - plus1.each { |k| puts "client: #{locale}.#{k}" } - plus2.each { |k| puts "server: #{locale}.#{k}" } - end - end - -end From 44922b0c25eb116afd07ad2b66c916c06c3acbc6 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 00:45:11 +0200 Subject: [PATCH 064/124] zh_TW isn't broken anymore --- script/pull_translations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/pull_translations.rb b/script/pull_translations.rb index 78c0246471..c828348619 100755 --- a/script/pull_translations.rb +++ b/script/pull_translations.rb @@ -15,7 +15,7 @@ end # List of locales that will break Discourse and need to be fixed # by translators in Transifex. def broken_locales - ['ja', 'zh_TW'] + ['ja'] end def supported_locales From e22bf8ff280aa5e6d3abb6a82d139c10d2a2a217 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 00:47:04 +0200 Subject: [PATCH 065/124] Update German translations --- config/locales/client.de.yml | 49 +++++++- config/locales/server.de.yml | 105 ++++++++++++++++-- .../config/locales/server.de.yml | 2 + 3 files changed, 143 insertions(+), 13 deletions(-) diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index d1b08bcf3b..f1047c0452 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -196,6 +196,7 @@ de: privacy_policy: "Datenschutzrichtlinie" privacy: "Datenschutz" tos: "Nutzungsbedingungen" + rules: "Regeln" mobile_view: "Mobile Ansicht" desktop_view: "Desktop Ansicht" you: "Du" @@ -252,6 +253,9 @@ de: drafts: resume: "Fortsetzen" remove: "Entfernen" + new_topic: "Neues Thema Entwurf" + new_private_message: "Neuer privater Nachrichten Entwurf" + topic_reply: "Antwort Entwurf" topic_count_latest: one: "Zeige {{count}} neues oder aktualisiertes Thema" other: "Zeige {{count}} neue oder aktualisierte Themen" @@ -491,6 +495,7 @@ de: "12": "Gesendete Objekte" "13": "Posteingang" "14": "Ausstehend" + "15": "Entwürfe" categories: all: "Alle Kategorien" all_subcategories: "alle" @@ -513,7 +518,7 @@ de: one: "1 Thema" other: "%{count} Themen" topic_stat_sentence: - one: "1 neues Thema seit 1 %{unit}." + one: "%{count} neues Thema seit 1 %{unit}." other: "%{count} neue Themen seit 1 %{unit}." more: "(%{count}mehr) …" ip_lookup: @@ -532,6 +537,7 @@ de: post_count: "# Beiträge" confirm_delete_other_accounts: "Bist du sicher, dass du diese Konten löschen willst?" powered_by: "powered by ipinfo.io" + copied: "kopiert" user_fields: none: "(wähle eine Option aus)" user: @@ -634,6 +640,7 @@ de: revoke_access: "Entziehe Zugriffsrecht" undo_revoke_access: "Entziehe Zugriffsrecht widerrufen" api_approved: "Genehmigt:" + api_last_used_at: "Zuletzt benutzt am:" theme: "Design" home: "Standard-Startseite" staged: "Vorbereitet" @@ -673,6 +680,7 @@ de: choose_new: "Wähle ein neues Passwort" choose: "Wähle ein Passwort" second_factor_backup: + title: "Zwei-Faktor-Wiederherstellungs-Codes" regenerate: "Erneuern" disable: "Deaktivieren" enable: "Aktivieren" @@ -681,6 +689,9 @@ de: copied_to_clipboard: "Wurde in Zwischenablage kopiert" copy_to_clipboard_error: "Beim Kopieren in die Zwischenablage trat ein Fehler auf" remaining_codes: "Du hast noch {{count}} Wiederherstellungscodes übrig." + codes: + title: "Wiederherstellungscodes generiert" + description: "Jeder dieser Wiederherstellungscodes kann nur einmal benutzt werden. Bewahre diese an einem sicheren aber verfügbaren Ort auf." second_factor: title: "Zwei-Faktor-Authentifizierung" disable: "Zwei-Faktor-Authentifizierung deaktivieren" @@ -768,6 +779,18 @@ de: any: "(keine Einschränkung)" password_confirmation: title: "Wiederholung des Passworts" + auth_tokens: + title: "Kürzlich benutzte Geräte" + title_logs: "Authentifizierungs-Logs" + ip_address: "IP Adresse" + created: "Erzeugt" + first_seen: "Erstmalig gesehen" + last_seen: "Zuletzt gesehen" + operating_system: "Betriebssystem" + location: "Ort" + action: "Aktion" + login: "Einloggen" + logout: "Überall ausloggen" last_posted: "Letzter Beitrag" last_emailed: "Letzte E-Mail" last_seen: "Zuletzt gesehen" @@ -999,6 +1022,7 @@ de: hide_session: "Erinnere mich morgen" hide_forever: "Nein danke" hidden_for_session: "In Ordnung, ich frag dich morgen wieder. Du kannst dir auch jederzeit unter „Anmelden“ ein Benutzerkonto erstellen." + intro: "Hallo! Es sieht so aus, als ob dir diese Diskussion gefällt, aber Du hast bislang noch kein Konto angelegt." value_prop: "Wenn du ein Benutzerkonto anlegst, merken wir uns, was du gelesen hast, damit du immer dort fortsetzen kannst, wo du aufgehört hast. Du kannst auch Benachrichtigungen – hier oder per E-Mail – erhalten, wenn jemand auf deine Beiträge antwortet. Beiträge, die dir gefallen, kannst du mit einem Like versehen und diese Freude mit allen teilen. :heartpulse:" summary: enabled_description: "Du siehst gerade eine Zusammenfassung des Themas: die interessantesten Beiträge, die von der Community bestimmt wurden." @@ -1013,6 +1037,8 @@ de: disable: "Gelöschte Beiträge anzeigen" private_message_info: title: "Nachricht" + invite: "Lade andere ein" + edit: "Hinzufügen oder Entfernen" leave_message: "Möchtest du diese Nachricht wirklich verlassen?" remove_allowed_user: "Willst du {{name}} wirklich aus dieser Unterhaltung entfernen?" remove_allowed_group: "Willst du {{name}} wirklich aus dieser Unterhaltung entfernen?" @@ -1148,6 +1174,7 @@ de: default_header_text: Auswählen… no_content: Keine Treffer gefunden filter_placeholder: Suchen… + filter_placeholder_with_any: Suchen oder erzeugen create: "Erstelle: '{{content}}'" max_content_reached: one: "Du kannst nur einen Eintrag auswählen." @@ -1297,6 +1324,9 @@ de: shared_draft: label: "Gemeinsame Vorlage" desc: "Entwerfe ein Thema, das nur für das Team sichtbar ist." + toggle_topic_bump: + label: "Bump des Themas umschalten" + desc: "Antworten ohne das Bump-Datum des Themas zu ändern." notifications: tooltip: regular: @@ -1684,6 +1714,7 @@ de: reset_read: "„Gelesen“ zurücksetzen" make_public: "Umwandeln in öffentliches Thema" make_private: "in Nachricht umwandeln" + reset_bump_date: "Bump-Datum zurücksetzen" feature: pin: "Thema anheften" unpin: "Thema loslösen" @@ -2556,6 +2587,8 @@ de: moderation_tab: "Moderation" disabled: Deaktiviert timeout_error: "Entschuldige bitte, die Warteschlange ist zu lang, bitte wähle ein kürzeres Intervall" + exception_error: "Entschuldige, ein Fehler während der Ausführung des Query ist aufgetreten" + too_many_requests: Du hast diese Aktion zu oft ausgeführt. Bitte warte bevor Du es erneut probierst. reports: trend_title: "%{percent} Veränderung. Aktuell %{current}, war %{prev} in vorherigem Zeitraum." today: "Heute" @@ -2922,9 +2955,16 @@ de: revert: "Änderungen verwerfen" revert_confirm: "Möchtest du wirklich deine Änderungen verwerfen?" theme: + theme: "Theme" + component: "Komponente" + components: "Komponenten" import_theme: "Design importieren" customize_desc: "Anpassen:" title: "Designs" + modal_title: "Erstelle Theme" + create: "Erstelle" + create_type: "Typ:" + create_name: "Name:" long_title: "Farben, CSS und HTML-Inhalte deiner Seite erweitern" edit: "Bearbeiten" edit_confirm: "Dies ist ein Remote-Theme, wenn du CSS/HTML bearbeitest, werden deine Änderungen zurückgesetzt, wenn du das Theme das nächste Mal aktualisierst." @@ -2939,6 +2979,10 @@ de: color_scheme_select: "Wähle Farben für dieses Theme" custom_sections: "Benutzerdefinierte Abschnitte:" theme_components: "Theme-Komponenten" + switch_component: "Erzeuge Theme" + switch_component_alert: "Bist Du sicher, dass Du diese Komponente in ein Theme konvertieren möchtest? Dies wird es zu einem unabhängigen Theme machen und es wird als Kind von allen Themes entfernt werden." + switch_theme: "Komponente erzeugen" + switch_theme_alert: "Bist Du sicher, dass Du dieses Theme zu einer Komponente konvertieren möchtest? Es will als Elternteil von all Komponenten entfernt werden." uploads: "Uploads" no_uploads: "Du kannst Medieninhalte hochladen, die zu deinem Theme gehören, wie etwa Schriftarten und Bilder." add_upload: "Upload hinzufügen" @@ -2961,6 +3005,7 @@ de: public_key: "Gewähre dem folgenden öffentlichen Schlüssel den Zugriff auf das Repository:" about_theme: "Über das Theme" license: "Lizenz" + component_of: "Komponente von:" update_to_latest: "Aktualisieren auf neueste Version" check_for_updates: "Nach Aktualisierungen suchen" updating: "Wird aktualisiert..." @@ -2968,9 +3013,11 @@ de: add: "Hinzufügen" theme_settings: "Theme-Einstellungen" no_settings: "Dieses Theme hat keine Einstellungen." + empty: "Keine Teile" commits_behind: one: "Theme liegt 1 Commit zurück!" other: "Theme liegt {{count}} Commits zurück!" + compare_commits: "(Siehe neue Beiträge)" scss: text: "CSS" title: "Gib benutzerdefiniertes CSS ein, wir akzeptieren alle gültigen CSS und SCSS-Stile" diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index d73cf57140..437fa09f0b 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -39,6 +39,11 @@ de: bad_color_scheme: "Kann Motiv nicht aktualisieren, ungültiges Farbschema" other_error: "Etwas ist schief gelaufen beim Aktualisieren des Theme" error_importing: "Fehler beim Klonen des git-Repository, Zugriff verboten oder Repository nicht gefunden." + errors: + component_no_user_selectable: "Theme Komponenten können nicht Benutzer auswählbar sein " + component_no_default: "Theme Komponenten können nicht Standard Theme sein" + component_no_color_scheme: "Theme Komponenten können kein Farb-Schema haben" + no_multilevels_components: "Themes mit Unter-Themes können nicht selber Unter-Themes sein" settings_errors: invalid_yaml: "Der YAML-Code ist ungültig." data_type_not_a_number: "Der eingestellte Datentyp `%{name}` wird nicht unterstützt. Unterstützte Datentypen sind `integer`, `bool`, `list` und `enum`." @@ -225,6 +230,7 @@ de: topic_not_found: "Etwas ist schief gelaufen. Wurde das Thema eventuell geschlossen oder gelöscht, während du es angeschaut hast?" not_accepting_pms: "Entschuldige, %{username} akzeptiert gerade keine Nachrichten." max_pm_recepients: "Sorry, du kannst eine Nachricht nur an maximal %{recipients_limit} Emfpänger senden." + pm_reached_recipients_limit: "Entschuldige, aber Du kannst nicht mehr als %{recipients_limit} Empfänger in einer Nachricht haben." just_posted_that: "ist einer einer vor Kurzem von dir geschriebenen Nachricht zu ähnlich" invalid_characters: "enthält ungültige Zeichen" is_invalid: "scheint unklar, ist das ein ganzer Satz?" @@ -252,7 +258,7 @@ de: top_daily: "Top-Themen der letzten 24 Stunden" posts: "Letzte Beiträge" private_posts: "Neueste Nachrichten" - group_posts: "Letzte Beiträge von %(Gruppen_name)" + group_posts: "Neueste Beiträge von %{group_name}" group_mentions: "Neueste Nennungen von %{group_name}" user_posts: "Neueste Beiträge von @%{username}" user_topics: "Neueste Themen von @%{username}" @@ -474,7 +480,7 @@ de: topic_exists: one: "Diese Kategorie kann nicht gelöscht werden, weil sie %{count} Themen enthält. Das älteste Thema ist %{topic_link}." other: "Diese Kategorie kann nicht gelöscht werden, weil sie %{count} Themen enthält. Das älteste Thema ist %{topic_link}." - topic_exists_no_oldest: "Diese Kategorie kann nicht gelöscht werden, weil sie #{category.topic_count} Themen enthält." + topic_exists_no_oldest: "Diese Kategorie kann nicht gelöscht werden, weil sie %{count} Themen enthält." uncategorized_description: "Themen welche keine Kategorie benötigen oder in keine existierende Kategorie passen." trust_levels: newuser: @@ -602,6 +608,17 @@ de: email_login: invalid_token: "Entschuldige, aber der Link zum Anmelden ist zu alt. Wähle die Anmelden-Schaltfläche und nutze „Ich habe mein Passwort vergessen“, um einen neuen Link zu erhalten." title: "E-Mail-Anmeldung" + user_auth_tokens: + devices: + android: 'Android Gerät' + linux: 'Linux Computer' + windows: 'Windows Computer' + mac: 'Mac' + iphone: 'iPhone' + ipad: 'iPad' + ipod: 'iPod' + mobile: 'Mobiles Gerät' + unknown: 'Unbekanntes Gerät' change_email: confirmed: "Deine E-Mail-Adresse wurde aktualisiert." please_continue: "Weiter zu %{site_name}" @@ -675,6 +692,7 @@ de: self: "Du hast noch keine Aktivität." others: "Keine Aktivität." no_bookmarks: + self: "Du hast keine Beiträge mit Lesezeichen; Lesezeichen erlauben es Dir schnell zu einem spezifischen Beitrag zu referenzieren." others: "Keine Lesezeichen." no_likes_given: self: "Du hast noch keine Beiträge mit einem „Like“ markiert." @@ -682,6 +700,9 @@ de: no_replies: self: "Du hast auf keine Beiträge geantwortet." others: "Keine Antworten." + no_drafts: + self: "Du hast keine Entwürfe; beginne eine Antwort in einem beliebigen Thema und es wird automatisch als neuer Entwurf gespeichert." + others: "Du hast keine Berechtigung die Entwürfe dieses Benutzers zu sehen." topic_flag_types: spam: title: 'Spam' @@ -746,6 +767,7 @@ de: default: labels: count: Anzahl + percent: Prozent day: Tag post_edits: title: "Beitragsbearbeitungen" @@ -763,6 +785,7 @@ de: topic_count: Erstellte Themen post_count: Erstellte Beiträge pm_count: Erstellte Nachrichten + revision_count: Revisionen flags_status: title: "Meldungsstatus" values: @@ -990,6 +1013,7 @@ de: poll_pop3_timeout: "Die Verbindung zum POP3-Server schlägt mit einer Zeitüberschreitung fehl. Eingehende E-Mails konnten nicht abgerufen werden. Überprüfe deine POP3-Einstellungen." poll_pop3_auth_error: "Die Verbindung zum POP3-Server schlägt mit einem Authentisierungsfehler fehl. Überprüfe deine POP3-Einstellungen." force_https_warning: "Deine Webseite verwendet SSL, aber die `force_https` ist in deinen Website-Einstellungen noch nicht aktiviert." + out_of_date_themes: "Updates sind zu folgenden Themes verfügbar:" site_settings: censored_words: "Wörter, die automatisch durch ■■■■ ersetzt werden" delete_old_hidden_posts: "Automatisch alle Beiträge löschen, die länger als 30 Tage versteckt bleiben." @@ -1170,6 +1194,8 @@ de: enable_google_oauth2_logins: "Google OAuth2-Authentifizierung aktivieren. Dies ist der momentan von Google unterstützte Authentifizierungs-Mechanismus. Benötigt Client-ID und Secret." google_oauth2_client_id: "Client-ID deiner Google-Anwendung." google_oauth2_client_secret: "Client-Secret deiner Google-Anwendung." + google_oauth2_prompt: "Eine optionale Leerzeichen getrennte Liste von Zeichen Werten die angeben ob der Berechtigungsserver den Benutzer zur Re-Authentifizierung auffordert. Siehe https://developers.google.com/identity/protocols/OpenIDConnect#prompt für mögliche Werte." + google_oauth2_hd: "Eine optionale Google Apps gehostete Domain zu welches das Anmelden limitiert sein wird. Siehe https://developers.google.com/identity/protocols/OpenIDConnect#hd-param für weitere Informationen." enable_twitter_logins: "Aktiviere Twitter-Authentifizierung (benötigt twitter_consumer_key und twitter_consumer_secret)." twitter_consumer_key: "Consumer Key für Twitter-Authentifizierung, registriert auf https://apps.twitter.com/" twitter_consumer_secret: "Consumer Secret für Twitter-Authentifizierung, registriert auf https://apps.twitter.com" @@ -1364,6 +1390,7 @@ de: unsubscribe_via_email: "Erlaube es Benutzern eine E-Mail mit dem Betreff oder Text: \"unsubscribe\" zum Abbestellen der E-Mails zu senden." unsubscribe_via_email_footer: "Füge einen `mailto:`-Link zum Abbestellen im Fußbereich ausgehender E-Mails hinzu" delete_email_logs_after_days: "Lösche E-Mail Logs nach (N) Tagen. 0 um sie für immer zu behalten." + disallow_reply_by_email_after_days: "Verbiete Antworten per E-Mail nach (N) Tagen. 0 um es unbegrenzt zu lassen." max_emails_per_day_per_user: "Maximale Zahl an E-Mails, die Benutzern gesendet werden. 0 zum Deaktivieren der Grenze." enable_staged_users: "Erstelle automatisch vorbereitete Benutzer, wenn eingehende E-Mails verarbeitet werden." maximum_staged_users_per_email: "Maximale Anzahl vorbereiteter Benutzer, wenn eine eingehende E-Mail bearbeitet wird." @@ -1442,6 +1469,7 @@ de: dominating_topic_minimum_percent: "Anteil der Nachrichten eines Themas in Prozent, die ein einzelner Benutzer verfassen darf, bevor dieser Benutzer darauf hingewiesen wird, dass er dieses Thema dominiert." disable_avatar_education_message: "Deaktiviert den Hinweis für Benutzer, dass sie ihr Profilbild ändern können" suppress_uncategorized_badge: "Zeige kein Abzeichen für unkategorisierte Themen in der Themenliste." + header_dropdown_category_count: "Wie viele Kategorien im der Header Dropdown-Liste angezeigt werden können." permalink_normalizations: "Diesen regulären Ausdruck anwenden, bevor Permalinks verarbeitet werden; Beispiel: /(topic.*)\\?.*/\\1 wird Query-Strings von Themen-Routen entfernen. Format: regulärer Ausdruck + String, benutze \\1 usw. um Teilausdrücke zu verwenden" global_notice: "Zeigt allen Besuchern eine DRINGENDE NOTFALL-NACHRICHT als global sichtbares Banner an. Deaktiviert bei leerer Nachricht. (HTML ist erlaubt.)" disable_edit_notifications: "Unterdrückt Bearbeitungshinweise durch den System-Benutzer, wenn die 'download_remote_images_to_local' Einstellung aktiviert ist." @@ -1518,6 +1546,7 @@ de: min_trust_level_for_user_api_key: "Erforderliche Vertrauensstufe für die Generierung von Benutzer API Schlüsseln" allowed_user_api_auth_redirects: "Erlaubte URL für die Authentifizierungs-Umleitung von Benutzer API Schlüsseln" allowed_user_api_push_urls: "Erlaubte URL für Server-Push zur Benutzer API" + expire_user_api_keys_days: "Anzahl Tage bevor ein Benutzer API Schlüssel automatisch abläuft. (0 für niemals)" tagging_enabled: "Schlagwörter für Themen aktivieren" min_trust_to_create_tag: "Minimale Vertrauensstufe, um ein Schlagwort zu erstellen." max_tags_per_topic: "Maximale Anzahl an Schlagwörtern, die einem Thema hinzugefügt werden können." @@ -1603,6 +1632,8 @@ de: existing_topic_moderator_post: one: "Ein Beitrag wurde in ein neues Thema verschoben: %{topic_link}" other: "%{count} Beiträge wurden in ein neues Thema verschoben: %{topic_link}" + change_owner: + post_revision_text: "Eigentümer übertragen." topic_statuses: archived_enabled: "Dieses Thema ist nun archiviert. Es ist eingefroren und kann in keiner Weise mehr verändert werden." archived_disabled: "Dieses Thema wurde aus dem Archiv geholt. Es ist nicht länger eingefroren und kann verändert werden." @@ -1688,7 +1719,16 @@ de: second_factor_title: "Zwei-Faktor Authentifizierung" second_factor_description: "Bitte gib den erforderlichen Authentifizierungscode aus deiner App ein:" second_factor_backup_description: "Bitte gib einen deiner Wiederherstellungs-Codes ein:" + second_factor_backup_title: "Zwei-Faktoren Wiederherstellungs-Code" invalid_second_factor_code: "Ungültiger Authentifizierungscode. Jeder Code kann nur einmal benutzt werden." + second_factor_toggle: + totp: "Benutze stattdessen eine Authentifizierungs-App" + backup_code: "Benutze stattdessen einen Backup Code" + admin: + email: + sent_test: "gesendet!" + sent_test_disabled: "kann nicht gesendet werden da E-Mails deaktiviert sind" + sent_test_disabled_for_non_staff: "kann nicht gesendet werden da E-Mails für Nicht-Moderatoren deaktiviert sind" user: deactivated: "Deaktiviert wegen zu vielen unzustellbaren E-Mails an '%{email}'." deactivated_by_staff: "Deaktiviert vom Team" @@ -1961,6 +2001,18 @@ de: flags_agreed_and_post_deleted: title: "Gemeldeter Beitrag von Team entfernt" subject_template: "Gemeldeter Beitrag von Team entfernt" + text_body_template: | + Hallo, + + Dies ist eine automatische Nachricht von %{site_name} um Dich wissen zu lassen, dass Dein Beitrag entfernt wurde. + + <%{base_url}%{url}> + + %{flag_reason} + + Dieser Beitrag war von der Gemeinschaft gemeldet worden und ein Moderationsmitglied den Beitrag entfernt. + + Bitte sieh Dir unsere [Verhaltensregeln](%{base_url}/guidelines) für weitere Informationen an. usage_tips: text_body_template: | Für einige schnelle Tipps, um als neuer Benutzer loszulegen, [schaue einmal diesen Blog-Beitrag an](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). @@ -2181,6 +2233,13 @@ de: %{post_error} Bitte versuch es erneut, wenn du das Problem beheben kannst. + email_reject_post_too_short: + title: "E-Mail Ablehnungsbeitrag zu kurz" + subject_template: "[%{email_prefix}] Email Problem -- Beitrag zu kurz" + text_body_template: | + Es tut uns leid aber Deine E-Mail Nachricht an %{destination} (betitelt mit %{former_title}) funktionierte nicht. + + Um eine tiefere Konversation zu fördern sind kurze Antworten nicht erlaubt. Bitte antworte mit mindestens %{count} Zeichen? Alternativ kannst Du auch einen Beitrag per E-Mail mit "+1" beantworten. email_reject_invalid_post_action: title: "E-Mail abgelehnt weil fehlerhafte Beitragsaktion" subject_template: "[%{email_prefix}] E-Mail-Problem -- Beitragsaktion ungültig" @@ -2205,20 +2264,24 @@ de: email_reject_old_destination: title: "E-Mail abgelehnt weil alter Empfänger" subject_template: "[%{email_prefix}] E-Mail-Problem -- Du versuchst, auf eine alte Benachrichtigung zu antworten" + text_body_template: | + Es tut uns leid aber Deine E-Mail Nachricht an %{destination} (betitelt mit %{former_title}) funktionierte nicht. + + Wir akzeptieren nur Anworten auf die Original Nachrichten für %{number_of_days} Tage. Bitte [besuche das Thema](%{short_url}) um mit der Konversation fortzufahren. email_reject_topic_not_found: title: "E-Mail abgelehnt weil Thema nicht gefunden" subject_template: "[%{email_prefix}] E-Mail-Problem -- Thema nicht gefunden" text_body_template: | Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - Das Thema, auf das du geantwortet hast, gibt es nicht mehr -- vielleicht wurde es gelöscht? Wenn du glaubst, dass dies ein Irrtum ist, nimm bitte Kontakt mit einem Team-Mitglied auf. + Das Thema, auf das du geantwortet hast, gibt es nicht mehr -- vielleicht wurde es gelöscht? Wenn du glaubst, dass dies ein Irrtum ist, nimm bitte [Kontakt mit einem Team-Mitglied](%{base_url}/about) auf. email_reject_topic_closed: title: "E-Mail abgelehnt weil Thema geschlossen" subject_template: "[%{email_prefix}] E-Mail-Problem -- Thema geschlossen" text_body_template: | Entschuldige, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) konnte nicht zugestellt werden. - Das Thema, auf das du geantwortet hast, ist derzeit geschlossen und akzeptiert keine Antworten mehr. Wenn du glaubst, dass dies ein Irrtum ist, nimm Bitte Kontakt mit einem Team-Mitglied auf. + Das Thema, auf das du geantwortet hast, ist derzeit geschlossen und akzeptiert keine Antworten mehr. Wenn du glaubst, dass dies ein Irrtum ist, nimm bitte [Kontakt mit einem Team-Mitglied](%{base_url}/about) auf. email_reject_auto_generated: title: "E-Mail abgelehnt weil automatisch generierte Antwort" subject_template: "[%{email_prefix}] E-Mail-Problem -- Antwort automatisch generiert" @@ -2242,9 +2305,24 @@ de: Bitte stelle sicher, dass du die POP-Zugangsdaten in [den Einstellungen](%{base_url}/admin/site_settings/category/email) korrekt konfiguriert hast. Wenn es eine Weboberfläche für das POP-E-Mail-Postfach gibt, möchtest du dich eventuell dort anmelden und die Einstellungen überprüfen. + email_revoked: + title: "E-Mail widerrufen" + subject_template: "Ist Deine E-Mail-Adresse korrekt?" + text_body_template: | + Es tut uns leid aber wir haben Probleme Dich per E-Mail zu erreichen. Unsere letzten E-Mails an Dich sind alle als unzustellbar zurück gekommen. + + Bitte stelle sicher, dass [E-Mail Adresse](%{base_url}/my/preferences/email) gültig und aktiv ist? Füge bitte unsere E-Mail Adresse in Deinem Adressbuch hinzu, damit die Zustellbarkeit verbessert wird. too_many_spam_flags: title: "Neues Konto gesperrt wegen zu viel Spam" subject_template: "Neues Konto gesperrt" + text_body_template: | + Hello, + + Dies ist eine automatische Nachricht von %{site_name} um Dich wissen zu lassen, dass deine Beiträge temporär ausgeblendet wurden, weil diese von der Gemeinschaft gemeldet wurden. + + Als Vorsichtsmaßnahme wurde Dein Konto stumm geschaltet und Du kannst nun nicht mehr antworten neue Themen erzeugen bis ein Team Mitglied Dein Konto überprüft hat. Wir bitten diese Unannehmlichkeit zu entschuldigen. + + Für weitere Hinweise, schaue Dir bitte unsere [Verhaltensregeln](%{base_url}/guidelines) an. too_many_tl3_flags: title: "E-Mail abgelehnt wegen Meldungen durch Vertrauensstufe 3" subject_template: "Neues Konto gesperrt" @@ -2280,7 +2358,7 @@ de: Der Schwellenwert kann über die Einstellung `silence_new_user` geändert werden. spam_post_blocked: title: "Spam-Beitrag ausgeblendet" - subject_template: "Beiträge des neuen Benutzers ${username} wegen mehrfacher Verlinkung blockiert" + subject_template: "Beiträge des neuen Benutzers %{username} wegen mehrfacher Verlinkung blockiert" text_body_template: | Dies ist eine automatisierte Nachricht. @@ -2306,7 +2384,7 @@ de: text_body_template: | Es warten neuen Benutzer auf ihre Freigabe. - [Bitte bewerte diese im Administrationsbereich](/admin/users/list/pending). + [Bitte überprüfe diese im Administrationsbereich](%{base_url}/admin/users/list/pending). download_remote_images_disabled: title: "Download von externen Bildern deaktiviert" subject_template: "Download von externen Bildern deaktiviert" @@ -2558,7 +2636,7 @@ de: title: "Konto stummgeschaltet" subject_template: "[%{email_prefix}] Dein Konto wurde stummgeschaltet" text_body_template: | - Du wurdest im Forum stummgeschaltet bis %{suspended_till}. + Du wurdest im Forum stummgeschaltet bis %{silenced_till}. %{reason} @@ -2678,16 +2756,16 @@ de: %{new_email} signup_after_approval: title: "Konto bestätigen nach Genehmigung" - subject_template: "You've been approved on %{site_name}!" + subject_template: "Dein Konto bei %{site_name} wurde genehmigt!" text_body_template: | Willkommen bei%{site_name}! - Ein Team-Mitglied hat dein Benutzerkonto auf %{site_name} bestätigt. + Ein Team-Mitglied hat dein Benutzerkonto auf %{site_name} genehmigt. Du kannst dein neues Konto nun verwenden, indem du dich hier anmeldest: - %{base_url}/u/activate-account/%{email_token} + %{base_url} - Wenn sich der obenstehende Link nicht anklicken lässt, versuche ihn zu kopieren und in die Adresszeile deines Webbrowsers einzufügen. + Wenn sich der obenstehende Link nicht anklicken lässt, versuche ihn zu kopieren und in die Adresszeile deines Browsers einzufügen. %{new_user_tips} @@ -2710,6 +2788,7 @@ de: recent_topics: "Aktuell" see_more: "Mehr" search_title: "Diese Site durchsuchen" + search_button: "Suche" offline: title: "App kann nicht geladen werden" offline_page_message: "Sieht so aus als wärst du offline! Bitte überprüfe deine Netzwerkverbindung und probiere es nochmal." @@ -2761,6 +2840,8 @@ de: sender_message_blank: "Nachricht ist leer" sender_message_to_blank: "message.to ist leer" sender_text_part_body_blank: "text_part.body ist leer" + sender_body_blank: "Textkörper ist leer" + sender_post_deleted: "Beitrag wurde gelöscht" color_schemes: base_theme_name: "Basis" light: "Helles Schema" @@ -2892,7 +2973,7 @@ de: tos_topic: title: "Nutzungsbedingungen" body: | - SiteDie folgenden Geschäftsbedingungen sind maßgebend für die gesamte Webseite und alle Inhalte, Dienstleistungen und Produkte, die auf oder über die Webseite zur Verfügung gestellt werden, einschließlich, aber nicht beschränkt auf die %{company_domain} Foren-Software, die %{company_domain} Support-Foren und den %{company_domain} Hosting-Dienst („Hosting”), (zusammengefasst: die „Webseite“). Die Webseite gehört und wird betrieben von („“). Die Website wird angeboten vorbehaltlich der uneingeschränkten Zustimmung aller hierin enthaltenen Bedingungen und aller anderen sonstigen betrieblichen Regeln, Richtlinien (einschließlich, ohne Einschränkung, der [Datenschutzrichtlinien](/privacy) and [Community-Richtlinien](/faq)) von %{company_domain} und Verfahren, die von Zeit zu Zeit auf dieser Webseite von %{company_name} veröffentlicht werden können (zusammen die „Vereinbarung“). + Die folgenden Geschäftsbedingungen sind maßgebend für die gesamte Webseite und alle Inhalte, Dienstleistungen und Produkte, die auf oder über die Webseite zur Verfügung gestellt werden, einschließlich, aber nicht beschränkt auf die %{company_domain} Foren-Software, die %{company_domain} Support-Foren und den %{company_domain} Hosting-Dienst („Hosting”), (zusammengefasst: die „Webseite“). Die Webseite gehört und wird betrieben von %{company_full_name} („%{company_name}“). Die Website wird angeboten vorbehaltlich der uneingeschränkten Zustimmung aller hierin enthaltenen Bedingungen und aller anderen sonstigen betrieblichen Regeln, Richtlinien (einschließlich, ohne Einschränkung, der [Datenschutzrichtlinien](/privacy) and [Community-Richtlinien](/faq)) von %{company_domain} und Verfahren, die von Zeit zu Zeit auf dieser Webseite von veröffentlicht werden können (zusammen die „Vereinbarung“). Bitte lies diese Vereinbarung sorgfältig durch, bevor du die Webseite verwendest oder darauf zugreifst. Durch die Benutzung oder den Zugriff auf einen Teil der Webseite stimmst du den Geschäftsbedingungen dieser Vereinbarung zu. Wenn du nicht allen Geschäftsbedingungen in dieser Vereinbarung zustimmst, dann darfst du weder auf die Seite zugreifen noch irgendwelche Dienstleistungen in Anspruch nehmen. Wenn diese Geschäftsbedingungen als Angebot von %{company_name} erachtet werden, beschränkt sich die Zustimmung ausdrücklich auf diese Bedingungen. Die Website wird nur für Benutzer angeboten, die mindestens 13 Jahre alt sind. diff --git a/plugins/discourse-narrative-bot/config/locales/server.de.yml b/plugins/discourse-narrative-bot/config/locales/server.de.yml index b9a81991f7..8aeeb347a1 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.de.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.de.yml @@ -7,6 +7,7 @@ de: site_settings: + discourse_narrative_bot_enabled: 'Aktiviere den Discourse Lerntrainings-Bot (discobot)' disable_discourse_narrative_bot_welcome_post: "Die vom Discourse Narrative Bot gesendete Willkommensnachricht deaktivieren" discourse_narrative_bot_ignored_usernames: "Benutzernamen, die vom Discourse Narrative Bot ignoriert werden sollen" discourse_narrative_bot_disable_public_replies: "Öffentliche Antworten von Discourse Narrative Bot deaktivieren" @@ -140,6 +141,7 @@ de: In der Zwischenzeit lass ich dich erst mal in Ruhe. new_user_narrative: reset_trigger: "neuer Benutzer" + title: "Zertifikat für erfolgreiche Beendigung des Lerntrainings" cert_title: "In Anerkennung deines erfolgreichen Abschlusses eines Tutorials für neue Benutzer" hello: title: ":robot: Grüß dich!" From f4956f79a5c1e2766c776547057b75f3a6587a27 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 01:04:43 +0200 Subject: [PATCH 066/124] Make Rubocop happy --- lib/tasks/i18n.rake | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/tasks/i18n.rake b/lib/tasks/i18n.rake index 278891afd2..25ea4c846a 100644 --- a/lib/tasks/i18n.rake +++ b/lib/tasks/i18n.rake @@ -3,12 +3,11 @@ require 'colored2' desc "Checks locale files for errors" task "i18n:check", [:locale] => [:environment] do |_, args| - locale = args[:locale] failed_locales = [] - if locale.present? - if LocaleSiteSetting.valid_value?(locale) - locales = [locale] + if args[:locale].present? + if LocaleSiteSetting.valid_value?(args[:locale]) + locales = [args[:locale]] else puts "ERROR: #{locale} is not a valid locale" exit 1 @@ -31,14 +30,15 @@ task "i18n:check", [:locale] => [:environment] do |_, args| puts "=" * 80 errors.each do |error| - message = case error[:type] - when LocaleFileChecker::TYPE_MISSING_INTERPOLATION_KEY - "Missing interpolation key".red - when LocaleFileChecker::TYPE_UNSUPPORTED_INTERPOLATION_KEY - "Unsupported interpolation key".red - when LocaleFileChecker::TYPE_MISSING_PLURAL_KEY - "Missing plural key".yellow - end + message = + case error[:type] + when LocaleFileChecker::TYPE_MISSING_INTERPOLATION_KEY + "Missing interpolation key".red + when LocaleFileChecker::TYPE_UNSUPPORTED_INTERPOLATION_KEY + "Unsupported interpolation key".red + when LocaleFileChecker::TYPE_MISSING_PLURAL_KEY + "Missing plural key".yellow + end details = error[:details] ? ": #{error[:details]}" : "" puts error[:key] << " -- " << message << details @@ -46,7 +46,7 @@ task "i18n:check", [:locale] => [:environment] do |_, args| end end - failed_locales.each do |locale| - puts "", "Failed to check locale files for #{locale}".red + failed_locales.each do |failed_locale| + puts "", "Failed to check locale files for #{failed_locale}".red end end From 9d352406200e38feaedf2b6ecb775b6e4a340f22 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 01:53:22 +0200 Subject: [PATCH 067/124] Revert "FIX: Notifications shouldn't use user locale unless allow_user_locale is enabled" This reverts commit c788737eede57efc7e8a93536a099a87b63fb594. --- app/mailers/user_notifications.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 76be39f1de..cde42b3ee6 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -20,15 +20,10 @@ class UserNotifications < ActionMailer::Base end def signup_after_approval(user, opts = {}) - locale = user_locale(user) - tips = I18n.t('system_messages.usage_tips.text_body_template', - base_url: Discourse.base_url, - locale: locale) - build_email(user.email, template: 'user_notifications.signup_after_approval', - locale: locale, - new_user_tips: tips) + locale: user_locale(user), + new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url, locale: locale)) end def notify_old_email(user, opts = {}) @@ -325,7 +320,7 @@ class UserNotifications < ActionMailer::Base protected def user_locale(user) - user.effective_locale + (user.locale.present? && I18n.available_locales.include?(user.locale.to_sym)) ? user.locale : nil end def email_post_markdown(post, add_posted_by = false) From 83e1315e42900b4052522bf14da83cf6985aae01 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 5 Sep 2018 15:57:42 +1000 Subject: [PATCH 068/124] FIX: correct urls in uploads table to point at dualstack Last week we added support for dual stack urls but did not remap the the old records in the uploads and optimized images table This caused a few minor edge cases worst was that if you rebaked old images S3 CDN was not repopulated. --- .../onceoff/correct_missing_dualstack_urls.rb | 26 ++++++++ .../correct_missing_dualstack_urls_spec.rb | 65 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 app/jobs/onceoff/correct_missing_dualstack_urls.rb create mode 100644 spec/jobs/correct_missing_dualstack_urls_spec.rb diff --git a/app/jobs/onceoff/correct_missing_dualstack_urls.rb b/app/jobs/onceoff/correct_missing_dualstack_urls.rb new file mode 100644 index 0000000000..f60e1c73ab --- /dev/null +++ b/app/jobs/onceoff/correct_missing_dualstack_urls.rb @@ -0,0 +1,26 @@ +module Jobs + class CorrectMissingDualstackUrls < Jobs::Onceoff + def execute_onceoff(args) + # s3 now uses dualstack urls, keep them around correctly + # in both uploads and optimized_image tables + base_url = Discourse.store.absolute_base_url + + return if !base_url.match?(/s3\.dualstack/) + + old = base_url.sub('.dualstack', '') + old_like = %"#{old}%" + + DB.exec(<<~SQL, from: old, to: base_url, old_like: old_like) + UPDATE uploads + SET url = replace(url, :from, :to) + WHERE url ilike :old_like + SQL + + DB.exec(<<~SQL, from: old, to: base_url, old_like: old_like) + UPDATE optimized_images + SET url = replace(url, :from, :to) + WHERE url ilike :old_like + SQL + end + end +end diff --git a/spec/jobs/correct_missing_dualstack_urls_spec.rb b/spec/jobs/correct_missing_dualstack_urls_spec.rb new file mode 100644 index 0000000000..493aaf6476 --- /dev/null +++ b/spec/jobs/correct_missing_dualstack_urls_spec.rb @@ -0,0 +1,65 @@ +require 'rails_helper' + +require_dependency 'jobs/onceoff/correct_missing_dualstack_urls' + +describe Jobs::CorrectMissingDualstackUrls do + + it 'corrects the urls' do + + SiteSetting.s3_upload_bucket = "s3-upload-bucket" + SiteSetting.s3_access_key_id = "s3-access-key-id" + SiteSetting.s3_secret_access_key = "s3-secret-access-key" + SiteSetting.enable_s3_uploads = true + + # we will only correct for our base_url, random urls will be left alone + expect(Discourse.store.absolute_base_url).to eq('//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com') + + current_upload = Upload.create!( + url: '//s3-upload-bucket.s3.us-east-1.amazonaws.com/somewhere/a.png', + original_filename: 'a.png', + filesize: 100, + user_id: -1, + ) + + bad_upload = Upload.create!( + url: '//s3-upload-bucket.s3.us-west-1.amazonaws.com/somewhere/a.png', + original_filename: 'a.png', + filesize: 100, + user_id: -1, + ) + + current_optimized = OptimizedImage.create!( + url: '//s3-upload-bucket.s3.us-east-1.amazonaws.com/somewhere/a.png', + filesize: 100, + upload_id: current_upload.id, + width: 100, + height: 100, + sha1: 'xxx', + extension: '.png' + ) + + bad_optimized = OptimizedImage.create!( + url: '//s3-upload-bucket.s3.us-west-1.amazonaws.com/somewhere/a.png', + filesize: 100, + upload_id: current_upload.id, + width: 110, + height: 100, + sha1: 'xxx', + extension: '.png' + ) + + Jobs::CorrectMissingDualstackUrls.new.execute_onceoff(nil) + + bad_upload.reload + expect(bad_upload.url).to eq('//s3-upload-bucket.s3.us-west-1.amazonaws.com/somewhere/a.png') + + current_upload.reload + expect(current_upload.url).to eq('//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com/somewhere/a.png') + + bad_optimized.reload + expect(bad_optimized.url).to eq('//s3-upload-bucket.s3.us-west-1.amazonaws.com/somewhere/a.png') + + current_optimized.reload + expect(current_optimized.url).to eq('//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com/somewhere/a.png') + end +end From d9c0dc8687032b52bca917e316a7d6e194e01f58 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 5 Sep 2018 16:11:44 +1000 Subject: [PATCH 069/124] correct prev commit s3. did not exists it is s3- --- app/jobs/onceoff/correct_missing_dualstack_urls.rb | 2 +- spec/jobs/correct_missing_dualstack_urls_spec.rb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/jobs/onceoff/correct_missing_dualstack_urls.rb b/app/jobs/onceoff/correct_missing_dualstack_urls.rb index f60e1c73ab..5634db3bec 100644 --- a/app/jobs/onceoff/correct_missing_dualstack_urls.rb +++ b/app/jobs/onceoff/correct_missing_dualstack_urls.rb @@ -7,7 +7,7 @@ module Jobs return if !base_url.match?(/s3\.dualstack/) - old = base_url.sub('.dualstack', '') + old = base_url.sub('s3.dualstack.', 's3-') old_like = %"#{old}%" DB.exec(<<~SQL, from: old, to: base_url, old_like: old_like) diff --git a/spec/jobs/correct_missing_dualstack_urls_spec.rb b/spec/jobs/correct_missing_dualstack_urls_spec.rb index 493aaf6476..329678c21e 100644 --- a/spec/jobs/correct_missing_dualstack_urls_spec.rb +++ b/spec/jobs/correct_missing_dualstack_urls_spec.rb @@ -15,21 +15,21 @@ describe Jobs::CorrectMissingDualstackUrls do expect(Discourse.store.absolute_base_url).to eq('//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com') current_upload = Upload.create!( - url: '//s3-upload-bucket.s3.us-east-1.amazonaws.com/somewhere/a.png', + url: '//s3-upload-bucket.s3-us-east-1.amazonaws.com/somewhere/a.png', original_filename: 'a.png', filesize: 100, user_id: -1, ) bad_upload = Upload.create!( - url: '//s3-upload-bucket.s3.us-west-1.amazonaws.com/somewhere/a.png', + url: '//s3-upload-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png', original_filename: 'a.png', filesize: 100, user_id: -1, ) current_optimized = OptimizedImage.create!( - url: '//s3-upload-bucket.s3.us-east-1.amazonaws.com/somewhere/a.png', + url: '//s3-upload-bucket.s3-us-east-1.amazonaws.com/somewhere/a.png', filesize: 100, upload_id: current_upload.id, width: 100, @@ -39,7 +39,7 @@ describe Jobs::CorrectMissingDualstackUrls do ) bad_optimized = OptimizedImage.create!( - url: '//s3-upload-bucket.s3.us-west-1.amazonaws.com/somewhere/a.png', + url: '//s3-upload-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png', filesize: 100, upload_id: current_upload.id, width: 110, @@ -51,13 +51,13 @@ describe Jobs::CorrectMissingDualstackUrls do Jobs::CorrectMissingDualstackUrls.new.execute_onceoff(nil) bad_upload.reload - expect(bad_upload.url).to eq('//s3-upload-bucket.s3.us-west-1.amazonaws.com/somewhere/a.png') + expect(bad_upload.url).to eq('//s3-upload-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png') current_upload.reload expect(current_upload.url).to eq('//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com/somewhere/a.png') bad_optimized.reload - expect(bad_optimized.url).to eq('//s3-upload-bucket.s3.us-west-1.amazonaws.com/somewhere/a.png') + expect(bad_optimized.url).to eq('//s3-upload-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png') current_optimized.reload expect(current_optimized.url).to eq('//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com/somewhere/a.png') From 72834f19ff448ce6d0012d0ddbbbd61d9a43ac37 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 5 Sep 2018 16:54:15 +0800 Subject: [PATCH 070/124] DEV: Add rake tasks to list posts with broken images. --- lib/tasks/uploads.rake | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 660a0484a8..0cf836dfab 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -715,3 +715,38 @@ task "uploads:fix_incorrect_extensions" => :environment do require_dependency "upload_fixer" UploadFixer.fix_all_extensions end + +task "uploads:list_posts_with_broken_images" => :environment do + if ENV["RAILS_DB"] + list_broken_posts + else + RailsMultisite::ConnectionManagement.each_connection do |db| + list_broken_posts + end + end +end + +def list_broken_posts + Post.where("raw LIKE '%upload:\/\/%'").find_each do |post| + begin + begin + analyzer = PostAnalyzer.new(post.raw, post.topic_id) + cooked_stripped = analyzer.send(:cooked_stripped) + end + + cooked_stripped.css("img").each do |img| + if dom_class = img["class"] + if (Post.white_listed_image_classes & dom_class.split).count > 0 + next + end + end + + if img["data-orig-src"] + puts "#{post.full_url} #{img["data-orig-src"]}" + end + end + rescue => e + puts "#{post.full_url} Error: #{e.message}" + end + end +end From 2c5d9269a0e9e9db3f2429d9e2e3f6cea6d03e51 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 11:44:26 +0200 Subject: [PATCH 071/124] FIX: Notifications shouldn't use user locale unless allow_user_locale is enabled --- app/mailers/user_notifications.rb | 11 ++++++--- spec/mailers/user_notifications_spec.rb | 33 ++++++++++--------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index cde42b3ee6..76be39f1de 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -20,10 +20,15 @@ class UserNotifications < ActionMailer::Base end def signup_after_approval(user, opts = {}) + locale = user_locale(user) + tips = I18n.t('system_messages.usage_tips.text_body_template', + base_url: Discourse.base_url, + locale: locale) + build_email(user.email, template: 'user_notifications.signup_after_approval', - locale: user_locale(user), - new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url, locale: locale)) + locale: locale, + new_user_tips: tips) end def notify_old_email(user, opts = {}) @@ -320,7 +325,7 @@ class UserNotifications < ActionMailer::Base protected def user_locale(user) - (user.locale.present? && I18n.available_locales.include?(user.locale.to_sym)) ? user.locale : nil + user.effective_locale end def email_post_markdown(post, add_posted_by = false) diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index a0400e5d33..bd56dbd542 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -802,12 +802,15 @@ describe UserNotifications do describe "notifications from template" do - context "user locale has been set" do + context "user locale is allowed" do + before do + SiteSetting.default_locale = "en" + SiteSetting.allow_user_locale = true + end %w(signup signup_after_approval confirm_old_email notify_old_email confirm_new_email forgot_password admin_login account_created).each do |mail_type| include_examples "notification derived from template" do - SiteSetting.default_locale = "en" let(:locale) { "fr" } let(:mail_type) { mail_type } it "sets the locale" do @@ -817,29 +820,19 @@ describe UserNotifications do end end - context "user locale has not been set" do + context "user locale is not allowed" do + before do + SiteSetting.default_locale = "en" + SiteSetting.allow_user_locale = false + end + %w(signup signup_after_approval notify_old_email confirm_old_email confirm_new_email forgot_password admin_login account_created).each do |mail_type| include_examples "notification derived from template" do - SiteSetting.default_locale = "en" - let(:locale) { nil } + let(:locale) { "fr" } let(:mail_type) { mail_type } it "sets the locale" do - expects_build_with(has_entry(:locale, nil)) - end - end - end - end - - context "user locale is an empty string" do - %w(signup signup_after_approval notify_old_email confirm_new_email confirm_old_email - forgot_password admin_login account_created).each do |mail_type| - include_examples "notification derived from template" do - SiteSetting.default_locale = "en" - let(:locale) { "" } - let(:mail_type) { mail_type } - it "sets the locale" do - expects_build_with(has_entry(:locale, nil)) + expects_build_with(has_entry(:locale, "en")) end end end From 2801376df55400309fa7fa4063eedf1c666c0e40 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 14:36:30 +0200 Subject: [PATCH 072/124] FIX: Wizard didn't load translations correctly * Translations from the js.* namespace were not found, because the i18n-patches were not loaded. * The extra-locales didn't use a hash in the URL. --- app/assets/javascripts/wizard-application.js | 1 + app/views/wizard/index.html.erb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/wizard-application.js b/app/assets/javascripts/wizard-application.js index ef61997351..d5460545ae 100644 --- a/app/assets/javascripts/wizard-application.js +++ b/app/assets/javascripts/wizard-application.js @@ -3,6 +3,7 @@ //= require ./ember-addons/macro-alias //= require ./ember-addons/ember-computed-decorators //= require_tree ./discourse-common +//= require i18n-patches //= require_tree ./select-kit //= require wizard/router //= require wizard/wizard diff --git a/app/views/wizard/index.html.erb b/app/views/wizard/index.html.erb index cecca0e834..74092def2e 100644 --- a/app/views/wizard/index.html.erb +++ b/app/views/wizard/index.html.erb @@ -2,11 +2,11 @@ <%= discourse_stylesheet_link_tag :wizard, theme_ids: nil %> <%= preload_script 'ember_jquery' %> + <%= preload_script "locales/#{I18n.locale}" %> <%= preload_script 'wizard-vendor' %> <%= preload_script 'wizard-application' %> - <%= preload_script "locales/#{I18n.locale}" %> <%= render partial: "common/special_font_face" %> - + <%= csrf_meta_tags %> From 3134dd47633200df7bb7eebb7c719c29cc378e3f Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 14:40:38 +0200 Subject: [PATCH 073/124] FIX: Wizard didn't change locale when Enter key was used in drop-down --- .../wizard/components/wizard-field-dropdown.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/assets/javascripts/wizard/components/wizard-field-dropdown.js.es6 diff --git a/app/assets/javascripts/wizard/components/wizard-field-dropdown.js.es6 b/app/assets/javascripts/wizard/components/wizard-field-dropdown.js.es6 new file mode 100644 index 0000000000..3747a92bca --- /dev/null +++ b/app/assets/javascripts/wizard/components/wizard-field-dropdown.js.es6 @@ -0,0 +1,5 @@ +export default Ember.Component.extend({ + keyPress(e) { + e.stopPropagation(); + } +}); From f3aef2cc83905a976fa9bf7439bdf0a9d911206f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 5 Sep 2018 21:46:43 +0800 Subject: [PATCH 074/124] FIX: Incorrect/missing extension in short_url fails to map to upload. `Hash#invert` causes us to lose keys if the hash contains similar values. --- lib/pretty_text/helpers.rb | 11 ++++++++--- spec/components/pretty_text_spec.rb | 3 +++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index b8376a731c..09d7b1a31b 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -63,13 +63,18 @@ module PrettyText end if map.length > 0 - reverse_map = map.invert + reverse_map = {} + + map.each do |key, value| + reverse_map[value] ||= [] + reverse_map[value] << key + end Upload.where(sha1: map.values).pluck(:sha1, :url).each do |row| sha1, url = row - if short_url = reverse_map[sha1] - result[short_url] = url + if short_urls = reverse_map[sha1] + short_urls.each { |short_url| result[short_url] = url } end end end diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index fe0a6686a0..d115a69bff 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -1228,6 +1228,8 @@ HTML - test - ![upload](#{upload.short_url}) + + ![upload](#{upload.short_url.gsub!(".png", "")}) RAW cooked = <<~HTML @@ -1243,6 +1245,7 @@ HTML +

upload

HTML expect(PrettyText.cook(raw)).to eq(cooked.strip) From 17087eff2a0c02b8108ccee0f6af42ed5ad6100e Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 5 Sep 2018 17:18:52 +0200 Subject: [PATCH 075/124] FIX: Reset tags on category change (#6363) --- .../discourse/controllers/composer.js.es6 | 6 +++ .../discourse/lib/plugin-api.js.es6 | 2 +- .../discourse/templates/composer.hbs | 8 +++- .../select-kit/components/multi-select.js.es6 | 10 ++++- .../components/single-select.js.es6 | 10 ++++- .../select-kit/mixins/plugin-api.js.es6 | 20 ++++++++++ .../category-chooser-with-tags-test.js.es6 | 39 +++++++++++++++++++ .../components/single-select-test.js.es6 | 26 +++++++++++++ 8 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 test/javascripts/acceptance/category-chooser-with-tags-test.js.es6 diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 1a6bf47dd0..009fd35a8d 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -297,6 +297,12 @@ export default Ember.Controller.extend({ uploadIcon: () => uploadIcon(), actions: { + resetTagsSelection() { + if (this.get("model.tags")) { + this.set("model.tags", []); + } + }, + cancelUpload() { this.set("model.uploadCancelled", true); }, diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 7f3b9e4389..e544c80ed9 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -38,7 +38,7 @@ import Sharing from "discourse/lib/sharing"; import { addComposerUploadHandler } from "discourse/components/composer-editor"; // If you add any methods to the API ensure you bump up this number -const PLUGIN_API_VERSION = "0.8.24"; +const PLUGIN_API_VERSION = "0.8.25"; class PluginApi { constructor(version, container) { diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 94e8d44b64..7c75f0ba03 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -60,7 +60,13 @@ {{#if model.showCategoryChooser}}
- {{category-chooser fullWidthOnMobile=true value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}} + {{category-chooser + fullWidthOnMobile=true + value=model.categoryId + scopedCategoryId=scopedCategoryId + onSelect=(action "resetTagsSelection") + onSelectNone=(action "resetTagsSelection") + tabindex="3"}} {{popup-input-tip validation=categoryValidation}}
{{/if}} diff --git a/app/assets/javascripts/select-kit/components/multi-select.js.es6 b/app/assets/javascripts/select-kit/components/multi-select.js.es6 index 86d8fcd22a..b2a71ebf88 100644 --- a/app/assets/javascripts/select-kit/components/multi-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select.js.es6 @@ -2,7 +2,10 @@ import SelectKitComponent from "select-kit/components/select-kit"; import computed from "ember-addons/ember-computed-decorators"; import { on } from "ember-addons/ember-computed-decorators"; const { get, isNone, isEmpty, makeArray, run } = Ember; -import { applyOnSelectPluginApiCallbacks } from "select-kit/mixins/plugin-api"; +import { + applyOnSelectPluginApiCallbacks, + applyOnSelectNonePluginApiCallbacks +} from "select-kit/mixins/plugin-api"; export default SelectKitComponent.extend({ pluginApiIdentifiers: ["multi-select"], @@ -253,6 +256,11 @@ export default SelectKitComponent.extend({ !computedContentItem || computedContentItem.__sk_row_type === "noneRow" ) { + applyOnSelectNonePluginApiCallbacks( + this.get("pluginApiIdentifiers"), + this + ); + this._boundaryActionHandler("onSelectNone"); this.clearSelection(); return; } diff --git a/app/assets/javascripts/select-kit/components/single-select.js.es6 b/app/assets/javascripts/select-kit/components/single-select.js.es6 index 8be8102fce..7713d51194 100644 --- a/app/assets/javascripts/select-kit/components/single-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/single-select.js.es6 @@ -5,7 +5,10 @@ import { } from "ember-addons/ember-computed-decorators"; const { get, isNone, isEmpty, isPresent, run, makeArray } = Ember; -import { applyOnSelectPluginApiCallbacks } from "select-kit/mixins/plugin-api"; +import { + applyOnSelectPluginApiCallbacks, + applyOnSelectNonePluginApiCallbacks +} from "select-kit/mixins/plugin-api"; export default SelectKitComponent.extend({ pluginApiIdentifiers: ["single-select"], @@ -211,6 +214,11 @@ export default SelectKitComponent.extend({ !computedContentItem || computedContentItem.__sk_row_type === "noneRow" ) { + applyOnSelectNonePluginApiCallbacks( + this.get("pluginApiIdentifiers"), + this + ); + this._boundaryActionHandler("onSelectNone"); this.clearSelection(); return; } diff --git a/app/assets/javascripts/select-kit/mixins/plugin-api.js.es6 b/app/assets/javascripts/select-kit/mixins/plugin-api.js.es6 index b34f88c51a..daa1e362a7 100644 --- a/app/assets/javascripts/select-kit/mixins/plugin-api.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/plugin-api.js.es6 @@ -47,6 +47,15 @@ function modifyCollectionHeader(pluginApiIdentifiers, contentFunction) { _modifyCollectionHeaderCallbacks[pluginApiIdentifiers].push(contentFunction); } +let _onSelectNoneCallbacks = {}; +function onSelectNone(pluginApiIdentifiers, mutationFunction) { + if (Ember.isNone(_onSelectNoneCallbacks[pluginApiIdentifiers])) { + _onSelectNoneCallbacks[pluginApiIdentifiers] = []; + } + + _onSelectNoneCallbacks[pluginApiIdentifiers].push(mutationFunction); +} + let _onSelectCallbacks = {}; function onSelect(pluginApiIdentifiers, mutationFunction) { if (Ember.isNone(_onSelectCallbacks[pluginApiIdentifiers])) { @@ -102,6 +111,12 @@ export function applyOnSelectPluginApiCallbacks(identifiers, val, context) { }); } +export function applyOnSelectNonePluginApiCallbacks(identifiers, context) { + identifiers.forEach(key => { + (_onSelectNoneCallbacks[key] || []).forEach(c => c(context)); + }); +} + export function modifySelectKit(pluginApiIdentifiers) { return { appendContent: content => { @@ -131,6 +146,10 @@ export function modifySelectKit(pluginApiIdentifiers) { onSelect: callback => { onSelect(pluginApiIdentifiers, callback); return modifySelectKit(pluginApiIdentifiers); + }, + onSelectNone: callback => { + onSelectNone(pluginApiIdentifiers, callback); + return modifySelectKit(pluginApiIdentifiers); } }; } @@ -142,6 +161,7 @@ export function clearCallbacks() { _modifyHeaderComputedContentCallbacks = {}; _modifyCollectionHeaderCallbacks = {}; _onSelectCallbacks = {}; + _onSelectNoneCallbacks = {}; } const EMPTY_ARRAY = Object.freeze([]); diff --git a/test/javascripts/acceptance/category-chooser-with-tags-test.js.es6 b/test/javascripts/acceptance/category-chooser-with-tags-test.js.es6 new file mode 100644 index 0000000000..234eb2d856 --- /dev/null +++ b/test/javascripts/acceptance/category-chooser-with-tags-test.js.es6 @@ -0,0 +1,39 @@ +import { acceptance } from "helpers/qunit-helpers"; + +acceptance("CategoryChooser - with tags", { + loggedIn: true, + site: { can_tag_topics: true }, + settings: { + tagging_enabled: true, + allow_uncategorized_topics: false + } +}); + +QUnit.test("resets tags when changing category", async assert => { + const categoryChooser = selectKit(".category-chooser"); + const miniTagChooser = selectKit(".mini-tag-chooser"); + const findSelected = () => + find(".mini-tag-chooser .mini-tag-chooser-header .selected-name").text(); + + await visit("/"); + await click("#create-topic"); + await miniTagChooser.expand(); + await miniTagChooser.selectRowByValue("monkey"); + + assert.equal(findSelected(), "monkey"); + + await categoryChooser.expand(); + await categoryChooser.selectRowByValue(6); + + assert.equal(findSelected(), "optional tags"); + + await miniTagChooser.expand(); + await miniTagChooser.selectRowByValue("monkey"); + + assert.equal(findSelected(), "monkey"); + + await categoryChooser.expand(); + await categoryChooser.selectNoneRow(); + + assert.equal(findSelected(), "optional tags"); +}); diff --git a/test/javascripts/components/single-select-test.js.es6 b/test/javascripts/components/single-select-test.js.es6 index 056c963400..7e6c6158a4 100644 --- a/test/javascripts/components/single-select-test.js.es6 +++ b/test/javascripts/components/single-select-test.js.es6 @@ -501,6 +501,32 @@ componentTest("support modifying on select behavior through plugin api", { } }); +componentTest("support modifying on select none behavior through plugin api", { + template: + '{{single-select none="none" content=content}}', + + beforeEach() { + withPluginApi("0.8.25", api => { + api.modifySelectKit("select-kit").onSelectNone(() => { + find(".on-select-none-test").html("NONE"); + }); + }); + + this.set("content", [{ id: "1", name: "robin" }]); + }, + + async test(assert) { + await this.get("subject").expand(); + await this.get("subject").selectRowByValue(1); + await this.get("subject").expand(); + await this.get("subject").selectNoneRow(); + + assert.equal(find(".on-select-none-test").html(), "NONE"); + + clearCallbacks(); + } +}); + componentTest("with nameChanges", { template: "{{single-select content=content nameChanges=true}}", From 1c65969bb4e656d62f3e7572fa0f93f34f958b05 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 5 Sep 2018 13:19:36 -0400 Subject: [PATCH 076/124] post read-state icon alignment --- app/assets/stylesheets/desktop/topic-post.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 1e173a419c..ae5393f5eb 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -870,11 +870,11 @@ span.highlighted { } .read-state { - color: $tertiary-medium; - // We use absolute positioning here because we want it to display in the padding position: absolute; + // We use absolute positioning here because we want it to display in the padding + align-self: center; + color: $tertiary-medium; right: 0; - top: 2em; font-size: 0.571em; } From 26082688d1c01aad8174cccc36cddeda96aa6978 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 5 Sep 2018 20:43:05 +0200 Subject: [PATCH 077/124] FIX: Zero is a valid value for the page parameter --- lib/topic_query.rb | 2 +- spec/requests/list_controller_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/topic_query.rb b/lib/topic_query.rb index f5cc2b4597..202d344470 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -35,7 +35,7 @@ class TopicQuery { max_posts: zero_up_to_max_int, min_posts: zero_up_to_max_int, - page: one_up_to_max_int, + page: zero_up_to_max_int, exclude_category_ids: array_int_or_int } end diff --git a/spec/requests/list_controller_spec.rb b/spec/requests/list_controller_spec.rb index 86d276af7c..e494bebf57 100644 --- a/spec/requests/list_controller_spec.rb +++ b/spec/requests/list_controller_spec.rb @@ -30,9 +30,6 @@ RSpec.describe ListController do get "/latest?page=-1" expect(response.status).to eq(400) - get "/latest?page=0" - expect(response.status).to eq(400) - get "/latest?page=2147483648" expect(response.status).to eq(400) @@ -53,6 +50,9 @@ RSpec.describe ListController do get "/latest.json?min_posts=0" expect(response.status).to eq(200) + get "/latest?page=0" + expect(response.status).to eq(200) + get "/latest?page=1" expect(response.status).to eq(200) From e59622f2ba631f9c1b6c4cce54ebd0c5ce742535 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 5 Sep 2018 23:33:29 +0200 Subject: [PATCH 078/124] FIX: deactivate chart trends for now (#6364) --- app/models/report.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/report.rb b/app/models/report.rb index dfa4a508e9..f5304184a9 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -242,7 +242,7 @@ class Report report_about report, User.real, :count_by_signup_date end - add_prev_data report, User.real, :count_by_signup_date, report.prev_start_date, report.prev_end_date + # add_prev_data report, User.real, :count_by_signup_date, report.prev_start_date, report.prev_end_date end def self.report_new_contributors(report) @@ -262,7 +262,7 @@ class Report if report.facets.include?(:prev_period) prev_period_data = User.real.count_by_first_post(report.prev_start_date, report.prev_end_date) report.prev_period = prev_period_data.sum { |k, v| v } - report.prev_data = prev_period_data.map { |k, v| { x: k, y: v } } + # report.prev_data = prev_period_data.map { |k, v| { x: k, y: v } } end data.each do |key, value| From 8cff3c9bbc7428011f7d83f5e5dd73abb684d995 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 5 Sep 2018 17:48:31 -0400 Subject: [PATCH 079/124] UX: Prevent long names from overflowing post --- app/assets/stylesheets/common/base/topic-post.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 1f706e2059..d71315f7ce 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -15,6 +15,8 @@ } .names { + flex: 1 1 auto; + overflow: hidden; span.first { font-weight: bold; } @@ -23,7 +25,6 @@ font-size: $font-0; margin-right: 8px; display: inline-block; - max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; From 5bdc00c3bee4e0a49d1baa4347d8057860212ee4 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 6 Sep 2018 10:34:58 +1000 Subject: [PATCH 080/124] FIX: do not automatically route all actions to hovered posts This feature (hitting d when a post is hovered with mouse deletes) causes a lot of confusion and is very risky. --- .../javascripts/discourse/lib/keyboard-shortcuts.js.es6 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index 37d6db132f..01b9e3372e 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -36,6 +36,9 @@ const bindings = { "command+up": { handler: "goToFirstPost", anonymous: true }, j: { handler: "selectDown", anonymous: true }, k: { handler: "selectUp", anonymous: true }, + // we use this odd routing here vs a postAction: cause like + // has an animation so the widget handles that + // TODO: teach controller how to trigger the widget animation l: { click: ".topic-post.selected button.toggle-like" }, "m m": { handler: "setTrackingToMuted" }, // mark topic as muted "m r": { handler: "setTrackingToRegular" }, // mark topic as regular @@ -301,10 +304,6 @@ export default { $(".topic-post.selected article.boxed").data("post-id"), 10 ); - if (!selectedPostId) { - // If no post was selected, automatically select the hovered post. - selectedPostId = parseInt($("article.boxed:hover").data("post-id"), 10); - } if (selectedPostId) { const topicController = container.lookup("controller:topic"); const post = topicController From 5baecffb0d723f57b2b2fc68fcb4e6806fd1f6eb Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 5 Sep 2018 19:54:45 -0700 Subject: [PATCH 081/124] improved opengraph site setting copy --- config/locales/server.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2226486931..4e2767a502 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1410,8 +1410,8 @@ en: selectable_avatars_enabled: "Force users to choose an avatar from the list." selectable_avatars: "List of avatars users can choose from." - default_opengraph_image_url: "URL of the default opengraph image." - twitter_summary_large_image_url: "URL of the default Twitter summary card image (should be at least 280px in width, and at least 150px in height)." + default_opengraph_image_url: "Default opengraph image, used when the page has no other suitable image or site logo." + twitter_summary_large_image_url: "Default Twitter summary card image (should be at least 280px in width, and at least 150px in height)." allow_all_attachments_for_group_messages: "Allow all email attachments for group messages." From 434035f167eb15f3e2d8c6a52577d66d0e140b71 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 6 Sep 2018 09:58:01 +0800 Subject: [PATCH 082/124] FIX: Link post to uploads in `PostCreator`. * This ensures that uploads are linked to their post on creation instead of a background job which may be delayed if Sidekiq is facing difficulties. --- app/models/post.rb | 35 +++++++++- lib/cooked_post_processor.rb | 42 +++++------ lib/post_creator.rb | 12 +++- lib/post_jobs_enqueuer.rb | 4 +- script/import_scripts/lithium.rb | 2 +- spec/components/cooked_post_processor_spec.rb | 61 ++++------------ spec/components/post_creator_spec.rb | 23 ++++-- spec/models/post_spec.rb | 70 +++++++++++++++++++ 8 files changed, 165 insertions(+), 84 deletions(-) diff --git a/app/models/post.rb b/app/models/post.rb index 2a74e880c2..293bed1a29 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -536,7 +536,7 @@ class Post < ActiveRecord::Base QuotedPost.extract_from(self) # make sure we trigger the post process - trigger_post_process(true) + trigger_post_process(bypass_bump: true) publish_change_to_clients!(:rebaked) @@ -650,10 +650,11 @@ class Post < ActiveRecord::Base end # Enqueue post processing for this post - def trigger_post_process(bypass_bump = false) + def trigger_post_process(bypass_bump: false, skip_link_post_uploads: false) args = { post_id: id, - bypass_bump: bypass_bump + bypass_bump: bypass_bump, + skip_link_post_uploads: skip_link_post_uploads } args[:image_sizes] = image_sizes if image_sizes.present? args[:invalidate_oneboxes] = true if invalidate_oneboxes.present? @@ -784,6 +785,34 @@ class Post < ActiveRecord::Base locked_by_id.present? end + def link_post_uploads(fragments: nil) + upload_ids = [] + fragments ||= Nokogiri::HTML::fragment(self.cooked) + + fragments.css("a/@href", "img/@src").each do |media| + if upload = Upload.get_from_url(media.value) + upload_ids << upload.id + end + end + + upload_ids |= Upload.where(id: downloaded_images.values).pluck(:id) + values = upload_ids.map! { |upload_id| "(#{self.id},#{upload_id})" }.join(",") + + PostUpload.transaction do + PostUpload.where(post_id: self.id).delete_all + + if values.size > 0 + DB.exec("INSERT INTO post_uploads (post_id, upload_id) VALUES #{values}") + end + end + end + + def downloaded_images + JSON.parse(self.custom_fields[Post::DOWNLOADED_IMAGES].presence || "{}") + rescue JSON::ParserError + {} + end + private def parse_quote_into_arguments(quote) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 8daef42e89..467a7fbc56 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -35,7 +35,11 @@ class CookedPostProcessor post_process_oneboxes post_process_images post_process_quotes - keep_reverse_index_up_to_date + + unless @opts[:skip_link_post_uploads] + @post.link_post_uploads(fragments: @doc) + end + optimize_urls update_post_image enforce_nofollow @@ -58,26 +62,6 @@ class CookedPostProcessor BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id) if @post.is_reply_by_email? end - def keep_reverse_index_up_to_date - upload_ids = [] - - @doc.css("a/@href", "img/@src").each do |media| - if upload = Upload.get_from_url(media.value) - upload_ids << upload.id - end - end - - upload_ids |= downloaded_images.values.select { |id| Upload.exists?(id) } - - values = upload_ids.map { |u| "(#{@post.id},#{u})" }.join(",") - PostUpload.transaction do - PostUpload.where(post_id: @post.id).delete_all - if upload_ids.size > 0 - DB.exec("INSERT INTO post_uploads (post_id, upload_id) VALUES #{values}") - end - end - end - def post_process_images extract_images.each do |img| src = img["src"].sub(/^https?:/i, "") @@ -159,15 +143,25 @@ class CookedPostProcessor end def large_images - @large_images ||= JSON.parse(@post.custom_fields[Post::LARGE_IMAGES].presence || "[]") rescue [] + @large_images ||= + begin + JSON.parse(@post.custom_fields[Post::LARGE_IMAGES].presence || "[]") + rescue JSON::ParserError + [] + end end def broken_images - @broken_images ||= JSON.parse(@post.custom_fields[Post::BROKEN_IMAGES].presence || "[]") rescue [] + @broken_images ||= + begin + JSON.parse(@post.custom_fields[Post::BROKEN_IMAGES].presence || "[]") + rescue JSON::ParserError + [] + end end def downloaded_images - @downloaded_images ||= JSON.parse(@post.custom_fields[Post::DOWNLOADED_IMAGES].presence || "{}") rescue {} + @downloaded_images ||= @post.downloaded_images end def extract_images diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 80f890ddba..9cfe9c2fcc 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -173,6 +173,7 @@ class PostCreator update_topic_auto_close update_user_counts create_embedded_topic + link_post_uploads ensure_in_allowed_users if guardian.is_staff? unarchive_message @@ -188,7 +189,7 @@ class PostCreator publish track_latest_on_category - enqueue_jobs unless @opts[:skip_jobs] + enqueue_jobs(skip_link_post_uploads: true) unless @opts[:skip_jobs] BadgeGranter.queue_badge_grant(Badge::Trigger::PostRevision, post: @post) trigger_after_events unless opts[:skip_events] @@ -213,12 +214,13 @@ class PostCreator @post end - def enqueue_jobs + def enqueue_jobs(skip_link_post_uploads: false) return unless @post && !@post.errors.present? PostJobsEnqueuer.new(@post, @topic, new_topic?, import_mode: @opts[:import_mode], - post_alert_options: @opts[:post_alert_options] + post_alert_options: @opts[:post_alert_options], + skip_link_post_uploads: skip_link_post_uploads ).enqueue_jobs end @@ -349,6 +351,10 @@ class PostCreator rollback_from_errors!(embed) unless embed.save end + def link_post_uploads + @post.link_post_uploads + end + def handle_spam if @spam GroupMessage.create(Group[:moderators].name, diff --git a/lib/post_jobs_enqueuer.rb b/lib/post_jobs_enqueuer.rb index 4dde3b73af..a0abef96c0 100644 --- a/lib/post_jobs_enqueuer.rb +++ b/lib/post_jobs_enqueuer.rb @@ -38,7 +38,9 @@ class PostJobsEnqueuer end def trigger_post_post_process - @post.trigger_post_process + @post.trigger_post_process( + skip_link_post_uploads: @opts[:skip_link_post_uploads] + ) end def after_post_create diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index 4d82412522..442df14f8a 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -886,7 +886,7 @@ SQL post.raw = new_raw post.cooked = post.cook(new_raw) cpp = CookedPostProcessor.new(post) - cpp.keep_reverse_index_up_to_date + cpp.link_post_uploads post.custom_fields["import_post_process"] = true post.save end diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 27ceeb6b63..8bf669506f 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -4,18 +4,29 @@ require "cooked_post_processor" describe CookedPostProcessor do context ".post_process" do + let(:upload) do + Fabricate(:upload, + url: '/uploads/default/original/1X/1/1234567890123456.jpg' + ) + end + + let(:post) do + Fabricate(:post, raw: <<~RAW) + + RAW + end - let(:post) { build(:post) } let(:cpp) { CookedPostProcessor.new(post) } let(:post_process) { sequence("post_process") } it "post process in sequence" do cpp.expects(:post_process_oneboxes).in_sequence(post_process) cpp.expects(:post_process_images).in_sequence(post_process) - cpp.expects(:keep_reverse_index_up_to_date).in_sequence(post_process) cpp.expects(:optimize_urls).in_sequence(post_process) cpp.expects(:pull_hotlinked_images).in_sequence(post_process) cpp.post_process + + expect(PostUpload.exists?(post: post, upload: upload)).to eq(true) end end @@ -40,52 +51,6 @@ describe CookedPostProcessor do end end - context ".keep_reverse_index_up_to_date" do - let(:video_upload) { Fabricate(:upload, url: '/uploads/default/original/1X/1/1234567890123456.mp4') } - let(:image_upload) { Fabricate(:upload, url: '/uploads/default/original/1X/1/1234567890123456.jpg') } - let(:audio_upload) { Fabricate(:upload, url: '/uploads/default/original/1X/1/1234567890123456.ogg') } - let(:attachment_upload) { Fabricate(:upload, url: '/uploads/default/original/1X/1/1234567890123456.csv') } - - let(:raw) do - <<~RAW - Link - - - - - - RAW - end - - let(:post) { Fabricate(:post, raw: raw) } - let(:cpp) { CookedPostProcessor.new(post) } - - it "finds all the uploads in the post" do - cpp.keep_reverse_index_up_to_date - - expect(PostUpload.where(post: post).map(&:upload_id).sort).to eq( - [video_upload.id, image_upload.id, audio_upload.id, attachment_upload.id].sort - ) - end - - it "cleans the reverse index up for the current post" do - cpp.keep_reverse_index_up_to_date - - post_uploads_ids = post.post_uploads.pluck(:id) - - cpp.keep_reverse_index_up_to_date - - expect(post.reload.post_uploads.pluck(:id)).to_not eq(post_uploads_ids) - end - - end - context ".post_process_images" do shared_examples "leave dimensions alone" do diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 7499b6a276..ecb30681df 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -186,11 +186,26 @@ describe PostCreator do end it 'queues up post processing job when saved' do - Jobs.expects(:enqueue).with(:feature_topic_users, has_key(:topic_id)) - Jobs.expects(:enqueue).with(:process_post, has_key(:post_id)) - Jobs.expects(:enqueue).with(:post_alert, has_key(:post_id)) - Jobs.expects(:enqueue).with(:notify_mailing_list_subscribers, has_key(:post_id)) creator.create + + post = Post.last + post_id = post.id + topic_id = post.topic_id + + process_post_args = Jobs::ProcessPost.jobs.first["args"].first + expect(process_post_args["skip_link_post_uploads"]).to eq(true) + expect(process_post_args["post_id"]).to eq(post_id) + + feature_topic_users_args = Jobs::FeatureTopicUsers.jobs.first["args"].first + expect(feature_topic_users_args["topic_id"]).to eq(topic_id) + + post_alert_args = Jobs::PostAlert.jobs.first["args"].first + expect(post_alert_args["post_id"]).to eq(post_id) + + notify_mailing_list_subscribers_args = + Jobs::NotifyMailingListSubscribers.jobs.first["args"].first + + expect(notify_mailing_list_subscribers_args["post_id"]).to eq(post_id) end it 'passes the invalidate_oneboxes along to the job if present' do diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 1c43e0a00a..0467843dd7 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -1153,4 +1153,74 @@ describe Post do expect(post.revisions.pluck(:number)).to eq([1, 2]) end + describe '#link_post_uploads' do + let(:video_upload) do + Fabricate(:upload, + url: '/uploads/default/original/1X/1/1234567890123456.mp4' + ) + end + + let(:image_upload) do + Fabricate(:upload, + url: '/uploads/default/original/1X/1/1234567890123456.jpg' + ) + end + + let(:audio_upload) do + Fabricate(:upload, + url: '/uploads/default/original/1X/1/1234567890123456.ogg' + ) + end + + let(:attachment_upload) do + Fabricate(:upload, + url: '/uploads/default/original/1X/1/1234567890123456.csv' + ) + end + + let(:raw) do + <<~RAW + Link + + + + + + RAW + end + + let(:post) { Fabricate(:post, raw: raw) } + + it "finds all the uploads in the post" do + post.custom_fields[Post::DOWNLOADED_IMAGES] = { + "/uploads/default/original/1X/1/1234567890123456.csv": attachment_upload.id + } + + post.save_custom_fields + post.link_post_uploads + + expect(PostUpload.where(post: post).pluck(:upload_id)).to contain_exactly( + video_upload.id, image_upload.id, audio_upload.id, attachment_upload.id + ) + end + + it "cleans the reverse index up for the current post" do + post.link_post_uploads + + post_uploads_ids = post.post_uploads.pluck(:id) + + post.link_post_uploads + + expect(post.reload.post_uploads.pluck(:id)).to_not contain_exactly( + post_uploads_ids + ) + end + end + end From b6a139b58182015fb895b80dd239b5be83f14a37 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 6 Sep 2018 12:41:43 +0800 Subject: [PATCH 083/124] Fix broken spec. --- app/jobs/regular/pull_hotlinked_images.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 1acefc21fc..cc9cc0593f 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -130,7 +130,7 @@ module Jobs changes = { raw: raw, edit_reason: I18n.t("upload.edit_reason") } post.revise(Discourse.system_user, changes, bypass_bump: true) elsif has_downloaded_image || has_new_large_image || has_new_broken_image - post.trigger_post_process(true) + post.trigger_post_process(bypass_bump: true) end end From d4b05d7bc5ec85c6f3841bb35df2c2d2c9836a16 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 6 Sep 2018 14:08:03 +0800 Subject: [PATCH 084/124] Always link post to uploads in post process. The operation is cheap anyway so no point skipping. --- app/models/post.rb | 3 +-- lib/cooked_post_processor.rb | 6 +----- lib/post_creator.rb | 7 +++---- lib/post_jobs_enqueuer.rb | 4 +--- spec/components/post_creator_spec.rb | 1 - 5 files changed, 6 insertions(+), 15 deletions(-) diff --git a/app/models/post.rb b/app/models/post.rb index 293bed1a29..92ed377312 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -650,11 +650,10 @@ class Post < ActiveRecord::Base end # Enqueue post processing for this post - def trigger_post_process(bypass_bump: false, skip_link_post_uploads: false) + def trigger_post_process(bypass_bump: false) args = { post_id: id, bypass_bump: bypass_bump, - skip_link_post_uploads: skip_link_post_uploads } args[:image_sizes] = image_sizes if image_sizes.present? args[:invalidate_oneboxes] = true if invalidate_oneboxes.present? diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 467a7fbc56..c64edce285 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -35,16 +35,12 @@ class CookedPostProcessor post_process_oneboxes post_process_images post_process_quotes - - unless @opts[:skip_link_post_uploads] - @post.link_post_uploads(fragments: @doc) - end - optimize_urls update_post_image enforce_nofollow pull_hotlinked_images(bypass_bump) grant_badges + @post.link_post_uploads(fragments: @doc) DiscourseEvent.trigger(:post_process_cooked, @doc, @post) nil end diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 9cfe9c2fcc..aa6c75c72b 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -189,7 +189,7 @@ class PostCreator publish track_latest_on_category - enqueue_jobs(skip_link_post_uploads: true) unless @opts[:skip_jobs] + enqueue_jobs unless @opts[:skip_jobs] BadgeGranter.queue_badge_grant(Badge::Trigger::PostRevision, post: @post) trigger_after_events unless opts[:skip_events] @@ -214,13 +214,12 @@ class PostCreator @post end - def enqueue_jobs(skip_link_post_uploads: false) + def enqueue_jobs return unless @post && !@post.errors.present? PostJobsEnqueuer.new(@post, @topic, new_topic?, import_mode: @opts[:import_mode], - post_alert_options: @opts[:post_alert_options], - skip_link_post_uploads: skip_link_post_uploads + post_alert_options: @opts[:post_alert_options] ).enqueue_jobs end diff --git a/lib/post_jobs_enqueuer.rb b/lib/post_jobs_enqueuer.rb index a0abef96c0..4dde3b73af 100644 --- a/lib/post_jobs_enqueuer.rb +++ b/lib/post_jobs_enqueuer.rb @@ -38,9 +38,7 @@ class PostJobsEnqueuer end def trigger_post_post_process - @post.trigger_post_process( - skip_link_post_uploads: @opts[:skip_link_post_uploads] - ) + @post.trigger_post_process end def after_post_create diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index ecb30681df..2ad2736ab2 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -193,7 +193,6 @@ describe PostCreator do topic_id = post.topic_id process_post_args = Jobs::ProcessPost.jobs.first["args"].first - expect(process_post_args["skip_link_post_uploads"]).to eq(true) expect(process_post_args["post_id"]).to eq(post_id) feature_topic_users_args = Jobs::FeatureTopicUsers.jobs.first["args"].first From 1f636c445bcbc66b1765bcc3966887adcb14bdbf Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 6 Sep 2018 14:29:45 +0800 Subject: [PATCH 085/124] PERF: Add fast path to find uploads before resorting to `LIKE` query. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For a normal upload url Before ``` Warming up -------------------------------------- 264.000 i/100ms Calculating ------------------------------------- 2.754k (± 8.4%) i/s - 13.728k in 5.022066s ``` After ``` Warming up -------------------------------------- 341.000 i/100ms Calculating ------------------------------------- 3.435k (±11.6%) i/s - 17.050k in 5.045676s ``` --- app/models/upload.rb | 12 ++++++------ spec/models/upload_spec.rb | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/models/upload.rb b/app/models/upload.rb index 4ab07e4ce1..f035751098 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -175,12 +175,12 @@ class Upload < ActiveRecord::Base end return if uri&.path.blank? - - path = uri.path[/(\/original\/\dX\/[\/\.\w]+)/, 1] - - return if path.blank? - - Upload.find_by("url LIKE ?", "%#{path}") + data = uri.path.match(/(\/original\/\dX\/[\/\.\w]+\/([a-zA-Z0-9]+)[\.\w]+)/) + return if data.blank? + sha1 = data[2] + upload = nil + upload = Upload.find_by(sha1: sha1) if sha1 + upload || Upload.find_by("url LIKE ?", "%#{data[1]}") end def self.migrate_to_new_scheme(limit = nil) diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index a52af4a0bd..b19bcd3d3e 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -74,8 +74,9 @@ describe Upload do end context ".get_from_url" do - let(:url) { "/uploads/default/original/3X/1/0/10f73034616a796dfd70177dc54b6def44c4ba6f.png" } - let(:upload) { Fabricate(:upload, url: url) } + let(:sha1) { "10f73034616a796dfd70177dc54b6def44c4ba6f" } + let(:url) { "/uploads/default/original/3X/1/0/#{sha1}.png" } + let(:upload) { Fabricate(:upload, url: url, sha1: sha1) } it "works when the file has been uploaded" do expect(Upload.get_from_url(upload.url)).to eq(upload) From 56b6a4779d194983a526698905a1799fbd9b1b43 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 6 Sep 2018 17:24:12 +1000 Subject: [PATCH 086/124] FIX: make route to tag more robust There are some edge cases where code would fail here, so adding protection --- app/assets/javascripts/discourse/lib/url.js.es6 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index bbd556780b..786ed36081 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -153,7 +153,12 @@ const DiscourseURL = Ember.Object.extend({ }, routeToTag(a) { - if (a && a.host !== document.location.host) { + // skip when we are provided nowhere to route to + if (!a || !a.href) { + return false; + } + + if (a.host !== document.location.host) { document.location = a.href; return false; } From 6c1e70d5545df680ad5ccfbc2fffbe37f36199bc Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 6 Sep 2018 10:35:07 +0200 Subject: [PATCH 087/124] FIX: do no reset tags selection on category selection (#6369) We will instead implement a server side solution to this in the future. --- .../discourse/controllers/composer.js.es6 | 6 --- .../discourse/templates/composer.hbs | 2 - .../category-chooser-with-tags-test.js.es6 | 39 ------------------- 3 files changed, 47 deletions(-) delete mode 100644 test/javascripts/acceptance/category-chooser-with-tags-test.js.es6 diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 009fd35a8d..1a6bf47dd0 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -297,12 +297,6 @@ export default Ember.Controller.extend({ uploadIcon: () => uploadIcon(), actions: { - resetTagsSelection() { - if (this.get("model.tags")) { - this.set("model.tags", []); - } - }, - cancelUpload() { this.set("model.uploadCancelled", true); }, diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 7c75f0ba03..dc71db36f7 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -64,8 +64,6 @@ fullWidthOnMobile=true value=model.categoryId scopedCategoryId=scopedCategoryId - onSelect=(action "resetTagsSelection") - onSelectNone=(action "resetTagsSelection") tabindex="3"}} {{popup-input-tip validation=categoryValidation}}
diff --git a/test/javascripts/acceptance/category-chooser-with-tags-test.js.es6 b/test/javascripts/acceptance/category-chooser-with-tags-test.js.es6 deleted file mode 100644 index 234eb2d856..0000000000 --- a/test/javascripts/acceptance/category-chooser-with-tags-test.js.es6 +++ /dev/null @@ -1,39 +0,0 @@ -import { acceptance } from "helpers/qunit-helpers"; - -acceptance("CategoryChooser - with tags", { - loggedIn: true, - site: { can_tag_topics: true }, - settings: { - tagging_enabled: true, - allow_uncategorized_topics: false - } -}); - -QUnit.test("resets tags when changing category", async assert => { - const categoryChooser = selectKit(".category-chooser"); - const miniTagChooser = selectKit(".mini-tag-chooser"); - const findSelected = () => - find(".mini-tag-chooser .mini-tag-chooser-header .selected-name").text(); - - await visit("/"); - await click("#create-topic"); - await miniTagChooser.expand(); - await miniTagChooser.selectRowByValue("monkey"); - - assert.equal(findSelected(), "monkey"); - - await categoryChooser.expand(); - await categoryChooser.selectRowByValue(6); - - assert.equal(findSelected(), "optional tags"); - - await miniTagChooser.expand(); - await miniTagChooser.selectRowByValue("monkey"); - - assert.equal(findSelected(), "monkey"); - - await categoryChooser.expand(); - await categoryChooser.selectNoneRow(); - - assert.equal(findSelected(), "optional tags"); -}); From 3c09026fe45f04d317b2f1f9d917369549e8c389 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 6 Sep 2018 16:54:15 +0200 Subject: [PATCH 088/124] Minor improvements to rake i18n:check --- lib/i18n/locale_file_checker.rb | 23 +++++++++++++++-------- lib/tasks/i18n.rake | 13 +++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/i18n/locale_file_checker.rb b/lib/i18n/locale_file_checker.rb index 558d442c5c..3250b075e7 100644 --- a/lib/i18n/locale_file_checker.rb +++ b/lib/i18n/locale_file_checker.rb @@ -2,9 +2,9 @@ require 'i18n/i18n_interpolation_keys_finder' require 'yaml' class LocaleFileChecker - TYPE_MISSING_INTERPOLATION_KEY = 1 - TYPE_UNSUPPORTED_INTERPOLATION_KEY = 2 - TYPE_MISSING_PLURAL_KEY = 3 + TYPE_MISSING_INTERPOLATION_KEYS = 1 + TYPE_UNSUPPORTED_INTERPOLATION_KEYS = 2 + TYPE_MISSING_PLURAL_KEYS = 3 def check(locale) @errors = {} @@ -86,8 +86,8 @@ class LocaleFileChecker missing_keys.delete("count") end - add_error(keys, TYPE_MISSING_INTERPOLATION_KEY, missing_keys) unless missing_keys.empty? - add_error(keys, TYPE_UNSUPPORTED_INTERPOLATION_KEY, unsupported_keys) unless unsupported_keys.empty? + 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? end end @@ -107,7 +107,7 @@ 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_KEY, missing_plural_keys) unless missing_plural_keys.empty? + add_error(keys, TYPE_MISSING_PLURAL_KEYS, missing_plural_keys, pluralized: true) unless missing_plural_keys.empty? end end @@ -136,10 +136,17 @@ class LocaleFileChecker end end - def add_error(keys, type, details) + def add_error(keys, type, details, pluralized:) @errors[@relative_locale_path] ||= [] + + if pluralized + joined_key = keys[1..-2].join(".") << " [#{keys.last}]" + else + joined_key = keys[1..-1].join(".") + end + @errors[@relative_locale_path] << { - key: keys[1..-1].join("."), + key: joined_key, type: type, details: details.to_s } diff --git a/lib/tasks/i18n.rake b/lib/tasks/i18n.rake index 25ea4c846a..79a0deb2d3 100644 --- a/lib/tasks/i18n.rake +++ b/lib/tasks/i18n.rake @@ -32,12 +32,12 @@ task "i18n:check", [:locale] => [:environment] do |_, args| errors.each do |error| message = case error[:type] - when LocaleFileChecker::TYPE_MISSING_INTERPOLATION_KEY - "Missing interpolation key".red - when LocaleFileChecker::TYPE_UNSUPPORTED_INTERPOLATION_KEY - "Unsupported interpolation key".red - when LocaleFileChecker::TYPE_MISSING_PLURAL_KEY - "Missing plural key".yellow + when LocaleFileChecker::TYPE_MISSING_INTERPOLATION_KEYS + "Missing interpolation keys".red + when LocaleFileChecker::TYPE_UNSUPPORTED_INTERPOLATION_KEYS + "Unsupported interpolation keys".red + when LocaleFileChecker::TYPE_MISSING_PLURAL_KEYS + "Missing plural keys".yellow end details = error[:details] ? ": #{error[:details]}" : "" @@ -49,4 +49,5 @@ task "i18n:check", [:locale] => [:environment] do |_, args| failed_locales.each do |failed_locale| puts "", "Failed to check locale files for #{failed_locale}".red end + exit 1 unless failed_locales.empty? end From f13c34aaedec037aa83df9368aac6aadf7e3f567 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 6 Sep 2018 17:27:17 +0200 Subject: [PATCH 089/124] Adds a check for invalid message formats to rake i18n:check --- lib/i18n/locale_file_checker.rb | 25 +++++++++++++++++++++++-- lib/tasks/i18n.rake | 6 +++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/i18n/locale_file_checker.rb b/lib/i18n/locale_file_checker.rb index 3250b075e7..4d47c06af4 100644 --- a/lib/i18n/locale_file_checker.rb +++ b/lib/i18n/locale_file_checker.rb @@ -5,6 +5,7 @@ class LocaleFileChecker TYPE_MISSING_INTERPOLATION_KEYS = 1 TYPE_UNSUPPORTED_INTERPOLATION_KEYS = 2 TYPE_MISSING_PLURAL_KEYS = 3 + TYPE_INVALID_MESSAGE_FORMAT = 4 def check(locale) @errors = {} @@ -19,8 +20,7 @@ class LocaleFileChecker check_interpolation_keys check_plural_keys - - # TODO check MessageFormat + check_message_format end @errors @@ -111,6 +111,27 @@ class LocaleFileChecker end end + def check_message_format + mf_locale, mf_filename = JsLocaleHelper.find_message_format_locale([@locale], true) + + traverse_hash(@locale_yaml, []) do |keys, value| + next unless keys.last.ends_with?("_MF") + + begin + JsLocaleHelper.with_context do |ctx| + ctx.load(mf_filename) if File.exist?(mf_filename) + ctx.eval("mf = new MessageFormat('#{mf_locale}');") + ctx.eval("mf.precompile(mf.parse(#{value.to_s.inspect}))") + end + rescue MiniRacer::EvalError => error + error_message = error.message.sub(/at undefined[:\d]+/, "").strip + add_error(keys, TYPE_INVALID_MESSAGE_FORMAT, error_message, pluralized: false) + end + end + + JsLocaleHelper.reset_context + end + def reference_value(keys) value = @reference_yaml[REFERENCE_LOCALE] diff --git a/lib/tasks/i18n.rake b/lib/tasks/i18n.rake index 79a0deb2d3..ddd7e25891 100644 --- a/lib/tasks/i18n.rake +++ b/lib/tasks/i18n.rake @@ -37,7 +37,9 @@ task "i18n:check", [:locale] => [:environment] do |_, args| when LocaleFileChecker::TYPE_UNSUPPORTED_INTERPOLATION_KEYS "Unsupported interpolation keys".red when LocaleFileChecker::TYPE_MISSING_PLURAL_KEYS - "Missing plural keys".yellow + "Missing plural keys".magenta + when LocaleFileChecker::TYPE_INVALID_MESSAGE_FORMAT + "Invalid message format".yellow end details = error[:details] ? ": #{error[:details]}" : "" @@ -49,5 +51,7 @@ task "i18n:check", [:locale] => [:environment] do |_, args| failed_locales.each do |failed_locale| puts "", "Failed to check locale files for #{failed_locale}".red end + + puts "" exit 1 unless failed_locales.empty? end From f0dab5a5e4c4d6c3bf4d2d5146ff3138e692b05f Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Thu, 6 Sep 2018 23:43:24 +0530 Subject: [PATCH 090/124] DEV: Add local_dates post custom field --- .../lib/discourse_local_dates/engine.rb | 2 - plugins/discourse-local-dates/plugin.rb | 38 ++++++++++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/plugins/discourse-local-dates/lib/discourse_local_dates/engine.rb b/plugins/discourse-local-dates/lib/discourse_local_dates/engine.rb index ee920f0617..2338d5e298 100644 --- a/plugins/discourse-local-dates/lib/discourse_local_dates/engine.rb +++ b/plugins/discourse-local-dates/lib/discourse_local_dates/engine.rb @@ -1,6 +1,4 @@ module ::DiscourseLocalDates - PLUGIN_NAME = "discourse-local-dates" - class Engine < ::Rails::Engine engine_name DiscourseLocalDates::PLUGIN_NAME isolate_namespace DiscourseLocalDates diff --git a/plugins/discourse-local-dates/plugin.rb b/plugins/discourse-local-dates/plugin.rb index f027e75b5f..ec0b902e7a 100644 --- a/plugins/discourse-local-dates/plugin.rb +++ b/plugins/discourse-local-dates/plugin.rb @@ -12,6 +12,42 @@ register_asset "moment-timezone.js", :vendored_core_pretty_text enabled_site_setting :discourse_local_dates_enabled after_initialize do + module ::DiscourseLocalDates + PLUGIN_NAME ||= "discourse-local-dates".freeze + POST_CUSTOM_FIELD ||= "local_dates".freeze + end + + [ + "../lib/discourse_local_dates/engine.rb", + ].each { |path| load File.expand_path(path, __FILE__) } + + register_post_custom_field_type(DiscourseLocalDates::POST_CUSTOM_FIELD, :json) + + on(:post_process_cooked) do |doc, post| + dates = doc.css('span.discourse-local-date').map do |cooked_date| + date = {} + cooked_date.attributes.values.each do |attribute| + if attribute.name && ['data-date', 'data-time'].include?(attribute.name) + unless attribute.value == 'undefined' + date[attribute.name.gsub('data-', '')] = CGI.escapeHTML(attribute.value || "") + end + end + end + date + end + + if dates.present? + post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates.to_json + post.save_custom_fields + elsif post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD].present? + PostCustomField.where(post_id: post.id, name: DiscourseLocalDates::POST_CUSTOM_FIELD).destroy_all + end + end + + add_to_class(:post, :local_dates) do + custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] || [] + end + on(:reduce_cooked) do |fragment| container = fragment.css(".discourse-local-date").first @@ -21,5 +57,3 @@ after_initialize do end end end - -load File.expand_path('../lib/discourse_local_dates/engine.rb', __FILE__) From e894f895d42c10fb2f21c3643d3c034d8ee1888e Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 7 Sep 2018 00:31:45 +0530 Subject: [PATCH 091/124] DEV: Extract dates before post_process_cooked event --- plugins/discourse-local-dates/plugin.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/discourse-local-dates/plugin.rb b/plugins/discourse-local-dates/plugin.rb index ec0b902e7a..cb243194d7 100644 --- a/plugins/discourse-local-dates/plugin.rb +++ b/plugins/discourse-local-dates/plugin.rb @@ -23,7 +23,7 @@ after_initialize do register_post_custom_field_type(DiscourseLocalDates::POST_CUSTOM_FIELD, :json) - on(:post_process_cooked) do |doc, post| + on(:before_post_process_cooked) do |doc, post| dates = doc.css('span.discourse-local-date').map do |cooked_date| date = {} cooked_date.attributes.values.each do |attribute| @@ -39,8 +39,9 @@ after_initialize do if dates.present? post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates.to_json post.save_custom_fields - elsif post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD].present? - PostCustomField.where(post_id: post.id, name: DiscourseLocalDates::POST_CUSTOM_FIELD).destroy_all + elsif !post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD].nil? + post.custom_fields.delete(DiscourseLocalDates::POST_CUSTOM_FIELD) + post.save_custom_fields end end From 1f5442360966c6e4a6be0dc5793b530392fe942b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 6 Sep 2018 17:27:58 -0400 Subject: [PATCH 092/124] Update translations --- config/locales/client.es.yml | 27 ++++++++++ config/locales/client.fr.yml | 34 ++++++++++++ config/locales/client.pl_PL.yml | 5 +- config/locales/client.pt.yml | 9 ++++ config/locales/client.zh_TW.yml | 95 +++++++++++++++++++++++++++------ config/locales/server.es.yml | 23 ++++++++ config/locales/server.fr.yml | 5 ++ config/locales/server.zh_TW.yml | 1 - 8 files changed, 181 insertions(+), 18 deletions(-) diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 4881321158..747b88f221 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -196,6 +196,7 @@ es: privacy_policy: "Política de Privacidad" privacy: "Privacidad" tos: "Condiciones de uso" + rules: "Reglas" mobile_view: "Versión móvil" desktop_view: "Versión de escritorio" you: "Tú" @@ -536,6 +537,7 @@ es: post_count: "# posts" confirm_delete_other_accounts: "¿Seguro que quieres eliminar estas cuentas?" powered_by: "powered by ipinfo.io" + copied: "copiado" user_fields: none: "(selecciona una opción)" user: @@ -638,6 +640,7 @@ es: revoke_access: "Revocar acceso" undo_revoke_access: "Deshacer revocación de acceso" api_approved: "Aprobado:" + api_last_used_at: "Fecha de último uso:" theme: "Theme" home: "Página de inicio por defecto" staged: "Temporal" @@ -776,6 +779,17 @@ es: any: "cualquiera" password_confirmation: title: "Introduce de nuevo la contraseña" + auth_tokens: + title: "Dispositivos utilizados recientemente" + ip_address: "Dirección IP" + created: "Creado" + first_seen: "Primera vez" + last_seen: "Última vez" + operating_system: "Sistema operativo" + location: "Ubicación" + action: "Acción" + login: "Inicio de sesión" + logout: "Cierre de sesión en todos los dispositivos" last_posted: "Último post" last_emailed: "Último Enviado por email" last_seen: "Visto por última vez" @@ -1020,6 +1034,8 @@ es: disable: "Mostrar Posts Eliminados" private_message_info: title: "Mensaje" + invite: "Invitar a otros..." + edit: "Añadir o quitar..." leave_message: "¿Realmente quieres dejar este mensaje?" remove_allowed_user: "¿Seguro que quieres eliminar a {{name}} de este mensaje?" remove_allowed_group: "¿Seguro que quieres eliminar a {{name}} de este mensaje?" @@ -1155,6 +1171,7 @@ es: default_header_text: Seleccionar... no_content: Ninguna coincidencia encontrada filter_placeholder: Buscar... + filter_placeholder_with_any: Buscar o crear... create: "Crear: '{{content}}'" max_content_reached: one: "Puedes seleccionar únicamente {{count}} item." @@ -2568,6 +2585,7 @@ es: disabled: Desactivado timeout_error: "Lo sentimos, la solicitud está durando demasiado, por favor selecciona un periodo más corto" exception_error: "Lo siento, ha ocurrido un error al ejecutar la consulta" + too_many_requests: "Has realizado esta acción demasiadas veces. Por favor, espera antes de intentarlo de nuevo." reports: trend_title: "%{percent} de cambio. Actualmente %{current}, era %{prev} en el periodo previo." today: "Hoy" @@ -2934,9 +2952,16 @@ es: revert: "Revertir los cambios" revert_confirm: "¿Estás seguro de querer revertir los cambios?" theme: + theme: "Tema" + component: "Componente" + components: "Componentes" import_theme: "Importar Theme" customize_desc: "Personalizar:" title: "Themes" + modal_title: "Crear tema" + create: "Crear" + create_type: "Tipo:" + create_name: "Nombre:" long_title: "Modifique los colores, los CSS y los contenidos HTML de su sitio" edit: "Editar" edit_confirm: "Este es un theme remoto, si editas CSS/HTML, los cambios serán borrados en la próxima actualización al theme." @@ -2951,6 +2976,7 @@ es: color_scheme_select: "Selecciona colores para ser usados en el theme" custom_sections: "Personalizaciones:" theme_components: "Componentes del Theme" + switch_component: "Convertir a tema" uploads: "Subidos" no_uploads: "Puedes subir archivos asociados con tu theme como fuentes e imágenes " add_upload: "Agregar Subido" @@ -2983,6 +3009,7 @@ es: commits_behind: one: "Theme está 1 commit detrás!" other: "Theme está {{count}} commits detrás!" + compare_commits: "(Ver nuevos commits)" scss: text: "CSS" title: "Ingresa tu CSS, aceptamos estilos válidos de CSS y SCSS" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 571a0f5311..6700aa5de7 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -196,6 +196,7 @@ fr: privacy_policy: "Politique de confidentialité" privacy: "Confidentialité" tos: "Conditions générales d'utilisation" + rules: "Règles" mobile_view: "Vue mobile" desktop_view: "Vue bureau" you: "Vous" @@ -536,6 +537,7 @@ fr: post_count: "# messages" confirm_delete_other_accounts: "Êtes-vous sûr de vouloir supprimer tous ces comptes ?" powered_by: "propulsé par ipinfo.io" + copied: "copié" user_fields: none: "(choisir une option)" user: @@ -638,6 +640,7 @@ fr: revoke_access: "Révoquer l'accès" undo_revoke_access: "Annuler la révocation d'accès" api_approved: "Approuvé :" + api_last_used_at: "Dernièrement utilisé le :" theme: "Thème" home: "Page d'accueil par défaut" staged: "Distant" @@ -777,6 +780,18 @@ fr: any: "tous" password_confirmation: title: "Confirmation du mot de passe" + auth_tokens: + title: "Appareils utilisés récemment" + title_logs: "Journaux d'authentification" + ip_address: "Adresse IP" + created: "Créé" + first_seen: "Première utilisation" + last_seen: "Dernière utilisation" + operating_system: "Système d'exploitation" + location: "Localisation" + action: "Action" + login: "Connecter" + logout: "Déconnecter tout" last_posted: "Dernier message" last_emailed: "Dernier courriel" last_seen: "Vu" @@ -1009,6 +1024,7 @@ fr: hide_forever: "non merci" hidden_for_session: "Très bien, je vous demanderai demain. Vous pouvez toujours cliquer sur « Se connecter » pour créer un compte." intro: "Bonjour ! Vous semblez apprécier la discussion, mais n'avez pas encore créé de compte." + value_prop: "Quand vous créez votre compte, nous stockons ce que vous avez lu pour qu'à votre retour vous puissiez continuer là on vous vous êtes arrêtés. Vous recevez aussi des notifications, ici et par courriel, dès que quelqu'un vous répond. Et vous pouvez aimer les messages pour partager vos coups de cœurs. :heartpulse:" summary: enabled_description: "Vous visualisez un résumé de ce sujet : les messages les plus intéressants choisis par la communauté." description: "Il y a {{replyCount}} réponses." @@ -1022,6 +1038,8 @@ fr: disable: "Afficher les messages supprimés" private_message_info: title: "Message direct" + invite: "Inviter d'autres utilisateurs…" + edit: "Ajouter ou supprimer…" leave_message: "Êtes-vous sûr de vouloir quitter cette conversation ?" remove_allowed_user: "Êtes-vous sûr de vouloir supprimer {{name}} de ce message direct ?" remove_allowed_group: "Êtes-vous sûr de vouloir supprimer {{name}} de ce message direct ?" @@ -1157,6 +1175,7 @@ fr: default_header_text: Sélectionner… no_content: Aucune correspondance trouvée filter_placeholder: Rechercher... + filter_placeholder_with_any: Rechercher ou créer… create: "Créer : '{{content}}'" max_content_reached: one: "Vous ne pouvez séléctionner que {{count}} élément." @@ -2570,6 +2589,7 @@ fr: disabled: Désactivé timeout_error: "Désolé, la requête prend trop de temps, veuillez sélectionner un intervalle plus court" exception_error: "Désolé, une erreur s'est produite à l'exécution de la requête" + too_many_requests: Vous avez effectué cette action trop de fois. Merci d'attendre avant de ressayer. reports: trend_title: "%{percent} modifié. Actuellement %{current}, était %{prev} pour la période précédente." today: "Aujourd'hui" @@ -2936,9 +2956,16 @@ fr: revert: "Annuler les changements" revert_confirm: "Êtes-vous sûr de vouloir annuler vos changements ?" theme: + theme: "Thème" + component: "Composant" + components: "Composants" import_theme: "Importer un thème" customize_desc: "Personaliser :" title: "Thèmes" + modal_title: "Créer un thème" + create: "Créer" + create_type: "Type :" + create_name: "Nom :" long_title: "Modifier les couleurs, le CSS et le contenu HTML de votre site" edit: "Modifier" edit_confirm: "Ceci est un thème distant, si vous modifiez le CSS/HTML vos modifications seront écrasées la prochaine fois que vous le mettez à jour." @@ -2953,6 +2980,10 @@ fr: color_scheme_select: "Sélectionner les couleurs utilisées par le thème" custom_sections: "Sections personnalisées :" theme_components: "Composants du thème :" + switch_component: "Convertir en thème" + switch_component_alert: "Êtes-vous sûr de vouloir convertir ce composant en thème ? Cela va en faire un thème indépendant et sera supprimé des tous les thèmes l'utilisant." + switch_theme: "Convertir en composant" + switch_theme_alert: "Êtes-vous sûr de vouloir convertir ce thème en composant ? Il sera supprimé comme thème parent des composants utilisés." uploads: "Uploads" no_uploads: "Vous pouvez envoyer des ressources associées à votre thème comme des polices ou des images" add_upload: "Ajouter un fichier" @@ -2975,6 +3006,7 @@ fr: public_key: "Accorder un accès dépôt à la clef publique suivante :" about_theme: "À propos du thème" license: "Licence" + component_of: "Composant de :" update_to_latest: "Mettre à jour" check_for_updates: "Vérifier les mises à jour" updating: "Mise à jour…" @@ -2982,9 +3014,11 @@ fr: add: "Ajouter" theme_settings: "Paramètres thème" no_settings: "Ce thème n'a pas de paramètres." + empty: "Aucun élément" commits_behind: one: "Le thème est en retard de 1 commit !" other: "Le thème est en retard de {{count}} commits !" + compare_commits: "(Voir les nouveaux changements)" scss: text: "CSS" title: "Entrez du CSS personnalisé, nous acceptons tous les styles CSS et SCSS valides." diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index cac2634f30..e634626789 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -183,11 +183,13 @@ pl_PL: enabled: 'wylistowanie %{when}' disabled: 'odlistowanie %{when}' banner: - enabled: 'Ten temat został ustawiony jako baner . Będzie widoczny na górze każdej strony, póki nie zostanie ukryty przez użytkownika.' + enabled: 'ustawił ten baner %{when}. Będzie widoczny na górze każdej strony, póki nie zostanie ukryty przez użytkownika.' disabled: 'Ten temat nie jest już banerem. Nie będzie dalej wyświetlany na górze każdej strony.' topic_admin_menu: "akcje administratora" wizard_required: "Witaj na Twoim na nowym forum Discourse! Zacznijmy od kreatora ustawień ✨" emails_are_disabled: "Wysyłanie e-maili zostało globalnie wyłączone przez administrację. Powiadomienia e-mail nie będą dostarczane." + bootstrap_mode_enabled: "Aby ułatwić uruchomienie Twojej strony, jesteś w trybie bootstrap. Wszyscy nowi użytkownicy otrzymają poziom zaufania 1 i będą otrzymywać codzienne wiadomości e-mail. To zostanie automatyczonie wyłączone, kiedy %{min_users}osób dołączy." + bootstrap_mode_disabled: "Tryb bootstrap zostanie wyłączony w ciągu 24 godzin." themes: default_description: "Domyślny" s3: @@ -678,6 +680,7 @@ pl_PL: revoke_access: "Zablokuj dostęp" undo_revoke_access: "Cofnij zablokowanie dostępu" api_approved: "Zatwierdzony:" + api_last_used_at: "Ostatnio użyto:" theme: "Motyw" home: "Domyślna strona domowa" staff_counters: diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 8daa66a8bb..b406a30004 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -48,6 +48,9 @@ pt: x_seconds: one: "1s" other: "%{count}s" + less_than_x_minutes: + one: "< 1m" + other: "< %{count}m" x_minutes: one: "1m" other: "%{count}m" @@ -57,6 +60,9 @@ pt: x_days: one: "1d" other: "%{count}d" + x_months: + one: "1 mês" + other: "%{count}meses" about_x_years: one: "1a" other: "%{count}a" @@ -119,6 +125,7 @@ pt: user_left: "%{who} removeram-se desta mensagem %{when}" removed_user: "removeu %{who} %{when}" removed_group: "removeu %{who} %{when}" + autobumped: "automaticamente colidido %{when}" autoclosed: enabled: 'fechado %{when}' disabled: 'aberto %{when}' @@ -143,6 +150,8 @@ pt: topic_admin_menu: "Ações administrativas dos Tópicos" wizard_required: "Bem-vindo ao seu novo Discourse! Vamos começar com o assistente de configuração ✨" emails_are_disabled: "Todos os envios de e-mail foram globalmente desativados por um administrador. Nenhum e-mail de notificação será enviado." + bootstrap_mode_enabled: "Para que o inicio do teu Site seja o mais simples possivél, estás agora em modo de inicialização simples. A todos os novos utilizadores será concedido o Nível de Confiança 1 e o resumo por e-mail enviado diariamente estará ativado. Isto será automaticamente desligado quando %{min_users}utilizadores se tiverem juntado ao fórum." + bootstrap_mode_disabled: "O modo de inicialização simples será desactivado em 24 horas." themes: default_description: "Predefinição" s3: diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 6b4d108abd..01109362d7 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -164,7 +164,6 @@ zh_TW: guidelines: "守則" privacy_policy: "隱私權政策" privacy: "隱私" - terms_of_service: "服務條款" mobile_view: "手機版網站" desktop_view: "電腦版網站" you: "你" @@ -438,8 +437,6 @@ zh_TW: reorder: title: "重新排序分類" title_long: "重新排序分類列表" - fix_order: "固定位置" - fix_order_tooltip: "並非所有的分類皆有唯一的位置參數, 可能會有出乎意料之外的結果." save: "儲存順序" apply_all: "申請" position: "位置" @@ -630,7 +627,6 @@ zh_TW: upload_title: "上傳你的圖片" upload_picture: "上傳圖片" image_is_not_a_square: "警告:我們裁切了你的圖片,因為該圖片不是正方形的。" - cache_notice: "更改了頭像成功,但是鑒於瀏覽器緩存可能需要一段時間後才會生效。" change_profile_background: title: "基本資料背景圖案" instructions: "個人資料背景會被置中,且默認寬度為850px。" @@ -804,7 +800,6 @@ zh_TW: most_liked_users: "讚誰最多" most_replied_to_users: "最多回覆至" no_likes: "暫無讚" - associated_accounts: "登入" ip_address: title: "最近的 IP 位址" registration_ip_address: @@ -955,9 +950,6 @@ zh_TW: preferences: "需要登入後更改設置" forgot: "我記不清賬號詳情了" not_approved: "你的帳號尚未獲得批准。一旦你的帳號獲得批准,你會收到一封電子郵件。" - google: - title: "使用 Google 帳號" - message: "使用 Google 帳號認証 (請確定你的網頁瀏覽器未阻擋彈出視窗)" google_oauth2: title: "使用 Google 帳號" message: "使用 Google 帳號認證 ( 請確定你的網頁瀏覽器不會阻擋彈出視窗 )" @@ -1090,13 +1082,26 @@ zh_TW: title: "你忘記添加收信人了嗎?" body: "目前該私信只發給了你自己!" admin_options_title: "此討論話題可選用之工作人員設定選項" + composer_actions: + reply_as_private_message: + label: 新增訊息 + desc: 建立一則私訊 + reply_to_topic: + label: 回復到討論話題 + desc: 回到到討論話題,但不是特定貼文 + create_topic: + label: "新增討論話題" notifications: title: "當有人以「@用戶名稱」提及您、回覆您的貼文、或是傳送訊息給您的時候通知您的設定。" none: "目前無法載入通知訊息。" empty: "未找到任何通知。" more: "檢視較舊的通知" total_flagged: "所有被投訴的文章" + invitee_accepted: "{{username}} 接受了您的邀請" + moved_post: "{{username}} 移動 {{description}}" + linked: "{{username}} {{description}}" granted_badge: "得到 '{{description}}'" + topic_reminder: "{{username}} {{description}}" popup: mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' group_mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' @@ -1104,6 +1109,7 @@ zh_TW: replied: '{{username}}在“{{topic}}”回覆了你 - {{site_title}}' posted: '{{username}}在“{{topic}}”中發佈了帖子 - {{site_title}}' linked: '{{username}}在“{{topic}}”中連結了你的帖子 - {{site_title}}' + confirm_body: '成功! 通知已啟用' upload_selector: title: "加入一張圖片" title_with_attachments: "加入一張圖片或一個檔案" @@ -1118,20 +1124,29 @@ zh_TW: uploading: "正在上傳" select_file: "選取檔案" image_link: "連結你的圖片將指向" + default_image_alt_text: 圖像 search: sort_by: "排序" relevance: "最相關" latest_post: "最新發帖" + latest_topic: "最新的討論話題" most_viewed: "最多閲讀" most_liked: "最多讚" select_all: "選擇全部" clear_all: "清除全部" too_short: "你的搜索詞太短。" title: "搜尋討論話題、文章、用戶或分類" + full_page_title: "搜尋討論話題或貼文" no_results: "未找到任何結果。" no_more_results: "沒有找到更多的結果。" searching: "正在搜尋..." post_format: "#{{post_number}} {{username}}" + results_page: "'{{term}}' 的搜尋結果" + start_new_topic: "或許開始一個新的討論話題?" + or_search_google: "或是嘗試利用 Google 搜尋:" + search_google: "嘗試利用 Google 搜尋:" + search_google_button: "Google" + search_google_title: "搜尋這個網站" context: user: "搜尋 @{{username}} 的文章" category: "搜索 #{{category}} 分類" @@ -1141,19 +1156,28 @@ zh_TW: title: 高級搜索 posted_by: label: 發帖人 + in_category: + label: 已分類 in_group: label: 在該群組中 with_badge: label: 有該徽章 + with_tags: + label: 已標記 filters: likes: 我給了讚的 posted: 我參與發帖 watching: 我正在關注 tracking: 我正在追蹤 + private: 在我的訊息 first: 是第一帖 pinned: 是置頂的 unpinned: 不是置頂的 + seen: 我已讀的 + unseen: 我還未讀的 wiki: 公共編輯 + images: 包含圖像 + all_tags: 以上所有的標籤 statuses: label: 當主題 open: 是開放的 @@ -1189,8 +1213,10 @@ zh_TW: dismiss_new: "設定新文章為已讀" toggle: "批量切換選擇討論話題" actions: "批量操作" + change_category: "設定分類" close_topics: "關閉討論話題" archive_topics: "已封存的討論話題" + notification_level: "通知" choose_new_category: "為主題選擇新類別:" selected: other: "你已選擇了 {{count}} 個討論話題。" @@ -1229,6 +1255,7 @@ zh_TW: other: "本主題中的 {{count}} 帖" create: '新討論話題' create_long: '建立新討論話題' + open_draft: "開啟草稿" private_message: '發送訊息' archive_message: help: '移動消息到存檔' @@ -1236,6 +1263,9 @@ zh_TW: move_to_inbox: title: '移動到收件箱' help: '移動消息到收件箱' + edit_message: + help: '編輯這個訊息的第一篇貼文' + title: '編輯訊息' list: '討論話題' new: '新討論話題' unread: '未讀' @@ -1275,6 +1305,34 @@ zh_TW: jump_reply_up: jump to earlier reply jump_reply_down: jump to later reply deleted: "此討論話題已被刪除" + auto_update_input: + tomorrow: "明天" + this_weekend: "這週末" + next_week: "下週" + two_weeks: "兩週" + next_month: "下個月" + three_months: "三個月" + six_months: "六個月" + one_year: "一年" + forever: "永久" + pick_date_and_time: "挑選日期與時間" + set_based_on_last_post: "依照上一篇貼文來關閉" + publish_to_category: + title: "定時發表" + temp_open: + title: "暫時開啟" + auto_reopen: + title: "自動開啟討論話題" + temp_close: + title: "暫時關閉" + auto_close: + title: "自動關閉討論話題" + label: "自動關閉討論話題的期限:" + error: "請輸入一個有效的值。" + auto_delete: + title: "自動刪除討論話題" + reminder: + title: "提醒我" auto_close_title: '自動關閉設定' auto_close_immediate: other: "主題中的最後一帖是 %{hours} 小時前發出的,所以主題將會立即關閉。" @@ -1351,6 +1409,7 @@ zh_TW: visible: "出現在列表上" reset_read: "重置讀取資料" make_public: "設置為公共主題" + make_private: "設置為私訊" feature: pin: "置頂主題" unpin: "取消置頂主題" @@ -1424,6 +1483,7 @@ zh_TW: success_email: "我們發了一封郵件邀請{{emailOrUsername}}。邀請被接受後你會收到通知。檢查用戶頁中的邀請標籤頁來追蹤你的邀請。" success_username: "我們已經邀請該使用者加入此主題討論" error: "抱歉,我們不能邀請這個人。可能他已經被邀請了?(邀請有頻率限制)" + success_existing_email: "已經有一個有此電子郵件 {{emailOrUsername}} 的使用者存在。我們已邀請那位使用者來參與這個話題。" login_reply: '登入以發表回應' filters: n_posts: @@ -1455,6 +1515,7 @@ zh_TW: instructions: other: "請選擇一位新用戶作為此 {{count}} 篇由 {{old_user}} 撰寫之文章的擁有者。" change_timestamp: + title: "變更時間標籤..." action: "變更時間戳記" invalid_timestamp: "時間戳記不能為將來的時刻。" error: "更改主題時間時發生錯誤。" @@ -1462,6 +1523,10 @@ zh_TW: multi_select: select: '選取' selected: '選取了 ({{count}})' + select_post: + label: '選擇' + selected_post: + label: '已選取' delete: 刪除選取的文章 cancel: 取消選取 select_all: 選擇全部 @@ -1546,7 +1611,6 @@ zh_TW: inappropriate: "撤回投訴" bookmark: "移除書籤" like: "撤回讚" - vote: "撤回投票" people: off_topic: "投訴為離題內容" spam: "投訴為垃圾內容" @@ -1555,7 +1619,6 @@ zh_TW: notify_user: "已送出一則訊息" bookmark: "收藏" like: "讚了它" - vote: "已投票" by_you: off_topic: "你已投訴此文章偏離討論話題" spam: "你已投訴此文章為垃圾" @@ -1564,7 +1627,6 @@ zh_TW: notify_user: "您已送出訊息給這位用戶" bookmark: "你已將此文章加上書籤" like: "你已在此文章按讚" - vote: "你已在此文章投票支持" by_you_and_others: off_topic: other: "你與其他 {{count}} 人已投訴此文章為離題內容" @@ -1580,8 +1642,6 @@ zh_TW: other: "你與 {{count}} 個人將此文章加上書籤" like: other: "你與其他 {{count}} 人對此按讚" - vote: - other: "你與其他 {{count}} 人已投票給此文章" by_others: off_topic: other: "{{count}} 人已投訴此文章為離題內容" @@ -1597,8 +1657,6 @@ zh_TW: other: "{{count}} 個人將此文章加上書籤" like: other: "{{count}} 人對此按讚" - vote: - other: "{{count}} 人已投票給此文章" revisions: controls: first: "第一版" @@ -2084,6 +2142,8 @@ zh_TW: start_date: "開始日期" end_date: "結束日期" groups: "所有群組" + trending_search: + more: ' 搜尋記錄檔 ' commits: latest_changes: "最近的變更:請經常更新!" by: "由" @@ -2091,6 +2151,10 @@ zh_TW: title: "投訴" agree: "同意" agree_title: "確認此投訴為有效且正確" + agree_flag_hide_post: "隱藏貼文" + agree_flag_restore_post: "同意並還原貼文" + agree_flag_suspend: "停權使用者" + agree_flag_silence: "靜音使用者" delete: "刪除" delete_title: "刪除此標記文章。" delete_post_defer_flag_title: "刪除文章,如果刪除的是討論話題的第一則文章,討論話題也將一併刪除" @@ -2421,7 +2485,6 @@ zh_TW: address_placeholder: "name@example.com" type_placeholder: "digest, signup..." reply_key_placeholder: "回覆金鑰" - skipped_reason_placeholder: "原因" logs: title: "記錄" action: "動作" diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 60152d406c..31a4d40a47 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -42,6 +42,7 @@ es: errors: component_no_user_selectable: "Los componentes de tema no pueden ser seleccionables por los usuarios" component_no_default: "Los componentes de temas no pueden ser el tema por defecto" + component_no_color_scheme: "Los componentes de tema no pueden tener esquemas de color" no_multilevels_components: "Loa temas con temas hijos no pueden ser hijos de otros temas" settings_errors: invalid_yaml: "YAML provisto es inválido." @@ -627,6 +628,17 @@ es: email_login: invalid_token: "Lo sentimos, ese enlace de inicio de sesión de correo electrónico es demasiado viejo. Seleccione el botón Iniciar sesión y use 'Olvidé mi contraseña' para obtener un nuevo enlace." title: "Email login" + user_auth_tokens: + devices: + android: 'Dispositivo Android' + linux: 'Ordenador Linux' + windows: 'Ordenador Windows' + mac: 'Mac' + iphone: 'iPhone' + ipad: 'iPad' + ipod: 'iPod' + mobile: 'Dispositivo móvil' + unknown: 'Dispositivo desconocido' change_email: confirmed: "Tu email ha sido actualizado." please_continue: "Continuar a %{site_name}" @@ -1724,6 +1736,10 @@ es: second_factor_toggle: totp: "Usar una aplicación de verificación en su lugar" backup_code: "Usar un código de respaldo en su lugar" + admin: + email: + sent_test: "¡enviado!" + sent_test_disabled: "no se ha podido enviar el email porque están desactivados" user: deactivated: "Ha sido desactivado a causa de muchos rebotes al email '%{email}'." deactivated_by_staff: "Desactivado por el staff" @@ -2294,6 +2310,13 @@ es: Si dispones de una interfaz de usuario web para la cuenta de correo POP, es posible que debas iniciar sesión allí y comprobar su configuración. + email_revoked: + title: "Email revocado" + subject_template: "¿Es tu correo electrónico correcto?" + text_body_template: | + Lo sentimos, pero estamos teniendo problemas para contactarte por correo. Nuestros últimos mensajes nos han sido devueltos como imposibles de entregar. + + ¿Podrías comprobar que [tu dirección de email](%{base_url}/my/preferences/email) es válida y funciona? También puedes añadir nuestra dirección a tu agenda para mejorar la entregabilidad. too_many_spam_flags: title: "Demasiadas banderas por Spam" subject_template: "Nueva cuenta retenida" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 680359e302..10a94707ec 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -42,6 +42,7 @@ fr: errors: component_no_user_selectable: "Les composants du thème ne peuvent pas être sélectionnés par les utilisateurs" component_no_default: "Les composants du thème ne peuvent pas être un thème par défaut" + component_no_color_scheme: "Les composants de thème ne peuvent pas avoir de palette de couleur" no_multilevels_components: "Les thèmes incluant d'autres thèmes ne peuvent pas être inclus eux-mêmes" settings_errors: invalid_yaml: "Le YAML est invalide" @@ -229,6 +230,7 @@ fr: topic_not_found: "Une erreur est survenue. Peut-être que ce sujet a été fermé ou supprimé pendant que vous le regardiez ?" not_accepting_pms: "Désolé, %{username} n'accepte pas de messages pour le moment." max_pm_recepients: "Désolé, vous pouvez envoyer un message à un maximum de %{recipients_limit} destinataires." + pm_reached_recipients_limit: "Désolé, vous ne pouvez pas avoir plus de %{recipients_limit} destinataires dans un message." just_posted_that: "est trop similaire à ce que vous avez récemment posté" invalid_characters: "contient des caractères invalides" is_invalid: "ne semble pas clair, est-ce une phrase complète ?" @@ -474,6 +476,9 @@ fr: cannot_delete: uncategorized: "Vous ne pouvez pas supprimer Sans Catégorie" has_subcategories: "Vous ne pouvez pas supprimer cette catégorie car elle a des sous-catégories." + topic_exists: + one: "Vous ne pouvez pas supprimer cette catégorie car elle contient 1 sujet. Le plus vieux sujet est %{topic_link}." + other: "Vous ne pouvez pas supprimer cette catégorie car elle contient %{count} sujets. Le plus vieux sujet est %{topic_link}." topic_exists_no_oldest: "Vous ne pouvez pas supprimer cette catégorie car le nombre de sujet est de %{count}." uncategorized_description: "Sujets qui n'ont pas besoin d'une catégorie ou qui ne correspondent à aucune catégorie existante." trust_levels: diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 14a5dfe22a..8cbcf3595e 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -1799,7 +1799,6 @@ zh_TW: recent_topics: "最近" see_more: "更多" search_title: "搜尋該網頁" - search_google: "Google" login_required: welcome_message: | ## [歡迎來到 %{title}](#welcome) From ea2f13c71bcea1737b98183eed69c8589e60f8f4 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 6 Sep 2018 17:36:30 -0400 Subject: [PATCH 093/124] recover terms_of_service translation for zh_TW --- config/locales/client.zh_TW.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 01109362d7..78cb241fb3 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -164,6 +164,7 @@ zh_TW: guidelines: "守則" privacy_policy: "隱私權政策" privacy: "隱私" + terms_of_service: "服務條款" mobile_view: "手機版網站" desktop_view: "電腦版網站" you: "你" From 797cbf8653f9b8c75e38695d9abc7654a631eea7 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 7 Sep 2018 00:02:47 +0200 Subject: [PATCH 094/124] FIX: Remove user fields when anonymizing user --- app/controllers/users_controller.rb | 4 ++-- app/jobs/regular/anonymize_user.rb | 12 ++++++++++++ app/models/invite_redeemer.rb | 2 +- app/models/user.rb | 4 +++- spec/services/user_anonymizer_spec.rb | 15 +++++++++++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 92a9bd4796..8268d2594d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -117,7 +117,7 @@ class UsersController < ApplicationController val = val[0...UserField.max_length] if val return render_json_error(I18n.t("login.missing_user_field")) if val.blank? && f.required? - attributes[:custom_fields]["user_field_#{f.id}"] = val + attributes[:custom_fields]["#{User::USER_FIELD_PREFIX}#{f.id}"] = val end end @@ -352,7 +352,7 @@ class UsersController < ApplicationController if field_val.blank? return fail_with("login.missing_user_field") if f.required? else - fields["user_field_#{f.id}"] = field_val[0...UserField.max_length] + fields["#{User::USER_FIELD_PREFIX}#{f.id}"] = field_val[0...UserField.max_length] end end diff --git a/app/jobs/regular/anonymize_user.rb b/app/jobs/regular/anonymize_user.rb index e2a6b36009..3a6d6068e8 100644 --- a/app/jobs/regular/anonymize_user.rb +++ b/app/jobs/regular/anonymize_user.rb @@ -23,6 +23,8 @@ module Jobs .where(user_id: @user_id) .where.not(raw_email: nil) .update_all(raw_email: nil) + + anonymize_user_fields end def ip_where(column = 'user_id') @@ -46,5 +48,15 @@ module Jobs ).update_all(ip_address: new_ip) end + def anonymize_user_fields + user_field_ids = UserField.pluck(:id) + user = User.find(@user_id) + return if user_field_ids.blank? || user.blank? + + user_field_ids.each do |field_id| + user.custom_fields.delete("#{User::USER_FIELD_PREFIX}#{field_id}") + end + user.save! + end end end diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb index 904bf56bb0..0c1702c364 100644 --- a/app/models/invite_redeemer.rb +++ b/app/models/invite_redeemer.rb @@ -44,7 +44,7 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password, :user_custom_f user_fields.each do |f| field_val = field_params[f.id.to_s] - fields["user_field_#{f.id}"] = field_val[0...UserField.max_length] unless field_val.blank? + fields["#{User::USER_FIELD_PREFIX}#{f.id}"] = field_val[0...UserField.max_length] unless field_val.blank? end user.custom_fields = fields end diff --git a/app/models/user.rb b/app/models/user.rb index 82c97f9c63..fe0e40bf46 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -984,13 +984,15 @@ class User < ActiveRecord::Base result end + USER_FIELD_PREFIX ||= "user_field_" + def user_fields return @user_fields if @user_fields user_field_ids = UserField.pluck(:id) if user_field_ids.present? @user_fields = {} user_field_ids.each do |fid| - @user_fields[fid.to_s] = custom_fields["user_field_#{fid}"] + @user_fields[fid.to_s] = custom_fields["#{USER_FIELD_PREFIX}#{fid}"] end end @user_fields diff --git a/spec/services/user_anonymizer_spec.rb b/spec/services/user_anonymizer_spec.rb index 5b61f8b1d1..2dd931a1f6 100644 --- a/spec/services/user_anonymizer_spec.rb +++ b/spec/services/user_anonymizer_spec.rb @@ -256,6 +256,21 @@ describe UserAnonymizer do UserProfileView.add(user.id, '127.0.0.1', another_user.id, Time.now, true) expect { make_anonymous }.to_not change { UserProfileView.count } end + + it "removes user field values" do + field1 = Fabricate(:user_field) + field2 = Fabricate(:user_field) + + user.custom_fields = { + "some_field": "123", + "user_field_#{field1.id}": "foo", + "user_field_#{field2.id}": "bar", + "another_field": "456" + } + + expect { make_anonymous }.to change { user.custom_fields } + expect(user.reload.custom_fields).to eq("some_field" => "123", "another_field" => "456") + end end end From 3dea48f1d9cc232cdf0e9437d1ce24352cd89c05 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 7 Sep 2018 00:19:23 +0200 Subject: [PATCH 095/124] Resetting miniracer context results in segfault --- lib/i18n/locale_file_checker.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/i18n/locale_file_checker.rb b/lib/i18n/locale_file_checker.rb index 4d47c06af4..b9aa206bb0 100644 --- a/lib/i18n/locale_file_checker.rb +++ b/lib/i18n/locale_file_checker.rb @@ -128,8 +128,6 @@ class LocaleFileChecker add_error(keys, TYPE_INVALID_MESSAGE_FORMAT, error_message, pluralized: false) end end - - JsLocaleHelper.reset_context end def reference_value(keys) From c1c9637b3959a8b188a33e2da50e66c31e187a41 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 7 Sep 2018 08:33:27 +0800 Subject: [PATCH 096/124] Skip Discobot flag tutorial if `allow_flagging_staff` is disabled. https://meta.discourse.org/t/interacting-with-discobot/96574 --- .../new_user_narrative.rb | 1 + .../new_user_narrative_spec.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/new_user_narrative.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/new_user_narrative.rb index a814f1cd73..bc134eb7e3 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/new_user_narrative.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/new_user_narrative.rb @@ -95,6 +95,7 @@ module DiscourseNarrativeBot }, tutorial_flag: { + prerequisite: Proc.new { SiteSetting.allow_flagging_staff }, next_state: :tutorial_search, next_instructions: Proc.new { I18n.t("#{I18N_KEY}.search.instructions", base_uri: Discourse.base_uri) }, flag: { diff --git a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/new_user_narrative_spec.rb b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/new_user_narrative_spec.rb index 1b1700c5f3..0d0d144f5e 100644 --- a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/new_user_narrative_spec.rb +++ b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/new_user_narrative_spec.rb @@ -401,6 +401,22 @@ describe DiscourseNarrativeBot::NewUserNarrative do expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_flag) end + + describe 'when allow_flagging_staff is false' do + it 'should go to the right state' do + SiteSetting.allow_flagging_staff = false + post.update!(raw: skip_trigger) + + DiscourseNarrativeBot::TrackSelector.new( + :reply, + user, + post_id: post.id + ).select + + expect(narrative.get_data(user)[:state].to_sym) + .to eq(:tutorial_search) + end + end end end From 879067d0009ad4db4669af59215a4921e75afbc1 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 7 Sep 2018 10:44:57 +1000 Subject: [PATCH 097/124] FIX: check admin theme cookie against user selectable previously admin got a free pass and could set theme via cookie to anything including themes that are not selectable this refactor ensures that only "preview" gets a free pass, all the rest goes through the same pipeline --- app/controllers/application_controller.rb | 10 +++++----- lib/guardian.rb | 4 ++-- spec/components/guardian_spec.rb | 7 +++++-- spec/requests/application_controller_spec.rb | 10 ++++++++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b6076636cf..e24baddfff 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -381,7 +381,8 @@ class ApplicationController < ActionController::Base theme_ids = [] if preview_theme_id = request[:preview_theme_id]&.to_i - theme_ids << preview_theme_id + ids = [preview_theme_id] + theme_ids = ids if guardian.allow_themes?(ids, include_preview: true) end user_option = current_user&.user_option @@ -394,10 +395,9 @@ class ApplicationController < ActionController::Base end end - theme_ids = user_option&.theme_ids || [] if theme_ids.blank? - - unless guardian.allow_themes?(theme_ids) - theme_ids = [] + if theme_ids.blank? + ids = user_option&.theme_ids || [] + theme_ids = ids if guardian.allow_themes?(ids) end if theme_ids.blank? && SiteSetting.default_theme_id != -1 diff --git a/lib/guardian.rb b/lib/guardian.rb index 8c7c6afb8d..3f7a96eba3 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -364,10 +364,10 @@ class Guardian UserExport.where(user_id: @user.id, created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day)).count == 0 end - def allow_themes?(theme_ids) + def allow_themes?(theme_ids, include_preview: false) return true if theme_ids.blank? - if is_staff? && (theme_ids - Theme.theme_ids).blank? + if include_preview && is_staff? && (theme_ids - Theme.theme_ids).blank? return true end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 13ab35f426..fc620ecc03 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -2581,8 +2581,11 @@ describe Guardian do let(:theme2) { Fabricate(:theme) } it "allows staff to use any themes" do - expect(Guardian.new(moderator).allow_themes?([theme.id, theme2.id])).to eq(true) - expect(Guardian.new(admin).allow_themes?([theme.id, theme2.id])).to eq(true) + expect(Guardian.new(moderator).allow_themes?([theme.id, theme2.id])).to eq(false) + expect(Guardian.new(admin).allow_themes?([theme.id, theme2.id])).to eq(false) + + expect(Guardian.new(moderator).allow_themes?([theme.id, theme2.id], include_preview: true)).to eq(true) + expect(Guardian.new(admin).allow_themes?([theme.id, theme2.id], include_preview: true)).to eq(true) end it "only allows normal users to use user-selectable themes or default theme" do diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 03a998965d..fb64f91b0b 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -92,6 +92,7 @@ RSpec.describe ApplicationController do describe "#handle_theme" do let(:theme) { Fabricate(:theme, user_selectable: true) } let(:theme2) { Fabricate(:theme, user_selectable: true) } + let(:non_selectable_theme) { Fabricate(:theme, user_selectable: false) } let(:user) { Fabricate(:user) } let(:admin) { Fabricate(:admin) } @@ -148,6 +149,15 @@ RSpec.describe ApplicationController do get "/", params: { preview_theme_id: theme2.id } expect(response.status).to eq(200) expect(controller.theme_ids).to eq([theme2.id]) + + get "/", params: { preview_theme_id: non_selectable_theme.id } + expect(controller.theme_ids).to eq([non_selectable_theme.id]) + end + + it "does not allow non privileged user to preview themes" do + sign_in(user) + get "/", params: { preview_theme_id: non_selectable_theme.id } + expect(controller.theme_ids).to eq([SiteSetting.default_theme_id]) end it "cookie can fail back to user if out of sync" do From b5b4e2602a70ea4c214fcc42dbdc4dbf37db2cd2 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 7 Sep 2018 10:18:13 +0800 Subject: [PATCH 098/124] Formatting fixes. --- .../discourse_narrative_bot/advanced_user_narrative.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb index 6cf3416485..fc052dc71c 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb @@ -132,13 +132,15 @@ module DiscourseNarrativeBot def init_tutorial_recover data = get_data(@user) - post = PostCreator.create!(@user, raw: I18n.t( + post = PostCreator.create!(@user, + raw: I18n.t( "#{I18N_KEY}.recover.deleted_post_raw", i18n_post_args(discobot_username: self.discobot_user.username) ), - topic_id: data[:topic_id], - skip_bot: true, - skip_validations: true) + topic_id: data[:topic_id], + skip_bot: true, + skip_validations: true + ) set_state_data(:post_id, post.id) From def4fbaf017753e67b5ff7007773b2e44f2f3225 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 7 Sep 2018 11:24:05 +0800 Subject: [PATCH 099/124] UX: Join dates in tooltips using line breaks. --- .../assets/javascripts/discourse-local-dates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js index 0d5eb86d43..8eef14aa20 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js +++ b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js @@ -60,7 +60,7 @@ html += ""; html += ""; - var joinedPreviews = previews.join(" – "); + var joinedPreviews = previews.join("\n"); $element .html(html) From 89e5d91f0a7e7e11bb268b86e7dfa00e62ebdf38 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 7 Sep 2018 11:32:44 +0800 Subject: [PATCH 100/124] FIX: Tooltip should use format option instead of defaulting to "LLL". --- .../assets/javascripts/discourse-local-dates.js | 5 ++++- .../discourse-local-dates/spec/lib/pretty_text_spec.rb | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js index 8eef14aa20..9539dc4788 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js +++ b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js @@ -27,7 +27,10 @@ } var previews = options.timezones.split("|").map(function(timezone) { - var dateTime = relativeTime.tz(timezone).format("LLL"); + var dateTime = relativeTime + .tz(timezone) + .format(options.format || "LLL"); + var timezoneParts = _formatTimezone(timezone); if (dateTime.match(/TZ/)) { diff --git a/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb b/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb index f81f8084f9..d76857b9ab 100644 --- a/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb +++ b/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb @@ -4,15 +4,15 @@ describe PrettyText do it 'supports inserting date' do freeze_time cooked = PrettyText.cook <<~MD - [date=2018-05-08 time=22:00 format=LLL timezones="Europe/Paris|America/Los_Angeles"] + [date=2018-05-08 time=22:00 format="L LTS" timezones="Europe/Paris|America/Los_Angeles"] MD expect(cooked).to include('class="discourse-local-date"') expect(cooked).to include('data-date="2018-05-08"') - expect(cooked).to include('data-format="LLL"') + expect(cooked).to include('data-format="L LTS"') expect(cooked).to include('data-timezones="Europe/Paris|America/Los_Angeles"') - expect(cooked).to include('May 8, 2018 3:00 PM (America: Los Angeles)') - expect(cooked).to include('May 9, 2018 12:00 AM (Europe: Paris)') + expect(cooked).to include('05/08/2018 3:00:00 PM (America: Los Angeles)') + expect(cooked).to include('05/09/2018 12:00:00 AM (Europe: Paris)') end it 'uses a simplified syntax in emails' do From 039afe0d2c50c22aa1f2ad25afe907a5ce752e6f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 7 Sep 2018 15:19:34 +0800 Subject: [PATCH 101/124] Apply prettier. --- .../javascripts/wizard/test/test_helper.js | 25 +++++--- test/javascripts/test_helper.js | 60 +++++++++++-------- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/wizard/test/test_helper.js b/app/assets/javascripts/wizard/test/test_helper.js index 91abd402c9..f2281cea0e 100644 --- a/app/assets/javascripts/wizard/test/test_helper.js +++ b/app/assets/javascripts/wizard/test/test_helper.js @@ -22,11 +22,14 @@ //= require pretender //= require ./wizard-pretender - // Trick JSHint into allow document.write var d = document; -d.write('
'); -d.write(''); +d.write( + '
' +); +d.write( + "" +); if (window.Logster) { Logster.enabled = false; @@ -35,7 +38,12 @@ if (window.Logster) { } Ember.Test.adapter = window.QUnitAdapter.create(); -var createPretendServer = requirejs('wizard/test/wizard-pretender', null, null, false).default; +var createPretendServer = requirejs( + "wizard/test/wizard-pretender", + null, + null, + false +).default; var server; QUnit.testStart(function() { @@ -46,13 +54,12 @@ QUnit.testDone(function() { server.shutdown(); }); - -var _testApp = requirejs('wizard/test/helpers/start-app').default(); -var _buildResolver = requirejs('discourse-common/resolver').buildResolver; -window.setResolver(_buildResolver('wizard').create({ namespace: _testApp })); +var _testApp = requirejs("wizard/test/helpers/start-app").default(); +var _buildResolver = requirejs("discourse-common/resolver").buildResolver; +window.setResolver(_buildResolver("wizard").create({ namespace: _testApp })); Object.keys(requirejs.entries).forEach(function(entry) { - if ((/\-test/).test(entry)) { + if (/\-test/.test(entry)) { requirejs(entry, null, null, true); } }); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index b728169e11..c24d425fdd 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -52,12 +52,16 @@ window.MessageBus.stop(); // Trick JSHint into allow document.write var d = document; d.write(''); -d.write('
'); -d.write(''); +d.write( + '
' +); +d.write( + "" +); Ember.Test.adapter = window.QUnitAdapter.create(); -Discourse.rootElement = '#ember-testing'; +Discourse.rootElement = "#ember-testing"; Discourse.setupForTesting(); Discourse.injectTestHelpers(); Discourse.start(); @@ -70,21 +74,23 @@ if (window.Logster) { } var origDebounce = Ember.run.debounce, - pretender = require('helpers/create-pretender', null, null, false), - fixtures = require('fixtures/site-fixtures', null, null, false).default, - flushMap = require('discourse/models/store', null, null, false).flushMap, - ScrollingDOMMethods = require('discourse/mixins/scrolling', null, null, false).ScrollingDOMMethods, - _DiscourseURL = require('discourse/lib/url', null, null, false).default, - applyPretender = require('helpers/qunit-helpers', null, null, false).applyPretender, - server; + pretender = require("helpers/create-pretender", null, null, false), + fixtures = require("fixtures/site-fixtures", null, null, false).default, + flushMap = require("discourse/models/store", null, null, false).flushMap, + ScrollingDOMMethods = require("discourse/mixins/scrolling", null, null, false) + .ScrollingDOMMethods, + _DiscourseURL = require("discourse/lib/url", null, null, false).default, + applyPretender = require("helpers/qunit-helpers", null, null, false) + .applyPretender, + server; function dup(obj) { return jQuery.extend(true, {}, obj); } function resetSite(siteSettings, extras) { - var createStore = require('helpers/create-store').default; - var siteAttrs = $.extend({}, fixtures['site.json'].site, extras || {}); + var createStore = require("helpers/create-store").default; + var siteAttrs = $.extend({}, fixtures["site.json"].site, extras || {}); siteAttrs.store = createStore(); siteAttrs.siteSettings = siteSettings; Discourse.Site.resetCurrent(Discourse.Site.create(siteAttrs)); @@ -114,7 +120,7 @@ QUnit.testStart(function(ctx) { _DiscourseURL.redirectedTo = url; }; - var ps = require('preload-store').default; + var ps = require("preload-store").default; ps.reset(); window.sandbox = sinon.sandbox.create(); @@ -123,10 +129,10 @@ QUnit.testStart(function(ctx) { window.sandbox.stub(ScrollingDOMMethods, "unbindOnScroll"); // Unless we ever need to test this, let's leave it off. - $.fn.autocomplete = function() { }; + $.fn.autocomplete = function() {}; // Don't debounce in test unless we're testing debouncing - if (ctx.module.indexOf('debounce') === -1) { + if (ctx.module.indexOf("debounce") === -1) { Ember.run.debounce = Ember.run; } }); @@ -136,7 +142,7 @@ QUnit.testDone(function() { window.sandbox.restore(); // Destroy any modals - $('.modal-backdrop').remove(); + $(".modal-backdrop").remove(); flushMap(); server.shutdown(); @@ -151,18 +157,22 @@ window.controllerFor = helpers.controllerFor; window.fixture = helpers.fixture; function getUrlParameter(name) { - name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); - var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); - var results = regex.exec(location.search); - return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); -}; + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); + var results = regex.exec(location.search); + return results === null + ? "" + : decodeURIComponent(results[1].replace(/\+/g, " ")); +} -var skipCore = (getUrlParameter('qunit_skip_core') == '1'); -var pluginPath = getUrlParameter('qunit_single_plugin') ? "\/"+getUrlParameter('qunit_single_plugin')+"\/" : "\/plugins\/"; +var skipCore = getUrlParameter("qunit_skip_core") == "1"; +var pluginPath = getUrlParameter("qunit_single_plugin") + ? "/" + getUrlParameter("qunit_single_plugin") + "/" + : "/plugins/"; Object.keys(requirejs.entries).forEach(function(entry) { - var isTest = (/\-test/).test(entry); - var regex = new RegExp(pluginPath) + var isTest = /\-test/.test(entry); + var regex = new RegExp(pluginPath); var isPlugin = regex.test(entry); if (isTest && (!skipCore || isPlugin)) { From d7885559946840c7e18cd230aedc1c83aabca6b7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 7 Sep 2018 16:01:20 +0800 Subject: [PATCH 102/124] DEV: Manage pretender with yarn. --- .../javascripts/wizard/test/test_helper.js | 2 +- config/application.rb | 1 + package.json | 3 ++- test/javascripts/test_helper.js | 2 +- yarn.lock | 21 ++++++++++++++++--- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/wizard/test/test_helper.js b/app/assets/javascripts/wizard/test/test_helper.js index f2281cea0e..1d2ddbdb16 100644 --- a/app/assets/javascripts/wizard/test/test_helper.js +++ b/app/assets/javascripts/wizard/test/test_helper.js @@ -19,7 +19,7 @@ //= require locales/en //= require fake_xml_http_request //= require route-recognizer -//= require pretender +//= require pretender/pretender //= require ./wizard-pretender // Trick JSHint into allow document.write diff --git a/config/application.rb b/config/application.rb index 44599fa165..90218a83eb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -92,6 +92,7 @@ module Discourse if Rails.env == "development" || Rails.env == "test" config.assets.paths << "#{config.root}/test/javascripts" config.assets.paths << "#{config.root}/test/stylesheets" + config.assets.paths << "#{config.root}/node_modules" end # Allows us to skip minifincation on some files diff --git a/package.json b/package.json index 0fc51ebb72..549ba135a2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "chrome-remote-interface": "^0.25.6", "eslint": "^4.19.1", "prettier": "^1.13.0", - "puppeteer": "^1.4.0" + "puppeteer": "^1.4.0", + "pretender": "^1.6" } } diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index c24d425fdd..0e0f819935 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -11,7 +11,7 @@ //= require ember-qunit //= require fake_xml_http_request //= require route-recognizer -//= require pretender +//= require pretender/pretender //= require discourse-loader //= require preload-store diff --git a/yarn.lock b/yarn.lock index d908e1a096..098993b5d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -466,6 +466,10 @@ extract-zip@^1.6.5: mkdirp "0.5.0" yauzl "2.4.1" +fake-xml-http-request@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-1.6.0.tgz#bd0ac79ae3e2660098282048a12c730a6f64d550" + fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" @@ -813,9 +817,16 @@ prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" -prettier@1.13.4: - version "1.13.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.4.tgz#31bbae6990f13b1093187c731766a14036fa72e6" +pretender@^1.6: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pretender/-/pretender-1.6.1.tgz#77d1e42ac8c6b298f5cd43534a87645df035db8c" + dependencies: + fake-xml-http-request "^1.6.0" + route-recognizer "^0.3.3" + +prettier@^1.13.0: + version "1.14.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.2.tgz#0ac1c6e1a90baa22a62925f41963c841983282f9" process-nextick-args@~1.0.6: version "1.0.7" @@ -886,6 +897,10 @@ rimraf@^2.2.8, rimraf@^2.6.1: dependencies: glob "^7.0.5" +route-recognizer@^0.3.3: + version "0.3.4" + resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" From 81003a0f997044816356b7ebbe9be1663e5e1974 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 7 Sep 2018 16:24:18 +0800 Subject: [PATCH 103/124] Pin prettier to 1.13. --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 549ba135a2..c320e2c6f9 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "chrome-launcher": "^0.10.2", "chrome-remote-interface": "^0.25.6", "eslint": "^4.19.1", - "prettier": "^1.13.0", + "prettier": "^1.13", "puppeteer": "^1.4.0", "pretender": "^1.6" } diff --git a/yarn.lock b/yarn.lock index 098993b5d8..b9d97b2be1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -824,7 +824,7 @@ pretender@^1.6: fake-xml-http-request "^1.6.0" route-recognizer "^0.3.3" -prettier@^1.13.0: +prettier@^1.13: version "1.14.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.2.tgz#0ac1c6e1a90baa22a62925f41963c841983282f9" From fb96ab446466a28a8de7ce7f28cb5ca1e2e5cd1e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 7 Sep 2018 21:26:36 +0800 Subject: [PATCH 104/124] Delete pretender.js --- vendor/assets/javascripts/pretender.js | 476 ------------------------- 1 file changed, 476 deletions(-) delete mode 100644 vendor/assets/javascripts/pretender.js diff --git a/vendor/assets/javascripts/pretender.js b/vendor/assets/javascripts/pretender.js deleted file mode 100644 index c547ed2850..0000000000 --- a/vendor/assets/javascripts/pretender.js +++ /dev/null @@ -1,476 +0,0 @@ - (function(self) { -'use strict'; - -var appearsBrowserified = typeof self !== 'undefined' && - typeof process !== 'undefined' && - Object.prototype.toString.call(process) === '[object Object]'; - -var RouteRecognizer = appearsBrowserified ? require('route-recognizer') : self.RouteRecognizer; -var FakeXMLHttpRequest = appearsBrowserified ? require('fake-xml-http-request') : self.FakeXMLHttpRequest; - -/** - * parseURL - decompose a URL into its parts - * @param {String} url a URL - * @return {Object} parts of the URL, including the following - * - * 'https://www.yahoo.com:1234/mypage?test=yes#abc' - * - * { - * host: 'www.yahoo.com:1234', - * protocol: 'https:', - * search: '?test=yes', - * hash: '#abc', - * href: 'https://www.yahoo.com:1234/mypage?test=yes#abc', - * pathname: '/mypage', - * fullpath: '/mypage?test=yes' - * } - */ -function parseURL(url) { - // TODO: something for when document isn't present... #yolo - var anchor = document.createElement('a'); - anchor.href = url; - - if (!anchor.host) { - anchor.href = anchor.href; // IE: load the host and protocol - } - - var pathname = anchor.pathname; - if (pathname.charAt(0) !== '/') { - pathname = '/' + pathname; // IE: prepend leading slash - } - - var host = anchor.host; - if (anchor.port === '80' || anchor.port === '443') { - host = anchor.hostname; // IE: remove default port - } - - return { - host: host, - protocol: anchor.protocol, - search: anchor.search, - hash: anchor.hash, - href: anchor.href, - pathname: pathname, - fullpath: pathname + (anchor.search || '') + (anchor.hash || '') - }; -} - - -/** - * Registry - * - * A registry is a map of HTTP verbs to route recognizers. - */ - -function Registry(/* host */) { - // Herein we keep track of RouteRecognizer instances - // keyed by HTTP method. Feel free to add more as needed. - this.verbs = { - GET: new RouteRecognizer(), - PUT: new RouteRecognizer(), - POST: new RouteRecognizer(), - DELETE: new RouteRecognizer(), - PATCH: new RouteRecognizer(), - HEAD: new RouteRecognizer(), - OPTIONS: new RouteRecognizer() - }; -} - -/** - * Hosts - * - * a map of hosts to Registries, ultimately allowing - * a per-host-and-port, per HTTP verb lookup of RouteRecognizers - */ -function Hosts() { - this._registries = {}; -} - -/** - * Hosts#forURL - retrieve a map of HTTP verbs to RouteRecognizers - * for a given URL - * - * @param {String} url a URL - * @return {Registry} a map of HTTP verbs to RouteRecognizers - * corresponding to the provided URL's - * hostname and port - */ -Hosts.prototype.forURL = function(url) { - var host = parseURL(url).host; - var registry = this._registries[host]; - - if (registry === undefined) { - registry = (this._registries[host] = new Registry(host)); - } - - return registry.verbs; -}; - -function Pretender(/* routeMap1, routeMap2, ...*/) { - this.hosts = new Hosts(); - - this.handlers = []; - this.handledRequests = []; - this.passthroughRequests = []; - this.unhandledRequests = []; - this.requestReferences = []; - - // reference the native XMLHttpRequest object so - // it can be restored later - this._nativeXMLHttpRequest = self.XMLHttpRequest; - - // capture xhr requests, channeling them into - // the route map. - self.XMLHttpRequest = interceptor(this, this._nativeXMLHttpRequest); - - // 'start' the server - this.running = true; - - // trigger the route map DSL. - for (var i = 0; i < arguments.length; i++) { - this.map(arguments[i]); - } -} - -function interceptor(pretender, nativeRequest) { - function FakeRequest() { - // super() - FakeXMLHttpRequest.call(this); - } - // extend - var proto = new FakeXMLHttpRequest(); - proto.send = function send() { - if (!pretender.running) { - throw new Error('You shut down a Pretender instance while there was a pending request. ' + - 'That request just tried to complete. Check to see if you accidentally shut down ' + - 'a pretender earlier than you intended to'); - } - - FakeXMLHttpRequest.prototype.send.apply(this, arguments); - if (!pretender.checkPassthrough(this)) { - pretender.handleRequest(this); - } else { - var xhr = createPassthrough(this); - xhr.send.apply(xhr, arguments); - } - }; - - - function createPassthrough(fakeXHR) { - // event types to handle on the xhr - var evts = ['error', 'timeout', 'abort', 'readystatechange']; - - // event types to handle on the xhr.upload - var uploadEvents = []; - - // properties to copy from the native xhr to fake xhr - var lifecycleProps = ['readyState', 'responseText', 'responseXML', 'status', 'statusText']; - - var xhr = fakeXHR._passthroughRequest = new pretender._nativeXMLHttpRequest(); - - if (fakeXHR.responseType === 'arraybuffer') { - lifecycleProps = ['readyState', 'response', 'status', 'statusText']; - xhr.responseType = fakeXHR.responseType; - } - - // use onload if the browser supports it - if ('onload' in xhr) { - evts.push('load'); - } - - // add progress event for async calls - // avoid using progress events for sync calls, they will hang https://bugs.webkit.org/show_bug.cgi?id=40996. - if (fakeXHR.async && fakeXHR.responseType !== 'arraybuffer') { - evts.push('progress'); - uploadEvents.push('progress'); - } - - // update `propertyNames` properties from `fromXHR` to `toXHR` - function copyLifecycleProperties(propertyNames, fromXHR, toXHR) { - for (var i = 0; i < propertyNames.length; i++) { - var prop = propertyNames[i]; - if (fromXHR[prop]) { - toXHR[prop] = fromXHR[prop]; - } - } - } - - // fire fake event on `eventable` - function dispatchEvent(eventable, eventType, event) { - eventable.dispatchEvent(event); - if (eventable['on' + eventType]) { - eventable['on' + eventType](event); - } - } - - // set the on- handler on the native xhr for the given eventType - function createHandler(eventType) { - xhr['on' + eventType] = function(event) { - copyLifecycleProperties(lifecycleProps, xhr, fakeXHR); - dispatchEvent(fakeXHR, eventType, event); - }; - } - - // set the on- handler on the native xhr's `upload` property for - // the given eventType - function createUploadHandler(eventType) { - if (xhr.upload) { - xhr.upload['on' + eventType] = function(event) { - dispatchEvent(fakeXHR.upload, eventType, event); - }; - } - } - - xhr.open(fakeXHR.method, fakeXHR.url, fakeXHR.async, fakeXHR.username, fakeXHR.password); - - var i; - for (i = 0; i < evts.length; i++) { - createHandler(evts[i]); - } - for (i = 0; i < uploadEvents.length; i++) { - createUploadHandler(uploadEvents[i]); - } - - if (fakeXHR.async) { - xhr.timeout = fakeXHR.timeout; - xhr.withCredentials = fakeXHR.withCredentials; - } - for (var h in fakeXHR.requestHeaders) { - xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]); - } - return xhr; - } - - proto._passthroughCheck = function(method, args) { - if (this._passthroughRequest) { - return this._passthroughRequest[method].apply(this._passthroughRequest, args); - } - return FakeXMLHttpRequest.prototype[method].apply(this, args); - }; - - proto.abort = function abort() { - return this._passthroughCheck('abort', arguments); - }; - - proto.getResponseHeader = function getResponseHeader() { - return this._passthroughCheck('getResponseHeader', arguments); - }; - - proto.getAllResponseHeaders = function getAllResponseHeaders() { - return this._passthroughCheck('getAllResponseHeaders', arguments); - }; - - FakeRequest.prototype = proto; - - if (nativeRequest.prototype._passthroughCheck) { - console.warn('You created a second Pretender instance while there was already one running. ' + - 'Running two Pretender servers at once will lead to unexpected results and will ' + - 'be removed entirely in a future major version.' + - 'Please call .shutdown() on your instances when you no longer need them to respond.'); - } - return FakeRequest; -} - -function verbify(verb) { - return function(path, handler, async) { - return this.register(verb, path, handler, async); - }; -} - -function scheduleProgressEvent(request, startTime, totalTime) { - setTimeout(function() { - if (!request.aborted && !request.status) { - var ellapsedTime = new Date().getTime() - startTime.getTime(); - request.upload._progress(true, ellapsedTime, totalTime); - request._progress(true, ellapsedTime, totalTime); - scheduleProgressEvent(request, startTime, totalTime); - } - }, 50); -} - -function isArray(array) { - return Object.prototype.toString.call(array) === '[object Array]'; -} - -var PASSTHROUGH = {}; - -Pretender.prototype = { - get: verbify('GET'), - post: verbify('POST'), - put: verbify('PUT'), - 'delete': verbify('DELETE'), - patch: verbify('PATCH'), - head: verbify('HEAD'), - options: verbify('OPTIONS'), - map: function(maps) { - maps.call(this); - }, - register: function register(verb, url, handler, async) { - if (!handler) { - throw new Error('The function you tried passing to Pretender to handle ' + - verb + ' ' + url + ' is undefined or missing.'); - } - - handler.numberOfCalls = 0; - handler.async = async; - this.handlers.push(handler); - - var registry = this.hosts.forURL(url)[verb]; - - registry.add([{ - path: parseURL(url).fullpath, - handler: handler - }]); - - return handler; - }, - passthrough: PASSTHROUGH, - checkPassthrough: function checkPassthrough(request) { - var verb = request.method.toUpperCase(); - - var path = parseURL(request.url).fullpath; - - verb = verb.toUpperCase(); - - var recognized = this.hosts.forURL(request.url)[verb].recognize(path); - var match = recognized && recognized[0]; - if (match && match.handler === PASSTHROUGH) { - this.passthroughRequests.push(request); - this.passthroughRequest(verb, path, request); - return true; - } - - return false; - }, - handleRequest: function handleRequest(request) { - var verb = request.method.toUpperCase(); - var path = request.url; - - var handler = this._handlerFor(verb, path, request); - - if (handler) { - handler.handler.numberOfCalls++; - var async = handler.handler.async; - this.handledRequests.push(request); - - var pretender = this; - - var _handleRequest = function(statusHeadersAndBody) { - if (!isArray(statusHeadersAndBody)) { - var note = 'Remember to `return [status, headers, body];` in your route handler.'; - throw new Error('Nothing returned by handler for ' + path + '. ' + note); - } - - var status = statusHeadersAndBody[0], - headers = pretender.prepareHeaders(statusHeadersAndBody[1]), - body = pretender.prepareBody(statusHeadersAndBody[2], headers); - - pretender.handleResponse(request, async, function() { - request.respond(status, headers, body); - pretender.handledRequest(verb, path, request); - }); - }; - - try { - var result = handler.handler(request); - if (result && typeof result.then === 'function') { - // `result` is a promise, resolve it - result.then(function(resolvedResult) { - _handleRequest(resolvedResult); - }); - } else { - _handleRequest(result); - } - } catch (error) { - this.erroredRequest(verb, path, request, error); - this.resolve(request); - } - } else { - this.unhandledRequests.push(request); - this.unhandledRequest(verb, path, request); - } - }, - handleResponse: function handleResponse(request, strategy, callback) { - var delay = typeof strategy === 'function' ? strategy() : strategy; - delay = typeof delay === 'boolean' || typeof delay === 'number' ? delay : 0; - - if (delay === false) { - callback(); - } else { - var pretender = this; - pretender.requestReferences.push({ - request: request, - callback: callback - }); - - if (delay !== true) { - scheduleProgressEvent(request, new Date(), delay); - setTimeout(function() { - pretender.resolve(request); - }, delay); - } - } - }, - resolve: function resolve(request) { - for (var i = 0, len = this.requestReferences.length; i < len; i++) { - var res = this.requestReferences[i]; - if (res.request === request) { - res.callback(); - this.requestReferences.splice(i, 1); - break; - } - } - }, - requiresManualResolution: function(verb, path) { - var handler = this._handlerFor(verb.toUpperCase(), path, {}); - if (!handler) { return false; } - - var async = handler.handler.async; - return typeof async === 'function' ? async() === true : async === true; - }, - prepareBody: function(body) { return body; }, - prepareHeaders: function(headers) { return headers; }, - handledRequest: function(/* verb, path, request */) { /* no-op */}, - passthroughRequest: function(/* verb, path, request */) { /* no-op */}, - unhandledRequest: function(verb, path/*, request */) { - throw new Error('Pretender intercepted ' + verb + ' ' + - path + ' but no handler was defined for this type of request'); - }, - erroredRequest: function(verb, path, request, error) { - error.message = 'Pretender intercepted ' + verb + ' ' + - path + ' but encountered an error: ' + error.message; - throw error; - }, - _handlerFor: function(verb, url, request) { - var registry = this.hosts.forURL(url)[verb]; - var matches = registry.recognize(parseURL(url).fullpath); - - var match = matches ? matches[0] : null; - if (match) { - request.params = match.params; - request.queryParams = matches.queryParams; - } - - return match; - }, - shutdown: function shutdown() { - self.XMLHttpRequest = this._nativeXMLHttpRequest; - - // 'stop' the server - this.running = false; - } -}; - -Pretender.parseURL = parseURL; -Pretender.Hosts = Hosts; -Pretender.Registry = Registry; - -if (typeof module === 'object') { - module.exports = Pretender; -} else if (typeof define !== 'undefined') { - define('pretender', [], function() { - return Pretender; - }); -} -self.Pretender = Pretender; -}(self)); From 9e77fd8fc3707a9fe6285b11aaf0dc1b14080cf0 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 7 Sep 2018 10:03:30 -0400 Subject: [PATCH 105/124] FIX: wrong category links on subfolder install in rss feed for a category topic list --- app/controllers/list_controller.rb | 4 ++-- spec/requests/list_controller_spec.rb | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index a26aa6eff1..a5f297e6b1 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -219,8 +219,8 @@ class ListController < ApplicationController discourse_expires_in 1.minute @title = "#{@category.name} - #{SiteSetting.title}" - @link = "#{Discourse.base_url}#{@category.url}" - @atom_link = "#{Discourse.base_url}#{@category.url}.rss" + @link = "#{Discourse.base_url_no_prefix}#{@category.url}" + @atom_link = "#{Discourse.base_url_no_prefix}#{@category.url}.rss" @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" @topic_list = TopicQuery.new(current_user).list_new_in_category(@category) diff --git a/spec/requests/list_controller_spec.rb b/spec/requests/list_controller_spec.rb index e494bebf57..733ece50fd 100644 --- a/spec/requests/list_controller_spec.rb +++ b/spec/requests/list_controller_spec.rb @@ -393,6 +393,15 @@ RSpec.describe ListController do expect(response.status).to eq(200) expect(response.content_type).to eq('application/rss+xml') end + + it "renders RSS in subfolder correctly" do + GlobalSetting.stubs(:relative_url_root).returns('/forum') + Discourse.stubs(:base_uri).returns("/forum") + get "/c/#{category.slug}.rss" + expect(response.status).to eq(200) + expect(response.body).to_not include("/forum/forum") + expect(response.body).to include("http://test.localhost/forum/c/#{category.slug}") + end end describe "category default views" do From 2ad882113e6f0d32155b84788f352e9adc7373bc Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 7 Sep 2018 16:49:44 +0200 Subject: [PATCH 106/124] FIX: corrects top-referred and trending-search dates (#6372) --- .../admin-dashboard-next-general.js.es6 | 36 +++++++++++++++---- .../templates/dashboard_next_general.hbs | 4 ++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 index 84ad362327..687f66333e 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 @@ -23,11 +23,6 @@ export default Ember.Controller.extend(PeriodComputationMixin, { ), shouldDisplayDurability: Ember.computed.and("diskSpace"), - @computed - topReferredTopicsTopions() { - return { table: { total: false, limit: 8 } }; - }, - @computed activityMetrics() { return [ @@ -48,9 +43,38 @@ export default Ember.Controller.extend(PeriodComputationMixin, { }; }, + @computed + topReferredTopicsOptions() { + return { + table: { total: false, limit: 8 } + }; + }, + + @computed + topReferredTopicsFilters() { + return { + startDate: moment() + .subtract(2, "days") + .startOf("day"), + endDate: this.get("today") + }; + }, + + @computed + trendingSearchFilters() { + return { + startDate: moment() + .subtract(2, "days") + .startOf("day"), + endDate: this.get("today") + }; + }, + @computed trendingSearchOptions() { - return { table: { total: false, limit: 8 } }; + return { + table: { total: false, limit: 8 } + }; }, usersByTypeReport: staticReport("users_by_type"), diff --git a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs index c0ce8d74a1..98ca3f0304 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs @@ -154,12 +154,14 @@
{{admin-report + filters=topReferredTopicsFilters dataSourceName="top_referred_topics" - reportOptions=topReferredTopicsTopions}} + reportOptions=topReferredTopicsOptions}} {{admin-report dataSourceName="trending_search" reportOptions=trendingSearchOptions + filters=trendingSearchFilters isEnabled=logSearchQueriesEnabled disabledLabel="admin.dashboard.reports.trending_search.disabled"}} {{{i18n "admin.dashboard.reports.trending_search.more"}}} From afaa722c32feb3abe642ec114d258c7b1293e6ae Mon Sep 17 00:00:00 2001 From: Joshua Rosenfeld Date: Sun, 9 Sep 2018 13:41:26 -0400 Subject: [PATCH 107/124] sort official plugin list, remove duplicate entry `discourse-sitemap` was listed twice, sorted list to help avoid duplication --- lib/plugin/metadata.rb | 81 +++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb index da5389b7cb..7e69b0fbb1 100644 --- a/lib/plugin/metadata.rb +++ b/lib/plugin/metadata.rb @@ -4,61 +4,60 @@ module Plugin; end class Plugin::Metadata OFFICIAL_PLUGINS ||= Set.new([ + "Canned Replies", "customer-flair", "discourse-adplugin", + "discourse-affiliate", "discourse-akismet", + "discourse-assign", + "discourse-auto-deactivate", "discourse-backup-uploads-to-s3", + "discourse-bbcode", + "discourse-bbcode-color", "discourse-cakeday", - "Canned Replies", + "discourse-characters-required", + "discourse-chat-integration", + "discourse-checklist", + "discourse-crowd", "discourse-data-explorer", "discourse-details", + "discourse-etiquette", + "discourse-footnote", + "discourse-github-linkback", + "discourse-gradle-issue", + "discourse-invite-tokens", + "discourse-local-dates", + "discourse-math", + "discourse-moderator-attention", + "discourse-narrative-bot", "discourse-nginx-performance-report", + "discourse-no-bump", + "discourse-oauth2-basic", + "discourse-patreon", + "discourse-plugin-discord-auth", + "discourse-plugin-linkedin-auth", + "discourse-plugin-office365-auth", + "discourse-policy", + "discourse-presence", + "discourse-prometheus", "discourse-push-notifications", - "discourse-chat-integration", + "discourse-saved-searches", + "discourse-signatures", + "discourse-sitemap", "discourse-solved", - "Spoiler Alert!", - "staff-notes", + "discourse-staff-notes", + "discourse-styleguide", + "discourse-tooltips", + "discourse-translator", + "discourse-user-card-badges", + "discourse-voting", + "docker_manager", "GitHub badges", "lazyYT", "logster-rate-limit-checker", "poll", - "discourse-plugin-linkedin-auth", - "discourse-plugin-office365-auth", - "discourse-plugin-discord-auth", - "discourse-oauth2-basic", - "discourse-math", - "discourse-bbcode-color", - "discourse-bbcode", - "discourse-affiliate", - "discourse-translator", - "discourse-patreon", - "discourse-prometheus", - "discourse-assign", - "discourse-narrative-bot", - "discourse-presence", - "discourse-staff-notes", - "discourse-voting", - "docker_manager", - "discourse-signatures", - "discourse-local-dates", - "discourse-crowd", - "discourse-footnote", - "discourse-gradle-issue", - "discourse-no-bump", - "discourse-moderator-attention", - "discourse-sitemap", - "discourse-tooltips", - "discourse-styleguide", - "discourse-saved-searches", - "discourse-user-card-badges", - "discourse-policy", - "discourse-github-linkback", - "discourse-characters-required", - "discourse-auto-deactivate", - "discourse-invite-tokens", - "discourse-checklist", - "discourse-etiquette", - "discourse-sitemap" + "Spoiler Alert!", + "staff-notes" ]) FIELDS ||= [:name, :about, :version, :authors, :url, :required_version] From 04d26c65e219001f4fec6de829337017b087b119 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 10:10:39 +0800 Subject: [PATCH 108/124] Refactor `Upload.get_from_url` to check length of sha1. --- app/models/upload.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/upload.rb b/app/models/upload.rb index f035751098..00f17cbead 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -7,6 +7,8 @@ require_dependency "file_store/local_store" require_dependency "base62" class Upload < ActiveRecord::Base + SHA1_LENGTH = 40 + belongs_to :user has_many :post_uploads, dependent: :destroy @@ -154,10 +156,10 @@ class Upload < ActiveRecord::Base if url =~ /(upload:\/\/)?([a-zA-Z0-9]+)(\..*)?/ sha1 = Base62.decode($2).to_s(16) - if sha1.length > 40 + if sha1.length > SHA1_LENGTH nil else - sha1.rjust(40, '0') + sha1.rjust(SHA1_LENGTH, '0') end end end @@ -179,7 +181,7 @@ class Upload < ActiveRecord::Base return if data.blank? sha1 = data[2] upload = nil - upload = Upload.find_by(sha1: sha1) if sha1 + upload = Upload.find_by(sha1: sha1) if sha1&.length == SHA1_LENGTH upload || Upload.find_by("url LIKE ?", "%#{data[1]}") end From 849653759015fdf79874f0e0ba78cb51abf28768 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 15:14:30 +0800 Subject: [PATCH 109/124] Add `RECOVER_FROM_S3` to `uploads:list_posts_with_broken_images` rake task. --- lib/file_store/s3_store.rb | 2 ++ lib/tasks/uploads.rake | 49 +++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/file_store/s3_store.rb b/lib/file_store/s3_store.rb index 076ea2171a..aa444ba1fc 100644 --- a/lib/file_store/s3_store.rb +++ b/lib/file_store/s3_store.rb @@ -9,6 +9,8 @@ module FileStore class S3Store < BaseStore TOMBSTONE_PREFIX ||= "tombstone/" + attr_reader :s3_helper + def initialize(s3_helper = nil) @s3_helper = s3_helper || S3Helper.new(s3_bucket, TOMBSTONE_PREFIX) end diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 0cf836dfab..7d12991443 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -718,15 +718,17 @@ end task "uploads:list_posts_with_broken_images" => :environment do if ENV["RAILS_DB"] - list_broken_posts + list_broken_posts(recover_from_s3: ENV["RECOVER_MISSING"]) else RailsMultisite::ConnectionManagement.each_connection do |db| - list_broken_posts + list_broken_posts(recover_from_s3: ENV["RECOVER_MISSING"]) end end end -def list_broken_posts +def list_broken_posts(recover_from_s3: false) + object_keys = nil + Post.where("raw LIKE '%upload:\/\/%'").find_each do |post| begin begin @@ -743,6 +745,22 @@ def list_broken_posts if img["data-orig-src"] puts "#{post.full_url} #{img["data-orig-src"]}" + + if recover_from_s3 && Discourse.store.external? + object_keys ||= begin + s3_helper = Discourse.store.s3_helper + + s3_helper.list("original").map(&:key).concat( + s3_helper.list("#{FileStore::S3Store::TOMBSTONE_PREFIX}original").map(&:key) + ) + end + + recover_from_s3_by_sha1( + post: post, + sha1: Upload.sha1_from_short_url(img["data-orig-src"]), + object_keys: object_keys + ) + end end end rescue => e @@ -750,3 +768,28 @@ def list_broken_posts end end end + +def recover_from_s3_by_sha1(post:, sha1:, object_keys: []) + object_keys.each do |key| + if key =~ /#{sha1}/ + url = "https:#{SiteSetting.Upload.absolute_base_url}/#{key}" + + begin + tmp = FileHelper.download( + url, + max_file_size: SiteSetting.max_image_size_kb.kilobytes, + tmp_file_name: "recover_from_s3" + ) + + upload = UploadCreator.new( + tmp, + File.basename(key) + ).create_for(post.user_id) + + post.rebake! if upload.persisted? + ensure + tmp&.close + end + end + end +end From 0aca80e92ac3c85b5d76a668dfd694e5fa753393 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 15:16:29 +0800 Subject: [PATCH 110/124] Fixes to `uploads:list_posts_with_broken_images`. --- lib/tasks/uploads.rake | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 7d12991443..19473582c6 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -718,10 +718,10 @@ end task "uploads:list_posts_with_broken_images" => :environment do if ENV["RAILS_DB"] - list_broken_posts(recover_from_s3: ENV["RECOVER_MISSING"]) + list_broken_posts(recover_from_s3: ENV["RECOVER_FROM_S3"]) else RailsMultisite::ConnectionManagement.each_connection do |db| - list_broken_posts(recover_from_s3: ENV["RECOVER_MISSING"]) + list_broken_posts(recover_from_s3: ENV["RECOVER_FROM_S3"]) end end end @@ -781,12 +781,14 @@ def recover_from_s3_by_sha1(post:, sha1:, object_keys: []) tmp_file_name: "recover_from_s3" ) - upload = UploadCreator.new( - tmp, - File.basename(key) - ).create_for(post.user_id) + if tmp + upload = UploadCreator.new( + tmp, + File.basename(key) + ).create_for(post.user_id) - post.rebake! if upload.persisted? + post.rebake! if upload.persisted? + end ensure tmp&.close end From 8c374f339c01b8a4a5536a24df52e93d6a08f9f7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 15:30:28 +0800 Subject: [PATCH 111/124] DEV: Don't pin JS packages to a particular patch version. --- package.json | 10 +++---- yarn.lock | 79 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index c320e2c6f9..dbeab0e8e5 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,12 @@ "license": "MIT", "dependencies": {}, "devDependencies": { - "babel-eslint": "^8.2.3", - "chrome-launcher": "^0.10.2", - "chrome-remote-interface": "^0.25.6", - "eslint": "^4.19.1", + "babel-eslint": "^8.2", + "chrome-launcher": "^0.10", + "chrome-remote-interface": "^0.25", + "eslint": "^4.19", "prettier": "^1.13", - "puppeteer": "^1.4.0", + "puppeteer": "^1.4", "pretender": "^1.6" } } diff --git a/yarn.lock b/yarn.lock index b9d97b2be1..62f9d572c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -181,15 +181,15 @@ babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-eslint@^8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf" +babel-eslint@^8.2: + version "8.2.6" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.6.tgz#6270d0c73205628067c0f7ae1693a9e797acefd9" dependencies: "@babel/code-frame" "7.0.0-beta.44" "@babel/traverse" "7.0.0-beta.44" "@babel/types" "7.0.0-beta.44" babylon "7.0.0-beta.44" - eslint-scope "~3.7.1" + eslint-scope "3.7.1" eslint-visitor-keys "^1.0.0" babylon@7.0.0-beta.44: @@ -207,6 +207,10 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -239,7 +243,7 @@ chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" -chrome-launcher@^0.10.2: +chrome-launcher@^0.10: version "0.10.2" resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.10.2.tgz#f7d860ddec627b6f01015736b5ae1e33b3d165b1" dependencies: @@ -252,9 +256,9 @@ chrome-launcher@^0.10.2: mkdirp "0.5.1" rimraf "^2.6.1" -chrome-remote-interface@^0.25.6: - version "0.25.6" - resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.25.6.tgz#172bc82d87091a2ae90711a7b6c940da64ce32ef" +chrome-remote-interface@^0.25: + version "0.25.7" + resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.25.7.tgz#827e85fbef3cc561a9ef2404eb7eee355968c5bc" dependencies: commander "2.11.x" ws "3.3.x" @@ -295,7 +299,16 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@1.6.0, concat-stream@^1.6.0: +concat-stream@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" dependencies: @@ -363,7 +376,7 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -eslint-scope@^3.7.1, eslint-scope@~3.7.1: +eslint-scope@3.7.1, eslint-scope@^3.7.1: version "3.7.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" dependencies: @@ -374,9 +387,9 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@^4.19.1: +eslint@^4.19: version "4.19.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" + resolved "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" dependencies: ajv "^5.3.0" babel-code-frame "^6.22.0" @@ -457,13 +470,13 @@ external-editor@^2.0.4: iconv-lite "^0.4.17" tmp "^0.0.33" -extract-zip@^1.6.5: - version "1.6.6" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" +extract-zip@^1.6.6: + version "1.6.7" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" dependencies: - concat-stream "1.6.0" + concat-stream "1.6.2" debug "2.6.9" - mkdirp "0.5.0" + mkdirp "0.5.1" yauzl "2.4.1" fake-xml-http-request@^1.6.0: @@ -562,9 +575,9 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" -https-proxy-agent@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.1.1.tgz#a7ce4382a1ba8266ee848578778122d491260fd9" +https-proxy-agent@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" dependencies: agent-base "^4.1.0" debug "^3.1.0" @@ -728,12 +741,6 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -mkdirp@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" - dependencies: - minimist "0.0.8" - mkdirp@0.5.1, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -844,18 +851,18 @@ pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" -puppeteer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.4.0.tgz#437f0f3450d76e437185c0bf06f446e80f184692" +puppeteer@^1.4: + version "1.8.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.8.0.tgz#9e8bbd2f5448cc19cac220efc0512837104877ad" dependencies: debug "^3.1.0" - extract-zip "^1.6.5" - https-proxy-agent "^2.1.0" + extract-zip "^1.6.6" + https-proxy-agent "^2.2.1" mime "^2.0.3" progress "^2.0.0" proxy-from-env "^1.0.0" rimraf "^2.6.1" - ws "^3.0.0" + ws "^5.1.1" readable-stream@^2.2.2: version "2.3.3" @@ -1063,7 +1070,7 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@3.3.x, ws@^3.0.0: +ws@3.3.x: version "3.3.2" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.2.tgz#96c1d08b3fefda1d5c1e33700d3bfaa9be2d5608" dependencies: @@ -1071,6 +1078,12 @@ ws@3.3.x, ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" +ws@^5.1.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + dependencies: + async-limiter "~1.0.0" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" From 68572b8afc90c16a0b807eabcb262b25f1190a0c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 16:02:13 +0800 Subject: [PATCH 112/124] Print error messages on why upload fails to save. --- lib/tasks/uploads.rake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 19473582c6..b13b9f16b0 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -787,7 +787,11 @@ def recover_from_s3_by_sha1(post:, sha1:, object_keys: []) File.basename(key) ).create_for(post.user_id) - post.rebake! if upload.persisted? + if upload.persisted? + post.rebake! + else + puts "#{post.full_url}\n#{upload.errors.full_messages.join("\n")}" + end end ensure tmp&.close From df04e69cde6d2511bbf17a203e6b92e4996f9fc3 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 16:34:40 +0800 Subject: [PATCH 113/124] FIX: `S3Helper#list` creates incorrect prefix. --- lib/s3_helper.rb | 6 +++++- spec/components/s3_helper_spec.rb | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb index cf81491292..79b0302375 100644 --- a/lib/s3_helper.rb +++ b/lib/s3_helper.rb @@ -136,7 +136,11 @@ class S3Helper end def list(prefix = "") - s3_bucket.objects(prefix: "#{@s3_bucket_folder_path}/#{prefix}") + if @s3_bucket_folder_path.present? + prefix = File.join(@s3_bucket_folder_path, prefix) + end + + s3_bucket.objects(prefix: prefix) end def tag_file(key, tags) diff --git a/spec/components/s3_helper_spec.rb b/spec/components/s3_helper_spec.rb index 412f0805e2..9ef8dd9304 100644 --- a/spec/components/s3_helper_spec.rb +++ b/spec/components/s3_helper_spec.rb @@ -53,4 +53,18 @@ describe "S3Helper" do helper.update_tombstone_lifecycle(100) end + describe '#list' do + it 'creates the prefix correctly' do + { + 'some/bucket' => 'bucket/testing', + 'some' => 'testing' + }.each do |bucket_name, prefix| + s3_helper = S3Helper.new(bucket_name) + bucket = stub('s3_bucket') + s3_helper.expects(:s3_bucket).returns(bucket) + bucket.expects(:objects).with(prefix: prefix) + s3_helper.list('testing') + end + end + end end From d4080c020f834e424d8149b4c7917695993c819e Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 10 Sep 2018 10:40:19 +0200 Subject: [PATCH 114/124] FIX: sets trends to 7 days instead of 3 (#6379) --- .../admin/controllers/admin-dashboard-next-general.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 index 687f66333e..2de7b16f16 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 @@ -54,7 +54,7 @@ export default Ember.Controller.extend(PeriodComputationMixin, { topReferredTopicsFilters() { return { startDate: moment() - .subtract(2, "days") + .subtract(6, "days") .startOf("day"), endDate: this.get("today") }; @@ -64,7 +64,7 @@ export default Ember.Controller.extend(PeriodComputationMixin, { trendingSearchFilters() { return { startDate: moment() - .subtract(2, "days") + .subtract(6, "days") .startOf("day"), endDate: this.get("today") }; From 4a966c639dd4318eb8046982d42b24889c878a8e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 17:01:11 +0800 Subject: [PATCH 115/124] DEV: Update `uploads:list_posts_with_broken_images` to recover from tombstone. --- lib/s3_helper.rb | 4 ++-- lib/tasks/uploads.rake | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb index 79b0302375..45a29585d0 100644 --- a/lib/s3_helper.rb +++ b/lib/s3_helper.rb @@ -43,10 +43,10 @@ class S3Helper rescue Aws::S3::Errors::NoSuchKey end - def copy(source, destination) + def copy(source, destination, options: {}) s3_bucket .object(destination) - .copy_from(copy_source: File.join(@s3_bucket_name, source)) + .copy_from(options.merge(copy_source: File.join(@s3_bucket_name, source))) end # make sure we have a cors config for assets diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index b13b9f16b0..96943ac41f 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -772,6 +772,16 @@ end def recover_from_s3_by_sha1(post:, sha1:, object_keys: []) object_keys.each do |key| if key =~ /#{sha1}/ + tombstone_prefix = FileStore::S3Store::TOMBSTONE_PREFIX + + if key.starts_with?(tombstone_prefix) + Discourse.store.s3_helper.copy( + key, + key.sub(tombstone_prefix, ""), + options: { acl: "public-read" } + ) + end + url = "https:#{SiteSetting.Upload.absolute_base_url}/#{key}" begin @@ -790,7 +800,7 @@ def recover_from_s3_by_sha1(post:, sha1:, object_keys: []) if upload.persisted? post.rebake! else - puts "#{post.full_url}\n#{upload.errors.full_messages.join("\n")}" + puts "#{post.full_url}: #{upload.errors.full_messages.join(", ")}" end end ensure From 84fc7abb73acd7bb8bf936f2b87b4eec0e404359 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 10 Sep 2018 12:52:14 +0100 Subject: [PATCH 116/124] FIX: Allow `rake destroy:topics` to delete topics in sub-categories --- app/services/destroy_task.rb | 11 ++++++----- lib/tasks/destroy.rake | 8 +++++--- spec/services/destroy_task_spec.rb | 15 +++++++++++---- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/app/services/destroy_task.rb b/app/services/destroy_task.rb index 86768a79f7..aa5c8c86bd 100644 --- a/app/services/destroy_task.rb +++ b/app/services/destroy_task.rb @@ -2,12 +2,13 @@ # we are capturing all log output into a log array to return # to the rake task rather than using `puts` statements. class DestroyTask - def self.destroy_topics(category) - c = Category.find_by_slug(category) + def self.destroy_topics(category, parent_category = nil) + c = Category.find_by_slug(category, parent_category) log = [] - return "A category with the slug: #{category} could not be found" if c.nil? + descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category + return "A category with the slug: #{descriptive_slug} could not be found" if c.nil? topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1) - log << "There are #{topics.count} topics to delete in #{category} category" + log << "There are #{topics.count} topics to delete in #{descriptive_slug} category" topics.each do |topic| log << "Deleting #{topic.slug}..." first_post = topic.ordered_posts.first @@ -24,7 +25,7 @@ class DestroyTask categories = Category.all log = [] categories.each do |c| - log << destroy_topics(c.slug) + log << destroy_topics(c.slug, c.parent_category&.slug) end log end diff --git a/lib/tasks/destroy.rake b/lib/tasks/destroy.rake index c079674048..ffcf829df4 100644 --- a/lib/tasks/destroy.rake +++ b/lib/tasks/destroy.rake @@ -2,10 +2,12 @@ # content and users from your site, but keeping your site settings, # theme, and category structure. desc "Remove all topics in a category" -task "destroy:topics", [:category] => :environment do |t, args| +task "destroy:topics", [:category, :parent_category] => :environment do |t, args| category = args[:category] - puts "Going to delete all topics in the #{category} category" - puts log = DestroyTask.destroy_topics(category) + parent_category = args[:parent_category] + descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category + puts "Going to delete all topics in the #{descriptive_slug} category" + puts log = DestroyTask.destroy_topics(category, parent_category) end desc "Remove all topics in all categories" diff --git a/spec/services/destroy_task_spec.rb b/spec/services/destroy_task_spec.rb index afcb205250..697dd6b1fa 100644 --- a/spec/services/destroy_task_spec.rb +++ b/spec/services/destroy_task_spec.rb @@ -9,11 +9,18 @@ describe DestroyTask do let!(:c2) { Fabricate(:category) } let!(:t2) { Fabricate(:topic, category: c2) } let!(:p2) { Fabricate(:post, topic: t2) } + let!(:sc) { Fabricate(:category, parent_category: c) } + let!(:t3) { Fabricate(:topic, category: sc) } + let!(:p3) { Fabricate(:post, topic: t3) } it 'destroys all topics in a category' do - before_count = Topic.where(category_id: c.id).count - DestroyTask.destroy_topics(c.slug) - expect(Topic.where(category_id: c.id).count).to eq before_count - 1 + expect { DestroyTask.destroy_topics(c.slug) } + .to change { Topic.where(category_id: c.id).count }.by (-1) + end + + it 'destroys all topics in a sub category' do + expect { DestroyTask.destroy_topics(sc.slug, c.slug) } + .to change { Topic.where(category_id: sc.id).count }.by(-1) end it "doesn't destroy system topics" do @@ -23,7 +30,7 @@ describe DestroyTask do it 'destroys topics in all categories' do DestroyTask.destroy_topics_all_categories - expect(Post.where(topic_id: [t.id, t2.id]).count).to eq 0 + expect(Post.where(topic_id: [t.id, t2.id, t3.id]).count).to eq 0 end end From 94ff428571001904e6d427604e69a2f300bb42bc Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 20:07:11 +0800 Subject: [PATCH 117/124] Pass the right value to rake task. --- lib/tasks/uploads.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 96943ac41f..b190b3a5c8 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -718,10 +718,10 @@ end task "uploads:list_posts_with_broken_images" => :environment do if ENV["RAILS_DB"] - list_broken_posts(recover_from_s3: ENV["RECOVER_FROM_S3"]) + list_broken_posts(recover_from_s3: !!ENV["RECOVER_FROM_S3"]) else RailsMultisite::ConnectionManagement.each_connection do |db| - list_broken_posts(recover_from_s3: ENV["RECOVER_FROM_S3"]) + list_broken_posts(recover_from_s3: !!ENV["RECOVER_FROM_S3"]) end end end From 2b7e50cab8317b6e9935c284cf68326fe965a2d4 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 10 Sep 2018 11:25:41 -0400 Subject: [PATCH 118/124] Prevent fade-out from overlapping button in admin nav --- app/assets/stylesheets/common/admin/admin_base.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index b302b7834c..eba24bfd93 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -498,7 +498,7 @@ $mobile-breakpoint: 700px; content: ""; position: absolute; right: 0px; - width: 30px; + width: 15px; height: calc(100% - 5px); background: linear-gradient( to right, From 81c87df18a11e779ebb82e2555e972108ace4659 Mon Sep 17 00:00:00 2001 From: Rishabh Nambiar <5862206+rishabhnambiar@users.noreply.github.com> Date: Mon, 10 Sep 2018 21:50:51 +0530 Subject: [PATCH 119/124] FIX: don't raise an error on integer usernames --- lib/user_name_suggester.rb | 2 +- spec/components/user_name_suggester_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/user_name_suggester.rb b/lib/user_name_suggester.rb index 2069113cfd..afff6f1223 100644 --- a/lib/user_name_suggester.rb +++ b/lib/user_name_suggester.rb @@ -39,7 +39,7 @@ module UserNameSuggester end def self.sanitize_username(name) - name = ActiveSupport::Inflector.transliterate(name) + name = ActiveSupport::Inflector.transliterate(name.to_s) # 1. replace characters that aren't allowed with '_' name.gsub!(UsernameValidator::CONFUSING_EXTENSIONS, "_") name.gsub!(/[^\w.-]/, "_") diff --git a/spec/components/user_name_suggester_spec.rb b/spec/components/user_name_suggester_spec.rb index 6046aa6ef7..a9137d8b74 100644 --- a/spec/components/user_name_suggester_spec.rb +++ b/spec/components/user_name_suggester_spec.rb @@ -18,6 +18,10 @@ describe UserNameSuggester do expect(UserNameSuggester.suggest(nil)).to eq(nil) end + it "doesn't raise an error on integer username" do + expect(UserNameSuggester.suggest(999)).to eq('999') + end + it 'corrects weird characters' do expect(UserNameSuggester.suggest("Darth%^Vader")).to eq('Darth_Vader') end From 6afc86398c23e86e928120fd62f69915d58cddb0 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 10 Sep 2018 13:28:16 -0400 Subject: [PATCH 120/124] Update translations --- config/locales/client.ar.yml | 37 +-- config/locales/client.es.yml | 14 +- config/locales/client.fr.yml | 4 + config/locales/client.pl_PL.yml | 1 + config/locales/client.zh_TW.yml | 1 - config/locales/server.de.yml | 4 +- config/locales/server.es.yml | 9 +- config/locales/server.fr.yml | 225 +++++++++++++++++- config/locales/server.ro.yml | 7 + .../config/locales/server.es.yml | 2 + .../config/locales/server.fr.yml | 1 + 11 files changed, 281 insertions(+), 24 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 05d03a5828..ee39319494 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -212,7 +212,7 @@ ar: banner: enabled: 'اجعل هذا إعلانا %{when}. سوف يظهر اعلى جميع الصفحات حتى يتم الغاؤه بواسطة المستخدم.' disabled: 'أزل هذا الإعلان %{when}. لن يظهر بعد الآن في أعلى كلّ صفحة.' - topic_admin_menu: "صلاحيات المدير علي الموضوعات" + topic_admin_menu: "صلاحيات المدير علي المواضيع" wizard_required: "مرحبًا في نسختك الجديدة من دسكورس! فلنبدأ مع مُرشد الإعدادات ✨" emails_are_disabled: "لقد عطّل أحد المدراء الرّسائل الصادرة للجميع. لن تُرسل إشعارات عبر البريد الإلكتروني أيا كان نوعها." themes: @@ -287,7 +287,7 @@ ar: many: "{{count}} محرفا" other: "{{count}} حرف" suggested_topics: - title: "الموضوعات المقترحة" + title: "المواضيع المقترحة" pm_title: "رسائل مقترحة " about: simple_title: "عنّا" @@ -298,7 +298,7 @@ ar: stat: all_time: "منذ التأسيس" like_count: "الإعجابات" - topic_count: "الموضوعات" + topic_count: "المواضيع" post_count: "المنشورات" user_count: "الأعضاء" active_user_count: "الأعضاء النشطون" @@ -393,10 +393,10 @@ ar: likes_given: "المعطاة" likes_received: "المتلقاة" topics_entered: "المُشاهدة" - topics_entered_long: "الموضوعات المُشاهدة" + topics_entered_long: "المواضيع التي تمت مشاهدتها" time_read: "وقت القراءة" - topic_count: "الموضوعات" - topic_count_long: "الموضوعات المنشورة" + topic_count: "المواضيع" + topic_count_long: "المواضيع المنشورة" post_count: "الردود" post_count_long: "الردود المنشورة" no_results: "لا نتائج." @@ -490,7 +490,7 @@ ar: filter_placeholder: "اسم المستخدم" remove_owner: "حذف كمالك" owner: "المالك" - topics: "الموضوعات" + topics: "المواضيع" posts: "المنشورات" mentions: "الإشارات" messages: "الرسائل" @@ -531,7 +531,7 @@ ar: "1": "الإعجابات المعطاة" "2": "الإعجابات المتلقاة" "3": "العلامات المرجعية" - "4": "الموضوعات" + "4": "المواضيع" "5": "الردود" "6": "الردود" "7": "الإشارات" @@ -552,8 +552,8 @@ ar: apply_all: "تطبيق" position: "مكان" posts: "المنشورات" - topics: "الموضوعات" - latest: "آخر الموضوعات" + topics: "المواضيع" + latest: "آخر المواضيع" latest_by: "الاحدث بـ" toggle_ordering: "تبديل التحكم في الترتيب" subcategories: "أقسام فرعية" @@ -625,7 +625,7 @@ ar: dismiss_notifications_tooltip: "اجعل كل الإشعارات مقروءة" first_notification: "إشعارك الأول! قم بالضغط عليه للبدء." disable_jump_reply: "لا تنتقل إلى منشوري بعد النشر" - dynamic_favicon: "أظهر عدد الموضوعات الجديدة/المحدّثة في أيقونة المتصفح" + dynamic_favicon: "أظهر عدد المواضيع الجديدة/المحدّثة في أيقونة المتصفح" theme_default_on_all_devices: "اجعل هذة الواجهة افتراضية على جميع اجهزتي" external_links_in_new_tab: "فتح الروابط الخارجية في تبويب جديد" enable_quoting: "فعل خاصية إقتباس النصوص المظللة" @@ -651,7 +651,7 @@ ar: few_per_day: "أرسل لي رسالة لكل منشور جديد (تقريبا إثنتان يوميا)" tag_settings: "الأوسمة" watched_tags: "مراقب" - watched_tags_instructions: "ستراقب آليا كل الموضوعات التي تستخدم هذه الأوسمة. ستصلك إشعارات بالمنشورات و الموضوعات الجديدة، وسيظهر أيضا عدّاد للمنشورات الجديدة بجانب كل موضوع." + watched_tags_instructions: "ستراقب آليا كل المواضيع التي تستخدم هذه الأوسمة. ستصلك إشعارات بالمنشورات و الموضوعات الجديدة، وسيظهر أيضا عدّاد للمنشورات الجديدة بجانب كل موضوع." tracked_tags: "متابع" tracked_tags_instructions: "ستتابع آليا كل الموضوعات التي تستخدم هذه الأوسمة. وسيظهر أيضا عدّاد للمنشورات الجديدة بجانب كل موضوع." muted_tags: "مكتوم" @@ -1150,7 +1150,7 @@ ar: category_page_style: categories_only: "الأقسام فقط" categories_with_featured_topics: "أقسام ذات مواضيع مُميزة" - categories_and_latest_topics: "الأقسام و الموضوعات الأخيرة" + categories_and_latest_topics: "الأقسام والمواضيع الأخيرة" shortcut_modifier_key: shift: 'Shift' ctrl: 'Ctrl' @@ -1886,7 +1886,7 @@ ar: abandon: confirm: "أمتأكد من التخلي عن المنشور؟" no_value: "لا، أبقها" - yes_value: "نعم، لا أريدها" + yes_value: "نعم، لا أريده" via_email: "وصل هذا المنشور عبر البريد" via_auto_generated_email: "وصل هذا المنشور عبر بريد مولّد آلياً" whisper: "هذا المنشور سري خاص بالمشرفين" @@ -3598,7 +3598,7 @@ ar: topic_title: "موضوع" post_id: "رقم المشاركة" post_title: "مشاركة" - category_id: "رقم القسم" + category_id: "رقم التصنيف" category_title: "قسم" external_url: "رابط خارجي" delete_confirm: هل أنت متأكد من حذف هذا الرابط الثابت ؟ @@ -3613,7 +3613,8 @@ ar: next: "التالي" step: "%{current} من %{total}" upload: "رفع" - uploading: "يرفع..." + uploading: "جاري الرفع..." + upload_error: "عذرا ، حدث خطأ أثناء تحميل هذا الملف. حاول مرة اخرى." quit: "ربما لاحقا" invites: add_user: "أضف" @@ -3622,3 +3623,7 @@ ar: admin: "مدير" moderator: "مشرف" regular: "مستخدم عادي" + previews: + topic_title: "موضوع النقاش" + share_button: "شاركها" + reply_button: "رد" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 747b88f221..76e91ce598 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -781,6 +781,7 @@ es: title: "Introduce de nuevo la contraseña" auth_tokens: title: "Dispositivos utilizados recientemente" + title_logs: "Logs de Autenticación" ip_address: "Dirección IP" created: "Creado" first_seen: "Primera vez" @@ -1021,6 +1022,8 @@ es: hide_session: "Recordar mañana" hide_forever: "no, gracias" hidden_for_session: "Vale, te preguntaremos mañana. Recuerda que también puedes usar el botón 'Iniciar sesión' para crear una cuenta en cualquier momento." + intro: "¡Hola! Parece que estás disfrutando del debate, pero no tienes una cuenta registrada aún." + value_prop: "Cuando registras una cuenta, recordamos exactamente lo que has leído, para que puedas volver justo donde estabas leyendo. También recibes notificaciones, por aquí y por email, cuando alguien responde a tus mensajes. ¡También puedes darle a \"Me gusta\" a los mensajes para compartir amor! :heartpulse:" summary: enabled_description: "Estás viendo un resumen de este tema: los posts más interesantes determinados por la comunidad." description: "Hay {{replyCount}} respuestas." @@ -1106,7 +1109,7 @@ es: change_email: "Cambiar dirección de email" provide_new_email: "Poner un nuevo email, y te reenviaremos una confirmación de email." submit_new_email: "Actualizar Dirección de Email" - sent_activation_email_again: "Te hemos enviado otro e-mail de activación a {{currentemail}}. Podría tardar algunos minutos en llegar; asegúrate de revisar tu carpeta de spam." + sent_activation_email_again: "Te hemos enviado otro e-mail de activación a {{currentEmail}}. Podría tardar algunos minutos en llegar; asegúrate de revisar tu carpeta de spam." to_continue: "Por favor, inicia sesión" preferences: "Debes tener una sesión iniciada para cambiar tus preferencias de usuario." forgot: "No me acuerdo de los detalles de mi cuenta." @@ -1321,6 +1324,9 @@ es: shared_draft: label: "Borrador Compartido" desc: "Haz borrador al tema que será visible únicamente por el staff" + toggle_topic_bump: + label: "Alternar BUMP del tema" + desc: "Responder sin alterar la fecha dump del tema" notifications: tooltip: regular: @@ -1708,6 +1714,7 @@ es: reset_read: "Restablecer datos de lectura" make_public: "Convertir en tema público" make_private: "Crear Mensaje Personal" + reset_bump_date: "Resetear fecha Bump" feature: pin: "Destacar tema" unpin: "Dejar de destacar tema" @@ -2977,6 +2984,9 @@ es: custom_sections: "Personalizaciones:" theme_components: "Componentes del Theme" switch_component: "Convertir a tema" + switch_component_alert: "¿Seguro que quieres convertir este componente en theme? Esto lo convertirá en un theme independiente y se eliminará como un secundario de todos los themes." + switch_theme: "Hacer componente" + switch_theme_alert: "¿Seguro que quieres convertir este theme en componente? Se eliminará como principal de todos los componentes." uploads: "Subidos" no_uploads: "Puedes subir archivos asociados con tu theme como fuentes e imágenes " add_upload: "Agregar Subido" @@ -2999,6 +3009,7 @@ es: public_key: "Conceda la siguiente clave pública de acceso para el repositorio:" about_theme: "Acerca del Theme" license: "Licencia" + component_of: "Componente de:" update_to_latest: "Actualizar a lo último" check_for_updates: "Verificar por Actualizaciones" updating: "Actualizando..." @@ -3006,6 +3017,7 @@ es: add: "Agregar" theme_settings: "Ajustes del Theme" no_settings: "Este theme no tiene ajustes." + empty: "Sin items" commits_behind: one: "Theme está 1 commit detrás!" other: "Theme está {{count}} commits detrás!" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 6700aa5de7..41e5800218 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -1325,6 +1325,9 @@ fr: shared_draft: label: "Ebauche partagée" desc: "Créer ébauche d'un sujet qui ne sera visible qu'aux responsables" + toggle_topic_bump: + label: "Basculer l'actualisation des sujets" + desc: "Répondre sans modifier la date d'actualisation du sujet" notifications: tooltip: regular: @@ -1712,6 +1715,7 @@ fr: reset_read: "Réinitialiser les données de lecture" make_public: "Rendre le sujet public" make_private: "Transformer en message direct" + reset_bump_date: "Réinitialiser la date d'actualisation" feature: pin: "Épingler le sujet" unpin: "Désépingler le sujet" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index e634626789..bd231ab53b 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -2584,6 +2584,7 @@ pl_PL: page_views: "Wyświetlenia strony" page_views_short: "Wyświetlenia strony" show_traffic_report: "Pokaż szczegółowy raport ruchu" + activity_metrics: Metryka aktywności reports: today: "Dzisiaj" yesterday: "Wczoraj" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 78cb241fb3..01109362d7 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -164,7 +164,6 @@ zh_TW: guidelines: "守則" privacy_policy: "隱私權政策" privacy: "隱私" - terms_of_service: "服務條款" mobile_view: "手機版網站" desktop_view: "電腦版網站" you: "你" diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 437fa09f0b..22b21031a7 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -1267,8 +1267,8 @@ de: external_system_avatars_url: "URL des externen Profilbild-Dienstes. Erlaubte Platzhalter sind {username} {first_letter} {color} {size}" selectable_avatars_enabled: "Forciere Benutzer, ein Avatar aus der Liste auszuwählen." selectable_avatars: "Liste von Avataren, aus der Benutzer wählen können." - default_opengraph_image_url: "URL des standardmäßigen Open-Graph-Bildes." - twitter_summary_large_image_url: "URL des Bildes, das standardmäßig in Twitter-Cards angezeigt wird (sollte mindestens 280 Pixel breit und 150 Pixel hoch sein)." + default_opengraph_image_url: "Standard-OpenGraph-Bild, das verwendet wird, wenn die Seite kein anderes passendes Bild oder Seitenlogo hat." + twitter_summary_large_image_url: "Standard-Bild für die Twitter-Zusammenfassungskarte (sollte mindestens 280px breit und 150px hoch sein)." allow_all_attachments_for_group_messages: "Erlaube alle E-Mail-Anhänge für Gruppen-Nachrichten." png_to_jpg_quality: "Qualität der umgewandelten JPG-Datei (1 ist die niedrigste, 99 die beste Qualität, 100 deaktiviert die Funktion)." allow_staff_to_upload_any_file_in_pm: "Erlaube Team-Mitgliedern, in Nachrichten alle Dateien hochzuladen." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 31a4d40a47..d349691671 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -1214,6 +1214,8 @@ es: enable_google_oauth2_logins: "Habilita la autenticación con Google Oauth2. Este es el método de autenticación a la que Google da soporte actualmente. Requiere de una clave de cliente y una clave secreta." google_oauth2_client_id: "Client ID de tu aplicación Google." google_oauth2_client_secret: "Client secret de tu aplicación Google." + google_oauth2_prompt: "Una lista opcional delimitada por espacios de valores de cadena que especifica si el servidor de autorizaciones solicita al usuario la reautenticación y el consentimiento. Consulte https://developers.google.com/identity/protocols/OpenIDConnect#prompt para conocer los valores posibles." + google_oauth2_hd: "Un dominio opcional hospedado de Google Apps al que se limitará el inicio de sesión. Consulte https://developers.google.com/identity/protocols/OpenIDConnect#hd-param para más detalles." enable_twitter_logins: "Activar autenticación por Twitter, requiere una twitter_consumer_key y un twitter_consumer_secret" twitter_consumer_key: "Clave del consumidor para la autenticación de Twitter, registrado en https://apps.twitter.com/" twitter_consumer_secret: "Secreto del consumidor para la autenticación de Twitter, registrado en https://apps.twitter.com/" @@ -1234,6 +1236,8 @@ es: backup_frequency: "El número de días entre backups." enable_s3_backups: "Sube copias de seguridad a S3 cuando complete. IMPORTANTE: requiere credenciales validas de S3 puestas Archivos configuración." s3_backup_bucket: "El bucket remoto para mantener copias de seguridad. AVISO: Asegúrate de que es un bucket privado." + s3_endpoint: "El punto final se puede modificar para realizar una copia de seguridad en un servicio compatible con S3 como DigitalOcean Spaces o Minio. ADVERTENCIA: utilice el valor predeterminado si usa AWS S3" + s3_force_path_style: "Haga cumplir el direccionamiento de estilo de ruta para su punto final personalizado. IMPORTANTE: se requiere para usar cargas y copias de seguridad desde Minio." s3_disable_cleanup: "Desactivar el eliminado de backups de S3 cuando se eliminen de forma local." backup_time_of_day: "Hora UTC del día cuando debería ejecutarse el backup." backup_with_uploads: "Incluir archivos adjuntos en los backups programados. Desactivando esta opción tan solo se ejecutará una copia de seguridad de la base de datos." @@ -1283,8 +1287,8 @@ es: external_system_avatars_url: "Dirección URL del servicio externo para los avatares. Sustituciones permitidas: {username} {first_letter} {color} {size}" selectable_avatars_enabled: "Forzar a los usuarios a elegir una foto de perfil de la lista." selectable_avatars: "Lista de fotos de perfil de la cual los usuarios pueden elegir." - default_opengraph_image_url: "URL de la imagen opengraph por defecto." - twitter_summary_large_image_url: "URL de la imagen por defecto para la tarjeta resumen de Twitter (debería ser al menos de 280px de ancho y 150 px de alto)." + default_opengraph_image_url: "URL de la imagen opengraph por defecto, utilizado cuando la página no tiene otra imagen o logotipo del sitio adecuado." + twitter_summary_large_image_url: "URL de la imagen por defecto para la tarjeta resumen de Twitter (debería ser al menos de 280px de ancho y 150px de alto)." allow_all_attachments_for_group_messages: "Permitir todos los archivos adjuntos de email para los mensajes a grupos." png_to_jpg_quality: "Calidad del archivo JPG convertido (1 es calidad mínima, 99 es máxima calidad, 100 para deshabilitar conversión)." allow_staff_to_upload_any_file_in_pm: "Permitir a los miembros del staff subir cualquier archivo en MP." @@ -1406,6 +1410,7 @@ es: unsubscribe_via_email: "Permitir a los usuarios darse de baja de los emails respondiendo con el texto 'unsubscribe' en el asunto o el cuerpo del mensaje" unsubscribe_via_email_footer: "Adjuntar un enlace para darse de baja al pie de los emails enviados" delete_email_logs_after_days: "Eliminar logs de email después de (N) días. Si es 0 permanecerán de forma indefinida." + disallow_reply_by_email_after_days: "Deshabilitar la respuesta por email después de (N) días. Si es 0 quedará de forma indefinida." max_emails_per_day_per_user: "Máximo número de emails a enviar a los usuarios por día. Establece 0 para desactivar el límite" enable_staged_users: "Crear cuentas provisionales automáticamente al procesar emails entrantes." maximum_staged_users_per_email: "Máximo número de usuarios provisionales creados al procesar un email entrante." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 10a94707ec..f0c4fccea3 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -460,6 +460,27 @@ fr: Vous voulez peut-être fermer ce sujet via l'administration :wrench: (dans le coin supérieur droit et le bas) afin que des réponses ne s'accumulent pas après une annonce. lounge_welcome: title: "Bienvenue dans le salon" + body: |2 + + Félicitations ! :confetti_ball: + + Si vous voyez ce sujet, vous avez été promu **habitué** (niveau de confiance 3). + + Dorénavant vous pouvez … + + * Modifier le titre de n'importe quel sujet + * Modifier la catégorie de n'importe quel sujet + * Avoir des liens qui sont suivis ([automatic nofollow](http://fr.wikipedia.org/wiki/Nofollow) est ôté) + * Accéder à la catégorie privée Salon qui est réservée aux utilisateurs de niveau de confiance 3 et plus + * Cacher du spam avec un seul signalement + + Voilà la [liste des habitués actuels](/badges/3/regular). N'hésitez pas à dire bonjour. + + Merci pour votre contribution à notre communauté ! + + (Pour plus d'informations sur les niveaux de confiance, [voir ce sujet][trust]. Notez que seul les utilisateurs qui continuent de remplir les conditions gardent leur statut d'habitués.) + + [trust]: https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/ category: topic_prefix: "À propos de la catégorie %{category}" replace_paragraph: "(Remplacez ce premier paragraphe par une brève description de votre nouvelle catégorie. Ce guide apparaîtra dans la zone de sélection de la catégorie, alors essayez de rester en dessous de 200 caractères. **Cette catégorie n'apparaîtra pas sur la page des catégories jusqu'à ce que vous ayez modifié cette description ou créé des sujets.**" @@ -607,6 +628,17 @@ fr: email_login: invalid_token: "Désolé, ce lien de connexion courriel est trop vieux. Séléctionner le bouton 'Se connecter' et utiliser 'Mot de passe oublié' pour obtenir un nouveau lien." title: "Connexion courriel" + user_auth_tokens: + devices: + android: 'Appareil Android' + linux: 'Ordinateur Linux' + windows: 'Ordinateur Windows' + mac: 'Mac' + iphone: 'iPhone' + ipad: 'iPad' + ipod: 'iPod' + mobile: 'Appareil portable' + unknown: 'Appareil inconnu' change_email: confirmed: "Votre adresse de courriel a été mise à jour." please_continue: "Continuer vers %{site_name}" @@ -616,6 +648,8 @@ fr: authorizing_old: title: "Merci d'avoir confirmé votre adresse de courriel" description: "Nous envoyons un courriel sur votre nouvelle adresse pour confirmation." + associated_accounts: + revoke_failed: "Echec de révocation de votre compte avec %{provider_name}." activation: action: "Cliquer ici pour activer votre compte" already_done: "Désolé, ce lien de confirmation n'est plus valide. Votre compte est peut-être déjà activé ?" @@ -678,6 +712,7 @@ fr: self: "Vous n'avez aucune activité pour le moment." others: "Aucune activité." no_bookmarks: + self: "Vous n'avez pas de sujet dans vos signets; les signets vous permettent de retrouver rapidement des sujets d'intérêt." others: "Aucun signet." no_likes_given: self: "Vous n'avez aimé aucun message" @@ -685,19 +720,25 @@ fr: no_replies: self: "Vous n'avez répondu à aucun message." others: "Aucune réponse." + no_drafts: + self: "Vous n'avez pas d'ébauche; commencez à répondre à un sujet et la réponse sera automatiquement sauvegardée comme nouvelle ébauche." + others: "Vous n'avez pas la permission de consulter les ébauches de cet utilisateur." topic_flag_types: spam: title: 'Spam' description: 'Ce message est une publicité. Il n''est pas utile ou pertinent pour ce site, mais de nature promotionnelle.' long_form: 'signalé comme spam' + short_description: 'Ceci est une publicité' inappropriate: title: 'Inapproprié' description: 'Ce message contient du contenu qu''une personne raisonnable jugerait offensant, abusif ou en violation de la charte de notre communauté.' long_form: 'signalé comme inapproprié' + short_description: 'Une transgression de notre charte communautaire' notify_moderators: title: "Autre chose" description: 'Ce sujet demande l''attention des responsables d''après la charte de la communauté, les conditions générales de service ou pour une autre raison.' long_form: 'signalé pour modération' + short_description: 'Nécessite l''attention du staff pour une autre raison' email_title: 'Le sujet « %{title} » nécessite l''attention d''un modérateur' email_body: "%{link}\n\n%{message}" flagging: @@ -1016,6 +1057,7 @@ fr: search_recent_posts_size: "Combien de messages récents à garder dans l'index" log_search_queries: "Archiver les requêtes de recherche des utilisateurs" search_query_log_max_size: "Nombre maximum de requêtes de recherche à conserver" + search_query_log_max_retention_days: "Durée maximum de conservation de requêtes de recherche" allow_uncategorized_topics: "Autorise la création de sujets sans catégorie. ATTENTION : S'il existe des sujets non-catégorisés, vous devez les catégoriser avant de désactiver cette fonction." allow_duplicate_topic_titles: "Autoriser la création de sujet avec le même titre." unique_posts_mins: "Combien de temps avant qu'un utilisateur puisse poster le même contenu à nouveau" @@ -1117,6 +1159,7 @@ fr: share_links: "Choix des éléments qui doivent apparaître dans la fenêtre de partage, et leur ordre." site_contact_username: "Un pseudo de responsable valide pour envoyer tous les message automatiques. Si laissé vide, le compte système sera utilisé." send_welcome_message: "Envoyer à tous les nouveaux utilisateurs un message de bienvenue avec un guide de démarrage rapide." + send_tl1_welcome_message: "Envoyer à tous les utilisateurs de niveau de confiance 1 un message de bienvenue." suppress_reply_directly_below: "Ne pas afficher le panneau extensible des réponses d'un message quand la seule réponse est juste en dessous ce dernier." suppress_reply_directly_above: "Ne pas afficher 'en réponse à' sur un message quand la seule réponse est juste en dessus de ce dernier." suppress_reply_when_quoting: "Ne pas affiché le panneau \"En réponse à\" sur un message qui répond à une citation." @@ -1171,6 +1214,8 @@ fr: enable_google_oauth2_logins: "Activer l'authentification Google Oauth2. C'est la méthode d'authentification que Google supporte désormais. Nécessite une clé et une phrase secrète." google_oauth2_client_id: "Identifiante du client de votre application Google." google_oauth2_client_secret: "Clé secrète du client de votre application Google." + google_oauth2_prompt: "Liste facultative de valeurs de chaînes de caractères délimitées par des espaces, qui spécifie si le serveur d'autorisation invite l'utilisateur à se réauthentifier et à donner son consentement. Voir https://developers.google.com/identity/protocols/OpenIDConnect#prompt pour des valeurs possibles." + google_oauth2_hd: "Un domaine optionnel Google Apps Hosted auquel la connexion sera limitée. Voir https://developers.google.com/identity/protocols/OpenIDConnect#hd-param pour plus de détails." enable_twitter_logins: "Activer l'authentification Twitter, nécessite twitter_consumer_key et twitter_consumer_secret" twitter_consumer_key: "Clé consommateur pour l'authentification Twitter, enregistrée sur https://apps.twitter.com/" twitter_consumer_secret: "Secret consommateur pour l'authentification Twitter, enregistré sur http://dev.twitter.com" @@ -1191,6 +1236,8 @@ fr: backup_frequency: "Nombre de jours entre sauvegardes" enable_s3_backups: "Envoyer vos sauvegardes à S3 lorsqu'elles sont terminées. IMPORTANT : Vous devez avoir renseigné vos identifiants S3 dans les paramètres de fichiers." s3_backup_bucket: "Bucket distant qui contiendra les sauvegardes. ATTENTION: Vérifiez que c'est un bucket privé" + s3_endpoint: "Le terminal peut être modifié pour être sauvegardé sur un service compatible S3 comme DigitalOcean Spaces ou Minio. ATTENTION: Utiliser défaut si vous utilisez AWS S3" + s3_force_path_style: "Appliquez l'adressage de type chemin pour votre terminal personnalisé. IMPORTANT : Nécessaire pour utiliser les téléchargements et sauvegardes Minio." s3_disable_cleanup: "Désactiver la suppression des sauvegardes de S3 lors de leur suppression locale." backup_time_of_day: "Heure (UTC) de planification de la sauvegarde." backup_with_uploads: "Inclure les fichiers envoyés dans les sauvegardes. Si désactivé, seule la base de données sera sauvegardée." @@ -1216,6 +1263,8 @@ fr: max_topic_invitations_per_day: "Nombre maximum d'invitations à un sujet qu'un utilisateur peut envoyer par jour." max_logins_per_ip_per_hour: "Nombre maximum de connexions autorisées par adresse IP et par heure" max_logins_per_ip_per_minute: "Nombre maximum de connexions autorisées par adresse IP, par minute" + max_post_deletions_per_minute: "Nombre maximum de sujet qu'un utilisateur peut supprimer par minute." + max_post_deletions_per_day: "Nombre maximum de sujet qu'un utilisateur peut supprimer par jour." alert_admins_if_errors_per_minute: "Nombre d'erreurs par minute nécessaires pour déclencher une alerte administrateur. Une valeur de 0 désactive cette fonctionnalité. N. B. : nécessite un redémarrage." alert_admins_if_errors_per_hour: "Nombre d'erreurs par heure nécessaires pour déclencher une alerte administrateur. Une valeur de 0 désactive cette fonctionnalité. N. B. : nécessite un redémarrage." categories_topics: "Nombre de sujets à afficher dans la page /categories." @@ -1236,8 +1285,10 @@ fr: avatar_sizes: "Liste des tailles des avatars automatiquement générés" external_system_avatars_enabled: "Utilisez un service d'avatars externe." external_system_avatars_url: "URL du service d'avatars externe. Les remplacements autorisés sont {username} {first_letter} {color} {size}" - default_opengraph_image_url: "URL de l'image par défaut pour les balises Open Graph." - twitter_summary_large_image_url: "URL de l'image par défaut de la carte de résumé Twitter (devrait au moins mesurer 280px en largeur et 150px en hauteur). " + selectable_avatars_enabled: "Obliger les utilisateurs à choisir un avatar de la liste." + selectable_avatars: "Liste d'avatars parmi lesquels les utilisateurs peuvent choisir." + default_opengraph_image_url: "Image opengraph par défaut, utilisée quand la page n'a pas d'autre image or logo de site approprié." + twitter_summary_large_image_url: "Image par défaut de la carte de résumé Twitter (devrait au moins mesurer 280px en largeur et 150px en hauteur). " allow_all_attachments_for_group_messages: "Autorise toutes les pièces-jointes pour les messages de groupes." png_to_jpg_quality: "Qualité du fichier JPEG converti (1 est la plus faible, 99 est la meilleure, 100 pour désactiver)." allow_staff_to_upload_any_file_in_pm: "Autoriser les responsables à envoyer n'importe quel fichier dans les messages directs." @@ -1359,6 +1410,7 @@ fr: unsubscribe_via_email: "Autorise les utilisateurs à se désinscrire des courriels en envoyant un courriel avec \"unsubscribe\" dans le sujet ou le corps du message." unsubscribe_via_email_footer: "Inclure un lien dans le pied des courriels envoyés pour se désabonner" delete_email_logs_after_days: "Efface les journaux de messagerie après (N) jours. 0 pour conserver indéfiniment." + disallow_reply_by_email_after_days: "Interdire la réponse par courriel après (N) jours. 0 pour permettre indéfiniment." max_emails_per_day_per_user: "Nombre maximum de courriels à envoyer aux utilisateurs par jour. 0 pour désactiver la limite" enable_staged_users: "Créer automatiquement les utilisateurs distants lors du traitement des courriels entrants." maximum_staged_users_per_email: "Nombre maximum d'utilisateurs distants créés lors du traitement d'un courriel entrant." @@ -1385,9 +1437,11 @@ fr: pop3_polling_host: "L'hôte pour le pooling des courriels via POP3." pop3_polling_username: "Le nom d'utilisateur du compte POP3 pour le pooling des courriels." pop3_polling_password: "Le mot de passe du compte POP3 pour le pooling des courriels." + pop3_polling_delete_from_server: "Courriels supprimés du serveur. NOTE: Si vous désactivez ceci vous devrez vider manuellement votre boîte de réception courriel." log_mail_processing_failures: "Enregistrer tous les problèmes de mails sur\nhttp://yoursitename.com/logs" email_in: "Autoriser les utilisateurs à poster de nouveaux sujets par courriel (requêtes manuelles ou via pop3 requises). Configurer les adresses dans l'onglet \"Paramètres\" de chaque catégorie." email_in_min_trust: "Le niveau de confiance minimum qu'un utilisateur doit avoir pour être autorisé à poster de nouveaux sujets par courriel." + email_in_spam_header: "Entête courriel pour détecter du spam." email_prefix: "Le [label] qui sera utilisé dans le sujet des courriels. Par défaut il prend la valeur de 'title'." email_site_title: "Le titre du site utilisé comme expéditeur pour les courriels du site. Par défaut il prend la valeur de 'title'. Si votre 'title\" utilise des caractères interdits dans les courriels, utiliser ce paramètre." find_related_post_with_key: "N'utilisez que le 'reply key' pour trouver le message auquel on a répondu. ATTENTION : la désactivation de cette option permet l'usurpation d'identité de l'utilisateur sur la base de l'adresse e-mail." @@ -1435,6 +1489,7 @@ fr: dominating_topic_minimum_percent: "Quel est le pourcentage de messages un utilisateur doit poster dans un sujet avant d'être rappelé à l'ordre pour laissé la communauté répondre." disable_avatar_education_message: "Désactiver le message incitant à changer l'avatar." suppress_uncategorized_badge: "Ne pas afficher le badge pour les sujets non catégorisés dans les listes des sujets." + header_dropdown_category_count: "Nombre de catégories qui peuvent être affiché dans le menu déroulant d'en-tête." permalink_normalizations: "Appliquer l'expression régulière suivante avant de détecter les permaliens, par exemple /(\\/topic.*)\\?.*/\\1 supprimera les chaînes de requête des chemins de sujet. Le format est regex+string, utilisez \\1 etc. pour capturer des séquences" global_notice: "Affiche un bandeau global URGENT pour tout les utilisateurs du site, laissez vide pour le cacher (HTML autorisé)." disable_edit_notifications: "Désactiver les notifications de modifications par l'utilisateur système lorsque l'option 'download_remote_images_to_local' est activée." @@ -1469,6 +1524,7 @@ fr: enable_emoji_shortcuts: "Les textes de smiley courants :) :p :( seront convertis en emojis" emoji_set: "Comment aimeriez-vous vos emoji ?" enforce_square_emoji: "Forcer tous les Emojis à être carrés." + emoji_autocomplete_min_chars: "Nombre minimum de caractères nécessaires pour invoquer la fenêtre contextuelle d'emoji" approve_post_count: "Le nombre de messages d'un utilisateur nouveau ou basique devant être approuvés" approve_unless_trust_level: "Les messages des utilisateurs qui n'ont pas atteint ce niveau de confiance doivent être approuvés" approve_new_topics_unless_trust_level: "Les nouveaux sujets des utilisateurs en dessous de ce niveau de confiance doivent être approuvés" @@ -1503,12 +1559,14 @@ fr: default_categories_muted: "Liste de catégories silencées par défaut." default_categories_watching_first_post: "Liste des catégories dont le premier message de chaque nouveau sujet sera surveillé par défaut." retain_web_hook_events_period_days: "Nombre de jours de conservation des événement des Web hooks." + retry_web_hook_events: "Réessayez automatiquement 4 fois les événements de hook web qui ont échoué. Les intervalles entre les tentatives sont de 1, 5, 25 et 125 minutes." allow_user_api_keys: "Autoriser la génération des clés de l'API utilisateur" allow_user_api_key_scopes: "Liste des champs d'action autorisés pour les clés de l'API utilisateur" max_api_keys_per_user: "Nombre maximum de clés de l'API utilisateur par utilisateur" min_trust_level_for_user_api_key: "Niveau de confiance requis pour générer des clés pour l'API utilisateur" allowed_user_api_auth_redirects: "URL autorisées pour la redirection d'authentification pour les clés de l'API utilisateur" allowed_user_api_push_urls: "URL autorisées pour le service push du serveur vers l'API utilisateur" + expire_user_api_keys_days: "Nombre de jours avant l'expiration automatique d'une clé API utilisateur (0 pour jamais)" tagging_enabled: "Activer les tags sur les sujets ?" min_trust_to_create_tag: "Le niveau de confiance minimum requis pour créer un tag." max_tags_per_topic: "Le nombre maximum de tags pouvant être ajoutés à un sujet." @@ -1594,6 +1652,8 @@ fr: existing_topic_moderator_post: one: "Un message a été intégré dans un sujet existant : %{topic_link}" other: "%{count} messages ont été intégrés dans un sujet existant : %{topic_link}" + change_owner: + post_revision_text: "Propriété transférée" topic_statuses: archived_enabled: "Ce sujet est maintenant archivé. Il est gelé et ne peut plus être modifié d'aucune façon." archived_disabled: "Ce sujet est maintenant désarchivé. Il n'est plus figé et peut être modifié." @@ -1678,6 +1738,17 @@ fr: already_logged_in: "Oups, on dirait que vous essayez d'accepter une invitation d'un autre utilisateur. Si vous n'êtes pas %{current_user}, veuillez vous déconnecter et réessayer." second_factor_title: "Authentification à deux facteurs" second_factor_description: "Veuillez saisir le code d'authentification requise de votre app :" + second_factor_backup_description: "Veuillez entrer un de vos codes de secours :" + second_factor_backup_title: "Code de secours de l'authentification à deux étapes" + invalid_second_factor_code: "Code d'authentification invalide. Chaque code ne peut être utilisé qu'une fois." + second_factor_toggle: + totp: "Utilisez plutôt une application d'authentification" + backup_code: "Utilisez plutôt un code de secours" + admin: + email: + sent_test: "envoyé !" + sent_test_disabled: "impossible d'envoyer, les courriels sont désactivés" + sent_test_disabled_for_non_staff: "impossible d'envoyer, les courriels sont désactivés sauf pour les responsables" user: deactivated: "A été désactivé à cause de trop de courriels rejetés vers '%{email}'." deactivated_by_staff: "Désactivé par un responsable" @@ -1951,6 +2022,27 @@ fr: Pour plus d'informations, merci de vous référer à notre [charte communautaire](%{base_url}/guidelines). + flags_agreed_and_post_deleted: + title: "Message signalé, retiré par un responsable" + subject_template: "Message signalé, retiré par un responsable" + text_body_template: |+ + Bonjour, + + Ceci est un message automatique de %{site_name} pour vous informer que votre message a été caché. + + <%{base_url}%{url} > + + %{flag_reason} + + Les message a été signalé par la communauté et un responsable a décidé de le retirer. + + Pour plus d'informations, merci de vous référer à notre [charte communautaire](%{base_url}/guidelines). + + usage_tips: + text_body_template: | + Pour connaître quelques astuces pour vous aider à démarrer en tant que nouveau membre, [rendez-vous sur cette page](https://meta.discourse.org/t/fr-trucs-et-astuces-pour-nouveaux-utilisateurs/84100/). + + Au fur et à mesure que vous participerez ici, nous apprendrons à vous connaître et les limitations temporaires des nouveaux utilisateurs seront levées. Avec le temps, vous gagnerez [des niveaux de confiance](https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/) (en anglais) qui incluent des fonctionnalités spéciales pour nous aider à gérer notre communauté ensemble. welcome_user: title: "Bienvenue Utilisateur" subject_template: "Bienvenue sur %{site_name} !" @@ -1962,6 +2054,13 @@ fr: Nous croyons au [comportement civilisé de la communauté](%{base_url}/guidelines) en tout temps. Amusez-vous bien ! + welcome_tl1_user: + title: "Bienvenue Utilisateur" + subject_template: "Merci de passer du temps avec nous" + text_body_template: | + Salut ! Nous avons vu que vous lisez beaucoup, ce qui est super, alors nous vous avons monté d'un [niveau de confiance !](https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/) + + Nous sommes ravis que vous passez du temps avec nous, et on adorerez en savoir plus sur vous. Prenez un moment pour [remplir votre profil](/my/preferences/profile), ou sentez-vous libre de [démarrer un nouveau sujet](/categories). welcome_invite: title: "Bienvenue invité" subject_template: "Bienvenue sur %{site_name} !" @@ -2159,6 +2258,13 @@ fr: %{post_error} Si vous pouvez corriger les erreurs, veuillez réessayer. + email_reject_post_too_short: + title: "Courriel rejeté trop court" + subject_template: "[%{email_prefix}] Problème de courriel -- Message trop court" + text_body_template: | + Nous sommes désolés, mais l'envoi de votre courriel à %{destination} (intitulé %{former_title}) n'a pas fonctionné. + + Afin de favoriser des discussions plus approfondies, les réponses très courtes ne sont pas permises. Est-ce que vous pouvez reformuler votre réponse avec au moins %{count} caractères ? Alternativement, vous pouvez aimer un message par courriel en répondant "+1". email_reject_invalid_post_action: title: "Courriel rejeté - Message action invalide" subject_template: "[%{email_prefix}] Problème de courriel -- Action de message invalide" @@ -2183,6 +2289,10 @@ fr: email_reject_old_destination: title: "Courriel rejeté - Mauvaise ancienne destination" subject_template: "[%{email_prefix}] Problème de courriel -- Vous essayez de répondre à une ancienne notification" + text_body_template: | + Nous sommes désolé, mais l'envoi de votre courriel à destination de %{destination} (intitulé %{former_title}) n'a pas fonctionné. + + Pour des raisons de sécurité, nous n'acceptons des réponses aux notifications que pendant %{number_of_days} jours. Veuillez vous rendre sur [le sujet](%{short_url}) pour continuer la conversation. email_reject_topic_not_found: title: "Courriel rejeté - Sujet introuvable" subject_template: "[%{email_prefix}] Problème de courriel -- Sujet introuvable" @@ -2220,9 +2330,24 @@ fr: Assurez-vous que les paramètres de connexion POP sont corrects dans les [paramètres du site](%{base_url}/admin/site_settings/category/email). S'il y a une interface web pour le compte POP, vous devrez peut-être vous y connecter pour vérifier les paramètres. + email_revoked: + title: "Courriel revoqué" + subject_template: "Est-ce que votre adresse courriel est correct ?" + text_body_template: | + Nous sommes désolés, mais nous avons de la difficulté à vous joindre par courriel. Nos derniers courriels vous ont tous été renvoyés comme non livrables. + + Pouvez-vous vous assurer que [votre adresse email](%{base_url}/my/preferences/email) est valide et fonctionne ? Vous pouvez également ajouter notre adresse e-mail à votre carnet d'adresses / liste de contacts pour améliorer la délivrabilité. too_many_spam_flags: title: "Trop de signalements de spam" subject_template: "Nouveau compte bloqué" + text_body_template: | + Bonjour, + + Ceci est un message automatique de %{site_name} pour vous informer que vos messages ont été temporairement masqués suite à des signalements de la communauté. + + Par mesure de précaution, votre nouveau compte a été mis sous silence et vous ne pourrez plus créer de nouvelles réponses et sujets tant qu'un responsable n'a pas vérifié votre compte. Veuillez nous excuser pour la gêne occasionnée. + + Pour plus d'informations, merci de vous référer à la [charte de la communauté](%{base_url}/guidelines). too_many_tl3_flags: title: "Trop de flags TL3" subject_template: "Nouveau compte bloqué" @@ -2690,6 +2815,7 @@ fr: recent_topics: "Récents" see_more: "Plus" search_title: "Rechercher dans ce site" + search_button: "Rechercher" offline: title: "Impossible de charger l'application" offline_page_message: "Il semble que vous n'êtes pas connectés à Internet. Veuillez vérifier votre connexion réseau puis réessayez." @@ -2723,11 +2849,45 @@ fr: flag_reason: sockpuppet: "Un nouvel utilisateur a créé un sujet et un autre nouvel utilisateur avec la même adresse IP (%{ip_address}) a répondu. Voir le paramètre `flag_sockpuppets`." spam_hosts: "Ce nouvel utilisateur a tenté de créer plusieurs messages avec des liens vers le même domaine (%{domain}). Voir le paramètre `newuser_spam_host_threshold`." + skipped_email_log: + exceeded_emails_limit: "max_emails_per_day_per_user dépassé" + exceeded_bounces_limit: "bounce_score_threshold dépassé" + mailing_list_no_echo_mode: "Notifications de la liste de diffusion désactivées pour les messages de l'utilisateur." + user_email_no_user: "Impossible de trouver l'utilisateur avec l'id %{user_id}" + user_email_post_not_found: "Impossible de trouver le message avec l'id %{post_id}" + user_email_anonymous_user: "L'utilisateur est anonyme" + user_email_user_suspended_not_pm: "L'utilisateur est suspendu, pas un message" + user_email_seen_recently: "L'utilisateur a été vu récemment" + user_email_notification_already_read: "La notification de ce courriel a déjà été lue" + user_email_notification_topic_nil: "post.topic vaut nil" + user_email_post_user_deleted: "L'auteur du message a été supprimé." + user_email_post_deleted: "le message a été supprimé son auteur" + user_email_user_suspended: "l'utilisateur a été suspendu" + user_email_already_read: "l'utilisateur a déjà lu ce message" + sender_message_blank: "le message est vide" + sender_message_to_blank: "message.to est vide" + sender_text_part_body_blank: "text_part.body est vide" + sender_body_blank: "sans contenu" + sender_post_deleted: "le message a été supprimé" color_schemes: base_theme_name: "Base" + light: "Palette claire" dark: "Palette sombre" + neutral: "Palette neutre" + grey_amber: "Palette gris ambre" + shades_of_blue: "Palette nuances de bleu" + latte: "Palette latte" + summer: "Palette été" + dark_rose: "Palette rose foncé" + default_theme_name: "Clair" light_theme_name: "Clair" dark_theme_name: "Sombre" + neutral_theme_name: "Neutre" + grey_amber_theme_name: "Gris ambre" + shades_of_blue_theme_name: "Nuances de bleu" + latte_theme_name: "Latte" + summer_theme_name: "Eté" + dark_rose_theme_name: "Rose foncé" about: "À propos" guidelines: "Charte" privacy: "Confidentialité" @@ -3062,41 +3222,73 @@ fr: editor: name: Éditeur description: Première modification d'un message + long_description: | + Ce badge est accordé après la première modification d'un de vos messages. Bien que la possibilité d'éditer un message soit limitée dans le temps, l'édition est toujours encouragée — vous pouvez améliorer les messages, réparer des petites erreurs, ou ajouter des éléments que vous avez oubliés lors de la rédaction. Éditer vos messages pour les rendre encore meilleurs ! basic_user: name: Actif + description: "Accès accordé à toutes les fonctions communautaires essentielles" + long_description: | + Ce badge est accordé lorsque vous atteignez le niveau de confiance 1. Merci d'être resté dans le coin et d'avoir lu quelques sujets pour en apprendre plus sur notre communauté. Les restrictions "nouvel utilisateur" ont été levées, et vous avez accès aux fonctionnalités essentielles telles que la messagerie personnelle, le signalement, l'édition des wikis, et la possibilité de poster des images et de multiples liens. member: name: Membre + description: "Accès accordé aux invitations, aux messages de groupe et à plus de J'aime" + long_description: | + Ce badge est accordé lorsque vous atteignez le niveau de confiance 2. Merci d'avoir participé durant plusieurs semaines à notre communauté. Vous pouvez désormais envoyer des invitations personnelles depuis votre page utilisateur ou un sujet, envoyer des messages groupés, et avez des J'aime supplémentaires chaque jour. regular: name: Habitué + description: "Accès accordé à la re-catégorisation, au renommage, au suivi de liens, au Wiki et à plus de J'aime" + long_description: | + Ce badge est accordé lorsque vous atteignez le niveau de confiance 3. Merci d'avoir été un participant régulier à notre communauté pendant ces quelques mois, l'un de nos lecteurs les plus actifs et un contributeur sérieux à ce qui rend notre communauté si belle. Vous pouvez désormais re-catégoriser et renommer des sujets, accéder à la section privée, signaler des spams, et vous avez plein de J'aime en plus chaque jour. leader: name: Meneur + description: "Accès accordé à l'édition globale, l'épinglage, la fermeture, l'archivage, la séparation et la fusion de sujets, et toujours plus de J'aime" + long_description: | + Ce badge est accordé lorsque vous atteignez le niveau de confiance 4. Vous êtes un meneur choisi par l'équipe dans cette communauté, et vous montrez l'exemple dans vos actions et vos mots. Vous avez la capacité de modifier tous les messages, utiliser les actions de modérations telles qu'épingler, fermer, cacher, archiver, scinder et fusionner. welcome: name: Bienvenue description: A reçu un J'aime + long_description: | + Ce badge est accordé lorsque vous recevez votre premier J'aime sur un de vos messages. Félicitations, vous avez écrit quelque chose que les membres de votre communauté ont trouvé intéressant, cool, ou utile ! autobiographer: name: Autobiographe description: "A complété les informations de son profile" + long_description: | + Ce badge est accordé pour avoir complété votre profil utilisateur et au choix d'une photo de profil. En dire plus à la communauté sur vous et vos intérêts permet de la rendre plus agréable et plus connectée. Rejoignez-nous ! anniversary: name: Jubilaire description: "Membre actif depuis un an, avec au moins un message" + long_description: | + Ce badge est accordé après avoir été membre du site pendant une année, avec au moins un message crée dans cette année. Merci d'être resté avec nous et de contribuer ainsi à notre communauté ! Nous n'aurions pas pu le faire sans vous. nice_post: name: Jolie réponse description: A reçu 10 J'aime sur une réponse + long_description: | + Ce badge est accordé quand votre réponse obtient 10 J'aime. Votre réponse a fait bonne impression sur la communauté et a aidé la conversation à progresser. good_post: name: Bonne réponse description: A reçu 25 J'aime sur une réponse + long_description: | + Ce badge est accordé quand votre réponse obtient 25 J'aime. Votre réponse est exceptionnel et a rendu la conversation beaucoup plus intéressante ! great_post: name: Super réponse description: A reçu 50 J'aime sur une réponse + long_description: | + Ce badge est accordé quand votre réponse obtient 50 J'aime. Votre réponse était inspirante, fascinante, hilarante, ou pertinente et la communauté l'a adorée ! nice_topic: name: Sujet intéressant description: A reçu 10 J'aime sur un sujet + long_description: | + Ce badge est accordé quand votre sujet obtient 10 J'aime. Vous avez commencé une conversation intéressante que la communauté a apprécié ! good_topic: name: Bon sujet description: A reçu 25 J'aime sur un sujet + long_description: | + Ce badge est accordé quand votre sujet obtient 25 J'aime. Vous avez lancé une conversation vibrante autour de laquelle la communauté s'est ralliée ! great_topic: name: Super sujet description: A reçu 50 J'aime sur un sujet + long_description: | + Ce badge est accordé quand votre sujet obtient 50 J'aime. Vous avez initié une conversation fascinante et la communauté a apprécié la discussion dynamique résultante ! nice_share: name: Partage sympa description: Message partagé avec 25 visiteurs uniques @@ -3105,9 +3297,13 @@ fr: good_share: name: Bon partage description: Message partagé avec 300 visiteurs uniques + long_description: | + Ce badge est accordé après le partage d'un lien vers un message consulté par 300 visiteurs extérieurs. Bon travail ! Vous avez diffusé une discussion intéressante à beaucoup de nouvelles personnes et nous avez aidés à grandir. great_share: name: Super partage description: Message partagé avec 1000 visiteurs uniques + long_description: | + Ce badge est accordé après le partage d'un lien qui a été cliqué par 1000 visiteurs extérieurs. Whaou ! Vous avez fait la promotion d'une discussion intéressante auprès d'une nouvelle audience d'envergure et avez aidé notre communauté à grandir de manière significative ! first_like: name: Premier J'aime description: A aimé un message @@ -3116,6 +3312,8 @@ fr: first_flag: name: Premier signalement description: A signalé un message + long_description: | + Ce badge est accordé la première fois que vous signalez un message. Les signalements sont essentiels à la santé de votre communauté. Si vous remarquez des messages nécessitant l'intervention d'un modérateur n'hésitez pas à les signaler. Si vous voyez un problème, :flag_black: signalez-le ! promoter: name: Ambassadeur description: A invité un utilisateur @@ -3124,9 +3322,13 @@ fr: campaigner: name: Militant description: A invité 3 utilisateurs basiques + long_description: | + Ce badge est accordé lorsque vous avez invité 3 personnes qui ont ensuite passé assez de temps sur le site pour devenir des utilisateurs de base. Une communauté dynamique a besoin d'un apport régulier de nouveaux arrivants qui participent régulièrement et ajouter de nouvelles voix aux conversations. champion: name: Champion description: A invité 5 membres + long_description: | + Ce badge est accordé lorsque vous avez invité 5 personnes qui ont ensuite passé assez de temps sur le site pour devenir des membres à part entière. Whaou ! Merci d'élargir la diversité de notre communauté avec de nouveaux membres ! first_share: name: Premier partage description: A partagé un message @@ -3140,9 +3342,13 @@ fr: first_quote: name: Première citation description: A cité un message + long_description: | + Ce badge est accordé la première fois que vous citez un message dans votre réponse. Citer les sections pertinentes des messages précédents dans votre réponse permet de garder les discussions reliées entre elles et sur le sujet. La meilleure façon de citer est de mettre en évidence une section d'un message, puis appuyer sur un bouton de réponse. Citer généreusement ! read_guidelines: name: Règlement lu description: "A lu le règlement de la communauté" + long_description: | + Ce badge est accordé après lecture de la charte de la communauté. Respecter et partager ces règles simples permet de construire une communauté sûre, agréable et durable. Souvenez-vous toujours qu'il y a un autre être humain, tout comme vous, de l'autre côté de cet écran. Soyez courtois ! reader: name: Lecteur description: A lu tous les messages d'un sujet contenant plus de 100 messages @@ -3191,6 +3397,8 @@ fr: crazy_in_love: name: Fou amoureux description: A utilisé 50 likes en un jour 20 fois + long_description: | + Ce badge est accordé lorsque vous utilisez la totalité vos 50 J'aime par jour pendant 20 jours. Whaou ! Vous êtes un modèle d'encouragement pour les membres de la communauté ! thank_you: name: "Merci " description: A 20 messages ayant reçu un J'aime et a donné 10 j'aime @@ -3204,6 +3412,7 @@ fr: empathetic: name: Empathique description: A 500 messages ayant reçu un J'aime et a donné 1000 J'aime + long_description: "Ce badge est accordé quand vous avez reçu 500 J'aime et en avez donné 1000 ou plus en retour. Whaou ! Vous êtes un modèle de générosité et d'amour mutuel :two_hearts:. \n" first_emoji: name: Premier Emoji description: A utilisé un emoji dans un message @@ -3212,9 +3421,13 @@ fr: first_mention: name: Première mention description: A mentionné un utilisateur dans un message + long_description: | + Ce badge est accordé la première fois que vous mentionnez le @pseudo de quelqu'un dans votre message. Chaque mention génère une notification à cette personne pour qu'elle soit informée de votre message. Il suffit de commencer à taper @ (arobase) pour mentionner un utilisateur ou, si autorisé, un groupe – c'est un moyen pratique de porter quelque chose à leur attention. first_onebox: name: Premier onebox description: A inséré un lien qui a été transformé en onebox + long_description: | + Ce badge est accordé la première fois que vous publiez un lien seul sur une ligne, qui a été développé automatiquement dans un onebox avec un bref résumé, un titre, et (le cas échéant) une image. first_reply_by_email: name: Première réponse par courriel description: A répondu à un message par courriel @@ -3228,12 +3441,18 @@ fr: enthusiast: name: Passionné description: A visité 10 jours + long_description: | + Ce badge est décerné pour avoir visité 10 jours consécutifs. Merci d'être resté avec nous pendant plus d'une semaine ! aficionado: name: Aficionado description: A visité 100 jours + long_description: | + Ce badge est décerné pour avoir visité 100 jours consécutifs. C'est plus de trois mois ! devotee: name: Adepte description: A visité 365 jours + long_description: | + Ce badge est décerné pour avoir visité 365 jours consécutifs. Waouh, une année entière ! badge_title_metadata: "%{display_name} badge sur %{site_title}" admin_login: success: "Courriel envoyé" @@ -3257,6 +3476,7 @@ fr: button: "Créer" title: "Créer un compte administrateur" help: "créez un nouveau compte pour commencer" + no_emails: "Malheureusement aucune adresse courriel d'administrateur n'a été définie lors de la configuration. Merci d'ajouter un courriel de développeur dans le fichier de configuration ou de créer un compte administrateur depuis le console." confirm_email: title: "Confirmer votre adresse courriel" message: "

Nous avons envoyé un courriel d’activation à %{email}. Veuillez suivre les instructions du courriel pour activer votre compte.

S'il n'est pas réceptionné, assurez-vous que l'adresse courriel est correctement configurée pour votre Discourse et vérifiez vos courriers indésirables.

" @@ -3359,6 +3579,7 @@ fr: fields: favicon_url: label: "Petite icône" + description: "Icône utilisée pour représenter le site dans les navigateurs Web et qui rend bien à de petites tailles comme 32px par 32px. PNG ou JPG recommandés." apple_touch_icon_url: label: "Grande icône" description: "Icône utilisée pour représenter le site sur les appareils modernes et que rend bien à des tailles plus grandes. La taille minimale conseillée est de 512px par 512px." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 4c30c0f5c4..950558c1d5 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -1474,6 +1474,13 @@ ro: Anumite probleme îți sunt raportate pe spațul de lucru al adminului. [Te rugăm să le verifici și să le rezolvi](%{base_url}/admin). + new_user_of_the_month: + text_body_template: | + Felicitări, ai primit ecusonul **Premiul pentru utilizatorul nou al lunii %{month_year}**. :trophy: + + În fiecare lună, doi utilizatori noi primesc acest premiu ce va fi vizibil permanent pe [pagina de ecusoane](%{url}) + + Ai devenit un membru valoros al comunității noastre. Mulțumim pentru participare! unsubscribe_link: | Pentru dezabonare de la aceste emailuri, [click aici](%{unsubscribe_url}). unsubscribe_link_and_mail: | diff --git a/plugins/discourse-narrative-bot/config/locales/server.es.yml b/plugins/discourse-narrative-bot/config/locales/server.es.yml index 89ca36a526..a985e171f5 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.es.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.es.yml @@ -7,6 +7,7 @@ es: site_settings: + discourse_narrative_bot_enabled: 'Habilitar Discourse Narrative Bot (discobot)' disable_discourse_narrative_bot_welcome_post: "Deshabilitar el post de bienvenida por Discourse Narrative Bot" discourse_narrative_bot_ignored_usernames: "Nombres de usuario que el Discourse Narrative Bot debe ignorar" discourse_narrative_bot_disable_public_replies: "Deshabilitar respuestas públicas del Discourse Narrative Bot" @@ -138,6 +139,7 @@ es: Mientras tanto, me quedaré fuera de tu camino. new_user_narrative: reset_trigger: "usuario nuevo" + title: "Certificado de completación para el nuevo usuario" cert_title: "En reconocimiento de la finalización exitosa del tutorial de usuario nuevo" hello: title: ":robot: Saludos!" diff --git a/plugins/discourse-narrative-bot/config/locales/server.fr.yml b/plugins/discourse-narrative-bot/config/locales/server.fr.yml index febf4cf441..186981964d 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.fr.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.fr.yml @@ -7,6 +7,7 @@ fr: site_settings: + discourse_narrative_bot_enabled: 'Activer le rotbot de narration Discourse (discobot)' disable_discourse_narrative_bot_welcome_post: "Désactiver le message de bienvenue de l'assistant Discourse" discourse_narrative_bot_ignored_usernames: "Noms d'utilisateurs que l'assistant Discourse doit ignorer" discourse_narrative_bot_disable_public_replies: "Désactiver les réponses publiques de l'assistant Discourse" From 4653627a4062ac5b69dd2f5a2ed3729d99547153 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 10 Sep 2018 14:22:45 -0400 Subject: [PATCH 121/124] update plugin-translations.rb script to update .tx/config file in plugins when languages are added or removed --- script/plugin-translations.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/script/plugin-translations.rb b/script/plugin-translations.rb index e43a72afd9..f4ed4165c9 100644 --- a/script/plugin-translations.rb +++ b/script/plugin-translations.rb @@ -49,6 +49,7 @@ class PluginTxUpdater system_cmd('bundle exec bin/pull_translations.rb') system_cmd('git add config/locales/*') system_cmd('git add Gemfile.lock') rescue true # might be gitignored + system_cmd('git add .tx/config') rescue true system_cmd('git commit -m "Update translations"') system_cmd('git push origin master') if @push rescue => e From e64402cb3beef7a52b235133a35005c0ab51cb96 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Sep 2018 08:24:02 +1000 Subject: [PATCH 122/124] SECURITY: correct edge case when SSO provides unvalidated emails --- app/models/discourse_single_sign_on.rb | 3 ++- spec/models/discourse_single_sign_on_spec.rb | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index 97a75c312e..c531b31d73 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -162,7 +162,8 @@ class DiscourseSingleSignOn < SingleSignOn # Use a mutex here to counter SSO requests that are sent at the same time w # the same email payload DistributedMutex.synchronize("discourse_single_sign_on_#{email}") do - unless user = User.find_by_email(email) + user = User.find_by_email(email) if !require_activation + if !user try_name = name.presence try_username = username.presence diff --git a/spec/models/discourse_single_sign_on_spec.rb b/spec/models/discourse_single_sign_on_spec.rb index 52b26a3a76..68c72d1193 100644 --- a/spec/models/discourse_single_sign_on_spec.rb +++ b/spec/models/discourse_single_sign_on_spec.rb @@ -377,6 +377,15 @@ describe DiscourseSingleSignOn do sso.require_activation = true user = sso.lookup_or_create_user(ip_address) expect(user.active).to eq(false) + + user.activate + + sso.external_id = "B" + + expect do + sso.lookup_or_create_user(ip_address) + end.to raise_error(ActiveRecord::RecordInvalid) + end it 'does not deactivate user if email provided is capitalized' do From 103f9b5dc75ae4b2cb0ac985a871489d332db4b0 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Sep 2018 09:34:02 +1000 Subject: [PATCH 123/124] UX: missing translation in AWS site settings --- config/locales/client.en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3aa3cbe34a..fc28a6e444 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -194,6 +194,7 @@ en: ap_southeast_1: "Asia Pacific (Singapore)" ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "China (Beijing)" + cn_northwest_1: "China (Ningxia)" eu_central_1: "EU (Frankfurt)" eu_west_1: "EU (Ireland)" eu_west_2: "EU (London)" From b3b9ac3b191556ff20f1255340a3c4b5dc25b4a3 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 10 Sep 2018 19:43:15 -0400 Subject: [PATCH 124/124] Version bump to v2.2.0.beta1 --- lib/version.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/version.rb b/lib/version.rb index 0259dc7e6a..b68559aa66 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -3,9 +3,9 @@ module Discourse unless defined? ::Discourse::VERSION module VERSION #:nodoc: MAJOR = 2 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = 'beta6' + PRE = 'beta1' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end