/*! * Matomo - free/libre analytics platform * * @link https://matomo.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ /** * global ajax queue * * @type {Array} array holding XhrRequests with automatic cleanup */ var globalAjaxQueue = []; globalAjaxQueue.active = 0; /** * Removes all finished requests from the queue. * * @return {void} */ globalAjaxQueue.clean = function () { for (var i = this.length; i--;) { if (!this[i] || this[i].readyState == 4) { this.splice(i, 1); } } }; /** * Extend Array.push with automatic cleanup for finished requests * * @return {Object} */ globalAjaxQueue.push = function () { this.active += arguments.length; // cleanup ajax queue this.clean(); // call original array push return Array.prototype.push.apply(this, arguments); }; /** * Extend with abort function to abort all queued requests * * @return {void} */ globalAjaxQueue.abort = function () { // abort all queued requests for (var i = this.length; i--;) { this[i] && this[i].abort && this[i].abort(); // abort if possible } // remove all elements from array this.splice(0, this.length); this.active = 0; }; /** * Global ajax helper to handle requests within piwik * * @type {Object} * @constructor */ function ajaxHelper() { /** * Format of response * @type {String} */ this.format = 'json'; /** * A timeout for the request which will override any global timeout * @type {Boolean} */ this.timeout = null; /** * Callback function to be executed on success */ this.callback = function () {}; /** * Use this.callback if an error is returned * @type {Boolean} */ this.useRegularCallbackInCaseOfError = false; /** * Callback function to be executed on error */ this.errorCallback; this.withToken = false; /** * Callback function to be executed on complete (after error or success) */ this.completeCallback = function () {}; /** * Params to be passed as GET params * @type {Object} * @see ajaxHelper._mixinDefaultGetParams */ this.getParams = {}; /** * Base URL used in the AJAX request. Can be set by setUrl. * * It is set to '?' rather than 'index.php?' to increase chances that it works * including for users who have an automatic 301 redirection from index.php? to ? * POST values are missing when there is such 301 redirection. So by by-passing * this 301 redirection, we avoid this issue. * * @type {String} * @see ajaxHelper.setUrl */ this.getUrl = '?'; /** * Params to be passed as GET params * @type {Object} * @see ajaxHelper._mixinDefaultPostParams */ this.postParams = {}; /** * Element to be displayed while loading * @type {String} */ this.loadingElement = null; /** * Element to be displayed on error * @type {String} */ this.errorElement = '#ajaxError'; /** * Handle for current request * @type {XMLHttpRequest} */ this.requestHandle = null; this.defaultParams = ['idSite', 'period', 'date', 'segment']; /** * Adds params to the request. * If params are given more then once, the latest given value is used for the request * * @param {object} params * @param {string} type type of given parameters (POST or GET) * @return {void} */ this.addParams = function (params, type) { if (typeof params == 'string') { params = broadcast.getValuesFromUrl(params); } var arrayParams = ['compareSegments', 'comparePeriods', 'compareDates']; for (var key in params) { if (arrayParams.indexOf(key) !== -1 && !params[key] ) { continue; } if(type.toLowerCase() == 'get') { this.getParams[key] = params[key]; } else if(type.toLowerCase() == 'post') { this.postParams[key] = params[key]; } } }; this.withTokenInUrl = function () { this.withToken = true; }; /** * Sets the base URL to use in the AJAX request. * * @param {string} url */ this.setUrl = function (url) { this.addParams(broadcast.getValuesFromUrl(url), 'GET'); }; /** * Gets this helper instance ready to send a bulk request. Each argument to this * function is a single request to use. */ this.setBulkRequests = function () { var urls = []; for (var i = 0; i != arguments.length; ++i) { urls.push($.param(arguments[i])); } this.addParams({ module: 'API', method: 'API.getBulkRequest', urls: urls, format: 'json' }, 'post'); }; /** * Set a timeout (in milliseconds) for the request. This will override any global timeout. * * @param {int} timeout Timeout in milliseconds * @return {void} */ this.setTimeout = function (timeout) { this.timeout = timeout; }; /** * Sets the callback called after the request finishes * * @param {function} callback Callback function * @return {void} */ this.setCallback = function (callback) { this.callback = callback; }; /** * Set that the callback passed to setCallback() should be used if an application error (i.e. an * Exception in PHP) is returned. */ this.useCallbackInCaseOfError = function () { this.useRegularCallbackInCaseOfError = true; }; /** * Set callback to redirect on success handler * &update=1(+x) will be appended to the current url * * @param {object} [params] to modify in redirect url * @return {void} */ this.redirectOnSuccess = function (params) { this.setCallback(function() { piwikHelper.redirect(params); }); }; /** * Sets the callback called in case of an error within the request * * @param {function} callback Callback function * @return {void} */ this.setErrorCallback = function (callback) { this.errorCallback = callback; }; /** * Sets the complete callback which is called after an error or success callback. * * @param {function} callback Callback function * @return {void} */ this.setCompleteCallback = function (callback) { this.completeCallback = callback; }; /** * error callback to use by default * * @param deferred * @param status */ this.defaultErrorCallback = function(deferred, status) { // do not display error message if request was aborted if(status == 'abort') { return; } var loadingError = $('#loadingError'); if (Piwik_Popover.isOpen() && deferred && deferred.status === 500) { if (deferred && deferred.status === 500) { $(document.body).html(piwikHelper.escape(deferred.responseText)); } } else { loadingError.show(); } } this.errorCallback = this.defaultErrorCallback; /** * Sets the response format for the request * * @param {string} format response format (e.g. json, html, ...) * @return {void} */ this.setFormat = function (format) { this.format = format; }; /** * Set the div element to show while request is loading * * @param {String} [element] selector for the loading element */ this.setLoadingElement = function (element) { if (!element) { element = '#ajaxLoadingDiv'; } this.loadingElement = element; }; /** * Set the div element to show on error * * @param {String} element selector for the error element */ this.setErrorElement = function (element) { if (!element) { return; } this.errorElement = element; }; /** * Detect whether are allowed to use the given default parameter or not * @param string parameter * @returns {boolean} * @private */ this._useGETDefaultParameter = function (parameter) { if (parameter && this.defaultParams) { var i; for (i = 0; i < this.defaultParams.length; i++) { if (this.defaultParams[i] === parameter) { return true; } } } return false; } /** * Removes a default parameter that is usually send automatically along the request. * * @param {String} parameter A name such as "period", "date", "segment". */ this.removeDefaultParameter = function (parameter) { if (parameter && this.defaultParams) { var i; for (i = 0; i < this.defaultParams.length; i++) { if (this.defaultParams[i] === parameter) { this.defaultParams.splice(i, 1); } } } } /** * Send the request * @return {void} */ this.send = function () { if ($(this.errorElement).length) { $(this.errorElement).hide(); } if (this.loadingElement) { $(this.loadingElement).fadeIn(); } this.requestHandle = this._buildAjaxCall(); globalAjaxQueue.push(this.requestHandle); }; /** * Aborts the current request if it is (still) running * @return {void} */ this.abort = function () { if (this.requestHandle && typeof this.requestHandle.abort == 'function') { this.requestHandle.abort(); this.requestHandle = null; } }; /** * Builds and sends the ajax requests * @return {XMLHttpRequest} * @private */ this._buildAjaxCall = function () { var that = this; var parameters = this._mixinDefaultGetParams(this.getParams); var url = this.getUrl; if (url[url.length - 1] != '?') { url += '&'; } // we took care of encoding &segment properly already, so we don't use $.param for it ($.param URL encodes the values) if(parameters['segment']) { url += 'segment=' + parameters['segment'] + '&'; delete parameters['segment']; } if(parameters['date']) { url += 'date=' + decodeURIComponent(parameters['date']) + '&'; delete parameters['date']; } url += $.param(parameters); var ajaxCall = { type: 'POST', async: true, url: url, dataType: this.format || 'json', complete: this.completeCallback, error: function () { --globalAjaxQueue.active; if (that.errorCallback) { that.errorCallback.apply(this, arguments); } }, success: function (response, status, request) { if (that.loadingElement) { $(that.loadingElement).hide(); } if (response && response.result == 'error' && !that.useRegularCallbackInCaseOfError) { var placeAt = null; var type = 'toast'; if ($(that.errorElement).length && response.message) { $(that.errorElement).show(); placeAt = that.errorElement; type = null; } if (response.message) { var UI = require('piwik/UI'); var notification = new UI.Notification(); notification.show(response.message, { placeat: placeAt, context: 'error', type: type, id: 'ajaxHelper' }); notification.scrollToNotification(); } } else { that.callback(response, status, request); } --globalAjaxQueue.active; var piwik = window.piwik; if (piwik && piwik.ajaxRequestFinished ) { piwik.ajaxRequestFinished(); } }, data: this._mixinDefaultPostParams(this.postParams) }; if (this.timeout !== null) { ajaxCall.timeout = this.timeout; } return $.ajax(ajaxCall); }; this._isRequestToApiMethod = function () { return (this.getParams && this.getParams['module'] === 'API' && this.getParams['method']) || (this.postParams && this.postParams['module'] === 'API' && this.postParams['method']); }; this._isWidgetizedRequest = function () { return (broadcast.getValueFromUrl('module') == 'Widgetize'); }; this._getDefaultPostParams = function () { if (this.withToken || this._isRequestToApiMethod() || piwik.shouldPropagateTokenAuth) { return { token_auth: piwik.token_auth, force_api_session: '1' }; } return {}; }; /** * Mixin the default parameters to send as POST * * @param {object} params parameter object * @return {object} * @private */ this._mixinDefaultPostParams = function (params) { var defaultParams = this._getDefaultPostParams(); for (var index in defaultParams) { if (!params[index]) { params[index] = defaultParams[index]; } } return params; }; /** * Mixin the default parameters to send as GET * * @param {object} params parameter object * @return {object} * @private */ this._mixinDefaultGetParams = function (params) { var piwikUrl = piwikHelper.getAngularDependency('piwikUrl'); var segment = piwikUrl.getSearchParam('segment'); var defaultParams = { idSite: piwik.idSite || broadcast.getValueFromUrl('idSite'), period: piwik.period || broadcast.getValueFromUrl('period'), segment: segment }; // never append token_auth to url if (params.token_auth) { params.token_auth = null; delete params.token_auth; } for (var key in defaultParams) { if (this._useGETDefaultParameter(key) && !params[key] && !this.postParams[key] && defaultParams[key]) { params[key] = defaultParams[key]; } } // handle default date & period if not already set if (this._useGETDefaultParameter('date') && !params.date && !this.postParams.date) { params.date = piwik.currentDateString; } return params; }; return this; }