Skip to content

Commit

Permalink
Implemented AmpContext and IframeMessagingClient (#6310)
Browse files Browse the repository at this point in the history
* Added ampcontext.js, ampcontext-lib and updated gulpfile.

* Changed how ampcontext-lib creates window.context

(for dev: cherry pick on to frizz-ampcontext)

* Split out PostMessenger as a parent class

* Changed name of class

* Made changes as per Hongfei. Had to expose getRandom().

* super() must be called before we can use this

* Fixed broken funtion documentation

* Fixed bug in lib and changed error thrown

* Made changes to fix presubmit failures

* Made fixes as per reviewers

* Fixed lint/style and presubmit errors.
  • Loading branch information
bradfrizzell authored and lannka committed Nov 30, 2016
1 parent 02c4584 commit 555b554
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 1 deletion.
33 changes: 33 additions & 0 deletions 3p/ampcontext-lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright 2016 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 {AmpContext} from './ampcontext.js';
import {initLogConstructor} from '../src/log';
initLogConstructor();


/**
* If window.context does not exist, we must instantiate a replacement and
* assign it to window.context, to provide the creative with all the required
* functionality.
*/
try {
const windowContextCreated = new Event('amp-windowContextCreated');
window.context = new AmpContext(window);
// Allows for pre-existence, consider validating correct window.context lib instance?
window.dispatchEvent(windowContextCreated);
} catch (err) {
// do nothing with error
}
182 changes: 182 additions & 0 deletions 3p/ampcontext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* Copyright 2016 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 './polyfills';
import {dev, user} from '../src/log';
import {IframeMessagingClient} from './iframe-messaging-client';

/**
Enum for the different postmessage types for the window.context
postmess api.
*/
export const MessageType_ = {
SEND_EMBED_STATE: 'send-embed-state',
EMBED_STATE: 'embed-state',
SEND_EMBED_CONTEXT: 'send-embed-context',
EMBED_CONTEXT: 'embed-context',
SEND_INTERSECTIONS: 'send-intersections',
INTERSECTION: 'intersection',
EMBED_SIZE: 'embed-size',
EMBED_SIZE_CHANGED: 'embed-size-changed',
EMBED_SIZE_DENIED: 'embed-size-denied',
};

export class AmpContext extends IframeMessagingClient {

/**
* @param {Window} win The window that the instance is built inside.
*/
constructor(win) {
super(win);
this.setupMetadata_();

/** Calculate the hostWindow / ampWindow_ */
const sentinelMatch = this.sentinel.match(/((\d+)-\d+)/);
dev().assert(sentinelMatch, 'Incorrect sentinel format');
this.depth = Number(sentinelMatch[2]);
const ancestors = [];
for (let win = this.win_; win && win != win.parent; win = win.parent) {
// Add window keeping the top-most one at the front.
ancestors.push(win.parent);
}
ancestors.reverse();

/** @private */
this.ampWindow_ = ancestors[this.depth];
}

/** @override */
getHostWindow() {
return this.ampWindow_;
}

/** @override */
getSentinel() {
return this.sentinel;
}

/** @override */
registerCallback_(messageType, callback) {
user().assertEnumValue(MessageType_, messageType);
this.callbackFor_[messageType] = callback;
return () => { delete this.callbackFor_[messageType]; };
}

/**
* Send message to runtime to start sending page visibility messages.
* @param {function(Object)} callback Function to call every time we receive a
* page visibility message.
* @returns {function()} that when called stops triggering the callback
* every time we receive a page visibility message.
*/
observePageVisibility(callback) {
const stopObserveFunc = this.registerCallback_(
MessageType_.EMBED_STATE, callback);
this.ampWindow_.postMessage/*REVIEW*/({
sentinel: this.sentinel,
type: MessageType_.SEND_EMBED_STATE,
}, '*');

return stopObserveFunc;
};

/**
* Send message to runtime to start sending intersection messages.
* @param {function(Object)} callback Function to call every time we receive an
* intersection message.
* @returns {function()} that when called stops triggering the callback
* every time we receive an intersection message.
*/
observeIntersection(callback) {
const stopObserveFunc = this.registerCallback_(
MessageType_.INTERSECTION, callback);
this.ampWindow_.postMessage/*REVIEW*/({
sentinel: this.sentinel,
type: MessageType_.SEND_INTERSECTIONS,
}, '*');

return stopObserveFunc;
};

/**
* Send message to runtime requesting to resize ad to height and width.
* This is not guaranteed to succeed. All this does is make the request.
* @param {int} height The new height for the ad we are requesting.
* @param {int} width The new width for the ad we are requesting.
*/
requestResize(height, width) {
this.ampWindow_.postMessage/*REVIEW*/({
sentinel: this.sentinel,
type: MessageType_.EMBED_SIZE,
width,
height,
}, '*');
};

/**
* Allows a creative to set the callback function for when the resize
* request returns a success. The callback should be set before resizeAd
* is ever called.
* @param {function(requestedHeight, requestedWidth)} callback Function
* to call if the resize request succeeds.
*/
onResizeSuccess(callback) {
this.registerCallback_(MessageType_.EMBED_SIZE_CHANGED, function(obj) {
callback(obj.requestedHeight, obj.requestedWidth); });
};

/**
* Allows a creative to set the callback function for when the resize
* request is denied. The callback should be set before resizeAd
* is ever called.
* @param {function(requestedHeight, requestedWidth)} callback Function
* to call if the resize request is denied.
*/
onResizeDenied(callback) {
this.registerCallback_(MessageType_.EMBED_SIZE_DENIED, function(obj) {
callback(obj.requestedHeight, obj.requestedWidth); });
};

/**
* Takes the current name on the window, and attaches it to
* the name of the iframe.
* @param {HTMLIframeElement} iframe The iframe we are adding the context to.
*/
addContextToIframe(iframe) {
iframe.name = this.win_.name;
}

/**
* Parse the metadata attributes from the name and add them to
* the class instance.
* @private
*/
setupMetadata_() {
try {
const data = JSON.parse(decodeURI(this.win_.name));
const context = data._context;
this.location = context.location;
this.canonicalUrl = context.canonicalUrl;
this.pageViewId = context.pageViewId;
this.sentinel = context.sentinel;
this.startTime = context.startTime;
this.referrer = context.referrer;
} catch (err) {
user().error('AMPCONTEXT', '- Could not parse metadata.');
throw new Error('Could not parse metadata.');
}
}
};
124 changes: 124 additions & 0 deletions 3p/iframe-messaging-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Copyright 2016 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 './polyfills';
import {listen} from '../src/event-helper';
import {getRandom} from '../src/3p-frame';
import {map} from '../src/types';
import {user} from '../src/log';
import {startsWith} from '../src/string';

/**
* @abstract
*/
export class IframeMessagingClient {

/**
* @param {Window} win A window object.
*/
constructor(win) {
/** @private {!Window} */
this.win_ = win;
/** Map messageType keys to callback functions for when we receive
* that message
* @private {!Object}
*/
this.callbackFor_ = map();
this.setupEventListener_();
}

/**
* Register callback function for message with type messageType.
* As it stands right now, only one callback can exist at a time.
* All future calls will overwrite any previously registered
* callbacks.
* @param {string} messageType The type of the message.
* @param {function(object)} callback The callback function to call
* when a message with type messageType is received.
*/
registerCallback_(messageType, callback) {
// NOTE : no validation done here. any callback can be register
// for any callback, and then if that message is received, this
// class *will execute* that callback
this.callbackFor_[messageType] = callback;
return () => { delete this.callbackFor_[messageType]; };
}

/**
* Sets up event listener for post messages of the desired type.
* The actual implementation only uses a single event listener for all of
* the different messages, and simply diverts the message to be handled
* by different callbacks.
* To add new messages to listen for, call registerCallback with the
* messageType to listen for, and the callback function.
* @private
*/
setupEventListener_() {
listen(this.win_, 'message', message => {
// Does it look a message from AMP?
if (message.source != this.getHostWindow()) {
return;
}
if (!message.data) {
return;
}
if (!startsWith(String(message.data), 'amp-')) {
return;
}

// See if we can parse the payload.
try {
const payload = JSON.parse(message.data.substring(4));
// Check the sentinel as well.
if (payload.sentinel == this.getSentinel() &&
this.callbackFor_[payload.type]) {
try {
// We should probably report exceptions within callback
this.callbackFor_[payload.type](payload);
} catch (err) {
user().error(
'IFRAME-MSG',
`- Error in registered callback ${payload.type}`,
err);
}
}
} catch (e) {
// JSON parsing failed. Ignore the message.
}
});
}

/**
* This must be overwritten by classes that extend this base class
* As implemented, this will only work for messaging the parent iframe.
*/
getSentinel() {
if (!this.sentinel) {
this.sentinel = '0-' + getRandom(this.win_);
}
return this.sentinel;
}

/**
* Only valid for the trivial case when we will always be messaging our parent
* Should be overwritten for subclasses
*/
getHostWindow() {
if (!this.hostWindow) {
this.hostWindow = this.win_.parent;
}
return this.hostWindow;
}
};
1 change: 1 addition & 0 deletions build-system/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ module.exports = {
// run against the entire transitive closure of deps.
'!{node_modules,build,dist,dist.tools,' +
'dist.3p/[0-9]*,dist.3p/current-min}/**/*.*',
'!dist.3p/current/**/ampcontext-lib.js',
'!validator/dist/**/*.*',
'!validator/node_modules/**/*.*',
'!validator/nodejs/node_modules/**/*.*',
Expand Down
1 change: 1 addition & 0 deletions build-system/tasks/presubmit-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ var forbiddenTerms = {
message: 'Should only be called from JS binary entry files.',
whitelist: [
'3p/integration.js',
'3p/ampcontext-lib.js',
'ads/alp/install-alp.js',
'ads/inabox/inabox-host.js',
'dist.3p/current/integration.js',
Expand Down
11 changes: 11 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,17 @@ function compile(watch, shouldMinify, opt_preventRemoveAndMakeDir,
includePolyfills: true,
});

compileJs('./3p/', 'ampcontext-lib.js',
'./dist.3p/' + (shouldMinify ? internalRuntimeVersion : 'current'), {
minifiedName: 'ampcontext-v0.js',
checkTypes: opt_checkTypes,
watch: watch,
minify: false,
preventRemoveAndMakeDir: opt_preventRemoveAndMakeDir,
externs: ['ads/ads.extern.js',],
includeBasicPolyfills: false,
});

// For compilation with babel we start with the amp-babel entry point,
// but then rename to the amp.js which we've been using all along.
compileJs('./src/', 'amp-babel.js', './dist', {
Expand Down
2 changes: 1 addition & 1 deletion src/3p-frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export function getSubDomain(win) {
* @param {!Window} win
* @return {string}
*/
function getRandom(win) {
export function getRandom(win) {
let rand;
if (win.crypto && win.crypto.getRandomValues) {
// By default use 2 32 bit integers.
Expand Down

0 comments on commit 555b554

Please sign in to comment.