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'
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'
}
}
+type HookContext = {
+ video?: MVideo
+ playlist?: MVideoPlaylist
+}
+
class ClientHtml {
private static htmlCache: { [path: string]: string } = {}
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)
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
}
return html
}
- const description = getPlainTextDescriptionCached(videoPlaylist.description)
+ const description = mdToOneLinePlainText(videoPlaylist.description)
let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name)
customHtml = ClientHtml.addDescriptionTag(customHtml, description)
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
}
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()
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)
const twitterCard = 'summary'
const schemaType = 'ProfilePage'
- customHtml = ClientHtml.addTags(customHtml, {
+ customHtml = await ClientHtml.addTags(customHtml, {
url,
originUrl,
escapedTitle: escapeHTML(title),
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()
}
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)
return metaTags
}
- private static generateSchemaTags (tags: Tags) {
+ private static async generateSchemaTags (tags: Tags, context: HookContext) {
const schema = {
'@context': 'http://schema.org',
'@type': tags.schemaType,
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
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()
}
}