diff --git a/.gitattributes b/.gitattributes index ae7015b472a..546b134a0ce 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,11 +1,14 @@ # Set default behaviour, in case users don't have core.autocrlf set. * text=auto -# Explicitly declare text files we want to always be normalized and converted +# Treat email fixtures as binary files so CRLF are not converted to LF. +*.eml binary + +# Explicitly declare text files we want to always be normalized and converted # to native line endings on checkout. *.yml text -# Custom for Visual Studio, very unlikely, but lets keep it +# Custom for Visual Studio, very unlikely, but lets keep it *.cs diff=csharp *.sln merge=union *.csproj merge=union diff --git a/Gemfile b/Gemfile index 3fa66d5d309..01aef3667eb 100644 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.36' +gem 'onebox', '1.8.40' gem 'http_accept_language', '~>2.0.5', require: false @@ -67,9 +67,6 @@ gem 'multi_json' gem 'mustache' gem 'nokogiri' -# this may end up deprecating nokogiri -gem 'oga', require: false - gem 'omniauth' gem 'omniauth-openid' gem 'openid-redis-store' @@ -178,6 +175,9 @@ gem 'logster' gem 'sassc', require: false +gem 'rotp' +gem 'rqrcode' + if ENV["IMPORT"] == "1" gem 'mysql2' gem 'redcarpet' diff --git a/Gemfile.lock b/Gemfile.lock index b4c381a1a82..48ca0c83c0d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,7 +41,6 @@ GEM annotate (2.7.2) activerecord (>= 3.2, < 6.0) rake (>= 10.4, < 13.0) - ansi (1.5.0) arel (8.0.0) ast (2.3.0) aws-partitions (1.24.0) @@ -64,7 +63,7 @@ GEM coderay (>= 1.0.0) erubis (>= 2.6.6) rack (>= 0.9.0) - binding_of_caller (0.7.2) + binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) bootsnap (1.0.0) msgpack (~> 1.0) @@ -74,13 +73,14 @@ GEM uniform_notifier (~> 1.10.0) byebug (9.0.6) certified (1.0.0) + chunky_png (1.3.8) coderay (1.1.2) concurrent-ruby (1.0.5) connection_pool (2.2.1) cppjieba_rb (0.3.0) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.2) + crass (1.0.3) debug_inspector (0.0.3) diff-lcs (1.3) discourse-qunit-rails (0.0.11) @@ -165,7 +165,7 @@ GEM lru_redux (1.1.0) mail (2.6.6) mime-types (>= 1.16, < 4) - memory_profiler (0.9.8) + memory_profiler (0.9.10) message_bus (2.1.2) rack (>= 1.1.3) metaclass (0.0.4) @@ -185,13 +185,13 @@ GEM mock_redis (0.17.3) moneta (1.0.0) msgpack (1.1.0) - multi_json (1.12.1) + multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) mustache (1.0.5) - nokogiri (1.8.1) + nokogiri (1.8.2) mini_portile2 (~> 2.3.0) - nokogumbo (1.4.13) + nokogumbo (1.5.0) nokogiri oauth (0.5.1) oauth2 (1.3.1) @@ -200,10 +200,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - oga (2.10) - ast - ruby-ll (~> 2.1) - oj (3.1.0) + oj (3.4.0) omniauth (1.6.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) @@ -232,7 +229,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.36) + onebox (1.8.40) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -302,6 +299,9 @@ GEM redis (~> 3.0, >= 3.0.4) request_store (1.3.2) rinku (2.0.2) + rotp (3.3.0) + rqrcode (0.10.1) + chunky_png (~> 1.0) rspec (3.6.0) rspec-core (~> 3.6.0) rspec-expectations (~> 3.6.0) @@ -334,9 +334,6 @@ GEM rainbow (>= 2.2.2, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-ll (2.1.2) - ansi - ast ruby-openid (2.7.0) ruby-prof (0.16.2) ruby-progressbar (1.9.0) @@ -345,10 +342,10 @@ GEM nokogiri (>= 1.6.0) ruby_dep (1.5.0) safe_yaml (1.0.4) - sanitize (4.5.0) + sanitize (4.6.0) crass (~> 1.0.2) nokogiri (>= 1.4.4) - nokogumbo (~> 1.4.1) + nokogumbo (~> 1.4) sass (3.4.24) sassc (1.11.2) bundler @@ -459,7 +456,6 @@ DEPENDENCIES multi_json mustache nokogiri - oga oj omniauth omniauth-facebook @@ -469,7 +465,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.36) + onebox (= 1.8.40) openid-redis-store pg (~> 0.21.0) pry-nav @@ -487,6 +483,8 @@ DEPENDENCIES redis redis-namespace rinku + rotp + rqrcode rspec rspec-html-matchers rspec-rails diff --git a/README.md b/README.md index 9a4c6f7595d..1069b7ee84c 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,6 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https ## Contributing [](https://travis-ci.org/discourse/discourse) -[](https://codeclimate.com/github/discourse/discourse) Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that accepts contributions from the public – including you! diff --git a/app/assets/javascripts/admin/components/staff-actions.js.es6 b/app/assets/javascripts/admin/components/staff-actions.js.es6 new file mode 100644 index 00000000000..9e742526afa --- /dev/null +++ b/app/assets/javascripts/admin/components/staff-actions.js.es6 @@ -0,0 +1,22 @@ +import DiscourseURL from 'discourse/lib/url'; + +export default Ember.Component.extend({ + classNames: ['table', 'staff-actions'], + + willDestroyElement() { + this.$().off('click.discourse-staff-logs'); + }, + + didInsertElement() { + this._super(); + + this.$().on('click.discourse-staff-logs', '[data-link-post-id]', e => { + let postId = $(e.target).attr('data-link-post-id'); + + this.store.find('post', postId).then(p => { + DiscourseURL.routeTo(p.get('url')); + }); + return false; + }); + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index 41869d976e2..770d5588b06 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -19,6 +19,11 @@ export default Ember.Controller.extend(CanCheckEmails, { primaryGroupDirty: propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'), + canDisableSecondFactor: Ember.computed.and( + 'model.second_factor_enabled', + 'model.can_disable_second_factor' + ), + automaticGroups: function() { return this.get("model.automaticGroups").map((g) => g.name).join(", "); }.property("model.automaticGroups"), @@ -63,6 +68,7 @@ export default Ember.Controller.extend(CanCheckEmails, { deleteAllPosts() { return this.get("model").deleteAllPosts(); }, anonymize() { return this.get('model').anonymize(); }, destroy() { return this.get('model').destroy(); }, + disableSecondFactor() { return this.get('model').disableSecondFactor(); }, viewActionLogs() { this.get('adminTools').showActionLogs(this, { diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index e3f1530badf..99aec6aaf71 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -168,6 +168,14 @@ const AdminUser = Discourse.User.extend({ }).catch(popupAjaxError); }, + disableSecondFactor() { + return ajax(`/admin/users/${this.get('id')}/disable_second_factor`, { + type: 'PUT' + }).then(() => { + this.set('second_factor_enabled', false); + }).catch(popupAjaxError); + }, + refreshBrowsers() { return ajax("/admin/users/" + this.get('id') + "/refresh_browsers", { type: 'POST' diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js.es6 index e60d815f7c7..78d39869cf4 100644 --- a/app/assets/javascripts/admin/models/staff-action-log.js.es6 +++ b/app/assets/javascripts/admin/models/staff-action-log.js.es6 @@ -10,7 +10,7 @@ const StaffActionLog = Discourse.Model.extend({ }.property('action_name'), formattedDetails: function() { - var formatted = ""; + let formatted = ""; formatted += this.format('email', 'email'); formatted += this.format('admin.logs.ip_address', 'ip_address'); formatted += this.format('admin.logs.topic_id', 'topic_id'); @@ -26,9 +26,13 @@ const StaffActionLog = Discourse.Model.extend({ return formatted; }.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'), - format: function(label, propertyName) { + format(label, propertyName) { if (this.get(propertyName)) { - return ('<b>' + I18n.t(label) + ':</b> ' + escapeExpression(this.get(propertyName)) + '<br/>'); + let value = escapeExpression(this.get(propertyName)); + if (propertyName === 'post_id') { + value = `<a href data-link-post-id="${value}">${value}</a>`; + } + return `<b>${I18n.t(label)}:</b> ${value}<br/>`; } else { return ''; } diff --git a/app/assets/javascripts/admin/templates/backups.hbs b/app/assets/javascripts/admin/templates/backups.hbs index 5cfd1231144..33229e22534 100644 --- a/app/assets/javascripts/admin/templates/backups.hbs +++ b/app/assets/javascripts/admin/templates/backups.hbs @@ -1,12 +1,12 @@ <div class='admin-backups'> <div class="admin-controls"> - <div class="span15"> + <nav> <ul class="nav nav-pills"> {{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=""}} </ul> - </div> + </nav> <div class="pull-right"> {{#if model.canRollback}} {{d-button action="rollback" diff --git a/app/assets/javascripts/admin/templates/badges-index.hbs b/app/assets/javascripts/admin/templates/badges-index.hbs index 674eae8de29..4a71e7d69a0 100644 --- a/app/assets/javascripts/admin/templates/badges-index.hbs +++ b/app/assets/javascripts/admin/templates/badges-index.hbs @@ -1,4 +1,4 @@ -{{#d-section class="current-badge span13"}} +{{#d-section class="current-badge content-body"}} <p>{{i18n 'admin.badges.none_selected'}}</p> <div> diff --git a/app/assets/javascripts/admin/templates/badges-show.hbs b/app/assets/javascripts/admin/templates/badges-show.hbs index 94239f9d03c..6fa158d0f30 100644 --- a/app/assets/javascripts/admin/templates/badges-show.hbs +++ b/app/assets/javascripts/admin/templates/badges-show.hbs @@ -1,4 +1,4 @@ -{{#d-section class="current-badge span13"}} +{{#d-section class="current-badge content-body"}} <form class="form-horizontal"> <div> <label for="name">{{i18n 'admin.badges.name'}}</label> @@ -144,7 +144,7 @@ {{/d-section}} {{#if grant_count}} - <div class="span13 current-badge-actions"> + <div class="content-body current-badge-actions"> <div> {{#link-to 'badges.show' this}}{{i18n 'badges.granted' count=grant_count}}{{/link-to}} </div> diff --git a/app/assets/javascripts/admin/templates/badges.hbs b/app/assets/javascripts/admin/templates/badges.hbs index aeeaac9c9e2..b5789d4c42a 100644 --- a/app/assets/javascripts/admin/templates/badges.hbs +++ b/app/assets/javascripts/admin/templates/badges.hbs @@ -1,6 +1,6 @@ <div class="badges"> - <div class='content-list span6'> + <div class='content-list'> <h3>{{i18n 'admin.badges.title'}}</h3> <ul> {{#each model as |badge|}} diff --git a/app/assets/javascripts/admin/templates/components/admin-nav.hbs b/app/assets/javascripts/admin/templates/components/admin-nav.hbs index c5b1d3b58a7..eb705949e54 100644 --- a/app/assets/javascripts/admin/templates/components/admin-nav.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-nav.hbs @@ -1,7 +1,7 @@ <div class='admin-controls'> - <div class='span15'> + <nav> <ul class="nav nav-pills"> {{yield}} </ul> - </div> + </nav> </div> diff --git a/app/assets/javascripts/admin/templates/customize-colors.hbs b/app/assets/javascripts/admin/templates/customize-colors.hbs index 920f6f19540..761996b6cb9 100644 --- a/app/assets/javascripts/admin/templates/customize-colors.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors.hbs @@ -1,4 +1,4 @@ -<div class='content-list span6 color-schemes'> +<div class='content-list color-schemes'> <h3>{{i18n 'admin.customize.colors.long_title'}}</h3> <ul> {{#each model as |scheme|}} diff --git a/app/assets/javascripts/admin/templates/customize-email-templates.hbs b/app/assets/javascripts/admin/templates/customize-email-templates.hbs index 152c02e44a2..7dd95b092f5 100644 --- a/app/assets/javascripts/admin/templates/customize-email-templates.hbs +++ b/app/assets/javascripts/admin/templates/customize-email-templates.hbs @@ -1,5 +1,5 @@ <div class='row'> - <div class='content-list span6'> + <div class='content-list'> <ul> {{#each sortedTemplates as |et|}} <li> diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs index c9474917ef6..e2f9f8b99a2 100644 --- a/app/assets/javascripts/admin/templates/customize-themes.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -1,5 +1,5 @@ {{#unless editingTheme}} -<div class='content-list span6'> +<div class='content-list'> <h3>{{i18n 'admin.customize.theme.long_title'}}</h3> <ul> {{#each sortedThemes as |theme|}} diff --git a/app/assets/javascripts/admin/templates/email-index.hbs b/app/assets/javascripts/admin/templates/email-index.hbs index 7fc5ce5d838..80b3d5d062d 100644 --- a/app/assets/javascripts/admin/templates/email-index.hbs +++ b/app/assets/javascripts/admin/templates/email-index.hbs @@ -19,7 +19,7 @@ <div class='controls'> {{text-field value=testEmailAddress placeholderKey="admin.email.test_email_address"}} </div> - <div class='span10 controls'> + <div class='controls'> <button class='btn btn-primary' {{action "sendTestEmail"}} disabled={{sendTestEmailDisabled}}>{{i18n 'admin.email.send_test'}}</button> {{#if sentTestEmail}}<span class='result-message'>{{i18n 'admin.email.sent_test'}}</span>{{/if}} </div> diff --git a/app/assets/javascripts/admin/templates/emojis.hbs b/app/assets/javascripts/admin/templates/emojis.hbs index 675e8249be0..4cf568fd450 100644 --- a/app/assets/javascripts/admin/templates/emojis.hbs +++ b/app/assets/javascripts/admin/templates/emojis.hbs @@ -6,7 +6,7 @@ <p>{{emoji-uploader done="emojiUploaded"}}</p> {{#if sortedEmojis}} - <div class="span8"> + <div> <table id="custom_emoji"> <thead> <tr> diff --git a/app/assets/javascripts/admin/templates/groups-type.hbs b/app/assets/javascripts/admin/templates/groups-type.hbs index a1fa8298970..47bbb1c51e7 100644 --- a/app/assets/javascripts/admin/templates/groups-type.hbs +++ b/app/assets/javascripts/admin/templates/groups-type.hbs @@ -1,6 +1,6 @@ <div class='row groups'> {{#if sortedGroups}} - <div class='content-list span6'> + <div class='content-list'> <h3>{{i18n 'admin.groups.edit'}}</h3> <ul> {{#each sortedGroups as |group|}} @@ -25,7 +25,7 @@ </div> {{/if}} - <div class="span13"> + <div class="content-body"> {{outlet}} </div> </div> diff --git a/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs index 00e67a9aa5c..e8ee4d896dd 100644 --- a/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs @@ -58,7 +58,7 @@ {{#unless item.editing}} {{d-button action="destroy" actionParam=item icon="trash-o" class="btn-danger"}} {{d-button action="edit" actionParam=item icon="pencil"}} - {{#if isBlocked}} + {{#if item.isBlocked}} {{d-button action="allow" actionParam=item icon="check" label="admin.logs.screened_ips.actions.do_nothing"}} {{else}} {{d-button action="block" actionParam=item icon="ban" label="admin.logs.screened_ips.actions.block"}} diff --git a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs index 5df150c79fb..3445abf8df5 100644 --- a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs @@ -39,7 +39,7 @@ </div> <div class="clearfix"></div> -<div class='table staff-actions'> +{{#staff-actions}} <div class="heading-container"> <div class="col heading first staff_user">{{i18n 'admin.logs.staff_actions.staff_user'}}</div> <div class="col heading action">{{i18n 'admin.logs.action'}}</div> @@ -86,4 +86,4 @@ {{i18n 'search.no_results'}} {{/each}} {{/conditional-loading-spinner}} -</div> +{{/staff-actions}} diff --git a/app/assets/javascripts/admin/templates/user-badges.hbs b/app/assets/javascripts/admin/templates/user-badges.hbs index 167f4ffa1d6..52faf9b4333 100644 --- a/app/assets/javascripts/admin/templates/user-badges.hbs +++ b/app/assets/javascripts/admin/templates/user-badges.hbs @@ -1,9 +1,9 @@ <div class='admin-controls'> - <div class='span15'> + <nav> <ul class='nav nav-pills'> <li>{{#link-to 'adminUser' user}}{{d-icon "caret-left"}} {{user.username}}{{/link-to}}</li> </ul> - </div> + </nav> </div> {{#conditional-loading-spinner condition=loading}} diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index 28b526da55e..11f96713650 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -156,6 +156,22 @@ </div> </div> {{/if}} + + <div class='display-row'> + <div class='field'>{{i18n 'user.second_factor.title'}}</div> + <div class='value'> + {{#if model.second_factor_enabled}} + {{i18n "yes_value"}} + {{else}} + {{i18n "no_value"}} + {{/if}} + </div> + <div class='controls'> + {{#if canDisableSecondFactor}} + {{d-button action="disableSecondFactor" icon="unlock-alt" label="user.second_factor.disable"}} + {{/if}} + </div> + </div> </section> {{#if userFields}} diff --git a/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs b/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs index 1561a95670b..1a848710a33 100644 --- a/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs +++ b/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs @@ -1,10 +1,10 @@ <div class='admin-controls'> - <div class='span15'> + <nav> <ul class="nav nav-pills"> <li>{{#link-to 'adminUser' model}}{{d-icon "caret-left"}} {{model.username}}{{/link-to}}</li> <li>{{#link-to 'adminUsersList.show' 'member'}}{{i18n 'admin.user.trust_level_2_users'}}{{/link-to}}</li> </ul> - </div> + </nav> </div> <div class="admin-container tl3-requirements"> diff --git a/app/assets/javascripts/admin/templates/users-list.hbs b/app/assets/javascripts/admin/templates/users-list.hbs index 398732685b1..645fc83c1ff 100644 --- a/app/assets/javascripts/admin/templates/users-list.hbs +++ b/app/assets/javascripts/admin/templates/users-list.hbs @@ -1,5 +1,5 @@ <div class='admin-controls'> - <div class='span15'> + <nav> <ul class="nav nav-pills"> {{nav-item route='adminUsersList.show' routeParam='active' label='admin.users.nav.active'}} {{nav-item route='adminUsersList.show' routeParam='new' label='admin.users.nav.new'}} @@ -11,7 +11,7 @@ {{nav-item route='adminUsersList.show' routeParam='silenced' label='admin.users.nav.silenced'}} {{nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}} </ul> - </div> + </nav> <div class="pull-right"> {{#unless siteSettings.enable_sso}} {{d-button action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}} diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 84242d03f65..aa3f057e006 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -90,7 +90,7 @@ registerIconRenderer({ if (params.label) { html += " aria-hidden='true'"; } html += `></${tagName}>`; if (params.label) { - html += "<span class='sr-only'>" + I18n.t(params.label) + "</span>"; + html += `<span class='sr-only'>${params.label}</span>`; } return html; }, diff --git a/app/assets/javascripts/discourse/components/composer-action-title.js.es6 b/app/assets/javascripts/discourse/components/composer-action-title.js.es6 index 2bc69273004..5b4a148e4d1 100644 --- a/app/assets/javascripts/discourse/components/composer-action-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-action-title.js.es6 @@ -38,11 +38,11 @@ export default Ember.Component.extend({ <a class="post-link" href="${postLink.href}">${postLink.anchor}</a> ${userAvatar} <span class="username">${userLink.anchor}</span> - ${iconHTML("mail-forward", { class: "reply-to-glyph" })} `; if (originalUser) { editTitle += ` + ${iconHTML("mail-forward", { class: "reply-to-glyph" })} ${originalUser.avatar} <span class="original-username">${originalUser.username}</span> `; diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 6b3ca5b4001..86a9b02e501 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -372,7 +372,13 @@ export default Ember.Component.extend({ post.set('refreshedPost', true); } - $oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id)); + $oneboxes.each((_, o) => load({ + elem: o, + refresh, + ajax, + categoryId: this.get('composer.category.id'), + topicId: this.get('composer.topic.id') + })); }, _warnMentionedGroups($preview) { diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6 index 2a828a9483a..23d5443b0cc 100644 --- a/app/assets/javascripts/discourse/components/composer-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-title.js.es6 @@ -80,7 +80,14 @@ export default Ember.Component.extend({ const link = document.createElement('a'); link.href = this.get('composer.title'); - let loadOnebox = load(link, false, ajax, this.currentUser.id, true); + const loadOnebox = load({ + elem: link, + refresh: false, + ajax, + synchronous: true, + categoryId: this.get('composer.category.id'), + topicId: this.get('composer.topic.id') + }); if (loadOnebox && loadOnebox.then) { loadOnebox.then( () => { diff --git a/app/assets/javascripts/discourse/components/group-post.js.es6 b/app/assets/javascripts/discourse/components/group-post.js.es6 new file mode 100644 index 00000000000..51e67b8490e --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-post.js.es6 @@ -0,0 +1,6 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + @computed('post.url') + postUrl: Discourse.getURL +}); diff --git a/app/assets/javascripts/discourse/components/login-modal.js.es6 b/app/assets/javascripts/discourse/components/login-modal.js.es6 index e366392b342..c4d710966ac 100644 --- a/app/assets/javascripts/discourse/components/login-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/login-modal.js.es6 @@ -11,7 +11,7 @@ export default Ember.Component.extend({ } Ember.run.schedule('afterRender', () => { - $('#login-account-password, #login-account-name').keydown(e => { + $('#login-account-password, #login-account-name, #login-second-factor').keydown(e => { if (e.keyCode === 13) { this.sendAction(); } diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index 2045b6daa63..a33492a997b 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -167,9 +167,9 @@ export default Ember.Controller.extend({ return this.currentUser && this.currentUser.staff && hasResults; }, - @computed('expanded', 'model.grouped_search_result.can_create_topic') - canCreateTopic(expanded, userCanCreateTopic) { - return this.currentUser && userCanCreateTopic && !expanded; + @computed('model.grouped_search_result.can_create_topic') + canCreateTopic(userCanCreateTopic) { + return this.currentUser && userCanCreateTopic; }, @computed('expanded') diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index 6eb266d98f2..31d339b7c05 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -4,6 +4,7 @@ import showModal from 'discourse/lib/show-modal'; import { setting } from 'discourse/lib/computed'; import { findAll } from 'discourse/models/login-method'; import { escape } from 'pretty-text/sanitizer'; +import computed from 'ember-addons/ember-computed-decorators'; // This is happening outside of the app via popup const AuthErrors = [ @@ -31,6 +32,9 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set('authenticate', null); this.set('loggingIn', false); this.set('loggedIn', false); + this.set('secondFactorRequired', false); + $("#credentials").show(); + $("#second-factor").hide(); }, // Determines whether at least one login button is enabled @@ -38,9 +42,10 @@ export default Ember.Controller.extend(ModalFunctionality, { return findAll(this.siteSettings).length > 0; }.property(), - loginButtonText: function() { - return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title'); - }.property('loggingIn'), + @computed('loggingIn') + loginButtonLabel(loggingIn) { + return loggingIn ? 'login.logging_in' : 'login.title'; + }, loginDisabled: Em.computed.or('loggingIn', 'loggedIn'), @@ -67,13 +72,24 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set('loggingIn', true); ajax("/session", { - data: { login: this.get('loginName'), password: this.get('loginPassword') }, - type: 'POST' + type: 'POST', + data: { + login: this.get('loginName'), + password: this.get('loginPassword'), + second_factor_token: this.get('loginSecondFactor') + }, }).then(function (result) { // Successful login if (result && result.error) { self.set('loggingIn', false); - if (result.reason === 'not_activated') { + + if (result.reason === 'invalid_second_factor' && !self.get('secondFactorRequired')) { + $('#modal-alert').hide(); + self.set('secondFactorRequired', true); + $("#credentials").hide(); + $("#second-factor").show(); + return; + } else if (result.reason === 'not_activated') { self.send('showNotActivated', { username: self.get('loginName'), sentTo: escape(result.sent_to_email), diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 index 91c62436d6a..bcf40ca88cf 100644 --- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 +++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 @@ -8,6 +8,7 @@ import { userPath } from 'discourse/lib/url'; export default Ember.Controller.extend(PasswordValidation, { isDeveloper: Ember.computed.alias('model.is_developer'), admin: Ember.computed.alias('model.admin'), + secondFactorRequired: Ember.computed.alias('model.second_factor_required'), passwordRequired: true, errorMessage: null, successMessage: null, @@ -32,7 +33,8 @@ export default Ember.Controller.extend(PasswordValidation, { url: userPath(`password-reset/${this.get('model.token')}.json`), type: 'PUT', data: { - password: this.get('accountPassword') + password: this.get('accountPassword'), + second_factor_token: this.get('secondFactor') } }).then(result => { if (result.success) { @@ -45,10 +47,22 @@ export default Ember.Controller.extend(PasswordValidation, { DiscourseURL.redirectTo(result.redirect_to || '/'); } } else { - if (result.errors && result.errors.password && result.errors.password.length > 0) { + if (result.errors && result.errors.user_second_factor) { + this.setProperties({ + secondFactorRequired: true, + password: null, + errorMessage: result.message + }); + } else if (this.get('secondFactorRequired')) { + this.setProperties({ + secondFactorRequired: false, + errorMessage: null + }); + } else if (result.errors && result.errors.password && result.errors.password.length > 0) { this.get('rejectedPasswords').pushObject(this.get('accountPassword')); this.get('rejectedPasswordsMessages').set(this.get('accountPassword'), result.errors.password[0]); } + if (result.message) { this.set('errorMessage', result.message); } diff --git a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 new file mode 100644 index 00000000000..990d2eb6f9b --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 @@ -0,0 +1,73 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { default as DiscourseURL, userPath } from 'discourse/lib/url'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Controller.extend({ + loading: false, + password: null, + secondFactorImage: null, + secondFactorKey: null, + showSecondFactorKey: false, + errorMessage: null, + newUsername: null, + + loaded: Ember.computed.and('secondFactorImage', 'secondFactorKey'), + + @computed('loading') + submitButtonText(loading) { + return loading ? 'loading' : 'submit'; + }, + + toggleSecondFactor(enable) { + if (!this.get('second_factor_token')) return; + this.set('loading', true); + + this.get('content').toggleSecondFactor(this.get('second_factor_token'), enable) + .then(response => { + if (response.error) { + this.set('errorMessage', response.error); + return; + } + + this.set('errorMessage',null); + DiscourseURL.redirectTo(userPath(`${this.get('content').username.toLowerCase()}/preferences`)); + }) + .catch(popupAjaxError) + .finally(() => this.set('loading', false)); + }, + + actions: { + confirmPassword() { + if (!this.get('password')) return; + this.set('loading', true); + + this.get('content').loadSecondFactorCodes(this.get('password')) + .then(response => { + if(response.error) { + this.set('errorMessage', response.error); + return; + } + + this.setProperties({ + errorMessage: null, + secondFactorKey: response.key, + secondFactorImage: response.qr, + }); + }) + .catch(popupAjaxError) + .finally(() => this.set('loading', false)); + }, + + showSecondFactorKey() { + this.set('showSecondFactorKey', true); + }, + + enableSecondFactor() { + this.toggleSecondFactor(true); + }, + + disableSecondFactor() { + this.toggleSecondFactor(false); + } + } +}); diff --git a/app/assets/javascripts/discourse/helpers/period-title.js.es6 b/app/assets/javascripts/discourse/helpers/period-title.js.es6 index 37af602c29d..5a8315117d1 100644 --- a/app/assets/javascripts/discourse/helpers/period-title.js.es6 +++ b/app/assets/javascripts/discourse/helpers/period-title.js.es6 @@ -30,7 +30,7 @@ export default htmlHelper((period, options) => { break; } - return `${title} <span class='top-date-string'>${dateString}</span>`; + return `<span class="date-section">${title}</span><span class='top-date-string'>${dateString}</span>`; } else { return title; } diff --git a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 index 281d44554b3..c539469475e 100644 --- a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 +++ b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 @@ -2,27 +2,27 @@ export default { name: 'register-service-worker', initialize() { - // only allow service worker on android for now - if (!/(android)/i.test(navigator.userAgent)) { - - // remove old service worker - if ('serviceWorker' in navigator && navigator.serviceWorker.getRegistrations) { - navigator.serviceWorker.getRegistrations().then((registrations) => { - for(let registration of registrations) { - registration.unregister(); - }; - }); - } - - } else { - - const isSecure = (document.location.protocol === 'https:') || + window.addEventListener('load', () => { + const isSecured = (document.location.protocol === 'https:') || (location.hostname === "localhost"); + const isSupported= isSecured && ('serviceWorker' in navigator); - if (isSecure && ('serviceWorker' in navigator)) { - navigator.serviceWorker.register(`${Discourse.BaseUri}/service-worker.js`); + if (isSupported) { + if (Discourse.ServiceWorkerURL) { + navigator.serviceWorker + .register(`${Discourse.BaseUri}/${Discourse.ServiceWorkerURL}`) + .catch(error => { + Ember.Logger.info(`Failed to register Service Worker: ${error}`); + }); + } else { + navigator.serviceWorker.getRegistrations().then(registrations => { + for(let registration of registrations) { + registration.unregister(); + }; + }); + } } - } + }); } }; diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 13845062a4e..d4dc44f2cbb 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -26,7 +26,7 @@ export default { } // don't track links in quotes or in elided part - let tracking = $link.parents('aside.quote,.elided').length === 0; + let tracking = $link.parents('aside.quote, .elided').length === 0; let href = $link.attr('href') || $link.data('href'); @@ -113,8 +113,10 @@ export default { return false; } + const isInternal = DiscourseURL.isInternal(href); + // If we're on the same site, use the router and track via AJAX - if (tracking && DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) { + if (tracking && isInternal && !$link.hasClass('attachment')) { ajax("/clicks/track", { data: { url: href, @@ -128,9 +130,11 @@ export default { return false; } - // Otherwise, use a custom URL with a redirect - // consider CTRL+mouse-left-click / CMD+mouse-left-click or mouse-middle-click as well - if (Discourse.User.currentProp('external_links_in_new_tab') || ((e.ctrlKey || e.metaKey) && (e.which === 1)) || (e.which === 2)) { + const modifierLeftClicked = (e.ctrlKey || e.metaKey) && e.which === 1; + const middleClicked = e.which === 2; + const openExternalInNewTab = Discourse.User.currentProp('external_links_in_new_tab'); + + if (modifierLeftClicked || middleClicked || (!isInternal && openExternalInNewTab)) { window.open(destUrl, '_blank').focus(); } else { DiscourseURL.redirectTo(destUrl); diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 332d6a98fc8..487b4467f9b 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -22,6 +22,7 @@ const SERVER_SIDE_ONLY = [ /^\/wizard/, /\.rss$/, /\.json$/, + /^\/admin\/upgrade$/ ]; export function rewritePath(path) { diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 671e3ca7993..987e8682479 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -203,7 +203,7 @@ export function validateUploadedFile(file, opts) { // check that the uploaded file is authorized if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) { - if (Discourse.User.current("staff")) { + if (Discourse.User.currentProp('staff')) { return true; } } @@ -239,16 +239,28 @@ export function validateUploadedFile(file, opts) { const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i; +function extensionsToArray(exts) { + return exts.toLowerCase() + .replace(/[\s\.]+/g, "") + .split("|") + .filter(ext => ext.indexOf("*") === -1); +} + function extensions() { - return Discourse.SiteSettings.authorized_extensions - .toLowerCase() - .replace(/[\s\.]+/g, "") - .split("|") - .filter(ext => ext.indexOf("*") === -1); + return extensionsToArray(Discourse.SiteSettings.authorized_extensions); +} + +function staffExtensions() { + return extensionsToArray(Discourse.SiteSettings.authorized_extensions_for_staff); } function imagesExtensions() { - return extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext)); + let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext)); + if (Discourse.User.currentProp('staff')) { + const staffExts = staffExtensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext)); + exts = _.union(exts, staffExts); + } + return exts; } function extensionsRegex() { @@ -259,7 +271,14 @@ function imagesExtensionsRegex() { return new RegExp("\\.(" + imagesExtensions().join("|") + ")$", "i"); } +function staffExtensionsRegex() { + return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i"); +} + function isAuthorizedFile(fileName) { + if (Discourse.User.currentProp('staff') && staffExtensionsRegex().test(fileName)) { + return true; + } return extensionsRegex().test(fileName); } @@ -268,7 +287,8 @@ function isAuthorizedImage(fileName){ } export function authorizedExtensions() { - return authorizesAllExtensions() ? "*" : extensions().join(", "); + const exts = Discourse.User.currentProp('staff') ? [...extensions(), ...staffExtensions()] : extensions(); + return exts.filter(ext => ext.length > 0).join(", "); } export function authorizedImagesExtensions() { @@ -276,7 +296,9 @@ export function authorizedImagesExtensions() { } export function authorizesAllExtensions() { - return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0; + return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 || ( + Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 && + Discourse.User.currentProp('staff')); } export function authorizesOneOrMoreExtensions() { @@ -322,7 +344,7 @@ export function allowsImages() { } export function allowsAttachments() { - return authorizesAllExtensions() || extensions().length > imagesExtensions().length; + return authorizesAllExtensions() || authorizedExtensions().split(", ").length > imagesExtensions().length; } export function uploadLocation(url) { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 9925d4798a0..eef39aa8acc 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -304,6 +304,20 @@ const User = RestModel.extend({ }); }, + loadSecondFactorCodes(password) { + return ajax("/u/second_factors.json", { + data: { password }, + type: 'POST' + }); + }, + + toggleSecondFactor(token, enable) { + return ajax("/u/second_factor.json", { + data: { second_factor_token: token, enable }, + type: 'PUT' + }); + }, + loadUserAction(id) { const stream = this.get('stream'); return ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => { diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 132ba755bd8..0696f12ced4 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -111,6 +111,7 @@ export default function() { this.route('username'); this.route('email'); + this.route('second-factor'); this.route('about', { path: '/about-me' }); this.route('badgeTitle', { path: '/badge_title' }); this.route('card-badge', { path: '/card-badge' }); diff --git a/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 new file mode 100644 index 00000000000..b688ec813bc --- /dev/null +++ b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 @@ -0,0 +1,15 @@ +import RestrictedUserRoute from "discourse/routes/restricted-user"; + +export default RestrictedUserRoute.extend({ + model() { + return this.modelFor('user'); + }, + + renderTemplate() { + return this.render({ into: 'user' }); + }, + + setupController(controller, model) { + controller.setProperties({ model, newUsername: model.get('username') }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index 1eb3d4b3e17..128a3017315 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -15,6 +15,10 @@ export default RestrictedUserRoute.extend({ }, actions: { + showTwoFactorModal() { + showModal('second-factor-intro'); + }, + showAvatarSelector() { showModal('avatar-selector'); diff --git a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs index 3403e70d494..49dd92606d4 100644 --- a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs +++ b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs @@ -1,4 +1,4 @@ -<a href={{category.url}}> +<a class="category-title-link" href={{category.url}}> {{#if category.read_restricted}} {{d-icon 'lock'}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs index 99fc0a5b97d..0722426f163 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs @@ -3,7 +3,6 @@ onChangeCallback='triggerResize' id="private-message-users" includeMessageableGroups='true' - class="span8" placeholderKey="composer.users_placeholder" tabindex="1" usernames=usernames diff --git a/app/assets/javascripts/discourse/templates/components/group-post.hbs b/app/assets/javascripts/discourse/templates/components/group-post.hbs index 9f51a1584f0..a6d3d6b1016 100644 --- a/app/assets/javascripts/discourse/templates/components/group-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-post.hbs @@ -7,7 +7,7 @@ <div class='group-post-info'> <div class="group-post-topic"> <div class='group-post-title'> - <a href={{post.url}}>{{{post.topic.fancyTitle}}}</a> + <a href={{postUrl}}>{{{post.topic.fancyTitle}}}</a> </div> <div class="group-post-category">{{category-link post.category}}</div> {{#if post.user.name}} diff --git a/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs new file mode 100644 index 00000000000..a1d5e033405 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs @@ -0,0 +1,16 @@ +<div id="second-factor" style="display: none;"> + <h3>{{i18n 'login.second_factor_title'}}</h3> + <p>{{i18n 'login.second_factor_description'}}</p> + + <table> + <tr> + <td> + <label for='login-second-factor'>{{i18n 'login.second_factor_label'}} </label> + </td> + <td> + {{yield}} + </td> + <td></td> + </tr> + </table> +</div> diff --git a/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs b/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs index 0e191a6c8da..02ccc7820ee 100644 --- a/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/modal/login.hbs @@ -1,9 +1,9 @@ -{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword action="login"}} +{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword loginSecondFactor=loginSecondFactor action="login"}} {{#d-modal-body title="login.title" class="login-modal"}} {{login-buttons action="externalLogin"}} {{#if canLoginLocal}} <form id='login-form' method='post'> - <div> + <div id="credentials"> <table> <tr> <td> @@ -15,10 +15,10 @@ </tr> <tr> <td> - <label for='login-account-password'>{{i18n 'login.password'}} </label> + <label for='login-account-password'>{{i18n 'login.password'}} </label> </td> <td> - {{text-field value=loginPassword type="password" id="login-account-password" maxlength="200"}} + {{text-field value=loginPassword type="password" id="login-account-password" maxlength="200"}} </td> </tr> <tr> @@ -29,8 +29,13 @@ </tr> </table> </div> - - + {{#second-factor-form}} + {{text-field value=loginSecondFactor + id="login-second-factor" + autocorrect="off" + autocapitalize="off" + autofocus="autofocus"}} + {{/second-factor-form}} </form> {{/if}} {{authMessage}} @@ -43,11 +48,11 @@ {{/if}} {{#if canLoginLocal}} - <button class='btn btn-large btn-primary' - disabled={{loginDisabled}} - {{action "login"}}> - {{d-icon "unlock"}} {{loginButtonText}} - </button> + {{d-button action="login" + icon="unlock" + label=loginButtonLabel + disabled=loginDisabled + class='btn btn-large btn-primary'}} {{#if showSignupLink}} <button class="btn btn-large" id="new-account-link" {{action "showCreateAccount"}}> diff --git a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs index 539101bdaf7..a7b6c0705b8 100644 --- a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs +++ b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs @@ -1,6 +1,6 @@ {{#d-modal-body id="keyboard-shortcuts-help"}} <div class="row"> - <div class="span6"> + <div> <h4>{{i18n 'keyboard_shortcuts_help.jump_to.title'}}</h4> <ul> <li>{{{i18n 'keyboard_shortcuts_help.jump_to.home'}}}</li> @@ -24,7 +24,7 @@ <li>{{{i18n 'keyboard_shortcuts_help.navigation.next_prev'}}}</li> </ul> </div> - <div class="span6"> + <div> <h4>{{i18n 'keyboard_shortcuts_help.application.title'}}</h4> <ul> <li>{{{i18n 'keyboard_shortcuts_help.application.hamburger_menu'}}}</li> @@ -46,7 +46,7 @@ <li>{{{i18n 'keyboard_shortcuts_help.actions.quote_post'}}}</li> </ul> </div> - <div class="span6"> + <div> <h4>{{i18n 'keyboard_shortcuts_help.actions.title'}}</h4> <ul> <li>{{{i18n 'keyboard_shortcuts_help.actions.bookmark_topic'}}}</li> diff --git a/app/assets/javascripts/discourse/templates/modal/login.hbs b/app/assets/javascripts/discourse/templates/modal/login.hbs index 9b47c129588..75e2a59ffe4 100644 --- a/app/assets/javascripts/discourse/templates/modal/login.hbs +++ b/app/assets/javascripts/discourse/templates/modal/login.hbs @@ -1,9 +1,9 @@ -{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword action="login"}} +{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword loginSecondFactor=loginSecondFactor action="login"}} {{#d-modal-body title="login.title" class="login-modal"}} {{login-buttons action="externalLogin"}} {{#if canLoginLocal}} <form id='login-form' method='post'> - <div> + <div id="credentials"> <table> <tr> <td><label for='login-account-name'>{{i18n 'login.username'}}</label></td> @@ -22,6 +22,9 @@ </tr> </table> </div> + {{#second-factor-form}} + {{text-field value=loginSecondFactor id="login-second-factor" autocorrect="off" autocapitalize="off" autofocus="autofocus"}} + {{/second-factor-form}} </form> {{/if}} {{authMessage}} @@ -30,9 +33,11 @@ <div class="modal-footer"> {{#if canLoginLocal}} - <button form="login-form" type="submit" class="btn btn-large btn-primary" disabled={{loginDisabled}} {{action "login"}}> - {{d-icon "unlock"}} {{loginButtonText}} - </button> + {{d-button action="login" + icon="unlock" + label=loginButtonLabel + disabled=loginDisabled + class='btn btn-large btn-primary'}} {{#if showSignupLink}} <button class="btn btn-large" id="new-account-link" {{action "createAccount"}}> diff --git a/app/assets/javascripts/discourse/templates/modal/second-factor-intro.hbs b/app/assets/javascripts/discourse/templates/modal/second-factor-intro.hbs new file mode 100644 index 00000000000..f573c45cc91 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/second-factor-intro.hbs @@ -0,0 +1,6 @@ +{{#d-modal-body title="user.second_factor.title"}} + <div>{{{i18n 'user.second_factor.extended_description'}}}</div> +{{/d-modal-body}} + +<div class="modal-footer"> +</div> diff --git a/app/assets/javascripts/discourse/templates/password-reset.hbs b/app/assets/javascripts/discourse/templates/password-reset.hbs index e999fedb1ef..01d1db9df3a 100644 --- a/app/assets/javascripts/discourse/templates/password-reset.hbs +++ b/app/assets/javascripts/discourse/templates/password-reset.hbs @@ -16,20 +16,28 @@ {{/if}} {{else}} <form> + {{#if secondFactorRequired}} + <h2>{{i18n 'login.second_factor_title'}}</h2> + <p>{{i18n 'login.second_factor_description'}}</p> + <div class="input"> + {{input value=secondFactor id="second-factor" autofocus="autofocus"}} + </div> + {{d-button action="submit" class='btn-primary' label='submit'}} + {{else}} + <h2>{{i18n 'user.change_password.choose'}}</h2> - <h2>{{i18n 'user.change_password.choose'}}</h2> + <div class="input"> + {{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn autofocus="autofocus"}} + {{input-tip validation=passwordValidation}} + </div> - <div class="input"> - {{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn autofocus="autofocus"}} - {{input-tip validation=passwordValidation}} - </div> + <div class="instructions"> + <div class="caps-lock-warning {{unless capsLockOn 'invisible'}}"> + {{d-icon "exclamation-triangle"}} {{i18n 'login.caps_lock_warning'}}</div> + </div> - <div class="instructions"> - <div class="caps-lock-warning {{unless capsLockOn 'invisible'}}"> - {{d-icon "exclamation-triangle"}} {{i18n 'login.caps_lock_warning'}}</div> - </div> - - <button class='btn btn-primary' {{action "submit"}}>{{i18n 'user.change_password.set_password'}}</button> + {{d-button action="submit" class='btn-primary' label='user.change_password.set_password'}} + {{/if}} {{#if errorMessage}} <br/><br/> diff --git a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs new file mode 100644 index 00000000000..29a3a61df30 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs @@ -0,0 +1,112 @@ +<section class='user-content user-preferences'> + <form class="form-horizontal"> + <div class="control-group"> + <div class="controls"> + <h3>{{i18n 'user.second_factor.title'}}</h3> + </div> + </div> + + {{#if errorMessage}} + <div class="control-group"> + <div class="instructions"> + <div class='alert alert-error'>{{errorMessage}}</div> + </div> + </div> + {{/if}} + + {{#if model.second_factor_enabled}} + <label class='control-label'>{{i18n 'login.second_factor_label'}}</label> + + <div class="control-group"> + <div class="controls"> + {{text-field value=second_factor_token + id="second_factor_token" + classNames="input-large" + autofocus="autofocus"}} + </div> + + <div class='instructions'> + {{i18n 'user.second_factor.disable_description'}} + </div> + </div> + + <div class="control-group"> + <div class="controls"> + {{d-button action="disableSecondFactor" + class="btn btn-primary" + disabled=loading + label=submitButtonText}} + </div> + </div> + {{else}} + {{#if loaded}} + <div class="control-group"> + <div class="controls"> + {{i18n 'user.second_factor.enable_description'}} + </div> + </div> + + <div class="control-group"> + <div class="controls"> + {{{secondFactorImage}}} + + <p> + {{#if showSecondFactorKey}} + {{secondFactorKey}} + {{else}} + <a {{action "showSecondFactorKey"}}>{{i18n 'user.second_factor.show_key_description'}}</a> + {{/if}} + </p> + </div> + </div> + + <div class="control-group"> + <label class="control-label input-prepend">{{i18n 'login.second_factor_label'}}</label> + + <div class="controls"> + {{text-field value=second_factor_token + id="second-factor-token" + classNames="input-xxlarge" + autofocus="autofocus"}} + </div> + </div> + + <div class="control-group"> + <div class="controls"> + {{d-button action="enableSecondFactor" + class="btn btn-primary" + disabled=loading + label=submitButtonText}} + </div> + </div> + {{else}} + <div class="control-group"> + <label class='control-label'>{{i18n 'user.password.title'}}</label> + + <div class="controls"> + {{text-field value=password + id="password" + type="password" + classNames="input-xxlarge" + autofocus="autofocus"}} + </div> + + <div class='instructions'> + {{i18n 'user.second_factor.confirm_password_description'}} + </div> + </div> + + <div class="control-group"> + <div class="controls"> + {{d-button action="confirmPassword" + class="btn btn-primary" + disabled=loading + label=submitButtonText}} + + {{#if saved}}{{i18n 'saved'}}{{/if}} + </div> + </div> + {{/if}} + {{/if}} + </form> +</section> diff --git a/app/assets/javascripts/discourse/templates/preferences/account.hbs b/app/assets/javascripts/discourse/templates/preferences/account.hbs index 38569dd9bdd..225db632b9c 100644 --- a/app/assets/javascripts/discourse/templates/preferences/account.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/account.hbs @@ -66,6 +66,25 @@ {{passwordProgress}} </div> </div> +<div class="control-group pref-second-factor"> + <label class="control-label">{{i18n 'user.second_factor.title'}}</label> + + <div class="controls"> + {{#link-to "preferences.second-factor" class="btn"}} + {{#if model.second_factor_enabled}} + {{d-icon "unlock-alt"}} + {{i18n 'user.second_factor.disable'}} + {{else}} + {{d-icon "lock"}} + {{i18n 'user.second_factor.enable'}} + {{/if}} + {{/link-to}} + </div> + + <div class="instructions"> + <a href {{action "showTwoFactorModal"}}>{{i18n 'user.second_factor.info_prompt'}}</a> + </div> +</div> {{/if}} <div class="control-group pref-avatar"> diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index 5f8dc6d18ad..c02499e76bf 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -5,13 +5,13 @@ <h2>{{i18n 'user.invited.title'}}</h2> {{#if model.can_see_invite_details}} - <div class='user-invite-controls'> - <div class='span15'> + <div class='admin-controls'> + <nav> <ul class="nav nav-pills"> {{nav-item route='userInvited.show' routeParam='pending' i18nLabel=pendingLabel}} {{nav-item route='userInvited.show' routeParam='redeemed' i18nLabel=redeemedLabel}} </ul> - </div> + </nav> <div class="pull-right"> {{d-button icon="plus" action="showInvite" label="user.invited.create" class="btn"}} diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index f20660c64ac..a5858926760 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -39,7 +39,16 @@ <div class='profile-image'></div> <div class='details'> <div class='primary'> - {{bound-avatar model "huge"}} + <div class='user-profile-avatar'> + {{bound-avatar model "huge"}} + {{#if model.primary_group_name}} + {{avatar-flair + flairURL=model.primary_group_flair_url + flairBgColor=model.primary_group_flair_bg_color + flairColor=model.primary_group_flair_color + groupName=model.primary_group_name}} + {{/if}} + </div> <section class='controls'> <ul> {{#if model.can_send_private_message_to_user}} diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 index 0b4d32661a6..a82525157f6 100644 --- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 @@ -5,11 +5,17 @@ import { relativeAge } from 'discourse/lib/formatter'; import { iconNode } from 'discourse-common/lib/icon-library'; import RawHtml from 'discourse/widgets/raw-html'; -const SCROLLAREA_HEIGHT = 300; const SCROLLER_HEIGHT = 50; -const SCROLLAREA_REMAINING = SCROLLAREA_HEIGHT - SCROLLER_HEIGHT; const LAST_READ_HEIGHT = 20; +function scrollareaHeight() { + return ($(window).height() < 425) ? 150 : 300; +} + +function scrollareaRemaining() { + return scrollareaHeight() - SCROLLER_HEIGHT; +} + function clamp(p, min=0.0, max=1.0) { return Math.max(Math.min(p, max), min); } @@ -27,7 +33,7 @@ createWidget('timeline-last-read', { tagName: 'div.timeline-last-read', buildAttributes(attrs) { - const bottom = SCROLLAREA_HEIGHT - (LAST_READ_HEIGHT / 2); + const bottom = scrollareaHeight() - (LAST_READ_HEIGHT / 2); const top = attrs.top > bottom ? bottom : attrs.top; return { style: `height: ${LAST_READ_HEIGHT}px; top: ${top}px` }; }, @@ -115,7 +121,7 @@ createWidget('timeline-scrollarea', { buildKey: () => `timeline-scrollarea`, buildAttributes() { - return { style: `height: ${SCROLLAREA_HEIGHT}px` }; + return { style: `height: ${scrollareaHeight()}px` }; }, defaultState(attrs) { @@ -168,8 +174,8 @@ createWidget('timeline-scrollarea', { const percentage = state.percentage; if (percentage === null) { return; } - const before = SCROLLAREA_REMAINING * percentage; - const after = (SCROLLAREA_HEIGHT - before) - SCROLLER_HEIGHT; + const before = scrollareaRemaining() * percentage; + const after = (scrollareaHeight() - before) - SCROLLER_HEIGHT; let showButton = false; const hasBackPosition = @@ -179,13 +185,13 @@ createWidget('timeline-scrollarea', { (position.lastRead && position.lastRead !== position.total); if (hasBackPosition) { - const lastReadTop = Math.round(position.lastReadPercentage * SCROLLAREA_HEIGHT); + const lastReadTop = Math.round(position.lastReadPercentage * scrollareaHeight()); showButton = ((before + SCROLLER_HEIGHT - 5) < lastReadTop) || (before > (lastReadTop + 25)); // Don't show if at the bottom of the timeline - if (lastReadTop > (SCROLLAREA_HEIGHT - (LAST_READ_HEIGHT / 2))) { + if (lastReadTop > (scrollareaHeight() - (LAST_READ_HEIGHT / 2))) { showButton = false; } } @@ -200,7 +206,7 @@ createWidget('timeline-scrollarea', { ]; if (hasBackPosition) { - const lastReadTop = Math.round(position.lastReadPercentage * SCROLLAREA_HEIGHT); + const lastReadTop = Math.round(position.lastReadPercentage * scrollareaHeight()); result.push(this.attach('timeline-last-read', { top: lastReadTop, lastRead: position.lastRead, diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 index 3a0523bef66..27873269ee1 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 @@ -105,7 +105,6 @@ const rule = { let title = topicInfo.title; - if (options.enableEmoji) { title = performEmojiUnescape(topicInfo.title, { getURL: options.getURL, emojiSet: options.emojiSet diff --git a/app/assets/javascripts/pretty-text/oneboxer.js.es6 b/app/assets/javascripts/pretty-text/oneboxer.js.es6 index 245c87cbca1..aef317865dd 100644 --- a/app/assets/javascripts/pretty-text/oneboxer.js.es6 +++ b/app/assets/javascripts/pretty-text/oneboxer.js.es6 @@ -43,16 +43,20 @@ function loadNext(ajax) { let timeoutMs = 150; let removeLoading = true; - const { url, refresh, $elem, userId } = loadingQueue.shift(); + const { url, refresh, $elem, categoryId, topicId } = loadingQueue.shift(); // Retrieve the onebox return ajax("/onebox", { dataType: 'html', - data: { url, refresh }, + data: { + url, + refresh, + category_id: categoryId, + topic_id: topicId + }, cache: true }).then(html => { let $html = $(html); - localCache[normalize(url)] = $html; $elem.replaceWith($html); applySquareGenericOnebox($html, normalize(url)); @@ -60,7 +64,7 @@ function loadNext(ajax) { if (result && result.jqXHR && result.jqXHR.status === 429) { timeoutMs = 2000; removeLoading = false; - loadingQueue.unshift({ url, refresh, $elem, userId }); + loadingQueue.unshift({ url, refresh, $elem, categoryId, topicId }); } else { failedCache[normalize(url)] = true; } @@ -75,14 +79,14 @@ function loadNext(ajax) { // Perform a lookup of a onebox based an anchor $element. // It will insert a loading indicator and remove it when the loading is complete or fails. -export function load(e, refresh, ajax, userId, synchronous) { - const $elem = $(e); +export function load({ elem , refresh = true, ajax, synchronous = false, categoryId, topicId }) { + const $elem = $(elem); // If the onebox has loaded or is loading, return if ($elem.data('onebox-loaded')) return; if ($elem.hasClass('loading-onebox')) return; - const url = e.href; + const url = elem.href; // Unless we're forcing a refresh... if (!refresh) { @@ -99,7 +103,7 @@ export function load(e, refresh, ajax, userId, synchronous) { $elem.addClass('loading-onebox'); // Add to the loading queue - loadingQueue.push({ url, refresh, $elem, userId }); + loadingQueue.push({ url, refresh, $elem, categoryId, topicId }); // Load next url in queue if (synchronous) { diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index 4fd6e362954..6dd1523f53e 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -137,6 +137,7 @@ const DEFAULT_LIST = [ 'div.quote-controls', 'div.title', 'div[align]', + 'div[data-theme-*]', 'div[dir]', 'dl', 'dt', diff --git a/app/assets/javascripts/select-kit/components/category-chooser.js.es6 b/app/assets/javascripts/select-kit/components/category-chooser.js.es6 index 65030131c7d..82c9f27d091 100644 --- a/app/assets/javascripts/select-kit/components/category-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-chooser.js.es6 @@ -3,6 +3,7 @@ import { on } from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators"; import PermissionType from "discourse/models/permission-type"; import Category from "discourse/models/category"; +import { categoryBadgeHTML } from "discourse/helpers/category-link"; const { get, isNone, isEmpty } = Ember; export default ComboBoxComponent.extend({ @@ -57,6 +58,36 @@ export default ComboBoxComponent.extend({ } }, + computeHeaderContent() { + let content = this.baseHeaderComputedContent(); + + if (this.get("hasSelection")) { + const category = Category.findById(content.value); + const parentCategoryId = category.get("parent_category_id"); + const hasParentCategory = Ember.isPresent(parentCategoryId); + + let badge = ""; + + if (hasParentCategory) { + const parentCategory = Category.findById(parentCategoryId); + badge += categoryBadgeHTML(parentCategory, { + link: false, + allowUncategorized: true + }).htmlSafe(); + } + + badge += categoryBadgeHTML(category, { + link: false, + hideParent: hasParentCategory ? true : false, + allowUncategorized: true + }).htmlSafe(); + + content.label = badge; + } + + return content; + }, + @on("didRender") _bindComposerResizing() { this.appEvents.on("composer:resized", this, this.applyDirection); diff --git a/app/assets/javascripts/select-kit/components/category-row.js.es6 b/app/assets/javascripts/select-kit/components/category-row.js.es6 index ccd131c75d5..8d1c8cd3ed2 100644 --- a/app/assets/javascripts/select-kit/components/category-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-row.js.es6 @@ -32,18 +32,21 @@ export default SelectKitRowComponent.extend({ } }, - @computed("category") - badgeForCategory(category) { + @computed("category", "parentCategory") + badgeForCategory(category, parentCategory) { return categoryBadgeHTML(category, { link: this.get("categoryLink"), allowUncategorized: this.get("allowUncategorized"), - hideParent: this.get("hideParentCategory") + hideParent: parentCategory ? true : false }).htmlSafe(); }, @computed("parentCategory") badgeForParentCategory(parentCategory) { - return categoryBadgeHTML(parentCategory, {link: false}).htmlSafe(); + return categoryBadgeHTML(parentCategory, { + link: this.get("categoryLink"), + allowUncategorized: this.get("allowUncategorized") + }).htmlSafe(); }, @computed("parentCategoryid") diff --git a/app/assets/javascripts/select-kit/components/composer-actions.js.es6 b/app/assets/javascripts/select-kit/components/composer-actions.js.es6 index 44c58379744..174563ba040 100644 --- a/app/assets/javascripts/select-kit/components/composer-actions.js.es6 +++ b/app/assets/javascripts/select-kit/components/composer-actions.js.es6 @@ -180,8 +180,7 @@ export default DropdownSelectBoxComponent.extend({ options.action = Composer.CREATE_TOPIC; options.categoryId = this.get("composerModel.topic.category.id"); - this.get("composerController").close(); - this.get("composerController").open(options); + this._replyFromExisting(options, _postSnapshot, _topicSnapshot); break; case "reply_as_private_message": diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index 0de5b95894e..d28f7a1caae 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -3,7 +3,7 @@ import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from 'discourse/lib/ajax-error'; import { default as computed } from "ember-addons/ember-computed-decorators"; import renderTag from "discourse/lib/render-tag"; -const { get, isEmpty, isPresent, run } = Ember; +const { get, isEmpty, isPresent, run, makeArray } = Ember; export default ComboBox.extend({ allowContentReplacement: true, @@ -14,6 +14,9 @@ export default ComboBox.extend({ filterable: true, noTags: Ember.computed.empty("computedTags"), allowAny: true, + maximumSelectionSize: Ember.computed.alias("siteSettings.max_tags_per_topic"), + caretUpIcon: Ember.computed.alias("caretIcon"), + caretDownIcon: Ember.computed.alias("caretIcon"), init() { this._super(); @@ -29,12 +32,41 @@ export default ComboBox.extend({ }); }, + @computed("limitReached", "maximumSelectionSize") + maxContentRow(limitReached, count) { + if (limitReached) { + return I18n.t("select_kit.max_content_reached", { count }); + } + }, + + mutateAttributes() { + this.set("value", null); + }, + + @computed("limitReached") + caretIcon(limitReached) { + return limitReached ? null : "plus"; + }, + + @computed("computedTags.[]", "maximumSelectionSize") + limitReached(computedTags, maximumSelectionSize) { + if (computedTags.length >= maximumSelectionSize) { + return true; + } + + return false; + }, + @computed("tags") computedTags(tags) { - return Ember.makeArray(tags); + return makeArray(tags); }, validateCreate(term) { + if (this.get("limitReached") || !this.site.get("can_create_tag")) { + return false; + } + const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g"); term = term.replace(filterRegexp, "").trim().toLowerCase(); @@ -50,8 +82,11 @@ export default ComboBox.extend({ }, validateSelect() { - return this.get("computedTags").length < this.get("siteSettings.max_tags_per_topic") && - this.site.get("can_create_tag"); + return this.get("computedTags").length < this.get("siteSettings.max_tags_per_topic"); + }, + + filterComputedContent(computedContent) { + return computedContent; }, didRender() { @@ -111,7 +146,7 @@ export default ComboBox.extend({ @computed("tags.[]", "filter") collectionHeader(tags, filter) { - if (!Ember.isEmpty(tags)) { + if (!isEmpty(tags)) { let output = ""; if (tags.length >= 20) { @@ -132,13 +167,16 @@ export default ComboBox.extend({ computeHeaderContent() { let content = this.baseHeaderComputedContent(); + const joinedTags = this.get("computedTags").join(", "); if (isEmpty(this.get("computedTags"))) { content.label = I18n.t("tagging.choose_for_topic"); } else { - content.label = this.get("computedTags").join(","); + content.label = joinedTags; } + content.title = content.name = content.value = joinedTags; + return content; }, @@ -148,43 +186,38 @@ export default ComboBox.extend({ delete tags[tags.indexOf(tag)]; this.set("tags", tags.filter(t => t)); this.set("content", []); - this.set("searchDebounce", run.debounce(this, this._searchTags, 200)); + this.set("searchDebounce", run.debounce(this, this._searchTags, this.get("filter"), 250)); }, onExpand() { - this.set("searchDebounce", run.debounce(this, this._searchTags, 200)); + if (isEmpty(this.get("content"))) { + this.set("searchDebounce", run.debounce(this, this._searchTags, this.get("filter"), 250)); + } }, onFilter(filter) { filter = isEmpty(filter) ? null : filter; - this.set("searchDebounce", run.debounce(this, this._searchTags, filter, 200)); + this.set("searchDebounce", run.debounce(this, this._searchTags, filter, 250)); }, onSelect(tag) { if (isEmpty(this.get("computedTags"))) { - this.set("tags", Ember.makeArray(tag)); + this.set("tags", makeArray(tag)); } else { this.set("tags", this.get("computedTags").concat(tag)); } this.set("content", []); - this.set("searchDebounce", run.debounce(this, this._searchTags, 200)); + this.set("searchDebounce", run.debounce(this, this._searchTags, this.get("filter"), 250)); } }, - muateAttributes() { - this.set("value", null); - }, - _searchTags(query) { this.startLoading(); - const selectedTags = Ember.makeArray(this.get("computedTags")).filter(t => t); - const self = this; - + const selectedTags = makeArray(this.get("computedTags")).filter(t => t); const sortTags = this.siteSettings.tags_sort_alphabetically; - const data = { q: query, limit: this.siteSettings.max_tag_search_results, diff --git a/app/assets/javascripts/select-kit/components/none-category-row.js.es6 b/app/assets/javascripts/select-kit/components/none-category-row.js.es6 index 13636aa770c..51b6b4add0f 100644 --- a/app/assets/javascripts/select-kit/components/none-category-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/none-category-row.js.es6 @@ -1,9 +1,20 @@ import CategoryRowComponent from "select-kit/components/category-row"; +import { categoryBadgeHTML } from "discourse/helpers/category-link"; +import computed from "ember-addons/ember-computed-decorators"; export default CategoryRowComponent.extend({ layoutName: "select-kit/templates/components/category-row", classNames: "none category-row", + @computed("category") + badgeForCategory(category) { + return categoryBadgeHTML(category, { + link: this.get("categoryLink"), + allowUncategorized: true, + hideParent: true + }).htmlSafe(); + }, + click() { this.sendAction("clearSelection"); } diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6 index ab9852b1541..b8bdf5c6674 100644 --- a/app/assets/javascripts/select-kit/components/select-kit.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6 @@ -37,7 +37,6 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi tabindex: 0, none: null, highlightedValue: null, - noContentLabel: "select_kit.no_content", valueAttribute: "id", nameProperty: "name", autoFilterable: false, @@ -70,6 +69,8 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi allowContentReplacement: false, collectionHeader: null, allowAutoSelectFirst: true, + maximumSelectionSize: null, + maxContentRow: null, init() { this._super(); @@ -155,8 +156,10 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi }, @computed("filter", "filteredComputedContent.[]") - shouldDisplayNoContentRow(filter, filteredComputedContent) { - return filter.length > 0 && filteredComputedContent.length === 0; + noContentRow(filter, filteredComputedContent) { + if (filter.length > 0 && filteredComputedContent.length === 0) { + return I18n.t("select_kit.no_content"); + } }, @computed("filter", "filterable", "autoFilterable", "renderedFilterOnce") diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit.hbs index 5e6e936db4a..cd92fb83122 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit.hbs @@ -40,11 +40,11 @@ select=(action "select") highlight=(action "highlight") create=(action "create") - noContentLabel=noContentLabel highlightedValue=highlightedValue computedValue=computedValue - shouldDisplayNoContentRow=shouldDisplayNoContentRow rowComponentOptions=rowComponentOptions + noContentRow=noContentRow + maxContentRow=maxContentRow }} {{/if}} </div> diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs index bb02559029f..35d46789dd3 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs @@ -30,22 +30,26 @@ }} {{/if}} -{{#each filteredComputedContent as |computedContent|}} - {{component rowComponent - computedContent=computedContent - highlightedValue=highlightedValue - computedValue=computedValue - templateForRow=templateForRow - select=select - highlight=highlight - options=rowComponentOptions - }} -{{/each}} - -{{#if shouldDisplayNoContentRow}} - {{#if noContentLabel}} +{{#if maxContentRow}} + <li class="select-kit-row max-content"> + {{maxContentRow}} + </li> +{{else}} + {{#if noContentRow}} <li class="select-kit-row no-content"> - {{i18n noContentLabel}} + {{noContentRow}} </li> + {{else}} + {{#each filteredComputedContent as |computedContent|}} + {{component rowComponent + computedContent=computedContent + highlightedValue=highlightedValue + computedValue=computedValue + templateForRow=templateForRow + select=select + highlight=highlight + options=rowComponentOptions + }} + {{/each}} {{/if}} {{/if}} diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 2786a6e61a8..c9f786c9b1f 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -130,14 +130,23 @@ $mobile-breakpoint: 700px; } } -.content-list li a span.count { - font-size: $font-down-1; - float: right; - margin-right: 10px; - background-color: $primary-low; - padding: 2px 5px; - border-radius: 5px; - color: $primary; +.content-list { + width: 27%; + float: left; + li a span.count { + font-size: $font-down-1; + float: right; + margin-right: 10px; + background-color: $primary-low; + padding: 2px 5px; + border-radius: 5px; + color: $primary; + } +} + +.content-body { + float: left; + width: 60%; } .admin-content { @@ -196,7 +205,7 @@ $mobile-breakpoint: 700px; width: 460px; right: 0; z-index: z("dropdown"); - box-shadow: 0 2px 6px rgba(0,0,0, .8); + box-shadow: shadow("card"); margin-top: -2px; background-color: $secondary; padding: 12px 12px 5px; @@ -259,6 +268,10 @@ $mobile-breakpoint: 700px; background-color: $primary-low; padding: 10px 10px 3px 0; @include clearfix; + nav { + float: left; + margin-left: 12px; + } .nav.nav-pills { li.active { a { @@ -506,7 +519,6 @@ $mobile-breakpoint: 700px; background-color: $secondary; border: 1px solid $primary-low; border-radius: 3px; - box-shadow: inset 0 1px 1px rgba(51, 51, 51, 0.3); transition: border linear 0.2s, box-shadow linear 0.2s; li.select2-search-choice { diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 9a23fb7d738..e3204d60f5b 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -220,3 +220,7 @@ margin-top: 20px; } } + +#custom_emoji { + width: 27%; +} diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index ad742d5971a..2504fbd97ac 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -19,7 +19,7 @@ z-index: z("composer","content"); transition: height 250ms ease, background 250ms ease, transform 250ms ease, max-width 250ms ease; background-color: $secondary; - box-shadow: 0 -1px 40px rgba(0,0,0, .12); + box-shadow: shadow("composer"); .reply-area { display: flex; diff --git a/app/assets/stylesheets/common/base/crawler_layout.scss b/app/assets/stylesheets/common/base/crawler_layout.scss index 615d5b080ff..9d072e7c70d 100644 --- a/app/assets/stylesheets/common/base/crawler_layout.scss +++ b/app/assets/stylesheets/common/base/crawler_layout.scss @@ -6,7 +6,7 @@ body.crawler { z-index: z("max"); background-color: #fff; padding: 10px; - box-shadow: 0 2px 4px -1px rgba(0,0,0,0.25); + box-shadow: shadow("header"); } div.topic-list div[itemprop='itemListElement'] { padding: 10px 0; diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 613fa8301ee..21060272c52 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -165,7 +165,7 @@ textarea { &:focus:required:invalid:focus { border-color: $danger; - box-shadow: 0 0 6px $danger; + box-shadow: shadow("focus-danger"); } } @@ -196,7 +196,7 @@ input { border-radius: 0; &:focus { border-color: $tertiary; - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); outline: 0; } } @@ -208,7 +208,7 @@ textarea { border: 1px solid $primary-medium; &:focus { border-color: $tertiary; - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); outline: 0; } } diff --git a/app/assets/stylesheets/common/base/emoji.scss b/app/assets/stylesheets/common/base/emoji.scss index f1a18dd5564..7af43b61a7d 100644 --- a/app/assets/stylesheets/common/base/emoji.scss +++ b/app/assets/stylesheets/common/base/emoji.scss @@ -5,14 +5,12 @@ img.emoji { } .emoji-picker { - box-shadow: 0 1px 5px rgba(0,0,0,0.4); background-clip: padding-box; z-index: z("modal","content"); position: fixed; display: none; flex-direction: row; height: 300px; - border-radius: 3px; color: dark-light-choose(darken($primary, 40%), blend-primary-secondary(90%)); background-color: $secondary; border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index b28652565ff..4daaeafac0a 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -4,7 +4,7 @@ top: 0; z-index: z("header"); background-color: $header_background; - box-shadow: 0 2px 4px -1px rgba(0,0,0, .25); + box-shadow: shadow("header"); .docked & { position: fixed; @@ -33,11 +33,15 @@ .panel { float: right; position: relative; + display: flex; + align-items: center; + } + + .header-buttons { + margin-top: .2em; } .login-button, button.sign-up-button { - float: left; - margin-top: 7px; padding: 6px 10px; .fa { margin-right: 3px; } } @@ -54,6 +58,7 @@ .header-dropdown-toggle, .drop-down, .panel-body { .flagged-posts, .queued-posts { background: $danger; + min-width: 6px; } } @@ -62,14 +67,17 @@ margin: 0 0 0 5px; list-style: none; - > li { float: left; } .icon { position: relative; - display: block; - padding: 3px; + display: flex; + align-items: center; + justify-content: center; + width: 2.2857em; + height: 2.2857em; + padding: .2143em; color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); text-decoration: none; cursor: pointer; @@ -77,11 +85,13 @@ border-left: 1px solid transparent; border-right: 1px solid transparent; transition: all linear .15s; - - + img.avatar { + width: 2.2857em; + height: 2.2857em; + } &:hover { color: $primary; - background-color: $primary-low; + background-color: $primary-low; border-top: 1px solid transparent; border-left: 1px solid transparent; border-right: 1px solid transparent; @@ -119,8 +129,7 @@ } .d-icon { - width: 32px; - height: 32px; + width: 100%; font-size: $font-up-4; line-height: $line-height-large; display: inline-block; diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index ad27956a56b..ad8c9cca530 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -1,6 +1,7 @@ .menu-panel.slide-in { position: fixed; right: 0; + box-shadow: shadow("header"); .panel-body { position: absolute; @@ -19,7 +20,7 @@ .menu-panel { border: 1px solid $primary-low; - box-shadow: 0 2px 2px rgba(0,0,0, .25); + box-shadow: shadow("menu-panel"); background-color: $secondary; z-index: z("header"); padding: 0.5em; @@ -44,7 +45,6 @@ overflow-y: auto; overflow-x: hidden; } - } .menu-links.columned { @@ -69,7 +69,6 @@ margin-left: 0.5em; color: dark-light-choose($primary-medium, $secondary-medium); } - } li.category-link { @@ -77,7 +76,7 @@ background-color: transparent; display: inline-flex; margin: 0.25em 0.5em; - width: 44%; + width: 43%; .badge-notification { color: dark-light-choose($primary-medium, $secondary-medium); background-color: transparent; @@ -90,7 +89,6 @@ overflow: hidden; text-overflow: ellipsis; display: inline-block; - max-width: 80%; &.bar, &.bullet { color: $primary; } @@ -100,7 +98,7 @@ padding-top: 2px; } span { - z-index: z("base") * -1; + z-index: z("base") * -1; } } } diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 2a5b765cd37..6d52ce436d8 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -17,7 +17,7 @@ overflow: auto; height: auto; background-color: $secondary; - box-shadow: 0 2px 2px rgba(0,0,0, .25); + box-shadow: shadow("card"); background-clip: padding-box; } @@ -45,8 +45,7 @@ .modal-backdrop, .modal-backdrop.fade.in { - -webkit-animation: fade .3s; - animation: fade .3s; + animation: fade .3s; opacity: .9; filter: alpha(opacity=90); } @@ -57,22 +56,12 @@ to { opacity: .9 } } -@-webkit-keyframes fade { - from { opacity: 0 } - to { opacity: .9 } -} - // slide in @keyframes slidein { from { transform: translateY(-20%); } to { transform: translateY(0); } } -@-webkit-keyframes slidein { - from { -webkit-transform: translateY(-20%); } - to { -webkit-transform: translateY(0); } -} - .modal-outer-container { display:table; table-layout: fixed; @@ -85,6 +74,7 @@ margin: 0 auto; background-color: $secondary; background-clip: padding-box; + box-shadow: shadow("modal"); .select-kit { width: 220px; @@ -93,8 +83,7 @@ .create-account.in .modal-inner-container, .login-modal.in .modal-inner-container { - -webkit-animation: slidein .3s; - animation: slidein .3s; + animation: slidein .3s; } diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index 61d92687fe2..c692cfceb7d 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -463,10 +463,22 @@ aside.onebox.stackexchange .onebox-body { .label2 { float: right; } - .site-icon { - width: 16px; - height: 16px; - margin-right: 3px; +} + +.onebox { + &.whitelistedgeneric, + &.gfycat { + .site-icon { + width: 16px; + height: 16px; + margin-right: 3px; + } + } +} + +.onebox.gfycat p { + span.label1 a { + white-space: nowrap; } } diff --git a/app/assets/stylesheets/common/base/share_link.scss b/app/assets/stylesheets/common/base/share_link.scss index 8a5f9145ca4..2914ceb9a12 100644 --- a/app/assets/stylesheets/common/base/share_link.scss +++ b/app/assets/stylesheets/common/base/share_link.scss @@ -4,7 +4,7 @@ position: absolute; left: 20px; z-index: z("dropdown"); - box-shadow: 0 1px 5px rgba(0,0,0, .4); + box-shadow: shadow("card"); background-color: $secondary; padding: 6px 10px 10px 10px; width: 300px; diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 6ab19d0eb11..11248496fcc 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -176,6 +176,7 @@ $tag-color: $primary-medium; .mobile-view .topic-list-item .discourse-tags { display: inline-flex; + flex-wrap: wrap; font-size: $font-down-1; margin-top: 0; .discourse-tag { diff --git a/app/assets/stylesheets/common/base/topic-admin-menu.scss b/app/assets/stylesheets/common/base/topic-admin-menu.scss index 56f5ba16e65..b3fb88d0908 100644 --- a/app/assets/stylesheets/common/base/topic-admin-menu.scss +++ b/app/assets/stylesheets/common/base/topic-admin-menu.scss @@ -12,8 +12,9 @@ background-color: $secondary; width: 205px; padding: 10px; - border: 1px solid $primary-low; + border: 1px solid $primary-low; z-index: z("dropdown"); + box-shadow: shadow("card"); ul { list-style: none; diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 9614af03bc4..8650e42eb43 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -263,7 +263,7 @@ aside.quote { } -.topic-avatar, .avatar-flair-preview, .user-card-avatar, .topic-map .poster { +.topic-avatar, .avatar-flair-preview, .user-card-avatar, .topic-map .poster, .user-profile-avatar { .avatar-flair { display: flex; align-items: center; @@ -275,7 +275,7 @@ aside.quote { right: -6px; } } -.topic-avatar .avatar-flair, .avatar-flair-preview .avatar-flair { +.topic-avatar .avatar-flair, .avatar-flair-preview .avatar-flair, .collapsed-info .user-profile-avatar .avatar-flair { background-size: 20px 20px; width: 20px; height: 20px; @@ -288,7 +288,7 @@ aside.quote { right: -8px; } } -.user-card-avatar .avatar-flair { +.user-card-avatar .avatar-flair, .user-profile-avatar .avatar-flair { background-size: 40px 40px; width: 40px; height: 40px; @@ -398,7 +398,11 @@ kbd background-color: $secondary; border: 1px solid $primary-low; border-radius: 3px; - box-shadow: 0 1px 0 rgba(0,0,0, .8); + box-shadow: shadow("kbd"); + background: dark-light-choose(#fafafa, #333); + border: 1px solid dark-light-choose(#ccc, #555); + border-bottom: medium none dark-light-choose(#fff, #000); + color: $primary; display: inline-block; font-size: $font-down-1; diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index bc426d3e0ae..9742fb7226d 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -124,7 +124,6 @@ background-color: $primary-low; margin-right: 5px; margin-bottom: 10px; - box-shadow: 1px 1px 3px rgba(0.0, 0.0, 0.0, 0.2); .check-display { position: absolute; diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 5efd7ed9ca2..8c39904e934 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -120,6 +120,16 @@ } } } + + .user-profile-avatar { + position: relative; + float: left; + height: 100%; + .avatar-flair { + bottom: 8px; + right: 16px; + } + } } .controls { @@ -176,6 +186,13 @@ } } } + + .user-profile-avatar { + .avatar-flair { + bottom: 8px; + right: 2px; + } + } } } diff --git a/app/assets/stylesheets/common/components/badges.scss b/app/assets/stylesheets/common/components/badges.scss index d7123032baa..a416c0a905a 100644 --- a/app/assets/stylesheets/common/components/badges.scss +++ b/app/assets/stylesheets/common/components/badges.scss @@ -211,11 +211,9 @@ .badge-group { @extend %badge; - padding: 4px 5px 2px 5px; + padding: 2px 5px; color: $primary; - text-shadow: 0 1px 0 rgba($primary, 0.1); background-color: $primary-low; border-color: $primary-low; font-size: $font-down-1; - box-shadow: inset 0 1px 0 rgba(0,0,0, 0.22); } diff --git a/app/assets/stylesheets/common/components/banner.scss b/app/assets/stylesheets/common/components/banner.scss index 47e09f43f31..c70a20bc979 100644 --- a/app/assets/stylesheets/common/components/banner.scss +++ b/app/assets/stylesheets/common/components/banner.scss @@ -5,7 +5,6 @@ #banner { padding: 10px; background: $tertiary-low; - box-shadow: 0 2px 4px -1px rgba(0,0,0, .25); color: $primary; z-index: z("base") + 1; overflow: auto; diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index 059f09d7405..2dd074265be 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -119,8 +119,6 @@ .btn-social { color: #fff; - text-shadow: 0 1px 0 rgba($primary, 0.2); - box-shadow: inset 0 1px 0 rgba(0,0,0, 0.1); &:hover { color: #fff; } diff --git a/app/assets/stylesheets/common/components/keyboard_shortcuts.scss b/app/assets/stylesheets/common/components/keyboard_shortcuts.scss index 04045f843f9..643c314463d 100644 --- a/app/assets/stylesheets/common/components/keyboard_shortcuts.scss +++ b/app/assets/stylesheets/common/components/keyboard_shortcuts.scss @@ -15,8 +15,12 @@ } #keyboard-shortcuts-help { - .span6 { - width:32%; + div.row { + width: 100%; + div { + float: left; + width:32%; + } } ul { list-style: none; @@ -29,7 +33,7 @@ b { padding: 2px 6px; border-radius: 4px; - box-shadow: 0 2px 0 rgba(0,0,0,0.2),0 0 0 1px dark-light-choose(#fff,#000) inset; + box-shadow: shadow("kbd"); background: dark-light-choose(#fafafa, #333); border: 1px solid dark-light-choose(#ccc, #555); border-bottom: medium none dark-light-choose(#fff, #000); diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 59d0dfb0bcd..f028ab048dd 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -103,6 +103,26 @@ $z-layers: ( @return map-deep-get($z-layers, $layers...); } + +// Box-shadow +// -------------------------------------------------- + +$box-shadow: ( + "modal": 0 8px 60px rgba(0, 0, 0, 0.6), + "composer": 0 -1px 40px rgba(0, 0, 0, 0.12), + "menu-panel": 0 6px 14px rgba(0, 0, 0, 0.15), + "card": 0 4px 14px rgba(0, 0, 0, 0.15), + "dropdown": 0 2px 3px 0 rgba(0, 0, 0, 0.2), + "header": 0 2px 4px -1px rgba(0, 0, 0, 0.25), + "kbd": (0 2px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px dark-light-choose(#fff, #000) inset), + "focus": 0 0 6px 0 $tertiary, + "focus-danger": 0 0 6px 0 $danger +); + +@function shadow($key) { + @return map-get($box-shadow, $key); +} + // Color utilities // -------------------------------------------------- diff --git a/app/assets/stylesheets/common/input_tip.scss b/app/assets/stylesheets/common/input_tip.scss index 0d5de0c8abe..7a57d21069a 100644 --- a/app/assets/stylesheets/common/input_tip.scss +++ b/app/assets/stylesheets/common/input_tip.scss @@ -9,7 +9,7 @@ &.bad { background: $danger-medium; color: white; - box-shadow: 1px 1px 5px rgba(0,0,0, .7); + box-shadow: shadow("dropdown"); } &.hide, &.good { display: none; diff --git a/app/assets/stylesheets/common/select-kit/category-drop.scss b/app/assets/stylesheets/common/select-kit/category-drop.scss index 1fdb3dbdb23..91a9bf9d71a 100644 --- a/app/assets/stylesheets/common/select-kit/category-drop.scss +++ b/app/assets/stylesheets/common/select-kit/category-drop.scss @@ -6,7 +6,6 @@ .badge-wrapper { font-size: $font-0; font-weight: normal; - line-height: $line-height-large; &.box { margin: 0; @@ -17,40 +16,24 @@ } } - &.bar.has-selection .category-drop-header { - padding: 4.5px 10px; + &.bar.has-selection .category-drop-header, + &.box.has-selection .category-drop-header, + &.none.has-selection .category-drop-header { + padding: 5px 10px; } &.bullet.has-selection .category-drop-header { - padding: 6px 10px; - span.badge-category { - line-height: $line-height-medium; - } - .selected-name { - .bullet { - line-height: $line-height-medium; - } - } - } - - &.box.has-selection .category-drop-header { - padding: 3px 10px; - } - - &.none.has-selection .category-drop-header { - padding: 4.5px 10px; + padding: 5px 10px; } .category-drop-header { background: $primary-low; color: $primary; - border: none; - padding: 6px 10px; - font-size: $font-up-1; - line-height: $line-height-medium; + border: 1px solid transparent; + padding: 5px 10px; + font-size: $font-0; transition: none; - .badge-wrapper { margin-right: 0; } @@ -64,6 +47,11 @@ } } + &.is-expanded .category-drop-header { + border: 1px solid $tertiary; + box-shadow: shadow("focus"); + } + .select-kit-collection { display: flex; flex-direction: column; @@ -93,8 +81,7 @@ width: auto; min-width: 300px; border-radius: 0; - -webkit-box-shadow: 0 2px 2px rgba(0,0,0,0.4); - box-shadow: 0 2px 2px rgba(0,0,0,0.4); + box-shadow: shadow("dropdown"); } .select-kit-row { diff --git a/app/assets/stylesheets/common/select-kit/combo-box.scss b/app/assets/stylesheets/common/select-kit/combo-box.scss index e69bfdcdcdc..b9ce024633a 100644 --- a/app/assets/stylesheets/common/select-kit/combo-box.scss +++ b/app/assets/stylesheets/common/select-kit/combo-box.scss @@ -32,8 +32,7 @@ &.is-focused { border: 1px solid $tertiary; - -webkit-box-shadow: $tertiary 0 0 6px 0px; - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); } } @@ -47,8 +46,7 @@ &.is-highlighted { .select-kit-header { border: 1px solid $tertiary; - -webkit-box-shadow: $tertiary 0 0 6px 0px; - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); } } @@ -56,14 +54,12 @@ .select-kit-wrapper { display: block; border: 1px solid $tertiary; - -webkit-box-shadow: $tertiary 0 0 6px 0px; - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); } .select-kit-header { border-color: transparent; - -webkit-box-shadow: none; - box-shadow: none; + box-shadow: none; } .select-kit-body { diff --git a/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss b/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss index 591360d727d..c229b004223 100644 --- a/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss +++ b/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss @@ -16,8 +16,7 @@ .select-kit-body { border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); background-clip: padding-box; - -webkit-box-shadow: 0 2px 2px rgba(0,0,0,0.4); - box-shadow: 0 2px 2px rgba(0,0,0,0.4); + box-shadow: shadow("dropdown"); max-width: 300px; width: 300px; } @@ -120,6 +119,12 @@ margin-left: 5px; } + &:hover { + .d-icon { + color: $primary-low; + } + } + &.is-focused { outline-style: auto; outline-color: $tertiary; diff --git a/app/assets/stylesheets/common/select-kit/legacy-combo-box.scss b/app/assets/stylesheets/common/select-kit/legacy-combo-box.scss index 3ba9f7ad942..2b72735d4ca 100644 --- a/app/assets/stylesheets/common/select-kit/legacy-combo-box.scss +++ b/app/assets/stylesheets/common/select-kit/legacy-combo-box.scss @@ -79,7 +79,7 @@ } .select2-container-active { - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); } .select2-results .select2-no-results, .select2-results .select2-searching, .select2-results .select2-selection-limit { diff --git a/app/assets/stylesheets/common/select-kit/mini-tag-chooser.scss b/app/assets/stylesheets/common/select-kit/mini-tag-chooser.scss index 25f4441ffa6..362e9b187af 100644 --- a/app/assets/stylesheets/common/select-kit/mini-tag-chooser.scss +++ b/app/assets/stylesheets/common/select-kit/mini-tag-chooser.scss @@ -7,8 +7,7 @@ &.is-expanded { .select-kit-header { border: 1px solid $tertiary; - -webkit-box-shadow: $tertiary 0 0 6px 0px; - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); } } @@ -21,14 +20,15 @@ .select-kit-body { max-width: 500px; width: 500px; - border: 1px solid $primary-low; + border: 1px solid $tertiary; + box-shadow: shadow("focus") } .select-kit-filter { border-top: 0; } - .select-kit-wrapper { + &.is-expanded .select-kit-wrapper, .select-kit-wrapper { display: none; } @@ -59,8 +59,7 @@ } .selected-tag { - background: $primary-low; - border-radius: 2px; + background: $primary-very-low; padding: 2px 4px; margin: 2px; border: 0; @@ -69,10 +68,17 @@ box-shadow: 0 0 2px $danger, 0 1px 0 rgba(0,0,0,0.05); } - &:before { + &:after { content: '\f00d'; + color: $primary-low-mid; font-family: 'FontAwesome'; } + + &:hover { + &:after { + color: $danger; + } + } } } } diff --git a/app/assets/stylesheets/common/select-kit/multi-select.scss b/app/assets/stylesheets/common/select-kit/multi-select.scss index de862610259..db3734bfb63 100644 --- a/app/assets/stylesheets/common/select-kit/multi-select.scss +++ b/app/assets/stylesheets/common/select-kit/multi-select.scss @@ -24,7 +24,7 @@ border: 1px solid $primary-medium; &.is-focused { - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); border-radius: 0; } } @@ -40,7 +40,7 @@ .multi-select-header { border-radius: 0; border-bottom: 1px solid transparent; - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); } } @@ -48,7 +48,7 @@ .select-kit-wrapper { display: block; border: 1px solid $tertiary; - box-shadow: $tertiary 0 0 6px 0px; + box-shadow: shadow("focus"); border-radius: 0; } @@ -101,7 +101,6 @@ margin: 0; outline: 0; border: 0; - -webkit-box-shadow: none; box-shadow: none; border-radius: 0; height: 21px; diff --git a/app/assets/stylesheets/common/select-kit/period-chooser.scss b/app/assets/stylesheets/common/select-kit/period-chooser.scss index 3994057f830..1759b9ddf68 100644 --- a/app/assets/stylesheets/common/select-kit/period-chooser.scss +++ b/app/assets/stylesheets/common/select-kit/period-chooser.scss @@ -18,10 +18,15 @@ h2.selected-name { overflow: auto; - color: black; + color: $secondary; display: inline-block; box-sizing: border-box; + .date-section { + color: $primary; + margin-right: 5px; + } + .top-date-string { font-size: $font-down-1; color: dark-light-choose($primary-medium, $secondary-high); @@ -31,7 +36,7 @@ } .d-icon { - color: black; + color: $primary; opacity: 1; margin: 5px 0 10px 5px; font-size: $font-up-3; @@ -46,8 +51,7 @@ .period-chooser-row { font-weight: bold; - padding: 5px; - color: #222; + padding: 5px;; font-size: $font-up-1; align-items: center; display: flex; @@ -56,6 +60,10 @@ flex: 1; } + .date-section { + color: $primary; + } + .top-date-string { font-weight: normal; font-size: $font-down-1; diff --git a/app/assets/stylesheets/common/select-kit/select-kit.scss b/app/assets/stylesheets/common/select-kit/select-kit.scss index 91e7fe922f7..b5c1597ef32 100644 --- a/app/assets/stylesheets/common/select-kit/select-kit.scss +++ b/app/assets/stylesheets/common/select-kit/select-kit.scss @@ -165,6 +165,11 @@ white-space: nowrap; } + &.max-content { + white-space: nowrap; + color: $danger; + } + .name { margin: 0; overflow: hidden; diff --git a/app/assets/stylesheets/common/select-kit/tag-drop.scss b/app/assets/stylesheets/common/select-kit/tag-drop.scss index 24f08ebc060..0909948f968 100644 --- a/app/assets/stylesheets/common/select-kit/tag-drop.scss +++ b/app/assets/stylesheets/common/select-kit/tag-drop.scss @@ -6,10 +6,10 @@ .tag-drop-header { background: $primary-low; color: $primary; - border: none; - padding: 4.5px 10px; - font-size: $font-up-1; - line-height: $line-height-large; + border: 1px solid transparent; + padding: 5px 10px; + font-size: $font-0; + transition: none; .d-icon { opacity: 1; @@ -17,6 +17,11 @@ } } + &.is-expanded .tag-drop-header { + border: 1px solid $tertiary; + box-shadow: shadow("focus"); + } + .select-kit-collection { display: flex; flex-direction: column; @@ -44,8 +49,7 @@ width: auto; min-width: 150px; border-radius: 0; - -webkit-box-shadow: 0 2px 2px rgba(0,0,0,0.4); - box-shadow: 0 2px 2px rgba(0,0,0,0.4); + box-shadow: shadow("dropdown"); } .select-kit-row { diff --git a/app/assets/stylesheets/common/topic-entrance.scss b/app/assets/stylesheets/common/topic-entrance.scss index 03caf20de59..1f20b3ac562 100644 --- a/app/assets/stylesheets/common/topic-entrance.scss +++ b/app/assets/stylesheets/common/topic-entrance.scss @@ -3,7 +3,7 @@ border: 1px solid $primary-low; padding: 5px; background: $secondary; - box-shadow: 0 0 2px rgba(0,0,0, .2); + box-shadow: shadow("card"); z-index: z("dropdown"); position: absolute; diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index b3d84c056ea..f5e77393ca2 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -32,6 +32,9 @@ &.timeline-fullscreen.show { max-height: 700px; transition: max-height 0.4s ease-out; + @media screen and (max-height: 425px) { + max-height: 75vh; + } .topic-timeline { .timeline-footer-controls { display: inherit; @@ -49,9 +52,12 @@ left: 0; right: 0; border-top: 1px solid dark-light-choose($primary-low, $secondary-low); - box-shadow: 0px -2px 4px -1px rgba(0,0,0,.25); + box-shadow: shadow("composer"); padding-top: 20px; z-index: z("fullscreen"); + @media screen and (max-height: 425px) { + padding-top: 10px; + } .back-button { display: none; } @@ -77,6 +83,9 @@ display: block; display: -webkit-box; -webkit-line-clamp: 8; + @media screen and (max-height: 425px) { + -webkit-line-clamp: 5; + } -webkit-box-orient: vertical; } .username { diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index d7fc0fb05f5..30c278b6dc5 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -10,10 +10,14 @@ align-items: center; } } + + #private-message-users { + width: 404px; + } } .category-input { - margin-left: 10px; + margin-left: 10px; } .edit-title { @@ -27,7 +31,7 @@ } } } - + .with-tags { .d-editor-preview-wrapper { margin-top: -77px; @@ -75,7 +79,7 @@ overflow-y: auto; z-index: z("composer","popover"); padding: 10px 10px 35px 10px; - box-shadow: 3px 3px 3px rgba(0,0,0, 0.34); + box-shadow: shadow("card"); background: $highlight-medium; &.urgent { diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 5f718e9502a..273e967534c 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -244,7 +244,7 @@ input { &:focus { border-color: scale-color($danger, $lightness: -30%); - box-shadow: 0 0 6px $danger; + box-shadow: shadow("focus-danger"); } } @@ -287,7 +287,7 @@ input { &:focus { border-color: $success; - box-shadow: 0 0 6px $success; + box-shadow: shadow("focus"); } } @@ -351,46 +351,6 @@ input { @include clearfix; } -.span { - &4 { - width: 196px; - margin-right: 12px; - float: left; - } - - &6 { - width: 27.027%; - float: left; - } - - &8 { - width: 404px; - float: left; - } - - &10 { - width: 508px; - float: left; - } - - &13 { - width: 59.8198%; - float: left; - } - - &15 { - /* intentionally no width set here, do not add one */ - margin-left: 12px; - float: left; - } - - &24 { - width: 1236px; - float: left; - color: amarillo; - } -} - .offset { &2 { margin-left: 116px; diff --git a/app/assets/stylesheets/desktop/header.scss b/app/assets/stylesheets/desktop/header.scss index ad83f6d2c2e..4a6f4069832 100644 --- a/app/assets/stylesheets/desktop/header.scss +++ b/app/assets/stylesheets/desktop/header.scss @@ -5,9 +5,9 @@ .d-header { left: 0; padding-top: 3px; - height: 60px; + height: 4.2857em; .d-icon-home { - padding:8px; + padding: 8px; font-size: $font-up-5; } @@ -17,9 +17,10 @@ } } -@media all -and (max-width : 570px) { - .extra-info-wrapper {display: none;} +@media all and (max-width: 570px) { + .extra-info-wrapper { + display: none; + } } #main { diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index dd652652031..d9e5627bb17 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -663,7 +663,7 @@ $topic-avatar-width: 45px; list-style: none; background-color: $secondary; border: 1px solid $primary-low; - box-shadow: 0 1px 5px rgba(0,0,0, .4); + box-shadow: shadow("dropdown"); background-clip: padding-box; span { font-size: $font-down-1; @@ -778,13 +778,12 @@ $topic-avatar-width: 45px; } &:active { @include linear-gradient(darken($tertiary, 18%), darken($tertiary, 12%)); - box-shadow: inset 0 1px 3px rgba(0,0,0, 0.2); color: $secondary; } &[disabled] { text-shadow: 0 1px 0 rgba($primary, 0.2); @include linear-gradient($tertiary, darken($tertiary, 20%)); - @include box-shadow((inset 0 1px 0 rgba(0,0,0, 0.33), inset 0 -1px 2px rgba($primary, 0.2))); + @include box-shadow(inset 0 1px 0 rgba(0,0,0, 0.33)); } } } diff --git a/app/assets/stylesheets/desktop/user-card.scss b/app/assets/stylesheets/desktop/user-card.scss index 8f09dd9fc8b..2450fd71dc9 100644 --- a/app/assets/stylesheets/desktop/user-card.scss +++ b/app/assets/stylesheets/desktop/user-card.scss @@ -10,7 +10,7 @@ $user_card_background: $secondary; left: -9999px; top: -9999px; z-index: z("usercard"); - box-shadow: 1px 2px 6px rgba(0,0,0, .25); + box-shadow: shadow("card"); margin-top: -2px; color: $user_card_primary; background: $user_card_background center center; diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index d342a3a83eb..0fb3e102f9f 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -109,12 +109,6 @@ } } -.user-invite-controls { - background-color: $primary-low; - padding: 5px 10px 0 0; - height: 35px; -} - .user-invite-search { clear: both; margin: 15px 0px -15px 0px; diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 8e7022e8e5e..8fdff1c9a23 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -345,7 +345,7 @@ tr.category-topic-link { background-color: $secondary; border: 1px solid dark-light-choose(rgba(0, 0, 0, 0.2), $primary); border-radius: 5px; - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: shadow("dropdown"); background-clip: padding-box; margin: 1px 0 20px; .title {font-weight: bold; display: block;} diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index dafbfd69e5d..703bba38ceb 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -52,10 +52,6 @@ } } -.docked #topic-progress { - box-shadow: 0 0 3px rbga(0,0,0, .5); -} - #topic-progress-wrapper { position: fixed; width: 0; diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index fccb2542ef4..7a652d99046 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -127,6 +127,10 @@ max-width: 700px; } } + + .user-profile-avatar .avatar-flair { + right: 2px; + } } .controls { @@ -177,6 +181,9 @@ } } } + .user-profile-avatar .avatar-flair { + bottom: 12px; + } } } diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 13aa89d3ca5..298161a88d1 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -25,7 +25,8 @@ class Admin::UsersController < Admin::AdminController :generate_api_key, :revoke_api_key, :anonymize, - :reset_bounce_score] + :reset_bounce_score, + :disable_second_factor] def index users = ::AdminUserIndexQuery.new(params).find_users @@ -289,7 +290,8 @@ class Admin::UsersController < Admin::AdminController silenced_till: params[:silenced_till], reason: params[:reason], message_body: message, - keep_posts: true + keep_posts: true, + post_id: params[:post_id] ) if silencer.silence && message.present? Jobs.enqueue( @@ -339,6 +341,23 @@ class Admin::UsersController < Admin::AdminController } end + def disable_second_factor + guardian.ensure_can_disable_second_factor!(@user) + user_second_factor = @user.user_second_factor + raise Discourse::InvalidParameters unless user_second_factor + + user_second_factor.destroy! + StaffActionLogger.new(current_user).log_disable_second_factor_auth(@user) + + Jobs.enqueue( + :critical_user_email, + type: :account_second_factor_disabled, + user_id: @user.id + ) + + render json: success_json + end + def destroy user = User.find_by(id: params[:id].to_i) guardian.ensure_can_delete_user!(user) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2ec9882e3a3..653ba952fc1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -107,7 +107,7 @@ class ApplicationController < ActionController::Base end def render_rate_limit_error(e) - render_json_error e.description, type: :rate_limit, status: 429 + render_json_error e.description, type: :rate_limit, status: 429, extras: { wait_seconds: e&.available_in } end # If they hit the rate limiter @@ -192,7 +192,9 @@ class ApplicationController < ActionController::Base render_json_error message, type: type, status: status_code else begin + # 404 pages won't have the session and theme_keys without these: current_user + handle_theme rescue Discourse::InvalidAccess return render plain: message, status: status_code end diff --git a/app/controllers/onebox_controller.rb b/app/controllers/onebox_controller.rb index 7c0d16af322..f093342c772 100644 --- a/app/controllers/onebox_controller.rb +++ b/app/controllers/onebox_controller.rb @@ -14,13 +14,21 @@ class OneboxController < ApplicationController return render(body: nil, status: 429) if Oneboxer.is_previewing?(current_user.id) user_id = current_user.id + category_id = params[:category_id].to_i + topic_id = params[:topic_id].to_i invalidate = params[:refresh] == 'true' url = params[:url] hijack do Oneboxer.preview_onebox!(user_id) - preview = Oneboxer.preview(url, invalidate_oneboxes: invalidate) + preview = Oneboxer.preview(url, + invalidate_oneboxes: invalidate, + user_id: user_id, + category_id: category_id, + topic_id: topic_id + ) + preview.strip! if preview.present? Oneboxer.onebox_previewed!(user_id) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 079ba724efb..2421ebbcae8 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -188,6 +188,10 @@ class SessionController < ApplicationController end def create + unless params[:second_factor_token].blank? + RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! + end + params.require(:login) params.require(:password) @@ -221,28 +225,50 @@ class SessionController < ApplicationController if payload = login_error_check(user) render json: payload else + if user.totp_enabled? && !user.authenticate_totp(params[:second_factor_token]) + return render json: failed_json.merge( + error: I18n.t("login.invalid_second_factor_code"), + reason: "invalid_second_factor" + ) + end + (user.active && user.email_confirmed?) ? login(user) : not_activated(user) end end def email_login raise Discourse::NotFound if !SiteSetting.enable_local_logins_via_email + second_factor_token = params[:second_factor_token] + token = params[:token] + valid_token = !!EmailToken.valid_token_format?(token) + user = EmailToken.confirmable(token)&.user - if EmailToken.valid_token_format?(params[:token]) && (user = EmailToken.confirm(params[:token])) + if valid_token && user&.totp_enabled? + RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! + + if !second_factor_token.present? + @second_factor_required = true + return render layout: 'no_ember' + elsif !user.authenticate_totp(second_factor_token) + @error = I18n.t('login.invalid_second_factor_code') + return render layout: 'no_ember' + end + end + + if user = EmailToken.confirm(token) if login_not_approved_for?(user) @error = login_not_approved[:error] - return render layout: 'no_ember' elsif payload = login_error_check(user) @error = payload[:error] - return render layout: 'no_ember' else log_on_user(user) - redirect_to path("/") + return redirect_to path("/") end else @error = I18n.t('email_login.invalid_token') - return render layout: 'no_ember' end + + render layout: 'no_ember' end def forgot_password @@ -261,7 +287,7 @@ class SessionController < ApplicationController Jobs.enqueue(:critical_user_email, type: :forgot_password, user_id: user.id, email_token: email_token.token) end - json = { result: "ok" } + json = success_json unless SiteSetting.hide_email_address_taken json[:user_found] = user_presence end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 03a88119c48..069562e3e3e 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -148,9 +148,9 @@ class StaticController < ApplicationController def service_worker_asset respond_to do |format| format.js do - - # we take 1 hour to give a new service worker to all users - immutable_for 1.hour + # https://github.com/w3c/ServiceWorker/blob/master/explainer.md#updating-a-service-worker + # Maximum cache that the service worker will respect is 24 hours. + immutable_for 24.hours render( plain: Rails.application.assets_manifest.find_sources('service-worker.js').first, diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 613215df520..6fdbb5ad924 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -12,7 +12,7 @@ class UsersController < ApplicationController requires_login only: [ :username, :update, :user_preferences_redirect, :upload_user_image, :pick_avatar, :destroy_user_image, :destroy, :check_emails, :topic_tracking_state, - :preferences + :preferences, :create_second_factor, :update_second_factor ] skip_before_action :check_xhr, only: [ @@ -470,12 +470,24 @@ class UsersController < ApplicationController end end + totp_enabled = @user&.totp_enabled? + + if !totp_enabled || @user.authenticate_totp(params[:second_factor_token]) + secure_session["second-factor-#{token}"] = "true" + end + + valid_second_factor = secure_session["second-factor-#{token}"] == "true" + if !@user @error = I18n.t('password_reset.no_token') elsif request.put? @invalid_password = params[:password].blank? || params[:password].length > User.max_password_length - if @invalid_password + if !valid_second_factor + RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! + @user.errors.add(:user_second_factor, :invalid) + @error = I18n.t('login.invalid_second_factor_code') + elsif @invalid_password @user.errors.add(:password, :invalid) else @user.password = params[:password] @@ -484,6 +496,7 @@ class UsersController < ApplicationController if @user.save Invite.invalidate_for_email(@user.email) # invite link can't be used to log in anymore secure_session["password-#{token}"] = nil + secure_session["second-factor-#{token}"] = nil logon_after_password_reset end end @@ -496,9 +509,14 @@ class UsersController < ApplicationController else store_preloaded( "password_reset", - MultiJson.dump(is_developer: UsernameCheckerService.is_developer?(@user.email), admin: @user.admin?) + MultiJson.dump( + is_developer: UsernameCheckerService.is_developer?(@user.email), + admin: @user.admin?, + second_factor_required: !valid_second_factor + ) ) end + return redirect_to(wizard_path) if request.put? && Wizard.user_requires_completion?(@user) end @@ -521,7 +539,11 @@ class UsersController < ApplicationController } end else - render json: { is_developer: UsernameCheckerService.is_developer?(@user.email), admin: @user.admin? } + render json: { + is_developer: UsernameCheckerService.is_developer?(@user.email), + admin: @user.admin?, + second_factor_required: !valid_second_factor + } end end end @@ -550,7 +572,7 @@ class UsersController < ApplicationController def admin_login return redirect_to(path("/")) if current_user - if request.put? + if request.put? && params[:email].present? RateLimiter.new(nil, "admin-login-hr-#{request.remote_ip}", 6, 1.hour).performed! RateLimiter.new(nil, "admin-login-min-#{request.remote_ip}", 3, 1.minute).performed! @@ -561,15 +583,29 @@ class UsersController < ApplicationController else @message = I18n.t("admin_login.errors.unknown_email_address") end - elsif params[:token].present? - if EmailToken.valid_token_format?(params[:token]) - @user = EmailToken.confirm(params[:token]) + elsif (token = params[:token]).present? + valid_token = EmailToken.valid_token_format?(token) - if @user&.admin? - log_on_user(@user) - return redirect_to path("/") + if valid_token + if params[:second_factor_token].present? + RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! + end + + email_token_user = EmailToken.confirmable(token)&.user + totp_enabled = email_token_user.totp_enabled? + + if !totp_enabled || email_token_user.authenticate_totp(params[:second_factor_token]) + @user = EmailToken.confirm(token) + + if @user && @user.admin? + log_on_user(@user) + return redirect_to path("/") + else + @message = I18n.t("admin_login.errors.unknown_email_address") + end else - @message = I18n.t("admin_login.errors.unknown_email_address") + @second_factor_required = true + @message = I18n.t("login.second_factor_title") end else @message = I18n.t("admin_login.errors.invalid_token") @@ -609,7 +645,7 @@ class UsersController < ApplicationController end end - json = { result: "ok" } + json = success_json json[:user_found] = user_presence unless SiteSetting.hide_email_address_taken render json: json rescue RateLimiter::LimitExceeded @@ -899,6 +935,60 @@ class UsersController < ApplicationController render layout: 'no_ember' end + def create_second_factor + RateLimiter.new(nil, "login-hr-#{request.remote_ip}", SiteSetting.max_logins_per_ip_per_hour, 1.hour).performed! + RateLimiter.new(nil, "login-min-#{request.remote_ip}", SiteSetting.max_logins_per_ip_per_minute, 1.minute).performed! + + unless current_user.confirm_password?(params[:password]) + return render json: failed_json.merge( + error: I18n.t("login.incorrect_password") + ) + end + + qrcode_svg = RQRCode::QRCode.new(current_user.totp_provisioning_uri).as_svg( + offset: 0, + color: '000', + shape_rendering: 'crispEdges', + module_size: 4 + ) + + render json: success_json.merge( + key: current_user.user_second_factor.data, + qr: qrcode_svg + ) + end + + def update_second_factor + params.require(:second_factor_token) + + [request.remote_ip, current_user.id].each do |key| + RateLimiter.new(nil, "second-factor-min-#{key}", 3, 1.minute).performed! + end + + user_second_factor = current_user.user_second_factor + raise Discourse::InvalidParameters unless user_second_factor + + unless current_user.authenticate_totp(params[:second_factor_token]) + return render json: failed_json.merge( + error: I18n.t("login.invalid_second_factor_code") + ) + end + + if params[:enable] == "true" + user_second_factor.update!(enabled: true) + else + user_second_factor.destroy! + + Jobs.enqueue( + :critical_user_email, + type: :account_second_factor_disabled, + user_id: current_user.id + ) + end + + render json: success_json + end + private def honeypot_value @@ -975,7 +1065,12 @@ class UsersController < ApplicationController result.merge!(params.permit(:active, :staged, :approved)) end - result + modify_user_params(result) + end + + # Plugins can use this to modify user parameters + def modify_user_params(attrs) + attrs end def user_locale diff --git a/app/controllers/users_email_controller.rb b/app/controllers/users_email_controller.rb index e408a84f2a8..2fd289edb07 100644 --- a/app/controllers/users_email_controller.rb +++ b/app/controllers/users_email_controller.rb @@ -33,12 +33,32 @@ class UsersEmailController < ApplicationController def confirm expires_now - updater = EmailUpdater.new - @update_result = updater.confirm(params[:token]) - if @update_result == :complete - updater.user.user_stat.reset_bounce_score! - log_on_user(updater.user) + token = EmailToken.confirmable(params[:token]) + user = token&.user + + change_request = + if user + user.email_change_requests.where(new_email_token_id: token.id).first + end + + if change_request&.change_state == EmailChangeRequest.states[:authorizing_new] && + user.totp_enabled? && !user.authenticate_totp(params[:second_factor_token]) + + @update_result = :invalid_second_factor + + if params[:second_factor_token].present? + RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! + @show_invalid_second_factor_error = true + end + else + updater = EmailUpdater.new + @update_result = updater.confirm(params[:token]) + + if @update_result == :complete + updater.user.user_stat.reset_bounce_score! + log_on_user(updater.user) + end end render layout: 'no_ember' diff --git a/app/jobs/onceoff/init_category_tag_stats.rb b/app/jobs/onceoff/init_category_tag_stats.rb index c108ca91632..43abdc0b429 100644 --- a/app/jobs/onceoff/init_category_tag_stats.rb +++ b/app/jobs/onceoff/init_category_tag_stats.rb @@ -1,6 +1,8 @@ module Jobs class InitCategoryTagStats < Jobs::Onceoff def execute_onceoff(args) + CategoryTagStat.exec_sql "DELETE FROM category_tag_stats" + CategoryTagStat.exec_sql <<~SQL INSERT INTO category_tag_stats (category_id, tag_id, topic_count) SELECT topics.category_id, tags.id, COUNT(topics.id) @@ -8,6 +10,7 @@ module Jobs INNER JOIN topic_tags ON tags.id = topic_tags.tag_id INNER JOIN topics ON topics.id = topic_tags.topic_id AND topics.deleted_at IS NULL + AND topics.category_id IS NOT NULL GROUP BY tags.id, topics.category_id SQL end diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index 5eca7dd91b5..0a0899e5728 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -146,8 +146,14 @@ module Jobs @extra[:end_date] = @extra[:end_date].to_date if @extra[:end_date].is_a?(String) @extra[:category_id] = @extra[:category_id].present? ? @extra[:category_id].to_i : nil @extra[:group_id] = @extra[:group_id].present? ? @extra[:group_id].to_i : nil + + report_hash = {} Report.find(@extra[:name], @extra).data.each do |row| - yield [row[:x].to_s(:db), row[:y].to_s(:db)] + report_hash[row[:x].to_s(:db)] = row[:y].to_s(:db) + end + + (@extra[:start_date]..@extra[:end_date]).each do |date| + yield [date.to_s(:db), report_hash.fetch(date.to_s(:db), 0)] end end diff --git a/app/jobs/scheduled/poll_feed.rb b/app/jobs/scheduled/poll_feed.rb index 2f885c69a28..494fcf2a706 100644 --- a/app/jobs/scheduled/poll_feed.rb +++ b/app/jobs/scheduled/poll_feed.rb @@ -3,9 +3,6 @@ # require 'digest/sha1' require 'excon' -require 'rss' -require_dependency 'feed_item_accessor' -require_dependency 'feed_element_installer' require_dependency 'final_destination' require_dependency 'post_creator' require_dependency 'post_revisor' @@ -27,12 +24,25 @@ module Jobs end def poll_feed + ensure_rss_loaded + # defer loading rss feed = Feed.new import_topics(feed.topics) end private + @@rss_loaded = false + + # rss lib is very expensive memory wise, no need to load it till it is needed + def ensure_rss_loaded + return if @@rss_loaded + require 'rss' + require_dependency 'feed_item_accessor' + require_dependency 'feed_element_installer' + @@rss_loaded = true + end + def not_polled_recently? $redis.set( 'feed-polled-recently', diff --git a/app/jobs/scheduled/reindex_search.rb b/app/jobs/scheduled/reindex_search.rb index e354b36d007..0e9768d2922 100644 --- a/app/jobs/scheduled/reindex_search.rb +++ b/app/jobs/scheduled/reindex_search.rb @@ -1,7 +1,7 @@ module Jobs # if locale changes or search algorithm changes we may want to reindex stuff class ReindexSearch < Jobs::Scheduled - every 1.day + every 2.hours def execute(args) rebuild_problem_topics @@ -38,13 +38,14 @@ module Jobs end end - def rebuild_problem_posts(limit = 10000) + def rebuild_problem_posts(limit = 20000) post_ids = load_problem_post_ids(limit) post_ids.each do |id| - post = Post.find_by(id: id) # could be deleted while iterating through batch - SearchIndexer.index(post, force: true) if post + if post = Post.find_by(id: id) + SearchIndexer.index(post, force: true) + end end end @@ -67,6 +68,7 @@ module Jobs WHERE pd.post_id IS NULL )', SiteSetting.default_locale, Search::INDEX_VERSION) .limit(limit) + .order('posts.id DESC') .pluck(:id) end diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index f5d8e537ecb..c9cd1a032d2 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -120,6 +120,15 @@ class UserNotifications < ActionMailer::Base ) end + def account_second_factor_disabled(user, opts = {}) + build_email( + user.email, + template: 'user_notifications.account_second_factor_disabled', + locale: user_locale(user), + email: user.email + ) + end + def short_date(dt) if dt.year == Time.now.year I18n.l(dt, format: :short_no_year) @@ -258,6 +267,7 @@ class UserNotifications < ActionMailer::Base opts[:use_site_subject] = true opts[:add_re_to_subject] = true opts[:show_category_in_subject] = false + opts[:show_group_in_subject] = true if SiteSetting.group_in_subject # We use the 'user_posted' event when you are emailed a post in a PM. opts[:notification_type] = 'posted' @@ -372,6 +382,7 @@ class UserNotifications < ActionMailer::Base use_site_subject: opts[:use_site_subject], add_re_to_subject: opts[:add_re_to_subject], show_category_in_subject: opts[:show_category_in_subject], + show_group_in_subject: opts[:show_group_in_subject], notification_type: notification_type, use_invite_template: opts[:use_invite_template], user: user @@ -422,6 +433,21 @@ class UserNotifications < ActionMailer::Base show_category_in_subject = nil end + if post.topic.private_message? + subject_pm = + if opts[:show_group_in_subject] + if group = post.topic.allowed_groups&.first + if group.full_name + "[#{group.full_name}] " + else + "[#{group.name}] " + end + end + else + I18n.t('subject_pm') + end + end + if SiteSetting.private_email? title = I18n.t("system_messages.private_topic_title", id: post.topic_id) end @@ -523,6 +549,7 @@ class UserNotifications < ActionMailer::Base add_re_to_subject: add_re_to_subject, show_category_in_subject: show_category_in_subject, private_reply: post.topic.private_message?, + subject_pm: subject_pm, include_respond_instructions: !(user.suspended? || user.staged?), template: template, site_description: SiteSetting.site_description, diff --git a/app/models/badge.rb b/app/models/badge.rb index abb618cc785..60da562949d 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -240,7 +240,7 @@ end # Table name: badges # # id :integer not null, primary key -# name :string(255) not null +# name :string not null # description :text # badge_type_id :integer not null # grant_count :integer default(0), not null @@ -248,7 +248,7 @@ end # updated_at :datetime not null # allow_title :boolean default(FALSE), not null # multiple_grant :boolean default(FALSE), not null -# icon :string(255) default("fa-certificate") +# icon :string default("fa-certificate") # listable :boolean default(TRUE) # target_posts :boolean default(FALSE) # query :text @@ -263,5 +263,6 @@ end # # Indexes # -# index_badges_on_name (name) UNIQUE +# index_badges_on_badge_type_id (badge_type_id) +# index_badges_on_name (name) UNIQUE # diff --git a/app/models/badge_grouping.rb b/app/models/badge_grouping.rb index f1201e72140..4fdcef66e3a 100644 --- a/app/models/badge_grouping.rb +++ b/app/models/badge_grouping.rb @@ -22,7 +22,7 @@ end # Table name: badge_groupings # # id :integer not null, primary key -# name :string(255) not null +# name :string not null # description :text # position :integer not null # created_at :datetime not null diff --git a/app/models/badge_type.rb b/app/models/badge_type.rb index 4648e3ad58e..b1d6e91edd9 100644 --- a/app/models/badge_type.rb +++ b/app/models/badge_type.rb @@ -12,7 +12,7 @@ end # Table name: badge_types # # id :integer not null, primary key -# name :string(255) not null +# name :string not null # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/category.rb b/app/models/category.rb index 2589903c2c5..cdc03b5bf51 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -521,7 +521,7 @@ end # topics_year :integer default(0) # topics_month :integer default(0) # topics_week :integer default(0) -# slug :string(255) not null +# slug :string not null # description :text # text_color :string(6) default("FFFFFF"), not null # read_restricted :boolean default(FALSE), not null @@ -534,7 +534,7 @@ end # posts_year :integer default(0) # posts_month :integer default(0) # posts_week :integer default(0) -# email_in :string(255) +# email_in :string # email_in_allow_strangers :boolean default(FALSE) # topics_day :integer default(0) # posts_day :integer default(0) @@ -559,7 +559,7 @@ end # # Indexes # -# index_categories_on_email_in (email_in) UNIQUE -# index_categories_on_forum_thread_count (topic_count) -# unique_index_categories_on_name ((COALESCE(parent_category_id, '-1'::integer)), name) UNIQUE +# index_categories_on_email_in (email_in) UNIQUE +# index_categories_on_topic_count (topic_count) +# unique_index_categories_on_name (COALESCE(parent_category_id, '-1'::integer), name) UNIQUE # diff --git a/app/models/category_tag.rb b/app/models/category_tag.rb index 20ab52d7bc2..507d13e3845 100644 --- a/app/models/category_tag.rb +++ b/app/models/category_tag.rb @@ -10,8 +10,8 @@ end # id :integer not null, primary key # category_id :integer not null # tag_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/category_tag_group.rb b/app/models/category_tag_group.rb index 3c642d39649..c262539b961 100644 --- a/app/models/category_tag_group.rb +++ b/app/models/category_tag_group.rb @@ -10,8 +10,8 @@ end # id :integer not null, primary key # category_id :integer not null # tag_group_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/category_tag_stat.rb b/app/models/category_tag_stat.rb index e9c8275d618..8aff0447c8d 100644 --- a/app/models/category_tag_stat.rb +++ b/app/models/category_tag_stat.rb @@ -50,7 +50,9 @@ class CategoryTagStat < ActiveRecord::Base topics.category_id as category_id FROM tags INNER JOIN topic_tags ON tags.id = topic_tags.tag_id - INNER JOIN topics ON topics.id = topic_tags.topic_id AND topics.deleted_at IS NULL + INNER JOIN topics ON topics.id = topic_tags.topic_id + AND topics.deleted_at IS NULL + AND topics.category_id IS NOT NULL GROUP BY tags.id, topics.category_id ) x WHERE stats.tag_id = x.tag_id @@ -59,3 +61,20 @@ class CategoryTagStat < ActiveRecord::Base SQL end end + +# == Schema Information +# +# Table name: category_tag_stats +# +# id :integer not null, primary key +# category_id :integer not null +# tag_id :integer not null +# topic_count :integer default(0), not null +# +# Indexes +# +# index_category_tag_stats_on_category_id (category_id) +# index_category_tag_stats_on_category_id_and_tag_id (category_id,tag_id) UNIQUE +# index_category_tag_stats_on_category_id_and_topic_count (category_id,topic_count) +# index_category_tag_stats_on_tag_id (tag_id) +# diff --git a/app/models/child_theme.rb b/app/models/child_theme.rb index 6e101bd8aae..e4eb2d0ef7f 100644 --- a/app/models/child_theme.rb +++ b/app/models/child_theme.rb @@ -10,8 +10,8 @@ end # id :integer not null, primary key # parent_theme_id :integer # child_theme_id :integer -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index dbe60b1c766..0e1f09c12c2 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -187,7 +187,7 @@ end # Table name: color_schemes # # id :integer not null, primary key -# name :string(255) not null +# name :string not null # version :integer default(1), not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/color_scheme_color.rb b/app/models/color_scheme_color.rb index 39a7b51eec6..51e0c4ae8d0 100644 --- a/app/models/color_scheme_color.rb +++ b/app/models/color_scheme_color.rb @@ -9,8 +9,8 @@ end # Table name: color_scheme_colors # # id :integer not null, primary key -# name :string(255) not null -# hex :string(255) not null +# name :string not null +# hex :string not null # color_scheme_id :integer not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/concerns/second_factor_manager.rb b/app/models/concerns/second_factor_manager.rb new file mode 100644 index 00000000000..a9a04cd390c --- /dev/null +++ b/app/models/concerns/second_factor_manager.rb @@ -0,0 +1,38 @@ +module SecondFactorManager + extend ActiveSupport::Concern + + def totp + self.create_totp + ROTP::TOTP.new(self.user_second_factor.data, issuer: SiteSetting.title) + end + + def create_totp(opts = {}) + if !self.user_second_factor + self.create_user_second_factor!({ + method: UserSecondFactor.methods[:totp], + data: ROTP::Base32.random_base32 + }.merge(opts)) + end + end + + def totp_provisioning_uri + self.totp.provisioning_uri(self.email) + end + + def authenticate_totp(token) + totp = self.totp + last_used = 0 + + if self.user_second_factor.last_used + last_used = self.user_second_factor.last_used.to_i + end + + authenticated = !token.blank? && totp.verify_with_drift_and_prior(token, 0, last_used) + self.user_second_factor.update!(last_used: DateTime.now) if authenticated + !!authenticated + end + + def totp_enabled? + !!(self&.user_second_factor&.enabled?) + end +end diff --git a/app/models/draft.rb b/app/models/draft.rb index 48bd38ce13b..be412cc72a3 100644 --- a/app/models/draft.rb +++ b/app/models/draft.rb @@ -59,7 +59,7 @@ end # # id :integer not null, primary key # user_id :integer not null -# draft_key :string(255) not null +# draft_key :string not null # data :text not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/draft_sequence.rb b/app/models/draft_sequence.rb index 863b00aa700..460556d004d 100644 --- a/app/models/draft_sequence.rb +++ b/app/models/draft_sequence.rb @@ -33,7 +33,7 @@ end # # id :integer not null, primary key # user_id :integer not null -# draft_key :string(255) not null +# draft_key :string not null # sequence :integer not null # # Indexes diff --git a/app/models/email_log.rb b/app/models/email_log.rb index c17878c93d6..0fafa1bcc04 100644 --- a/app/models/email_log.rb +++ b/app/models/email_log.rb @@ -74,8 +74,8 @@ end # Table name: email_logs # # id :integer not null, primary key -# to_address :string(255) not null -# email_type :string(255) not null +# to_address :string not null +# email_type :string not null # user_id :integer # created_at :datetime not null # updated_at :datetime not null @@ -83,16 +83,19 @@ end # post_id :integer # topic_id :integer # skipped :boolean default(FALSE) -# skipped_reason :string(255) +# skipped_reason :string # bounce_key :string # bounced :boolean default(FALSE), not null # message_id :string # # Indexes # +# idx_email_logs_user_created_filtered (user_id,created_at) # index_email_logs_on_created_at (created_at) # index_email_logs_on_message_id (message_id) +# index_email_logs_on_post_id (post_id) # index_email_logs_on_reply_key (reply_key) # index_email_logs_on_skipped_and_created_at (skipped,created_at) +# index_email_logs_on_topic_id (topic_id) # index_email_logs_on_user_id_and_created_at (user_id,created_at) # diff --git a/app/models/email_token.rb b/app/models/email_token.rb index 2338108ac4f..2a10dd4e234 100644 --- a/app/models/email_token.rb +++ b/app/models/email_token.rb @@ -94,8 +94,8 @@ end # # id :integer not null, primary key # user_id :integer not null -# email :string(255) not null -# token :string(255) not null +# email :string not null +# token :string not null # confirmed :boolean default(FALSE), not null # expired :boolean default(FALSE), not null # created_at :datetime not null diff --git a/app/models/embeddable_host.rb b/app/models/embeddable_host.rb index b76a6725b84..2fb3e4ed159 100644 --- a/app/models/embeddable_host.rb +++ b/app/models/embeddable_host.rb @@ -60,10 +60,10 @@ end # Table name: embeddable_hosts # # id :integer not null, primary key -# host :string(255) not null +# host :string not null # category_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # path_whitelist :string # class_name :string # diff --git a/app/models/facebook_user_info.rb b/app/models/facebook_user_info.rb index 7b099e03bb8..eb996113106 100644 --- a/app/models/facebook_user_info.rb +++ b/app/models/facebook_user_info.rb @@ -9,13 +9,13 @@ end # id :integer not null, primary key # user_id :integer not null # facebook_user_id :integer not null -# username :string(255) -# first_name :string(255) -# last_name :string(255) -# email :string(255) -# gender :string(255) -# name :string(255) -# link :string(255) +# username :string +# first_name :string +# last_name :string +# email :string +# gender :string +# name :string +# link :string # created_at :datetime not null # updated_at :datetime not null # avatar_url :string diff --git a/app/models/github_user_info.rb b/app/models/github_user_info.rb index c79a3b0e913..8e776f33ab1 100644 --- a/app/models/github_user_info.rb +++ b/app/models/github_user_info.rb @@ -8,7 +8,7 @@ end # # id :integer not null, primary key # user_id :integer not null -# screen_name :string(255) not null +# screen_name :string not null # github_user_id :integer not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/google_user_info.rb b/app/models/google_user_info.rb index 26f3dda50dc..343fe9945fd 100644 --- a/app/models/google_user_info.rb +++ b/app/models/google_user_info.rb @@ -8,15 +8,15 @@ end # # id :integer not null, primary key # user_id :integer not null -# google_user_id :string(255) not null -# first_name :string(255) -# last_name :string(255) -# email :string(255) -# gender :string(255) -# name :string(255) -# link :string(255) -# profile_link :string(255) -# picture :string(255) +# google_user_id :string not null +# first_name :string +# last_name :string +# email :string +# gender :string +# name :string +# link :string +# profile_link :string +# picture :string # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/group.rb b/app/models/group.rb index 61f7e79f137..6cc39fb1c6c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -661,7 +661,7 @@ end # Table name: groups # # id :integer not null, primary key -# name :string(255) not null +# name :string not null # created_at :datetime not null # updated_at :datetime not null # automatic :boolean default(FALSE), not null @@ -669,7 +669,7 @@ end # automatic_membership_email_domains :text # automatic_membership_retroactive :boolean default(FALSE) # primary_group :boolean default(FALSE), not null -# title :string(255) +# title :string # grant_trust_level :integer # incoming_email :string # has_messages :boolean default(FALSE), not null diff --git a/app/models/group_archived_message.rb b/app/models/group_archived_message.rb index 91d2062cae5..5ec3b9d6bac 100644 --- a/app/models/group_archived_message.rb +++ b/app/models/group_archived_message.rb @@ -32,8 +32,8 @@ end # id :integer not null, primary key # group_id :integer not null # topic_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/group_mention.rb b/app/models/group_mention.rb index 30eb647ebc8..3cef10dd735 100644 --- a/app/models/group_mention.rb +++ b/app/models/group_mention.rb @@ -10,8 +10,8 @@ end # id :integer not null, primary key # post_id :integer # group_id :integer -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/invite.rb b/app/models/invite.rb index 5a57084a076..6b63855f014 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -262,7 +262,7 @@ end # # id :integer not null, primary key # invite_key :string(32) not null -# email :string(255) +# email :string # invited_by_id :integer not null # user_id :integer # redeemed_at :datetime diff --git a/app/models/muted_user.rb b/app/models/muted_user.rb index 59f8089c6f7..1ba464105d0 100644 --- a/app/models/muted_user.rb +++ b/app/models/muted_user.rb @@ -10,8 +10,8 @@ end # id :integer not null, primary key # user_id :integer not null # muted_user_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/oauth2_user_info.rb b/app/models/oauth2_user_info.rb index 7ce0cace41d..5b27d0cf710 100644 --- a/app/models/oauth2_user_info.rb +++ b/app/models/oauth2_user_info.rb @@ -9,10 +9,10 @@ end # # id :integer not null, primary key # user_id :integer not null -# uid :string(255) not null -# provider :string(255) not null -# email :string(255) -# name :string(255) +# uid :string not null +# provider :string not null +# email :string +# name :string # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index 82b64797258..54c8d6c4901 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -321,7 +321,7 @@ end # width :integer not null # height :integer not null # upload_id :integer not null -# url :string(255) not null +# url :string not null # # Indexes # diff --git a/app/models/permalink.rb b/app/models/permalink.rb index 4bfaa5a9cd5..62e3ef84f50 100644 --- a/app/models/permalink.rb +++ b/app/models/permalink.rb @@ -101,8 +101,8 @@ end # topic_id :integer # post_id :integer # category_id :integer -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # external_url :string(1000) # # Indexes diff --git a/app/models/plugin_store_row.rb b/app/models/plugin_store_row.rb index db1d9d3fdab..d9bb1c05db6 100644 --- a/app/models/plugin_store_row.rb +++ b/app/models/plugin_store_row.rb @@ -6,9 +6,9 @@ end # Table name: plugin_store_rows # # id :integer not null, primary key -# plugin_name :string(255) not null -# key :string(255) not null -# type_name :string(255) not null +# plugin_name :string not null +# key :string not null +# type_name :string not null # value :text # # Indexes diff --git a/app/models/post.rb b/app/models/post.rb index 5863cc49c12..890edc39ee2 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -814,7 +814,7 @@ end # notify_user_count :integer default(0), not null # like_score :integer default(0), not null # deleted_by_id :integer -# edit_reason :string(255) +# edit_reason :string # word_count :integer # version :integer default(1), not null # cook_method :integer default(1), not null @@ -827,8 +827,9 @@ end # via_email :boolean default(FALSE), not null # raw_email :text # public_version :integer default(1), not null -# action_code :string(255) +# action_code :string # image_url :string +# locked_by_id :integer # # Indexes # diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index 919b7d42155..57e67c5efae 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -31,7 +31,7 @@ class PostAnalyzer cooked = PrettyText.cook(raw, opts) end - result = Oneboxer.apply(cooked, topic_id: @topic_id) do |url, _| + result = Oneboxer.apply(cooked) do |url| @found_oneboxes = true Oneboxer.invalidate(url) if opts[:invalidate_oneboxes] Oneboxer.cached_onebox(url) @@ -130,7 +130,7 @@ class PostAnalyzer def cooked_stripped @cooked_stripped ||= begin doc = Nokogiri::HTML.fragment(cook(@raw, topic_id: @topic_id)) - doc.css("pre, code, aside.quote, .onebox, .elided").remove + doc.css("pre, code, aside.quote > .title, aside.quote .mention, .onebox, .elided").remove doc end end diff --git a/app/models/post_detail.rb b/app/models/post_detail.rb index 8f888516732..c43219749cc 100644 --- a/app/models/post_detail.rb +++ b/app/models/post_detail.rb @@ -11,8 +11,8 @@ end # # id :integer not null, primary key # post_id :integer -# key :string(255) -# value :string(255) +# key :string +# value :string # extra :text # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/post_search_data.rb b/app/models/post_search_data.rb index e890e875473..58940e693b9 100644 --- a/app/models/post_search_data.rb +++ b/app/models/post_search_data.rb @@ -9,7 +9,7 @@ end # post_id :integer not null, primary key # search_data :tsvector # raw_data :text -# locale :string(255) +# locale :string # version :integer default(0) # # Indexes diff --git a/app/models/post_stat.rb b/app/models/post_stat.rb index ec293b66636..9de208b538d 100644 --- a/app/models/post_stat.rb +++ b/app/models/post_stat.rb @@ -11,8 +11,8 @@ end # drafts_saved :integer # typing_duration_msecs :integer # composer_open_duration_msecs :integer -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/queued_post.rb b/app/models/queued_post.rb index 50f2997541b..e8f0bdcca46 100644 --- a/app/models/queued_post.rb +++ b/app/models/queued_post.rb @@ -121,7 +121,7 @@ end # Table name: queued_posts # # id :integer not null, primary key -# queue :string(255) not null +# queue :string not null # state :integer not null # user_id :integer not null # raw :text not null @@ -131,8 +131,8 @@ end # approved_at :datetime # rejected_by_id :integer # rejected_at :datetime -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index 3030b4aa22d..1b8bfba1ed3 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -155,6 +155,6 @@ end # license_url :string # commits_behind :integer # remote_updated_at :datetime -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # diff --git a/app/models/screened_email.rb b/app/models/screened_email.rb index 74c0c9593e9..a0dec7f12f1 100644 --- a/app/models/screened_email.rb +++ b/app/models/screened_email.rb @@ -67,7 +67,7 @@ end # Table name: screened_emails # # id :integer not null, primary key -# email :string(255) not null +# email :string not null # action_type :integer not null # match_count :integer default(0), not null # last_match_at :datetime @@ -77,6 +77,6 @@ end # # Indexes # -# index_blocked_emails_on_email (email) UNIQUE -# index_blocked_emails_on_last_match_at (last_match_at) +# index_screened_emails_on_email (email) UNIQUE +# index_screened_emails_on_last_match_at (last_match_at) # diff --git a/app/models/screened_url.rb b/app/models/screened_url.rb index be97e0d8426..47e6e949db6 100644 --- a/app/models/screened_url.rb +++ b/app/models/screened_url.rb @@ -42,8 +42,8 @@ end # Table name: screened_urls # # id :integer not null, primary key -# url :string(255) not null -# domain :string(255) not null +# url :string not null +# domain :string not null # action_type :integer not null # match_count :integer default(0), not null # last_match_at :datetime diff --git a/app/models/single_sign_on_record.rb b/app/models/single_sign_on_record.rb index f6acfa9ddce..d556a208bc9 100644 --- a/app/models/single_sign_on_record.rb +++ b/app/models/single_sign_on_record.rb @@ -10,13 +10,13 @@ end # # id :integer not null, primary key # user_id :integer not null -# external_id :string(255) not null +# external_id :string not null # last_payload :text not null # created_at :datetime not null # updated_at :datetime not null -# external_username :string(255) -# external_email :string(255) -# external_name :string(255) +# external_username :string +# external_email :string +# external_name :string # external_avatar_url :string(1000) # # Indexes diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index d0ce4465eb4..481ec0eb7ff 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -157,7 +157,7 @@ end # Table name: site_settings # # id :integer not null, primary key -# name :string(255) not null +# name :string not null # data_type :integer not null # value :text # created_at :datetime not null diff --git a/app/models/stylesheet_cache.rb b/app/models/stylesheet_cache.rb index 0d318ce0db0..08b98c30859 100644 --- a/app/models/stylesheet_cache.rb +++ b/app/models/stylesheet_cache.rb @@ -43,11 +43,11 @@ end # Table name: stylesheet_cache # # id :integer not null, primary key -# target :string(255) not null -# digest :string(255) not null +# target :string not null +# digest :string not null # content :text not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # theme_id :integer default(-1), not null # source_map :text # diff --git a/app/models/tag.rb b/app/models/tag.rb index 500ba8e6e3a..9bc1d29350c 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -79,8 +79,8 @@ end # id :integer not null, primary key # name :string not null # topic_count :integer default(0), not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/tag_group.rb b/app/models/tag_group.rb index ccc97798d0f..b1c7088c709 100644 --- a/app/models/tag_group.rb +++ b/app/models/tag_group.rb @@ -42,8 +42,8 @@ end # # id :integer not null, primary key # name :string not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # parent_tag_id :integer # one_per_topic :boolean default(FALSE) # diff --git a/app/models/tag_group_membership.rb b/app/models/tag_group_membership.rb index 76e9be22c36..3bc5610e278 100644 --- a/app/models/tag_group_membership.rb +++ b/app/models/tag_group_membership.rb @@ -10,8 +10,8 @@ end # id :integer not null, primary key # tag_id :integer not null # tag_group_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/tag_user.rb b/app/models/tag_user.rb index 52db737cb5e..d54b787c405 100644 --- a/app/models/tag_user.rb +++ b/app/models/tag_user.rb @@ -161,8 +161,8 @@ end # tag_id :integer not null # user_id :integer not null # notification_level :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/theme.rb b/app/models/theme.rb index 5fc90241d9b..280fa6847f6 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -295,9 +295,9 @@ end # Table name: themes # # id :integer not null, primary key -# name :string(255) not null +# name :string not null # user_id :integer not null -# key :string(255) not null +# key :string not null # created_at :datetime not null # updated_at :datetime not null # compiler_version :integer default(0), not null diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 386b0fb903a..eb1dcd2bc26 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -162,8 +162,8 @@ end # name :string(30) not null # value :text not null # value_baked :text -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # compiler_version :integer default(0), not null # error :string # upload_id :integer diff --git a/app/models/topic.rb b/app/models/topic.rb index 3b3c74b956a..b5244d73756 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -84,6 +84,7 @@ class Topic < ActiveRecord::Base topic_title_length: true, censored_words: true, quality_title: { unless: :private_message? }, + max_emojis: true, unique_among: { unless: Proc.new { |t| (SiteSetting.allow_duplicate_topic_titles? || t.private_message?) }, message: :has_already_been_used, allow_blank: true, @@ -222,10 +223,15 @@ class Topic < ActiveRecord::Base ApplicationController.banner_json_cache.clear end - if tags_changed - TagUser.auto_watch(topic_id: id) - TagUser.auto_track(topic_id: id) - self.tags_changed = false + if tags_changed || saved_change_to_attribute?(:category_id) + + SearchIndexer.queue_post_reindex(self.id) + + if tags_changed + TagUser.auto_watch(topic_id: id) + TagUser.auto_track(topic_id: id) + self.tags_changed = false + end end SearchIndexer.index(self) @@ -473,7 +479,7 @@ class Topic < ActiveRecord::Base search_data = "#{title} #{raw[0...MAX_SIMILAR_BODY_LENGTH]}".strip filter_words = Search.prepare_data(search_data) - ts_query = Search.ts_query(filter_words, nil, "|") + ts_query = Search.ts_query(term: filter_words, joiner: "|") candidates = Topic .visible @@ -1311,7 +1317,7 @@ end # Table name: topics # # id :integer not null, primary key -# title :string(255) not null +# title :string not null # last_posted_at :datetime # created_at :datetime not null # updated_at :datetime not null @@ -1326,7 +1332,7 @@ end # avg_time :integer # deleted_at :datetime # highest_post_number :integer default(0), not null -# image_url :string(255) +# image_url :string # like_count :integer default(0), not null # incoming_link_count :integer default(0), not null # category_id :integer @@ -1337,15 +1343,15 @@ end # bumped_at :datetime not null # has_summary :boolean default(FALSE), not null # vote_count :integer default(0), not null -# archetype :string(255) default("regular"), not null +# archetype :string default("regular"), not null # featured_user4_id :integer # notify_moderators_count :integer default(0), not null # spam_count :integer default(0), not null # pinned_at :datetime # score :float # percent_rank :float default(1.0), not null -# subtype :string(255) -# slug :string(255) +# subtype :string +# slug :string # deleted_by_id :integer # participant_count :integer default(1) # word_count :integer @@ -1361,7 +1367,7 @@ end # idx_topics_front_page (deleted_at,visible,archetype,category_id,id) # idx_topics_user_id_deleted_at (user_id) # idxtopicslug (slug) -# index_forum_threads_on_bumped_at (bumped_at) +# index_topics_on_bumped_at (bumped_at) # index_topics_on_created_at_and_visible (created_at,visible) # index_topics_on_id_and_deleted_at (id,deleted_at) # index_topics_on_lower_title (lower((title)::text)) diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb index 8e3071ea4c8..2f2b26e1952 100644 --- a/app/models/topic_embed.rb +++ b/app/models/topic_embed.rb @@ -183,7 +183,7 @@ class TopicEmbed < ActiveRecord::Base def self.topic_id_for_embed(embed_url) embed_url = normalize_url(embed_url).sub(/^https?\:\/\//, '') - TopicEmbed.where("embed_url ~* '^https?://#{embed_url}$'").pluck(:topic_id).first + TopicEmbed.where("embed_url ~* '^https?://#{Regexp.escape(embed_url)}$'").pluck(:topic_id).first end def self.first_paragraph_from(html) diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb index a55cd5ca487..45bf194b879 100644 --- a/app/models/topic_link.rb +++ b/app/models/topic_link.rb @@ -284,16 +284,16 @@ end # reflection :boolean default(FALSE) # clicks :integer default(0), not null # link_post_id :integer -# title :string(255) +# title :string # crawled_at :datetime # quote :boolean default(FALSE), not null # extension :string(10) # # Indexes # -# index_forum_thread_links_on_forum_thread_id (topic_id) -# index_forum_thread_links_on_forum_thread_id_and_post_id_and_url (topic_id,post_id,url) UNIQUE -# index_topic_links_on_extension (extension) -# index_topic_links_on_link_post_id_and_reflection (link_post_id,reflection) -# index_topic_links_on_post_id (post_id) +# index_topic_links_on_extension (extension) +# index_topic_links_on_link_post_id_and_reflection (link_post_id,reflection) +# index_topic_links_on_post_id (post_id) +# index_topic_links_on_topic_id (topic_id) +# unique_post_links (topic_id,post_id,url) UNIQUE # diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb index 3afce0bfbd5..90cddefe2f4 100644 --- a/app/models/topic_link_click.rb +++ b/app/models/topic_link_click.rb @@ -118,5 +118,5 @@ end # # Indexes # -# index_forum_thread_link_clicks_on_forum_thread_link_id (topic_link_id) +# by_link (topic_link_id) # diff --git a/app/models/topic_search_data.rb b/app/models/topic_search_data.rb index 641d79246c6..ddf065327be 100644 --- a/app/models/topic_search_data.rb +++ b/app/models/topic_search_data.rb @@ -8,7 +8,7 @@ end # # topic_id :integer not null, primary key # raw_data :text -# locale :string(255) not null +# locale :string not null # search_data :tsvector # version :integer default(0) # diff --git a/app/models/topic_tag.rb b/app/models/topic_tag.rb index de53e4cf8ad..8c0a8df5012 100644 --- a/app/models/topic_tag.rb +++ b/app/models/topic_tag.rb @@ -3,7 +3,7 @@ class TopicTag < ActiveRecord::Base belongs_to :tag after_create do - if topic.archetype != Archetype.private_message + if topic && topic.archetype != Archetype.private_message tag.increment!(:topic_count) if topic.category_id @@ -17,7 +17,7 @@ class TopicTag < ActiveRecord::Base end after_destroy do - if topic.archetype != Archetype.private_message + if topic && topic.archetype != Archetype.private_message if topic.category_id && stat = CategoryTagStat.where(tag_id: tag_id, category: topic.category_id).first stat.topic_count == 1 ? stat.destroy : stat.decrement!(:topic_count) end @@ -34,8 +34,8 @@ end # id :integer not null, primary key # topic_id :integer not null # tag_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/topic_timer.rb b/app/models/topic_timer.rb index 160f6c40a7d..39f882dbafa 100644 --- a/app/models/topic_timer.rb +++ b/app/models/topic_timer.rb @@ -157,8 +157,8 @@ end # based_on_last_post :boolean default(FALSE), not null # deleted_at :datetime # deleted_by_id :integer -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # category_id :integer # public_type :boolean default(TRUE) # diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 1d5618fbaf2..f620c4648a4 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -474,6 +474,6 @@ end # # Indexes # -# index_forum_thread_users_on_forum_thread_id_and_user_id (topic_id,user_id) UNIQUE -# index_topic_users_on_user_id_and_topic_id (user_id,topic_id) UNIQUE +# index_topic_users_on_topic_id_and_user_id (topic_id,user_id) UNIQUE +# index_topic_users_on_user_id_and_topic_id (user_id,topic_id) UNIQUE # diff --git a/app/models/twitter_user_info.rb b/app/models/twitter_user_info.rb index 42d4f4d83cf..a57608a4684 100644 --- a/app/models/twitter_user_info.rb +++ b/app/models/twitter_user_info.rb @@ -8,10 +8,11 @@ end # # id :integer not null, primary key # user_id :integer not null -# screen_name :string(255) not null +# screen_name :string not null # twitter_user_id :integer not null # created_at :datetime not null # updated_at :datetime not null +# email :string(1000) # # Indexes # diff --git a/app/models/unsubscribe_key.rb b/app/models/unsubscribe_key.rb index 6cc8b7f25f4..967d8f34a13 100644 --- a/app/models/unsubscribe_key.rb +++ b/app/models/unsubscribe_key.rb @@ -30,8 +30,8 @@ end # # key :string(64) not null, primary key # user_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # unsubscribe_key_type :string # topic_id :integer # post_id :integer diff --git a/app/models/upload.rb b/app/models/upload.rb index 0e776d52676..6c683d18734 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -156,11 +156,11 @@ end # # id :integer not null, primary key # user_id :integer not null -# original_filename :string(255) not null +# original_filename :string not null # filesize :integer not null # width :integer # height :integer -# url :string(255) not null +# url :string not null # created_at :datetime not null # updated_at :datetime not null # sha1 :string(40) diff --git a/app/models/user.rb b/app/models/user.rb index 464d3fe9d78..f993bddab13 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -18,6 +18,7 @@ class User < ActiveRecord::Base include Searchable include Roleable include HasCustomFields + include SecondFactorManager # TODO: Remove this after 7th Jan 2018 self.ignored_columns = %w{email} @@ -60,6 +61,7 @@ class User < ActiveRecord::Base has_one :github_user_info, dependent: :destroy has_one :google_user_info, dependent: :destroy has_one :oauth2_user_info, dependent: :destroy + has_one :user_second_factor, dependent: :destroy has_one :user_stat, dependent: :destroy has_one :user_profile, dependent: :destroy, inverse_of: :user has_one :single_sign_on_record, dependent: :destroy @@ -743,7 +745,8 @@ class User < ActiveRecord::Base def activate if email_token = self.email_tokens.active.where(email: self.email).first - EmailToken.confirm(email_token.token) + user = EmailToken.confirm(email_token.token) + self.update!(active: true) if user.nil? else self.update!(active: true) end @@ -1174,7 +1177,7 @@ end # username :string(60) not null # created_at :datetime not null # updated_at :datetime not null -# name :string(255) +# name :string # seen_notification_id :integer default(0), not null # last_posted_at :datetime # password_hash :string(64) @@ -1196,10 +1199,10 @@ end # flag_level :integer default(0), not null # ip_address :inet # moderator :boolean default(FALSE) -# title :string(255) +# title :string # uploaded_avatar_id :integer -# primary_group_id :integer # locale :string(10) +# primary_group_id :integer # registration_ip_address :inet # staged :boolean default(FALSE), not null # first_seen_at :datetime diff --git a/app/models/user_action.rb b/app/models/user_action.rb index 02123a1d444..073e2ec08ae 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -428,9 +428,9 @@ end # # Indexes # -# idx_unique_rows (action_type,user_id,target_topic_id,target_post_id,acting_user_id) UNIQUE -# idx_user_actions_speed_up_user_all (user_id,created_at,action_type) -# index_actions_on_acting_user_id (acting_user_id) -# index_actions_on_user_id_and_action_type (user_id,action_type) -# index_user_actions_on_target_post_id (target_post_id) +# idx_unique_rows (action_type,user_id,target_topic_id,target_post_id,acting_user_id) UNIQUE +# idx_user_actions_speed_up_user_all (user_id,created_at,action_type) +# index_user_actions_on_acting_user_id (acting_user_id) +# index_user_actions_on_target_post_id (target_post_id) +# index_user_actions_on_user_id_and_action_type (user_id,action_type) # diff --git a/app/models/user_api_key.rb b/app/models/user_api_key.rb index 39f8d4a80f9..dc1edca2d60 100644 --- a/app/models/user_api_key.rb +++ b/app/models/user_api_key.rb @@ -70,8 +70,8 @@ end # key :string not null # application_name :string not null # push_url :string -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # revoked_at :datetime # scopes :text default([]), not null, is an Array # diff --git a/app/models/user_archived_message.rb b/app/models/user_archived_message.rb index 36c3ccd1ac4..556667e3f79 100644 --- a/app/models/user_archived_message.rb +++ b/app/models/user_archived_message.rb @@ -37,8 +37,8 @@ end # id :integer not null, primary key # user_id :integer not null # topic_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/user_auth_token.rb b/app/models/user_auth_token.rb index 5597d523679..045fc2f9d43 100644 --- a/app/models/user_auth_token.rb +++ b/app/models/user_auth_token.rb @@ -183,8 +183,8 @@ end # legacy :boolean default(FALSE), not null # client_ip :inet # rotated_at :datetime not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # seen_at :datetime # # Indexes diff --git a/app/models/user_badge.rb b/app/models/user_badge.rb index 71a065e0e0b..53979ec9cf8 100644 --- a/app/models/user_badge.rb +++ b/app/models/user_badge.rb @@ -49,4 +49,5 @@ end # index_user_badges_on_badge_id_and_user_id (badge_id,user_id) # index_user_badges_on_badge_id_and_user_id_and_post_id (badge_id,user_id,post_id) UNIQUE # index_user_badges_on_badge_id_and_user_id_and_seq (badge_id,user_id,seq) UNIQUE +# index_user_badges_on_user_id (user_id) # diff --git a/app/models/user_email.rb b/app/models/user_email.rb index 7ddbf73602e..1b533410319 100644 --- a/app/models/user_email.rb +++ b/app/models/user_email.rb @@ -47,8 +47,8 @@ end # user_id :integer not null # email :string(513) not null # primary :boolean default(FALSE), not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/user_export.rb b/app/models/user_export.rb index 20269d9b926..34d8fa7ed94 100644 --- a/app/models/user_export.rb +++ b/app/models/user_export.rb @@ -26,8 +26,8 @@ end # Table name: user_exports # # id :integer not null, primary key -# file_name :string(255) not null +# file_name :string not null # user_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # diff --git a/app/models/user_field.rb b/app/models/user_field.rb index 6183f63401a..77c1f3383de 100644 --- a/app/models/user_field.rb +++ b/app/models/user_field.rb @@ -16,12 +16,12 @@ end # Table name: user_fields # # id :integer not null, primary key -# name :string(255) not null -# field_type :string(255) not null -# created_at :datetime -# updated_at :datetime +# name :string not null +# field_type :string not null +# created_at :datetime not null +# updated_at :datetime not null # editable :boolean default(FALSE), not null -# description :string(255) not null +# description :string not null # required :boolean default(TRUE), not null # show_on_profile :boolean default(FALSE), not null # position :integer default(0) diff --git a/app/models/user_field_option.rb b/app/models/user_field_option.rb index 2b3036fa40d..a4412553425 100644 --- a/app/models/user_field_option.rb +++ b/app/models/user_field_option.rb @@ -7,7 +7,7 @@ end # # id :integer not null, primary key # user_field_id :integer not null -# value :string(255) not null -# created_at :datetime -# updated_at :datetime +# value :string not null +# created_at :datetime not null +# updated_at :datetime not null # diff --git a/app/models/user_history.rb b/app/models/user_history.rb index 7bd0f68587b..5e809af46e8 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -66,7 +66,8 @@ class UserHistory < ActiveRecord::Base change_name: 48, post_locked: 49, post_unlocked: 50, - check_personal_message: 51) + check_personal_message: 51, + disabled_second_factor: 52) end # Staff actions is a subset of all actions, used to audit actions taken by staff users. @@ -110,7 +111,8 @@ class UserHistory < ActiveRecord::Base :backup_destroy, :post_locked, :post_unlocked, - :check_personal_message] + :check_personal_message, + :disabled_second_factor] end def self.staff_action_ids @@ -182,23 +184,23 @@ end # details :text # created_at :datetime not null # updated_at :datetime not null -# context :string(255) -# ip_address :string(255) -# email :string(255) +# context :string +# ip_address :string +# email :string # subject :text # previous_value :text # new_value :text # topic_id :integer # admin_only :boolean default(FALSE) # post_id :integer -# custom_type :string(255) +# custom_type :string # category_id :integer # # Indexes # -# index_staff_action_logs_on_action_and_id (action,id) -# index_staff_action_logs_on_subject_and_id (subject,id) -# index_staff_action_logs_on_target_user_id_and_id (target_user_id,id) # index_user_histories_on_acting_user_id_and_action_and_id (acting_user_id,action,id) +# index_user_histories_on_action_and_id (action,id) # index_user_histories_on_category_id (category_id) +# index_user_histories_on_subject_and_id (subject,id) +# index_user_histories_on_target_user_id_and_id (target_user_id,id) # diff --git a/app/models/user_open_id.rb b/app/models/user_open_id.rb index 183f2414823..188a04ef150 100644 --- a/app/models/user_open_id.rb +++ b/app/models/user_open_id.rb @@ -11,8 +11,8 @@ end # # id :integer not null, primary key # user_id :integer not null -# email :string(255) not null -# url :string(255) not null +# email :string not null +# url :string not null # created_at :datetime not null # updated_at :datetime not null # active :boolean not null diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index d10b63f034d..c320be92183 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -117,12 +117,12 @@ end # Table name: user_profiles # # user_id :integer not null, primary key -# location :string(255) -# website :string(255) +# location :string +# website :string # bio_raw :text # bio_cooked :text -# dismissed_banner_key :integer # profile_background :string(255) +# dismissed_banner_key :integer # bio_cooked_version :integer # badge_granted_title :boolean default(FALSE) # card_background :string(255) diff --git a/app/models/user_search.rb b/app/models/user_search.rb index 8a2013b5b50..a9eceda1e9a 100644 --- a/app/models/user_search.rb +++ b/app/models/user_search.rb @@ -49,7 +49,7 @@ class UserSearch if @term.present? if SiteSetting.enable_names? && @term !~ /[_\.-]/ - query = Search.ts_query(@term, "simple") + query = Search.ts_query(term: @term, ts_config: "simple") users = users.includes(:user_search_data) .references(:user_search_data) diff --git a/app/models/user_second_factor.rb b/app/models/user_second_factor.rb new file mode 100644 index 00000000000..acd16cf134a --- /dev/null +++ b/app/models/user_second_factor.rb @@ -0,0 +1,23 @@ +class UserSecondFactor < ActiveRecord::Base + belongs_to :user + + def self.methods + @methods ||= Enum.new( + totp: 1, + ) + end +end + +# == Schema Information +# +# Table name: user_second_factors +# +# id :integer not null, primary key +# user_id :integer not null +# method :string +# data :string +# enabled :boolean default(FALSE), not null +# last_used :datetime +# created_at :datetime not null +# updated_at :datetime not null +# diff --git a/app/models/user_warning.rb b/app/models/user_warning.rb index dd89c7f9957..612406ac0e9 100644 --- a/app/models/user_warning.rb +++ b/app/models/user_warning.rb @@ -12,8 +12,8 @@ end # topic_id :integer not null # user_id :integer not null # created_by_id :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/watched_word.rb b/app/models/watched_word.rb index 0861fb3a22d..73a3f661166 100644 --- a/app/models/watched_word.rb +++ b/app/models/watched_word.rb @@ -60,8 +60,8 @@ end # id :integer not null, primary key # word :string not null # action :integer not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index de0db8bbf97..dd8df00b67d 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -69,6 +69,6 @@ end # wildcard_web_hook :boolean default(FALSE), not null # verify_certificate :boolean default(TRUE), not null # active :boolean default(FALSE), not null -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # diff --git a/app/models/web_hook_event.rb b/app/models/web_hook_event.rb index dc3e56d28ba..204efba8aaf 100644 --- a/app/models/web_hook_event.rb +++ b/app/models/web_hook_event.rb @@ -36,8 +36,8 @@ end # response_headers :string # response_body :text # duration :integer default(0) -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb index 8cc9316cb5d..e0ad2abcfc7 100644 --- a/app/serializers/admin_detailed_user_serializer.rb +++ b/app/serializers/admin_detailed_user_serializer.rb @@ -25,7 +25,9 @@ class AdminDetailedUserSerializer < AdminUserSerializer :user_fields, :bounce_score, :reset_bounce_score_after, - :can_view_action_logs + :can_view_action_logs, + :second_factor_enabled, + :can_disable_second_factor has_one :approved_by, serializer: BasicUserSerializer, embed: :objects has_one :api_key, serializer: ApiKeySerializer, embed: :objects @@ -34,6 +36,14 @@ class AdminDetailedUserSerializer < AdminUserSerializer has_one :tl3_requirements, serializer: TrustLevel3RequirementsSerializer, embed: :objects has_many :groups, embed: :object, serializer: BasicGroupSerializer + def second_factor_enabled + object.totp_enabled? + end + + def can_disable_second_factor + object&.id != scope.user.id + end + def can_revoke_admin scope.can_revoke_admin?(object) end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 3f1b2a7254d..f12ad4fccac 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -82,7 +82,7 @@ class PostSerializer < BasicPostSerializer end def topic_slug - object.topic && object.topic.slug + topic&.slug end def include_topic_title? @@ -98,15 +98,15 @@ class PostSerializer < BasicPostSerializer end def topic_title - object.topic.title + topic&.title end def topic_html_title - object.topic.fancy_title + topic&.fancy_title end def category_id - object.topic.category_id + topic&.category_id end def moderator? @@ -376,6 +376,12 @@ class PostSerializer < BasicPostSerializer private + def topic + @topic = object.topic + @topic ||= Topic.with_deleted.find(object.topic_id) if scope.is_staff? + @topic + end + def post_actions @post_actions ||= (@topic_view&.all_post_actions || {})[object.id] end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 30bb1038c6f..1e45721b685 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -72,7 +72,8 @@ class UserSerializer < BasicUserSerializer :primary_group_flair_url, :primary_group_flair_bg_color, :primary_group_flair_color, - :staged + :staged, + :second_factor_enabled has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_many :groups, embed: :object, serializer: BasicGroupSerializer @@ -145,6 +146,14 @@ class UserSerializer < BasicUserSerializer (scope.is_staff? && object.staged?) end + def include_second_factor_enabled? + (object&.id == scope.user&.id) || scope.is_staff? + end + + def second_factor_enabled + object.totp_enabled? + end + def can_change_bio !(SiteSetting.enable_sso && SiteSetting.sso_overrides_bio) end diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index fdf6b506690..645866db8be 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require_dependency 'search' class SearchIndexer @@ -14,111 +15,152 @@ class SearchIndexer HtmlScrubber.scrub(html) end - def self.update_index(table, id, raw_data) - raw_data = Search.prepare_data(raw_data, :index) - - table_name = "#{table}_search_data" - foreign_key = "#{table}_id" - + def self.inject_extra_terms(raw) # insert some extra words for I.am.a.word so "word" is tokenized # I.am.a.word becomes I.am.a.word am a word - search_data = raw_data.gsub(/[^[:space:]]*[\.]+[^[:space:]]*/) do |with_dot| + raw.gsub(/[^[:space:]]*[\.]+[^[:space:]]*/) do |with_dot| split = with_dot.split(".") if split.length > 1 - with_dot + (" " << split[1..-1].join(" ")) + with_dot + ((+" ") << split[1..-1].join(" ")) else with_dot end end + end + + def self.update_index(table: , id: , raw_data:) + search_data = raw_data.map do |data| + inject_extra_terms(Search.prepare_data(data || "", :index)) + end + + table_name = "#{table}_search_data" + foreign_key = "#{table}_id" # for user login and name use "simple" lowercase stemmer stemmer = table == "user" ? "simple" : Search.ts_config + ranked_index = <<~SQL + setweight(to_tsvector('#{stemmer}', coalesce(:a,'')), 'A') || + setweight(to_tsvector('#{stemmer}', coalesce(:b,'')), 'B') || + setweight(to_tsvector('#{stemmer}', coalesce(:c,'')), 'C') || + setweight(to_tsvector('#{stemmer}', coalesce(:d,'')), 'D') + SQL + + indexed_data = search_data.select { |d| d.length > 0 }.join(' ') + + params = { + a: search_data[0], + b: search_data[1], + c: search_data[2], + d: search_data[3], + raw_data: indexed_data, + id: id, + locale: SiteSetting.default_locale, + version: Search::INDEX_VERSION + } + # Would be nice to use AR here but not sure how to execut Postgres functions # when inserting data like this. - rows = Post.exec_sql_row_count("UPDATE #{table_name} - SET - raw_data = :raw_data, - locale = :locale, - search_data = TO_TSVECTOR('#{stemmer}', :search_data), - version = :version - WHERE #{foreign_key} = :id", - raw_data: raw_data, - search_data: search_data, - id: id, - locale: SiteSetting.default_locale, - version: Search::INDEX_VERSION) + rows = Post.exec_sql_row_count(<<~SQL, params) + UPDATE #{table_name} + SET + raw_data = :raw_data, + locale = :locale, + search_data = #{ranked_index}, + version = :version + WHERE #{foreign_key} = :id + SQL + if rows == 0 - Post.exec_sql("INSERT INTO #{table_name} - (#{foreign_key}, search_data, locale, raw_data, version) - VALUES (:id, TO_TSVECTOR('#{stemmer}', :search_data), :locale, :raw_data, :version)", - raw_data: raw_data, - search_data: search_data, - id: id, - locale: SiteSetting.default_locale, - version: Search::INDEX_VERSION) + Post.exec_sql(<<~SQL, params) + INSERT INTO #{table_name} + (#{foreign_key}, search_data, locale, raw_data, version) + VALUES (:id, #{ranked_index}, :locale, :raw_data, :version) + SQL end rescue - # don't allow concurrency to mess up saving a post + # TODO is there any way we can safely avoid this? + # best way is probably pushing search indexer into a dedicated process so it no longer happens on save + # instead in the post processor end def self.update_topics_index(topic_id, title, cooked) - search_data = title.dup << " " << scrub_html_for_search(cooked)[0...Topic::MAX_SIMILAR_BODY_LENGTH] - update_index('topic', topic_id, search_data) + scrubbed_cooked = scrub_html_for_search(cooked)[0...Topic::MAX_SIMILAR_BODY_LENGTH] + + # a bit inconsitent that we use title as A and body as B when in + # the post index body is C + update_index(table: 'topic', id: topic_id, raw_data: [title, scrubbed_cooked]) end - def self.update_posts_index(post_id, cooked, title, category) - search_data = scrub_html_for_search(cooked) << " " << title.dup.force_encoding('UTF-8') - search_data << " " << category if category - update_index('post', post_id, search_data) + def self.update_posts_index(post_id, title, category, tags, cooked) + update_index(table: 'post', id: post_id, raw_data: [title, category, tags, scrub_html_for_search(cooked)]) end def self.update_users_index(user_id, username, name) - search_data = username.dup << " " << (name || "") - update_index('user', user_id, search_data) + update_index(table: 'user', id: user_id, raw_data: [username, name]) end def self.update_categories_index(category_id, name) - update_index('category', category_id, name) + update_index(table: 'category', id: category_id, raw_data: [name]) end def self.update_tags_index(tag_id, name) - update_index('tag', tag_id, name) + update_index(table: 'tag', id: tag_id, raw_data: [name]) + end + + def self.queue_post_reindex(topic_id) + return if @disabled + + ActiveRecord::Base.exec_sql(<<~SQL, topic_id: topic_id) + UPDATE post_search_data + SET version = 0 + WHERE post_id IN (SELECT id FROM posts WHERE topic_id = :topic_id) + SQL end def self.index(obj, force: false) return if @disabled - if obj.class == Post && (obj.saved_change_to_cooked? || force) - if obj.topic - category_name = obj.topic.category.name if obj.topic.category - SearchIndexer.update_posts_index(obj.id, obj.cooked, obj.topic.title, category_name) - SearchIndexer.update_topics_index(obj.topic_id, obj.topic.title, obj.cooked) if obj.is_first_post? + category_name, tag_names = nil + topic = nil + + if Topic === obj + topic = obj + elsif Post === obj + topic = obj.topic + end + + category_name = topic.category&.name if topic + tag_names = topic.tags.pluck(:name).join(' ') if topic + + if Post === obj && (obj.saved_change_to_cooked? || force) + if topic + SearchIndexer.update_posts_index(obj.id, topic.title, category_name, tag_names, obj.cooked) + SearchIndexer.update_topics_index(topic.id, topic.title, obj.cooked) if obj.is_first_post? else Rails.logger.warn("Orphan post skipped in search_indexer, topic_id: #{obj.topic_id} post_id: #{obj.id} raw: #{obj.raw}") end end - if obj.class == User && (obj.saved_change_to_username? || obj.saved_change_to_name? || force) + if User === obj && (obj.saved_change_to_username? || obj.saved_change_to_name? || force) SearchIndexer.update_users_index(obj.id, obj.username_lower || '', obj.name ? obj.name.downcase : '') end - if obj.class == Topic && (obj.saved_change_to_title? || force) + if Topic === obj && (obj.saved_change_to_title? || force) if obj.posts post = obj.posts.find_by(post_number: 1) if post - category_name = obj.category.name if obj.category - SearchIndexer.update_posts_index(post.id, post.cooked, obj.title, category_name) + SearchIndexer.update_posts_index(post.id, obj.title, category_name, tag_names, post.cooked) SearchIndexer.update_topics_index(obj.id, obj.title, post.cooked) end end end - if obj.class == Category && (obj.saved_change_to_name? || force) + if Category === obj && (obj.saved_change_to_name? || force) SearchIndexer.update_categories_index(obj.id, obj.name) end - if obj.class == Tag && (obj.saved_change_to_name? || force) + if Tag === obj && (obj.saved_change_to_name? || force) SearchIndexer.update_tags_index(obj.id, obj.name) end end @@ -127,14 +169,14 @@ class SearchIndexer attr_reader :scrubbed def initialize - @scrubbed = "" + @scrubbed = +"" end def self.scrub(html) me = new parser = Nokogiri::HTML::SAX::Parser.new(me) begin - copy = "<div>" + copy = +"<div>" copy << html unless html.nil? copy << "</div>" parser.parse(html) unless html.nil? diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 6261e655809..6058c1b5842 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -289,13 +289,14 @@ class StaffActionLogger def log_silence_user(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create( - params(opts).merge( - action: UserHistory.actions[:silence_user], - target_user_id: user.id, - details: opts[:details] - ) + create_args = params(opts).merge( + action: UserHistory.actions[:silence_user], + target_user_id: user.id, + details: opts[:details] ) + create_args[:post_id] = opts[:post_id] if opts[:post_id] + + UserHistory.create(create_args) end def log_unsilence_user(user, opts = {}) @@ -304,6 +305,12 @@ class StaffActionLogger target_user_id: user.id)) end + def log_disable_second_factor_auth(user, opts = {}) + raise Discourse::InvalidParameters.new(:user) unless user + UserHistory.create(params(opts).merge(action: UserHistory.actions[:disabled_second_factor], + target_user_id: user.id)) + end + def log_grant_admin(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user UserHistory.create(params(opts).merge(action: UserHistory.actions[:grant_admin], diff --git a/app/services/user_silencer.rb b/app/services/user_silencer.rb index 5db360637b8..a9fff88dd0c 100644 --- a/app/services/user_silencer.rb +++ b/app/services/user_silencer.rb @@ -33,10 +33,12 @@ class UserSilencer SystemMessage.create(@user, message_type) if @by_user + log_params = { context: context, details: details } + log_params[:post_id] = @opts[:post_id].to_i if @opts[:post_id] + @user_history = StaffActionLogger.new(@by_user).log_silence_user( @user, - context: context, - details: details + log_params ) end diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 3ee86fa4d41..3211308db20 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -44,6 +44,7 @@ Discourse.SiteSettings = ps.get('siteSettings'); Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>'; Discourse.MarkdownItURL = '<%= asset_url('markdown-it-bundle.js') %>'; + Discourse.ServiceWorkerURL = '<%= Rails.application.assets_manifest.assets['service-worker.js'] %>' I18n.defaultLocale = '<%= SiteSetting.default_locale %>'; Discourse.start(); Discourse.set('assetVersion','<%= Discourse.assets_digest %>'); diff --git a/app/views/session/email_login.html.erb b/app/views/session/email_login.html.erb index 7bd6cf03fd9..43d988162f9 100644 --- a/app/views/session/email_login.html.erb +++ b/app/views/session/email_login.html.erb @@ -4,6 +4,19 @@ </div> <%end%> +<%if @second_factor_required%> + <div style="display: flex;"> + <div style="margin: auto;"> + <%= form_tag(method: "post") do%> + <h2><%=t "login.second_factor_title" %></h2> + <%= label_tag(:second_factor_token, t("login.second_factor_description")) %> + <div><%= text_field_tag(:second_factor_token) %></div> + <%= submit_tag(t("submit"), class: "btn btn-large btn-primary") %> + <%end%> + </div> + </div> +<%end%> + <% content_for :title do %><%=t "email_login.title" %><% end %> <%- content_for(:no_ember_head) do %> diff --git a/app/views/users/admin_login.html.erb b/app/views/users/admin_login.html.erb index 9b40e951ec7..b6fc06f9725 100644 --- a/app/views/users/admin_login.html.erb +++ b/app/views/users/admin_login.html.erb @@ -5,6 +5,13 @@ <body> <% if @message %> <%= @message %> + <% if @second_factor_required %> + <%=form_tag({}, method: :put) do %> + <%= label_tag(:second_factor_token, t('login.second_factor_description')) %> + <%= text_field_tag(:second_factor_token, nil, autofocus: true) %><br><br> + <%= submit_tag t('login.submit')%> + <% end %> + <% end %> <% else %> <%=form_tag({}, method: :put) do %> <%= label_tag(:email, t('admin_login.email_input')) %> diff --git a/app/views/users_email/confirm.html.erb b/app/views/users_email/confirm.html.erb index 0538ddfaac3..35877cd95d0 100644 --- a/app/views/users_email/confirm.html.erb +++ b/app/views/users_email/confirm.html.erb @@ -7,6 +7,17 @@ <h2><%= t 'change_email.confirmed' %></h2> <br> <a class="btn" href="/"><%= t('change_email.please_continue', site_name: SiteSetting.title) %></a> + <% elsif @update_result == :invalid_second_factor%> + <h2><%= t('login.second_factor_title') %></h2> + <br> + <%=form_tag({}, method: :put) do %> + <%= label_tag(:second_factor_token, t('login.second_factor_description')) %> + <%= text_field_tag(:second_factor_token, nil, autofocus: true) %><br> + <% if @show_invalid_second_factor_error %> + <div class='alert alert-error'><%= t('login.invalid_second_factor_code') %></div> + <% end %> + <%= submit_tag t('submit'), class: "btn btn-primary" %> + <% end %> <% else %> <div class='alert alert-error'> <%=t 'change_email.already_done' %> diff --git a/config/application.rb b/config/application.rb index eb2b23be4d8..bb98916354c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -129,13 +129,14 @@ module Discourse # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [ - :password, - :pop3_polling_password, - :api_key, - :s3_secret_access_key, - :twitter_consumer_secret, - :facebook_app_secret, - :github_client_secret + :password, + :pop3_polling_password, + :api_key, + :s3_secret_access_key, + :twitter_consumer_secret, + :facebook_app_secret, + :github_client_secret, + :second_factor_token, ] # Enable the asset pipeline diff --git a/config/boot.rb b/config/boot.rb index 929f1a62fe0..16ed53b095a 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -10,7 +10,7 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) -if ENV['RAILS_ENV'] != 'production' +if ENV['RAILS_ENV'] != 'production' && ENV['RAILS_ENV'] != 'profile' require 'bootsnap' Bootsnap.setup( diff --git a/config/initializers/100-lograge.rb b/config/initializers/100-lograge.rb index a1089a7b273..6ab44c6abe8 100644 --- a/config/initializers/100-lograge.rb +++ b/config/initializers/100-lograge.rb @@ -8,6 +8,13 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" Rails.application.configure do config.lograge.enabled = true + Lograge.ignore(lambda do |event| + # this is our hijack magic status, + # no point logging this cause we log again + # direct from hijack + event.payload[:status] == 418 + end) + config.lograge.custom_payload do |controller| begin username = @@ -46,7 +53,7 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" database: RailsMultisite::ConnectionManagement.current_db, } - if data = Thread.current[:_method_profiler] + if data = (Thread.current[:_method_profiler] || event.payload[:timings]) sql = data[:sql] if sql @@ -60,6 +67,13 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" output[:redis] = redis[:duration] * 1000 output[:redis_calls] = redis[:calls] end + + net = data[:net] + + if net + output[:net] = net[:duration] * 1000 + output[:net_calls] = net[:calls] + end end output diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index 7abb2bad4da..936df7f7c8b 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -122,6 +122,7 @@ ca: split_topic: "va dividir aquest tema %{when}" invited_user: "va invitar %{who} %{when}" invited_group: "va invitar %{who} %{when}" + user_left: "%{who} ha deixat aquest missatge %{when}" removed_user: "va eliminar %{who} %{when}" removed_group: "va eliminar %{who} %{when}" autoclosed: @@ -178,7 +179,7 @@ ca: sign_up: "Registra't" log_in: "Inicia sessió" age: "Edat" - joined: "T'hi has afegit" + joined: "Es va registrar" admin_title: "Admin" flags_title: "Avisos" show_more: "mostrar més" @@ -491,6 +492,7 @@ ca: mute: "Silenci" edit: "Editar preferències" download_archive: + button_text: "Descarrega tot" confirm: "Segur que vols descarregat les teves publicacions?" success: "Descàrrega inciada, quan el procés s'acabi t'ho notificarem amb un missatge." rate_limit_error: "Les publicacions es poden descarregar un cop al dia, si us plau torna a provar-ho demà." @@ -1286,7 +1288,7 @@ ca: one: "hi ha 1 nova publicació a aquest tema des de la teva darrera lectura" other: "hi ha {{count}} noves publicacions a aquest tema des de la teva darrera lectura" likes: - one: "hi ha 1 <i>M'agrada</i> a aquest tema" + one: "hi ha 1 M'agrada a aquest tema" other: "hi ha {{count}} m'agrades en aquest tema" back_to_list: "Torna al llistat de temes" options: "Opcions de tema" @@ -1336,6 +1338,10 @@ ca: '3_2': 'Rebràs alertes perquè estàs mirant aquest tema.' '3_1': 'Rebràs alertes perquè has creat aquest tema.' '3': 'Rebràs alertes perquè estàs mirant aquest tema.' + '2_8': 'Rebràs notificacions, perquè estàs seguint aquesta categoria.' + '2_4': 'Rebràs notificacions, perquè has escrit una resposta a aquest tema.' + '2_2': 'Rebràs notificacions, perquè estàs seguint aquest tema.' + '2': 'Rebràs notificacions, perquè has <a href="/users/{{username}}/preferences">llegit aquest tema</a>.' '1_2': 'Només se''t notificarà si algú menciona el teu @nom o contesta la teva entrada.' '1': 'Se''t notificarà si algú menciona el teu @nom o contesta la teva entrada.' '0_7': 'No estàs fent cas de les alertes d''aquesta categoria.' @@ -1528,7 +1534,7 @@ ca: one: "{{count}} Resposta" other: "{{count}} Respostes" has_likes: - one: "{{count}} <i>M'agrada</i>" + one: "{{count}} M'agrada" other: "{{count}} M'agrades" has_likes_title: one: "La publicació agrada a 1 persona" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 1ccb8cd93ce..f6d67fd6998 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -112,15 +112,15 @@ cs: x_days: one: "za 1 den" few: "za %{count} dny" - other: "za %{count} dní" + other: "o %{count} dní později" x_months: one: "za 1 měsíc" few: "za %{count} měsíce" - other: "za %{count} měsíců" + other: "o %{count} měsíců později" x_years: one: "za 1 rok" few: "za %{count} roků" - other: "za %{count} let" + other: "o %{count} let později" previous_month: 'Předchozí měsíc' next_month: 'Další měsíc' placeholder: datum @@ -135,7 +135,7 @@ cs: action_codes: public_topic: "Téma zveřejněno %{when}" private_topic: "Téma změněno na soukromé %{when}" - split_topic: "rozděl toto téma %{when}" + split_topic: "rozdělil toto téma %{when}" invited_user: "%{who} pozván %{when}" invited_group: "%{who} pozvána %{when}" removed_user: "%{who} smazán %{when}" @@ -1062,7 +1062,7 @@ cs: title_or_link_placeholder: "Sem vložte název téma" edit_reason_placeholder: "proč byla nutná úprava?" show_edit_reason: "(přidat důvod úpravy)" - reply_placeholder: "Piš tady. Pro formátování používej Markdown, BBCode nebo HTML. Přetáhni nebo vlož obrázky." + reply_placeholder: "Pište sem. Můžete použít Markdown, BBCode nebo HTML. Obrázky nahrajte přetáhnutím nebo vložením ze schránky." view_new_post: "Zobrazit váš nový příspěvek." saving: "Ukládám" saved: "Uloženo!" @@ -1168,7 +1168,7 @@ cs: advanced: title: Pokročilé hledání posted_by: - label: Zaslal + label: Od uživatele in_category: label: V kategorii in_group: @@ -1178,15 +1178,19 @@ cs: with_tags: label: Se štítkem filters: - likes: líbí se mi + label: Pouze v tématech/příspěvcích, které + likes: se mi líbí posted: Přidal jsem příspěvek watching: Sleduji tracking: Sleduji. + private: v mých zprávách first: jsou první příspěvek v tématu pinned: jsou připnuty unpinned: nejsou připnuty + seen: která jsem četl unseen: jsem nečetl wiki: jsou wiki + all_tags: Všechny uvedené štítky statuses: label: Kde příspěvky open: jsou otevřeny @@ -1207,7 +1211,7 @@ cs: not_logged_in_user: 'stránka uživatele s přehledem o aktuální činnosti a nastavení' current_user: 'jít na vaši uživatelskou stránku' topics: - new_messages_marker: "poslední navštívení" + new_messages_marker: "poslední návštěva" bulk: select_all: "Vybrat vše" clear_all: "Zrušit vše" @@ -1321,13 +1325,16 @@ cs: toggle_information: "zobrazit/skrýt detaily tématu" read_more_in_category: "Chcete si toho přečíst víc? Projděte si témata v {{catLink}} nebo {{latestLink}}." read_more: "Chcete si přečíst další informace? {{catLink}} nebo {{latestLink}}." - read_more_MF: "{ UNREAD, plural, =0 {} one { Je tu <a href='/unread'>1 nepřečtené</a> } other { Je tu <a href='/unread'># nepřečtených</a> } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} <a href='/new'>1 nové</a> téma} other { {BOTH, select, true{and } false {are } other{}} <a href='/new'># nových</a> témat} } remaining, nebo {CATEGORY, select, true {si projděte ostatní témata v kategorii {catLink}} false {{latestLink}} other {}}" + read_more_MF: "{ UNREAD, plural, =0 {} one { Zbývá <a href='/unread'>1 nepřečtené</a> téma } other { Je tu <a href='/unread'># nepřečtených</a> témat } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} <a href='/new'>1 nové</a> téma} other { {BOTH, select, true{and } false {are } other{}} <a href='/new'># nových</a> témat} }, nebo {CATEGORY, select, true {zobrazit další témata v kategorii {catLink}} false {{latestLink}} other {}}" browse_all_categories: Projděte všechny kategorie view_latest_topics: zobrazte si populární témata suggest_create_topic: Co takhle založit nové téma? jump_reply_up: přejít na předchozí odpověď jump_reply_down: přejít na následující odpověď deleted: "Téma bylo smazáno" + auto_update_input: + later_today: "Později během dnešního dne" + later_this_week: "Později během tohoto týdne" auto_close_title: 'Nastavení automatického zavření' auto_close_immediate: one: "Poslední příspěvek v témetu je již 1 hodinu starý, takže toto téma bude okamžitě uzavřeno." @@ -1358,6 +1365,10 @@ cs: '3_2': 'Budete dostávat oznámení, protože hlídáte toto téma.' '3_1': 'Budete dostávat oznámení, protože jste autorem totoho tématu.' '3': 'Budete dostávat oznámení, protože hlídáte toto téma.' + '2_8': 'Uvidíte počet nových odpovědí, jelikož sledujete tuto kategorii.' + '2_4': 'Uvidíte počet nových odpovědí, jelikož jste přispěli do tohoto tématu.' + '2_2': 'Uvidíte počet nových odpovědí, jelikož sledujete toto téma.' + '2': 'Uvidíte počet nových odpovědí, protože <a href="/u/{{username}}/preferences">jste četl(a) toto téma</a>.' '1_2': 'Budete informováni pokud někdo zmíní vaše @jméno nebo odpoví na váš příspěvek.' '1': 'Budete informováni pokud někdo zmíní vaše @jméno nebo odpoví na váš příspěvek.' '0_7': 'Ignorujete všechna oznámení v této kategorii.' @@ -1600,7 +1611,7 @@ cs: about: "toto je wiki příspěvek" archetypes: save: 'Uložit nastavení' - few_likes_left: "Díky za šíření lásky! Zbývá ti pro dnešek už jen několi \"líbí se\"." + few_likes_left: "Díky za šíření lásky! Zbývá ti pro dnešek už jen několik málo lajků." controls: reply: "otevře okno pro sepsání odpovědi na tento příspěvek" like: "to se mi líbí" @@ -1913,9 +1924,9 @@ cs: posts_long: "v tomto tématu je {{number}} příspěvků" posts_likes_MF: | Toto téma má {count, plural, one {1 příspěvek} other {# příspěvků}} {ratio, select, - low {s velkým poměrem líbí se na příspěvek} - med {s velmi velkým poměrem líbí se na příspěvek} - high {s extrémně velkým poměrem líbí se na příspěvek} + low {s vysokým počtem lajků} + med {s velmi vysokým počtem lajků} + high {s extrémně vysokým počtem lajků} other {}} original_post: "Původní příspěvek" views: "Zobrazení" @@ -1930,7 +1941,7 @@ cs: one: "líbí se" few: "líbí se" other: "líbí se" - likes_long: "v tomto tématu je {{number}} 'líbí se'" + likes_long: "v tomto tématu je {{number}} lajků" users: "Účastníci" users_lowercase: one: "uživatel" @@ -1947,12 +1958,12 @@ cs: with_topics: "%{filter} témata" with_category: "%{filter} %{category} témata" latest: - title: "Nejaktuálnější" + title: "Aktuální" title_with_count: one: "Nedávné (1)" few: "Nedávná ({{count}})" other: "Nedávná ({{count}})" - help: "nejaktuálnější témata" + help: "aktuální témata" hot: title: "Populární" help: "populární témata z poslední doby" @@ -2735,7 +2746,7 @@ cs: reputation: Reputace permissions: Oprávnění activity: Aktivita - like_count: Rozdaných / obdržených 'líbí se' + like_count: Rozdaných / obdržených lajků last_100_days: 'Za posledních 100 dní' private_topics_count: Počet soukromých témat posts_read_count: Přečteno příspěvků @@ -2781,6 +2792,7 @@ cs: activate_failed: "Nasstal problém při aktivování tohoto uživatele." deactivate_account: "Deaktivovat účet" deactivate_failed: "Nastal problém při deaktivování tohoto uživatele." + silence_accept: 'Ano, umlčet tohoto uživatele' bounce_score: "Bounce skóre" reset_bounce_score: label: "obnovit výchozí" @@ -2899,6 +2911,7 @@ cs: developer: 'Vývojáři' embedding: "Embedding" legal: "Právní záležitosti" + api: 'API' user_api: 'Uživatelské API' uncategorized: 'Ostatní' backups: "Zálohy" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index c1fde58866d..56e79ecfa82 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -821,7 +821,7 @@ de: other: "gegeben" likes_received: one: "vergeben" - other: "vergeben" + other: "erhalten" days_visited: one: "Tag vorbeigekommen" other: "Tage vorbeigekommen" @@ -1057,7 +1057,6 @@ de: default_header_text: Auswählen… no_content: Keine Treffer gefunden filter_placeholder: Suchen… - create: "Erstelle {{content}}" emoji_picker: filter_placeholder: Emoji suchen people: Personen @@ -2424,11 +2423,9 @@ de: moderation_history: "Moderationshistorie" agree: "Zustimmen" agree_title: "Meldung bestätigen, weil diese gültig und richtig ist" - agree_flag_hide_post: "Zustimmen und Beitrag verstecken" agree_flag_hide_post_title: "Verstecke diesen Beitrag und sende dem Benutzer/der Benutzerin eine Nachricht mit der Bitte, ihn zu bearbeiten." agree_flag_restore_post: "Zustimmen und Beitrag wiederherstellen" agree_flag_restore_post_title: "Beitrag wiederherstellen, sodass alle Benutzer/-innen ihn sehen können." - agree_flag: "Zustimmen und Beitrag behalten" agree_flag_title: "Meldung zustimmen und den Beitrag unbearbeitet lassen." delete: "Löschen" delete_title: "Lösche den Beitrag, auf den diese Meldung verweist." diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 5dee9bb3a9c..b20362728f4 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -1054,7 +1054,6 @@ el: default_header_text: Επιλογή... no_content: Δεν βρέθηκαν αποτελέσματα filter_placeholder: Αναζήτηση... - create: "Δημιουργία {{content}}" emoji_picker: filter_placeholder: Αναζήτηση για emoji people: Άνθρωποι @@ -2369,11 +2368,9 @@ el: topics: "Επισημασμένα Νήματα" agree: "Συμφωνώ" agree_title: "Επιβεβαίωσε αυτή τη σήμανση ως έγκυρη και σωστή" - agree_flag_hide_post: "Αποδοχή και απόκρυψη ανάρτησης" agree_flag_hide_post_title: "Κρύψε αυτή την ανάρτηση και αυτόματα στείλε στον χρήστη ένα μήνυμα που θα τους προτρέπει να το επεξεργαστούν." agree_flag_restore_post: "Αποδοχή και επαναφορά ανάρτησης" agree_flag_restore_post_title: "Επαναφορά της ανάρτησης ώστε όλοι οι χρήστες να μπορούν να το δουν." - agree_flag: "Αποδοχή και διατήρηση της ανάρτησης" agree_flag_title: "Αποδοχή επισήμανσης και διατήρηση της ανάρτησης αμετάβλητης." delete: "Διαγραφή" delete_title: "Διέγραψε την ανάρτηση στην οποία αναφέρεται αυτή η επισήμανση." diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 49d0be51dc3..a0069439d1e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -207,6 +207,7 @@ en: not_implemented: "That feature hasn't been implemented yet, sorry!" no_value: "No" yes_value: "Yes" + submit: "Submit" generic_error: "Sorry, an error has occurred." generic_error_with_reason: "An error occurred: %{error}" sign_up: "Sign Up" @@ -707,6 +708,17 @@ en: choose_new: "Choose a new password" choose: "Choose a password" + second_factor: + title: "Two Factor Authentication" + enable: "Enable Two Factor Authentication" + disable: "Disable Two Factor Authentication" + confirm_password_description: "Confirm your password to continue enabling Two Factor Authentication." + enable_description: "To complete Two Factor Authentication setup, scan the following QR code and submit a Two Factor Authentication code." + disable_description: "Enter a Two Factor Authentication code to disable." + show_key_description: "Or enter the key manually." + info_prompt: "What is Two Factor Authentication?" + extended_description: "Two Factor Authentication adds an extra security step to logging in by requiring a one-time token in addition to your password. These tokens are generated by compatible apps for iPhone or Android such as <a href=\"https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2\" target='_blank'>Google Authenticator</a>, <a href=\"https://play.google.com/store/apps/details?id=com.authy.authy\" target='_blank'>Authy</a>, and <a href=\"https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp\" target='_blank'>FreeOTP</a>." + change_about: title: "Change About Me" error: "There was an error changing this value." @@ -1097,6 +1109,9 @@ en: title: "Log In" username: "User" password: "Password" + second_factor_title: "Two Factor Authentication Required" + second_factor_description: "Enter a generated verification code." + second_factor_label: "Code" email_placeholder: "email or username" caps_lock_warning: "Caps Lock is on" error: "Unknown error" @@ -1183,7 +1198,8 @@ en: default_header_text: Select... no_content: No matches found filter_placeholder: Search... - create: "Create {{content}}" + create: "Create: '{{content}}'" + max_content_reached: "You can only select {{count}} items." emoji_picker: filter_placeholder: Search for emoji @@ -1307,16 +1323,16 @@ en: desc: Reply to a specific post reply_as_new_topic: label: Reply as linked topic - desc: Create a new topic + desc: Create a new topic linked to this topic reply_as_private_message: label: New message - desc: Create a private message + desc: Create a new personal message reply_to_topic: label: Reply to topic - desc: Reply to the original post without replying to a specific post + desc: Reply to the topic, not any specific post toggle_whisper: label: Toggle whipser - desc: Whispers will only be visible by staff members + desc: Whispers are only visible to staff members create_topic: label: "New Topic" @@ -3261,6 +3277,7 @@ en: post_locked: "post locked" post_unlocked: "post unlocked" check_personal_message: "check personal message" + disabled_second_factor: "disable 2 factor authentication" screened_emails: title: "Screened Emails" description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed." diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 5c3311303bb..c2cd21dee2d 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -1059,7 +1059,6 @@ es: default_header_text: Seleccionar... no_content: Ninguna coincidencia encontrada filter_placeholder: Buscar... - create: "Crear {{content}}" emoji_picker: filter_placeholder: Buscar emoji people: People @@ -2445,11 +2444,9 @@ es: moderation_history: "Histórico de moderación" agree: "De acuerdo" agree_title: "Confirmar esta indicación como válido y correcto." - agree_flag_hide_post: "Coincidir y Ocultar Mensaje" agree_flag_hide_post_title: "Ocultar este post y enviar automáticamente un mensaje al usuario para que lo edite de forma urgente" agree_flag_restore_post: "Coincidir y Restaurar Mensaje" agree_flag_restore_post_title: "Restaurar el post para que todos los usuarios puedan verlo." - agree_flag: "Estar de acuerdo y mantener post" agree_flag_title: "Estar de acuerdo con el reporte y mantener la publicación intacta." ignore_flag: "Ignorar" ignore_flag_title: "Quitar este reporte; no requiere tomar medidas en este momento." diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 081d116139b..523ee1ccd1b 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -1021,7 +1021,6 @@ et: default_header_text: Vali... no_content: Midagi ei leitud filter_placeholder: Otsi... - create: "Loo {{content}}" emoji_picker: filter_placeholder: Otsi emojit people: Inimesed diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 6af668a8b9d..c0c07f992de 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -45,12 +45,16 @@ fa_IR: other: "< %{count} ثانیه" x_seconds: other: "%{count} ثانیه" + less_than_x_minutes: + other: "< %{count} دقیقه" x_minutes: other: "%{count} دقیقه" about_x_hours: other: "%{count} ساعت" x_days: other: "%{count} روز" + x_months: + other: "%{count} ماه" about_x_years: other: "%{count} سال" over_x_years: @@ -83,6 +87,7 @@ fa_IR: other: "%{count} سال بعد" previous_month: 'ماه قبل' next_month: 'ماه بعد' + placeholder: تاریخ share: topic: 'پیوندی به این موضوع را به اشتراک بگذارید' post: 'ارسال #%{postNumber}' @@ -97,6 +102,7 @@ fa_IR: split_topic: "این موضوع %{when} جدا شد " invited_user: "%{who} در %{when} دعوت شده" invited_group: "%{who} در %{when} دعوت شده" + user_left: "%{who}%{when} خود را از این پیغام حذف کرد." removed_user: "%{who} در %{when} حذف شد" removed_group: "%{who} در %{when} حذف شد" autoclosed: @@ -234,6 +240,7 @@ fa_IR: uploading: "در حال بارگذاری..." uploading_filename: "بارگذاری {{filename}}..." uploaded: "بارگذاری شد!" + pasting: "چسباندن..." enable: "فعال کردن" disable: "ازکاراندازی" undo: "بیاثر کردن" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index e900163c6dc..606a9423568 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -1059,7 +1059,6 @@ fi: default_header_text: Valitse... no_content: Ei osumia filter_placeholder: Hae... - create: "Luo {{content}}" emoji_picker: filter_placeholder: Etsi emojia people: Ihmiset @@ -2438,11 +2437,9 @@ fi: moderation_history: "Valvontahistoria" agree: "Ole samaa mieltä" agree_title: "Vahvista, että lippu on annettu oikeasta syystä" - agree_flag_hide_post: "Ole samaa mieltä ja piilota viesti" agree_flag_hide_post_title: "Piilota viesti ja lähetä automaattinen yksityisviesti, joka kehottaa käyttäjää muokkaamaan viestiä." agree_flag_restore_post: "Ole samaa mieltä ja palauta viesti" agree_flag_restore_post_title: "Palauta viesti niin että kaikki käyttäjät näkevät sen." - agree_flag: "Ole samaa mieltä ja säilytä viesti" agree_flag_title: "Ole samaa mieltä lipun kanssa ja pidä viesti ennallaan" ignore_flag: "Sivuuta" ignore_flag_title: "Poista tämä lippu; toimenpiteitä ei tarvita juuri nyt." diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 3ca5f14f9c0..7db09ec015c 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -1057,7 +1057,6 @@ fr: default_header_text: Sélectionner… no_content: Aucune correspondance trouvée filter_placeholder: Rechercher... - create: "Créer {{content}}" emoji_picker: filter_placeholder: Chercher un emoji people: Personnes @@ -2427,11 +2426,9 @@ fr: moderation_history: "Historique de la modération" agree: "Accepter" agree_title: "Confirmer que le signalement est valide et correcte" - agree_flag_hide_post: "Accepter et cacher le message" agree_flag_hide_post_title: "Cacher le message et envoyer automatiquement un message à l'utilisateur l'incitant à le modifier." agree_flag_restore_post: "Accepter et rétablir le message" agree_flag_restore_post_title: "Rétablir le message afin que tous les utilisateurs puissent le voir." - agree_flag: "Accepter et conserver le message" agree_flag_title: "Accepter le signalement et conserver le message tel quel." delete: "Supprimer" delete_title: "Supprimer le message signalé." diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index e36c3001fd8..6cd66cd73a5 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -744,7 +744,6 @@ id: password_label: "Atur Kata Sandi" select_kit: filter_placeholder: Cari... - create: "Buat {{konten}}" emoji_picker: filter_placeholder: Cari emoji people: Orang diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index d42929dd86d..e05a3d5f297 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -1055,7 +1055,6 @@ it: default_header_text: Selezione... no_content: Nessun risultato trovato filter_placeholder: Ricerca... - create: "Crea {{content}}" emoji_picker: filter_placeholder: Ricerca per emoji people: Persone @@ -2402,11 +2401,9 @@ it: topics: "Argomenti Segnalati" agree: "Acconsento" agree_title: "Conferma che questa segnalazione è valida e corretta" - agree_flag_hide_post: "Accetta e Nascondi il Messaggio" agree_flag_hide_post_title: "Nascondi questo messaggio e invia automaticamente all'utente un messaggio privato che lo invita a modificarlo." agree_flag_restore_post: "Accetta e Ripristina il Messaggio" agree_flag_restore_post_title: "Ripristina il messaggio in modo che tutti gli utenti possano vederlo." - agree_flag: "Accetto e Mantieni il Messaggio" agree_flag_title: "Accetta la segnalazione e mantieni invariato il messaggio." delete: "Cancella" delete_title: "Cancella il messaggio a cui si riferisce la segnalazione." diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index f39726ff713..3733350c171 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -1000,7 +1000,6 @@ ja: default_header_text: 選択... no_content: 一致する項目が見つかりませんでした filter_placeholder: 検索... - create: "{{content}} を作成" emoji_picker: filter_placeholder: 絵文字を探す people: ピープル diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 90581f22b43..557ba11bdc2 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -122,7 +122,7 @@ ko: enabled: '%{when} 목록에 게시' disabled: '%{when} 목록에서 감춤' banner: - enabled: 'made this a banner %{when}. It will appear at the top of every page until it is dismissed by the user.' + enabled: '%{when} 이 내용을 배너로 만드세요 . 사용자가 제거하지 않을 때 까지 배너는 모든 페이지 상단에 노출됩니다.' disabled: '이 배너를 제거했습니다 %{when}. 더 이상 모든 페이지의 상단에 표시되지 않습니다.' topic_admin_menu: "토픽 관리" wizard_required: "새로운 Discourse에 오신것을 환영합니다! <a href='%{url}' data-auto-route='true'>설치 마법사</a> 로 시작해봅시다✨" @@ -2195,8 +2195,8 @@ ko: critical_available: "중요 업데이트를 사용할 수 있습니다." updates_available: "업데이트를 사용할 수 있습니다." please_upgrade: "업그레이드하세요." - no_check_performed: "A check for updates has not been performed. Ensure sidekiq is running." - stale_data: "A check for updates has not been performed lately. Ensure sidekiq is running." + no_check_performed: "업데이트 확인이 수행되지 않았습니다. sidekiq가 실행 중인지 확인하십시오." + stale_data: "최근 업데이트 확인이 수행되지 않았습니다. sidekiq가 실행 중인지 확인하십시오." version_check_pending: "최근에 업데이트 되었군요! 환상적입니다!!" installed_version: "설치됨" latest_version: "최근" @@ -2312,7 +2312,7 @@ ko: automatic_membership_email_domains: "이 목록의 있는 항목과 사용자들이 등록한 이메일 도메인이 일치할때 이 그룹에 포함" automatic_membership_retroactive: "이미 등록된 사용자에게 같은 이메일 도메인 규칙 적용하기" default_title: "이 그룹의 모든 사용자를 위한 기본 제목" - primary_group: "Automatically set as primary group" + primary_group: "기본 그룹으로 자동적으로 설정" group_owners: 소유자 add_owners: 소유자 추가하기 incoming_email: "사용자 설정 수신 이메일 주소" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index 57258dd4199..cba0adfda43 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -764,6 +764,7 @@ nb_NO: title: "invitasjoner" user: "Invitert bruker" sent: "Sendt" + none: "Ingen invitasjoner å vise." truncated: one: "Viser den første invitasjonen." other: "Viser de {{count}} første invitisajonene." @@ -1059,7 +1060,6 @@ nb_NO: default_header_text: Velg… no_content: Ingen treff funnet filter_placeholder: Søk… - create: "Opprett {{content}}" emoji_picker: filter_placeholder: Søk etter emoji people: Folk @@ -1927,7 +1927,7 @@ nb_NO: edit: 'rediger' edit_long: "Rediger" view: 'Se tråder i kategori' - general: 'Generellt' + general: 'Generelt' settings: 'Innstillinger' topic_template: "Mal for tråd" tags: "Stikkord" @@ -2447,11 +2447,9 @@ nb_NO: moderation_history: "Moderatorhistorikk" agree: "Godta" agree_title: "Bekreft at denne rapporteringen er gyldig og korrekt" - agree_flag_hide_post: "Samtykk og skjul innlegg" agree_flag_hide_post_title: "Gjem dette innlegget og send en melding til brukeren automatisk med en forespørsel om gjøre endringer." agree_flag_restore_post: "Samtykk og gjenopprett innlegg" agree_flag_restore_post_title: "Gjenopprett innlegget slik at alle brukere kan se det." - agree_flag: "Samtykk og behold innlegg" agree_flag_title: "Samtykk med flagg og behold innlegget uendret." ignore_flag: "Ignorer" ignore_flag_title: "Fjern denne rapporteringen; den krever ingen handling på dette tidspunktet." @@ -2483,7 +2481,6 @@ nb_NO: system: "System" error: "Noe gikk galt" reply_message: "Svar" - suspended_for_post: "Brukeren ble utestengt for dette innlegget." no_results: "Det finnes ingen rapporterte innlegg." topic_flagged: "Denne tråden er blitt rapportert." show_full: "vis hele innlegget" @@ -2655,6 +2652,7 @@ nb_NO: label: "Last opp" title: "Last opp en sikkerhetskopi til denne instansen" uploading: "Laster opp…" + success: "Opplastingen av '{{filename}}' var vellykket. Filen blir nå bearbeidet og det vil ta opp til et minutt før den dukker opp i listen." error: "Det oppstod en feil ved opplastingen av '{{filename}}': {{message}}" operations: is_running: "En prosess pågår…" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 373ccc956c8..c552e29164e 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -1123,7 +1123,6 @@ pl_PL: default_header_text: Wybierz... no_content: Nie znaleziono dopasowań filter_placeholder: Wyszukiwanie... - create: "Utwórz {{content}}" emoji_picker: filter_placeholder: Szukaj emoji people: Ludzie @@ -2581,9 +2580,7 @@ pl_PL: moderation_history: "Historia moderacji" agree: "Potwierdź" agree_title: "Potwierdź to zgłoszenie jako uzasadnione i poprawne" - agree_flag_hide_post: "Potwierdź i ukryj post" agree_flag_restore_post: "Potwierdź i przywróć post" - agree_flag: "Potwierdź i zachowaj post" delete: "Usuń" delete_title: "Usuń wpis do którego odnosi się flaga." delete_post_defer_flag: "Usuń post i ignoruj flagę" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 795e66f6503..eead0fe643e 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -51,6 +51,10 @@ ro: one: "1s" few: "%{count} s" other: "%{count} s" + less_than_x_minutes: + one: "< 1m" + few: "< 1m" + other: "< %{count}m" x_minutes: one: "1m" few: "%{count} m" @@ -63,6 +67,10 @@ ro: one: "1z" few: "%{count} z" other: "%{count} z" + x_months: + one: "1mon" + few: "1mon" + other: "%{count}mon" about_x_years: one: "1a" few: "%{count} a" @@ -119,6 +127,7 @@ ro: other: "După %{count} de ani" previous_month: 'Luna anterioară' next_month: 'Luna următoare' + placeholder: dată share: topic: 'Distribuie un link cu acest subiect' post: 'Distribuie postarea #%{postNumber}' @@ -133,6 +142,7 @@ ro: split_topic: "desparte această discuție %{when}" invited_user: "a invitat pe %{who} %{when}" invited_group: "a invitat pe %{who} %{when}" + user_left: "%{who} s-a îndepărtat de la acest mesaj %{when}" removed_user: "a scos pe %{who} %{when}" removed_group: "scos pe %{who} %{when}" autoclosed: @@ -153,7 +163,11 @@ ro: visible: enabled: 'afișat %{when}' disabled: 'ascuns %{when}' + banner: + enabled: 'a creat acest banner %{when}. Va apărea la începutul fiecărei pagini până când va fi ascuns de către utilizator.' + disabled: 'a îndepărtat acest banner %{when}. Nu va mai apărea la începutul fiecărei pagini.' topic_admin_menu: "Opțiuni administrare subiect" + wizard_required: "Bine ai venit la noul tău Discourse! Să începem cu <a href='%{url}' data-auto-route='true'>expertul de configurare</a> ✨" emails_are_disabled: "Trimiterea de emailuri a fost dezactivată global de către un administrator. Nu vor fi trimise notificări email de nici un fel." bootstrap_mode_enabled: "Pentru a ușura lansarea site-ului tău ești în modul bootstrap. Toți noii utilizatori vor primi nivelul de încredere 1 și vor avea activată primirea zilnică a unui email-rezumat. Această setare va fi dezactivată automat de îndată ce numărul total de utilizatori depășește %{min_users}." bootstrap_mode_disabled: "Modul bootstrap va fi dezactivat în următoarele 24 de ore." @@ -169,8 +183,10 @@ ro: cn_north_1: "China (Beijing)" eu_central_1: "EU (Frankfurt)" eu_west_1: "EU (Irlanda)" + eu_west_2: "UE (Londra)" sa_east_1: "South America (Sao Paulo)" us_east_1: "US East (N. Virginia)" + us_east_2: "America de Est (Ohio)" us_gov_west_1: "AWS GovCloud (US)" us_west_1: "US West (N. California)" us_west_2: "US West (Oregon)" @@ -274,6 +290,7 @@ ro: uploading: "Se încarcă..." uploading_filename: "Se încarcă {{filename}}..." uploaded: "Încărcat!" + pasting: "Lipire..." enable: "Activează" disable: "Dezactivează" undo: "Anulează acțiunea" @@ -372,6 +389,8 @@ ro: add_members: "Adaugă membri" delete_member_confirm: "Șterge utilizatorul '%{username}' din grupul '%{group}'?" name_placeholder: "Numele grupului, fără spații, la fel ca regula pentru nume utilizator" + public_admission: "Permite utilizatorilor să adere la grup fără restricții (Presupune ca grupul să fie vizibil)" + public_exit: "Permite utilizatorilor să părăsească grupul în mod liber." empty: posts: "Nu există postări ale membrilor acestui grup." members: "Nu există nici un membru în acest grup." @@ -383,9 +402,16 @@ ro: join: "Alătură-te grupului" leave: "Părăsește grupul" request: "Cere să te alături grupului" + message: "Mesaj" automatic_group: Grup automat closed_group: Grup închis is_group_user: "Ești membru al acestui grup" + allow_membership_requests: "Permite utilizatorilor să trimită cereri de membru către proprietarul grupului." + membership_request_template: "Șablonul personalizat pentru a fi afișat utilizatorilor atunci când trimiteți o solicitare de membru" + membership_request: + submit: "Trimite cerere" + title: "Cerere de aderare @%{group_name}" + reason: "Aduceți la cunoștință proprietarilor grupului motivul pentru care dorești să aderi la acest grup" membership: "Apartenență" name: "Nume" user_count: "Număr de membri" @@ -395,13 +421,26 @@ ro: index: title: "Grupuri" empty: "Nu există nici un grup vizibil." + title: + one: "Grup" + few: "Grupuri" + other: "Grupuri" activity: "Activitate" members: "Membri" topics: "Subiecte" posts: "Postări" mentions: "Mențiuni" messages: "Mesaje" + notification_level: "Nivelul de notificare prestabilit pentru mesajele de grup" + visibility_levels: + title: "Cine poate vedea acest grup?" + public: "Toată lumea" + members: "Proprietarii grupului, membrii și adminii" + staff: "Proprietarii grupului și echipa" + owners: "Proprietarii și adminii din grup" alias_levels: + mentionable: "Cine poate @menționa acest grup?" + messageable: "Cine poate trimite mesaje acestui grup?" nobody: "Nimeni" only_admins: "Doar adminii" mods_and_admins: "Doar moderatorii și adminii" @@ -450,6 +489,7 @@ ro: '14': "În așteptare" categories: all: "Toate categoriile" + all_subcategories: "toate în %{categoryName}" no_subcategory: "Niciuna" category: "Categorie" category_list: "Afișează lista de categorii" @@ -490,6 +530,7 @@ ro: topics_entered: "Subiecte la care particip" post_count: "# postări" confirm_delete_other_accounts: "Ești sigur că vrei să ștergi aceste conturi?" + powered_by: "creat de <a href='https://ipinfo.io'>ipinfo.io</a>" user_fields: none: "(alege o opțiune)" user: @@ -498,6 +539,7 @@ ro: mute: "Silențios" edit: "Editează preferințe" download_archive: + button_text: "Descarcă tot" confirm: "Ești sigur că vrei să îți descarci postările?" success: "Descărcarea a început, vei fi notificat printr-un mesaj atunci când procesul se va termina." rate_limit_error: "Postările pot fi descărcate doar o singură dată pe zi, te rugăm să încerci din nou mâine." @@ -522,11 +564,14 @@ ro: disable: "Dezactivează notificări" enable: "Activează notificările" each_browser_note: "Observație: Setările trebuie modificate pe fiecare browser utilizat." + dismiss: 'Înlătură' dismiss_notifications: "Elimină tot" dismiss_notifications_tooltip: "Marchează toate notificările ca citite" first_notification: "Prima notificare pe care ai primit-o! Selectează-o ca să continui. " disable_jump_reply: "Nu sări la postarea mea după ce răspund" dynamic_favicon: "Arată subiectele noi/actualizate în pictograma browserului." + theme_default_on_all_devices: "Fă această temă implicită pentru toate dispozitivele mele" + allow_private_messages: "Permite altor utilizatori să îmi trimită mesaje private" external_links_in_new_tab: "Deschide toate adresele externe într-un tab nou" enable_quoting: "Activează citarea răspunsurilor pentru textul selectat" change: "Schimbă" @@ -534,7 +579,9 @@ ro: admin: "{{user}} este admin" moderator_tooltip: "Acest utilizator este moderator" admin_tooltip: "Acest utilizator este admin" + silenced_tooltip: "Acest utilizator este silențios" suspended_notice: "Acest utilizator este suspendat până la {{date}}." + suspended_permanently: "Acest utilizator este suspendat." suspended_reason: "Motiv: " github_profile: "Github" email_activity_summary: "Sumarul activității" @@ -565,6 +612,7 @@ ro: watched_first_post_tags_instructions: "Vei fi notificat cu privire la prima postare din fiecare nou subiect cu aceste etichete." muted_categories: "Setat pe silențios" muted_categories_instructions: "Nu vei fi notificat despre nimic legat de noile subiecte din aceste categorii și subiectele respective nu vor apărea în lista cu cele mai recente subiecte." + no_category_access: "În calitate de moderator ai acces limitat la categorii, salvarea este dezactivată." delete_account: "Șterge-mi contul" delete_account_confirm: "Ești sigur că vrei să ștergi contul? Această acțiune este ireversibilă!" deleted_yourself: "Contul tău a fost șters cu succes." @@ -576,11 +624,15 @@ ro: muted_users_instructions: "Oprește toate notificările de la aceşti utilizatori." muted_topics_link: "Arată subiectele dezactivate" watched_topics_link: "Arată subiectele urmărite activ" + tracked_topics_link: "Afișează subiectele urmărite" automatically_unpin_topics: "Detașare automată a subiectelor când ajung la sfârșitul paginii." apps: "Aplicații" revoke_access: "Revocă accesul" undo_revoke_access: "Anulează revocarea accesului" api_approved: "Aprobate:" + theme: "Temă" + home: "Pagina inițială implicită" + staged: "Pus în scenă" staff_counters: flags_given: "Marcaje ajutătoare" flagged_posts: "Postări marcate" @@ -598,6 +650,15 @@ ro: move_to_archive: "Arhivează" failed_to_move: "Mutarea mesajelor selectate a eșuat (poate că v-a căzut rețeaua)" select_all: "Selectează tot" + preferences_nav: + account: "Cont" + profile: "Profil" + emails: "Adrese de email" + notifications: "Notificări" + categories: "Categorii" + tags: "Etichete" + interface: "Interfață" + apps: "Aplicații" change_password: success: "(email trimis)" in_progress: "(se trimite email)" @@ -619,10 +680,12 @@ ro: taken: "Această adresă există deja în baza de date." error: "A apărut o eroare la schimbarea de email. Poate această adresă este deja utilizată?" success: "Am trimis un email către adresa respectivă. Urmează instrucțiunile de confirmare." + success_staff: "Ți-am trimis un email la adresa curentă. Urmează instrucțiunile de confirmare." change_avatar: title: "Schimbă poza de profil" gravatar: "<a href='//gravatar.com/emails' target='_blank'>Gravatar</a>, bazat pe" gravatar_title: "Schimbă-ți avatarul de pe site-ul Gravatar." + gravatar_failed: "Gravatarul nu a putut fi preluat. Există unul asociat cu adresa de email respectivă?" refresh_gravatar_title: "Reactualizează Gravatarul" letter_based: "Poză de profil atribuită de sistem." uploaded_avatar: "Poză preferată" @@ -639,6 +702,7 @@ ro: instructions: "Fundalul va fi centrat şi va avea o dimensiune standard de 590px." email: title: "Email" + instructions: "nu afișa niciodată către public" ok: "Îți vom trimite un email pentru confirmare." invalid: "Introduceți o adresă de email validă." authenticated: "Emailul a fost autentificat de către {{provider}}." @@ -655,6 +719,7 @@ ro: ok: "Numele tău este OK." username: title: "Nume utilizator" + instructions: "unic, fără spații, scurt" short_instructions: "Ceilalți te pot menționa ca @{{username}}." available: "Numele de utilizator este disponibil." not_available: "Nu este disponibil. Încerci {{suggestion}}?" @@ -731,6 +796,7 @@ ro: title: "Invitații" user: "Utilizator invitat" sent: "Trimis(e)" + none: "Nu există invitații." truncated: one: "Se afișează prima invitație." few: "Se afișează primele {{count}} invitații." @@ -747,8 +813,12 @@ ro: expired: "Această invitație a expirat." rescind: "Elimină" rescinded: "Invitație eliminată" + rescind_all: "Elimină toate invitațiile" + rescinded_all: "Toate invitațiile au fost eliminate!" + rescind_all_confirm: "Sigur elimini toate invitațiile?" reinvite: "Retrimite invitaţia" reinvite_all: "Retrimite toate invitațiile" + reinvite_all_confirm: "Sigur retrimiți toate invitațiile?" reinvited: "Invitaţia a fost retrimisă" reinvited_all: "Toate invitațiile retrimise!" time_read: "Timp de citire" @@ -759,8 +829,10 @@ ro: link_generated: "Link de invitare generat cu succes!" valid_for: "Link-ul de invitare este valid doar pentru următoarele adrese de email: %{email}" bulk_invite: + none: "Încă nu ai invitat pe nimeni. Trimite invitații indivituale sau invită mai multe persoane odată <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>uploadând un fișier CSV</a>." text: "Invitație multiplă din fișier" success: "Fișier încărcat cu succes, vei fi înștiințat printr-un mesaj când procesarea este completă." + error: "Scuze, fișierul trebuie să fie în format CSV." password: title: "Parolă" too_short: "Parola este prea scurtă." @@ -781,10 +853,22 @@ ro: one: "o postare creată" few: "postări create" other: "de postări create" + likes_given: + one: "oferit" + few: "oferite" + other: "oferite" + likes_received: + one: "primit" + few: "primite" + other: "primite" days_visited: one: "vizită" few: "vizite" other: "de vizite" + topics_entered: + one: "subiect vizualizat" + few: "subiecte vizualizate" + other: "subiecte vizualizate" posts_read: one: "postare citită" few: "postări citite" @@ -876,6 +960,11 @@ ro: first_post: Prima postare mute: Blochează unmute: Deblochează + last_post: Postat + time_read: Citit + time_read_recently: '%{time_read} recent' + time_read_tooltip: '%{time_read} total citit' + time_read_recently_tooltip: '%{time_read} total citit (%{recent_time_read} în ultimele 60 de zile)' last_reply_lowercase: Ultimul răspuns replies_lowercase: one: răspuns @@ -902,6 +991,7 @@ ro: private_message_info: title: "Mesaj" invite: "Invită alte persoane..." + leave_message: "Sigur părăsești acest mesaj?" remove_allowed_user: "Chiar dorești să îl elimini pe {{name}} din acest mesaj privat?" remove_allowed_group: "Ești sigur că vrei să îl scoți pe {{name}} din acest mesaj?" email: 'Email' @@ -926,6 +1016,9 @@ ro: complete_email_found: "Am găsit un cont care se potrivește cu adresa <b>%{email}</b>, vei primi un email cu instrucțiunile de resetare a parolei în cel mai scurt timp." complete_username_not_found: "Nici un cont nu se potriveşte cu utilizatorul <b>%{username}</b>" complete_email_not_found: "Nici un cont nu se potrivește cu adresa <b>%{email}</b>" + help: "Email-ul nu a sosit? Asigură-te că verifici mai întâi în folderul Spam. <p>Nu ești sigur ce adresă de email folosești? Scrie adresa de email și îți vom spune dacă aceasta există.</p><p>Dacă nu mai ai acces în contul tău la adresa de email, te rog contactează <a href='/about'>echipa de ajutor.</a></p>" + button_ok: "Ok" + button_help: "Ajutor" login: title: "Autentificare cu" username: "Utilizator" @@ -939,18 +1032,22 @@ ro: logging_in: "În curs de autentificare..." or: "sau" authenticating: "Se autentifică..." + awaiting_activation: "Contul tău așteaptă activarea, folosește linkul pentru recuperarea parolei pentru a primi un alt link de activare a adresei de email." awaiting_approval: "Contul tău nu a fost aprobat încă de un admin . Vei primi un email când se aprobă." requires_invite: "Ne pare rău, accesul la forum se face pe bază de invitație." not_activated: "Nu te poți loga încă. Am trimis anterior un email de activare pentru <b>{{sentTo}}</b>. Urmează instrucțiunile din email pentru a-ți activa contul." not_allowed_from_ip_address: "Nu te poți conecta cu această adresă IP." admin_not_allowed_from_ip_address: "Nu te poți conecta ca administrator cu această adresă IP." resend_activation_email: "Click aici pentru a retrimite emailul de activare." + resend_title: "Retrimite emailul de activare" change_email: "Schimbă adresa de email" + provide_new_email: "Furnizează o nouă adresă de email și îți vom retrimite linkul de confirmare." submit_new_email: "Actualizează adresa de email" sent_activation_email_again: "Am trimis un alt email de activare pentru tine la <b>{{currentEmail}}</b>. Poate dura câteva minute până ajunge; vezi și în secțiunea de spam a mailului." to_continue: "Te rog să te autentifici." preferences: "Trebuie să fii autentificat pentru a schimba preferințele." forgot: "Nu îmi amintesc detaliile contului meu." + not_approved: "Contul tău încă nu a fost aprobat. Vei primi notificarea prin email când ești gata de logare." google: title: "Google" message: "Autentificare cu Google (Verifică browserul să nu blocheze ferestrele pop-up)" @@ -972,12 +1069,27 @@ ro: github: title: "GitHub" message: "Autentificare cu GitHub (Verifică browserul să nu blocheze ferestrele pop-up)" + invites: + accept_title: "Initație" + welcome_to: "Bine ai venit la %{site_name}!" + invited_by: "Ai fost invitat de:" + social_login_available: "De asemenea te poți loga cu un alt cont care folosește această adresă de email." + your_email: "Adresa de email a contului este <b>%{email}</b>." + accept_invite: "Acceptă invitație" + success: "Contul tău a fost creat și acum ești logat." + name_label: "Nume" + password_label: "Setează parolă" + optional_description: "(opțional)" + password_reset: + continue: "Continuă la %{site_name}" emoji_set: apple_international: "Apple/International" google: "Google" twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Google clasic" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Numai categorii" categories_with_featured_topics: "Categorii cu subiecte promovate" @@ -986,12 +1098,28 @@ ro: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + select_kit: + default_header_text: Selectează... + no_content: Nu s-au găsit potriviri + filter_placeholder: Caută... + emoji_picker: + filter_placeholder: Caută emoji + people: Oameni + nature: Natură + food: Mâncare + activity: Activitate + travel: Călătorie + objects: Obiecte + celebration: Sărbători + custom: Emojii personalizate + recent: Folosite recent composer: emoji: "Emoji :)" more_emoji: "mai multe..." options: "Opțiuni" whisper: "discret" unlist: "nelistat" + blockquote_text: "Bloc citat" add_warning: "Aceasta este o avertizare oficială." toggle_whisper: "Comută modul discret" toggle_unlisted: "Comută modul nelistat" @@ -1001,6 +1129,7 @@ ro: saved_local_draft_tip: "salvat local" similar_topics: "Subiectul tău seamănă cu..." drafts_offline: "schiță offline" + group_mentioned_limit: "1Atenție!1 Ai menționat 2{{group}}2, dar acest grup are mai mulți membri decât limita de {{max}} utilizatori configurată de către administrator. Nimeni nu va fi notificat." group_mentioned: one: "Prin menționarea {{group}}, urmează să notifici <a href='{{group_link}}'>o persoană</a> – ești sigur?" few: "Prin menționarea {{group}}, urmează să notifici <a href='{{group_link}}'>{{count}} persoane</a> – ești sigur?" @@ -1023,6 +1152,7 @@ ro: cancel: "Anulează" create_topic: "Creează un subiect" create_pm: "Mesaj" + create_whisper: "Șoaptă" title: "Sau apasă Ctrl+Enter" users_placeholder: "Adaugă un utilizator" title_placeholder: "Despre ce e vorba în acest subiect - pe scurt?" @@ -1060,6 +1190,7 @@ ro: olist_title: "Listă numerotată" ulist_title: "Listă cu marcatori" list_item: "Element listă" + toggle_direction: "Schimbă direcția" help: "Ajutor pentru formatarea cu Markdown" modal_ok: "Ok" modal_cancel: "Anulare" @@ -1068,12 +1199,50 @@ ro: title: "Ai uitat să adaugi destinatari?" body: "În acest moment mesajul acesta nu este trimis decât către tine însuți!" admin_options_title: "Setări opționale pentru moderatori cu privire la acest subiect" + composer_actions: + reply_as_new_topic: + label: Răspunde cu link către subiect + desc: Creează un nou subiect + reply_as_private_message: + label: Mesaj nou + desc: Creează mesaj privat + reply_to_topic: + label: Răspunde la subiect + desc: Răspunde la postarea originală fără a răspunde la vreo postare specifică + toggle_whisper: + label: Comută modul discret notifications: + tooltip: + message: + one: "1 mesaj necitit" + few: "{{count}} mesaje necitite" + other: "{{count}} mesaje necitite" title: "notificări la menționările @numelui tău, răspunsuri la postările sau subiectele tale, mesaje, etc." none: "Nu pot încărca notificările în acest moment." empty: "Nu au fost găsite notificări." more: "vezi notificările mai vechi" total_flagged: "toate postările marcate" + mentioned: "<span>{{username}}</span> {{description}}" + group_mentioned: "<span>{{username}}</span> {{description}}" + quoted: "<span>{{username}}</span> {{description}}" + replied: "<span>{{username}}</span> {{description}}" + posted: "<span>{{username}}</span> {{description}}" + edited: "<span>{{username}}</span> {{description}}" + liked: "<span>{{username}}</span> {{description}}" + liked_2: "<span>{{username}}, {{username2}}</span> {{description}}" + liked_many: + one: "<span>{{username}}, {{username2}} și încă o persoană</span> {{description}}" + few: "<span>{{username}}, {{username2}} și încă o persoană</span> {{description}}" + other: "<span>{{username}}, {{username2}} și alți {{count}}</span> {{description}}" + private_message: "<span>{{username}}</span> {{description}}" + invited_to_private_message: "<p><span>{{username}}</span> {{description}}" + invited_to_topic: "<span>{{username}}</span> {{description}}" + invitee_accepted: "<span>{{username}}</span> ți-a acceptat invitația" + moved_post: "<span>{{username}}</span> a mutat {{description}}" + linked: "<span>{{username}}</span> {{description}}" + granted_badge: "A câștigat '{{description}}'" + topic_reminder: "<span>{{username}}</span> {{description}}" + watching_first_post: "<span>Subiect nou</span> {{description}}" alt: mentioned: "Menționat de" quoted: "Citat de" @@ -1081,6 +1250,8 @@ ro: posted: "Postat de" edited: "Editează postarea prin" liked: "V-a apreciat postarea" + private_message: "Mesaj personal de la" + invited_to_private_message: "Invitat la un mesaj privat de către" invited_to_topic: "Invitat la un subiect de către" invitee_accepted: "Invitație acceptată de către" moved_post: "Postarea ta a fost mutată de către" @@ -1108,10 +1279,12 @@ ro: uploading: "Se încarcă" select_file: "Alege fișier" image_link: "Link-ul de la imagine va duce la" + default_image_alt_text: imagine search: sort_by: "Sortează după" relevance: "Relevanță" latest_post: "Cele mai recente postări" + latest_topic: "Ultimul subiect" most_viewed: "Cele mai văzute" most_liked: "Cele mai apreciate" select_all: "Selectează tot" @@ -1122,6 +1295,11 @@ ro: no_more_results: "Nu s-au mai găsit alte rezultate." searching: "Se caută..." post_format: "#{{post_number}} de {{username}}" + results_page: "Caută rezultate pentru '{{term}}'" + start_new_topic: "Creează un nou subiect?" + or_search_google: "Sau încearcă să cauți cu Google:" + search_google_button: "Google" + search_google_title: "Caută în această pagină" context: user: "Caută postări după @{{username}}" category: "Caută în categoria #{{category}} " @@ -1131,19 +1309,27 @@ ro: title: Căutare avansată posted_by: label: Postat de + in_category: + label: Categorie in_group: label: În grupul with_badge: label: Cu ecusonul + with_tags: + label: Etichetat filters: likes: pe care le-am apreciat posted: în care am postat watching: pe care le urmăresc activ tracking: le urmăresc + private: În mesajele mele first: sunt chiar pe primul loc în listă pinned: sunt fixate unpinned: nu sunt fixate + seen: Citesc + unseen: Nu am citit wiki: sunt wiki + images: include poza (pozele) statuses: label: Unde subiectele open: sunt deschise @@ -1179,14 +1365,19 @@ ro: dismiss_new: "Anulează cele noi" toggle: "activează selecția multiplă a subiectelor" actions: "Acțiuni multiple" + change_category: "Alege categoria" close_topics: "Închide subiectele" archive_topics: "Arhivează subiectele" + notification_level: "Notificări" choose_new_category: "Alege o nouă categorie pentru acest subiect" selected: one: "Ai selectat <b>un</b> subiect." few: "Ai selectat <b>{{count}}</b> subiecte." other: "Ai selectat <b>{{count}}</b> de subiecte." + change_tags: "Înlocuiește etichete" + append_tags: "Adaugă etichete" choose_new_tags: "Alege etichete noi pentru aceste subiecte:" + choose_append_tags: "Alege etichete noi pentru aceste subiecte:" changed_tags: "Etichetele pentru aceste subiecte au fost schimbate." none: unread: "Nu sunt subiecte necitite." @@ -1281,7 +1472,48 @@ ro: jump_reply_up: sări la un răspuns mai vechi jump_reply_down: sări la un răspuns mai nou deleted: "Subiectul a fost șters" + topic_status_update: + title: "Temporizator subiect" + save: "Setează temporizator" + num_of_hours: "Număr de ore:" + remove: "Elimină temporizator" + publish_to: "Publică la:" + when: "Când:" + public_timer_types: Temporizatoare subiect + auto_update_input: + none: "Alege un interval de timp" + later_today: "Mai târziu azi" + tomorrow: "Mâine" + later_this_week: "Mai târziu săptămâna asta" + this_weekend: "Acest weekend" + next_week: "Săptămâna viitoare" + two_weeks: "Două săptămâni" + next_month: "Luna viitoare" + three_months: "Trei luni" + six_months: "6 luni" + one_year: "Un an" + forever: "Pentru totdeanua" + pick_date_and_time: "Alege data și ora" + set_based_on_last_post: "Închide pe baza ultimei postări" + publish_to_category: + title: "Programează publicarea" + temp_open: + title: "Deschide temporar" + auto_reopen: + title: "Deschide automat subiect" + temp_close: + title: "Închide temporar" + auto_close: + title: "Închide automat subiectul" + label: "Ore pentru închidere automată a subiectului:" + error: "Introduceţi o valoare validă." + based_on_last_post: "Nu închide până când ultima postare din subiect este măcar atât de veche." + auto_delete: + title: "Șterge automat subiect" + reminder: + title: "Amintește-mi" status_update_notice: + auto_open: "Acest subiect va fi deschis automat la %{timeLeft}." auto_close: "Acest subiect va fi automat închis în %{timeLeft}." auto_close_based_on_last_post: "Acest subiect va fi automat închis după %{duration} de la ultimul răspuns." auto_close_title: 'Setări de închidere automată' @@ -1299,6 +1531,8 @@ ro: go_bottom: "sfârșit" go: "mergi" jump_bottom: "sari la ultimul mesaj" + jump_prompt: "sari la..." + jump_prompt_of: "din %{count} postări" jump_prompt_long: "La ce postare dorești să sari?" jump_bottom_with_number: "sari la mesajul %{post_number}" total: toate postările @@ -1313,6 +1547,7 @@ ro: '3_2': 'Vei primi notificări deoarece urmărești activ acest subiect.' '3_1': 'Vei primi notificări deoarece ai creat acest subiect.' '3': 'Vei primi notificări deoarece urmărești activ acest subiect.' + '2_2': 'Vei vedea numărul răspunsurilor noi deoarece urmărești acest subiect.' '2': 'Vei vedea numărul răspunsurilor noi deoarece <a href="/u/{{username}}/preferences">citești acest subiect</a>' '1_2': 'Vei fi notificat dacă cineva îți menționează @numele sau îți scrie un răspuns.' '1': 'Vei fi notificat dacă cineva îți menționează @numele sau îți scrie un răspuns.' @@ -1357,12 +1592,16 @@ ro: visible: "Fă vizibil" reset_read: "Resetează datele despre subiecte citite" make_public: "Transformă în subiect public" + make_private: "Transformă în mesaj privat" feature: pin: "Fixează subiectul" unpin: "Anulează subiect fixat" pin_globally: "Promovează discuţia global" make_banner: "Adaugă statutul de banner" remove_banner: "Șterge statutul de banner" + reply: + title: 'Răspunde' + help: 'începe să scrii un răspuns la acest subiect' clear_pin: title: "Anulează subiect fixat" help: "Elimină subiectul fixat pentru a nu mai apărea la începutul listei de subiecte." @@ -1478,6 +1717,15 @@ ro: multi_select: select: 'selectează' selected: 'selectate ({{count}})' + select_post: + label: 'alege' + title: 'Adaugă postare în selecție' + selected_post: + label: 'selectat' + title: 'Apasă pentru a exclude postarea din selecție' + select_replies: + label: 'selectează +răspunsuri' + title: 'Adaugă postarea și toate răspunsurile la selecție' delete: șterge selecția cancel: anulare selecție select_all: selectează tot @@ -1492,6 +1740,7 @@ ro: post_number: "postarea {{number}}" last_edited_on: "postare editată ultima oară la" reply_as_new_topic: "Răspunde cu link către subiect" + reply_as_new_private_message: "Răspunde cu un mesaj nou aceluiași destinatar" continue_discussion: "În continuarea discuției de la {{postLink}}:" follow_quote: "mergi la postarea citată" show_full: "Arată postarea în întregime" @@ -1500,6 +1749,7 @@ ro: one: "(post retras de autor, va fi şters automat într-o oră, cu excepţia cazului în care mesajul este marcat)" few: "(postări retrase de autor, vor fi şterse automat în %{count} ore, cu excepţia cazului în care mesajele sunt marcate)" other: "(postări retrase de autor, vor fi şterse automat în %{count} de ore, cu excepţia cazului în care mesajele sunt marcate)" + collapse: "restrânge" expand_collapse: "extinde/restrânge" gap: one: "vezi un răspuns ascuns" @@ -1549,12 +1799,24 @@ ro: has_liked: "ai apreciat acest răspuns" undo_like: "anulează aprecierea" edit: "editează această postare" + edit_action: "Modifică" edit_anonymous: "Ne pare rău, dar trebuie să fii autentificat pentru a edita această postare." flag: "marchează această postare ca mesaj privat sau trimite o notificare privată despre ea către un admin/moderator" delete: "șterge această postare" undelete: "restaurează această postare" share: "distribuie un link către această postare" more: "Mai mult" + delete_replies: + confirm: "Dorești să ștergi și răspunsurile la această postare?" + direct_replies: + one: "Da, și {{count}} răspuns direct" + few: "Da, și {{count}} răspunsuri directe" + other: "Da, și {{count}} răspunsuri directe" + all_replies: + one: "Da, și 1 răspuns" + few: "Da, și toate {{count}} răspunsurile" + other: "Da, și toate {{count}} răspunsurile" + just_the_post: "Nu, doare această postare" admin: "acțiuni administrative pentru postare" wiki: "Fă postarea Wiki" unwiki: "Nu mai fă Wiki din postare" @@ -1563,6 +1825,9 @@ ro: rebake: "Reconstruieşte HTML" unhide: "Arată" change_owner: "Schimbă proprietarul" + grant_badge: "Acordă ecuson" + lock_post: "Blochează postarea" + unlock_post: "Deblochează postarea" actions: flag: 'Marchează cu marcaj de avertizare' undo: @@ -1580,6 +1845,10 @@ ro: notify_user: "a trimis un mesaj" bookmark: "a pus semn de carte la asta" like: "a apreciat asta" + like_capped: + one: "și {{count}} altul au apreciat asta" + few: "și {{count}} alții au apreciat asta" + other: "și {{count}} alții au apreciat asta" vote: "a votat la asta" by_you: off_topic: "Ai marcat asta cu mesaj de avertizare, ca fiind în afara subiectului" @@ -1656,6 +1925,16 @@ ro: one: "O persoană a votat pentru acest mesaj" few: "{{count}} persoane au votat pentru acest mesaj" other: "{{count}} de persoane au votat pentru acest mesaj" + delete: + confirm: + one: "Ești sigur că vrei să ștergi aca postare?" + few: "Ești sigur că vrei să ștergi acele {{count}} postări?" + other: "Ești sigur că vrei să ștergi acele {{count}} postări?" + merge: + confirm: + one: "Ești sigur că vrei să comasezi acele postări?" + few: "Ești sigur că vrei să comasezi acele {{count}} postări?" + other: "Ești sigur că vrei să comasezi acele {{count}} postări?" revisions: controls: first: "Prima revizie" @@ -1665,14 +1944,25 @@ ro: hide: "Ascunde revizia" show: "Afișează revizia" revert: "Restaurează această revizie" + edit_post: "Modifică postarea" comparing_previous_to_current_out_of_total: "<strong>{{previous}}</strong> <i class='fa fa-arrows-h'></i> <strong>{{current}}</strong> / {{total}}" displays: inline: title: "Arată output-ul randat, cu adăugiri și ștergeri intercalate" + button: 'HTML' side_by_side: title: "Arată una lângă alta diferențele din output-ul randat" + button: 'HTML' side_by_side_markdown: title: "Arată una lângă alta diferențele din sursa brută" + raw_email: + displays: + text_part: + title: "Arată partea de text a emailului" + button: 'Text' + html_part: + title: "Arată partea HTML a emailului" + button: 'HTML' category: can: 'can… ' none: '(nicio categorie)' @@ -1685,6 +1975,8 @@ ro: settings: 'Setări' topic_template: "Șablon subiect" tags: "Etichete" + tags_allowed_tags: "Permite doar folosirea acestor etichete în această categorie:" + tags_allowed_tag_groups: "Permite doar folosirea etichetelor din aceste grupuri în această categorie:" tags_placeholder: "(Opțional) lista etichetelor permise" tag_groups_placeholder: "(Opțional) lista grupurilor de etichete permise" topic_featured_link_allowed: "Permite link-uri promovate în această categorie." @@ -1721,6 +2013,8 @@ ro: email_in_disabled_click: 'activează opțiunea "primire email ".' suppress_from_homepage: "Elimină această categorie de pe pagina principală." all_topics_wiki: "Transformă subiectele noi în wiki-uri, implicit." + sort_order: "Sortează lista de subiecte după:" + default_view: "Listă implicită de subiecte:" allow_badges_label: "Permite acordarea de ecusoane în această categorie" edit_permissions: "Editează permisiuni" add_permission: "Adaugă permisiune" @@ -1758,6 +2052,9 @@ ro: created: "Creat" sort_ascending: 'Crescător' sort_descending: 'Descrescător' + subcategory_list_styles: + rows: "Rânduri" + rows_with_featured_topics: "Rânduri cu subiecte promovate" flagging: title: 'Îți mulțumim că ne ajuți să păstrăm o comunitate civilizată!' action: 'Marcare' @@ -2013,6 +2310,7 @@ ro: granted_on: "Acordat acum %{date}" others_count: "Numărul celorlalți care au acest ecuson (%{count})" title: Ecusoane + multiple_grant: "Poți primi asta de mai multe ori" badge_count: one: "%{count} ecuson" few: "%{count} ecusoane" @@ -2026,6 +2324,7 @@ ro: few: "%{count} acordate" other: "%{count} acordate" select_badge_for_title: Selectați un ecuson pentru a-l folosi ca titlu + none: "(nimic)" badge_grouping: getting_started: name: Cum începem @@ -2048,11 +2347,18 @@ ro: </p> tagging: all_tags: "Toate etichetele" + other_tags: "Alte etichete" selector_all_tags: "toate etichetele" selector_no_tags: "fără etichete" changed: "etichete schimbate:" tags: "Etichete" + choose_for_topic: "etichete opționale" delete_tag: "Șterge etichetă" + delete_confirm: + one: "Ești sigur că vrei să ștergi această etichetă și să o scoți dintr-un subiect care o folosește?" + few: "Ești sigur că vrei să ștergi această etichetă și să o scoți din {{count}} subiecte care o folosesc?" + other: "Ești sigur că vrei să ștergi această etichetă și să o scoți din {{count}} subiecte care o folosesc?" + delete_confirm_no_topics: "Ești sigur că vrei să ștergi această etichetă?" rename_tag: "Redenumire etichetă" rename_instructions: "Alege un nume nou pentru etichetă:" sort_by: "Sortat după:" @@ -2156,6 +2462,8 @@ ro: backups: "back-up" traffic_short: "Trafic" traffic: "Cereri web" + page_views: "Vizualizări" + page_views_short: "Vizualizări" show_traffic_report: "Arată raportul detaliat cu privire la trafic" reports: today: "Astăzi" @@ -2177,10 +2485,20 @@ ro: by: "de către" flags: title: "Marcaje de avertizare" + active_posts: "Postări marcate" + old_posts: "Postări marcate vechi" + topics: "Subiecte marcate" + moderation_history: "Istoric de moderare" agree: "De acord" agree_title: "Confirmă acest marcaj ca valid și corect" + agree_flag_hide_post: "Ascunde postarea" + agree_flag_restore_post_title: "Restabilește postarea pentru astfel încât toți utilizatorii să o poată vedea." + agree_flag_suspend: "Suspendă utilizator" + agree_flag: "Păstrează postarea" + ignore_flag: "Ignoră" delete: "Șterge" delete_title: "Șterge postarea la care se referă marcajul de avertizare." + delete_post_defer_flag: "Șterge postarea și ignoră marcarea" delete_post_defer_flag_title: "Șterge postarea; dacă este prima, șterge subiectul" delete_post_agree_flag_title: "Șterge postarea; dacă este prima, șterge subiectul" delete_flag_modal_title: "Șterge și..." @@ -2193,9 +2511,11 @@ ro: clear_topic_flags: "Terminat" clear_topic_flags_title: "Subiectul a fost analizat iar problema rezolvată. Fă click pe Terminat pentru a înlătura marcajele." more: "(mai multe răspunsuri...)" + suspend_user: "Suspendă utilizator" dispositions: agreed: "aprobate" disagreed: "respinse" + deferred: "ignorat" flagged_by: "Marcat de către" resolved_by: "Rezolvat de " took_action: "A luat măsuri" @@ -2203,9 +2523,21 @@ ro: error: "Ceva nu a funcționat" reply_message: "Răspunde" topic_flagged: "Aceast <strong>subiect</strong> a fost marcat cu marcaj de avertizare." + show_full: "arată întreaga postare" visit_topic: "Vizualizează subiectul pentru a acționa." was_edited: "Mesajul a fost editat după primul marcaj" previous_flags_count: "Acest mesaj a fost deja marcat de {{count}} (de) ori." + details: "detalii" + flagged_topics: + topic: "Subiect" + type: "Tip" + users: "Utilizatori" + short_names: + off_topic: "în afara subiectului" + inappropriate: "neadecvat" + spam: "spam" + notify_user: "personalizat" + notify_moderators: "personalizat" groups: primary: "Grup primar" no_primary: "(nu există grup primar)" @@ -2224,6 +2556,7 @@ ro: add_members: "Adaugă membri" custom: "Personalizat" bulk_complete: "Utilizatorii au fost adăugați în grup." + bulk_complete_users_not_added: "Acești utilizatori nu au fost adăugați (asigură-te că au cont):" bulk: "Adaugă în grup mai mulți utilizatori odată" bulk_paste: "Lipește o listă de utilizatori sau email-uri, câte unul pe linie:" bulk_select: "(selectează un grup)" @@ -2236,6 +2569,8 @@ ro: add_owners: Adaugă proprietari incoming_email: "Adresă de primire emailuri personalizată" incoming_email_placeholder: "introducere adresă de email" + none_selected: "Alege un grup pentru a începe" + no_custom_groups: "Creează un nou grup personalizat" api: generate_master: "Generează cheie API principală" none: "Nu sunt chei API principale active deocamdată." @@ -2267,6 +2602,7 @@ ro: warn_local_payload_url: "Se pare că dorești să setezi un webhook pentru un url local. Un eveniment livrat către o adresă locală ar putea avea efecte secundare și genera comportamente neașteptate. Continuă?" secret_invalid: "Secretul nu trebuie să aibă nici un caracter spațiu-gol." secret_too_short: "Secretul trebuie sa conțină cel puțin 12 caractere." + secret_placeholder: "Un șir de caractere opțional, folosit la generarea semnăturii" event_type_missing: "Va trebui să setezi cel puțin un tip de evenimente." content_type: "Tip conținut" secret: "Secret" @@ -2369,6 +2705,8 @@ ro: without_uploads: "Da (nu include fişierele)" download: label: "Descărcare" + title: "Trimite email cu link de descărcare" + alert: "Un link pentru descărcarea acestei copii de rezervă ți-a fost trimis pe email." destroy: title: "Șterge backup-ul" confirm: "Ești sigur că vrei să ștergi acest backup?" @@ -2400,15 +2738,21 @@ ro: title: "Personalizare" long_title: "Personalizările site-ului" preview: "previzualizare" + explain_preview: "Vizualizează site-ul cu această temă activată" save: "Salvare" new: "Nou" new_style: "Stil nou" import: "Importă" delete: "Șterge" + delete_confirm: "Șterge această temă" about: "Modifică foaia de stil CSS și header-ele HTML din site. Adaugă o personalizare pentru a începe." color: "Culoare" opacity: "Opacitate" copy: "Copiază" + copy_to_clipboard: "Copiază în clipboard" + copied_to_clipboard: "Copiat în clipboard" + copy_to_clipboard_error: "Eroare la copierea datelor în clipboard" + theme_owner: "Nemodificabil, deținut de:" email_templates: title: "Șabloane de emailuri" subject: "Subiect" @@ -2417,8 +2761,58 @@ ro: none_selected: "Selectează un șablon pentru a începe editarea" revert: "Revocă schimbările" revert_confirm: "Ești sigur că vrei să revoci schimbările?" + theme: + import_theme: "Importă temă" + customize_desc: "Personalizează:" + title: "Teme" + long_title: "Modifică culori, CSS și conținutul HTML al site-ului" + edit: "Modifică" + common: "Comun" + desktop: "Desktop" + mobile: "Mobil" + preview: "Previzualizează" + is_default: "Tema este activată implicit" + user_selectable: "Tema poate fi aleasă de către utilizatori" + color_scheme: "Schemă de culori" + color_scheme_select: "Alege culorile folosite de către temă" + custom_sections: "Secțiuni personalizate:" + theme_components: "Componentele temei" + uploads: "Urcări" + variable_name: "Numele variabilei SCSS:" + upload: "Urcă" + css_html: "CSS/HTML personalizat" + edit_css_html: "Modifică CSS/HTML" + edit_css_html_help: "Nu ai modificat CSS sau HTML" + import_file_tip: "Fișier .dcstyle.json care conține tema" + about_theme: "Despre temă" + license: "Licență" + component_of: "Tema este o componentă a:" + update_to_latest: "Actualizat la ultima" + check_for_updates: "Caută actualizări" + updating: "Actualizare în curs..." + up_to_date: "Tema este actuală, ultima verificare:" + add: "Adaugă" + scss: + text: "CSS" + header: + text: "Antet" + after_header: + text: "După antet" + footer: + text: "Subsol" + embedded_scss: + text: "CSS încorporat" + head_tag: + text: "1" + title: "HTML care va fi inserat înainte de 1 tag" + body_tag: + text: "1" colors: + select_base: + title: "Alege schema de culori de bază" + description: "Schemă de bază:" title: "Culori" + edit: "Modifică scheme de culori" long_title: "Scheme de culori" new_name: "Nouă schemă de culori" copy_name_prefix: "Copie a" @@ -2520,6 +2914,13 @@ ro: type_placeholder: "rezumat, înregistrare..." reply_key_placeholder: "cheie de răspuns" skipped_reason_placeholder: "motivul" + moderation_history: + no_results: "Niciun istoric de moderare nu e disponibil." + actions: + delete_user: "Utilizator șters" + suspend_user: "Utilizator suspendat" + delete_post: "Postare ștearsă" + delete_topic: "Subiect șters" logs: title: "Rapoarte" action: "Acțiune" @@ -2537,6 +2938,8 @@ ro: block: "blochează" do_nothing: "nu acționa" staff_actions: + all: "tot" + filter: "Filtru:" title: "Acțiunile membrilor echipei" clear_filters: "Arată tot" staff_user: "Utilizator-membru al echipei" @@ -2557,6 +2960,8 @@ ro: change_trust_level: "schimbă nivelul de încredere" change_username: "schimbă numele utilizatorului" change_site_setting: "schimbă setările site-ului" + change_theme: "schimbă temă" + delete_theme: "șterge temă" change_site_text: "schimbă textul site-ului" suspend_user: "suspendă utilizatorul" unsuspend_user: "reactivează utilizator" @@ -2575,9 +2980,20 @@ ro: revoke_admin: "Revocă titlul de Admin" grant_moderation: "Acordă titlul de Moderator" revoke_moderation: "Revocă titlul de Moderator" + backup_create: "creaază copie de rezervă" deleted_tag: "etichetă ștearsă" renamed_tag: "etichetă redenumită" revoke_email: "revoca email" + lock_trust_level: "blochează nivel de încredere" + unlock_trust_level: "deblochează nivel de încredere" + activate_user: "activează utilizator" + deactivate_user: "dezactivează utilizator" + backup_download: "descarcă copie de rezervă" + backup_destroy: "distruge copie de rezervă" + reviewed_post: "postare revizuită" + post_locked: "postare blocată" + post_unlocked: "postare deblocată" + check_personal_message: "verifică mesaj personal" screened_emails: title: "Email-uri filtrate" description: "Când cineva încearcă să creeze un cont nou, următorul email va fi verificat iar înregistrarea va fi blocată, sau o altă acțiune va fi inițiată." @@ -2608,8 +3024,28 @@ ro: roll_up: text: "Consolidează" title: "Crează noi înregistrari cu subrețele blocate dacă există cel puțin 'min_ban_entries_for_roll_up' înregistrări." + search_logs: + term: "Termen" + searches: "Căutări" + unique: "Unic" + types: + all_search_types: "Toate tipurile de căutare" + header: "Antet" + full_page: "Întreaga pagină" logster: title: "Raport de erori" + watched_words: + search: "caută" + show_words: "arată cuvinte" + actions: + block: 'Blochează' + censor: 'Cenzurează' + require_approval: 'Necesită aprobare' + form: + label: 'Cuvânt nou:' + add: 'Adaugă' + success: 'Succes' + upload: "Urcă" impersonate: title: "Joacă rolul utilizatorului" help: "Folosește această unealtă pentru a imita un cont de utilizator pentru depanare. Va trebui să te deconectezi după ce termini." @@ -2671,8 +3107,18 @@ ro: suspend_duration: "Pentru cât timp va fi suspendat utilizatorul?" suspend_reason_label: "Motivul suspendării? Acest text <b>va fi vizibil</b> pe pagina de profil a utilizatorului, și va fi arătat utilizatorului atunci când încearcă să se autentifice. Încearcă să fii succint." suspend_reason: "Motiv" + suspend_message: "Mesaj email" suspended_by: "Suspendat de" + silence_reason: "Motiv" + silence_message: "Mesaj email" + silence_message_placeholder: "(lăsați necompletat pentru a trimite mesajul implicit)" + suspended_until: "(până la %{until})" + cant_suspend: "Utilizatorul nu poate fi suspendat." delete_all_posts: "Șterge toate postările" + penalty_post_actions: "Ce dorești să faci cu postarea asociată?" + penalty_post_delete: "Șterge postarea" + penalty_post_edit: "Modifică postarea" + penalty_post_none: "Nu face nimic" delete_all_posts_confirm_MF: "Ești pe cale să ștergi {POSTS, plural, one {1 postare} other {# postări}} și {TOPICS, plural, one {1 subiect} other {# subiecte}}. Ești sigur?" moderator: "Moderator?" admin: "Admin?" @@ -2688,6 +3134,7 @@ ro: logged_out: "Acest utilizator a ieșit de pe toate dispozitivele" revoke_admin: 'Revocă titlu de admin' grant_admin: 'Acordă titlu de admin' + grant_admin_confirm: "V-am trimis un email pentru a verifica noul administrator. Vă rog să îl deschideți și să urmați instrucțiunile din el." revoke_moderation: 'Revocă titlu de moderator' grant_moderation: 'Acordă titlu de moderator' unsuspend: 'Reactivează' @@ -2865,6 +3312,7 @@ ro: developer: 'Dezvoltator' embedding: "Embedding" legal: "Legal" + api: 'API' user_api: 'API Utilizator' uncategorized: 'Altele' backups: "Back-up" @@ -2873,6 +3321,7 @@ ro: user_preferences: "Preferințe" tags: "Etichete" search: "Căutare" + groups: "Grupuri" badges: title: Ecusoane new_badge: Ecuson nou @@ -2955,6 +3404,7 @@ ro: sample: "Folosește următorul cod HTML în site-ul tău pentru a crea și pentru a încorpora subiecte Discourse. Înlocuiește <b>ÎNLOCUIEȘTE_MĂ</b> cu URL-ul canonic al paginii pe care dorești să o încorporezi." title: "Embedding" host: "Host-uri permise" + class_name: "Numele clasei" path_whitelist: "Cale permise" edit: "editează" category: "Postează în categoria" @@ -2999,6 +3449,10 @@ ro: upload: "Încărcare" uploading: "Se încarcă..." quit: "Poate mai târziu" + staff_count: + one: "Comunitatea ta are 1 membru (tu)." + few: "Comunitatea ta are %{count} membri cu tot cu tine." + other: "Comunitatea ta are %{count} membri cu tot cu tine." invites: add_user: "adaugă" none_added: "Nu ați invitat nici un membru al echipei. Ești sigur că vrei să continui?" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 211f98c3bb8..5309896b4bb 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -54,6 +54,11 @@ ru: few: "%{count}с" many: "%{count}с" other: "%{count}с" + less_than_x_minutes: + one: "< 1мин" + few: "~ 1m" + many: "> 1m" + other: "< %{count}мин" x_minutes: one: "1мин" few: "%{count}мин" @@ -1100,6 +1105,8 @@ ru: alt: 'Alt' select_kit: default_header_text: Выбрать... + no_content: Совпадений не найдено + filter_placeholder: Поиск... emoji_picker: filter_placeholder: Искать emoji people: People @@ -1197,6 +1204,17 @@ ru: title: "Забыли указать получателей?" body: "В списке получателей сейчас только вы сами!" admin_options_title: "Дополнительные настройки темы для персонала" + composer_actions: + reply_as_new_topic: + label: Ответить в новой связанной теме + desc: Создать новую тему + reply_as_private_message: + label: Новое сообщение + desc: Написать личное сообщение + reply_to_topic: + label: Ответить на тему + create_topic: + label: "Новая тема" notifications: title: "уведомления об упоминании @псевдонима, ответах на ваши посты и темы, сообщения и т.д." none: "Уведомления не могут быть загружены." @@ -1204,6 +1222,12 @@ ru: more: "посмотреть более ранние уведомления" total_flagged: "всего сообщений с жалобами" granted_badge: "Заслужил(а) '{{description}}'" + watching_first_post: "<span>Новая тема</span> {{description}}" + group_message_summary: + one: "{{count}} сообщение в вашей группе: {{group_name}} " + few: "{{count}} сообщений в вашей группе: {{group_name}} " + many: "{{count}} сообщений в вашей группе: {{group_name}} " + other: "{{count}} сообщений в вашей группе: {{group_name}} " alt: mentioned: "Упомянуто" quoted: "Процитировано пользователем" @@ -1211,6 +1235,7 @@ ru: posted: "Опубликовано" edited: "Изменил ваше сообщение" liked: "Понравилось ваше сообщение" + private_message: "Личное сообщение от" invited_to_topic: "Приглашение в тему от" invitee_accepted: "Приглашение принято" moved_post: "Ваша тема перенесена участником " @@ -1257,6 +1282,8 @@ ru: more_results: "Найдено множество результатов. Пожалуйста, уточните, критерии поиска." cant_find: "Не можете найти нужную информацию?" start_new_topic: "Создать новую тему?" + search_google_button: "Google" + search_google_title: "Искать на этом сайте" context: user: "Искать сообщения от @{{username}}" category: "Искать в разделе #{{category}}" @@ -1266,6 +1293,8 @@ ru: title: Расширенный поиск posted_by: label: Автор + in_category: + label: Категоризированный in_group: label: Группа with_badge: @@ -1275,6 +1304,7 @@ ru: posted: В которых я писал watching: За которыми я наблюдаю tracking: За которыми я слежу + private: В моих сообщениях first: Только первые сообщения в темах pinned: Закреплены unpinned: Не закреплены @@ -1435,6 +1465,7 @@ ru: this_weekend: "В эти выходные" next_week: "На следующей неделе" next_month: "В следующем месяце" + forever: "Навсегда" pick_date_and_time: "Выбрать дату и время" temp_open: title: "Открыть на время" @@ -1444,6 +1475,7 @@ ru: title: "Закрыть на время" auto_close: title: "Автоматическое закрытие темы" + label: "Закрыть тему через:" error: "Пожалуйста, введите корректное значение." auto_delete: title: "Автоматическое удаление темы" @@ -1760,6 +1792,7 @@ ru: has_liked: "Вам понравилось это сообщение" undo_like: "больше не нравится" edit: "Изменить сообщение" + edit_action: "Изменить" edit_anonymous: "Войдите, чтобы отредактировать это сообщение." flag: "пожаловаться на сообщение" delete: "удалить сообщение" @@ -2009,6 +2042,8 @@ ru: subcategory_list_styles: rows: "Строки" rows_with_featured_topics: "Строки с обсуждаемыми темами" + boxes: "Блоки" + boxes_with_featured_topics: "Блоки с обсуждаемыми темами" flagging: title: 'Спасибо за вашу помощь в поддержании порядка!' action: 'Пожаловаться на сообщение' @@ -2249,10 +2284,14 @@ ru: hamburger_menu: '<b>=</b> Открыть меню гамбургер' user_profile_menu: '<b>p</b> Открыть меню профиля' show_incoming_updated_topics: '<b>.</b> Показать обновленные темы' + search: '<b>/</b> или <b>ctrl</b>+<b>alt</b>+<b>f</b> Поиск' help: '<b>?</b> Показать сочетания клавиш' dismiss_new_posts: '<b>x</b>, <b>r</b> Отложить новые сообщения' dismiss_topics: '<b>x</b>, <b>t</b> Отложить темы' log_out: '<b>shift</b>+<b>z</b> <b>shift</b>+<b>z</b> Выйти' + composing: + title: 'Редактирование' + return: '<b>shift</b>+<b>c</b> Вернуться в редактор' actions: title: 'Темы' bookmark_topic: ' <b>f</b> Добавить / удалить из заклодок' @@ -2300,6 +2339,8 @@ ru: many: "выдано %{count}" other: "выдано %{count}" select_badge_for_title: Использовать награду в качестве Вашего титула + none: "(нет)" + successfully_granted: "Награда %{badge} успешно присвоена %{username}" badge_grouping: getting_started: name: Начало работы @@ -2322,11 +2363,19 @@ ru: </p> tagging: all_tags: "Все теги" + other_tags: "Изменить тэги" selector_all_tags: "Все теги" selector_no_tags: "Нет тегов" changed: "Теги изменены:" tags: "Теги" + choose_for_topic: "Выберите теги для этой темы (опционально)" delete_tag: "Удалить тег" + delete_confirm: + one: "Вы действительно хотите удалить этот тэг и удалить его из 1 топика, которому он присвоен?" + few: "Вы действительно хотите удалить этот тэг и удалить его из {{count}} топиков, которым он присвоен?" + many: "Вы действительно хотите удалить этот тэг и удалить его из {{count}} топиков, которым он присвоен?" + other: "Вы действительно хотите удалить этот тэг и удалить его из {{count}} топиков, которым он присвоен?" + delete_confirm_no_topics: "Вы действительно хотите удалить этот тэг?" rename_tag: "Редактировать тег" rename_instructions: "Выберите новое название тега:" sort_by: "Сортировка:" @@ -2421,6 +2470,7 @@ ru: no_problems: "Проблем не обнаружено." moderators: 'Модераторы:' admins: 'Администраторы:' + silenced: 'Отключенные:' suspended: 'Заморожен:' private_messages_short: "Сообщ." private_messages_title: "Сообщений" @@ -2453,12 +2503,29 @@ ru: by: "от" flags: title: "Жалобы" + active_posts: "Сообщения с жалобами" + old_posts: "Старые сообщения с жалобами" + topics: "Темы с жалобами" moderation_history: "История модерации" agree: "Принять" agree_title: "Подтвердить корректность жалобы" + agree_flag_hide_post: "Скрыть запись" + agree_flag_hide_post_title: "Скрыть это сообщение и автоматически отправить пользователю личное сообщение с просьбой отредактировать его" + agree_flag_restore_post: "Согласиться (восстановить сообщение)" + agree_flag_restore_post_title: "Восстановить сообщение, чтобы все пользователи могли его видеть." + agree_flag_suspend: "Заморозить Пользователя" + agree_flag_suspend_title: "Согласиться с флагом и заблокировать пользователя." + agree_flag_silence: "Отключить пользователя" + agree_flag_silence_title: "Согласиться с флагом и отключить пользователя." + agree_flag: "Оставить сообщение нетронутым." + agree_flag_title: "Согласиться с флагом и оставить сообщение нетронутым." + ignore_flag: "Игнорировать" + ignore_flag_title: "Удалить этот флаг; в это время он не требует никаких действий." delete: "Удалить" delete_title: "Удалить обжалованное сообщение." + delete_post_defer_flag: "Удалить сообщение и отклонить флаг" delete_post_defer_flag_title: "Удалить сообщение; если это первое сообщение, удалить тему целиком" + delete_post_agree_flag: "Удалить сообщение и согласиться с флагом" delete_post_agree_flag_title: "Удалить сообщение; если это первое сообщение, удалить тему целиком" delete_flag_modal_title: "Удалить и..." delete_spammer: "Удалить спамера" @@ -2470,19 +2537,37 @@ ru: clear_topic_flags: "Готово" clear_topic_flags_title: "Тема была просмотрена, и все проблемы были решены. Нажмите Готово, чтобы удалить все жалобы." more: "(ещё ответы...)" + suspend_user: "Заблокировать пользователя" + suspend_user_title: "Заблокировать пользователя за это сообщение" dispositions: agreed: "принято" disagreed: "отклонено" + deferred: "пропущено" flagged_by: "Отмечено" resolved_by: "Разрешено" took_action: "Принята мера" system: "Системные" error: "что-то пошло не так" reply_message: "Ответить" + no_results: "Сообщений с жалобами нет." topic_flagged: "Эта <strong>тема</strong> была помечена." + show_full: "показать сообщение полностью" visit_topic: "Посетите тему чтобы принять меры" was_edited: "Сообщение было отредактировано после первой жалобы" previous_flags_count: "На это сообщение уже пожаловались {{count}} раз(а)." + show_details: "Показать детали жалобы" + details: "детали" + flagged_topics: + topic: "Тема" + type: "Набирать текст" + users: "Пользователи" + last_flagged: "Последняя жалоба" + short_names: + off_topic: "Не по теме" + inappropriate: "неприемлемый" + spam: "спам" + notify_user: "персональный" + notify_moderators: "персональный" groups: primary: "Основная группа" no_primary: "(нет основной группы)" @@ -2570,6 +2655,7 @@ ru: details: "Происходит, когда сообщение создается, редактируется, удаляется или восстанавливается." user_event: name: "Событие пользователя" + details: "Когда пользователь входит, выходит, создается, подтверждается или изменяется." delivery_status: title: "Статус передачи" inactive: "Неактивна" @@ -2638,6 +2724,7 @@ ru: label: "Загрузить" title: "Загрузить копию на сервер" uploading: "Загрузка..." + success: "'{{filename}}' успешно загружен. Файл теперь обрабатывается и займет минуту, чтобы его строки отразились в списке." error: "При загрузке '{{filename}}' произошла ошибка: {{message}}" operations: is_running: "Операция в данный момент исполняется..." @@ -2728,8 +2815,10 @@ ru: theme_components: "Компоненты стиля" uploads: "Загрузки" no_uploads: "Вы можете загружать различные ресурсы для вашего стиля, такие как шрифты и изображения" + add_upload: "Добавить вложение" upload_file_tip: "Выберите файл для загрузки (png, woff2 и т.д.)" variable_name: "Имя переменной SCSS:" + upload: "Загрузить" child_themes_check: "Стиль включает дочерние стили" css_html: "Настройка CSS/HTML" edit_css_html: "Редактировать CSS/HTML" @@ -2745,6 +2834,11 @@ ru: updating: "Обновляю..." up_to_date: "Стиль текущей версии, последняя проверка:" add: "Добавить" + commits_behind: + one: "Тема находится на 1 коммит сзади!" + few: "Тема находится на {{count}} коммитов сзади!" + many: "Тема находится на {{count}} коммитов сзади!" + other: "Тема находится на {{count}} коммитов сзади!" scss: text: "CSS" title: "Введите CSS; допускаются все стили CSS и SCSS" @@ -2874,6 +2968,15 @@ ru: type_placeholder: "дайджест, подписка..." reply_key_placeholder: "Ключ ответа" skipped_reason_placeholder: "Причина" + moderation_history: + performed_by: "Выполнено пользователем " + no_results: "История модерации недоступна." + actions: + delete_user: "Удалить Пользователя" + suspend_user: "Пользователь приостановлен" + silence_user: "Пользователь отключен" + delete_post: "Комментарий удален" + delete_topic: "Тема удалена" logs: title: "Логи" action: "Действие" @@ -2929,6 +3032,8 @@ ru: change_category_settings: "изменена настройка раздела" delete_category: "удален раздел" create_category: "создан раздел" + silence_user: "заблокировать пользователя" + unsilence_user: "разблокировать пользователя" grant_admin: "выданы права администратора" revoke_admin: "отозваны права администратора" grant_moderation: "выданы права модератора" @@ -2944,6 +3049,11 @@ ru: change_readonly_mode: "изменение режима \"только для чтения\"" backup_download: "скачать резервную копию" backup_destroy: "удалить резервную копию" + reviewed_post: "просмотренное сообщение" + custom_staff: "действия в плагинах" + post_locked: "сообщение заблокировано" + post_unlocked: "сообщение разблокировано" + check_personal_message: "проверить личное сообщение" screened_emails: title: "Почтовые адреса" description: "Когда кто-то создаёт новую учётную запись, проверяется данный почтовый адрес и регистрация блокируется или производятся другие дополнительные действия." @@ -2975,17 +3085,48 @@ ru: text: "Группировка" title: "Создание новой записи бана целой подсети если уже имеется хотя бы 'min_ban_entries_for_roll_up' записей отдельных IP адресов." search_logs: + title: "Логи поиска" + term: "Правило" + searches: "Поиски" + click_through: "Пролистать" + unique: "Уникальный" + unique_title: "уникальные пользователи, выполняющие поиск" types: all_search_types: "Все типы поиска" header: "Шапка" + full_page: "Полная страница" + click_through_only: "Все (только листинг)" + header_search_results: "Результаты поиска по заголовкам" logster: title: "Журнал ошибок" watched_words: + title: "Отслеживаемые слова" + search: "поиск" + clear_filter: "Очистить" + show_words: "показать слова" + word_count: + one: "1 слово" + few: "%{count}слова" + many: "%{count}слов" + other: "%{count}слов" actions: + block: 'Заблокировать' + censor: 'Цензура' + require_approval: 'Требующие одобрения' flag: 'Жалоба' + action_descriptions: + block: 'Запретить публикацию сообщений, содержащих эти слова. Пользователь увидит сообщение об ошибке при попытке отправить свое сообщение.' + censor: 'Разрешить сообщения, содержащие эти слова, но заменять их символами, которые скрывают цензурные выражения.' + require_approval: 'Комментарии, содержащие эти слова, будут требовать одобрения персонала, прежде чем их можно будет увидеть.' + flag: 'Разрешить сообщения, содержащие эти слова, но помечать их как неприемлемые, чтобы модераторы могли их оценивать.' form: + label: 'Новое слово:' + placeholder: 'слово целиком, звездочка (*) используется как знак подстановки ' placeholder_regexp: "Регулярное выражение" add: 'Добавить' + success: 'Успех' + upload: "Загрузить" + upload_successful: "Загрузка прошла успешна. Слова добавлены." impersonate: title: "Войти от имени пользователя" help: "Используйте этот инструмент, чтобы войти от имени пользователя. Может быть полезно для отладки. После этого необходимо выйти и зайти под своей учетной записью снова." @@ -3005,6 +3146,7 @@ ru: pending: "Ожидает одобрения" staff: 'Персонал' suspended: 'Замороженные' + silenced: 'Отключенный' suspect: 'Подозрительные' approved: "Подтвердить?" approved_selected: @@ -3029,6 +3171,7 @@ ru: staff: "Персонал" admins: 'Администраторы' moderators: 'Модераторы' + silenced: 'Отключенные пользователи' suspended: 'Замороженные пользователи' suspect: 'Подозрительные пользователи' reject_successful: @@ -3050,11 +3193,31 @@ ru: unsuspend_failed: "Ошибка разморозки пользователя {{error}}" suspend_duration: "На сколько времени заморозить пользователя?" suspend_reason_label: "Причина заморозки? Данный текст <b>будет виден всем</b> на странице профиля пользователя и будет отображаться, когда пользователь пытается войти. Введите краткое описание." + suspend_reason_hidden_label: "Почему вы приостанавливаете пользователя? Этот текст будет показан пользователю, когда он попытается войти в систему. Будьте краткими." suspend_reason: "Причина" + suspend_reason_placeholder: "Причина приостановки" + suspend_message: "Сообщение почты" + suspend_message_placeholder: "При желании, предоставьте дополнительную информацию о приостановке, и она будет отправлена пользователю по электронной почте." suspended_by: "Заморожен (кем)" + silence_reason: "Причина" + silenced_by: "Отключен пользователем " + silence_modal_title: "Отключенный пользователь" + silence_duration: "Как долго пользователь будет отключен?" + silence_reason_label: "Почему вы собираетесь отключить пользователя?" + silence_reason_placeholder: "Причина отключения" + silence_message: "Сообщение почты" + silence_message_placeholder: "(оставьте незаполненным, чтобы отправить дефолтное сообщение)" + suspended_until: "(пока не будет %{until})" cant_suspend: "Этого пользователя нельзя заморозить." delete_all_posts: "Удалить все сообщения" + penalty_post_actions: "Что нужно сделать со связанным сообщением?" + penalty_post_delete: "Удалить сообщение" + penalty_post_edit: "Редактировать сообщение" + penalty_post_none: "Ничего не делать" delete_all_posts_confirm_MF: "Вы собираетесь удалить {POSTS, plural, one {1 сообщение} other {# сообщений}} и {TOPICS, plural, one {1 тему} other {# тем}}. Вы уверены?" + silence: "Отключить" + unsilence: "Подключить" + silenced: "Отключен?" moderator: "Модератор?" admin: "Администратор?" suspended: "Заморожен?" @@ -3075,6 +3238,9 @@ ru: grant_moderation: 'Выдать права Модератора' unsuspend: 'Разморозить' suspend: 'Заморозить' + show_flags_received: "Показать полученные флаги" + flags_received_by: "Флаги, полученные %{username}" + flags_received_none: "Этот пользователь не получил никаких флагов." reputation: Репутация permissions: Права activity: Активность @@ -3127,12 +3293,18 @@ ru: activate_failed: "Во время активации пользователя произошла ошибка." deactivate_account: "Деактивировать" deactivate_failed: "Во время деактивации пользователя произошла ошибка." + unsilence_failed: 'При подключении этого пользователя произошла ошибка.' + silence_failed: 'При отключении этого пользователя произошла ошибка.' + silence_confirm: 'Вы уверены, что хотите отключить этого пользователя? Он не сможет создавать новые темы или сообщения.' + silence_accept: 'Да, отключить этого пользователя' bounce_score: "Возвратов Писем" reset_bounce_score: label: "Сбросить" title: "сбросить карму к 0" + visit_profile: "Посетите<a href='%{url}'>страницу настроек этого пользователя </a>, чтобы отредактировать его профиль" deactivate_explanation: "Дезактивированные пользователи должны заново подтвердить свой e-mail." suspended_explanation: "Замороженный пользователь не может войти (авторизоваться)." + silence_explanation: "Отключенный пользователь не может публиковать или отвечать на темы." staged_explanation: "Имитированный пользователь может отправлять сообщения только по эл.почте в определённые темы." bounce_score_explanation: none: "Нет возвратов полученных недавно от этой эл.почты." @@ -3345,6 +3517,7 @@ ru: sample: "Используйте следующий HTML-код на своём сайте, для возможности создания связанных тем. Замените <b>REPLACE_ME</b> канонической ссылкой страницы, куда производится встраивание." title: "Встраивание" host: "Разрешённые Хосты" + class_name: "Имя класса" path_whitelist: "Разрешённый Путь" edit: "изменить" category: "Опубликовать в разделе" @@ -3364,6 +3537,7 @@ ru: embed_classname_whitelist: "Разрешённые CSS-классы" feed_polling_enabled: "Импорт сообщений через RSS/ATOM" feed_polling_url: "Ссылка на RSS/ATOM" + feed_polling_frequency_mins: "Частота опроса ленты (в минутах)" save: "Сохранить настройки встраивания" permalink: title: "Постоянные ссылки" diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 873a2d0353f..824557fc181 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -67,6 +67,10 @@ sk: one: "1d" few: "%{count}d" other: "%{count}d" + x_months: + one: "%{count} mesiac" + few: "%{count} mesiace" + other: "%{count} mesiace" about_x_years: one: "1r" few: "%{count}r" @@ -379,11 +383,23 @@ sk: title: 'Upraviť skupinu' full_name: 'Celé meno' add_members: "Pridať používateľov" + public_admission: "Povoliť používateľom slobodne sa stať člen skupiny (kupina musí byť verejne viditeľná)" + public_exit: "Povoliť používateľom slobodne opustiť skupinu." + empty: + posts: "Nie sú tu žiadne príspevky od členov tejto skupiny" + members: "V tejto skupine niesú žiadny členovia" + messages: "Pre túto skupinu niesú žiadne správy" add: "Pridať" join: "Pridať sa do skupiny" leave: "Opustiť skupinu" request: "Požiadať o pridanie do skupiny" + message: "Správa" + automatic_group: Automatická skúpina closed_group: Uzavretá skupina + is_group_user: "Si členom tejto skupiny" + membership_request: + submit: "Odošli požiadavku" + title: "Požiadavka o členstvo v %{group_name}" membership: "Členstvo" name: "Meno" user_count: "Počet členov" @@ -403,6 +419,9 @@ sk: posts: "Príspevky" mentions: "Zmienky" messages: "Správy" + visibility_levels: + title: "Kto môže vidieť túto skupinu?" + public: "Každý" alias_levels: nobody: "Nikto" only_admins: "Iba administrátori" @@ -539,6 +558,7 @@ sk: Toto nastavenie nahradí Zhrnutie aktivíť.<br /> Stíšené témy a kategórie nie sú v týchto emailoch zahrnuté. individual: "Pošli email pri každom novom príspevku" + individual_no_echo: "Pošli email pri každom novom príspevku okrem vlastných." many_per_day: "Pošli mi email pri každom novom príspevku (cca {{dailyEmailEstimate}} denne)" few_per_day: "Pošli mi email pri každom novom príspevku (cca 2 denne)" tag_settings: "Štítky" @@ -569,10 +589,12 @@ sk: muted_users_instructions: "Pozastaviť všetky notifikácie od týchto používateľov." muted_topics_link: "Zobraziť stíšené témy" watched_topics_link: "Zobraziť sledované témy" + tracked_topics_link: "Zobraziť sledované témy" apps: "Appky" revoke_access: "Odvolať prístup" undo_revoke_access: "Zrušiť odvolanie prístupu" api_approved: "Schválený:" + theme: "Téma" staff_counters: flags_given: "nápomocné značky" flagged_posts: "označkované príspevky" @@ -590,6 +612,13 @@ sk: move_to_archive: "Archív" failed_to_move: "Zlyhalo presunutie označených správ (možno je chyba vo vašom pripojení)" select_all: "Označ všetky" + preferences_nav: + account: "Účet" + profile: "Profil" + emails: "Emaily" + notifications: "Upozornenia" + categories: "Kategórie" + apps: "Aplikácie" change_password: success: "(email odoslaný)" in_progress: "(email sa odosiela)" @@ -649,6 +678,7 @@ sk: short_instructions: "Ostatní vás môžu zmieniť ako @{{username}}" available: "Vaše používateľské meno je voľné" not_available: "Nie je k dispozícii. Skúste {{suggestion}}?" + not_available_no_suggestion: "Nedostupné" too_short: "Vaše používateľské meno je prikrátke" too_long: "Vaše používateľské meno je pridlhé" checking: "Kontrolujeme dostupnosť používateľského meno" @@ -734,6 +764,8 @@ sk: expired: "Táto pozvánka vypršala." rescind: "Odstrániť" rescinded: "Pozvánka odstránená" + rescind_all: "Odstrániť všetky pozvánky" + rescinded_all: "Všetky pozvánky odstránené!" reinvite: "Poslať pozvánku znovu" reinvite_all: "Poslať všetky pozvánky znovu" reinvited: "Poslať pozvánku znovu" @@ -901,6 +933,7 @@ sk: complete_email_found: "Našli sme účet priradený k <b>%{email}</b>, čoskoro dostanete e-mail s pokynmi, ako si obnoviť svoje heslo." complete_username_not_found: "Žiadny účet nemá priradené používateľské meno <b>%{username}</b>" complete_email_not_found: "Žiadny účet nie je s e-mailom <b>%{email}</b>" + button_help: "Pomoc" login: title: "Prihlásenie" username: "Používateľ" @@ -920,6 +953,8 @@ sk: not_allowed_from_ip_address: "Nie je možné prihlásenie z tejto IP adresy." admin_not_allowed_from_ip_address: "Nie je možné prihlásenie ako admin z tejto IP adresy." resend_activation_email: "Kliknite sem pre opätovné odoslanie aktivačného emailu." + change_email: "Zmeniť emailovú adresu" + submit_new_email: "Aktualizovať emailovú adresu" sent_activation_email_again: "Odoslali sme vám ďalší aktivačný email na <b>{{currentEmail}}</b>. Môže trvať niekoľko minút kým príde; pre istotu si skontrolujte spamový priečinok." to_continue: "Prosím, prihláste sa" preferences: "Na zmenu používateľských nastavení musíte byť prihlásený." @@ -949,6 +984,8 @@ sk: accept_title: "Pozvánka" accept_invite: "Prijať pozvánku" success: "Váš účet bol vytvorený a ste prihlásený." + name_label: "Meno" + optional_description: "(nepovinné)" emoji_set: apple_international: "Apple/Medzinárodné" google: "Google" @@ -962,6 +999,12 @@ sk: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + select_kit: + filter_placeholder: Hľadať + emoji_picker: + people: Ľudia + food: Jedlo + travel: Cestovanie composer: emoji: "Emoji :)" more_emoji: "viac ..." @@ -1034,6 +1077,9 @@ sk: modal_cancel: "Zrušiť" cant_send_pm: "Ľutujeme, nemôžete poslať správu používateľovi %{username}." admin_options_title: "Nepovinné zamestnanecké nastavenia pre túto tému" + composer_actions: + reply_as_private_message: + label: Nová správa notifications: title: "oznámenia o zmienkach pomocou @meno, odpovede na Vaše príspevky a témy, správy, atď." none: "Notifikácie sa nepodarilo načítať" @@ -1232,6 +1278,15 @@ sk: jump_reply_up: prejsť na predchádzajúcu odpoveď jump_reply_down: prejsť na nasledujúcu odpoveď deleted: "Téma bola vymazaná" + auto_update_input: + tomorrow: "Zajtra" + this_weekend: "Tento víkend" + next_week: "Budúci týždeň" + two_weeks: "Dva týždne" + next_month: "Budúci mesiac" + three_months: "Tri mesiace" + six_months: "Šesť mesiacov" + one_year: "Jeden rok" auto_close_title: 'Nastavenia automatického zatvárania' auto_close_immediate: one: "Posledný príspevok k téme je starý už 1 hodinu, takže téma bude okamžite uzavretá. " @@ -1245,6 +1300,7 @@ sk: go_bottom: "na spodok" go: "Choď" jump_bottom: "choď na posledný príspevok" + jump_prompt: "choď na..." jump_bottom_with_number: "choď na príspevok číslo %{post_number}" total: Všetkých príspevkov current: tento príspevok @@ -2087,6 +2143,18 @@ sk: visit_topic: "Navšťívte tému pre prijatie opatrení" was_edited: "Príspevok bol upravený po prvom označení" previous_flags_count: "Tento príspevok bol už označený {{count}} krát." + details: "detaily" + flagged_topics: + topic: "Téma" + type: "Typ" + users: "Používatelia" + last_flagged: "Naposledy označené" + short_names: + off_topic: "mimo tému" + inappropriate: "nevhodné" + spam: "spam" + notify_user: "vlastné" + notify_moderators: "vlastné" groups: primary: "Hlavná skupina" no_primary: "(bez hlavnej skupiny)" @@ -2131,6 +2199,13 @@ sk: info_html: "Váš API kľúč Vám umožní vytváranie a aktualizovanie tém prostredníctvom volaní JSON." all_users: "Všetci používatelia" note_html: "Držte tento kľúč <strong>v tajnosti</strong>, všetci užívatelia ktorí ho vlastnia môžu vytvárať ľubovoľné príspevky pod ľubovoľným používateľským menom. " + web_hooks: + create: "Vytvoriť" + save: "Uložiť" + destroy: "Vymazať" + description: "Popis" + go_back: "Späť na zoznam" + payload_url_placeholder: "https://example.com/postreceive" plugins: title: "Pluginy" installed: "Nainštalované pluginy" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index c5a7a4275c3..ab264a3e039 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -45,12 +45,16 @@ tr_TR: other: "< %{count}s" x_seconds: other: "%{count}s" + less_than_x_minutes: + other: "< %{count}dk" x_minutes: other: "%{count}d" about_x_hours: other: "%{count}s" x_days: other: "%{count}g" + x_months: + other: "%{count}ay" about_x_years: other: "%{count}y" over_x_years: @@ -83,6 +87,7 @@ tr_TR: other: "%{count} yıl sonra" previous_month: 'Önceki Ay' next_month: 'Sonraki Ay' + placeholder: tarih share: topic: 'bu konunun bağlantısını paylaşın' post: '#%{postNumber} numaralı gönderiyi paylaşın' @@ -97,6 +102,7 @@ tr_TR: split_topic: "bu konuyu %{when} ayırdı" invited_user: "%{when} %{who} davet edildi" invited_group: "%{who} %{when} davet edildi" + user_left: "%{who} bu iletiden ayrıldı %{when}" removed_user: "%{when} %{who} silindi" removed_group: "%{who} %{when} kaldırıldı" autoclosed: @@ -117,6 +123,8 @@ tr_TR: visible: enabled: '%{when} listelendi' disabled: '%{when} listelenmedi' + banner: + enabled: 'Bu konu manşete taşınmıştır %{when}. Kullanıcı tarafından yoksayılana kadar her sayfanın en üstünde belirecektir.' topic_admin_menu: "konuyla alakalı yönetici eylemleri" wizard_required: "Yeni Discourse'unuza hoşgeldiniz! Hadi kuruluma başlayalım! <a href='%{url}' data-auto-route='true'>Kurulum Sihirbazı</a> ✨" emails_are_disabled: "Tüm giden e-postalar yönetici tarafından genel olarak devre dışı bırakıldı. Herhangi bir e-posta bildirimi gönderilmeyecek." @@ -231,6 +239,7 @@ tr_TR: uploading: "Yükleniyor..." uploading_filename: "{{filemame}} yükleniyor..." uploaded: "Yüklendi!" + pasting: "Yapıştırılıyor..." enable: "Etkinleştir" disable: "Devredışı Bırak" undo: "Geri Al" @@ -323,7 +332,7 @@ tr_TR: add_members: "Üyeleri ekle" delete_member_confirm: "'%{username}' adlı kullanıcıyı '%{group}' grubundan çıkart?" name_placeholder: "Grup adı, kullanıcı adındaki gibi boşluksuz olmalı" - public_admission: "Kullanıcıların gruba özgürce katılmalarına izin ver (Herkese açık olarak görünür grup gerektirir)" + public_admission: "Kullanıcıların gruba özgürce katılmalarına izin ver (grubun Herkese açık olması gerekir)" public_exit: "Kullanıcıların grubu özgürce terk etmesine izin ver" empty: posts: "Bu grubun üyelerinden konu yok." @@ -340,14 +349,14 @@ tr_TR: automatic_group: Otomatik Grup closed_group: Kapanmış Grup is_group_user: "Bu grubun bir üyesisiniz." - allow_membership_requests: "Kullanıcıların grup sahiplerine üyelik talepleri göndermesine izin ver" + allow_membership_requests: "Kullanıcılar'ın grup sahiplerine üyelik talepleri göndermesine izin ver" membership_request: submit: "İstek Gönderin" title: "@%{group_name} için Katılma isteği " reason: "Grup sahiplerine bu gruba neden üye olduğunuzu bildirin" membership: "Üyelik" name: "İsim" - user_count: "Grup Sayısı" + user_count: "Üyeler" bio: "Grup Hakkında" selector_placeholder: "Üye ekle" owner: "sahip" @@ -456,6 +465,7 @@ tr_TR: topics_entered: "açılan konular" post_count: "# gönderi" confirm_delete_other_accounts: "Bu hesapları silmek isteğinize emin misiniz?" + powered_by: "<a href='https://ipinfo.io'>ipinfo.io</a> tarafından güçlendirildi" user_fields: none: "(bir seçenek seçin)" user: @@ -716,6 +726,7 @@ tr_TR: title: "Davetler" user: "Davet Edilen Kullanıcı" sent: "Gönderildi" + none: "Gösterilecek davet yok." truncated: other: "İlk {{count}} davet gösteriliyor." redeemed: "Kabul Edilen Davetler" @@ -772,6 +783,8 @@ tr_TR: other: "alınan" days_visited: other: "ziyaret edilen gün" + topics_entered: + other: "görüntülenmiş başlıklar" posts_read: other: "okunmuş gönderi" bookmark_count: @@ -964,6 +977,7 @@ tr_TR: accept_title: "Davet" welcome_to: "%{site_name} topluluğuna hoş geldiniz!" invited_by: "Davet gönderen:" + social_login_available: "Ayrıca bu eposta adresini kullanan tüm sosyal ağ girişleriyle oturum açabileceksiniz." your_email: "Hesap e-posta adresiniz <b>%{email}</b>." accept_invite: "Daveti kabul et" success: "Hesabınız oluşturuldu ve şimdi giriş yaptınız." @@ -989,9 +1003,9 @@ tr_TR: ctrl: 'Ctrl' alt: 'Alt' select_kit: + default_header_text: Seç... no_content: Hiçbir sonuç bulunamadı filter_placeholder: Ara... - create: "{{content}} oluştur" emoji_picker: filter_placeholder: Emoji ara people: İnsanlar @@ -999,10 +1013,15 @@ tr_TR: food: Gıda activity: Etkinlik travel: Seyahat + objects: Nesneler celebration: Kutlama + custom: Özel emojiler recent: Son zamanlarda kullanılmış default_tone: Cilt rengi yok light_tone: Açık cilt tonu + medium_light_tone: Orta açık cilt tonu + medium_tone: Orta cilt tonu + medium_dark_tone: Orta koyu cilt tonu dark_tone: Koyu cilt tonu composer: emoji: "Emoji :)" @@ -1048,7 +1067,9 @@ tr_TR: edit_reason_placeholder: "neden düzenleme yapıyorsunuz?" show_edit_reason: "(düzenleme sebebi ekle)" topic_featured_link_placeholder: "Başlığı olan bir link giriniz." + remove_featured_link: "Konudan bağlantıyı kaldır." reply_placeholder: "Buraya yazın. Biçimlendirmek için Markdown, BBCode ya da HTML kullanabilirsin. Resimleri sürükleyebilir ya da yapıştırabilirsin." + reply_placeholder_no_images: "Buraya yazın. Biçimlendirme için Markdown, BBCode ya da HTML kullanın." view_new_post: "Yeni gönderinizi görüntüleyin." saving: "Kaydediliyor" saved: "Kaydedildi!" @@ -1079,6 +1100,8 @@ tr_TR: ulist_title: "Madde İşaretli Liste" list_item: "Liste öğesi" help: "Markdown Düzenleme Yardımı" + collapse: "yazım alanını küçült" + abandon: "yazım alanını kapat ve taslağı sil" modal_ok: "Tamam" modal_cancel: "İptal" cant_send_pm: "Üzgünüz, %{username} kullanıcısına ileti gönderemezsiniz." @@ -1087,11 +1110,35 @@ tr_TR: body: "Bu ileti şu an sadece sana gönderiliyor!" admin_options_title: "Bu konu için isteğe bağlı görevli ayarları" notifications: + tooltip: + regular: + other: "{{count}} görülmemiş bildirim" + message: + other: "{{count}} okunmamış ileti" title: "@isim bahsedilişleri, gönderileriniz ve konularınıza verilen cevaplar, iletilerle vb. ilgili bildiriler" none: "Şu an için bildirimler yüklenemiyor." empty: "Bildirim yok." more: "daha eski bildirimleri görüntüle" total_flagged: "toplam bildirilen gönderiler" + mentioned: "<span>{{username}}</span> {{description}}" + group_mentioned: "<span>{{username}}</span> {{description}}" + quoted: "<span>{{username}}</span> {{description}}" + replied: "<span>{{username}}</span> {{description}}" + posted: "<span>{{username}}</span> {{description}}" + edited: "<span>{{username}}</span> {{description}}" + liked: "<span>{{username}}</span> {{description}}" + liked_2: "<span>{{username}}, {{username2}}</span> {{description}}" + liked_many: + other: "<span>{{username}}, {{username2}} ve {{count}} diğer</span> {{description}}" + private_message: "<span>{{username}}</span> {{description}}" + invited_to_private_message: "<p><span>{{username}}</span> {{description}}" + invited_to_topic: "<span>{{username}}</span> {{description}}" + invitee_accepted: "<span>{{username}}</span> davetinizi kabul etti" + moved_post: "<span>{{username}}</span> {{description}} taşıdı" + linked: "<span>{{username}}</span> {{description}}" + granted_badge: "'{{description}}' kazandı" + topic_reminder: "<span>{{username}}</span> {{description}}" + watching_first_post: "<span>Yeni Konu</span> {{description}}" alt: mentioned: "Bahsedildi, şu kişi tarafından" quoted: "Alıntılandı, şu kişi tarafından" @@ -1105,6 +1152,7 @@ tr_TR: linked: "Gönderinize bağlantı" granted_badge: "Rozet alındı" group_message_summary: "Grup gelen kutusundaki iletiler" + topic_reminder: "Bir hatırlatıcı" popup: mentioned: '{{username}}, "{{topic}}" başlıklı konuda sizden bahsetti - {{site_title}}' group_mentioned: '{{username}} sizden bahsetti "{{topic}}" - {{site_title}}' @@ -1126,6 +1174,7 @@ tr_TR: uploading: "Yükleniyor" select_file: "Dosya seçin" image_link: "resminizin yönleneceği bağlantı" + default_image_alt_text: resim search: sort_by: "Sırala" relevance: "Alaka" @@ -1136,12 +1185,19 @@ tr_TR: select_all: "Tümünü Seç" clear_all: "Tümünü Temizle" too_short: "Aradığın terim çok kısa." + result_count: + other: "<span class='term'>{{term}}</span> için {{count}}{{plus}} sonuç" title: "konu, gönderi, kullanıcı veya kategori ara" no_results: "Hiç bir sonuç bulunamadı." no_more_results: "Başka sonuç yok." searching: "Aranıyor..." post_format: "{{username}} tarafından #{{post_number}}" + results_page: "'{{term}}' için arama sonuçları" more_results: "Daha fazla sonuç var. Lütfen arama kriterlerini daraltın." + cant_find: "Aradığınızı bulamıyor musunuz?" + start_new_topic: "Belki de yeni bir konu oluşturmalısınız?" + or_search_google: "Ya da Google'la aramayı deneyin:" + search_google: "Google'la aramayı deneyin:" search_google_button: "Google" search_google_title: "Bu sitede ara" context: @@ -1153,20 +1209,29 @@ tr_TR: title: Gelişmiş Arama posted_by: label: Gönderen + in_category: + label: Kategorilendirilmiş in_group: label: Şu Grupta with_badge: label: Rozetle + with_tags: + label: Etiketlenmiş filters: likes: beğendiğim posted: gönderide bulunduğum watching: gözlediğim tracking: takip ettiğim + private: İletilerimde + bookmarks: İmledim first: en ilk gönderidir. pinned: tutturulmuş unpinned: tutturulmamış + seen: Okudum unseen: Okumadım wiki: wiki olan + images: resim(ler)i dahil et + all_tags: Yukarıdaki tüm etiketler statuses: label: Şöyle olan konular open: açık @@ -1294,22 +1359,50 @@ tr_TR: jump_reply_down: Daha sonraki cevaba geç deleted: "Konu silindi " topic_status_update: + title: "Konu Zamanlayıcısı" + save: "Zamanlayıcı Ayarla" + num_of_hours: "Saat değeri:" + remove: "Zamanlayıcıyı Kaldır" + publish_to: "Şuraya Yayınla:" when: "Ne zaman:" public_timer_types: Konu Zamanlayıcıları private_timer_types: Kullanıcı Konusu Zamanlayıcılar auto_update_input: none: "Bir zaman çerçevesi seçin" + later_today: "Bugünün sonlarında" tomorrow: "Yarın" + later_this_week: "Bu haftanın sonlarında" this_weekend: "Bu hafta sonu" next_week: "Gelecek hafta" + two_weeks: "İki Hafta" next_month: "Gelecek ay" + three_months: "Üç Ay" + six_months: "Altı Ay" one_year: "Bir Yıl" forever: "Sonsuza dek" pick_date_and_time: "Tarih ve saat seç" + set_based_on_last_post: "Son gönderiye göre kapat" + publish_to_category: + title: "Yayınlamayı Zamanla" + temp_open: + title: "Geçici Olarak Aç" + auto_reopen: + title: "Konuyu Otomatik Aç" + temp_close: + title: "Geçici Olarak Kapat" auto_close: + title: "Konuyu Otomatik Kapat" + label: "Şu kadar saat sonra konuyu otomatik kapat:" error: "Lütfen geçerli bir değer giriniz." + based_on_last_post: "Konudaki son gönderi en az bu kadar eski olmadıkça kapatma." + auto_delete: + title: "Konuyu Otomatik Sil" reminder: title: "Bana hatırlat" + status_update_notice: + auto_open: "Bu konu %{timeLeft} otomatik olarak açılacak." + auto_close: "Bu konu %{timeLeft} otomatik olarak kapanacak." + auto_publish_to_category: "Bu konu %{timeLeft} <a href=%{categoryUrl}>#%{categoryName}</a> kategorisinde yayınlanacak." auto_close_title: 'Otomatik Kapatma Ayarları' auto_close_immediate: other: "Konudaki son gönderi zaten %{count} saat eski, bu yüzden konu hemen kapatılacak." @@ -2996,6 +3089,8 @@ tr_TR: upload: "Yükle" uploading: "Yükleniyor..." quit: "Belki Sonra" + staff_count: + other: "Topluluğunuzda %{count} görevli üye var." invites: add_user: "ekle" none_added: "Herhangi bir görevli davet etmediniz. Devam etmek istediğinize emin misiniz?" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 30a91db4cd2..d4543a45ac8 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -45,12 +45,16 @@ vi: other: "< %{count}s" x_seconds: other: "%{count}s" + less_than_x_minutes: + other: "< %{count} phút" x_minutes: other: "%{count}m" about_x_hours: other: "%{count}h" x_days: other: "%{count}d" + x_months: + other: "%{count} tháng" about_x_years: other: "%{count}y" over_x_years: @@ -83,6 +87,7 @@ vi: other: "còn %{count} năm" previous_month: 'Tháng trước' next_month: 'Tháng sau' + placeholder: ngày share: topic: 'chia sẻ chủ đề này' post: 'đăng #%{postNumber}' @@ -97,6 +102,7 @@ vi: split_topic: "tách chủ đề này lúc %{when}" invited_user: "đã mời %{who} lúc %{when}" invited_group: "đã mời %{who} lúc %{when}" + user_left: "%{who}tự xóa mình khỏi tin nhắn này %{when}" removed_user: "xoá %{who} lúc %{when}" removed_group: "xoá %{who} lúc %{when}" autoclosed: @@ -234,6 +240,7 @@ vi: uploading: "Đang tải lên..." uploading_filename: "Đang tải lên {{filename}}..." uploaded: "Đã tải lên!" + pasting: "Đang gõ" enable: "Kích hoạt" disable: "Vô hiệu hóa" undo: "Hoàn tác" @@ -326,6 +333,8 @@ vi: add_members: "Thêm thành viên" delete_member_confirm: "Xoá '%{username}' khỏi nhóm '%{group}' ?" name_placeholder: "Tên nhóm, không có khoảng trắng, tương tự như luật đặt tên người dùng" + public_admission: "Cho phép Thành viên tham gia nhóm một cách tự do (nhóm hiển thị công khai)" + public_exit: "Cho phép Thành viên thoát khỏi nhóm một cách tự do" empty: posts: "Không có bài viết nào của các thành viên trong nhóm này" members: "Không có thành viên nào trong nhóm này" @@ -342,8 +351,11 @@ vi: closed_group: Nhóm kín is_group_user: "Bạn là một thành viên của nhóm này" allow_membership_requests: "Cho phép người dùng gửi đơn xin gia nhập nhóm đến người quản lí nhóm." + membership_request_template: "đã tùy chỉnh để hiển thị cho người dùng khi gửi yêu cầu thành viên" membership_request: submit: "Gửi yêu c" + title: "Yêu cầu tham gia @%{group_name}" + reason: "Cho phép chủ sở hữu nhóm biết lý do bạn thuộc nhóm này" membership: "Thành viên" name: "Tên" user_count: "Số lượng thành viên" @@ -413,6 +425,7 @@ vi: '14': "Đang chờ xử lý" categories: all: "tất cả chuyên mục" + all_subcategories: "tất cả trong %{categoryName}" no_subcategory: "không có gì" category: "Chuyên mục" category_list: "Hiễn thị danh sách chuyên mục" @@ -482,6 +495,7 @@ vi: disable: "Khóa Notification" enable: "Cho phép Notification" each_browser_note: "Lưu ý: Bạn phải thay đổi trong cấu hình mỗi trình duyệt bạn sử dụng." + dismiss: 'Hủy bỏ' dismiss_notifications: "Bỏ qua tất cả" dismiss_notifications_tooltip: "Đánh dấu đã đọc cho tất cả các thông báo chưa đọc" first_notification: "Thông báo đầu tiên của bạn! Chọn để bắt đầu" @@ -700,6 +714,7 @@ vi: title: "Lời mời" user: "User được mời" sent: "Đã gửi" + none: "Không tìm thấy lời mời nào." truncated: other: "Hiện {{count}} thư mời đầu tiên" redeemed: "Lời mời bù lại" @@ -716,8 +731,10 @@ vi: rescinded: "Lời mời bị xóa" rescind_all: "Xóa tất cả lời m" rescinded_all: "Tất cả lời mời đã được xóa!" + rescind_all_confirm: "Bạn có muốn xóa bỏ tất cả các lời mời?" reinvite: "Mời lại" reinvite_all: "Gửi lại tất cả lời mời" + reinvite_all_confirm: "Bạn có chắc chắn gửi lại tất cả các lời mời?" reinvited: "Gửi lại lời mời" reinvited_all: "Tất cả lời mời đã được gửi lại" time_read: "Đọc thời gian" @@ -743,12 +760,19 @@ vi: title: "Tóm tắt" stats: "Thống kê" time_read: "thời gian đọc" + recent_time_read: "đã đọc gần đây" topic_count: other: "Chủ đề đã được tạo" post_count: other: "Bài viết đã được tạo" + likes_given: + other: "nhận" + likes_received: + other: "Đã nhận" days_visited: other: "Ngày đã ghé thăm" + topics_entered: + other: "chủ đề đã xem" posts_read: other: "Bài viết đã đọc" bookmark_count: diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index e2e11312579..7b38316673e 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -1014,7 +1014,6 @@ zh_CN: default_header_text: 选择... no_content: 无符合的结果 filter_placeholder: 搜索…… - create: "创建{{content}}" emoji_picker: filter_placeholder: 查找表情符号 people: 人物 @@ -2297,11 +2296,13 @@ zh_CN: moderation_history: "管理日志" agree: "确认标记" agree_title: "确认这个标记有效且正确" - agree_flag_hide_post: "确认并隐藏帖子" agree_flag_hide_post_title: "隐藏帖子并自动发送私信给作者使其修改" agree_flag_restore_post: "确认并恢复帖子" agree_flag_restore_post_title: "恢复帖子为所有用户可见。" - agree_flag: "确认并保持帖子" + agree_flag_suspend: "暂停用户" + agree_flag_suspend_title: "同意标记并暂停用户。" + agree_flag_silence: "禁言用户" + agree_flag_silence_title: "同意标记并禁言用户。" agree_flag_title: "确认标记并保持帖子不变。" delete: "删除" delete_title: "删除标记指向的帖子。" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 7b60baa22f5..c2549c23b3b 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -429,7 +429,6 @@ ar: change_failed_explanation: "حاولت تخفيض رتبة %{user_name} إلى '%{new_trust_level}'. أيضا مستوى الثقة لهم حاليا '%{current_trust_level}'. %{user_name} سيبقى في '%{current_trust_level}' - إذا رغبت في تخفيض رتبة عضو أنظر لمستوى الثقة أولاً." rate_limiter: slow_down: "لقد قمت بهذا الإجراء عدّة مرات. فحاول مجددا لاحقا." - too_many_requests: "لدينا حد يومي لعدد المرات التي يمكن للعمل أن ينجز بها. رجاءا انتظر %{time_left} قبل المحاولة مجدداً." by_type: first_day_replies_per_day: "لقد وصلت للعدد الأقصى للردود التي يمكن للعضو الجديد إنشائها في يومهم الأول. رجاء انتظر %{time_left} قبل المحاولة مجددا." first_day_topics_per_day: "لقد وصلت للعدد الأقصى للمواضيع التي يمكن للعضو الجديد إنشائها في يومهم الأول. رجاء انتظر %{time_left} قبل المحاولة مجددا." @@ -917,7 +916,6 @@ ar: image_magick_warning: 'ضُبط الخادوم لإنشاء مصغّرات للصّور الكبيرة، ولكنّ ImageMagick غير مثبّت. ثبّت ImageMagick مستخدمًا مدير الحزم الذي تفضّله أو <a href="http://www.imagemagick.org/script/binary-releases.php" target="_blank">نزّل آخر إصدارة</a>.' failing_emails_warning: 'يوجد %{num_failed_jobs} مهام بريد إلكتروني فشلت. تحقق من app.yml الخاص بك وتأكد من إعدادات خادم البريد أنها صحيحة. <a href="/sidekiq/retries" target="_blank">أنظر للمهام الفاشلة في Sidekiq</a>.' subfolder_ends_in_slash: "إعدادات المجلدات الداخلية خاطئ;ال DISCOURSE_RELATIVE_URL_ROOT يجب ان تنتهي ب سلاش." - missing_mailgun_api_key: "ضُبط الموقع ليُرسل الرّسائل الإلكترونيّ عبر mailgun ولكنّك لم توفّر مفتاح API ليُستخدم في تأكيد رسائل webhook." site_settings: censored_words: "الكلمات التي ستُستبدل آليًّا ب ■■■■" censored_pattern: "نمط التّعبير النّمطيّ الذي سيُستبدل آليًّا ب ■■■■" @@ -926,12 +924,10 @@ ar: set_locale_from_accept_language_header: "اختيار لغة الواجة للمستخدمين المتخفون طبقا للغة المختارة بمتصفح الشبكة. ( إعداد تجريبى، لا يعمل مع ذاكرة المتصفح )" min_post_length: "الحد الأدنى المسموح به لطول المنشور بالأحرف" min_first_post_length: "الحد الأدنى المسموح به لطول أول منشور (المنشور الاول بالموضوع) بالأحرف" - min_personal_message_post_length: "الحد الأدنى المسموح به لطول المنشور بالأحرف في الرسائل" max_post_length: "الحد الأقصي المسموح به لطول المنشور بالأحرف" topic_featured_link_enabled: "السماح بنشر روابط في الموضوعات" min_topic_title_length: "الحد الأدنى المسموح به موضوع طول اللقب في الأحرف" max_topic_title_length: "الحد الأعلى المسموح به لطول عنوان موضوع في الأحرف" - min_personal_message_title_length: "الحد الأدنى المسموح به لطول عنوان لرسالة في الأحرف" min_search_term_length: "الحد الأدنى الصالح لطول مصطلح في الأحرف" search_tokenize_chinese_japanese_korean: "ارغام الباحث على تجميع احرف اللغات الصينية واليابانية والكورية حتى على المواقع الغير معدة لتلك اللغات." search_prefer_recent_posts: "اذا كان البحث في المنتدي بطئ, هذا الخيار سوف يحاول فهرسة اخر المنشورات اولاً" @@ -1090,7 +1086,6 @@ ar: max_bookmarks_per_day: "أقصى عدد من العلامات لكلّ مستخدم يوميًّا." max_edits_per_day: "أقصى عدد من عمليّات التّحرير لكلّ مستخدم يوميًّا." max_topics_per_day: "أقصى عدد للمواضيع التي يمكن للعضو إنشائها باليوم." - max_personal_messages_per_day: "أقصى عدد لرسائل الأعضاء التي يمكن إنشائها باليوم." max_invites_per_day: "أقصى عدد للدعوات التي يمكن للعضو إرسالها باليوم." max_topic_invitations_per_day: "أقصى عدد لدعوات الموضوع التي يمكن للعضو إرسالها باليوم." alert_admins_if_errors_per_minute: "عدد الأخطاء في الدقيقة الواحدة لكى يرسل تنبيه للمدير. قيمة 0 تعطل هذه الميزة. ملاحظة: تتطلب إعادة تشغيل." @@ -1263,7 +1258,6 @@ ar: enforce_square_emoji: "أجبِر النّسبة الباعيّة لكلّ الإيموجي لتكون مربّعة." approve_post_count: "عدد المنشورات للمستخدم الجديد او الاساسي يجب ان تتم الموافقه عليه " approve_unless_trust_level: "مشاركات للأعضاء أدنى من مستوى الثقة هذا يجب أن تتم الموافقة عليها." - default_email_personal_messages: "أرسل بريد إلكتروني عندما يراسل شخص ما العضو إفتراضيا." default_email_direct: "ارسل بريد الكتروني عندما يقوم احدهم بالرد/الاقتباس الي/ذكر او دعوه مستخدم افتراضيا" default_email_mailing_list_mode: "ارسل بريد إلكتروني لكل مشاركة جديدة افتراضيا." disable_mailing_list_mode: "عدم السماح للمستخدمين بتفعيل خيار المراسله الجماعيه" diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index 508d7ba7652..f99df09f818 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -175,7 +175,6 @@ bs_BA: title: "korisnik" change_failed_explanation: "You attempted to demote %{user_name} to '%{new_trust_level}'. However their trust level is already '%{current_trust_level}'. %{user_name} will remain at '%{current_trust_level}' - if you wish to demote user lock trust level first" rate_limiter: - too_many_requests: "We have a daily limit on how many times that action can be taken. Please wait %{time_left} before trying again." hours: one: "1 sat" few: "Par sati" @@ -357,11 +356,9 @@ bs_BA: delete_old_hidden_posts: "Auto-delete any hidden posts that stay hidden for more than 30 days." allow_user_locale: "Allow users to choose their own language interface preference" min_post_length: "Minimum allowed post length in characters" - min_personal_message_post_length: "Minimum allowed post length in characters for private messages" max_post_length: "Maximum allowed post length in characters" min_topic_title_length: "Minimum allowed topic title length in characters" max_topic_title_length: "Maximum allowed topic title length in characters" - min_personal_message_title_length: "Minimum allowed title length for a private message in characters" min_search_term_length: "Minimum valid search term length in characters" allow_duplicate_topic_titles: "Allow topics with identical, duplicate titles." unique_posts_mins: "How many minutes before a user can make a post with the same content again" @@ -461,7 +458,6 @@ bs_BA: max_bookmarks_per_day: "Maximum number of bookmarks per user per day." max_edits_per_day: "Maximum number of edits per user per day." max_topics_per_day: "Maximum number of topics a user can create per day." - max_personal_messages_per_day: "Maximum number of private messages users can create per day." suggested_topics: "Number of suggested topics shown at the bottom of a topic." limit_suggested_to_category: "Only show topics from the current category in suggested topics." clean_up_uploads: "Remove orphan unreferenced uploads to prevent illegal hosting. WARNING: you may want to back up of your /uploads directory before enabling this setting." diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index da26d75d3c9..e65ec18c102 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -392,7 +392,6 @@ ca: change_failed_explanation: "Has provat de degradar %{user_name} a '%{new_trust_level}'. En qualsevol cas, el seu nivell de confiança ja és '%{current_trust_level}'. %{user_name} restarà a '%{current_trust_level}' - Si vols degradar una persona usuària, bloca-li abans el nivell de confiança" rate_limiter: slow_down: "Has provat de fer el mateix moltes vegades, prova-ho més tard." - too_many_requests: "Tenim un límit diari sobre quants cops es pot realitzar aquesta acció. Si us plau, espera %{time_left} abans de tornar a provar-ho." by_type: first_day_replies_per_day: "Has arribat a la quantitat màxima de respostes que una nova persona usuària pot crear durant el seu primer dia. Si us plau, espera %{time_left} abans de tornar a provar-ho." first_day_topics_per_day: "Has arribat a la quantitat màxima de temes que una nova persona usuària pot crear durant el seu primer dia. Si us plau, espera %{time_left} abans de tornar a provar-ho." @@ -771,7 +770,6 @@ ca: email_polling_errored_recently: one: "L'enquesta per correu electrònic ha generat un error durant les darreres 24 hores. Fes un cop d'ull als <a href='/logs' target='_blank'>registres</a> per saber-ne més." other: "L'enquesta per correu electrònic ha generat %{count} errors durant les darreres 24 hores. Fes un cop d'ull als <a href='/logs' target='_blank'>registres</a> per saber-ne més." - missing_mailgun_api_key: "El servidor està configurat per enviar correus amb <i>mailgun</i> però no has facilitat una clau API emprada per verificar els missatges de ganxo de web." bad_favicon_url: "La càrrega de <i>favicon</i> està fallant. Revisa la configuració de favicon_url a la <a href='/admin/site_settings'>Configuració del lloc</a>." poll_pop3_timeout: "S'ha esgotat del temps de connexió al servidor POP3. No s'ha pogut lliurar el correu entrant. Si us plau, revisa la teva <a href='/admin/site_settings/category/email'>configuració de POP3</a> i el proveïdor de serveis." poll_pop3_auth_error: "La connexió al servidor POP3 està fallant amb un error d'autenticació. Si us plau, revisa la teva <a href='/admin/site_settings/category/email'>configuració de POP3</a>." @@ -783,13 +781,11 @@ ca: set_locale_from_accept_language_header: "configura la llengua d'interfície per a persones anònimes des dels encapçalaments dels seus navegadors. (EXPERIMENTAL, no funciona amb memòria cau anònima)" min_post_length: "Mínim permès en caràcters per a l'extensió de publicacions " min_first_post_length: "Mínim permès en caràcters per a l'extensió de primera publicació (cos del tema)" - min_personal_message_post_length: "Mínim permès en caràcters per a l'extensió de publicacions a missatges" max_post_length: "Màxim permès en caràcters per a l'extensió de publicacions " topic_featured_link_enabled: "Activa la publicació d'enllaç amb temes." show_topic_featured_link_in_digest: "Mostra el tema de l'enllaç destacat al correu electrònic de resum automàtic." min_topic_title_length: "Mida mínima permesa del títol del tema en caràcters " max_topic_title_length: "Mida màxima permesa del títol del tema en caràcters " - min_personal_message_title_length: "Mida mínima permesa del títol d'un missatge en caràcters " min_search_term_length: "Mida mínima permesa d'un camp de cerca en caràcters " search_tokenize_chinese_japanese_korean: "Força la cerca per assegurar les dades en Xinès/Japonès/Coreà fins i tot en llocs que no siguin en aquestes llengües" search_prefer_recent_posts: "Si la cerca al teu fòrum va lenta, aquesta opció prova primer amb un índex de publicacions recents" @@ -835,7 +831,6 @@ ca: summary_likes_required: "Mínim nombre de m'agrades a un tema abans d'activar 'Resumeix aquest tema'" summary_percent_filter: "Quan una persona clica 'Resumeix aquest tema', mostra el '% o' principal de publicacions" summary_max_results: "Màxim de publicacions resultat de 'Resumeix aquest tema'" - enable_personal_messages: "Permet que les persones amb nivell de confiança 1 puguin crear missatges i respondre'ls (configurable amb el mínim de nivell de confiança per enviar missatges). Fixa't que l'equip sempre pot enviar qualsevol mena de missatges." enable_long_polling: "El bus de missatges emprat per a la notificació pot fer servir el mostreig llarg" long_polling_base_url: "La URL bàsica emprada per a mostreig llarg (quan una xarxa de lliurament de continguts serveix contingut dinàmic, assegura't de configurar-ho per llençar l'origen). Per exemple, http://origin.site.com" long_polling_interval: "Quantitat de temps que el servidor hauria d'esperar abans de respondre clients quan no hi ha dades d'enviament (connectat només a persones usuàries)" @@ -955,7 +950,6 @@ ca: max_bookmarks_per_day: "Quantitat màxima de preferits per persona i dia." max_edits_per_day: "Quantitat màxima d'edicions per persona i dia." max_topics_per_day: "Quantitat màxima de temes que una persona pot crear cada dia." - max_personal_messages_per_day: "Quantitat màxima de missatges que una persona pot crear cada dia." max_invites_per_day: "Quantitat màxima d'invitacions que una persona pot enviar cada dia." max_topic_invitations_per_day: "Quantitat màxima d'invitacions a temes que una persona por enviar cada dia." alert_admins_if_errors_per_minute: "Quantitat d'errors per minut per desencadenar una alerta admin. Un valor 0 inhabilita aquesta característica. NOTA: cal reiniciar." @@ -1019,7 +1013,6 @@ ca: max_users_notified_per_group_mention: "Quantitat màxima de persones que poden rebre alertes si un grup és mencionat (si el valor límit no troba alertes, serà augmentat)" create_thumbnails: "Crea miniatures i finestres d'imatge de massa amplada per cabre a una publicació." email_time_window_mins: "Espera (n) minuts abans d'enviar qualsevol alerta de correu, per tal de possibilitar l'edició i la finalització de les publicacions." - personal_email_time_window_seconds: "Espera (n) segons abans d'enviar qualsevol correu privat d'alerta, per tal de possibilitar l'edició i la finalització dels missatges." email_posts_context: "Quantes respostes prèvies per incloure com a context a correus d'alerta." flush_timings_secs: "Amb quanta freqüència descarreguem al servidor les dades de temporització, en segons." title_max_word_length: "La mida màxima permesa de la paraula, en caràcters al títol d'un tema." @@ -1181,7 +1174,6 @@ ca: code_formatting_style: "El botó de codi al compositor serà per defecte d'aquest estil de format de codi" default_email_digest_frequency: "Amb quina freqüència les persones rebran els correus resumits per defecte." default_include_tl0_in_digests: "Inclou publicacions de noves persones a correus resumits per defecte. Les persones ho poden canviar a les seves preferències." - default_email_personal_messages: "Envia un correu s'enviïn missatges a la persona usuària per defecte." default_email_direct: "Envia un correu quan se citi/respongui/mencioni la persona usuària o persones convidades per defecte." default_email_mailing_list_mode: "Envia un correu per a cada nova publicació per defecte." default_email_mailing_list_mode_frequency: "Per defecte, les persones que activen el mode llista de correu rebran correus amb aquesta freqüència." diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index cd757daaa3b..3b2076df2fc 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -103,6 +103,7 @@ cs: user_exists: "Netřeba posílat pozvánku na <b>%{email}</b>. Tento email <a href='/u/%{username}/summary'>je veden u tohoto účtu!</a>" bulk_invite: file_should_be_csv: "Nahraný soubor by měl být ve formátu csv." + error: "Nastala chyba při nahrávání souboru. Prosím opakujte akci později." backup: operation_already_running: "Právě probíhá operace %{operation}. V tuto chvíli nelze zahájit novou operaci %{operation}." backup_file_should_be_tar_gz: "Záloha by měla být archiv s příponou .tar.gz." @@ -280,7 +281,7 @@ cs: image_placeholder: broken: "Tento obrázek je rozbitý" rate_limiter: - too_many_requests: "Děláte tuto akci příliš často. Prosím počkejte %{time_left} a zkuste to znovu." + slow_down: "Tuto akci provádíte příliš často, zkuste to prosím později." by_type: pms_per_day: "Odeslal jsi maximum povolených zpráv za den. Další můžeš odeslat za {time_left}." hours: @@ -446,6 +447,7 @@ cs: long_form: 'hlasoval pro tento příspěvek' user_activity: no_bookmarks: + self: "Nemáte žádné příspěvky v záložkách. Přidání do záložek vám umožní téma v budoucnu snadněji nalézt." others: "Žádné záložky." no_likes_given: self: "Zatím se ti nelíbil žádný příspěvek." @@ -686,13 +688,18 @@ cs: types: category: 'Kategorie' user: 'Uživatelé' - original_poster: "Původní zasilatel" + original_poster: "Autor tématu" most_posts: "Více příspěvků" - most_recent_poster: "Poslední zasilatel" - frequent_poster: "Častý zasilatel" + most_recent_poster: "Poslední přispěvatel" + frequent_poster: "Častý přispěvatel" redirected_to_top_reasons: new_user: "Vítejte v naší komunitě! Tohle jsou poslední populární témata." not_seen_in_a_month: "Vítejte zprátky! Chvíli jsme se neviděli. Tohle jsou nejpopulárnější témata od vaší poslední návštěvy." + move_posts: + new_topic_moderator_post: + one: "Příspěvek byl oddělen do nového tématu: %{topic_link}" + few: "%{count}příspěvky byly odděleny do nového tématu: %{topic_link}" + other: "%{count}příspěvky byly odděleny do nového tématu: %{topic_link}" topic_statuses: archived_enabled: "Toto téma je archivováno. Je zmraženo a již nemůže být nijak změněno." archived_disabled: "Toto téma je vráceno z archivu. Již není zmraženo a může být měněno." @@ -788,7 +795,7 @@ cs: post_not_found: "Can't find a post with id %{post_id}" notification_already_read: "The notification this email is about has already been read" topic_nil: "post.topic is nil" - post_deleted: "post was deleted by the author" + post_deleted: "příspěvek byl odstraněn autorem" user_suspended: "user was suspended" already_read: "user has already read this post" message_blank: "message is blank" diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index 0570d3858b8..218bd228378 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -421,7 +421,6 @@ da: change_failed_explanation: "Du har forsøgt at degradere %{user_name} til '%{new_trust_level}'. Imidlertid er deres niveau af tillid allerede '%{current_trust_level}'. %{user_name} vil forblive'%{current_trust_level}' - hvis du ønsker at degradere brugeren, skal du først låse brugerens tillids niveau / Trust Level" rate_limiter: slow_down: "Du har udført denne handling for mange gange, prøv igen senere" - too_many_requests: "Vi har en daglig grænse for hvor mange gange den pågældende handling kan udføres. Vent venligst %{time_left} før du prøver igen." by_type: first_day_replies_per_day: "Du har nået det maksimalt antal tilladte svar, en ny bruger kan lave på deres første dag. Vent venligst %{time_left} før du fortsætter med at besvare tanker og indlæg." first_day_topics_per_day: "Du har nået det maksimalt antal af tilladte emner, en ny bruger kan lave på første dag.\nVent venligst %{time_left} før du prøver igen." @@ -807,7 +806,6 @@ da: email_polling_errored_recently: one: "Email afstemning har afstedkommet en fejl i de seneste 24 timer. Venligst se <a href='/logs' target='_blank'>the logs</a> for detaljer." other: "Email afstemning har afstedkommet %{count} fejl i de seneste 24 timer. Se venligst <a href='/logs' target='_blank'>the logs</a> for detaljer." - missing_mailgun_api_key: "Serveren er konfigureret til at sende emails via Mailgun, men der er ikke indtastet en API nøgle som bruges til at verificere webhook beskederne." bad_favicon_url: "Favicon kan ikke loades. Tjek dine favicon_url indstilling i <a href='/admin/site_settings'>Site Settings</a>" poll_pop3_timeout: "Forbindelsen til til POP3 serveren er udløbet. Indkomne mail kunne ikke hentes. Venligst tjek <a href='/admin/site_settings/category/email'>POP3 settings</a> og udbyderen." poll_pop3_auth_error: "Forbindelsen til POP3 serveren melder fejl. Venligst tjek <a href='/admin/site_settings/category/email'>POP3 settings</a>." @@ -819,13 +817,11 @@ da: set_locale_from_accept_language_header: "set interface language for anonymous users from their web browser's language headers. (EXPERIMENTAL, does not work with anonymous cache)" min_post_length: "Minimumlængde tilladt for indlæg i tegn" min_first_post_length: "Minimum tilladte antal tegn (i emne felt) i første indlæg" - min_personal_message_post_length: "Minimumlængde tilladt for indlæg i tegn" max_post_length: "Maksimal længde af indlæg i tegn" topic_featured_link_enabled: "Tillad opslag af et link med emner." show_topic_featured_link_in_digest: "Vis link med det fremhævede emne i e-mail-sammendraget." min_topic_title_length: "Minimumslængde af emnetitel i tegn." max_topic_title_length: "Maksimumslængde af emnetitel i tegn." - min_personal_message_title_length: "Minimumslængde af emnetitel i tegn." min_search_term_length: "Antal tegn i søgefeltet skal have et minimum" search_tokenize_chinese_japanese_korean: "Tving søg til at \"tokenize\" Chinese/Japanese/Korean selv på sites som ikke er CJK" search_prefer_recent_posts: "Såfremt søgning på forum er langsomt, kan denne option indeksere og vise nyeste indlæg først" @@ -869,7 +865,6 @@ da: summary_likes_required: "Minimum antal likes på et emne før 'opsummer dette emne' er aktiveret" summary_percent_filter: "Når en bruger kllikker 'Opsummer dette Emne' vises top % af indlæg" summary_max_results: "Max antal indlæg indeholdt i 'Opsummer dette Emne'" - enable_personal_messages: "Tillad Trust Level 1 (kan konfigureres minimum trust level til at sende beskeder), brugere til at sende og svare på beskeder. Bemærk at moderatorer beskeder, uanset hvad." enable_long_polling: "Message bus til underretninger kan bruge long polling" long_polling_base_url: "URL anvendt for afsteminger (når CDN leverer dynamisk indhold, så sæt dette til oprindelig / orginal) f.eks: http://origin.site.com" long_polling_interval: "Mængde tid før serveren bør vente før den svarer klienter når der ikke er ny data at sende (kun for brugere der er logget ind)" @@ -991,7 +986,6 @@ da: emoji_set: "Hvordan kunne du tænke dig din emoji?" default_email_digest_frequency: "Hvor ofte brugere som standard modtager mails med sammendrag." default_include_tl0_in_digests: "Inkluder som standard indlæg fra nye brugre i mails med sammendrag. Brugere kan ændre dette i deres indstillinger." - default_email_personal_messages: "Send som standard en email når nogen sender brugeren en besked." default_email_direct: "Send som standard en email når nogen citerer/svarer/nævner/inviterer brugeren." default_email_mailing_list_mode: "Send som standard en email for hvert nyt indlæg." default_email_always: "Send som standard en email-notifikation selv hvis brugeren er aktiv." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 725e788106f..7388ad3a1c5 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -457,7 +457,6 @@ de: broken: "Dieses Bild ist beschädigt" rate_limiter: slow_down: "Du hast diese Aktion zu oft durchgeführt. Versuche es später wieder." - too_many_requests: "Diese Aktion kann nur ein begrenztes Mal pro Tag durchgeführt werden. Bitte warte %{time_left} bis zum nächsten Versuch." by_type: first_day_replies_per_day: "Du hast die maximale Anzahl an Antworten erreicht, die ein neuer Benutzer am ersten Tag erstellen kann. Bitte warte %{time_left}, bis Du es wieder versuchst." first_day_topics_per_day: "Du hast die maximale Anzahl an Themen erreicht, die ein neuer Benutzer am ersten Tag erstellen kann. Bitte warte %{time_left}, bis Du es wieder versuchst." @@ -861,7 +860,6 @@ de: email_polling_errored_recently: one: "Beim Abrufen von E-Mails ist in den letzten 24 Stunden ein Fehler aufgetreten. Weitere Informationen findest du <a href='/logs' target='_blank'>im Fehlerprotokoll</a>." other: "Beim Abrufen von E-Mails sind in den letzten 24 Stunden %{count} Fehler aufgetreten. Weitere Informationen findest du <a href='/logs' target='_blank'>im Fehlerprotokoll</a>." - missing_mailgun_api_key: "Der Server wurde für den E-Mail-Versand über mailgun konfiguriert, aber du hast keinen API-Schlüssel hinterlegt, um die WebHook-Nachrichten zu überprüfen." bad_favicon_url: "Das Favicon lässt sich nicht laden. Prüfe die favicon_url-Einstellung in den <a href='/admin/site_settings'>Einstellungen</a>." 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 <a href='/admin/site_settings/category/email'>POP3-Einstellungen</a>." poll_pop3_auth_error: "Die Verbindung zum POP3-Server schlägt mit einem Authentisierungsfehler fehl. Überprüfe deine <a href='/admin/site_settings/category/email'>POP3-Einstellungen</a>." @@ -873,13 +871,11 @@ de: set_locale_from_accept_language_header: "Sprache der Benutzeroberfläche für anonyme Benutzer an Hand der Spracheinstellung ihres Browsers wählen (EXPERIMENTELL, funktioniert nicht mit Caches für anonyme Benutzer)" min_post_length: "Minimal zulässige Beitragslänge in Zeichen." min_first_post_length: "Minimal zulässige Länge des ersten Beitrags (eines Themas) in Zeichen" - min_personal_message_post_length: "Minimale zulässige Beitragslänge in Zeichen für Nachrichten" max_post_length: "Maximale zulässige Beitragslänge in Zeichen." topic_featured_link_enabled: "Beitrag mit Link zu hervorgehobenen Themen erlauben" show_topic_featured_link_in_digest: "Zeige den Hervorgehobene Themen Link in der E-Mail-Zusammenfassung." min_topic_title_length: "Minimale zulässige Titellänge von Themen in Zeichen." max_topic_title_length: "Maximale zulässige Titellänge von Themen in Zeichen." - min_personal_message_title_length: "Minimale zulässige Titellänge von Nachrichten in Zeichen." min_search_term_length: "Minimale zulässige Länge der Suche in Zeichen." search_tokenize_chinese_japanese_korean: "Zwinge die Suche Chinesisch, Japanisch und Koreanisch zu erkennen, auch wenn die Site keine dieser Sprachen nutzt" search_prefer_recent_posts: "Wenn das Durchsuchen deines großen Forums langsam ist, dann versucht diese Option zuerst einen Index der letzten Beiträge." @@ -934,8 +930,6 @@ de: summary_likes_required: "Mindestanzahl an Likes in einem Thema, bevor die \"Thema zusammenfassen\" Funktion aktiviert wird." summary_percent_filter: "Zeige die besten (n)% der Beiträge eines Themas in der \"Thema zusammenfassen\"-Ansicht." summary_max_results: "Maximale Anzahl der sichtbaren Beiträge beim Zusammenfassen von Themen" - enable_personal_messages: "Erlaube Benutzer mit der Vertrauensstufe 1 (konfigurierbar über die minimale Vertrauensstufe zum Senden von Nachrichten), Nachrichten zu erstellen und auf Nachrichten zu antworten. Beachte, dass das Team immer Nachrichten und Antworten senden können." - enable_personal_email_messages: "Erlaube Benutzern mit Vertrauensstufe 4 (konfigurierbar über die Mindestvertrauensstufe, um Nachrichten zu senden), private E-Mail-Nachrichten zu senden. Bitte beachte, dass Team-Mitglieder immer und in jedem Fall Nachrichten senden können." enable_long_polling: "Nachrichtenbus für Benachrichtigungen kann Long-Polling nutzen." long_polling_base_url: "Basis-URL für Long Polling (wenn zum Ausliefern von dynamischen Inhalten ein CDN verwendet wird, setze es auf Origin Pull), z. B. http://origin.site.com" long_polling_interval: "Wartezeit, bevor der Server auf Clients reagiert, wenn keine Daten gesendet werden müssen (nur für angemeldete Benutzer)" @@ -1070,7 +1064,6 @@ de: max_bookmarks_per_day: "Maximale Anzahl der Lesezeichen pro Benutzer und Tag." max_edits_per_day: "Maximale Anzahl der Bearbeitungen pro Benutzer und Tag." max_topics_per_day: "Maximale Anzahl der Themen, die ein Benutzer pro Tag erstellen kann." - max_personal_messages_per_day: "Maximale Zahl Direktnachrichten, die ein Benutzer pro Tag erstellen kann." max_invites_per_day: "Maximale Zahl an Einladungen, die ein Benutzer pro Tag verschicken kann." max_topic_invitations_per_day: "Maximale Zahl an Thema-Einladungen, die ein Benutzer pro Tag verschicken kann." max_logins_per_ip_per_hour: "Maximale Anzahl der erlaubten Anmeldungen pro IP-Adresse und Stunde" @@ -1141,7 +1134,6 @@ de: enable_mentions: "Erlaube Benutzern, andere Benutzer zu erwähnen." create_thumbnails: "Erzeuge ein Vorschaubild und eine Lightbox für Bilder, die zu groß sind, um in einem Beitrag angezeigt zu werden." email_time_window_mins: "Warte (n) Minuten bevor eine E-Mail-Benachrichtigung geschickt wird, um Benutzern Gelegenheit zu geben, ihre Beiträge abschließend bearbeiten zu können." - personal_email_time_window_seconds: "Warte (n) Sekunden bevor eine E-Mail-Benachrichtigung geschickt wird, um Benutzern Gelegenheit zu geben, ihre Beiträge abschließend bearbeiten zu können." email_posts_context: "Anzahl der Antworten, welche als Kontext einer Benachrichtigungs-E-Mail hinzugefügt werden." flush_timings_secs: "Sekunden, nach denen Zeiteinstellungen auf den Server übertragen werden." title_max_word_length: "Maximal erlaubte Wortlänge in Thementiteln, in Zeichen." @@ -1320,7 +1312,6 @@ de: watched_words_regular_expressions: "Beobachtete Wörter sind reguläre Ausdrücke." default_email_digest_frequency: "Wie häufig sollen Benutzer standardmäßig E-Mail-Zusammenfassungen erhalten?" default_include_tl0_in_digests: "Beiträge von neuen Benutzern in E-Mail-Zusammenfassungen standardmäßig anzeigen. Benutzer können dies in ihren Einstelllungen ändern." - default_email_personal_messages: "Sende einem Benutzer standardmäßig eine E-Mail, wenn dieser eine Nachricht von einem anderen Benutzer erhält." default_email_direct: "Aktiviere standardmäßig, dass eine E-Mail gesendet wird, sobald ein Benutzer einen anderen Benutzer zitiert / einem anderen Benutzer antwortet / oder einen anderen Benutzer erwähnt bzw. einlädt." default_email_mailing_list_mode: "Sende standardmäßig eine E-Mail für jeden neuen Beitrag." default_email_mailing_list_mode_frequency: "Benutzer, die den Mailinglisten-Modus einschalten, werden standardmäßig so häufig eine E-Mail erhalten." @@ -2451,21 +2442,6 @@ de: signup_after_approval: title: "Konto bestätigen nach Genehmigung" subject_template: "You've been approved on %{site_name}!" - text_body_template: | - Willkommen bei%{site_name}! - - Ein Team-Mitglied hat dein Benutzerkonto auf %{site_name} bestätigt. - - Klicke auf den folgenden Link, um dein neues Konto zu bestätigen und zu aktivieren: - %{base_url}/u/activate-account/%{email_token} - - Wenn sich der obenstehende Link nicht anklicken lässt, versuche ihn zu kopieren und in die Adresszeile deines Webbrowsers einzufügen. - - %{new_user_tips} - - Wir glauben an [zivilisiertes Community-Verhalten](%{base_url}/guidelines) zu jeder Zeit. - - Genieße deinen Aufenthalt! signup: title: "Konto bestätigen nach Anmeldung" subject_template: "[%{email_prefix}] Bestätige dein neues Konto" diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index 28aafed69e1..1b01dce4bb3 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -454,7 +454,6 @@ el: broken: "Η εικόνα είναι χαλασμένη" rate_limiter: slow_down: "Η εντολή αυτή έχει εκτελεστεί πάρα πολλές φορές, δοκιμάστε ξανά αργότερα." - too_many_requests: "Υπάρχει περιορισμός στις φορές που μπορεί να επαναληφθεί αυτή η ενέργεια. Παρακαλούμε παριμένετε %{time_left} προτού δοκιμάσετε ξανά." by_type: first_day_replies_per_day: "Έχετε φτάσει τον μέγιστο αριθμό απαντήσεων που δικαιούται ένας νέος χρήστης την πρώτη του μέρα. Παρακαλώ περιμένετε %{time_left} πριν προσπαθήσετε ξανά." first_day_topics_per_day: "Έχεται φτάσει τον μέγιστο αριθμό θεμάτων που δικαιούται ένας νέος χρήστης την πρώτη του μέρα. Παρακαλώ περιμένετε %{time_left} πριν προσπαθήσετε ξανά." @@ -860,7 +859,6 @@ el: email_polling_errored_recently: one: "To Email polling έχει παράγει ένα σφάλμα τις τελευταίες 24 ώρες . Δείτε <a href='/logs' target='_blank'>τα αρχεία καταγραφής</a> για περισσότερες λεπτομέρειες. " other: "To Email polling έχει παράγει %{count} σφάλματα τις τελευταίες 24 ώρες . Δείτε <a href='/logs' target='_blank'>τα αρχεία καταγραφής</a> για περισσότερες λεπτομέρειες. " - missing_mailgun_api_key: "Ο διακομιστής εχεί ρυθμιστεί για να στέλνει email μέσω του mailgun, αλλά δεν εχεις ρυθμίσει το κλειδί API που χρειάζεται για να επαληθευτούν τα webhook μυνήματα. " bad_favicon_url: "Το favicon δεν μπορεί να φορτωθεί 'Ελεγξε το favicon_url στις <a href='/admin/site_settings'>Ρυθμίσεις Iστοσελίδας</a>" poll_pop3_timeout: "Το χρονικό όριο σύνδεσης με τον διακομιστή POP3 έληξε. Τα εισερχόμενα email δεν μπορούν να ανακτηθούν. Παρακαλώ ελέγξτε τις <a href='/admin/site_settings/category/email'>POP3 ρυθμίσεις σας</a> και τον πάροχο υπηρεσιών." poll_pop3_auth_error: "Υπήρξε σφάλμα ελεχγου ταυτότητας κατά την σύνδεση με τον διακομιστή POP3. Παρακαλώ όπως ελέξετε τις <a href='/admin/site_settings/category/email'> POP3 ρυθμίσεις σας.</a> " @@ -872,13 +870,11 @@ el: set_locale_from_accept_language_header: "θέστε την γλώσσα του interface για ανώνυμους χρήστες από τη γλώσσα που χρησιμοποιεί το πρόγραμμα περιήγησης τους. (ΠΕΙΡΑΜΑΤΙΚΟ ΧΑΡΑΚΤΗΡΙΣΤΙΚΟ, δεν λειτουργεί με ανώνυμη cache)" min_post_length: "Ελάχιστο επιτρεπτό μήκος ανάρτησης σε χαρακτήρες" min_first_post_length: "Ελάχιστο επιτρεπτό μήκος πρώτης ανάρτησης (σώμα νήματος) σε χαρακτήρες" - min_personal_message_post_length: "Ελάχιστο επιτρεπτό μήκος ανάρτησης σε χαρακτήρες για μηνύματα" max_post_length: "Μέγιστο επιτρεπτό μήκος ανάρτησης σε χαρακτήρες" topic_featured_link_enabled: "Ενεργοποίηση ανάρτησης ενός συνδέσμου με νήματα." show_topic_featured_link_in_digest: "Δείξε το σύνδεσμο προτεινόμενου νήματος στο συνοπτικό email." min_topic_title_length: "Ελάχιστο επιτρεπτό μήκος τίτλου νήματος σε χαρακτήρες" max_topic_title_length: "Μέγιστο επιτρεπτό μήκος τίτλου νήματος σε χαρακτήρες" - min_personal_message_title_length: "Ελάχιστο επιτρεπτό μήκος τίτλου για ένα μήνυμα σε χαρακτήρες" min_search_term_length: "Ελάχιστο έγκυρο μήκος όρου αναζήτησης σε χαρακτήρες" search_tokenize_chinese_japanese_korean: "Ανάγκασε την αναζήτηση να κάνει tokenize Κινέζικα / Ιαπωνικά / κορεατικά, ακόμη και σε μη CJK ιστότοπους" search_prefer_recent_posts: "Εάν η αναζήτηση στην ιστοσελίδα σας είναι αργή, αυτή η επιλογή δημιουργεί κατάλογο των πιο πρόσφατων αναρτήσεων πρώτα" @@ -933,8 +929,6 @@ el: summary_likes_required: "Ελάχιστος αριθμός ''Μου αρέσει'' σε ένα θέμα πριν ενεργοποιηθεί το «Συνοψίστε αυτό το θέμα» " summary_percent_filter: "Όταν ο χρήστης επιλέξει «Συνοψίστε αυτό το θέμα», δείξε το κορυφαίο % των αναρτήσεων" summary_max_results: "Μέγιστος αριθμός αναρτήσεων στο «Συνοψίστε αυτό το θέμα»" - enable_personal_messages: "Επίτρεψε στους χρήστες επιπέδου εμπιστοσύνης 1 (ρυθμιζόμενο μέσω min trust level to send messages) να δημιουργήσουν μηνύματα και να απαντήσουν σε μηνύματα. Σημειώστε ότι οι συνεργάτες μπορούν πάντα να στέλνουν μηνύματα." - enable_personal_email_messages: "Επίτρεψε στους χρήστες επιπέδου εμπιστοσύνης 4 (ρυθμιζόμενο μέσω min trust level to send messages) να στέλνουν προσωπικά μηνύματα email. Σημειώστε ότι οι συνεργάτες μπορούν πάντα να στέλνουν μηνύματα." enable_long_polling: "Η αρτηρία μηνυμάτων που χρησιμοποιείτε για ειδοποιήσεις μπορεί να χρησιμοποιήσει μακρυά μέθοδο εξέτασης." long_polling_base_url: "Base URL που χρησιμοποιείτε για μακρύ ψήφισμα (όταν ένα CDN εξυπηρετεί δυναμικό περιεχόμενο, βεβαιωθείτε ότι το ορίσατε σε έλξη προέλευσης ) π.χ.: http://origin.site.com" long_polling_interval: "Χρονικό διάστημα που ο διακομιστής θα πρέπει να περιμένει πριν απαντήσει στους πελάτες όταν δεν υπάρχουν δεδομένα για την αποστολή (μόνο συνδεδεμένοι χρήστες )" @@ -1067,7 +1061,6 @@ el: max_bookmarks_per_day: "Μέγιστος αριθμός σελιδοδεικτών ανά χρήστη ανά ημέρα." max_edits_per_day: "Μέγιστος αριθμός επεξεργασιών ανά χρήστη ανά ημέρα." max_topics_per_day: "Μέγιστος αριθμός νημάτων που μπορεί να δημιουργήσει ο χρήστης σε μια ημέρα." - max_personal_messages_per_day: "Μέγιστος αριθμός μηνυμάτων που μπορεί να δημιουργήσει ο χρήστης σε μια ημέρα." max_invites_per_day: "Μέγιστος αριθμός προσκλήσεων που μπορεί να στείλει ο χρήστης σε μια ημέρα." max_topic_invitations_per_day: "Μέγιστος αριθμός προσκλήσεων νημάτων που μπορεί να στείλει ο χρήστης σε μια ημέρα." max_logins_per_ip_per_hour: "Μέγιστος αριθμός επιτρεπόμενων συνδέσεων ανα διεύθυνση IP την ώρα" @@ -1137,7 +1130,6 @@ el: max_users_notified_per_group_mention: "Μέγιστος αριθμός χρηστών που ίσως λάβουν μια ειδοποίηση εάν μια ομάδα αναφέρεται (εάν φτάσουν το κατώφλι, δεν θα σταλούν ειδοποιήσεις)" create_thumbnails: "Δημιούργησε μικρογραφίες και lightbox για εικόνες που είναι πολύ μεγάλες για να χωρέσουν σε μια ανάρτηση." email_time_window_mins: "Αναμονή (χ) λεπτών πριν την αποστολή οποιουδήποτε ειδοποιητικού email, έτσι ώστε να δωθεί στους χρήστες η ευκαιρία να επεξεργασθούν και να οριστικοποιήσουν τις αναρτήσεις τους. " - personal_email_time_window_seconds: "Αναμονή (χ) δευτερολέπτων πριν την αποστολή οποιουδήποτε προσωπικού ειδοποιητικού email, έτσι ώστε να δωθεί στους χρήστες η ευκαιρία να επεξεργασθούν και να οριστικοποιήσουν τα μηνύματα τους. " email_posts_context: "Πόσες προηγούμενες απαντήσεις να συμπεριλαμβάνονται ως περιεχόμενο στα ειδοποιητικά μηνύματα. " flush_timings_secs: "Πόσο συχνά θα καθαρίσουμε τα δεδομένα χρονισμού στο σέρβερ, σε δευτερόλεπτα." title_max_word_length: "Το μέγιστο επιτρεπόμενο όριο λέξεων, σε χαρακτήρες, σε ένα τίτλο νήματος." @@ -1314,7 +1306,6 @@ el: watched_words_regular_expressions: "Οι λέξεις που επιτηρούνται είναι regular expressions." default_email_digest_frequency: "Πόσο συχνά οι χρήστες λαμβάνουν συνοπτικά email από προεπιλογή." default_include_tl0_in_digests: "Συμπεριέλαβε αναρτήσεις από νέους χρήστες στα συνοπτικά email από προεπιλογή. Οι χρήστες μπορούν να το αλλάξουν αυτό στις προτιμήσεις τους. " - default_email_personal_messages: "Στείλε email όταν κάποιος στέλνει μήνυμα στο χρήστη από προεπιλογή." default_email_direct: "Στείλε email όταν κάποιος παραθέτει/απαντάει σε/αναφέρει ή προσκαλεί το χρήστη από προεπιλογή." default_email_mailing_list_mode: "Στείλε email για κάθε νέα ανάρτηση από προεπιλογή." default_email_mailing_list_mode_frequency: "Οι χρήστες, οι οποίοι έχουν ενεργοποιήσει την λειτουργία ταχυδρομικής λίστας θα λαμβάνουν emails τόσο συχνά από προεπιλογή." @@ -2333,21 +2324,6 @@ el: signup_after_approval: title: "Εγγραφή μετά από έγκριση " subject_template: "Έχετε εγκριθεί στην %{site_name}!" - text_body_template: | - Καλώς ήρθατε στην %{site_name}! - - Ένα μέλος του προσωπικού ενέκρινε τον λογαριασμό σας στην %{site_name}. - - Κάντε κλικ στον παρακάτω σύνδεσμο για να επικυρώσετε και να ενεργοποιήσετε τον νέο λογαριασμό σας: - %{base_url}/u/activate-account/%{email_token} - - Αν για κάποιο λόγο δεν μπορείτε να κάνετε κλικ στον παραπάνω σύνδεσμο, παρακαλούμε αντιγράψτε ολόκληρη την διεύθυνση και επικολλήστε την στην γραμμή διευθύνσεων του περιηγητή σας. - - %{new_user_tips} - - Επικροτούμε πάντα την [πολιτισμένη κοινωνική συμπεριφορά](%{base_url}/guidelines). - - Καλά να περάσετε! signup: title: "Εγγραφή" subject_template: "[%{email_prefix}] Επικυρώστε τον νέο σας λογαριασμό" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 74845543404..1fd79670022 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -49,6 +49,7 @@ en: loading: "Loading" powered_by_html: 'Powered by <a href="https://www.discourse.org">Discourse</a>, best viewed with JavaScript enabled' log_in: "Log In" + submit: "Submit" purge_reason: "Automatically deleted as abandoned, deactivated account" disable_remote_images_download_reason: "Remote images download was disabled because there wasn't enough disk space available." @@ -114,6 +115,7 @@ en: not_an_integer: must be an integer odd: must be odd record_invalid: ! 'Validation failed: %{errors}' + max_emojis: "can't have more than %{max_emojis_count} emoji" restrict_dependent_destroy: one: "Cannot delete record because a dependent %{record} exists" many: "Cannot delete record because dependent %{record} exist" @@ -206,6 +208,7 @@ en: too_many_mentions_newuser: one: "Sorry, new users can only mention one other user in a post." other: "Sorry, new users can only mention %{count} users in a post." + no_images_allowed_trust: "Sorry, you can't put images in a post" no_images_allowed: "Sorry, new users can't put images in posts." too_many_images: one: "Sorry, new users can only put one image in a post." @@ -538,7 +541,7 @@ en: rate_limiter: slow_down: "You have performed this action too many times, try again later." - too_many_requests: "We have a daily limit on how many times that action can be taken. Please wait %{time_left} before trying again." + too_many_requests: "You have performed this action too many times. Please wait %{time_left} before trying again." by_type: first_day_replies_per_day: "You've reached the maximum number of replies a new user can create on their first day. Please wait %{time_left} before trying again." first_day_topics_per_day: "You've reached the maximum number of topics a new user can create on their first day. Please wait %{time_left} before trying again." @@ -961,7 +964,7 @@ en: email_polling_errored_recently: one: "Email polling has generated an error in the past 24 hours. Look at <a href='/logs' target='_blank'>the logs</a> for more details." other: "Email polling has generated %{count} errors in the past 24 hours. Look at <a href='/logs' target='_blank'>the logs</a> for more details." - missing_mailgun_api_key: "The server is configured to send emails via mailgun but you haven't provided an API key used to verify the webhook messages." + missing_mailgun_api_key: "The server is configured to send emails via Mailgun but you haven't provided an API key used to verify the webhook messages." bad_favicon_url: "The favicon is failing to load. Check your favicon_url setting in <a href='/admin/site_settings'>Site Settings</a>." poll_pop3_timeout: "Connection to the POP3 server is timing out. Incoming email could not be retrieved. Please check your <a href='/admin/site_settings/category/email'>POP3 settings</a> and service provider." poll_pop3_auth_error: "Connection to the POP3 server is failing with an authentication error. Please check your <a href='/admin/site_settings/category/email'>POP3 settings</a>." @@ -984,6 +987,7 @@ en: min_topic_title_length: "Minimum allowed topic title length in characters" max_topic_title_length: "Maximum allowed topic title length in characters" min_personal_message_title_length: "Minimum allowed title length for a message in characters" + max_emojis_in_title: "Maximum allowed emojis in topic title" min_search_term_length: "Minimum valid search term length in characters" search_tokenize_chinese_japanese_korean: "Force search to tokenize Chinese/Japanese/Korean even on non CJK sites" search_prefer_recent_posts: "If searching your large forum is slow, this option tries an index of more recent posts first" @@ -1163,8 +1167,8 @@ en: google_oauth2_client_secret: "Client secret of your Google application." enable_twitter_logins: "Enable Twitter authentication, requires twitter_consumer_key and twitter_consumer_secret" - twitter_consumer_key: "Consumer key for Twitter authentication, registered at http://dev.twitter.com" - twitter_consumer_secret: "Consumer secret for Twitter authentication, registered at http://dev.twitter.com" + twitter_consumer_key: "Consumer key for Twitter authentication, registered at https://apps.twitter.com/" + twitter_consumer_secret: "Consumer secret for Twitter authentication, registered at https://apps.twitter.com/" enable_instagram_logins: "Enable Instagram authentication, requires instagram_consumer_key and instagram_consumer_secret" instagram_consumer_key: "Consumer key for Instagram authentication" @@ -1301,6 +1305,7 @@ en: min_trust_to_flag_posts: "The minimum trust level required to flag posts" min_trust_to_post_links: "The minimum trust level required to include links in posts" + min_trust_to_post_images: "The minimum trust level required to include images in a post" newuser_max_links: "How many links a new user can add to a post." newuser_max_images: "How many images a new user can add to a post." @@ -1334,6 +1339,7 @@ en: max_image_size_kb: "The maximum image upload size in kB. This must be configured in nginx (client_max_body_size) / apache or proxy as well." max_attachment_size_kb: "The maximum attachment files upload size in kB. This must be configured in nginx (client_max_body_size) / apache or proxy as well." authorized_extensions: "A list of file extensions allowed for upload (use '*' to enable all file types)" + authorized_extensions_for_staff: "A list of file extensions allowed for upload for staff users in addition to the list defined in the `authorized_extensions` site setting. (use '*' to enable all file types)" theme_authorized_extensions: "A list of file extensions allowed for theme uploads (use '*' to enable all file types)" max_similar_results: "How many similar topics to show above the editor when composing a new topic. Comparison is based on title and body." @@ -1470,6 +1476,7 @@ en: staff_user_custom_fields: "A whitelist of custom fields for a user that can be shown to staff." enable_user_directory: "Provide a directory of users for browsing" enable_group_directory: "Provide a directory of groups for browsing" + group_in_subject: "Set %{optional_pm} in email subject to name of first group in PM, see: https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" allow_anonymous_posting: "Allow users to switch to anonymous mode" anonymous_posting_min_trust_level: "Minimum trust level required to enable anonymous posting" anonymous_account_duration_minutes: "To protect anonymity create a new anonymous account every N minutes for each user. Example: if set to 600, as soon as 600 minutes elapse from last post AND user switches to anon, a new anonymous account is created." @@ -1756,6 +1763,7 @@ en: login: not_approved: "Your account hasn't been approved yet. You will be notified by email when you are ready to log in." incorrect_username_email_or_password: "Incorrect username, email or password" + incorrect_password: "Incorrect password" wait_approval: "Thanks for signing up. We will notify you when your account has been approved." active: "Your account is activated and ready to use." activate_email: "<p>You’re almost done! We sent an activation mail to <b>%{email}</b>. Please follow the instructions in the mail to activate your account.</p><p>If it doesn’t arrive, check your spam folder.</p>" @@ -1778,6 +1786,9 @@ en: auth_complete: "Authentication is complete." click_to_continue: "Click here to continue." already_logged_in: "Oops, looks like you are attempting to accept an invitation for another user. If you are not %{current_user}, please log out and try again." + second_factor_title: "Two Factor Authentication Required" + second_factor_description: "Enter a generated authentication code." + invalid_second_factor_code: "Invalid Two Factor Authentication Code" user: no_accounts_associated: "No accounts associated" @@ -2725,6 +2736,15 @@ en: + account_second_factor_disabled: + title: "Two Factor Authentication disabled" + subject_template: "[%{email_prefix}] Two Factor Authentication disabled" + text_body_template: | + Your account’s Two Factor Authentication at %{site_name} has been disabled. The account no longer needs a Two Factor Authentication code to sign in. + + If you have any questions, [contact our friendly staff](%{base_url}/about). + + digest: why: "A brief summary of %{site_link} since your last visit on %{last_seen_at}" since_last_visit: "Since your last visit" diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index e7e77f23c39..a8e29003216 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -465,7 +465,6 @@ es: broken: "Esta imagen está rota" rate_limiter: slow_down: "Has realizado esta acción muchas veces, inténtalo de nuevo más tarde." - too_many_requests: "Estas haciendo eso demasiado a menudo. Por favor espera %{time_left} antes de intentarlo de nuevo." by_type: first_day_replies_per_day: "Has llegado al límite de respuestas que un nuevo usuario puede crear en su primer día. Por favor, espera %{time_left} antes de intentarlo de nuevo." first_day_topics_per_day: "Has llegado al límite de temas que un nuevo usuario puede crear en su primer día. Por favor, espera %{time_left} antes de intentarlo de nuevo." @@ -869,7 +868,6 @@ es: email_polling_errored_recently: one: "El email polling ha generado un error en las pasadas 24 horas. Mira en <a href='/logs' target='_blank'>los logs</a> para más detalles." other: "El email polling ha generado %{count} errores en las pasadas 24 horas. Mira en <a href='/logs' target='_blank'>los logs</a> para más detalles." - missing_mailgun_api_key: "El servidor está configurado para enviar emails vía mailgun pero no se ha proporcionado una API key para verificar los mensajes webhook." bad_favicon_url: "El favicon está dando fallos en su carga. Revisa la opción favicon_url en <a href='/admin/site_settings'>Ajustes del sitio</a>." poll_pop3_timeout: "La conexión al servidor POP3 está rebasando el tiempo de espera. Los emails entrantes no han podido ser recogidos. Por favor revisa los <a href='/admin/site_settings/category/email'>ajustes de POP3</a> y tu proveedor de servicio." poll_pop3_auth_error: "La conexión al servidor POP3 está fallando debido a un error de autenticación. Por favor revisa los <a href='/admin/site_settings/category/email'>ajustes POP3</a>." @@ -881,13 +879,11 @@ es: set_locale_from_accept_language_header: "Establece el lenguaje de la interfaz para usuarios anónimos desde el lenguaje declarado por su navegador web. (EXPERIMENTAL, no funciona con caché anónimo)" min_post_length: "Extensión mínima de los posts, en número de caracteres" min_first_post_length: "Extensión mínima permitida en el primer mensaje (cuerpo del tema) en caracteres" - min_personal_message_post_length: "Extensión mínima de los posts en los mensajes, en número de caracteres" max_post_length: "Extensión máxima de los posts, en número de caracteres" topic_featured_link_enabled: "Activar publicar temas a partir de un enlace." show_topic_featured_link_in_digest: "Mostrar el enlace destacado por el tema en el email de resumen." min_topic_title_length: "Extensión mínima del título de los temas, en número de caracteres" max_topic_title_length: "Extensión máxima del título de los temas, en número de caracteres" - min_personal_message_title_length: "Extensión mínima del título de los temas en mensajes, en número de caracteres" min_search_term_length: "Extensión mínima de una búsqueda válida, en número de caracteres" search_tokenize_chinese_japanese_korean: "Forzar la búsqueda a tokenizar Chino/Japonés/Coreano incluso en sitios que no basados en esos idiomas" search_prefer_recent_posts: "Si la búsqueda en tu foro gigante es lenta, esta opción prueba primero un índice de posts más recientes" @@ -943,8 +939,6 @@ es: summary_likes_required: "Mínimo de \"me gusta\" en un tema para habilitar 'Resumen de este tema'" summary_percent_filter: "Cuando un usuario hace clic en 'Resumen de este tema', se muestra el n % mejores posts" summary_max_results: "Máximo de posts devueltos en \"Resumen de este tema\"" - enable_personal_messages: "Permitir a los usuarios con nivel 1 de confianza (configurable vía mínimo nivel para enviar mensajes) crear y responder a mensajes directos. Ten en cuenta que el staff siempre puede enviar mensajes directos." - enable_personal_email_messages: "Permita que el nivel de confianza 4 (configurable a través del nivel mínimo de confianza para enviar mensajes) envíe mensajes de correo electrónico privados. Tenga en cuenta que el personal siempre puede enviar mensajes, pase lo que pase." enable_long_polling: "Los mensajes usados para notificaciones pueden usar el long polling" long_polling_base_url: "URL base usada para el 'long polling' (cuando un CDN esta sirviendo contenido dinámico, asegúrate de ajustar esto al 'pull' de origen) ejemplo: http://origin.site.com" long_polling_interval: "Cantidad de tiempo que el servidor debe de esperar antes de responder a los clientes que no hay datos enviados (solamente usuarios con sesión iniciada)." @@ -1079,7 +1073,6 @@ es: max_bookmarks_per_day: "Máximo número de marcadores por usuario y día." max_edits_per_day: "Máximo número de ediciones por usuario y día." max_topics_per_day: "Máximo número de temas que un usuario puede crear al día." - max_personal_messages_per_day: "Máximo número de mensajes por usuario y día." max_invites_per_day: "Máximo número de invitaciones que un usuario puede enviar al día." max_topic_invitations_per_day: "Máximo número de invitaciones a un tema que un usuario puede enviar por día." max_logins_per_ip_per_hour: "Máximo número de inicios de sesión permitidos por direcciones IP por hora." @@ -1150,7 +1143,6 @@ es: enable_mentions: "Permitir a los usuarios mencionar a otros usuarios." create_thumbnails: "Crear miniaturas de imágenes y lightbox cuando estas son demasiado grandes para encajar en un post." email_time_window_mins: "Esperar (n) minutos antes de enviar cualquier email de notificación, para dar a los usuarios margen con el que editar y finalizar sus posts." - personal_email_time_window_seconds: "Espera (n) segundos antes de enviar cualquier email de notificación, para dar a los usuarios margen con el que editar y finalizar sus mensajes." email_posts_context: "Cuántas respuestas previas se incluirán como contexto en los emails de notificación." flush_timings_secs: "Cuán frecuente, en segundos, se alinean los datos de sincronización con el servidor." title_max_word_length: "La longitud máxima permitida de una palabra, en caracteres, en el título del tema." @@ -1329,7 +1321,6 @@ es: watched_words_regular_expressions: "Palabras observadas son expresiones regulares." default_email_digest_frequency: "Cuán a menudo recibirán los usuarios emails de resumen por defecto." default_include_tl0_in_digests: "Incluir temas de usuarios nuevos en los emails de resumen por defecto. Los usuarios pueden cambiar esto en sus preferencias." - default_email_personal_messages: "Enviar un email cuando alguien envíe un mensaje al usuario por defecto." default_email_direct: "Enviar un email cuando alguien cite/responda/mencione o invite al usuario por defecto." default_email_mailing_list_mode: "Enviar un email por cada nuevo post por defecto." default_email_mailing_list_mode_frequency: "Los usuarios que activen el modo lista de correo recibirán correos con esta frecuencia por defecto." @@ -2465,21 +2456,6 @@ es: signup_after_approval: title: "Entra después de ser aprobado" subject_template: "¡Tu solicitud ha sido aprobada en %{site_name}!" - text_body_template: | - ¡Te damos la bienvenida a %{site_name}! - - Un miembro del staff aprobó tu cuenta en %{site_name}. - - Haz clic en el siguiente enlace para confirmar y activar tu nueva cuenta: - %{base_url}/u/activate-account/%{email_token} - - Si no puedes hacer clic en el enlace, intenta copiándolo y pegándolo en la barra de direcciones de tu navegador. - - %{new_user_tips} - - Creemos en una comunidad con un [comportamiento civilizado](%{base_url}/guidelines). - - ¡Disfruta de tu estancia! signup: title: "Registrate" subject_template: "[%{email_prefix}] Confirma tu nueva cuenta" diff --git a/config/locales/server.et.yml b/config/locales/server.et.yml index 2bf46d961c3..92c9fe85225 100644 --- a/config/locales/server.et.yml +++ b/config/locales/server.et.yml @@ -357,7 +357,6 @@ et: change_failed_explanation: "Üritasid alandada %{user_name} tasemele '%{new_trust_level}'. Samas on tal juba tase'%{current_trust_level}'. %{user_name} jääb tasemele '%{current_trust_level}' - kui soovid alandada kasutaja taset, siis lukusta kõigepealt usaldustase." rate_limiter: slow_down: "Oled antud seda toimingut liiga palju proovinud. Proovi hiljem uuesti." - too_many_requests: "Antud tegevuse jaoks on meil päevane kordade limiit. Palun oota %{time_left} enne kui uuesti proovid." by_type: first_day_replies_per_day: "Oled ületanud uuele kasutajale esimesel päeval lubatud vastuste limiidi. Palun oota %{time_left} enne kui uuesti proovid." first_day_topics_per_day: "Oled ületanud uuele kasutajale esimesel päeval lubatud teemade lisamise limiidi. Palun oota %{time_left} enne kui uuesti proovid." @@ -679,7 +678,6 @@ et: max_post_length: "Maksimaalne lubatud postituse pikkus tähemärkides" min_topic_title_length: "Lühim lubatud teema pealkirja pikkus tähemärkides" max_topic_title_length: "Maksimaalne lubatud teema pealkirja pikkus tähemärkides" - min_personal_message_title_length: "Minimaalne lubatud teema pealkirja pikkus tähemärkides" enable_instagram_logins: "Luba autentimine Instagrami abil, nõuab instagram_consumer_key ja instagram_consumer_secret" instagram_consumer_key: "Teenusekasutaja avalik võti Instagrami abil autentimiseks" instagram_consumer_secret: "Teenusekasutaja salajane võti Instagrami abil autentimiseks" diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 09693d2d0ef..14abd92a92f 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -423,7 +423,6 @@ fa_IR: broken: "عکس خراب شده است." rate_limiter: slow_down: "شما این عمل را بیش از حد انجام داده اید, بعدا دوباره امتحان کنید." - too_many_requests: "ما روزانه محدودیت زمانی داریم برای اینکه این اقدام چند بار انجام شود. لطفا %{time_left} قبل از اینکه دوباره تلاش کنید صبر کنید . " by_type: first_day_replies_per_day: "شما به حداکثر تعداد پاسخ هایی که یک کاربر تازه میتواند در روز اولش ایجاد کند رسیده اید. لطفا به مدت %{time_left} صبر کنید قبل از اینکه دوباره امتحان کنید." first_day_topics_per_day: "شما به حداکثر تعداد موضوعاتی که یک کاربر تازه میتواند در روز اولش ایجاد کند رسیدهاید. لطفا به مدت %{time_left} صبر کنید قبل از اینکه دوباره امتحان کنید." @@ -797,7 +796,6 @@ fa_IR: subfolder_ends_in_slash: "تنظیمات زیرپوشه نادرست است، مقدار DISCOURSE_RELATIVE_URL_ROOT با نویسهی slash تمام میشود." email_polling_errored_recently: other: "رایگیری ایمیلی %{count} خطا در 24 ساعت گذشته ایجاد کرده. <a href='/logs' target='_blank'>گزارشات</a> را ببینید." - missing_mailgun_api_key: "سرور تنظیم شده که با mailgun ایمیل ارسال کند ولی شما کلید API را برای تایید پیام وبهوک تنظیم نکردید." bad_favicon_url: "favicon نمیتواند بارگذاری شود. مقدار favicon_url را در <a href='/admin/site_settings'>تنظیمات سایت</a> بررسی کنید." poll_pop3_timeout: "اتصال به سرور POP3 انجام نشد. ایمیلهای ورودی ممکن است دریافت نشوند. لطفا <a href='/admin/site_settings/category/email'>تنظیمات POP3 </a>و فراهم کننده خدمات را بررسی کنید." poll_pop3_auth_error: "اتصال به سرور POP3 ناموفق بود، خطای اعتبار سنجی. لطفا <a href='/admin/site_settings/category/email'>تنظیمات POP3</a> را بررسی کنید." @@ -809,13 +807,11 @@ fa_IR: set_locale_from_accept_language_header: "تنظیم زبان برای رابط کاربری کاربران ناشناس که از هدر مرورگر دریافت میشود. (تجربی، با کش ناشناس کار نمیکند)" min_post_length: "حداقل طول مجاز نوشته به نویسه" min_first_post_length: "حداقل طول نوشته به نویسه (topic body)" - min_personal_message_post_length: "حداقل طول نوشته به نویسه در پیامها " max_post_length: "حداکثر طول نوشته به نویسه" topic_featured_link_enabled: "فعال بودن ارسال لینک در موضوعات" show_topic_featured_link_in_digest: "نمایش پیوندهای برجسته موضوع در ایمیل خلاصه" min_topic_title_length: "حداقل طول عنوان نوشته به نویسه" max_topic_title_length: "حداکثر طول مجاز عنوان موضوع به نویسه" - min_personal_message_title_length: "حداقل طول مجاز عنوان برای پیام به نویسه" min_search_term_length: "حداقل طول واژه جستجوی معتبر به نویسه" search_tokenize_chinese_japanese_korean: "اجبار جستجو برای tokenize کردن چینی/ژاپنی/کرهای حتی در سایت هایی که از این زبانها استفاده نمیکنند" search_prefer_recent_posts: "اگر سرعت جستجوی انجمن پایین است، این گزینه آخرین نوشتهها را در ابتدا ایندکس گذاری میکند." @@ -864,7 +860,6 @@ fa_IR: summary_likes_required: "حداقل پسندها در این جستار قبل از اینکه \" خلاصه این موضوع\" فعال شود" summary_percent_filter: "وقتی کاربر بر روی ' خلاصه این موضوع' کلیک کرد٬ % بهترین نوشتهها را نشان بده" summary_max_results: "حداکثر نوشتههای برگردانده شد با \" خلاصه این موضوع\"" - enable_personal_messages: "اجازه ارسال پیام به کاربران سطح اعتماد 1 (قابل تنظیم با حداقل سطح اعتماد برای ارسال پیام). توجه کنید که همکاران در هر شرایطی میتوانند پیام ارسال کنند." enable_long_polling: "message bus استفاده شده برای آگاه سازی می تواند برای رای گیری طولانی استفاده شود. " long_polling_base_url: " URL پایه استفاده شده برای رای گیری طولانی (وقتی CDN خدمت محتوای پویا می دهد٬ از تنظیم بودن منشا این کشش مطمئن شوید) برای نمونه : http://origin.site.com" long_polling_interval: "مدت زمانی که سرور قبل پاسخ دادن به مشتریها باید صبر کند، وقتی در آنجا داده ای برای ارسال نیست (فقط کاربران وارد شده)" @@ -986,7 +981,6 @@ fa_IR: max_bookmarks_per_day: "حداکثر تعداد نشانک هر کاربر در روز" max_edits_per_day: "حداکثر تعداد ویرایش هر کاربر در روز" max_topics_per_day: "حداکثر تعداد موضوعاتی که هر کاربر در روز می توانند ایجاد کند" - max_personal_messages_per_day: "حداکثر تعداد پیام هایی که هر کاربر در روز می توانند ایجاد کند" max_invites_per_day: "حداکثر تعداد دعوتنامههایی که هر کاربر در روز می توانند ارسال کند" max_topic_invitations_per_day: "حداکثر تعداد دعوتنامههایی که یک کاربر میتواند برای عناوین در یک روز ارسال کند." max_logins_per_ip_per_hour: "حداکثر تعداد ورود به ازای هر آیپی در ساعت" @@ -1055,7 +1049,6 @@ fa_IR: max_users_notified_per_group_mention: "حداکثر تعداد کاربرانی که اگر به گروه اشاره شود، اعلان دریافت میکنند (اگر آستانه براورده شود هیچ اعلانی ارسال نخواهد شد)" create_thumbnails: "ایجاد تصویر بندانگشتی و کادر تصاویر کوچک عکسها را درست که برای جا شدن در نوشته بسیار بزگ هستند" email_time_window_mins: "قبل از ارسال هرگونه آگاهیسازی از طریق ایمیل (n) دقیقه صبر کن، برای اینکه به کاربران فرصت بدهید تا بتوانند نوشتههای خود را ویرایش و نهایی کنند. " - personal_email_time_window_seconds: "قبل از ارسال ایمیل آگاهسازی خصوصی، (n) ثانیه صبر کنید تا به کاربر اجازه ویرایش پیام نهایی خود را بدهید." email_posts_context: "چند پاسخ قبلی تا شامل متن خلاصه در ایمیل های آگاهی سازی شود. " flush_timings_secs: "هر چند وقت یک بار اطلاعات زمانبندی سرور را خالی کنیم، واحد ثانیه" title_max_word_length: "حداکثر طول مجاز کلمه٬ در نویسه، در عنوان موضوع." @@ -1221,7 +1214,6 @@ fa_IR: code_formatting_style: "کد دکمه در composer به صورت پیشفرض این حالت خواهد بود" default_email_digest_frequency: "به صورت پیشفرض هر چند وقت یک بار ایمیل خلاصه دریافت شود." default_include_tl0_in_digests: "قرار دادن نوشتههای کاربران جدید در خلاصه ایمیل به صورت پیشفرض. کاربران میتوانند این تنظیمات را از طریق تنظیمات شخصیشان ویرایش کنند." - default_email_personal_messages: "به صورت پیشفرض وقتی پیامی ارسال میشود به او ایمیل بفرست." default_email_direct: "ارسال ایمیل وقتی نقلقول، پاسخ، اشاره یا دعوت دریافت میکند" default_email_mailing_list_mode: "ارسال ایمیل برای نوشتههای جدید" default_email_mailing_list_mode_frequency: "کاربرانی که ارسال ایمیل را فعال کنند، در این بازه زمانی ایمیل دریافت میکنند." @@ -1990,21 +1982,6 @@ fa_IR: signup_after_approval: title: "ثبت نام بعد از تایید" subject_template: "شما در %{site_name} تایید شدید!" - text_body_template: | - به %{site_name} خوش آمدید! - - حساب کاربری شما در %{site_name} توسط همکاران تایید شده تسن. - - برای فعالسازی حسابکاربری خود روی لینک زیر کلیک کنید: - %{base_url}/u/activate-account/%{email_token} - - اگر لینک بالا قابل کلیک نیست، آن را در نوار آدرس مرورگر اینترنتی خود کپی کنید. - - %{new_user_tips} - - ما همیشه به [رفتار اجتماعی متمدنانه](%{base_url}/guidelines) اعتقاد داریم. - - از بودن در انجمن لذت ببرید! signup: title: "ثبتنام" subject_template: "[%{email_prefix}] حساب کاربری خود را تایید کنید." @@ -2124,6 +2101,7 @@ fa_IR: این نشان را وقتی که اولین پسند را دریافت کنید به شما اعطا میشود. تبریک، نوشتهای ارسال کردید که اعضای انجمن به آن علاقه نشان دادند، باحال، یا مفید بود! autobiographer: name: نویسنده شرححال + description: اطلاعات <a href="/my/preferences">صفحه شخصی</a> خود را تکمیل کرده anniversary: name: سالگرد description: برای یک سال کاربر فعال بوده، حداقل یک نوشته دارد. @@ -2175,13 +2153,13 @@ fa_IR: long_description: | این نشان زمانی اعطا میشود که یک لینک را به اشتراک بگذارید و 1000 کلیک دریافت کند. وای! شما یک بحث جالب را به حضار زیادی ترویج کردید، شما کمک بزرگ به رشد انجمن کردید! first_like: - name: اولین پسندیدن + name: اولین پسند description: یک پست را پسندیده است long_description: | این نشان زمانی اعطا میشود که برای بار اول یک نوشته را با گزینه :heart: بپسندید. پسندیدن نوشتهها یک راه عالی برای ابراز علاقه شما به نوشتههای اعضاست که بدانند نوشتههایشان جالب، مفید، باحال، یا خنده دار است. علاقهتان را به اشتراک بگذارید! first_flag: - name: اولین نشان - description: اولین نوشته را نشانهگذاری کرد + name: اولین پرچم + description: یک نوشته را پرچم گذاری کرده long_description: | این نشان زمانی اعطا میشود که برای بار اول یک نوشته را پرچم گذاری کنید. پرچم گذاری راهی است برای تمیز نگه داشتن انجمن، جای روشنی برای همه. اگر فکر میکنید نوشتهای به هر دلیلی نیاز به توجه مدیریت دارد لطفا از پرچم گذاری دریغ نکنید. میتوانید برای ارسال <b>پیام خصوصی</b> هم پرچم بزنید تا اعضا متوجه ایراد در نوشتههایشان شوند. اگر مشکلی میبینید، :flag_black: پرچمگذاری کنید! promoter: @@ -2205,7 +2183,7 @@ fa_IR: long_description: | این نشان زمانی اعطا میشود که یک پیوند پاسخ یا موضوع را با دکمه اشتراک گذاری، به اشتراک بگذارید. به اشتراک گذاشتن پیوندها یک راه علی برای نمایش دادن موضوعات جالب با سایر مردم جهان و رشد انجمتان است! first_link: - name: اولین پیوند + name: اولین لینک description: لینکی به موضوع دیگر قرار داده long_description: | این نشان زمانی اعطا میشود که برای بار اول پیوندی به موضوع دیگر اضافه کنید. پیوند به موضوعات به خوانندگان موضوع شما کمک میکند گفتگوهای جالب مرتبط را با نمایش اتصال بین موضوعات، پیدا کنند. با آزادی پیوند بگذارید! @@ -2216,6 +2194,7 @@ fa_IR: این نشان زمانی اعطا میشود که برای بار اول یک نوشته را در پاسخ خود نقلقول کنید. نقلقول بخشهای مربوطه کمک میکند که بحثهای موضوع به هم متصل شوند. بهترین راه برای نقل قول انتخاب متن در نوشته و کلیک روی دکمه پاسخ است. سخاوتمندانه نقلقول کنید! read_guidelines: name: خواندن دستورالعملها + description: ' <a href="/guidelines">دستورالعمل های انجمن</a> را خوانده' reader: name: اهل مطالعه description: همهی پاسخهای یک موضوع با ۱۰۰ پاسخ را مطالعه کرده. @@ -2232,18 +2211,18 @@ fa_IR: long_description: | این نشان زمانی اعطا میشود که یک پیوند با 300 کلیک به اشتراک بگذارید. با تشکر از شما بابت لینک شگفتانگیزی که قرار دادید که باعث جلو رفتن بحث و روشن شدن گفتگو شد! famous_link: - name: پیوند معروف + name: پیوند برجسته description: یک پیوند خروجی با 1000 کلیک ارسال کرده long_description: | این نشان زمانی اعطا میشود که یک پیوند با 1000 کلیک به اشتراک بگذارید. وای! شما یک لینک قرار دادید که به طور قابل ملاحظهای سطح گفتگو را با اضافه کردن جزئیات ضروری، زمینه و اطلاعات بالا برد. عالی بود! appreciated: - name: استقبال مینماید - description: 20 پست 1 بار پسندیده شده دارد. + name: تقدیر شده + description: ۱ پسند در ۲۰ پست دریافت کرده long_description: | این نشان زمانی اعطا میشود که حداقل یک پسند در 20 نوشته مختلف خود دریافت کنید. این انجمن از کمکهای شما به گفتگو ها لذت میبرد! respected: name: محترم - description: 100 پست 2 بار پسندیده شده دارد. + description: ۲ پسند در ۱۰۰ پست دریافت کرده long_description: | این نشان زمانی اعطا میشود که حداقل 2 پسند در 100 نوشته مختلف داشته باشید. انجمن در حال بزرگ شدن و احترام به تمام کمکهای شما به گفتگوها است. admired: @@ -2252,17 +2231,17 @@ fa_IR: long_description: | این نشان را در زمانی که حداقل 5 پسند در 300 نوشته مختلف داشته باشید، دریافت میکنید. انجمن همکاری مکرر و کمکهای باکیفیت شما در گفتگوها را تحسین میکند. out_of_love: - name: نبض احساس + name: مهربان description: در یک روز 50 نوشته را پسندیده است. long_description: | این نشان را در زمانی دریافت میکنید که تمام 50 پسند روزانه خود را استفاده کنید. به خاطر داشته باشید که برای پسندیدن نوشتههایی که از آن لذت بردید و تشویق و قدردانی از اعضای جامعه همکار برای ایجاد بحثهای بهتر درآینده، زمانی را اختصاص دهید. higher_love: - name: یکی هست + name: مهرورز description: در 5 روز، روزانه 50 نوشته را پسندیده است. long_description: | این مدال وقتی به شما تعلق میگیرد که برای 5 روز متوالی روزانه 50 بار نوشته دیگران را بپسندید. با تشکر از شما برای تشویق بهترین گفتگوها! crazy_in_love: - name: نگران منی + name: الههی مهر description: در 20 روز، روزانه 50 نوشته را پسندیده است. long_description: | این مدال وقتی به شما تعلق میگیرد که برای 20 روز متوالی روزانه 50 بار نوشته دیگران را بپسندید. شما نمونه فردی هستید که افراد انجمن را به طور منظم تشویق میکنید! @@ -2272,7 +2251,7 @@ fa_IR: long_description: | این مدال زمانی به شما تعلق میگیرد که نوشتههایتان 20 بار پسندیده شده باشد و شما هم 10 نوشته را بپسندید. وقتی شما نوشتههای دیگری را میپسندید زمان پسندیدن نوشتههای سایرین را نیز پیدای میکنید. gives_back: - name: برگشت میدهد + name: قدر شناس description: 100 نوشته پسندیده شده دارد و 100 نوشته را پسندیده است. long_description: | این نشان را بعد از دریافت 100 پسند در نوشتههایتان و پسندیدن 100 نوشته دریافت میکنید. با تشکر از شما! @@ -2283,27 +2262,39 @@ fa_IR: این مدال زمانی به شما تعلق میگیرد که نوشتههایتان 1000 بار پسندیده شده باشد و شما هم 500 نوشته را بپسندید. شما نمونه بخشندگی و قدردانی متقابل هستید :two_hearts:. first_emoji: name: اولین شکلک - description: از یک شکلک در پست استفاده کرد + description: در یک نوشته از شکلک استفاده کرده. long_description: | این نشان در زمانی که برای بار اول یک شکلک به نوشته اضافه کنید به شما تعلق میگیرد :thumbsup:. شکلکها اجازه میدهند که احساسات خود را در نوشته انتقال دهید، از خوشحالی :smiley: تا ناراحتی :anguished: تا عصبانیت :angry: و هرچیزی که در بین آنهاست :sunglasses:. فقط یک : (دو نقطه) تایپ کنید یا از نوار ابزار شکلک در ویرایشگر استفاده کنید تا بتوانید از صدها شکلک انتخاب کنید :ok_hand: first_mention: name: اولین اشاره - description: اشاره به یک کاربر در یک نوشته + description: کاربری را در یک نوشته مخاطب قرار داده. long_description: این نشان را در زمانی که برای بار اول به یک کاربر به صورت @نامکاربری اشاره کنید، دریافت میکنید. هر اشاره به نام کاربری یه اعلان برای آن شخص ارسال میکند، بنابراین آنها از نوشتهی شما مطلع خواهند شد. فقط علامت @ (علامت at) را تایپ کنید تا بتوانید به هر کاربری اشاره کنید یا، اگر اجازه وجود دارد، در گروه. -- این یک راه ساده برای جلب توجه آنها است. first_onebox: - name: اولین جعبه - description: لینک باز شده ارسال کرده + name: اولین لینک قابشده + description: یک لینک قاب شده پست کرده long_description: این مدال زمانی به شما تعلق میگیرد که برای بار اول یک پیوند قرار میدهید، که به صورت خودکار در جعبه باز شده و عنوان و خلاصه و عکس آن (در صورت وجود) نمایش داده میشود. first_reply_by_email: name: اولین پاسخ با ایمیل - description: پاسخ داده شده با ایمیل + description: به یک نوشته از طریق ایمیل پاسخ داده long_description: | این مدال وقتی دریافت میکنید که اولین پاسخ خود با ایمیل را ثبت کنید :e-mail:. new_user_of_the_month: - name: "کاربران جدید این ماه" + name: "تازهوارد منتخب ماه" description: مشارکتهای برجسته در ماه اول long_description: | این مدال را وقتی دریافت میکنید که به دو کاربر در هر ماه به خاطر همکاری هایشان تبریک بگویید، به عنوان اندازه گیری که چقدر نوشتههایشان پسندیده شده و توسط چه کسی. + enthusiast: + name: علاقهمند + description: ۱۰ روز به اینجا سر زده + long_description: 'به به! این مدال رو به شما دادیم چون ده روزه پشت سر هم به اینجا سر زدی. از اینکه بیش از یک هفته با ما هستی ازت متشکریم. ' + aficionado: + name: طرفدار + description: ۱۰۰ روز به اینجا سر زده. + long_description: این نشان برای بازدید ۱۰۰ روز متوالی به شما اهدا شده. یعنی بیشتر از سه ماه! از همراهیتون ممنونیم. + devotee: + name: هواخواه + description: ۳۶۵ روز به اینجا سر زده + long_description: این نشان برای ۳۶۵ روز بازدید متوالی به شما اهدا شده. دمت گرم! یک سال تمام! چه زود گذشت. نه؟ badge_title_metadata: "نشان %{display_name} در %{site_title}" admin_login: success: "ایمیل ارسال شد " diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 2713e7c926c..3031f5181a0 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -452,7 +452,6 @@ fi: broken: "Tämä kuva ei toimi" rate_limiter: slow_down: "Olet tehnyt tämän toiminnon liian monta kertaa, yritä uudelleen myöhemmin." - too_many_requests: "Tämä toiminto voidaan suorittaa vain määrätyn monta kertaa päivässä. Odota %{time_left} ja yritä sitten uudelleen." by_type: first_day_replies_per_day: "Uusi käyttäjä ei voi ensimmäisenä päivänään kirjoittaa enempää viestejä. Odota %{time_left} ja yritä sitten uudelleen." first_day_topics_per_day: "Uusi käyttäjä ei voi ensimmäisenä päivänään aloittaa enempää ketjuja. Odota %{time_left} ja yritä sitten uudelleen." @@ -856,7 +855,6 @@ fi: email_polling_errored_recently: one: "Sähköpostin pollaus on aiheuttanut virheen edellisen 24 tunnin aikana. Tarkastele <a href='/logs' target='_blank'>lokeja</a> saadaksesi lisätietoja." other: "Sähköpostin pollaus aiheutti %{count} virhettä edellisen 24 tunnin aikana. Tarkastele <a href='/logs' target='_blank'>lokeja</a> saadaksesi lisätietoja." - missing_mailgun_api_key: "Palvelin on asetettu lähettämään sähköposti mailgunin kautta, mutta et ole syöttänyt API tunnusta, jolla verifioidaan webhook-viestit." bad_favicon_url: "Faviconin asettaminen epäonnistui. Tarkista favicon_url -asetus <a href='/admin/site_settings'>sivuston asetuksissa</a>." poll_pop3_timeout: "Yhteyttä POP3-palvelimelle aikakatkaistaan ja saapuvaa sähköpostia ei voitu hakea. Tarkista <a href='/admin/site_settings/category/email'>POP3-asetukset</a> ja palveluntarjoaja." poll_pop3_auth_error: "Yhteys POP3-palvelimelle epäonnistuu autentikaatiovirheen vuoksi. Tarkista <a href='/admin/site_settings/category/email'>POP3-asetukset</a>." @@ -868,13 +866,11 @@ fi: set_locale_from_accept_language_header: "Aseta sivuston kieli kirjautumattomille käyttäjille selaimen kielivalinnan perusteella. (KOKEELLINEN, ei toimi anonyymin välimuistin kanssa)" min_post_length: "Viestin merkkien minimimäärä" min_first_post_length: "Ketjun aloitusviestin (leipätekstin) merkkien minimimäärä" - min_personal_message_post_length: "Viestin merkkien minimimäärä viesteille" max_post_length: "Viestin merkkien minimimäärä" topic_featured_link_enabled: "Ota käyttöön ketjulinkit." show_topic_featured_link_in_digest: "Näytä ketjulinkki tiivistelmäsähköpostissa." min_topic_title_length: "Viestin otsikon merkkien minimimäärä" max_topic_title_length: "Viestin otsikon merkkien maksimimäärä" - min_personal_message_title_length: "Viestin otsikon merkkien minimimäärä viestille" min_search_term_length: "Haun merkkien minimimäärä" search_tokenize_chinese_japanese_korean: "Pakota haku käsittelemään kiinaa/japania/koreaa myös muunkielisillä sivustoilla" search_prefer_recent_posts: "Jos hakeminen suurelta palstaltasi on hidasta, tämä asetus kokeilee hakemistorakennetta, jossa tuoreimmat viestit ovat ensin" @@ -928,8 +924,6 @@ fi: summary_likes_required: "Montako tykkäystä ketjussa pitää olla, jotta ketjun tiivistelmä otetaan käyttöön" summary_percent_filter: "Kun käyttäjä klikkaa 'Näytä ketjun tiivistelmä', näytä paras % viesteistä" summary_max_results: "Maksimimäärä viestejä, jotka näytetään ketjun tiivistelmässä" - enable_personal_messages: "Salli luottamustason 1 (muokattavissa asetuksista) käyttäjien lähettää yksityisviestejä ja vastata viesteihin. Huomaa, että henkilökunta voi aina lähettää viestejä." - enable_personal_email_messages: "Salli luottamustason 4 (säädettävissä toisella asetuksella) käyttäjien lähettää yksityisviestitoiminnolla sähköpostia. Huomioi, että henkilökunta voi lähettää riippumatta asetuksista." enable_long_polling: "Ilmoitusten käyttämä viestiväylä voi käyttää long pollingia" long_polling_base_url: "Base URL, jota käytetään long pollingissa (kun CDN on käytössä, varmista että tähän on asetettu origin pull) esim: http://origin.site.com" long_polling_interval: "Kuinka kauan palvelimen pitäisi odottaa ennen vastaamista asiakkaalle, kun ei ole mitään dataa jota lähettää (vain kirjautuneille käyttäjille)" @@ -1062,7 +1056,6 @@ fi: max_bookmarks_per_day: "Kirjanmerkkien päivittäinen maksimimäärä per käyttäjä." max_edits_per_day: "Muokkausten päivittäinen maksimimäärä per käyttäjä." max_topics_per_day: "Kuinka monta ketjua käyttäjä voi aloittaa päivässä." - max_personal_messages_per_day: "Viestien päivittäinen maksimimäärä per käyttäjä." max_invites_per_day: "Maksimimäärä kutsuja, jonka käyttäjä voi lähettää päivässä." max_topic_invitations_per_day: "Maksimimäärä ketjukutsuja, jonka yksittäinen käyttäjä voi lähettää päivässä" max_logins_per_ip_per_hour: "Enimmäismäärä kirjautumisia IP-osoitetta kohden tunnissa" @@ -1133,7 +1126,6 @@ fi: enable_mentions: "Salli käyttäjän mainita toinen käyttäjä." create_thumbnails: "Luo esikatselu- ja lightbox-kuvia, jotka ovat liian suuria mahtuakseen viestiin." email_time_window_mins: "Odota (n) minuuttia ennen ilmoitussähköpostien lähettämistä, jotta käyttäjällä on aikaa muokata ja viimeistellä viestinsä." - personal_email_time_window_seconds: "Odota (n) sekuntia ennen sähköposti-ilmoitusten lähettämistä käyttäjille, jotta kirjoittajalla on mahdollisuus tehdä muokkaukset ja viimeistellä viestinsä." email_posts_context: "Kuinka monta edellistä vastausta liitetään kontekstiksi sähköposti-ilmoituksessa." flush_timings_secs: "Kuinka usein timing data päivitetään palvelimelle, sekunneissa." title_max_word_length: "Sanan enimmäispituus merkkeinä ketjun otsikossa" @@ -1312,7 +1304,6 @@ fi: watched_words_regular_expressions: "Tarkkaillut sanat ovat säännöllisiä lausekkeita." default_email_digest_frequency: "Kuinka usein käyttäjille lähetetään sähköpostikooste oletuksena." default_include_tl0_in_digests: "Sisällytä uusien käyttäjien viestit sähköpostikoosteisiin oletuksena. Tätä voi muuttaa käyttäjäasetuksissa." - default_email_personal_messages: "Lähetä oletuksena sähköposti, kun joku lähettää käyttäjälle viestin." default_email_direct: "Lähetä oletuksena sähköposti, kun joku lainaa/vastaa/mainitsee tai kutsuu käyttäjän." default_email_mailing_list_mode: "Lähetä oletuksena sähköposti jokaisesta uudesta viestistä." default_email_mailing_list_mode_frequency: "Postituslistatilassa käyttäjä saa sähköpostia oletuksena näin usein." @@ -2442,19 +2433,6 @@ fi: signup_after_approval: title: "Liity hyväksynnän jälkeen" subject_template: "Sinut on hyväksytty sivustolle %{site_name}!" - text_body_template: | - Kiitos kun liityit %{site_name} ja tervetuloa! - - Henkilökunnan jäsen hyväksyi tunnuksesi sivustolla %{site_name}. - - Vahvista ja aktivoi uusi tunnuksesi klikkaamalla linkkiä: - %{base_url}/u/activate-account/%{email_token} - - %{new_user_tips} - - Vaalimme [sivistynyttä yhteisökäyttäytymistä](%{base_url}/guidelines) tilanteessa kuin tilanteessa. - - Toivottavasti viihdyt! signup: title: "Liity" subject_template: "[%{email_prefix}] Vahvista uusi käyttäjätilisi" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index abb5e38a641..3d4fd7cb834 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -451,7 +451,6 @@ fr: broken: "Cette image ne fonctionne pas" rate_limiter: slow_down: "Vous avez réalisé cette action un trop grand nombre de fois, essayez à nouveau plus tard." - too_many_requests: "Nous avons une limite journalière du nombre d'actions qui peuvent être effectuées. Veuillez patienter %{time_left} avant de recommencer." by_type: first_day_replies_per_day: "Vous avez atteint le nombre maximum de réponses qu'un nouvel utilisateur peut créer pour son premier jour. Patientez s'il vous plaît %{time_left} avant d'essayer à nouveau." first_day_topics_per_day: "Vous avez atteint le nombre maximum de sujets qu'un nouvel utilisateur peut créer pour son premier jour. Patientez s'il vous plaît %{time_left} avant d'essayer à nouveau." @@ -855,7 +854,6 @@ fr: email_polling_errored_recently: one: "La vérification des courriels a généré une erreur au cours des 24 dernières heures. Vérifiez <a href='/logs' target='_blank'>le journal</a> pour plus de détails." other: "La vérification des courriels a généré %{count} erreurs au cours des 24 dernières heures. Vérifiez <a href='/logs' target='_blank'>le journal</a> pour plus de détails." - missing_mailgun_api_key: "Le serveur est configuré pour envoyer des courriels via Mailgun mais vous n'avez pas spécifié une clé API utilisée pour vérifier les messages du webhook." bad_favicon_url: "Impossible de charger la favicon. Vérifiez le paramètre favicon_url dans les <a href='/admin/site_settings'>paramètres du site</a>" poll_pop3_timeout: "La connexion vers le serveur POP3 a expiré. Les courriels entrants n'ont pas pu être téléchargés. Veuillez vérifier <a href='/admin/site_settings/category/email'>les paramètres POP3</a> et votre fournisseur de service courriel." poll_pop3_auth_error: "La connexion vers le serveur POP3 échoue avec une erreur d'authentification. Veuillez vérifier <a href='/admin/site_settings/category/email'>les paramètres POP3</a>." @@ -867,13 +865,11 @@ fr: set_locale_from_accept_language_header: "configurer la langue de l'interface pour les visiteurs à partir des entêtes de langue de leur navigateur. (EXPÉRIMENTAL, ne fonctionne pas avec le cache anonyme)" min_post_length: "Longueur minimale autorisée des messages en nombre de caractères" min_first_post_length: "Longueur minimale d'un premier message (corps de sujet) en nombre de caractères" - min_personal_message_post_length: "Longueur minimale des messages en nombre de caractères" max_post_length: "Longueur maximale autorisée des messages en nombres de caractères" topic_featured_link_enabled: "Activer la création de sujets avec lien" show_topic_featured_link_in_digest: "Afficher les sujets avec lien dans le résumé par courriel." min_topic_title_length: "Longueur minimale autorisée des titres de sujet en nombre de caractères" max_topic_title_length: "Longueur maximale autorisée des titres de sujet en nombre de caractères" - min_personal_message_title_length: "Longueur minimale pour un titre de message en nombre de caractères" min_search_term_length: "Longueur minimale autorisée du texte saisie avant de lancer une recherche en nombre de caractères" search_tokenize_chinese_japanese_korean: "Forcer la tokenisation dans la recherche chinois/japonais/koréen, même sur des sites non-CJK." search_prefer_recent_posts: "Si la recherche dans votre forum est lente, cette option tente l'indéxation des messages les plus récents en premier" @@ -925,7 +921,6 @@ fr: summary_likes_required: "Nombre de J'aime minimum dans un sujet avant que le 'Résumé du sujet' soit activé" summary_percent_filter: "Quand un utilisateur clique sur « Résumer ce sujet », montrer le top % des messages" summary_max_results: "Nombre maximum de messages retournés par « Résumer ce sujet »" - enable_personal_messages: "Autoriser les utilisateurs de niveau de confiance 1 à créer des messages et à répondre (configurable via le niveau de confiance minimum pour envoyer des messages). Notez que les responsables peuvent toujours envoyer des messages." enable_long_polling: "Utiliser les requêtes longues pour le flux de notifications." long_polling_base_url: "Racine de l'URL utilisée pour les requêtes longues (dans le cas de l'utilisation d'un CDN pour fournir du contenu dynamique, pensez à le configurer en mode \"origin pull\") par exemple : http://origin.site.com" long_polling_interval: "Délai d'attente du serveur avant de répondre aux clients lorsqu'il n'y a pas de données à envoyer\n(réservé aux utilisateurs connectés)" @@ -1048,7 +1043,6 @@ fr: max_bookmarks_per_day: "Nombre maximum de signets par utilisateur et par jour." max_edits_per_day: "Nombre maximum de modifications par utilisateur chaque jour." max_topics_per_day: "Nombre maximum de sujet qu'utilisateur peut créer par jour." - max_personal_messages_per_day: "Nombre maximum de messages que les utilisateurs peuvent créer chaque jour." max_invites_per_day: "Nombre maximum d'invitations qu'un utilisateur peut envoyer par jour." 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" @@ -1117,7 +1111,6 @@ fr: max_users_notified_per_group_mention: "Nombre maximum d'utilisateurs qui peuvent recevoir une notification si un groupe est mentionné (si le seuil est atteint, aucune notification ne sera envoyée)" create_thumbnails: "Créer un aperçu pour les images imbriquées qui sont trop large pour le message." email_time_window_mins: "Attendre (n) minutes avant l'envoi des courriels de notification, afin de laisser une chance aux utilisateurs de modifier ou finaliser leurs messages." - personal_email_time_window_seconds: "Attendre (n) secondes avant d'envoyer des courriels de notification privés, afin de donner aux utilisateurs la chance d'éditer et de finaliser leurs messages." email_posts_context: "Combien de réponses précédentes doit-on inclure dans les courriels de notifications pour situer le contexte." flush_timings_secs: "À quelle fréquence les données de timing doivent être vider, en secondes." title_max_word_length: "Le nombre maximum de caractères dans le titre d'un sujet." @@ -1288,7 +1281,6 @@ fr: watched_words_regular_expressions: "Les mots surveillés sont des expressions régulières." default_email_digest_frequency: "Par défaut, à quelle fréquence les utilisateurs reçoivent les résumés par courriel." default_include_tl0_in_digests: "Par défaut, inclure les messages des nouveaux utilisateurs dans les résumés par courriel. Les utilisateurs peuvent changer cela dans leurs préférences." - default_email_personal_messages: "Envoyer un courriel quand quelqu'un envoie un message à un utilisateur." default_email_direct: "Envoyer un courriel quand quelqu'un cite/répond à/mentionne ou invite un utilisateur." default_email_mailing_list_mode: "Envoyer un courriel pour chaque nouveau message." default_email_mailing_list_mode_frequency: "Par défaut, les utilisateurs ayant activé la liste de diffusion recevront des courriels à cette fréquence." @@ -2236,22 +2228,6 @@ fr: signup_after_approval: title: "Inscription après approbation" subject_template: "Votre compte a été approuvé sur %{site_name} !" - text_body_template: |+ - Bienvenue sur %{site_name} ! - - Un responsable a approuvé votre compte sur %{site_name}. - - Cliquez sur le lien suivant pour confirmer et activer votre nouveau compte : - %{base_url}/u/activate-account/%{email_token} - - Si le lien ci-dessus n'est pas cliquable, essayez de le copier/coller dans la barre d'adresse de votre navigateur internet. - - %{new_user_tips} - - Nous croyons constamment au [comportement communautaire civilisé](%{base_url}/guidelines). - - Amusez-vous bien ! - signup: title: "Inscription" subject_template: "[%{email_prefix}] Activez votre nouveau compte" diff --git a/config/locales/server.gl.yml b/config/locales/server.gl.yml index 72189b93115..770dd3fa4b3 100644 --- a/config/locales/server.gl.yml +++ b/config/locales/server.gl.yml @@ -388,7 +388,6 @@ gl: allow_user_locale: "Permitir os usuarios escoller o idioma da interface" min_post_length: "Número mínimo de caracteres permitido para unha publicación" min_first_post_length: "Número mínimo de caracteres (corpo do tema) permitido para unha primeira publicación" - min_personal_message_post_length: "Número mínimo de caracteres permitido para unha mensaxe" max_post_length: "Número máximo de caracteres permitido para unha publicación" post_undo_action_window_mins: "Número de minutos que os usuarios teñen para desfacer accións recentes nunha publicación (gústames, denuncias, etc)." enable_badges: "Activar o sistema de insignias" diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index d96c139fb0e..e3485605899 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -436,7 +436,6 @@ he: broken: "תמונה זו שבורה" rate_limiter: slow_down: "ביצעתם פעולה זו מספר רב מדי של פעמים. נסו שוב מאוחר יותר." - too_many_requests: "יש לנו מגבלה יומית על מספר הפעמים שניתן לבצע פעולה זו. אנא המתינו %{time_left} לפני ניסיון חוזר." by_type: first_day_replies_per_day: "הגעתם למספר המירבי של תגובות שמתמשים חדשים יכולים ליצור ביומם הראשון. אנא המתינו %{time_left} לפני ניסיון חוזר לבצע פעולה זו." first_day_topics_per_day: "הגעתם למספר המירבי של נושאים שמשתמשים חדשים יכולים ליצור ביומם הראשון. אנא המתינו %{time_left} לפני ניסיון חוזר לבצע פעולה זו." @@ -837,7 +836,6 @@ he: email_polling_errored_recently: one: "ניסיונות שליחת מיילים יצרו תקלה ב 24 השעות האחרונות. צפו ב<a href='/logs' target='_blank'>יומנים</a> לפרטים נוספים." other: "ניסיונות שליחת מיילים יצרו %{count} תקלות ב 24 השעות האחרונות. צפו ב<a href='/logs' target='_blank'>יומנים</a> לפרטים נוספים." - missing_mailgun_api_key: "השרת מכוון לשלוח מיילים באמצעות mailgun אבל לא סיפקתם מפתח API שמוודא את הודעות ה webhook." bad_favicon_url: "ה favicon לא עולה. אנא בדקו את הגדרת ה favicon_url ב <a href='/admin/site_settings'>הגדרות האתר</a>." poll_pop3_timeout: "החיבור לשרת POP3 התנתק. דוא\"ל נכנס לא יכול להשלף ואינו מאוחזר. אנא בדקו את <a href='/admin/site_settings/category/email'> הגדרות ה-POP3 </a> שלכם ואת ספק השירות." poll_pop3_auth_error: "החיבור לשרת POP3 נכשל בשל שגיאת הזדהות. אנא בדקו את <a href='/admin/site_settings/category/email'> הגדרות ה-POP3 </a> שלכם." @@ -849,13 +847,11 @@ he: set_locale_from_accept_language_header: "קבעו את שפת הממשק עבור משתמשים אנונימיים לפי השפה בדפדפן. (נ-י-ס-י-ו-נ-י, לא עובד עם cache אנונימי)" min_post_length: "מספר התווים המותר כאורך מינימלי לפוסט" min_first_post_length: "אורך מינימלי מותר לפוסט ראשון (בגוף הנושא) בתווים " - min_personal_message_post_length: "אורך הפוסט המינימלי המותר בתווים להודעות" max_post_length: "מספר התווים המקסימלי כאורך פוסט" topic_featured_link_enabled: "איפשור של פרסום קישור עם נושאים." show_topic_featured_link_in_digest: "הצגת קישור מומלץ של הנושא במייל התמצות." min_topic_title_length: "מספר התווים המינימלי הנדרש לכותרת נושא" max_topic_title_length: "מספר התווים המקסימלי המותר לכותרת נושא" - min_personal_message_title_length: "אורך הכותרת המנימילי המותר להודעה בתווים" min_search_term_length: "מספר התווים המינמלי התקין כאורך מונח לחיפוש" search_tokenize_chinese_japanese_korean: "אלצו את החיפוש לנתח סינית/יפנית/קוריאנית גם באתרים שאינם בשפות אלו" search_prefer_recent_posts: "אם חיפוש בפורום הגדול שלכם איטי, אופציה זו מנסה לאנדקס קודם כל את הפוסטים החדשים יותר" @@ -904,7 +900,6 @@ he: summary_likes_required: "מינימום הלייקים לנושא לפני שהאפשרות \"סיכום נושא זה\" תתאפשר" summary_percent_filter: "כאשר משתמש/ת מקליקים על \"סיכום נושא זה\", הציגו את % הפוסטים הראשונים" summary_max_results: "מספר הפוסטים שיוחזרו באמצעות \"סיכום נושא זה\"" - enable_personal_messages: "הרשו למשתמשי רמת אמון 1 (ניתן להגדרה באמצעות רמת אמון מינימלית לשליחת הודעות) ליצור הודעות ולענות להודעות. שימו לב שהצוות תמיד יכול לשלוח הודעות, לא משנה מה." enable_long_polling: "באס הודעות שמשמש להתראות יכול להשתמש בתשאול ארוך (long polling)" long_polling_base_url: "בסיס ה-URL שנמצא בשימוש עבור long polling (כאשר CDN מחזיר תוכן דינמי, זכרו להגדיר את ערך זה ל-Origin pull, דוגמת http://origin.site.com)" long_polling_interval: "כמות הזמן שהשרת צריך לחכות לפני שעונה ללקוחות, כאשר אין מידע לשליחה (משתמשים רשומים מחוברים למערכת בלבד)" @@ -1027,7 +1022,6 @@ he: max_bookmarks_per_day: "מספר מקסימלי של סימניות למשתמשים ביום." max_edits_per_day: "מספר עריכות מקסימלי מותר למשתמשים ליום." max_topics_per_day: "מספר מקסימלי של נושאים שמשתמשים יכולים ליצור ביום." - max_personal_messages_per_day: "מספר מקסימלי של הודעות שמשתמשים יכולים ליצור ביום." max_invites_per_day: "מספר מקסימלי של הזמנות שיכולים משתמשים לשלוח ביום." max_topic_invitations_per_day: "מספר מירבי של הזמנות לנושא שמשתמשים יכולים לשלוח ביום. " max_logins_per_ip_per_hour: "מספר מקסימלי של התחברויות מורשות לכל כתובת IP בשעה" @@ -1096,7 +1090,6 @@ he: max_users_notified_per_group_mention: "מספר מקסימלי של משתמשים שיכולים לקבל התראה אם מוזכרת קבוצה (אם מגיעים לסף זה לא תועלה התראה)" create_thumbnails: "יצירת תמונות מוקטנות והארת תמונות גדולות מידי מלהיכלל בפוסט." email_time_window_mins: "המתינו (n) דקות לפני משלוח כל התראת מייל, כדי לאפשר למשתמשים הזדמנות לערוך ולוודא באופן סופי את הפוסטים שלהם." - personal_email_time_window_seconds: "המתינו (n) שניות לפני משלוח מיילים אישיים להתראה, על מנת לאפשר למשתמשים לערוך או לתקן את ההודעה." email_posts_context: "כמה תגובות קודמות יש לכלול כהקשר במיילים עם התראות." flush_timings_secs: "באיזו תדירות אנחנו מזרימים מידע לשרת, בשניות." title_max_word_length: "האורך המקסימלי המותר למילה בכותרת נושא, בתווים. " @@ -1263,7 +1256,6 @@ he: max_allowed_message_recipients: "מספר מקסימלי של נמענים מותר בהודעה." default_email_digest_frequency: "באיזו תדירות משתמשים יקבלו סיכומי מיילים כברירת מחדל." default_include_tl0_in_digests: "כללו פוסטים ממשתמשים חדשים בדוא\"ל מסכם כברירת מחדל. משתמשים יוכלו לשנות זאת בהעדפות האישיות." - default_email_personal_messages: "שלח מייל כשמישהו שולח הודעה למשתמש, בתור ברירת מחדל." default_email_direct: "שלח מייל כשמישהו מצטט/מגיב-ל/מזכיר או מזמין משתמש, בתור ברירת מחדל." default_email_mailing_list_mode: "שלח מייל עבור כל פוסט חדש בתור ברירת מחדל. " default_email_mailing_list_mode_frequency: "משתמשים שאיפשרו את מצב רשימת התפוצה יקבלו מיילים בתדירות זו כברירת מחדל." diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml index 322bc4eb797..7af5a0e37c4 100644 --- a/config/locales/server.id.yml +++ b/config/locales/server.id.yml @@ -219,7 +219,6 @@ id: leader: title: "pemimpin" rate_limiter: - too_many_requests: "Kami membatasi jumlah tindakan itu dapat dilakukan dalam satu hari. Mohon tunggu %{time_left} sebelum mencoba lagi." hours: other: "%{count} jam" minutes: diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 6f381120319..f7cb2cefb9c 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -448,7 +448,6 @@ it: broken: "Questa immagine è assente" rate_limiter: slow_down: "Hai eseguito questa operazione troppe volte, riprova più tardi." - too_many_requests: "C'è un limite giornaliero su quante volte si può fare questa azione. Per favore attendi %{time_left} prima di riprovare." by_type: first_day_replies_per_day: "Hai raggiunto il massimo numero di risposte che può creare un nuovo utente il suo primo giorno. Per favore attendi %{time_left} prima di riprovare." first_day_topics_per_day: "Hai raggiunto il massimo numero di argomenti che può creare un nuovo utente il suo primo giorno. Per favore attendi %{time_left} prima di riprovare." @@ -852,7 +851,6 @@ it: email_polling_errored_recently: one: "Il polling delle email ha generato un errore nelle ultime 24 ore. Controlla <a href='/logs' target='_blank'>i log</a> per maggiori dettagli." other: "Il polling delle email ha generato %{count}errori nelle ultime 24 ore. Controlla <a href='/logs' target='_blank'>i log</a> per maggiori dettagli. " - missing_mailgun_api_key: "Il server è configurato per inviare email via mailgun ma non sono state fornite le API key necessarie per verificare i messaggi webhook." bad_favicon_url: "La favicon non si carica. Verifica la configurazione della tua favicon_url in <a href='/admin/site_settings'>Impostazioni del sito</a>." poll_pop3_timeout: "La connessione al server POP3 sta scadendo. Le email in entrata non sono state scaricate. Per favore verifica il la tua <a href='/admin/site_settings/category/email'>configurazione POP3</a> e il fornitore di servizi." poll_pop3_auth_error: "La connessione al server POP3 è fallita per un errore di autenticazione. Per favore verifica la tua <a href='/admin/site_settings/category/email'>configurazione POP3</a>." @@ -864,13 +862,11 @@ it: set_locale_from_accept_language_header: "imposta la lingua di interfaccia per gli utenti anonimi in base ai language header del loro browser.\n(SPERIMENTALE, non funziona con cache anonima)" min_post_length: "Lunghezza minima dei messaggi in caratteri" min_first_post_length: "Lunghezza minima del primo messaggio (corpo del testo), in caratteri" - min_personal_message_post_length: "Lunghezza minima in caratteri per i messaggi" max_post_length: "Lunghezza massima dei messaggi in caratteri" topic_featured_link_enabled: "Abilita la pubblicazione di collegamenti con gli argomenti." show_topic_featured_link_in_digest: "Mostra i collegamenti in primo piano dell'argomento nella email riepilogativa." min_topic_title_length: "Numero minimo di caratteri per i titoli degli argomenti" max_topic_title_length: "Numero massimo di caratteri per i titoli degli argomenti" - min_personal_message_title_length: "Numero minimo di caratteri per un messaggio" min_search_term_length: "Numero minimo di caratteri per le parole cercate" search_tokenize_chinese_japanese_korean: "Attiva la tokenizzazione dei caratteri Cinesi/Giapponesi/Coreani nella ricerca anche sui siti non CJK" search_prefer_recent_posts: "Se il tuo forum è corposo e la ricerca è lenta, questa opzione tenta di indicizzare prima i messaggi più recenti" @@ -925,8 +921,6 @@ it: summary_likes_required: "Minimo numero di \"Mi piace\" in un argomento affinché venga abilitato 'Riassumi Questo Argomento'" summary_percent_filter: "Quando un utente clicca su 'Riassumi Questo Argomento', mostra i primi % messaggi" summary_max_results: "Massimo numero di messaggi mostrati in 'Riassumi Argomento'" - enable_personal_messages: "Autorizza gli utenti con livello di esperienza 1 (configurabile attraverso \"min livello di esperienza per l'invio di messaggi\") a creare e rispondere ai messaggi. Nota che lo staff può inviare messaggi in ogni caso." - enable_personal_email_messages: "Consenti agli utenti a livello di esperienza 4 (configurabile attraverso \"min trust level to send messages\") di inviare messaggi privati via email . Nota che lo staff può inviare messaggi in ogni caso." enable_long_polling: "Il message bus per le notifiche può usare il long polling" long_polling_base_url: "URL di base usato per il long polling (quando una CDN serve contenuto dinamico, bisogna impostarlo come origin pull) es. http://origin.site.com" long_polling_interval: "Tempo di attesa prima che il server risponda ai client che non ci sono dati da trasmettere (solo per utenti autenticati)" @@ -1059,7 +1053,6 @@ it: max_bookmarks_per_day: "Massimo numero di segnalibri per utente al giorno." max_edits_per_day: "Massimo numero di modifiche per utente al giorno." max_topics_per_day: "Massimo numero di argomenti che un utente può creare al giorno." - max_personal_messages_per_day: "Numero massimo di messaggi che gli utenti possono creare al giorno." max_invites_per_day: "Numero massimo di inviti che un utente può inviare in un giorno." max_topic_invitations_per_day: "Numero massimo di inviti ad argomenti che un utente può inviare al giorno." max_logins_per_ip_per_hour: "Numero massimo di accessi consentiti per indirizzo Ip per ora" @@ -1129,7 +1122,6 @@ it: max_users_notified_per_group_mention: "Massimo numero di utenti che possono ricevere una notifica se un gruppo è menzionato (se la soglia è raggiunta, nessuna notifica verrà inviata)" create_thumbnails: "Crea anteprime e lightbox delle immagini che sono troppo grandi per essere contenute in un messaggio." email_time_window_mins: "Aspetta (n) minuti prima di inviare email di notifica, per dare agli utenti la possibilità di modificare e completare i loro messaggi." - personal_email_time_window_seconds: "Attendi (n) secondi prima di inviare una notifica privata per email, per dare agli utenti la possibilità di modificare e finalizzare i loro messaggi." email_posts_context: "Quante risposte precedenti inserire come contesto nelle email di notifica." flush_timings_secs: "Frequenza di svuotamento dei dati temporali verso il server, in secondi." title_max_word_length: "La lunghezza massima di una parola, in caratteri, nel titolo di un argomento." @@ -1306,7 +1298,6 @@ it: watched_words_regular_expressions: "Le parole osservate sono espressioni regolari." default_email_digest_frequency: "Con quale frequenza gli utenti ricevono email riepilogative di default." default_include_tl0_in_digests: "Per impostazione predefinita, includi i messaggi dei nuovi utenti nelle email riepilogative. Gli utenti possono modificare questa impostazione nelle loro preferenze" - default_email_personal_messages: " Invia una email quando qualcuno scrive un messaggio ad un utente di default." default_email_direct: "Invia una email quando qualcuno cita/risponde/menziona un utente di default." default_email_mailing_list_mode: "Invia una email per ogni nuovo messaggio di default." default_email_mailing_list_mode_frequency: "Con quale frequenza gli utenti che hanno attivato la modalità mailing list riceveranno email di default." @@ -2327,21 +2318,6 @@ it: signup_after_approval: title: "Iscrizione Dopo Approvazione" subject_template: "Sei stato ammesso su %{site_name}!" - text_body_template: | - Benvenuto su %{site_name}! - - Un membro dello staff ha approvato il tuo account su %{site_name}. - - Clicca sul seguente collegamento per confermare e attivare il tuo nuovo account: - %{base_url}/u/activate-account/%{email_token} - - Se il collegamento qui sopra non è cliccabile, prova a copiarlo e incollarlo nella barra degli indirizzi del tuo browser. - - %{new_user_tips} - - Noi crediamo da sempre in un [comportamento comunitario civile](%{base_url}/guidelines). - - Buona permanenza! signup: title: "Iscrizione" subject_template: "[%{email_prefix}] Conferma il tuo nuovo account" diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index ecdff419afd..20df5c60043 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -249,7 +249,6 @@ ja: title: "ベーシックユーザー" change_failed_explanation: "%{user_name} を '%{new_trust_level}' に下げようとしましたが、既にトラストレベルが '%{current_trust_level}' です。%{user_name} は '%{current_trust_level}' のままになります - もしユーザーを降格させたい場合は、トラストレベルをロックしてください" rate_limiter: - too_many_requests: "このアクションを一日の間に実施可能な回数が決まっています。%{time_left}待ってから再度試してください。" by_type: public_group_membership: "グループへの参加/離脱が多すぎます。%{time_left}お待ち下さい。" hours: @@ -392,6 +391,7 @@ ja: unsubscribe: title: "配信停止" unwatch_category: "%{category}内の全トピックのウォッチを解除" + all: "%{sitename}からのメールを私に送信しないでください。" user_api_key: title: "アプリケーションアクセスの認証" description: "\"%{application_name}\" があなたのアカウントへのアクセスを要求しています: " @@ -552,11 +552,9 @@ ja: allow_user_locale: "ユーザーが言語を選択できるようにする" min_post_length: "投稿を許可する最少の文字数" min_first_post_length: "最初の投稿(投稿本文)を許可する最少の文字数" - min_personal_message_post_length: "メッセージに投稿可能な最少の文字数" max_post_length: "投稿を許可する最大の文字数" min_topic_title_length: "トピックタイトルとして許可する最小の文字数" max_topic_title_length: "トピックタイトルとして許可する最大の文字数" - min_personal_message_title_length: "プライベートメッセージのタイトルとして許容する最小の文字数" min_search_term_length: "検索ワードとして有効にする最小の文字数" allow_uncategorized_topics: "カテゴリなしのトピック作成を許可するか。警告:未分類のトピックがある場合は、これをオフにする前に、再分類する必要があります。" allow_duplicate_topic_titles: "トピックタイトルの重複を許可" @@ -676,7 +674,6 @@ ja: max_bookmarks_per_day: "ユーザが一日にブックマークできる最大数" max_edits_per_day: "ユーザが一日に編集できる最大数" max_topics_per_day: "ユーザが一日に作成できるトピックの最大数" - max_personal_messages_per_day: "ユーザが一日に作成できるメッセージの最大数" max_invites_per_day: "ユーザが一日に招待できる最大数" max_topic_invitations_per_day: "ユーザが一日にトピックに招待できる最大数" suggested_topics: "トピック下部に表示されるおすすめトピックの数" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index ac6ec69c3eb..351eb5c9c6d 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -405,7 +405,6 @@ ko: change_failed_explanation: "당신은 %{user_name} 사용자를 '%{new_trust_level}'으로 강등시키려 하였습니다. 하지만 해당 사용자의 회원등급는 이미 '%{current_trust_level}'입니다. %{user_name} 사용자의 회원등급는 '%{current_trust_level}'으로 유지됩니다. - if you wish to demote user lock trust level first" rate_limiter: slow_down: "해당 작업을 너무 많이 수행했습니다. 잠시후 다시 시도해보세요. " - too_many_requests: "지금 하시려는 행동에는 하루 제한이 있습니다. %{time_left} 동안 기다리시고 다시 시도해 주세요." by_type: first_day_replies_per_day: "새 유저가 가입한 첫 날에 작성할 수 있는 댓글의 최대 갯수에 도달했습니다. %{time_left}동안 기다리셨다가 다시 시도해보세요." first_day_topics_per_day: "새 유저가 가입한 첫 날에 작성할 수 있는 토픽의 최대 갯수에 도달했습니다. %{time_left}동안 기다리셨다가 다시 시도해보세요." @@ -781,7 +780,6 @@ ko: subfolder_ends_in_slash: "서브폴더 설정이 정확하지 않습니다. DISCOURSE_RELATIVE_URL_ROOT 다음에 슬래시가 있습니다." email_polling_errored_recently: other: "이메일 폴링에서 지난 24시간 동안%{count}개의 에러가 발생하였습니다. 세부 정보는 <a href='/logs' target='_blank'>로그</a>에서 확인하세요." - missing_mailgun_api_key: "서버가 mailgun으로 이메일을 보내도록 설정되어 있지만, webhook 메시지를 인증한 API키를 입력하지 않으셨습니다." bad_favicon_url: "파비콘 불러오기가 실패했습니다. <a href='/admin/site_settings'>사이트 설정</a>에서 favicon_url 설정을 확인해보세요." poll_pop3_timeout: "타임아웃으로 POP3 연결이 실패했습니다. 수신 이메일을 가져올 수 없습니다. <a href='/admin/site_settings/category/email'>POP3 설정</a> 과 서비스 제공자를 확인하세요." poll_pop3_auth_error: "인증 실패로 POP3 연결이 실패했습니다. <a href='/admin/site_settings/category/email'>POP3 설정</a>을 확인하세요." @@ -793,13 +791,11 @@ ko: set_locale_from_accept_language_header: "익명 사용자의 인터페이스 언어를 웹브라우저 언어 헤더를 기준으로 변경하기(실험적인 기능입니다. 익명 cache와 동작하지 않습니다.)" min_post_length: "글의 최소 글자 수" min_first_post_length: "첫 글 (내용)의 최소 길이" - min_personal_message_post_length: "메세지 내용의 최소 길이" max_post_length: "글의 최대 글자 수" topic_featured_link_enabled: "토픽에 링크 게시글 달기 활성화" show_topic_featured_link_in_digest: "요약 메일에 토픽 주요 링크 보이게 하기." min_topic_title_length: "글타래 제목의 최소 글자 수" max_topic_title_length: "글타래 제목의 최대 글자 수" - min_personal_message_title_length: "메세지 제목의 최소 길이" min_search_term_length: "검색을 하기 위한 최소 글자 수" search_tokenize_chinese_japanese_korean: "CJK(한중일) 설정이 되어 있지 않은 사이트라도, 검색시 한중일 언어를 토크나이징하도록 강제합니다." search_prefer_recent_posts: "이 옵션을 켜면, 포럼이 너무 커서 검색이 느리게 작동할 때 최근의 포스트부터 먼저 검색합니다." @@ -848,7 +844,6 @@ ko: summary_likes_required: "하나의 글타래에 대하여 요약본 보기 모드가 활성화되기 전까지 요구되는 최소 좋아요 수" summary_percent_filter: "요약본 보기를 클릭시, 글 중에 몇 %의 상위 글을 보여줄 것인가?" summary_max_results: "이 주제에 대한 요약 글 최대 갯수" - enable_personal_messages: "신뢰도 1 사용자(메시지 전송을 위한 최소 신뢰도로 설정가능)가 메시지를 작성하고 답장을 쓸 수 있도록 허용합니다. 운영진은 언제나 메시지를 보낼 수 있음을 참고하세요." enable_long_polling: "Message bus used for notification can use long polling" long_polling_base_url: "long polling에 사용 될 Base URL (CDN이 동적 콘텐트를 제공할 시에는 origin pull로 설정) eg: http://origin.site.com" long_polling_interval: "보낼 데이터가 없을 때 응답 전에 서버가 기다려야하는 시간 (로그인된 유저 전용)" @@ -973,7 +968,6 @@ ko: max_bookmarks_per_day: "사용자가 하루동안 할 수 있는 최대 북마크 개수" max_edits_per_day: "사용자가 하루동안 할 수 있는 최대 편집 수" max_topics_per_day: "사용자가 하루동안 생성할 수 있는 최대 글타래 개수" - max_personal_messages_per_day: "하루에 유저가 쓸 수 있는 최대 메세지 갯수" max_invites_per_day: "하루에 보낼 수 있는 초대장의 최대치입니다." max_topic_invitations_per_day: "하루에 유저가 최대로 보낼 수 있는 글타래 초대장 수" max_logins_per_ip_per_hour: "시간 당 동일 IP에서 로그인할 수 있는 최대 사용자 수" @@ -1041,7 +1035,6 @@ ko: max_users_notified_per_group_mention: "그룹이 멘션을 받으면 알림을 받는 최대사용자의 수(최대치에 도달하면 알림이 전송되지 않음)" create_thumbnails: "글의 너무 큰 크기의 이미지는 thumnails와 lightbox를 만든다." email_time_window_mins: "알림 메일을 보내기 전 대기 기간(분), 사용자에게 글의 변경하고 완료할 수 있는 기회를 준다." - personal_email_time_window_seconds: "사용자가 메시지를 재편집할 기회를 주기 위하여 개인 알림 메일을 보내기 이전에 설정한 시간만큼 대기합니다." email_posts_context: "알림메일의 내용에 추가할 기존 답글 수" flush_timings_secs: "사용자의 이용 시간 데이터를 서버로 보내는 기간(초)" title_max_word_length: "글타래 제목안에 단어들의 최대 길이" diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index ba68114b12c..2c91f12b9af 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -350,7 +350,6 @@ nb_NO: leader: title: "leder" rate_limiter: - too_many_requests: "Vi har en daglig begrensning for hvor mange ganger den handlingen kan utføres. Vent %{time_left} før du prøver igjen." by_type: first_day_replies_per_day: "Du har nådd det maksimale antall svar en ny bruker kan publisere på sin første dag. Vennligst vent %{time_left} før du prøver igjen." first_day_topics_per_day: "Du har nådd det maksimale antall tråder en ny bruker kan publisere på sin første dag. Vennligst vent %{time_left} før du prøver igjen." @@ -693,11 +692,9 @@ nb_NO: allow_user_locale: "Tillat brukere å velge eget språk" min_post_length: "Minimum tillatt lengde for innlegg i tegn" min_first_post_length: "Minimum tillatt lengde på for teksten i første innlegg" - min_personal_message_post_length: "Minste tillatte innleggslengde for meldinger i antall tegn" max_post_length: "Maksimum tillatt lengde for innlegg i tegn" min_topic_title_length: "Minimum tillatt lengde for tittel i tegn" max_topic_title_length: "Maksimum tillatt lengde for innlegg i tegn" - min_personal_message_title_length: "Minste antall tegn tillatt til bruk for tittellengde i meldinger" min_search_term_length: "Minimum lengde på søkeord i tegn" allow_duplicate_topic_titles: "Tillat flere tråder med identisk tittel." unique_posts_mins: "Hvor mange minutter før en bruker kan lage et innlegg med det samme innholdet igjen" diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 335e0233905..65352818d6e 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -435,7 +435,6 @@ nl: change_failed_explanation: "U probeerde %{user_name} naar '%{new_trust_level}' te degraderen. Zijn of haar vertrouwensniveau is echter al '%{current_trust_level}'. %{user_name} blijft op vertrouwensniveau '%{current_trust_level}'. Als u de gebruiker wil degraderen, vergrendel dan eerst het vertrouwensniveau." rate_limiter: slow_down: "U hebt deze actie te vaak uitgevoerd; probeer het later opnieuw." - too_many_requests: "Er is een dagelijks limiet voor hoe vaak die actie kan worden uitgevoerd. Wacht %{time_left} voordat u het opnieuw probeert." by_type: first_day_replies_per_day: "U hebt het maximale aantal antwoorden dat een nieuwe gebruiker op de eerste dag kan plaatsen bereikt. Wacht %{time_left} voordat u het opnieuw probeert." first_day_topics_per_day: "U hebt het maximale aantal topics dat een nieuwe gebruiker op de eerste dag kan plaatsen bereikt. Wacht %{time_left} voordat u het opnieuw probeert." @@ -834,7 +833,6 @@ nl: email_polling_errored_recently: one: "E-mailpolling heeft de afgelopen 24 uur een fout gegenereerd. Bekijk <a href='/logs' target='_blank'>de logboeken</a> voor meer details." other: "E-mailpolling heeft de afgelopen 24 uur %{count} fouten gegenereerd. Bekijk <a href='/logs' target='_blank'>de logboeken</a> voor meer details." - missing_mailgun_api_key: "De server is geconfigureerd om e-mails via Mailgun te verzenden, maar u hebt geen API-sleutel opgegeven voor verificatie van de webhookberichten." bad_favicon_url: "De favicon wordt niet geladen. Controleer uw favicon_url-instelling in de <a href='/admin/site_settings'>Website-instellingen</a>." poll_pop3_timeout: "Time-out voor verbinding met de POP3-server. Binnenkomende e-mail kon niet worden opgehaald. Controleer uw <a href='/admin/site_settings/category/email'>POP3-instellingen</a> en serviceprovider." poll_pop3_auth_error: "Verbinding met de POP3-server is mislukt met een authenticatiefout. Controleer uw <a href='/admin/site_settings/category/email'>POP3-instellingen</a>." @@ -846,13 +844,11 @@ nl: set_locale_from_accept_language_header: "Taal van interface voor anonieme gebruikers instellen op basis van de taalheaders van hun webbrowser. (EXPERIMENTEEL, werkt niet met anonieme cache)" min_post_length: "Minimaal toegestane lengte van een bericht in tekens" min_first_post_length: "Minimaal toegestane lengte van eerste bericht (topictekst) in tekens" - min_personal_message_post_length: "Minimaal toegestane berichtlengte in tekens voor privéberichten" max_post_length: "Maximaal toegestane lengte van een bericht in tekens" topic_featured_link_enabled: "Plaatsing van een koppeling met topics toestaan" show_topic_featured_link_in_digest: "Koppeling naar aanbevolen topics in de samenvattingsmail tonen" min_topic_title_length: "Minimaal toegestane lengte van een topictitel in tekens" max_topic_title_length: "Maximaal toegestane lengte van een topictitel in tekens" - min_personal_message_title_length: "Minimaal toegestane titellengte voor een bericht in tekens" min_search_term_length: "Minimumlengte van een zoekterm in tekens" search_prefer_recent_posts: "Als doorzoeken van uw grote forum traag werkt, probeert deze optie eerst een index van recentere berichten" search_recent_posts_size: "Het aantal in de index te behouden recente berichten " @@ -897,7 +893,6 @@ nl: summary_likes_required: "Minimale aantal likes in een topic voordat 'Dit topic samenvatten' wordt ingeschakeld" summary_percent_filter: "Wanneer een gebruiker op 'Dit topic samenvatten' klikt, de top % van berichten tonen" summary_max_results: "Maximale aantal getoonde berichten in 'Dit topic samenvatten'" - enable_personal_messages: "Gebruikers met vertrouwensniveau 1 (instelbaar via het minimale niveau om berichten te verzenden) toestaan om berichten aan te maken en op berichten te antwoorden. Houd er rekening mee dat stafleden altijd berichten kunnen verzenden." enable_long_polling: "Gebruikte 'message bus' voor melding kan 'long polling' gebruiken" long_polling_base_url: "Basis-URL voor 'long polling' (als een CDN dynamische inhoud aanbiedt, zorg er dan voor dat dit op 'origin pull' is ingesteld), zoals http://origin.site.com" long_polling_interval: "De hoeveelheid tijd die de server zou moeten wachten voordat het clients beantwoord als er geen data te verzenden is (alleen voor ingelogde gebruikers)" @@ -1000,7 +995,6 @@ nl: max_bookmarks_per_day: "Maximale aantal bladwijzers per gebruiker per dag." max_edits_per_day: "Maximale aantal bewerkingen per gebruiker per dag." max_topics_per_day: "Maximale aantal topics dat een gebruiker per dag kan plaatsen." - max_personal_messages_per_day: "Maximale aantal berichten dat een gebruiker per dag kan plaatsen." max_invites_per_day: "Maximale aantal uitnodigingen dat een gebruiker per dag kan versturen." max_topic_invitations_per_day: "Maximale aantal uitnodigingen voor een topic dat een gebruiker per dag kan versturen." max_logins_per_ip_per_hour: "Maximale aantal toegestane aanmeldingen per IP-adres per uur" @@ -1180,7 +1174,6 @@ nl: approve_post_count: "Het aantal berichten van een nieuwe of normale gebruiker dat moet worden goedgekeurd" approve_unless_trust_level: "Posts voor gebruikers onder dit vertrouwensniveau moeten worden goedgekeurd" approve_new_topics_unless_trust_level: "Nieuwe topics voor gebruikers onder dit vertrouwensniveau moeten worden goedgekeurd" - default_email_personal_messages: "Stuur standaard een e-mail als iemand de gebruiker bericht." default_email_direct: "Stuur standaard een e-mail wanneer iemand de gebruiker citeert/beantwoordt/vermeldt of uitnodigt." default_email_mailing_list_mode: "Stuur standaard een email voor elk nieuw bericht." default_email_always: "Standaard een e-mailmelding verzenden, zelfs wanneer de gebruiker actief is" diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 2ab110c6446..c5ebe7974c1 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -468,7 +468,6 @@ pl_PL: broken: "Ten obraz jest uszkodzony" rate_limiter: slow_down: "Powtórzyłeś to działanie zbyt wiele razy, spróbuj ponownie później." - too_many_requests: "Liczba wykonań tej czynności w ciągu dnia jest ograniczona. Odczekaj %{time_left} przed ponowną próbą." by_type: first_day_replies_per_day: "Osiągnąłeś maksymalną liczbę odpowiedzi, jakie może napisać nowy użytkownik pierwszego dnia. Musisz odczekać %{time_left} zanim znów spróbujesz." first_day_topics_per_day: "Osiągnąłeś maksymalną liczbę tematów, jakie może napisać nowy użytkownik pierwszego dnia. Musisz odczekać %{time_left}, zanim znów spróbujesz." @@ -923,7 +922,6 @@ pl_PL: few: "Email polling wygenerował %{count} błędy w przeciągu ostatniej doby. Więcej szczegółów w <a href='/logs' target='_blank'>logach</a>." many: "Email polling wygenerował %{count} błędów w przeciągu ostatniej doby. Więcej szczegółów w <a href='/logs' target='_blank'>logach</a>." other: "Email polling wygenerował %{count} błędów w przeciągu ostatniej doby. Więcej szczegółów w <a href='/logs' target='_blank'>logach</a>." - missing_mailgun_api_key: "Serwer jest skonfigurowany by wysyłać maile przez mailguna, ale nie ustawiłeś klucza API niezbędnego do zweryfikowania wiadomości webhook." bad_favicon_url: "Nie można załadować favikony. Sprawdź ustawiony favicon_url w <a href='/admin/site_settings'>Ustawieniach Strony</a>." poll_pop3_timeout: "Połączenie z serwerem POP3 zostało zerwane. Przychodząca wiadomość mogła nie zostać odebrany. Proszę sprawdź swoje <a href='/admin/site_settings/category/email'ustawienia POP3</a> i dostawcę usług." poll_pop3_auth_error: "Połączenie z serwerem POP3 nie powiodło się przez błąd uwierzytelnienia. Proszę sprawdź swoje <a href='/admin/site_settings/category/email'>ustawienia POP3</a>." @@ -935,13 +933,11 @@ pl_PL: set_locale_from_accept_language_header: "ustaw język interfejsu dla niezalogowanych użytkowników na podstawie języka w nagłówku ich przeglądarki. (EKSPERYMENTALNE, nie działa z anonimowym cache)" min_post_length: "Minimalna długość wpisu w znakach" min_first_post_length: "Minimalna długość treści (liczba znaków) pierwszego wpisu w temacie " - min_personal_message_post_length: "Minimalna długość treści wiadomości " max_post_length: "Maksymalna długość wpisu, w znakach" topic_featured_link_enabled: "Włącz dodawanie linków w tematach." show_topic_featured_link_in_digest: "Pokazuj polecany link tematu w podsumowaniu mailowym." min_topic_title_length: "Minimalna długość tytułu tematu, w znakach" max_topic_title_length: "Maksymalna długość tytułu tematu, w znakach" - min_personal_message_title_length: "Minimalna liczba znaków w temacie wiadomości " min_search_term_length: "Minimalna długość wyszukiwanego tekstu, w znakach" search_tokenize_chinese_japanese_korean: "Wymuś wyszukiwanie żeby tokenizowało Chiński/Japoński/Koreański nawet na stronach nie-CJK" search_prefer_recent_posts: "Jeśli wyszukiwanie na twoim duży forum jest wolne, ta opcja pozwala zaindeskować ostanie posty wpierw." @@ -993,7 +989,6 @@ pl_PL: summary_likes_required: "Minimalna liczba polubień w temacie zanim 'Podsumowanie tematu' jest dostępne" summary_percent_filter: "Gdy użytkownik kliknie na 'Podsumowaniu tematu', pokaż % najlepszych wpisów" summary_max_results: "Maksymalna liczba wpisów w 'Podsumowaniu tematu'" - enable_personal_messages: "Zezwalaj użytkownikom o poziomie zaufania 1 (możliwe do zmiany przez min trust level to send messages) na tworzenie wiadomości i odpowiadanie na nie. Zwróć uwagę, że administracja zawsze może wysyłać wiadomości bez względu na wszystko." enable_long_polling: "Message bus used for notification can use long polling" long_polling_base_url: "Podstawowy URL używany dla long polling (kiedy CDN dostarcza dynamicznych treści, upewnij się że ustawiłeś to w origin pull) np.: http://origin.site.com" long_polling_interval: "Okres czasu jaki serwer powinien odczekać przed odpowiedzią klientowi kiedy nie ma danych do przesłania (tylko do zalogowanych użytkowników)" @@ -1119,7 +1114,6 @@ pl_PL: max_bookmarks_per_day: "Maksymalna liczba zakładek per użytkownik per dzień." max_edits_per_day: "Maksymalna liczba edycji per użytkownik per dzień." max_topics_per_day: "Maksymalna liczba tematów jakie użytkownik może stworzyć jednego dnia." - max_personal_messages_per_day: "Maksymalna liczba wiadomości jakie użytkownik może wysłać jednego dnia." max_invites_per_day: "Maksymalna liczba zaproszeń jakie użytkownik może wysłać jednego dnia." max_topic_invitations_per_day: "Maksymalna dzienna liczba zaproszeń do tematu, jakie może wysłać użytkownik." max_logins_per_ip_per_hour: "Maksymalna liczba logowań dozwolona per adres IP na godzinę" @@ -1188,7 +1182,6 @@ pl_PL: max_users_notified_per_group_mention: "Maksymalna liczba użytkowników, którzy mogą otrzymać powiadomienie jeśli ktoś wspomniał o grupie (jeśli próg został osiągnięty, nie będzie żadnych powiadomień)" create_thumbnails: "Stwórz miniatury i obrazy lightbox, które są za duże, aby pasować do postu." email_time_window_mins: "Odczekaj (n) minut przed wysłaniem maila z powiadomieniem, aby dać użytkownikom szansę na edytowanie i ukończenie postów." - personal_email_time_window_seconds: "Poczekaj (n) sekund przed wysłaniem jakichkolwiek prywatnych powiadomień emailem, by dać szansę użytkownikom na edycję i sfinalizowanie ich wiadomości." email_posts_context: "Jak wiele poprzednich wiadomości zawrzeć jako kontekst i emailu powiadamiającym." flush_timings_secs: "W sekundach, jak często wysyłamy dane czasowe na serwer." title_max_word_length: "Maksymalna dozwolona długość słowa, w znakach, jako tytuł tematu." @@ -1356,7 +1349,6 @@ pl_PL: max_allowed_message_recipients: "Maksymalna dozwolona liczba odbiorców dla wiadomości." default_email_digest_frequency: "Domyślnie, jak często użytkownicy otrzymują emaile podsumowujące." default_include_tl0_in_digests: "Domyślnie uwzględniaj posty od nowych użytkowników w podsumowaniach mailowych. Użytkownicy mogą dokonywać zmian w swoich ustawieniach." - default_email_personal_messages: "Domyślnie wysyłaj email, gdy użytkownik otrzyma wiadomość." default_email_direct: "Domyślnie wysyłaj email, gdy ktoś zacytuje/odpowie/wspomni lub zaprosi użytkownika." default_email_mailing_list_mode: "Domyślnie wyślij email dla każdego nowego posta." default_email_mailing_list_mode_frequency: "Domyślnie użytkownicy, którzy włączą tryb listy mailingowej będą otrzymywać emaile tak często." @@ -2255,21 +2247,6 @@ pl_PL: signup_after_approval: title: "Zarejestruj się po zatwierdzeniu" subject_template: "Zostałeś zaakceptowany na forum %{site_name}!" - text_body_template: | - Witamy na %{site_name}! - - Nasz personel zatwierdził twoje konto na %{site_name}. - - Kliknij na podany link, aby potwierdzić i aktywować swoje nowe konto: - %{base_url}/u/activate-account/%{email_token} - - Jeśli podany link nie działa, spróbuj skopiować go i przykleić w pasku adresów swojej wyszukiwarki. - - %{new_user_tips} - - Wierzymy, że [cywilizowana dyskusja](%{base_url}/guidelines) jest zawsze możliwa. - - Baw się dobrze! signup: title: "Podpis" subject_template: "[%{site_name}] Potwierdź swoje nowe konto" diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index c3a80aebf43..c2d79d6f3b6 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -378,7 +378,6 @@ pt: change_failed_explanation: "Tentou despromover %{user_name} para '%{new_trust_level}'. Contudo o Nível de Confiança é atualmente '%{current_trust_level}'. %{user_name} irá permanecer em '%{current_trust_level}' - se deseja despromover o utilizador, bloqueie o Nível de Confiança primeiro" rate_limiter: slow_down: "Realizou esta ação demasiadas vezes, tente novamente mais tarde." - too_many_requests: "Possuímos um limite diário de número de vezes que uma ação pode ser tomada. Por favor aguarde %{time_left} antes de tentar novamente." by_type: first_day_replies_per_day: "Atingiu o número máximo de respostas que um novo utilizador pode criar no seu primeiro dia. Por favor espere %{time_left} antes de tentar novamente." first_day_topics_per_day: "Atingiu o número máximo de tópicos que um novo utilizador pode criar no seu primeiro dia. Por favor espere %{time_left} antes de tentar novamente." @@ -769,7 +768,6 @@ pt: email_polling_errored_recently: one: "A consulta automática de emails gerou um erro nas últimas 24 horas. Consulte em <a href='/logs' target='_blank'>os registos</a> para mais detalhes." other: "A consulta automática de emails gerou %{count} erros nas últimas 24 horas. Consulte em <a href='/logs' target='_blank'>os registos</a> para mais detalhes." - missing_mailgun_api_key: "O servidor está configurado para enviar email via mailgun mas você não forneceu uma chave de API para verificar as mensagens de webhook." bad_favicon_url: "Não estamos a conseguir carregar o favicon. Por favor, confirme a sua configuração favicon_url nas <a href='/admin/site_settings'>Configurações do sítio</a>." poll_pop3_timeout: "A tentativa de ligação ao servidor POP3 está a ultrapassar o tempo máximo. O email da caixa de entrada não pôde ser obtido. Por favor verifique a sua <a href='/admin/site_settings/category/email'>configuração de POP3</a> e fornecedor de serviço internet." poll_pop3_auth_error: "A tentativa de ligação ao servidor POP3 está a falhar por motivos de erro de autenticação. Por favor verifique a sua <a href='/admin/site_settings/category/email'>configuração de POP3</a>." @@ -781,13 +779,11 @@ pt: set_locale_from_accept_language_header: "defina a língua de interface para utilizadores anónimos com base nos cabeçalhos de língua dos seus navegadores. (EXPERIMENTAL, não funciona com cache anónima)" min_post_length: "Tamanho mínimo permitido por mensagem, em caracteres" min_first_post_length: "Tamanho mínimo permitido para a primeira mensagem (corpo do tópico), em caracteres" - min_personal_message_post_length: "Tamanho mínimo permitido para mensagens, em caracteres" max_post_length: "Tamanho máximo permitido por mensagem, em caracteres" topic_featured_link_enabled: "Permitir publicar uma ligação com tópicos." show_topic_featured_link_in_digest: "Mostrar a ligação destacada do tópico no email de resumo." min_topic_title_length: "Tamanho mínimo permitido por título de cada tópico, em caracteres" max_topic_title_length: "Tamanho máximo permitido por título de cada tópico, em caracteres" - min_personal_message_title_length: "Tamanho mínimo permitido por título nas mensagens, em caracteres" min_search_term_length: "Tamanho mínimo válido para termos de pesquisa, em caracteres" search_tokenize_chinese_japanese_korean: "Forçar pesquisa para atomizar Chinês/Japonês/Coreano mesmo em sites que não sejam CJC" search_prefer_recent_posts: "Se pesquisar no seu fórum vasto é lento, esta opção tenta usar um índex de publicações recentes primeiro" @@ -833,7 +829,6 @@ pt: summary_likes_required: "Número mínimo de gostos num tópico antes que 'Resumir Este Tópico' seja ativo." summary_percent_filter: "Quando um utilizador clica em 'Resumir Este Tópico', mostrar as melhores % de mensagens" summary_max_results: "Número máximo de mensagens devolvidas por 'Resumo deste Tópico'" - enable_personal_messages: "Permitir que utilizadores de nível de confiança 1 (configurável através do nível de confiança mínimo para enviar mensagens) criem mensagens e respostas a mensagens. Note que a equipa de apoio pode mandar mensagens de qualquer maneira." enable_long_polling: "O sistema de mensagens usado para notificações pode fazer solicitações longas" long_polling_base_url: "URL base usada para solicitação ao servidor (quando um CDN serve conteúdo dinâmico, certifique-se de configurá-lo para a 'pull' original) ex: http://origem.sítio.com" long_polling_interval: "Quantidade de tempo que um servidor deve esperar antes de notificar os clientes quando não há dados para serem enviados (apenas utilizadores ligados)" @@ -949,7 +944,6 @@ pt: max_bookmarks_per_day: "Número máximo de marcadores por utilizador por dia." max_edits_per_day: "Número máximo de edições por utilizador por dia." max_topics_per_day: "Número máximo de tópicos que um utilizador pode criar por dia." - max_personal_messages_per_day: "Número máximo de mensagens que os utilizadores podem criar por dia." max_invites_per_day: "Número máximo de convites que um utilizador pode enviar por dia." max_topic_invitations_per_day: "Número máximo de convites para tópicos que um utilizador pode enviar por dia." alert_admins_if_errors_per_minute: "Número de erros por minuto necessários para disparar um alerta de administração. Um valor maior que 0 desativa esta funcionalidade. NOTA: requer reinício." @@ -1013,7 +1007,6 @@ pt: max_users_notified_per_group_mention: "Número máximo de utilizadores que podem receber uma notificação se um grupo é mencionado (se o limite é atingido nenhuma notificação será levantada)" create_thumbnails: "Criar imagens miniatura e lightbox que são demasiado largas para caber numa mensagem." email_time_window_mins: "Esperar (n) minutos antes de enviar quaisquer emails de notificação, para dar aos utilizadores a hipótese de editarem e finalizarem as suas mensagens." - personal_email_time_window_seconds: "Espere (n) segundos antes de enviar emails de notificação privados, para dar aos utilizadores a oportunidade de editar e finalizar as suas mensagens." email_posts_context: "Quantas respostas prévias a serem incluídas como contexto em emails de notificação." flush_timings_secs: "Com que frequência o servidor é alimentado com dados de sincronização, em segundos." title_max_word_length: "Tamanho máximo permitido de comprimento de palavras, em caracteres, no título de um tópico." @@ -1170,7 +1163,6 @@ pt: code_formatting_style: "Botão de código no compositor por defeito usa este estilo de formatação" default_email_digest_frequency: "Por defeito, quantas frequentemente os utilizadores recebem emails de resumo." default_include_tl0_in_digests: "Incluir publicações de novos utilizadores nos emails de resumo por defeito. Utilizadores podem alterar isto nas suas preferências." - default_email_personal_messages: "Por defeito, enviar um email quando alguém envia mensagens ao utilizador." default_email_direct: "Por defeito, enviar um email quando alguém cita/responde a/menciona ou convida o utilizador." default_email_mailing_list_mode: "Por defeito, enviar um email por cada nova mensagem." default_email_mailing_list_mode_frequency: "Utilizadores que activem o modo de lista de distribuição receberão, por omissão, emails com esta frequência." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 575a863b2c9..448090358f5 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -398,7 +398,6 @@ pt_BR: broken: "Esta imagem está danificada" rate_limiter: slow_down: "Você fez essa ação demais, tente novamente mais tarde." - too_many_requests: "Nós possuímos um limite diário do número de vezes que uma ação pode ser tomada. Por favor aguarde %{time_left} antes de tentar novamente." by_type: first_day_replies_per_day: "Você atingiu o número máximo de respostas que um novo usuário pode criar em seu primeiro dia. Por favor aguarde %{time_left} antes de tentar novamente." first_day_topics_per_day: "Você atingiu o número máximo de tópicos que um novo usuário pode criar em seu primeiro dia. Por favor aguarde %{time_left} antes de tentar novamente." @@ -781,7 +780,6 @@ pt_BR: email_polling_errored_recently: one: "A apuração de email gerou um erro nas últimas 24 horas. Veja <a href='/logs' target='_blank'>os logs</a> para mais detalhes." other: "A apuração de email gerou %{count} erros nas últimas 24 horas. Veja <a href='/logs' target='_blank'>os logs</a> para mais detalhes." - missing_mailgun_api_key: "O servidor está configurado para enviar email pelo mailgun, mas você não forneceu uma chave de API usada para verificar as mensagens de webhook." bad_favicon_url: "O carregamento do favicon está falhando. Verifique a sua configuração de favicon_url nas <a href='/admin/site_settings'>Configurações do Site</a>." poll_pop3_timeout: "A conexão com o servidor POP3 está atingindo o tempo limite. Por favor, verifique suas <a href='/admin/site_settings/category/email'>configurações de POP3</a> e selecione um servidor de serviço." poll_pop3_auth_error: "A conexão com o servidor POP3 está falhando com um erro de autenticação. Por favor, verifique suas <a href='/admin/site_settings/category/email'>configurações de POP3</a>." @@ -793,11 +791,9 @@ pt_BR: set_locale_from_accept_language_header: "define a língua da interface para os usuários anônimos de acordo com os cabeçalhos de língua de seus navegadores. (EXPERIMENTAL, não funciona com cache anônimo)" min_post_length: "Comprimento mínimo permitido do post em caracteres" min_first_post_length: "Comprimento mínimo de caracteres permitido para primeira mensagem (corpo do tópico)" - min_personal_message_post_length: "Comprimento mínimo de caracteres permitido para mensagens" max_post_length: "Comprimento máximo permitido para o post em caracteres" min_topic_title_length: "Comprimento mínimo permitido para o título do post em caracteres" max_topic_title_length: "Comprimento máximo permitido para o título do post em caracteres" - min_personal_message_title_length: "Comprimento mínimo de caracteres permitido para o título de uma mensagem" min_search_term_length: "Comprimento mínimo válido para o termo de pesquisa em caracteres" search_tokenize_chinese_japanese_korean: "Força a busca a tokenizar Mandarim/Japonês/Coreano até para sites que não são nesses idiomas" search_prefer_recent_posts: "Se buscar o seu fórum está lento, essa opção tenta indexar as publicações mais recentes primeiro" @@ -843,7 +839,6 @@ pt_BR: summary_likes_required: "Curtidas mínimas em um tópico antes de 'Resumir este tópico, ficar habilitado" summary_percent_filter: "Quando um usuário clicar em 'Resumor este tópico', mostrar os melhores % mensagens" summary_max_results: "Máximo número de posts quando resumidos por Categoria " - enable_personal_messages: "Permitir que usuários de nível de confiança 1 (configurável via o nível mínimo de confiança para enviar mensagens) criem e respondam a mensagens. Note que a equipe sempre pode enviar mensagens, independente do caso." enable_long_polling: "O sistema de mensagens das notificações pode fazer solicitações longas." long_polling_base_url: "URL Utilizada para \"long polling\" ( Quando um CDN for configurado, tenha certeza que essa configuração seja a padrão) ex: http://origin.site.com" long_polling_interval: "Tempo que o servidor deve aguardar antes de responder quando não existe nenhum dado para ser enviado. (apenas usuários logados)" @@ -957,7 +952,6 @@ pt_BR: max_bookmarks_per_day: "Número máximo de novos favoritos por usuário por dia." max_edits_per_day: "Número máximo de edições que um usuário pode fazer por dia." max_topics_per_day: "Número máximo de postagens que um usuário pode criar por dia." - max_personal_messages_per_day: "Máximo número de mensagens que usuários podem criar por dia." max_invites_per_day: "Número máximo de convites que um usuário pode enviar por dia." max_topic_invitations_per_day: "Número máximo de convites para tópicos que um usuário pode enviar por dia." alert_admins_if_errors_per_minute: "Número de erros por minutos para desencadear um alerta aos administradores. Um valor igual a 0 desabilita esse recurso. NOTA: requer reinicialização." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index c0ce72e49ee..fbfb757e981 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -367,7 +367,6 @@ ro: change_failed_explanation: "Ai încercat să retrogradezi utilizatorul %{user_name} la nivelul '%{new_trust_level}'. Însă nivelul este deja '%{current_trust_level}'. %{user_name} va rămâne la '%{current_trust_level}' - dacă dorești să retrogradezi utilizatorul, trebuie să îi blochezi întâi nivelul de încredere." rate_limiter: slow_down: "Ai executat această acțiune de prea multe ori. Încearcă mai târziu." - too_many_requests: "Există o limită zilnică de executare a acestei acțiuni. Te rugăm așteaptă %{time_left} până să încerci iar." by_type: first_day_replies_per_day: "Ai atins numărul maxim de răspunsuri pe care un nou utilizator le poate crea în prima sa zi. Te rugăm să aștepți %{time_left} înainte să încerci din nou." first_day_topics_per_day: "Ai atins numărul maxim de subiecte pe care un nou utilizator poate să le creeze în prima sa zi. Te rugăm să aștepți %{time_left} înainte să încerci din nou." @@ -777,7 +776,6 @@ ro: one: "Email polling a generat o eroare în ultimele 24 de ore. Vizualizează <a href='/logs' target='_blank'>rapoartele</a> pentru mai multe detalii." few: "Email polling a generat %{count} erori în ultimele 24 de ore. Vizualizează <a href='/logs' target='_blank'>rapoartele</a> pentru mai multe detalii." other: "Retragerea de emailuri generat %{count} de erori în ultimele 24 de ore. Vizualizează <a href='/logs' target='_blank'>rapoartele</a> pentru mai multe detalii." - missing_mailgun_api_key: "Serverul este configurat să trimită emailuri printr-un mailgun dar nu ai furnizat nici o cheie API folosită pentru a verifica mesajele webhook." bad_favicon_url: "Eroare la încărcarea Favicon. Verifică setarea favicon_url în <a href='/admin/site_settings'>Setările site-ului</a>." poll_pop3_timeout: "Conexiunea la serverul POP3 a dat time-out. Mailurile primite nu au putut fi accesate. Te rugăm să verifici <a href='/admin/site_settings/category/email'>setările POP3</a> și furnizorul de servicii." poll_pop3_auth_error: "Conexiunea la serverul POP3 a eșuat cu o eroare de autentificare. Te rugăm să verifici <a href='/admin/site_settings/category/email'>setările POP3</a>." @@ -789,13 +787,11 @@ ro: set_locale_from_accept_language_header: "setează limba interfeței pentru utilizatorii anonimi pe baza header-elor de limbă ale web browser-elor lor. (EXPERIMENTAL, nu funcționează cu cache anonim)" min_post_length: "Lungimea minimă a postării, în caractere" min_first_post_length: "Numărul minim de caractere permis pentru prima postare (în corpul mesajului)" - min_personal_message_post_length: "Numărul minim de caractere permis pentru mesaje, per postare" max_post_length: "Lungimea maximă permisă a postării, în caractere" topic_featured_link_enabled: "Activează postarea unui link cu subiecte." show_topic_featured_link_in_digest: "Arată link-ul promovat al subiectului în rezumatul pe email." min_topic_title_length: "Minimul de caractere permis în titlul unui subiect" max_topic_title_length: "Maximul de caractere permis în titlul unui subiect" - min_personal_message_title_length: "Numărul minim de caractere permis în titlul unui mesaj" min_search_term_length: "Lungimea minimă a unui termen de căutare valid, în caractere." search_tokenize_chinese_japanese_korean: "Forțează căutarea să utilizeze tokens chineză/japoneză/coreană chiar și pe site-uri care nu sunt CJK." search_prefer_recent_posts: "Dacă ai un forum mare și căutarea este prea lentă, această opțiune încearcă să indexeze mai multe postări recente întâi." @@ -841,7 +837,6 @@ ro: summary_likes_required: "Minimul de aprecieri într-un subiect înainte de a se activa 'Rezumă acest subiect'." summary_percent_filter: "Când un utilizator face click pe 'Rezumatul acestui subiect', arată primele % de postări" summary_max_results: "Numărul maxim de postări returnate de \"Rezumatul acestui subiect\"" - enable_personal_messages: "Acordă nivelul de încredere 1 (configurabil via nivel minim de încredere pentru trimiterea de mesaje) utilizatorilor, pentru a le permite să creeze și să răspundă la mesaje. Atenție: echipa poate oricând să trimită mesaje, indiferent de setări." enable_long_polling: "Bus-ul de mesaje folosit pentru notificări poate utiliza long polling." long_polling_base_url: "URL de bază folosit pentru long polling (atunci când un CDN servește conținut dinamic, asigură-te că setezi asta pe origin pull) ex: http://origin.site.com" long_polling_interval: "Durata cât serverul va trebui să aștepte înainte de a răspunde clienților atunci când nu există date de trimis (exclusiv utilizatori autentificați)" @@ -958,7 +953,6 @@ ro: max_bookmarks_per_day: "Numărul maxim zilnic de semne de carte per utilizator." max_edits_per_day: "Numărul maxim zilnic de editări per utilizator." max_topics_per_day: "Numărul maxim de subiecte pe care un utilizator le poare crea zilnic." - max_personal_messages_per_day: "Număr maxim de mesaje pe care utilizatorii le pot crea pe zi." max_invites_per_day: "Număr maxim de invitații pe care un utilizator le poate trimite pe zi." max_topic_invitations_per_day: "Numărul maxim de invitații la subiect pe care un utilizator le poate trimite pe zi." alert_admins_if_errors_per_minute: "Numărul de erori pe minut la care să se declanșeze o alerta admin. Valoarea 0 dezactivează această funcționalitate. NOTĂ: necesită restart." @@ -1022,7 +1016,6 @@ ro: max_users_notified_per_group_mention: "Numărul maxim de utilizatori care pot primi o notificare dacă un grup este menționat (dacă pragul este atins, nici o notificare nu va fi trimisă)" create_thumbnails: "Creează previzualizări de tipul thumbnail și lightbox ale imaginilor ce sunt prea mari să încapă într-o postare." email_time_window_mins: "Așteaptă (n) minute până să trimiți un email de notificare, pentru a da utilizatorilor șansa să-și editeze și să-și finalizeze postările." - personal_email_time_window_seconds: "Așteaptă (n) secunde înainte de a trimite emailuri de notificare privată, pentru a-i da utilizatorului șansa să își editeze sau finalizeze mesajele." email_posts_context: "Câte răspunsuri precedente să fie incluse drept context în email-urile de notificare." flush_timings_secs: "Cât de frecvent se resetează datele legate de timp de pe server, în secunde." title_max_word_length: "Numărul maxim de caractere din titlul unui subiect." @@ -1178,7 +1171,6 @@ ro: code_formatting_style: "Butonul Cod din cadrul editorului va fi setat cu opțiunea implicită pe acest stil de formatare cod " default_email_digest_frequency: "Frecvența implicită cu care utilizatorii primesc emailuri-rezumat." default_include_tl0_in_digests: "Implicit, include postări de la utilizatori noi în emailurile-rezumat. Utilizatorii pot schimba această opțiune din preferințele lor." - default_email_personal_messages: "Implicit, trimite un email atunci când cineva trimite un mesaj unui utilizator." default_email_direct: "Implicit, trimite un mesaj când cineva citează/răspunde/menționează sau invită un utilizator." default_email_mailing_list_mode: "Implicit, trimite un email pentru fiecare nou mesaj." default_email_mailing_list_mode_frequency: "Implicit, utilizatorii care activează modul mailing list vor primi emailuri cu această frecvență." @@ -1433,6 +1425,20 @@ ro: system_messages: post_hidden: subject_template: "Postare ascunsă din cauza marcajelor de avertizare ale comunității" + text_body_template: | + Salut, + + Acesta este un mesaj automat de pe %{site_name} pentru a te informa că mesajul tău a fost ascuns. + + %{base_url}%{url} + + %{flag_reason} + + Mai mulți membri ai comunității au marcat acestă postare cu marcaje de avertizare înainte ca ea să fie ascunsă, așa că te rugăm să îți revizuiești postarea pentru a ține cont de feedback-ul primit. **Îți poți edita postarea după %{edit_delay} minute, și ea va fi automat re-afișată** + + Totuși, dacă postarea este ascunsă de comunitate pentru a doua oară, va rămâne așa până ce va fi gestionată de un membru al echipei -- și asta ar putea atrage și alte consecințe, inclusiv posibilitatea suspendării contului tău. + + Pentru informații suplimentare, te rugăm să citești [ghidul comunității](%{base_url}/guidelines). welcome_user: subject_template: "Bine ai venit pe %{site_name}!" welcome_invite: @@ -1533,8 +1539,41 @@ ro: Dacă există interfață utilizator web pentru contul tău POP, ar trebui să te autentifici pe ea și să îți verifici acolo setările. too_many_spam_flags: subject_template: "Cont nou suspendat" + text_body_template: | + Salut, + + Acesta este un mesaj automat de pe %{site_name} pentru a te informa că postările tale au fost temporar ascunse pentru că au fost marcate cu marcaje de avertizare de către comunitate.. + + Ca măsura de precauție, noului tău cont i s-a blocat posibilitatea de a crea noi răspunsuri sau subiecte până ce un un membru al echipei nu îl va verifica. Ne cerem scuze pentru inconveniență. + + Pentru informații suplimentare, te rugăm să citești [ghidul comunității](%{base_url}/guidelines). too_many_tl3_flags: subject_template: "Cont nou suspendat" + text_body_template: | + Salut, + + Acesta este un mesaj automat de pe %{site_name} pentru a te informa că ți-a fost suspendat contul din cauza unui număr mare de marcaje de avertizare primite de la comunitate. + + Ca măsura de precauție, noului tău cont i s-a blocat posibilitatea de a crea noi răspunsuri sau subiecte până ce un un membru al echipei nu îl va verifica. Ne cerem scuze pentru inconveniență. + + Pentru informații suplimentare, te rugăm să citești [ghidul comunității](%{base_url}/guidelines). + silenced_by_staff: + text_body_template: |+ + Salut, + + Acesta este un mesaj automat de pe %{site_name} pentru a te informa că ți-a fost suspendat temporar contul, ca măsură de precauție. + + Poți să continui să răsfoiești, dar nu vei mai putea să răspunzi sau să creezi subiecte până când un [membru al echipei](%{base_url}/about) nu îți verifică postările recente. Ne cerem scuze pentru inconveniență. + + Pentru informații suplimentare, te rugăm să citești [ghidul comunității](%{base_url}/guidelines). + + unsilenced: + text_body_template: | + Salut, + + Acesta este un mesaj automat din partea %{site_name} pentru a vă informa cu privire la deblocarea contului dumneavoastră după o revizie a personalului. + + Acum puteți crea discuții și răspunsuri noi. pending_users_reminder: subject_template: one: "un utilizator în așteptarea aprobării" diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index e82b9ec4c32..bf41ab5ad0b 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -184,11 +184,13 @@ ru: many: "К сожалению, новые пользователи могут добавить только %{count} вложений в сообщение." other: "К сожалению, новые пользователи могут добавить только %{count} вложений в сообщение." no_links_allowed: "Извините, новые пользователи не могут размещать ссылки." + links_require_trust: "Извините, Вы не можете включать ссылки в Ваши сообщения." too_many_links: one: "Извините, новые пользователи могут размещать только одну ссылку в сообщении." few: "Извините, новые пользователи могут размещать только %{count} ссылок в сообщении." many: "Извините, новые пользователи могут размещать только %{count} ссылок в сообщении." other: "Извините, новые пользователи могут размещать только %{count} ссылок в сообщении." + contains_blocked_words: "Ваш пост содержит слова, которые не разрешены." spamming_host: "Извините, вы не можете разместить ссылку в этом сообщении." user_is_suspended: "Заблокированным пользователям запрещено писать." topic_not_found: "Что-то пошло не так. Возможно, эта тема была закрыта или заархивирована, пока вы ее читали?" @@ -220,6 +222,7 @@ ru: top_weekly: "Лучшие темы за неделю" top_daily: "Лучшие темы за день" posts: "Последние сообщения" + private_posts: "Последние личные сообщения" group_posts: "Последние сообщения от %{group_name}" group_mentions: "Последние упоминания от %{group_name}" user_posts: "Последние сообщения, отправленные @%{username}" @@ -230,6 +233,7 @@ ru: excerpt_image: "изображение" queue: delete_reason: "Удалено по запросу модератора." + not_found: "Сообщение не найдено или уже обновлено." groups: success: bulk_add: "%{users_added}пользователей было добавлено в группу." @@ -337,6 +341,10 @@ ru: attributes: payload_url: invalid: "URL недействителен. URL должен включает в себя HTTP: // или https: //. И ни один пустой не допускается." + watched_word: + attributes: + word: + too_many: "Слишком много слов для этого действия" <<: *errors user_profile: no_info_other: "<div class='missing-profile'>%{name} еще не заполнил поле «Обо мне» в своём профайле. </div>" @@ -382,6 +390,7 @@ ru: invalid_email_in: "'%{email}' не является корректным адресом электронной почты" email_already_used_in_group: "'%{email}' уже используется группой '%{group_name}'." email_already_used_in_category: "'%{email}' уже используется категорией '%{category_name}'." + description_incomplete: "Описание категории должна иметь по крайней мере один абзац." cannot_delete: uncategorized: "Нельзя удалить раздел, предназначенный для тем вне разделов" has_subcategories: "Невозможно удалить этот раздел, т.к. в нем есть подразделы." @@ -406,7 +415,6 @@ ru: change_failed_explanation: "Вы пытаетесь понизить пользователя %{user_name} до уровня доверия '%{new_trust_level}'. Однако, его уровень доверия уже '%{current_trust_level}'. %{user_name} останется с уровнем доверия '%{current_trust_level}'. Если вы все же хотите понизить пользователя, заблокируйте вначале уровень доверия." rate_limiter: slow_down: "Вы выполняли это действие слишком часто, попробуйте еще раз позже." - too_many_requests: "Вы повторяете действие слишком часто. Пожалуйста, подождите %{time_left} до следующей попытки." by_type: first_day_replies_per_day: "Вы достигли лимита на создание ответов для нового пользователя в первый день на сайте. Пожалуйста, попробуйте через %{time_left}." first_day_topics_per_day: "Вы достигли лимита на создание тем для нового пользователя в первый день на сайте. Пожалуйста, попробуйте через %{time_left}." @@ -830,12 +838,10 @@ ru: allow_user_locale: "Позволять пользователям выбирать язык интерфейса" min_post_length: "Минимально допустимое количество символов в одном сообщении." min_first_post_length: "Минимально допустимое количество символов в первом сообщении (или теле темы)" - min_personal_message_post_length: "Минимально допустимое количество символов в сообщении в беседе." max_post_length: "Максимально допустимое количество символов в одном сообщении." topic_featured_link_enabled: "Разрешить публиковать ссылку с темами." min_topic_title_length: "Минимально допустимое количество символов в названии темы." max_topic_title_length: "Максимально допустимое количество символов в названии темы." - min_personal_message_title_length: "Минимально допустимое количество символов в заголовке сообщения в беседе." min_search_term_length: "Минимальное количество символов в поисковом запросе." allow_duplicate_topic_titles: "Разрешить создание тем с одинаковыми названиями." unique_posts_mins: "Количество минут до того, как пользователь сможет разместить сообщение с тем же содержанием." @@ -952,7 +958,6 @@ ru: max_bookmarks_per_day: "Максимальное количество созданных закладок пользователем в день." max_edits_per_day: "Максимальное количество редактирований пользователем в день." max_topics_per_day: "Максимальное количество тем, которые пользователь может создать за один день." - max_personal_messages_per_day: "Максимальное количество тем, которые пользователь может создать за один день." max_invites_per_day: "Максимальное количество приглашений, которое может отправить пользователь за один день." max_topic_invitations_per_day: "Максимальное количество приглашений в тему, которое может отправить пользователь в течении дня." alert_admins_if_errors_per_minute: "Количество ошибок в минуту для предупреждения администратора. Значение 0 отключает эту опцию. ВНИМАНИЕ: требуется перезагрузка." @@ -1105,7 +1110,6 @@ ru: emoji_set: "Какую коллекцию Emoji использовать?" enforce_square_emoji: "Принудительно использовать квадратный формат для всех смайлов." approve_unless_trust_level: "Сообщения для пользователей ниже этого уровня доверия подлежат проверки" - default_email_personal_messages: "По умолчанию присылать почтовое уведомление, когда кто-то оставляет пользователю личное сообщение." default_email_mailing_list_mode: "По умолчанию присылать почтовое уведомление, когда появляется новое сообщение." default_other_external_links_in_new_tab: "По умолчанию открывать внешние ссылки в новой вкладке." default_other_dynamic_favicon: "Показывать количество новых/обновлённых тем на иконке веб-браузера по умолчанию." diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 91194d8d50d..4f555f3d0e8 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -355,7 +355,6 @@ sk: broken: "Tento obrázok je poškodený" rate_limiter: slow_down: "Vykonali ste túto akciu príliš veľa krát, skúste neskôr" - too_many_requests: "Na vykonávanie tejto akcie máme nastavený denný limit. Prosím počkajte %{time_left} než skúsite znovu." by_type: first_day_replies_per_day: "Dosiahli ste maximálny počet odpovedí stanovený pre nového používateľa v jeho prvý deň. Prosím čakajte %{time_left} , než skúsite znova" first_day_topics_per_day: "Dosiahli ste maximálny počet tém stanovený pre nového používateľa v jeho prvý deň. Prosím čakajte %{time_left} , než skúsite znova" @@ -731,11 +730,9 @@ sk: allow_user_locale: "Povoliť používateľom zvoliť si vlastný jazyk" min_post_length: "Minimálny povolený počet znakov v príspevkoch" min_first_post_length: "Minimálny povolený počet znakov pre prvý príspevok (obsah témy)" - min_personal_message_post_length: "Minimálny povolený počet znakov v správe" max_post_length: "Maximálny povolený počet znakov v príspevkoch" min_topic_title_length: "Minimálny povolený počet znakov v názve témy" max_topic_title_length: "Maximálny povolený počet znakov v názve témy" - min_personal_message_title_length: "Minimálny povolený počet znakov v správe" min_search_term_length: "Minimálny povolený počet znakov vo vyhľadávaní" search_tokenize_chinese_japanese_korean: "Prinúť vyhľádávanie rozložiť Čínštinu/Japončinu/Kórejčinu dokonca i pre nie CJK stránky" allow_uncategorized_topics: "Pvoliť vytváranie tém bez kategórií. UPOZORNENIE: Pokiaľ existujú nekategorizované témy, musíte ich zaradiť do kategórii skôr než túto možnosť vypnete." @@ -870,7 +867,6 @@ sk: max_bookmarks_per_day: "Maximálny počet záložiek na jedného používateľa denne." max_edits_per_day: "Maximálny počet úprav používateľa denne." max_topics_per_day: "Maximálny počet tém používateľa denne." - max_personal_messages_per_day: "Maximálny počet správ ktoré môže používateľ denne vytvoriť." max_invites_per_day: "Maximálny počet pozvánok ktoré môže používateľ denne zaslať." max_topic_invitations_per_day: "Maximálny počet pozvánok do tém ktoré môže používateľ denne zaslať." suggested_topics: "Zobrazovaný počet navrhovaných tém na konci témy." @@ -1029,7 +1025,6 @@ sk: approve_unless_trust_level: "Príspevky používateľov pod touto úrovňou důvery musia byť schválené" auto_close_messages_post_count: "Maximálny počet povolených príspevkov v správe kým je automaticky uzavretá (0 znamená vypnuté)" auto_close_topics_post_count: "Maximálny počet povolených príspevkov v téme kým je automaticky uzavretá (0 znamená vypnuté)" - default_email_personal_messages: "Štandardne poslať email použivateľovi, ktorému niekto poslal správu." default_email_direct: "Štandardne poslať email ak niekto cituje/odpovedá na/zmieni alebo pozve používateľa." default_email_mailing_list_mode: "Štandardne poslať email pre každý nový príspevok." default_email_always: "Štandardne poslať emailovú správu dokonca i vtedy, ak je používateľ aktívny." diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index d71d4083fac..fccd25fc9df 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -214,7 +214,6 @@ sq: title: "udhëheqës" rate_limiter: slow_down: "E keni bërë këtë veprim shumë shpesh, prisni pak e provoni më vonë. " - too_many_requests: "Sistemi ka një limit për sa herë ai veprim mund të kryhet në një ditë. Prisni %{time_left} para se të provoni përsëri. " by_type: first_day_replies_per_day: "Keni arritur maksimumin e përgjigjeve që një përdorues i ri mund të postojë në ditën e parë. Prisni %{time_left} dhe provoni përsëri." first_day_topics_per_day: "Keni postuar maksimumin e temave që një anëtar i ri mund të krijojë në ditën e parë. Prisni %{time_left} para se të provoni përsëri. " @@ -546,11 +545,9 @@ sq: allow_user_locale: "Allow users to choose their own language interface preference" min_post_length: "Minimum allowed post length in characters" min_first_post_length: "Minimum allowed first post (topic body) length in characters" - min_personal_message_post_length: "Minimum allowed post length in characters for messages" max_post_length: "Maximum allowed post length in characters" min_topic_title_length: "Minimum allowed topic title length in characters" max_topic_title_length: "Maximum allowed topic title length in characters" - min_personal_message_title_length: "Minimum allowed title length for a message in characters" min_search_term_length: "Minimum valid search term length in characters" allow_uncategorized_topics: "Allow topics to be created without a category. WARNING: If there are any uncategorized topics, you must recategorize them before turning this off." allow_duplicate_topic_titles: "Allow topics with identical, duplicate titles." @@ -671,7 +668,6 @@ sq: max_bookmarks_per_day: "Maximum number of bookmarks per user per day." max_edits_per_day: "Maximum number of edits per user per day." max_topics_per_day: "Maximum number of topics a user can create per day." - max_personal_messages_per_day: "Maximum number of messages users can create per day." max_invites_per_day: "Maximum number of invites a user can send per day." max_topic_invitations_per_day: "Maximum number of topic invitations a user can send per day." suggested_topics: "Number of suggested topics shown at the bottom of a topic." diff --git a/config/locales/server.sr.yml b/config/locales/server.sr.yml index a5ff51bc385..f2a4d6fdc88 100644 --- a/config/locales/server.sr.yml +++ b/config/locales/server.sr.yml @@ -118,7 +118,6 @@ sr: site_settings: min_topic_title_length: "Minimalna dozvoljena dužina naslova teme u znakovima" max_topic_title_length: "Maksimalna dozvoljena dužina naslova teme u znakovima" - min_personal_message_title_length: "Minimalna dozvoljena dužina naslova poruke u znakovima" min_search_term_length: "Minimalna dozvoljena dužina termina za pretragu u znakovima" apple_touch_icon_url: "Ikonica koja se koristi za Apple touch uređaje. Preporučena veličina 144px sa 144px." max_replies_in_first_day: "Maksimalan broj odgovora koje korisnik može da kreira u prvih 24 sata nakon kreiranja svoje prve poruke." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index fe5e0795745..878bb0f76b7 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -349,7 +349,6 @@ sv: change_failed_explanation: "Du försökte degradera %{user_name} till '%{new_trust_level}'. Användarens förtroendenivå är redan '%{current_trust_level}'. %{user_name} kommer behålla '%{current_trust_level}'. Om du vill degradera användaren, lås förtroendenivån först" rate_limiter: slow_down: "Du har utfört den här handlingen för många gånger, försök igen senare." - too_many_requests: "Du gör det där för ofta. Var god vänta %{time_left} innan du försöker igen." by_type: first_day_replies_per_day: "Du har uppnått maximalt antal inlägg en ny användare kan göra under första dagen. Var god vänta %{time_left} innan du försöker igen." first_day_topics_per_day: "Du har uppnått maximalt antal nya ämnen som nya användare kan skapa under första dagen. Var god vänta %{time_left} innan du försöker igen." @@ -725,7 +724,6 @@ sv: email_polling_errored_recently: one: "E-postpolling har genererat ett fel de senaste 24 timmarna. Se <a href='/logs' target='_blank'>loggarna</a> för mer detaljer." other: "E-postpolling har genererat %{count} fel de senaste 24 timmarna. Se <a href='/logs' target='_blank'>loggarna</a> för mer detaljer." - missing_mailgun_api_key: "Servern är konfigurerad att skicka e-post via mailgun men du har inte tillhandahållit en API-nyckel att använda för verifiera webhook-meddelanden. " bad_favicon_url: "Uppladdningen av favoritikonen misslyckas. Kontrollera inställningen favicon_url i <a href='/admin/site_settings'>webbplatsinställningarna</a>." poll_pop3_timeout: "Anslutningen till POP3-servern har nått tidsgränsen. Inkommande e-postmeddelanden kunde inte mottas. Var vänlig kontrollera dina <a href='/admin/site_settings/category/email'>POP3-inställningar</a> och tjänsteleverantör." poll_pop3_auth_error: "Anslutning till POP3-servern misslyckas med ett autentiseringsfel. Var vänlig kontrollera dina <a href='/admin/site_settings/category/email'>POP3-inställningar</a>." @@ -736,13 +734,11 @@ sv: set_locale_from_accept_language_header: "sätt gränssnittets språk för anonyma användare från deras webbläsares rubriks språk. (EXPERIMENTELLT, det fungerar inte med anonym cache)" min_post_length: "Minsta tillåtna inläggslängd i antal tecken" min_first_post_length: "Lägsta antal tillåtna tecken i första inlägget (ämnestext)" - min_personal_message_post_length: "Lägst antal tillåtna tecken i inlägg för meddelanden" max_post_length: "Högsta tillåtna längd på inlägg i antal tecken" topic_featured_link_enabled: "Möjliggör att lägga upp en länk med ämnen." show_topic_featured_link_in_digest: "Visa ämnet i skiss länken som fanns i det nerkortade emailet." min_topic_title_length: "Lägsta tillåtna längd på ämnesrubrik i antal tecken" max_topic_title_length: "Högsta tillåtna längd på ämnesrubrik i antal tecken" - min_personal_message_title_length: "Lägst tillåtna längd på rubrik för ett meddelande i antal tecken" min_search_term_length: "Lägsta giltiga teckenlängd på sökterm" search_tokenize_chinese_japanese_korean: "Framtvinga sökning för att tokenisera kinesiska/japanska/koreanska även på webbplatser som inte är på dessa språk" search_prefer_recent_posts: "Om sökningar på ditt stora forum går långsamt, försök detta alternativ som är ett index av de senaste inläggen först." @@ -787,7 +783,6 @@ sv: summary_likes_required: "Minsta antal gillningar i ett ämne innan 'Sammanfatta det här ämnet' möjliggörs" summary_percent_filter: "Visa högsta % av inläggen när en användare bockar i 'Sammanfatta det här ämnet'" summary_max_results: "Maximalt antal inlägg som returneras av 'Sammanfatta det här ämnet'" - enable_personal_messages: "Tillåt användare med förtroendenivå 1 (konfigurera via minsta förtroendenivå för att skicka meddelanden) att skapa meddelanden och svara på meddelanden. Notera att personalen oavsett alltid kan skicka meddelanden. " enable_long_polling: "Meddelande-buss som används för notifiering kan använda long polling" long_polling_base_url: "URL som används för long polling (när en CDN levererar dynamiskt innehåll, kontrollera att det här är inställt till origin pull) se: http://origin.site.com" long_polling_interval: "Tid som servern bör vänta innan den svarar på klienter när det inte finns någon data att skicka (endast loggad på användare)" @@ -901,7 +896,6 @@ sv: max_bookmarks_per_day: "Max antal bokmärken per användare och dag." max_edits_per_day: "Max antal redigeringar per användare och dag." max_topics_per_day: "Max antal ämnen en användare kan skapa per dag." - max_personal_messages_per_day: "Max antal meddelanden användare kan skapa per dag." max_invites_per_day: "Max antal inbjudningar en användare kan skicka per dag." max_topic_invitations_per_day: "Max antal ämnesinbjudningar en användare kan skicka per dag." alert_admins_if_errors_per_minute: "Antal felindikeringar per minut för att utlösa ett administratörslarm. Ange 0 för att inaktivera den här funktionen. OBS: kräver omstart." @@ -963,7 +957,6 @@ sv: max_users_notified_per_group_mention: "Maximalt antal användare som kan få ett meddelande om en grupp anges (om tröskeln är uppfyllt kommer inga meddelande att skickas)" create_thumbnails: "Skapa miniatyrer och \"lightboxa\" bilder som är för stora för att passa in i ett inlägg." email_time_window_mins: "Vänta (n) minuter innan något notifikationsmejl skickas ut, för att ge användare chansen att redigera och slutföra deras inlägg." - personal_email_time_window_seconds: "Vänta (n) sekunder innan någon privat e-postnotifikation skickas ut för att ge användare chansen att redigera och slutföra deras meddelande." email_posts_context: "Hur många tidigare svar som ska inkluderas som kontext i e-postnotifikationer." flush_timings_secs: "Hur frekvent vi spolar tidsdata till servern, i sekunder." title_max_word_length: "Den maximala tillåtna ordlängden, i tecken, i ett ämnes rubrik." @@ -1119,7 +1112,6 @@ sv: code_formatting_style: "Kodknappen i redigeraren kommer att använda den här kodformatteringsstilen som standard" default_email_digest_frequency: "Standardinställning för hur ofta användare mottar e-postsammanfattningar." default_include_tl0_in_digests: "Standardinställning för hur ofta nya användare får e-postsammanfattningar. Användare kan ändra det i sina inställningar." - default_email_personal_messages: "Ange standardinställningen till att skicka ett e-post när någon skickar användaren ett meddelande." default_email_direct: "Ange standardinställningen till att skicka ett e-post när någon citerar/svarar/nämner eller bjuder in användaren." default_email_mailing_list_mode: "Ange standardinställningen till att skicka ett e-post för varje nytt inlägg." default_email_mailing_list_mode_frequency: "Standardinställning för hur ofta användare som aktiverar utskicksläge kommer att motta e-post." diff --git a/config/locales/server.te.yml b/config/locales/server.te.yml index e48114f9a13..5af4961c4ea 100644 --- a/config/locales/server.te.yml +++ b/config/locales/server.te.yml @@ -185,7 +185,6 @@ te: basic: title: "ప్రాథమిక సభ్యుడు" rate_limiter: - too_many_requests: "ఈ చర్యకు రోజువారీపరిమితి ఉంది. దయచేసి %{time_left} సమయం తర్వాత ప్రయత్నించండి" hours: one: "ఒక గంట" other: "%{count} గంటలు" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 472a752b7e7..40c01b079b3 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -306,7 +306,6 @@ tr_TR: change_failed_explanation: " %{user_name} adlı kullanıcıyı '%{new_trust_level}' seviyesine düşürmeye çalıştınız. Ancak, halihazırda kullanıcının güven seviyesi zaten '%{current_trust_level}'. %{user_name} '%{current_trust_level}' seviyesinde kalacak - eğer seviyesini düşürmek istiyorsanız öncelikle güven seviyesini kilitlemelisiniz" rate_limiter: slow_down: "Bu eylemi birçok sefer gerçekleştirdiniz, daha sonra tekrar deneyin." - too_many_requests: "Bu eylem için günlük limitinizi aştınız. Lütfen tekrar denemek için %{time_left} bekleyin. " by_type: first_day_replies_per_day: "Yeni bir kullanıcının ilk gününde verebileceği en fazla cevap sayısına ulaştınız. Lütfen tekrar denemek için %{time_left} bekleyin." first_day_topics_per_day: "Yeni bir kullanıcının ilk gününde oluşturabileceği en fazla konu sayısına ulaştınız. Lütfen tekrar denemek için %{time_left} bekleyin." @@ -641,11 +640,9 @@ tr_TR: set_locale_from_accept_language_header: "anonim kullanıcıların arayüz dilini tarayıcılarının dil başlığından al. (DENEYSELDİR, anonim önbellek ile çalışmaz)" min_post_length: "Gönderide olması gereken en az karakter sayısı" min_first_post_length: "İlk gönderi için (konu içi) izin verilen en az karakter sayısı" - min_personal_message_post_length: "İleti gönderileri için izin verilen en az karakter sayısı" max_post_length: "Gönderide izin verilen en fazla karakter sayısı" min_topic_title_length: "Konuda olması gereken en az karakter sayısı" max_topic_title_length: "Konu başlığında izin verilen en fazla karakter sayısı" - min_personal_message_title_length: "İleti başlıkları için izin verilen en az karakter sayısı" min_search_term_length: "Arama için girilecek kelimede olması gereken en az karakter sayısı" search_tokenize_chinese_japanese_korean: " CJK olmayan siteler dahil, -Çince/Japonca/Korece için aramayı bilgileri sıfırlamaya zorla" search_prefer_recent_posts: "Eğer büyük forumunuzda arama yavaş ise, bu seçenek daha yeni gönderilerin dizine eklenmesini deneyecek" @@ -689,7 +686,6 @@ tr_TR: summary_likes_required: "'Bu Konuyu Özetle'nin etkinleştirilmesi için konuda olması gereken en az beğeni sayısı" summary_percent_filter: "Kullanıcı 'Bu Konuyu Özetle'ye tıkladığında, gönderinin ilk % kısmını göster" summary_max_results: "'Bu Konuyu Özetle'den dönen en fazla gönderi sayısı" - enable_personal_messages: "Güven seviyesi 1'e(özel ileti göndermek için en az seviye ayarıyla belirlenebilir) sahip kullanıcıların ileti oluşturup cevaplamasına izin ver. Görevliler her durumda ileti gönderebilir." enable_long_polling: "Bildiri için kullanılan ileti yolu uzun sorgular yapabilir" long_polling_base_url: "Uzun sorgular için kullanılan baz URL (CDN dinamik içerik sunuyorsa, bunu origin olarak ayarladığına emin ol) ör: http://origin.site.com" long_polling_interval: "Gönderilecek bilgi olmadığı zaman sunucunun kullanıcılara geri dönmeden önce beklemesi gereken zaman (sadece giriş yapmış kullanıcın için)" @@ -794,7 +790,6 @@ tr_TR: max_bookmarks_per_day: "Kullanıcı başına düşen günlük en fazla imleme sayısı." max_edits_per_day: "Bir günde, bir kullanıcının yapabileceği en fazla düzenleme sayısı." max_topics_per_day: "Bir günde, bir kullanıcının oluşturabileceği en fazla konu sayısı." - max_personal_messages_per_day: "Bir günde kullanıcıların oluşturabileceği en fazla ileti sayısı" max_invites_per_day: "Bir günde, bir kullanıcının yollayabileceği en fazla davet sayısı." max_topic_invitations_per_day: "Bir günde, bir kullanıcının yollayabileceği en fazla başlık daveti sayısı." categories_topics: "/categories sayfasında gösterilecek olan konu sayısı." @@ -970,7 +965,6 @@ tr_TR: enforce_square_emoji: "Tüm emojileri kare en-boy oranına zorla" approve_unless_trust_level: "Bu güven seviyesi altındaki kullanıcılardan gelen gönderilerin onaylanması gerekir" default_email_digest_frequency: "Öntanımlı olarak kullanıcılar ne sıklıkla e-posta özeti alacak." - default_email_personal_messages: "Öntanımlı olarak birisi bir kullanıcıya ileti attığında e-posta gönder." default_email_direct: "Öntanımlı olarak birisi bir kullanıcı hakkında alıntı yapma, cevaplama, bahsetme ya da davet etme eylemlerini gerçekleştirdiğinde e-posta gönder." default_email_mailing_list_mode: "Öntanımlı olarak her yeni gönderi için bir e-posta gönder." disable_mailing_list_mode: "Kullanıcıların duyuru listesi modunu etkinleştirmesini engelle." diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 7dbc3e88a08..a702350c099 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -143,7 +143,6 @@ uk: member: title: "учасник" rate_limiter: - too_many_requests: "У нас є обмеження щодо того, скільки разів за добу можна виконати цю дію. Будь ласка, зачекайте %{time_left} перед тим, як спробувати ще раз." hours: one: "1 година" few: "%{count} години" diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index dcaccb35e23..c9bd7155328 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -297,7 +297,6 @@ vi: change_failed_explanation: "Bạn đã cố gắng để giảm hạng %{user_name} xuống '%{new_trust_level}'. Tuy nhiên cấp độ tin cậy hiện tại của họ đã là '%{current_trust_level}'. %{user_name} sẽ được giữ lại ở cấp độ '%{current_trust_level}' - nếu bạn muốn giảm hạng thành viên, trước tiên hãy khóa cấp độ tin cậy" rate_limiter: slow_down: "Hành động này đã được thực hiện quá nhiều lần. Bạn vui lòng thử lại sau." - too_many_requests: "Hành động bạn vừa thực hiện bị giới hạn theo ngày. Hãy chờ %{time_left} và thử lại." by_type: first_day_replies_per_day: "Bạn vừa vượt quá số lần trả lời tối đa trong ngày đầu của thành viên mới. Xin hãy chờ %{time_left} và thử lại sau." first_day_topics_per_day: "Bạn vừa đạt tới số lần mở topic tối đa cho thành viên mới. Xin vui lòng chờ %{time_left} trước khi thử lại." @@ -616,11 +615,9 @@ vi: set_locale_from_accept_language_header: "đặt ngôn ngữ giao diện cho người dùng ẩn danh từ tiêu đề ngôn ngữ trình duyệt web của họ. (KINH NGHIỆM, không hoạt động với bộ nhớ cache ẩn danh)" min_post_length: "Số kí tự tối thiểu trong bài đăng." min_first_post_length: "Chiều dài tối thiểu cho bài viết đầu tiên (nội dung chủ đề) tính theo ký tự." - min_personal_message_post_length: "Số kí tự tối thiểu trong tin nhắn." max_post_length: "Số kí tự tối đa trong bài đăng." min_topic_title_length: "Số kí tự tối thiểu trong tiêu đề chủ đề." max_topic_title_length: "Số kí tự tối đa trong tiêu đề chủ đề." - min_personal_message_title_length: "Chiều dài tối thiểu cho phép theo số kí tự của một thông điệp" min_search_term_length: "Số kí tự tối thiểu trong từ khóa tìm kiếm." search_tokenize_chinese_japanese_korean: "Bắt buộc tìm kiếm tách từ Chinese/Japanese/Korean ngay cả trên các site không phải là CJK" allow_uncategorized_topics: "Cho phép các chủ đề được tạo ra mà không gán chuyên mục. LƯU Ý: Nếu có bất kỳ chủ đề nào chưa gán chuyên mục, bạn phải phân loại chúng trước khi thay đổi." @@ -761,7 +758,6 @@ vi: max_bookmarks_per_day: "Số tối đa người dùng có thể đánh dấu mỗi ngày." max_edits_per_day: "Số tối đa người dùng có thể chỉnh sửa mỗi ngày." max_topics_per_day: "Số chủ đề tối đa người dùng có thể tạo mỗi ngày." - max_personal_messages_per_day: "Số tin nhắn tối đa người dùng có thể tạo mỗi ngày." max_invites_per_day: "Số tối đa người dùng có thể gửi lời mời mỗi ngày." max_topic_invitations_per_day: "Số tối đa lời mời chủ đề thành viên có thể gửi mỗi ngày." alert_admins_if_errors_per_minute: "Số lỗi trong một phút để kích hoạt cảnh báo admin, điền 0 để tắt tính năng này. LƯU Ý: yêu cầu khởi động lại." @@ -817,7 +813,6 @@ vi: max_mentions_per_post: "Số tối đa thông báo @name mà tất cả mọi người có thể sử dụng trong bài viết." create_thumbnails: "Tạo ảnh nhỏ và ảnh lightbox nếu quá lớn để vừa trong một bài đăng." email_time_window_mins: "Chờ (n) phút trước khi gửi bất kỳ một email thông báo nào, để cung cấp cho người dùng cơ hội để chỉnh sửa và hoàn tất bài viết của họ." - personal_email_time_window_seconds: "Đợi (n) giây trước khi gửi bất kỳ thông báo email nào, để cho thành viên cơ hội để chỉnh sửa và hoàn thiện các thông điệp của họ." email_posts_context: "Có bao nhiêu trả lời trước được kèm theo như là bối cảnh trong email thông báo." flush_timings_secs: "Tần suất tuôn dữ liệu thời gian tới server, theo giây." title_max_word_length: "Chiều dài tối đa chữ cho phép, tính theo ký tự, trong một tiêu đề chủ đề." @@ -934,7 +929,6 @@ vi: enforce_square_emoji: "Sử dụng kích thước vuông cho tất cả các emoji." approve_post_count: "Số lượng bài viết từ một thành viên mới hoặc thành viên cũ phải được duyệt" approve_unless_trust_level: "Bài viết của thành viên dưới cấp độ tin cậy này phải được duyệt" - default_email_personal_messages: "Gửi email khi ai đó nhắn tin cho thành viên theo mặc định." default_email_direct: "Gửi email khi ai đó trích dẫn/trả lời/đề cập hoặc mời thành viên theo mặc định." default_email_mailing_list_mode: "Mặc định gửi email cho mỗi bài viết mới." disable_mailing_list_mode: "Không cho phép thành viên bật chế độ danh sách thư." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 0a592faa869..f52dc202c40 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -437,7 +437,6 @@ zh_CN: broken: "此图片已损坏" rate_limiter: slow_down: "你执行这个操作太多次了,请稍后再试。" - too_many_requests: "你的请求过于频繁,请等待%{time_left}之后再试。" by_type: first_day_replies_per_day: "你回帖的数量已经超出身为新用户当天允许的上限,请等待%{time_left}之后再试。" first_day_topics_per_day: "你创建新主题的数量已经超出身为新用户当天允许的上限,请等待%{time_left}之后再试。" @@ -815,7 +814,6 @@ zh_CN: subfolder_ends_in_slash: "你的子目录设置不正确;DISCOURSE_RELATIVE_URL_ROOT以斜杠结尾。" email_polling_errored_recently: other: "邮件轮询在过去的 24 小时内出现了 %{count} 个错误。看一看<a href='/logs' target='_blank'>日志</a>寻找详情。" - missing_mailgun_api_key: "服务器设置使用 Mailgun 发送邮件,但并未设置验证 webhook 私信的 API 密钥。" bad_favicon_url: "网站图标无法载入。检查<a href='/admin/site_settings'>站点设置</a>中的 favicon_url。" poll_pop3_timeout: "至 POP3 服务器的连接超时。无法获取进站邮件。请检查<a href='/admin/site_settings/category/email'>POP3 设置</a>和服务商。" poll_pop3_auth_error: "至 POP3 服务器的连接验证失败。请检查<a href='/admin/site_settings/category/email'>POP3 设置</a>。" @@ -827,13 +825,11 @@ zh_CN: set_locale_from_accept_language_header: "为未登录用户按照他们的浏览器发送的请求头部设置界面语言。(实验性,无法和匿名缓存共同使用)" min_post_length: "帖子允许的最少字符数" min_first_post_length: "第一帖(主题内容)允许的最少字符数" - min_personal_message_post_length: "私信允许的最小字符数" max_post_length: "帖子允许的最大字符数" topic_featured_link_enabled: "允许发链接帖。" show_topic_featured_link_in_digest: "在摘要邮件中显示主题特色链接。" min_topic_title_length: "标题允许的最少字符数" max_topic_title_length: "标题允许的最大字符数" - min_personal_message_title_length: "私信标题允许的最小字符数" min_search_term_length: "搜索条件允许的最少字符数" search_tokenize_chinese_japanese_korean: "在非中/日/韩语站点强制切割中/日/韩语搜索分词" search_prefer_recent_posts: "如果搜索大型论坛较慢,这个选项将优先尝试最新的帖子" @@ -888,8 +884,6 @@ zh_CN: summary_likes_required: "在一个主题启用'摘要模式'的最小赞的数量" summary_percent_filter: "当用户点击摘要,显示前 % 几的帖子" summary_max_results: "“概括主题”返回的最大帖子数量" - enable_personal_messages: "允许信任等级1(可以另外选择发送私信的信任等级)的用户创建私信和回复私信。注意:管理人员不受限制。" - enable_personal_email_messages: "允许信任等级 4 (可通过发送私信的最低信任等级进行设置) 的用户创建私信和回复私信。注意:管理人员不受限制。" enable_long_polling: "启用 Message bus 使通知功能可以使用长轮询(long polling)" long_polling_base_url: "长轮询的基本 URL(当用 CDN 分发动态能让时,请设置此至原始拉取地址)例如:http://origin.site.com" long_polling_interval: "当没有数据向客户端发送时服务器端应等待的时间(仅对已登录用户有效)" @@ -1022,7 +1016,6 @@ zh_CN: max_bookmarks_per_day: "每个用户每天能做书签数量的最大值。" max_edits_per_day: "每个用户每天能编辑的次数的最大值。" max_topics_per_day: "每个用户每天能创建的主题数量的最大值。" - max_personal_messages_per_day: "每个用户每天能发私信数量的最大值。" max_invites_per_day: "每个用户每天能创建的邀请数量的最大值。" max_topic_invitations_per_day: "每个用户每天能创建的邀请至主题数量的最大值。" max_logins_per_ip_per_hour: "一小时内同一个IP(网络)地址能允许最大的登陆次数。" @@ -1093,7 +1086,6 @@ zh_CN: enable_mentions: "允许用户提及其他用户。" create_thumbnails: "为太大而无法恰当地显示在帖子里的图片创建 lightbox 缩略图。" email_time_window_mins: "等待多少(n)分钟才给用户发送通知电子邮件,好让他们有机会自己来编辑和完善他们的帖子。" - personal_email_time_window_seconds: "等待多少(n)秒再给用户发送通知电子邮件,这可以让用户有时间来编辑和完善他们的私信。" email_posts_context: "在通知邮件中包含的作为上下文的回复数量。" flush_timings_secs: "向服务器刷新时间数据的频率,以秒为单位。" title_max_word_length: "在主题的标题中,允许的词语长度的最大字符数。" @@ -1271,7 +1263,6 @@ zh_CN: watched_words_regular_expressions: "监视词是正则表达式。" default_email_digest_frequency: "用户收到摘要邮件的默认频率。" default_include_tl0_in_digests: "在摘要邮件中默认包含新用户帖子。用户可以自行在参数设置中更改这个设置。" - default_email_personal_messages: "默认在有人发私信给用户时发送一封邮件通知。" default_email_direct: "默认在有人引用、回复、提及或者邀请用户时发送一封邮件通知。" default_email_mailing_list_mode: "默认为每一个新帖子发送一封邮件通知。" default_email_mailing_list_mode_frequency: "邮件列表模式下,用户收到邮件的默认频率。" @@ -2368,21 +2359,6 @@ zh_CN: signup_after_approval: title: "在审批之后注册" subject_template: "你已经被 %{site_name} 论坛批准加入了!" - text_body_template: | - 欢迎来到%{site_name}! - - 管理人员批准了你在%{site_name}的帐号。 - - 点击下面的链接确认并激活你的新帐号: - %{base_url}/u/activate-account/%{email_token} - - 如果以上链接无法点击,请将它复制并粘贴到浏览器的地址栏。 - - %{new_user_tips} - - 我们相信任何时候[讨论应该文明](%{base_url}/guidelines)。 - - 享受你的时光! signup: title: "注册" subject_template: "[%{email_prefix}] 确认你的新账户" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 9f065006446..cec5b5db8fc 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -395,7 +395,6 @@ zh_TW: change_failed_explanation: "你嘗試將%{user_name}降至%{new_trust_level}。然而他們的信任等級已經是%{current_trust_level}。%{user_name}將仍處于%{current_trust_level}——如果你想要降級用戶,先鎖定信任等級。" rate_limiter: slow_down: "您這個動作重複太多次,請稍後再試。" - too_many_requests: "你的瀏覽速度過於頻繁,請等待 %{time_left} 後再試。" by_type: first_day_replies_per_day: "你回帖的數量已經超出身為新用戶當天允許的上限,請等待%{time_left}之後再試。" first_day_topics_per_day: "你創建新主題的數量已經超出身為新用戶當天允許的上限,請等待%{time_left}之後再試。" @@ -754,7 +753,6 @@ zh_TW: subfolder_ends_in_slash: "你的子目錄設置不正確;DISCOURSE_RELATIVE_URL_ROOT以斜杠結尾。" email_polling_errored_recently: other: "郵件輪詢在過去的 24 小時內出現了 %{count} 個錯誤。看一看<a href='/logs' target='_blank'>日誌</a>尋找詳情。" - missing_mailgun_api_key: "伺服器設置使用 Mailgun 寄送電子郵件,但你尚未設置用來驗證 webhook 訊息的 API 密鑰。" bad_favicon_url: "網站表徵圖無法載入。檢查<a href='/admin/site_settings'>站點設置</a>中的 favicon_url。" poll_pop3_timeout: "至 POP3 伺服器的連接超時。無法獲取進站郵件。請檢查<a href='/admin/site_settings/category/email'>POP3 設置</a>和服務商。" poll_pop3_auth_error: "至 POP3 伺服器的連接驗證失敗。請檢查<a href='/admin/site_settings/category/email'>POP3 設置</a>。" @@ -766,13 +764,11 @@ zh_TW: set_locale_from_accept_language_header: "為未登錄用戶按照他們的瀏覽器發送的請求頭部設置界面語言。(實驗性,無法和匿名緩存共同使用)" min_post_length: "文章允許的最小文字數" min_first_post_length: "第一篇文章允許的最少文字數" - min_personal_message_post_length: "私訊允許的最小文字數" max_post_length: "文章允許的最大文字數" topic_featured_link_enabled: "允許發連結帖。" show_topic_featured_link_in_digest: "在摘要郵件中顯示主題特色連結。" min_topic_title_length: "標題允許的最小文字數" max_topic_title_length: "標題允許的最大文字數" - min_personal_message_title_length: "標題允許的最小文字數" min_search_term_length: "搜尋條件允許的最小文字數" search_tokenize_chinese_japanese_korean: "在非中/日/韓語站點強制切割中/日/韓語搜索分詞" search_prefer_recent_posts: "如果搜索大型論壇較慢,這個選項將優先嘗試最新的帖子" @@ -820,7 +816,6 @@ zh_TW: summary_likes_required: "如果使用了\"此話題的摘用\",話題顯示時需滿足最小得到\"讚\"的數量" summary_percent_filter: "當用戶點擊 \"此話題的摘要\",顯示前面多少 % 的文章" summary_max_results: "“概括主題”返回的最大帖子數量" - enable_personal_messages: "允許信任等級1(可以另外選擇發送消息的信任等級)的用戶創建消息和回覆消息。注意:管理人員不受限制。" enable_long_polling: "啟用消息匯流排使通知功能可以使用長輪詢(long polling)" long_polling_base_url: "長輪詢的基本 URL(當用 CDN 分發動態內容,請設置此至原始拉取地址)例如:http://origin.site.com" long_polling_interval: "當沒有數據向客戶端發送時伺服器端應等待的時間(僅對已登錄用戶有效)" @@ -939,7 +934,6 @@ zh_TW: max_bookmarks_per_day: "每個用戶每天最多能建立\"書籤\"的數量" max_edits_per_day: "每個用戶每天最大的\"編輯次數\"的數量" max_topics_per_day: "每個用戶每天最多建立\"討論話題\"的數量" - max_personal_messages_per_day: "用戶每天能建立訊息的數量上限" max_invites_per_day: "每個用戶每天最多能邀請用戶的數量。" max_topic_invitations_per_day: "每個用戶每天能創建的邀請至主題數量的最大值。" alert_admins_if_errors_per_minute: "觸發管理員警示的每分鐘錯誤數量。設為 0 會停用這個功能。注意:需要重啟。" @@ -1003,7 +997,6 @@ zh_TW: max_users_notified_per_group_mention: "當群組被提及時,接受提醒的最大用戶數 ( 超過閾值後將不發送提醒 )" create_thumbnails: "為太大而無法在帖子裡正常顯示的圖片創造縮略圖及 lightbox 圖片。" email_time_window_mins: "等待多少 (n) 分鐘才給用戶發送通知電子郵件,好讓他們有機會自己來編輯和完善他們的帖子。" - personal_email_time_window_seconds: "等待多少(n)秒再給用戶發送通知電子郵件,這可以讓用戶有時間來編輯和完善他們的消息。" email_posts_context: "在通知電郵中包含的作為上下文的回覆數量。" flush_timings_secs: "刷新時間資料的頻率,以秒為單位" title_max_word_length: "在主題的標題中,允許的詞語長度的最大字元數。" @@ -1164,7 +1157,6 @@ zh_TW: code_formatting_style: "編輯器中的代碼格式化按鈕設置的預設格式" default_email_digest_frequency: "用戶收到摘要郵件的預設頻率。" default_include_tl0_in_digests: "在摘要郵件中預設包含新用戶的貼文。用戶可以自行在參數設置中更改這個設定。" - default_email_personal_messages: "預設在有人發消息給用戶時發送一封郵件通知。" default_email_direct: "預設在有人引用、回覆、提及或者邀請用戶時發送一封郵件通知。" default_email_mailing_list_mode: "預設為每一個新帖子發送一封郵件通知。" default_email_mailing_list_mode_frequency: "郵件列表模式下,用戶收到郵件的預設頻率。" @@ -1899,21 +1891,6 @@ zh_TW: signup_after_approval: title: "在同意之後註冊" subject_template: "你已受到 %{site_name} 的認可!" - text_body_template: | - 歡迎加入 %{site_name}! - - 管理員已通過你的帳號申請。 - - 請點擊以下連結,以確認啟用你的新帳號: - %{base_url}/u/activate-account/%{email_token} - - 如果上面的連結無法點擊,請複製該連結,並貼到瀏覽器中開啟。 - - %{new_user_tips} - - 我們永遠相信[文明的社群行為](%{base_url}/guidelines)。 - - 好好享受您在此的時光吧! signup: title: "註冊" subject_template: "[%{email_prefix}] 確認你的新帳號" diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf index a1f0551589f..f7544fdce1a 100644 --- a/config/nginx.sample.conf +++ b/config/nginx.sample.conf @@ -188,7 +188,7 @@ server { # This big block is needed so we can selectively enable # acceleration for backups and avatars # see note about repetition above - location ~ ^/(letter_avatar/|user_avatar|highlight-js|stylesheets|favicon/proxied) { + location ~ ^/(letter_avatar/|user_avatar|highlight-js|stylesheets|favicon/proxied|service-worker) { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/config/routes.rb b/config/routes.rb index 5c0e947df68..c2677c6008a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -129,6 +129,7 @@ Discourse::Application.routes.draw do get "tl3_requirements" put "anonymize" post "reset_bounce_score" + put "disable_second_factor" end get "users/:id.json" => 'users#show', defaults: { format: 'json' } get 'users/:id/:username' => 'users#show', constraints: { username: RouteFormat.username } @@ -302,6 +303,7 @@ Discourse::Application.routes.draw do get "session/current" => "session#current" get "session/csrf" => "session#csrf" get "session/email-login/:token" => "session#email_login" + post "session/email-login/:token" => "session#email_login" get "composer_messages" => "composer_messages#index" post "composer/parse_html" => "composer#parse_html" @@ -329,12 +331,16 @@ Discourse::Application.routes.draw do end end + post "#{root_path}/second_factors" => "users#create_second_factor" + put "#{root_path}/second_factor" => "users#update_second_factor" + put "#{root_path}/update-activation-email" => "users#update_activation_email" get "#{root_path}/hp" => "users#get_honeypot_value" post "#{root_path}/email-login" => "users#email_login" get "#{root_path}/admin-login" => "users#admin_login" put "#{root_path}/admin-login" => "users#admin_login" get "#{root_path}/admin-login/:token" => "users#admin_login" + put "#{root_path}/admin-login/:token" => "users#admin_login" post "#{root_path}/toggle-anon" => "users#toggle_anon" post "#{root_path}/read-faq" => "users#read_faq" get "#{root_path}/search/users" => "users#search_users" @@ -349,6 +355,7 @@ Discourse::Application.routes.draw do get "#{root_path}/activate-account/:token" => "users#activate_account" put({ "#{root_path}/activate-account/:token" => "users#perform_account_activation" }.merge(index == 1 ? { as: 'perform_activate_account' } : {})) get "#{root_path}/authorize-email/:token" => "users_email#confirm" + put "#{root_path}/authorize-email/:token" => "users_email#confirm" get({ "#{root_path}/confirm-admin/:token" => "users#confirm_admin", constraints: { token: /[0-9a-f]+/ } @@ -381,6 +388,7 @@ Discourse::Application.routes.draw do put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: { username: RouteFormat.username } get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: { username: RouteFormat.username } put "#{root_path}/:username/preferences/username" => "users#username", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/preferences/second-factor" => "users#preferences", constraints: { username: RouteFormat.username } delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormat.username } put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username } get "#{root_path}/:username/preferences/card-badge" => "users#card_badge", constraints: { username: RouteFormat.username } @@ -697,7 +705,15 @@ Discourse::Application.routes.draw do post "draft" => "draft#update" delete "draft" => "draft#destroy" - get "service-worker" => "static#service_worker_asset", format: :js + if service_worker_asset = Rails.application.assets_manifest.assets['service-worker.js'] + # https://developers.google.com/web/fundamentals/codelabs/debugging-service-workers/ + # Normally the browser will wait until a user closes all tabs that contain the + # current site before updating to a new Service Worker. + # Support the old Service Worker path to avoid routing error filling up the + # logs. + get "/service-worker.js" => redirect(service_worker_asset), format: :js + get service_worker_asset => "static#service_worker_asset", format: :js + end get "cdn_asset/:site/*path" => "static#cdn_asset", format: false get "brotli_asset/*path" => "static#brotli_asset", format: false diff --git a/config/site_settings.yml b/config/site_settings.yml index e68b1000ec0..abb5d65dd56 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -437,6 +437,8 @@ groups: enable_group_directory: client: true default: true + group_in_subject: + default: false posting: min_post_length: @@ -500,6 +502,7 @@ posting: client: true default: 2 min: 1 + max_emojis_in_title: 1 allow_uncategorized_topics: client: true default: true @@ -715,7 +718,7 @@ email: pop3_polling_username: '' pop3_polling_password: '' log_mail_processing_failures: false - incoming_email_prefer_html: false + incoming_email_prefer_html: true email_in: default: false client: true @@ -794,6 +797,11 @@ files: default: 'jpg|jpeg|png|gif' refresh: true type: list + authorized_extensions_for_staff: + client: true + default: '' + refresh: true + type: list crawl_images: default: true max_image_width: @@ -903,6 +911,9 @@ trust: min_trust_to_post_links: default: 0 enum: 'TrustLevelSetting' + min_trust_to_post_images: + default: 0 + enum: 'TrustLevelSetting' allow_flagging_staff: true tl1_requires_topics_entered: 5 tl1_requires_read_posts: @@ -969,7 +980,7 @@ security: allow_moderators_to_create_categories: false non_crawler_user_agents: hidden: true - default: 'trident|webkit|gecko|chrome|safari|msie|opera' + default: 'trident|webkit|gecko|chrome|safari|msie|opera|goanna' type: list crawler_user_agents: hidden: true diff --git a/db/migrate/20180109222722_create_user_second_factors.rb b/db/migrate/20180109222722_create_user_second_factors.rb new file mode 100644 index 00000000000..ff038c17762 --- /dev/null +++ b/db/migrate/20180109222722_create_user_second_factors.rb @@ -0,0 +1,12 @@ +class CreateUserSecondFactors < ActiveRecord::Migration[5.1] + def change + create_table :user_second_factors do |t| + t.integer :user_id, null: false + t.integer :method, null: false + t.string :data, null: false + t.boolean :enabled, null: false, default: false + t.timestamp :last_used + t.timestamps + end + end +end diff --git a/docs/INSTALL-cloud.md b/docs/INSTALL-cloud.md index aaca49a3ad7..3cee1f61fa6 100644 --- a/docs/INSTALL-cloud.md +++ b/docs/INSTALL-cloud.md @@ -62,14 +62,17 @@ Launch the setup tool at Answer the following questions when prompted: Hostname for your Discourse? [discourse.example.com]: - Email address for admin account? [me@example.com]: + Email address for admin account(s)? [me@example.com,you@example.com]: SMTP server address? [smtp.example.com]: - SMTP user name? [postmaster@discourse.example.com]: - SMTP port [587]: - SMTP password? []: + SMTP port? [587]: + SMTP user name? [user@example.com]: + SMTP password? [pa$$word]: + Let's Encrypt account email? (ENTER to skip) [me@example.com]: This will generate an `app.yml` configuration file on your behalf, and then kicks off bootstrap. Bootstrapping takes between **2-8 minutes** to set up your Discourse. If you need to change these settings after bootstrapping, you can run `./discourse-setup` again (it will read your old values from the file) or edit `/containers/app.yml` with `nano` and then `./launcher rebuild app`, otherwise your changes will not take effect. +**NOTE:** You should not attempt to enable Let's Encrypt unless the DNS record for hostname resolves to your server. You can run `./discourse-setup` again later to make any changes. + ### Start Discourse Once bootstrapping is complete, your Discourse should be accessible in your web browser via the domain name `discourse.example.com` you entered earlier, provided you configured DNS. If not, you can visit the server IP directly, e.g. `http://192.168.1.1`. diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index c46ee178b33..cacfe81962d 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -5,14 +5,14 @@ require_dependency "rate_limiter" class Auth::DefaultCurrentUserProvider - CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER".freeze - API_KEY ||= "api_key".freeze - USER_API_KEY ||= "HTTP_USER_API_KEY".freeze - USER_API_CLIENT_ID ||= "HTTP_USER_API_CLIENT_ID".freeze - API_KEY_ENV ||= "_DISCOURSE_API".freeze - USER_API_KEY_ENV ||= "_DISCOURSE_USER_API".freeze - TOKEN_COOKIE ||= "_t".freeze - PATH_INFO ||= "PATH_INFO".freeze + CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER" + API_KEY ||= "api_key" + USER_API_KEY ||= "HTTP_USER_API_KEY" + USER_API_CLIENT_ID ||= "HTTP_USER_API_CLIENT_ID" + API_KEY_ENV ||= "_DISCOURSE_API" + USER_API_KEY_ENV ||= "_DISCOURSE_USER_API" + TOKEN_COOKIE ||= "_t" + PATH_INFO ||= "PATH_INFO" COOKIE_ATTEMPTS_PER_MIN ||= 10 # do all current user initialization here @@ -86,8 +86,11 @@ class Auth::DefaultCurrentUserProvider raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active @env[API_KEY_ENV] = true - limiter_min = RateLimiter.new(nil, "admin_api_min_#{api_key}", GlobalSetting.max_admin_api_reqs_per_key_per_minute, 60) - limiter_min.performed! + # 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 end # user api key handling diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index e9637755f09..263d139e729 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -368,15 +368,13 @@ class CookedPostProcessor end def post_process_oneboxes - args = { - post_id: @post.id, - invalidate_oneboxes: !!@opts[:invalidate_oneboxes], - } - - # apply oneboxes - Oneboxer.apply(@doc, topic_id: @post.topic_id) do |url| + Oneboxer.apply(@doc) do |url| @has_oneboxes = true - Oneboxer.onebox(url, args) + Oneboxer.onebox(url, + invalidate_oneboxes: !!@opts[:invalidate_oneboxes], + user_id: @post&.user_id, + category_id: @post&.topic&.category_id + ) end oneboxed_images.each do |img| diff --git a/lib/discourse.rb b/lib/discourse.rb index 036836c6057..45b6416c6b1 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -236,15 +236,11 @@ module Discourse end def self.route_for(uri) - - uri = URI(uri) rescue nil unless (uri.is_a?(URI)) + uri = URI(uri) rescue nil unless uri.is_a?(URI) return unless uri path = uri.path || "" - if (uri.host == Discourse.current_hostname && - path.start_with?(Discourse.base_uri)) || - !uri.host - + if !uri.host || (uri.host == Discourse.current_hostname && path.start_with?(Discourse.base_uri)) path.slice!(Discourse.base_uri) return Rails.application.routes.recognize_path(path) end diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb index 9bc9cfd5dd5..ac95582586d 100644 --- a/lib/email/message_builder.rb +++ b/lib/email/message_builder.rb @@ -62,7 +62,7 @@ module Email subject = String.new(SiteSetting.email_subject) subject.gsub!("%{site_name}", @template_args[:email_prefix]) subject.gsub!("%{optional_re}", @opts[:add_re_to_subject] ? I18n.t('subject_re', @template_args) : '') - subject.gsub!("%{optional_pm}", @opts[:private_reply] ? I18n.t('subject_pm', @template_args) : '') + subject.gsub!("%{optional_pm}", @opts[:private_reply] ? @template_args[:subject_pm] : '') subject.gsub!("%{optional_cat}", @template_args[:show_category_in_subject] ? "[#{@template_args[:show_category_in_subject]}] " : '') subject.gsub!("%{topic_title}", @template_args[:topic_title]) if @template_args[:topic_title] # must be last for safety else diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 19389398ca9..bb5576a5ad8 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -238,11 +238,13 @@ module Email text_content_type = @mail.text_part&.content_type elsif @mail.content_type.to_s["text/html"] html = fix_charset(@mail) - else + elsif @mail.content_type.blank? || @mail.content_type["text/plain"] text = fix_charset(@mail) text_content_type = @mail.content_type end + return unless text.present? || html.present? + if text.present? text = trim_discourse_markers(text) text, elided_text = trim_reply_and_extract_elided(text) @@ -690,11 +692,17 @@ module Email raise InvalidPostAction.new(e) end + def is_whitelisted_attachment?(attachment) + attachment.content_type !~ SiteSetting.attachment_content_type_blacklist_regex && + attachment.filename !~ SiteSetting.attachment_filename_blacklist_regex + end + def attachments # strip blacklisted attachments (mostly signatures) - @attachments ||= @mail.attachments.select do |attachment| - attachment.content_type !~ SiteSetting.attachment_content_type_blacklist_regex && - attachment.filename !~ SiteSetting.attachment_filename_blacklist_regex + @attachments ||= begin + attachments = @mail.attachments.select { |attachment| is_whitelisted_attachment?(attachment) } + attachments << @mail if @mail.attachment? && is_whitelisted_attachment?(@mail) + attachments end end diff --git a/lib/final_destination.rb b/lib/final_destination.rb index c6c989de9a9..a1180b4618a 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -73,7 +73,7 @@ class FinalDestination "Host" => @uri.hostname } - result['cookie'] = @cookie if @cookie + result['Cookie'] = @cookie if @cookie result end @@ -164,7 +164,7 @@ class FinalDestination ) location = nil - headers = nil + response_headers = nil response_status = response.status.to_i @@ -181,31 +181,29 @@ class FinalDestination return @uri end - headers = {} + response_headers = {} if cookie_val = get_response.get_fields('set-cookie') - headers['set-cookie'] = cookie_val.join + response_headers[:cookies] = cookie_val end - # TODO this is confusing why grab location for anything not - # between 300-400 ? if location_val = get_response.get_fields('location') - headers['location'] = location_val.join + response_headers[:location] = location_val.join end end - unless headers - headers = {} - response.headers.each do |k, v| - headers[k.to_s.downcase] = v - end + unless response_headers + response_headers = { + cookies: response.data[:cookies] || response.headers[:"set-cookie"], + location: response.headers[:location] + } end if (300..399).include?(response_status) - location = headers["location"] + location = response_headers[:location] end - if set_cookie = headers["set-cookie"] - @cookie = set_cookie + if cookies = response_headers[:cookies] + @cookie = Array.wrap(cookies).map { |c| c.split(';').first.strip }.join('; ') end if location diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index aa92423b57c..4344e5fd34f 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -22,7 +22,9 @@ module PostGuardian result = if authenticated? && post && !@user.anonymous? # post made by staff, but we don't allow staff flags - return false if !SiteSetting.allow_flagging_staff? && post.user.staff? + return false if is_flag && + (!SiteSetting.allow_flagging_staff?) && + post.user.staff? return false if [:notify_user, :notify_moderators].include?(action_key) && !SiteSetting.enable_personal_messages? diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index 26a1056262f..c9f056b034c 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -72,4 +72,8 @@ module UserGuardian user == @user || is_staff? end + def can_disable_second_factor?(user) + user && can_administer_user?(user) + end + end diff --git a/lib/hijack.rb b/lib/hijack.rb index 9814ea4bef1..25a6acc7dbc 100644 --- a/lib/hijack.rb +++ b/lib/hijack.rb @@ -51,12 +51,14 @@ module Hijack instance.response.headers[k] = v end + view_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin instance.instance_eval(&blk) rescue => e # TODO we need to reuse our exception handling in ApplicationController Discourse.warn_exception(e, message: "Failed to process hijacked response correctly", env: env) end + view_runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - view_start unless instance.response_body || response.committed? instance.status = 500 @@ -94,6 +96,34 @@ module Hijack # happens if client terminated before we responded, ignore io = nil ensure + + if Rails.configuration.try(:lograge).try(:enabled) + if timings + db_runtime = 0 + if timings[:sql] + db_runtime = timings[:sql][:duration] + end + + subscriber = Lograge::RequestLogSubscriber.new + payload = ActiveSupport::HashWithIndifferentAccess.new( + controller: self.class.name, + action: action_name, + params: request.filtered_parameters, + headers: request.headers, + format: request.format.ref, + method: request.request_method, + path: request.fullpath, + view_runtime: view_runtime * 1000.0, + db_runtime: db_runtime * 1000.0, + timings: timings, + status: response.status + ) + + event = ActiveSupport::Notifications::Event.new("hijack", Time.now, Time.now + timings[:total_duration], "", payload) + subscriber.process_action(event) + end + end + MethodProfiler.clear Thread.current[Logster::Logger::LOGSTER_ENV] = nil diff --git a/lib/html_normalize.rb b/lib/html_normalize.rb deleted file mode 100644 index 1243a4426a6..00000000000 --- a/lib/html_normalize.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true -# -# this class is used to normalize html output for internal comparisons in specs -# -require 'oga' - -class HtmlNormalize - - def self.normalize(html) - parsed = Oga.parse_html(html.strip, strict: true) - if parsed.children.length != 1 - puts parsed.children.count - raise "expecting a single child" - end - new(parsed.children.first).format - end - - SELF_CLOSE = Set.new(%w{area base br col command embed hr img input keygen line meta param source track wbr}) - - BLOCK = Set.new(%w{ - html - body - aside - p - h1 h2 h3 h4 h5 h6 - ol ul - address - blockquote - dl - div - fieldset - form - hr - noscript - table - pre - }) - - def initialize(doc) - @doc = doc - end - - def format - buffer = String.new - dump_node(@doc, 0, buffer) - buffer.strip! - buffer - end - - def inline?(node) - Oga::XML::Text === node || !BLOCK.include?(node.name.downcase) - end - - def dump_node(node, indent = 0, buffer) - - if Oga::XML::Text === node - if node.parent&.name - buffer << node.text - end - return - end - - name = node.name.downcase - - block = BLOCK.include?(name) - - buffer << " " * indent * 2 if block - - buffer << "<" << name - - attrs = node&.attributes - if (attrs && attrs.length > 0) - attrs.sort! { |x, y| x.name <=> y.name } - attrs.each do |a| - buffer << " " - buffer << a.name - if a.value - buffer << "='" - buffer << a.value - buffer << "'" - end - end - end - - buffer << ">" - - if block - buffer << "\n" - end - - children = node.children - children = trim(children) if block - - inline_buffer = nil - - children&.each do |child| - if block && inline?(child) - inline_buffer ||= String.new - dump_node(child, indent + 1, inline_buffer) - else - if inline_buffer - buffer << " " * (indent + 1) * 2 - buffer << inline_buffer.strip - inline_buffer = nil - else - dump_node(child, indent + 1, buffer) - end - end - end - - if inline_buffer - buffer << " " * (indent + 1) * 2 - buffer << inline_buffer.strip - inline_buffer = nil - end - - if block - buffer << "\n" unless buffer[-1] == "\n" - buffer << " " * indent * 2 - end - - unless SELF_CLOSE.include?(name) - buffer << "</" << name - buffer << ">\n" - end - end - - def trim(nodes) - start = 0 - finish = nodes.length - - nodes.each do |n| - if Oga::XML::Text === n && n.text.blank? - start += 1 - else - break - end - end - - nodes.reverse_each do |n| - if Oga::XML::Text === n && n.text.blank? - finish -= 1 - else - break - end - end - - nodes[start...finish] - end - -end diff --git a/lib/middleware/request_tracker.rb b/lib/middleware/request_tracker.rb index f0ce009a6cf..d53b029ee82 100644 --- a/lib/middleware/request_tracker.rb +++ b/lib/middleware/request_tracker.rb @@ -24,6 +24,14 @@ class Middleware::RequestTracker MethodProfiler.patch(Redis::Client, [ :call, :call_pipeline ], :redis) + + MethodProfiler.patch(Net::HTTP, [ + :request + ], :net) + + MethodProfiler.patch(Excon::Connection, [ + :request + ], :net) @patched_instrumentation = true end diff --git a/lib/onebox/engine/discourse_local_onebox.rb b/lib/onebox/engine/discourse_local_onebox.rb deleted file mode 100644 index b4add001ac0..00000000000 --- a/lib/onebox/engine/discourse_local_onebox.rb +++ /dev/null @@ -1,129 +0,0 @@ -module Onebox - module Engine - class DiscourseLocalOnebox - include Engine - - # Use this onebox before others - def self.priority - 1 - end - - def self.===(other) - url = other.to_s - return false unless url[Discourse.base_url] - - route = Discourse.route_for(url) - - !!(route[:controller] =~ /topics|uploads|users/) - rescue ActionController::RoutingError - false - end - - def to_html - uri = URI(@url) - path = uri.path || "" - route = Discourse.route_for(uri) - - case route[:controller] - when "uploads" then upload_html(path) - when "topics" then topic_html(route) - when "users" then user_html(route) - end - end - - private - - def upload_html(path) - case File.extname(path) - when /^\.(mov|mp4|webm|ogv)$/i - "<video width='100%' height='100%' controls><source src='#{@url}'><a href='#{@url}'>#{@url}</a></video>" - when /^\.(mp3|ogg|wav|m4a)$/i - "<audio controls><source src='#{@url}'><a href='#{@url}'>#{@url}</a></audio>" - end - end - - def topic_html(route) - link = "<a href='#{@url}'>#{@url}</a>" - source_topic_id = @url[/[&?]source_topic_id=(\d+)/, 1].to_i - source_topic = Topic.find_by(id: source_topic_id) if source_topic_id > 0 - - if route[:post_number].present? && route[:post_number].to_i > 1 - post = Post.find_by(topic_id: route[:topic_id], post_number: route[:post_number]) - return link unless can_see_post?(post, source_topic) - - topic = post.topic - slug = Slug.for(topic.title) - excerpt = post.excerpt(SiteSetting.post_onebox_maxlength) - excerpt.gsub!(/[\r\n]+/, " ") - excerpt.gsub!("[/quote]", "[quote]") # don't break my quote - - quote = "[quote=\"#{post.user.username}, topic:#{topic.id}, slug:#{slug}, post:#{post.post_number}\"]\n#{excerpt}\n[/quote]" - - args = {} - args[:topic_id] = source_topic_id if source_topic_id > 0 - - PrettyText.cook(quote, args) - else - topic = Topic.find_by(id: route[:topic_id]) - return link unless can_see_topic?(topic, source_topic) - - first_post = topic.ordered_posts.first - - args = { - topic_id: topic.id, - avatar: PrettyText.avatar_img(topic.user.avatar_template, "tiny"), - original_url: @url, - title: PrettyText.unescape_emoji(CGI::escapeHTML(topic.title)), - category_html: CategoryBadge.html_for(topic.category), - quote: first_post.excerpt(SiteSetting.post_onebox_maxlength), - } - - template = File.read("#{Rails.root}/lib/onebox/templates/discourse_topic_onebox.hbs") - Mustache.render(template, args) - end - end - - def user_html(route) - link = "<a href='#{@url}'>#{@url}</a>" - username = route[:username] || '' - user = User.find_by(username_lower: username.downcase) - - if user - args = { - user_id: user.id, - username: user.username, - avatar: PrettyText.avatar_img(user.avatar_template, "extra_large"), - name: user.name, - bio: user.user_profile.bio_excerpt(230), - location: user.user_profile.location, - joined: I18n.t('joined'), - created_at: user.created_at.strftime(I18n.t('datetime_formats.formats.date_only')), - website: user.user_profile.website, - website_name: UserSerializer.new(user).website_name, - original_url: @url - } - - template = File.read("#{Rails.root}/lib/onebox/templates/discourse_user_onebox.hbs") - Mustache.render(template, args) - else - return link - end - end - - def can_see_post?(post, source_topic) - return false if post.nil? || post.hidden || post.trashed? || post.topic.nil? - Guardian.new.can_see_post?(post) || same_category?(post.topic.category, source_topic) - end - - def can_see_topic?(topic, source_topic) - return false if topic.nil? || topic.trashed? || topic.private_message? - Guardian.new.can_see_topic?(topic) || same_category?(topic.category, source_topic) - end - - def same_category?(category, source_topic) - source_topic.try(:category_id) == category.try(:id) - end - - end - end -end diff --git a/lib/oneboxer.rb b/lib/oneboxer.rb index 121984f680d..f89e0426e9b 100644 --- a/lib/oneboxer.rb +++ b/lib/oneboxer.rb @@ -28,13 +28,13 @@ module Oneboxer def self.preview(url, options = nil) options ||= {} invalidate(url) if options[:invalidate_oneboxes] - onebox_raw(url)[:preview] + onebox_raw(url, options)[:preview] end def self.onebox(url, options = nil) options ||= {} invalidate(url) if options[:invalidate_oneboxes] - onebox_raw(url)[:onebox] + onebox_raw(url, options)[:onebox] end def self.cached_onebox(url) @@ -76,41 +76,22 @@ module Oneboxer doc end - def self.append_source_topic_id(url, topic_id) - # hack urls to create proper expansions - if url =~ Regexp.new("^#{Discourse.base_url.gsub(".", "\\.")}.*$", true) - uri = URI.parse(url) rescue nil - if uri && uri.path - route = Rails.application.routes.recognize_path(uri.path) rescue nil - if route && route[:controller] == 'topics' - url += (url =~ /\?/ ? "&" : "?") + "source_topic_id=#{topic_id}" - end - end - end - url - end - def self.apply(string_or_doc, args = nil) doc = string_or_doc doc = Nokogiri::HTML::fragment(doc) if doc.is_a?(String) changed = false each_onebox_link(doc) do |url, element| - if args && args[:topic_id] - url = append_source_topic_id(url, args[:topic_id]) - end - onebox, _preview = yield(url, element) + onebox, _ = yield(url, element) if onebox parsed_onebox = Nokogiri::HTML::fragment(onebox) next unless parsed_onebox.children.count > 0 # special logic to strip empty p elements - if element.parent && - element.parent.node_name && - element.parent.node_name.downcase == "p" && - element.parent.children.count == 1 + if element&.parent&.node_name&.downcase == "p" && element&.parent&.children&.count == 1 element = element.parent end + changed = true element.swap parsed_onebox.to_html end @@ -149,7 +130,116 @@ module Oneboxer "onebox__#{url}" end - def self.onebox_raw(url) + def self.onebox_raw(url, opts = {}) + local_onebox(url, opts) || external_onebox(url) + rescue => e + # no point warning here, just cause we have an issue oneboxing a url + # we can later hunt for failed oneboxes by searching logs if needed + Rails.logger.info("Failed to onebox #{url} #{e} #{e.backtrace}") + # return a blank hash, so rest of the code works + blank_onebox + end + + def self.local_onebox(url, opts = {}) + return unless route = Discourse.route_for(url) + + html = + case route[:controller] + when "uploads" then local_upload_html(url) + when "topics" then local_topic_html(url, route, opts) + when "users" then local_user_html(url, route) + end + + html = html.presence || "<a href='#{url}'>#{url}</a>" + { onebox: html, preview: html } + end + + def self.local_upload_html(url) + case File.extname(URI(url).path || "") + when /^\.(mov|mp4|webm|ogv)$/i + "<video width='100%' height='100%' controls><source src='#{url}'><a href='#{url}'>#{url}</a></video>" + when /^\.(mp3|ogg|wav|m4a)$/i + "<audio controls><source src='#{url}'><a href='#{url}'>#{url}</a></audio>" + end + end + + def self.local_topic_html(url, route, opts) + return unless current_user = User.find_by(id: opts[:user_id]) + + if current_category = Category.find_by(id: opts[:category_id]) + return unless Guardian.new(current_user).can_see_category?(current_category) + end + + if current_topic = Topic.find_by(id: opts[:topic_id]) + return unless Guardian.new(current_user).can_see_topic?(current_topic) + end + + topic = Topic.find_by(id: route[:topic_id]) + + return unless topic + return if topic.private_message? + + if current_category&.id != topic.category_id + return unless Guardian.new.can_see_topic?(topic) + end + + post_number = route[:post_number].to_i + + post = post_number > 1 ? + topic.posts.where(post_number: post_number).first : + topic.ordered_posts.first + + return if !post || post.hidden || post.post_type != Post.types[:regular] + + if post_number > 1 && current_topic&.id == topic.id + excerpt = post.excerpt(SiteSetting.post_onebox_maxlength) + excerpt.gsub!(/[\r\n]+/, " ") + excerpt.gsub!("[/quote]", "[quote]") # don't break my quote + + quote = "[quote=\"#{post.user.username}, topic:#{topic.id}, post:#{post.post_number}\"]\n#{excerpt}\n[/quote]" + + PrettyText.cook(quote) + else + args = { + topic_id: topic.id, + avatar: PrettyText.avatar_img(post.user.avatar_template, "tiny"), + original_url: url, + title: PrettyText.unescape_emoji(CGI::escapeHTML(topic.title)), + category_html: CategoryBadge.html_for(topic.category), + quote: post.excerpt(SiteSetting.post_onebox_maxlength), + } + + template = File.read("#{Rails.root}/lib/onebox/templates/discourse_topic_onebox.hbs") + Mustache.render(template, args) + end + end + + def self.local_user_html(url, route) + username = route[:username] || "" + + if user = User.find_by(username_lower: username.downcase) + args = { + user_id: user.id, + username: user.username, + avatar: PrettyText.avatar_img(user.avatar_template, "extra_large"), + name: user.name, + bio: user.user_profile.bio_excerpt(230), + location: user.user_profile.location, + joined: I18n.t('joined'), + created_at: user.created_at.strftime(I18n.t('datetime_formats.formats.date_only')), + website: user.user_profile.website, + website_name: UserSerializer.new(user).website_name, + original_url: url + } + + template = File.read("#{Rails.root}/lib/onebox/templates/discourse_user_onebox.hbs") + Mustache.render(template, args) + else + nil + end + end + + 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) uri = fd.resolve @@ -169,14 +259,8 @@ module Oneboxer r = Onebox.preview(uri.to_s, options) - { onebox: r.to_s, preview: r.try(:placeholder_html).to_s } + { onebox: r.to_s, preview: r&.placeholder_html.to_s } end - rescue => e - # no point warning here, just cause we have an issue oneboxing a url - # we can later hunt for failed oneboxes by searching logs if needed - Rails.logger.info("Failed to onebox #{url} #{e} #{e.backtrace}") - # return a blank hash, so rest of the code works - blank_onebox end end diff --git a/lib/search.rb b/lib/search.rb index 30c5b00b996..a391ebbd5a3 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -1,7 +1,7 @@ require_dependency 'search/grouped_search_results' class Search - INDEX_VERSION = 1.freeze + INDEX_VERSION = 2.freeze def self.per_facet 5 @@ -409,7 +409,7 @@ class Search if match.to_s.length >= SiteSetting.min_search_term_length posts.where("posts.id IN ( SELECT post_id FROM post_search_data pd1 - WHERE pd1.search_data @@ #{Search.ts_query("##{match}")})") + WHERE pd1.search_data @@ #{Search.ts_query(term: "##{match}")})") end end end @@ -511,12 +511,17 @@ class Search end end + @in_title = false + if word == 'order:latest' || word == 'l' @order = :latest nil elsif word == 'order:latest_topic' @order = :latest_topic nil + elsif word == 'in:title' + @in_title = true + nil elsif word =~ /topic:(\d+)/ topic_id = $1.to_i if topic_id > 1 @@ -681,7 +686,12 @@ class Search posts = posts.joins('JOIN users u ON u.id = posts.user_id') posts = posts.where("posts.raw || ' ' || u.username || ' ' || COALESCE(u.name, '') ilike ?", "%#{term_without_quote}%") else - posts = posts.where("post_search_data.search_data @@ #{ts_query}") + # A is for title + # B is for category + # C is for tags + # D is for cooked + weights = @in_title ? 'A' : (SiteSetting.tagging_enabled ? 'ABCD' : 'ABD') + posts = posts.where("post_search_data.search_data @@ #{ts_query(weight_filter: weights)}") exact_terms = @term.scan(/"([^"]+)"/).flatten exact_terms.each do |exact| posts = posts.where("posts.raw ilike ?", "%#{exact}%") @@ -743,11 +753,9 @@ class Search posts = posts.order("posts.like_count DESC") end else - posts = posts.order("TS_RANK_CD(TO_TSVECTOR(#{default_ts_config}, topics.title), #{ts_query}) DESC") - data_ranking = "TS_RANK_CD(post_search_data.search_data, #{ts_query})" if opts[:aggregate_search] - posts = posts.order("SUM(#{data_ranking}) DESC") + posts = posts.order("MAX(#{data_ranking}) DESC") else posts = posts.order("#{data_ranking} DESC") end @@ -772,7 +780,7 @@ class Search self.class.default_ts_config end - def self.ts_query(term, ts_config = nil, joiner = "&") + def self.ts_query(term: , ts_config: nil, joiner: "&", weight_filter: nil) data = Post.exec_sql("SELECT TO_TSVECTOR(:config, :term)", config: 'simple', @@ -786,16 +794,17 @@ class Search query = ActiveRecord::Base.connection.quote( all_terms - .map { |t| "'#{PG::Connection.escape_string(t)}':*" } + .map { |t| "'#{PG::Connection.escape_string(t)}':*#{weight_filter}" } .join(" #{joiner} ") ) "TO_TSQUERY(#{ts_config || default_ts_config}, #{query})" end - def ts_query(ts_config = nil) + def ts_query(ts_config = nil, weight_filter: nil) @ts_query_cache ||= {} - @ts_query_cache["#{ts_config || default_ts_config} #{@term}"] ||= Search.ts_query(@term, ts_config) + @ts_query_cache["#{ts_config || default_ts_config} #{@term} #{weight_filter}"] ||= + Search.ts_query(term: @term, ts_config: ts_config, weight_filter: weight_filter) end def wrap_rows(query) diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb index 92442b56060..3055eb95689 100644 --- a/lib/stylesheet/manager.rb +++ b/lib/stylesheet/manager.rb @@ -34,7 +34,8 @@ class Stylesheet::Manager theme_key = SiteSetting.default_theme_key end - cache_key = "#{target}_#{theme_key}" + current_hostname = Discourse.current_hostname + cache_key = "#{target}_#{theme_key}_#{current_hostname}" tag = cache[cache_key] return tag.dup.html_safe if tag @@ -45,7 +46,7 @@ class Stylesheet::Manager tag = "" else builder.compile unless File.exists?(builder.stylesheet_fullpath) - tag = %[<link href="#{builder.stylesheet_path}" media="#{media}" rel="stylesheet" data-target="#{target}"/>] + tag = %[<link href="#{builder.stylesheet_path(current_hostname)}" media="#{media}" rel="stylesheet" data-target="#{target}"/>] end cache[cache_key] = tag @@ -181,12 +182,12 @@ class Stylesheet::Manager "#{cache_fullpath}/#{stylesheet_filename_no_digest}" end - def stylesheet_cdnpath - "#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{Discourse.current_hostname}" + def stylesheet_cdnpath(hostname) + "#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{hostname}" end - def stylesheet_path - stylesheet_cdnpath + def stylesheet_path(hostname) + stylesheet_cdnpath(hostname) end def root_path diff --git a/lib/tasks/search.rake b/lib/tasks/search.rake index 7d1ce3ab30c..bb609dff673 100644 --- a/lib/tasks/search.rake +++ b/lib/tasks/search.rake @@ -5,52 +5,37 @@ end def reindex_search(db = RailsMultisite::ConnectionManagement.current_db) puts "Reindexing '#{db}'" puts "" - puts "Posts:" - Post.exec_sql("select p.id, p.cooked, c.name category, t.title, p.post_number, t.id topic_id from - posts p - join topics t on t.id = p.topic_id - left join categories c on c.id = t.category_id - ").each do |p| - post_id = p["id"] - cooked = p["cooked"] - title = p["title"] - category = p["cat"] - post_number = p["post_number"].to_i - topic_id = p["topic_id"].to_i - - SearchIndexer.update_posts_index(post_id, cooked, title, category) - SearchIndexer.update_topics_index(topic_id, title , cooked) if post_number == 1 - + puts "Posts" + Post.includes(topic: [:category, :tags]).find_each do |p| + if p.post_number == 1 + SearchIndexer.index(p.topic, force: true) + else + SearchIndexer.index(p, force: true) + end putc "." end puts - puts "Users:" - User.exec_sql("select id, name, username from users").each do |u| - id = u["id"] - name = u["name"] - username = u["username"] - SearchIndexer.update_users_index(id, username, name) - + puts "Users" + User.find_each do |u| + SearchIndexer.index(u, force: true) putc "." end puts puts "Categories" - Category.exec_sql("select id, name from categories").each do |c| - id = c["id"] - name = c["name"] - SearchIndexer.update_categories_index(id, name) - - putc '.' + Category.find_each do |c| + SearchIndexer.index(c, force: true) + putc "." end - puts '', 'Tags' + puts + puts "Tags" - Tag.exec_sql('select id, name from tags').each do |t| - SearchIndexer.update_tags_index(t['id'], t['name']) - putc '.' + Tag.find_each do |t| + SearchIndexer.index(t, force: true) + putc "." end puts diff --git a/lib/tasks/site_settings.rake b/lib/tasks/site_settings.rake new file mode 100644 index 00000000000..710c9b1e071 --- /dev/null +++ b/lib/tasks/site_settings.rake @@ -0,0 +1,62 @@ +require 'yaml' + +class SiteSettingsTask + def self.export_to_hash + site_settings = SiteSetting.all_settings + h = {} + site_settings.each do |site_setting| + h.store(site_setting[:setting].to_s, site_setting[:value]) + end + h + end +end + +desc "Exports site settings" +task "site_settings:export" => :environment do + h = SiteSettingsTask.export_to_hash + puts h.to_yaml +end + +desc "Imports site settings" +task "site_settings:import" => :environment do + yml = (STDIN.tty?) ? '' : STDIN.read + if yml == '' + puts "" + puts "Please specify a settings yml file" + puts "Example: rake site_settings:import < settings.yml" + exit 1 + end + + puts "" + puts "starting import..." + puts "" + + h = SiteSettingsTask.export_to_hash + counts = { updated: 0, not_found: 0, errors: 0 } + + site_settings = YAML::load(yml) + site_settings.each do |site_setting| + key = site_setting[0] + val = site_setting[1] + if h.has_key?(key) + if val != h[key] #only update if different + begin + result = SiteSetting.set_and_log(key, val) + puts "Changed #{key} FROM: #{result.previous_value} TO: #{result.new_value}" + counts[:updated] += 1 + rescue => e + puts "ERROR: #{e.message}" + counts[:errors] += 1 + end + end + else + puts "NOT FOUND: existing site setting not found for #{key}" + counts[:not_found] += 1 + end + end + puts "" + puts "Results:" + puts " Updated: #{counts[:updated]}" + puts " Not Found: #{counts[:not_found]}" + puts " Errors: #{counts[:errors]}" +end diff --git a/lib/text_cleaner.rb b/lib/text_cleaner.rb index 31ff605cef3..0914fe88881 100644 --- a/lib/text_cleaner.rb +++ b/lib/text_cleaner.rb @@ -17,7 +17,8 @@ class TextCleaner remove_all_periods_from_the_end: SiteSetting.title_prettify, remove_extraneous_space: SiteSetting.title_prettify && SiteSetting.default_locale == "en", fixes_interior_spaces: true, - strip_whitespaces: true + strip_whitespaces: true, + strip_zero_width_spaces: true } end @@ -47,6 +48,8 @@ class TextCleaner text = normalize_whitespaces(text) # Strip whitespaces text.strip! if opts[:strip_whitespaces] + # Strip zero width spaces + text.gsub!(/\u200b/, '') if opts[:strip_zero_width_spaces] text end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index fd9464fffc3..55047e977b2 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -610,7 +610,7 @@ class TopicQuery end if search = options[:search] - result = result.where("topics.id in (select pp.topic_id from post_search_data pd join posts pp on pp.id = pd.post_id where pd.search_data @@ #{Search.ts_query(search.to_s)})") + result = result.where("topics.id in (select pp.topic_id from post_search_data pd join posts pp on pp.id = pd.post_id where pd.search_data @@ #{Search.ts_query(term: search.to_s)})") end # NOTE protect against SYM attack can be removed with Ruby 2.2 diff --git a/lib/validators/max_emojis_validator.rb b/lib/validators/max_emojis_validator.rb new file mode 100644 index 00000000000..f208f66b1e0 --- /dev/null +++ b/lib/validators/max_emojis_validator.rb @@ -0,0 +1,11 @@ +class MaxEmojisValidator < ActiveModel::EachValidator + + def validate_each(record, attribute, value) + if Emoji.unicode_unescape(value).scan(/:([\w\-+]*(?::t\d)?):/).size > SiteSetting.max_emojis_in_title + record.errors.add( + attribute, :max_emojis, + max_emojis_count: SiteSetting.max_emojis_in_title + ) + end + end +end diff --git a/lib/validators/post_validator.rb b/lib/validators/post_validator.rb index 18e8d4330d3..076dd2d086c 100644 --- a/lib/validators/post_validator.rb +++ b/lib/validators/post_validator.rb @@ -82,8 +82,25 @@ class Validators::PostValidator < ActiveModel::Validator # Ensure new users can not put too many images in a post def max_images_validator(post) - return if acting_user_is_trusted?(post) || private_message?(post) - add_error_if_count_exceeded(post, :no_images_allowed, :too_many_images, post.image_count, SiteSetting.newuser_max_images) + return if post.acting_user.blank? + + if post.acting_user.trust_level < TrustLevel[SiteSetting.min_trust_to_post_images] + add_error_if_count_exceeded( + post, + :no_images_allowed_trust, + :no_images_allowed_trust, + post.image_count, + 0 + ) + elsif post.acting_user.trust_level == TrustLevel[0] + add_error_if_count_exceeded( + post, + :no_images_allowed, + :too_many_images, + post.image_count, + SiteSetting.newuser_max_images + ) + end end # Ensure new users can not put too many attachments in a post diff --git a/lib/validators/upload_validator.rb b/lib/validators/upload_validator.rb index 6c2013565db..8f6c4cede1e 100644 --- a/lib/validators/upload_validator.rb +++ b/lib/validators/upload_validator.rb @@ -29,11 +29,11 @@ class Validators::UploadValidator < ActiveModel::Validator end def is_authorized?(upload, extension) - authorized_extensions(upload, extension, authorized_uploads(upload)) + extension_authorized?(upload, extension, authorized_extensions(upload)) end def authorized_image_extension(upload, extension) - authorized_extensions(upload, extension, authorized_images(upload)) + extension_authorized?(upload, extension, authorized_images(upload)) end def maximum_image_file_size(upload) @@ -41,7 +41,7 @@ class Validators::UploadValidator < ActiveModel::Validator end def authorized_attachment_extension(upload, extension) - authorized_extensions(upload, extension, authorized_attachments(upload)) + extension_authorized?(upload, extension, authorized_attachments(upload)) end def maximum_attachment_file_size(upload) @@ -50,38 +50,50 @@ class Validators::UploadValidator < ActiveModel::Validator private - def authorized_uploads(upload) - authorized_uploads = Set.new + def extensions_to_set(exts) + extensions = Set.new - extensions = upload.for_theme ? SiteSetting.theme_authorized_extensions : SiteSetting.authorized_extensions - - extensions + exts .gsub(/[\s\.]+/, "") .downcase .split("|") - .each { |extension| authorized_uploads << extension unless extension.include?("*") } + .each { |extension| extensions << extension unless extension.include?("*") } - authorized_uploads + extensions + end + + def authorized_extensions(upload) + extensions = upload.for_theme ? SiteSetting.theme_authorized_extensions : SiteSetting.authorized_extensions + extensions_to_set(extensions) end def authorized_images(upload) - authorized_uploads(upload) & FileHelper.images + authorized_extensions(upload) & FileHelper.images end def authorized_attachments(upload) - authorized_uploads(upload) - FileHelper.images + authorized_extensions(upload) - FileHelper.images end def authorizes_all_extensions?(upload) + if upload.user&.staff? + return true if SiteSetting.authorized_extensions_for_staff.include?("*") + end extensions = upload.for_theme ? SiteSetting.theme_authorized_extensions : SiteSetting.authorized_extensions extensions.include?("*") end - def authorized_extensions(upload, extension, extensions) + def extension_authorized?(upload, extension, extensions) return true if authorizes_all_extensions?(upload) + staff_extensions = Set.new + if upload.user&.staff? + staff_extensions = extensions_to_set(SiteSetting.authorized_extensions_for_staff) + return true if staff_extensions.include?(extension.downcase) + end + unless authorized = extensions.include?(extension.downcase) - message = I18n.t("upload.unauthorized", authorized_extensions: extensions.to_a.join(", ")) + message = I18n.t("upload.unauthorized", authorized_extensions: (extensions | staff_extensions).to_a.join(", ")) upload.errors.add(:original_filename, message) end diff --git a/lib/version.rb b/lib/version.rb index 3089a596acf..415e9a57064 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 2 MINOR = 0 TINY = 0 - PRE = 'beta2' + PRE = 'beta3' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/plugins/discourse-narrative-bot/config/locales/client.ro.yml b/plugins/discourse-narrative-bot/config/locales/client.ro.yml index 31d1c440135..43a7a03b6ca 100644 --- a/plugins/discourse-narrative-bot/config/locales/client.ro.yml +++ b/plugins/discourse-narrative-bot/config/locales/client.ro.yml @@ -5,4 +5,9 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -ro: {} +ro: + js: + discourse_narrative_bot: + welcome_post_type: + new_user_track: "Pornește tutorialul pentru toți utilizatorii noi" + welcome_message: "Trimite tuturor utilizatorilor noi un mesaj de întâmpinare cu un ghid de pornire rapidă" diff --git a/plugins/discourse-narrative-bot/config/locales/server.cs.yml b/plugins/discourse-narrative-bot/config/locales/server.cs.yml index a7647ca2610..c07c4e75765 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.cs.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.cs.yml @@ -87,7 +87,7 @@ cs: '9': "Ano" '10': "Dle znamení ano" '11': "Odpověď nejistá, zeptej se znovu" - '12': "Zeptej se jindy" + '12': "Zeptej se později" '13': "To ti teď raději neřeknu" '14': "To nyní nedokážu předpovědět" '15': "Soustřeď se a zeptej se znovu" diff --git a/plugins/discourse-narrative-bot/config/locales/server.fa_IR.yml b/plugins/discourse-narrative-bot/config/locales/server.fa_IR.yml index 3fad2994116..cb766a13ec5 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.fa_IR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.fa_IR.yml @@ -160,6 +160,8 @@ fa_IR: - https://en.wikipedia.org/wiki/Inherently_funny_word - https://en.wikipedia.org/wiki/Death_by_coconut - https://en.wikipedia.org/wiki/Calculator_spelling + reply: |- + عالی! این کار برای اکثر پیوندها <img src="%{base_uri}/images/font-awesome-link.png" width="16" height="16"> صدق میکنه. به یاد داشته باش که لینک بایستی در یک خط به تنهایی و _بدون پس و پیش_ قرار بگیرد. not_found: |- متاسفم، نمیتوانم پیوند را در پاسخ شما پیدا کنم! :cry: @@ -167,6 +169,14 @@ fa_IR: <https://en.wikipedia.org/wiki/Exotic_Shorthair> images: + instructions: |- + این عکسی از اسب تک شاخ است: + + <img src="%{base_uri}/images/unicorn.png" width="520" height="520"> + + اگر پسندیدی (کیه که نپسنده!) برای اینکه من بدونم لطفا دکمهی پسند :heart: زیر این نوشته را فشار بده. + + میتونی با **یک عکس پاسخ بدهی؟** هر عکسی بذاری اشکالی نداره. میتونی عکس رو بکشی و رها کنی، یا از گزینه ی آپلود استفاده کنی، یا حتی کپی پیست کنی. reply: |- عجب عکس شگفتانگیزی! من دکمهی پسند :heart: رو برای اینکه بفهمی چقدر قدردان آن هستم، فشار دادم. like_not_found: |- @@ -211,11 +221,31 @@ fa_IR: هر متنی را که در نوشتهی من انتخاب کنی، کلید <kbd>**نقلقول**</kbd>را برایت نشان میدهد. همچنین انتخاب کلید **پاسخ** بعد از انتخاب متن هم همان کار را میکند. میتوانی یکبار دیگر امتحان کنی؟ bookmark: + instructions: |- + اگر میخواهی بیشتر یاد بگیری، در زیر <img src="%{base_uri}/images/font-awesome-ellipsis.png" width="16" height="16"> و <img src="%{base_uri}/images/font-awesome-bookmark.png" width="16" height="16"> **نشانهگذاری این پیام خصوصی** را انتخاب کن. اگر این کار را بکنی، ممکن است هدیهای :gift: در آینده برای شما باشد. reply: |- عالی! الان تو میتوانی راه برگشت به این پیغام خصوصی را هر زمان که دوست داشتی از [برگهی نشانکها در پروفایل کاربری](%{profile_page_url}/activity/bookmarks) پیدا کنی. فقط کافی است که روی عکس پروفایل خود در بالا سمت چپ انتخاب کنی ↖ + not_found: |- + اوه، هیچ نشانکی در این موضوع نمیبینم. آیا آیکون نشانک را زیر هر نوشته پیدا کردی؟ در صورت لزوم از آیکون "بیشتر نشان بده" <img src="%{base_uri}/images/font-awesome-ellipsis.png" width="16" height="16"> برای آشکار کردن دیگر قابلیت ها استفاده کن. emoji: + instructions: |- + شاید دیده باشی که من از عکسهای کوچکی مثل :blue_car::dash: در پاسخهایم استفاده میکنم. اینها [شکلک](https://en.wikipedia.org/wiki/Emoji) نام دارند. آیا میتوانی که یک شکلک را به پاسخت اضافه کنی؟ هر یک از موارد زیر این کار را میتواند انجام بدهد: + + - بنویس `:) ;) :O` + + - علامت دونقطه <kbd>:</kbd> را بنویس و سپس اسم شکلک را کامل کن `:tada:` + + - کلیک شکلک <img src="%{base_uri}/images/font-awesome-smile.png" width="16" height="16"> را در ویرایشگر، یا در صفحهکلید موبایلت فشار بده reply: |- :sparkles: خیلی شکلک شگفت انگیزی بود :sparkles: + not_found: |- + اوپس، هیچ شکلکی در پاسخت نمیبینم؟ ای وای! :sob: + + سعی کن علامت دونقطه <kbd>:</kbd> رو تایپ کنی تا صفحهی انتخاب شکلک رو باز کنی، سپس اولین حروف چیزی که میخوای را تایپ کن. مانند `:bird:` + + یا کلید شکلک <img src="%{base_uri}/images/font-awesome-smile.png" width="16" height="16"> را در ویرایشگر انتخاب کن. + + (اگر با موبایلت مینویسی، میتوانی شکلک را مستقیما از طریق صفحه کلید خود موبایل انتخاب بکنی.) mention: instructions: |- شاید گاهی اوقات بخواهی که توجه شخصی را، اگرچه در حال پاسخ دادن به ایشان نیستی، جلب کنی. بنویس `@` و سپس نام کاربری ایشان را کامل کن که به ایشان اشاره کنی. @@ -226,9 +256,27 @@ fa_IR: not_found: |- من اسم خودم را هیچ جا پیدا نکردم. :frowning: آیا میتوانی دوباره تلاش کنی که به اسم من `@%{discobot_username}` اشاره کنی؟ flag: + instructions: |- + ما دوست داریم که گفتگوها دوستانه باشد و به کمک شما برای [متمدنانه نگه داشتن تالار](%{guidelines_url}) احتیاج داریم. اگر مشکلی مشاهده کردی، آن را پرچم گذاری کن که به صورت نامحسوس نویسنده یا [کارکنان ما](%{about_url}) را در جریان موضوع قرار بدهی. + + > :imp: من چیز زشتی اینجا نوشتم + + فکر کنم بدانی که چه کار بایستی بکنید. بفرمایید این نوشته را به عنوان نامناسب **پرچم گذاری** کنید! reply: |- [کارکنان ما](/groups/staff) به طور خصوصی از پرچم گذاری شما آگاه میشوند. اگر اعضای تالار به تعداد کافی یک نوشته را پرچم گذاری کنند، آن نوشته به عنوان پیشگیری خود به خود پنهان میشود. (به خاطر اینکه من در حقیقت چیز زشتی ننوشتم :angel: خودم پرچم گذاری شما را فعلا پاک کردم.) + not_found: |- + اوه نه، نوشتهی زشت من هنوز پرچم گذاری نشده. :worried: آیا میتوانی با استفاده از آیکون **پرچم** <img src="%{base_uri}/images/font-awesome-flag.png" width="16" height="16"> این نوشته را به عنوان نامناسب گزارش کنی؟ فراموش نکن که برای آشکارسازی قابلیتهای بیشتر میتوانی کلید "بیشتر نشان بده" <img src="%{base_uri}/images/font-awesome-ellipsis.png" width="16" height="16"> را برای هر نوشته انتخاب کنی. search: + instructions: |- + من در این مبحث یک سوپرایز برای شما دارم. اگر برای یک چالش آماده ای، **آیکون جستجو** <img src="%{base_uri}/images/font-awesome-search.png" width="16" height="16"> را درگوشه ی بالا سمت چپ ↖ انتخاب کن که جستجو اش بکنی. + + سعی کن که عبارت "سنجاب" را در این مبحث جستجو کنی + hidden_message: |- + چطور تونستی این سنجاب رو نادیده بگیری؟ :wink: + + <img src="%{base_uri}/images/capybara-eating.gif"/> + + آیا متوجه شدی که الان تو در ابتدای مبحث هستی؟ به این سنجاب گرسنهی بیچاره، با **پاسخ دادن توسط شکلک `:herb:`** (در پاسخ بنویس `:herb:`) غذا بده تا علاوه بر آن به صورت اتوماتیک به آخر مبحث برگردی. reply: |- بله پیداش کردی :tada: @@ -237,6 +285,8 @@ fa_IR: - برای پرش به هر قسمتی از یک مبحث دراز، از کنترلکننده های زمانی سمت چپ (یا پایین در موبایل) استفاده بکن. - اگر یه صفحه کلید فیزیکی داری، <kbd>؟</kbd> رو فشار بده تا میانبرهای بدردبخوری که داریم را مشاهده کنی. + not_found: |- + :thinking: ظاهرا مشکلی برای شما بوجود آمده. متاسفیم. آیا دقیقا عبارت **سنجاب** را جستجو<img src="%{base_uri}/images/font-awesome-search.png" width="16" height="16"> کردی؟ certificate: alt: 'گواهی موفقیت' advanced_user_narrative: diff --git a/plugins/discourse-presence/config/locales/client.ro.yml b/plugins/discourse-presence/config/locales/client.ro.yml index 31d1c440135..2a98a275bfe 100644 --- a/plugins/discourse-presence/config/locales/client.ro.yml +++ b/plugins/discourse-presence/config/locales/client.ro.yml @@ -5,4 +5,8 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -ro: {} +ro: + js: + presence: + replying: "răspunde" + editing: "editează" diff --git a/plugins/discourse-presence/config/locales/client.sk.yml b/plugins/discourse-presence/config/locales/client.sk.yml index bcc3a93dc5b..4f72b9d611e 100644 --- a/plugins/discourse-presence/config/locales/client.sk.yml +++ b/plugins/discourse-presence/config/locales/client.sk.yml @@ -5,4 +5,12 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -sk: {} +sk: + js: + presence: + replying: "odpovedanie" + editing: "upravovanie" + replying_to_topic: + one: "odpovedá" + few: "odpovedajú" + other: "odpovedajú" diff --git a/plugins/discourse-presence/config/locales/server.ro.yml b/plugins/discourse-presence/config/locales/server.ro.yml index 31d1c440135..bba2871a96c 100644 --- a/plugins/discourse-presence/config/locales/server.ro.yml +++ b/plugins/discourse-presence/config/locales/server.ro.yml @@ -5,4 +5,7 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -ro: {} +ro: + site_settings: + presence_enabled: 'Afișează utilizatorii care răspund acum la subiectul curent sau editează subiectul?' + presence_max_users_shown: 'Numărul maxim de utilizatori afișați.' diff --git a/plugins/poll/config/locales/client.ro.yml b/plugins/poll/config/locales/client.ro.yml index df87f6641dc..1b9159d8bd6 100644 --- a/plugins/poll/config/locales/client.ro.yml +++ b/plugins/poll/config/locales/client.ro.yml @@ -59,6 +59,8 @@ ro: insert: Introdu sondaj help: options_count: Trebuie să introduci cel puțin 2 opțiuni + invalid_values: Valoarea minimă trebuie să fie mai mică decât valoarea maximă. + min_step_value: Valoarea minimă a pasului este 1 poll_type: label: Tip regular: Opțiune unică diff --git a/plugins/poll/config/locales/client.sk.yml b/plugins/poll/config/locales/client.sk.yml index 5424aff0985..1e8b8f6e331 100644 --- a/plugins/poll/config/locales/client.sk.yml +++ b/plugins/poll/config/locales/client.sk.yml @@ -60,6 +60,7 @@ sk: help: options_count: Zadajte aspoň 2 možnosti invalid_values: Minimálna hodnota musí byť menšia ako maximálna hodnota. + min_step_value: Minimálna hodnota pre krok je 1 poll_type: label: Typ regular: Jedna možnosť diff --git a/plugins/poll/config/locales/client.zh_TW.yml b/plugins/poll/config/locales/client.zh_TW.yml index 078e9f1b6da..5b2348af813 100644 --- a/plugins/poll/config/locales/client.zh_TW.yml +++ b/plugins/poll/config/locales/client.zh_TW.yml @@ -36,7 +36,7 @@ zh_TW: open: title: "開啟投票" label: "開啟" - confirm: "你確定要開啟這個投票麼?" + confirm: "你確定要開啟這個投票嗎?" close: title: "關閉投票" label: "關閉" diff --git a/plugins/poll/config/locales/server.ro.yml b/plugins/poll/config/locales/server.ro.yml index 80260a08bbf..10b34064840 100644 --- a/plugins/poll/config/locales/server.ro.yml +++ b/plugins/poll/config/locales/server.ro.yml @@ -7,8 +7,10 @@ ro: site_settings: + poll_enabled: "Permiți sondaje?" poll_maximum_options: "Numărul maxim admis de opțiuni într-un sondaj" poll_edit_window_mins: "Număr de minute după crearea postării pe parcursul cărora sondajele pot fi editate." + poll_minimum_trust_level_to_create: "Definește nivelul minim de încredere pentru a crea sondaje." poll: multiple_polls_without_name: "Există mai multe sondaje fără nume. Folosește atributul '<code>name</code>' pentru a identifica sondajele proprii" multiple_polls_with_same_name: "Există mai multe sondaje cu același nume: <strong>%{name}</strong>. Folosește atributul '<code>name</code>' pentru a identifica sondajele." @@ -40,5 +42,6 @@ ro: poll_must_be_open_to_vote: "Sondajul trebuie să fie deschis pentru votare." topic_must_be_open_to_toggle_status: "Subiectul trebuie să fie activ pentru a se schimba statutul." only_staff_or_op_can_toggle_status: "Doar un administrator sau autorul postării inițiale poate schimba starea unui sondaj." + insufficient_rights_to_create: "Nu ai permisiunea de a crea sondaje." email: link_to_poll: "Click pentru afișarea sondajului." diff --git a/plugins/poll/spec/lib/pretty_text_spec.rb b/plugins/poll/spec/lib/pretty_text_spec.rb index 85ee2c2a00e..fedc70eb986 100644 --- a/plugins/poll/spec/lib/pretty_text_spec.rb +++ b/plugins/poll/spec/lib/pretty_text_spec.rb @@ -1,10 +1,9 @@ require 'rails_helper' -require 'html_normalize' describe PrettyText do def n(html) - HtmlNormalize.normalize(html) + html.strip end it 'supports multi choice polls' do @@ -95,14 +94,13 @@ describe PrettyText do cooked = PrettyText.cook md expected = <<~MD - <div class="poll" data-poll-status="open" data-poll-name="poll" data-poll-type="multiple"> + <div class="poll" data-poll-status="open" data-poll-type="multiple" data-poll-name="poll"> <div> <div class="poll-container"> <ol> - <li data-poll-option-id='b6475cbf6acb8676b20c60582cfc487a'>test 1 <img alt=':slight_smile:' class='emoji' src='/images/emoji/twitter/slight_smile.png?v=5' title=':slight_smile:'> <b>test</b> - </li> - <li data-poll-option-id='7158af352698eb1443d709818df097d4'>test 2</li> + <li data-poll-option-id="b6475cbf6acb8676b20c60582cfc487a">test 1 <img src="/images/emoji/twitter/slight_smile.png?v=5" title=":slight_smile:" class="emoji" alt=":slight_smile:"> <b>test</b> </li> + <li data-poll-option-id="7158af352698eb1443d709818df097d4">test 2</li> </ol> </div> <div class="poll-info"> @@ -110,12 +108,9 @@ describe PrettyText do <span class="info-number">0</span> <span class="info-text">voters</span> </p> - <p> - Choose up to <strong>2</strong> options</p> </div> </div> <div class="poll-buttons"> - <a title="Cast your votes">Vote now!</a> <a title="Display the poll results">Show results</a> </div> </div> diff --git a/public/500.cs.html b/public/500.cs.html index a3177c6bdac..5402855206f 100644 --- a/public/500.cs.html +++ b/public/500.cs.html @@ -8,6 +8,6 @@ <h1>Jejda</h1> <p>Software, na němž běží toto diskuzní fórum, narazil na nečekané problémy. Omlouváme se za způsobené nepříjemnosti.</p> <p>Podrobné informace o chybě byly zaznamenány a automatické oznámení vygenerováno. Podíváme se na to.</p> - <p>Žádná další akce není třeba. Pokud chyba nezmizí, můžete poskytnout podrobnosti, včetně kroků, jak chybu reprodukovat, založením nového tématu v kategorii zpětné vazby k webu.</p> + <p>Žádná další akce není potřeba. Ale pokud chyba přetrvá, doplňte prosím podrobnosti, včetně kroků jak chybu reprodukovat, založením nového tématu v kategorii fóra určené pro zpětnou vazbu.</p> </body> </html> diff --git a/script/bench.rb b/script/bench.rb index ac6fe614fc8..fa6e68d169c 100644 --- a/script/bench.rb +++ b/script/bench.rb @@ -11,6 +11,9 @@ require "fileutils" @mem_stats = false @unicorn = false @dump_heap = false +@concurrency = 1 +@skip_asset_bundle = false +@unicorn_workers = 3 opts = OptionParser.new do |o| o.banner = "Usage: ruby bench.rb [options]" @@ -35,9 +38,18 @@ opts = OptionParser.new do |o| o.on("-m", "--memory_stats") do @mem_stats = true end - o.on("-u", "--unicorn", "Use unicorn to serve pages as opposed to thin") do + o.on("-u", "--unicorn", "Use unicorn to serve pages as opposed to puma") do @unicorn = true end + o.on("-c", "--concurrency [NUM]", "Run benchmark with this number of concurrent requests (default: 1)") do |i| + @concurrency = i.to_i + end + o.on("-w", "--unicorn_workers [NUM]", "Run benchmark with this number of unicorn workers (default: 3)") do |i| + @unicorn_workers = i.to_i + end + o.on("-s", "--skip-bundle-assets", "Skip bundling assets") do + @skip_asset_bundle = true + end end opts.parse! @@ -106,19 +118,40 @@ end ENV["RAILS_ENV"] = "profile" -discourse_env_vars = %w(DISCOURSE_DUMP_HEAP RUBY_GC_HEAP_INIT_SLOTS RUBY_GC_HEAP_FREE_SLOTS RUBY_GC_HEAP_GROWTH_FACTOR RUBY_GC_HEAP_GROWTH_MAX_SLOTS RUBY_GC_MALLOC_LIMIT RUBY_GC_OLDMALLOC_LIMIT RUBY_GC_MALLOC_LIMIT_MAX RUBY_GC_OLDMALLOC_LIMIT_MAX RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR) +discourse_env_vars = %w( + DISCOURSE_DUMP_HEAP + RUBY_GC_HEAP_INIT_SLOTS + RUBY_GC_HEAP_FREE_SLOTS + RUBY_GC_HEAP_GROWTH_FACTOR + RUBY_GC_HEAP_GROWTH_MAX_SLOTS + RUBY_GC_MALLOC_LIMIT + RUBY_GC_OLDMALLOC_LIMIT + RUBY_GC_MALLOC_LIMIT_MAX + RUBY_GC_OLDMALLOC_LIMIT_MAX + RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR + RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR + RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR + RUBY_GLOBAL_METHOD_CACHE_SIZE +) if @include_env puts "Running with tuned environment" - discourse_env_vars - %w(RUBY_GC_MALLOC_LIMIT).each do |v| + discourse_env_vars.each do |v| ENV.delete v end + + ENV['RUBY_GLOBAL_METHOD_CACHE_SIZE'] = '131072' + ENV['RUBY_GC_HEAP_GROWTH_MAX_SLOTS'] = '40000' + ENV['RUBY_GC_HEAP_INIT_SLOTS'] = '400000' + ENV['RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR'] = '1.5' + else # clean env puts "Running with the following custom environment" - discourse_env_vars.each do |w| - puts "#{w}: #{ENV[w]}" - end +end + +discourse_env_vars.each do |w| + puts "#{w}: #{ENV[w]}" if ENV[w].to_s.length > 0 end def port_available?(port) @@ -153,8 +186,9 @@ api_key = `bundle exec rake api_key:get`.split("\n")[-1] def bench(path, name) puts "Running apache bench warmup" add = "" - add = "-c 3 " if @unicorn + add = "-c #{@concurrency} " if @concurrency > 1 `ab #{add} -n 20 -l "http://127.0.0.1:#{@port}#{path}"` + puts "Benchmarking #{name} @ #{path}" `ab #{add} -n #{@iterations} -l -e tmp/ab.csv "http://127.0.0.1:#{@port}#{path}"` @@ -168,16 +202,19 @@ end begin # critical cause cache may be incompatible - puts "precompiling assets" - run("bundle exec rake assets:precompile") + unless @skip_asset_bundle + puts "precompiling assets" + run("bundle exec rake assets:precompile") + end pid = if @unicorn ENV['UNICORN_PORT'] = @port.to_s + ENV['UNICORN_WORKERS'] = @unicorn_workers.to_s FileUtils.mkdir_p(File.join('tmp', 'pids')) spawn("bundle exec unicorn -c config/unicorn.conf.rb") else - spawn("bundle exec thin start -p #{@port}") + spawn("bundle exec puma -p #{@port} -e production") end while port_available? @port @@ -223,6 +260,17 @@ begin puts "Your Results: (note for timings- percentile is first, duration is second in millisecs)" + if @unicorn + puts "Unicorn: (workers: #{@unicorn_workers})" + else + # TODO we want to also bench puma clusters + puts "Puma: (single threaded)" + end + puts "Include env: #{@include_env}" + puts "Iterations: #{@iterations}, Best of: #{@best_of}" + puts "Concurrency: #{@concurrency}" + puts + # Prevent using external facts because it breaks when running in the # discourse/discourse_bench docker container. Facter::Util::Config.external_facts_dirs = [] diff --git a/script/benchmarks/middleware/test.rb b/script/benchmarks/middleware/test.rb new file mode 100644 index 00000000000..58595dfcf96 --- /dev/null +++ b/script/benchmarks/middleware/test.rb @@ -0,0 +1,71 @@ +require 'memory_profiler' +require 'benchmark/ips' + +ENV["RAILS_ENV"] = "production" + +require File.expand_path("../../../../config/environment", __FILE__) + +def req + _t = "9c1a318cb72cca57daf413cc511f0993" + + data = { + "timings[1]" => "1001", + "timings[2]" => "1001", + "timings[3]" => "1001", + "topic_id" => "490310" + } + + data = data.map do |k, v| + "#{CGI.escape(k)}=#{v}" + end.join("&") + + { + "REQUEST_METHOD" => "POST", + "SCRIPT_NAME" => "", + "PATH_INFO" => "/topics/timings.json", + "QUERY_STRING" => "", + "SERVER_NAME" => "localhost", + "SERVER_PORT" => "80", + "HTTP_CONTENT_TYPE" => "application/x-www-form-urlencoded", + "HTTP_VERSION" => "HTTP/1.0", + "HTTP_COOKIE" => "_t=#{_t}", + "rack.input" => StringIO.new(data), + "rack.version" => [1, 2], + "rack.url_scheme" => "http" + } +end + +1.times do + s = Time.now + Rails.application.call(req) + puts(Time.now - s) +end +exit +# +# +StackProf.run(mode: :wall, out: 'report.dump') do + 1000.times do + Rails.application.call(req) + end +end +# +# MemoryProfiler.start +# Rails.application.call(req) +# MemoryProfiler.stop.pretty_print +# exit + +# # exit +# exit + +# Benchmark.ips do |x| +# x.report("default") do +# Rails.application.call(req) +# end +# end + +# status, headers, body = Rails.application.call(req) +# p status +# p headers +# body.each do |s| +# p s.to_s +# end diff --git a/script/boot_mem.rb b/script/boot_mem.rb new file mode 100644 index 00000000000..61da1fa6c6b --- /dev/null +++ b/script/boot_mem.rb @@ -0,0 +1,21 @@ +# simple script to measure memory at boot + +if ENV['RAILS_ENV'] != "production" + exec "RAILS_ENV=production ruby #{__FILE__}" +end + +require 'memory_profiler' + +MemoryProfiler.report do + require File.expand_path("../../config/environment", __FILE__) + + Rails.application.routes.recognize_path('abc') rescue nil + + # load up the yaml for the localization bits, in master process + I18n.t(:posts) + + # load up all models and schema + (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| + table.classify.constantize.first rescue nil + end +end.pretty_print diff --git a/script/profile_db_generator.rb b/script/profile_db_generator.rb index 18cf34b5f68..37f757177d9 100644 --- a/script/profile_db_generator.rb +++ b/script/profile_db_generator.rb @@ -45,7 +45,7 @@ def create_admin(seq) User.new.tap { |admin| admin.email = "admin@localhost#{seq}.fake" admin.username = "admin#{seq}" - admin.password = "password" + admin.password = "password12345abc" admin.save! admin.grant_admin! admin.change_trust_level!(TrustLevel[4]) diff --git a/spec/components/concern/second_factor_manager_spec.rb b/spec/components/concern/second_factor_manager_spec.rb new file mode 100644 index 00000000000..afc969d915e --- /dev/null +++ b/spec/components/concern/second_factor_manager_spec.rb @@ -0,0 +1,93 @@ +require 'rails_helper' + +RSpec.describe SecondFactorManager do + let(:user_second_factor) { Fabricate(:user_second_factor) } + let(:user) { user_second_factor.user } + let(:another_user) { Fabricate(:user) } + + describe '#totp' do + it 'should return the right data' do + totp = nil + + expect do + totp = another_user.totp + end.to change { UserSecondFactor.count }.by(1) + + expect(totp.issuer).to eq(SiteSetting.title) + expect(totp.secret).to eq(another_user.reload.user_second_factor.data) + end + end + + describe '#create_totp' do + it 'should create the right record' do + second_factor = another_user.create_totp(enabled: true) + + expect(second_factor.method).to eq(UserSecondFactor.methods[:totp]) + expect(second_factor.data).to be_present + expect(second_factor.enabled).to eq(true) + end + + describe 'when user has a second factor' do + it 'should return nil' do + expect(user.create_totp).to eq(nil) + end + end + end + + describe '#totp_provisioning_uri' do + it 'should return the right uri' do + expect(user.totp_provisioning_uri).to eq( + "otpauth://totp/#{SiteSetting.title}:#{user.email}?secret=#{user_second_factor.data}&issuer=#{SiteSetting.title}" + ) + end + end + + describe '#authenticate_totp' do + it 'should be able to authenticate a token' do + freeze_time do + expect(user.user_second_factor.last_used).to eq(nil) + + token = user.totp.now + + expect(user.authenticate_totp(token)).to eq(true) + expect(user.user_second_factor.last_used).to eq(DateTime.now) + expect(user.authenticate_totp(token)).to eq(false) + end + end + + describe 'when token is blank' do + it 'should be false' do + expect(user.authenticate_totp(nil)).to eq(false) + expect(user.user_second_factor.last_used).to eq(nil) + end + end + + describe 'when token is invalid' do + it 'should be false' do + expect(user.authenticate_totp('111111')).to eq(false) + expect(user.user_second_factor.last_used).to eq(nil) + end + end + end + + describe '#totp_enabled?' do + describe 'when user does not have a second factor record' do + it 'should return false' do + expect(another_user.totp_enabled?).to eq(false) + end + end + + describe "when user's second factor record is disabled" do + it 'should return false' do + user.user_second_factor.update!(enabled: false) + expect(user.totp_enabled?).to eq(false) + end + end + + describe "when user's second factor record is enabled" do + it 'should return true' do + expect(user.totp_enabled?).to eq(true) + end + end + end +end diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index d2b90f5e4d7..618b69a349d 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -479,10 +479,11 @@ describe CookedPostProcessor do before do Oneboxer.expects(:onebox) - .with("http://www.youtube.com/watch?v=9bZkp7q19f0", post_id: 123, invalidate_oneboxes: true) + .with("http://www.youtube.com/watch?v=9bZkp7q19f0", invalidate_oneboxes: true, user_id: nil, category_id: post.topic.category_id) .returns("<div>GANGNAM STYLE</div>") cpp.post_process_oneboxes end + it "inserts the onebox without wrapping p" do expect(cpp).to be_dirty expect(cpp.html).to match_html "<div>GANGNAM STYLE</div>" diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 3c5017aec38..838e3bb9b4e 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -210,10 +210,10 @@ describe Email::Receiver do expect { process(:reply_with_8bit_encoding) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq("hab vergessen kritische zeichen einzufügen:\näöüÄÖÜß") - end - it "prefers text over html" do + it "prefers text over html when site setting is disabled" do + SiteSetting.incoming_email_prefer_html = false expect { process(:text_and_html_reply) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq("This is the *text* part.") end @@ -363,6 +363,7 @@ describe Email::Receiver do end it "supports attached images in TEXT part" do + SiteSetting.incoming_email_prefer_html = false SiteSetting.queue_jobs = true expect { process(:no_body_with_image) }.to change { topic.posts.count } @@ -390,6 +391,12 @@ describe Email::Receiver do expect(topic.posts.last.raw).to_not match(/text\.txt/) end + it "supports emails with just an attachment" do + SiteSetting.authorized_extensions = "pdf" + expect { process(:attached_pdf_file) }.to change { topic.posts.count } + expect(topic.posts.last.raw).to match(/discourse\.pdf/) + end + it "supports liking via email" do expect { process(:like) }.to change(PostAction, :count) end diff --git a/spec/components/final_destination_spec.rb b/spec/components/final_destination_spec.rb index 38cd37e2bfa..aa5364c5b10 100644 --- a/spec/components/final_destination_spec.rb +++ b/spec/components/final_destination_spec.rb @@ -204,6 +204,63 @@ describe FinalDestination do expect(final.cookie).to eq('evil=trout') end end + + it "correctly extracts cookies during GET" do + stub_request(:head, "https://eviltrout.com").to_return(status: 405) + + stub_request(:get, "https://eviltrout.com") + .to_return(status: 302, body: "" , headers: { + "Location" => "https://eviltrout.com", + "Set-Cookie" => ["foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com", + "bar=1", + "baz=2; expires=Tue, 19-Feb-2019 10:14:24 GMT; path=/; domain=eviltrout.com"] + }) + + stub_request(:head, "https://eviltrout.com") + .with(headers: { "Cookie" => "bar=1; baz=2; foo=219ffwef9w0f" }) + .to_return(status: 200, body: "") + + final = FinalDestination.new("https://eviltrout.com", opts) + expect(final.resolve.to_s).to eq("https://eviltrout.com") + expect(final.status).to eq(:resolved) + expect(final.cookie).to eq("bar=1; baz=2; foo=219ffwef9w0f") + end + end + + it "should use the correct format for cookies when there is only one cookie" do + stub_request(:head, "https://eviltrout.com") + .to_return(status: 302, body: "" , headers: { + "Location" => "https://eviltrout.com", + "Set-Cookie" => "foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com" + }) + + stub_request(:head, "https://eviltrout.com") + .with(headers: { "Cookie" => "foo=219ffwef9w0f" }) + .to_return(status: 200, body: "") + + final = FinalDestination.new("https://eviltrout.com", opts) + expect(final.resolve.to_s).to eq("https://eviltrout.com") + expect(final.status).to eq(:resolved) + expect(final.cookie).to eq("foo=219ffwef9w0f") + end + + it "should use the correct format for cookies when there are multiple cookies" do + stub_request(:head, "https://eviltrout.com") + .to_return(status: 302, body: "" , headers: { + "Location" => "https://eviltrout.com", + "Set-Cookie" => ["foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com", + "bar=1", + "baz=2; expires=Tue, 19-Feb-2019 10:14:24 GMT; path=/; domain=eviltrout.com"] + }) + + stub_request(:head, "https://eviltrout.com") + .with(headers: { "Cookie" => "bar=1; baz=2; foo=219ffwef9w0f" }) + .to_return(status: 200, body: "") + + final = FinalDestination.new("https://eviltrout.com", opts) + expect(final.resolve.to_s).to eq("https://eviltrout.com") + expect(final.status).to eq(:resolved) + expect(final.cookie).to eq("bar=1; baz=2; foo=219ffwef9w0f") end end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 77667c4e014..0fc91d7b792 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -83,6 +83,12 @@ describe Guardian do expect(Guardian.new(user).post_can_act?(staff_post, :spam)).to eq(false) end + it "allows liking of staff when allow_flagging_staff is false" do + SiteSetting.allow_flagging_staff = false + staff_post = Fabricate(:post, user: Fabricate(:moderator)) + expect(Guardian.new(user).post_can_act?(staff_post, :like)).to eq(true) + end + it "returns false when liking yourself" do expect(Guardian.new(post.user).post_can_act?(post, :like)).to be_falsey end diff --git a/spec/components/html_normalize_spec.rb b/spec/components/html_normalize_spec.rb deleted file mode 100644 index 7c75d8a15bd..00000000000 --- a/spec/components/html_normalize_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'rails_helper' -require 'html_normalize' - -describe HtmlNormalize do - - def n(html) - HtmlNormalize.normalize(html) - end - - it "handles attributes without values" do - expect(n "<img alt>").to eq("<img alt>") - end - - it "handles self closing tags" do - - source = <<-HTML -<div> - <span><img src='testing'> - boo</span> -</div> -HTML - expect(n source).to eq(source.strip) - end - - it "Can handle aside" do - - source = <<~HTML - <aside class="quote" data-topic="2" data-post="1"> - <div class="title"> - <div class="quote-controls"></div> - <a href="http://test.localhost/t/this-is-a-test-topic-slight-smile/x/2">This is a test topic <img src="/images/emoji/emoji_one/slight_smile.png?v=5" title="slight_smile" alt="slight_smile" class="emoji"></a></div> - <blockquote> - <p>ddd</p> - </blockquote></aside> -HTML - expected = <<~HTML - <aside class="quote" data-post="1" data-topic="2"> - <div class="title"> - <div class="quote-controls"></div> - <a href="http://test.localhost/t/this-is-a-test-topic-slight-smile/x/2">This is a test topic <img src="/images/emoji/emoji_one/slight_smile.png?v=5" title="slight_smile" alt="slight_smile" class="emoji"></a> - </div> - <blockquote> - <p>ddd</p> - </blockquote> - </aside> -HTML - - expect(n expected).to eq(n source) - end - - it "Can normalize attributes" do - - source = "<a class='a b' name='sam'>b</a>" - same = "<a name='sam' class='a b' >b</a>" - - expect(n source).to eq(n same) - end - - it "Can indent divs nicely" do - source = "<div> <div><div>hello world</div> </div> </div>" - expected = <<~HTML - <div> - <div> - <div> - hello world - </div> - </div> - </div> -HTML - - expect(n source).to eq(expected.strip) - end -end diff --git a/spec/components/onebox/engine/discourse_local_onebox_spec.rb b/spec/components/onebox/engine/discourse_local_onebox_spec.rb deleted file mode 100644 index efc60b97501..00000000000 --- a/spec/components/onebox/engine/discourse_local_onebox_spec.rb +++ /dev/null @@ -1,161 +0,0 @@ -require 'rails_helper' - -describe Onebox::Engine::DiscourseLocalOnebox do - - before { SiteSetting.external_system_avatars_enabled = false } - - def build_link(url) - %|<a href="#{url}" rel="nofollow noopener">#{url}</a>| - end - - context "for a link to a post" do - let(:post) { Fabricate(:post) } - let(:post2) { Fabricate(:post, topic: post.topic, post_number: 2) } - - it "returns a link if post isn't found" do - url = "#{Discourse.base_url}/t/not-exist/3/2" - expect(Onebox.preview(url).to_s).to eq(build_link(url)) - end - - it "returns a link if not allowed to see the post" do - url = "#{Discourse.base_url}#{post2.url}" - Guardian.any_instance.expects(:can_see_post?).returns(false) - expect(Onebox.preview(url).to_s).to eq(build_link(url)) - end - - it "returns a link if post is hidden" do - hidden_post = Fabricate(:post, topic: post.topic, post_number: 2, hidden: true, hidden_reason_id: Post.hidden_reasons[:flag_threshold_reached]) - url = "#{Discourse.base_url}#{hidden_post.url}" - expect(Onebox.preview(url).to_s).to eq(build_link(url)) - end - - it "returns some onebox goodness if post exists and can be seen" do - url = "#{Discourse.base_url}#{post2.url}?source_topic_id=#{post2.topic_id + 1}" - html = Onebox.preview(url).to_s - expect(html).to include(post2.excerpt) - expect(html).to include(post2.topic.title) - - url = "#{Discourse.base_url}#{post2.url}/?source_topic_id=#{post2.topic_id + 1}" - html = Onebox.preview(url).to_s - expect(html).to include(post2.excerpt) - expect(html).to include(post2.topic.title) - - html = Onebox.preview("#{Discourse.base_url}#{post2.url}").to_s - expect(html).to include(post2.user.username) - expect(html).to include(post2.excerpt) - end - end - - context "for a link to a topic" do - let(:post) { Fabricate(:post) } - let(:topic) { post.topic } - - it "returns a link if topic isn't found" do - url = "#{Discourse.base_url}/t/not-found/123" - expect(Onebox.preview(url).to_s).to eq(build_link(url)) - end - - it "returns a link if not allowed to see the topic" do - url = topic.url - Guardian.any_instance.expects(:can_see_topic?).returns(false) - expect(Onebox.preview(url).to_s).to eq(build_link(url)) - end - - it "replaces emoji in the title" do - topic.update_column(:title, "Who wants to eat a :hamburger:") - expect(Onebox.preview(topic.url).to_s).to match(/hamburger\.png/) - end - - it "returns some onebox goodness if topic exists and can be seen" do - html = Onebox.preview(topic.url).to_s - expect(html).to include(topic.ordered_posts.first.user.username) - expect(html).to include("<blockquote>") - - html = Onebox.preview("#{topic.url}/?u=codinghorror").to_s - expect(html).to include(topic.ordered_posts.first.user.username) - expect(html).to include("<blockquote>") - end - end - - context "for a link to a user profile" do - let(:user) { Fabricate(:user) } - - it "returns a link if user isn't found" do - url = "#{Discourse.base_url}/u/none" - expect(Onebox.preview(url).to_s).to eq(build_link(url)) - end - - it "returns some onebox goodness if user exists" do - html = Onebox.preview("#{Discourse.base_url}/u/#{user.username}").to_s - expect(html).to include(user.username) - expect(html).to include(user.name) - expect(html).to include(user.created_at.strftime("%B %-d, %Y")) - expect(html).to include('<aside class="onebox">') - end - end - - context "for a link to an internal audio or video file" do - - let(:sha) { Digest::SHA1.hexdigest("discourse") } - let(:path) { "/uploads/default/original/3X/5/c/#{sha}" } - - it "returns nil if file type is not audio or video" do - url = "#{Discourse.base_url}#{path}.pdf" - stub_request(:get, url).to_return(body: '') - expect(Onebox.preview(url).to_s).to eq("") - end - - it "returns some onebox goodness for audio file" do - url = "#{Discourse.base_url}#{path}.MP3" - html = Onebox.preview(url).to_s - # </source> will be removed by the browser - # need to fix https://github.com/rubys/nokogumbo/issues/14 - expect(html).to eq(%|<audio controls=""><source src="#{url}"></source>#{build_link(url)}</audio>|) - end - - it "returns some onebox goodness for video file" do - url = "#{Discourse.base_url}#{path}.mov" - html = Onebox.preview(url).to_s - expect(html).to eq(%|<video width="100%" height="100%" controls=""><source src="#{url}"></source>#{build_link(url)}</video>|) - end - end - - context "When deployed to a subfolder" do - let(:base_uri) { "/subfolder" } - let(:base_url) { "http://test.localhost#{base_uri}" } - - before do - Discourse.stubs(:base_url).returns(base_url) - Discourse.stubs(:base_uri).returns(base_uri) - end - - context "for a link to a post" do - let(:post) { Fabricate(:post) } - let(:post2) { Fabricate(:post, topic: post.topic, post_number: 2) } - - it "returns some onebox goodness if post exists and can be seen" do - url = "#{Discourse.base_url}#{post2.url}?source_topic_id=#{post2.topic_id + 1}" - html = Onebox.preview(url).to_s - expect(html).to include(post2.excerpt) - expect(html).to include(post2.topic.title) - end - end - end - - context "When login_required is enabled" do - before { SiteSetting.login_required = true } - - context "for a link to a topic" do - let(:post) { Fabricate(:post) } - let(:topic) { post.topic } - - it "returns some onebox goodness if post exists and can be seen" do - html = Onebox.preview(topic.url).to_s - expect(html).to include(topic.ordered_posts.first.user.username) - expect(html).to include("<blockquote>") - end - end - - end - -end diff --git a/spec/components/oneboxer_spec.rb b/spec/components/oneboxer_spec.rb index 6579e1f6e2e..b6637d30f2f 100644 --- a/spec/components/oneboxer_spec.rb +++ b/spec/components/oneboxer_spec.rb @@ -11,4 +11,88 @@ describe Oneboxer do expect(Oneboxer.onebox("http://boom.com")).to eq("") end + context "local oneboxes" do + + def link(url) + url = "#{Discourse.base_url}#{url}" + %{<a href="#{url}">#{url}</a>} + end + + def preview(url, user = nil, category = nil, topic = nil) + Oneboxer.preview("#{Discourse.base_url}#{url}", + user_id: user&.id, + category_id: category&.id, + topic_id: topic&.id).to_s + end + + it "links to a topic/post" do + staff = Fabricate(:user) + Group[:staff].add(staff) + + secured_category = Fabricate(:category) + secured_category.permissions = { staff: :full } + secured_category.save! + + replier = Fabricate(:user) + + public_post = Fabricate(:post) + public_topic = public_post.topic + public_reply = Fabricate(:post, topic: public_topic, post_number: 2, user: replier) + public_hidden = Fabricate(:post, topic: public_topic, post_number: 3, hidden: true) + + user = public_post.user + public_category = public_topic.category + + secured_topic = Fabricate(:topic, user: staff, category: secured_category) + secured_post = Fabricate(:post, user: staff, topic: secured_topic) + secured_reply = Fabricate(:post, user: staff, topic: secured_topic, post_number: 2) + + expect(preview(public_topic.relative_url, user, public_category)).to include(public_topic.title) + expect(preview(public_post.url, user, public_category)).to include(public_topic.title) + + onebox = preview(public_reply.url, user, public_category) + expect(onebox).to include(public_reply.excerpt) + expect(onebox).to include(PrettyText.avatar_img(replier.avatar_template, "tiny")) + + onebox = preview(public_reply.url, user, public_category, public_topic) + expect(onebox).not_to include(public_topic.title) + expect(onebox).to include(replier.avatar_template.sub("{size}", "40")) + + expect(preview(public_hidden.url, user, public_category)).to match_html(link(public_hidden.url)) + expect(preview(secured_topic.relative_url, user, public_category)).to match_html(link(secured_topic.relative_url)) + expect(preview(secured_post.url, user, public_category)).to match_html(link(secured_post.url)) + expect(preview(secured_reply.url, user, public_category)).to match_html(link(secured_reply.url)) + + expect(preview(public_topic.relative_url, user, secured_category)).to match_html(link(public_topic.relative_url)) + expect(preview(public_reply.url, user, secured_category)).to match_html(link(public_reply.url)) + expect(preview(secured_post.url, user, secured_category)).to match_html(link(secured_post.url)) + expect(preview(secured_reply.url, user, secured_category)).to match_html(link(secured_reply.url)) + + expect(preview(public_topic.relative_url, staff, secured_category)).to include(public_topic.title) + expect(preview(public_post.url, staff, secured_category)).to include(public_topic.title) + expect(preview(public_reply.url, staff, secured_category)).to include(public_reply.excerpt) + expect(preview(public_hidden.url, staff, secured_category)).to match_html(link(public_hidden.url)) + expect(preview(secured_topic.relative_url, staff, secured_category)).to include(secured_topic.title) + expect(preview(secured_post.url, staff, secured_category)).to include(secured_topic.title) + expect(preview(secured_reply.url, staff, secured_category)).to include(secured_reply.excerpt) + expect(preview(secured_reply.url, staff, secured_category, secured_topic)).not_to include(secured_topic.title) + end + + it "links to an user profile" do + user = Fabricate(:user) + + expect(preview("/u/does-not-exist")).to match_html(link("/u/does-not-exist")) + expect(preview("/u/#{user.username}")).to include(user.name) + end + + it "links to an upload" do + path = "/uploads/default/original/3X/e/8/e8fcfa624e4fb6623eea57f54941a58ba797f14d" + + expect(preview("#{path}.pdf")).to match_html(link("#{path}.pdf")) + expect(preview("#{path}.MP3")).to include("<audio ") + expect(preview("#{path}.mov")).to include("<video ") + end + + end + end diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 971d7dd6005..487ec569681 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -1,6 +1,5 @@ require 'rails_helper' require 'pretty_text' -require 'html_normalize' describe PrettyText do @@ -9,11 +8,11 @@ describe PrettyText do end def n(html) - HtmlNormalize.normalize(html) + html.strip end def cook(*args) - n(PrettyText.cook(*args)) + PrettyText.cook(*args) end let(:wrapped_image) { "<div class=\"lightbox-wrapper\"><a href=\"//localhost:3000/uploads/default/4399/33691397e78b4d75.png\" class=\"lightbox\" title=\"Screen Shot 2014-04-14 at 9.47.10 PM.png\"><img src=\"//localhost:3000/uploads/default/_optimized/bd9/b20/bbbcd6a0c0_655x500.png\" width=\"655\" height=\"500\"><div class=\"meta\">\n<span class=\"filename\">Screen Shot 2014-04-14 at 9.47.10 PM.png</span><span class=\"informations\">966x737 1.47 MB</span><span class=\"expand\"></span>\n</div></a></div>" } @@ -33,13 +32,13 @@ describe PrettyText do topic = Fabricate(:topic, title: "this is a test topic :slight_smile:") expected = <<~HTML - <aside class="quote" data-topic="#{topic.id}" data-post="2"> + <aside class="quote" data-post="2" data-topic="#{topic.id}"> <div class="title"> - <div class="quote-controls"></div> - <a href="http://test.localhost/t/this-is-a-test-topic-slight-smile/#{topic.id}/2">This is a test topic <img src="/images/emoji/twitter/slight_smile.png?v=5" title="slight_smile" alt="slight_smile" class="emoji"></a> + <div class="quote-controls"></div> + <a href="http://test.localhost/t/this-is-a-test-topic-slight-smile/#{topic.id}/2">This is a test topic <img src="/images/emoji/twitter/slight_smile.png?v=5" title="slight_smile" alt="slight_smile" class="emoji"></a> </div> <blockquote> - <p>ddd</p> + <p>ddd</p> </blockquote> </aside> HTML @@ -126,13 +125,13 @@ describe PrettyText do topic = Fabricate(:topic, title: "this is a test topic") expected = <<~HTML - <aside class="quote group-#{group.name}" data-topic="#{topic.id}" data-post="2"> + <aside class="quote group-#{group.name}" data-post="2" data-topic="#{topic.id}"> <div class="title"> - <div class="quote-controls"></div> - <img alt class='avatar' height='20' src='//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png' width='20'><a href='http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2'>This is a test topic</a> + <div class="quote-controls"></div> + <img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"><a href="http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2">This is a test topic</a> </div> <blockquote> - <p>ddd</p> + <p>ddd</p> </blockquote> </aside> HTML diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index 3f2228a990f..ed54be8c1da 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -395,6 +395,27 @@ describe Search do let(:tag_group) { Fabricate(:tag_group) } let(:category) { Fabricate(:category) } + context 'post searching' do + it 'can find posts with tags' do + SiteSetting.tagging_enabled = true + + post = Fabricate(:post, raw: 'I am special post') + DiscourseTagging.tag_topic_by_names(post.topic, Guardian.new(Fabricate.build(:admin)), [tag.name]) + post.topic.save + + # we got to make this index (it is deferred) + Jobs::ReindexSearch.new.rebuild_problem_posts + + result = Search.execute(tag.name) + expect(result.posts.length).to eq(1) + + SiteSetting.tagging_enabled = false + + result = Search.execute(tag.name) + expect(result.posts.length).to eq(0) + end + end + context 'tagging is disabled' do before { SiteSetting.tagging_enabled = false } @@ -856,7 +877,7 @@ describe Search do str = " grigio:babel deprecated? " str << "page page on Atmosphere](https://atmospherejs.com/grigio/babel)xxx: aaa.js:222 aaa'\"bbb" - ts_query = Search.ts_query(str, "simple") + ts_query = Search.ts_query(term: str, ts_config: "simple") Post.exec_sql("SELECT to_tsvector('bbb') @@ " << ts_query) end @@ -923,6 +944,19 @@ describe Search do end end + context 'in:title' do + it 'allows for search in title' do + topic = Fabricate(:topic, title: 'I am testing a title search') + _post = Fabricate(:post, topic_id: topic.id, raw: 'this is the first post') + + results = Search.execute('title in:title') + expect(results.posts.length).to eq(1) + + results = Search.execute('first in:title') + expect(results.posts.length).to eq(0) + end + end + context 'pagination' do let(:number_of_results) { 2 } let!(:post1) { Fabricate(:post, raw: 'hello hello hello hello hello') } diff --git a/spec/components/text_cleaner_spec.rb b/spec/components/text_cleaner_spec.rb index 8a25c29de82..e4a4dd2a9fc 100644 --- a/spec/components/text_cleaner_spec.rb +++ b/spec/components/text_cleaner_spec.rb @@ -159,6 +159,11 @@ describe TextCleaner do expect(TextCleaner.clean_title(" \t Hello there \n ")).to eq("Hello there") end + it "strips zero width spaces" do + expect(TextCleaner.clean_title("Hello there")).to eq("Hello there") + expect(TextCleaner.clean_title("Hello there").length).to eq(11) + end + context "title_prettify site setting is enabled" do before { SiteSetting.title_prettify = true } diff --git a/spec/components/validators/max_emojis_validator_spec.rb b/spec/components/validators/max_emojis_validator_spec.rb new file mode 100644 index 00000000000..a81bed2fb8d --- /dev/null +++ b/spec/components/validators/max_emojis_validator_spec.rb @@ -0,0 +1,50 @@ +# encoding: UTF-8 + +require 'rails_helper' +require 'validators/max_emojis_validator' + +describe MaxEmojisValidator do + + # simulate Rails behavior (singleton) + def validate + @validator ||= MaxEmojisValidator.new(attributes: :title) + @validator.validate_each(record, :title, record.title) + end + + shared_examples "validating any topic title" do + it 'adds an error when emoji count is greater than SiteSetting.max_emojis_in_title' do + SiteSetting.max_emojis_in_title = 3 + record.title = '🧐 Lots of emojis here 🎃 :joy: :sunglasses:' + validate + expect(record.errors[:title][0]).to eq(I18n.t("errors.messages.max_emojis", max_emojis_count: 3)) + end + end + + describe 'topic' do + let(:record) { Fabricate.build(:topic) } + + it 'does not add an error when emoji count is good' do + SiteSetting.max_emojis_in_title = 2 + + record.title = 'To Infinity and beyond! 🚀 :woman:t5:' + validate + expect(record.errors[:title]).to_not be_present + end + + include_examples "validating any topic title" + end + + describe 'private message' do + let(:record) { Fabricate.build(:private_message_topic) } + + it 'does not add an error when emoji count is good' do + SiteSetting.max_emojis_in_title = 1 + + record.title = 'To Infinity and beyond! 🚀' + validate + expect(record.errors[:title]).to_not be_present + end + + include_examples "validating any topic title" + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index e56fa93b3e6..703021890fc 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -12,7 +12,7 @@ describe Admin::UsersController do @user = log_in(:admin) end - context '.index' do + context '#index' do it 'returns success' do get :index, format: :json expect(response).to be_success @@ -44,7 +44,7 @@ describe Admin::UsersController do end end - describe '.show' do + describe '#show' do context 'an existing user' do it 'returns success' do get :show, params: { id: @user.id }, format: :json @@ -60,7 +60,7 @@ describe Admin::UsersController do end end - context '.approve_bulk' do + context '#approve_bulk' do let(:evil_trout) { Fabricate(:evil_trout) } @@ -83,7 +83,7 @@ describe Admin::UsersController do end - context '.generate_api_key' do + context '#generate_api_key' do let(:evil_trout) { Fabricate(:evil_trout) } it 'calls generate_api_key' do @@ -92,7 +92,7 @@ describe Admin::UsersController do end end - context '.revoke_api_key' do + context '#revoke_api_key' do let(:evil_trout) { Fabricate(:evil_trout) } @@ -103,7 +103,7 @@ describe Admin::UsersController do end - context '.approve' do + context '#approve' do let(:evil_trout) { Fabricate(:evil_trout) } @@ -120,7 +120,7 @@ describe Admin::UsersController do end - context '.suspend' do + context '#suspend' do let(:user) { Fabricate(:evil_trout) } it "works properly" do @@ -220,7 +220,7 @@ describe Admin::UsersController do end - context '.revoke_admin' do + context '#revoke_admin' do before do @another_admin = Fabricate(:admin) end @@ -238,7 +238,7 @@ describe Admin::UsersController do end end - context '.grant_admin' do + context '#grant_admin' do before do @another_user = Fabricate(:coding_horror) end @@ -265,7 +265,7 @@ describe Admin::UsersController do end end - context '.add_group' do + context '#add_group' do let(:user) { Fabricate(:user) } let(:group) { Fabricate(:group) } @@ -292,7 +292,7 @@ describe Admin::UsersController do end end - context '.primary_group' do + context '#primary_group' do let(:group) { Fabricate(:group) } before do @@ -347,7 +347,7 @@ describe Admin::UsersController do end end - context '.trust_level' do + context '#trust_level' do before do @another_user = Fabricate(:coding_horror, created_at: 1.month.ago) end @@ -401,7 +401,7 @@ describe Admin::UsersController do end end - describe '.revoke_moderation' do + describe '#revoke_moderation' do before do @moderator = Fabricate(:moderator) end @@ -425,7 +425,7 @@ describe Admin::UsersController do end end - context '.grant_moderation' do + context '#grant_moderation' do before do @another_user = Fabricate(:coding_horror) end @@ -448,7 +448,7 @@ describe Admin::UsersController do end end - context '.reject_bulk' do + context '#reject_bulk' do let(:reject_me) { Fabricate(:user) } let(:reject_me_too) { Fabricate(:user) } @@ -514,7 +514,7 @@ describe Admin::UsersController do end end - context '.destroy' do + context '#destroy' do let(:delete_me) { Fabricate(:user) } it "returns a 403 if the user doesn't exist" do @@ -615,6 +615,31 @@ describe Admin::UsersController do expect(@reg_user).to be_silenced end + it "can have an associated post" do + silence_post = Fabricate(:post, user: @reg_user) + + put :silence, params: { + user_id: @reg_user.id, + post_id: silence_post.id, + post_action: 'edit', + post_edit: "this is the new contents for the post" + }, format: :json + expect(response).to be_success + + silence_post.reload + expect(silence_post.raw).to eq("this is the new contents for the post") + + log = UserHistory.where( + target_user_id: @reg_user.id, + action: UserHistory.actions[:silence_user] + ).first + expect(log).to be_present + expect(log.post_id).to eq(silence_post.id) + + @reg_user.reload + expect(@reg_user).to be_silenced + end + it "will set a length of time if provided" do future_date = 1.month.from_now.to_date put( diff --git a/spec/controllers/onebox_controller_spec.rb b/spec/controllers/onebox_controller_spec.rb index 374f6a4a2a3..4a8cb5b9bf6 100644 --- a/spec/controllers/onebox_controller_spec.rb +++ b/spec/controllers/onebox_controller_spec.rb @@ -14,8 +14,8 @@ describe OneboxController do before { @user = log_in(:admin) } it 'invalidates the cache if refresh is passed' do - Oneboxer.expects(:preview).with(url, invalidate_oneboxes: true) - get :show, params: { url: url, refresh: 'true', user_id: @user.id }, format: :json + Oneboxer.expects(:preview).with(url, invalidate_oneboxes: true, user_id: @user.id, category_id: 0, topic_id: 0) + get :show, params: { url: url, refresh: 'true' }, format: :json end describe "cached onebox" do @@ -41,13 +41,13 @@ describe OneboxController do stub_request(:get, url) .to_return(status: 200, headers: {}, body: onebox_html).then.to_raise - get :show, params: { url: url, user_id: @user.id, refresh: "true" }, format: :json + get :show, params: { url: url, refresh: "true" }, format: :json expect(response).to be_success expect(response.body).to include('Fred') expect(response.body).to include('bodycontent') - get :show, params: { url: url, user_id: @user.id }, format: :json + get :show, params: { url: url }, format: :json expect(response).to be_success expect(response.body).to include('Fred') expect(response.body).to include('bodycontent') @@ -59,7 +59,7 @@ describe OneboxController do it "returns 429" do Oneboxer.expects(:is_previewing?).returns(true) - get :show, params: { url: url, user_id: @user.id }, format: :json + get :show, params: { url: url }, format: :json expect(response.status).to eq(429) end @@ -70,8 +70,8 @@ describe OneboxController do let(:body) { "this is the onebox body" } before do - Oneboxer.expects(:preview).with(url, invalidate_oneboxes: false).returns(body) - get :show, params: { url: url, user_id: @user.id }, format: :json + Oneboxer.expects(:preview).returns(body) + get :show, params: { url: url }, format: :json end it 'returns the onebox response in the body' do @@ -84,19 +84,82 @@ describe OneboxController do describe "missing onebox" do it "returns 404 if the onebox is nil" do - Oneboxer.expects(:preview).with(url, invalidate_oneboxes: false).returns(nil) - get :show, params: { url: url, user_id: @user.id }, format: :json + Oneboxer.expects(:preview).returns(nil) + get :show, params: { url: url }, format: :json expect(response.response_code).to eq(404) end it "returns 404 if the onebox is an empty string" do - Oneboxer.expects(:preview).with(url, invalidate_oneboxes: false).returns(" \t ") - get :show, params: { url: url, user_id: @user.id }, format: :json + Oneboxer.expects(:preview).returns(" \t ") + get :show, params: { url: url }, format: :json expect(response.response_code).to eq(404) end end + describe "local onebox" do + + it 'does not cache local oneboxes' do + post = create_post + url = Discourse.base_url + post.url + + get :show, params: { url: url, category_id: post.topic.category_id }, format: :json + expect(response.body).to include('blockquote') + + post.trash! + + get :show, params: { url: url, category_id: post.topic.category_id }, format: :json + expect(response.body).not_to include('blockquote') + end + end + + it 'does not onebox when you have no permission on category' do + log_in + + post = create_post + url = Discourse.base_url + post.url + + get :show, params: { url: url, category_id: post.topic.category_id }, format: :json + expect(response.body).to include('blockquote') + + post.topic.category.set_permissions(staff: :full) + post.topic.category.save + + get :show, params: { url: url, category_id: post.topic.category_id }, format: :json + expect(response.body).not_to include('blockquote') + end + + it 'does not allow onebox of PMs' do + user = log_in + + post = create_post(archetype: 'private_message', target_usernames: [user.username]) + url = Discourse.base_url + post.url + + get :show, params: { url: url }, format: :json + expect(response.body).not_to include('blockquote') + end + + it 'does not allow whisper onebox' do + log_in + + post = create_post + whisper = create_post(topic_id: post.topic_id, post_type: Post.types[:whisper]) + url = Discourse.base_url + whisper.url + + get :show, params: { url: url }, format: :json + expect(response.body).not_to include('blockquote') + end + + it 'allows onebox to public topics/posts in PM' do + log_in + + post = create_post + url = Discourse.base_url + post.url + + get :show, params: { url: url }, format: :json + expect(response.body).to include('blockquote') + end + end end diff --git a/spec/controllers/session_controller_spec.rb b/spec/controllers/session_controller_spec.rb index 96b58c5483d..1f980a1fcb3 100644 --- a/spec/controllers/session_controller_spec.rb +++ b/spec/controllers/session_controller_spec.rb @@ -584,6 +584,55 @@ describe SessionController do end end + context 'when user has 2-factor logins' do + let!(:user_second_factor) { Fabricate(:user_second_factor, user: user) } + + describe 'when second factor token is missing' do + it 'should return the right response' do + post :create, params: { + login: user.username, + password: 'myawesomepassword', + }, format: :json + + expect(JSON.parse(response.body)['error']).to eq(I18n.t( + 'login.invalid_second_factor_code' + )) + end + end + + describe 'when second factor token is invalid' do + it 'should return the right response' do + post :create, params: { + login: user.username, + password: 'myawesomepassword', + second_factor_token: '00000000' + }, format: :json + + expect(JSON.parse(response.body)['error']).to eq(I18n.t( + 'login.invalid_second_factor_code' + )) + end + end + + describe 'when second factor token is valid' do + it 'should log the user in' do + post :create, params: { + login: user.username, + password: 'myawesomepassword', + second_factor_token: ROTP::TOTP.new(user_second_factor.data).now + }, format: :json + + user.reload + + expect(session[:current_user_id]).to eq(user.id) + expect(user.user_auth_tokens.count).to eq(1) + + expect(UserAuthToken.hash_token(cookies[:_t])) + .to eq(user.user_auth_tokens.first.auth_token) + end + end + end + describe 'with a blocked IP' do before do screened_ip = Fabricate(:screened_ip_address) @@ -777,7 +826,32 @@ describe SessionController do login: user.username, password: 'myawesomepassword' }, format: :json - expect(response).not_to be_success + expect(response.status).to eq(429) + json = JSON.parse(response.body) + expect(json["error_type"]).to eq("rate_limit") + end + + it 'rate limits second factor attempts' do + RateLimiter.enable + RateLimiter.clear_all! + + 3.times do + post :create, params: { + login: user.username, + password: 'myawesomepassword', + second_factor_token: '000000' + }, format: :json + + expect(response).to be_success + end + + post :create, params: { + login: user.username, + password: 'myawesomepassword', + second_factor_token: '000000' + }, format: :json + + expect(response.status).to eq(429) json = JSON.parse(response.body) expect(json["error_type"]).to eq("rate_limit") end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 2a91a611965..abf27ceb47d 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -148,6 +148,36 @@ describe UploadsController do expect(id).to be end + it 'respects `authorized_extensions_for_staff` setting when staff upload file' do + SiteSetting.authorized_extensions = "" + SiteSetting.authorized_extensions_for_staff = "*" + @user.update_columns(moderator: true) + + post :create, params: { + file: text_file, + type: "composer", + format: :json + } + + expect(response).to be_success + data = JSON.parse(response.body) + expect(data["id"]).to be + end + + it 'ignores `authorized_extensions_for_staff` setting when non-staff upload file' do + SiteSetting.authorized_extensions = "" + SiteSetting.authorized_extensions_for_staff = "*" + + post :create, params: { + file: text_file, + type: "composer", + format: :json + } + + data = JSON.parse(response.body) + expect(data["errors"].first).to eq(I18n.t("upload.unauthorized", authorized_extensions: '')) + end + it 'returns an error when it could not determine the dimensions of an image' do Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index f9b802a02db..19a324d7f4b 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -343,7 +343,7 @@ describe UsersController do ) expect(response).to be_success - expect(response.body).to include('{"is_developer":false,"admin":false}') + expect(response.body).to include('{"is_developer":false,"admin":false,"second_factor_required":false}') user.reload @@ -406,6 +406,43 @@ describe UsersController do expect(email_token.confirmed).to eq(false) expect(UserAuthToken.where(id: user_token.id).count).to eq(1) end + + context '2 factor authentication required' do + let!(:second_factor) { Fabricate(:user_second_factor, user: user) } + + it 'does not change with an invalid token' do + token = user.email_tokens.create!(email: user.email).token + + get :password_reset, params: { token: token } + + expect(response.body).to include('{"is_developer":false,"admin":false,"second_factor_required":true}') + + put :password_reset, + params: { token: token, password: 'hg9ow8yHG32O', second_factor_token: '000000' } + + expect(response.body).to include(I18n.t("login.invalid_second_factor_code")) + + user.reload + expect(user.confirm_password?('hg9ow8yHG32O')).not_to eq(true) + expect(user.user_auth_tokens.count).not_to eq(1) + end + + it 'changes password with valid 2-factor tokens' do + token = user.email_tokens.create(email: user.email).token + + get :password_reset, params: { token: token } + + put :password_reset, params: { + token: token, + password: 'hg9ow8yHG32O', + second_factor_token: ROTP::TOTP.new(second_factor.data).now + } + + user.reload + expect(user.confirm_password?('hg9ow8yHG32O')).to eq(true) + expect(user.user_auth_tokens.count).to eq(1) + end + end end context 'submit change' do @@ -475,7 +512,7 @@ describe UsersController do end end - describe '.admin_login' do + describe '#admin_login' do let(:admin) { Fabricate(:admin) } let(:user) { Fabricate(:user) } @@ -514,6 +551,32 @@ describe UsersController do expect(session[:current_user_id]).to eq(admin.id) end end + + describe 'when 2 factor authentication is enabled' do + let(:second_factor) { Fabricate(:user_second_factor, user: admin) } + render_views + + it 'does not log in when token required' do + second_factor + token = admin.email_tokens.create(email: admin.email).token + get :admin_login, params: { token: token } + expect(response).not_to redirect_to('/') + expect(session[:current_user_id]).not_to eq(admin.id) + expect(response.body).to include(I18n.t('login.second_factor_description')); + end + + it 'logs in when a valid 2-factor token is given' do + token = admin.email_tokens.create(email: admin.email).token + + put :admin_login, params: { + token: token, + second_factor_token: ROTP::TOTP.new(second_factor.data).now + } + + expect(response).to redirect_to('/') + expect(session[:current_user_id]).to eq(admin.id) + end + end end end diff --git a/spec/fabricators/user_second_factor_fabricator.rb b/spec/fabricators/user_second_factor_fabricator.rb new file mode 100644 index 00000000000..1bb88567873 --- /dev/null +++ b/spec/fabricators/user_second_factor_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:user_second_factor) do + user + data 'rcyryaqage3jexfj' + enabled true + method UserSecondFactor.methods[:totp] +end diff --git a/spec/fixtures/emails/attached_pdf_file.eml b/spec/fixtures/emails/attached_pdf_file.eml new file mode 100644 index 00000000000..91c2a5e13d8 --- /dev/null +++ b/spec/fixtures/emails/attached_pdf_file.eml @@ -0,0 +1,1158 @@ +Date: Fri, 16 Feb 2018 17:45:22 +0100 +From: Foo Bar <discourse@bar.com> +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Message-ID: <100@foo.bar.mail> +Content-Type: application/pdf; name="discourse.pdf" +Content-Disposition: attachment; filename="discourse.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9DcmVhdG9yIChNb3ppbGxhLzUuMCBcKFdpbmRvd3Mg +TlQgMTAuMDsgV2luNjQ7IHg2NFwpIEFwcGxlV2ViS2l0LzUzNy4zNiBcKEtIVE1MLCBsaWtlIEdl +Y2tvXCkgQ2hyb21lLzY0LjAuMzI4Mi4xNjcgU2FmYXJpLzUzNy4zNikKL1Byb2R1Y2VyIChTa2lh +L1BERiBtNjQpCi9DcmVhdGlvbkRhdGUgKEQ6MjAxODAyMTYxNjQ0NDMrMDAnMDAnKQovTW9kRGF0 +ZSAoRDoyMDE4MDIxNjE2NDQ0MyswMCcwMCcpPj4KZW5kb2JqCjIgMCBvYmoKPDwvVHlwZSAvWE9i +amVjdAovU3VidHlwZSAvSW1hZ2UKL1dpZHRoIDExODgKL0hlaWdodCA1NzUKL0NvbG9yU3BhY2Ug +L0RldmljZVJHQgovQml0c1BlckNvbXBvbmVudCA4Ci9GaWx0ZXIgL0RDVERlY29kZQovQ29sb3JU +cmFuc2Zvcm0gMAovTGVuZ3RoIDIyMDQ3Pj4gc3RyZWFtCv/Y/+AAEEpGSUYAAQEAAEgASAAA/9sA +hAALCwsLCwsUCwsUHBQUFBwmHBwcHCYwJiYmJiYwOTAwMDAwMDk5OTk5OTk5RUVFRUVFUFBQUFBa +WlpaWlpaWlpaAQ4PDxcVFycVFSdeQDRAXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5e +Xl5eXl5eXl5eXl5eXl5eXl7/wgARCAI/BKQDASIAAhEBAxEB/8QAGwABAAIDAQEAAAAAAAAAAAAA +AAIDAQQFBgf/2gAIAQEAAAAA+uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAADS+dAeht8yB6z01env5UxnaAB5L419F+l+B09/t38X13zD02n1+Vu6e/s6 +dFPJ9V6OYAAAAAAAAAAAAAc75F6bTlp7nM9Jd5D0HK2tXe5HtfZ8fo6XUcijY6wAHnvn/G+u7/zf +mT3Op7z5ft8bq73jPQ9Lzmp6bZ0vPfcgAAAAAAAAAAAAAc75F296jm9Xjehu873uN0NfW0/Zez5d +bsOVRsdQAAqzYjIQnGQYyVeb9SAAAAAAAAAAAAADT+fAd+3zYHqfSGtsgAAAAAAAAAAAAAAAAAAA +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 +yrUBcwgLstVdlqrstVdlqrstVdlqrstVbPRfTcS5bRSfUItXZaq7LVXZaq7LVXZaq7LVXZaq2em6 +nN3cLQTP/EchSFIUhSFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFc +FcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFc +FcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFc +FcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFcFIUhSFIUhSFI +/wCdnbSMmYpu3n42/wAJj21G3NTnBokrtB0CZWDsDhuc4NbcU3aKbv5/6nc5wYLnJ+3wYAVLb2vM +OEeX7UJpgDCXBcRzrGu8TXwV2v4sLZy1VEvNWoHZAp5ezaHVRk2JHouNYajs8RC7UQHZEgTgqtRw +NjflJTKtaGMABJbKftLmOxtziNVdzn/yf9KnVqN8eReQqby+7oDCZSbUfUv+ZNqvZTIzh1slDaTb +dhg6DCY+97ho3BGtV5nMAtYmEv2m/S0R91tThApzFxzVOv8AgDV02qq/aAwzAxGIXFLS6BzSAnV6 +lO4PAkCcEypU4nDqAYicPOalQtMBU6heYKftNOm4tM4LtNOSMcF2gGLfmgyhtVIycYCp1m1ZjTru +2h1tP6oINjNbHc15GhVYy/6bqnjVM3MBW21ahqcFiFHaMQAcFslY1qMnNba83hipsZwpe2ZTqdr8 +MFsbi6jB09jUcQIbmmGWg9+oS1shNLnSJVz7ZnVU3Ez3mP0PUoOlw+6N04IXOYDMKncRJ9wqM4gA +6GU+gH1G1ciENncOUO5Z+6bTtqOfPiQpw9z/AJkNkAaQDrI9FwHOa4Pdn0Qouuve6cIVOiWFpJm0 +QuyugsDsJnJcDGZ+K5VKdlJ7c7jh9VSZw6YYuDUDnFj4u9F2aA2w4txxTqfDpvvN1/pqqDOHTDTn +qjQdzBroa7NNpBj7h0A/hcIGrxHY4QnbNJJaYmD9wjQe5pudif8ApGhMmcZBR2cuuL3YnBWfiCp0 +Eec1KZcZCp0ywyU+jUqVX42tcAjs3K9oPiTdlt1+K7Jdizl2Y0VGjwp9fSN1Zl7ICa19xnJcMLZ6 +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 +j+lI/pSP6Uj+lItySQkXlp/Skf0pH9KR/Skf0pH9KR/SkMl7J6JaE9w36tLjZ1BNRP8AzJHNl8GS +F7j1ipLQmkqymZ0jLYg0lQU5eR51S5hgW5EeMhBD4XwW34xcitSRv7CEE31S+fzCMNtInWBWrFq2 +EiewciaKqI1zSalhhlgcOepLKNxJM070NCZjByMN4w1RMzTYTlfReTIF7YpMY9x7k3GJbGqOT9qf +kPR0VyH/AKC0TEkOt5FeNo7N7Y9tlpUkqbrWGfXAmvTPwGSPIog/IvjGnBD5JcUQKSPGB1GhKjfY +a9iPyF6Sy0CaQdxvx65PMvc97/zQwq3t8DFmXlRbJtnGPCGA3AtOpseP0/A6ossHLYl97uldKcTi +tSiLzr/s7ivuHRoXMbI2TafI0nRnB8BqGx+D+iZ+B/0fj/5Qq+r/AKH9D3L1E4m8egKlLx9A3V2P +po3eA9v/AL/P7V/IelM8dpz7jhNLG+hcrqanZhCGNFhhZVbr8kxpRJRLr0z8BiEKsGtzPkpZNYnT +EKOr2YzYbeM7HdcgwLKfkL0+/YfIQqnx6k3Tv0UCp+Q44skwxdesVsz8L2FfSK2Z+Lv4FZtYwzkg +tyftXac0dK+50r7nSvudK+50r7nSvudK+5opaHLxp0r7nSvudK+50r7nSvudK+50r7i9efwJzbX/ +ABH3Hcdx3Hcdp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2na +dp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nad +p2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp2nadp +2nadp2nadp2ncdx3Hcdx3HcRERERERERERERERERERERERERERERERERERERERERERERERERERER +ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER +EREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREX7hVbRh +ybqIEB9T9PLpgGGx6OWhI0D8PQbWRI/n3/6PrDF+nucnAqIMFT7Lz9Tx8TLXVg2FQT7DxW1e6c/Q +ardk08ZREOaRvU8wYVKuj2N7AjbGBYOZ94oW84JZ0OOuhuv/AEPWmgfmbHfTkLLXBaKrrii8jtEy +Q7SPlDUayZeXyOWzGnXlzcHbDI2wgk/rnUiWxqcsX1WKRGLIOOsaNVlc4baEDUsnBpuGBTAr8frM +eLQ6IWjMGhuKpJjTnucRfq0kzoX4CLWsOEpaeySPPo5s6D1GQtkVXTfqNYgtipNBrAztetec/wDk +Q8js4hm0xhN4Spy8G6U4jweUGf4edD+BjTbXxtITDWZhJtThOceRTTse/iY3uv4Bs2v9GRZ6TFHy +U5Gpbde3yGUJD7BdHTns0PzSe6UdmChyciGjsJT2Gb1V9podImswiX0G83vFTZlAnWDwRfsW3LOX +x6eFIOoPOVLKbJ0Es+/JjVtWsholkHSWt7plZ1ZYDe0vc9m65qSvO4xwdhyh9EBNaEZYkRa5BS3h +MYW2h+pdFzTSgjpLVJYSTo7t2R7/AKzXi0OkNETgDxaaI2n7QuT8WEaUEnaFhPuZxtN+Caejm7Fk +zpLKMY8SwO8MIDyclNiaW0aZpoa60Lt0RFDY4xyi3meBaH9TWl5f4cwmZsUpePj3acibWAhRxHqO +div4UVvGhUMCuzyVJ2y/n08vqixj91OIyE9qJJKIiiod8iMysvz6NuHSUdKPaeiG4hiXUx95q+p9 +95r/AJDhERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER +ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER +ERERERERERERERERERERERERERERERERERERF+6HVsr2KbEvImmqvR2TzUvv/wDBDs+FtJoSPtyT +PNV49UyZZWsP2/TGbbVWlYuWU4U42XsKHje7Nh+SPYW0s5VZgxYNNtQbGlUpOExaxJ+SoKbBjK5/ +IhuFY+BEbjYvBguVVXw1wyWrDqPYva+pdGlMg3LOicXFf1P+SFDlYXykKBajY+hbNtMEc1A5ESjZ +iC2A+50JW55A5VjciRV7ionKHwYUu2m5we2aL8ieaj2xuiGonDwfRXE1Nca8CUrGVOPwzNEKq8ju +WqM50NbpWJEKk7TiciGQOxwJriJWXFOo6+mUVZUS6EZx949hqtTK4jvI0Ohjl92Muwyn2/RVYlES +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 +P6RP6RP6RP6RP6RP6RCSCiU3myCAIQ21rU/pE/pE/pE/pE/pE/pE/pEGgFiU3pf0GFSanX/yjbbV +f8M8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8 +wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzD +PMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8 +wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzD +PMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8wzzDPMM8w/4NtttxTinFOKcU +4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKc +U4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOK +cU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcU4pxTinFO +KcU4pxTinFOKcU4pxTinFOKcU4pxTinFOKcX+waTIgKVg+N5QtLdVJ8Kz9OluHmWKo6HV9ppQOXM +PnZps+mqqn3ouJNNYW/k9qmuYVDVLDqQc7eyx3gfKYusPccn3giWZH8uaqWZSW1FjUOMXoewZis4 +LdVuBzW0XQAArVhovpWXmCwH1RFsG9LiPgxsudK9DqyhrIESEPUS5TyFSlNDp7xt8cxgVlxao94F +AI1aFrAsN6ekQy3vkZFvGvtpCBCtCVq1V7sEpcH9d1HfEs4rgqFHFJGC7FMzCtV6Yl7b3LgVdygw +4AUdVF9lhMywBDm8GCul6xmCN02sdNLesIxnDqJW/B8yr8k5xbQrsmblZc0gAKOo3h4Ye4eWored +ANpeYIzohnIjKZKro1SCrNc6n5yTYG2b5gkAWxe8ClCldAKmhDBYbapkEyb3iVy7QyBacVo/aWFQ +3tCpp9+msSHX4QMhrZ9BB0oubOv2liVVNSgUEfgrEI4aQOAbHtHbpgH6zQ94QCijT2mYFqn4xGO0 +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 +22xRFldhyk0tgizQBtdTjKnWCtuWSjQNGpiEKGZNVLYKHoTFMQp6LbaQaAZZKB1ShMYiDp6gNCzj +EVXz0N0JYldKl1AG3eRtCYhwNZattUcS4D8robDtHyC3qDramVeJYTJFKCm0LF5JdlULoiFSmxq5 +UCBE9qSUBsauDyOStFLoGzOkeBY29SSsQsUWoVt0kpUw3LruAqolo6VNNalZ2VDsmT7xFn5CeYBR +VsrKtaa6Df5KarqHTdlCI06bwpg/cUbzg3LQC6w0AjY0tcwzRybaohFQtuNJMBY2omRYxnLBTBQN +oOq18Y9FZLSg2LH7MfTXR9qiuG0yNl31slgKqlChXpArA4yHTp+9RJ0UQovNi9SLM+QgewDmC0DV +9ZU4HPkhIuBEFA+0vlw2qaVedGJQLFWMYR+8xeNYphkekMDkre8RoTZ0Ye0Hi72ot3ZKelXrmMKb +Jk1p+priFaGesQm0ta0z9Y1Zu1tbYsGa8UEWhWKxrBM6qL1emmkHUVK3oepw/TvmnJvKUBCkuKUc +ZVy3HSGIMKgENAr8NV7CoNd/rUCCzR6/5W16ioF9vTiVQE+8BAAaB+eIBEsZVAvtWnwwrrXGP1MQ +2FB92PAgNTUejCeDlYU0w9JdkzDqDLv7ehQGwbzKNEGws0VrrKNWRGRRpQV29CzoCF0vWASGu6bt +W1tD/wBApKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95 +TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc9 +5TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc +95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvK +c95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95Tnv +Kc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95TnvKc95Tn +vKc95TnvKc95TnvKc95TnvKc95Tnv/tAIxdL6yja9mz9fzpoqFV4IWLQR1O1K1euClVOhUzhUdAL +oLWH3iitQKlLaWhR8xg2gsKHq0NRES3omi6tDREhwOPwN8zHkN1FUusFR3gLWmi6NKv5htjWrLYL +joeGkul6y3AsAQfDtEUfCNosapZk9phWjac4v9IBsIpTZNQpSw3YRFTYahSmGLiKU2TUKUw/kkWm +w6FKWDcsJTYahSl/PrLI1ekeKqF6Sw1lkfFnAovPNaQRaHSCJZn0R9TQ94KCudUclfEptRwcb1Wk +TUKfh0+34Q2G80bE0qfrXoPY/wCwOuKlYbXQx1jUhLgwzezpDAsikfpphSq9+Y1wWoylqbLKekxE +AuOIMO6L9sXEQyWjjP3/AABxtGL3V/aCrVxNxYDAw62yHa6EaBYc3pb7RWsYIA3kxXWMvQh1NBva +/aBCi+W0VaxrpxKTEBii7D0DPtHoWI9EMdoICpqBjazVn63Ax7Q8rSHlhBroIcS6MTbi3CljlGMR +G0DW1+0soBSpQ3wzk5l5hgAxDk6r0iHAUAwCWdVmXoVgb0QzwRe5lUAu1HJxXWU+y9UC21MnFfnw +hBcNPdjgE5e8RxgJYbq5O8tWBNuSixSX+8GgwInUC0LWYouE6gt1Rq29THhoALDKAv8A30+JZoQA +HgP63CHqo97F1qsvByBJV1q/hG3kSDXMKKslPf62qFoYJZg0DNXeBTzBbXTKEsRYg1aZ+kClbv6k +cNQJELQoChiWUFA9T2wtcAp/eOI6jiyY70HudX/YwhCoaZa0t+pkViUxVRAV3hdjfviXqlqkUYaw +a5jWh1L5R+TCmLo4/wDJ6d5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvK +d5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd +5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5 +TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5T +vKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd5TvKd/9oAtAczQZ9n86w69A +BrKqWvRVwF2xpA3xsZFYa1lO59SV3gYJ6JK7wMAm5r7RvWCmnOixAhmwz7Sgzulgua5ImcOoS+0E +IsaFlxBATQWXc6iYXXWJBgDZOrtE7EtLriAGAuwYrGEf7g4KEEFTWnWAukkF4UW3ftMpWMgHqyRo +iNjVG3p6HQMr8Ri0Q1BrxrCFxdMLdvzleiHlitEvD6sAkqLdXY+gn6UsVf6EzJmhsir3Dc/CUdqv +Q1hl7HR+tw9BBXiNVjeeIK9AacN37S3F1h+l+lBBADRz6IjqUTGhs11mWtbq6RPwDENBn2BLpKpC +A5PViBfrIlY2O0EwQCDrYo6CXUb0bR6qZroJpGBEUKwKl8CxIAhMmpg7S7uOZJZwjI7xGAjLmqyM +6/eETYqCU10KzUuVUFXd71M3+sWSgwOCjsEKOTMLT+wygFhdrNe/vBcW20cJlpoyU5zWC+sCAGoc +AMA0b7wUaWDGM1T95k6gHKtsNjAI5J0CjhGpmEVaJTjTWDY8LFA7VUfU0VVGl0pPaOyeVzYp7TQ9 +vznR9n7zyOYkVfTK6aQYEoNAQW6dPtHWEQvUXVBSEBZSy0G+qMg0i5c8FVprOUP+ehUVsv0/TQOZ +0G/wgdfBfMABWr9/rso5Lds6/EebNo6P/INJ00a8V9ozX4U70Z+kkYgtemxBAihFtVY+ID9u5aY+ +0pKXWkJhQDT2gTqHjgafgkHDKNJQ4fVB1lDr6UTN9BW3XLf7zW5vd7vpQQAZ0A6HoAaelF3+foWC +8kTsD2IP2IF7afUj0WMOHcI3Nl00m5tT8bRpKNPrQutq+f8AyjP/AMVgAAAAAAAAAAAAAAAAAAAA +AAUNxsX9I0KoW7VjXMAhol/4q2k6OxLePXkoPTLEuGxv9oRFDRZT9N4ylAF6avt6dbYr4L+lAVbR +dMY5/O1LobZdUmgaA3XrAabKaX7jrAQPRjR5Pwg9sPeCJZ9agWwcoibwehK3ggtWcfSBZRz6oEF1 +0iF2mNYOoF4fwPiN2WXLKlbSxj3lVNAWJz1MP6isxEQ1dFhfMCvIBbhLHAsKiQMRbOoGBLSxthRz +rxLYbVhTIc4joWsqm9UtzNlnLV3csuOxbUGNVtBO61VlwVAlVmG82yz8VFQWKCjVCywJpQq17m5F +zd4CzA6DX51into1OuYqQuoLXV16QulY2KOgtj0zp1WCwxCXW0FXNpeYq4SBdWuEZm6jbkWHzWZM +WTTVoYLiQzDVsrqa1KDSxFMVdK6ShgQATIl0zRNwLarzrxBQk0rFubcRAqttxAPOIoNXzTSrt+Yg +7gKpsWqqNVJpN21ZdPb8wTStonJtKicAZt01lG+iARWwVKtVO1V1OaUlupuFYw1hFslcjgiUOdwX +0scRi6+ddMQ2Llxaq9nPxAasYaz4Dxq7xGJeGjN/On4QM6ZNdLl83P16puh94hnFqjA8TVgLcdNI +QPrGm9Z+nCBGgbR4pC7mIHXVFmqEFp6rhl5LNc9IMQLxTY/n8COECvFYzrLFUvse0aMHUMyk5OjP +0csQOMGwrrvFlIxrDU5gd0Czo6HSB4D2tPaUOrXdGs6Fyuqxd3+sTLC2E67y3ItUhoXeeZZtUBXm +w6R+JqFaxWouqQBEwUY6be0DAYFmkDwBpw09oqkKVpWrvOnFir61FRVNLJkoWKMaHEqkGzmCgzaN +L2hisK2zV3miDTQOmSZFFil3InoGEaVcDRBhRjQ2jRw9QlJqssKwJAqsm7rN/mC9NsVs0sT2MFCX +VbREiNrX3lPUpvAanWOhHBoM3BUJdaK9MPtMOz0ildl4DnprcDLXQLGZ82p12PwhVEfeABRgPrAU +WMBRFOpKsThBaaOPp6Bl6w+gxalawpUCtPQBUNdYAooP9qqQGyXFLZ4A/ObdpbtLdpbtLdpbtLdp +btLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpb +tLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbt +LdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtL +dpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLdpbtLd +pbtLdpbtLdpbtLdpbtLdv/IxbYW1n1RJQarNcnrn1GmpOslpdcf4yVADTW/+jKBwBCNCr0uBbDgk +0KJqbfnPjN5fsEAvS2rjSpqiFifsxL9SxeWnaLVqwANUhgwkUGV9+kTq1dvzDYkbb92GOi3F8OMw +QMDcLO4mKiRm6aoZ4rpANKrWGELshfnKPdhY5F1AAYB6bsVhAOocaPETYqWJBvbEp4EKQC+u6CVc +0pqpd56RKNBqb7woLJoXTj/sHjR3Quq6m8KCWDXf88qSoqNxXiVogEusMfnAWtTERxsBU4bL6kwO +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 +IFBwIjBCoSNAQWGQkbHw/9oACAECAQE/ANHR1bZGNc0yF8LKFFLMAINpEwAEWN49sYItphwaW5/Z +W0syAopZwFtDGCmS0ebPVtkqNa03FE0j6vdB1ICJTXUmmQ5Go3j3TgnGi5wcTiiaJ+vug6kBFy2i +ow0y1p7CgSiQBJTdqpuNoO8kBAz8wOX/AH86HXvt8GaoX2+PPkDYEBbbPDwUQz9KnNouz3WmbkAb +p0MROBTaFNpkDVR6LWc4NlmaolxbLs+aPNk3JpfOPcyq8tbIVJ5c2To+OyNR9olU33Cd7jAlZiVT +MjHRXCCAgQOyD3WiUx1wnRRAOaAjLsq50CU10id7RJhVKbvSoIwOhSAc0BGW8GMVxRCJkz2QJhAz +ooiUBHZqUNCkIDkhD8OX/8QALhEAAgIBAgMFBwUAAAAAAAAAAQIAEQMSIQQTYBBQYXChFDFBQoCR +4SAwQHGQ/9oACAEDAQE/APITj8jKw0moefQOo/eas4NEmM+ZRZY1/cGRvZdV7/mK+ZlLhjQ8Zqz3 +QJgbOSRZ+84TJkOUKxPQfHYndgVFxRnHy+kZc5OrTHTOw0lfSDE/s2it/wAxBnVSgXY+EUcQPl9I +y5ySQtThcWQZQzjyF1CEgCzF4nGxoHtAJhFfyD3SO1vdtBjIWhOL1cuVSRL0i/fAYGFVGIqh0LUI +vYxcCKbA7VYqbHU3x7nH6r/doaYQlbeVQ7n+HnAos1PCZBR26K5phN7n6dWNC5w+RSCWmoHdeiSL +2nKNUDEUKKH07UIRXRVwm/8AHL//2QplbmRzdHJlYW0KZW5kb2JqCjMgMCBvYmoKPDwvVHlwZSAv +WE9iamVjdAovU3VidHlwZSAvSW1hZ2UKL1dpZHRoIDM3NQovSGVpZ2h0IDUxMgovQ29sb3JTcGFj +ZSAvRGV2aWNlUkdCCi9CaXRzUGVyQ29tcG9uZW50IDgKL0ZpbHRlciAvRENURGVjb2RlCi9Db2xv +clRyYW5zZm9ybSAwCi9MZW5ndGggMTQ0MzM+PiBzdHJlYW0K/9j/4AAQSkZJRgABAQAASABIAAD/ +2wCEAAsLCwsLCxQLCxQcFBQUHCYcHBwcJjAmJiYmJjA5MDAwMDAwOTk5OTk5OTlFRUVFRUVQUFBQ +UFpaWlpaWlpaWloBDg8PFxUXJxUVJ15ANEBeXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5eXl5e +Xl5eXl5eXl5eXl5eXl5eXl5eXv/CABEIAgABdwMBIgACEQEDEQH/xAAcAAEAAgMBAQEAAAAAAAAA +AAAAAQMCBAUGBwj/2gAIAQEAAAAA+tyAAAAAAAIkAAACNYCdkAETra3S142RrZXgNbZGj8fAy+2R +yc+ow5je2ET8Q8r973PmG93Od6jzHX850OR2eN7Dync8L77e9O0fj/0by9XZ42z5/wC2ac6vTyw5 +je2ETyvPZe08/wCd6er7LzmHL9ByOxx9vR73E5f0DcaPx/saVu9xcq/tkcqrobeHMb2wiQAAAKvM +AT6sRIGAAAAAAAAnGIAAAAAAAnLKXmuR7PZef1elMbujM7eltef7DDW2c79erY0drDC+c+oDzXc2 +YnznwDT999weW51exNOWvV6vzfpvJRd3cMeV1tHH0nlItqvw73eBxuyifhvNv4f6D61dkYWNebaN +jm7rT6XM6VeeVNwrsABE/CPfdD5x9S73krJ4V3rfHX9GnqV+cjH08aPM3fUfNuzs6fT4/rOyAET8 +u99pcP2G5zL8aNbpaOHSja89vuf6DgdXUr32vRXu6fS3gAiaOV5b314AAAAAARIHE2FaenmAAADR +W5YV7yJA85tcu3Xx9RtAAAA1am/SuRMAAAAAAASgAAAAAAATE8Wzo2S4E31bdGtOzX2+MV1Z24z2 ++VVhnFFt23uA836CyOZ4uO56rKfNaNkV8nvc3pcz2PmKNxr2xHM9xqcXVxudLkdv0oPL+g2I8b8s +o9V9E3+gpXK7Ecnq5AOP1snP2Ng5+xsAEfP/AJ96Hp0en9dz8fF9C3r9oAAAAAjU+L87q+k9j2zm +a+fRvAAAAAInV8xq+i7IAAAAAARIHHvVp6OYAAANJbONe6iQODs86yjH0ewAAADWrbtS1EwAAAAA +ABKJgAAAAAACUTVpbTYcO3e1JmMKbcKd2Io2Jt3gAA8/2NiI+b7frPnH0/b4fHuvVV5y7Hndujoc +/Yx7PTAADgdfYVfMNH3HivqO8ABTVtgAAAK/EcbueV+mdbGZRIAAADHHMrtFOry+J7fY89tatVce +i2QAAAa+FO7VOwGEZZKcq7UWAAAAISCAAAAAAACSvl8zsdNqzsojKJVWgAAAam1KvymPFz9V3uXj +RVdRdfqdPk2WrNVh6LIAAOH1rnG41nNtn3VSmrZqi7X2dYZZVt8AAGrsy4fKupx2vUgAAAAABp+a +3Lp2erpWMU7GQAAAK8YsjG4aOhOz0M9GzXnCOjYAAADDGLYjMISAAAAAAAIkAAAAAABp8Gv0G5lq +YXYXK81WFsxbUm3T29bPHYqicdsGhh0MlPP16OvG/pq8N7n217ldWOaGOHV1q8Mpu1S3dDDUnZsY +aqra0esDDMAAEJAAa9dmGet0YkAAAAEJMchqZzjZbqZ4YxG5mAAADCMbcZyEV4W5sZiSQAAAAAAA +AAAAAAwZSgkAARIAAwzFUozyqrzRZXmxhlDJgxsiLJmQBVnkxxyxLIMZmJghLGZiUxMgAxyMYyiG +cESiUTOMkSRlAkAAxmEzihMwmIyiJiYknHKJkAAQSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/xAAZAQEBAQEBAQAAAAAAAAAAAAAAAQIEBQP/2gAI +AQIQAAAAAAUIBYAHpffV4/PAAA7fvN8nIAAAAAAAABUAAAFQAAADi7QAAAAAAAAAAAAAAAAAAA5O +sAAAAAAB2cYAAAel4/1oAAAxvn6AAACwAAAZ0AAACXGwAAAxsAAAAAARUUAAEUM6AAAAAAAYm6AA +AmV0AAARQAAAAAAAAAAAAAAAAAAD/8QAGgEBAQEBAQEBAAAAAAAAAAAAAAECBAMFBv/aAAgBAxAA +AAAABIWgSgB8jmzn6H1AAAPn8uvPu7gAAAAAAAAJNAAACKAAABKAAAAAAAAAAAAAAAAAAAEoAAAA +AADm7/EAAAcn6D5uAAAB7+MAAAJQAABPHfoAAATM3QAABKAAAAAADTK3IAANMgAAAAAAAFgAAAAA +AAAAAAAAAAAAAAAAAAAAAB//xAA6EAABBAECBAQCCQQBBAMAAAABAAIDERIhMQQTQVEQFCAiMmEw +M0BCUFJxgZEjJGChQwVicMGQorH/2gAIAQEAAT8C/wAKzZ3CzZ3CzZ3CzZ3CzZ3CzZ3CzZ3CzZ3H +07ntYLeaTJon6McD4Oe1gt5r9U57GC3ED0uexgt5pNc14tpv6EPY4W036ZPq3fp9APHN7vhQebp/ +idBa5nyXM+SacvTx2XP937IXft3Ucwe8xdWqWMTRmM9VDnM8Ry7Qbrn8U+I8S0gNH3U3iHPnjaPh +cy07iZQ2c/kOi4czObnLWuyxE3HObJqGDQLlxcMXzM7fCufxTIxxLiC0/dTpeIdO+OIgAC15rieU +3iNKuqT5OIdO+KIgYi15rieU3iNKuqTJXni3xHYBeam8tzOudJsk7OJEMpByFp5c7gr0AyXMli4h +schtrh/tcLI+Vhkf1On6eMn1bv08IGERfqmNxc9p7FMa3HNwvouTGHBps2o9n/opGMAdj90ob+En +wpnwqXbxO2iuTsrk7Jt17vSYYnOycLKdw7x9Ri3/APVwvDugcS7W+vhFBy5JH3ea8k6jG2Soz0Un +C25r4nYFgpDgv6cjM/rOqaMWhvZTcMXyc6J2DlHwoGTpTm5+hK8k6hG+S4x0QgqZ0t/EKpeT/txB +lsbtCCpnTX8QpeT/ALcQZbG7T+GcZudG/G9CvJf0eTl97JOhynbPfwjZeS/tzBlubtcaCY2RE3J0 +UbBGwMHTxk+rd+ngJ5WigVm7LK9VzZN7TJy0KyNlk4/uhv44OHwlFjt+qY7LwN1oql7qpe6blXu+ +2uGQpeUZ3K8ozuV5RncryjO5XlGdyvKM7leUZ3K8ozufprCsKwrCsKwrCsKwrCsKwrCsKwrCsKwr +CsKwrCsKwrCsKwrCsKwrCsKwrCsKwrCsKwrCsKwrCsKx4UqCoKgqCoKgqCoKgqCoKgqCoKgqCoKg +qCoKgqCoKgqCoKgqCoKgqCoKgqCoKgqCoKgqCoKgqCoKgq9D5wJWN6G0ziv7d8vYlBwJrxkfJzRH +HW16rnPbkySgQLvonTxsNOKfPEw04p08TdCUHtJxB6WjxEQFk7rnxZY2nTxNdiSnuxYXJ08bTTip +HYRl46Bc2VuJdicuydPE12JKdPEw4uKbMHSuj7KR8nN5bK2vVZmNtzEfsufFjnegTJWP+FGeIAG9 +9kOJhJoHdTScqMvTXTBwunNPboufEXY2mS2SHfmoKWdsYPUgrmtGRcdAhPEQTe26ZNG84tPrfmzX +JNvH3b+iWflyMZ+ZPlc4AdiT/KD3BhZ0K4XisSc+uPjJk2cPxJGPRPbJLnJjXtoBSBwc7EOB+WoK +GcZfbMsuyGURdbLy2pMa+AttpPsrRRMfceQ2yTg8OOAcDe27Six4zYQ45HptqpWnkFg7KQODnYhw +J7agqQOMBHWlgCAI4y13fZFjxmwhxs9NtVI1zS7AOB+WoKjDhM7IbgJ8QfxALxYxUkfLLHMbo07B +Oa+TJ+O5b/pOyZPniSC2tFCx45WQ2Dlg/lEV9+/9qbPD2C/kgwGRphYWd+ip5ibw+JsHfosXtOdH +SQlOEjhI7E6kJ7Xkl4b94GlIHS5vDT8NItPPYRsAfWQTMOw9P/UHXLXUKLgnSCyaTP8Ap8xPu0U8 +BgdqoH8yIP7+Fi66+IIOo8Q5rvhNrJpNAq+izZ3HhzosschaybeN6rJt43qudFlhkL8OdEXY5C/D +IVaBvUeGQvHr6QQdvp+No8WmsoNtxBPZOcJBiVxrA1jSDuuFry7K7eEz3Mc7Hoy00ytla1zryClB +58evdNMjIuYHaZbfunTSkuLb0NAVon++L3e2wqv+pC3FrGnXusGsZC5u9j/axx4sGzq0qaON7xEG +izqT8lxHt4d2PZGOEQUfhpH28TzRsKH7FN93Ec09cv4CxLIcXtDmd2riHf27nN7J0cIgo/DSykc9 +9O9rR/6Qyza+/wDjTHSS0wOxpoP8pl4DPdcwczzGvxf/AFUk0rcmD4gf9bozOIc4OoWAFzpA2TU+ +2qtf1i/lZ9LJTTK2PT8xypQOzZvf03GwtdKHErBuiYWuONg0uMjidGAVG3FgaOg8HxNfZPUUsAXB +35U6MPIcfurksw5fTdO4djid9dwnsD2YHZNhDT8Tj+pTeHYwg66bDssBnn12WAz5nU6Ii9CvKs2s +12vRGFjsr+9uhCwY/wDaKXlWVjZx7WqFUvLM2s12vRcttuP5lyGab6CkeHZpVihWi5beXyxoNkY2 +mPldKpclmWfWqXl4+WIx01Xlma2T7t0+JrzlqCOy8swABtikyMRih9M5ocKcpojJHg00mcBg4Paa +pYtOp6fiT5cXVSjkzXOjywvVc+Puucze9KXOjq7QIIsfacvdRQdosgr9thWcqXuuvo5I3OdYUTC2 +7WEjnOH3ckYX4taDtaHDuwrTal5d+93/ACmNwYG9vtOO6DSFhp4VrarW/wDI3S0aCDvTI9/M5UW+ +5JTXSNJE2w1yTZ436BN4iJxoHdc1mIfeh2XPiyxv5J/ExtyA1IQmHxE6Y2mzxPNAocRE40Dv4GeJ +rsSUeIiacSU7iImHEnZeYjdeB1q1FxDH4jqUJ2BoLzv2XNa7EsO5pDiYSaB+SdxETTRO3g+aOM4u +3TeIiecWndCdoYC879l5iHHO9NkeIiG/6ozjmYD8t2ufG0DI6kXojPEGh177JkjJBbNfXM87N6Ib +eMrsWq15mNij4mNzsR6H5Mm5oGQIo0pOdM11ChWgPVHKV7TiRiDusHcqEVsRaZE8TYn4GnIfuhE/ +HlEO3/ZYOwmFfETSLXuafafq6U0bnFtflIWRPJbjVFMfmD0o0hmwOiwyt37KntjfDiSXE69NVy3A +Sit21/pOY72abMKbm8Rx4EY1f7KJjgY7G2SDH57f8l/6XLd5cNrXL/2nkshfHj1OvRB/vwrYLE88 +u/7U1j+XCK2OqYHRFji29CK67qMOfRr/AJCVKJC54p2u1INf7fafq8VCxweCR9wBRh0Ra4tvQiuu +64bVrjt7j63xOANG00U2j48U6iFJIXGgmwkqKEMN3qmv7rLxLgND1RcAaPXxBDhY9DYImG2hbbfQ +8iIuyr0PiZJ8XRNa1gxb6HxMk+LomtawYt2+j48HEO/ZMCCCzcH6pps14F9OxxP6qWPmMx/hZvwP +Eu+L4Qg+YEgWdPvCtVE9xdg4nbZwXDioGj5fgb2CRpY7qnxvhOLlTj7gmlzU0qKOvcfRymYcvouQ +zXK3WK1TYWtN2T+qa3FuP4I9jXinBeUA+EryZP3lHAyP5/iTpC11Jj81zWZYrnRrmt36LnMq0CCL +H2m/dSDlkF0sKzlS9119G+NxdYUbS3dYPc5w6ZIxOxAB2XIdjWi5Dt7TG4tDe32nHdYkLHTwrW1W +t/5GTSzBNDwB8ZHuzDAQPmU17m/W/wAovaDR/VCZhQmY5NlY7ZCVjtFzmFQy5gZblOk2x/NSne6N +tt7qabFgczquYBeR2XOj1+S50dWnTtABHUrmAAlx60udHjl2TZGv0H2K3Os3smm236HvIKilblqn +PDW5Iu95eNE3YeEnxe5uTVg7F2IoWKCc18hdpXtT8pW0G1p1T8pWkBtadUGlx2Og6prXaBoNdimB +2Qq6+aaHMEZrYINfoSPv2pQTjXdcp9OHYU1Oa6yfmE63udp91Pytu9fJBrw3Y6PtYuBzrZxRa5xL +q3IVHnZfL7FTm2K3TRTa8Spd0FL9R/Hg34R9M5ocKcmsaz4ftZT4XnVYqRx5VeEbrA/ArHosepzQ +7dOjsUjw56apooDwf8Si6ovOR167LJ1DXdZuIu+i5jtrTCS0E/aSj8l0XRfeW59dejEFAAbINANq +gqCobf4mSBujL2Q4g9Qmysdp4l7BoSg9p2Po39Ngb/ai8D0E0LRJcbPgFShkv2nwm+rKkHw13Vlu +QtZOGW+3Ve4HU3YUbnHTagmF2QDin/WNVuw5l/spCQNOqdbXHW/anF1/Je55NGqXudetUmPLj+yB +e7EXvaycDqev7LJ5si/seuqbqPGU9PEDXwbkHX4EBwooi0WNK5bViCsG6fJCMBFoOvZctqIDhRXL +b/KMYKMbSjG0oxtKDGiq6LltRjafsZYD6JV18CFR6fhbxYTm2g2vFo18C6imm1mLpZhZhZj7UVsr +XRdVr6S3qPENtVXgWklNFKiSe1rA1SwNUuWUBQr7VXdV/kxd2TiSgSED4knKgg7fLosgsxSvS1YW +QVirWQukbyoKzqCsgEXALIKwsgsgsgj8kMiUTSL1ki8K9LWQKy0WQWW1euzkr139B8KRHhdeBFu1 +RbpotTrSoobIDcdlqaC1rGkOqN5WqJsogrUXotReiALf4QB0VHoqO3gBuiFR/wBog2VWhX3ULNIA +iiv2/hC9L9R1WJtaobeivAeD/XVfgZ38a8Dr+HjwKCvwtWht+CUqVf8AkqlXqr/4Gv/EACwQAAIC +AQQBAwMEAwEBAAAAAAERACExEEFRYXEgQIEwkaFQscHxYNHhgPD/2gAIAQEAAT8hH+EkgBmf3U/u +p/dT+6n91P7qf3U/svrqgDuEWDjREA8kogcO5K9KkR7KiZg5Bf0VKhhg+n8x9DIaEoMxitQBJakz +KPzj84k0vTb2IfCV72pcw4MQf8zblfmCPf3h2gKYFJsO5RQ1HdwBSGMV7jC0ggNhF9QsxajILMhi +oS/BabHuDTjBjqJOL2JnuDugLDqJOL2JnuEWrEfiY8jq7QcpICCUOqgwQAnCIsjaVJi6Iur8xorE +A5wuVCQRlWCYipMm+FBCP/txNSCAZy5g0Z1FVRU5gxejAt2uxHQj6Tp+Cu4eNc7r91xJiKDTcktL +Gf8AcIyYhlb+8LRQadQgIYkCw4L5m/iAfaABIQmmCI1kDYV1AckJYW/vOF1RiF/Fh/DnhbFiF/Fh +/DhsUYYHAo7Ni/GZhQwU57iMdG/hwoB9gE+ZgOFa/mNBioJeEgW8WbXENM5b/eBxk8wsMncZg0zR +jssuXCV5GljdrIAWM+9CU9wvoZmZmYv6oxzOydk7J2TsnZOydk7J2TsnZOydk7J2TsnZOydk7J2T +snZOydk7J2TsnZOydk7J2TsnZOydk7J2TsnZOydk7IxEnVOqdU6p1TqnVOqdU6p1TqnVOqdU6p1T +qnVOqdU6p1TqnVOqdU6p1TqnVOqdU6p1TqnVOqdU6p1TqnVOqdUAD0MWigfI/wCwDRsb7modDkAH +760aM26TZQ2zD95kkGaNPmZFEyg0O4d4DoNDkqW8H8EF6rKjahEyzWCnw4VWxngeTFIQEGziZdBm +ig+Ye2khEL+AQCZ3Ci2M8DyZmIGaKHmCbtFwMTjveUG8YheAh0kRexhNGyn8QoaPgbUBuCwc8QwA +MhZxcdPOmsBQ3Jqnw8QdBL9yh2FACPMqLXy7G8oK5gQR8S3op429Yi5TQUEQBP0HfQIs+BC4OAMA +CbQT8Q5pZB9q/bUw9sFHvAhiy7J3h4MQFwG4MtkQgjZhIxfJQUsYShq8iLbgxSjJ0zAicP2GbxLw +IICMOR2hQ1moEPAiC4HcGLfdUOVCLskXFOZeJZARhyO0bQQFwe7hOJvxhjMQhd2G4BhGE9gzDgyA +EAOUVmGUAIDsDChFFjpmZreLrdLCD3Lcby0FXIRcKBSQj6C24bhBAMohOJfNgbkBQpwgcsQBD4IK +2ESW8RQqIn7esuEUekJN6F4InwWITisb8wMBMcwhbZpvODWjGIFaxqeIAmUYCAhIyHEdrhFR+9G7 +EB09RxiwS1vNjblvPwgegWeo4SAGYCoISfxAACMHTdxRrf02ptV9dECEaBi9eACL/sMmgC6KxCFC +MASz+biAYaDnSlW7iFUzCSI4hpwn/ExaDFaRSE3BwDkuTABQSWHiEOmAWFRI1kblLmYG4OLGJsmi +rT/cbEpAouYAs/5hAhsl4IdkgIA+AQpyw3WuYQGfKImALMfvNgCICy4d3FIKEkQoQBZ/hDeIFLIx +AojGxRWzMelsj4xtFm35XJhDgwCIUblGoikG5xFgzvhZ+BCsNZIaR+Rz9Yn4ML5cp4ARI7IoRFFl +HJxiAxwA0Ls5fiE55ZfMNYQSYU5K37nCgwBsBowuzgqjR4DBBVJbw14QlDaB+6AZ7D7BAAbAYAoi +jvh1FA+EogyB4M/0GoTNFJKAaIo4OH5B+ylhghz9u4RYWySJHcRsnCNgfxIBjkGfiIZAGhBsGbso +mOYEuETJFQFQQiQQbvMNWllknJP1kYYzCW9onqAfQnufiEwAeQ/UimFoRmEoQMyLBye40MNaoHM4 +CojLsrEIw3HsXWagEZg49yCfBiFR2YDfZwkYchhQkQYX9MtwTs2YvBAbzSxEoWIiwC/EBAyaDcqL +mFRBhE0B7zOoS9za28RkJwkQDvf50w9ILS/VEIhEIhEIhEIhEIhEIhEIhEIhEIhEIhEIhEIhEIhE +IhEIhEIhEIhEIhEIhEIhEIhEPSP8nHZgL9BYKCcAIvAioofMNMLTsEMdQOa2UbjxLK8jO5Oyp8PE +EkJmkdofSFkIuZjk7CY6g7cJCjfjTOICiVQPZhjgUSih5MMbWVEqAAGAQUbUDkNfBRO6gXJLpDaM +AoBZQP2gBmHYU+HC7vqK0aTBoAmoCsHCij8wGwl0htdZnmK15mZHDUaB54nxk2ii4Cg3LBBtBk/E +c5GvWHatxlwPWuMyaiDMJSJZ6gMWzAdRGPQWQ7gNVbcE3CHRWEVkJCAeQ8RUQCwTvZ8XBBOnti23 +Bvx5CoeAB1Hl4gBFAFHhiPFNSzjG0dEWLdS6JEIJyBO8NJwAy5EzIhAdpIWVKgfKEAmJMiOPDzC0 +YZOmaloyf4boAvUAqTWIsi8j+8cTqC/MwSqv5gxBN3SjDxrQMMmoQpowcVMPAqwDW5jm4PwPcQqX +zOIW9gmGTUMkhar1tKYtQwCM6oXU2NTJ1Eh5IYjlBrJggBw7gIQA4DnUYOwdxqQCEZmcGLJXiAAE +C+iXm5LyU/GIgCwLOqgjeBBR/EGwoD0KiN4EFH8QZCg+mtfKDAhCFAoJdReG+i917BUEbA5LgiYQ +2eVaJ+8zRsg4CXVjtBfS2hjX6GxUQY/AeYpkUzkAby9p3Y7ei+YZs/e4M5kBOo8yQEGaEEIGVz+i +WfCfxowl2A+Jibcn9SOBEOzCUI2m2sHPmODBJtUDmc7AEZdlQhBjbpF1moBWA+5BsWIRDcwE+zhJ +MngISANn6ZlB/wApjIGB7pYibLFi0X4gA8kgB1RcxoJDCJKAPeZ0YXubW3gSRuGoPVCwv1av00OV +QZ20CcFrUJmQ4ADSiAA3cIDIgN4ju0g7CrmOkUg7CrmFkWzsKoeQ7MMJiDGeGKN+JYmUgLiWjZtg +SNkpCXg2HiWYEn5gJlgywZUAksoIG5YOEYM24EIfiWCKBIvaGjlGxC9kUtNgiot9TiC+/mITWO+x +1DFixcMkx3A0dMDsGQZcHaFzWYZgYFA97cAzunLgQO8pk+BBUvZnVjEdoIvDjAMHCIgCxiPBhYmW +AgZuHMR+1CqmhJ8Qt4o3YzKxwUDlQx4MEqAc5gigJF73BFuoZKht0tLdHeNXARBygczaKq/n2RQ2 +hUXoYPEymHR+IPrI0xHyZ94XAIEMMDZUuIy2GjEYOPfEgBmA1DQkAOMYnf6sMcDZqCyUjxpn0Ady +BA8pdE2Z4FDuASIMhfzGqjN1t+JkII9zmCcQ2RuErdtAAUiA+E/D6loQGpKZEwhQs3kuEgRE2FK6 +Be7QGIh9C5cuXLly5cuXLly5cuXLly5cuXLly5cuXLly5cuXLly5cuXLly/SBcBGEOwQ6hR41KpA +zBB1xAQDGNGManOKn7okoCCGNRGLQIcGAcR4TnjQAyYFTBecUiruVjow3QrCxOCB0I57jQGcgj9o +Ba+ZW5vBQJFSQDgeJDg5EkgMEBj5jI8JATlxKgq9xMCATIfELUw2U0h2iGgBVQWH7IC8GEk61CHa +hFQjOLCzADEabYjAo9rjV7r8TMbsI3CQCdp9oXxDYNlYZxDILOEu3Ta2m2CZmyiLMJE2HlHMKOw8 +reEHYdFQgDYQVcTyIXzLt02nUKE3eRt7Mk4AghqNjxDUgg4jC4fKg7/SvhoUgoIUIoozw0OiFlP2 +JwvjE3NlKXAQQx7kjgSyuYYl4RmkyT9LzAgg3oTKAAQ0cBCs4sYJoUAOHBtGFMBcUcPckOMYZKCK +3Fb9dfp9fptigrMBCZgL0VVVCBPkgO9lzCwRkCB3zF3AcrmeIgInMLaqcXuQG4ahhZQjgIUPMIwD +zL1CMqMoMwMUYDKAojDRiEVVwAFWRA0AchvAAAm3xK2N4fAJ9YIHBhsaQY51NCKKNt5xQCpYO9Eh +BhQQpwXCCngVwWgNLeY3Dn4AmwJReYgkFtzEAAdQtgVICMFAvqX5HByj4hAU8INcYcLKEF/ENyEs +/EGIRvuYkDJ/eCl/dPkAjL5c/jEBSVyhHShBKJSATs9QIpQhyCzASakPUDN6YesACHs8zFfXGhgi +cCXoNB+hP0MelI+gN9MtJJm98RlZzGT6jYcsD9yYetNpvN/V0j8wANUIohnVD/FHHH6GI/Yv2L9S +i0GhouYrmKzG5lQxmDKhzLKl5hJhe0LuWJYMOVP9xmKjLMDMvmDiP6nMGNT604tEIhE4hooookSK +EQC4QzEIkUUUX1l9Vxx6OPRx6OPRxxxxx6OOOD2y+jt7Af8ArX//xAArEAEAAgICAQQABwADAQEA +AAABABEhMVFhQRBxgZEgMEChsdHwUMHx4YD/2gAIAQEAAT8Q0P8AjzR+qQJQZVn/AIj+5/4j+5/4 +j+5/4j+5/wCI/uf+I/uf+I/uf+Y/v800enZqkE6eAc/W/Qs2tABfFtQQpqBBXxbN5PwWjXFkL+Z4 +w/SH2fkuRVsBLPFnn8P+1w+gLkNfi/dHoDJQZZZYx5YfsHT6uAYF0T/Z/wDJ/s/+R1aq1T6mj0VW +/wAThmvm7nXlzcKm++maWjD2WprpoHgafhlNoHnSpb4PmnmJHope2pva/mOWW1BRct4qaKIJgat8 +45l0YD+wvLWVxKUt4mBLWnb+0UazJBbIQ2fxmFZsgSmxW1/P3G8Q1yWGj3XzqYF1PlkinxdOoNDo +3Jg0e6+dTAup8skU+Lp1DAJ7RYonO/MQDXGU0uq/7jdfjMgte2PMD9BP8FOKqvPMwyhIG1/avPmL +SY8AoNHvm/r1/wBrh9Nody4TFwsRGxnjUpNRC+Xn+o0YAA1XwcZeYxHL+EuhEralvHip+4PT6N9X +E+nn38zB8sS9PKs+jYFgwTr/AO+Z1/8AfMfOQ/b1NHobbIMQHgHB8Q+WAowD/jFS0VC0+6I8ub9D +QqHUw0u2/gTD1GKM3Rb/AHDEV2hQp0UpyxFzDkjYsMrfaZz2XV0q5YIJI4BH/wC+0ZTCg2lUDXj6 +Jg8pEXA21/sQi/qoAu7zrgn8rs5Wvk5m4lPSAXd51wT+V2crXycwyLxDYMYvTQeIRuA28SsP5X8T +32hLyzljfEMncDDTjHk1zEMIu3MJrAZJrZN+aMvzv1/2uH0xkWiwcSndDGGFCxqtbx8xlfa5NK8P +MzQpqnkhhdYsdp5Z+4PRAOQYoNQ+GKMUyEs6VgfQoHVMM6H++J0P98RgBZ449TR+qQ5Bkm84n+o/ +qf6j+p/qP6n+o/qf6j+p/qP6n+o/qCBMXt/X4QDRV/kBhY+51Pudb7nW+51vudb7nW+51vudb7nW ++51vudb7nW+51vudb7nW+51vudb7nW+51vudb7nW+51vudb7nW+51vudb7nW+51vudb7nW+51vud +b7nW+51vudb7nW+51vudb7nW+51vudb7nW+51vudb7nU+4PgRl4KS/YnWnWnWnWnWnWnWnWnWnWn +WnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWnWgehELCGj1zN +HnyNB9o+wGD/ABqk+o0NqPtX8eo1LpBdgopOZaDXVazVpwdkxxgLEBooKL7mkkLAJpQNHvAdto7g +mkDQ9woJQQ8tob1KNlLMU6QKvct1KG4C0qUPVyiIoKlR0AUfLGblwKjtrNe0wZgLE8CwUX3K5rBa +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 +w9sWHmWksd7t5r8APWj1jgplbmRzdHJlYW0KZW5kb2JqCjUgMCBvYmoKPDwvVHlwZSAvQ2F0YWxv +ZwovUGFnZXMgNiAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlIC9QYWdlcwovQ291bnQgMQov +S2lkcyBbNyAwIFJdPj4KZW5kb2JqCjcgMCBvYmoKPDwvVHlwZSAvUGFnZQovUmVzb3VyY2VzIDw8 +L1Byb2NTZXRzIFsvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJXQovRXh0R1N0YXRl +IDw8L0cwIDggMCBSPj4KL1hPYmplY3QgPDwvWDAgMiAwIFIKL1gxIDMgMCBSPj4KL0ZvbnQgPDwv +RjAgOSAwIFIKL0YxIDE0IDAgUgovRjIgMTkgMCBSPj4+PgovTWVkaWFCb3ggWzAgMCA2MTMgNzkz +XQovQW5ub3RzIFs8PC9UeXBlIC9Bbm5vdAovU3VidHlwZSAvTGluawovRiA0Ci9Cb3JkZXIgWzAg +MCAwXQovUmVjdCBbMzcuMDA1NzY4IDc0MS41ODM4NiA5Ny4wNDkwMjYgNzU5Ljk5NzEzXQovQSA8 +PC9UeXBlIC9BY3Rpb24KL1MgL1VSSQovVVJJIChodHRwczovL3d3dy5kaXNjb3Vyc2Uub3JnLyk+ +Pj4+IDw8L1R5cGUgL0Fubm90Ci9TdWJ0eXBlIC9MaW5rCi9GIDQKL0JvcmRlciBbMCAwIDBdCi9S +ZWN0IFs0NTEuMzA0MjYgNzQ2LjM4NzMzIDQ3Ny4zMjMgNzU1LjE5MzY2XQovQSA8PC9UeXBlIC9B +Y3Rpb24KL1MgL1VSSQovVVJJIChodHRwczovL3d3dy5kaXNjb3Vyc2Uub3JnL2ZlYXR1cmVzKT4+ +Pj4gPDwvVHlwZSAvQW5ub3QKL1N1YnR5cGUgL0xpbmsKL0YgNAovQm9yZGVyIFswIDAgMF0KL1Jl +Y3QgWzQ4NS4zMjg3NyA3NDYuMzg3MzMgNTIwLjE1Mzg3IDc1NS4xOTM2Nl0KL0EgPDwvVHlwZSAv +QWN0aW9uCi9TIC9VUkkKL1VSSSAoaHR0cDovL21ldGEuZGlzY291cnNlLm9yZy8pPj4+PiA8PC9U +eXBlIC9Bbm5vdAovU3VidHlwZSAvTGluawovRiA0Ci9Cb3JkZXIgWzAgMCAwXQovUmVjdCBbNTI4 +LjE1OTY3IDc0Ni4zODczMyA1NDYuMTcyNjEgNzU1LjE5MzY2XQovQSA8PC9UeXBlIC9BY3Rpb24K +L1MgL1VSSQovVVJJIChodHRwOi8vdHJ5LmRpc2NvdXJzZS5vcmcvKT4+Pj4gPDwvVHlwZSAvQW5u +b3QKL1N1YnR5cGUgL0xpbmsKL0YgNAovQm9yZGVyIFswIDAgMF0KL1JlY3QgWzU1NC4xNzg0MSA3 +NDYuMzg3MzMgNTc2LjE5NDI3IDc1NS4xOTM2Nl0KL0EgPDwvVHlwZSAvQWN0aW9uCi9TIC9VUkkK +L1VSSSAoaHR0cHM6Ly9kaXNjb3Vyc2Uub3JnL2J1eSk+Pj4+IDw8L1R5cGUgL0Fubm90Ci9TdWJ0 +eXBlIC9MaW5rCi9GIDQKL0JvcmRlciBbMCAwIDBdCi9SZWN0IFsyNzEuMTc0NSA1OTMuMDc2ODQg +MzQyLjAyNTU0IDYxMy4wOTEzMV0KL0EgPDwvVHlwZSAvQWN0aW9uCi9TIC9VUkkKL1VSSSAoaHR0 +cHM6Ly9kaXNjb3Vyc2Uub3JnL2J1eSk+Pj4+XQovQ29udGVudHMgNCAwIFIKL1BhcmVudCA2IDAg +Uj4+CmVuZG9iago4IDAgb2JqCjw8L2NhIDEKL0JNIC9Ob3JtYWw+PgplbmRvYmoKOSAwIG9iago8 +PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL09wZW4jMjBTYW5zCi9FbmNv +ZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsxMCAwIFJdCi9Ub1VuaWNvZGUgMTMg +MCBSPj4KZW5kb2JqCjEwIDAgb2JqCjw8L1R5cGUgL0ZvbnQKL0ZvbnREZXNjcmlwdG9yIDExIDAg +UgovQmFzZUZvbnQgL09wZW4jMjBTYW5zCi9TdWJ0eXBlIC9DSURGb250VHlwZTIKL0NJRFRvR0lE +TWFwIC9JZGVudGl0eQovQ0lEU3lzdGVtSW5mbyA8PC9SZWdpc3RyeSAoQWRvYmUpCi9PcmRlcmlu +ZyAoSWRlbnRpdHkpCi9TdXBwbGVtZW50IDA+PgovVyBbMCBbNjAwLjA5NzY2IDAgMCAyNTkuNzY1 +NjNdIDE1IFsyNDUuMTE3MTkgMCAyNjYuMTEzMjhdIDE4IDM1IDU3MS43NzczNCAzNiBbNjMyLjgx +MjUgMCA2MzAuODU5MzggNzI5LjAwMzkxIDU1Ni4xNTIzNCA1MTYuMTEzMjhdIDQ2IFs1MTkuMDQy +OTcgMCAwIDAgNjAyLjA1MDc4IDAgNjE4LjE2NDA2IDU0OC44MjgxMyA1NTMuMjIyNjYgMCAwIDky +NS43ODEyNV0gNjcgWzU1Ni4xNTIzNCA2MTIuNzkyOTcgNDc2LjA3NDIyIDYxMi43OTI5NyA1NjEu +MDM1MTYgMzM4Ljg2NzE5IDU0Ny44NTE1NiA2MTMuNzY5NTNdIDc1IDc4IDI1Mi45Mjk2OSA3OSBb +OTMwLjE3NTc4IDYxMy43Njk1MyA2MDQuMDAzOTEgNjEyLjc5Mjk3IDAgNDA4LjIwMzEzIDQ3Ny4w +NTA3OCAzNTMuMDI3MzQgNjEzLjc2OTUzIDUwMC45NzY1NiA3NzcuODMyMDMgMCA1MDMuOTA2MjUg +NDY3Ljc3MzQ0XSA5NyBbMjU5Ljc2NTYzXSAxOTUgWzUwMCAwIDAgMTY5LjkyMTg4XSAyMTAgWzU5 +MS43OTY4OF1dCi9EVyAwPj4KZW5kb2JqCjExIDAgb2JqCjw8L1R5cGUgL0ZvbnREZXNjcmlwdG9y +Ci9Gb250TmFtZSAvT3BlbiMyMFNhbnMKL0ZsYWdzIDQKL0FzY2VudCAxMDY4Ljg0NzY2Ci9EZXNj +ZW50IDI5Mi45Njg3NQovU3RlbVYgNDUuODk4NDM4Ci9DYXBIZWlnaHQgNzEzLjg2NzE5Ci9JdGFs +aWNBbmdsZSAwCi9Gb250QkJveCBbLTU0OS44MDQ2OSAtMjcwLjk5NjA5IDEyMDQuMTAxNTYgMTA0 +Ny44NTE1Nl0KL0ZvbnRGaWxlMiAxMiAwIFI+PgplbmRvYmoKMTIgMCBvYmoKPDwvRmlsdGVyIC9G +bGF0ZURlY29kZQovTGVuZ3RoIDYzOTQKL0xlbmd0aDEgOTY4OD4+IHN0cmVhbQp4nN06aXhURban +7tJbet+ThuR2LglLAiF0QhK2NEm6kxCUEAjTHRhMIGBAVsMywiCgIhBQBH0MizqCTAYQ5QYxBgYR +RgWjIIMODDKO8lxAfIjLuMyD5Oadut2dBNR53/v7bqX6Vp2qOufU2erUBSAAEAcrgIWB8+6dNq/w +5c27AAy9AIh2Ts3saalDRp4HENuwf3F2zW/mMYsYHbbduEqYNXdqzSdff34AwPhXhB2pm73gNwff +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 +UBSMhR82Oi9mZZuDnDp6CsI3I8kM6sI2X1Vjublq/UMjqYVFQVkySb2t9NLq13YkFnrZtpY2Pizr +1mr+Mj5XTSzxHMONmCTNuhVkWnWhoIjsKllxtqsMSMl/8ZhD1vXiuzU+ndv0KEqi0tMZVHmKY9AJ +lHniCegA2nlKUk9pDDqBUMUKHGW4ge9B0OW44YBY5bwkUcxBOWjv6YgqtjvX1s1/fu/m0X2SwzJu +SVGJwx1HLEMDKb+bZDDiD0/3FH+IBjLIM9TcQZ494xDy3f5mC0bcj3cP5DFVcTXGDtS/Ij9JN8NB +0eOh6Uk7ldu/fwOyFwplbmRzdHJlYW0KZW5kb2JqCjE0IDAgb2JqCjw8L1R5cGUgL0ZvbnQKL1N1 +YnR5cGUgL1R5cGUwCi9CYXNlRm9udCAvT3BlbiMyMFNhbnMKL0VuY29kaW5nIC9JZGVudGl0eS1I +Ci9EZXNjZW5kYW50Rm9udHMgWzE1IDAgUl0KL1RvVW5pY29kZSAxOCAwIFI+PgplbmRvYmoKMTUg +MCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgMTYgMCBSCi9CYXNlRm9udCAvT3Bl +biMyMFNhbnMKL1N1YnR5cGUgL0NJREZvbnRUeXBlMgovQ0lEVG9HSURNYXAgL0lkZW50aXR5Ci9D +SURTeXN0ZW1JbmZvIDw8L1JlZ2lzdHJ5IChBZG9iZSkKL09yZGVyaW5nIChJZGVudGl0eSkKL1N1 +cHBsZW1lbnQgMD4+Ci9XIFswIFs2MDAuMDk3NjYgMCAwIDI1OS43NjU2M10gNDAgWzU2MC4wNTg1 +OV0gNTAgWzYyNy45Mjk2OV0gNjcgWzYwNC4wMDM5MSAwIDUxNC4xNjAxNiAwIDU5MC44MjAzMSAw +IDU2NC45NDE0MV0gNzQgNzggMzA1LjE3NTc4IDc5IFs5ODEuOTMzNTkgNjU3LjIyNjU2IDYxOS4x +NDA2MyA2MzIuODEyNSAwIDQ1NC4xMDE1NiA0OTcuMDcwMzFdXQovRFcgMD4+CmVuZG9iagoxNiAw +IG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL09wZW4jMjBTYW5zCi9GbGFn +cyA0Ci9Bc2NlbnQgMTA2OC44NDc2NgovRGVzY2VudCAyOTIuOTY4NzUKL1N0ZW1WIDY4Ljg0NzY1 +NgovQ2FwSGVpZ2h0IDcxMy44NjcxOQovSXRhbGljQW5nbGUgMAovRm9udEJCb3ggWy02MTkuMTQw +NjMgLTI5Mi45Njg3NSAxMzE4Ljg0NzY2IDEwNjguODQ3NjZdCi9Gb250RmlsZTIgMTcgMCBSPj4K +ZW5kb2JqCjE3IDAgb2JqCjw8L0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCA0MzMyCi9MZW5n +dGgxIDY4NTI+PiBzdHJlYW0KeJztOGt4U9eRc+5LsizZkizLNsL4yteWcfyQbZmHeVmRLfkhA34S +XeO6FtjEEBsMJqFA25ikNFTg0gTS7aa7+dw8HMKy4RqyRLBs82rzTkg2m81maTZu0zTfboDky9ew +WcBXO+dKNiZJ9/F9+3Pv9dwzZ2bOzJyZOXOvDAQAkmEEWCgb2tY39FLStdsATM1I/XJzeLBPumvF +7wGycEqeGwx/Z4jpZ3IQz0SCOLBlfXjx/iUCQMofkXa2f3D7dz45vPJRxBcDsAP9/X3hlELdPpRF +PuThtDc5v+FuxM8iLLx1YOeGc6k1cwHMCoC+ecPQrYN/O/zI7QD8EIBucv0d28W0kjmHUX89yh9d +PxgeOm46+ZcA3CM4fw2o7yTr/G9Xv/Hut1OXfaFP0gO9XvZljNHxnaLX3rlqnDpiGNWPomwSMBC/ +cJ1+dOoIgGH0qlHdaRjVNM2+3tMo70ME7BACHleawQ1ddClJxlgR7nV4lgpyNgCGQiPczr4Ie/ki +KOHuhZBuAVRjYFoZJ+xlTuF4CpZzXVBNeUwrVDOHYDlTj2tWQQrSGhF2IqxKgITQi1CPsCgx+qg8 +XUt1TAP7Fgi6EtjA78Wo9UCUz4Id/AWIcrsRenH+KuwQciDKjMf6+Wak7YWobj9EhT0I3SgrJMZ6 +5PVDN/cQFApGeIL3YAYwS/wa3KOK8BCUM+NwEP014ujhfKBnG2PXuPOkiXsH1vBmGOOyoQvHLu4M +dLFZUIi2BN4HY8xWOMRsjX2Pu6zhY7rzMEbp3B81+TG6hn0IxtgrOO4EN/Ie4EYBhPfAxo2BgeLs +x7CILYAcrp+8iGOrFsdE3BE/iEBpWxEETeZD2IS+ZQhHoJfF3HEXEmsw7pTGQewKO4i+0hjqwY2w +lO4F4zDGL4etNNbkkdh5pHexDqii63VGKE1AJ8Z9uRbzbwDdgzhiHrQcxOE4HTEv7um4fxXQjz0a +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 +bGF0ZURlY29kZQovTGVuZ3RoIDI4OD4+IHN0cmVhbQp4nF2RzW6DMAzH73kKH7tDBQ1QWgkhbWyT +OOxDo3sAmhgWaYQohANvvxB3TJqlRPrF/tuOHVX1Y62Vg+jdjqJBB53S0uI0zlYgXLFXmh04SCXc +jcIthtawyIubZXI41LobWVEARB/eOzm7wO5ejle8Y9GblWiV7mH3WTWem9mYbxxQO4hZWYLEzmd6 +ac1rOyBEQbavpfcrt+y95i/ishgEHvhA3YhR4mRagbbVPbIi9lZC8eytZKjlP/+JVNdOfLU2RCc+ +Oo55XK7ET4HSLFDCA2XkSynyeCDKiBKinIh06ZkoJ3ogOod+bpX5bx9b2+kTFaOaxypos5QeqVjO +bylItP5u3cI2OjFb66cWVhXGtQ5Kady2aUazqtbzAy9dk7UKZW5kc3RyZWFtCmVuZG9iagoxOSAw +IG9iago8PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL0ZvbnRBd2Vzb21l +Ci9FbmNvZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsyMCAwIFJdCi9Ub1VuaWNv +ZGUgMjMgMCBSPj4KZW5kb2JqCjIwIDAgb2JqCjw8L1R5cGUgL0ZvbnQKL0ZvbnREZXNjcmlwdG9y +IDIxIDAgUgovQmFzZUZvbnQgL0ZvbnRBd2Vzb21lCi9TdWJ0eXBlIC9DSURGb250VHlwZTIKL0NJ +RFRvR0lETWFwIC9JZGVudGl0eQovQ0lEU3lzdGVtSW5mbyA8PC9SZWdpc3RyeSAoQWRvYmUpCi9P +cmRlcmluZyAoSWRlbnRpdHkpCi9TdXBwbGVtZW50IDA+PgovVyBbMCBbNTAwXSAxNSBbOTI4LjU3 +MTQxXV0KL0RXIDA+PgplbmRvYmoKMjEgMCBvYmoKPDwvVHlwZSAvRm9udERlc2NyaXB0b3IKL0Zv +bnROYW1lIC9Gb250QXdlc29tZQovRmxhZ3MgNAovQXNjZW50IDg1Ny4xNDI4OAovRGVzY2VudCAx +NDIuODU3MTQ3Ci9TdGVtViAyMDkuMjYzNAovQ2FwSGVpZ2h0IDY5OS43NzY3OQovSXRhbGljQW5n +bGUgMAovRm9udEJCb3ggWy0uNTU4MDM1NzMgLTE0Mi44NTcxNDcgMTI4Ni4yNzIzNCA4NTcuMTQy +ODhdCi9Gb250RmlsZTIgMjIgMCBSPj4KZW5kb2JqCjIyIDAgb2JqCjw8L0ZpbHRlciAvRmxhdGVE +ZWNvZGUKL0xlbmd0aCAyMDgxCi9MZW5ndGgxIDg0MTY+PiBzdHJlYW0KeJztWV1sHFcVPjOzM7Nr +x76TNI5cklpj13ZC5Hid4DoQVdE6ddyXYBtjQqho6o137LXi/dHuJq1DFEZISCChNE994CGK+kQQ +ChZtpD6QygqIl1IrEqUtkFgupRCgCTMDqfoj7XLu7L2zd4ctqQRCgHbsz/fcc849f/fc6x0bJACI +gg0KQDaZsX74tdQAwI6zAJETmeQzeWVA3oEatxHmUm4uCdL6DIDxY4BWI50pPXN4ufObAFIUoMVJ +p60kUVquou5PEb04TXV++509SH8HMbKwtDz/9firLQDbf47zlblMMr9r9yNraOt99HcdaCwR6ZX+ +O88/9xR59F60VQf6vPaZV/fxsVKpQEzSwI9a9lcA/alBBYkYoLwSkxg/eOSrlCP/RH4QdlYZ/iqq +lahqKDbkfcGXULAKQD1/AjxJI9HsGo/SFCqd2/UyxLJqQ5braHwNQq3yPqWxOa7dx3Uaga6LVG0n +qJ7C5r5vu2aXxdCi8jgFnYjg/765slgiUBvFfPV63hlVjF2Qa7hnGMt3EU98XG4NkGB2HqHQa/w2 +1Q5qd0asjVpfd0MN7QXTe8KHLeRRrWVC9xusvt68DoJ/2je1OtT2JMhbHHmsak0+gDV5KqgRz6Xm +j+YtizF/LGzoYHmrob032FhEvsR0IuFeQpC6uG3YrvO6M8SYzQjbS9mGhMzqxnmRUKxI7xL7COsl +8XqJPaML+Qt7lxBrEsjs6v6wvIIY0dYeRBvuwecpMLY9iGDOfUTYeRFy5bWu7+lQvwhnk8sS7Py1 +4XwPWytjjDKvCRtnaZ0QJ3C9SoH6ksZiQf02fj6RVnDsbbTfrC6b+NnnPMmuq/cI56NfheJ+50vY +u2HlGzgqEFH5OQL4rNBjPRTI96FxCPcZYjvtwVAPt4bzYH7H/DOE3xSsnvw8c71gLcYywMbgHtIZ +j+2DqduVininom5Hg7MSnFWqEw3dx/zsx6r6GfGeDs4V1O4H/T34dPg8sTj53eznFRPX1EYjWuOb +fD9i9zvv9djJY2bxm4gHBB9jLJ7PIb1F0BNjPojyzgb3QhgPIlIo/wqLezezE8NxZ4zFHgbqtVOo +dmP70dDI6yvsg09jjK3ivSzug7imjmb7y+YPaIJN1uMy7yvhvpXpGUV9jclkpP1zwc+GsM+BrWgo +hnBPRN+HmRibN6oT720xb02Q6/DPe0Osp1hL/WqdLOhH9Dfvr32pZj/Iv35PtqFeO44PNeoLdo/3 +RRvzp0PxHRZ8mTwWIbfeRnkx2904P4l2dzG+otfbpvQYO/+0HxV+xpn9QF+tQJ/Gfh+poj8biNgj +/HcVk/fF6n11hPrrH3pD4D/E10YaQPhsIYXPRQBZ6HUbhmjNWYzxQMeGfjre51EYdlQ/LUvv4ox+ +SvbwM/gNZOTxs7EKQ2DCZbgCL8LP4M3unp6unlmn3elyDjoTznEn65xzzjsXnSvOj5w1Z92565Td +drfLHXQPuhPucTfrnnPPuxfdK+7L7pq77t51y1671+Ud9Ca8417WO+ed9y56V7yXvTVv3bt7Dz+/ +o2cTvhfyCI7hdDsJZ8qZdfKO7VxwLjkrzqpzw9lwHBdcwzXdITfhTrmzbt613QvuJXfFXXVvuBuu +44FneN1ewpvyZr28Z3sXvEveirfq3fA2qMfK25WvVo5Vvlg5VNm7fvvWnVs/MNqMTUaLETOihm5o +hmpEDMWQyT3yN/JX4hGXOOQv5C65Q94lfyZ/In8kt8kfyO/JO+R35G3yW/IW2SDr5Ba5SX5Dfk1+ +Rd4kb5DXyS/Ja+QXbdfD7yb/iYe+MHG3En33kcMK1Vb4b3g0tUWPbIrGWv/dhptd/X/W1ewZ+Bew +0kQTTTTRRBNNNNFEE/+zkOn/sBRDoy9zOsD+7s3dyubuzRvSajmRly/lVfPDvAYfAn3wDbACmq3a +qNmNcwPfTxDSVu3hnp0S/ujtl40tI71mZNuWjq26tC1ilz8ony9/IOlSQdGPDI/0li+/cPPZ8kfX +crlrkip1Seq13Fnpy30yKkh6VblsDx/pl46erWnkrpU/evbmC+XLfTSKTqk/eHOIAwT/z2vFmcTi +1OEAoxXYDKOMjqDONKNV2AJPMlpDfo7ROnwBzjA6CpvgFUbHoAXeYHQrdMBbjG6HLniP/hUkQv9M +c1zaymgJtkkFRsvQLn2L0Qr0SM8xOoI6LzFahV5pjdEa8j1G6/C8HGV0FDrl7zM6Blvl64xuhd3y +64xuh0cVeCyXXy4sLqRL5ljytGU+nsymls19Q3tHBs3RpSXTFxXNglW0Cqet1OB4Llsafdoq5jLW +tLVwailZGJ+cmDkyeujA5Mz44WNTk9MzgspRq1BczGXN4cH9g0O+VUE4tWQlixaanrcKZilnltKW +WYumaM2V6NL5XMGXzONCs1RIpqxMsnDSTJZKhcUTp3yVbK60OGcVMbZCyWTGa8mkS6X8gXicrk9W +ZYOLuYbM+BKayRatODyGe5yHZSjAIixAGkrYvWOQhNNgIfU4UllIodyEffiuvxdGYBDpUVjCL1NY +VfRnFo4WjnR1CjXH0XoWpaPwtC/LQQbHacQCnEILSdQdh0mYgBk4glqHsEMnkR6Hw3AMppCe9meN +rBz1PRUxAio1YRj97UcMCbE2XjmFni30XfRzpFHP+7ZM1Mz5P9O+pFFt6Jo5pLjXeRwLwpp55pFy +CugjhdyMn+dJ5CWRW/LtncD8a1ayONLZnB9ltW4F30p95I12Ju3bzGPl4vjF/Sfr1g36nj65Zhwr +VI0m62eMN8rfAb/YINsKZW5kc3RyZWFtCmVuZG9iagoyMyAwIG9iago8PC9GaWx0ZXIgL0ZsYXRl +RGVjb2RlCi9MZW5ndGggMjIzPj4gc3RyZWFtCnicXZDPasMwDMbvfgodu0NxmnMIjJZCDvvD0j2A +YyuZYZGN4hzy9pW90EEFNsjf9xOfpc/dpSOfQH9ysD0mGD05xiWsbBEGnDypUw3O27R35baziUoL +3G9LwrmjMaimAdBfoi6JNzi8ujDgi9If7JA9TXD4PvfS92uMvzgjJahU24LDUSa9mfhuZgRdsGPn +RPdpOwrz77htEaEu/ekvjQ0Ol2gssqEJVVNJtdBcpVqF5J70nRpG+2O4uK/ZXVV1ce/vmcv/e4Sy +K7PkKUsoQXIET/jYUwwxU/ncAU0Cb2IKZW5kc3RyZWFtCmVuZG9iagp4cmVmCjAgMjQKMDAwMDAw +MDAwMCA2NTUzNSBmIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDAyNjUgMDAwMDAgbiAKMDAw +MDAyMjQ5OCAwMDAwMCBuIAowMDAwMDM3MTE2IDAwMDAwIG4gCjAwMDAwNDM4MTAgMDAwMDAgbiAK +MDAwMDA0Mzg1NyAwMDAwMCBuIAowMDAwMDQzOTEyIDAwMDAwIG4gCjAwMDAwNDUxNTUgMDAwMDAg +biAKMDAwMDA0NTE5MiAwMDAwMCBuIAowMDAwMDQ1MzI4IDAwMDAwIG4gCjAwMDAwNDYwMjQgMDAw +MDAgbiAKMDAwMDA0NjI2MyAwMDAwMCBuIAowMDAwMDUyNzQzIDAwMDAwIG4gCjAwMDAwNTMxNTUg +MDAwMDAgbiAKMDAwMDA1MzI5MiAwMDAwMCBuIAowMDAwMDUzNjkwIDAwMDAwIG4gCjAwMDAwNTM5 +MjkgMDAwMDAgbiAKMDAwMDA1ODM0NyAwMDAwMCBuIAowMDAwMDU4NzA2IDAwMDAwIG4gCjAwMDAw +NTg4NDMgMDAwMDAgbiAKMDAwMDA1OTA3MyAwMDAwMCBuIAowMDAwMDU5MzExIDAwMDAwIG4gCjAw +MDAwNjE0NzggMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDI0Ci9Sb290IDUgMCBSCi9JbmZvIDEg +MCBSPj4Kc3RhcnR4cmVmCjYxNzcyCiUlRU9GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== diff --git a/spec/fixtures/emails/encoded_display_name.eml b/spec/fixtures/emails/encoded_display_name.eml index 1b5d6bb3a10..132e4404011 100644 --- a/spec/fixtures/emails/encoded_display_name.eml +++ b/spec/fixtures/emails/encoded_display_name.eml @@ -1,11 +1,11 @@ -Return-Path: <random.name@bar.ru> -From: =?UTF-8?B?0KHQu9GD0YfQsNC50L3QsNGP?= =?UTF-8?B?INCY0LzRjw==?= <random.name@bar.ru> -To: meat@bar.com -Subject: I need help -Date: Fri, 15 Jan 2016 00:12:43 +0100 -Message-ID: <29@foo.bar.mail> -Mime-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: quoted-printable - -Будьте здоровы! +Return-Path: <random.name@bar.ru> +From: =?UTF-8?B?0KHQu9GD0YfQsNC50L3QsNGP?= =?UTF-8?B?INCY0LzRjw==?= <random.name@bar.ru> +To: meat@bar.com +Subject: I need help +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <29@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +Будьте здоровы! diff --git a/spec/fixtures/emails/forwarded_email_3.eml b/spec/fixtures/emails/forwarded_email_3.eml index 1f37822b0a7..549dd44a68b 100644 --- a/spec/fixtures/emails/forwarded_email_3.eml +++ b/spec/fixtures/emails/forwarded_email_3.eml @@ -1,18 +1,18 @@ -Message-ID: <60@foo.bar.mail> -From: Ba Bar <ba@bar.com> -To: Team <team@bar.com> -Date: Mon, 9 Dec 2016 13:37:42 +0100 -Subject: Fwd: Ça Discourse ? - -@team, can you have a look at this email below? - -Objet: Ça Discourse ? -Date: 2017-01-04 11:27 -De: Un Français <un@francais.fr> -À: ba@bar.com - -Bonjour, - -Ça Discourse bien aujourd'hui ? - -Bises +Message-ID: <60@foo.bar.mail> +From: Ba Bar <ba@bar.com> +To: Team <team@bar.com> +Date: Mon, 9 Dec 2016 13:37:42 +0100 +Subject: Fwd: Ça Discourse ? + +@team, can you have a look at this email below? + +Objet: Ça Discourse ? +Date: 2017-01-04 11:27 +De: Un Français <un@francais.fr> +À: ba@bar.com + +Bonjour, + +Ça Discourse bien aujourd'hui ? + +Bises diff --git a/spec/fixtures/emails/inline_image.eml b/spec/fixtures/emails/inline_image.eml index af3283702a9..b169f8167a0 100644 --- a/spec/fixtures/emails/inline_image.eml +++ b/spec/fixtures/emails/inline_image.eml @@ -1,76 +1,76 @@ -Return-Path: <discourse@bar.com> -From: Foo Bar <discourse@bar.com> -To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com -Date: Fri, 15 Jan 2016 00:12:43 +0100 -Message-ID: <28@foo.bar.mail> -Mime-Version: 1.0 -Content-Type: multipart/related; boundary=001a114b2eccff183a052998ec68 - ---001a114b2eccff183a052998ec68 -Content-Type: multipart/alternative; boundary=001a114b2eccff1836052998ec67 - ---001a114b2eccff1836052998ec67 -Content-Type: text/plain; charset=UTF-8 - -Before - -[image: 内嵌图片 1] - -After - ---001a114b2eccff1836052998ec67 -Content-Type: text/html; charset=UTF-8 - -<div dir="ltr"><b>Before</b><div><br></div><div><img src="cid:ii_1525434659ddb4cb" alt="内嵌图片 1"><br></div><div><br></div><div><i>After</i> -</div></div> - ---001a114b2eccff1836052998ec67-- ---001a114b2eccff183a052998ec68 -Content-Type: image/png; name="logo.png" -Content-Disposition: inline; filename="logo.png" -Content-Transfer-Encoding: base64 -Content-ID: <ii_1525434659ddb4cb> -X-Attachment-Id: ii_1525434659ddb4cb - -iVBORw0KGgoAAAANSUhEUgAAAPQAAABCCAMAAABXYgukAAABhlBMVEUAAAAjHyAjHyAjHyAjHyAj -HyAjHyAjHyAjHyAjHyAjHyAjHyAjHyAjHyAjHyAjHyAjHyAAqVAAru/lGyTxXCL/+a5UHiHZGyTq -NyPtRCM7HyHwWCLrPCOEHSL95Z31g0W1HCMgs1wAqnghKC0Aq5YJirsAqm6oHCMPb5QArccUXnsS -Z4hAvWj80ouP1oXoKyR4HSL0eTz4q2j+76XsQCOf24vpLyNsHiJgHiEYTGHyZiucHSPzcDTf76IL -ga7qMyMwuGIArdEaQ1T7yIJwzHn2l1f6vnovHyDuTCPNHCQfMTr3oV/wVCL5tHEHk8gArKDnJyTv -UCIAruUWVW4ArLMQrlYNeKEEnNVQwm0cOkcAq4KA0X/mHyQAqVoArKkAq4wCpeIArdvnIyT1jU4A -qmQUXV3v9KikRSHP6pwwIyBHHiHKUSLxaDTtSCMCpLpgx3PaHyTtSSy/5Zetw3b83JSVSiT0i1eg -MSI+qVa2JCMrbVbiX0Ygs2YArL2LkGApkWQLf2kSZmpFMZD0AAAAEHRSTlMAEFCAv8+vQCBw72CP -358w5xEcGAAABxJJREFUeF7lmuW/uzoMh1eKrUApbMfd9efu7u5u193d739+gbHQBtg49x3b99U5 -H7asD0mTNNAoE9GobohUukmtxqDLbhoCi5nOICNruigWc+0BRXY80UN8ELEtQ/QWo4OGTFzRX4Y/ -UMy+ISqpOUi7mYmK4oPDLKpLJ4PBzMVOZAwEdVNgje6dmZ+fX5vZ+1mhrwcwto+stTKNnRgdxH3t -q0B7r6W4d/fEirivHR24HE48Ja47yO+NX22nujlxuDU/ialrXq9NmWUmQd7TIQZNTX9zBCczGYCy -mjVrmoxyIgnrDvLJS5d3jUSa3XXr/fbU+Ayipvi2mTUMbmAeT5AvJcCgy3P3fkbUNrTs6QWrNtAU -Mb+cSJBnR7B27fv4U5TBkQlaG0czKW3HzPfiwAYvK9r/OXI1hq5hWzI5FkF/GTHPHRgp1vc/qq5G -WcGpC7QnQPMR88E4tEdK9ctvMjQjKYAB+bxufcloXKoQM9ah4ENAllxLzFodQ1wBWos29M12e99I -Ly0sHius1cSCXF6n6J6MHD3dbh+f7Qm9O/hqSaIm9e26IXVPtdu3RnrqVLB4GsV37eQo0X0Ggrtc -Z4Nz5wXIRQB129JjSbm60A/6YrC8gjZ13aQD85UouqOuBODKN/VqeF2A0nunxwK3E4fqOm3mU5vf -pKZu0qafB/BpZASuVJVNTd4kqW03Mq3lEK3YsEs1AtAsK1hJvbrRF/q7IAjXcVOmK+MU2rWqKwi2 -m/2aR5UUSKgnXcEdMgBkHX76ey4kFr/rPqZYtnlGyC0wA9BJ7v6hCvTGbQGyctDEQKMGIFDEpEsa -U65oVaE5lBBfsuBl0dJkQpZLiqCPj1SCDpfKoOFvnOlsQ2CZBNiQeDXolMhMmUHML5t2GiQHfbU9 -Vwn6Tpjl7yaGdgrPnzYTeRmQTLF4BWiQhm+08BAzokbQ+ytCbwIAxdCmCuDIIY/llg7caXVoFoUR -2jk+OkoZOkSCrkBfqQi9O4beKodOQ86yKAPmzJme61gaTUOdOXIMeC5tuh4ESDXoZsN2YIhBLY0D -Mxg2nE5Gy7wg5Dp9sAr0xXhPb5WHd7ZmYqTM4AnPgswFaZaDIeXhkl4BGjKGlX3FYt0dzZWdAvve -U6DXWtNVoM+q0FYxNE+Auow891yEGLzzDwE76tqEXQXalqGFlrClzETguR18Sh7wt/ZUgD4VBMH9 -cmjY057clTPYamV9MM1NNWgFaKOhAArdyhm2c4+uXBn6Sutl+0aVLf1BGD4ob04sITC21aNP5zCN -QLdIrwBtSjgY2+0Y1iV56Z1Sp7/3+peshSBYDqWS1UDQcvr2NMWbfnkfbBbch97QEA24Ipo2GC6R -Aj3aGu/bnBwKguBcGKIHeaUdGSc4H2ExHN2AWh0aMgHUhOrQYu3lVJ829KOFJLpXlEqLe2/CUTPQ -C1rsAJrkoUG+h3qDqtCTY9M3+tar4GEo9SYahk6kZStw+0B7O4C2CqBBhDIpu1aHFmNnTvbpxhJH -S0dLgqDzb2bZVfa0LkOb0Eo6laGV/kPoAM2tvJCnWxOXejF/8iiIi3T4GJjN8qU7DBbm98jeLq4s -UGAlShVa2RUYG8zR0iGHCn20NTXbj3lZcbRTBE1IZwUeLN7Lu7r7t5brIczMtKWeT90e0EQpFBYY -tvpBzxye68e8GobSCIEVBqlhEFgA1Ft00HXgfTQGjMqyGcnYvA4RgQJelL3TO2MAKoPvpvK5XQA9 -P3GhR7FKmO+H4ZMlgKZF0LzbY5sAbTN1cGBzdJKC+mqbimkDakB2IndlaGBODRAPoKna7xOanINy -0GNflHefT4OUOXwmQDaGhtbCoFSXFkYhNnRKuaG8fOhlB0DdQA+MHEhNOmRjH0MDKqdU2kiEde1S -zWpylk2wlDx291YZ8tdBrOcx8wN0HEbQrkDye7QKDvQVWBbQYOmovBed1j3IezkxW4U++u0BqE0v -Dp0C4t3bCfKr16HKzEgBNMdrhHWVzke0wrtRco3ZBdD4tintb36+oOSxn9KMdXEhhny0/fTFi+3t -R0FHb2I3vz2Gh3758C5+y47o5WTY18xBhyW0aAyNDXC5bGJmBP3rbEq8+ubhu403r4JMq69j5PD2 -deTDokRGMTOeTObHw8RUrti43uNBJ4aGW4r7AV/Ho0gM/XtE/MfinY37t1fWtx6H4cbr5cVIz+/8 -+S6M9XYTxxmGhlkzDGpkEQfYPI7qp9X9DuMJF56JY3t6Ism+ZaLpNjYsPNfP1+nJv/7+59+t891T -4/X1t6GsJ5tLaN9I8q1Yvvo/eBl/EK7kL9mNIpFO+9hHZR+y8W+KXjq2vtIBf3J681luXFlfYc6h -eOV7GJkb/5d5+KCbjaGDZlZjiKChwA8RNEx1hwcanvIPGbThAPKQQBvQuQ4FtKeb8GrOQOg/pxLS -uIDrr6oAAAAASUVORK5CYII= ---001a114b2eccff183a052998ec68-- +Return-Path: <discourse@bar.com> +From: Foo Bar <discourse@bar.com> +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <28@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: multipart/related; boundary=001a114b2eccff183a052998ec68 + +--001a114b2eccff183a052998ec68 +Content-Type: multipart/alternative; boundary=001a114b2eccff1836052998ec67 + +--001a114b2eccff1836052998ec67 +Content-Type: text/plain; charset=UTF-8 + +Before + +[image: 内嵌图片 1] + +After + +--001a114b2eccff1836052998ec67 +Content-Type: text/html; charset=UTF-8 + +<div dir="ltr"><b>Before</b><div><br></div><div><img src="cid:ii_1525434659ddb4cb" alt="内嵌图片 1"><br></div><div><br></div><div><i>After</i> +</div></div> + +--001a114b2eccff1836052998ec67-- +--001a114b2eccff183a052998ec68 +Content-Type: image/png; name="logo.png" +Content-Disposition: inline; filename="logo.png" +Content-Transfer-Encoding: base64 +Content-ID: <ii_1525434659ddb4cb> +X-Attachment-Id: ii_1525434659ddb4cb + +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 +uIDrr6oAAAAASUVORK5CYII= +--001a114b2eccff183a052998ec68-- diff --git a/spec/fixtures/emails/reply_with_8bit_encoding.eml b/spec/fixtures/emails/reply_with_8bit_encoding.eml index ed99f6752a8..2d025dd929b 100644 --- a/spec/fixtures/emails/reply_with_8bit_encoding.eml +++ b/spec/fixtures/emails/reply_with_8bit_encoding.eml @@ -1,12 +1,12 @@ -Return-Path: <discourse@bar.com> -From: Foo Bar <discourse@bar.com> -To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com -Date: Fri, 15 Jan 2016 00:12:43 +0100 -Message-ID: <43@foo.bar.mail> -MIME-Version: 1.0 -Content-Type: text/plain; charset=iso-8859-1 -Content-Disposition: inline -Content-Transfer-Encoding: 8bit - -hab vergessen kritische zeichen einzuf�gen: -������� +Return-Path: <discourse@bar.com> +From: Foo Bar <discourse@bar.com> +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <43@foo.bar.mail> +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-1 +Content-Disposition: inline +Content-Transfer-Encoding: 8bit + +hab vergessen kritische zeichen einzuf�gen: +������� diff --git a/spec/integration/rate_limiting_spec.rb b/spec/integration/rate_limiting_spec.rb index b1cc23afce8..93ad7bc53a9 100644 --- a/spec/integration/rate_limiting_spec.rb +++ b/spec/integration/rate_limiting_spec.rb @@ -26,6 +26,7 @@ describe 'rate limiter integration' do end it 'can cleanly limit requests' do + freeze_time #request.set_header("action_dispatch.show_exceptions", true) admin = Fabricate(:admin) @@ -46,5 +47,9 @@ describe 'rate limiter integration' do } expect(response.status).to eq(429) + + data = JSON.parse(response.body) + + expect(data["extras"]["wait_seconds"]).to eq(60) end end diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index 81cca747984..a86424b323d 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -379,7 +379,7 @@ describe UserNotifications do expect(mail[:from].display_names).to eql(['john']) # subject should include "[PM]" - expect(mail.subject).to match("[PM]") + expect(mail.subject).to include("[PM] ") # 1 "visit message" link expect(mail.html_part.to_s.scan(/Visit Message/).count).to eq(1) @@ -409,6 +409,65 @@ describe UserNotifications do expect(mail.text_part.to_s).to_not include(response.raw) expect(mail.text_part.to_s).to_not include(topic.url) end + + it "doesn't include group name in subject" do + group = Fabricate(:group) + topic.allowed_groups = [ group ] + mail = UserNotifications.user_private_message( + response.user, + post: response, + notification_type: notification.notification_type, + notification_data_hash: notification.data_hash + ) + + expect(mail.subject).to include("[PM] ") + end + + context "when SiteSetting.group_name_in_subject is true" do + before do + SiteSetting.group_in_subject = true + end + + let(:group) { Fabricate(:group, name: "my_group") } + let(:mail) { UserNotifications.user_private_message( + response.user, + post: response, + notification_type: notification.notification_type, + notification_data_hash: notification.data_hash + ) } + + shared_examples "includes first group name" do + it "includes first group name in subject" do + expect(mail.subject).to include("[my_group] ") + end + + context "when first group has full name" do + it "includes full name in subject" do + group.full_name = "My Group" + group.save + expect(mail.subject).to include("[My Group] ") + end + end + end + + context "one group in pm" do + before do + topic.allowed_groups = [ group ] + end + + include_examples "includes first group name" + end + + context "multiple groups in pm" do + let(:group2) { Fabricate(:group) } + + before do + topic.allowed_groups = [ group, group2 ] + end + + include_examples "includes first group name" + end + end end it 'adds a warning when mail limit is reached' do diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 8ec72679688..b5571fb370e 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -169,6 +169,7 @@ describe Post do let(:post_two_images) { post_with_body("<img src='http://discourse.org/logo.png'> <img src='http://bbc.co.uk/sherlock.jpg'>", newuser) } let(:post_with_avatars) { post_with_body('<img alt="smiley" title=":smiley:" src="/assets/emoji/smiley.png" class="avatar"> <img alt="wink" title=":wink:" src="/assets/emoji/wink.png" class="avatar">', newuser) } let(:post_with_favicon) { post_with_body('<img src="/assets/favicons/wikipedia.png" class="favicon">', newuser) } + let(:post_image_within_quote) { post_with_body('[quote]<img src="coolimage.png">[/quote]', newuser) } let(:post_with_thumbnail) { post_with_body('<img src="/assets/emoji/smiley.png" class="thumbnail">', newuser) } let(:post_with_two_classy_images) { post_with_body("<img src='http://discourse.org/logo.png' class='classy'> <img src='http://bbc.co.uk/sherlock.jpg' class='classy'>", newuser) } @@ -188,6 +189,28 @@ describe Post do expect(post_with_avatars.image_count).to eq(0) end + it "allows images by default" do + expect(post_one_image).to be_valid + end + + it "doesn't allow more than `min_trust_to_post_images`" do + SiteSetting.min_trust_to_post_images = 4 + post_one_image.user.trust_level = 3 + expect(post_one_image).not_to be_valid + end + + it "doesn't allow more than `min_trust_to_post_images`" do + SiteSetting.min_trust_to_post_images = 4 + post_one_image.user.trust_level = 3 + expect(post_image_within_quote).not_to be_valid + end + + it "doesn't allow more than `min_trust_to_post_images`" do + SiteSetting.min_trust_to_post_images = 4 + post_one_image.user.trust_level = 4 + expect(post_one_image).to be_valid + end + it "doesn't count favicons as images" do PrettyText.stubs(:cook).returns(post_with_favicon.raw) expect(post_with_favicon.image_count).to eq(0) diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index faa6145a80c..bdccd26fcbd 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -15,6 +15,13 @@ describe Tag do SiteSetting.min_trust_level_to_tag_topics = 0 end + it "can delete tags on deleted topics" do + tag = Fabricate(:tag) + topic = Fabricate(:topic, tags: [tag]) + topic.trash! + expect { tag.destroy }.to change { Tag.count }.by(-1) + end + describe '#top_tags' do it "returns nothing if nothing has been tagged" do make_some_tags(tag_a_topic: false) diff --git a/spec/models/topic_embed_spec.rb b/spec/models/topic_embed_spec.rb index 447e8ff667f..1f00c699ec0 100644 --- a/spec/models/topic_embed_spec.rb +++ b/spec/models/topic_embed_spec.rb @@ -72,6 +72,11 @@ describe TopicEmbed do expect(TopicEmbed.topic_id_for_embed('http://example.com/post/24')).to eq(nil) expect(TopicEmbed.topic_id_for_embed('http://example.com/post')).to eq(nil) end + + it "finds the topic id when the embed_url contains a query string" do + topic_embed = Fabricate(:topic_embed, embed_url: "http://example.com/post/248?key=foo") + expect(TopicEmbed.topic_id_for_embed('http://example.com/post/248?key=foo')).to eq(topic_embed.topic_id) + end end describe '.find_remote' do diff --git a/spec/models/user_second_factor_spec.rb b/spec/models/user_second_factor_spec.rb new file mode 100644 index 00000000000..2a61b064222 --- /dev/null +++ b/spec/models/user_second_factor_spec.rb @@ -0,0 +1,9 @@ +require 'rails_helper' + +RSpec.describe UserSecondFactor do + describe '.methods' do + it 'should retain the right order' do + expect(described_class.methods[:totp]).to eq(1) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8e95731065f..6385f61aca2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1619,4 +1619,21 @@ describe User do end end + describe "#activate" do + let!(:inactive) { Fabricate(:user, active: false) } + + it 'confirms email token and activates user' do + inactive.activate + inactive.reload + expect(inactive.email_confirmed?).to eq(true) + expect(inactive.active).to eq(true) + end + + it 'activates user even if email token is already confirmed' do + token = inactive.email_tokens.find_by(email: inactive.email) + token.update_column(:confirmed, true) + inactive.activate + expect(inactive.active).to eq(true) + end + end end diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb new file mode 100644 index 00000000000..8d665aea2ea --- /dev/null +++ b/spec/requests/admin/users_controller_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +RSpec.describe Admin::UsersController do + let(:admin) { Fabricate(:admin) } + let(:user) { Fabricate(:user) } + + describe '#disable_second_factor' do + let(:second_factor) { user.create_totp } + + describe 'as an admin' do + before do + sign_in(admin) + second_factor + expect(user.reload.user_second_factor).to eq(second_factor) + end + + it 'should able to disable the second factor for another user' do + SiteSetting.queue_jobs = true + + expect do + put "/admin/users/#{user.id}/disable_second_factor.json" + end.to change { Jobs::CriticalUserEmail.jobs.length }.by(1) + + expect(response.status).to eq(200) + expect(user.reload.user_second_factor).to eq(nil) + + job_args = Jobs::CriticalUserEmail.jobs.first["args"].first + + expect(job_args["user_id"]).to eq(user.id) + expect(job_args["type"]).to eq('account_second_factor_disabled') + end + + it 'should not be able to disable the second factor for the current user' do + put "/admin/users/#{admin.id}/disable_second_factor.json" + + expect(response.status).to eq(403) + end + + describe 'when user does not have second factor enabled' do + it 'should raise the right error' do + user.user_second_factor.destroy! + + put "/admin/users/#{user.id}/disable_second_factor.json" + + expect(response.status).to eq(400) + end + end + end + end +end diff --git a/spec/requests/session_controller_spec.rb b/spec/requests/session_controller_spec.rb index 951968984f2..5b003d39ffc 100644 --- a/spec/requests/session_controller_spec.rb +++ b/spec/requests/session_controller_spec.rb @@ -136,6 +136,50 @@ RSpec.describe SessionController do date: I18n.l(user.suspended_till, format: :date_only) )) end + + context 'user has 2-factor logins' do + let!(:user_second_factor) { Fabricate(:user_second_factor, user: user) } + + describe 'requires second factor' do + it 'should return a second factor prompt' do + get "/session/email-login/#{email_token.token}" + + expect(response.status).to eq(200) + + response_body = CGI.unescapeHTML(response.body) + + expect(response_body).to include(I18n.t( + "login.second_factor_title" + )) + + expect(response_body).to_not include(I18n.t( + "login.invalid_second_factor_code" + )) + end + end + + describe 'errors on incorrect 2-factor' do + it 'does not log in with incorrect two factor' do + post "/session/email-login/#{email_token.token}", params: { second_factor_token: "0000" } + + expect(response.status).to eq(200) + + expect(CGI.unescapeHTML(response.body)).to include(I18n.t( + "login.invalid_second_factor_code" + )) + end + end + + describe 'allows successful 2-factor' do + it 'logs in correctly' do + post "/session/email-login/#{email_token.token}", params: { + second_factor_token: ROTP::TOTP.new(user_second_factor.data).now + } + + expect(response).to redirect_to("/") + end + end + end end end end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index ba41c482ecd..cd2628ba3c7 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -401,4 +401,118 @@ RSpec.describe UsersController do end end end + + describe '#create_second_factor' do + context 'when not logged in' do + it 'should return the right response' do + post "/users/second_factors.json", params: { + password: 'wrongpassword' + } + + expect(response.status).to eq(403) + end + end + + context 'when logged in' do + before do + sign_in(user) + end + + describe 'create 2fa request' do + it 'fails on incorrect password' do + post "/users/second_factors.json", params: { + password: 'wrongpassword' + } + + expect(response.status).to eq(200) + + expect(JSON.parse(response.body)['error']).to eq(I18n.t( + "login.incorrect_password") + ) + end + + it 'succeeds on correct password' do + post "/users/second_factors.json", params: { + password: 'somecomplicatedpassword' + } + + expect(response.status).to eq(200) + + response_body = JSON.parse(response.body) + + expect(response_body['key']).to eq(user.user_second_factor.data) + expect(response_body['qr']).to be_present + end + end + end + end + + describe '#update_second_factor' do + let(:user_second_factor) { Fabricate(:user_second_factor, user: user) } + + context 'when not logged in' do + it 'should return the right response' do + put "/users/second_factor.json", params: { + second_factor_token: ROTP::TOTP.new(user_second_factor.data).now + } + + expect(response.status).to eq(403) + end + end + + context 'when logged in' do + before do + sign_in(user) + user_second_factor + end + + context 'when user has totp setup' do + context 'when token is missing' do + it 'returns the right response' do + put "/users/second_factor.json", params: { + enable: 'true', + } + + expect(response.status).to eq(400) + end + end + + context 'when token is invalid' do + it 'returns the right response' do + put "/users/second_factor.json", params: { + second_factor_token: '000000', + enable: 'true', + } + + expect(response.status).to eq(200) + + expect(JSON.parse(response.body)['error']).to eq(I18n.t( + "login.invalid_second_factor_code" + )) + end + end + + context 'when token is valid' do + it 'should allow second factor for the user to be enabled' do + put "/users/second_factor.json", params: { + second_factor_token: ROTP::TOTP.new(user_second_factor.data).now, + enable: 'true', + } + + expect(response.status).to eq(200) + expect(user.reload.user_second_factor.enabled).to be true + end + + it 'should allow second factor for the user to be disabled' do + put "/users/second_factor.json", params: { + second_factor_token: ROTP::TOTP.new(user_second_factor.data).now, + } + + expect(response.status).to eq(200) + expect(user.reload.user_second_factor).to eq(nil) + end + end + end + end + end end diff --git a/spec/requests/users_email_controller_spec.rb b/spec/requests/users_email_controller_spec.rb index 04912f8f206..624ba871e54 100644 --- a/spec/requests/users_email_controller_spec.rb +++ b/spec/requests/users_email_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe UsersEmailController do - describe '.confirm' do + describe '#confirm' do it 'errors out for invalid tokens' do get "/u/authorize-email/asdfasdf" @@ -60,20 +60,56 @@ describe UsersEmailController do expect(user.user_stat.bounce_score).to eq(0) expect(user.user_stat.reset_bounce_score_after).to eq(nil) end + + context 'second factor required' do + let!(:second_factor) { Fabricate(:user_second_factor, user: user) } + + it 'requires a second factor token' do + get "/u/authorize-email/#{user.email_tokens.last.token}" + + expect(response.status).to eq(200) + + response_body = response.body + + expect(response_body).to include(I18n.t("login.second_factor_title")) + expect(response_body).not_to include(I18n.t("login.invalid_second_factor_code")) + end + + it 'adds an error on a second factor attempt' do + get "/u/authorize-email/#{user.email_tokens.last.token}", params: { + second_factor_token: "000000" + } + + expect(response.status).to eq(200) + expect(response.body).to include(I18n.t("login.invalid_second_factor_code")) + end + + it 'confirms with a correct second token' do + get "/u/authorize-email/#{user.email_tokens.last.token}", params: { + second_factor_token: ROTP::TOTP.new(second_factor.data).now + } + + expect(response.status).to eq(200) + + response_body = response.body + + expect(response.body).not_to include(I18n.t("login.second_factor_title")) + expect(response.body).not_to include(I18n.t("login.invalid_second_factor_code")) + end + end end end - describe '.update' do + describe '#update' do + let(:user) { Fabricate(:user) } let(:new_email) { 'bubblegum@adventuretime.ooo' } it "requires you to be logged in" do - put "/u/asdf/preferences/email.json" + put "/u/#{user.username}/preferences/email.json", params: { email: new_email } expect(response.status).to eq(403) end context 'when logged in' do - let(:user) { Fabricate(:user) } - before do sign_in(user) end diff --git a/spec/serializers/web_hook_post_serializer_spec.rb b/spec/serializers/web_hook_post_serializer_spec.rb index 6190f8e27ef..51cd4507461 100644 --- a/spec/serializers/web_hook_post_serializer_spec.rb +++ b/spec/serializers/web_hook_post_serializer_spec.rb @@ -3,10 +3,13 @@ require 'rails_helper' RSpec.describe WebHookPostSerializer do let(:admin) { Fabricate(:admin) } let(:post) { Fabricate(:post) } - let(:serializer) { WebHookPostSerializer.new(post, scope: Guardian.new(admin), root: false) } + + def serialized_for_user(u) + WebHookPostSerializer.new(post, scope: Guardian.new(u), root: false).as_json + end it 'should only include the required keys' do - count = serializer.as_json.keys.count + count = serialized_for_user(admin).keys.count difference = count - 41 expect(difference).to eq(0), lambda { @@ -21,4 +24,18 @@ RSpec.describe WebHookPostSerializer do message << "\nPlease verify if those key(s) are required as part of the web hook's payload." } end + + it 'should only include deleted topic title for staffs' do + topic = post.topic + PostDestroyer.new(Discourse.system_user, post).destroy + post.reload + + [nil, post.user, Fabricate(:user)].each do |user| + expect(serialized_for_user(user)[:topic_title]).to eq(nil) + end + + [Fabricate(:moderator), admin].each do |user| + expect(serialized_for_user(user)[:topic_title]).to eq(topic.title) + end + end end diff --git a/spec/services/search_indexer_spec.rb b/spec/services/search_indexer_spec.rb index efbfce82c1b..fb73ea2fbb4 100644 --- a/spec/services/search_indexer_spec.rb +++ b/spec/services/search_indexer_spec.rb @@ -7,7 +7,7 @@ describe SearchIndexer do data = "你好世界" expect(data.split(" ").length).to eq(1) - SearchIndexer.update_posts_index(post_id, "你好世界", "", nil) + SearchIndexer.update_posts_index(post_id, "你好世界", "", "", nil) raw_data = PostSearchData.where(post_id: post_id).pluck(:raw_data)[0] expect(raw_data.split(' ').length).to eq(2) @@ -15,18 +15,18 @@ describe SearchIndexer do it 'correctly indexes a post according to version' do # Preparing so that they can be indexed to right version - SearchIndexer.update_posts_index(post_id, "dummy", "", nil) + SearchIndexer.update_posts_index(post_id, "dummy", "", nil, nil) PostSearchData.find_by(post_id: post_id).update_attributes!(version: -1) data = "<a>This</a> is a test" - SearchIndexer.update_posts_index(post_id, data, "", nil) + SearchIndexer.update_posts_index(post_id, "", "", nil, data) raw_data, locale, version = PostSearchData.where(post_id: post_id).pluck(:raw_data, :locale, :version)[0] expect(raw_data).to eq("This is a test") expect(locale).to eq("en") expect(version).to eq(Search::INDEX_VERSION) - SearchIndexer.update_posts_index(post_id, "tester", "", nil) + SearchIndexer.update_posts_index(post_id, "tester", "", nil, nil) raw_data = PostSearchData.where(post_id: post_id).pluck(:raw_data)[0] expect(raw_data).to eq("tester") diff --git a/spec/services/user_silencer_spec.rb b/spec/services/user_silencer_spec.rb index 188bd0435fd..73bab2e19ff 100644 --- a/spec/services/user_silencer_spec.rb +++ b/spec/services/user_silencer_spec.rb @@ -60,7 +60,7 @@ describe UserSilencer do end it "logs it with context" do - SystemMessage.stubs(:create).returns(Fabricate.build(:post)) + SystemMessage.stubs(:create) expect { UserSilencer.silence(user, Fabricate(:admin)) }.to change { UserHistory.count }.by(1) diff --git a/test/javascripts/acceptance/composer-actions-test.js.es6 b/test/javascripts/acceptance/composer-actions-test.js.es6 index f15498e119e..f6a48248ac0 100644 --- a/test/javascripts/acceptance/composer-actions-test.js.es6 +++ b/test/javascripts/acceptance/composer-actions-test.js.es6 @@ -72,6 +72,7 @@ QUnit.test('replying to post - reply_as_new_topic', assert => { const composerActions = selectKit('.composer-actions'); const categoryChooser = selectKit('.title-wrapper .category-chooser'); const categoryChooserReplyArea = selectKit('.reply-area .category-chooser'); + const quote = 'test replying as new topic when initially replied to post'; visit('/t/internationalization-localization/280'); @@ -80,13 +81,13 @@ QUnit.test('replying to post - reply_as_new_topic', assert => { click('#topic-title .submit-edit'); click('article#post_3 button.reply'); - fillIn('.d-editor-input', 'test replying as new topic when initially replied to post'); + fillIn('.d-editor-input', quote); composerActions.expand().selectRowByValue('reply_as_new_topic'); andThen(() => { assert.equal(categoryChooserReplyArea.header().name(), 'faq'); assert.equal(find('.action-title').text().trim(), I18n.t("topic.create_long")); - assert.equal(find('.d-editor-input').val(), 'test replying as new topic when initially replied to post'); + assert.ok(find('.d-editor-input').val().includes(quote)); }); }); @@ -132,7 +133,7 @@ QUnit.test('interactions', assert => { andThen(() => { assert.equal(find('.action-title').text().trim(), I18n.t("topic.create_long")); - assert.equal(find('.d-editor-input').val(), quote); + assert.ok(find('.d-editor-input').val().includes(quote)); assert.equal(composerActions.rowByIndex(0).value(), 'reply_to_post'); assert.equal(composerActions.rowByIndex(1).value(), 'reply_as_private_message'); assert.equal(composerActions.rowByIndex(2).value(), 'reply_to_topic'); diff --git a/test/javascripts/acceptance/password-reset-test.js.es6 b/test/javascripts/acceptance/password-reset-test.js.es6 index 4706b88c29b..b105315b47d 100644 --- a/test/javascripts/acceptance/password-reset-test.js.es6 +++ b/test/javascripts/acceptance/password-reset-test.js.es6 @@ -24,6 +24,25 @@ acceptance("Password Reset", { return response({success: "OK", message: I18n.t('password_reset.success')}); } }); + + server.get('/u/confirm-email-token/requiretwofactor.json', () => { //eslint-disable-line + return response({ success: "OK" }); + }); + + server.put('/u/password-reset/requiretwofactor.json', request => { //eslint-disable-line + const body = parsePostData(request.requestBody); + if (body.password === "perf3ctly5ecur3" && body.second_factor_token === "123123") { + return response({ success: "OK", message: I18n.t('password_reset.success') }); + } else if (body.second_factor_token === "123123") { + return response({ success: false, errors: { password: ["invalid"] } }); + } else { + return response({ + success: false, + message: "invalid token", + errors: { user_second_factor: ["invalid token"] } + }); + } + }); } }); @@ -58,4 +77,45 @@ QUnit.test("Password Reset Page", assert => { andThen(() => { assert.ok(!exists(".password-reset form"), "form is gone"); }); -}); \ No newline at end of file +}); + +QUnit.test("Password Reset Page With Second Factor", assert => { + PreloadStore.store('password_reset', { + is_developer: false, + second_factor_required: true + }); + + visit("/u/password-reset/requiretwofactor"); + + andThen(() => { + assert.notOk(exists("#new-account-password"), "does not show the input"); + assert.ok(exists("#second-factor"), "shows the second factor prompt"); + }); + + fillIn('#second-factor', '0000'); + click('.password-reset form button'); + + andThen(() => { + assert.ok(exists(".alert-error"), "shows 2 factor error"); + + assert.ok( + find(".alert-error").html().indexOf("invalid token") > -1, + "shows server validation error message" + ); + }); + + fillIn('#second-factor', '123123'); + click('.password-reset form button'); + + andThen(() => { + assert.notOk(exists(".alert-error"), "hides error"); + assert.ok(exists("#new-account-password"), "shows the input"); + }); + + fillIn('.password-reset input', 'perf3ctly5ecur3'); + click('.password-reset form button'); + + andThen(() => { + assert.ok(!exists(".password-reset form"), "form is gone"); + }); +}); diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index 0014cefd652..5176eed55cd 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -1,5 +1,27 @@ import { acceptance } from "helpers/qunit-helpers"; -acceptance("User Preferences", { loggedIn: true }); +acceptance("User Preferences", { + loggedIn: true, + beforeEach() { + const response = (object) => { + return [ + 200, + {"Content-Type": "application/json"}, + object + ]; + }; + + server.post('/u/second_factors.json', () => { //eslint-disable-line + return response({ + key: "rcyryaqage3jexfj", + qr: '<div id="test-qr">qr-code</div>' + }); + }); + + server.put('/u/second_factor.json', () => { //eslint-disable-line + return response({ error: 'invalid token' }); + }); + } +}); QUnit.test("update some fields", assert => { visit("/u/eviltrout/preferences"); @@ -73,3 +95,29 @@ QUnit.test("email", assert => { assert.equal(find('.tip.bad').text().trim(), I18n.t('user.email.invalid'), 'it should display invalid email tip'); }); }); + +QUnit.test("second factor", assert => { + visit("/u/eviltrout/preferences/second-factor"); + + andThen(() => { + assert.ok(exists("#password"), "it has a password input"); + }); + + fillIn('#password', 'secrets'); + click(".user-content .btn-primary"); + + andThen(() => { + assert.ok(exists("#test-qr"), "shows qr code"); + assert.notOk(exists("#password"), "it hides the password input"); + }); + + fillIn("#second-factor-token", '111111'); + click('.btn-primary'); + + andThen(() => { + assert.ok( + find(".alert-error").html().indexOf("invalid token") > -1, + "shows server validation error message" + ); + }); +}); diff --git a/test/javascripts/acceptance/sign-in-test.js.es6 b/test/javascripts/acceptance/sign-in-test.js.es6 index 12d77228a59..1320be56ee2 100644 --- a/test/javascripts/acceptance/sign-in-test.js.es6 +++ b/test/javascripts/acceptance/sign-in-test.js.es6 @@ -76,6 +76,33 @@ QUnit.test("sign in - not activated - edit email", assert => { }); }); +QUnit.test("second factor", assert => { + visit("/"); + click("header .login-button"); + + andThen(() => { + assert.ok(exists('.login-modal'), "it shows the login modal"); + }); + + fillIn('#login-account-name', 'eviltrout'); + fillIn('#login-account-password', 'need-second-factor'); + click('.modal-footer .btn-primary'); + + andThen(() => { + assert.not(exists('#modal-alert:visible'), 'it hides the login error'); + assert.not(exists('#credentials:visible'), 'it hides the username and password prompt'); + assert.ok(exists('#second-factor:visible'), 'it displays the second factor prompt'); + assert.not(exists('.modal-footer .btn-primary:disabled'), "enables the login button"); + }); + + fillIn('#login-second-factor', '123456'); + click('.modal-footer .btn-primary'); + + andThen(() => { + assert.ok(exists('.modal-footer .btn-primary:disabled'), "disables the login button"); + }); +}); + QUnit.test("create account", assert => { visit("/"); click("header .sign-up-button"); @@ -106,4 +133,4 @@ QUnit.test("create account", assert => { andThen(() => { assert.ok(exists('.modal-footer .btn-primary:disabled'), "create account is disabled"); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index b0bb47e3e8d..28e2607b21b 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -227,6 +227,17 @@ export default function() { current_email: 'current@example.com' }); } + if (data.password === 'need-second-factor') { + if (data.second_factor_token) { + return response({ username: 'eviltrout' }); + } + + return response({ error: "Invalid Second Factor", + reason: "invalid_second_factor", + sent_to_email: 'eviltrout@example.com', + current_email: 'current@example.com' }); + } + return response(400, {error: 'invalid login'}); }); diff --git a/test/javascripts/helpers/site-settings.js b/test/javascripts/helpers/site-settings.js index 8df19c95c92..f84fd111e05 100644 --- a/test/javascripts/helpers/site-settings.js +++ b/test/javascripts/helpers/site-settings.js @@ -59,6 +59,7 @@ Discourse.SiteSettingsOriginal = { "autohighlight_all_code":false, "email_in":false, "authorized_extensions":".jpg|.jpeg|.png|.gif|.svg|.txt|.ico|.yml", + "authorized_extensions_for_staff": "", "max_image_width":690, "max_image_height":500, "allow_profile_backgrounds":true,