/*! * Matomo - free/libre analytics platform * * @link https://matomo.org * @license https://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 = { /** * 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, isWidgetizedDashboard: function() { return broadcast.getValueFromUrl('module') == 'Widgetize' && broadcast.getValueFromUrl('moduleToWidgetize') == 'Dashboard'; }, isWidgetizeRequestWithoutSession: function() { // whenever a token_auth is set in the URL, we assume a widget or page is tried to be shown widgetized. return broadcast.getValueFromUrl('token_auth') != '' && broadcast.getValueFromUrl('force_api_session') != '1'; }, /** * Returns if the current page is the login page * @return {boolean} */ isLoginPage: function() { return !!$('body#loginPage').length; }, /** * Returns if the current page is the no data page * @return {boolean} */ isNoDataPage: function() { return !!$('body#site-without-data').length; }, /** * 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 * @param {array} paramsToRemove Optional parameters to remove from the URL. * @return {void} */ propagateNewPage: function (str, showAjaxLoading, strHash, paramsToRemove, wholeNewUrl) { // abort all existing ajax requests globalAjaxQueue.abort(); paramsToRemove = paramsToRemove || []; 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; var newUrl; if (!wholeNewUrl) { // remove all array query params that are currently set. if we don't do this the array parameters we add // just get added to the existing parameters. params_vals.forEach(function (param) { if (/\[]=/.test(decodeURIComponent(param))) { var paramName = decodeURIComponent(param).split('[]=')[0]; removeParam(paramName); } }); // remove parameters if needed paramsToRemove.forEach(function (paramName) { removeParam(paramName); }); // update/add parameters based on whether the parameter is an array param or not params_vals.forEach(function (param) { if (!param.length) { return; // updating with empty string would destroy some values } if (/\[]=/.test(decodeURIComponent(param))) { // array param value currentSearchStr = broadcast.addArrayParamValue(param, currentSearchStr); if (currentHashStr.length !== 0) { currentHashStr = broadcast.addArrayParamValue(param, currentHashStr); } } else { // update both the current search query and hash string currentSearchStr = broadcast.updateParamValue(param, currentSearchStr); if (currentHashStr.length !== 0) { currentHashStr = broadcast.updateParamValue(param, 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. newUrl = currentSearchStr + currentHashStr; } else { newUrl = wholeNewUrl; } if (oldUrl == newUrl) { window.location.reload(); } else { this.forceReload = true; window.location.href = newUrl; } return false; function removeParam(paramName) { var paramRegex = new RegExp(paramName + '(\\[]|%5B%5D)?=[^&?#]*&?', 'gi'); currentSearchStr = currentSearchStr.replace(paramRegex, ''); currentHashStr = currentHashStr.replace(paramRegex, ''); } }, /************************************************* * * 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) || broadcast.getParamValue(encodeURIComponent(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 != '' || urlStr.indexOf(paramName + '=') !== -1) { // 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 + '|' + encodeURIComponent(paramName) + ')=' + valFromUrl, 'ig'); } urlStr = urlStr.replace(regToBeReplace, newParamValue); } else if (newParamValue != '') { urlStr += (urlStr == '') ? newParamValue : '&' + newParamValue; } return urlStr; }, /** * Adds a query param value. Use it to add an array parameter value where you don't want to remove an existing value first. * * @param newParamValue * @param urlStr */ addArrayParamValue: function (newParamValue, urlStr) { if (urlStr.indexOf('?') === -1) { urlStr += '?'; } else { urlStr += '&'; } return urlStr + newParamValue; }, /** * 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 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]; } } var MatomoUrl = window.CoreHome.MatomoUrl; MatomoUrl.updateHash( Object.assign({}, MatomoUrl.hashParsed.value, { popover }), ); }, /** * 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 Matomo 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) { url = url.slice(url.indexOf('?')+1); } var urlPieces = url.split('&'); // look for the latest occurrence of the parameter if available for (var i=urlPieces.length-1; i>=0; i--) { if (urlPieces[i].indexOf(lookFor) === 0) { return getSingleValue(urlPieces[i]); } } // gather parameter array if available lookFor = param + '[]='; var result = []; for (var j=0; j!@\$\.()=,;0-9a-zA-Z]/gi, ''); } return value; } }, /** * 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 = window.location.search; } return searchString; } }; window.broadcast = broadcast; // hack to get broadcast to work in vue (jest) tests