private route: ActivatedRoute,
private router: Router,
private auth: AuthService,
- private serverService: ServerService
+ private serverService: ServerService,
+ private redirectService: RedirectService
) {
super(data)
this.algorithmChangeSub = this.route.queryParams.subscribe(
queryParams => {
- const algorithm = queryParams['alg']
- if (algorithm) {
- this.data.model = algorithm
- } else {
- this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM
- }
+ this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
}
)
}
}
setSort () {
- const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM
+ const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm()
? this.data.model
: undefined
protected storageService: LocalStorageService,
protected cfr: ComponentFactoryResolver,
private videoService: VideoService,
+ private redirectService: RedirectService,
private hooks: HooksService
) {
super()
- this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM)
+ this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
this.headerComponentInjector = this.getInjector()
}
}
protected loadPageRouteParams (queryParams: Params) {
- const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM
+ const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
this.sort = this.parseAlgorithm(algorithm)
}
switch (algorithm) {
case 'most-viewed':
return '-trending'
+
case 'most-liked':
return '-likes'
+
default:
return '-' + algorithm as VideoSortField
}
}
goToDefaultRoute () {
- return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE)
+ return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
}
ngOnInit () {
export class RedirectService {
// Default route could change according to the instance configuration
static INIT_DEFAULT_ROUTE = '/videos/trending'
- static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
- static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
private previousUrl: string
private currentUrl: string
private redirectingToHomepage = false
+ private defaultTrendingAlgorithm = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
+ private defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
constructor (
private router: Router,
// The config is first loaded from the cache so try to get the default route
const tmpConfig = this.serverService.getTmpConfig()
if (tmpConfig?.instance?.defaultClientRoute) {
- RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
+ this.defaultRoute = tmpConfig.instance.defaultClientRoute
}
if (tmpConfig?.trending?.videos?.algorithms?.default) {
- RedirectService.DEFAULT_TRENDING_ALGORITHM = tmpConfig.trending.videos.algorithms.default
+ this.defaultTrendingAlgorithm = tmpConfig.trending.videos.algorithms.default
}
// Load default route
const defaultRouteConfig = config.instance.defaultClientRoute
const defaultTrendingConfig = config.trending.videos.algorithms.default
- if (defaultRouteConfig) {
- RedirectService.DEFAULT_ROUTE = defaultRouteConfig
- }
-
- if (defaultTrendingConfig) {
- RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig
- }
+ if (defaultRouteConfig) this.defaultRoute = defaultRouteConfig
+ if (defaultTrendingConfig) this.defaultTrendingAlgorithm = defaultTrendingConfig
})
// Track previous url
})
}
+ getDefaultRoute () {
+ return this.defaultRoute
+ }
+
+ getDefaultTrendingAlgorithm () {
+ return this.defaultTrendingAlgorithm
+ }
+
redirectToPreviousRoute () {
const exceptions = [
'/verify-account',
this.redirectingToHomepage = true
- console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
+ console.log('Redirecting to %s...', this.defaultRoute)
- this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
+ this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
.then(() => this.redirectingToHomepage = false)
.catch(() => {
this.redirectingToHomepage = false
console.error(
'Cannot navigate to %s, resetting default route to %s.',
- RedirectService.DEFAULT_ROUTE,
+ this.defaultRoute,
RedirectService.INIT_DEFAULT_ROUTE
)
- RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
- return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
+ this.defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
+ return this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
})
}
import { HttpClient } from '@angular/common/http'
import { Inject, Injectable, LOCALE_ID } from '@angular/core'
import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers'
-import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
import { environment } from '../../../environments/environment'
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
- private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
-
configReloaded = new Subject<ServerConfig>()
private localeObservable: Observable<any>
if (!this.configObservable) {
this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
.pipe(
- tap(config => this.saveConfigLocally(config)),
tap(config => {
this.config = config
this.configLoaded = true
)
}
- private saveConfigLocally (config: ServerConfig) {
- peertubeLocalStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config))
- }
-
private loadConfigLocally () {
- const configString = peertubeLocalStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY)
-
- if (configString) {
- try {
- const parsed = JSON.parse(configString)
- Object.assign(this.config, parsed)
- } catch (err) {
- console.error('Cannot parse config saved in local storage.', err)
- }
+ const configString = window['PeerTubeServerConfig']
+ if (!configString) return
+
+ try {
+ const parsed = JSON.parse(configString)
+ Object.assign(this.config, parsed)
+ } catch (err) {
+ console.error('Cannot parse config saved in from index.html.', err)
}
}
}
<!-- description tag -->
<!-- custom css tag -->
<!-- meta tags -->
+ <!-- server config -->
<!-- /!\ Do not remove it /!\ -->
</head>
<!DOCTYPE html>
<html>
<head>
- <!-- title tag -->
-
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<meta property="og:platform" content="PeerTube" />
+
+ <!-- /!\ The following comment is used by the server to prerender some tags /!\ -->
+
+ <!-- title tag -->
+ <!-- description tag -->
<!-- custom css tag -->
+ <!-- meta tags -->
+ <!-- server config -->
+
+ <!-- /!\ Do not remove it /!\ -->
+
<link rel="icon" type="image/png" href="/client/assets/images/favicon.png" />
</head>
import './embed.scss'
import videojs from 'video.js'
import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import {
+ ClientHookName,
+ HTMLServerConfig,
+ PluginType,
ResultList,
- ServerConfig,
UserRefreshToken,
VideoCaption,
VideoDetails,
VideoPlaylist,
VideoPlaylistElement,
- VideoStreamingPlaylistType,
- PluginType,
- ClientHookName
+ VideoStreamingPlaylistType
} from '../../../../shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
import { TranslationsManager } from '../../assets/player/translations-manager'
+import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
import { Hooks, loadPlugin, runHook } from '../../root-helpers/plugins'
import { Tokens } from '../../root-helpers/users'
-import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
import { objectToUrlEncoded } from '../../root-helpers/utils'
-import { PeerTubeEmbedApi } from './embed-api'
import { RegisterClientHelpers } from '../../types/register-client-option.model'
+import { PeerTubeEmbedApi } from './embed-api'
type Translations = { [ id: string ]: string }
CLIENT_SECRET: 'client_secret'
}
+ config: HTMLServerConfig
+
private translationsPromise: Promise<{ [id: string]: string }>
- private configPromise: Promise<ServerConfig>
private PeertubePlayerManagerModulePromise: Promise<any>
private playlist: VideoPlaylist
constructor (private videoWrapperId: string) {
this.wrapperElement = document.getElementById(this.videoWrapperId)
+
+ try {
+ this.config = JSON.parse(window['PeerTubeServerConfig'])
+ } catch (err) {
+ console.error('Cannot parse HTML config.', err)
+ }
}
getVideoUrl (id: string) {
return this.refreshFetch(url.toString(), { headers: this.headers })
}
- loadConfig (): Promise<ServerConfig> {
- return this.refreshFetch('/api/v1/config')
- .then(res => res.json())
- }
-
removeElement (element: HTMLElement) {
element.parentElement.removeChild(element)
}
this.playerElement.setAttribute('playsinline', 'true')
this.wrapperElement.appendChild(this.playerElement)
+ // Issue when we parsed config from HTML, fallback to API
+ if (!this.config) {
+ this.config = await this.refreshFetch('/api/v1/config')
+ .then(res => res.json())
+ }
+
const videoInfoPromise = videoResponse.json()
.then((videoInfo: VideoDetails) => {
if (!alreadyHadPlayer) this.loadPlaceholder(videoInfo)
return videoInfo
})
- const [ videoInfoTmp, serverTranslations, captionsResponse, config, PeertubePlayerManagerModule ] = await Promise.all([
+ const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
videoInfoPromise,
this.translationsPromise,
captionsPromise,
- this.configPromise,
this.PeertubePlayerManagerModulePromise
])
- await this.ensurePluginsAreLoaded(config, serverTranslations)
+ await this.ensurePluginsAreLoaded(serverTranslations)
const videoInfo: VideoDetails = videoInfoTmp
this.buildCSS()
- await this.buildDock(videoInfo, config)
+ await this.buildDock(videoInfo)
this.initializeApi()
private async initCore () {
if (this.userTokens) this.setHeadersFromTokens()
- this.configPromise = this.loadConfig()
this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
}
}
- private async buildDock (videoInfo: VideoDetails, config: ServerConfig) {
+ private async buildDock (videoInfo: VideoDetails) {
if (!this.controls) return
// On webtorrent fallback, player may have been disposed
const title = this.title ? videoInfo.name : undefined
- const description = config.tracker.enabled && this.warningTitle
+ const description = this.config.tracker.enabled && this.warningTitle
? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
: undefined
return window.location.pathname.split('/')[1] === 'video-playlists'
}
- private async ensurePluginsAreLoaded (config: ServerConfig, translations?: { [ id: string ]: string }) {
- if (config.plugin.registered.length === 0) return
+ private async ensurePluginsAreLoaded (translations?: { [ id: string ]: string }) {
+ if (this.config.plugin.registered.length === 0) return
- for (const plugin of config.plugin.registered) {
+ for (const plugin of this.config.plugin.registered) {
for (const key of Object.keys(plugin.clientScripts)) {
const clientScript = plugin.clientScripts[key]
TITLE: '<!-- title tag -->',
DESCRIPTION: '<!-- description tag -->',
CUSTOM_CSS: '<!-- custom css tag -->',
- META_TAGS: '<!-- meta tags -->'
+ META_TAGS: '<!-- meta tags -->',
+ SERVER_CONFIG: '<!-- server config -->'
}
// ---------------------------------------------------------------------------
import { readFile } from 'fs-extra'
import { join } from 'path'
import validator from 'validator'
+import { escapeHTML } from '@shared/core-utils/renderer'
+import { HTMLServerConfig } from '@shared/models'
import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
import { isTestInstance, sha256 } from '../helpers/core-utils'
-import { escapeHTML } from '@shared/core-utils/renderer'
import { logger } from '../helpers/logger'
+import { mdToPlainText } from '../helpers/markdown'
import { CONFIG } from '../initializers/config'
import {
ACCEPT_HEADERS,
import { getActivityStreamDuration } from '../models/video/video-format-utils'
import { VideoPlaylistModel } from '../models/video/video-playlist'
import { MAccountActor, MChannelActor } from '../types/models'
-import { mdToPlainText } from '../helpers/markdown'
+import { getHTMLServerConfig } from './config'
type Tags = {
ogType: string
if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
const buffer = await readFile(path)
+ const serverConfig = await getHTMLServerConfig()
let html = buffer.toString()
html = await ClientHtml.addAsyncPluginCSS(html)
html = ClientHtml.addCustomCSS(html)
html = ClientHtml.addTitleTag(html)
+ html = ClientHtml.addDescriptionTag(html)
+ html = ClientHtml.addServerConfig(html, serverConfig)
ClientHtml.htmlCache[path] = html
if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
const buffer = await readFile(path)
+ const serverConfig = await getHTMLServerConfig()
let html = buffer.toString()
html = ClientHtml.addFaviconContentHash(html)
html = ClientHtml.addLogoContentHash(html)
html = ClientHtml.addCustomCSS(html)
+ html = ClientHtml.addServerConfig(html, serverConfig)
html = await ClientHtml.addAsyncPluginCSS(html)
ClientHtml.htmlCache[path] = html
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag)
}
+ private static addServerConfig (htmlStringPage: string, serverConfig: HTMLServerConfig) {
+ const serverConfigString = JSON.stringify(serverConfig)
+ const configScriptTag = `<script type="application/javascript">window.PeerTubeServerConfig = '${serverConfigString}'</script>`
+
+ return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.SERVER_CONFIG, configScriptTag)
+ }
+
private static async addAsyncPluginCSS (htmlStringPage: string) {
const globalCSSContent = await readFile(PLUGIN_GLOBAL_CSS_PATH)
if (globalCSSContent.byteLength === 0) return htmlStringPage
import { getServerCommit } from '@server/helpers/utils'
import { CONFIG, isEmailEnabled } from '@server/initializers/config'
import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants'
-import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
+import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
import { Hooks } from './plugins/hooks'
import { PluginManager } from './plugins/plugin-manager'
import { getThemeOrDefault } from './plugins/theme-utils'
import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
-let serverCommit: string
-
async function getServerConfig (ip?: string): Promise<ServerConfig> {
- if (serverCommit === undefined) serverCommit = await getServerCommit()
-
const { allowed } = await Hooks.wrapPromiseFun(
isSignupAllowed,
{
)
const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
+
+ const signup = {
+ allowed,
+ allowedForCurrentIP,
+ requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
+ }
+
+ const htmlConfig = await getHTMLServerConfig()
+
+ return { ...htmlConfig, signup }
+}
+
+// Config injected in HTML
+let serverCommit: string
+async function getHTMLServerConfig (): Promise<HTMLServerConfig> {
+ if (serverCommit === undefined) serverCommit = await getServerCommit()
+
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
return {
},
serverVersion: PEERTUBE_VERSION,
serverCommit,
- signup: {
- allowed,
- allowedForCurrentIP,
- requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
- },
transcoding: {
hls: {
enabled: CONFIG.TRANSCODING.HLS.ENABLED
getServerConfig,
getRegisteredThemes,
getEnabledResolutions,
- getRegisteredPlugins
+ getRegisteredPlugins,
+ getHTMLServerConfig
}
// ---------------------------------------------------------------------------
UpdatedAt
} from 'sequelize-typescript'
import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
-import { AttributesOnly } from '@shared/core-utils'
-import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
+import { abusePredefinedReasonsMap, AttributesOnly } from '@shared/core-utils'
import {
AbuseFilter,
AbuseObject,
import 'mocha'
import * as chai from 'chai'
import * as request from 'supertest'
-import { Account, VideoPlaylistPrivacy } from '@shared/models'
+import { Account, HTMLServerConfig, ServerConfig, VideoPlaylistPrivacy } from '@shared/models'
import {
addVideoInPlaylist,
cleanupTests,
doubleFollow,
flushAndRunMultipleServers,
getAccount,
+ getConfig,
getCustomConfig,
getVideosList,
makeHTMLRequest,
waitJobs
} from '../../shared/extra-utils'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { omit } from 'lodash'
const expect = chai.expect
-function checkIndexTags (html: string, title: string, description: string, css: string) {
+function checkIndexTags (html: string, title: string, description: string, css: string, config: ServerConfig) {
expect(html).to.contain('<title>' + title + '</title>')
expect(html).to.contain('<meta name="description" content="' + description + '" />')
expect(html).to.contain('<style class="custom-css-style">' + css + '</style>')
+
+ const htmlConfig: HTMLServerConfig = omit(config, 'signup')
+ expect(html).to.contain(`<script type="application/javascript">window.PeerTubeServerConfig = '${JSON.stringify(htmlConfig)}'</script>`)
}
describe('Test a client controllers', function () {
describe('Index HTML', function () {
it('Should have valid index html tags (title, description...)', async function () {
+ const resConfig = await getConfig(servers[0].url)
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
const description = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
- checkIndexTags(res.text, 'PeerTube', description, '')
+ checkIndexTags(res.text, 'PeerTube', description, '', resConfig.body)
})
it('Should update the customized configuration and have the correct index html tags', async function () {
}
})
+ const resConfig = await getConfig(servers[0].url)
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
- checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }')
+ checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
})
it('Should have valid index html updated tags (title, description...)', async function () {
+ const resConfig = await getConfig(servers[0].url)
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
- checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }')
+ checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
})
it('Should use the original video URL for the canonical tag', async function () {
})
})
+ describe('Embed HTML', function () {
+
+ it('Should have the correct embed html tags', async function () {
+ const resConfig = await getConfig(servers[0].url)
+ const res = await makeHTMLRequest(servers[0].url, servers[0].video.embedPath)
+
+ checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
+ })
+ })
+
after(async function () {
await cleanupTests(servers)
})
uuid: string
name?: string
url?: string
+
account?: {
name: string
}
+
+ embedPath?: string
}
remoteVideo?: {
dismissable: boolean
}
}
+
+export type HTMLServerConfig = Omit<ServerConfig, 'signup'>