FEATURE: Review posts with media. (#10693)
To check if a post contains any embedded media, we look if the "image_sizes" attribute is present in the new post manager arguments. We want to see one boxed links, but we only store the raw content of the post. To work around this, I extracted the onebox logic from the composer editor into a module.
This commit is contained in:
parent
f3156a6478
commit
f85f73be88
|
@ -16,8 +16,6 @@ import {
|
||||||
fetchUnseenHashtags,
|
fetchUnseenHashtags,
|
||||||
} from "discourse/lib/link-hashtags";
|
} from "discourse/lib/link-hashtags";
|
||||||
import Composer from "discourse/models/composer";
|
import Composer from "discourse/models/composer";
|
||||||
import { load, LOADING_ONEBOX_CSS_CLASS } from "pretty-text/oneboxer";
|
|
||||||
import { applyInlineOneboxes } from "pretty-text/inline-oneboxer";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
||||||
|
@ -43,6 +41,7 @@ import {
|
||||||
resolveAllShortUrls,
|
resolveAllShortUrls,
|
||||||
} from "pretty-text/upload-short-url";
|
} from "pretty-text/upload-short-url";
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
|
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||||
|
|
||||||
const REBUILD_SCROLL_MAP_EVENTS = ["composer:resized", "composer:typed-reply"];
|
const REBUILD_SCROLL_MAP_EVENTS = ["composer:resized", "composer:typed-reply"];
|
||||||
|
|
||||||
|
@ -531,36 +530,6 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_loadInlineOneboxes(inline) {
|
|
||||||
applyInlineOneboxes(inline, ajax, {
|
|
||||||
categoryId: this.get("composer.category.id"),
|
|
||||||
topicId: this.get("composer.topic.id"),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_loadOneboxes(oneboxes) {
|
|
||||||
const post = this.get("composer.post");
|
|
||||||
let refresh = false;
|
|
||||||
|
|
||||||
// If we are editing a post, we'll refresh its contents once.
|
|
||||||
if (post && !post.get("refreshedPost")) {
|
|
||||||
refresh = true;
|
|
||||||
post.set("refreshedPost", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.values(oneboxes).forEach((onebox) => {
|
|
||||||
onebox.forEach(($onebox) => {
|
|
||||||
load({
|
|
||||||
elem: $onebox,
|
|
||||||
refresh,
|
|
||||||
ajax,
|
|
||||||
categoryId: this.get("composer.category.id"),
|
|
||||||
topicId: this.get("composer.topic.id"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_warnMentionedGroups($preview) {
|
_warnMentionedGroups($preview) {
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
var found = this.warnedGroupMentions || [];
|
var found = this.warnedGroupMentions || [];
|
||||||
|
@ -934,50 +903,28 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint oneboxes
|
// Paint oneboxes
|
||||||
debounce(
|
const paintFunc = () => {
|
||||||
this,
|
const post = this.get("composer.post");
|
||||||
() => {
|
let refresh = false;
|
||||||
const oneboxes = {};
|
|
||||||
const inlineOneboxes = {};
|
|
||||||
|
|
||||||
// Oneboxes = `a.onebox` -> `a.onebox-loading` -> `aside.onebox`
|
//If we are editing a post, we'll refresh its contents once.
|
||||||
// Inline Oneboxes = `a.inline-onebox-loading` -> `a.inline-onebox`
|
if (post && !post.get("refreshedPost")) {
|
||||||
|
refresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
let loadedOneboxes = $preview.find(
|
const paintedCount = loadOneboxes(
|
||||||
`aside.onebox, a.${LOADING_ONEBOX_CSS_CLASS}, a.inline-onebox`
|
$preview[0],
|
||||||
).length;
|
ajax,
|
||||||
|
this.get("composer.topic.id"),
|
||||||
|
this.get("composer.category.id"),
|
||||||
|
this.siteSettings.max_oneboxes_per_post,
|
||||||
|
refresh
|
||||||
|
);
|
||||||
|
|
||||||
$preview.find(`a.onebox, a.inline-onebox-loading`).each((_, link) => {
|
if (refresh && paintedCount > 0) post.set("refreshedPost", true);
|
||||||
const $link = $(link);
|
};
|
||||||
const text = $link.text();
|
|
||||||
const isInline = $link.attr("class") === "inline-onebox-loading";
|
|
||||||
const m = isInline ? inlineOneboxes : oneboxes;
|
|
||||||
|
|
||||||
if (loadedOneboxes < this.siteSettings.max_oneboxes_per_post) {
|
debounce(this, paintFunc, 450);
|
||||||
if (m[text] === undefined) {
|
|
||||||
m[text] = [];
|
|
||||||
loadedOneboxes++;
|
|
||||||
}
|
|
||||||
m[text].push(link);
|
|
||||||
} else {
|
|
||||||
if (m[text] !== undefined) {
|
|
||||||
m[text].push(link);
|
|
||||||
} else if (isInline) {
|
|
||||||
$link.removeClass("inline-onebox-loading");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Object.keys(oneboxes).length > 0) {
|
|
||||||
this._loadOneboxes(oneboxes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(inlineOneboxes).length > 0) {
|
|
||||||
this._loadInlineOneboxes(inlineOneboxes);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
450
|
|
||||||
);
|
|
||||||
|
|
||||||
// Short upload urls need resolution
|
// Short upload urls need resolution
|
||||||
resolveAllShortUrls(ajax, this.siteSettings, $preview[0]);
|
resolveAllShortUrls(ajax, this.siteSettings, $preview[0]);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Component from "@ember/component";
|
||||||
import { afterRender } from "discourse-common/utils/decorators";
|
import { afterRender } from "discourse-common/utils/decorators";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { cookAsync } from "discourse/lib/text";
|
import { cookAsync } from "discourse/lib/text";
|
||||||
|
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||||
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
|
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
|
||||||
|
|
||||||
const CookText = Component.extend({
|
const CookText = Component.extend({
|
||||||
|
@ -11,10 +12,25 @@ const CookText = Component.extend({
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
cookAsync(this.rawText).then((cooked) => {
|
cookAsync(this.rawText).then((cooked) => {
|
||||||
this.set("cooked", cooked);
|
this.set("cooked", cooked);
|
||||||
|
if (this.paintOneboxes) this._loadOneboxes();
|
||||||
this._resolveUrls();
|
this._resolveUrls();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@afterRender
|
||||||
|
_loadOneboxes() {
|
||||||
|
const refresh = false;
|
||||||
|
|
||||||
|
loadOneboxes(
|
||||||
|
this.element,
|
||||||
|
ajax,
|
||||||
|
this.topicId,
|
||||||
|
this.categoryId,
|
||||||
|
this.siteSettings.max_oneboxes_per_post,
|
||||||
|
refresh
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
@afterRender
|
@afterRender
|
||||||
_resolveUrls() {
|
_resolveUrls() {
|
||||||
resolveAllShortUrls(ajax, this.siteSettings, this.element, this.opts);
|
resolveAllShortUrls(ajax, this.siteSettings, this.element, this.opts);
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { applyInlineOneboxes } from "pretty-text/inline-oneboxer";
|
||||||
|
import { load, LOADING_ONEBOX_CSS_CLASS } from "pretty-text/oneboxer";
|
||||||
|
|
||||||
|
export function loadOneboxes(
|
||||||
|
container,
|
||||||
|
ajax,
|
||||||
|
topicId,
|
||||||
|
categoryId,
|
||||||
|
maxOneboxes,
|
||||||
|
refresh
|
||||||
|
) {
|
||||||
|
const oneboxes = {};
|
||||||
|
const inlineOneboxes = {};
|
||||||
|
|
||||||
|
// Oneboxes = `a.onebox` -> `a.onebox-loading` -> `aside.onebox`
|
||||||
|
// Inline Oneboxes = `a.inline-onebox-loading` -> `a.inline-onebox`
|
||||||
|
|
||||||
|
let loadedOneboxes = container.querySelectorAll(
|
||||||
|
`aside.onebox, a.${LOADING_ONEBOX_CSS_CLASS}, a.inline-onebox`
|
||||||
|
).length;
|
||||||
|
|
||||||
|
container
|
||||||
|
.querySelectorAll(`a.onebox, a.inline-onebox-loading`)
|
||||||
|
.forEach((link) => {
|
||||||
|
const text = link.textContent;
|
||||||
|
const isInline = link.getAttribute("class") === "inline-onebox-loading";
|
||||||
|
const m = isInline ? inlineOneboxes : oneboxes;
|
||||||
|
|
||||||
|
if (loadedOneboxes < maxOneboxes) {
|
||||||
|
if (m[text] === undefined) {
|
||||||
|
m[text] = [];
|
||||||
|
loadedOneboxes++;
|
||||||
|
}
|
||||||
|
m[text].push(link);
|
||||||
|
} else {
|
||||||
|
if (m[text] !== undefined) {
|
||||||
|
m[text].push(link);
|
||||||
|
} else if (isInline) {
|
||||||
|
link.classList.remove("inline-onebox-loading");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let newBoxes = 0;
|
||||||
|
|
||||||
|
if (Object.keys(oneboxes).length > 0) {
|
||||||
|
_loadOneboxes(oneboxes, ajax, newBoxes, topicId, categoryId, refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(inlineOneboxes).length > 0) {
|
||||||
|
_loadInlineOneboxes(inlineOneboxes, ajax, topicId, categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBoxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _loadInlineOneboxes(inline, ajax, topicId, categoryId) {
|
||||||
|
applyInlineOneboxes(inline, ajax, {
|
||||||
|
categoryId: topicId,
|
||||||
|
topicId: categoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _loadOneboxes(oneboxes, ajax, count, topicId, categoryId, refresh) {
|
||||||
|
Object.values(oneboxes).forEach((onebox) => {
|
||||||
|
onebox.forEach((o) => {
|
||||||
|
load({
|
||||||
|
elem: o,
|
||||||
|
refresh,
|
||||||
|
ajax,
|
||||||
|
categoryId: categoryId,
|
||||||
|
topicId: topicId,
|
||||||
|
});
|
||||||
|
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -18,7 +18,14 @@
|
||||||
<div class="post-contents">
|
<div class="post-contents">
|
||||||
{{reviewable-post-header reviewable=reviewable createdBy=reviewable.created_by tagName=""}}
|
{{reviewable-post-header reviewable=reviewable createdBy=reviewable.created_by tagName=""}}
|
||||||
|
|
||||||
{{cook-text reviewable.payload.raw class="post-body" opts=(hash removeMissing=true)}}
|
{{cook-text
|
||||||
|
reviewable.payload.raw
|
||||||
|
class="post-body"
|
||||||
|
categoryId=reviewable.category_id
|
||||||
|
topicId=reviewable.topic_id
|
||||||
|
paintOneboxes=true
|
||||||
|
opts=(hash removeMissing=true)
|
||||||
|
}}
|
||||||
|
|
||||||
{{yield}}
|
{{yield}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2145,6 +2145,7 @@ en:
|
||||||
new_user_notice_tl: "Minimum trust level required to see new user post notices."
|
new_user_notice_tl: "Minimum trust level required to see new user post notices."
|
||||||
returning_user_notice_tl: "Minimum trust level required to see returning user post notices."
|
returning_user_notice_tl: "Minimum trust level required to see returning user post notices."
|
||||||
returning_users_days: "How many days should pass before a user is considered to be returning."
|
returning_users_days: "How many days should pass before a user is considered to be returning."
|
||||||
|
review_media_unless_trust_level: "Staff will review posts of users with lower trust levels if it contains embedded media."
|
||||||
enable_page_publishing: "Allow staff members to publish topics to new URLs with their own styling."
|
enable_page_publishing: "Allow staff members to publish topics to new URLs with their own styling."
|
||||||
show_published_pages_login_required: "Anonymous users can see published pages, even when login is required."
|
show_published_pages_login_required: "Anonymous users can see published pages, even when login is required."
|
||||||
|
|
||||||
|
@ -4812,6 +4813,7 @@ en:
|
||||||
email_auth_res_enqueue: "This email failed a DMARC check, it most likely isn't from whom it seems to be from. Check the raw email headers for more information."
|
email_auth_res_enqueue: "This email failed a DMARC check, it most likely isn't from whom it seems to be from. Check the raw email headers for more information."
|
||||||
email_spam: "This email was flagged as spam by the header defined in `email_in_spam_header`."
|
email_spam: "This email was flagged as spam by the header defined in `email_in_spam_header`."
|
||||||
suspect_user: "This new user entered profile information without reading any topics or posts, which strongly suggests they may be a spammer. See `approve_suspect_users`."
|
suspect_user: "This new user entered profile information without reading any topics or posts, which strongly suggests they may be a spammer. See `approve_suspect_users`."
|
||||||
|
contains_media: "This posts includes embedded media. See `review_media_unless_trust_level`."
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
agree:
|
agree:
|
||||||
|
|
|
@ -960,6 +960,9 @@ posting:
|
||||||
returning_users_days:
|
returning_users_days:
|
||||||
default: 120
|
default: 120
|
||||||
max: 36500
|
max: 36500
|
||||||
|
review_media_unless_trust_level:
|
||||||
|
default: 0
|
||||||
|
enum: "TrustLevelSetting"
|
||||||
enable_page_publishing:
|
enable_page_publishing:
|
||||||
default: false
|
default: false
|
||||||
show_published_pages_login_required:
|
show_published_pages_login_required:
|
||||||
|
|
|
@ -108,6 +108,11 @@ class NewPostManager
|
||||||
|
|
||||||
return :category if post_needs_approval_in_its_category?(manager)
|
return :category if post_needs_approval_in_its_category?(manager)
|
||||||
|
|
||||||
|
return :contains_media if (
|
||||||
|
manager.args[:image_sizes].present? &&
|
||||||
|
user.trust_level < SiteSetting.review_media_unless_trust_level.to_i
|
||||||
|
)
|
||||||
|
|
||||||
:skip
|
:skip
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -239,6 +239,42 @@ describe NewPostManager do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with media' do
|
||||||
|
let(:user) { manager.user }
|
||||||
|
let(:manager_opts) do
|
||||||
|
{
|
||||||
|
raw: 'this is new post content', topic_id: topic.id, first_post_checks: false,
|
||||||
|
image_sizes: {
|
||||||
|
"http://localhost:3000/uploads/default/original/1X/652fc9667040b1b89dc4d9b061a823ddb3c0cef0.jpeg" => {
|
||||||
|
"width" => "500", "height" => "500"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.update!(trust_level: 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'queues the post for review because if it contains embedded media.' do
|
||||||
|
SiteSetting.review_media_unless_trust_level = 1
|
||||||
|
manager = NewPostManager.new(topic.user, manager_opts)
|
||||||
|
|
||||||
|
result = NewPostManager.default_handler(manager)
|
||||||
|
|
||||||
|
expect(result.action).to eq(:enqueued)
|
||||||
|
expect(result.reason).to eq(:contains_media)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not enqueue the post if the poster is a trusted user' do
|
||||||
|
SiteSetting.review_media_unless_trust_level = 0
|
||||||
|
manager = NewPostManager.new(topic.user, manager_opts)
|
||||||
|
|
||||||
|
result = NewPostManager.default_handler(manager)
|
||||||
|
|
||||||
|
expect(result).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "new topic handler" do
|
context "new topic handler" do
|
||||||
|
|
Loading…
Reference in New Issue