import React from 'react' ; import { renderToStaticMarkup } from 'react-dom/server' ; import { first , take } from 'rxjs/operators' ; import { i18n } from '@osd/i18n' ; import Axios from 'axios' ; import AxiosHttpAdapter from 'axios/lib/adapters/http' ; import { UiPlugins } from '../plugins' ; import { CoreContext } from '../core_context' ; import { Template } from './views' ; import { IRenderOptions , RenderingSetupDeps , InternalRenderingServiceSetup , RenderingMetadata , BrandingValidation , BrandingAssignment , } from './types' ; import { OpenSearchDashboardsConfigType } from '../opensearch_dashboards_config' ; const DEFAULT_TITLE = 'OpenSearch Dashboards' ; internal export class RenderingService { constructor ( private readonly coreContext ) { } private logger = this . coreContext . logger ; public async setup ( { http , status , uiPlugins , } ) { const opensearchDashboardsConfig = await this . coreContext . configService . atPath < OpenSearchDashboardsConfigType > ( 'opensearchDashboards' ) . pipe ( first ( ) ) . toPromise ( ) ; return { render : async ( request , uiSettings , { includeUserSettings = true , vars } = { } ) => { const env = { mode : this . coreContext . env . mode , packageInfo : this . coreContext . env . packageInfo , } ; const basePath = http . basePath . get ( request ) ; const uiPublicUrl = `${ basePath }/ui` ; const serverBasePath = http . basePath . serverBasePath ; const settings = { defaults : uiSettings . getRegistered ( ) , user : includeUserSettings ? await uiSettings . getUserProvided ( ) : { } , } ; const darkMode = settings . user ?. [ 'theme:darkMode' ] ?. userValue ? Boolean ( settings . user [ 'theme:darkMode' ] . userValue ) : false ; const brandingAssignment = await this . assignBrandingConfig ( darkMode , opensearchDashboardsConfig ) ; const metadata = { strictCsp : http . csp . strict , uiPublicUrl , bootstrapScriptUrl : `${ basePath }/bootstrap.js` , i18n : i18n . translate , locale : i18n . getLocale ( ) , darkMode , injectedMetadata : { version : env . packageInfo . version , buildNumber : env . packageInfo . buildNum , branch : env . packageInfo . branch , basePath , serverBasePath , env , anonymousStatusPage : status . isStatusPageAnonymous ( ) , i18n : { translationsUrl : `${ basePath }/translations/${ i18n . getLocale ( ) }.json` , } , csp : { warnLegacyBrowsers : http . csp . warnLegacyBrowsers } , vars : vars ?? { } , uiPlugins : await Promise . all ( [ ... uiPlugins . public ] . map ( async ( [ id , plugin ] ) => ( { id , plugin , config : await this . getUiConfig ( uiPlugins , id ) , } ) ) ) , legacyMetadata : { uiSettings : settings , } , branding : { darkMode , assetFolderUrl : `${ uiPublicUrl }/default_branding` , logo : { defaultUrl : brandingAssignment . logoDefault , darkModeUrl : brandingAssignment . logoDarkmode , } , mark : { defaultUrl : brandingAssignment . markDefault , darkModeUrl : brandingAssignment . markDarkmode , } , loadingLogo : { defaultUrl : brandingAssignment . loadingLogoDefault , darkModeUrl : brandingAssignment . loadingLogoDarkmode , } , faviconUrl : brandingAssignment . favicon , applicationTitle : brandingAssignment . applicationTitle , } , } , } ; return `<!DOCTYPE html>${ renderToStaticMarkup ( < Template metadata = { metadata } / > ) }` ; } , } ; } public async stop ( ) { } param { boolean } darkMode param { Readonly < OpenSearchDashboardsConfigType > } opensearchDashboardsConfig returns { BrandingAssignment } private assignBrandingConfig = async ( darkMode , opensearchDashboardsConfig ) : Promise < BrandingAssignment > => { const brandingValidation = await this . checkBrandingValid ( darkMode , opensearchDashboardsConfig ) ; const branding = opensearchDashboardsConfig . branding ; const logoDefault = brandingValidation . isLogoDefaultValid ? branding . logo . defaultUrl : undefined ; const markDefault = brandingValidation . isMarkDefaultValid ? branding . mark . defaultUrl : undefined ; const loadingLogoDefault = brandingValidation . isLoadingLogoDefaultValid ? branding . loadingLogo . defaultUrl : undefined ; let logoDarkmode = brandingValidation . isLogoDarkmodeValid ? branding . logo . darkModeUrl : undefined ; let markDarkmode = brandingValidation . isMarkDarkmodeValid ? branding . mark . darkModeUrl : undefined ; let loadingLogoDarkmode = brandingValidation . isLoadingLogoDarkmodeValid ? branding . loadingLogo . darkModeUrl : undefined ; /**
         * For dark mode URLs, we added another validation:
         * user can only provide a dark mode URL after providing a valid default mode URL,
         * If user provides a valid dark mode URL but fails to provide a valid default mode URL,
         * return undefined for the dark mode URL
         */ if ( logoDarkmode && ! logoDefault ) { this . logger . get ( 'branding' ) . error ( 'Must provide a valid logo default mode URL before providing a logo dark mode URL' ) ; logoDarkmode = undefined ; } if ( markDarkmode && ! markDefault ) { this . logger . get ( 'branding' ) . error ( 'Must provide a valid mark default mode URL before providing a mark dark mode URL' ) ; markDarkmode = undefined ; } if ( loadingLogoDarkmode && ! loadingLogoDefault ) { this . logger . get ( 'branding' ) . error ( 'Must provide a valid loading logo default mode URL before providing a loading logo dark mode URL' ) ; loadingLogoDarkmode = undefined ; } const favicon = brandingValidation . isFaviconValid ? branding . faviconUrl : undefined ; const applicationTitle = brandingValidation . isTitleValid ? branding . applicationTitle : DEFAULT_TITLE ; const brandingAssignment = { logoDefault , logoDarkmode , markDefault , markDarkmode , loadingLogoDefault , loadingLogoDarkmode , favicon , applicationTitle , } ; return brandingAssignment ; } ; param { boolean } darkMode param { Readonly < OpenSearchDashboardsConfigType > } opensearchDashboardsConfig returns { BrandingValidation } private checkBrandingValid = async ( darkMode , opensearchDashboardsConfig ) : Promise < BrandingValidation > => { const branding = opensearchDashboardsConfig . branding ; const isLogoDefaultValid = await this . isUrlValid ( branding . logo . defaultUrl , 'logo default' ) ; const isLogoDarkmodeValid = darkMode ? await this . isUrlValid ( branding . logo . darkModeUrl , 'logo darkMode' ) : false ; const isMarkDefaultValid = await this . isUrlValid ( branding . mark . defaultUrl , 'mark default' ) ; const isMarkDarkmodeValid = darkMode ? await this . isUrlValid ( branding . mark . darkModeUrl , 'mark darkMode' ) : false ; const isLoadingLogoDefaultValid = await this . isUrlValid ( branding . loadingLogo . defaultUrl , 'loadingLogo default' ) ; const isLoadingLogoDarkmodeValid = darkMode ? await this . isUrlValid ( branding . loadingLogo . darkModeUrl , 'loadingLogo darkMode' ) : false ; const isFaviconValid = await this . isUrlValid ( branding . faviconUrl , 'favicon' ) ; const isTitleValid = this . isTitleValid ( branding . applicationTitle , 'applicationTitle' ) ; const brandingValidation = { isLogoDefaultValid , isLogoDarkmodeValid , isMarkDefaultValid , isMarkDarkmodeValid , isLoadingLogoDefaultValid , isLoadingLogoDarkmodeValid , isFaviconValid , isTitleValid , } ; return brandingValidation ; } ; private async getUiConfig ( uiPlugins , pluginId ) { const browserConfig = uiPlugins . browserConfigs . get ( pluginId ) ; return ( ( await browserConfig ?. pipe ( take ( 1 ) ) . toPromise ( ) ) ?? { } ) as Record < string , any > ; } param { string } url param { string } configName returns { boolean } public isUrlValid = async ( url , configName ? ) : Promise < boolean > => { if ( url === '/' ) { return false ; } if ( url . match ( /\.(png|svg|gif|PNG|SVG|GIF)$/ ) === null ) { this . logger . get ( 'branding' ) . error ( `${ configName } config is invalid. Using default branding.` ) ; return false ; } return await Axios . get ( url , { adapter : AxiosHttpAdapter , maxRedirects : 0 } ) . then ( ( ) => { return true ; } ) . catch ( ( ) => { this . logger . get ( 'branding' ) . error ( `${ configName } URL was not found or invalid. Using default branding.` ) ; return false ; } ) ; } ; param { string } title param { string } configName returns { boolean } public isTitleValid = ( title , configName ? ) : boolean => { if ( ! title ) { return false ; } if ( title . length > 36 ) { this . logger . get ( 'branding' ) . error ( `${ configName } config is not found or invalid. Title length should be between 1 to 36 characters. Using default title.` ) ; return false ; } return true ; } ; }