/*! * Piwik - free/libre analytics platform * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ /** * broadcast object is to help maintain a hash for link clicks and ajax calls * so we can have back button and refresh button working. * * @type {object} */ var broadcast = { /** * Initialisation state * @type {Boolean} */ _isInit: false, /** * Last known hash url without popover parameter */ currentHashUrl: false, /** * Last known popover parameter */ currentPopoverParameter: false, /** * Callbacks for popover parameter change */ popoverHandlers: [], /** * Holds the stack of popovers opened in sequence. When closing a popover, the last popover in the stack * is opened (if any). */ popoverParamStack: [], /** * Force reload once */ forceReload: false, /** * Suppress content update on hash changing */ updateHashOnly: false, /** * Initializes broadcast object * * @deprecated in 3.2.2, will be removed in Piwik 4 * * @return {void} */ init: function (noLoadingMessage) { if (broadcast._isInit) { return; } broadcast._isInit = true; angular.element(document).injector().invoke(function (historyService) { historyService.init(); }); if(noLoadingMessage != true) { piwikHelper.showAjaxLoading(); } }, /** * ========== PageLoad function ================= * This function is called when: * 1. after calling $.history.init(); * 2. after calling $.history.load(); //look at broadcast.changeParameter(); * 3. after pushing "Go Back" button of a browser * * * Note: the method is manipulated in Overlay/javascripts/Piwik_Overlay.js - keep this in mind when making changes. * * @deprecated since 3.2.2, will be removed in Piwik 4 * * @param {string} hash to load page with * @return {void} */ pageload: function (hash) { broadcast.init(); // Unbind any previously attached resize handlers $(window).off('resize'); // do not update content if it should be suppressed if (broadcast.updateHashOnly) { broadcast.updateHashOnly = false; return; } // hash doesn't contain the first # character. if (hash && 0 === (''+hash).indexOf('/')) { hash = (''+hash).substr(1); } if (hash) { if (/^popover=/.test(hash)) { var hashParts = [ '', hash.replace(/^popover=/, '') ]; } else { var hashParts = hash.split('&popover='); } var hashUrl = hashParts[0]; var popoverParam = ''; if (hashParts.length > 1) { popoverParam = hashParts[1]; // in case the $ was encoded (e.g. when using copy&paste on urls in some browsers) popoverParam = decodeURIComponent(popoverParam); // revert special encoding from broadcast.propagateNewPopoverParameter() popoverParam = popoverParam.replace(/\$/g, '%'); popoverParam = decodeURIComponent(popoverParam); } var pageUrlUpdated = (popoverParam == '' || (broadcast.currentHashUrl !== false && broadcast.currentHashUrl != hashUrl)); var popoverParamUpdated = (popoverParam != '' && hashUrl == broadcast.currentHashUrl); if (broadcast.currentHashUrl === false) { // new page load pageUrlUpdated = true; popoverParamUpdated = (popoverParam != ''); } if (!broadcast.isWidgetizedDashboard() && (pageUrlUpdated || broadcast.forceReload)) { Piwik_Popover.close(); if (hashUrl != broadcast.currentHashUrl || broadcast.forceReload) { // restore ajax loaded state broadcast.loadAjaxContent(hashUrl); // make sure the "Widgets & Dashboard" is deleted on reload $('.top_controls .dashboard-manager').hide(); $('#dashboardWidgetsArea').dashboard('destroy'); // remove unused controls require('piwik/UI').UIControl.cleanupUnusedControls(); } } broadcast.forceReload = false; broadcast.currentHashUrl = hashUrl; broadcast.currentPopoverParameter = popoverParam; Piwik_Popover.close(); if (popoverParamUpdated) { var popoverParamParts = popoverParam.split(':'); var handlerName = popoverParamParts[0]; popoverParamParts.shift(); var param = popoverParamParts.join(':'); if (typeof broadcast.popoverHandlers[handlerName] != 'undefined' && !broadcast.isLoginPage()) { broadcast.popoverHandlers[handlerName](param); } } } else { // start page Piwik_Popover.close(); if (!broadcast.isWidgetizedDashboard()) { $('.pageWrap #content:not(.admin)').empty(); } } }, isWidgetizedDashboard: function() { return broadcast.getValueFromUrl('module') == 'Widgetize' && broadcast.getValueFromUrl('moduleToWidgetize') == 'Dashboard'; }, /** * Returns if the current page is the login page * @return {boolean} */ isLoginPage: function() { return !!$('body#loginPage').length; }, /** * propagateAjax -- update hash values then make ajax calls. * example : * 1) View keywords report * 2) Main menu li also goes through this function. * * Will propagate your new value into the current hash string and make ajax calls. * * NOTE: this method will only make ajax call and replacing main content. * * @deprecated in 3.2.2, will be removed in Piwik 4. * * @param {string} ajaxUrl querystring with parameters to be updated * @param {boolean} [disableHistory] the hash change won't be available in the browser history * @return {void} */ propagateAjax: function (ajaxUrl, disableHistory) { broadcast.init(); // abort all existing ajax requests globalAjaxQueue.abort(); // available in global scope var currentHashStr = broadcast.getHash(); ajaxUrl = ajaxUrl.replace(/^\?|&#/, ''); var params_vals = ajaxUrl.split("&"); for (var i = 0; i < params_vals.length; i++) { currentHashStr = broadcast.updateParamValue(params_vals[i], currentHashStr); } // if the module is not 'Goals', we specifically unset the 'idGoal' parameter // this is to ensure that the URLs are clean (and that clicks on graphs work as expected - they are broken with the extra parameter) var action = broadcast.getParamValue('action', currentHashStr); if (action != 'goalReport' && action != 'ecommerceReport' && action != 'products' && action != 'sales' && (''+ ajaxUrl).indexOf('&idGoal=') === -1) { currentHashStr = broadcast.updateParamValue('idGoal=', currentHashStr); } // unset idDashboard if use doesn't display a dashboard var module = broadcast.getParamValue('module', currentHashStr); if (module != 'Dashboard') { currentHashStr = broadcast.updateParamValue('idDashboard=', currentHashStr); } if (module != 'CustomDimensions') { currentHashStr = broadcast.updateParamValue('idDimension=', currentHashStr); } if (disableHistory) { var newLocation = window.location.href.split('#')[0] + '#?' + currentHashStr; // window.location.replace changes the current url without pushing it on the browser's history stack window.location.replace(newLocation); } else { // Let history know about this new Hash and load it. broadcast.forceReload = true; angular.element(document).injector().invoke(function (historyService) { historyService.load(currentHashStr); }); } }, /** * Returns the current hash with updated parameters that were provided in ajaxUrl * * Parameters like idGoal and idDashboard will be automatically reset if the won't be relevant anymore * * NOTE: this method does not issue any ajax call, but returns the hash instead * * @param {string} ajaxUrl querystring with parameters to be updated * @return {string} current hash with updated parameters */ buildReportingUrl: function (ajaxUrl) { // available in global scope var currentHashStr = broadcast.getHash(); ajaxUrl = ajaxUrl.replace(/^\?|&#/, ''); var params_vals = ajaxUrl.split("&"); for (var i = 0; i < params_vals.length; i++) { currentHashStr = broadcast.updateParamValue(params_vals[i], currentHashStr); } // if the module is not 'Goals', we specifically unset the 'idGoal' parameter // this is to ensure that the URLs are clean (and that clicks on graphs work as expected - they are broken with the extra parameter) var action = broadcast.getParamValue('action', currentHashStr); if (action != 'goalReport' && action != 'ecommerceReport' && action != 'products' && action != 'sales') { currentHashStr = broadcast.updateParamValue('idGoal=', currentHashStr); } // unset idDashboard if use doesn't display a dashboard var module = broadcast.getParamValue('module', currentHashStr); if (module != 'Dashboard') { currentHashStr = broadcast.updateParamValue('idDashboard=', currentHashStr); } return '#' + currentHashStr; }, /** * propagateNewPage() -- update url value and load new page, * Example: * 1) We want to update idSite to both search query and hash then reload the page, * 2) update period to both search query and hash then reload page. * * Expecting: * str = "param1=newVal1¶m2=newVal2"; * * NOTE: This method will refresh the page with new values. * * @param {string} str url with parameters to be updated * @param {boolean} [showAjaxLoading] whether to show the ajax loading gif or not. * @param {string} strHash additional parameters that should be updated on the hash * @return {void} */ propagateNewPage: function (str, showAjaxLoading, strHash) { // abort all existing ajax requests globalAjaxQueue.abort(); if (typeof showAjaxLoading === 'undefined' || showAjaxLoading) { piwikHelper.showAjaxLoading(); } var params_vals = str.split("&"); // available in global scope var currentSearchStr = window.location.search; var currentHashStr = broadcast.getHashFromUrl(); if (!currentSearchStr) { currentSearchStr = '?'; } var oldUrl = currentSearchStr + currentHashStr; for (var i = 0; i < params_vals.length; i++) { if(params_vals[i].length == 0) { continue; // updating with empty string would destroy some values } // update both the current search query and hash string currentSearchStr = broadcast.updateParamValue(params_vals[i], currentSearchStr); if (currentHashStr.length != 0) { currentHashStr = broadcast.updateParamValue(params_vals[i], currentHashStr); } } var updatedUrl = new RegExp('&updated=([0-9]+)'); var updatedCounter = updatedUrl.exec(currentSearchStr); if (!updatedCounter) { currentSearchStr += '&updated=1'; } else { updatedCounter = 1 + parseInt(updatedCounter[1]); currentSearchStr = currentSearchStr.replace(new RegExp('(&updated=[0-9]+)'), '&updated=' + updatedCounter); } if (strHash && currentHashStr.length != 0) { var params_hash_vals = strHash.split("&"); for (var i = 0; i < params_hash_vals.length; i++) { currentHashStr = broadcast.updateParamValue(params_hash_vals[i], currentHashStr); } } // Now load the new page. var newUrl = currentSearchStr + currentHashStr; var $rootScope = piwikHelper.getAngularDependency('$rootScope'); if ($rootScope) { $rootScope.$on('$locationChangeStart', function (event) { if (event) { event.preventDefault(); } }) } if (oldUrl == newUrl) { window.location.reload(); } else { this.forceReload = true; window.location.href = newUrl; } return false; }, /************************************************* * * Broadcast Supporter Methods: * *************************************************/ /** * updateParamValue(newParamValue,urlStr) -- Helping propagate functions to update value to url string. * eg. I want to update date value to search query or hash query * * Expecting: * urlStr : A Hash or search query string. e.g: module=whatever&action=index=date=yesterday * newParamValue : A param value pair: e.g: date=2009-05-02 * * Return module=whatever&action=index&date=2009-05-02 * * @param {string} newParamValue param to be updated * @param {string} urlStr url to be updated * @return {string} urlStr with updated param */ updateParamValue: function (newParamValue, urlStr) { var p_v = newParamValue.split("="); var paramName = p_v[0]; var valFromUrl = broadcast.getParamValue(paramName, urlStr); // if set 'idGoal=' then we remove the parameter from the URL automatically (rather than passing an empty value) var paramValue = p_v[1]; if (paramValue == '') { newParamValue = ''; } var getQuotedRegex = function(str) { return (str+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); }; if (valFromUrl != '') { // replacing current param=value to newParamValue; valFromUrl = getQuotedRegex(valFromUrl); var regToBeReplace = new RegExp(paramName + '=' + valFromUrl, 'ig'); if (newParamValue == '') { // if new value is empty remove leading &, as well regToBeReplace = new RegExp('[\&]?' + paramName + '=' + valFromUrl, 'ig'); } urlStr = urlStr.replace(regToBeReplace, newParamValue); } else if (newParamValue != '') { urlStr += (urlStr == '') ? newParamValue : '&' + newParamValue; } return urlStr; }, /** * Loads a popover by adding a 'popover' query parameter to the current URL and * indirectly executing the popover handler. * * This function should be called to open popovers that can be opened by URL alone. * That is, if you want users to be able to copy-paste the URL displayed when a popover * is open into a new browser window/tab and have the same popover open, you should * call this function. * * In order for this function to open a popover, there must be a popover handler * associated with handlerName. To associate one, call broadcast.addPopoverHandler. * * @param {String} handlerName The name of the popover handler. * @param {String} value The String value that should be passed to the popover * handler. */ propagateNewPopoverParameter: function (handlerName, value) { var $location = angular.element(document).injector().get('$location'); var popover = ''; if (handlerName && '' != value && 'undefined' != typeof value) { popover = handlerName + ':' + value; // between jquery.history and different browser bugs, it's impossible to ensure // that the parameter is en- and decoded the same number of times. in order to // make sure it doesn't change, we have to manipulate the url encoding a bit. popover = encodeURIComponent(popover); popover = popover.replace(/%/g, '\$'); broadcast.popoverParamStack.push(popover); } else { broadcast.popoverParamStack.pop(); if (broadcast.popoverParamStack.length) { popover = broadcast.popoverParamStack[broadcast.popoverParamStack.length - 1]; } } $location.search('popover', popover); setTimeout(function () { angular.element(document).injector().get('$rootScope').$apply(); }, 1); }, /** * Resets the popover param stack ensuring when a popover is closed, no new popover will * be loaded. */ resetPopoverStack: function () { broadcast.popoverParamStack = []; }, /** * Adds a handler for the 'popover' query parameter. * * @see broadcast#propagateNewPopoverParameter * * @param {String} handlerName The handler name, eg, 'visitorProfile'. Should identify * the popover that the callback will open up. * @param {Function} callback This function should open the popover. It should take * one string parameter. */ addPopoverHandler: function (handlerName, callback) { broadcast.popoverHandlers[handlerName] = callback; }, /** * Loads the given url with ajax and replaces the content * * Note: the method is replaced in Overlay/javascripts/Piwik_Overlay.js - keep this in mind when making changes. * * @param {string} urlAjax url to load * @return {Boolean} */ loadAjaxContent: function (urlAjax) { if(broadcast.getParamValue('module', urlAjax) == 'API') { broadcast.lastUrlRequested = null; $('#content').html("Loading content from the API and displaying it within Piwik is not allowed."); piwikHelper.hideAjaxLoading(); return false; } piwikHelper.hideAjaxError('loadingError'); piwikHelper.showAjaxLoading(); $('#content').empty(); $("object").remove(); urlAjax = urlAjax.match(/^\?/) ? urlAjax : "?" + urlAjax; broadcast.lastUrlRequested = urlAjax; function sectionLoaded(content, status, request) { if (request) { var responseHeader = request.getResponseHeader('Content-Type'); if (responseHeader && 0 <= responseHeader.toLowerCase().indexOf('json')) { var message = 'JSON cannot be displayed for'; if (this.getParams && this.getParams['module']) { message += ' module=' + this.getParams['module']; } if (this.getParams && this.getParams['action']) { message += ' action=' + this.getParams['action']; } $('#content').text(message); piwikHelper.hideAjaxLoading(); return; } } // if content is whole HTML document, do not show it, otherwise recursive page load could occur var htmlDocType = '= 0) { var endStr = url.indexOf("&", startStr); if (endStr == -1) { endStr = url.length; } var value = url.substring(startStr + param.length + 1, endStr); // we sanitize values to add a protection layer against XSS // &segment= value is not sanitized, since segments are designed to accept any user input if(param != 'segment') { value = value.replace(/[^_%~\*\+\-\<\>!@\$\.()=,;0-9a-zA-Z]/gi, ''); } return value; } else { return ''; } }, /** * Returns the hash without the starting # * @return {string} hash part of the current url */ getHash: function () { return broadcast.getHashFromUrl().replace(/^#/, '').split('#')[0]; }, /** * Removes the hash portion of a URL and returns the rest. * * @param {string} url * @return {string} url w/o hash */ _removeHashFromUrl: function (url) { var searchString = ''; if (url) { var urlParts = url.split('#'); searchString = urlParts[0]; } else { searchString = location.search; } return searchString; } };