Skip to content

Instantly share code, notes, and snippets.

@mailopl
Created November 13, 2013 14:37
Show Gist options
  • Save mailopl/7450102 to your computer and use it in GitHub Desktop.
Save mailopl/7450102 to your computer and use it in GitHub Desktop.
var ipc = ipc || {};
// Step 1: Maintain BC with ipc.adtech
ipc.adloader = ipc.adtech = function () {
/**
* DFP acount ID
*/
var _dfp_account_id = '',
/**
* Used to track the current request
*/
_user_id = (new Date().getTime()) + "" + Math.floor((Math.random()*1000)),
/**
* Have we started loading yet?
*/
_loading_ads = false,
/**
* Display mode, can be mobile, desktop or responsive
*/
_display_mode = 'desktop',
/**
* The current environment
*/
_environment = 'live',
/**
* If the display width is less than this, switch to mobile mode when
* responsive
*/
_mobile_mode_width_threshold = 742,
/**
* The slot configuration taken from ipc tags
*/
_slots_config = {},
/**
* Slots ready to render when data is returned
*/
_ready_slots = [],
/**
* Actual DFP slots that have been created using _googletag
*/
_defined_slots = {},
_has_third_parties = true,
/**
* Third parties loaded and waiting on results from
*/
_pending_third_parties = { 'running' : 0 },
/**
* URL for beaconing third party load times too
*/
_beacon_url = '',
/**
* Store the provider to be used as part of the migration
* Default to adtech but can be set to DFP by url param,
* includes config or cookie
*/
_provider = "adtech",
/**
* Local googletag object
*/
_googletag = {},
/**
* Holds timing information for a piece of 3rd party javascript or adcall
* @property _timings_log
* @type {Array} Stores all the additional parameter that need to be augmented
* @access private
*/
_timings_log = [],
/**
* key/value of data that can be used by ads
* separate to targeting params as these will
* not be pushed into ad slots
* @type {object}
*/
_ad_params = {},
/**
* Targeting parameters to push into ad calls, see pushTargetingParam
*/
_targeting_params = {};
/**
* keeps track of which functions have been applied, see _apply_function
* @type {object}
*/
_applied_functions = {},
/**
* check to see if we have shown an out of page slot as we can only have one per page
* @type {object}
*/
_out_of_page_unit_defined = false,
/**
* Cleans up the private variables
* for the next set of url that needs
* cleaning up
* @method _clean
* @access private
* @return void
*/
_clean = function () {
adcall = {};
_index_for_key = -1;
_params_to_push_after_key = [];
_params_to_push_before_key = [];
},
/**
* Returns true if in debug mode
*
* @method _is_debug
* @return bool
*/
_is_debug = function() {
return (typeof(_get_variable('adloader_debug')) !== 'undefined');
},
/**
* The over ride functionality is a stop gap measure but the config in ipcTags is confirmed
* @deprecated to replaced by the variable store in #51
* @method _set_display_mode
* @param display_mode
*/
_set_display_mode = function (display_mode) {
if (typeof(ipcTags) !== 'undefined' && typeof(ipcTags.dfp_config.display_mode) !== 'undefined') {
_display_mode = ipcTags.dfp_config.display_mode;
} else {
_display_mode = display_mode;
}
},
/**
* @method _is_mobile
* @return bool
*/
_is_mobile = function () {
return navigator.userAgent.match(/(Android .*Mobile|Mobile .*Android|Opera Mini|Opera Mobi|iPhone|iPod|Nintendo Wii|Window Mobile|Windows Phone|BlackBerry|N900|PSP|webOS|alcatel|audiovox|benq|blackberry|cdm-|docomo|ericsson|ezwap|go\.web\/|htc|lg|lg-|lge-|midp-2\.0|mitsu\/|mmp\/2\.0|mobileexplorer|mot-|nec-|nokia|palmsource|panasonic|philips|portalmmm|rim handheld|rim pager|sagem-|samsung-sgh|sec-s|sie-|sharp|symbianos|symbian os|up\.browser|up\.link|up\/4\.|[wW]indows [cC][eE]|iPhone|MIDP)/i) != null;
},
/**
* Desktop and mobile are treated slightly
* differently, smaller display widths get
* are treated as mobile as well
* @method _get_display_mode
* @return _display_mode
*/
_get_display_mode = function () {
if(_display_mode == 'responsive'){
if(_is_mobile()) {
return 'mobile';
} else {
return 'desktop';
}
}
return _display_mode;
},
/**
* Sets the ad provider. The passed provider can be overridden
* by query params or a cookie, both called 'adloader_provider'
*
* @method _set_provider
* @param provider
*/
_get_variable = function (name, default_value) {
var query_value = _get_query_variable(name);
var cookie_value = _read_cookie(name);
if (query_value != '') {
ipc.utils.ipcCreateCookie(name, query_value, 1);
return query_value;
} else if(cookie_value != false){
return cookie_value;
} else if (typeof(default_value) !== 'undefined') {
return default_value;
} else {
return;
}
},
/**
* @method _get_provider
* @return _provider
*/
_get_provider = function () {
return _provider;
},
/**
* @method _set_environment
*/
_set_environment = function (environment) {
_environment = environment;
},
/**
* @method _get_environment
* @return _environment
*/
_get_environment = function () {
return _environment;
},
/**
* Pushes timings onto a stack to beacon later
* @method _log_timing
* @access private
* @return void
*/
_log_timing = function(o) {
if (o.name && o.totalTime) {
_timings_log.push(o.name + '=' + o.totalTime);
}
},
/**
* Read ipcTags json data from the site, only push in specified values from
* the required tags field
* @method _read_site_tags
*/
_read_site_tags = function () {
// check integrity of ipcTags
if(typeof(ipcTags.dfp_config.slots) === 'undefined' || _is_object_empty(ipcTags.dfp_config.slots)){
_log_event('NoSlotsDefined', 'error');
} else {
_slots_config = {};
_out_of_page_unit_defined = false;
// define the ad slots, filter out any unwanted/duplicate ones
for(var slot in ipcTags.dfp_config.slots){
var slot_config = ipcTags.dfp_config.slots[slot];
// Length of one means an array inside an array so break it out to ensure out of page test works correctly
if (slot_config.sizes.length === 1) {
slot_config.sizes = slot_config.sizes[0];
}
// 1x1 ads need to be defined as out of page slots, only one is
// allowed per page though
if ((slot_config.sizes[0] === 1) && (slot_config.sizes[1] === 1)) {
if (_out_of_page_unit_defined) {
_log_event('Attempt to define multiple Out Of Page Units', 'error');
continue;
}
_out_of_page_unit_defined = true;
}
_slots_config[slot] = slot_config;
}
}
// add any targeting params from ipcTags
if (typeof(ipcTags.dfp_config.required_tags) !== 'undefined') {
for (var i=0; i < ipcTags.dfp_config.required_tags.length; i++) {
if (typeof(ipcTags[ipcTags.dfp_config.required_tags[i]]) !== 'undefined' && ipcTags[ipcTags.dfp_config.required_tags[i]] !== null) {
_push_targeting_param({
'name' : ipcTags.dfp_config.required_tags[i],
'value' : _format_targeting_param(ipcTags[ipcTags.dfp_config.required_tags[i]])
});
} else {
_log_event('Null Tag: ' + ipcTags.dfp_config.required_tags[i], 'error');
}
}
}
// add environment into targeting params
_push_targeting_param({
'name' : 'env',
'value' : _get_environment()
});
// add screen dimensions into targeting params
_push_targeting_param({
'name' : 'browwidth',
'value' : String(_get_display_width())
});
// add screen dimensions into targeting params
_push_targeting_param({
'name' : 'browheight',
'value' : String(_get_display_height())
});
_put_ad_param('browwidth', String(_get_display_width()));
_put_ad_param('browheight', String(_get_display_height()));
ipc.adloader.targeting.addCookieTargeting();
},
/**
* Initiate 3rd party code using iFrames and meebo dynamic iframe approach
* http://www.aaronpeters.nl/blog/iframe-loading-techniques-performance#dynamic
* Set a timer dependant on how long we are willing to wait to get the data back
* @todo variable timer probably set in Includes
*/
_load_third_parties = function () {
var beacon_data = { };
beacon_data.expected = [];
var iframeId = "dfp-third-party-loader-adprobe";
// remove existing iframe if present
if(document.getElementById(iframeId)){
var iframeCurrent = document.getElementById(iframeId);
iframeCurrent.parentNode.removeChild(iframeCurrent);
}
beacon_data.expected.push('ipcThirdPartyLoaderadprobe');
_pending_third_parties.running++;
_pending_third_parties.ipcThirdPartyLoaderadprobe = '';
_pending_third_parties.ipcThirdPartyLoaderadprobe = [];
_pending_third_parties.ipcThirdPartyLoaderadprobe['name'] = 'adprobe';
_pending_third_parties.ipcThirdPartyLoaderadprobe['status'] = 'running';
var iframe = document.body.appendChild(document.createElement('iframe')),
doc = iframe.contentWindow.document;
iframe.id = iframeId;
// style the iframe so it does not appear to the user
iframe.style.cssText = "position:absolute;width:0px;height:0px;left:-9999px;top:0px;";
// Set start_time here as if the script is too fast then it can return before the setTimout is created
// so can't be cleared by _register_third_party causing a secondary call to _load_ads
_pending_third_parties.ipcThirdPartyLoaderadprobe.start_time = new Date().getTime();
ipcThirdPartyLoaderadprobe = setTimeout(function() {
_timeout_third_party('ipcThirdPartyLoaderadprobe');
}, 10000);
doc.open().write('<!doctype html><head><scr' + 'ipt type="text/javascript">' +
'window.ipc_adloader_name = \'ipcThirdPartyLoaderadprobe\'; ' +
'adprobe = {};\n' +
'adprobe.wlOrd = new Date().getTime();\n' +
'adprobe.publisher_id = 1568;\n' +
'adprobe.website_id = 6654;\n' +
'adprobe.content_unit_id = 13103;\n' +
'adprobe.content_unit_ids = \'13103,13104\';\n' +
'adprobe.placement_unit_ids = [];\n' +
'adprobe.placement_unit_ids.push( {\'size\': 225, \'contentid\': 13103});\n' +
'adprobe.placement_unit_ids.push( {\'size\': 170, \'contentid\': 13104});\n' +
'adprobe.url = \'req.connect.wunderloop.net/AP/\' + adprobe.publisher_id + \'/\' + adprobe.website_id + \'/\' + adprobe.content_unit_id + \'/js?cus=\' + adprobe.content_unit_ids + \'&ord=\' + adprobe.wlOrd;\n' +
'var elem = document.createElement(\'script\');\n' +
'elem.src = (document.location.protocol == "https:" ? "https://" : "http://") + adprobe.url;\n' +
'elem.type = "text/javascript";\n' +
'var scpt = document.getElementsByTagName(\'script\')[0]; scpt.parentNode.insertBefore(elem, scpt);\n' +
'function sendData() {\n' +
' if (typeof(window[\'wl\' + adprobe.content_unit_id + \'camp\']) === "undefined") {\n' +
' return;\n' +
' }\n' +
' \n' +
' // function to push params\n' +
' function push_placements(content_ids) {\n' +
' for (var i=0; i < content_ids.length; i+=1) {\n' +
' var content_unit_key = \'wl\' + content_ids[i];\n' +
' var content_unit_id_name = \'wl\' + content_ids[i] + \'camp\';\n' +
' if(typeof(window[content_unit_id_name]) !== \'undefined\') {\n' +
' var value = \'yes\';\n' +
' if (window[content_unit_id_name] === \'\') {\n' +
' value = \'no\';\n' +
' }\n' +
' parent.ipc.adloader.putAdParam(content_unit_id_name, window[content_unit_id_name]);\n' +
' parent.ipc.adloader.pushTargetingParam({\n' +
' \'name\' : content_unit_key,\n' +
' \'value\' : value,\n' +
' \'scope\' : \'slots\'\n' +
' });\n' +
' }\n' +
' }\n' +
' }\n' +
' var content_unit_ids = [];\n' +
' if(adprobe.placement_unit_ids.length > 0) {\n' +
' for (i=0; i < adprobe.placement_unit_ids.length; i++) {\n' +
' content_unit_ids.push(adprobe.placement_unit_ids[i].contentid);\n' +
' }\n' +
' } else {\n' +
' content_unit_ids = adprobe.content_unit_ids.split(\',\');\n' +
' }\n' +
' push_placements(content_unit_ids);\n' +
' parent.ipc.adloader.registerThirdPartyLoad(ipc_adloader_name);\n' +
' clearInterval(dataSender);\n' +
'}\n' +
'var dataSender = setInterval(\'sendData()\', 25);\n' +
'</scr' + 'ipt></head><body></body></html>');
doc.close(); //iframe onload event happens
var iframeId = "dfp-third-party-loader-grapeshot";
// remove existing iframe if present
if(document.getElementById(iframeId)){
var iframeCurrent = document.getElementById(iframeId);
iframeCurrent.parentNode.removeChild(iframeCurrent);
}
beacon_data.expected.push('ipcThirdPartyLoadergrapeshot');
_pending_third_parties.running++;
_pending_third_parties.ipcThirdPartyLoadergrapeshot = '';
_pending_third_parties.ipcThirdPartyLoadergrapeshot = [];
_pending_third_parties.ipcThirdPartyLoadergrapeshot['name'] = 'grapeshot';
_pending_third_parties.ipcThirdPartyLoadergrapeshot['status'] = 'running';
var iframe = document.body.appendChild(document.createElement('iframe')),
doc = iframe.contentWindow.document;
iframe.id = iframeId;
// style the iframe so it does not appear to the user
iframe.style.cssText = "position:absolute;width:0px;height:0px;left:-9999px;top:0px;";
// Set start_time here as if the script is too fast then it can return before the setTimout is created
// so can't be cleared by _register_third_party causing a secondary call to _load_ads
_pending_third_parties.ipcThirdPartyLoadergrapeshot.start_time = new Date().getTime();
ipcThirdPartyLoadergrapeshot = setTimeout(function() {
_timeout_third_party('ipcThirdPartyLoadergrapeshot');
}, 10000);
doc.open().write('<!doctype html><head><scr' + 'ipt type="text/javascript">' +
'window.ipc_adloader_name = \'ipcThirdPartyLoadergrapeshot\'; ' +
'var elem = document.createElement(\'script\');\n' +
'elem.src = \'http://ipcmedia.grapeshot.co.uk/channels.cgi?url=\' + escape(top.location.href);\n' +
'elem.type = "text/javascript";\n' +
'var scpt = document.getElementsByTagName(\'script\')[0];\n' +
'scpt.parentNode.insertBefore(elem, scpt);\n' +
'function sendData() {\n' +
' if (typeof(gs_channels) === "undefined") {\n' +
' return;\n' +
' }\n' +
' \n' +
' if (!gs_channels) {\n' +
' gs_channels = \'default\';\n' +
' }\n' +
' \n' +
' parent.ipc.adloader.pushTargetingParam({\n' +
' \'name\' : \'gs_chans\',\n' +
' \'value\' : gs_channels\n' +
' });\n' +
' parent.ipc.adloader.registerThirdPartyLoad(ipc_adloader_name);\n' +
' clearInterval(dataSender);\n' +
'}\n' +
'var dataSender = setInterval(\'sendData()\', 25);\n' +
'</scr' + 'ipt></head><body></body></html>');
doc.close(); //iframe onload event happens
_beacon_data('requests', beacon_data);
_log_event(beacon_data, 'info');
},
/**
* Timeout a third party as it took too long to return
* Make sure we load the ads if all third parties have
* completed/timed out
*/
_timeout_third_party = function (third_party) {
_log_event('Timing Out '+third_party, 'info');
if (_pending_third_parties[third_party]['status'] == 'running') {
_pending_third_parties.running--;
_pending_third_parties[third_party]['status'] = 'timeout';
}
if (_pending_third_parties.running == 0 && _get_provider() === 'dfp') {
_load_ads();
}
},
/**
* Register third party load, called by a third party when it has received its data
* If the third party does not exist it has timed out so beacon as such
* Clears the timeout as well
*/
_register_third_party_load = function (third_party) {
clearTimeout(window[third_party]);
var currTime = new Date().getTime()
duration = currTime - _pending_third_parties[third_party]['start_time'];
if (_pending_third_parties[third_party]['status'] == 'running') {
_pending_third_parties[third_party]['status'] = 'complete';
_pending_third_parties.running--;
var beacon_data = { };
beacon_data.third_party = third_party;
beacon_data.status = 'success';
beacon_data.duration = duration;
_beacon_data('responses', beacon_data);
_log_event(beacon_data, 'info');
if (_pending_third_parties.running == 0 && _get_provider() === 'dfp') {
_load_ads();
}
} else {
var beacon_data = { };
beacon_data.third_party = third_party;
beacon_data.status = 'failure';
beacon_data.duration = duration;
_beacon_data('responses', beacon_data);
_log_event(beacon_data, 'info');
}
},
/**
* Load dfp ads, reload the ads if the initial
* load has already taken place.
*
* Normally called when all third parties have
* completed or timed out.
*
* If ads have already been loaded then reload the ads
*/
_load_ads = function () {
if (!_loading_ads) {
_googletag.cmd.push(function() {
_set_global_targeting();
_define_slots();
// Single request does not seem to work properly with gpt_mobile
if(_get_display_mode() == 'mobile') {
_googletag.pubads().enableAsyncRendering();
} else {
if ( (typeof(ipcTags.dfp_config.disableSingleRequest) !== 'undefined') && (ipcTags.dfp_config.disableSingleRequest === true) ) {
_log_event('singleRequest Mode Not Enabled', 'info');
} else {
_log_event('singleRequest Mode Enabled', 'info');
_googletag.pubads().enableSingleRequest();
}
}
_googletag.pubads().collapseEmptyDivs();
_googletag.enableServices();
});
// This looks unneeded but we have to display the slots in this way
// or we get some weird issues
for (var i=0; i<_ready_slots.length; i++) {
if (!_slot_exists(_ready_slots[i])) {
_log_event('Not displaying '+_ready_slots[i], 'info');
continue;
}
_log_event('Displaying '+_ready_slots[i], 'info');
_googletag.cmd.push(function(_ready_slots_item) {
return function(){_googletag.display(_ready_slots_item)};
}(_ready_slots[i]));
}
_loading_ads = true;
} else {
_reload_ads();
}
},
/**
* Initiates a refresh of the ad slots.
*
* Resets targeting parameters first,
* if third parties are present then third parties
* will be reloaded and the ads will be refreshed
* once they have all loaded or timed out
*/
_refresh = function() {
// reset targeting and whatnot, only keep items that have been marked
// as persistent. Refreshes are essentially a blank slate, targeting
// parameters, written data etc. all have a persistent option that will
// keep them in place when refreshing.
_ad_params = {};
_clear_targeting_params();
_clear_written_data();
_read_site_tags();
if(_has_third_parties){
_load_third_parties();
} else {
_reload_ads();
}
},
/**
* Reset targeting on each slot and then
* call dfp refresh on all ad slots
*/
_reload_ads = function () {
for(var slotName in _defined_slots){
_googletag.cmd.push(function() {
_defined_slots[slotName].clearTargeting();
_set_slot_targeting(_defined_slots[slotName], slotName);
});
}
_googletag.cmd.push(function() {
_googletag.pubads().refresh();
});
},
/**
* Define slots
* Creates and sets targeting on dfp slots,
* saves the slots for later as well
*/
_define_slots = function () {
var unitName = "/" + _dfp_account_id;
if ( ( typeof(ipcTags.site) !== "undefined" ) && ( ipcTags.site !== "" ) ) {
var site = ipcTags.site;
} else {
var site = "undefined";
_log_event('Site is not defined', 'error');
}
// add '_mobile' to 'ipcTags.site' if we are in mobile mode
if(_get_display_mode() == 'mobile') {
unitName = unitName + "/" + site + "_mobile";
} else {
unitName = unitName + "/" + site;
}
if ( ( typeof(ipcTags.section) !== "undefined" ) && ( ipcTags.section !== "" ) ) {
unitName = unitName + "/" + _slugify(ipcTags.section);
} else {
_log_event('NoSection', 'error');
}
if ( ( typeof(ipcTags.subsection) !== "undefined" ) && ( ipcTags.subsection !== "" ) ) {
unitName = unitName + "/" + _slugify(ipcTags.subsection);
}
for(var slot in _slots_config){
var slot_config = _slots_config[slot];
// 1x1 ads need to be defined as out of page slots, only one is
// allowed per page though
if ((slot_config.sizes[0] === 1) && (slot_config.sizes[1] === 1)) {
_defined_slots[slot] = _googletag.defineOutOfPageSlot(unitName, slot).addService(_googletag.pubads());
} else {
_defined_slots[slot] = _googletag.defineSlot(unitName, slot_config.sizes, slot).addService(_googletag.pubads());
}
_set_slot_targeting(_defined_slots[slot], slot);
}
},
/**
* Register slot
* Registers a dfp slot that will be defined later on, called by the site
* that the adloader is embedded into
*/
_register_slot = function (slot_id) {
if (_loading_ads) {
if (_slot_exists(slot_id)) {
_log_event('Registering slot to _googletag ' + slot_id, 'info');
_googletag.cmd.push(function() {
_googletag.display(slot_id);
});
} else {
_log_event('Unable to registering slot to _googletag as slot does not exist ' + slot_id, 'warn');
}
} else {
_log_event('Queueing registering slot ' + slot_id, 'info');
_ready_slots.push(slot_id);
}
},
/**
* Check that a slot has been defined in _slots_config, in the event of
* multiple out of page slots additional ones after the first are not defined,
* so we need to detect if this has happened before we display a slot
*/
_slot_exists = function(slot_id) {
if (typeof(_slots_config[slot_id]) === "undefined") {
_log_event('Attempt to use non-existant slot: ' + slot_id, 'error');
return false;
}
return true;
},
/**
* Sets global targeting for dfp ad slots
*/
_set_global_targeting = function (dfp_slot, name) {
targeting_params = _targeting_params;
if(_has_test_targeting()) {
targeting_params = _get_test_targeting();
}
for (var name in targeting_params) {
if(targeting_params[name]['scope'] == 'global') {
_googletag.pubads().setTargeting(name, targeting_params[name]['value']);
}
}
},
/**
* Sets slot level targeting for dfp ad slots
*/
_set_slot_targeting = function (dfp_slot, name) {
dfp_slot.setTargeting( "name", ipcTags.dfp_config.slots[name]['name'] );
if(_has_test_targeting()) {
return;
}
for (var name in _targeting_params) {
if(_targeting_params[name]['scope'] == 'slots') {
dfp_slot.setTargeting(name, _targeting_params[name]['value']);
}
}
},
/**
* Formats targeting params for DFP, converts ints to strings, removes
* spaces and special characters. Only used for interal params at the moment
* (the ones that are passed in via ipcTags)
*
* @method _format_targeting_param
* @access private
* @param {Mixed} The unformatted parameter
* @return {Mixed} The formatted parameter
*/
_format_targeting_param = function(param) {
var formatted_param = false;
if(param instanceof Array) {
formatted_param = []
for(i = 0; i < param.length; i++) {
formatted_param[i] = _slugify(param[i]);
}
} else {
formatted_param = _slugify(param);
}
return formatted_param;
},
/**
* Slugifies strings for use as targeting variables
*
* @method _slugify
* @access private
* @param {String} The unformatted string
* @return {String} The formatted string
*/
_slugify = function(str) {
str = String(str);
str = str.toLowerCase();
str = str.replace(/[^\w\s]/g, ' ');
str = str.replace(/\ +/g, '_');
str = str.replace(/\-$/g, '');
str = str.replace(/^\-/g, '');
return str;
},
/**
* Returns true if some test targeting has been set (see _get_test_targeting)
*
* @method _has_test_targeting
* @return bool
*/
_has_test_targeting = function () {
if (_get_query_variable('adloader_test_mode') != '' || _read_cookie('adloader_test_targeting') != false) {
return true;
}
return false;
},
/**
* Custom targeting can be set via cookies allowing for
* detailed testing of ad placements
*
* Format should be like so...
* test_var1=test1&test_var2=test2&test_var3=test3
*
* In addition to the cookie setting a query parameter
* can be set which switches targeting to test mode,
* where the only targeting param will be 'testmode' => true
* this can be set by adding 'adloader_test_mode=1' to
* the query string
*
* @method _get_custom_targeting
* @return mixed either an object containing
* targeting params or boolean false if there
* are custom params
*/
_get_test_targeting = function () {
if(!_has_test_targeting) {
return false;
}
test_targeting_params = {};
var custom_targeting = {};
var custom_targeting_string = _read_cookie('adloader_test_targeting');
if (_get_query_variable('adloader_test_mode') != '') {
test_targeting_params['testmode'] = {
'name' : 'testmode',
'value' : 'on',
'scope' : 'global'
}
} else if(custom_targeting_string != false) {
var vars = custom_targeting_string.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
test_targeting_params[pair[0]] = {
'name' : pair[0],
'value' : decodeURIComponent(pair[1]),
'scope' : 'global'
}
}
}
return test_targeting_params;
},
/**
* Parse the string of unit sizes into an array of ints, there must be a better way to do this!
* If this comment is in production we either:
* a) didn't have time to refactor this
* b) decided it was ok so could be left
* c) didn't touch it as it's a work of beauty
*
* Hint, it's not c and I doubt it will be b!
*/
_parse_unit_sizes = function (unit_details_sizes) {
var unit_sizes = [];
unit_sizes_array = unit_details_sizes.split(":");
for (var i=0; i<unit_sizes_array.length; i++) {
unit_sizes_size = unit_sizes_array[i].split(",");
unit_sizes.push([parseInt(unit_sizes_size[0]), parseInt(unit_sizes_size[1])]);
}
return unit_sizes;
},
/**
* Simple helper method that extracts
* query parameters from the current request
*
* @method _get_query_variable
* @access private
* @return {String} the value
*/
_get_query_variable = function(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if (pair[0] == variable) {
return decodeURIComponent(pair[1]);
}
}
return '';
},
/**
* Simple helper method that extracts
* hash parameters from the current request
*
* @method _get_hash_variable
* @access private
* @return {String} the value
*/
_get_hash_variable = function() {
var length = window.location.hash.length;
return window.location.hash.substring(1, length);
},
/**
* Simple helper method that checks for the
* existence of a value within an array
* @method _in_array
* @access private
* @return void
*/
_in_array = function(needle, haystack) {
for (key in haystack) {
if (haystack[key] === needle) {
return true;
}
}
return false;
},
/**
* Push data param onto stack
* @access private
* @method _put_ad_param
* @return void
*/
_put_ad_param = function(key, value){
_ad_params[key] = value;
},
/**
* Get data param onto stack
* @access private
* @method _get_ad_param
* @return mixed
*/
_get_ad_param = function(key){
if(typeof(_ad_params[key]) == 'undefined') {
return;
}
return _ad_params[key];
},
/**
* Push a targeting parameter into the ad slots, see pushTargetingParam
* @access private
* @method _push_targeting_param
* @return void
*
* @todo In the future scope could be expanded to target individual ad
* slots, possibly by passing an array of sizes as the scope.
*/
_push_targeting_param = function (config) {
if(typeof(config['scope']) == 'undefined') {
config['scope'] = 'global';
}
if(typeof(config['persistent']) == 'undefined') {
config['persistent'] = false;
}
// DFP limits parameter names to 10 characters
var name = config['name'];
if(name.length > 10) {
_log_event('Targeting parameter name over 10 character limit: ' + name, 'warn');
name = name.slice(0, 10);
}
_targeting_params[name] = config;
},
/**
* Clears targeting parameters between refreshes, keeps any that have been
* set as persistant.
* @access private
* @method _clear_targeting_params
* @return void
*/
_clear_targeting_params = function () {
_cleared_targeting_params = {};
for (var name in _targeting_params) {
if(_targeting_params[name]['persistent']) {
_cleared_targeting_params[name] = _targeting_params[name];
}
}
_targeting_params = _cleared_targeting_params;
},
/**
* Log error depending on severity
* info - information on progress to enable debugging
* warn - caught errors e.g. Out Of Page units defined twice
* error - an error which meant ads could not be displayed
*
* Info logs to console in all environments but only in debug mode
* warn and error go to console in dev/qa/stage or ipc.utils.trackEvent in live
* @deprecated To be replaced with ipc.analytics.log see #64
* @access private
* @method _log_event
* @return void
*/
_log_event = function (message, severity){
if ( _is_debug() || ((_get_environment() !== 'live') && (severity !== "info")) ) {
console.log("DFP : " + severity + " : " + JSON.stringify(message));
if (typeof(ipcTags.debug) === 'undefined') {
ipcTags.debug = [];
}
ipcTags.debug.push({
'time': new Date(),
'severity': severity,
'message': JSON.stringify(message)
});
}
if ((_get_environment() === 'live') && (typeof(ipc.utils) !== "undefined") && (severity !== "info")) {
ipc.utils.trackEvent('DFP : ' + severity, message, window.location.href);
}
},
/**
* Beacon data as json
* @todo cross browser
*/
_beacon_data = function( endpoint, data, beacon_url ) {
if (typeof(beacon_url) !== 'undefined') {
_beacon_url = beacon_url;
}
if(_beacon_url != '') {
data.user_id = _user_id;
xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", _beacon_url + '/' + endpoint, true);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("data="+JSON.stringify(data));
}
},
/**
* Beacon data as json
* @todo cross browser
*/
_beacon_debug_data = function( beacon_url ) {
_beacon_data( 'debug.php', ipcTags.debug, beacon_url);
event_log = _googletag.getEventLog();
clean_event_log = [];
// This is unreliable as the varibale names can change see #65
for (var i = 0; i < event_log['H'].length; i++) {
event = event_log['H'][i]['Sa'];
clean_event_log.push(event);
}
_beacon_data( 'debug.php', clean_event_log, beacon_url);
},
/**
* Load google console
*/
_load_google_console = function( ) {
_googletag.console.mc();
},
/**
* Simple read cookie method
* @access private
* @method _read_cookie
* @return mixed, false if cookie does not exist
*/
_read_cookie = function (name) {
var value = (document.cookie.match('(^|; )'+name+'=([^;]*)')||0)[2];
if(typeof value == 'undefined') {
return false;
}
return value;
},
_is_object_empty = function(obj) {
for(var prop in obj) {
if(obj.hasOwnProperty(prop))
return false;
}
return true;
},
/**
* Get the users display width
* @access private
* @method _get_display_width
* @return int
*/
_get_display_width = function () {
var width = 0;
if (typeof window.innerWidth != 'undefined') {
// the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerWidth and window.innerHeight
width = window.innerWidth;
} else if (typeof document.documentElement != 'undefined' && typeof document.documentElement.clientWidth != 'undefined' && document.documentElement.clientWidth != 0) {
// IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document)
width = document.documentElement.clientWidth;
} else {
// older versions of IE
width = document.getElementsByTagName('body')[0].clientWidth;
}
return Number(width);
},
/**
* Get the users display height
* @access private
* @method _get_display_height
* @return int
*/
_get_display_height = function () {
var height = 0;
if (typeof window.innerHeight != 'undefined') {
// the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerHeight and window.innerHeight
height = window.innerHeight;
} else if (typeof document.documentElement != 'undefined' && typeof document.documentElement.clientHeight != 'undefined' && document.documentElement.clientHeight != 0) {
// IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document)
height = document.documentElement.clientHeight;
} else {
// older versions of IE
height = document.getElementsByTagName('body')[0].clientHeight;
}
return Number(height);
},
/**
* See writeData
* @access private
* @method _write_data
* @return void
*/
_write_data = function (config) {
if(typeof(config['persistent']) === 'undefined') {
config['persistent'] = false;
}
config['id'] += '_adloader';
var dataDiv = document.getElementById(config['id']);
var dataDivClass = 'adloader_written_data';
// If persistent do not overwrite
if(config['persistent']) {
if(dataDiv) {
return;
}
dataDivClass += '_persistent';
}
if(!dataDiv) {
var dataDiv = document.createElement('div');
dataDiv.setAttribute("id", config['id']);
dataDiv.setAttribute("class", dataDivClass);
}
dataDiv.innerHTML = config['data'];
document.body.appendChild(dataDiv);
},
/**
* Clears any non persistent written data during refreshes
* @access private
* @method _clear_written_data
* @return void
*/
_clear_written_data = function() {
elems = document.querySelectorAll('div.adloader_written_data');
if(elems.length > 0) {
for(var i; i<elems.length; i++) {
elems[i].parentNode.removeChild(elems[i]);
}
}
}
/**
* See applyStyling
* @access private
* @method _apply_takeover
* @return void
*/
_apply_styling = function(config) {
if(typeof(config['persistent']) === 'undefined') {
config['persistent'] = false;
}
// Find the element by id or tag name
var elem = false;
if(config['keyType'] == 'id') {
elem = document.getElementById(config['key']);
} else if(config['keyType'] == 'tag') {
elems = document.getElementsByTagName(config['key']);
if (elems.length > 0) {
elem = elems[0];
}
}
if(elem) {
// If persistent, do not alter class if it has already been altered
if(config['persistent']) {
var persistent_class = 'adloader_modified';
if(_has_class(elem, persistent_class)) {
return;
}
elem.className += persistent_class;
}
// apply styling
var style_str = '';
if(elem.getAttribute('cssText')) {
style_str = elem.getAttribute('cssText');
} else {
style_str = elem.getAttribute('style');
}
// combine all the styles into a string so it works crossbrowser
for(var styleName in config['styles']) {
style_str = style_str + styleName + ': ' + config['styles'][styleName] + '; ';
}
// crossbrowser checking, damn ie
if(elem.style.setAttribute) {
// ie
elem.style.setAttribute("cssText", style_str);
} else {
// decent browsers
elem.setAttribute("style", style_str);
}
// Add click event if needed
if(typeof(config['clickable']) !== 'undefined') {
clickable = function(event) {
event = _normalise_event(event);
// Ensure that the target element has been clicked,
// otherwise child elements will fire this event
var clicked = false;
if(config['keyType'] == 'id' && event.target.id == elem.id) {
clicked = true;
} else if(config['keyType'] == 'tag' && event.target.tagName == elem.tagName) {
clicked = true;
}
if(clicked) {
if(typeof(config['clickable']['function']) !== 'undefined') {
var arguments = [event];
if(typeof(config['clickable']['function_args']) !== 'undefined') {
arguments = arguments.concat(config['clickable']['function_args']);
}
config['clickable']['function'].apply(config['clickable']['function'], arguments);
} else if(typeof(config['clickable']['url']) !== 'undefined') {
if(typeof(config['clickable']['url_target']) !== 'undefined') {
window.open(config['clickable']['url'], config['clickable']['url_target']);
} else {
window.location = config['clickable']['url'];
}
}
}
}
_add_event(elem, 'click', clickable);
elem.style.cursor = 'pointer';
}
}
},
/**
* See applyFunction
* @access private
* @method _apply_function
* @return void
*/
_apply_function = function(config) {
if(typeof(config['persistent']) === 'undefined') {
config['persistent'] = false;
}
if(config['persistent']) {
if(typeof(_applied_functions[config['id']]) !== 'undefined') {
return;
}
_applied_functions[config['id']] = true;
}
// Execute the function with the supplied arguments
if(typeof(config['function_args']) === 'undefined') {
config['function_args'] = [];
}
config['function'].apply(config['function'], config['function_args']);
},
/**
* Check to see if an element has a class assigned to it
* @access private
* @method _has_class
* @param {dom element} the element to check
* @param {string} the class name to check for
* @return void
*/
_has_class = function(elem, target_class) {
if(elem.className.match(/(?:^|\s)+target_class+(?!\S)/)) {
return true;
}
return false;
},
_add_event = function(elem, event, fn) {
if(elem.attachEvent) { //Internet Explorer
elem.attachEvent("on" + event, function() {fn.call(elem);});
} else if(elem.addEventListener) { //Firefox & company
elem.addEventListener(event, fn, false); //don't need the 'call' trick because in FF everything already works in the right way
}
},
/**
* Stolen from jquery and adapted, normalises event objects
*/
_normalise_event = function(event) {
if(typeof(event) == 'undefined') {
event = window.event;
}
// Fix target property, if necessary
if ( !event.target ) {
// Fixes #1925 where srcElement might not be defined either
event.target = event.srcElement || document;
}
// check if target is a textnode (safari)
if ( event.target.nodeType === 3 ) {
event.target = event.target.parentNode;
}
// Add relatedTarget, if necessary
if ( !event.relatedTarget && event.fromElement ) {
event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
}
// Calculate pageX/Y if missing and clientX/Y available
if ( event.pageX == null && event.clientX != null ) {
var doc = document.documentElement,
body = document.body;
event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
}
// Add which for key events
if ( event.which == null && (event.charCode != null || event.keyCode != null) ) {
event.which = event.charCode != null ? event.charCode : event.keyCode;
}
// Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
if ( !event.metaKey && event.ctrlKey ) {
event.metaKey = event.ctrlKey;
}
// Add which for click: 1 === left; 2 === middle; 3 === right
// Note: button is not normalized, so don't use it
if ( !event.which && event.button !== undefined ) {
event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
}
return event;
}
return {
/**
* Sets up dfp parameters.
* All options are required apart from beaconUrl:
*
* {
* 'accountId': '',
* 'environment': ''
* 'googleTag': googletag,
* 'beaconUrl': ''
* }
*/
init: function(config) {
_dfp_account_id = config['accountId'];
_set_environment(config['environment']);
_googletag = config['googleTag'];
if(typeof(config['beaconUrl']) != 'undefined') {
_beacon_url = config['beaconUrl'];
}
_read_site_tags();
_load_third_parties();
if (_pending_third_parties.running == 0) {
_load_ads();
}
},
/**
* Sets the display mode
*/
setDisplayMode: function (display_mode) {
_set_display_mode(display_mode);
},
/**
* Gets the display mode
*/
getDisplayMode: function () {
return _get_display_mode();
},
/**
* Sets the provider
*/
setProvider: function (provider) {
_provider = _get_variable('adloader_provider', provider);
},
/**
* Get the provider
*/
getProvider: function () {
return _get_provider();
},
/**
* Sets the environment
*/
setEnvironment: function (environment) {
_set_environment(environment);
},
/**
* Get the environment
*/
getEnvironment: function () {
return _get_environment();
},
/**
* Refresh the ads
*/
refresh: function() {
_refresh();
},
/**
* @todo deprecated method, should be removed
*/
getAugmentedUrl: function (url) {
_log_event('Deprecated method called: getAugmentedUrl', 'error');
},
/**
* Set googletag
*/
setGoogletag: function (googletag) {
_googletag = googletag;
},
/**
* Read in site data from ipc tags json object
*/
readSiteTags: function () {
_read_site_tags();
},
/**
* Initiate third party code
*/
loadThirdParties: function () {
_load_third_parties();
},
/**
* Load the ads
* Only call the _load_ads function if there are no pending third parties, if there are pending third parties
* the _load_ads call will be made when they are all back or they have timed out
*/
loadAds: function () {
if (_pending_third_parties.running == 0) {
_load_ads();
}
},
/**
* Register third party load
*/
registerThirdPartyLoad: function (third_party) {
_register_third_party_load(third_party);
},
/**
* Register slots to load
*/
registerSlot: function (slot_id) {
_register_slot(slot_id);
},
/**
* @todo deprecated method, should be removed
*/
getBeaconUrl: function(host, namespace) {
_log_event('Deprecated method called: getBeaconUrl', 'error');
},
/**
* @todo deprecated method, should be removed
*/
pushParams: function (o) {
_log_event('Deprecated method called: pushParams', 'error');
},
/**
* Put parameters into the targeting stack.
* @access public
* @method ipc.adloader.putAdParam
* @static
* @param {string} The key of the parameter to return
* @return mixed or null if the key doesn't exist
*/
putAdParam: function(key, value) {
_put_ad_param(key, value);
},
/**
* Get parameters from the targeting stack.
* @access public
* @method ipc.adloader.getAdParam
* @static
* @param {string} The key of the parameter to return
* @return mixed or null if the key doesn't exist
*/
getAdParam: function(key) {
return _get_ad_param(key);
},
/**
* @todo deprecated method, should be removed
*/
pushTransferVariables: function (o) {
_log_event('Deprecated method called: pushTransferVariables', 'error');
},
/**
* @todo deprecated method, should be removed
*/
getTransferVariables: function () {
_log_event('Deprecated method called: getTransferVariables', 'error');
},
/**
* @todo deprecated method, should be removed
*/
injectTransferVariables: function (safe_vars) {
_log_event('Deprecated method called: injectTransferVariables', 'error');
},
/**
* @todo deprecated method, should be removed
*/
pushNvp: function (o) {
_log_event('Deprecated method called: pushNvp', 'error');
},
/**
* Push a targeting parameter into the ad slots, parameters can be either
* global
* @access public
* @method ipc.adloader.pushTargetingParam
* @static
* @param {object} config for the data, takes the form:
* {
* "name" : "" - {string} a unique identifier for the ad parameter.
* "value" : "" - {mixed} the value for the parameter.
* "persistent" : "" - {boolean} optional, default false, if true this parameter will not be cleared during ad refreshes.
* "scope" : "" - {string} optional, default global, the scope for the parameter, can be global or slots, if local then the parameter can have a different value between refreshes.
* "targets" : - {object} optional, this field can be used to target this parameter an individual slots or sizes. Overrides the scope parameter.
* {
* "slots" : - {array} optional, slot names to target with this parameter
* "sizes" : - {array} optional, size arrays to target with this parameter
* }
* }
* @return void
*
* @todo In the future scope could be expanded to target individual ad
* slots, possibly by passing an array of sizes as the scope.
*/
pushTargetingParam: function (config) {
_push_targeting_param(config);
},
/**
* @todo deprecated method, should be removed
*/
displayParamsToAdd: function () {
_log_event('Deprecated method called: displayParamsToAdd', 'error');
},
/*
* Push timings to the stack. These parameters are
* pushed on to the array which are later used to augment the url
* @access public
* @method ipc.adloader.logTiming
* @static
* @param {Object} takes two attributes 'name': [required], 'fn': [required]
* @return void
*/
logTiming: function(o) {
_log_timing(o);
},
/*
* Writes data to the page, to be called from iFrames
* @access public
* @method ipc.adloader.writeData
* @param {object} config for the data, takes the form:
* {
* 'id' : '' - {String} A unique identifier for the ad
* slot making the request
* 'data' : '' - {object} html to write into the page
* 'persistent' : '' - {boolean} Optional, if true further
* calls to write data from the same source will be ignored
* }
* @return void
*/
writeData: function(config) {
_write_data(config);
},
/**
* Applies styling to an element targeted either by its ID or it’s tag
* (eg.body). Styles are is a key/value object which can contain standard
* js element.style options
* (https://developer.mozilla.org/en-US/docs/CSS/CSS_Reference).
* @access public
* @method ipc.adloader.applyStyling
* @param {object} config for the styling, takes the form:
* {
* 'key' : '' - {string} tag name or element id
* 'keyType' : '' - {string} id/tag
* 'persistent' : '' - {boolean} optional, true or false, will
* not run on refreshes if true
* 'clickable' : - {object} Optional, config for making
* the target element clickable
* {
* 'url' : - {string} target url to trigger when
* clicked
* 'url_target' : - {string} optional, defaults to blank
* 'function' : - {function} function to trigger when
* clicked
* 'function_args' : - {array} optional, function arguments
* }
* 'styles' : - {object} required,
* {
* 'backgroundColor' : ''
* 'backgroundImage' : ''
* etc.
* }
* }
* @return void
*/
applyStyling: function(config) {
_apply_styling(config);
},
/**
* Executes a function on the parent page, could be a function that is
* on page eg. inskin functions, or it could be an anonymous function.
* @access public
* @method ipc.adloader.applyFunction
* @param {object} config for the function, takes the form:
* {
* 'id' : '', - {string} just a unique identifier
* 'persistent' : '', - {boolean} optional, true or false, will
* not run on refreshes if true
* 'function' : '', - {object} anonymous function, or reference
* 'function_args' : '' - {array} optional, array of arguments for
* function
* }
* @return void
*/
applyFunction: function(config) {
_apply_function(config);
},
/**
* Debug the debug data away
* @access public
* @method ipc.adloader.beaconDebugData
* @param {string} endpoint, default to _beacon_url
* @return void
*/
beaconDebugData: function(beacon_url) {
_beacon_debug_data(beacon_url);
},
/**
* Load google console
* @access public
* @method ipc.adloader.loadGoogleConsole
* @return void
*/
loadGoogleConsole: function() {
_load_google_console();
},
/**
* Log an event
* @access public
* @method ipc.adtech.loadEvent
* @return void
*/
logEvent: function(message, severity) {
_log_event(message, severity);
}
};
}();
/**
* This class should contain functions that deal with ad targeting
*/
ipc.adloader.targeting = function () {
/**
* Reads a cookie value and pushes it in as a targeting
* parameter.
*
* Type can be one of two values:
*
* 'value': The cookie's literal value will be pushed, or false if the cookie does not exist
* 'boolean': True/false based on the existence of the cookie
*
* {
* 'name': 'ric_sc',
* 'cookie_name': '__ric_sc1123',
* 'type': 'value'
* }
*
* @access private
* @method _add_cookie_targeting_param
* @param {object} config for this cookie targeting parameter
* @return void
*/
_add_cookie_targeting_param = function(config) {
var targeting_config = {
'name': config['name'],
'value': 'false',
'persistent': true
};
var cookie_value = ipc.adloader.utils.readCookie(config['cookie_name']);
if(cookie_value) {
if(config['type'] == 'value') {
targeting_config.value = ipc.adloader.utils.slugify(cookie_value);
} else if(config['type'] == 'boolean') {
targeting_config.value = 'true';
}
}
ipc.adloader.pushTargetingParam(targeting_config);
}
return {
/**
* Reads ipcTags to see if any cookies need to be added as targeting parameters
* @access public
* @method addCookieTargeting
* @return void
*/
addCookieTargeting: function() {
if(typeof(window.ipcTags.dfp_config.read_cookies) == 'undefined') {
return;
}
var read_cookies = window.ipcTags.dfp_config.read_cookies;
for (var name in read_cookies) {
_add_cookie_targeting_param(read_cookies[name]);
}
}
};
}();
/**
* This class should contain helper functions, usually for performing everyday tasks or xbrowser functionality
*/
ipc.adloader.utils = function () {
return {
/**
* Simple read cookie method
* @access private
* @method _read_cookie
* @return mixed, false if cookie does not exist
*/
readCookie: function (name) {
var value = (document.cookie.match('(^|; )'+name+'=([^;]*)')||0)[2];
if(typeof value == 'undefined') {
return false;
}
return value;
},
/**
* Slugifies strings for use as targeting variables
*
* @method _slugify
* @access private
* @param {String} The unformatted string
* @return {String} The formatted string
*/
slugify: function(str) {
str = String(str);
str = str.toLowerCase();
str = str.replace(/[^\w\s]/g, ' ');
str = str.replace(/\ +/g, '_');
str = str.replace(/\-$/g, '');
str = str.replace(/^\-/g, '');
return str;
}
};
}();
if( typeof document.querySelector == "undefined") {
// IE7 support for querySelectorAll in 274 bytes. Supports multiple / grouped selectors and the attribute selector with a "for" attribute. http://www.codecouch.com/
(function(d,s){d=document,s=d.createStyleSheet();d.querySelectorAll=function(r,c,i,j,a){a=d.all,c=[],r=r.replace(/\[for\b/gi,'[htmlFor').split(',');for(i=r.length;i--;){s.addRule(r[i],'k:v');for(j=a.length;j--;)a[j].currentStyle.k&&c.push(a[j]);s.removeRule(0)}return c}})()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment