mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-25 13:51:13 +00:00
Improve handling/escaping of a few possible html entities for posts titles (&) - does not fix backend, only frontend display (#2087)
Co-authored-by: Beehaw Dev <dev@beehaw.dev>
This commit is contained in:
parent
02bb1b84e1
commit
8977b5353a
5 changed files with 92 additions and 18 deletions
|
@ -1,6 +1,7 @@
|
|||
import { Component } from "inferno";
|
||||
import { Post } from "lemmy-js-client";
|
||||
import * as sanitizeHtml from "sanitize-html";
|
||||
import { unescapeHTML } from "@utils/helpers";
|
||||
import { relTags } from "../../config";
|
||||
import { Icon } from "../common/icon";
|
||||
|
||||
|
@ -25,17 +26,21 @@ export class MetadataCard extends Component<MetadataCardProps> {
|
|||
{post.name !== post.embed_title && (
|
||||
<>
|
||||
<h5 className="card-title d-inline">
|
||||
<a className="text-body" href={post.url} rel={relTags}>
|
||||
{post.embed_title}
|
||||
<a
|
||||
className="text-body"
|
||||
href={unescapeHTML(post.url)}
|
||||
rel={relTags}
|
||||
>
|
||||
{unescapeHTML(post.embed_title)}
|
||||
</a>
|
||||
</h5>
|
||||
<span className="d-inline-block ms-2 mb-2 small text-muted">
|
||||
<a
|
||||
className="text-muted fst-italic"
|
||||
href={post.url}
|
||||
href={unescapeHTML(post.url)}
|
||||
rel={relTags}
|
||||
>
|
||||
{new URL(post.url).hostname}
|
||||
{new URL(unescapeHTML(post.url)).hostname}
|
||||
<Icon icon="external-link" classes="ms-1" />
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
capitalizeFirstLetter,
|
||||
futureDaysToUnixTime,
|
||||
hostname,
|
||||
unescapeHTML,
|
||||
} from "@utils/helpers";
|
||||
import { isImage, isVideo } from "@utils/media";
|
||||
import {
|
||||
|
@ -206,9 +207,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<>
|
||||
{this.listing()}
|
||||
{this.state.imageExpanded && !this.props.hideImage && this.img}
|
||||
{this.showBody && post.url && post.embed_title && (
|
||||
<MetadataCard post={post} />
|
||||
)}
|
||||
{this.showBody &&
|
||||
unescapeHTML(post.url) &&
|
||||
unescapeHTML(post.embed_title) && <MetadataCard post={post} />}
|
||||
{this.showBody && this.body()}
|
||||
</>
|
||||
) : (
|
||||
|
@ -285,8 +286,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<iframe
|
||||
allowFullScreen
|
||||
className="post-metadata-iframe"
|
||||
src={post.embed_video_url}
|
||||
title={post.embed_title}
|
||||
src={unescapeHTML(post.embed_video_url)}
|
||||
title={unescapeHTML(post.embed_title)}
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
|
@ -309,7 +310,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
get imageSrc(): string | undefined {
|
||||
const post = this.postView.post;
|
||||
const url = post.url;
|
||||
const url = unescapeHTML(post.url);
|
||||
const thumbnail = post.thumbnail_url;
|
||||
|
||||
if (thumbnail) {
|
||||
|
@ -323,7 +324,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
thumbnail() {
|
||||
const post = this.postView.post;
|
||||
const url = post.url;
|
||||
const url = unescapeHTML(post.url);
|
||||
const thumbnail = post.thumbnail_url;
|
||||
|
||||
if (!this.props.hideImage && url && isImage(url) && this.imageSrc) {
|
||||
|
@ -449,7 +450,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
>
|
||||
<span
|
||||
className="d-inline"
|
||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
||||
dangerouslySetInnerHTML={mdToHtmlInline(unescapeHTML(post.name))}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
|
@ -457,7 +458,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
postTitleLine() {
|
||||
const post = this.postView.post;
|
||||
const url = post.url;
|
||||
const url = unescapeHTML(post.url);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -473,7 +474,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
href={url}
|
||||
title={url}
|
||||
rel={relTags}
|
||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
||||
dangerouslySetInnerHTML={mdToHtmlInline(
|
||||
unescapeHTML(post.name),
|
||||
)}
|
||||
></a>
|
||||
) : (
|
||||
this.postLink
|
||||
|
@ -486,7 +489,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
* MetadataCard/body toggle.
|
||||
*/}
|
||||
{!this.props.showBody &&
|
||||
((post.url && post.embed_title) || post.body) &&
|
||||
(unescapeHTML(post.url && post.embed_title) || post.body) &&
|
||||
this.showPreviewButton()}
|
||||
|
||||
{post.removed && (
|
||||
|
@ -548,7 +551,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
urlLine() {
|
||||
const post = this.postView.post;
|
||||
const url = post.url;
|
||||
const url = unescapeHTML(post.url);
|
||||
|
||||
return (
|
||||
<p className="small m-0">
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
restoreScrollPosition,
|
||||
saveScrollPosition,
|
||||
} from "@utils/browser";
|
||||
import { debounce, randomStr } from "@utils/helpers";
|
||||
import { debounce, randomStr, unescapeHTML } from "@utils/helpers";
|
||||
import { isImage } from "@utils/media";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import autosize from "autosize";
|
||||
|
@ -325,8 +325,9 @@ export class Post extends Component<any, PostState> {
|
|||
|
||||
get documentTitle(): string {
|
||||
const siteName = this.state.siteRes.site_view.site.name;
|
||||
const postTitle = unescapeHTML(this.state.postRes.data.post_view.post.name);
|
||||
return this.state.postRes.state === "success"
|
||||
? `${this.state.postRes.data.post_view.post.name} - ${siteName}`
|
||||
? `${postTitle} - ${siteName}`
|
||||
: siteName;
|
||||
}
|
||||
|
||||
|
|
60
src/shared/utils/helpers/html-entities.ts
Normal file
60
src/shared/utils/helpers/html-entities.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
const matchEscHtmlRx = /["'&<>]/;
|
||||
const matchUnEscRx = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
|
||||
|
||||
export function escapeHTML(str: string): string {
|
||||
const matchEscHtml = matchEscHtmlRx.exec(str);
|
||||
if (!matchEscHtml) {
|
||||
return str;
|
||||
}
|
||||
let escape;
|
||||
let html = "";
|
||||
let index = 0;
|
||||
let lastIndex = 0;
|
||||
for (index = matchEscHtml.index; index < str.length; index++) {
|
||||
switch (str.charCodeAt(index)) {
|
||||
case 34: // "
|
||||
escape = """;
|
||||
break;
|
||||
case 38: // &
|
||||
escape = "&";
|
||||
break;
|
||||
case 39: // '
|
||||
escape = "'";
|
||||
break;
|
||||
case 60: // <
|
||||
escape = "<";
|
||||
break;
|
||||
case 62: // >
|
||||
escape = ">";
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastIndex !== index) {
|
||||
html += str.substring(lastIndex, index);
|
||||
}
|
||||
|
||||
lastIndex = index + 1;
|
||||
html += escape;
|
||||
}
|
||||
|
||||
return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
|
||||
}
|
||||
|
||||
export function unescapeHTML(str: string): string {
|
||||
const matchUnEsc = matchUnEscRx.exec(str);
|
||||
if (!matchUnEsc) {
|
||||
return str;
|
||||
}
|
||||
|
||||
const res = str
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/:/g, ":")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&/g, "&");
|
||||
|
||||
return unescapeHTML(res);
|
||||
}
|
|
@ -18,16 +18,19 @@ import numToSI from "./num-to-si";
|
|||
import poll from "./poll";
|
||||
import randomStr from "./random-str";
|
||||
import removeAuthParam from "./remove-auth-param";
|
||||
import returnStringFromString from "./return-str";
|
||||
import sleep from "./sleep";
|
||||
import validEmail from "./valid-email";
|
||||
import validInstanceTLD from "./valid-instance-tld";
|
||||
import validTitle from "./valid-title";
|
||||
import validURL from "./valid-url";
|
||||
import { escapeHTML, unescapeHTML } from "./html-entities";
|
||||
|
||||
export {
|
||||
capitalizeFirstLetter,
|
||||
debounce,
|
||||
editListImmutable,
|
||||
escapeHTML,
|
||||
formatPastDate,
|
||||
futureDaysToUnixTime,
|
||||
getIdFromString,
|
||||
|
@ -45,7 +48,9 @@ export {
|
|||
poll,
|
||||
randomStr,
|
||||
removeAuthParam,
|
||||
returnStringFromString,
|
||||
sleep,
|
||||
unescapeHTML,
|
||||
validEmail,
|
||||
validInstanceTLD,
|
||||
validTitle,
|
||||
|
|
Loading…
Reference in a new issue