Skip to content

Commit

Permalink
[Reporting/UI Settings] Validation for the Reporting UI Setting Custo…
Browse files Browse the repository at this point in the history
…m Logo (#94746)

* Validation for the Reporting UI Setting Custom Logo

* add more validations

* check if image is too large

* fix i18n

* clean redundant

* large test strings not necessary

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
tsullivan and kibanamachine committed Mar 17, 2021
1 parent 37ff43b commit 0d1a1af
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 32 deletions.
4 changes: 3 additions & 1 deletion x-pack/plugins/reporting/server/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReportingConfigType> = {
Expand Down
71 changes: 71 additions & 0 deletions x-pack/plugins/reporting/server/config/ui_settings.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
81 changes: 81 additions & 0 deletions x-pack/plugins/reporting/server/config/ui_settings.ts
Original file line number Diff line number Diff line change
@@ -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<object, unknown>) {
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<string, UiSettingsParams<null>>);
}
42 changes: 11 additions & 31 deletions x-pack/plugins/reporting/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReportingSetup, ReportingStart, ReportingSetupDeps, ReportingStartDeps> {
Expand All @@ -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;
Expand Down

0 comments on commit 0d1a1af

Please sign in to comment.