Files
mattermostest/src/main/Validator.ts
Tasos Boulis 131b5fa2ac [MM-22239] Downloads dropdown (#2227)
* WIP: show/hide temp downloads dropdown

* WIP: Position downloads dropdown correctly under the button

* WIP: Use correct width for dropdown so that right radius and shadows are displayed

* WIP: Add items to download list after finished downloading

* WIP: Add download item base components

* Add "clear all" functionality

* Use type Record<> for downloads saved in config

* Add styling to files in the downloads dropdown

* Open file in folder when clicking it from downloads dropdown. Center svg in parent element

* Update scrollbar styling

* Update scrollbar styling

* Update state of downloaded items if deleted from folder

* Add progress bar in downloads

* Use "x-uncompressed-content-length" in file downloads.

* Keep downloads open when clicking outside their browserview

* Use correct color for downloads dropdown button

* Add better styling to downloads dropdown button

* Allow only 50 download files maximum. Oldest file is being removed if reached

* Autoclose downloads dropdown after 4s of download finish

* Add file thumbnails

* Dont show second dialog if first dismissed

* Add red badge when downloads running and dropdown closed

* Add menu item for Downloads

* Add support for more code file extensions

* Open downloads dropdown instead of folder from the menu

* Run lint:js and fix problems

* Add tests for utils

* Fix issue with dropdown not displaying

* Remove unecessary comment

* Move downloads to separate json file, outside Config

* Add downloads dropdown menu for the 3-dot button

* Dont show dev tools for downloads

* Add cancel download functionality

* Add dark mode styling

* Use View state for downloadsMenu open state

* Fix some style issues

* Add image preview for downloaded images

* Remove extra devTool in weback config

* Fix issue with paths on windows

* Align items left in downloads menu

* Use pretty-bytes for file sizes

* Show download remaining time

* Close downloads dropdown when clicking outside

* Show different units in received bytes when they are different from the total units (kb/mb)

* Dont hide downloads when mattermost view is clicked

* Keep downloads open if download button is clicked

* Use closest() to check for download clicks

* Fix unit tests.
Add tests for new Views and downloadManager
Add @types/jest as devDependency for intellisense

* Remove unecessary tsconfig for jest

* Fix types error

* Add all critical tests for downloadsManager

* WIP: add e2e tests for downloads

* WIP: add e2e tests for downloads

* Rename downloads spec file

* WIP: make vscode debugger work for e2e tests

* Remove unused mock

* Remove defaults for v4 config

* Use electron-mocha for e2e debugger

* Fix e2e tests spawning JsonFileManager twice

* Add async fs functions and add tests for download item UI

* Add async fs functions and add tests for download item UI

* Improve tests with "waitForSelector" to wait for visible elements

* Wait for page load before assertions

* Add tests for file uploads/downloads

* Dont show native notification for completed downloads if dropdown is open

* Increment filenames if file already exists

* Fix antializing in downloads dropdown

* Fix styling of downloads header

* Increase dimensions of green/red icons in downloads

* Fix styling of 3-dot button

* Fix unit tests

* Show 3-dot button only on hover or click

* PR review fixes

* Revert vscode debug fixes

* Mock fs.constants

* Mock fs instead of JsonFileManager in downlaods tests

* Mock fs instead of JsonFileManager in downlaods tests

* Add necessary mocks for downloads manager

* Mark file as deleted if user deleted it

* Fix min-height of downloads dropdown and 3-dot icon position

* Add more tests

* Make size of downloads dropdown dynamic based on content

* Combine log statements

* Close 3-dot menu if user clicks elsewhere

* Move application updates inside downloads dropdown

* Fix update issues

* Fix ipc event payload

* Add missing prop

* Remove unused translations

* Fix failing test

* Fix version unknown

* Remove commented out component
2022-10-07 11:40:27 +03:00

299 lines
11 KiB
TypeScript

// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import log from 'electron-log';
import Joi from 'joi';
import {Args} from 'types/args';
import {ConfigV0, ConfigV1, ConfigV2, ConfigV3, TeamWithTabs} from 'types/config';
import {DownloadedItems} from 'types/downloads';
import {SavedWindowState} from 'types/mainWindow';
import {AppState} from 'types/appState';
import {ComparableCertificate} from 'types/certificate';
import {PermissionType, TrustedOrigin} from 'types/trustedOrigin';
import {TAB_MESSAGING} from 'common/tabs/TabView';
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<Args>({
hidden: Joi.boolean(),
disableDevMode: Joi.boolean(),
dataDir: Joi.string(),
version: Joi.boolean(),
fullscreen: Joi.boolean(),
});
const boundsInfoSchema = Joi.object<SavedWindowState>({
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<AppState>({
lastAppVersion: Joi.string(),
skippedVersion: Joi.string(),
updateCheckedDate: Joi.string(),
});
const downloadsSchema = Joi.object<DownloadedItems>().pattern(
Joi.string(),
{
type: Joi.string().valid('file', 'update'),
filename: Joi.string().allow(null),
state: Joi.string().valid('interrupted', 'progressing', 'completed', 'cancelled', 'deleted', 'available'),
progress: Joi.number().min(0).max(100),
location: Joi.string().allow(''),
mimeType: Joi.string().allow(null),
addedAt: Joi.number().min(0),
receivedBytes: Joi.number().min(0),
totalBytes: Joi.number().min(0),
});
const configDataSchemaV0 = Joi.object<ConfigV0>({
url: Joi.string().required(),
});
const configDataSchemaV1 = Joi.object<ConfigV1>({
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<ConfigV2>({
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().default('en-US'),
spellCheckerURL: Joi.string().allow(null),
darkMode: Joi.boolean().default(false),
downloadLocation: Joi.string(),
});
const configDataSchemaV3 = Joi.object<ConfigV3>({
version: Joi.number().min(3).default(3),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
order: Joi.number().integer().min(0),
lastActiveTab: Joi.number().integer().min(0).default(0),
tabs: Joi.array().items(Joi.object({
name: Joi.string().required(),
order: Joi.number().integer().min(0),
isOpen: Joi.boolean(),
})).default([]),
})).default([]),
showTrayIcon: Joi.boolean().default(false),
trayIconTheme: Joi.any().allow('').valid('light', 'dark', 'use_system').default('use_system'),
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),
startInFullscreen: Joi.boolean().default(false),
autostart: Joi.boolean().default(true),
hideOnStart: Joi.boolean().default(false),
spellCheckerLocales: Joi.array().items(Joi.string()).default([]),
spellCheckerURL: Joi.string().allow(null),
darkMode: Joi.boolean().default(false),
downloadLocation: Joi.string(),
lastActiveTeam: Joi.number().integer().min(0).default(0),
autoCheckForUpdates: Joi.boolean().default(true),
alwaysMinimize: Joi.boolean(),
alwaysClose: Joi.boolean(),
logLevel: Joi.string().default('info'),
appLanguage: Joi.string().allow(''),
});
// 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<ComparableCertificate>({
data: Joi.string(),
issuerName: Joi.string(),
}),
);
const originPermissionsSchema = Joi.object<TrustedOrigin>().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: Args) {
return validateAgainstSchema(data, argsSchema);
}
// validate bounds_info.json
export function validateBoundsInfo(data: SavedWindowState) {
return validateAgainstSchema(data, boundsInfoSchema);
}
// validate app_state.json
export function validateAppState(data: AppState) {
return validateAgainstSchema(data, appStateSchema);
}
// validate downloads.json
export function validateDownloads(data: DownloadedItems) {
return validateAgainstSchema(data, downloadsSchema);
}
// validate v.0 config.json
export function validateV0ConfigData(data: ConfigV0) {
return validateAgainstSchema(data, configDataSchemaV0);
}
function cleanURL(url: string): string {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return updatedURL;
}
function cleanTeam<T extends {name: string; url: string}>(team: T) {
return {
...team,
url: cleanURL(team.url),
};
}
function cleanTeamWithTabs(team: TeamWithTabs) {
return {
...cleanTeam(team),
tabs: team.tabs.map((tab) => {
return {
...tab,
isOpen: tab.name === TAB_MESSAGING ? true : tab.isOpen,
};
}),
};
}
function cleanTeams<T extends {name: string; url: string}>(teams: T[], func: (team: T) => T) {
let newTeams = teams;
if (Array.isArray(newTeams) && newTeams.length) {
// first replace possible backslashes with forward slashes
newTeams = newTeams.map((team) => func(team));
// next filter out urls that are still invalid so all is not lost
newTeams = newTeams.filter(({url}) => urlUtils.isValidURL(url));
}
return newTeams;
}
// validate v.1 config.json
export function validateV1ConfigData(data: ConfigV1) {
data.teams = cleanTeams(data.teams, cleanTeam);
return validateAgainstSchema(data, configDataSchemaV1);
}
export function validateV2ConfigData(data: ConfigV2) {
data.teams = cleanTeams(data.teams, cleanTeam);
if (data.spellCheckerURL && !urlUtils.isValidURL(data.spellCheckerURL)) {
log.error('Invalid download location for spellchecker dictionary, removing from config');
delete data.spellCheckerURL;
}
return validateAgainstSchema(data, configDataSchemaV2);
}
export function validateV3ConfigData(data: ConfigV3) {
data.teams = cleanTeams(data.teams, cleanTeamWithTabs);
if (data.spellCheckerURL && !urlUtils.isValidURL(data.spellCheckerURL)) {
log.error('Invalid download location for spellchecker dictionary, removing from config');
delete data.spellCheckerURL;
}
return validateAgainstSchema(data, configDataSchemaV3);
}
// validate certificate.json
export function validateCertificateStore(data: string | Record<string, ComparableCertificate>) {
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, certificateStoreSchema);
}
// validate allowedProtocols.json
export function validateAllowedProtocols(data: string[]) {
return validateAgainstSchema(data, allowedProtocolsSchema);
}
export function validateTrustedOriginsStore(data: string | Record<PermissionType, TrustedOrigin>) {
const jsonData: Record<PermissionType, TrustedOrigin> = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, trustedOriginsSchema);
}
export function validateOriginPermissions(data: string | TrustedOrigin) {
const jsonData: TrustedOrigin = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, originPermissionsSchema);
}
function validateAgainstSchema<T>(data: T, schema: Joi.ObjectSchema<T> | Joi.ArraySchema): T | null {
if (typeof data !== 'object') {
log.error(`Input 'data' is not an object we can validate: ${typeof data}`);
return null;
}
if (!schema) {
log.error('No schema provided to validate');
return null;
}
const {error, value} = schema.validate(data, defaultOptions);
if (error) {
log.error(`Validation failed due to: ${error}`);
return null;
}
return value;
}