diff options
author | Chocobozzz <me@florianbigard.com> | 2021-11-30 08:31:56 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-11-30 08:31:56 +0100 |
commit | 55cb8bc73c259cb8e41c913aacdc3087c7421049 (patch) | |
tree | ec2b3d32f57a47769516659dd37c1eaf11cc25bc /server/lib/client-html.ts | |
parent | 1ffb76221a963810be8d59345ac651c1882c755b (diff) | |
download | PeerTube-55cb8bc73c259cb8e41c913aacdc3087c7421049.tar.gz PeerTube-55cb8bc73c259cb8e41c913aacdc3087c7421049.tar.zst PeerTube-55cb8bc73c259cb8e41c913aacdc3087c7421049.zip |
Correctly escape meta tags
Diffstat (limited to 'server/lib/client-html.ts')
-rw-r--r-- | server/lib/client-html.ts | 105 |
1 files changed, 54 insertions, 51 deletions
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 84eb33348..b2948254b 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -2,6 +2,7 @@ import express from 'express' | |||
2 | import { readFile } from 'fs-extra' | 2 | import { readFile } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import validator from 'validator' | 4 | import validator from 'validator' |
5 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' | ||
5 | import { escapeHTML } from '@shared/core-utils/renderer' | 6 | import { escapeHTML } from '@shared/core-utils/renderer' |
6 | import { HTMLServerConfig } from '@shared/models' | 7 | import { HTMLServerConfig } from '@shared/models' |
7 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' | 8 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' |
@@ -27,7 +28,6 @@ import { VideoChannelModel } from '../models/video/video-channel' | |||
27 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 28 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
28 | import { MAccountActor, MChannelActor } from '../types/models' | 29 | import { MAccountActor, MChannelActor } from '../types/models' |
29 | import { ServerConfigManager } from './server-config-manager' | 30 | import { ServerConfigManager } from './server-config-manager' |
30 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' | ||
31 | 31 | ||
32 | type Tags = { | 32 | type Tags = { |
33 | ogType: string | 33 | ogType: string |
@@ -38,11 +38,12 @@ type Tags = { | |||
38 | numberOfItems: number | 38 | numberOfItems: number |
39 | } | 39 | } |
40 | 40 | ||
41 | siteName: string | 41 | escapedSiteName: string |
42 | title: string | 42 | escapedTitle: string |
43 | escapedDescription: string | ||
44 | |||
43 | url: string | 45 | url: string |
44 | originUrl: string | 46 | originUrl: string |
45 | description: string | ||
46 | 47 | ||
47 | disallowIndexation?: boolean | 48 | disallowIndexation?: boolean |
48 | 49 | ||
@@ -100,15 +101,15 @@ class ClientHtml { | |||
100 | res.status(HttpStatusCode.NOT_FOUND_404) | 101 | res.status(HttpStatusCode.NOT_FOUND_404) |
101 | return html | 102 | return html |
102 | } | 103 | } |
104 | const description = mdToPlainText(video.description) | ||
103 | 105 | ||
104 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name)) | 106 | let customHtml = ClientHtml.addTitleTag(html, video.name) |
105 | customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(video.description)) | 107 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) |
106 | 108 | ||
107 | const url = WEBSERVER.URL + video.getWatchStaticPath() | 109 | const url = WEBSERVER.URL + video.getWatchStaticPath() |
108 | const originUrl = video.url | 110 | const originUrl = video.url |
109 | const title = escapeHTML(video.name) | 111 | const title = video.name |
110 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) | 112 | const siteName = CONFIG.INSTANCE.NAME |
111 | const description = mdToPlainText(video.description) | ||
112 | 113 | ||
113 | const image = { | 114 | const image = { |
114 | url: WEBSERVER.URL + video.getPreviewStaticPath() | 115 | url: WEBSERVER.URL + video.getPreviewStaticPath() |
@@ -128,9 +129,9 @@ class ClientHtml { | |||
128 | customHtml = ClientHtml.addTags(customHtml, { | 129 | customHtml = ClientHtml.addTags(customHtml, { |
129 | url, | 130 | url, |
130 | originUrl, | 131 | originUrl, |
131 | siteName, | 132 | escapedSiteName: escapeHTML(siteName), |
132 | title, | 133 | escapedTitle: escapeHTML(title), |
133 | description, | 134 | escapedDescription: escapeHTML(description), |
134 | image, | 135 | image, |
135 | embed, | 136 | embed, |
136 | ogType, | 137 | ogType, |
@@ -161,14 +162,15 @@ class ClientHtml { | |||
161 | return html | 162 | return html |
162 | } | 163 | } |
163 | 164 | ||
164 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(videoPlaylist.name)) | 165 | const description = mdToPlainText(videoPlaylist.description) |
165 | customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(videoPlaylist.description)) | 166 | |
167 | let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name) | ||
168 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) | ||
166 | 169 | ||
167 | const url = WEBSERVER.URL + videoPlaylist.getWatchStaticPath() | 170 | const url = WEBSERVER.URL + videoPlaylist.getWatchStaticPath() |
168 | const originUrl = videoPlaylist.url | 171 | const originUrl = videoPlaylist.url |
169 | const title = escapeHTML(videoPlaylist.name) | 172 | const title = videoPlaylist.name |
170 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) | 173 | const siteName = CONFIG.INSTANCE.NAME |
171 | const description = mdToPlainText(videoPlaylist.description) | ||
172 | 174 | ||
173 | const image = { | 175 | const image = { |
174 | url: videoPlaylist.getThumbnailUrl() | 176 | url: videoPlaylist.getThumbnailUrl() |
@@ -190,10 +192,10 @@ class ClientHtml { | |||
190 | customHtml = ClientHtml.addTags(customHtml, { | 192 | customHtml = ClientHtml.addTags(customHtml, { |
191 | url, | 193 | url, |
192 | originUrl, | 194 | originUrl, |
193 | siteName, | 195 | escapedSiteName: escapeHTML(siteName), |
196 | escapedTitle: escapeHTML(title), | ||
197 | escapedDescription: escapeHTML(description), | ||
194 | embed, | 198 | embed, |
195 | title, | ||
196 | description, | ||
197 | image, | 199 | image, |
198 | list, | 200 | list, |
199 | ogType, | 201 | ogType, |
@@ -259,14 +261,15 @@ class ClientHtml { | |||
259 | return ClientHtml.getIndexHTML(req, res) | 261 | return ClientHtml.getIndexHTML(req, res) |
260 | } | 262 | } |
261 | 263 | ||
262 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(entity.getDisplayName())) | 264 | const description = mdToPlainText(entity.description) |
263 | customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(entity.description)) | 265 | |
266 | let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName()) | ||
267 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) | ||
264 | 268 | ||
265 | const url = entity.getLocalUrl() | 269 | const url = entity.getLocalUrl() |
266 | const originUrl = entity.Actor.url | 270 | const originUrl = entity.Actor.url |
267 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) | 271 | const siteName = CONFIG.INSTANCE.NAME |
268 | const title = escapeHTML(entity.getDisplayName()) | 272 | const title = entity.getDisplayName() |
269 | const description = mdToPlainText(entity.description) | ||
270 | 273 | ||
271 | const image = { | 274 | const image = { |
272 | url: entity.Actor.getAvatarUrl(), | 275 | url: entity.Actor.getAvatarUrl(), |
@@ -281,9 +284,9 @@ class ClientHtml { | |||
281 | customHtml = ClientHtml.addTags(customHtml, { | 284 | customHtml = ClientHtml.addTags(customHtml, { |
282 | url, | 285 | url, |
283 | originUrl, | 286 | originUrl, |
284 | title, | 287 | escapedTitle: escapeHTML(title), |
285 | siteName, | 288 | escapedSiteName: escapeHTML(siteName), |
286 | description, | 289 | escapedDescription: escapeHTML(description), |
287 | image, | 290 | image, |
288 | ogType, | 291 | ogType, |
289 | twitterCard, | 292 | twitterCard, |
@@ -367,14 +370,14 @@ class ClientHtml { | |||
367 | let text = title || CONFIG.INSTANCE.NAME | 370 | let text = title || CONFIG.INSTANCE.NAME |
368 | if (title) text += ` - ${CONFIG.INSTANCE.NAME}` | 371 | if (title) text += ` - ${CONFIG.INSTANCE.NAME}` |
369 | 372 | ||
370 | const titleTag = `<title>${text}</title>` | 373 | const titleTag = `<title>${escapeHTML(text)}</title>` |
371 | 374 | ||
372 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.TITLE, titleTag) | 375 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.TITLE, titleTag) |
373 | } | 376 | } |
374 | 377 | ||
375 | private static addDescriptionTag (htmlStringPage: string, description?: string) { | 378 | private static addDescriptionTag (htmlStringPage: string, description?: string) { |
376 | const content = description || CONFIG.INSTANCE.SHORT_DESCRIPTION | 379 | const content = description || CONFIG.INSTANCE.SHORT_DESCRIPTION |
377 | const descriptionTag = `<meta name="description" content="${content}" />` | 380 | const descriptionTag = `<meta name="description" content="${escapeHTML(content)}" />` |
378 | 381 | ||
379 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.DESCRIPTION, descriptionTag) | 382 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.DESCRIPTION, descriptionTag) |
380 | } | 383 | } |
@@ -406,8 +409,8 @@ class ClientHtml { | |||
406 | private static generateOpenGraphMetaTags (tags: Tags) { | 409 | private static generateOpenGraphMetaTags (tags: Tags) { |
407 | const metaTags = { | 410 | const metaTags = { |
408 | 'og:type': tags.ogType, | 411 | 'og:type': tags.ogType, |
409 | 'og:site_name': tags.siteName, | 412 | 'og:site_name': tags.escapedSiteName, |
410 | 'og:title': tags.title, | 413 | 'og:title': tags.escapedTitle, |
411 | 'og:image': tags.image.url | 414 | 'og:image': tags.image.url |
412 | } | 415 | } |
413 | 416 | ||
@@ -417,7 +420,7 @@ class ClientHtml { | |||
417 | } | 420 | } |
418 | 421 | ||
419 | metaTags['og:url'] = tags.url | 422 | metaTags['og:url'] = tags.url |
420 | metaTags['og:description'] = mdToPlainText(tags.description) | 423 | metaTags['og:description'] = tags.escapedDescription |
421 | 424 | ||
422 | if (tags.embed) { | 425 | if (tags.embed) { |
423 | metaTags['og:video:url'] = tags.embed.url | 426 | metaTags['og:video:url'] = tags.embed.url |
@@ -432,8 +435,8 @@ class ClientHtml { | |||
432 | 435 | ||
433 | private static generateStandardMetaTags (tags: Tags) { | 436 | private static generateStandardMetaTags (tags: Tags) { |
434 | return { | 437 | return { |
435 | name: tags.title, | 438 | name: tags.escapedTitle, |
436 | description: mdToPlainText(tags.description), | 439 | description: tags.escapedDescription, |
437 | image: tags.image.url | 440 | image: tags.image.url |
438 | } | 441 | } |
439 | } | 442 | } |
@@ -442,8 +445,8 @@ class ClientHtml { | |||
442 | const metaTags = { | 445 | const metaTags = { |
443 | 'twitter:card': tags.twitterCard, | 446 | 'twitter:card': tags.twitterCard, |
444 | 'twitter:site': CONFIG.SERVICES.TWITTER.USERNAME, | 447 | 'twitter:site': CONFIG.SERVICES.TWITTER.USERNAME, |
445 | 'twitter:title': tags.title, | 448 | 'twitter:title': tags.escapedTitle, |
446 | 'twitter:description': tags.description, | 449 | 'twitter:description': tags.escapedDescription, |
447 | 'twitter:image': tags.image.url | 450 | 'twitter:image': tags.image.url |
448 | } | 451 | } |
449 | 452 | ||
@@ -465,8 +468,8 @@ class ClientHtml { | |||
465 | const schema = { | 468 | const schema = { |
466 | '@context': 'http://schema.org', | 469 | '@context': 'http://schema.org', |
467 | '@type': tags.schemaType, | 470 | '@type': tags.schemaType, |
468 | 'name': tags.title, | 471 | 'name': tags.escapedTitle, |
469 | 'description': tags.description, | 472 | 'description': tags.escapedDescription, |
470 | 'image': tags.image.url, | 473 | 'image': tags.image.url, |
471 | 'url': tags.url | 474 | 'url': tags.url |
472 | } | 475 | } |
@@ -496,59 +499,59 @@ class ClientHtml { | |||
496 | const twitterCardMetaTags = this.generateTwitterCardMetaTags(tagsValues) | 499 | const twitterCardMetaTags = this.generateTwitterCardMetaTags(tagsValues) |
497 | const schemaTags = this.generateSchemaTags(tagsValues) | 500 | const schemaTags = this.generateSchemaTags(tagsValues) |
498 | 501 | ||
499 | const { url, title, embed, originUrl, disallowIndexation } = tagsValues | 502 | const { url, escapedTitle, embed, originUrl, disallowIndexation } = tagsValues |
500 | 503 | ||
501 | const oembedLinkTags: { type: string, href: string, title: string }[] = [] | 504 | const oembedLinkTags: { type: string, href: string, escapedTitle: string }[] = [] |
502 | 505 | ||
503 | if (embed) { | 506 | if (embed) { |
504 | oembedLinkTags.push({ | 507 | oembedLinkTags.push({ |
505 | type: 'application/json+oembed', | 508 | type: 'application/json+oembed', |
506 | href: WEBSERVER.URL + '/services/oembed?url=' + encodeURIComponent(url), | 509 | href: WEBSERVER.URL + '/services/oembed?url=' + encodeURIComponent(url), |
507 | title | 510 | escapedTitle |
508 | }) | 511 | }) |
509 | } | 512 | } |
510 | 513 | ||
511 | let tagsString = '' | 514 | let tagsStr = '' |
512 | 515 | ||
513 | // Opengraph | 516 | // Opengraph |
514 | Object.keys(openGraphMetaTags).forEach(tagName => { | 517 | Object.keys(openGraphMetaTags).forEach(tagName => { |
515 | const tagValue = openGraphMetaTags[tagName] | 518 | const tagValue = openGraphMetaTags[tagName] |
516 | 519 | ||
517 | tagsString += `<meta property="${tagName}" content="${tagValue}" />` | 520 | tagsStr += `<meta property="${tagName}" content="${tagValue}" />` |
518 | }) | 521 | }) |
519 | 522 | ||
520 | // Standard | 523 | // Standard |
521 | Object.keys(standardMetaTags).forEach(tagName => { | 524 | Object.keys(standardMetaTags).forEach(tagName => { |
522 | const tagValue = standardMetaTags[tagName] | 525 | const tagValue = standardMetaTags[tagName] |
523 | 526 | ||
524 | tagsString += `<meta property="${tagName}" content="${tagValue}" />` | 527 | tagsStr += `<meta property="${tagName}" content="${tagValue}" />` |
525 | }) | 528 | }) |
526 | 529 | ||
527 | // Twitter card | 530 | // Twitter card |
528 | Object.keys(twitterCardMetaTags).forEach(tagName => { | 531 | Object.keys(twitterCardMetaTags).forEach(tagName => { |
529 | const tagValue = twitterCardMetaTags[tagName] | 532 | const tagValue = twitterCardMetaTags[tagName] |
530 | 533 | ||
531 | tagsString += `<meta property="${tagName}" content="${tagValue}" />` | 534 | tagsStr += `<meta property="${tagName}" content="${tagValue}" />` |
532 | }) | 535 | }) |
533 | 536 | ||
534 | // OEmbed | 537 | // OEmbed |
535 | for (const oembedLinkTag of oembedLinkTags) { | 538 | for (const oembedLinkTag of oembedLinkTags) { |
536 | tagsString += `<link rel="alternate" type="${oembedLinkTag.type}" href="${oembedLinkTag.href}" title="${oembedLinkTag.title}" />` | 539 | tagsStr += `<link rel="alternate" type="${oembedLinkTag.type}" href="${oembedLinkTag.href}" title="${oembedLinkTag.escapedTitle}" />` |
537 | } | 540 | } |
538 | 541 | ||
539 | // Schema.org | 542 | // Schema.org |
540 | if (schemaTags) { | 543 | if (schemaTags) { |
541 | tagsString += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>` | 544 | tagsStr += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>` |
542 | } | 545 | } |
543 | 546 | ||
544 | // SEO, use origin URL | 547 | // SEO, use origin URL |
545 | tagsString += `<link rel="canonical" href="${originUrl}" />` | 548 | tagsStr += `<link rel="canonical" href="${originUrl}" />` |
546 | 549 | ||
547 | if (disallowIndexation) { | 550 | if (disallowIndexation) { |
548 | tagsString += `<meta name="robots" content="noindex" />` | 551 | tagsStr += `<meta name="robots" content="noindex" />` |
549 | } | 552 | } |
550 | 553 | ||
551 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.META_TAGS, tagsString) | 554 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.META_TAGS, tagsStr) |
552 | } | 555 | } |
553 | } | 556 | } |
554 | 557 | ||