Skip to content

Commit

Permalink
Add png output to reports 65 (elastic#24759)
Browse files Browse the repository at this point in the history
* Changes for new PNG image output for reports

Change PNG button name from Download to Generate

Changes for new PNG image output for reports

Change PNG button name from Download to Generate

Remove compatabilityShim from PNG and move to PDF folders

Changed API parameters to contain only required parameters

Added test cases for PNG reporting and added title back into the PNG API call

Merge of security changes for PNG reporting

Fixed issues with test cases for both PDF and PNG

Updated test snapshots for PNG and PDF test changes

* Adding new security changes made for PDF into new PNG code

* Removed commented out lines that were not needed
  • Loading branch information
bgaddis56 authored and stacey-gammon committed Nov 1, 2018
1 parent e7a3bba commit 9946113
Show file tree
Hide file tree
Showing 34 changed files with 851 additions and 43 deletions.
7 changes: 6 additions & 1 deletion x-pack/plugins/reporting/common/constants.ts
Expand Up @@ -11,7 +11,12 @@ export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY =

export const API_BASE_URL = '/api/reporting';

export const WHITELISTED_JOB_CONTENT_TYPES = ['application/json', 'application/pdf', 'text/csv'];
export const WHITELISTED_JOB_CONTENT_TYPES = [
'application/json',
'application/pdf',
'text/csv',
'image/png',
];

export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo';

Expand Down
Expand Up @@ -5,7 +5,7 @@
*/

import url from 'url';
import { oncePerServer } from '../../../../server/lib/once_per_server';
import { oncePerServer } from '../../../server/lib/once_per_server';

function getAbsoluteUrlFn(server) {
const config = server.config();
Expand Down
Expand Up @@ -3,8 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KbnServer, Size } from '../../../../../types';
import { LayoutTypes } from '../../../common/constants';
import { KbnServer, Size } from '../../../types';
import { LayoutTypes } from '../constants';
import { Layout } from './layout';
import { PreserveLayout } from './preserve_layout';
import { PrintLayout } from './print_layout';
Expand Down
Expand Up @@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Size, ViewZoomWidthHeight } from '../../../../../types';
import { Size, ViewZoomWidthHeight } from '../../../types';

export interface PageSizeParams {
pageMarginTop: number;
Expand Down
Expand Up @@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import path from 'path';
import { Size } from '../../../../../types';
import { LayoutTypes } from '../../../common/constants';
import { Size } from '../../../types';
import { LayoutTypes } from '../constants';
import { Layout, PageSizeParams } from './layout';

// We use a zoom of two to bump up the resolution of the screenshot a bit.
Expand Down
Expand Up @@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import path from 'path';
import { EvaluateOptions, KbnServer, Size } from '../../../../../types';
import { LayoutTypes } from '../../../common/constants';
import { EvaluateOptions, KbnServer, Size } from '../../../types';
import { LayoutTypes } from '../constants';
import { Layout } from './layout';
import { CaptureConfig } from './types';

Expand Down
Expand Up @@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Size } from '../../../../../types';
import { Size } from '../../../types';

