UNPKG

@dailymotion/vast-client

Version:
1,375 lines (1,304 loc) 141 kB
function createAd() { let adAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return { id: adAttributes.id || null, sequence: adAttributes.sequence || null, adType: adAttributes.adType || null, adServingId: null, categories: [], expires: null, viewableImpression: [], system: null, title: null, description: null, advertiser: null, pricing: null, survey: null, // @deprecated in VAST 4.1 errorURLTemplates: [], impressionURLTemplates: [], creatives: [], extensions: [], adVerifications: [], blockedAdCategories: [], followAdditionalWrappers: true, allowMultipleAds: false, fallbackOnNoAd: null }; } function createAdVerification() { return { resource: null, vendor: null, browserOptional: false, apiFramework: null, type: null, parameters: null, trackingEvents: {} }; } function createCompanionAd() { let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return { id: creativeAttributes.id || null, adType: 'companionAd', width: creativeAttributes.width || 0, height: creativeAttributes.height || 0, assetWidth: creativeAttributes.assetWidth || null, assetHeight: creativeAttributes.assetHeight || null, expandedWidth: creativeAttributes.expandedWidth || null, expandedHeight: creativeAttributes.expandedHeight || null, apiFramework: creativeAttributes.apiFramework || null, adSlotId: creativeAttributes.adSlotId || null, pxratio: creativeAttributes.pxratio || '1', renderingMode: creativeAttributes.renderingMode || 'default', staticResources: [], htmlResources: [], iframeResources: [], adParameters: null, altText: null, companionClickThroughURLTemplate: null, companionClickTrackingURLTemplates: [], trackingEvents: {} }; } function isCompanionAd(ad) { return ad.adType === 'companionAd'; } function createCreative() { let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return { id: creativeAttributes.id || null, adId: creativeAttributes.adId || null, sequence: creativeAttributes.sequence || null, apiFramework: creativeAttributes.apiFramework || null, universalAdIds: [], creativeExtensions: [] }; } function createCreativeCompanion() { let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; const { id, adId, sequence, apiFramework } = createCreative(creativeAttributes); return { id, adId, sequence, apiFramework, type: 'companion', required: null, variations: [] }; } const supportedMacros = ['ADCATEGORIES', 'ADCOUNT', 'ADPLAYHEAD', 'ADSERVINGID', 'ADTYPE', 'APIFRAMEWORKS', 'APPBUNDLE', 'ASSETURI', 'BLOCKEDADCATEGORIES', 'BREAKMAXADLENGTH', 'BREAKMAXADS', 'BREAKMAXDURATION', 'BREAKMINADLENGTH', 'BREAKMINDURATION', 'BREAKPOSITION', 'CLICKPOS', 'CLICKTYPE', 'CLIENTUA', 'CONTENTID', 'CONTENTPLAYHEAD', // @deprecated VAST 4.1 'CONTENTURI', 'DEVICEIP', 'DEVICEUA', 'DOMAIN', 'EXTENSIONS', 'GDPRCONSENT', 'IFA', 'IFATYPE', 'INVENTORYSTATE', 'LATLONG', 'LIMITADTRACKING', 'MEDIAMIME', 'MEDIAPLAYHEAD', 'OMIDPARTNER', 'PAGEURL', 'PLACEMENTTYPE', 'PLAYERCAPABILITIES', 'PLAYERSIZE', 'PLAYERSTATE', 'PODSEQUENCE', 'REGULATIONS', 'SERVERSIDE', 'SERVERUA', 'TRANSACTIONID', 'UNIVERSALADID', 'VASTVERSIONS', 'VERIFICATIONVENDORS']; function track(URLTemplates, macros, options) { const URLs = resolveURLTemplates(URLTemplates, macros, options); URLs.forEach(URL => { if (typeof window !== 'undefined' && window !== null) { const i = new Image(); i.src = URL; } }); } /** * Replace the provided URLTemplates with the given values * * @param {Array} URLTemplates - An array of tracking url templates. * @param {Object} [macros={}] - An optional Object of parameters to be used in the tracking calls. * @param {Object} [options={}] - An optional Object of options to be used in the tracking calls. */ function resolveURLTemplates(URLTemplates) { let macros = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; const resolvedURLs = []; const URLArray = extractURLsFromTemplates(URLTemplates); // Set default value for invalid ERRORCODE if (macros['ERRORCODE'] && !options.isCustomCode && !/^[0-9]{3}$/.test(macros['ERRORCODE'])) { macros['ERRORCODE'] = 900; } // Calc random/time based macros macros['CACHEBUSTING'] = addLeadingZeros(Math.round(Math.random() * 1.0e8)); macros['TIMESTAMP'] = new Date().toISOString(); // RANDOM/random is not defined in VAST 3/4 as a valid macro tho it's used by some adServer (Auditude) macros['RANDOM'] = macros['random'] = macros['CACHEBUSTING']; for (const macro in macros) { macros[macro] = encodeURIComponentRFC3986(macros[macro]); } for (const URLTemplateKey in URLArray) { const resolveURL = URLArray[URLTemplateKey]; if (typeof resolveURL !== 'string') { continue; } resolvedURLs.push(replaceUrlMacros(resolveURL, macros)); } return resolvedURLs; } /** * Replace the macros tracking url with their value. * If no value is provided for a supported macro and it exists in the url, * it will be replaced by -1 as described by the VAST 4.1 iab specifications * * @param {String} url - Tracking url. * @param {Object} macros - Object of macros to be replaced in the tracking calls */ function replaceUrlMacros(url, macros) { url = replaceMacrosValues(url, macros); // match any macros from the url that was not replaced const remainingMacros = url.match(/[^[\]]+(?=])/g); if (!remainingMacros) { return url; } let supportedRemainingMacros = remainingMacros.filter(macro => supportedMacros.indexOf(macro) > -1); if (supportedRemainingMacros.length === 0) { return url; } supportedRemainingMacros = supportedRemainingMacros.reduce((accumulator, macro) => { accumulator[macro] = -1; return accumulator; }, {}); return replaceMacrosValues(url, supportedRemainingMacros); } /** * Replace the macros tracking url with their value. * * @param {String} url - Tracking url. * @param {Object} macros - Object of macros to be replaced in the tracking calls */ function replaceMacrosValues(url, macros) { let replacedMacrosUrl = url; for (const key in macros) { const value = macros[key]; // this will match [${key}] and %%${key}%% and replace it replacedMacrosUrl = replacedMacrosUrl.replace(new RegExp("(?:\\[|%%)(".concat(key, ")(?:\\]|%%)"), 'g'), value); } return replacedMacrosUrl; } /** * Extract the url/s from the URLTemplates. * If the URLTemplates is an array of urls * If the URLTemplates object has a url property * If the URLTemplates is a single string * * @param {Array|String} URLTemplates - An array|string of url templates. */ function extractURLsFromTemplates(URLTemplates) { if (Array.isArray(URLTemplates)) { return URLTemplates.map(URLTemplate => { return URLTemplate && URLTemplate.hasOwnProperty('url') ? URLTemplate.url : URLTemplate; }); } return URLTemplates; } /** * Filter URLTemplates elements . * To be valid, urls should: * - have the same protocol as the client * or * - be protocol-relative urls * * Returns an object with two arrays * - validUrls : An array of valid URLs * - invalidUrls: An array of invalid URLs * * @param {Array} URLTemplates - A Array of string/object containing urls templates. * @returns {Object} * */ function filterUrlTemplates(URLTemplates) { return URLTemplates.reduce((acc, urlTemplate) => { const url = urlTemplate.url || urlTemplate; isValidUrl(url) ? acc.validUrls.push(url) : acc.invalidUrls.push(url); return acc; }, { validUrls: [], invalidUrls: [] }); } function isValidUrl(url) { const regex = /^(https?:\/\/|\/\/)/; return regex.test(url); } /** * Returns a boolean after checking if the object exists in the array. * true - if the object exists, false otherwise * * @param {Object} obj - The object who existence is to be checked. * @param {Array} list - List of objects. */ function containsTemplateObject(obj, list) { for (let i = 0; i < list.length; i++) { if (isTemplateObjectEqual(list[i], obj)) { return true; } } return false; } /** * Returns a boolean after comparing two Template objects. * true - if the objects are equivalent, false otherwise * * @param {Object} obj1 * @param {Object} obj2 */ function isTemplateObjectEqual(obj1, obj2) { if (obj1 && obj2) { const obj1Properties = Object.getOwnPropertyNames(obj1); const obj2Properties = Object.getOwnPropertyNames(obj2); // If number of properties is different, objects are not equivalent if (obj1Properties.length !== obj2Properties.length) { return false; } if (obj1.id !== obj2.id || obj1.url !== obj2.url) { return false; } return true; } return false; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent function encodeURIComponentRFC3986(str) { return encodeURIComponent(str).replace(/[!'()*]/g, c => "%".concat(c.charCodeAt(0).toString(16))); } /** * Return a string of the input number with leading zeros defined by the length param * * @param {Number} input - number to convert * @param {Number} length - length of the desired string * * @return {String} */ function addLeadingZeros(input) { let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 8; return input.toString().padStart(length, '0'); } function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } function flatten(arr) { return arr.reduce((flat, toFlatten) => { return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten); }, []); } /** * Joins two arrays of objects without duplicates * * @param {Array} arr1 * @param {Array} arr2 * * @return {Array} */ function joinArrayOfUniqueTemplateObjs() { let arr1 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; let arr2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; const firstArr = Array.isArray(arr1) ? arr1 : []; const secondArr = Array.isArray(arr2) ? arr2 : []; const arr = firstArr.concat(secondArr); return arr.reduce((res, val) => { if (!containsTemplateObject(val, res)) { res.push(val); } return res; }, []); } /** * Check if a provided value is a valid time value according to the IAB definition * Check if a provided value is a valid time value according to the IAB definition: Must be a positive number or -1. * if not implemented by ad unit or -2 if value is unknown. * @param {Number} time * * @return {Boolean} */ function isValidTimeValue(time) { return Number.isFinite(time) && time >= -2; } /** * Check if we are in a browser environment * @returns {Boolean} */ function isBrowserEnvironment() { return typeof window !== 'undefined'; } function formatMacrosValues(macros) { return typeof macros !== 'object' ? macros : JSON.stringify(macros); } const util = { track, resolveURLTemplates, extractURLsFromTemplates, filterUrlTemplates, containsTemplateObject, isTemplateObjectEqual, encodeURIComponentRFC3986, replaceUrlMacros, isNumeric, flatten, joinArrayOfUniqueTemplateObjs, isValidTimeValue, addLeadingZeros, isValidUrl, isBrowserEnvironment, formatMacrosValues }; /** * This module provides support methods to the parsing classes. */ /** * Returns the first element of the given node which nodeName matches the given name. * @param {Node} node - The node to use to find a match. * @param {String} name - The name to look for. * @return {Object|undefined} */ function childByName(node, name) { const childNodes = Array.from(node.childNodes); return childNodes.find(childNode => childNode.nodeName === name); } /** * Returns all the elements of the given node which nodeName match the given name. * @param {Node} node - The node to use to find the matches. * @param {String} name - The name to look for. * @return {Array} */ function childrenByName(node, name) { const childNodes = Array.from(node.childNodes); return childNodes.filter(childNode => childNode.nodeName === name); } /** * Converts relative vastAdTagUri. * @param {String} vastAdTagUrl - The url to resolve. * @param {String} originalUrl - The original url. * @return {String} */ function resolveVastAdTagURI(vastAdTagUrl, originalUrl) { if (!originalUrl) { return vastAdTagUrl; } if (vastAdTagUrl.startsWith('//')) { const { protocol } = location; return "".concat(protocol).concat(vastAdTagUrl); } if (!vastAdTagUrl.includes('://')) { // Resolve relative URLs (mainly for unit testing) const baseURL = originalUrl.slice(0, originalUrl.lastIndexOf('/')); return "".concat(baseURL, "/").concat(vastAdTagUrl); } return vastAdTagUrl; } /** * Converts a boolean string into a Boolean. * @param {String} booleanString - The boolean string to convert. * @return {Boolean} */ function parseBoolean(booleanString) { return ['true', 'TRUE', 'True', '1'].includes(booleanString); } /** * Parses a node text (for legacy support). * @param {Object} node - The node to parse the text from. * @return {String} */ function parseNodeText(node) { return node && (node.textContent || node.text || '').trim(); } /** * Copies an attribute from a node to another. * @param {String} attributeName - The name of the attribute to clone. * @param {Object} nodeSource - The source node to copy the attribute from. * @param {Object} nodeDestination - The destination node to copy the attribute at. */ function copyNodeAttribute(attributeName, nodeSource, nodeDestination) { const attributeValue = nodeSource.getAttribute(attributeName); if (attributeValue) { nodeDestination.setAttribute(attributeName, attributeValue); } } /** * Converts element attributes into an object, where object key is attribute name * and object value is attribute value * @param {Element} element * @returns {Object} */ function parseAttributes(element) { const nodeAttributes = Array.from(element.attributes); return nodeAttributes.reduce((acc, nodeAttribute) => { acc[nodeAttribute.nodeName] = nodeAttribute.nodeValue; return acc; }, {}); } /** * Parses a String duration into a Number. * @param {String} durationString - The dureation represented as a string. * @return {Number} */ function parseDuration(durationString) { if (durationString === null || typeof durationString === 'undefined') { return -1; } // Some VAST doesn't have an HH:MM:SS duration format but instead jus the number of seconds if (util.isNumeric(durationString)) { return parseInt(durationString); } const durationComponents = durationString.split(':'); if (durationComponents.length !== 3) { return -1; } const secondsAndMS = durationComponents[2].split('.'); let seconds = parseInt(secondsAndMS[0]); if (secondsAndMS.length === 2) { seconds += parseFloat("0.".concat(secondsAndMS[1])); } const minutes = parseInt(durationComponents[1] * 60); const hours = parseInt(durationComponents[0] * 60 * 60); if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) || minutes > 60 * 60 || seconds > 60) { return -1; } return hours + minutes + seconds; } /** * Sorts and filters ads that are part of an Ad Pod. * @param {Array} ads - An array of ad objects. * @returns {Array} An array of sorted ad objects based on the sequence attribute. */ function getSortedAdPods() { let ads = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; return ads.filter(ad => parseInt(ad.sequence, 10)).sort((a, b) => a.sequence - b.sequence); } /** * Filters out AdPods of given ads array and returns only standalone ads without sequence attribute. * @param {Array} ads - An array of ad objects. * @returns {Array} An array of standalone ad. */ function getStandAloneAds() { let ads = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; return ads.filter(ad => !parseInt(ad.sequence, 10)); } /** * Parses the attributes and assign them to object * @param {Object} attributes attribute * @param {Object} verificationObject with properties which can be assigned */ function assignAttributes(attributes, verificationObject) { if (attributes) { Array.from(attributes).forEach(_ref => { let { nodeName, nodeValue } = _ref; if (nodeName && nodeValue && verificationObject.hasOwnProperty(nodeName)) { let value = nodeValue; if (typeof verificationObject[nodeName] === 'boolean') { value = parseBoolean(value); } verificationObject[nodeName] = value; } }); } } /** * Merges the data between an unwrapped ad and his wrapper. * @param {Ad} unwrappedAd - The 'unwrapped' Ad. * @param {Ad} wrapper - The wrapper Ad. * @return {void} */ function mergeWrapperAdData(unwrappedAd, wrapper) { var _wrapper$creatives; unwrappedAd.errorURLTemplates = wrapper.errorURLTemplates.concat(unwrappedAd.errorURLTemplates); unwrappedAd.impressionURLTemplates = wrapper.impressionURLTemplates.concat(unwrappedAd.impressionURLTemplates); unwrappedAd.extensions = wrapper.extensions.concat(unwrappedAd.extensions); if (wrapper.viewableImpression.length > 0) { unwrappedAd.viewableImpression = [...unwrappedAd.viewableImpression, ...wrapper.viewableImpression]; } // values from the child wrapper will be overridden unwrappedAd.followAdditionalWrappers = wrapper.followAdditionalWrappers; unwrappedAd.allowMultipleAds = wrapper.allowMultipleAds; unwrappedAd.fallbackOnNoAd = wrapper.fallbackOnNoAd; const wrapperCompanions = (wrapper.creatives || []).filter(creative => creative && creative.type === 'companion'); const wrapperCompanionClickTracking = wrapperCompanions.reduce((result, creative) => { (creative.variations || []).forEach(variation => { (variation.companionClickTrackingURLTemplates || []).forEach(companionClickTrackingURLTemplate => { if (!util.containsTemplateObject(companionClickTrackingURLTemplate, result)) { result.push(companionClickTrackingURLTemplate); } }); }); return result; }, []); unwrappedAd.creatives = wrapperCompanions.concat(unwrappedAd.creatives); const wrapperHasVideoClickTracking = wrapper.videoClickTrackingURLTemplates && wrapper.videoClickTrackingURLTemplates.length; const wrapperHasVideoCustomClick = wrapper.videoCustomClickURLTemplates && wrapper.videoCustomClickURLTemplates.length; unwrappedAd.creatives.forEach(creative => { // merge tracking events if (wrapper.trackingEvents && wrapper.trackingEvents[creative.type]) { for (const eventName in wrapper.trackingEvents[creative.type]) { const urls = wrapper.trackingEvents[creative.type][eventName]; if (!Array.isArray(creative.trackingEvents[eventName])) { creative.trackingEvents[eventName] = []; } creative.trackingEvents[eventName] = creative.trackingEvents[eventName].concat(urls); } } if (creative.type === 'linear') { // merge video click tracking url if (wrapperHasVideoClickTracking) { creative.videoClickTrackingURLTemplates = creative.videoClickTrackingURLTemplates.concat(wrapper.videoClickTrackingURLTemplates); } // merge video custom click url if (wrapperHasVideoCustomClick) { creative.videoCustomClickURLTemplates = creative.videoCustomClickURLTemplates.concat(wrapper.videoCustomClickURLTemplates); } // VAST 2.0 support - Use Wrapper/linear/clickThrough when Inline/Linear/clickThrough is null if (wrapper.videoClickThroughURLTemplate && (creative.videoClickThroughURLTemplate === null || typeof creative.videoClickThroughURLTemplate === 'undefined')) { creative.videoClickThroughURLTemplate = wrapper.videoClickThroughURLTemplate; } } // pass wrapper companion trackers to all companions if (creative.type === 'companion' && wrapperCompanionClickTracking.length) { (creative.variations || []).forEach(variation => { variation.companionClickTrackingURLTemplates = util.joinArrayOfUniqueTemplateObjs(variation.companionClickTrackingURLTemplates, wrapperCompanionClickTracking); }); } }); if (wrapper.adVerifications) { // As specified by VAST specs unwrapped ads should contains wrapper adVerification script unwrappedAd.adVerifications = unwrappedAd.adVerifications.concat(wrapper.adVerifications); } if (wrapper.blockedAdCategories) { unwrappedAd.blockedAdCategories = unwrappedAd.blockedAdCategories.concat(wrapper.blockedAdCategories); } // Merge Wrapper's creatives containing icon elements if ((_wrapper$creatives = wrapper.creatives) !== null && _wrapper$creatives !== void 0 && _wrapper$creatives.length) { // As specified by VAST specs, wrapper should not contain any mediafiles const wrapperCreativesWithIconsNode = wrapper.creatives.filter(creative => { var _creative$icons; return ((_creative$icons = creative.icons) === null || _creative$icons === void 0 ? void 0 : _creative$icons.length) && !creative.mediaFiles.length; }); if (wrapperCreativesWithIconsNode.length) { unwrappedAd.creatives = unwrappedAd.creatives.concat(wrapperCreativesWithIconsNode); } } } const parserUtils = { childByName, childrenByName, resolveVastAdTagURI, parseBoolean, parseNodeText, copyNodeAttribute, parseAttributes, parseDuration, getStandAloneAds, getSortedAdPods, assignAttributes, mergeWrapperAdData }; /** * This module provides methods to parse a VAST CompanionAd Element. */ /** * Parses a CompanionAd. * @param {Object} creativeElement - The VAST CompanionAd element to parse. * @param {Object} creativeAttributes - The attributes of the CompanionAd (optional). * @return {Object} creative - The creative object. */ function parseCreativeCompanion(creativeElement, creativeAttributes) { const creative = createCreativeCompanion(creativeAttributes); creative.required = creativeElement.getAttribute('required') || null; creative.variations = parserUtils.childrenByName(creativeElement, 'Companion').map(companionResource => { const companionAd = createCompanionAd(parserUtils.parseAttributes(companionResource)); companionAd.htmlResources = parserUtils.childrenByName(companionResource, 'HTMLResource').reduce((urls, resource) => { const url = parserUtils.parseNodeText(resource); return url ? urls.concat(url) : urls; }, []); companionAd.iframeResources = parserUtils.childrenByName(companionResource, 'IFrameResource').reduce((urls, resource) => { const url = parserUtils.parseNodeText(resource); return url ? urls.concat(url) : urls; }, []); companionAd.staticResources = parserUtils.childrenByName(companionResource, 'StaticResource').reduce((urls, resource) => { const url = parserUtils.parseNodeText(resource); return url ? urls.concat({ url, creativeType: resource.getAttribute('creativeType') || null }) : urls; }, []); companionAd.altText = parserUtils.parseNodeText(parserUtils.childByName(companionResource, 'AltText')) || null; const trackingEventsElement = parserUtils.childByName(companionResource, 'TrackingEvents'); if (trackingEventsElement) { parserUtils.childrenByName(trackingEventsElement, 'Tracking').forEach(trackingElement => { const eventName = trackingElement.getAttribute('event'); const trackingURLTemplate = parserUtils.parseNodeText(trackingElement); if (eventName && trackingURLTemplate) { if (!Array.isArray(companionAd.trackingEvents[eventName])) { companionAd.trackingEvents[eventName] = []; } companionAd.trackingEvents[eventName].push(trackingURLTemplate); } }); } companionAd.companionClickTrackingURLTemplates = parserUtils.childrenByName(companionResource, 'CompanionClickTracking').map(clickTrackingElement => { return { id: clickTrackingElement.getAttribute('id') || null, url: parserUtils.parseNodeText(clickTrackingElement) }; }); companionAd.companionClickThroughURLTemplate = parserUtils.parseNodeText(parserUtils.childByName(companionResource, 'CompanionClickThrough')) || null; const adParametersElement = parserUtils.childByName(companionResource, 'AdParameters'); if (adParametersElement) { companionAd.adParameters = { value: parserUtils.parseNodeText(adParametersElement), xmlEncoded: adParametersElement.getAttribute('xmlEncoded') || null }; } return companionAd; }); return creative; } function createCreativeLinear() { let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; const { id, adId, sequence, apiFramework } = createCreative(creativeAttributes); return { id, adId, sequence, apiFramework, type: 'linear', duration: 0, skipDelay: null, mediaFiles: [], mezzanine: null, interactiveCreativeFile: null, closedCaptionFiles: [], videoClickThroughURLTemplate: null, videoClickTrackingURLTemplates: [], videoCustomClickURLTemplates: [], adParameters: null, icons: [], trackingEvents: {} }; } function isCreativeLinear(ad) { return ad.type === 'linear'; } function createClosedCaptionFile() { let closedCaptionAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return { type: closedCaptionAttributes.type || null, language: closedCaptionAttributes.language || null, fileURL: null }; } function createIcon() { return { program: null, height: 0, width: 0, xPosition: 0, yPosition: 0, apiFramework: null, offset: null, duration: 0, type: null, staticResource: null, htmlResource: null, iframeResource: null, pxratio: '1', iconClickThroughURLTemplate: null, iconClickTrackingURLTemplates: [], iconViewTrackingURLTemplate: null, iconClickFallbackImages: [], altText: null, hoverText: null }; } function createInteractiveCreativeFile() { let interactiveCreativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return { type: interactiveCreativeAttributes.type || null, apiFramework: interactiveCreativeAttributes.apiFramework || null, variableDuration: parserUtils.parseBoolean(interactiveCreativeAttributes.variableDuration), fileURL: null }; } function createMediaFile() { return { id: null, fileURL: null, fileSize: 0, deliveryType: 'progressive', mimeType: null, mediaType: null, codec: null, bitrate: 0, minBitrate: 0, maxBitrate: 0, width: 0, height: 0, apiFramework: null, // @deprecated in VAST 4.1. <InteractiveCreativeFile> should be used instead. scalable: null, maintainAspectRatio: null }; } function createMezzanine() { return { id: null, fileURL: null, delivery: null, codec: null, type: null, width: 0, height: 0, fileSize: 0, mediaType: '2D' }; } /** * This module provides methods to parse a VAST Linear Element. */ /** * Parses a Linear element. * @param {Object} creativeElement - The VAST Linear element to parse. * @param {any} creativeAttributes - The attributes of the Linear (optional). * @return {Object} creative - The creativeLinear object. */ function parseCreativeLinear(creativeElement, creativeAttributes) { let offset; const creative = createCreativeLinear(creativeAttributes); creative.duration = parserUtils.parseDuration(parserUtils.parseNodeText(parserUtils.childByName(creativeElement, 'Duration'))); const skipOffset = creativeElement.getAttribute('skipoffset'); if (typeof skipOffset === 'undefined' || skipOffset === null) { creative.skipDelay = null; } else if (skipOffset.charAt(skipOffset.length - 1) === '%' && creative.duration !== -1) { const percent = parseInt(skipOffset, 10); creative.skipDelay = creative.duration * (percent / 100); } else { creative.skipDelay = parserUtils.parseDuration(skipOffset); } const videoClicksElement = parserUtils.childByName(creativeElement, 'VideoClicks'); if (videoClicksElement) { const videoClickThroughElement = parserUtils.childByName(videoClicksElement, 'ClickThrough'); if (videoClickThroughElement) { creative.videoClickThroughURLTemplate = { id: videoClickThroughElement.getAttribute('id') || null, url: parserUtils.parseNodeText(videoClickThroughElement) }; } else { creative.videoClickThroughURLTemplate = null; } parserUtils.childrenByName(videoClicksElement, 'ClickTracking').forEach(clickTrackingElement => { creative.videoClickTrackingURLTemplates.push({ id: clickTrackingElement.getAttribute('id') || null, url: parserUtils.parseNodeText(clickTrackingElement) }); }); parserUtils.childrenByName(videoClicksElement, 'CustomClick').forEach(customClickElement => { creative.videoCustomClickURLTemplates.push({ id: customClickElement.getAttribute('id') || null, url: parserUtils.parseNodeText(customClickElement) }); }); } const adParamsElement = parserUtils.childByName(creativeElement, 'AdParameters'); if (adParamsElement) { creative.adParameters = { value: parserUtils.parseNodeText(adParamsElement), xmlEncoded: adParamsElement.getAttribute('xmlEncoded') || null }; } parserUtils.childrenByName(creativeElement, 'TrackingEvents').forEach(trackingEventsElement => { parserUtils.childrenByName(trackingEventsElement, 'Tracking').forEach(trackingElement => { let eventName = trackingElement.getAttribute('event'); const trackingURLTemplate = parserUtils.parseNodeText(trackingElement); if (eventName && trackingURLTemplate) { if (eventName === 'progress') { offset = trackingElement.getAttribute('offset'); if (!offset) { return; } if (offset.charAt(offset.length - 1) === '%') { eventName = "progress-".concat(offset); } else { eventName = "progress-".concat(parserUtils.parseDuration(offset)); } } if (!Array.isArray(creative.trackingEvents[eventName])) { creative.trackingEvents[eventName] = []; } creative.trackingEvents[eventName].push(trackingURLTemplate); } }); }); parserUtils.childrenByName(creativeElement, 'MediaFiles').forEach(mediaFilesElement => { parserUtils.childrenByName(mediaFilesElement, 'MediaFile').forEach(mediaFileElement => { creative.mediaFiles.push(parseMediaFile(mediaFileElement)); }); const interactiveCreativeElement = parserUtils.childByName(mediaFilesElement, 'InteractiveCreativeFile'); if (interactiveCreativeElement) { creative.interactiveCreativeFile = parseInteractiveCreativeFile(interactiveCreativeElement); } const closedCaptionElements = parserUtils.childByName(mediaFilesElement, 'ClosedCaptionFiles'); if (closedCaptionElements) { parserUtils.childrenByName(closedCaptionElements, 'ClosedCaptionFile').forEach(closedCaptionElement => { const closedCaptionFile = createClosedCaptionFile(parserUtils.parseAttributes(closedCaptionElement)); closedCaptionFile.fileURL = parserUtils.parseNodeText(closedCaptionElement); creative.closedCaptionFiles.push(closedCaptionFile); }); } const mezzanineElement = parserUtils.childByName(mediaFilesElement, 'Mezzanine'); const requiredAttributes = getRequiredAttributes(mezzanineElement, ['delivery', 'type', 'width', 'height']); if (requiredAttributes) { const mezzanine = createMezzanine(); mezzanine.id = mezzanineElement.getAttribute('id'); mezzanine.fileURL = parserUtils.parseNodeText(mezzanineElement); mezzanine.delivery = requiredAttributes.delivery; mezzanine.codec = mezzanineElement.getAttribute('codec'); mezzanine.type = requiredAttributes.type; mezzanine.width = parseInt(requiredAttributes.width, 10); mezzanine.height = parseInt(requiredAttributes.height, 10); mezzanine.fileSize = parseInt(mezzanineElement.getAttribute('fileSize'), 10); mezzanine.mediaType = mezzanineElement.getAttribute('mediaType') || '2D'; creative.mezzanine = mezzanine; } }); const iconsElement = parserUtils.childByName(creativeElement, 'Icons'); if (iconsElement) { parserUtils.childrenByName(iconsElement, 'Icon').forEach(iconElement => { creative.icons.push(parseIcon(iconElement)); }); } return creative; } /** * Parses the MediaFile element from VAST. * @param {Object} mediaFileElement - The VAST MediaFile element. * @return {Object} - Parsed mediaFile object. */ function parseMediaFile(mediaFileElement) { const mediaFile = createMediaFile(); mediaFile.id = mediaFileElement.getAttribute('id'); mediaFile.fileURL = parserUtils.parseNodeText(mediaFileElement); mediaFile.deliveryType = mediaFileElement.getAttribute('delivery'); mediaFile.codec = mediaFileElement.getAttribute('codec'); mediaFile.mimeType = mediaFileElement.getAttribute('type'); mediaFile.mediaType = mediaFileElement.getAttribute('mediaType') || '2D'; mediaFile.apiFramework = mediaFileElement.getAttribute('apiFramework'); mediaFile.fileSize = parseInt(mediaFileElement.getAttribute('fileSize') || 0); mediaFile.bitrate = parseInt(mediaFileElement.getAttribute('bitrate') || 0); mediaFile.minBitrate = parseInt(mediaFileElement.getAttribute('minBitrate') || 0); mediaFile.maxBitrate = parseInt(mediaFileElement.getAttribute('maxBitrate') || 0); mediaFile.width = parseInt(mediaFileElement.getAttribute('width') || 0); mediaFile.height = parseInt(mediaFileElement.getAttribute('height') || 0); const scalable = mediaFileElement.getAttribute('scalable'); if (scalable && typeof scalable === 'string') { mediaFile.scalable = parserUtils.parseBoolean(scalable); } const maintainAspectRatio = mediaFileElement.getAttribute('maintainAspectRatio'); if (maintainAspectRatio && typeof maintainAspectRatio === 'string') { mediaFile.maintainAspectRatio = parserUtils.parseBoolean(maintainAspectRatio); } return mediaFile; } /** * Parses the InteractiveCreativeFile element from VAST MediaFiles node. * @param {Object} interactiveCreativeElement - The VAST InteractiveCreativeFile element. * @return {Object} - Parsed interactiveCreativeFile object. */ function parseInteractiveCreativeFile(interactiveCreativeElement) { const interactiveCreativeFile = createInteractiveCreativeFile(parserUtils.parseAttributes(interactiveCreativeElement)); interactiveCreativeFile.fileURL = parserUtils.parseNodeText(interactiveCreativeElement); return interactiveCreativeFile; } /** * Parses the Icon element from VAST. * @param {Object} iconElement - The VAST Icon element. * @return {Object} - Parsed icon object. */ function parseIcon(iconElement) { const icon = createIcon(); icon.program = iconElement.getAttribute('program'); icon.height = parseInt(iconElement.getAttribute('height') || 0); icon.width = parseInt(iconElement.getAttribute('width') || 0); icon.xPosition = parseXPosition(iconElement.getAttribute('xPosition')); icon.yPosition = parseYPosition(iconElement.getAttribute('yPosition')); icon.apiFramework = iconElement.getAttribute('apiFramework'); icon.pxratio = iconElement.getAttribute('pxratio') || '1'; icon.offset = parserUtils.parseDuration(iconElement.getAttribute('offset')); icon.duration = parserUtils.parseDuration(iconElement.getAttribute('duration')); icon.altText = iconElement.getAttribute('altText'); icon.hoverText = iconElement.getAttribute('hoverText'); parserUtils.childrenByName(iconElement, 'HTMLResource').forEach(htmlElement => { icon.type = htmlElement.getAttribute('creativeType') || 'text/html'; icon.htmlResource = parserUtils.parseNodeText(htmlElement); }); parserUtils.childrenByName(iconElement, 'IFrameResource').forEach(iframeElement => { icon.type = iframeElement.getAttribute('creativeType') || 0; icon.iframeResource = parserUtils.parseNodeText(iframeElement); }); parserUtils.childrenByName(iconElement, 'StaticResource').forEach(staticElement => { icon.type = staticElement.getAttribute('creativeType') || 0; icon.staticResource = parserUtils.parseNodeText(staticElement); }); const iconClicksElement = parserUtils.childByName(iconElement, 'IconClicks'); if (iconClicksElement) { icon.iconClickThroughURLTemplate = parserUtils.parseNodeText(parserUtils.childByName(iconClicksElement, 'IconClickThrough')); parserUtils.childrenByName(iconClicksElement, 'IconClickTracking').forEach(iconClickTrackingElement => { icon.iconClickTrackingURLTemplates.push({ id: iconClickTrackingElement.getAttribute('id') || null, url: parserUtils.parseNodeText(iconClickTrackingElement) }); }); const iconClickFallbackImagesElement = parserUtils.childByName(iconClicksElement, 'IconClickFallbackImages'); if (iconClickFallbackImagesElement) { parserUtils.childrenByName(iconClickFallbackImagesElement, 'IconClickFallbackImage').forEach(iconClickFallbackImageElement => { icon.iconClickFallbackImages.push({ url: parserUtils.parseNodeText(iconClickFallbackImageElement) || null, width: iconClickFallbackImageElement.getAttribute('width') || null, height: iconClickFallbackImageElement.getAttribute('height') || null }); }); } } icon.iconViewTrackingURLTemplate = parserUtils.parseNodeText(parserUtils.childByName(iconElement, 'IconViewTracking')); return icon; } /** * Parses an horizontal position into a String ('left' or 'right') or into a Number. * @param {String} xPosition - The x position to parse. * @return {String|Number} */ function parseXPosition(xPosition) { if (['left', 'right'].indexOf(xPosition) !== -1) { return xPosition; } return parseInt(xPosition || 0); } /** * Parses an vertical position into a String ('top' or 'bottom') or into a Number. * @param {String} yPosition - The x position to parse. * @return {String|Number} */ function parseYPosition(yPosition) { if (['top', 'bottom'].indexOf(yPosition) !== -1) { return yPosition; } return parseInt(yPosition || 0); } /** * Getting required attributes from element * @param {Object} element - DOM element * @param {Array} attributes - list of attributes * @return {Object|null} null if a least one element not present */ function getRequiredAttributes(element, attributes) { const values = {}; let error = false; attributes.forEach(name => { if (!element || !element.getAttribute(name)) { error = true; } else { values[name] = element.getAttribute(name); } }); return error ? null : values; } function createCreativeNonLinear() { let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; const { id, adId, sequence, apiFramework } = createCreative(creativeAttributes); return { id, adId, sequence, apiFramework, type: 'nonlinear', variations: [], trackingEvents: {} }; } function createNonLinearAd() { return { id: null, width: 0, height: 0, expandedWidth: 0, expandedHeight: 0, scalable: true, maintainAspectRatio: true, minSuggestedDuration: 0, apiFramework: 'static', adType: 'nonLinearAd', type: null, staticResource: null, htmlResource: null, iframeResource: null, nonlinearClickThroughURLTemplate: null, nonlinearClickTrackingURLTemplates: [], adParameters: null }; } function isNonLinearAd(ad) { return ad.adType === 'nonLinearAd'; } /** * This module provides methods to parse a VAST NonLinear Element. */ /** * Parses a NonLinear element. * @param {any} creativeElement - The VAST NonLinear element to parse. * @param {any} creativeAttributes - The attributes of the NonLinear (optional). * @return {Object} creative - The CreativeNonLinear object. */ function parseCreativeNonLinear(creativeElement, creativeAttributes) { const creative = createCreativeNonLinear(creativeAttributes); parserUtils.childrenByName(creativeElement, 'TrackingEvents').forEach(trackingEventsElement => { let eventName, trackingURLTemplate; parserUtils.childrenByName(trackingEventsElement, 'Tracking').forEach(trackingElement => { eventName = trackingElement.getAttribute('event'); trackingURLTemplate = parserUtils.parseNodeText(trackingElement); if (eventName && trackingURLTemplate) { if (!Array.isArray(creative.trackingEvents[eventName])) { creative.trackingEvents[eventName] = []; } creative.trackingEvents[eventName].push(trackingURLTemplate); } }); }); parserUtils.childrenByName(creativeElement, 'NonLinear').forEach(nonlinearResource => { const nonlinearAd = createNonLinearAd(); nonlinearAd.id = nonlinearResource.getAttribute('id') || null; nonlinearAd.width = nonlinearResource.getAttribute('width'); nonlinearAd.height = nonlinearResource.getAttribute('height'); nonlinearAd.expandedWidth = nonlinearResource.getAttribute('expandedWidth'); nonlinearAd.expandedHeight = nonlinearResource.getAttribute('expandedHeight'); nonlinearAd.scalable = parserUtils.parseBoolean(nonlinearResource.getAttribute('scalable')); nonlinearAd.maintainAspectRatio = parserUtils.parseBoolean(nonlinearResource.getAttribute('maintainAspectRatio')); nonlinearAd.minSuggestedDuration = parserUtils.parseDuration(nonlinearResource.getAttribute('minSuggestedDuration')); nonlinearAd.apiFramework = nonlinearResource.getAttribute('apiFramework'); parserUtils.childrenByName(nonlinearResource, 'HTMLResource').forEach(htmlElement => { nonlinearAd.type = htmlElement.getAttribute('creativeType') || 'text/html'; nonlinearAd.htmlResource = parserUtils.parseNodeText(htmlElement); }); parserUtils.childrenByName(nonlinearResource, 'IFrameResource').forEach(iframeElement => { nonlinearAd.type = iframeElement.getAttribute('creativeType') || 0; nonlinearAd.iframeResource = parserUtils.parseNodeText(iframeElement); }); parserUtils.childrenByName(nonlinearResource, 'StaticResource').forEach(staticElement => { nonlinearAd.type = staticElement.getAttribute('creativeType') || 0; nonlinearAd.staticResource = parserUtils.parseNodeText(staticElement); }); const adParamsElement = parserUtils.childByName(nonlinearResource, 'AdParameters'); if (adParamsElement) { nonlinearAd.adParameters = { value: parserUtils.parseNodeText(adParamsElement), xmlEncoded: adParamsElement.getAttribute('xmlEncoded') || null }; } nonlinearAd.nonlinearClickThroughURLTemplate = parserUtils.parseNodeText(parserUtils.childByName(nonlinearResource, 'NonLinearClickThrough')); parserUtils.childrenByName(nonlinearResource, 'NonLinearClickTracking').forEach(clickTrackingElement => { nonlinearAd.nonlinearClickTrackingURLTemplates.push({ id: clickTrackingElement.getAttribute('id') || null, url: parserUtils.parseNodeText(clickTrackingElement) }); }); creative.variations.push(nonlinearAd); }); return creative; } function createExtension() { return { name: null, value: null, attributes: {}, children: [] }; } function isEmptyExtension(extension) { return extension.value === null && Object.keys(extension.attributes).length === 0 && extension.children.length === 0; } /** * Parses an array of Extension elements. * @param {Node[]} extensions - The array of extensions to parse. * @param {String} type - The type of extensions to parse.(Ad|Creative) * @return {AdExtension[]|CreativeExtension[]} - The nodes parsed to extensions */ function parseExtensions(extensions) { const exts = []; extensions.forEach(extNode => { const ext = _parseExtension(extNode); if (ext) { exts.push(ext); } }); return exts; } /** * Parses an extension child node * @param {Node} extNode - The extension node to parse * @return {AdExtension|CreativeExtension|null} - The node parsed to extension */ function _parseExtension(extNode) { // Ignore comments if (extNode.nodeName === '#comment') return null; const ext = createExtension(); const extNodeAttrs = extNode.attributes; const childNodes = extNode.childNodes; ext.name = extNode.nodeName; // Parse attributes if (extNode.attributes) { for (const extNodeAttrKey in extNodeAttrs) { if (extNodeAttrs.hasOwnProperty(extNodeAttrKey)) { const extNodeAttr = extNodeAttrs[extNodeAttrKey]; if (extNodeAttr.nodeName && extNodeAttr.nodeValue) { ext.attributes[extNodeAttr.nodeName] = extNodeAttr.nodeValue; } } } } // Parse all children for (const childNodeKey in childNodes) { if (childNodes.hasOwnProperty(childNodeKey)) { const parsedChild = _parseExtension(childNodes[childNodeKey]); if (parsedChild) { ext.children.push(parsedChild); } } } /* Only parse value of Nodes with only eather no children or only a cdata or text to avoid useless parsing that would result to a concatenation of all children */ if (ext.children.length === 0 || ext.children.length === 1 && ['#cdata-section', '#text'].indexOf(ext.children[0].name) >= 0) { const txt = parserUtils.parseNodeText(extNode); if (txt !== '') { ext.value = txt; } // Remove the children if it's a cdata or simply text to avoid useless children ext.children = []; } // Only return not empty objects to not pollute extentions return isEmptyExtension(ext) ? null : ext; } /** * Parses the creatives from the Creatives Node. * @param {any} creativeNodes - The creative nodes to parse. * @return {Array<Creative>} - An array of Creative objects. */ function parseCreatives(creativeNodes) { const creatives = []; creativeNodes.forEach(creativeElement => { const creativeAttributes = { id: creativeElement.getAttribute('id') || null, adId: parseCreativeAdIdAttribute(creativeElement), sequence: creativeElement.getAttribute('sequence') || null, apiFramework: creativeElement.getAttribute('apiFramework') || null }; const universalAdIds = []; const universalAdIdElements = parserUtils.childrenByName(creativeElement, 'UniversalAdId'); universalAdIdElements.forEach(universalAdIdElement => { const universalAdId = { idRegistry: universalAdIdElement.getAttribute('idRegistry') || 'unknown', value: parserUtils.parseNodeText(universalAdIdElement) }; universalAdIds.push(universalAdId); }); let creativeExtensions; const creativeExtensionsElement = parserUtils.childByName(creativeElement, 'CreativeExtensions'); if (creativeExtensionsElement) { creativeExtensions = parseExtensions(parserUtils.childrenByName(creativeExtensionsElement, 'CreativeExtension')); } for (const creativeTypeElementKey in creativeElement.childNodes) { const creativeTypeElement = creativeElement.childNodes[creativeTypeElementKey]; let parsedCreative; switch (creativeTypeElement.nodeName) { case 'Linear': parsedCreative = parseCreativeLinear(creativeTypeElement, creativeAttributes); break; case 'NonLinearAds': parsedCreative = parseCreativeNonLinear(creativeTypeElement, creativeAttributes); break; case 'CompanionAds': parsedCreative = parseCreativeCompanion(creativeTypeElement, creativeAttributes); break; } if (parsedCreative) { if (universalAdIds) { parsedCreative.universalAdIds = universalAdIds; } if (creativeExtensions) { parsedCreative.creativeExtensions = creativeExtensions; } creatives.push(parsedCreative); } } }); return creatives; } /** * Parses the creative adId Attribute. * @param {any} creativeElement - The creative element to retrieve the adId from. * @return {String|null} */ function parseCreativeAdIdAttribute(creativeElement) { return creativeElement.getAttribute('AdID') || // VAST 2 spec creativeElement.getAttribute('adID') || // VAST 3 spec creativeElement.getAttribute('adId') || // VAST 4 spec null; } const requiredValues = { Wrapper: { subElements: ['VASTAdTagURI', 'Impression'] }, BlockedAdCategories: { attributes: ['authority'] }, InLine: { subElements: ['AdSystem', 'AdTitle', 'Impression', 'AdServingId', 'Creatives'] }, Category: { attributes: ['authority'] }, Pricing: { attributes: ['model', 'currency'] }, Verification: { oneOfinLineResources: ['JavaScriptResource', 'ExecutableResource'], attributes: ['vendor'] }, UniversalAdId: { attributes: ['idRegistry'] }, JavaScriptResource: { attributes: ['apiFramework', 'browserOptional'] }, ExecutableResource: { attributes: ['apiFramework', 'type'] }, Tracking: { attributes: ['event'] }, Creatives: { subElements: ['Creative'] }, Creative: { subElements: ['UniversalAdId'] }, Linear: { subElements: ['MediaFiles', 'D