-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
♻️ Build individual 3p iframe integration JS (#32448)
* Add build task for ad vendors * Move out shared parts of integration.js
- Loading branch information
Showing
9 changed files
with
549 additions
and
360 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
/** | ||
* Copyright 2021 The AMP HTML Authors. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS-IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import {IntegrationAmpContext} from './ampcontext-integration'; | ||
import {dict} from '../src/utils/object.js'; | ||
import {endsWith} from '../src/string'; | ||
import {getAmpConfig, getEmbedType, getLocation} from './frame-metadata'; | ||
import {getSourceUrl, isProxyOrigin, parseUrlDeprecated} from '../src/url'; | ||
import { | ||
initLogConstructor, | ||
isUserErrorMessage, | ||
setReportError, | ||
userAssert, | ||
} from '../src/log'; | ||
import {installEmbedStateListener, manageWin} from './environment'; | ||
import {internalRuntimeVersion} from '../src/internal-version'; | ||
import {parseJson} from '../src/json'; | ||
import {run, setExperimentToggles} from './3p'; | ||
import {urls} from '../src/config'; | ||
|
||
/** | ||
* Whether the embed type may be used with amp-embed tag. | ||
* @const {!Object<string, boolean>} | ||
*/ | ||
const AMP_EMBED_ALLOWED = { | ||
_ping_: true, | ||
'1wo': true, | ||
'24smi': true, | ||
adsloom: true, | ||
adstyle: true, | ||
bringhub: true, | ||
dable: true, | ||
engageya: true, | ||
epeex: true, | ||
firstimpression: true, | ||
forkmedia: true, | ||
glomex: true, | ||
idealmedia: true, | ||
insticator: true, | ||
jubna: true, | ||
kuadio: true, | ||
'mantis-recommend': true, | ||
mediaad: true, | ||
mgid: true, | ||
miximedia: true, | ||
mywidget: true, | ||
nativery: true, | ||
lentainform: true, | ||
opinary: true, | ||
outbrain: true, | ||
plista: true, | ||
postquare: true, | ||
ppstudio: true, | ||
pubexchange: true, | ||
pulse: true, | ||
rbinfox: true, | ||
readmo: true, | ||
recreativ: true, | ||
runative: true, | ||
smartclip: true, | ||
smi2: true, | ||
speakol: true, | ||
strossle: true, | ||
svknative: true, | ||
taboola: true, | ||
temedya: true, | ||
vlyby: true, | ||
whopainfeed: true, | ||
yahoofedads: true, | ||
yahoonativeads: true, | ||
yektanet: true, | ||
zen: true, | ||
zergnet: true, | ||
}; | ||
|
||
// For backward compat, we always allow these types without the iframe | ||
// opting in. | ||
const defaultAllowedTypesInCustomFrame = [ | ||
// Entries must be reasonably safe and not allow overriding the injected | ||
// JS URL. | ||
// Each custom iframe can override this through the second argument to | ||
// draw3p. See amp-ad docs. | ||
'facebook', | ||
'twitter', | ||
'doubleclick', | ||
'yieldbot', | ||
'_ping_', | ||
]; | ||
|
||
/** | ||
* Initialize 3p frame. | ||
* @param {!Window} win | ||
*/ | ||
export function init(win) { | ||
initLogConstructor(); | ||
const config = getAmpConfig(); | ||
|
||
// Overriding to short-circuit src/mode#getMode() | ||
win.__AMP_MODE = config.mode; | ||
|
||
setReportError(console.error.bind(console)); | ||
|
||
setExperimentToggles(config.experimentToggles); | ||
} | ||
|
||
/** | ||
* Visible for testing. | ||
* Draws a 3p embed to the window. Expects the data to include the 3p type. | ||
* @param {!Window} win | ||
* @param {!Object} data | ||
* @param {function(!Object, function(!Object))|undefined} configCallback | ||
* Optional callback that allows user code to manipulate the incoming | ||
* configuration. See | ||
* https://github.com/ampproject/amphtml/issues/1210 for some context | ||
* on this. | ||
*/ | ||
export function draw3pInternal(win, data, configCallback) { | ||
const type = data['type']; | ||
|
||
userAssert( | ||
isTagNameAllowed(type, win.context.tagName), | ||
'Embed type %s not allowed with tag %s', | ||
type, | ||
win.context.tagName | ||
); | ||
if (configCallback) { | ||
configCallback(data, (data) => { | ||
userAssert(data, 'Expected configuration to be passed as first argument'); | ||
run(type, win, data); | ||
}); | ||
} else { | ||
run(type, win, data); | ||
} | ||
} | ||
|
||
/** | ||
* Draws an embed, optionally synchronously, to the DOM. | ||
* @param {function(!Object, function(!Object))} opt_configCallback If provided | ||
* will be invoked with two arguments: | ||
* 1. The configuration parameters supplied to this embed. | ||
* 2. A callback that MUST be called for rendering to proceed. It takes | ||
* no arguments. Configuration is expected to be modified in-place. | ||
* @param {!Array<string>=} opt_allowed3pTypes List of advertising network | ||
* types you expect. | ||
* @param {!Array<string>=} opt_allowedEmbeddingOrigins List of domain suffixes | ||
* that are allowed to embed this frame. | ||
*/ | ||
export function draw3p( | ||
opt_configCallback, | ||
opt_allowed3pTypes, | ||
opt_allowedEmbeddingOrigins | ||
) { | ||
try { | ||
const location = getLocation(); | ||
|
||
ensureFramed(window); | ||
validateParentOrigin(window, location); | ||
validateAllowedTypes(window, getEmbedType(), opt_allowed3pTypes); | ||
if (opt_allowedEmbeddingOrigins) { | ||
validateAllowedEmbeddingOrigins(window, opt_allowedEmbeddingOrigins); | ||
} | ||
window.context = new IntegrationAmpContext(window); | ||
manageWin(window); | ||
installEmbedStateListener(); | ||
|
||
// Ugly type annotation is due to Event.prototype.data being denylisted | ||
// and the compiler not being able to discern otherwise | ||
// TODO(alanorozco): Do this more elegantly once old impl is cleaned up. | ||
draw3pInternal( | ||
window, | ||
/** @type {!IntegrationAmpContext} */ (window.context).data || {}, | ||
opt_configCallback | ||
); | ||
|
||
window.context.bootstrapLoaded(); | ||
} catch (e) { | ||
if (window.context && window.context.report3pError) { | ||
// window.context has initiated yet | ||
if (e.message && isUserErrorMessage(e.message)) { | ||
// report user error to parent window | ||
window.context.report3pError(e); | ||
} | ||
} | ||
|
||
const c = window.context || {mode: {test: false}}; | ||
if (!c.mode.test) { | ||
lightweightErrorReport(e, c.canary); | ||
throw e; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Throws if the current frame's parent origin is not equal to | ||
* the claimed origin. | ||
* Only check for browsers that support ancestorOrigins | ||
* @param {!Window} window | ||
* @param {!Location} parentLocation | ||
* @visibleForTesting | ||
*/ | ||
export function validateParentOrigin(window, parentLocation) { | ||
const ancestors = window.location.ancestorOrigins; | ||
// Currently only webkit and blink based browsers support | ||
// ancestorOrigins. In that case we proceed but mark the origin | ||
// as non-validated. | ||
if (!ancestors || !ancestors.length) { | ||
return; | ||
} | ||
userAssert( | ||
ancestors[0] == parentLocation.origin, | ||
'Parent origin mismatch: %s, %s', | ||
ancestors[0], | ||
parentLocation.origin | ||
); | ||
} | ||
|
||
/** | ||
* Check that this iframe intended this particular ad type to run. | ||
* @param {!Window} window | ||
* @param {string} type 3p type | ||
* @param {!Array<string>|undefined} allowedTypes May be undefined. | ||
* @visibleForTesting | ||
*/ | ||
export function validateAllowedTypes(window, type, allowedTypes) { | ||
const thirdPartyHost = parseUrlDeprecated(urls.thirdParty).hostname; | ||
|
||
// Everything allowed in default iframe. | ||
if (window.location.hostname == thirdPartyHost) { | ||
return; | ||
} | ||
if (urls.thirdPartyFrameRegex.test(window.location.hostname)) { | ||
return; | ||
} | ||
if (window.location.hostname == 'ads.localhost') { | ||
return; | ||
} | ||
if (defaultAllowedTypesInCustomFrame.indexOf(type) != -1) { | ||
return; | ||
} | ||
userAssert( | ||
allowedTypes && allowedTypes.indexOf(type) != -1, | ||
'3p type for custom iframe not allowed: %s', | ||
type | ||
); | ||
} | ||
|
||
/** | ||
* Check that parent host name was allowed. | ||
* @param {!Window} window | ||
* @param {!Array<string>} allowedHostnames Suffixes of allowed host names. | ||
* @visibleForTesting | ||
*/ | ||
export function validateAllowedEmbeddingOrigins(window, allowedHostnames) { | ||
if (!window.document.referrer) { | ||
throw new Error('Referrer expected: ' + window.location.href); | ||
} | ||
const ancestors = window.location.ancestorOrigins; | ||
// We prefer the unforgable ancestorOrigins, but referrer is better than | ||
// nothing. | ||
const ancestor = ancestors ? ancestors[0] : window.document.referrer; | ||
let {hostname} = parseUrlDeprecated(ancestor); | ||
if (isProxyOrigin(ancestor)) { | ||
// If we are on the cache domain, parse the source hostname from | ||
// the referrer. The referrer is used because it should be | ||
// trustable. | ||
hostname = parseUrlDeprecated(getSourceUrl(window.document.referrer)) | ||
.hostname; | ||
} | ||
for (let i = 0; i < allowedHostnames.length; i++) { | ||
// Either the hostname is allowed | ||
if (allowedHostnames[i] == hostname) { | ||
return; | ||
} | ||
// Or it ends in .$hostname (aka is a sub domain of an allowed domain. | ||
if (endsWith(hostname, '.' + allowedHostnames[i])) { | ||
return; | ||
} | ||
} | ||
throw new Error( | ||
'Invalid embedding hostname: ' + hostname + ' not in ' + allowedHostnames | ||
); | ||
} | ||
|
||
/** | ||
* Throws if this window is a top level window. | ||
* @param {!Window} window | ||
* @visibleForTesting | ||
*/ | ||
export function ensureFramed(window) { | ||
if (window == window.parent) { | ||
throw new Error('Must be framed: ' + window.location.href); | ||
} | ||
} | ||
|
||
/** | ||
* Expects the fragment to contain JSON. | ||
* @param {string} fragment Value of location.fragment | ||
* @return {?JsonObject} | ||
* @visibleForTesting | ||
*/ | ||
export function parseFragment(fragment) { | ||
try { | ||
let json = fragment.substr(1); | ||
// Some browser, notably Firefox produce an encoded version of the fragment | ||
// while most don't. Since we know how the string should start, this is easy | ||
// to detect. | ||
if (json.startsWith('{%22')) { | ||
json = decodeURIComponent(json); | ||
} | ||
return /** @type {!JsonObject} */ (json ? parseJson(json) : dict()); | ||
} catch (err) { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Not all types of embeds are allowed to be used with all tag names on the | ||
* AMP side. This function checks whether the current usage is permissible. | ||
* @param {string} type | ||
* @param {string|undefined} tagName The tagName that was used to embed this | ||
* 3p-frame. | ||
* @return {boolean} | ||
*/ | ||
export function isTagNameAllowed(type, tagName) { | ||
if (tagName == 'AMP-EMBED') { | ||
return !!AMP_EMBED_ALLOWED[type]; | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* Reports an error to the server. Must only be called once per page. | ||
* Not for use in event handlers. | ||
* | ||
* We don't use the default error in error.js handler because it has | ||
* too many deps for this small JS binary. | ||
* | ||
* @param {!Error} e | ||
* @param {boolean} isCanary | ||
*/ | ||
function lightweightErrorReport(e, isCanary) { | ||
new Image().src = | ||
urls.errorReporting + | ||
'?3p=1&v=' + | ||
encodeURIComponent(internalRuntimeVersion()) + | ||
'&m=' + | ||
encodeURIComponent(e.message) + | ||
'&ca=' + | ||
(isCanary ? 1 : 0) + | ||
'&r=' + | ||
encodeURIComponent(document.referrer) + | ||
'&s=' + | ||
encodeURIComponent(e.stack || ''); | ||
} |
Oops, something went wrong.