Skip to content

Commit

Permalink
fix: custom validation refactor (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
YSNIS authored and Seavenly committed Jul 9, 2019
1 parent cf2f794 commit b6f5fce
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 157 deletions.
10 changes: 3 additions & 7 deletions src/models/Banner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,13 @@ const Banner = {

function render(totalOptions) {
const options = validateOptions(totalOptions);
const renderProm = getBannerMarkup(options) // Promise<Object(markup)>
const renderProm = getBannerMarkup(options) // Promise<Object( markup, options )>
.then(
pipe(
partial(objectAssign, { options }), // Object(markup, options)
insertMarkup // Promise<Object(meta)>
)
insertMarkup // Promise<Object(meta, options)>
)
.then(
pipe(
partial(objectAssign, { wrapper, options, events }), // Object(meta, wrapper, options, events)
partial(objectAssign, { wrapper, events }), // Object(meta, wrapper, options, events)
concatTracker, // Object(meta, wrapper, options, events, track)
passThrough(Modal.init), // Object(meta, wrapper, options, events, track)
passThrough(setSize), // Object(meta, wrapper, options, events, track)
Expand All @@ -76,7 +73,6 @@ const Banner = {
.catch(err => logger.error({ error: `${err}` }));

logger.waitFor(renderProm);

updateOptions(options);
}

Expand Down
3 changes: 0 additions & 3 deletions src/models/Banner/validateOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import objectEntries from 'core-js-pure/stable/object/entries';
import numberIsNaN from 'core-js-pure/stable/number/is-nan';

import { logger } from '../../services/logger';
import { objectFlattenToArray } from '../../utils';

const Types = {
ANY: 'ANY',
Expand Down Expand Up @@ -195,8 +194,6 @@ export default function validateOptions({ id, account, amount, countryCode, styl
validOptions.style = getValidStyleOptions(style);
}

validOptions.style._flattened = objectFlattenToArray(validOptions.style);

objectAssign(validOptions, populateDefaults(VALID_OPTIONS, otherOptions, ''));

return validOptions;
Expand Down
136 changes: 9 additions & 127 deletions src/services/banner/customTemplate.js
Original file line number Diff line number Diff line change
@@ -1,106 +1,7 @@
import stringIncludes from 'core-js-pure/stable/string/includes';
import objectEntries from 'core-js-pure/stable/object/entries';
import { ZalgoPromise } from 'zalgo-promise';
import stringStartsWith from 'core-js-pure/stable/string/starts-with';

import { logger, ERRORS } from '../logger';
import publicKey from './public.key';
import { memoize, objectGet, objectFlattenToArray } from '../../utils';

function str2ab(str) {
const bufView = new Uint8Array(str.length);
for (let i = str.length; i >= 0; i -= 1) {
bufView[i] = str.charCodeAt(i);
}
return bufView.buffer;
}

function logCustomValidationError(account, style, err) {
if (err) {
logger.warn(err);
logger.error({
message: err,
account,
customStyle: style.styles,
sign: style.sign
});
} else {
logger.warn(
'Invalid custom styles. Please ensure the correct account number, styles, and signature have been entered. Banner has been hidden.'
);
logger.error({
message: ERRORS.INVALID_STYLE_OPTIONS,
account,
customStyle: style.custom,
sign: style.sign
});
}
}

// Alphabetizes keys and returns their values as a string
function getObjectValuesAsString(obj) {
return objectFlattenToArray(obj)
.sort()
.join('');
}

/**
* Will validate custom banner styling based on the pre-validated hash of the account
* and custom styles string being passsed into the render call
* @param {String} account The account number passed in from the render call
* @param {Object} style Object containing the styles passed in from the render call
*/
function validateSign(sign, account, styles) {
return new ZalgoPromise(resolve => {
// Validate signature and styles
try {
// If using demo build, check if PayPal domain
if (__DEMO__ && stringIncludes(window.location.host, 'paypal.com')) {
return resolve(true);
}

const message = str2ab(`${account}${getObjectValuesAsString(styles)}`);
const signature = str2ab(window.atob(sign));
const binaryDer = str2ab(window.atob(publicKey));

const rsaConfig = [
'spki',
binaryDer,
{
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256'
},
true,
['verify']
];

const rsaType = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };

// If IE, create mvp window.crypto polyfill
const crypto = window.crypto || {};
if (!window.crypto) {
const promisify = fn => (...args) =>
new ZalgoPromise((res, rej) => {
const x = fn(...args);
x.oncomplete = data => (objectGet(data, 'target.result') ? res(data.target.result) : rej());
});

crypto.subtle = {
importKey: promisify(window.msCrypto.subtle.importKey),
verify: promisify(window.msCrypto.subtle.verify)
};
}

return crypto.subtle.importKey(...rsaConfig).then(data => {
crypto.subtle
.verify(rsaType, data, signature, message)
.then(resolve)
.catch(() => resolve(false));
});
} catch (err) {
return resolve(false);
}
});
}
import { memoize } from '../../utils';

function fetcher(url) {
return new ZalgoPromise(resolve => {
Expand All @@ -121,33 +22,14 @@ function fetcher(url) {
});
}

// Removes sign, flattened, and possibly ratio if undefined from the style object. Returns everything else.
function trimStyles(obj) {
const styleObject = objectEntries(obj).reduce((accum, [key, val]) => {
return val !== undefined
? {
...accum,
[key]: val
}
: accum;
}, {});
delete styleObject._flattened;
return styleObject;
}

function getCustomTemplate(sign, account, styles) {
const styleObject = trimStyles(styles);
function getCustomTemplate(styles) {
const source = styles.markup;
const markupProm = ZalgoPromise.resolve(source.match(/^(?:(https?:\/\/)|\.{0,2}\/)/) ? fetcher(source) : source);
return markupProm.then(markup =>
validateSign(sign, account, { ...styleObject, markup }).then(validated => {
if (!validated) {
logCustomValidationError(account, markup);
}

return validated ? markup : '';
})
);
if (__DEMO__) {
return ZalgoPromise.resolve(
stringStartsWith(source, 'http') || stringStartsWith(source, './') ? fetcher(source) : source
);
}
return ZalgoPromise.resolve(stringStartsWith(source, 'https://www.paypalobjects.com') ? fetcher(source) : '');
}

export default memoize(getCustomTemplate);
51 changes: 38 additions & 13 deletions src/services/banner/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-param-reassign */
import objectEntries from 'core-js-pure/stable/object/entries';
import { ZalgoPromise } from 'zalgo-promise';

import { memoizeOnProps, objectGet } from '../../utils';
import { logger, EVENTS } from '../logger';
import { memoizeOnProps, objectGet, objectMerge, objectFlattenToArray } from '../../utils';
import { logger, EVENTS, ERRORS } from '../logger';
import getCustomTemplate from './customTemplate';

// Using same JSONP callback namespace as original merchant.js
Expand All @@ -25,7 +27,8 @@ const LOCALE_MAP = {
* @param {Object} options Banner options
* @returns {Promise<string>} Banner Markup
*/
function fetcher({ account, amount, countryCode }) {
function fetcher(options) {
const { account, amount, countryCode } = options;
return new ZalgoPromise(resolve => {
// Create JSONP callback
const callbackName = `c${Math.floor(Math.random() * 10 ** 19)}`;
Expand Down Expand Up @@ -71,30 +74,52 @@ function fetcher({ account, amount, countryCode }) {
document.head.removeChild(script);
delete window.__PP[callbackName];
try {
resolve({ markup: JSON.parse(markup.replace(/<\/?div>/g, '')) });
resolve({ markup: JSON.parse(markup.replace(/<\/?div>/g, '')), options });
} catch (err) {
resolve({ markup });
resolve({ markup, options });
}
};
});
}

/**
* Extract annotations from markup into a single options object
* @param {string} markup String of banner HTML markup
* @returns {Object} Banner annotations
*/
function getBannerOptions(markup) {
const annotationsString = markup.match(/^<!--([\s\S]+?)-->/);
if (annotationsString) {
try {
return JSON.parse(annotationsString[1]);
} catch (err) {
throw new Error(ERRORS.INVALID_CUSTOM_BANNER_JSON);
}
}
return {};
}

const memoFetcher = memoizeOnProps(fetcher, ['account', 'amount', 'countryCode']);

export default function getBannerMarkup(options) {
if (objectGet(options, 'style.layout') !== 'custom') {
return memoFetcher(options);
}

const sign = objectGet(options, 'sign');
return ZalgoPromise.all([memoFetcher(options), getCustomTemplate(sign, options.account, options.style)]).then(
([data, template]) => {
if (typeof data.markup === 'object') {
// eslint-disable-next-line no-param-reassign
data.markup.template = template;
return ZalgoPromise.all([memoFetcher(options), getCustomTemplate(options.style)]).then(([data, template]) => {
if (typeof data.markup === 'object') {
if (template === '') {
logger.error({ message: ERRORS.INVALID_STYLE_OPTIONS });
}
data.markup.template = template;

const bannerOptions = getBannerOptions(template);

return data;
const mergedOptions = objectMerge(options, bannerOptions);
mergedOptions.style._flattened = objectFlattenToArray(mergedOptions.style);

return { markup: data.markup, options: mergedOptions };
}
);
return { markup: data.markup, options };
});
}
3 changes: 2 additions & 1 deletion src/services/logger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const ERRORS = {
HIDDEN: 'Overflow fallback failed. Hiding banner.',
INVALID_STYLE_OPTIONS: 'Invalid account, styles, signature combination.',
INVALID_LEGACY_BANNER: 'Invalid legacy banner placement/offerType combination',
MODAL_LOAD_FAILURE: 'Modal failed to initialize.'
MODAL_LOAD_FAILURE: 'Modal failed to initialize.',
INVALID_CUSTOM_BANNER_JSON: 'Invalid JSON in custom banner creative'
};

const logs = [];
Expand Down
11 changes: 5 additions & 6 deletions src/utils/container/insertMarkup.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function insertStringIntoIframe(container, markup) {
iframeWindow.document.close();
}

resolve({ meta: { ...iframeWindow.meta } });
resolve(iframeWindow.meta);
});
});
}
Expand All @@ -32,7 +32,6 @@ function insertJsonIntoIframe(container, markup, options) {
// element innerHTML and writing to iframe document as string to parse html
const iframeWindow = container.contentWindow;
const { meta } = markup;

const templateNode = Template.getTemplateNode(options, markup);
const newNode = iframeWindow.document.importNode(templateNode, true);

Expand Down Expand Up @@ -60,7 +59,7 @@ function insertJsonIntoIframe(container, markup, options) {
arrayFrom(newNode.children).forEach(el => iframeWindow.document.body.appendChild(el));

ZalgoPromise.all(proms).then(() => {
resolve({ meta });
resolve(meta);
});
});
}
Expand All @@ -84,12 +83,12 @@ export default curry(
new ZalgoPromise(resolve => {
if (container.tagName === 'IFRAME') {
if (typeof markup === 'string') {
insertStringIntoIframe(container, markup).then(resolve);
insertStringIntoIframe(container, markup).then(meta => resolve({ meta, options }));
} else {
insertJsonIntoIframe(container, markup, options).then(resolve);
insertJsonIntoIframe(container, markup, options).then(meta => resolve({ meta, options }));
}
} else {
resolve(handleLegacy(container, markup, options));
resolve({ meta: handleLegacy(container, markup, options), options });
}
})
);

0 comments on commit b6f5fce

Please sign in to comment.