mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-28 23:31:14 +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 { Component } from "inferno";
|
||||||
import { Post } from "lemmy-js-client";
|
import { Post } from "lemmy-js-client";
|
||||||
import * as sanitizeHtml from "sanitize-html";
|
import * as sanitizeHtml from "sanitize-html";
|
||||||
|
import { unescapeHTML } from "@utils/helpers";
|
||||||
import { relTags } from "../../config";
|
import { relTags } from "../../config";
|
||||||
import { Icon } from "../common/icon";
|
import { Icon } from "../common/icon";
|
||||||
|
|
||||||
|
@ -25,17 +26,21 @@ export class MetadataCard extends Component<MetadataCardProps> {
|
||||||
{post.name !== post.embed_title && (
|
{post.name !== post.embed_title && (
|
||||||
<>
|
<>
|
||||||
<h5 className="card-title d-inline">
|
<h5 className="card-title d-inline">
|
||||||
<a className="text-body" href={post.url} rel={relTags}>
|
<a
|
||||||
{post.embed_title}
|
className="text-body"
|
||||||
|
href={unescapeHTML(post.url)}
|
||||||
|
rel={relTags}
|
||||||
|
>
|
||||||
|
{unescapeHTML(post.embed_title)}
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
<span className="d-inline-block ms-2 mb-2 small text-muted">
|
<span className="d-inline-block ms-2 mb-2 small text-muted">
|
||||||
<a
|
<a
|
||||||
className="text-muted fst-italic"
|
className="text-muted fst-italic"
|
||||||
href={post.url}
|
href={unescapeHTML(post.url)}
|
||||||
rel={relTags}
|
rel={relTags}
|
||||||
>
|
>
|
||||||
{new URL(post.url).hostname}
|
{new URL(unescapeHTML(post.url)).hostname}
|
||||||
<Icon icon="external-link" classes="ms-1" />
|
<Icon icon="external-link" classes="ms-1" />
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
futureDaysToUnixTime,
|
futureDaysToUnixTime,
|
||||||
hostname,
|
hostname,
|
||||||
|
unescapeHTML,
|
||||||
} from "@utils/helpers";
|
} from "@utils/helpers";
|
||||||
import { isImage, isVideo } from "@utils/media";
|
import { isImage, isVideo } from "@utils/media";
|
||||||
import {
|
import {
|
||||||
|
@ -206,9 +207,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<>
|
<>
|
||||||
{this.listing()}
|
{this.listing()}
|
||||||
{this.state.imageExpanded && !this.props.hideImage && this.img}
|
{this.state.imageExpanded && !this.props.hideImage && this.img}
|
||||||
{this.showBody && post.url && post.embed_title && (
|
{this.showBody &&
|
||||||
<MetadataCard post={post} />
|
unescapeHTML(post.url) &&
|
||||||
)}
|
unescapeHTML(post.embed_title) && <MetadataCard post={post} />}
|
||||||
{this.showBody && this.body()}
|
{this.showBody && this.body()}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -285,8 +286,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<iframe
|
<iframe
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
className="post-metadata-iframe"
|
className="post-metadata-iframe"
|
||||||
src={post.embed_video_url}
|
src={unescapeHTML(post.embed_video_url)}
|
||||||
title={post.embed_title}
|
title={unescapeHTML(post.embed_title)}
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -309,7 +310,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
get imageSrc(): string | undefined {
|
get imageSrc(): string | undefined {
|
||||||
const post = this.postView.post;
|
const post = this.postView.post;
|
||||||
const url = post.url;
|
const url = unescapeHTML(post.url);
|
||||||
const thumbnail = post.thumbnail_url;
|
const thumbnail = post.thumbnail_url;
|
||||||
|
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
|
@ -323,7 +324,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
thumbnail() {
|
thumbnail() {
|
||||||
const post = this.postView.post;
|
const post = this.postView.post;
|
||||||
const url = post.url;
|
const url = unescapeHTML(post.url);
|
||||||
const thumbnail = post.thumbnail_url;
|
const thumbnail = post.thumbnail_url;
|
||||||
|
|
||||||
if (!this.props.hideImage && url && isImage(url) && this.imageSrc) {
|
if (!this.props.hideImage && url && isImage(url) && this.imageSrc) {
|
||||||
|
@ -449,7 +450,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="d-inline"
|
className="d-inline"
|
||||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
dangerouslySetInnerHTML={mdToHtmlInline(unescapeHTML(post.name))}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -457,7 +458,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
postTitleLine() {
|
postTitleLine() {
|
||||||
const post = this.postView.post;
|
const post = this.postView.post;
|
||||||
const url = post.url;
|
const url = unescapeHTML(post.url);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -473,7 +474,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
href={url}
|
href={url}
|
||||||
title={url}
|
title={url}
|
||||||
rel={relTags}
|
rel={relTags}
|
||||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
dangerouslySetInnerHTML={mdToHtmlInline(
|
||||||
|
unescapeHTML(post.name),
|
||||||
|
)}
|
||||||
></a>
|
></a>
|
||||||
) : (
|
) : (
|
||||||
this.postLink
|
this.postLink
|
||||||
|
@ -486,7 +489,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
* MetadataCard/body toggle.
|
* MetadataCard/body toggle.
|
||||||
*/}
|
*/}
|
||||||
{!this.props.showBody &&
|
{!this.props.showBody &&
|
||||||
((post.url && post.embed_title) || post.body) &&
|
(unescapeHTML(post.url && post.embed_title) || post.body) &&
|
||||||
this.showPreviewButton()}
|
this.showPreviewButton()}
|
||||||
|
|
||||||
{post.removed && (
|
{post.removed && (
|
||||||
|
@ -548,7 +551,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
urlLine() {
|
urlLine() {
|
||||||
const post = this.postView.post;
|
const post = this.postView.post;
|
||||||
const url = post.url;
|
const url = unescapeHTML(post.url);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="small m-0">
|
<p className="small m-0">
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
restoreScrollPosition,
|
restoreScrollPosition,
|
||||||
saveScrollPosition,
|
saveScrollPosition,
|
||||||
} from "@utils/browser";
|
} from "@utils/browser";
|
||||||
import { debounce, randomStr } from "@utils/helpers";
|
import { debounce, randomStr, unescapeHTML } from "@utils/helpers";
|
||||||
import { isImage } from "@utils/media";
|
import { isImage } from "@utils/media";
|
||||||
import { RouteDataResponse } from "@utils/types";
|
import { RouteDataResponse } from "@utils/types";
|
||||||
import autosize from "autosize";
|
import autosize from "autosize";
|
||||||
|
@ -325,8 +325,9 @@ export class Post extends Component<any, PostState> {
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
const siteName = this.state.siteRes.site_view.site.name;
|
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"
|
return this.state.postRes.state === "success"
|
||||||
? `${this.state.postRes.data.post_view.post.name} - ${siteName}`
|
? `${postTitle} - ${siteName}`
|
||||||
: 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 poll from "./poll";
|
||||||
import randomStr from "./random-str";
|
import randomStr from "./random-str";
|
||||||
import removeAuthParam from "./remove-auth-param";
|
import removeAuthParam from "./remove-auth-param";
|
||||||
|
import returnStringFromString from "./return-str";
|
||||||
import sleep from "./sleep";
|
import sleep from "./sleep";
|
||||||
import validEmail from "./valid-email";
|
import validEmail from "./valid-email";
|
||||||
import validInstanceTLD from "./valid-instance-tld";
|
import validInstanceTLD from "./valid-instance-tld";
|
||||||
import validTitle from "./valid-title";
|
import validTitle from "./valid-title";
|
||||||
import validURL from "./valid-url";
|
import validURL from "./valid-url";
|
||||||
|
import { escapeHTML, unescapeHTML } from "./html-entities";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
debounce,
|
debounce,
|
||||||
editListImmutable,
|
editListImmutable,
|
||||||
|
escapeHTML,
|
||||||
formatPastDate,
|
formatPastDate,
|
||||||
futureDaysToUnixTime,
|
futureDaysToUnixTime,
|
||||||
getIdFromString,
|
getIdFromString,
|
||||||
|
@ -45,7 +48,9 @@ export {
|
||||||
poll,
|
poll,
|
||||||
randomStr,
|
randomStr,
|
||||||
removeAuthParam,
|
removeAuthParam,
|
||||||
|
returnStringFromString,
|
||||||
sleep,
|
sleep,
|
||||||
|
unescapeHTML,
|
||||||
validEmail,
|
validEmail,
|
||||||
validInstanceTLD,
|
validInstanceTLD,
|
||||||
validTitle,
|
validTitle,
|
||||||
|
|
Loading…
Reference in a new issue