]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/lib/client-html.ts
Force live stream termination
[github/Chocobozzz/PeerTube.git] / server / lib / client-html.ts
index 945bc712fe388b5300ad1531161155172ed47ab2..058f29f03f1c8c4f2a81ac8cc31dcfa7bb477a69 100644 (file)
@@ -1,9 +1,10 @@
 import express from 'express'
-import { readFile } from 'fs-extra'
-import memoizee from 'memoizee'
+import { pathExists, readFile } from 'fs-extra'
 import { join } from 'path'
 import validator from 'validator'
+import { isTestOrDevInstance } from '@server/helpers/core-utils'
 import { toCompleteUUID } from '@server/helpers/custom-validators/misc'
+import { mdToOneLinePlainText } from '@server/helpers/markdown'
 import { ActorImageModel } from '@server/models/actor/actor-image'
 import { root } from '@shared/core-utils'
 import { escapeHTML } from '@shared/core-utils/renderer'
@@ -12,34 +13,26 @@ import { HTMLServerConfig } from '@shared/models'
 import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
 import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
 import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
-import { isTestInstance } from '../helpers/core-utils'
 import { logger } from '../helpers/logger'
-import { mdToOneLinePlainText } from '../helpers/markdown'
 import { CONFIG } from '../initializers/config'
 import {
   ACCEPT_HEADERS,
   CUSTOM_HTML_TAG_COMMENTS,
   EMBED_SIZE,
   FILES_CONTENT_HASH,
-  MEMOIZE_LENGTH,
-  MEMOIZE_TTL,
   PLUGIN_GLOBAL_CSS_PATH,
   WEBSERVER
 } from '../initializers/constants'
 import { AccountModel } from '../models/account/account'
-import { getActivityStreamDuration } from '../models/video/formatter/video-format-utils'
 import { VideoModel } from '../models/video/video'
 import { VideoChannelModel } from '../models/video/video-channel'
 import { VideoPlaylistModel } from '../models/video/video-playlist'
-import { MAccountActor, MChannelActor } from '../types/models'
+import { MAccountActor, MChannelActor, MVideo, MVideoPlaylist } from '../types/models'
+import { getActivityStreamDuration } from './activitypub/activity'
 import { getBiggestActorImage } from './actor-image'
+import { Hooks } from './plugins/hooks'
 import { ServerConfigManager } from './server-config-manager'
 
-const getPlainTextDescriptionCached = memoizee(mdToOneLinePlainText, {
-  maxAge: MEMOIZE_TTL.MD_TO_PLAIN_TEXT_CLIENT_HTML,
-  max: MEMOIZE_LENGTH.MD_TO_PLAIN_TEXT_CLIENT_HTML
-})
-
 type Tags = {
   ogType: string
   twitterCard: 'player' | 'summary' | 'summary_large_image'
@@ -72,6 +65,11 @@ type Tags = {
   }
 }
 
+type HookContext = {
+  video?: MVideo
+  playlist?: MVideoPlaylist
+}
+
 class ClientHtml {
 
   private static htmlCache: { [path: string]: string } = {}
@@ -112,7 +110,7 @@ class ClientHtml {
       res.status(HttpStatusCode.NOT_FOUND_404)
       return html
     }
-    const description = getPlainTextDescriptionCached(video.description)
+    const description = mdToOneLinePlainText(video.description)
 
     let customHtml = ClientHtml.addTitleTag(html, video.name)
     customHtml = ClientHtml.addDescriptionTag(customHtml, description)
@@ -137,18 +135,19 @@ class ClientHtml {
     const twitterCard = CONFIG.SERVICES.TWITTER.WHITELISTED ? 'player' : 'summary_large_image'
     const schemaType = 'VideoObject'
 
-    customHtml = ClientHtml.addTags(customHtml, {
+    customHtml = await ClientHtml.addTags(customHtml, {
       url,
       originUrl,
       escapedSiteName: escapeHTML(siteName),
       escapedTitle: escapeHTML(title),
       escapedDescription: escapeHTML(description),
+      disallowIndexation: video.privacy !== VideoPrivacy.PUBLIC,
       image,
       embed,
       ogType,
       twitterCard,
       schemaType
-    })
+    }, { video })
 
     return customHtml
   }
@@ -173,7 +172,7 @@ class ClientHtml {
       return html
     }
 
-    const description = getPlainTextDescriptionCached(videoPlaylist.description)
+    const description = mdToOneLinePlainText(videoPlaylist.description)
 
     let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name)
     customHtml = ClientHtml.addDescriptionTag(customHtml, description)
