/* Copyright(c) 2013-2018 Rhizome and Ilya Kreymer. Released under the GNU General Public License. This file is part of pywb, https://github.com/webrecorder/pywb pywb is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. pywb is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with pywb. If not, see . */ /** * @param {Object} content_info - Information about the contents to be replayed */ function ContentFrame(content_info) { if (!(this instanceof ContentFrame)) return new ContentFrame(content_info); this.last_inner_hash = window.location.hash; this.last_url = content_info.url; this.last_ts = content_info.request_ts; this.content_info = content_info; // bind event callbacks this.outer_hash_changed = this.outer_hash_changed.bind(this); this.handle_event = this.handle_event.bind(this); this.wbBanner = null; this.checkBannerToId = null; window.addEventListener('hashchange', this.outer_hash_changed, false); window.addEventListener('message', this.handle_event); if (document.readyState === 'complete') { this.init_iframe(); } else { document.addEventListener('DOMContentLoaded', this.init_iframe.bind(this), {once: true}); } window.__WB_pmw = function (win) { this.pm_source = win; return this; }; } /** * @desc Initializes the replay iframe. If a banner exists (exposed on window as WBBanner) * then the init function of the banner is called. */ ContentFrame.prototype.init_iframe = function () { if (typeof (this.content_info.iframe) === 'string') { this.iframe = document.querySelector(this.content_info.iframe); } else { this.iframe = this.content_info.iframe; } if (!this.iframe) { console.warn('no iframe found ' + this.content_info.iframe + ' found'); return; } this.extract_prefix(); if (window.WBBanner) { this.wbBanner = window.WBBanner; this.wbBanner.init(); } this.load_url(this.content_info.url, this.content_info.request_ts); }; /** * @desc Initializes the prefixes used to load the pages to be replayed */ ContentFrame.prototype.extract_prefix = function () { this.app_prefix = this.content_info.app_prefix || this.content_info.prefix; this.content_prefix = this.content_info.content_prefix || this.content_info.prefix; if (this.app_prefix && this.content_prefix) { return; } var inx = window.location.href.indexOf(this.content_info.url); if (inx < 0) { inx = window.location.href.indexOf('/http') + 1; if (inx <= 0) { inx = window.location.href.indexOf('///') + 1; if (inx <= 0) { console.warn('No Prefix Found!'); } } } this.prefix = window.location.href.substr(0, inx); this.app_prefix = this.app_prefix || this.prefix; this.content_prefix = this.content_prefix || this.prefix; }; /** * @desc Returns an absolute URL (with correct prefix and replay modifier) given * the replayed pages URL and optional timestamp and content_url * @param {string} url - The URL of the replayed page * @param {?string} ts - The timestamp of the replayed page * @param {?boolean} content_url - Is the abs URL to be constructed using the content_prefix or app_prefix * @returns {string} */ ContentFrame.prototype.make_url = function (url, ts, content_url) { var mod, prefix; if (content_url) { mod = 'mp_'; prefix = this.content_prefix; } else { mod = ''; prefix = this.app_prefix; } if (ts || mod) { mod += '/'; } if (ts) { return prefix + ts + mod + url; } else { return prefix + mod + url; } }; /** * @desc Handles and routes all messages received from the replay iframe. * @param {MessageEvent} event - A message event potentially containing a message from the replay iframe */ ContentFrame.prototype.handle_event = function (event) { var frame_win = this.iframe.contentWindow; if (event.source === window.parent) { // Pass to replay frame frame_win.postMessage(event.data, '*'); } else if (event.source === frame_win) { // Check if iframe url change message if (typeof (event.data) === 'object' && event.data['wb_type']) { this.handle_message(event); } else { // Pass to parent window.parent.postMessage(event.data, '*'); } } }; /** * @desc Handles messages intended for the content frame (indicated by data.wb_type). If a banner * is exposed, calls the onMessage function of the exposed banner. * @param {MessageEvent} event - The message event containing a message from the replay iframe */ ContentFrame.prototype.handle_message = function (event) { if (this.wbBanner) { this.wbBanner.onMessage(event); } var state = event.data; var type = state.wb_type; if (type === 'load' || type === 'replace-url') { this.set_url(state); } else if (type === 'hashchange') { this.inner_hash_changed(state); } }; /** * @desc Updates the URL of the top frame * @param {Object} state - The contents of a message rreceived from the replay iframe */ ContentFrame.prototype.set_url = function (state) { if (state.url && (state.url !== this.last_url || state.request_ts !== this.last_ts)) { var new_url = this.make_url(state.url, state.request_ts, false); window.history.replaceState(state, '', new_url); this.last_url = state.url; this.last_ts = state.request_ts; } }; /** * @desc Checks to see if the banner is still indicating the replay iframe is still loading * 2 seconds after the load event is fired by the replay iframe. If the banner is still * indicating the replayed page is loading. Updates the displayed information using * newURL and newTS * @param {string} newUrl - The new URL of the replay iframe * @param {?string} newTs - The new timestamp of the replay iframe. Is falsy if * operating in live mode */ ContentFrame.prototype.initBannerUpdateCheck = function (newUrl, newTs) { if (!this.wbBanner) return; var contentFrame = this; var replayIframeLoaded = function () { contentFrame.iframe.removeEventListener('load', replayIframeLoaded); contentFrame.checkBannerToId = setTimeout(function () { contentFrame.checkBannerToId = null; if (contentFrame.wbBanner.stillIndicatesLoading()) { contentFrame.wbBanner.updateCaptureInfo( newUrl, newTs, contentFrame.content_prefix.indexOf('/live') !== -1 ); } }, 2000); }; if (this.checkBannerToId) { clearTimeout(this.checkBannerToId); } this.iframe.addEventListener('load', replayIframeLoaded); }; /** * @desc Navigates the replay iframe to a newURL and if a banner is exposed * the initBannerUpdateCheck function is called. * @param {string} newUrl - The new URL of the replay iframe * @param {?string} newTs - The new timestamp of the replay iframe. Is falsy if * operating in live mode */ ContentFrame.prototype.load_url = function (newUrl, newTs) { this.iframe.src = this.make_url(newUrl, newTs, true); if (this.wbBanner) { this.initBannerUpdateCheck(newUrl, newTs); } }; /** * @desc Updates this frames hash to the one inside the replay iframe * @param {Object} state - The contents of message received from the replay iframe */ ContentFrame.prototype.inner_hash_changed = function (state) { if (window.location.hash !== state.hash) { window.location.hash = state.hash; } this.last_inner_hash = state.hash; }; /** * @desc Updates the hash of the replay iframe on a hash change in this frame * @param event */ ContentFrame.prototype.outer_hash_changed = function (event) { if (window.location.hash === this.last_inner_hash) { return; } if (this.iframe) { var message = {'wb_type': 'outer_hashchange', 'hash': window.location.hash}; this.iframe.contentWindow.postMessage(message, '*', undefined, true); } }; /** * @desc Cleans up any event listeners added by the content frame */ ContentFrame.prototype.close = function () { window.removeEventListener('hashchange', this.outer_hash_changed); window.removeEventListener('message', this.handle_event); };