/* * * @copyright Copyright (c) OX Software GmbH, Germany <info@open-xchange.com> * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>. * * Any use of the work other than as authorized under this license or copyright law is prohibited. * */ import { get } from '../cache.js' import Ajv from 'ajv' import fs from 'node:fs' import { getRedisKey } from '../util.js' const appRoot = process.env.APP_ROOT const ajv = new Ajv({ allErrors: true }) ajv.addSchema( fs.readdirSync('src/schemas/', 'utf8').map(file => { return JSON.parse(fs.readFileSync(`src/schemas/${file}`, 'utf8')) }) ) const template = { // custom values name: 'OX App Suite', short_name: 'OX App Suite', icons: [ { src: `${appRoot}themes/default/logo_512.png`, type: 'image/png', sizes: '512x512', purpose: 'any' } ], // fixed values // theme_color is taken from index.html and is changed by the theme theme_color: 'white', start_url: `${appRoot}#pwa=true`, display: 'standalone', background_color: 'white', scope: `${appRoot}`, id: `${appRoot}#pwa=true`, protocol_handlers: [ { protocol: 'mailto', url: `${appRoot}#app=io.ox/mail&mailto=%s` } ] } function getTypeFromPath (path) { const ext = path.split('.').pop() switch (ext) { case 'png': return 'image/png' case 'svg': return 'image/svg+xml' default: throw new Error('Unsupported file type or no file type.') } } function buildManifestFromData (userData) { const manifestData = {} if (userData.icon) { manifestData.icons = [{ src: userData.icon, type: getTypeFromPath(userData.icon), sizes: `${userData.iconWidthHeight}x${userData.iconWidthHeight}`, purpose: 'any' }] } if (userData.shortName) { manifestData.short_name = userData.shortName if (!userData.name) manifestData.name = userData.shortName } if (userData.name) manifestData.name = userData.name if (userData.backgroundColor) { manifestData.background_color = userData.backgroundColor manifestData.theme_color = userData.backgroundColor } return manifestData } async function fetchWebManifest (url) { const serverConfigURL = new URL('api/apps/manifests?action=config', url) const conf = await fetch(serverConfigURL) if (conf.ok) { const data = (await conf.json()).data if (String(data.pwa?.enabled) !== 'true') return {} const combinedManifest = data.pwa.raw_manifest || { ...template, ...buildManifestFromData(data.pwa) } const valid = ajv.validate('https://json.schemastore.org/web-manifest-combined.json', combinedManifest) if (!valid) { throw new Error(JSON.stringify(ajv.errors[0], null, 2)) } const webmanifest = JSON.stringify(combinedManifest, null, 2) return webmanifest } else { throw new Error(`Failed to fetch ${serverConfigURL}`) } } export default async function serveWebmanifest (fastify) { fastify.get('/pwa.json', async (req, res) => { const urlData = req.urlData() const url = `https://${urlData.host}${urlData.path}` try { const cached = await get(getRedisKey({ name: `cachedManifest:${url}` }), async () => [await fetchWebManifest(url), 86400]) res.type('application/manifest+json') res.send(cached) } catch (err) { res.statusCode = 500 res.send(`Failed to load config for url ${url}: ${err}`) } }) }