/**
* Copyright 2018 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.
*/
// src/polyfills.js must be the first import.
import './polyfills'; // eslint-disable-line sort-imports-es6-autofix/sort-imports-es6
import ampToolboxCacheUrl from '../third_party/amp-toolbox-cache-url/dist/amp-toolbox-cache-url.esm';
import {IframeMessagingClient} from './iframe-messaging-client';
import {
dev,
devAssert,
initLogConstructor,
setReportError,
user,
} from '../src/log';
import {dict} from '../src/utils/object';
import {isProxyOrigin, parseUrlDeprecated} from '../src/url';
import {loadScript} from './3p';
import {parseJson} from '../src/json';
/**
* @fileoverview
* Boostrap Iframe for communicating with the recaptcha API.
*
* Here are the following iframe messages using .postMessage()
* used between the iframe and recaptcha service:
* amp-recaptcha-ready / Service <- Iframe :
* Iframe and Recaptcha API are ready.
* amp-recaptcha-action / Service -> Iframe :
* Execute and action using supplied data
* amp-recaptcha-token / Service <- Iframe :
* Response to 'amp-recaptcha-action'. The token
* returned by the recaptcha API.
* amp-recaptcha-error / Service <- Iframe :
* Response to 'amp-recaptcha-action'. Error
* From attempting to get a token from action.
*/
/** @const {string} */
const TAG = 'RECAPTCHA';
/** @const {string} */
const RECAPTCHA_API_URL = 'https://www.google.com/recaptcha/api.js?render=';
/** {?IframeMessaginClient} */
let iframeMessagingClient = null;
/** {?string} */
let sitekey = null;
/**
* Initialize 3p frame.
*/
function init() {
initLogConstructor();
setReportError(console.error.bind(console));
}
// Immediately call init
init();
/**
* Main function called by the recaptcha bootstrap frame
* @param {string} recaptchaApiBaseUrl
*/
export function initRecaptcha(recaptchaApiBaseUrl = RECAPTCHA_API_URL) {
const win = window;
/**
* Get the data from our name attribute
* sitekey {string} - reCAPTCHA sitekey used to identify the site
* sentinel {string} - string used to psuedo-confirm that we are
* receiving messages from the recaptcha frame
*/
let dataObject;
try {
dataObject = parseJson(win.name);
} catch (e) {
throw new Error(TAG + ' Could not parse the window name.');
}
// Get our sitekey from the iframe name attribute
devAssert(
dataObject.sitekey,
'The sitekey is required for the iframe'
);
sitekey = dataObject.sitekey;
const recaptchaApiUrl = recaptchaApiBaseUrl + sitekey;
loadScript(
win,
recaptchaApiUrl,
function() {
const {grecaptcha} = win;
grecaptcha.ready(function() {
initializeIframeMessagingClient(win, grecaptcha, dataObject);
iframeMessagingClient./*OK*/ sendMessage('amp-recaptcha-ready');
});
},
function() {
dev().error(TAG + ' Failed to load recaptcha api script');
}
);
}
window.initRecaptcha = initRecaptcha;
/**
* Function to initialize our IframeMessagingClient
* @param {Window} win
* @param {*} grecaptcha
* @param {!JsonObject} dataObject
*/
function initializeIframeMessagingClient(win, grecaptcha, dataObject) {
iframeMessagingClient = new IframeMessagingClient(win);
iframeMessagingClient.setSentinel(dataObject.sentinel);
iframeMessagingClient.registerCallback(
'amp-recaptcha-action',
actionTypeHandler.bind(this, win, grecaptcha)
);
}
/**
* Function to handle executing actions using the grecaptcha Object,
* and sending the token back to the parent amp-recaptcha component
*
* Data Object will have the following fields
* action {string} - action to be dispatched with grecaptcha.execute
* id {number} - id given to us by a counter in the recaptcha service
*
* @param {Window} win
* @param {*} grecaptcha
* @param {Object} data
*/
function actionTypeHandler(win, grecaptcha, data) {
doesOriginDomainMatchIframeSrc(win, data)
.then(() => {
const executePromise = grecaptcha.execute(sitekey, {
action: data.action,
});
// .then() promise pollyfilled by recaptcha api script
executePromise./*OK*/ then(
function(token) {
iframeMessagingClient./*OK*/ sendMessage(
'amp-recaptcha-token',
dict({
'id': data.id,
'token': token,
})
);
},
function(err) {
user().error(TAG, '%s', err.message);
iframeMessagingClient./*OK*/ sendMessage(
'amp-recaptcha-error',
dict({
'id': data.id,
'error': err.message,
})
);
}
);
})
.catch(error => {
dev().error(TAG, '%s', error.message);
});
}
/**
* Function to verify our origin domain from the
* parent window.
* @param {Window} win
* @param {Object} data
* @return {!Promise}
*/
export function doesOriginDomainMatchIframeSrc(win, data) {
if (!data.origin) {
return Promise.reject(new Error('Could not retreive the origin domain'));
}
// Using the deprecated parseUrl here, as we don't have access
// to the URL service in a 3p frame.
const originLocation = parseUrlDeprecated(data.origin);
if (isProxyOrigin(data.origin)) {
const curlsSubdomain = originLocation.hostname.split('.')[0];
return compareCurlsDomain(win, curlsSubdomain, data.origin);
}
return ampToolboxCacheUrl
.createCurlsSubdomain(data.origin)
.then(curlsSubdomain => {
return compareCurlsDomain(win, curlsSubdomain, data.origin);
});
}
/**
* Function to compare curls domains with the passed subdomain
* and window
* @param {Window} win
* @param {string} curlsSubdomain
* @param {string} origin
* @return {!Promise}
*/
function compareCurlsDomain(win, curlsSubdomain, origin) {
// Get the hostname after the culrs subdomain of the current iframe window
const locationWithoutCurlsSubdomain = win.location.hostname
.split('.')
.slice(1)
.join('.');
const curlsHostname = curlsSubdomain + '.' + locationWithoutCurlsSubdomain;
if (curlsHostname === win.location.hostname) {
return Promise.resolve();
}
return Promise.reject(
new Error('Origin domain does not match Iframe src: ' + origin)
);
}