@@ -200,19 +199,20 @@ class ClientHtml {
     const twitterCard = CONFIG.SERVICES.TWITTER.WHITELISTED ? 'player' : 'summary'
     const schemaType = 'ItemList'
 
-    customHtml = ClientHtml.addTags(customHtml, {
+    customHtml = await ClientHtml.addTags(customHtml, {
       url,
       originUrl,
       escapedSiteName: escapeHTML(siteName),
       escapedTitle: escapeHTML(title),
       escapedDescription: escapeHTML(description),
+      disallowIndexation: videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC,
       embed,
       image,
       list,
       ogType,
       twitterCard,
       schemaType
-    })
+    }, { playlist: videoPlaylist })
 
     return customHtml
   }
@@ -239,7 +239,10 @@ class ClientHtml {
   static async getEmbedHTML () {
     const path = ClientHtml.getEmbedPath()
 
-    if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
+    // Disable HTML cache in dev mode because webpack can regenerate JS files
+    if (!isTestOrDevInstance() && ClientHtml.htmlCache[path]) {
+      return ClientHtml.htmlCache[path]
+    }
 
     const buffer = await readFile(path)
     const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
@@ -272,7 +275,7 @@ class ClientHtml {
       return ClientHtml.getIndexHTML(req, res)
     }
 
-    const description = getPlainTextDescriptionCached(entity.description)
+    const description = mdToOneLinePlainText(entity.description)
 
     let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName())
     customHtml = ClientHtml.addDescriptionTag(customHtml, description)
@@ -293,7 +296,7 @@ class ClientHtml {
     const twitterCard = 'summary'
     const schemaType = 'ProfilePage'
 
-    customHtml = ClientHtml.addTags(customHtml, {
+    customHtml = await ClientHtml.addTags(customHtml, {
       url,
       originUrl,
       escapedTitle: escapeHTML(title),
@@ -304,14 +307,14 @@ class ClientHtml {
       twitterCard,
       schemaType,
       disallowIndexation: !entity.Actor.isOwned()
-    })
+    }, {})
 
     return customHtml
   }
 
   private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) {
     const path = ClientHtml.getIndexPath(req, res, paramLang)
-    if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
+    if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
 
     const buffer = await readFile(path)
     const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
@@ -405,7 +408,20 @@ class ClientHtml {
   }
 
   private static async addAsyncPluginCSS (htmlStringPage: string) {
-    const globalCSSContent = await readFile(PLUGIN_GLOBAL_CSS_PATH)
+    if (!pathExists(PLUGIN_GLOBAL_CSS_PATH)) {
+      logger.info('Plugin Global CSS file is not available (generation may still be in progress), ignoring it.')
+      return htmlStringPage
+    }
+
+    let globalCSSContent: Buffer
+
+    try {
+      globalCSSContent = await readFile(PLUGIN_GLOBAL_CSS_PATH)
+    } catch (err) {
+      logger.error('Error retrieving the Plugin Global CSS file, ignoring it.', { err })
+      return htmlStringPage
+    }
+
     if (globalCSSContent.byteLength === 0) return htmlStringPage
 
     const fileHash = sha256(globalCSSContent)
@@ -472,7 +488,7 @@ class ClientHtml {
     return metaTags
   }
 
-  private static generateSchemaTags (tags: Tags) {
+  private static async generateSchemaTags (tags: Tags, context: HookContext) {
     const schema = {
       '@context': 'http://schema.org',
       '@type': tags.schemaType,
@@ -492,20 +508,19 @@ class ClientHtml {
       schema['uploadDate'] = tags.embed.createdAt
 
       if (tags.embed.duration) schema['duration'] = tags.embed.duration
-      if (tags.embed.views) schema['iterationCount'] = tags.embed.views
 
       schema['thumbnailUrl'] = tags.image.url
       schema['contentUrl'] = tags.url
     }
 
-    return schema
+    return Hooks.wrapObject(schema, 'filter:html.client.json-ld.result', context)
   }
 
-  private static addTags (htmlStringPage: string, tagsValues: Tags) {
+  private static async addTags (htmlStringPage: string, tagsValues: Tags, context: HookContext) {
     const openGraphMetaTags = this.generateOpenGraphMetaTags(tagsValues)
     const standardMetaTags = this.generateStandardMetaTags(tagsValues)
     const twitterCardMetaTags = this.generateTwitterCardMetaTags(tagsValues)
-    const schemaTags = this.generateSchemaTags(tagsValues)
+    const schemaTags = await this.generateSchemaTags(tagsValues, context)
 
     const { url, escapedTitle, embed, originUrl, disallowIndexation } = tagsValues
 
@@ -579,7 +594,7 @@ async function serveIndexHTML (req: express.Request, res: express.Response) {
       await generateHTMLPage(req, res, req.params.language)
       return
     } catch (err) {
-      logger.error('Cannot generate HTML page.', err)
+      logger.error('Cannot generate HTML page.', { err })
       return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end()
     }
   }