diff --git a/Gemfile b/Gemfile index ca22ff21f3..2e34b733af 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.60' +gem 'onebox', '1.8.61' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 69567ed4a4..60cc4407f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,20 +44,20 @@ GEM arel (9.0.0) ast (2.4.0) aws-eventstream (1.0.1) - aws-partitions (1.92.0) - aws-sdk-core (3.21.2) + aws-partitions (1.104.0) + aws-sdk-core (3.27.0) aws-eventstream (~> 1.0) aws-partitions (~> 1.0) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-kms (1.5.0) - aws-sdk-core (~> 3) + aws-sdk-kms (1.9.0) + aws-sdk-core (~> 3, >= 3.26.0) aws-sigv4 (~> 1.0) - aws-sdk-s3 (1.14.0) - aws-sdk-core (~> 3, >= 3.21.2) + aws-sdk-s3 (1.19.0) + aws-sdk-core (~> 3, >= 3.26.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.0) - aws-sigv4 (1.0.2) + aws-sigv4 (1.0.3) barber (0.12.0) ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) @@ -257,7 +257,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.60) + onebox (1.8.61) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -510,7 +510,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.60) + onebox (= 1.8.61) openid-redis-store pg pry-nav diff --git a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 index c2f9df5205..9c92228b8c 100644 --- a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 @@ -2,6 +2,7 @@ import debounce from "discourse/lib/debounce"; import { renderSpinner } from "discourse/helpers/loading-spinner"; import { escapeExpression } from "discourse/lib/utilities"; import { bufferedRender } from "discourse-common/lib/buffered-render"; +import { observes, on } from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend( bufferedRender({ @@ -21,30 +22,38 @@ export default Ember.Component.extend( $div.scrollTop = $div.scrollHeight; }, - _updateFormattedLogs: debounce(function() { - const logs = this.get("logs"); - if (logs.length === 0) { + @on("init") + @observes("logs.[]") + _resetFormattedLogs() { + if (this.get("logs").length === 0) { this._reset(); // reset the cached logs whenever the model is reset - } else { - // do the log formatting only once for HELLish performance - let formattedLogs = this.get("formattedLogs"); - for (let i = this.get("index"), length = logs.length; i < length; i++) { - const date = logs[i].get("timestamp"), - message = escapeExpression(logs[i].get("message")); - formattedLogs += "[" + date + "] " + message + "\n"; - } - // update the formatted logs & cache index - this.setProperties({ - formattedLogs: formattedLogs, - index: logs.length - }); - // force rerender this.rerenderBuffer(); } + }, + + @on("init") + @observes("logs.[]") + _updateFormattedLogs: debounce(function() { + const logs = this.get("logs"); + if (logs.length === 0) return; + + // do the log formatting only once for HELLish performance + let formattedLogs = this.get("formattedLogs"); + for (let i = this.get("index"), length = logs.length; i < length; i++) { + const date = logs[i].get("timestamp"), + message = escapeExpression(logs[i].get("message")); + formattedLogs += "[" + date + "] " + message + "\n"; + } + // update the formatted logs & cache index + this.setProperties({ + formattedLogs: formattedLogs, + index: logs.length + }); + // force rerender + this.rerenderBuffer(); + Ember.run.scheduleOnce("afterRender", this, this._scrollDown); - }, 150) - .observes("logs.[]") - .on("init"), + }, 150), buildBuffer(buffer) { const formattedLogs = this.get("formattedLogs"); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 index 18237198ba..7bd96b326a 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 @@ -1,24 +1,17 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; -import Backup from "admin/models/backup"; export default Ember.Controller.extend(ModalFunctionality, { adminBackupsLogs: Ember.inject.controller(), - _startBackup(withUploads) { - this.currentUser.set("hideReadOnlyAlert", true); - Backup.start(withUploads).then(() => { - this.get("adminBackupsLogs.logs").clear(); - this.send("backupStarted"); - }); - }, - actions: { - startBackup() { - this._startBackup(); + startBackupWithUploads() { + this.send("closeModal"); + this.send("startBackup", true); }, - startBackupWithoutUpload() { - this._startBackup(false); + startBackupWithoutUploads() { + this.send("closeModal"); + this.send("startBackup", false); }, cancel() { diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 index 2d9f185cca..a0b9342c61 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -10,6 +10,7 @@ export default Discourse.Route.extend({ activate() { this.messageBus.subscribe(LOG_CHANNEL, log => { if (log.message === "[STARTED]") { + Discourse.User.currentProp("hideReadOnlyAlert", true); this.controllerFor("adminBackups").set( "model.isOperationRunning", true @@ -62,15 +63,14 @@ export default Discourse.Route.extend({ }, actions: { - startBackup() { + showStartBackupModal() { showModal("admin-start-backup", { admin: true }); this.controllerFor("modal").set("modalClass", "start-backup-modal"); }, - backupStarted() { - this.controllerFor("adminBackups").set("isOperationRunning", true); + startBackup(withUploads) { this.transitionTo("admin.backups.logs"); - this.send("closeModal"); + Backup.start(withUploads); }, destroyBackup(backup) { @@ -100,17 +100,8 @@ export default Discourse.Route.extend({ I18n.t("yes_value"), function(confirmed) { if (confirmed) { - Discourse.User.currentProp("hideReadOnlyAlert", true); - backup.restore().then(function() { - self - .controllerFor("adminBackupsLogs") - .get("logs") - .clear(); - self - .controllerFor("adminBackups") - .set("model.isOperationRunning", true); - self.transitionTo("admin.backups.logs"); - }); + self.transitionTo("admin.backups.logs"); + backup.restore(); } } ); diff --git a/app/assets/javascripts/admin/templates/backups.hbs b/app/assets/javascripts/admin/templates/backups.hbs index 2dcd6e02ba..fe7861839f 100644 --- a/app/assets/javascripts/admin/templates/backups.hbs +++ b/app/assets/javascripts/admin/templates/backups.hbs @@ -21,7 +21,7 @@ label="admin.backups.operations.cancel.label" icon="times"}} {{else}} - {{d-button action="startBackup" + {{d-button action="showStartBackupModal" class="btn-primary" title="admin.backups.operations.backup.title" label="admin.backups.operations.backup.label" diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs index 68f12bc404..6d3e0648cf 100644 --- a/app/assets/javascripts/admin/templates/customize-themes.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -1,8 +1,6 @@ {{#unless editingTheme}}
-
-

{{i18n 'admin.customize.theme.long_title'}}

