[Screenshotting] fix server crash with non-numeric width or height (#132384)

* throw error early if invalid layout

* add layout id test

* add new error type

* add error type to usage tracking

* fix tweak

* add comment note

* fix telemetry check

* fix ts

* fix moot change

* fix ts

* Update x-pack/plugins/screenshotting/server/layouts/create_layout.ts

Co-authored-by: Jean-Louis Leysens <jloleysens@gmail.com>

* fix ts

* fix snapshots

* fix bundling issue in canvas

* convert LayoutTypes to a string literal union

* cleanup

* remove screenshotting from reporting plugin example required bundles

* export as type

* fix ts

* fix more ts

Co-authored-by: Jean-Louis Leysens <jloleysens@gmail.com>
This commit is contained in:
Tim Sullivan 2022-06-03 09:34:54 -07:00 committed by GitHub
parent fc76e0c8c6
commit 53688bf336
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 309 additions and 109 deletions

View file

@ -17,6 +17,5 @@
"navigation",
"screenshotMode",
"share"
],
"requiredBundles": ["screenshotting"]
]
}

View file

@ -40,7 +40,6 @@ import type {
JobParamsPNGV2,
} from '@kbn/reporting-plugin/common/types';
import type { ReportingStart } from '@kbn/reporting-plugin/public';
import { LayoutTypes } from '@kbn/screenshotting-plugin/public';
import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common';
import { useApplicationContext } from '../application_context';
import { ROUTES } from '../constants';
@ -85,9 +84,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
const getPDFJobParamsDefault = (): JobAppParamsPDF => {
return {
layout: {
id: LayoutTypes.PRESERVE_LAYOUT,
},
layout: { id: 'preserve_layout' },
relativeUrls: ['/app/reportingExample#/intended-visualization'],
objectType: 'develeloperExample',
title: 'Reporting Developer Example',
@ -97,9 +94,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
const getPDFJobParamsDefaultV2 = (): JobParamsPDFV2 => {
return {
version: '8.0.0',
layout: {
id: LayoutTypes.PRESERVE_LAYOUT,
},
layout: { id: 'preserve_layout' },
locatorParams: [
{ id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', params: { myTestState: {} } },
],
@ -112,9 +107,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
const getPNGJobParamsDefaultV2 = (): JobParamsPNGV2 => {
return {
version: '8.0.0',
layout: {
id: LayoutTypes.PRESERVE_LAYOUT,
},
layout: { id: 'preserve_layout' },
locatorParams: {
id: REPORTING_EXAMPLE_LOCATOR_ID,
version: '0.5.0',
@ -129,9 +122,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
const getCaptureTestPNGJobParams = (): JobParamsPNGV2 => {
return {
version: '8.0.0',
layout: {
id: LayoutTypes.PRESERVE_LAYOUT,
},
layout: { id: 'preserve_layout' },
locatorParams: {
id: REPORTING_EXAMPLE_LOCATOR_ID,
version: '0.5.0',
@ -147,7 +138,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
return {
version: '8.0.0',
layout: {
id: print ? LayoutTypes.PRINT : LayoutTypes.PRESERVE_LAYOUT,
id: print ? 'print' : 'preserve_layout',
dimensions: {
// Magic numbers based on height of components not rendered on this screen :(
height: 2400,

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import type { RedirectOptions } from '@kbn/share-plugin/public';
import { JobAppParamsPDFV2 } from '@kbn/reporting-plugin/common/types';
import type { RedirectOptions } from '@kbn/share-plugin/public';
import { CanvasAppLocatorParams, CANVAS_APP_LOCATOR } from '../../../../common/locator';
import { CanvasWorkpad } from '../../../../types';
@ -45,10 +45,7 @@ export function getPdfJobParams(
}
return {
layout: {
dimensions: { width, height },
id: 'canvas',
},
layout: { dimensions: { width, height }, id: 'canvas' },
objectType: 'canvas workpad',
locatorParams,
title,

View file

@ -33,6 +33,16 @@ export abstract class ReportingError extends Error {
}
}
/**
* While validating the page layout parameters for a screenshot type report job
*/
export class InvalidLayoutParametersError extends ReportingError {
static code = 'invalid_layout_parameters_error' as const;
public get code() {
return InvalidLayoutParametersError.code;
}
}
/**
* While performing some reporting action, like fetching data from ES, our
* access token expired.

View file

@ -12,6 +12,7 @@ import {
BrowserCouldNotLaunchError,
BrowserUnexpectedlyClosedError,
BrowserScreenshotError,
InvalidLayoutParametersError,
} from '.';
describe('mapToReportingError', () => {
@ -22,6 +23,9 @@ describe('mapToReportingError', () => {
});
test('Screenshotting error', () => {
expect(mapToReportingError(new errors.InvalidLayoutParametersError())).toBeInstanceOf(
InvalidLayoutParametersError
);
expect(mapToReportingError(new errors.BrowserClosedUnexpectedly())).toBeInstanceOf(
BrowserUnexpectedlyClosedError
);

View file

@ -14,13 +14,25 @@ import {
BrowserScreenshotError,
PdfWorkerOutOfMemoryError,
VisualReportingSoftDisabledError,
InvalidLayoutParametersError,
} from '.';
/**
* Map an error object from the Screenshotting plugin into an error type of the Reporting domain.
*
* NOTE: each type of ReportingError code must be referenced in each applicable `errorCodesSchema*` object in
* x-pack/plugins/reporting/server/usage/schema.ts
*
* @param {unknown} error - a kind of error object
* @returns {ReportingError} - the converted error object
*/
export function mapToReportingError(error: unknown): ReportingError {
if (error instanceof ReportingError) {
return error;
}
switch (true) {
case error instanceof errors.InvalidLayoutParametersError:
return new InvalidLayoutParametersError((error as Error).message);
case error instanceof errors.BrowserClosedUnexpectedly:
return new BrowserUnexpectedlyClosedError((error as Error).message);
case error instanceof errors.FailedToCaptureScreenshot:

View file

@ -8,7 +8,7 @@
import type { ReportApiJSON } from '../types';
import type { ReportMock } from './types';
const buildMockReport = (baseObj: ReportMock) => ({
const buildMockReport = (baseObj: ReportMock): ReportApiJSON => ({
index: '.reporting-2020.04.12',
migration_version: '7.15.0',
max_attempts: 1,

View file

@ -9,7 +9,6 @@ import apm from 'elastic-apm-node';
import type { Logger } from '@kbn/core/server';
import * as Rx from 'rxjs';
import { finalize, map, tap } from 'rxjs/operators';
import { LayoutTypes } from '@kbn/screenshotting-plugin/common';
import type { ReportingCore } from '../..';
import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants';
import type { PngMetrics } from '../../../common/types';
@ -38,10 +37,7 @@ export function generatePngObservable(
.getScreenshots({
...options,
format: 'png',
layout: {
id: LayoutTypes.PRESERVE_LAYOUT,
...options.layout,
},
layout: { id: 'preserve_layout', ...options.layout },
})
.pipe(
tap(({ metrics }) => {

View file

@ -10,8 +10,8 @@ import * as Rx from 'rxjs';
import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants';
import { TaskRunResult } from '../../../lib/tasks';
import { PngScreenshotOptions, RunTaskFn, RunTaskFnFactory } from '../../../types';
import { decryptJobHeaders, getFullUrls, generatePngObservable } from '../../common';
import { RunTaskFn, RunTaskFnFactory } from '../../../types';
import { decryptJobHeaders, generatePngObservable, getFullUrls } from '../../common';
import { TaskPayloadPNG } from '../types';
export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNG>> =
@ -39,10 +39,8 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNG>> =
browserTimezone: job.browserTimezone,
layout: {
...job.layout,
// TODO: We do not do a runtime check for supported layout id types for now. But technically
// we should.
id: job.layout?.id,
} as PngScreenshotOptions['layout'],
id: 'preserve_layout',
},
});
}),
tap(({ buffer }) => stream.write(buffer)),

View file

@ -10,7 +10,7 @@ import * as Rx from 'rxjs';
import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants';
import { TaskRunResult } from '../../lib/tasks';
import { PngScreenshotOptions, RunTaskFn, RunTaskFnFactory } from '../../types';
import { RunTaskFn, RunTaskFnFactory } from '../../types';
import { decryptJobHeaders, generatePngObservable } from '../common';
import { getFullRedirectAppUrl } from '../common/v2/get_full_redirect_app_url';
import { TaskPayloadPNGV2 } from './types';
@ -38,12 +38,7 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNGV2>> =
return generatePngObservable(reporting, jobLogger, {
headers,
browserTimezone: job.browserTimezone,
layout: {
...job.layout,
// TODO: We do not do a runtime check for supported layout id types for now. But technically
// we should.
id: job.layout?.id,
} as PngScreenshotOptions['layout'],
layout: { ...job.layout, id: 'preserve_layout' },
urls: [[url, locatorParams]],
});
}),

View file

@ -10,8 +10,8 @@ import * as Rx from 'rxjs';
import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants';
import { TaskRunResult } from '../../../lib/tasks';
import { PdfScreenshotOptions, RunTaskFn, RunTaskFnFactory } from '../../../types';
import { decryptJobHeaders, getFullUrls, getCustomLogo } from '../../common';
import { RunTaskFn, RunTaskFnFactory } from '../../../types';
import { decryptJobHeaders, getCustomLogo, getFullUrls } from '../../common';
import { generatePdfObservable } from '../lib/generate_pdf';
import { TaskPayloadPDF } from '../types';
@ -43,12 +43,7 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDF>> =
urls,
browserTimezone,
headers,
layout: {
...layout,
// TODO: We do not do a runtime check for supported layout id types for now. But technically
// we should.
id: layout?.id,
} as PdfScreenshotOptions['layout'],
layout,
});
}),
tap(({ buffer }) => {

View file

@ -9,7 +9,6 @@ import apm from 'elastic-apm-node';
import * as Rx from 'rxjs';
import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants';
import type { PdfScreenshotOptions } from '../../types';
import { TaskRunResult } from '../../lib/tasks';
import { RunTaskFn, RunTaskFnFactory } from '../../types';
import { decryptJobHeaders, getCustomLogo } from '../common';
@ -41,12 +40,7 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDFV2>> =
logo,
browserTimezone,
headers,
layout: {
...layout,
// TODO: We do not do a runtime check for supported layout id types for now. But technically
// we should.
id: layout?.id,
} as PdfScreenshotOptions['layout'],
layout,
});
}),
tap(({ buffer }) => {

View file

@ -39,6 +39,9 @@ Object {
"browser_unexpectedly_closed_error": Object {
"type": "long",
},
"invalid_layout_parameters_error": Object {
"type": "long",
},
"kibana_shutting_down_error": Object {
"type": "long",
},
@ -143,6 +146,9 @@ Object {
"browser_unexpectedly_closed_error": Object {
"type": "long",
},
"invalid_layout_parameters_error": Object {
"type": "long",
},
"kibana_shutting_down_error": Object {
"type": "long",
},
@ -413,6 +419,9 @@ Object {
"browser_unexpectedly_closed_error": Object {
"type": "long",
},
"invalid_layout_parameters_error": Object {
"type": "long",
},
"kibana_shutting_down_error": Object {
"type": "long",
},
@ -517,6 +526,9 @@ Object {
"browser_unexpectedly_closed_error": Object {
"type": "long",
},
"invalid_layout_parameters_error": Object {
"type": "long",
},
"kibana_shutting_down_error": Object {
"type": "long",
},
@ -803,6 +815,9 @@ Object {
"browser_unexpectedly_closed_error": Object {
"type": "long",
},
"invalid_layout_parameters_error": Object {
"type": "long",
},
"kibana_shutting_down_error": Object {
"type": "long",
},
@ -935,6 +950,9 @@ Object {
"browser_unexpectedly_closed_error": Object {
"type": "long",
},
"invalid_layout_parameters_error": Object {
"type": "long",
},
"kibana_shutting_down_error": Object {
"type": "long",
},
@ -1540,6 +1558,9 @@ Object {
"browser_unexpectedly_closed_error": Object {
"type": "long",
},
"invalid_layout_parameters_error": Object {
"type": "long",
},
"kibana_shutting_down_error": Object {
"type": "long",
},
@ -1672,6 +1693,9 @@ Object {
"browser_unexpectedly_closed_error": Object {
"type": "long",
},
"invalid_layout_parameters_error": Object {
"type": "long",
},
"kibana_shutting_down_error": Object {
"type": "long",
},

View file

@ -332,6 +332,7 @@ test('Incorporate error code stats', () => {
browser_unexpectedly_closed_error: 8,
browser_screenshot_error: 27,
visual_reporting_soft_disabled_error: 1,
invalid_layout_parameters_error: 0,
},
},
printable_pdf_v2: {
@ -351,6 +352,7 @@ test('Incorporate error code stats', () => {
browser_unexpectedly_closed_error: 8,
browser_screenshot_error: 27,
visual_reporting_soft_disabled_error: 1,
invalid_layout_parameters_error: 0,
},
},
csv_searchsource_immediate: {
@ -377,6 +379,7 @@ test('Incorporate error code stats', () => {
"browser_could_not_launch_error": 2,
"browser_screenshot_error": 27,
"browser_unexpectedly_closed_error": 8,
"invalid_layout_parameters_error": 0,
"kibana_shutting_down_error": 1,
"queue_timeout_error": 1,
"unknown_error": 0,
@ -389,6 +392,7 @@ test('Incorporate error code stats', () => {
"browser_could_not_launch_error": 2,
"browser_screenshot_error": 27,
"browser_unexpectedly_closed_error": 8,
"invalid_layout_parameters_error": 0,
"kibana_shutting_down_error": 1,
"pdf_worker_out_of_memory_error": 99,
"queue_timeout_error": 1,

View file

@ -36,6 +36,7 @@ describe('Reporting telemetry schema', () => {
"PNG.error_codes.browser_could_not_launch_error.type": "long",
"PNG.error_codes.browser_screenshot_error.type": "long",
"PNG.error_codes.browser_unexpectedly_closed_error.type": "long",
"PNG.error_codes.invalid_layout_parameters_error.type": "long",
"PNG.error_codes.kibana_shutting_down_error.type": "long",
"PNG.error_codes.queue_timeout_error.type": "long",
"PNG.error_codes.unknown_error.type": "long",
@ -66,6 +67,7 @@ describe('Reporting telemetry schema', () => {
"PNGV2.error_codes.browser_could_not_launch_error.type": "long",
"PNGV2.error_codes.browser_screenshot_error.type": "long",
"PNGV2.error_codes.browser_unexpectedly_closed_error.type": "long",
"PNGV2.error_codes.invalid_layout_parameters_error.type": "long",
"PNGV2.error_codes.kibana_shutting_down_error.type": "long",
"PNGV2.error_codes.queue_timeout_error.type": "long",
"PNGV2.error_codes.unknown_error.type": "long",
@ -143,6 +145,7 @@ describe('Reporting telemetry schema', () => {
"last7Days.PNG.error_codes.browser_could_not_launch_error.type": "long",
"last7Days.PNG.error_codes.browser_screenshot_error.type": "long",
"last7Days.PNG.error_codes.browser_unexpectedly_closed_error.type": "long",
"last7Days.PNG.error_codes.invalid_layout_parameters_error.type": "long",
"last7Days.PNG.error_codes.kibana_shutting_down_error.type": "long",
"last7Days.PNG.error_codes.queue_timeout_error.type": "long",
"last7Days.PNG.error_codes.unknown_error.type": "long",
@ -173,6 +176,7 @@ describe('Reporting telemetry schema', () => {
"last7Days.PNGV2.error_codes.browser_could_not_launch_error.type": "long",
"last7Days.PNGV2.error_codes.browser_screenshot_error.type": "long",
"last7Days.PNGV2.error_codes.browser_unexpectedly_closed_error.type": "long",
"last7Days.PNGV2.error_codes.invalid_layout_parameters_error.type": "long",
"last7Days.PNGV2.error_codes.kibana_shutting_down_error.type": "long",
"last7Days.PNGV2.error_codes.queue_timeout_error.type": "long",
"last7Days.PNGV2.error_codes.unknown_error.type": "long",
@ -255,6 +259,7 @@ describe('Reporting telemetry schema', () => {
"last7Days.printable_pdf.error_codes.browser_could_not_launch_error.type": "long",
"last7Days.printable_pdf.error_codes.browser_screenshot_error.type": "long",
"last7Days.printable_pdf.error_codes.browser_unexpectedly_closed_error.type": "long",
"last7Days.printable_pdf.error_codes.invalid_layout_parameters_error.type": "long",
"last7Days.printable_pdf.error_codes.kibana_shutting_down_error.type": "long",
"last7Days.printable_pdf.error_codes.pdf_worker_out_of_memory_error.type": "long",
"last7Days.printable_pdf.error_codes.queue_timeout_error.type": "long",
@ -293,6 +298,7 @@ describe('Reporting telemetry schema', () => {
"last7Days.printable_pdf_v2.error_codes.browser_could_not_launch_error.type": "long",
"last7Days.printable_pdf_v2.error_codes.browser_screenshot_error.type": "long",
"last7Days.printable_pdf_v2.error_codes.browser_unexpectedly_closed_error.type": "long",
"last7Days.printable_pdf_v2.error_codes.invalid_layout_parameters_error.type": "long",
"last7Days.printable_pdf_v2.error_codes.kibana_shutting_down_error.type": "long",
"last7Days.printable_pdf_v2.error_codes.pdf_worker_out_of_memory_error.type": "long",
"last7Days.printable_pdf_v2.error_codes.queue_timeout_error.type": "long",
@ -463,6 +469,7 @@ describe('Reporting telemetry schema', () => {
"printable_pdf.error_codes.browser_could_not_launch_error.type": "long",
"printable_pdf.error_codes.browser_screenshot_error.type": "long",
"printable_pdf.error_codes.browser_unexpectedly_closed_error.type": "long",
"printable_pdf.error_codes.invalid_layout_parameters_error.type": "long",
"printable_pdf.error_codes.kibana_shutting_down_error.type": "long",
"printable_pdf.error_codes.pdf_worker_out_of_memory_error.type": "long",
"printable_pdf.error_codes.queue_timeout_error.type": "long",
@ -501,6 +508,7 @@ describe('Reporting telemetry schema', () => {
"printable_pdf_v2.error_codes.browser_could_not_launch_error.type": "long",
"printable_pdf_v2.error_codes.browser_screenshot_error.type": "long",
"printable_pdf_v2.error_codes.browser_unexpectedly_closed_error.type": "long",
"printable_pdf_v2.error_codes.invalid_layout_parameters_error.type": "long",
"printable_pdf_v2.error_codes.kibana_shutting_down_error.type": "long",
"printable_pdf_v2.error_codes.pdf_worker_out_of_memory_error.type": "long",
"printable_pdf_v2.error_codes.queue_timeout_error.type": "long",

View file

@ -89,6 +89,7 @@ const errorCodesSchemaPng: MakeSchemaFrom<JobTypes['PNGV2']['error_codes']> = {
browser_unexpectedly_closed_error: { type: 'long' },
browser_screenshot_error: { type: 'long' },
visual_reporting_soft_disabled_error: { type: 'long' },
invalid_layout_parameters_error: { type: 'long' },
};
const errorCodesSchemaPdf: MakeSchemaFrom<JobTypes['printable_pdf_v2']['error_codes']> = {
pdf_worker_out_of_memory_error: { type: 'long' },
@ -100,6 +101,7 @@ const errorCodesSchemaPdf: MakeSchemaFrom<JobTypes['printable_pdf_v2']['error_co
browser_unexpectedly_closed_error: { type: 'long' },
browser_screenshot_error: { type: 'long' },
visual_reporting_soft_disabled_error: { type: 'long' },
invalid_layout_parameters_error: { type: 'long' },
};
const availableTotalSchema: MakeSchemaFrom<AvailableTotal> = {

View file

@ -198,6 +198,7 @@ export interface ErrorCodeStats {
authentication_expired_error: number | null;
queue_timeout_error: number | null;
unknown_error: number | null;
invalid_layout_parameters_error: number | null;
pdf_worker_out_of_memory_error: number | null;
browser_could_not_launch_error: number | null;
browser_unexpectedly_closed_error: number | null;

View file

@ -6,6 +6,8 @@
*/
/* eslint-disable max-classes-per-file */
export class InvalidLayoutParametersError extends Error {}
export class PdfWorkerOutOfMemoryError extends Error {}
export class FailedToSpawnBrowserError extends Error {}

View file

@ -5,14 +5,13 @@
* 2.0.
*/
export type { LayoutParams } from './layout';
export { LayoutTypes } from './layout';
import * as errors from './errors';
export { errors };
export {
SCREENSHOTTING_APP_ID,
SCREENSHOTTING_EXPRESSION,
SCREENSHOTTING_EXPRESSION_INPUT,
} from './expression';
export type { LayoutParams, LayoutType } from './layout';
export { errors };
import * as errors from './errors';
export const PLUGIN_ID = 'screenshotting';

View file

@ -40,7 +40,7 @@ export interface LayoutSelectorDictionary {
/**
* Screenshot layout parameters.
*/
export type LayoutParams<Id = string> = Ensure<
export type LayoutParams<Id = LayoutType> = Ensure<
{
/**
* Unique layout name.
@ -68,8 +68,4 @@ export type LayoutParams<Id = string> = Ensure<
/**
* Supported layout types.
*/
export enum LayoutTypes {
PRESERVE_LAYOUT = 'preserve_layout',
PRINT = 'print',
CANVAS = 'canvas',
}
export type LayoutType = 'preserve_layout' | 'print' | 'canvas';

View file

@ -12,4 +12,3 @@ export function plugin() {
}
export type { LayoutParams } from '../common';
export { LayoutTypes } from '../common';

View file

@ -9,11 +9,9 @@
// we should get rid of this lib.
import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js';
import type { Values } from '@kbn/utility-types';
import { groupBy } from 'lodash';
import type { PackageInfo } from '@kbn/core/server';
import type { LayoutParams } from '../../../common';
import { LayoutTypes } from '../../../common';
import { groupBy } from 'lodash';
import type { LayoutParams, LayoutType } from '../../../common';
import type { Layout } from '../../layouts';
import type { CaptureMetrics, CaptureOptions, CaptureResult } from '../../screenshots';
import { EventLogger, Transactions } from '../../screenshots/event_logger';
@ -25,9 +23,7 @@ import { pngsToPdf } from './pdf_maker';
* => When creating a PDF intended for print multiple PNGs will be spread out across pages
* => When creating a PDF from a Canvas workpad, each page in the workpad will be placed on a separate page
*/
export type PdfLayoutParams = LayoutParams<
Values<Pick<typeof LayoutTypes, 'PRESERVE_LAYOUT' | 'CANVAS' | 'PRINT'>>
>;
export type PdfLayoutParams = LayoutParams<LayoutType>;
/**
* Options that should be provided to a PDF screenshot request.
@ -105,7 +101,7 @@ export async function toPdf(
): Promise<PdfScreenshotResult> {
let buffer: Buffer;
let pages: number;
const shouldConvertPngsToPdf = layout.id !== LayoutTypes.PRINT;
const shouldConvertPngsToPdf = layout.id !== 'print';
if (shouldConvertPngsToPdf) {
const timeRange = getTimeRange(results);
try {

View file

@ -7,12 +7,11 @@
import type { CaptureResult, CaptureOptions } from '../screenshots';
import type { LayoutParams } from '../../common';
import { LayoutTypes } from '../../common';
/**
* The layout parameters that are accepted by PNG screenshots
*/
export type PngLayoutParams = LayoutParams<LayoutTypes.PRESERVE_LAYOUT>;
export type PngLayoutParams = LayoutParams<'preserve_layout'>;
/**
* Options that should be provided to a screenshot PNG request

View file

@ -6,7 +6,7 @@
*/
import type { CustomPageSize, PredefinedPageSize } from 'pdfmake/interfaces';
import type { Size } from '../../common/layout';
import type { LayoutType, Size } from '../../common/layout';
export interface ViewZoomWidthHeight {
zoom: number;
@ -29,14 +29,14 @@ export interface PageSizeParams {
}
export abstract class BaseLayout {
public id: string = '';
public id: LayoutType;
public groupCount: number = 0;
public hasHeader: boolean = true;
public hasFooter: boolean = true;
public useReportingBranding: boolean = true;
constructor(id: string) {
constructor(id: LayoutType) {
this.id = id;
}

View file

@ -6,7 +6,6 @@
*/
import type { LayoutSelectorDictionary, Size } from '../../common/layout';
import { LayoutTypes } from '../../common';
import { DEFAULT_SELECTORS } from '.';
import type { Layout } from '.';
import { BaseLayout } from './base_layout';
@ -33,7 +32,7 @@ export class CanvasLayout extends BaseLayout implements Layout {
public useReportingBranding: boolean = false;
constructor(size: Size) {
super(LayoutTypes.CANVAS);
super('canvas');
this.height = size.height;
this.width = size.width;
this.scaledHeight = size.height * ZOOM;

View file

@ -5,28 +5,47 @@
* 2.0.
*/
import { map as mapRecord } from 'fp-ts/lib/Record';
import type { LayoutParams } from '../../common/layout';
import { LayoutTypes } from '../../common';
import { InvalidLayoutParametersError } from '../../common/errors';
import type { LayoutParams, LayoutType } from '../../common/layout';
import type { Layout } from '.';
import { CanvasLayout } from './canvas_layout';
import { PreserveLayout } from './preserve_layout';
import { PrintLayout } from './print_layout';
// utility for validating the layout type from user's job params
const LAYOUTS: LayoutType[] = ['canvas', 'print', 'preserve_layout'];
/**
* We naively round all numeric values in the object, this will break screenshotting
* if ever a have a non-number set as a value, but this points to an issue
* in the code responsible for creating the dimensions object.
* Layout dimensions must be sanitized as they are passed in the args that spawn the
* Chromium process. Width and height must be int32 value.
*
*/
const roundNumbers = mapRecord(Math.round);
const sanitizeLayout = (dimensions: { width: number; height: number }) => {
const { width, height } = dimensions;
if (isNaN(width) || isNaN(height)) {
throw new InvalidLayoutParametersError(`Invalid layout width or height`);
}
return {
width: Math.round(width),
height: Math.round(height),
};
};
export function createLayout({ id, dimensions, selectors, ...config }: LayoutParams): Layout {
if (dimensions && id === LayoutTypes.PRESERVE_LAYOUT) {
return new PreserveLayout(roundNumbers(dimensions), selectors);
const layoutId = id ?? 'print';
if (!LAYOUTS.includes(layoutId)) {
throw new InvalidLayoutParametersError(`Invalid layout type`);
}
if (dimensions && id === LayoutTypes.CANVAS) {
return new CanvasLayout(roundNumbers(dimensions));
if (dimensions) {
if (layoutId === 'preserve_layout') {
return new PreserveLayout(sanitizeLayout(dimensions), selectors);
}
if (layoutId === 'canvas') {
return new CanvasLayout(sanitizeLayout(dimensions));
}
}
// layoutParams is optional as PrintLayout doesn't use it

View file

@ -5,12 +5,11 @@
* 2.0.
*/
import { LayoutTypes } from '../../common';
import { createLayout, Layout } from '.';
export function createMockLayout(): Layout {
const layout = createLayout({
id: LayoutTypes.PRESERVE_LAYOUT,
id: 'preserve_layout',
dimensions: { height: 100, width: 100 },
zoom: 1,
}) as Layout;

View file

@ -7,7 +7,6 @@
import path from 'path';
import type { CustomPageSize } from 'pdfmake/interfaces';
import type { LayoutSelectorDictionary, Size } from '../../common/layout';
import { LayoutTypes } from '../../common';
import { DEFAULT_SELECTORS } from '.';
import type { Layout } from '.';
import { BaseLayout } from './base_layout';
@ -25,7 +24,7 @@ export class PreserveLayout extends BaseLayout implements Layout {
private readonly scaledWidth: number;
constructor(size: Size, selectors?: Partial<LayoutSelectorDictionary>) {
super(LayoutTypes.PRESERVE_LAYOUT);
super('preserve_layout');
this.height = size.height;
this.width = size.width;
this.scaledHeight = size.height * ZOOM;

View file

@ -8,7 +8,6 @@
import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces';
import type { Layout } from '.';
import { DEFAULT_SELECTORS } from '.';
import { LayoutTypes } from '../../common';
import type { LayoutParams, LayoutSelectorDictionary } from '../../common/layout';
import { DEFAULT_VIEWPORT } from '../browsers';
import { BaseLayout } from './base_layout';
@ -23,7 +22,7 @@ export class PrintLayout extends BaseLayout implements Layout {
private zoom: number;
constructor({ zoom = 1 }: Pick<LayoutParams, 'zoom'>) {
super(LayoutTypes.PRINT);
super('print');
this.zoom = zoom;
}

View file

@ -8,7 +8,7 @@
import type { Headers } from '@kbn/core/server';
import { defer, forkJoin, Observable, throwError } from 'rxjs';
import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators';
import { errors, LayoutTypes } from '../../common';
import { errors } from '../../common';
import {
Context,
DEFAULT_VIEWPORT,
@ -242,10 +242,7 @@ export class ScreenshotObservableHandler {
}
private shouldCapturePdf(): boolean {
return (
this.layout.id === LayoutTypes.PRINT &&
(this.options as PdfScreenshotOptions).format === 'pdf'
);
return this.layout.id === 'print' && (this.options as PdfScreenshotOptions).format === 'pdf';
}
public getScreenshots() {

View file

@ -7000,6 +7000,9 @@
},
"visual_reporting_soft_disabled_error": {
"type": "long"
},
"invalid_layout_parameters_error": {
"type": "long"
}
}
}
@ -7118,6 +7121,9 @@
},
"visual_reporting_soft_disabled_error": {
"type": "long"
},
"invalid_layout_parameters_error": {
"type": "long"
}
}
}
@ -7268,6 +7274,9 @@
},
"visual_reporting_soft_disabled_error": {
"type": "long"
},
"invalid_layout_parameters_error": {
"type": "long"
}
}
}
@ -7418,6 +7427,9 @@
},
"visual_reporting_soft_disabled_error": {
"type": "long"
},
"invalid_layout_parameters_error": {
"type": "long"
}
}
}
@ -8275,6 +8287,9 @@
},
"visual_reporting_soft_disabled_error": {
"type": "long"
},
"invalid_layout_parameters_error": {
"type": "long"
}
}
}
@ -8393,6 +8408,9 @@
},
"visual_reporting_soft_disabled_error": {
"type": "long"
},
"invalid_layout_parameters_error": {
"type": "long"
}
}
}
@ -8543,6 +8561,9 @@
},
"visual_reporting_soft_disabled_error": {
"type": "long"
},
"invalid_layout_parameters_error": {
"type": "long"
}
}
}
@ -8693,6 +8714,9 @@
},
"visual_reporting_soft_disabled_error": {
"type": "long"
},
"invalid_layout_parameters_error": {
"type": "long"
}
}
}

View file

@ -28,5 +28,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./usage'));
loadTestFile(require.resolve('./ilm_migration_apis'));
loadTestFile(require.resolve('./error_codes'));
loadTestFile(require.resolve('./validation'));
});
}

View file

@ -67,7 +67,7 @@ export default function ({ getService }: FtrProviderContext) {
{
browserTimezone: 'UTC',
title: 'test PDF disallowed',
layout: { id: 'preserve' },
layout: { id: 'preserve_layout' },
relativeUrls: ['/fooyou'],
objectType: 'dashboard',
version: '7.14.0',
@ -83,7 +83,7 @@ export default function ({ getService }: FtrProviderContext) {
{
browserTimezone: 'UTC',
title: 'test PDF allowed',
layout: { id: 'preserve' },
layout: { id: 'preserve_layout' },
relativeUrls: ['/fooyou'],
objectType: 'dashboard',
version: '7.14.0',
@ -101,7 +101,7 @@ export default function ({ getService }: FtrProviderContext) {
{
browserTimezone: 'UTC',
title: 'test PDF disallowed',
layout: { id: 'preserve' },
layout: { id: 'preserve_layout' },
relativeUrls: ['/fooyou'],
objectType: 'visualization',
version: '7.14.0',
@ -117,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) {
{
browserTimezone: 'UTC',
title: 'test PDF allowed',
layout: { id: 'preserve' },
layout: { id: 'preserve_layout' },
relativeUrls: ['/fooyou'],
objectType: 'visualization',
version: '7.14.0',
@ -135,7 +135,7 @@ export default function ({ getService }: FtrProviderContext) {
{
browserTimezone: 'UTC',
title: 'test PDF disallowed',
layout: { id: 'preserve' },
layout: { id: 'preserve_layout' },
relativeUrls: ['/fooyou'],
objectType: 'canvas',
version: '7.14.0',
@ -151,7 +151,7 @@ export default function ({ getService }: FtrProviderContext) {
{
browserTimezone: 'UTC',
title: 'test PDF allowed',
layout: { id: 'preserve' },
layout: { id: 'preserve_layout' },
relativeUrls: ['/fooyou'],
objectType: 'canvas',
version: '7.14.0',

View file

@ -0,0 +1,142 @@
/*
* 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 supertest from 'supertest';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const reportingAPI = getService('reportingAPI');
const retry = getService('retry');
const log = getService('log');
const supertestSvc = getService('supertest');
const status = (downloadReportPath: string, response: supertest.Response) => {
if (response.status === 503) {
log.debug(`Report at path ${downloadReportPath} is pending`);
} else if (response.status === 200) {
log.debug(`Report at path ${downloadReportPath} is complete`);
} else {
log.debug(`Report at path ${downloadReportPath} returned code ${response.status}`);
}
};
describe('Job parameter validation', () => {
before(async () => {
await reportingAPI.initEcommerce();
});
after(async () => {
await reportingAPI.teardownEcommerce();
await reportingAPI.deleteAllReports();
});
describe('printablePdfV2', () => {
it('allows width and height to have decimal', async () => {
const downloadReportPath = await reportingAPI.postJobJSON(
'/api/reporting/generate/printablePdfV2',
{ jobParams: createPdfV2Params(1541.5999755859375) }
);
await retry.tryForTime(18000, async () => {
const response: supertest.Response = await supertestSvc
.get(downloadReportPath)
.responseType('blob')
.set('kbn-xsrf', 'xxx');
status(downloadReportPath, response);
expect(response.status).equal(200);
});
});
it('fails if width or height are non-numeric', async () => {
const downloadReportPath = await reportingAPI.postJobJSON(
'/api/reporting/generate/printablePdfV2',
{ jobParams: createPdfV2Params('cucucachoo') }
);
await retry.tryForTime(18000, async () => {
const response: supertest.Response = await supertestSvc
.get(downloadReportPath)
.responseType('blob')
.set('kbn-xsrf', 'xxx');
expect(response.status).equal(500);
});
});
it('fails if there is an invalid layout ID', async () => {
const downloadReportPath = await reportingAPI.postJobJSON(
'/api/reporting/generate/printablePdfV2',
{ jobParams: createPdfV2Params(1541, 'landscape') }
);
await retry.tryForTime(18000, async () => {
const response: supertest.Response = await supertestSvc
.get(downloadReportPath)
.responseType('blob')
.set('kbn-xsrf', 'xxx');
expect(response.status).equal(500);
});
});
});
describe('pngV2', () => {
it('fails if width or height are non-numeric', async () => {
const downloadReportPath = await reportingAPI.postJobJSON('/api/reporting/generate/pngV2', {
jobParams: createPngV2Params('cucucachoo'),
});
await retry.tryForTime(18000, async () => {
const response: supertest.Response = await supertestSvc
.get(downloadReportPath)
.responseType('blob')
.set('kbn-xsrf', 'xxx');
expect(response.status).equal(500);
});
});
});
});
}
const createPdfV2Params = (testWidth: number | string, layoutId = 'preserve_layout') =>
`(browserTimezone:UTC,layout:` +
`(dimensions:(height:1492,width:${testWidth}),id:${layoutId}),` +
`locatorParams:\u0021((id:DASHBOARD_APP_LOCATOR,params:` +
`(dashboardId:\'6c263e00-1c6d-11ea-a100-8589bb9d7c6b\',` +
`preserveSavedFilters:\u0021t,` +
`timeRange:(from:\'2019-03-23T03:06:17.785Z\',to:\'2019-10-04T02:33:16.708Z\'),` +
`useHash:\u0021f,` +
`viewMode:view),` +
`version:\'8.2.0\')),` +
`objectType:dashboard,` +
`title:\'Ecom Dashboard\',` +
`version:\'8.2.0\')`;
const createPngV2Params = (testWidth: number | string) =>
`(browserTimezone:UTC,layout:` +
`(dimensions:(height:648,width:${testWidth}),id:preserve_layout),` +
`locatorParams:(id:VISUALIZE_APP_LOCATOR,params:` +
`(filters:\u0021(),` +
`indexPattern:\'5193f870-d861-11e9-a311-0fa548c5f953\',` +
`linked:\u0021t,` +
`query:(language:kuery,query:\'\'),` +
`savedSearchId:\'6091ead0-1c6d-11ea-a100-8589bb9d7c6b\',` +
`timeRange:(from:\'2019-03-23T03:06:17.785Z\',to:\'2019-10-04T02:33:16.708Z\'),` +
`uiState:(),` +
`vis:(aggs:\u0021((enabled:\u0021t,id:\'1\',params:(emptyAsNull:\u0021f),schema:metric,type:count),` +
`(enabled:\u0021t,` +
`id:\'2\',` +
`params:(field:customer_first_name.keyword,missingBucket:\u0021f,missingBucketLabel:Missing,order:desc,orderBy:\'1\',otherBucket:\u0021f,otherBucketLabel:Other,size:10),` +
`schema:segment,type:terms)),` +
`params:(maxFontSize:72,minFontSize:18,orientation:single,palette:(name:kibana_palette,type:palette),scale:linear,showLabel:\u0021t),` +
`title:\'Tag Cloud of Names\',` +
`type:tagcloud),` +
`visId:\'1bba55f0-507e-11eb-9c0d-97106882b997\'),` +
`version:\'8.2.0\'),` +
`objectType:visualization,` +
`title:\'Tag Cloud of Names\',` +
`version:\'8.2.0\')`;