// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import log from 'electron-log'; import Joi from '@hapi/joi'; import urlUtils from 'common/utils/url'; const defaultOptions = { stripUnknown: true, }; const defaultWindowWidth = 1000; const defaultWindowHeight = 700; const minWindowWidth = 400; const minWindowHeight = 240; const argsSchema = Joi.object({ hidden: Joi.boolean(), disableDevMode: Joi.boolean(), dataDir: Joi.string(), version: Joi.boolean(), }); const boundsInfoSchema = Joi.object({ x: Joi.number().integer().default(0), y: Joi.number().integer().default(0), width: Joi.number().integer().min(minWindowWidth).required().default(defaultWindowWidth), height: Joi.number().integer().min(minWindowHeight).required().default(defaultWindowHeight), maximized: Joi.boolean().default(false), fullscreen: Joi.boolean().default(false), }); const appStateSchema = Joi.object({ lastAppVersion: Joi.string(), skippedVersion: Joi.string(), updateCheckedDate: Joi.string(), }); const configDataSchemaV0 = Joi.object({ url: Joi.string().required(), }); const configDataSchemaV1 = Joi.object({ version: Joi.number().min(1).default(1), teams: Joi.array().items(Joi.object({ name: Joi.string().required(), url: Joi.string().required(), })).default([]), showTrayIcon: Joi.boolean().default(false), trayIconTheme: Joi.any().allow('').valid('light', 'dark').default('light'), minimizeToTray: Joi.boolean().default(false), notifications: Joi.object({ flashWindow: Joi.any().valid(0, 2).default(0), bounceIcon: Joi.boolean().default(false), bounceIconType: Joi.any().allow('').valid('informational', 'critical').default('informational'), }), showUnreadBadge: Joi.boolean().default(true), useSpellChecker: Joi.boolean().default(true), enableHardwareAcceleration: Joi.boolean().default(true), autostart: Joi.boolean().default(true), spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'), }); const configDataSchemaV2 = Joi.object({ version: Joi.number().min(2).default(2), teams: Joi.array().items(Joi.object({ name: Joi.string().required(), url: Joi.string().required(), order: Joi.number().integer().min(0), })).default([]), showTrayIcon: Joi.boolean().default(false), trayIconTheme: Joi.any().allow('').valid('light', 'dark').default('light'), minimizeToTray: Joi.boolean().default(false), notifications: Joi.object({ flashWindow: Joi.any().valid(0, 2).default(0), bounceIcon: Joi.boolean().default(false), bounceIconType: Joi.any().allow('').valid('informational', 'critical').default('informational'), }), showUnreadBadge: Joi.boolean().default(true), useSpellChecker: Joi.boolean().default(true), enableHardwareAcceleration: Joi.boolean().default(true), autostart: Joi.boolean().default(true), spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'), darkMode: Joi.boolean().default(false), downloadLocation: Joi.string(), }); // eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'}; const certificateStoreSchema = Joi.object().pattern( Joi.string().uri(), Joi.object({ data: Joi.string(), issuerName: Joi.string(), }), ); const originPermissionsSchema = Joi.object().keys({ canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want }); const trustedOriginsSchema = Joi.object({}).pattern( Joi.string().uri(), Joi.object().keys({ canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want }), ); const allowedProtocolsSchema = Joi.array().items(Joi.string().regex(/^[a-z-]+:$/i)); // validate bounds_info.json export function validateArgs(data) { return validateAgainstSchema(data, argsSchema); } // validate bounds_info.json export function validateBoundsInfo(data) { return validateAgainstSchema(data, boundsInfoSchema); } // validate app_state.json export function validateAppState(data) { return validateAgainstSchema(data, appStateSchema); } // validate v.0 config.json export function validateV0ConfigData(data) { return validateAgainstSchema(data, configDataSchemaV0); } // validate v.1 config.json export function validateV1ConfigData(data) { if (Array.isArray(data.teams) && data.teams.length) { // first replace possible backslashes with forward slashes let teams = data.teams.map(({name, url}) => { let updatedURL = url; if (updatedURL.includes('\\')) { updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/'); } return {name, url: updatedURL}; }); // next filter out urls that are still invalid so all is not lost teams = teams.filter(({url}) => urlUtils.isValidURL(url)); // replace original teams data.teams = teams; } return validateAgainstSchema(data, configDataSchemaV1); } export function validateV2ConfigData(data) { if (Array.isArray(data.teams) && data.teams.length) { // first replace possible backslashes with forward slashes let teams = data.teams.map(({name, url, order}) => { let updatedURL = url; if (updatedURL.includes('\\')) { updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/'); } return {name, url: updatedURL, order}; }); // next filter out urls that are still invalid so all is not lost teams = teams.filter(({url}) => urlUtils.isValidURL(url)); // replace original teams data.teams = teams; } return validateAgainstSchema(data, configDataSchemaV2); } // validate certificate.json export function validateCertificateStore(data) { const jsonData = (typeof data === 'object' ? data : JSON.parse(data)); return validateAgainstSchema(jsonData, certificateStoreSchema); } // validate allowedProtocols.json export function validateAllowedProtocols(data) { return validateAgainstSchema(data, allowedProtocolsSchema); } export function validateTrustedOriginsStore(data) { const jsonData = (typeof data === 'object' ? data : JSON.parse(data)); return validateAgainstSchema(jsonData, trustedOriginsSchema); } export function validateOriginPermissions(data) { const jsonData = (typeof data === 'object' ? data : JSON.parse(data)); return validateAgainstSchema(jsonData, originPermissionsSchema); } function validateAgainstSchema(data, schema) { if (typeof data !== 'object') { log.error(`Input 'data' is not an object we can validate: ${typeof data}`); return false; } if (!schema) { log.error('No schema provided to validate'); return false; } const {error, value} = schema.validate(data, defaultOptions); if (error) { log.error(`Validation failed due to: ${error}`); return false; } return value; }