-
+
{{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-start-backup.hbs b/app/assets/javascripts/admin/templates/modal/admin-start-backup.hbs index 250931599b..0990760888 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-start-backup.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-start-backup.hbs @@ -1,5 +1,5 @@ {{#d-modal-body title="admin.backups.operations.backup.confirm"}} - - + + {{/d-modal-body}} diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index be1f2d50da..61e771652e 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -15,7 +15,7 @@ {{#each group.buttons as |b|}} {{#if b.popupMenu}} {{toolbar-popup-menu-options - onPopupMenuAction=onPopupMenuAction + onSelect=onPopupMenuAction onExpand=(action b.action b) title=b.title headerIcon=b.icon diff --git a/app/assets/javascripts/discourse/templates/modal/change-owner.hbs b/app/assets/javascripts/discourse/templates/modal/change-owner.hbs index 480034cb68..62b55d40e3 100644 --- a/app/assets/javascripts/discourse/templates/modal/change-owner.hbs +++ b/app/assets/javascripts/discourse/templates/modal/change-owner.hbs @@ -1,11 +1,8 @@ {{#d-modal-body class='change-ownership'}} {{{i18n 'topic.change_owner.instructions' count=selectedPostsCount old_user=selectedPostsUsername}}} -

- {{{i18n 'topic.change_owner.instructions_warn'}}} -

- + {{user-selector single="true" usernames=new_user placeholderKey="topic.change_owner.placeholder" diff --git a/app/assets/javascripts/discourse/templates/preferences/account.hbs b/app/assets/javascripts/discourse/templates/preferences/account.hbs index e53f183169..84b58b05c7 100644 --- a/app/assets/javascripts/discourse/templates/preferences/account.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/account.hbs @@ -113,7 +113,7 @@ {{#if authProvider.method.can_revoke}} {{#conditional-loading-spinner condition=revoking size='small'}} - {{d-button action="revokeAccount" actionParam=authProvider.account title="user.associated_accounts.revoke" icon="trash" }} + {{d-button action="revokeAccount" actionParam=authProvider.account title="user.associated_accounts.revoke" class="btn-danger no-text" icon="trash" }} {{/conditional-loading-spinner}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/preferences/tags.hbs b/app/assets/javascripts/discourse/templates/preferences/tags.hbs index ef1dcc541f..6e7c273147 100644 --- a/app/assets/javascripts/discourse/templates/preferences/tags.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/tags.hbs @@ -8,7 +8,7 @@ {{tag-chooser tags=model.watched_tags blacklist=selectedTags - filterPlaceholder=null + filterPlaceholder="select_kit.filter_placeholder" allowCreate=false everyTag=true unlimitedTagCount=true}} @@ -20,7 +20,7 @@ {{tag-chooser tags=model.tracked_tags blacklist=selectedTags - filterPlaceholder=null + filterPlaceholder="select_kit.filter_placeholder" allowCreate=false everyTag=true unlimitedTagCount=true}} @@ -32,7 +32,7 @@ {{tag-chooser tags=model.watching_first_post_tags blacklist=selectedTags - filterPlaceholder=null + filterPlaceholder="select_kit.filter_placeholder" allowCreate=false everyTag=true unlimitedTagCount=true}} @@ -44,7 +44,7 @@ {{tag-chooser tags=model.muted_tags blacklist=selectedTags - filterPlaceholder=null + filterPlaceholder="select_kit.filter_placeholder" allowCreate=false everyTag=true unlimitedTagCount=true}} diff --git a/app/assets/javascripts/preload-application-data.js b/app/assets/javascripts/preload-application-data.js new file mode 100644 index 0000000000..58b138f670 --- /dev/null +++ b/app/assets/javascripts/preload-application-data.js @@ -0,0 +1,12 @@ +(function() { + var preloadedDataElement = document.getElementById("data-preloaded"); + + if (preloadedDataElement) { + var ps = require("preload-store").default; + var preloaded = JSON.parse(preloadedDataElement.dataset.preloaded); + + Object.keys(preloaded).forEach(function(key) { + ps.store(key, JSON.parse(preloaded[key])); + }); + } +})(); diff --git a/app/assets/javascripts/select-kit/components/category-drop/category-drop-header.js.es6 b/app/assets/javascripts/select-kit/components/category-drop/category-drop-header.js.es6 index 35f0e2adb3..9eb91db969 100644 --- a/app/assets/javascripts/select-kit/components/category-drop/category-drop-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-drop/category-drop-header.js.es6 @@ -42,16 +42,13 @@ export default ComboBoxSelectBoxHeaderComponent.extend({ if (categoryBackgroundColor || categoryTextColor) { let style = ""; if (categoryBackgroundColor) { - if (categoryStyle === "bar") { - style += `border-color: #${categoryBackgroundColor};`; - } else if (categoryStyle === "box") { - style += `background-color: #${categoryBackgroundColor};`; + if (categoryStyle === "box") { + style += `border-color: #${categoryBackgroundColor}; background-color: #${categoryBackgroundColor};`; if (categoryTextColor) { style += `color: #${categoryTextColor};`; } } } - return style.htmlSafe(); } } diff --git a/app/assets/javascripts/select-kit/components/single-select.js.es6 b/app/assets/javascripts/select-kit/components/single-select.js.es6 index 7713d51194..af3aa3a4f8 100644 --- a/app/assets/javascripts/select-kit/components/single-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/single-select.js.es6 @@ -210,6 +210,10 @@ export default SelectKitComponent.extend({ }, select(computedContentItem) { + if (this.get("hasSelection")) { + this.deselect(this.get("selection.value")); + } + if ( !computedContentItem || computedContentItem.__sk_row_type === "noneRow" diff --git a/app/assets/javascripts/select-kit/components/toolbar-popup-menu-options.js.es6 b/app/assets/javascripts/select-kit/components/toolbar-popup-menu-options.js.es6 index 770162e88e..39d8348119 100644 --- a/app/assets/javascripts/select-kit/components/toolbar-popup-menu-options.js.es6 +++ b/app/assets/javascripts/select-kit/components/toolbar-popup-menu-options.js.es6 @@ -12,10 +12,7 @@ export default DropdownSelectBoxComponent.extend({ return `

${title}

`; }, - mutateValue(value) { - this.sendAction("onPopupMenuAction", value); - this.setProperties({ value: null, highlighted: null }); - }, + autoHighlight() {}, computeContent(content) { return content diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index eba24bfd93..69269d1f12 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -550,8 +550,12 @@ $mobile-breakpoint: 700px; @include breakpoint(mobile) { margin: 0 -10px; } + label { + margin-bottom: 0; + } input { - margin: 0; + margin-right: 5px; + margin-bottom: 0; @include breakpoint(tablet) { max-width: 150px; } @@ -614,8 +618,6 @@ $mobile-breakpoint: 700px; } } .toggle { - margin-top: 8px; - float: right; span { font-weight: bold; } @@ -624,14 +626,6 @@ $mobile-breakpoint: 700px; display: inline-block; margin-right: 5px; } - #last-seen input[type="text"] { - float: none; - } - .ac-wrap { - display: inline-block; - vertical-align: middle; - padding: 0; - } .pull-right { padding-right: 10px; } diff --git a/app/assets/stylesheets/common/admin/backups.scss b/app/assets/stylesheets/common/admin/backups.scss index 48b3791d82..20b9836986 100644 --- a/app/assets/stylesheets/common/admin/backups.scss +++ b/app/assets/stylesheets/common/admin/backups.scss @@ -61,13 +61,8 @@ $rollback-darker: darken($rollback, 20%) !default; } .admin-backups-logs { - max-height: 500px; + max-height: 65vh; overflow: auto; - pre { - white-space: pre-wrap; - word-wrap: break-word; - max-height: 65vh; - } } button.ru { diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 2e4f9ffff7..970bddbc34 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -71,6 +71,10 @@ margin-bottom: 0; } } + + .create-actions { + margin-left: auto; + } } .admin-container { padding: 0; @@ -570,8 +574,16 @@ } } -.permalink-form .select-kit { - width: 150px; +.permalink-form { + display: flex; + align-items: center; + + .select-kit { + width: 150px; + } + input { + margin: 0 5px; + } } .permalink-title { diff --git a/app/assets/stylesheets/common/admin/emails.scss b/app/assets/stylesheets/common/admin/emails.scss index 2267abbc69..d7bb83c5de 100644 --- a/app/assets/stylesheets/common/admin/emails.scss +++ b/app/assets/stylesheets/common/admin/emails.scss @@ -1,13 +1,5 @@ // Styles for admin/emails -.email-preview { - .ac-wrap { - .item { - margin: 0.2em 0 0 0.4em; - } - } -} - // Emails .email-list { .filters input { diff --git a/app/assets/stylesheets/common/admin/settings.scss b/app/assets/stylesheets/common/admin/settings.scss index 90df1c5e43..2a8f4c22a3 100644 --- a/app/assets/stylesheets/common/admin/settings.scss +++ b/app/assets/stylesheets/common/admin/settings.scss @@ -39,8 +39,6 @@ } .input-setting-string, .input-setting-textarea { - box-sizing: border-box; - height: 30px; width: 100%; @media (max-width: $mobile-breakpoint) { width: 100%; diff --git a/app/assets/stylesheets/common/admin/staff_logs.scss b/app/assets/stylesheets/common/admin/staff_logs.scss index 883504061b..585b848e93 100644 --- a/app/assets/stylesheets/common/admin/staff_logs.scss +++ b/app/assets/stylesheets/common/admin/staff_logs.scss @@ -246,7 +246,7 @@ margin: 0 -0.25em 1em; display: flex; flex-wrap: wrap; - align-items: baseline; + align-items: center; } @media screen and (min-width: 800px) { .screened-ip-address-form { diff --git a/app/assets/stylesheets/common/admin/users.scss b/app/assets/stylesheets/common/admin/users.scss index 246dfc5c52..8169352589 100644 --- a/app/assets/stylesheets/common/admin/users.scss +++ b/app/assets/stylesheets/common/admin/users.scss @@ -78,14 +78,19 @@ margin-left: 0; } .btn { - margin: 2px 5px 2px 0; + margin-right: 5px; } } } -.admin-users .users-list { - .username .fa { - color: dark-light-choose($primary-medium, $secondary-medium); +.admin-users { + input { + margin-bottom: 0; + } + .users-list { + .username .fa { + color: dark-light-choose($primary-medium, $secondary-medium); + } } } diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index e365fbabf0..6fe89995f7 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -15,9 +15,9 @@ clear: both; margin-bottom: 5px; .combo-box .combo-box-header { - background: $primary-low; + background: $secondary; color: $primary; - border: 1px solid transparent; + border: 1px solid $primary-medium; padding: 5px 6px 5px 10px; font-size: $font-0; transition: none; diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 1d355ac9e8..18b898a8c5 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -222,7 +222,8 @@ } .add-warning { - margin-left: 1em; + margin-left: 0.75em; + margin-bottom: 0; display: flex; input { margin-right: 5px; @@ -399,8 +400,11 @@ div.ac-wrap { max-height: 150px; display: flex; flex-wrap: wrap; + align-items: center; background-color: $secondary; border: 1px solid $primary-medium; + min-height: 30px; + box-sizing: border-box; div.item { float: left; padding: 4px 10px; @@ -426,6 +430,7 @@ div.ac-wrap { border: 0; margin: 0; background: transparent; + min-height: unset; } } diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 6e353803b0..ad197a3a86 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -111,15 +111,17 @@ span.relative-date { } label { - display: block; + display: flex; margin-bottom: 5px; + align-items: flex-start; } input { &[type="radio"], &[type="checkbox"] { - margin: 3px 0; - line-height: $line-height-medium; + margin-top: 3px; + margin-right: 3px; + line-height: $line-height-small; cursor: pointer; } &[type="submit"], @@ -192,11 +194,13 @@ input { padding: $input-padding; margin-bottom: 9px; font-size: $font-0; - line-height: $line-height-large; + line-height: $line-height-small; color: $primary; background-color: $secondary; border: 1px solid $primary-medium; border-radius: 0; + box-sizing: border-box; + min-height: 30px; &:focus { border-color: $tertiary; box-shadow: shadow("focus"); diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 74faa11ce6..0582f57d6e 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -370,7 +370,8 @@ .edit-category-modal { input[type="number"] { - width: 50px; + min-width: 8em; + margin-bottom: 0; } .subcategory-list-style-field { @@ -468,7 +469,7 @@ .change-timestamp, .poll-ui-builder { .date-picker { - width: 9em; + min-width: 8em; } #date-container { diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index d71315f7ce..303883e5c2 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -161,6 +161,7 @@ .post-infos { display: flex; + flex: 0 0 auto; align-items: baseline; } } diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index e3a77bb605..0c8cec2f26 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -11,10 +11,12 @@ padding: 6px 12px; font-weight: 500; font-size: $font-0; - line-height: $line-height-medium; + line-height: $line-height-small; text-align: center; cursor: pointer; transition: all 0.25s; + box-sizing: border-box; + min-height: 30px; &:active, &.btn-active { @@ -50,6 +52,7 @@ &[href] { color: $primary; + min-height: unset; // ovverides button defaults } &:hover, &.btn-hover { diff --git a/app/assets/stylesheets/common/components/date-picker.scss b/app/assets/stylesheets/common/components/date-picker.scss index 801e7b3a06..b4fac1b2e8 100644 --- a/app/assets/stylesheets/common/components/date-picker.scss +++ b/app/assets/stylesheets/common/components/date-picker.scss @@ -14,7 +14,7 @@ .date-picker { text-align: center; - width: 80px; + width: 8em; margin: 0; } diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index c627545923..c1e295c998 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -30,7 +30,11 @@ padding: 6px 12px; color: $primary; font-size: $font-up-1; - line-height: $line-height-medium; + line-height: $line-height-small; + box-sizing: border-box; + min-height: 30px; + display: flex; + align-items: center; transition: background 0.15s; .d-icon { diff --git a/app/assets/stylesheets/common/select-kit/category-drop.scss b/app/assets/stylesheets/common/select-kit/category-drop.scss index e2b2105695..d61dccfa96 100644 --- a/app/assets/stylesheets/common/select-kit/category-drop.scss +++ b/app/assets/stylesheets/common/select-kit/category-drop.scss @@ -15,7 +15,7 @@ } &.bar.has-selection .category-drop-header { - border: none; + padding: 4px 5px 4px 10px; } &.box.has-selection .category-drop-header { diff --git a/app/assets/stylesheets/common/select-kit/combo-box.scss b/app/assets/stylesheets/common/select-kit/combo-box.scss index 39365ffa9c..d30e8724b8 100644 --- a/app/assets/stylesheets/common/select-kit/combo-box.scss +++ b/app/assets/stylesheets/common/select-kit/combo-box.scss @@ -18,11 +18,14 @@ } .select-kit-filter { - line-height: $line-height-medium; padding: $input-padding; border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + .spinner { + flex: 0 0 auto; + } + .filter-input, .filter-input:focus, .filter-input:active { @@ -41,8 +44,6 @@ padding: $input-padding; font-weight: 500; font-size: $font-0; - line-height: $line-height-large; - min-height: 2em; // when no content is available &.is-focused { border: 1px solid $tertiary; 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 9a4bb6b3b7..339ec4bb15 100644 --- a/app/assets/stylesheets/common/select-kit/mini-tag-chooser.scss +++ b/app/assets/stylesheets/common/select-kit/mini-tag-chooser.scss @@ -18,7 +18,6 @@ .select-kit-body { max-width: 32em; - width: 32em; } .select-kit-header { diff --git a/app/assets/stylesheets/common/select-kit/multi-select.scss b/app/assets/stylesheets/common/select-kit/multi-select.scss index 9b7e158aa9..f428b0bff9 100644 --- a/app/assets/stylesheets/common/select-kit/multi-select.scss +++ b/app/assets/stylesheets/common/select-kit/multi-select.scss @@ -18,6 +18,7 @@ .select-kit-filter { border: 0; flex: 1; + margin: 1px; } .multi-select-header { @@ -64,7 +65,7 @@ } .choices { - margin: 1px; + margin: 0 2px; box-sizing: border-box; display: flex; justify-content: flex-start; @@ -73,13 +74,11 @@ .choice { display: inline-flex; box-sizing: border-box; - padding: 0 5px; - border: 1px solid transparent; align-items: center; justify-content: space-between; flex-wrap: wrap; flex-direction: row; - margin: 1px; + margin: 1px 0px 2px 2px; } .filter { @@ -105,7 +104,7 @@ border: 0; box-shadow: none; border-radius: 0; - height: 21px; + min-height: unset; // overrides input defaults } } @@ -118,7 +117,7 @@ .color-preview { height: 5px; - margin: 0 2px 2px 2px; + margin: 0 2px 2px 0px; display: flex; width: 100%; } @@ -148,36 +147,18 @@ } .selected-name { - color: $primary; - background-clip: padding-box; - -webkit-touch-callout: none; - user-select: none; - background-color: $primary-low; - cursor: pointer; - outline: none; - line-height: $line-height-medium; - overflow: hidden; - flex: 0 1 auto; - flex-wrap: nowrap; - padding: 0; - display: flex; - flex-direction: column; - + flex: unset; .footer { display: flex; width: 100%; } .body { - display: flex; - align-items: center; - flex: 1; + background: $primary-low; + padding: 4px; } .name { - padding: 2px 4px; - line-height: $line-height-medium; - &:after { content: "\f00d"; color: $primary-low-mid; diff --git a/app/assets/stylesheets/common/select-kit/select-kit.scss b/app/assets/stylesheets/common/select-kit/select-kit.scss index 6a9fdb1617..ccf97fa606 100644 --- a/app/assets/stylesheets/common/select-kit/select-kit.scss +++ b/app/assets/stylesheets/common/select-kit/select-kit.scss @@ -66,10 +66,13 @@ align-items: center; justify-content: space-between; flex-direction: row; + min-height: 30px; + line-height: $line-height-small; .selected-name { text-align: left; flex: 1 1 auto; + padding: 1px 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; diff --git a/app/assets/stylesheets/desktop/groups.scss b/app/assets/stylesheets/desktop/groups.scss index b325241d7c..b66a1b8366 100644 --- a/app/assets/stylesheets/desktop/groups.scss +++ b/app/assets/stylesheets/desktop/groups.scss @@ -9,17 +9,3 @@ thead { padding-left: 2px; } } - -$filter-line-height: 1.5; - -.groups-header-filters { - .groups-header-filters-type { - .select-kit-header { - line-height: $filter-line-height; - } - } - - input { - line-height: $filter-line-height; - } -} diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index bcf73c5b47..c9470628d2 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -265,28 +265,34 @@ padding-top: 10px; padding-left: 30px; + .form-vertical { + width: 400px; + max-width: 100%; + } + h3 { color: $primary; margin: 20px 0 10px 0; } - textarea { - width: 530px; - } - .category-selector, - .tag-chooser { - width: 530px; + .tag-chooser, + textarea { + width: 100%; } input { &.user-selector { - width: 530px; + width: 100%; } + } - &[type="text"] { - @include breakpoint(medium) { - width: 450px; + .tag-controls, + .category-controls { + label { + align-items: center; + .d-icon { + margin-right: 3px; } } } @@ -314,4 +320,12 @@ .user-main & .user-field.text { padding-top: 0; } + + .image-upload-controls { + display: flex; + align-items: center; + .btn { + margin-right: 5px; + } + } } diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index d9c3f14e12..f7320c1fe9 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -19,6 +19,7 @@ box-sizing: border-box; display: flex; align-self: stretch; + align-items: center; margin: 0 3px 10px 3px; order: 10; // always last for consistent placement } diff --git a/app/controllers/admin/diagnostics_controller.rb b/app/controllers/admin/diagnostics_controller.rb deleted file mode 100644 index 81b076b072..0000000000 --- a/app/controllers/admin/diagnostics_controller.rb +++ /dev/null @@ -1,42 +0,0 @@ -require_dependency 'memory_diagnostics' - -class Admin::DiagnosticsController < Admin::AdminController - layout false - skip_before_action :check_xhr - - def memory_stats - text = nil - - if params.key?(:diff) - if !MemoryDiagnostics.snapshot_exists? - text = "No initial snapshot exists" - else - text = MemoryDiagnostics.compare - end - elsif params.key?(:snapshot) - MemoryDiagnostics.snapshot_current_process - text = "Writing snapshot to: #{MemoryDiagnostics.snapshot_filename}\n\nTo get a diff use ?diff=1" - else - text = MemoryDiagnostics.memory_report(class_report: params.key?(:full)) - end - - render plain: text - end - - def dump_heap - begin - # ruby 2.1 - GC.start(full_mark: true) - require 'objspace' - - io = File.open("discourse-heap-#{SecureRandom.hex(3)}.json", 'w') - ObjectSpace.dump_all(output: io) - io.close - - render plain: "HEAP DUMP:\n#{io.path}" - rescue - render plain: "HEAP DUMP:\nnot supported" - end - end - -end diff --git a/app/controllers/export_csv_controller.rb b/app/controllers/export_csv_controller.rb index 851d164368..d55053630a 100644 --- a/app/controllers/export_csv_controller.rb +++ b/app/controllers/export_csv_controller.rb @@ -5,6 +5,7 @@ class ExportCsvController < ApplicationController def export_entity guardian.ensure_can_export_entity!(export_params[:entity]) Jobs.enqueue(:export_csv_file, entity: export_params[:entity], user_id: current_user.id, args: export_params[:args]) + StaffActionLogger.new(current_user).log_entity_export(export_params[:entity]) render json: success_json end diff --git a/app/controllers/tag_groups_controller.rb b/app/controllers/tag_groups_controller.rb index 4fd4f0f6e0..d0ee16587c 100644 --- a/app/controllers/tag_groups_controller.rb +++ b/app/controllers/tag_groups_controller.rb @@ -1,5 +1,7 @@ class TagGroupsController < ApplicationController - requires_login except: [:index, :show] + + requires_login + before_action :ensure_staff skip_before_action :check_xhr, only: [:index, :show] before_action :fetch_tag_group, only: [:show, :update, :destroy] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8268d2594d..44df9dad04 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -865,16 +865,19 @@ class UsersController < ApplicationController end end - user.uploaded_avatar_id = upload_id + upload = Upload.find_by(id: upload_id) + + # old safeguard + user.create_user_avatar unless user.user_avatar + + guardian.ensure_can_pick_avatar!(user.user_avatar, upload) if AVATAR_TYPES_WITH_UPLOAD.include?(type) - # make sure the upload exists - unless Upload.where(id: upload_id).exists? + + if !upload return render_json_error I18n.t("avatar.missing") end - user.create_user_avatar unless user.user_avatar - if type == "gravatar" user.user_avatar.gravatar_upload_id = upload_id else @@ -882,6 +885,7 @@ class UsersController < ApplicationController end end + user.uploaded_avatar_id = upload_id user.save! user.user_avatar.save! diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f775b6e842..071e6bf250 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -129,7 +129,7 @@ module ApplicationHelper javascript = javascript.scrub javascript.gsub!(/\342\200\250/u, '
') javascript.gsub!(/(<\/)/u, '\u003C/') - javascript.html_safe + javascript else '' end @@ -401,4 +401,9 @@ module ApplicationHelper Stylesheet::Manager.stylesheet_link_tag(name, 'all', ids) end + + def preloaded_json + return '{}' if @preloaded.blank? + @preloaded.transform_values { |value| escape_unicode(value) }.to_json + end end diff --git a/app/jobs/onceoff.rb b/app/jobs/onceoff.rb index cee3608902..284557ee55 100644 --- a/app/jobs/onceoff.rb +++ b/app/jobs/onceoff.rb @@ -19,7 +19,7 @@ class Jobs::Onceoff < Jobs::Base begin return if OnceoffLog.where(job_name: job_name).exists? && !args[:force] execute_onceoff(args) - OnceoffLog.create(job_name: job_name) + OnceoffLog.create!(job_name: job_name) ensure $redis.del(running_key_name) if has_lock end diff --git a/app/jobs/onceoff/recover_post_uploads.rb b/app/jobs/onceoff/post_uploads_recovery.rb similarity index 89% rename from app/jobs/onceoff/recover_post_uploads.rb rename to app/jobs/onceoff/post_uploads_recovery.rb index ef3fb0b5aa..9c5862e220 100644 --- a/app/jobs/onceoff/recover_post_uploads.rb +++ b/app/jobs/onceoff/post_uploads_recovery.rb @@ -1,7 +1,7 @@ require_dependency "upload_recovery" module Jobs - class RecoverPostUploads < Jobs::Onceoff + class PostUploadsRecovery < Jobs::Onceoff MIN_PERIOD = 30 MAX_PERIOD = 120 diff --git a/app/models/group.rb b/app/models/group.rb index 1e38e15ce9..a07ddad2b5 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -25,7 +25,6 @@ class Group < ActiveRecord::Base before_save :cook_bio after_save :destroy_deletions - after_save :automatic_group_membership after_save :update_primary_group after_save :update_title @@ -35,6 +34,7 @@ class Group < ActiveRecord::Base after_save :expire_cache after_destroy :expire_cache + after_commit :automatic_group_membership, on: [:create, :update] after_commit :trigger_group_created_event, on: :create after_commit :trigger_group_updated_event, on: :update after_commit :trigger_group_destroyed_event, on: :destroy diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index 55e4080a02..762faafea4 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -38,18 +38,23 @@ class OptimizedImage < ActiveRecord::Base end end + # prefer to look up the thumbnail without grabbing any locks + thumbnail = find_by(upload_id: upload.id, width: width, height: height) + + # correct bad thumbnail if needed + if thumbnail && thumbnail.url.blank? + thumbnail.destroy! + thumbnail = nil + end + + return thumbnail if thumbnail + lock(upload.id, width, height) do - # do we already have that thumbnail? + # may have been generated since we got the lock thumbnail = find_by(upload_id: upload.id, width: width, height: height) - # make sure we have an url - if thumbnail && thumbnail.url.blank? - thumbnail.destroy - thumbnail = nil - end - # return the previous thumbnail if any - return thumbnail unless thumbnail.nil? + return thumbnail if thumbnail # create the thumbnail otherwise original_path = Discourse.store.path_for(upload) diff --git a/app/models/topic.rb b/app/models/topic.rb index c0bbb02163..e2e0fb449c 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -221,8 +221,9 @@ class Topic < ActiveRecord::Base unless skip_callbacks ensure_topic_has_a_category end + if title_changed? - write_attribute :fancy_title, Topic.fancy_title(title) + write_attribute(:fancy_title, Topic.fancy_title(title)) end if category_id_changed? || new_record? @@ -346,10 +347,9 @@ class Topic < ActiveRecord::Base end def self.fancy_title(title) - escaped = ERB::Util.html_escape(title) - return unless escaped + return unless escaped = ERB::Util.html_escape(title) fancy_title = Emoji.unicode_unescape(HtmlPrettify.render(escaped)) - fancy_title.length > Topic.max_fancy_title_length ? title : fancy_title + fancy_title.length > Topic.max_fancy_title_length ? escaped : fancy_title end def fancy_title diff --git a/app/models/upload.rb b/app/models/upload.rb index 947481cb2f..90dd0aa274 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -15,6 +15,7 @@ class Upload < ActiveRecord::Base has_many :posts, through: :post_uploads has_many :optimized_images, dependent: :destroy + has_many :user_uploads, dependent: :destroy attr_accessor :for_group_message attr_accessor :for_theme @@ -168,6 +169,10 @@ class Upload < ActiveRecord::Base Digest::SHA1.file(path).hexdigest end + def self.extract_upload_url(url) + url.match(/(\/original\/\dX[\/\.\w]*\/([a-zA-Z0-9]+)[\.\w]*)/) + end + def self.get_from_url(url) return if url.blank? @@ -177,7 +182,7 @@ class Upload < ActiveRecord::Base end return if uri&.path.blank? - data = uri.path.match(/(\/original\/\dX[\/\.\w]*\/([a-zA-Z0-9]+)[\.\w]*)/) + data = extract_upload_url(uri.path) return if data.blank? sha1 = data[2] upload = nil diff --git a/app/models/user.rb b/app/models/user.rb index 3062c9678f..581528e75f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -53,6 +53,7 @@ class User < ActiveRecord::Base has_many :groups, through: :group_users has_many :secure_categories, through: :groups, source: :categories + has_many :user_uploads, dependent: :destroy has_many :user_emails, dependent: :destroy has_one :primary_email, -> { where(primary: true) }, class_name: 'UserEmail', dependent: :destroy diff --git a/app/models/user_history.rb b/app/models/user_history.rb index 0ce8a8da6d..1aaf9c8dfc 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -81,7 +81,8 @@ class UserHistory < ActiveRecord::Base removed_unsilence_user: 62, removed_unsuspend_user: 63, post_rejected: 64, - merge_user: 65 + merge_user: 65, + entity_export: 66 ) end @@ -141,7 +142,8 @@ class UserHistory < ActiveRecord::Base :change_badge, :delete_badge, :post_rejected, - :merge_user + :merge_user, + :entity_export ] end diff --git a/app/models/user_upload.rb b/app/models/user_upload.rb new file mode 100644 index 0000000000..a72de8a0c6 --- /dev/null +++ b/app/models/user_upload.rb @@ -0,0 +1,4 @@ +class UserUpload < ActiveRecord::Base + belongs_to :upload + belongs_to :user +end diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index f76b9037e8..7224087368 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -11,8 +11,8 @@ class SearchIndexer @disabled = false end - def self.scrub_html_for_search(html) - HtmlScrubber.scrub(html) + def self.scrub_html_for_search(html, strip_diacritics: SiteSetting.search_ignore_accents) + HtmlScrubber.scrub(html, strip_diacritics: strip_diacritics) end def self.inject_extra_terms(raw) @@ -169,18 +169,10 @@ class SearchIndexer DIACRITICS ||= /([\u0300-\u036f]|[\u1AB0-\u1AFF]|[\u1DC0-\u1DFF]|[\u20D0-\u20FF])/ - def self.strip_diacritics(str) - s = str.unicode_normalize(:nfkd) - s.gsub!(DIACRITICS, "") - s.strip! - s - end - attr_reader :scrubbed def initialize(strip_diacritics: false) @scrubbed = +"" - # for now we are disabling this per: https://meta.discourse.org/t/discourse-should-ignore-if-a-character-is-accented-when-doing-a-search/90198/16?u=sam @strip_diacritics = strip_diacritics end @@ -189,7 +181,7 @@ class SearchIndexer me = new(strip_diacritics: strip_diacritics) Nokogiri::HTML::SAX::Parser.new(me).parse("
#{html}
") - me.scrubbed + me.scrubbed.squish end ATTRIBUTES ||= %w{alt title href data-youtube-title} @@ -204,8 +196,15 @@ class SearchIndexer end end + def strip_diacritics(str) + s = str.unicode_normalize(:nfkd) + s.gsub!(DIACRITICS, "") + s.strip! + s + end + def characters(str) - str = HtmlScrubber.strip_diacritics(str) if @strip_diacritics + str = strip_diacritics(str) if @strip_diacritics scrubbed << " #{str} " end end diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index aa2338b305..6ebb5af059 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -459,6 +459,14 @@ class StaffActionLogger )) end + def log_entity_export(entity, opts = {}) + UserHistory.create!(params(opts).merge( + action: UserHistory.actions[:entity_export], + ip_address: @admin.ip_address.to_s, + subject: entity + )) + end + def log_backup_download(backup, opts = {}) raise Discourse::InvalidParameters.new(:backup) unless backup UserHistory.create!(params(opts).merge( diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b815383d67..9dc4e992a3 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -55,6 +55,9 @@ <%= render partial: "common/discourse_stylesheet" %> <%= render partial: "common/special_font_face" %> + + <%= preload_script "preload-application-data" %> + <%= yield :head %> <%= build_plugin_html 'server:before-head-close' %> @@ -104,17 +107,6 @@
<% end %> - <%- if @preloaded.present? %> - - <%- end %> - <%= yield :data %> <%= render :partial => "common/discourse_javascript" %> diff --git a/config/application.rb b/config/application.rb index 8687840329..9c23554c14 100644 --- a/config/application.rb +++ b/config/application.rb @@ -119,6 +119,7 @@ module Discourse service-worker.js google-tag-manager.js google-universal-analytics.js + preload-application-data.js } # Precompile all available locales diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 2593e0c1f1..d6384ef418 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -1806,7 +1806,7 @@ bs_BA: action: "spoji izabrane postove" error: "Desila se greška prilikom spajanja označenih objava." change_owner: - title: "Change Owner of Posts" + title: "Change Owner" action: "change ownership" error: "There was an error changing the ownership of the posts." label: "New Owner of Posts" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 808cc1cc9a..39343bdb12 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2000,15 +2000,13 @@ en: error: "There was an error merging the selected posts." change_owner: - title: "Change Owner of Posts" + title: "Change Owner" action: "change ownership" error: "There was an error changing the ownership of the posts." - label: "New Owner of Posts" placeholder: "username of new owner" instructions: - one: "Please choose the new owner of the post by {{old_user}}." - other: "Please choose the new owner of the {{count}} posts by {{old_user}}." - instructions_warn: "Note that any notifications about this post will not be transferred to the new user retroactively." + one: "Please choose a new owner for the post by @{{old_user}}" + other: "Please choose a new owner for the {{count}} posts by @{{old_user}}" change_timestamp: title: "Change Timestamp..." @@ -3522,6 +3520,7 @@ en: change_badge: "change badge" delete_badge: "delete badge" merge_user: "merge user" + entity_export: "export entity" 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/server.en.yml b/config/locales/server.en.yml index 2a26a8c303..cf2f6ef211 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1144,6 +1144,7 @@ en: log_search_queries: "Log search queries performed by users" search_query_log_max_size: "Maximum amount of search queries to keep" search_query_log_max_retention_days: "Maximum amount of time to keep search queries, in days." + search_ignore_accents: "Ignore accents when searching for text." 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." unique_posts_mins: "How many minutes before a user can make a post with the same content again" diff --git a/config/routes.rb b/config/routes.rb index c661b0a727..631c0dcaaf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -290,9 +290,6 @@ Discourse::Application.routes.draw do post "preview" => "badges#preview" end end - - get "memory_stats" => "diagnostics#memory_stats", constraints: AdminConstraint.new - get "dump_heap" => "diagnostics#dump_heap", constraints: AdminConstraint.new end # admin namespace get "email_preferences" => "email#preferences_redirect", :as => "email_preferences_redirect" @@ -793,7 +790,7 @@ Discourse::Application.routes.draw do end end - resources :tag_groups, except: [:new, :edit] do + resources :tag_groups, constraints: StaffConstraint.new, except: [:new, :edit] do collection do get '/filter/search' => 'tag_groups#search' end diff --git a/config/site_settings.yml b/config/site_settings.yml index 91d17c9730..ac6cb09937 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -934,6 +934,7 @@ files: clean_orphan_uploads_grace_period_hours: 48 purge_deleted_uploads_grace_period_days: default: 30 + shadowed_by_global: true prevent_anons_from_downloading_files: default: false client: true @@ -1431,7 +1432,6 @@ search: zh_TW: 2 ko: 2 ja: 2 - search_tokenize_chinese_japanese_korean: false search_prefer_recent_posts: false search_recent_posts_size: @@ -1446,6 +1446,22 @@ search: search_query_log_max_retention_days: default: 365 # 1 year max: 1825 # 5 years + search_ignore_accents: + default: false + locale_default: + ar: true + ca: true + cs: true + el: true + es: true + fa_IR: true + fr: true + hu: true + pt: true + pt_BR: true + ro: true + sk: true + tr_TR: true uncategorized: version_checks: diff --git a/db/migrate/20180920042415_create_user_uploads.rb b/db/migrate/20180920042415_create_user_uploads.rb new file mode 100644 index 0000000000..034bc02bf6 --- /dev/null +++ b/db/migrate/20180920042415_create_user_uploads.rb @@ -0,0 +1,22 @@ +class CreateUserUploads < ActiveRecord::Migration[5.2] + def up + create_table :user_uploads do |t| + t.integer :upload_id, null: false + t.integer :user_id, null: false + t.datetime :created_at, null: false + end + + add_index :user_uploads, [:upload_id, :user_id], unique: true + + execute <<~SQL + INSERT INTO user_uploads(upload_id, user_id, created_at) + SELECT id, user_id, COALESCE(created_at, current_timestamp) + FROM uploads + WHERE user_id IS NOT NULL + SQL + end + + def down + drop_table :user_uploads + end +end diff --git a/lib/backup_restore/backuper.rb b/lib/backup_restore/backuper.rb index 50b52e08c3..e7fdc04ed6 100644 --- a/lib/backup_restore/backuper.rb +++ b/lib/backup_restore/backuper.rb @@ -54,13 +54,10 @@ module BackupRestore @success = true File.join(@archive_directory, @backup_filename) ensure - begin - notify_user - remove_old - clean_up - rescue => ex - Rails.logger.error("#{ex}\n" + ex.backtrace.join("\n")) - end + remove_old + clean_up + notify_user + log "Finished!" @success ? log("[SUCCESS]") : log("[FAILED]") end @@ -255,6 +252,8 @@ module BackupRestore def remove_old log "Removing old backups..." Backup.remove_old + rescue => ex + log "Something went wrong while removing old backups.", ex end def notify_user @@ -270,6 +269,8 @@ module BackupRestore end post + rescue => ex + log "Something went wrong while notifying user.", ex end def clean_up @@ -279,42 +280,49 @@ module BackupRestore disable_readonly_mode if Discourse.readonly_mode? mark_backup_as_not_running refresh_disk_space - log "Finished!" end def refresh_disk_space - log "Refreshing disk cache..." + log "Refreshing disk stats..." DiskSpace.reset_cached_stats + rescue => ex + log "Something went wrong while refreshing disk stats.", ex end def remove_tar_leftovers log "Removing '.tar' leftovers..." Dir["#{@archive_directory}/*.tar"].each { |filename| File.delete(filename) } + rescue => ex + log "Something went wrong while removing '.tar' leftovers.", ex end def remove_tmp_directory log "Removing tmp '#{@tmp_directory}' directory..." FileUtils.rm_rf(@tmp_directory) if Dir[@tmp_directory].present? - rescue - log "Something went wrong while removing the following tmp directory: #{@tmp_directory}" + rescue => ex + log "Something went wrong while removing the following tmp directory: #{@tmp_directory}", ex end def unpause_sidekiq log "Unpausing sidekiq..." Sidekiq.unpause! - rescue - log "Something went wrong while unpausing Sidekiq." + rescue => ex + log "Something went wrong while unpausing Sidekiq.", ex end def disable_readonly_mode return if @readonly_mode_was_enabled log "Disabling readonly mode..." Discourse.disable_readonly_mode + rescue => ex + log "Something went wrong while disabling readonly mode.", ex end def mark_backup_as_not_running log "Marking backup as finished..." BackupRestore.mark_as_not_running! + rescue => ex + log "Something went wrong while marking backup as finished.", ex end def ensure_directory_exists(directory) @@ -322,11 +330,12 @@ module BackupRestore FileUtils.mkdir_p(directory) end - def log(message) + def log(message, ex = nil) timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S") puts(message) publish_log(message, timestamp) save_log(message, timestamp) + Rails.logger.error("#{ex}\n" + ex.backtrace.join("\n")) if ex end def publish_log(message, timestamp) diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index a546a3072c..e8a98f0581 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -103,12 +103,9 @@ module BackupRestore else @success = true ensure - begin - notify_user - clean_up - rescue => ex - Rails.logger.error("#{ex}\n" + ex.backtrace.join("\n")) - end + clean_up + notify_user + log "Finished!" @success ? log("[SUCCESS]") : log("[FAILED]") end @@ -459,6 +456,8 @@ module BackupRestore else log "Could not send notification to '#{@user_info[:username]}' (#{@user_info[:email]}), because the user does not exists..." end + rescue => ex + log "Something went wrong while notifying user.", ex end def clean_up @@ -467,32 +466,35 @@ module BackupRestore unpause_sidekiq disable_readonly_mode if Discourse.readonly_mode? mark_restore_as_not_running - log "Finished!" end def remove_tmp_directory log "Removing tmp '#{@tmp_directory}' directory..." FileUtils.rm_rf(@tmp_directory) if Dir[@tmp_directory].present? - rescue - log "Something went wrong while removing the following tmp directory: #{@tmp_directory}" + rescue => ex + log "Something went wrong while removing the following tmp directory: #{@tmp_directory}", ex end def unpause_sidekiq log "Unpausing sidekiq..." Sidekiq.unpause! - rescue - log "Something went wrong while unpausing Sidekiq." + rescue => ex + log "Something went wrong while unpausing Sidekiq.", ex end def disable_readonly_mode return if @readonly_mode_was_enabled log "Disabling readonly mode..." Discourse.disable_readonly_mode + rescue => ex + log "Something went wrong while disabling readonly mode.", ex end def mark_restore_as_not_running log "Marking restore as finished..." BackupRestore.mark_as_not_running! + rescue => ex + log "Something went wrong while marking restore as finished.", ex end def ensure_directory_exists(directory) @@ -500,11 +502,12 @@ module BackupRestore FileUtils.mkdir_p(directory) end - def log(message) + def log(message, ex = nil) timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S") puts(message) publish_log(message, timestamp) save_log(message, timestamp) + Rails.logger.error("#{ex}\n" + ex.backtrace.join("\n")) if ex end def publish_log(message, timestamp) diff --git a/lib/distributed_mutex.rb b/lib/distributed_mutex.rb index b014a554a3..d47f059d0f 100644 --- a/lib/distributed_mutex.rb +++ b/lib/distributed_mutex.rb @@ -7,15 +7,28 @@ class DistributedMutex def initialize(key, redis = nil) @key = key + @using_global_redis = true if !redis @redis = redis || $redis @mutex = Mutex.new end + CHECK_READONLY_ATTEMPT ||= 10 + # NOTE wrapped in mutex to maintain its semantics def synchronize + @mutex.lock + attempts = 0 + while !try_to_get_lock sleep 0.001 + # in readonly we will never be able to get a lock + if @using_global_redis && Discourse.recently_readonly? + attempts += 1 + if attempts > CHECK_READONLY_ATTEMPT + raise Discourse::ReadOnly + end + end end yield diff --git a/lib/file_store/local_store.rb b/lib/file_store/local_store.rb index 9b7148efc8..6d4e733a35 100644 --- a/lib/file_store/local_store.rb +++ b/lib/file_store/local_store.rb @@ -17,6 +17,7 @@ module FileStore dir = Pathname.new(destination).dirname FileUtils.mkdir_p(dir) unless Dir.exists?(dir) FileUtils.move(source, destination, force: true) + FileUtils.touch(destination) end def has_been_uploaded?(url) diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index 0aef85c991..32dc680908 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -1,6 +1,23 @@ # mixin for all Guardian methods dealing with user permissions module UserGuardian + def can_pick_avatar?(user_avatar, upload) + return false unless self.user + + return true if is_admin? + + # can always pick blank avatar + return true if !upload + + return true if user_avatar.contains_upload?(upload.id) + return true if upload.user_id == user_avatar.user_id || upload.user_id == user.id + + UserUpload.exists?( + upload_id: upload.id, + user_id: [upload.user_id, user.id] + ) + end + def can_edit_user?(user) is_me?(user) || is_staff? end diff --git a/lib/memory_diagnostics.rb b/lib/memory_diagnostics.rb deleted file mode 100644 index 3a3fcd60f5..0000000000 --- a/lib/memory_diagnostics.rb +++ /dev/null @@ -1,169 +0,0 @@ -module MemoryDiagnostics - - def self.snapshot_exists? - File.exists?(snapshot_filename) - end - - def self.compare(from = nil, to = nil) - - from ||= snapshot_filename - if !to - filename = snapshot_filename + ".new" - snapshot_current_process(filename) - to = filename - end - - from = Marshal::load(IO.binread(from)); - to = Marshal::load(IO.binread(to)); - - diff = from - to - - require 'objspace' - diff = diff.map do |id| - ObjectSpace._id2ref(id) rescue nil - end - diff.compact! - - report = "#{diff.length} objects have leaked\n" - - report << "Summary:\n" - - summary = {} - diff.each do |obj| - begin - summary[obj.class] ||= 0 - summary[obj.class] += 1 - rescue - # don't care - end - end - - report << summary.sort { |a, b| b[1] <=> a[1] }[0..50].map { |k, v| - "#{k}: #{v}" - }.join("\n") - - report << "\n\nSample Items:\n" - - diff[0..5000].each do |v| - report << "#{v.class}: #{String === v ? v[0..300] : (40 + ObjectSpace.memsize_of(v)).to_s + " bytes"}\n" rescue nil - end - - report - end - - def self.snapshot_path - "#{Rails.root}/tmp/mem_snapshots" - end - - def self.snapshot_filename - "#{snapshot_path}/#{Process.pid}.snapshot" - end - - def self.snapshot_current_process(filename = nil) - filename ||= snapshot_filename - pid = fork do - snapshot(filename) - end - - Process.wait(pid) - end - - def self.snapshot(filename) - require 'objspace' - FileUtils.mkdir_p snapshot_path - object_ids = [] - - full_gc - - ObjectSpace.each_object do |o| - begin - object_ids << o.object_id - rescue - # skip - end - end - - IO.binwrite(filename, Marshal::dump(object_ids)) - end - - def self.memory_report(opts = {}) - begin - # ruby 2.1 - GC.start(full_mark: true) - rescue - GC.start - end - - classes = {} - large_objects = [] - - if opts[:class_report] - require 'objspace' - ObjectSpace.each_object do |o| - begin - classes[o.class] ||= 0 - classes[o.class] += 1 - if (size = ObjectSpace.memsize_of(o)) > 200 - large_objects << [size, o] - end - rescue - # all sorts of stuff can happen here BasicObject etc. - classes[:unknown] ||= 0 - classes[:unknown] += 1 - end - end - classes = classes.sort { |a, b| b[1] <=> a[1] }[0..40].map { |klass, count| "#{klass}: #{count}" } - - classes << "\nLarge Objects (#{large_objects.length} larger than 200 bytes total size #{large_objects.map { |x, _| x }.sum}):\n" - - classes += large_objects.sort { |a, b| b[0] <=> a[0] }[0..800].map do |size, object| - rval = "#{object.class}: size #{size}" - rval << " " << object.to_s[0..500].gsub("\n", "") if (String === object) || (Regexp === object) - rval << "\n" - rval - end - end - - stats = GC.stat.map { |k, v| "#{k}: #{v}" } - counts = ObjectSpace.count_objects.sort { |a, b| b[1] <=> a[1] }.map { |k, v| "#{k}: #{v}" } - - < 0 ? classes.join("\n") : "Class report omitted use ?full=1 to include it"} - -TEXT - - end - - def self.full_gc - # gc start may not collect everything - GC.start while new_count = decreased_count(new_count) - end - - def self.decreased_count(old) - count = count_objects - if !old || count < old - count - else - nil - end - end - - def self.count_objects - i = 0 - ObjectSpace.each_object do |obj| - i += 1 - end - end -end diff --git a/lib/oneboxer.rb b/lib/oneboxer.rb index a0f48d43b3..fee360a87f 100644 --- a/lib/oneboxer.rb +++ b/lib/oneboxer.rb @@ -248,13 +248,15 @@ module Oneboxer end end + def self.blacklisted_domains + SiteSetting.onebox_domains_blacklist.split("|") + end + def self.external_onebox(url) Rails.cache.fetch(onebox_cache_key(url), expires_in: 1.day) do - ignored = SiteSetting.onebox_domains_blacklist.split("|") - - fd = FinalDestination.new(url, ignore_redirects: ignore_redirects, ignore_hostnames: ignored, force_get_hosts: force_get_hosts) + fd = FinalDestination.new(url, ignore_redirects: ignore_redirects, ignore_hostnames: blacklisted_domains, force_get_hosts: force_get_hosts) uri = fd.resolve - return blank_onebox if uri.blank? || ignored.map { |hostname| uri.hostname.match?(hostname) }.any? + return blank_onebox if uri.blank? || blacklisted_domains.map { |hostname| uri.hostname.match?(hostname) }.any? options = { cache: {}, diff --git a/lib/promotion.rb b/lib/promotion.rb index c6527b47de..84b56783d4 100644 --- a/lib/promotion.rb +++ b/lib/promotion.rb @@ -24,7 +24,7 @@ class Promotion def review_tl0 if Promotion.tl1_met?(@user) && change_trust_level!(TrustLevel[1]) - @user.enqueue_member_welcome_message + @user.enqueue_member_welcome_message unless @user.badges.where(id: Badge::BasicUser).count > 0 return true end false diff --git a/lib/search/grouped_search_results.rb b/lib/search/grouped_search_results.rb index 04ad93fa13..1f81e43e2c 100644 --- a/lib/search/grouped_search_results.rb +++ b/lib/search/grouped_search_results.rb @@ -63,13 +63,14 @@ class Search end def self.blurb_for(cooked, term = nil, blurb_length = 200) - cooked = SearchIndexer::HtmlScrubber.scrub(cooked).squish - blurb = nil + cooked = SearchIndexer.scrub_html_for_search(cooked) + if term terms = term.split(/\s+/) blurb = TextHelper.excerpt(cooked, terms.first, radius: blurb_length / 2, seperator: " ") end + blurb = TextHelper.truncate(cooked, length: blurb_length, seperator: " ") if blurb.blank? Sanitize.clean(blurb) end diff --git a/lib/socket_server.rb b/lib/socket_server.rb index fda521ec0a..3286548957 100644 --- a/lib/socket_server.rb +++ b/lib/socket_server.rb @@ -70,7 +70,7 @@ class SocketServer rescue IOError, Errno::EPIPE # nothing to do here, case its normal on shutdown rescue => e - Rails.logger.warn("Failed to handle connection in stats socket #{e}:\n#{e.backtrace.join("\n")}") + Rails.logger.warn("Failed to handle connection #{e}:\n#{e.backtrace.join("\n")}") ensure socket&.close end diff --git a/lib/upload_creator.rb b/lib/upload_creator.rb index 2b138950e7..04c3e670ec 100644 --- a/lib/upload_creator.rb +++ b/lib/upload_creator.rb @@ -74,7 +74,10 @@ class UploadCreator end # return the previous upload if any - return @upload unless @upload.nil? + if @upload + UserUpload.find_or_create_by!(user_id: user_id, upload_id: @upload.id) if user_id + return @upload + end fixed_original_filename = nil if is_image @@ -132,6 +135,10 @@ class UploadCreator Jobs.enqueue(:create_avatar_thumbnails, upload_id: @upload.id, user_id: user_id) end + if @upload.errors.empty? + UserUpload.find_or_create_by!(user_id: user_id, upload_id: @upload.id) if user_id + end + @upload end ensure diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index d2a584fa88..ef13002d27 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -4,22 +4,40 @@ class UploadRecovery end def recover(posts = Post) - posts.where("raw LIKE '%upload:\/\/%'").find_each do |post| + posts.where("raw LIKE '%upload:\/\/%' OR raw LIKE '%href=%'").find_each do |post| begin analyzer = PostAnalyzer.new(post.raw, post.topic_id) - analyzer.cooked_stripped.css("img").each do |img| - if dom_class = img["class"] - if (Post.white_listed_image_classes & dom_class.split).count > 0 - next + analyzer.cooked_stripped.css("img", "a").each do |media| + if media.name == "img" + if dom_class = media["class"] + if (Post.white_listed_image_classes & dom_class.split).count > 0 + next + end end - end - if img["data-orig-src"] - if @dry_run - puts "#{post.full_url} #{img["data-orig-src"]}" - else - recover_post_upload(post, img["data-orig-src"]) + orig_src = media["data-orig-src"] + + if orig_src + if @dry_run + puts "#{post.full_url} #{orig_src}" + else + recover_post_upload(post, Upload.sha1_from_short_url(orig_src)) + end + end + elsif media.name == "a" + href = media["href"] + + if href && data = Upload.extract_upload_url(href) + sha1 = data[2] + + unless upload = Upload.get_from_url(href) + if @dry_run + puts "#{post.full_url} #{href}" + else + recover_post_upload(post, sha1) + end + end end end end @@ -32,9 +50,8 @@ class UploadRecovery private - def recover_post_upload(post, short_url) - sha1 = Upload.sha1_from_short_url(short_url) - return unless sha1.present? + def recover_post_upload(post, sha1) + return unless sha1.present? && sha1.length == Upload::SHA1_LENGTH attributes = { post: post, @@ -73,10 +90,12 @@ class UploadRecovery @paths.each do |path| if path =~ /#{sha1}/ begin - file = File.open(path, "r") - create_upload(file, File.basename(path), post) + tmp = Tempfile.new + tmp.write(File.read(path)) + tmp.rewind + create_upload(tmp, File.basename(path), post) ensure - file&.close + tmp&.close end end end diff --git a/plugins/discourse-local-dates/plugin.rb b/plugins/discourse-local-dates/plugin.rb index cb243194d7..bb4daab5af 100644 --- a/plugins/discourse-local-dates/plugin.rb +++ b/plugins/discourse-local-dates/plugin.rb @@ -37,7 +37,7 @@ after_initialize do end if dates.present? - post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates.to_json + post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates post.save_custom_fields elsif !post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD].nil? post.custom_fields.delete(DiscourseLocalDates::POST_CUSTOM_FIELD) diff --git a/plugins/discourse-local-dates/spec/models/post_spec.rb b/plugins/discourse-local-dates/spec/models/post_spec.rb new file mode 100644 index 0000000000..29793fb541 --- /dev/null +++ b/plugins/discourse-local-dates/spec/models/post_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +describe Post do + + before do + SiteSetting.queue_jobs = false + end + + describe '#local_dates' do + it "should have correct custom fields" do + post = Fabricate(:post, raw: <<~SQL) + [date=2018-09-17 time=01:39:00 format="LLL" timezones="Europe/Paris|America/Los_Angeles"] + SQL + CookedPostProcessor.new(post).post_process + + expect(post.local_dates.count).to eq(1) + expect(post.local_dates[0]["date"]).to eq("2018-09-17") + expect(post.local_dates[0]["time"]).to eq("01:39:00") + + post.raw = "Text removed" + post.save + CookedPostProcessor.new(post).post_process + + expect(post.local_dates).to eq([]) + end + end + +end diff --git a/script/bulk_import/discourse_merger.rb b/script/bulk_import/discourse_merger.rb index e3d4a752b0..14cf5a1fdf 100644 --- a/script/bulk_import/discourse_merger.rb +++ b/script/bulk_import/discourse_merger.rb @@ -173,7 +173,7 @@ class BulkImport::DiscourseMerger < BulkImport::Base columns = Category.columns.map(&:name) imported_ids = [] - last_id = Category.unscoped.maximum(:id) + last_id = Category.unscoped.maximum(:id) || 1 sql = "COPY categories (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do @@ -249,7 +249,7 @@ class BulkImport::DiscourseMerger < BulkImport::Base columns = Tag.columns.map(&:name) imported_ids = [] - last_id = Tag.unscoped.maximum(:id) + last_id = Tag.unscoped.maximum(:id) || 1 sql = "COPY tags (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do @@ -366,7 +366,7 @@ class BulkImport::DiscourseMerger < BulkImport::Base puts "merging badges..." columns = Badge.columns.map(&:name) imported_ids = [] - last_id = Badge.unscoped.maximum(:id) + last_id = Badge.unscoped.maximum(:id) || 1 sql = "COPY badges (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do diff --git a/spec/components/distributed_mutex_spec.rb b/spec/components/distributed_mutex_spec.rb index 69080c5051..3975ea0fc1 100644 --- a/spec/components/distributed_mutex_spec.rb +++ b/spec/components/distributed_mutex_spec.rb @@ -45,4 +45,29 @@ describe DistributedMutex do }.to raise_error(ThreadError) end + context "readonly redis" do + before do + $redis.slaveof "127.0.0.1", "99991" + end + + after do + $redis.slaveof "no", "one" + end + + it "works even if redis is in readonly" do + m = DistributedMutex.new("test_readonly") + start = Time.now + done = false + + expect { + m.synchronize do + done = true + end + }.to raise_error(Discourse::ReadOnly) + + expect(done).to eq(false) + expect(Time.now - start).to be < (1.second) + end + end + end diff --git a/spec/components/file_store/local_store_spec.rb b/spec/components/file_store/local_store_spec.rb index b9696ed091..45b91b80f9 100644 --- a/spec/components/file_store/local_store_spec.rb +++ b/spec/components/file_store/local_store_spec.rb @@ -10,7 +10,7 @@ describe FileStore::LocalStore do let(:optimized_image) { Fabricate(:optimized_image) } - describe ".store_upload" do + describe "#store_upload" do it "returns a relative url" do store.expects(:copy_file) @@ -19,7 +19,7 @@ describe FileStore::LocalStore do end - describe ".store_optimized_image" do + describe "#store_optimized_image" do it "returns a relative url" do store.expects(:copy_file) @@ -28,7 +28,7 @@ describe FileStore::LocalStore do end - describe ".remove_upload" do + describe "#remove_upload" do it "does not delete non uploaded" do FileUtils.expects(:mkdir_p).never @@ -36,26 +36,58 @@ describe FileStore::LocalStore do end it "moves the file to the tombstone" do - filename = File.basename(store.path_for(upload)) - store.remove_upload(upload) - expect(File.exist?(store.tombstone_dir + "/" + filename)) + begin + upload = UploadCreator.new( + file_from_fixtures("smallest.png"), + "smallest.png" + ).create_for(Fabricate(:user).id) + + path = store.path_for(upload) + mtime = File.mtime(path) + + sleep 0.01 # Delay a little for mtime to be updated + store.remove_upload(upload) + tombstone_path = path.sub("/uploads/", "/uploads/tombstone/") + + expect(File.exist?(tombstone_path)).to eq(true) + expect(File.mtime(tombstone_path)).to_not eq(mtime) + ensure + [path, tombstone_path].each do |file_path| + File.delete(file_path) if File.exist?(file_path) + end + end end end - describe ".remove_optimized_image" do - let(:optimized_image) { Fabricate(:optimized_image, url: "/uploads/default/_optimized/42/253dc8edf9d4ada1.png") } - + describe "#remove_optimized_image" do it "moves the file to the tombstone" do - FileUtils.expects(:mkdir_p) - FileUtils.expects(:move) - File.expects(:exists?).returns(true) - store.remove_optimized_image(optimized_image) + begin + upload = UploadCreator.new( + file_from_fixtures("smallest.png"), + "smallest.png" + ).create_for(Fabricate(:user).id) + + upload.create_thumbnail!(1, 1) + upload.reload + + optimized_image = upload.thumbnail(1, 1) + path = store.path_for(optimized_image) + + store.remove_optimized_image(optimized_image) + tombstone_path = path.sub("/uploads/", "/uploads/tombstone/") + + expect(File.exist?(tombstone_path)).to eq(true) + ensure + [path, tombstone_path].each do |file_path| + File.delete(file_path) if File.exist?(file_path) + end + end end end - describe ".has_been_uploaded?" do + describe "#has_been_uploaded?" do it "identifies relatives urls" do expect(store.has_been_uploaded?("/uploads/default/42/0123456789ABCDEF.jpg")).to eq(true) @@ -85,7 +117,7 @@ describe FileStore::LocalStore do Discourse.stubs(:base_uri).returns("/forum") end - describe ".absolute_base_url" do + describe "#absolute_base_url" do it "is present" do expect(store.absolute_base_url).to eq("http://test.localhost/uploads/default") @@ -98,7 +130,7 @@ describe FileStore::LocalStore do end - describe ".relative_base_url" do + describe "#relative_base_url" do it "is present" do expect(store.relative_base_url).to eq("/uploads/default") diff --git a/spec/components/guardian/user_guardian_spec.rb b/spec/components/guardian/user_guardian_spec.rb new file mode 100644 index 0000000000..3281b140af --- /dev/null +++ b/spec/components/guardian/user_guardian_spec.rb @@ -0,0 +1,100 @@ +require 'rails_helper' + +describe UserGuardian do + + let :user do + Fabricate.build(:user, id: 1) + end + + let :moderator do + Fabricate.build(:moderator, id: 2) + end + + let :admin do + Fabricate.build(:admin, id: 3) + end + + let :user_avatar do + UserAvatar.new(user_id: user.id) + end + + let :users_upload do + Upload.new(user_id: user_avatar.user_id, id: 1) + end + + let :already_uploaded do + u = Upload.new(user_id: 999, id: 2) + user_avatar.custom_upload_id = u.id + u + end + + let :not_my_upload do + Upload.new(user_id: 999, id: 3) + end + + let(:moderator_upload) do + Upload.new(user_id: moderator.id, id: 4) + end + + describe '#can_pick_avatar?' do + + let :guardian do + Guardian.new(user) + end + + context 'anon user' do + let(:guardian) { Guardian.new } + + it "should return the right value" do + expect(guardian.can_pick_avatar?(user_avatar, users_upload)).to eq(false) + end + end + + context 'current user' do + it "can not set uploads not owned by current user" do + expect(guardian.can_pick_avatar?(user_avatar, users_upload)).to eq(true) + expect(guardian.can_pick_avatar?(user_avatar, already_uploaded)).to eq(true) + expect(guardian.can_pick_avatar?(user_avatar, not_my_upload)).to eq(false) + expect(guardian.can_pick_avatar?(user_avatar, nil)).to eq(true) + end + + it "can handle uploads that are associated but not directly owned" do + yes_my_upload = not_my_upload + UserUpload.create!(upload_id: yes_my_upload.id, user_id: user_avatar.user_id) + expect(guardian.can_pick_avatar?(user_avatar, yes_my_upload)).to eq(true) + + UserUpload.destroy_all + + UserUpload.create!(upload_id: yes_my_upload.id, user_id: yes_my_upload.user_id) + expect(guardian.can_pick_avatar?(user_avatar, yes_my_upload)).to eq(true) + end + end + + context 'moderator' do + + let :guardian do + Guardian.new(moderator) + end + + it "is secure" do + expect(guardian.can_pick_avatar?(user_avatar, moderator_upload)).to eq(true) + expect(guardian.can_pick_avatar?(user_avatar, users_upload)).to eq(true) + expect(guardian.can_pick_avatar?(user_avatar, already_uploaded)).to eq(true) + expect(guardian.can_pick_avatar?(user_avatar, not_my_upload)).to eq(false) + expect(guardian.can_pick_avatar?(user_avatar, nil)).to eq(true) + end + end + + context 'admin' do + let :guardian do + Guardian.new(admin) + end + + it "is secure" do + expect(guardian.can_pick_avatar?(user_avatar, not_my_upload)).to eq(true) + expect(guardian.can_pick_avatar?(user_avatar, nil)).to eq(true) + end + end + + end +end diff --git a/spec/components/oneboxer_spec.rb b/spec/components/oneboxer_spec.rb index 79520340ab..b3524267ed 100644 --- a/spec/components/oneboxer_spec.rb +++ b/spec/components/oneboxer_spec.rb @@ -116,4 +116,12 @@ describe Oneboxer do expect(Oneboxer.external_onebox('https://discourse.org/')[:onebox]).to be_empty end + it "does not consider ignore_redirects domains as blacklisted" do + url = 'https://store.steampowered.com/app/271590/Grand_Theft_Auto_V/' + stub_request(:head, url).to_return(status: 200, body: "", headers: {}) + stub_request(:get, url).to_return(status: 200, body: "", headers: {}) + + expect(Oneboxer.external_onebox(url)[:onebox]).to be_present + end + end diff --git a/spec/components/promotion_spec.rb b/spec/components/promotion_spec.rb index ae69602979..6191c2c5d8 100644 --- a/spec/components/promotion_spec.rb +++ b/spec/components/promotion_spec.rb @@ -82,6 +82,17 @@ describe Promotion do expect(job["args"][0]["message_type"]).to eq("welcome_tl1_user") end + it "does not not send when the user already has the tl1 badge when recalculcating" do + SiteSetting.send_tl1_welcome_message = true + BadgeGranter.grant(Badge.find(1), user) + stat = user.user_stat + stat.topics_entered = SiteSetting.tl1_requires_topics_entered + stat.posts_read_count = SiteSetting.tl1_requires_read_posts + stat.time_read = SiteSetting.tl1_requires_time_spent_mins * 60 + Promotion.recalculate(user) + expect(Jobs::SendSystemMessage.jobs.length).to eq(0) + end + it "can be turned off" do SiteSetting.send_tl1_welcome_message = false @result = promotion.review diff --git a/spec/fixtures/images/smallest.png b/spec/fixtures/images/smallest.png new file mode 100644 index 0000000000..91a99b94e2 Binary files /dev/null and b/spec/fixtures/images/smallest.png differ diff --git a/spec/fixtures/pdf/small.pdf b/spec/fixtures/pdf/small.pdf new file mode 100644 index 0000000000..9ec444cfa4 --- /dev/null +++ b/spec/fixtures/pdf/small.pdf @@ -0,0 +1,5 @@ +%PDF-1. +1 0 obj<>endobj +2 0 obj<>endobj +3 0 obj<>endobj +trailer <> \ No newline at end of file diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 06553a4fde..f838326f97 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -165,4 +165,15 @@ describe ApplicationHelper do end end + describe 'preloaded_json' do + it 'returns empty JSON if preloaded is empty' do + @preloaded = nil + expect(helper.preloaded_json).to eq('{}') + end + + it 'escapes and strips invalid unicode and strips in json body' do + @preloaded = { test: %{["< \x80"]} } + expect(helper.preloaded_json).to eq(%{{"test":"[\\"\\u003c \uFFFD\\"]"}}) + end + end end diff --git a/spec/jobs/recover_post_uploads_spec.rb b/spec/jobs/post_uploads_recovery_spec.rb similarity index 91% rename from spec/jobs/recover_post_uploads_spec.rb rename to spec/jobs/post_uploads_recovery_spec.rb index 0448bc9adc..b415008d9c 100644 --- a/spec/jobs/recover_post_uploads_spec.rb +++ b/spec/jobs/post_uploads_recovery_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Jobs::RecoverPostUploads do +RSpec.describe Jobs::PostUploadsRecovery do describe '#grace_period' do it 'should restrict the grace period to the right range' do SiteSetting.purge_deleted_uploads_grace_period_days = diff --git a/spec/lib/upload_creator_spec.rb b/spec/lib/upload_creator_spec.rb index 827288abb7..5b0831355d 100644 --- a/spec/lib/upload_creator_spec.rb +++ b/spec/lib/upload_creator_spec.rb @@ -22,6 +22,18 @@ RSpec.describe UploadCreator do expect(upload.extension).to eq('txt') expect(File.extname(upload.url)).to eq('.txt') expect(upload.original_filename).to eq('utf-8.txt') + expect(user.user_uploads.count).to eq(1) + expect(upload.user_uploads.count).to eq(1) + + user2 = Fabricate(:user) + + expect do + UploadCreator.new(file, "utf-8\n.txt").create_for(user2.id) + end.to change { Upload.count }.by(0) + + expect(user.user_uploads.count).to eq(1) + expect(user2.user_uploads.count).to eq(1) + expect(upload.user_uploads.count).to eq(2) end end diff --git a/spec/lib/upload_recovery_spec.rb b/spec/lib/upload_recovery_spec.rb index 7a5929d8ff..1724085340 100644 --- a/spec/lib/upload_recovery_spec.rb +++ b/spec/lib/upload_recovery_spec.rb @@ -6,14 +6,23 @@ RSpec.describe UploadRecovery do let(:upload) do UploadCreator.new( - file_from_fixtures("logo.png"), + file_from_fixtures("smallest.png"), "logo.png" ).create_for(user.id) end + let(:upload2) do + UploadCreator.new( + file_from_fixtures("small.pdf", "pdf"), + "some.pdf" + ).create_for(user.id) + end + let(:post) do Fabricate(:post, - raw: "![logo.png](#{upload.short_url})", + raw: <<~SQL, + ![logo.png](#{upload.short_url}) + SQL user: user ).tap(&:link_post_uploads) end @@ -21,17 +30,20 @@ RSpec.describe UploadRecovery do let(:upload_recovery) { UploadRecovery.new } before do + SiteSetting.authorized_extensions = 'png|pdf' SiteSetting.queue_jobs = false end describe '#recover' do after do - public_path = "#{Discourse.store.public_dir}#{upload.url}" + [upload, upload2].each do |u| + public_path = "#{Discourse.store.public_dir}#{u.url}" - [ - public_path, - public_path.sub("uploads", "uploads/tombstone") - ].each { |path| File.delete(path) if File.exists?(path) } + [ + public_path, + public_path.sub("uploads", "uploads/tombstone") + ].each { |path| File.delete(path) if File.exists?(path) } + end end describe 'when given an invalid sha1' do @@ -43,6 +55,12 @@ RSpec.describe UploadRecovery do ) upload_recovery.recover + + post.update!( + raw: "test" + ) + + upload_recovery.recover end end @@ -54,7 +72,32 @@ RSpec.describe UploadRecovery do upload_recovery.recover(Post.where("updated_at >= ?", 1.day.ago)) end - it 'should recover the upload' do + describe 'for a missing attachment' do + let(:post) do + Fabricate(:post, + raw: <<~SQL, + some.pdf + blank + SQL + user: user + ).tap(&:link_post_uploads) + end + + it 'should recover the attachment' do + expect do + upload2.destroy! + end.to change { post.reload.uploads.count }.from(1).to(0) + + expect do + upload_recovery.recover + end.to change { post.reload.uploads.count }.from(0).to(1) + + expect(File.read(Discourse.store.path_for(post.uploads.first))) + .to eq(File.read(file_from_fixtures("small.pdf", "pdf"))) + end + end + + it 'should recover uploads and attachments' do stub_request(:get, "http://test.localhost#{upload.url}") .to_return(status: 200) @@ -65,6 +108,9 @@ RSpec.describe UploadRecovery do expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1) + + expect(File.read(Discourse.store.path_for(post.uploads.first))) + .to eq(File.read(file_from_fixtures("smallest.png"))) end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 89aa2c3e58..5d3aea6fdb 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -786,4 +786,29 @@ describe Group do group.reload expect(group.has_messages?).to eq true end + + describe '#automatic_group_membership' do + describe 'for a automatic_membership_retroactive group' do + let(:group) { Fabricate(:group, automatic_membership_retroactive: true) } + + it "should be triggered on create and update" do + expect { group } + .to change { Jobs::AutomaticGroupMembership.jobs.size }.by(1) + + job = Jobs::AutomaticGroupMembership.jobs.last + + expect(job["args"].first["group_id"]).to eq(group.id) + + Jobs::AutomaticGroupMembership.jobs.clear + + expect do + group.update!(name: 'asdiaksjdias') + end.to change { Jobs::AutomaticGroupMembership.jobs.size }.by(1) + + job = Jobs::AutomaticGroupMembership.jobs.last + + expect(job["args"].first["group_id"]).to eq(group.id) + end + end + end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index df1b61225d..5e28d0d618 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -296,10 +296,17 @@ describe Topic do expect(topic_image.fancy_title).to eq("Topic with <img src=‘something’> image in its title") end + it "always escapes title" do + topic_script.title = topic_script.title + "x" * Topic.max_fancy_title_length + expect(topic_script.fancy_title).to eq(ERB::Util.html_escape(topic_script.title)) + # not really needed, but just in case + expect(topic_script.fancy_title).not_to include("