Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/discourse/app/components/post-list/item/details.gjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
import PluginOutlet from "discourse/components/plugin-outlet";
import TopicStatus from "discourse/components/topic-status";
Expand Down Expand Up @@ -30,7 +31,10 @@ export default class PostListItemDetails extends Component {

get topicTitle() {
return this.args.titlePath
? this.args.post[this.args.titlePath]
? this.args.titlePath === "titleHtml" ||
this.args.titlePath === "topic_html_title"
? htmlSafe(this.args.post[this.args.titlePath])
: this.args.post[this.args.titlePath]
: this.args.post.title;
}

Expand Down
8 changes: 7 additions & 1 deletion frontend/discourse/app/models/user-action.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { equal, or } from "@ember/object/computed";
import { service } from "@ember/service";
import discourseComputed from "discourse/lib/decorators";
import { emojiUnescape } from "discourse/lib/text";
import { userPath } from "discourse/lib/url";
import { postUrl } from "discourse/lib/utilities";
import { escapeExpression, postUrl } from "discourse/lib/utilities";
import RestModel from "discourse/models/rest";
import UserActionGroup from "discourse/models/user-action-group";
import Category from "./category";
Expand Down Expand Up @@ -168,6 +169,11 @@ export default class UserAction extends RestModel {
return postUrl(this.slug, this.topic_id, this.reply_to_post_number);
}

@discourseComputed("title")
titleHtml(title) {
return title && emojiUnescape(escapeExpression(title));
}

addChild(action) {
let groups = this.childGroups;
if (!groups) {
Expand Down
2 changes: 0 additions & 2 deletions frontend/discourse/app/models/user-stream.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { tracked } from "@glimmer/tracking";
import replaceEmoji from "discourse/helpers/replace-emoji";
import { ajax } from "discourse/lib/ajax";
import { url } from "discourse/lib/computed";
import discourseComputed from "discourse/lib/decorators";
Expand Down Expand Up @@ -111,7 +110,6 @@ export default class UserStream extends RestModel {
});

result.user_actions?.forEach((action) => {
action.titleHtml = replaceEmoji(action.title);
copy.push(UserAction.create(action));
});

Expand Down
8 changes: 0 additions & 8 deletions frontend/discourse/app/routes/user/deleted-posts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { emojiUnescape } from "discourse/lib/text";
import { escapeExpression } from "discourse/lib/utilities";
import DiscourseRoute from "discourse/routes/discourse";

export default class UserDeletedPosts extends DiscourseRoute {
Expand All @@ -18,11 +16,5 @@ export default class UserDeletedPosts extends DiscourseRoute {
super.setupController(...arguments);

model.set("canLoadMore", model.itemsLoaded === 60);

model.content.forEach((item) => {
if (item.title) {
item.set("titleHtml", emojiUnescape(escapeExpression(item.title)));
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ module("Integration | Component | PostList | Index", function (hooks) {
assert.dom(".post-list-item__details .post-member-info").doesNotExist();
});

test("@titlePath", async function (assert) {
test("@titlePath with plain text title", async function (assert) {
const posts = cloneJSON(postModel).map((post) => {
post.topic_html_title = `Fancy title`;
return post;
Expand All @@ -76,6 +76,23 @@ module("Integration | Component | PostList | Index", function (hooks) {
assert.dom(".post-list-item__details .title a").hasText("Fancy title");
});

test("@titlePath with emoji title", async function (assert) {
const posts = cloneJSON(postModel).map((post) => {
post.titleHtml = `<img width="20" height="20" src="/images/emoji/twitter/smiley.png?v=15" title="smiley" alt="smiley" class="emoji"> Zion Event Post`;
return post;
});
await render(
<template><PostList @posts={{posts}} @titlePath="titleHtml" /></template>
);
assert
.dom(".post-list-item__details .title a")
.includesText("Zion Event Post");
assert.dom(".post-list-item__details .title a img.emoji").exists();
assert
.dom(".post-list-item__details .title a img.emoji")
.hasAttribute("alt", "smiley");
});

test("@idPath", async function (assert) {
const posts = cloneJSON(postModel).map((post) => {
post.post_id = post.id;
Expand Down
77 changes: 77 additions & 0 deletions frontend/discourse/tests/unit/models/user-action-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,81 @@ module("Unit | Model | user-action", function (hooks) {
assert.strictEqual(actions[0].children.length, 1);
assert.strictEqual(actions[0].children[0].items.length, 2);
});

test("titleHtml escapes HTML and unescapes emojis", function (assert) {
const store = getOwner(this).lookup("service:store");

// Test with plain text
const plainAction = store.createRecord("user-action", {
title: "Hello World",
});
assert.strictEqual(
plainAction.titleHtml,
"Hello World",
"returns plain text unchanged"
);

// Test with HTML that needs escaping
const htmlAction = store.createRecord("user-action", {
title: "<script>alert('xss')</script>",
});
assert.strictEqual(
htmlAction.titleHtml,
"&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;",
"escapes dangerous HTML"
);

// Test with emoji
const emojiAction = store.createRecord("user-action", {
title: "Great post :smile:",
});
assert.true(
emojiAction.titleHtml.includes("<img"),
"converts emoji to image tag"
);
assert.true(
emojiAction.titleHtml.includes("smile"),
"includes emoji name in output"
);

// Test with both HTML and emoji
const mixedAction = store.createRecord("user-action", {
title: "Cool <b>post</b> :heart:",
});
assert.true(
mixedAction.titleHtml.includes("&lt;b&gt;"),
"escapes HTML tags"
);
assert.true(
mixedAction.titleHtml.includes("<img"),
"still converts emojis"
);

// Test with null/undefined
const nullAction = store.createRecord("user-action", {
title: null,
});
assert.strictEqual(
nullAction.titleHtml,
null,
"returns null when title is null"
);

const undefinedAction = store.createRecord("user-action", {});
assert.strictEqual(
undefinedAction.titleHtml,
undefined,
"returns undefined when title is undefined"
);

// Test with empty string
const emptyAction = store.createRecord("user-action", {
title: "",
});
assert.strictEqual(
emptyAction.titleHtml,
"",
"returns empty string when title is empty"
);
});
});
Loading