diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index f907d637462e69..06975aa85f1e4c 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -5,10 +5,12 @@ * 2.0. */ -import { get } from 'lodash'; import { PluginConfigDescriptor } from 'kibana/server'; +import { get } from 'lodash'; + import { ConfigSchema, ReportingConfigType } from './schema'; export { buildConfig } from './config'; +export { registerUiSettings } from './ui_settings'; export { ConfigSchema, ReportingConfigType }; export const config: PluginConfigDescriptor = { diff --git a/x-pack/plugins/reporting/server/config/ui_settings.test.ts b/x-pack/plugins/reporting/server/config/ui_settings.test.ts new file mode 100644 index 00000000000000..dcd12e4c05f3fa --- /dev/null +++ b/x-pack/plugins/reporting/server/config/ui_settings.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { range } from 'lodash'; +import { PdfLogoSchema } from './ui_settings'; + +test('validates when provided with image data', () => { + const jpgString = + `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHCBcUFBUUExUYGRUaGRsZGxsZHB8bIh0iGhgbGxkbGx8dIy0kGx0rIiIbJTcoKi8xNDU0ISY6Pzo2` + + `+8snFz9eWgvYKS4ZsvS05zRQsDveIzH4Er4iDtr6iICIiAiIgIiICIiD//2Q==`; + expect(PdfLogoSchema.validate(jpgString)).toBe(jpgString); + + const pngString = + `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAO4AAADUCAMAAACs0e/bAAAAjVBMVEX////8/Pz4+Pj5+fnb29vz8/Px8fFeXl7r6+u/v79nZ` + + `tcAAAAASUVORK5CYII=`; + expect(PdfLogoSchema.validate(pngString)).toBe(pngString); + + const gifString = + `data:image/gif;base64,R0lGODlhoADIAPYAAO/w7wgFBwsLCxMTExsbGyMjI5SUlLS0tLu7u9vb2+Hh4e/v7/Ds7////0NDQ2RkZCkXJO/w8PLy8g8QD` + + `53IIefTH3WR4N8lXzvKWu/zlMI+5zGdO85rb/OY4z7nOd87znvv850APutCHTvSiG/3oSE+60pfO9KY7/elQj7rU5xIIADs=`; + expect(PdfLogoSchema.validate(gifString)).toBe(gifString); + + const svgString = + `data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXR` + + `AgPC9nPgogIDwvZz4KPC9zdmc+Cg==`; + expect(PdfLogoSchema.validate(svgString)).toBe(svgString); +}); + +test('validates if provided with null / undefined value', () => { + expect(() => PdfLogoSchema.validate(undefined)).not.toThrow(); + expect(() => PdfLogoSchema.validate(null)).not.toThrow(); +}); + +test('throws validation error if provided with data over max size', () => { + const largeJpgMock = + `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHCBcUFBUUExUYGRUaGRsZGxsZHB8bIh0iGhgbGxkbGx8dIy0kGx0rIiIbJTcoKi8xNDU0ISY6Pzo2` + + range(0, 2050) + .map( + () => + `Pi0zNDMBCwsLBgYGEAYGEDEcFRwxMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMf/AABEIAOgA2gMBIgACEQEDEQH/xAAcAAEAAgMBAQE` + ) + .join('') + + `+8snFz9eWgvYKS4ZsvS05zRQsDveIzH4Er4iDtr6iICIiAiIgIiICIiD//2Q==`; + expect(() => PdfLogoSchema.validate(largeJpgMock)).toThrowError(/too large/); +}); + +test('throws validation error if provided with non-image data', () => { + const invalidErrorMatcher = /try a different image/; + + expect(() => PdfLogoSchema.validate('')).toThrowError(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate(true)).toThrow(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate(false)).toThrow(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate({})).toThrow(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate([])).toThrow(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate(0)).toThrow(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate(0x00f)).toThrow(invalidErrorMatcher); + + const csvString = + `data:text/csv;base64,Il9pZCIsIl9pbmRleCIsIl9zY29yZSIsIl90eXBlIiwiZm9vLmJhciIsImZvby5iYXIua2V5d29yZCIKZjY1QU9IZ0J5bFZmWW04W` + + `TRvb1EsYmVlLDEsIi0iLGJheixiYXoKbks1QU9IZ0J5bFZmWW04WTdZcUcsYmVlLDEsIi0iLGJvbyxib28K`; + expect(() => PdfLogoSchema.validate(csvString)).toThrow(invalidErrorMatcher); + + const scriptString = + `data:application/octet-stream;base64,QEVDSE8gT0ZGCldFRUtPRllSLkNPTSB8IEZJTkQgIlRoaXMgaXMiID4gVEVNUC5CQV` + + `QKRUNITz5USElTLkJBVCBTRVQgV0VFSz0lJTMKQ0FMTCBURU1QLkJBVApERUwgIFRFTVAuQkFUCkRFTCAgVEhJUy5CQVQKRUNITyBXZWVrICVXRUVLJQo=`; + expect(() => PdfLogoSchema.validate(scriptString)).toThrow(invalidErrorMatcher); +}); diff --git a/x-pack/plugins/reporting/server/config/ui_settings.ts b/x-pack/plugins/reporting/server/config/ui_settings.ts new file mode 100644 index 00000000000000..337dbf4036b44f --- /dev/null +++ b/x-pack/plugins/reporting/server/config/ui_settings.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, UiSettingsParams } from 'kibana/server'; +import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../common/constants'; + +const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6); +const maxLogoSizeInKilobytes = kbToBase64Length(200); + +// inspired by x-pack/plugins/canvas/common/lib/dataurl.ts +const dataurlRegex = /^data:([a-z]+\/[a-z0-9-+.]+)(;[a-z-]+=[a-z0-9-]+)?(;([a-z0-9]+))?,/; +const imageTypes = ['image/svg+xml', 'image/jpeg', 'image/png', 'image/gif']; + +const isImageData = (str: any): boolean => { + const matches = str.match(dataurlRegex); + + if (!matches) { + return false; + } + + const [, mimetype, , , encoding] = matches; + const imageTypeIndex = imageTypes.indexOf(mimetype); + if (imageTypeIndex < 0 || encoding !== 'base64') { + return false; + } + + return true; +}; + +const isLessThanMaxSize = (str: any) => { + if (str.length > maxLogoSizeInKilobytes) { + return false; + } + + return true; +}; + +const validatePdfLogoBase64String = (str: any) => { + if (typeof str !== 'string' || !isImageData(str)) { + return i18n.translate('xpack.reporting.uiSettings.validate.customLogo.badFile', { + defaultMessage: `Sorry, that file will not work. Please try a different image file.`, + }); + } + if (!isLessThanMaxSize(str)) { + return i18n.translate('xpack.reporting.uiSettings.validate.customLogo.tooLarge', { + defaultMessage: `Sorry, that file is too large. The image file must be less than 200 kilobytes.`, + }); + } +}; + +export const PdfLogoSchema = schema.nullable(schema.any({ validate: validatePdfLogoBase64String })); + +export function registerUiSettings(core: CoreSetup) { + core.uiSettings.register({ + [UI_SETTINGS_CUSTOM_PDF_LOGO]: { + name: i18n.translate('xpack.reporting.pdfFooterImageLabel', { + defaultMessage: 'PDF footer image', + }), + value: null, + description: i18n.translate('xpack.reporting.pdfFooterImageDescription', { + defaultMessage: `Custom image to use in the PDF's footer`, + }), + sensitive: true, + type: 'image', + schema: PdfLogoSchema, + category: [PLUGIN_ID], + validation: { + maxSize: { + length: maxLogoSizeInKilobytes, + description: '200 kB', + }, + }, + }, + } as Record>); +} diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index bef60545d89b89..3dc7e7ef3df929 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -5,21 +5,22 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; -import { i18n } from '@kbn/i18n'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; -import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from '../common/constants'; +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { PLUGIN_ID } from '../common/constants'; import { ReportingCore } from './'; import { initializeBrowserDriverFactory } from './browsers'; -import { buildConfig, ReportingConfigType } from './config'; +import { buildConfig, registerUiSettings, ReportingConfigType } from './config'; import { LevelLogger, ReportingStore } from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; -import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; +import type { + ReportingRequestHandlerContext, + ReportingSetup, + ReportingSetupDeps, + ReportingStart, + ReportingStartDeps, +} from './types'; import { registerReportingUsageCollector } from './usage'; -import type { ReportingRequestHandlerContext } from './types'; - -const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6); export class ReportingPlugin implements Plugin { @@ -44,28 +45,7 @@ export class ReportingPlugin } }); - core.uiSettings.register({ - [UI_SETTINGS_CUSTOM_PDF_LOGO]: { - name: i18n.translate('xpack.reporting.pdfFooterImageLabel', { - defaultMessage: 'PDF footer image', - }), - value: null, - description: i18n.translate('xpack.reporting.pdfFooterImageDescription', { - defaultMessage: `Custom image to use in the PDF's footer`, - }), - sensitive: true, - type: 'image', - schema: schema.nullable(schema.byteSize({ max: '200kb' })), - category: [PLUGIN_ID], - // Used client-side for size validation - validation: { - maxSize: { - length: kbToBase64Length(200), - description: '200 kB', - }, - }, - }, - }); + registerUiSettings(core); const { elasticsearch, http } = core; const { features, licensing, security, spaces, taskManager } = plugins;