From caf2aaf4f9d38ad441a5562c3b8720f8779d6f78 Mon Sep 17 00:00:00 2001 From: Kim <1877318+kimsible@users.noreply.github.com> Date: Fri, 10 Jul 2020 10:20:11 +0200 Subject: Add ability to override client assets : logo - favicon - PWA icons - PWA manifest name and description (#2897) * Add client-overrides storage to config * Add static-serve for client overrides * Move backgroun-image logo from bundle to css tag for runtime content hash * Add dynamic JSON manifest * Add content hash for manifest, favicon and logo Co-authored-by: kimsible --- server/controllers/client.ts | 52 +++++++++++++++++++++++++++++++++++++--- server/initializers/config.ts | 3 ++- server/initializers/constants.ts | 17 ++++++++++++- server/lib/client-html.ts | 17 ++++++++++++- 4 files changed, 83 insertions(+), 6 deletions(-) (limited to 'server') diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 65b5a053c..88f51907b 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -1,3 +1,4 @@ +import { constants, promises as fs } from 'fs' import * as express from 'express' import { join } from 'path' import { root } from '../helpers/core-utils' @@ -39,20 +40,40 @@ clientsRouter.use( ) // Static HTML/CSS/JS client files - const staticClientFiles = [ - 'manifest.webmanifest', 'ngsw-worker.js', 'ngsw.json' ] + for (const staticClientFile of staticClientFiles) { const path = join(root(), 'client', 'dist', staticClientFile) - clientsRouter.get('/' + staticClientFile, (req: express.Request, res: express.Response) => { + clientsRouter.get(`/${staticClientFile}`, (req: express.Request, res: express.Response) => { res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) }) } +// Dynamic PWA manifest +clientsRouter.get('/manifest.webmanifest', asyncMiddleware(generateManifest)) + +// Static client overrides +const staticClientOverrides = [ + 'assets/images/logo.svg', + 'assets/images/favicon.png', + 'assets/images/icons/icon-36x36.png', + 'assets/images/icons/icon-48x48.png', + 'assets/images/icons/icon-72x72.png', + 'assets/images/icons/icon-96x96.png', + 'assets/images/icons/icon-144x144.png', + 'assets/images/icons/icon-192x192.png', + 'assets/images/icons/icon-512x512.png' +] + +for (const staticClientOverride of staticClientOverrides) { + const overridePhysicalPath = join(CONFIG.STORAGE.CLIENT_OVERRIDES_DIR, staticClientOverride) + clientsRouter.use(`/client/${staticClientOverride}`, asyncMiddleware(serveClientOverride(overridePhysicalPath))) +} + clientsRouter.use('/client/locales/:locale/:file.json', serveServerTranslations) clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.CLIENT })) @@ -130,3 +151,28 @@ function sendHTML (html: string, res: express.Response) { return res.send(html) } + +async function generateManifest (req: express.Request, res: express.Response) { + const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest') + const manifestJson = await fs.readFile(manifestPhysicalPath, 'utf8') + const manifest = JSON.parse(manifestJson) + + manifest.name = CONFIG.INSTANCE.NAME + manifest.short_name = CONFIG.INSTANCE.NAME + manifest.description = CONFIG.INSTANCE.SHORT_DESCRIPTION + + res.json(manifest) +} + +function serveClientOverride (path: string) { + return async (req: express.Request, res: express.Response, next: express.NextFunction) => { + try { + await fs.access(path, constants.F_OK) + // Serve override client + res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) + } catch { + // Serve dist client + next() + } + } +} diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 48e2cbc1a..32bd3bbe2 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -68,7 +68,8 @@ const CONFIG = { CAPTIONS_DIR: buildPath(config.get('storage.captions')), TORRENTS_DIR: buildPath(config.get('storage.torrents')), CACHE_DIR: buildPath(config.get('storage.cache')), - PLUGINS_DIR: buildPath(config.get('storage.plugins')) + PLUGINS_DIR: buildPath(config.get('storage.plugins')), + CLIENT_OVERRIDES_DIR: buildPath(config.get('storage.client_overrides')) }, WEBSERVER: { SCHEME: config.get('webserver.https') === true ? 'https' : 'http', diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 9a262fd4b..e730e3c84 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -1,4 +1,5 @@ import { join } from 'path' +import { randomBytes } from 'crypto' import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models' import { ActivityPubActorType } from '../../shared/models/activitypub' import { FollowState } from '../../shared/models/actors' @@ -710,6 +711,14 @@ registerConfigChangedHandler(() => { // --------------------------------------------------------------------------- +const FILES_CONTENT_HASH = { + MANIFEST: generateContentHash(), + FAVICON: generateContentHash(), + LOGO: generateContentHash() +} + +// --------------------------------------------------------------------------- + export { WEBSERVER, API_VERSION, @@ -792,8 +801,10 @@ export { VIDEO_PLAYLIST_PRIVACIES, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME, ASSETS_PATH, + FILES_CONTENT_HASH, loadLanguages, - buildLanguages + buildLanguages, + generateContentHash } // --------------------------------------------------------------------------- @@ -895,3 +906,7 @@ function buildLanguages () { return languages } + +function generateContentHash () { + return randomBytes(20).toString('hex') +} diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 3e6da2898..5996f3c70 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts @@ -1,6 +1,6 @@ import * as express from 'express' import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' -import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER } from '../initializers/constants' +import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER, FILES_CONTENT_HASH } from '../initializers/constants' import { join } from 'path' import { escapeHTML, sha256 } from '../helpers/core-utils' import { VideoModel } from '../models/video/video' @@ -101,6 +101,9 @@ export class ClientHtml { let html = buffer.toString() if (paramLang) html = ClientHtml.addHtmlLang(html, paramLang) + html = ClientHtml.addManifestContentHash(html) + html = ClientHtml.addFaviconContentHash(html) + html = ClientHtml.addLogoContentHash(html) html = ClientHtml.addCustomCSS(html) html = await ClientHtml.addAsyncPluginCSS(html) @@ -136,6 +139,18 @@ export class ClientHtml { return htmlStringPage.replace('', ``) } + private static addManifestContentHash (htmlStringPage: string) { + return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST) + } + + private static addFaviconContentHash(htmlStringPage: string) { + return htmlStringPage.replace('[faviconContentHash]', FILES_CONTENT_HASH.FAVICON) + } + + private static addLogoContentHash(htmlStringPage: string) { + return htmlStringPage.replace('[logoContentHash]', FILES_CONTENT_HASH.LOGO) + } + private static addTitleTag (htmlStringPage: string, title?: string) { let text = title || CONFIG.INSTANCE.NAME if (title) text += ` - ${CONFIG.INSTANCE.NAME}` -- cgit v1.2.3