export interface CaptureConfig {
zoom: number;
Expand Down
Expand Up @@ -9,7 +9,7 @@ import { first, tap, mergeMap } from 'rxjs/operators';
import fs from 'fs';
import getPort from 'get-port';
import { promisify } from 'bluebird';
import { LevelLogger } from '../../../../server/lib/level_logger';
import { LevelLogger } from '../../../server/lib/level_logger';

const fsp = {
readFile: promisify(fs.readFile, fs)
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/reporting/export_types/png/metadata.js
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export const metadata = {
id: 'png',
name: 'PNG'
};
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { cryptoFactory } from '../../../../server/lib/crypto';
import { oncePerServer } from '../../../../server/lib/once_per_server';

function createJobFn(server) {
const crypto = cryptoFactory(server);

return async function createJob({
objectType,
title,
relativeUrl,
browserTimezone,
layout
}, headers, request) {
const serializedEncryptedHeaders = await crypto.encrypt(headers);

return {
type: objectType,
title: title,
relativeUrl,
headers: serializedEncryptedHeaders,
browserTimezone,
layout,
basePath: request.getBasePath(),
forceNow: new Date().toISOString(),
};
};
}

export const createJobFactory = oncePerServer(createJobFn);
118 changes: 118 additions & 0 deletions x-pack/plugins/reporting/export_types/png/server/execute_job/index.js
@@ -0,0 +1,118 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import url from 'url';
import * as Rx from 'rxjs';
import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators';
import { oncePerServer } from '../../../../server/lib/once_per_server';
import { generatePngObservableFactory } from '../lib/generate_png';
import { cryptoFactory } from '../../../../server/lib/crypto';
import { getAbsoluteUrlFactory } from '../../../common/execute_job/get_absolute_url';
import { omit } from 'lodash';

const KBN_SCREENSHOT_HEADER_BLACKLIST = [
'accept-encoding',
'content-length',
'content-type',
'host',
'referer',
// `Transfer-Encoding` is hop-by-hop header that is meaningful
// only for a single transport-level connection, and shouldn't
// be stored by caches or forwarded by proxies.
'transfer-encoding',
];

function executeJobFn(server) {
const generatePngObservable = generatePngObservableFactory(server);
const crypto = cryptoFactory(server);
const getAbsoluteUrl = getAbsoluteUrlFactory(server);
const config = server.config();

const decryptJobHeaders = async (job) => {
const decryptedHeaders = await crypto.decrypt(job.headers);
return { job, decryptedHeaders };
};

const omitBlacklistedHeaders = ({ job, decryptedHeaders }) => {
const filteredHeaders = omit(decryptedHeaders, KBN_SCREENSHOT_HEADER_BLACKLIST);
return { job, filteredHeaders };
};

const getSavedObjectAbsoluteUrl = (job, relativeUrl) => {
if (relativeUrl) {
const { pathname: path, hash, search } = url.parse(relativeUrl);
return getAbsoluteUrl({ basePath: job.basePath, path, hash, search });
}

throw new Error(`Unable to generate report. Url is not defined.`);
};

const getConditionalHeaders = ({ job, filteredHeaders }) => {
const conditionalHeaders = {
headers: filteredHeaders,
conditions: {
hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'),
port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'),
basePath: config.get('server.basePath'),
protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol,
}
};

return { job, conditionalHeaders };
};

const addForceNowQuerystring = async ({ job, conditionalHeaders }) => {

const jobUrl = getSavedObjectAbsoluteUrl(job, job.relativeUrl);

if (!job.forceNow) {
return { job, conditionalHeaders, hashUrl: jobUrl };
}

const parsed = url.parse(jobUrl, true);
const hash = url.parse(parsed.hash.replace(/^#/, ''), true);

const transformedHash = url.format({
pathname: hash.pathname,
query: {
...hash.query,
forceNow: job.forceNow
}
});

const hashUrl = url.format({
...parsed,
hash: transformedHash
});

return { job, conditionalHeaders, hashUrl };
};

return function executeJob(jobToExecute, cancellationToken) {
const process$ = Rx.of(jobToExecute).pipe(
mergeMap(decryptJobHeaders),
catchError(() => Rx.throwError('Failed to decrypt report job data. Please re-generate this report.')),
map(omitBlacklistedHeaders),
map(getConditionalHeaders),
mergeMap(addForceNowQuerystring),
mergeMap(({ job, conditionalHeaders, hashUrl }) => {
return generatePngObservable(hashUrl, job.browserTimezone, conditionalHeaders, job.layout);
}),
map(buffer => ({
content_type: 'image/png',
content: buffer.toString('base64')
}))
);

const stop$ = Rx.fromEventPattern(cancellationToken.on);

return process$.pipe(
takeUntil(stop$)
).toPromise();
};
}

export const executeJobFactory = oncePerServer(executeJobFn);

0 comments on commit 9946113

Please sign in to comment.