mirror of
https://github.com/discourse/discourse.git
synced 2025-01-18 18:12:46 +08:00
Merge branch 'master' into theme-select-mobile
This commit is contained in:
commit
87a97cbf4b
2
Gemfile
2
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
|
||||
|
||||
|
|
18
Gemfile.lock
18
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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{{#unless editingTheme}}
|
||||
<div class='customize-themes-header'>
|
||||
<div class="title">
|
||||
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="create-actions">
|
||||
{{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{#d-modal-body title="admin.backups.operations.backup.confirm"}}
|
||||
<button {{action "startBackup"}} class="btn btn-primary">{{i18n 'yes_value'}}</button>
|
||||
<button {{action "startBackupWithoutUpload"}} class="btn">{{i18n 'admin.backups.operations.backup.without_uploads'}}</button>
|
||||
<button {{action "startBackupWithUploads"}} class="btn btn-primary">{{i18n 'yes_value'}}</button>
|
||||
<button {{action "startBackupWithoutUploads"}} class="btn">{{i18n 'admin.backups.operations.backup.without_uploads'}}</button>
|
||||
<button {{action "cancel"}} class="btn">{{i18n 'no_value'}}</button>
|
||||
{{/d-modal-body}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
{{#d-modal-body class='change-ownership'}}
|
||||
{{{i18n 'topic.change_owner.instructions' count=selectedPostsCount old_user=selectedPostsUsername}}}
|
||||
<p>
|
||||
{{{i18n 'topic.change_owner.instructions_warn'}}}
|
||||
</p>
|
||||
|
||||
<form>
|
||||
<label>{{i18n 'topic.change_owner.label'}}</label>
|
||||
<label></label>
|
||||
{{user-selector single="true"
|
||||
usernames=new_user
|
||||
placeholderKey="topic.change_owner.placeholder"
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
<td>
|
||||
{{#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}}
|
||||
</td>
|
||||
|
|
|
@ -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}}
|
||||
|
|
12
app/assets/javascripts/preload-application-data.js
Normal file
12
app/assets/javascripts/preload-application-data.js
Normal file
|
@ -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]));
|
||||
});
|
||||
}
|
||||
})();
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -12,10 +12,7 @@ export default DropdownSelectBoxComponent.extend({
|
|||
return `<h3>${title}</h3>`;
|
||||
},
|
||||
|
||||
mutateValue(value) {
|
||||
this.sendAction("onPopupMenuAction", value);
|
||||
this.setProperties({ value: null, highlighted: null });
|
||||
},
|
||||
autoHighlight() {},
|
||||
|
||||
computeContent(content) {
|
||||
return content
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -161,6 +161,7 @@
|
|||
|
||||
.post-infos {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
.date-picker {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
width: 8em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
}
|
||||
|
||||
&.bar.has-selection .category-drop-header {
|
||||
border: none;
|
||||
padding: 4px 5px 4px 10px;
|
||||
}
|
||||
|
||||
&.box.has-selection .category-drop-header {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
.select-kit-body {
|
||||
max-width: 32em;
|
||||
width: 32em;
|
||||
}
|
||||
|
||||
.select-kit-header {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require_dependency "upload_recovery"
|
||||
|
||||
module Jobs
|
||||
class RecoverPostUploads < Jobs::Onceoff
|
||||
class PostUploadsRecovery < Jobs::Onceoff
|
||||
MIN_PERIOD = 30
|
||||
MAX_PERIOD = 120
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
4
app/models/user_upload.rb
Normal file
4
app/models/user_upload.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
class UserUpload < ActiveRecord::Base
|
||||
belongs_to :upload
|
||||
belongs_to :user
|
||||
end
|
|
@ -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("<div>#{html}</div>")
|
||||
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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -55,6 +55,9 @@
|
|||
<%= render partial: "common/discourse_stylesheet" %>
|
||||
<%= render partial: "common/special_font_face" %>
|
||||
|
||||
<meta id="data-preloaded" data-preloaded="<%= preloaded_json %>">
|
||||
<%= preload_script "preload-application-data" %>
|
||||
|
||||
<%= yield :head %>
|
||||
|
||||
<%= build_plugin_html 'server:before-head-close' %>
|
||||
|
@ -104,17 +107,6 @@
|
|||
</form>
|
||||
<% end %>
|
||||
|
||||
<%- if @preloaded.present? %>
|
||||
<script>
|
||||
(function() {
|
||||
var ps = require('preload-store').default;
|
||||
<%- @preloaded.each do |key, json| %>
|
||||
ps.store("<%= key %>", <%= escape_unicode(json) %>);
|
||||
<% end %>
|
||||
})();
|
||||
</script>
|
||||
<%- end %>
|
||||
|
||||
<%= yield :data %>
|
||||
|
||||
<%= render :partial => "common/discourse_javascript" %>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 <b>{{old_user}}</b>."
|
||||
other: "Please choose the new owner of the {{count}} posts by <b>{{old_user}}</b>."
|
||||
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 <b>@{{old_user}}</b>"
|
||||
other: "Please choose a new owner for the {{count}} posts by <b>@{{old_user}}</b>"
|
||||
|
||||
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."
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
22
db/migrate/20180920042415_create_user_uploads.rb
Normal file
22
db/migrate/20180920042415_create_user_uploads.rb
Normal file
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}" }
|
||||
|
||||
<<TEXT
|
||||
#{`hostname`.strip} pid:#{Process.pid} #{`cat /proc/#{Process.pid}/cmdline`.strip.gsub(/[^a-z1-9\/]/i, ' ')}
|
||||
|
||||
GC STATS:
|
||||
#{stats.join("\n")}
|
||||
|
||||
Objects:
|
||||
#{counts.join("\n")}
|
||||
|
||||
Process Info:
|
||||
#{`cat /proc/#{Process.pid}/status`}
|
||||
|
||||
Classes:
|
||||
#{classes.length > 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
|
|
@ -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: {},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
28
plugins/discourse-local-dates/spec/models/post_spec.rb
Normal file
28
plugins/discourse-local-dates/spec/models/post_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
100
spec/components/guardian/user_guardian_spec.rb
Normal file
100
spec/components/guardian/user_guardian_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
BIN
spec/fixtures/images/smallest.png
vendored
Normal file
BIN
spec/fixtures/images/smallest.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 B |
5
spec/fixtures/pdf/small.pdf
vendored
Normal file
5
spec/fixtures/pdf/small.pdf
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
%PDF-1.
|
||||
1 0 obj<</Pages 2 0 R>>endobj
|
||||
2 0 obj<</Kids[3 0 R]/Count 1>>endobj
|
||||
3 0 obj<</Parent 2 0 R>>endobj
|
||||
trailer <</Root 1 0 R>>
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: "<a href=#{"/uploads/test/original/3X/a/6%0A/#{upload.sha1}.png"}>test</a>"
|
||||
)
|
||||
|
||||
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,
|
||||
<a class="attachment" href="#{upload2.url}">some.pdf</a>
|
||||
<a>blank</a>
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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("<script>")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'fancy title' do
|
||||
let(:topic) { Fabricate.build(:topic, title: "\"this topic\" -- has ``fancy stuff''") }
|
||||
let(:topic) { Fabricate.build(:topic, title: %{"this topic" -- has ``fancy stuff''}) }
|
||||
|
||||
context 'title_fancy_entities disabled' do
|
||||
before do
|
||||
|
@ -319,7 +326,6 @@ describe Topic do
|
|||
it "converts the title to have fancy entities and updates" do
|
||||
expect(topic.fancy_title).to eq("“this topic” – has “fancy stuff”")
|
||||
topic.title = "this is my test hello world... yay"
|
||||
topic.user.save!
|
||||
topic.save!
|
||||
topic.reload
|
||||
expect(topic.fancy_title).to eq("This is my test hello world… yay")
|
||||
|
@ -336,7 +342,7 @@ describe Topic do
|
|||
end
|
||||
|
||||
it "works with long title that results in lots of entities" do
|
||||
long_title = "NEW STOCK PICK: PRCT - LAST PICK UP 233%, NNCO.................................................................................................................................................................. ofoum"
|
||||
long_title = "NEW STOCK PICK: PRCT - LAST PICK UP 233%, NNCO#{"." * 150} ofoum"
|
||||
topic.title = long_title
|
||||
|
||||
expect { topic.save! }.to_not raise_error
|
||||
|
|
|
@ -31,7 +31,11 @@ describe CategoriesController do
|
|||
SiteSetting.categories_topics = 5
|
||||
SiteSetting.categories_topics.times { Fabricate(:topic) }
|
||||
get "/categories"
|
||||
expect(response.body).to include(%{"more_topics_url":"/latest"})
|
||||
|
||||
expect(response.body).to have_tag("meta#data-preloaded") do |element|
|
||||
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
|
||||
expect(json['topic_list_latest']).to include(%{"more_topics_url":"/latest"})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -30,6 +30,15 @@ describe ExportCsvController do
|
|||
expect(response).to be_forbidden
|
||||
expect(Jobs::ExportCsvFile.jobs.size).to eq(0)
|
||||
end
|
||||
|
||||
it "correctly logs the entity export" do
|
||||
post "/export_csv/export_entity.json", params: { entity: "user_archive" }
|
||||
|
||||
log_entry = UserHistory.last
|
||||
expect(log_entry.action).to eq(UserHistory.actions[:entity_export])
|
||||
expect(log_entry.acting_user_id).to eq(user.id)
|
||||
expect(log_entry.subject).to eq("user_archive")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -58,6 +67,15 @@ describe ExportCsvController do
|
|||
expect(job_data["entity"]).to eq("staff_action")
|
||||
expect(job_data["user_id"]).to eq(admin.id)
|
||||
end
|
||||
|
||||
it "correctly logs the entity export" do
|
||||
post "/export_csv/export_entity.json", params: { entity: "user_list" }
|
||||
|
||||
log_entry = UserHistory.last
|
||||
expect(log_entry.action).to eq(UserHistory.actions[:entity_export])
|
||||
expect(log_entry.acting_user_id).to eq(admin.id)
|
||||
expect(log_entry.subject).to eq("user_list")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
42
spec/requests/tag_groups_controller_spec.rb
Normal file
42
spec/requests/tag_groups_controller_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TagGroupsController do
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:tag_group) { Fabricate(:tag_group) }
|
||||
|
||||
describe '#index' do
|
||||
describe 'for a non staff user' do
|
||||
it 'should not be accessible' do
|
||||
get "/tag_groups.json"
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
|
||||
sign_in(user)
|
||||
get "/tag_groups.json"
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for a staff user' do
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
it "should return the right response" do
|
||||
tag_group
|
||||
|
||||
get "/tag_groups.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
tag_groups = JSON.parse(response.body)["tag_groups"]
|
||||
|
||||
expect(tag_groups.count).to eq(1)
|
||||
expect(tag_groups.first["id"]).to eq(tag_group.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -190,7 +190,10 @@ describe UsersController do
|
|||
)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to include('{"is_developer":false,"admin":false,"second_factor_required":false,"backup_enabled":false}')
|
||||
expect(response.body).to have_tag("meta#data-preloaded") do |element|
|
||||
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
|
||||
expect(json['password_reset']).to include('{"is_developer":false,"admin":false,"second_factor_required":false,"backup_enabled":false}')
|
||||
end
|
||||
|
||||
expect(session["password-#{token}"]).to be_blank
|
||||
expect(UserAuthToken.where(id: user_auth_token.id).count).to eq(0)
|
||||
|
@ -255,7 +258,10 @@ describe UsersController do
|
|||
|
||||
get "/u/password-reset/#{token}"
|
||||
|
||||
expect(response.body).to include('{"is_developer":false,"admin":false,"second_factor_required":true,"backup_enabled":false}')
|
||||
expect(response.body).to have_tag("meta#data-preloaded") do |element|
|
||||
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
|
||||
expect(json['password_reset']).to include('{"is_developer":false,"admin":false,"second_factor_required":true,"backup_enabled":false}')
|
||||
end
|
||||
|
||||
put "/u/password-reset/#{token}", params: {
|
||||
password: 'hg9ow8yHG32O',
|
||||
|
@ -1764,9 +1770,13 @@ describe UsersController do
|
|||
end
|
||||
|
||||
context 'while logged in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
let!(:user) { sign_in(Fabricate(:user)) }
|
||||
let(:upload) { Fabricate(:upload) }
|
||||
let(:upload) do
|
||||
Fabricate(:upload, user: user)
|
||||
end
|
||||
|
||||
it "raises an error when you don't have permission to toggle the avatar" do
|
||||
another_user = Fabricate(:user)
|
||||
|
@ -1803,6 +1813,9 @@ describe UsersController do
|
|||
end
|
||||
|
||||
it 'can successfully pick a gravatar' do
|
||||
|
||||
user.user_avatar.update_columns(gravatar_upload_id: upload.id)
|
||||
|
||||
put "/u/#{user.username}/preferences/avatar/pick.json", params: {
|
||||
upload_id: upload.id, type: "gravatar"
|
||||
}
|
||||
|
@ -1812,6 +1825,16 @@ describe UsersController do
|
|||
expect(user.user_avatar.reload.gravatar_upload_id).to eq(upload.id)
|
||||
end
|
||||
|
||||
it 'can not pick uploads that were not created by user' do
|
||||
upload2 = Fabricate(:upload)
|
||||
|
||||
put "/u/#{user.username}/preferences/avatar/pick.json", params: {
|
||||
upload_id: upload2.id, type: "custom"
|
||||
}
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it 'can successfully pick a custom avatar' do
|
||||
put "/u/#{user.username}/preferences/avatar/pick.json", params: {
|
||||
upload_id: upload.id, type: "custom"
|
||||
|
@ -2262,7 +2285,7 @@ describe UsersController do
|
|||
end
|
||||
|
||||
it "raises an error when logged in" do
|
||||
moderator = sign_in(Fabricate(:moderator))
|
||||
sign_in(Fabricate(:moderator))
|
||||
post_user
|
||||
|
||||
put "/u/update-activation-email.json", params: {
|
||||
|
@ -2274,7 +2297,7 @@ describe UsersController do
|
|||
|
||||
it "raises an error when the new email is taken" do
|
||||
active_user = Fabricate(:user)
|
||||
user = post_user
|
||||
post_user
|
||||
|
||||
put "/u/update-activation-email.json", params: {
|
||||
email: active_user.email
|
||||
|
@ -2284,7 +2307,7 @@ describe UsersController do
|
|||
end
|
||||
|
||||
it "raises an error when the email is blacklisted" do
|
||||
user = post_user
|
||||
post_user
|
||||
SiteSetting.email_domains_blacklist = 'example.com'
|
||||
put "/u/update-activation-email.json", params: { email: 'test@example.com' }
|
||||
expect(response.status).to eq(422)
|
||||
|
@ -2592,9 +2615,12 @@ describe UsersController do
|
|||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
expect(response.body).to include(
|
||||
"{\"message\":\"#{I18n.t("login.activate_email", email: user.email).gsub!("</", "<\\/")}\",\"show_controls\":true,\"username\":\"#{user.username}\",\"email\":\"#{user.email}\"}"
|
||||
)
|
||||
expect(response.body).to have_tag("meta#data-preloaded") do |element|
|
||||
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
|
||||
expect(json['accountCreated']).to include(
|
||||
"{\"message\":\"#{I18n.t("login.activate_email", email: user.email).gsub!("</", "<\\/")}\",\"show_controls\":true,\"username\":\"#{user.username}\",\"email\":\"#{user.email}\"}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,10 @@ require 'rails_helper'
|
|||
describe SearchIndexer do
|
||||
let(:post_id) { 99 }
|
||||
|
||||
def scrub(html, strip_diacritics: false)
|
||||
SearchIndexer.scrub_html_for_search(html, strip_diacritics: strip_diacritics)
|
||||
end
|
||||
|
||||
it 'correctly indexes chinese' do
|
||||
SiteSetting.default_locale = 'zh_CN'
|
||||
data = "你好世界"
|
||||
|
@ -16,26 +20,26 @@ describe SearchIndexer do
|
|||
|
||||
it 'extract youtube title' do
|
||||
html = "<div class=\"lazyYT\" data-youtube-id=\"lmFgeFh2nlw\" data-youtube-title=\"Metallica Mixer Explains Missing Bass on 'And Justice for All' [Exclusive]\" data-width=\"480\" data-height=\"270\" data-parameters=\"feature=oembed&wmode=opaque\"></div>"
|
||||
|
||||
scrubbed = SearchIndexer::HtmlScrubber.scrub(html)
|
||||
|
||||
expect(scrubbed).to eq(" Metallica Mixer Explains Missing Bass on 'And Justice for All' [Exclusive] ")
|
||||
scrubbed = scrub(html)
|
||||
expect(scrubbed).to eq("Metallica Mixer Explains Missing Bass on 'And Justice for All' [Exclusive]")
|
||||
end
|
||||
|
||||
it 'extract a link' do
|
||||
html = "<a href='http://meta.discourse.org/'>link</a>"
|
||||
|
||||
scrubbed = SearchIndexer::HtmlScrubber.scrub(html)
|
||||
|
||||
expect(scrubbed).to eq(" http://meta.discourse.org/ link ")
|
||||
scrubbed = scrub(html)
|
||||
expect(scrubbed).to eq("http://meta.discourse.org/ link")
|
||||
end
|
||||
|
||||
it 'removes diacritics' do
|
||||
it 'uses ignore_accent setting to strip diacritics' do
|
||||
html = "<p>HELLO Hétérogénéité Здравствуйте هتاف للترحيب 你好</p>"
|
||||
|
||||
scrubbed = SearchIndexer::HtmlScrubber.scrub(html, strip_diacritics: true)
|
||||
SiteSetting.search_ignore_accents = true
|
||||
scrubbed = SearchIndexer.scrub_html_for_search(html)
|
||||
expect(scrubbed).to eq("HELLO Heterogeneite Здравствуите هتاف للترحيب 你好")
|
||||
|
||||
expect(scrubbed).to eq(" HELLO Heterogeneite Здравствуите هتاف للترحيب 你好 ")
|
||||
SiteSetting.search_ignore_accents = false
|
||||
scrubbed = SearchIndexer.scrub_html_for_search(html)
|
||||
expect(scrubbed).to eq("HELLO Hétérogénéité Здравствуйте هتاف للترحيب 你好")
|
||||
end
|
||||
|
||||
it "doesn't index local files" do
|
||||
|
@ -54,9 +58,9 @@ describe SearchIndexer do
|
|||
</div>
|
||||
HTML
|
||||
|
||||
scrubbed = SearchIndexer::HtmlScrubber.scrub(html).gsub(/\s+/, " ")
|
||||
scrubbed = scrub(html)
|
||||
|
||||
expect(scrubbed).to eq(" Discourse 51%20PM Untitled design (21).jpg Untitled%20design%20(21) Untitled design (21).jpg 1280x1136 472 KB ")
|
||||
expect(scrubbed).to eq("Discourse 51%20PM Untitled design (21).jpg Untitled%20design%20(21) Untitled design (21).jpg 1280x1136 472 KB")
|
||||
end
|
||||
|
||||
it 'correctly indexes a post according to version' do
|
||||
|
|
|
@ -289,6 +289,35 @@ QUnit.test("Composer can toggle between edit and reply", async assert => {
|
|||
);
|
||||
});
|
||||
|
||||
QUnit.test("Composer can toggle whispers", async assert => {
|
||||
await visit("/t/this-is-a-test-topic/9");
|
||||
await click(".topic-post:eq(0) button.reply");
|
||||
|
||||
await selectKit(".toolbar-popup-menu-options").expand();
|
||||
await selectKit(".toolbar-popup-menu-options").selectRowByValue(
|
||||
"toggleWhisper"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
find(".composer-fields .whisper")
|
||||
.text()
|
||||
.indexOf(I18n.t("composer.whisper")) > 0,
|
||||
"it sets the post type to whisper"
|
||||
);
|
||||
|
||||
await selectKit(".toolbar-popup-menu-options").expand();
|
||||
await selectKit(".toolbar-popup-menu-options").selectRowByValue(
|
||||
"toggleWhisper"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
find(".composer-fields .whisper")
|
||||
.text()
|
||||
.indexOf(I18n.t("composer.whisper")) <= 0,
|
||||
"it removes the whisper mode"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Composer can toggle between reply and createTopic",
|
||||
async assert => {
|
||||
|
|
|
@ -835,3 +835,55 @@ componentTest("without forceEscape", {
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
componentTest("onSelect", {
|
||||
template:
|
||||
"<div class='test-external-action'></div>{{single-select content=content onSelect=(action externalAction)}}",
|
||||
|
||||
beforeEach() {
|
||||
this.set("externalAction", actual => {
|
||||
find(".test-external-action").text(actual);
|
||||
});
|
||||
|
||||
this.set("content", ["red", "blue"]);
|
||||
},
|
||||
|
||||
async test(assert) {
|
||||
await this.get("subject").expand();
|
||||
await this.get("subject").selectRowByValue("red");
|
||||
|
||||
assert.equal(
|
||||
find(".test-external-action")
|
||||
.text()
|
||||
.trim(),
|
||||
"red"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
componentTest("onDeselect", {
|
||||
template:
|
||||
"<div class='test-external-action'></div>{{single-select content=content onDeselect=(action externalAction)}}",
|
||||
|
||||
beforeEach() {
|
||||
this.set("externalAction", actual => {
|
||||
find(".test-external-action").text(actual);
|
||||
});
|
||||
|
||||
this.set("content", ["red", "blue"]);
|
||||
},
|
||||
|
||||
async test(assert) {
|
||||
await this.get("subject").expand();
|
||||
await this.get("subject").selectRowByValue("red");
|
||||
await this.get("subject").expand();
|
||||
await this.get("subject").selectRowByValue("blue");
|
||||
|
||||
assert.equal(
|
||||
find(".test-external-action")
|
||||
.text()
|
||||
.trim(),
|
||||
"red"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user