/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/iframe-helper.js
604 строки
17 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
import {deserializeMessage, isAmpMessage} from '#core/3p-frame-messaging'; import {addAttributesToElement} from '#core/dom'; import {closestAncestorElementBySelector} from '#core/dom/query'; import {setStyle} from '#core/dom/style'; import {remove} from '#core/types/array'; import {tryParseJson} from '#core/types/object/json'; import {getData} from '#utils/event-helper'; import {dev, devAssert} from '#utils/log'; import {parseUrlDeprecated} from './url'; /** * Sentinel used to force unlistening after a iframe is detached. * @type {string} */ const UNLISTEN_SENTINEL = 'unlisten'; /** * @typedef {{ * frame: !Element, * events: !{[key: string]: !Array<function(!JsonObject)}> * }} */ let WindowEventsDef; /** * Returns a mapping from a URL's origin to an array of windows and their * listenFor listeners. * @param {?Window} parentWin the window that created the iframe * @param {boolean=} opt_create create the mapping if it does not exist * @return {?{[key: string]: !Array<!WindowEventsDef>}} */ function getListenFors(parentWin, opt_create) { let {listeningFors} = parentWin; if (!listeningFors && opt_create) { listeningFors = parentWin.listeningFors = Object.create(null); } return listeningFors || null; } /** * Returns an array of WindowEventsDef that have had any listenFor listeners * registered for this sentinel. * @param {?Window} parentWin the window that created the iframe * @param {string} sentinel the sentinel of the message * @param {boolean=} opt_create create the array if it does not exist * @return {?Array<!WindowEventsDef>} */ function getListenForSentinel(parentWin, sentinel, opt_create) { const listeningFors = getListenFors(parentWin, opt_create); if (!listeningFors) { return listeningFors; } let listenSentinel = listeningFors[sentinel]; if (!listenSentinel && opt_create) { listenSentinel = listeningFors[sentinel] = []; } return listenSentinel || null; } /** * Returns an mapping of event names to listenFor listeners. * @param {?Window} parentWin the window that created the iframe * @param {!Element} iframe the iframe element who's context will trigger the * event * @param {boolean=} opt_is3P set to true if the iframe is 3p. * @return {?{[key: string]: !Array<function(!JsonObject, !Window, string, !MessageEvent)>}} */ function getOrCreateListenForEvents(parentWin, iframe, opt_is3P) { const sentinel = getSentinel_(iframe, opt_is3P); const listenSentinel = getListenForSentinel(parentWin, sentinel, true); let windowEvents; for (let i = 0; i < listenSentinel.length; i++) { const we = listenSentinel[i]; if (we.frame === iframe) { windowEvents = we; break; } } if (!windowEvents) { windowEvents = { frame: iframe, events: Object.create(null), }; listenSentinel.push(windowEvents); } return windowEvents.events; } /** * Returns an mapping of event names to listenFor listeners. * @param {?Window} parentWin the window that created the iframe * @param {string} sentinel the sentinel of the message * @param {string} origin the source window's origin * @param {?Window} triggerWin the window that triggered the event * @return {?{[key: string]: !Array<function(!JsonObject, !Window, string, !MessageEvent)>}} */ function getListenForEvents(parentWin, sentinel, origin, triggerWin) { const listenSentinel = getListenForSentinel(parentWin, sentinel); if (!listenSentinel) { return listenSentinel; } // Find the entry for the frame. // TODO(@nekodo): Add a WeakMap<Window, WindowEventsDef> cache to // speed up this process. let windowEvents; for (let i = 0; i < listenSentinel.length; i++) { const we = listenSentinel[i]; const {contentWindow} = we.frame; if (!contentWindow) { setTimeout(dropListenSentinel, 0, listenSentinel); } else if ( triggerWin == contentWindow || isDescendantWindow(contentWindow, triggerWin) ) { // 3p code path, we may accept messages from nested frames. windowEvents = we; break; } } return windowEvents ? windowEvents.events : null; } /** * Checks whether one window is a descendant of another by climbing * the parent chain. * @param {?Window} ancestor potential ancestor window * @param {?Window} descendant potential descendant window * @return {boolean} */ function isDescendantWindow(ancestor, descendant) { for (let win = descendant; win && win != win.parent; win = win.parent) { if (win == ancestor) { return true; } } return false; } /** * Removes any listenFors registered on listenSentinel that do not have * a contentWindow (the frame was removed from the DOM tree). * @param {!Array<!WindowEventsDef>} listenSentinel */ function dropListenSentinel(listenSentinel) { const noopData = {'sentinel': UNLISTEN_SENTINEL}; for (let i = listenSentinel.length - 1; i >= 0; i--) { const windowEvents = listenSentinel[i]; if (!windowEvents.frame.contentWindow) { listenSentinel.splice(i, 1); const {events} = windowEvents; for (const name in events) { // Splice here, so that each unlisten does not shift the array events[name].splice(0, Infinity).forEach((event) => { event(noopData); }); } } } } /** * Registers the global listenFor event listener if it has yet to be. * @param {?Window} parentWin */ function registerGlobalListenerIfNeeded(parentWin) { if (parentWin.listeningFors) { return; } const listenForListener = function (event) { if (!getData(event)) { return; } const data = parseIfNeeded(getData(event)); if (!data || !data['sentinel']) { return; } const listenForEvents = getListenForEvents( parentWin, data['sentinel'], event.origin, event.source ); if (!listenForEvents) { return; } let listeners = listenForEvents[data['type']]; if (!listeners) { return; } // We slice to avoid issues with adding another listener or unlistening // during iteration. We could move to a Doubly Linked List with // backtracking, but that's overly complicated. listeners = listeners.slice(); for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(data, event.source, event.origin, event); } }; parentWin.addEventListener('message', listenForListener); } /** * Allows listening for message from the iframe. Returns an unlisten * function to remove the listener. * * @param {?Element} iframe * @param {string} typeOfMessage * @param {?function(!JsonObject, !Window, string, !MessageEvent)} callback Called when a * message of this type arrives for this iframe. * @param {boolean=} opt_is3P set to true if the iframe is 3p. * @param {boolean=} opt_includingNestedWindows set to true if messages from * nested frames should also be accepted. * @param {boolean=} opt_allowOpaqueOrigin set to true if messages from opaque origins (origin == null) are allowed. * @return {!UnlistenDef} */ export function listenFor( iframe, typeOfMessage, callback, opt_is3P, opt_includingNestedWindows, opt_allowOpaqueOrigin ) { devAssert(iframe.src, 'only iframes with src supported'); devAssert( !iframe.parentNode, 'cannot register events on an attached ' + 'iframe. It will cause hair-pulling bugs like #2942' ); devAssert(callback); const parentWin = iframe.ownerDocument.defaultView; registerGlobalListenerIfNeeded(parentWin); const listenForEvents = getOrCreateListenForEvents( parentWin, iframe, opt_is3P ); const iframeOrigin = parseUrlDeprecated(iframe.src).origin; let events = listenForEvents[typeOfMessage] || (listenForEvents[typeOfMessage] = []); let unlisten; let listener = function (data, source, origin, event) { const sentinel = data['sentinel']; // Exclude messages that don't satisfy amp sentinel rules. if (sentinel == 'amp') { // For `amp` sentinel, nested windows are not allowed if (source != iframe.contentWindow) { return; } // For `amp` sentinel origin must match unless opaque origin is allowed const isOpaqueAndAllowed = origin == 'null' && opt_allowOpaqueOrigin; if (iframeOrigin != origin && !isOpaqueAndAllowed) { return; } } // Exclude nested frames if necessary. // Note that the source was already verified to be either the contentWindow // of the iframe itself or a descendant window within it. if (!opt_includingNestedWindows && source != iframe.contentWindow) { return; } if (data.sentinel == UNLISTEN_SENTINEL) { unlisten(); return; } callback(data, source, origin, event); }; events.push(listener); return (unlisten = function () { if (listener) { const index = events.indexOf(listener); if (index > -1) { events.splice(index, 1); } // Make sure references to the unlisten function do not keep // alive too much. listener = null; events = null; callback = null; } }); } /** * Returns a promise that resolves when one of given messages has been observed * for the first time. And remove listener for all other messages. * @param {!Element} iframe * @param {string|!Array<string>} typeOfMessages * @param {boolean=} opt_is3P * @return {!Promise<!{data: !JsonObject, source: !Window, origin: string, event: !MessageEvent}>} */ export function listenForOncePromise(iframe, typeOfMessages, opt_is3P) { const unlistenList = []; if (typeof typeOfMessages == 'string') { typeOfMessages = [typeOfMessages]; } return new Promise((resolve) => { for (let i = 0; i < typeOfMessages.length; i++) { const message = typeOfMessages[i]; const unlisten = listenFor( iframe, message, (data, source, origin, event) => { for (let i = 0; i < unlistenList.length; i++) { unlistenList[i](); } resolve({data, source, origin, event}); }, opt_is3P ); unlistenList.push(unlisten); } }); } /** * Posts a message to the iframe. * @param {!Element} iframe The iframe. * @param {string} type Type of the message. * @param {!JsonObject} object Message payload. * @param {string} targetOrigin origin of the target. * @param {boolean=} opt_is3P set to true if the iframe is 3p. */ export function postMessage(iframe, type, object, targetOrigin, opt_is3P) { postMessageToWindows( iframe, [{win: iframe.contentWindow, origin: targetOrigin}], type, object, opt_is3P ); } /** * Posts an identical message to multiple target windows with the same * sentinel. * The message is serialized only once. * @param {!Element} iframe The iframe. * @param {!Array<{win: !Window, origin: string}>} targets to send the message * to, pairs of window and its origin. * @param {string} type Type of the message. * @param {!JsonObject} object Message payload. * @param {boolean=} opt_is3P set to true if the iframe is 3p. */ export function postMessageToWindows(iframe, targets, type, object, opt_is3P) { if (!iframe.contentWindow) { return; } object['type'] = type; object['sentinel'] = getSentinel_(iframe, opt_is3P); let payload = object; if (opt_is3P) { // Serialize ourselves because that is much faster in Chrome. payload = 'amp-' + JSON.stringify(object); } for (let i = 0; i < targets.length; i++) { const target = targets[i]; target.win./*OK*/ postMessage(payload, target.origin); } } /** * Gets the sentinel string. * @param {!Element} iframe The iframe. * @param {boolean=} opt_is3P set to true if the iframe is 3p. * @return {string} Sentinel string. * @private */ function getSentinel_(iframe, opt_is3P) { return opt_is3P ? iframe.getAttribute('data-amp-3p-sentinel') : 'amp'; } /** * JSON parses event.data if it needs to be * @param {*} data * @return {?JsonObject} object message * @private * @visibleForTesting */ export function parseIfNeeded(data) { if (typeof data == 'string') { if (data.charAt(0) == '{') { data = tryParseJson(data, (e) => { dev().warn( 'IFRAME-HELPER', 'Postmessage could not be parsed. ' + 'Is it in a valid JSON format?', e ); }) || null; } else if (isAmpMessage(data)) { data = deserializeMessage(data); } else { data = null; } } return /** @type {?JsonObject} */ (data); } /** * Manages a postMessage API for an iframe with a subscription message and * a way to broadcast messages to all subscribed windows, which * in turn must all be descendants of the contentWindow of the iframe. */ export class SubscriptionApi { /** * @param {!Element} iframe The iframe. * @param {string} type Type of the subscription message. * @param {boolean} is3p set to true if the iframe is 3p. * @param {function(!JsonObject, !Window, string)} requestCallback Callback * invoked whenever a new window subscribes. */ constructor(iframe, type, is3p, requestCallback) { /** @private @const {!Element} */ this.iframe_ = iframe; /** @private @const {boolean} */ this.is3p_ = is3p; /** @private @const {!Array<{win: !Window, origin: string}>} */ this.clientWindows_ = []; /** @private @const {!UnlistenDef} */ this.unlisten_ = listenFor( this.iframe_, type, (data, source, origin) => { // This message might be from any window within the iframe, we need // to keep track of which windows want to be sent updates. if (!this.clientWindows_.some((entry) => entry.win == source)) { this.clientWindows_.push({win: source, origin}); } requestCallback(data, source, origin); }, this.is3p_, // For 3P frames we also allow nested frames within them to subscribe.. this.is3p_ /* opt_includingNestedWindows */ ); } /** * Sends a message to all subscribed windows. * @param {string} type Type of the message. * @param {!JsonObject} data Message payload. */ send(type, data) { // Remove clients that have been removed from the DOM. remove(this.clientWindows_, (client) => !client.win.parent); postMessageToWindows( this.iframe_, this.clientWindows_, type, data, this.is3p_ ); } /** * Destroys iframe. */ destroy() { this.unlisten_(); this.clientWindows_.length = 0; } } /** * @param {!Element} element * @return {boolean} */ export function looksLikeTrackingIframe(element) { const {height, width} = element.getLayoutSize(); // This heuristic is subject to change. if (width > 10 || height > 10) { return false; } // Iframe is not tracking iframe if open with user interaction return !closestAncestorElementBySelector(element, '.i-amphtml-overlay'); } // Most common ad sizes // Array of [width, height] pairs. const adSizes = [ [300, 250], [320, 50], [300, 50], [320, 100], ]; /** * Guess whether this element might be an ad. * @param {!Element} element An amp-iframe element. * @return {boolean} * @visibleForTesting */ export function isAdLike(element) { const {height, width} = element.getLayoutSize(); for (let i = 0; i < adSizes.length; i++) { const refWidth = adSizes[i][0]; const refHeight = adSizes[i][1]; if (refHeight > height) { continue; } if (refWidth > width) { continue; } // Fuzzy matching to account for padding. if (height - refHeight <= 20 && width - refWidth <= 20) { return true; } } return false; } /** * @param {!Element} iframe * @return {!Element} */ export function disableScrollingOnIframe(iframe) { addAttributesToElement(iframe, {'scrolling': 'no'}); // This shouldn't work, but it does on Firefox. // https://stackoverflow.com/a/15494969 setStyle(iframe, 'overflow', 'hidden'); return iframe; } /** * Returns true if win's properties can be accessed and win is defined. * This functioned is used to determine if a window is cross-domained * from the perspective of the current window. * @param {!Window} win * @return {boolean} */ export function canInspectWindow(win) { // TODO: this is not reliable. The compiler assumes that property reads are // side-effect free. The recommended fix is to use goog.reflect.sinkValue // but since we're not using the closure library I'm not sure how to do this. // See https://github.com/google/closure-compiler/issues/3156 try { // win['test'] could be truthy but not true the compiler shouldn't be able // to optimize this check away. return !!win.location.href && (win['test'] || true); } catch (unusedErr) { return false; } } /** @const {string} */ export const FIE_EMBED_PROP = '__AMP_EMBED__'; /** * Returns the embed created using `installFriendlyIframeEmbed` or `null`. * Caution: This will only return the FIE after the iframe has 'loaded'. If you * are checking before this signal you may be in a race condition that returns * null. * @param {!HTMLIFrameElement} iframe * @return {?./friendly-iframe-embed.FriendlyIframeEmbed} */ export function getFriendlyIframeEmbedOptional(iframe) { return /** @type {?./friendly-iframe-embed.FriendlyIframeEmbed} */ ( iframe[FIE_EMBED_PROP] ); } /** * @param {!Element} element * @return {boolean} */ export function isInFie(element) { return ( element.classList.contains('i-amphtml-fie') || !!closestAncestorElementBySelector(element, '.i-amphtml-fie') ); }