/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
3p/3p.js
306 строк
8 KB
Shihua Zheng
commit (#39097)
06 июн 2023, 02:37
Не верифицирован
06 июн 2023, 02:37
649e4ac
Код
Авторство
О чём код?
/** * @fileoverview Utility functions for scripts running inside of a third * party iframe. */ // Note: loaded by 3p system. Cannot rely on babel polyfills. import {rethrowAsync} from '#core/error'; import {isArray} from '#core/types'; import {hasOwn, map} from '#core/types/object'; import {devAssert, userAssert} from '#utils/log'; /** @typedef {function(!Window, !Object)} */ let ThirdPartyFunctionDef; /** * @const {!{[key: string]: ThirdPartyFunctionDef}} * @visibleForTesting */ let registrations; /** @type {number} */ let syncScriptLoads = 0; /** * Returns the registration map * @return {*} TODO(#23582): Specify return type */ export function getRegistrations() { if (!registrations) { registrations = map(); } return registrations; } /** * @param {string} id The specific 3p integration. * @param {ThirdPartyFunctionDef} draw Function that draws the 3p integration. */ export function register(id, draw) { const registrations = getRegistrations(); devAssert(!registrations[id], 'Double registration %s', id); registrations[id] = draw; } /** * Execute the 3p integration with the given id. * @param {string} id * @param {!Window} win * @param {!Object} data */ export function run(id, win, data) { const fn = registrations[id]; userAssert(fn, 'Unknown 3p: ' + id); fn(win, data); } /** * Synchronously load the given script URL. Only use this if you need a sync * load. Otherwise use {@link loadScript}. * Supports taking a callback that will be called synchronously after the given * script was executed. * @param {!Window} win * @param {string} url * @param {function()=} opt_cb */ export function writeScript(win, url, opt_cb) { win.document.write( // eslint-disable-next-line no-useless-concat '<' + 'script src="' + encodeURI(url) + '"><' + '/script>' ); if (opt_cb) { executeAfterWriteScript(win, opt_cb); } } /** * Asynchronously load the given script URL. * @param {!Window} win * @param {string} url * @param {function()=} opt_cb * @param {function()=} opt_errorCb */ export function loadScript(win, url, opt_cb, opt_errorCb) { /** @const {!Element} */ const s = win.document.createElement('script'); s.src = url; if (opt_cb) { s.onload = opt_cb; } if (opt_errorCb) { s.onerror = opt_errorCb; } win.document.body.appendChild(s); } /** * Call function in micro task or timeout as a fallback. * This is a lightweight helper, because we cannot guarantee that * Promises are available inside the 3p frame. * @param {!Window} win * @param {function()} fn */ export function nextTick(win, fn) { const P = win.Promise; if (P) { P.resolve()./*OK*/ then(fn); } else { win.setTimeout(fn, 0); } } /** * Run the function after all currently waiting sync scripts have been * executed. * @param {!Window} win * @param {function()} fn */ function executeAfterWriteScript(win, fn) { const index = syncScriptLoads++; win['__runScript' + index] = fn; // eslint-disable-next-line no-useless-concat win.document.write('<' + 'script>__runScript' + index + '()<' + '/script>'); } /** * Throws if the given src doesn't start with prefix(es). * @param {!Array<string>|string} prefix * @param {string} src */ export function validateSrcPrefix(prefix, src) { if (!isArray(prefix)) { prefix = [prefix]; } if (src !== undefined) { for (let p = 0; p < prefix.length; p++) { const protocolIndex = src.indexOf(prefix[p]); if (protocolIndex == 0) { return; } } } throw new Error('Invalid src ' + src); } /** * Throws if the given src doesn't contain the string * @param {string} string * @param {string} src */ export function validateSrcContains(string, src) { if (src.indexOf(string) === -1) { throw new Error('Invalid src ' + src); } } /** * Utility function to perform a potentially asynchronous task * exactly once for all frames of a given type and the provide the respective * value to all frames. * @param {!Window} global Your window * @param {string} taskId Must be not conflict with any other global variable * you use. Must be the same for all callers from all frames that want * the same result. * @param {function(function(*))} work Function implementing the work that * is to be done. Receives a second function that should be called with * the result when the work is done. * @param {function(*)} cb Callback function that is called when the work is * done. The first argument is the result. */ export function computeInMasterFrame(global, taskId, work, cb) { const {master} = global.context; let tasks = master.__ampMasterTasks; if (!tasks) { tasks = master.__ampMasterTasks = {}; } let cbs = tasks[taskId]; if (!tasks[taskId]) { cbs = tasks[taskId] = []; } cbs.push(cb); if (!global.context.isMaster) { return; // Only do work in master. } work((result) => { for (let i = 0; i < cbs.length; i++) { cbs[i].call(null, result); } tasks[taskId] = { push(cb) { cb(result); }, }; }); } /** * Validates given data. Throws an exception if the data does not * contains a mandatory field. If called with the optional param * opt_optionalFields, it also validates that the data contains no fields other * than mandatory and optional fields. * * Mandatory fields also accept a string Array as an item. All items in that * array are considered as alternatives to each other. So the validation checks * that the data contains exactly one of those alternatives. * * @param {!Object} data * @param {!Array<string|!Array<string>>} mandatoryFields * @param {Array<string>=} opt_optionalFields */ export function validateData(data, mandatoryFields, opt_optionalFields) { let allowedFields = opt_optionalFields || []; for (let i = 0; i < mandatoryFields.length; i++) { const field = mandatoryFields[i]; if (Array.isArray(field)) { validateExactlyOne(data, field); allowedFields = allowedFields.concat(field); } else { userAssert( // Allow zero values for height, width etc. data[field] != null, 'Missing attribute for %s: %s.', data.type, field ); allowedFields.push(field); } } if (opt_optionalFields) { validateAllowedFields(data, allowedFields); } } /** * Throws an exception if data does not contains exactly one field * mentioned in the alternativeFields array. * @param {!Object} data * @param {!Array<string>} alternativeFields */ function validateExactlyOne(data, alternativeFields) { userAssert( alternativeFields.filter((field) => data[field]).length === 1, '%s must contain exactly one of attributes: %s.', data.type, alternativeFields.join(', ') ); } /** * Throws a non-interrupting exception if data contains a field not supported * by this embed type. * @param {!Object} data * @param {!Array<string>} allowedFields */ function validateAllowedFields(data, allowedFields) { const defaultAvailableFields = { width: true, height: true, type: true, referrer: true, canonicalUrl: true, pageViewId: true, location: true, mode: true, consentNotificationId: true, blockOnConsent: true, ampSlotIndex: true, adHolderText: true, loadingStrategy: true, htmlAccessAllowed: true, adContainerId: true, }; for (const field in data) { if (!hasOwn(data, field) || field in defaultAvailableFields) { continue; } if (allowedFields.indexOf(field) < 0) { // Throw in a timeout, because we do not want to interrupt execution, // because that would make each removal an instant backward incompatible // change. rethrowAsync(new Error(`Unknown attribute for ${data.type}: ${field}.`)); } } } /** @private {!{[key: string]: boolean}} */ let experimentToggles = {}; /** * Returns true if an experiment is enabled. * @param {string} experimentId * @return {boolean} */ export function isExperimentOn(experimentId) { return experimentToggles && !!experimentToggles[experimentId]; } /** * Set experiment toggles. * @param {!{[key: string]: boolean}} toggles */ export function setExperimentToggles(toggles) { experimentToggles = toggles; }