diff --git a/.gitattributes b/.gitattributes index 6e0dcba74e25e82b1d95b174c78fdbb5033a739d..8c57377a70f3f741e9ab0d967483f2c712470cd2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -94,3 +94,4 @@ plugins/CustomAlerts/tests/UI/expected-ui-screenshots/CustomAlerts_alert_conditi plugins/CustomAlerts/tests/UI/expected-ui-screenshots/CustomAlerts_edit.png filter=lfs diff=lfs merge=lfs -text plugins/CustomAlerts/tests/UI/expected-ui-screenshots/CustomAlerts_report_mediums.png filter=lfs diff=lfs merge=lfs -text plugins/CustomDimensions/tests/System/expected/test___ScheduledReports.generateReport_year.original.pdf filter=lfs diff=lfs merge=lfs -text +plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf filter=lfs diff=lfs merge=lfs -text diff --git a/plugins/Dashboard/javascripts/dashboardObject.js b/plugins/Dashboard/javascripts/dashboardObject.js new file mode 100644 index 0000000000000000000000000000000000000000..b7cf16f3c58d5090cb4d8eec2e226bbbb991e6ce --- /dev/null +++ b/plugins/Dashboard/javascripts/dashboardObject.js @@ -0,0 +1,674 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +(function ($) { + + var layoutColumnSelector = '#dashboardWidgetsArea > .col'; + + /** + * Current dashboard column layout + * @type {object} + */ + var dashboardLayout = {}; + /** + * Id of current dashboard + * @type {int} + */ + var dashboardId = 1; + /** + * Name of current dashboard + * @type {string} + */ + var dashboardName = ''; + /** + * Holds a reference to the dashboard element + * @type {object} + */ + var dashboardElement = null; + /** + * Boolean indicating weather the layout config has been changed or not + * @type {boolean} + */ + var dashboardChanged = false; + + /** + * public methods of dashboard plugin + * all methods defined here are accessible with $(selector).dashboard('method', param, param, ...) + */ + var methods = { + + /** + * creates a dashboard object + * + * @param {object} options + */ + init: function (options) { + + dashboardElement = this; + + if (options.idDashboard) { + dashboardId = options.idDashboard; + } + + if (options.name) { + dashboardName = options.name; + } + + if (options.layout) { + generateLayout(options.layout); + } + + window.CoreHome.Matomo.postEvent('Dashboard.Dashboard.mounted', { element: this }); + + return this; + }, + + /** + * Destroys the dashboard object and all its childrens + * + * @return void + */ + destroy: function () { + if (dashboardElement && dashboardElement.length) { + window.CoreHome.Matomo.postEvent('Dashboard.Dashboard.unmounted', {element: dashboardElement[0]}); + } + + $(dashboardElement).remove(); + dashboardElement = null; + destroyWidgets(); + }, + + destroyWidgets: destroyWidgets, + + /** + * Load dashboard with the given id + * + * @param {int} dashboardIdToLoad + */ + loadDashboard: function (dashboardIdToLoad, forceReload) { + + $(dashboardElement).empty(); + dashboardName = ''; + dashboardLayout = null; + dashboardId = dashboardIdToLoad; + + if (!forceReload && piwikHelper.isReportingPage()) { + var MatomoUrl = window.CoreHome.MatomoUrl; + MatomoUrl.updateHash(Object.assign({}, MatomoUrl.hashParsed.value, { + subcategory: dashboardIdToLoad, + })); + } else { + piwik.postEvent('Dashboard.loadDashboard', dashboardIdToLoad); + } + + return this; + }, + + /** + * Change current column layout to the given one + * + * @param {String} newLayout + */ + setColumnLayout: function (newLayout) { + adjustDashboardColumns(newLayout); + }, + + /** + * Returns the current column layout + * + * @return {String} + */ + getColumnLayout: function () { + return dashboardLayout.config.layout; + }, + + /** + * Return the current dashboard name + * + * @return {String} + */ + getDashboardName: function () { + return dashboardName; + }, + + /** + * Return the current dashboard id + * + * @return {int} + */ + getDashboardId: function () { + return dashboardId; + }, + + /** + * Sets a new name for the current dashboard + * + * @param {String} newName + */ + setDashboardName: function (newName) { + dashboardName = newName; + dashboardChanged = true; + saveLayout(); + }, + + /** + * Adds a new widget to the dashboard + * + * @param {String} uniqueId + * @param {int} columnNumber + * @param {object} widgetParameters + * @param {boolean} addWidgetOnTop + * @param {boolean} isHidden + */ + addWidget: function (uniqueId, columnNumber, widgetParameters, addWidgetOnTop, isHidden) { + addWidgetTemplate(uniqueId, columnNumber, widgetParameters, addWidgetOnTop, isHidden); + saveLayout(); + }, + + /** + * Resets the current layout to the defaults + */ + resetLayout: function () { + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams({ + module: 'Dashboard', + action: 'resetLayout', + idDashboard: dashboardId + }, 'get'); + ajaxRequest.withTokenInUrl(); + ajaxRequest.setCallback( + function () { + methods.loadDashboard.apply(this, [dashboardId, true]) + } + ); + ajaxRequest.setLoadingElement(); + ajaxRequest.setFormat('html'); + ajaxRequest.send(); + }, + + rebuildMenu: rebuildMenu, + /** + * Removes the current dashboard + */ + removeDashboard: function () { + if (dashboardId == 1) { + return; // dashboard with id 1 should never be deleted, as it is the default + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.setLoadingElement(); + ajaxRequest.addParams({ + module: 'API', + method: 'Dashboard.removeDashboard', + idDashboard: dashboardId, + login: piwik.userLogin, + format: 'json' + }, 'get'); + ajaxRequest.setCallback( + function () { + Promise.resolve(rebuildMenu()).then(function () { + methods.loadDashboard.apply(this, [1]); + }); + } + ); + ajaxRequest.withTokenInUrl(); + ajaxRequest.setFormat('html'); + ajaxRequest.send(); + }, + + /** + * Saves the current layout aus new default widget layout + */ + saveLayoutAsDefaultWidgetLayout: function () { + saveLayout('saveLayoutAsDefault'); + }, + + /** + * Returns if the current loaded dashboard is the default dashboard + */ + isDefaultDashboard: function () { + return (dashboardId == 1); + } + }; + + function destroyWidgets() + { + var widgets = $('[widgetId]'); + for (var i = 0; i < widgets.length; i++) { + $(widgets[i]).dashboardWidget('destroy'); + } + } + + function removeNonExistingWidgets(availableWidgets, layout) + { + var existingModuleAction = {}; + $.each(availableWidgets, function (category, widgets) { + $.each(widgets, function (index, widget) { + existingModuleAction[widget.module + '.' + widget.action] = true; + }); + }); + + var columns = []; + $.each(layout.columns, function (i, column) { + var widgets = []; + + $.each(column, function (j, widget) { + if (!widget.parameters || !widget.parameters.module) { + return; + } + + var method = widget.parameters.module + '.' + widget.parameters.action + if (existingModuleAction[method]) { + widgets.push(widget); + } + + }); + + columns[i] = widgets; + }); + + layout.columns = columns; + + return layout; + } + + /** + * Generates the dashboard out of the given layout + * + * @param {object|string} layout + */ + function generateLayout(layout) { + + dashboardLayout = parseLayout(layout); + + widgetsHelper.getAvailableWidgets(function (availableWidgets) { + dashboardLayout = removeNonExistingWidgets(availableWidgets, dashboardLayout); + + piwikHelper.hideAjaxLoading(); + adjustDashboardColumns(dashboardLayout.config.layout); + + var dashboardContainsWidgets = false; + for (var column = 0; column < dashboardLayout.columns.length; column++) { + for (var i in dashboardLayout.columns[column]) { + if (typeof dashboardLayout.columns[column][i] != 'object') { + // Fix IE8 bug: the "i in" loop contains i="indexOf", which would yield type function. + // If we would continue with i="indexOf", an invalid widget would be created. + continue; + } + var widget = dashboardLayout.columns[column][i]; + dashboardContainsWidgets = true; + addWidgetTemplate(widget.uniqueId, column + 1, widget.parameters, false, widget.isHidden) + } + } + + if (!dashboardContainsWidgets) { + $(dashboardElement).trigger('dashboardempty'); + } + + makeWidgetsSortable(); + }); + } + + /** + * Adjust the dashboard columns to fit the new layout + * removes or adds new columns if needed and sets the column sizes. + * + * @param {String} layout new layout in format xx-xx-xx + * @return {void} + */ + function adjustDashboardColumns(layout) { + var columnWidth = layout.split('-'); + var columnCount = columnWidth.length; + + var currentCount = $('> .col', dashboardElement).length; + + if (currentCount < columnCount) { + $('.menuClear', dashboardElement).remove(); + for (var i = currentCount; i < columnCount; i++) { + if (dashboardLayout.columns.length < i) { + dashboardLayout.columns.push({}); + } + $(dashboardElement).append('
'); + } + $(dashboardElement).append(''); + } else if (currentCount > columnCount) { + for (var i = columnCount; i < currentCount; i++) { + if (dashboardLayout.columns.length >= i) { + dashboardLayout.columns.pop(); + } + // move widgets to other columns depending on columns height + $('[widgetId]', $(layoutColumnSelector + ':last')).each(function (id, elem) { + var cols = $(layoutColumnSelector).slice(0, columnCount); + var smallestColumn = $(cols[0]); + var smallestColumnHeight = null; + cols.each(function (colId, col) { + if (smallestColumnHeight == null || smallestColumnHeight > $(col).height()) { + smallestColumnHeight = $(col).height(); + smallestColumn = $(col); + } + }); + + $(elem).appendTo(smallestColumn); + }); + + $(layoutColumnSelector + ':last').remove(); + } + } + + var $dashboardElement = $(' > .col', dashboardElement); + + if (!$dashboardElement.length) { + return; + } + + switch (layout) { + case '100': + $dashboardElement.removeClass().addClass('col s12'); + break; + case '50-50': + $dashboardElement.removeClass().addClass('col s12 m6'); + break; + case '67-33': + $dashboardElement[0].className = 'col s12 m8'; + $dashboardElement[1].className = 'col s12 m4'; + break; + case '33-67': + $dashboardElement[0].className = 'col s12 m4'; + $dashboardElement[1].className = 'col s12 m8'; + break; + case '33-33-33': + $dashboardElement[0].className = 'col s12 m4'; + $dashboardElement[1].className = 'col s12 m4'; + $dashboardElement[2].className = 'col s12 m4'; + break; + case '40-30-30': + $dashboardElement[0].className = 'col s12 m6'; + $dashboardElement[1].className = 'col s12 m3'; + $dashboardElement[2].className = 'col s12 m3'; + break; + case '30-40-30': + $dashboardElement[0].className = 'col s12 m3'; + $dashboardElement[1].className = 'col s12 m6'; + $dashboardElement[2].className = 'col s12 m3'; + break; + case '30-30-40': + $dashboardElement[0].className = 'col s12 m3'; + $dashboardElement[1].className = 'col s12 m3'; + $dashboardElement[2].className = 'col s12 m6'; + break; + case '25-25-25-25': + $dashboardElement[0].className = 'col s12 m3'; + $dashboardElement[1].className = 'col s12 m3'; + $dashboardElement[2].className = 'col s12 m3'; + $dashboardElement[3].className = 'col s12 m3'; + break; + } + + makeWidgetsSortable(); + + // if dashboard column count is changed (not on initial load) + if (currentCount > 0 && dashboardLayout.config.layout != layout) { + dashboardChanged = true; + dashboardLayout.config.layout = layout; + saveLayout(); + } + + // trigger resize event on all widgets + $('.widgetContent').each(function () { + $(this).trigger('widget:resize'); + }); + } + + /** + * Returns the given layout as an layout object + * Used to parse old layout format into the new syntax + * + * @param {object} layout layout object or string + * @return {object} + */ + function parseLayout(layout) { + + // Handle layout array used in piwik before 1.7 + // column count was always 3, so use layout 33-33-33 as default + if ($.isArray(layout)) { + layout = { + config: {layout: '33-33-33'}, + columns: layout + }; + } + + if (!layout.config.layout) { + layout.config.layout = '33-33-33'; + } + + return layout; + } + + /** + * Reloads the widget with the given uniqueId + * + * @param {String|jQuery} $widget + */ + function reloadWidget($widget) { + if (typeof $widget === 'string') { + $widget = $('[widgetid="' + $widget + '"]', dashboardElement); + } + + $widget.dashboardWidget('reload', false, true); + } + + /** + * Adds an empty widget template to the dashboard in the given column + * @param {String} uniqueId + * @param {int} columnNumber + * @param {object} widgetParameters + * @param {boolean} addWidgetOnTop + * @param {boolean} isHidden + */ + function addWidgetTemplate(uniqueId, columnNumber, widgetParameters, addWidgetOnTop, isHidden) { + if (!columnNumber) { + columnNumber = 1; + } + + // do not try to add widget if given column number is to high + if (columnNumber > $('> .col', dashboardElement).length) { + return; + } + + var $widgetContent = $('
').attr('widgetId', uniqueId); + + if (addWidgetOnTop) { + $('> .col:nth-child(' + columnNumber + ')', dashboardElement).prepend($widgetContent); + } else { + $('> .col:nth-child(' + columnNumber + ')', dashboardElement).append($widgetContent); + } + + return $widgetContent.dashboardWidget({ + uniqueId: uniqueId, + widgetParameters: widgetParameters, + onChange: function () { + saveLayout(); + }, + isHidden: isHidden + }); + } + + /** + * Make all widgets on the dashboard sortable + */ + function makeWidgetsSortable() { + function onStart(event, ui) { + if (!jQuery.support.noCloneEvent) { + $('object', this).hide(); + } + } + + function onStop(event, ui) { + $('object', this).show(); + $('.widgetHover', this).removeClass('widgetHover'); + $('.widgetTopHover', this).removeClass('widgetTopHover'); + if ($('.widget:has(".piwik-graph")', ui.item).length) { + reloadWidget($('.widget', ui.item).attr('id')); + } + saveLayout(); + } + + //launch 'sortable' property on every dashboard widgets + $( layoutColumnSelector + ":data('ui-sortable')", dashboardElement ).sortable('destroy'); + + $('> .col', dashboardElement) + .sortable({ + items: 'div.sortable', + opacity: 0.6, + forceHelperSize: true, + forcePlaceholderSize: true, + placeholder: 'hover', + handle: '.widgetTop', + helper: 'clone', + start: onStart, + stop: onStop, + connectWith: layoutColumnSelector + }); + } + + /** + * Handle clicks for menu items for choosing between available dashboards + */ + function rebuildMenu() { + + if (piwikHelper.isReportingPage()) { + // dashboard in reporting page (regular Piwik UI) + return window.CoreHome.ReportingMenuStore.reloadMenuItems(); + } + + var _self = this; + + // widgetized + var success = function (dashboards) { + var dashboardMenuList = $('#Dashboard_embeddedIndex_1').closest('ul'); + var dashboardMenuListItems = dashboardMenuList.find('>li'); + + dashboardMenuListItems.filter(function () { + return $(this).attr('id').indexOf('Dashboard_embeddedIndex') == 0; + }).remove(); + + if (dashboards.length === 0) { + dashboards = [{iddashboard: 1, name: _pk_translate('Dashboard_Dashboard')}]; + } + + if (dashboards.length > 1 + || dashboardMenuListItems.length >= 1 + ) { + var items = []; + for (var i = 0; i < dashboards.length; i++) { + var $link = $('').attr('data-iddashboard', dashboards[i].iddashboard).text(dashboards[i].name).addClass('item'); + var $li = $('
  • ').attr('id', 'Dashboard_embeddedIndex_' + dashboards[i].iddashboard).addClass('dashboardMenuItem').attr('role', 'menuitem').append($link); + + items.push($li); + + if (dashboards[i].iddashboard == dashboardId) { + dashboardName = dashboards[i].name; + $li.addClass('active'); + } + } + dashboardMenuList.prepend(items); + } + + dashboardMenuList.find('a[data-iddashboard]').click(function (e) { + e.preventDefault(); + + var idDashboard = $(this).attr('data-iddashboard'); + + $('#Dashboard ul li').removeClass('active'); + + methods.loadDashboard.apply(_self, [idDashboard]); + + $(this).closest('li').addClass('active'); + }); + }; + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams({ + module: 'Dashboard', + action: 'getAllDashboards', + filter_limit: '-1' + }, 'get'); + ajaxRequest.withTokenInUrl(); + ajaxRequest.setCallback(success); + return ajaxRequest.send(); + } + + /** + * Save the current layout in database if it has changed + * @param {string} [action] action to perform (defaults to saveLayout) + */ + function saveLayout(action) { + var columns = []; + + var columnNumber = 0; + + $(layoutColumnSelector).each(function () { + columns[columnNumber] = []; + var items = $('[widgetId]', this); + for (var j = 0; j < items.length; j++) { + columns[columnNumber][j] = $(items[j]).dashboardWidget('getWidgetObject'); + + // Do not store segment in the dashboard layout + delete columns[columnNumber][j].parameters.segment; + + } + columnNumber++; + }); + + if (JSON.stringify(dashboardLayout.columns) != JSON.stringify(columns) || dashboardChanged || action) { + + dashboardLayout.columns = JSON.parse(JSON.stringify(columns)); + columns = null; + + if (!action) { + action = 'saveLayout'; + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams({ + module: 'Dashboard', + action: action, + idDashboard: dashboardId + }, 'get'); + ajaxRequest.addParams({ + layout: JSON.stringify(dashboardLayout), + name: dashboardName + }, 'post'); + ajaxRequest.setCallback( + function () { + if (dashboardChanged) { + dashboardChanged = false; + rebuildMenu(); + } + } + ); + + ajaxRequest.withTokenInUrl(); + ajaxRequest.setFormat('html'); + ajaxRequest.send(); + } + } + + /** + * Make plugin methods available + */ + $.fn.dashboard = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.dashboard'); + } + } + +})(jQuery); diff --git a/plugins/Dashboard/javascripts/dashboardWidget.js b/plugins/Dashboard/javascripts/dashboardWidget.js new file mode 100644 index 0000000000000000000000000000000000000000..10ca375e3db8262b81f61520295ee4b1f0afcab8 --- /dev/null +++ b/plugins/Dashboard/javascripts/dashboardWidget.js @@ -0,0 +1,402 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +(function ($) { + + $.widget('piwik.dashboardWidget', { + + /** + * Boolean indicating weather the widget is currently maximised + * @type {Boolean} + */ + isMaximised: false, + /** + * Unique Id of the widget + * @type {String} + */ + uniqueId: null, + /** + * Object holding the widget parameters + * @type {Object} + */ + widgetParameters: {}, + + /** + * Options available for initialization + */ + options: { + uniqueId: null, + isHidden: false, + onChange: null, + widgetParameters: {}, + title: null, + onRemove: null, + onRefresh: null, + onMaximise: null, + onMinimise: null, + autoMaximiseVisualizations: ['tableAllColumns', 'tableGoals'] + }, + + /** + * creates a widget object + */ + _create: function () { + + if (!this.options.uniqueId) { + piwikHelper.error('widgets can\'t be created without an uniqueId'); + return; + } else { + this.uniqueId = this.options.uniqueId; + } + + if (this.options.widgetParameters) { + this.widgetParameters = this.options.widgetParameters; + } + + this._createDashboardWidget(this.uniqueId); + + var self = this; + this.element.on('setParameters.dashboardWidget', function (e, params) { self.setParameters(params); }); + + this.reload(true, true); + }, + + /** + * Cleanup some events and dialog + * Called automatically upon removing the widgets domNode + */ + destroy: function () { + if (this.isMaximised) { + $('[widgetId="' + this.uniqueId + '"]').dialog('destroy'); + } + $('*', this.element).off('.dashboardWidget'); // unbind all events + $('.widgetContent', this.element).trigger('widget:destroy'); + require('piwik/UI').UIControl.cleanupUnusedControls(); + return this; + }, + + /** + * Returns the data currently set for the widget + * @return {object} + */ + getWidgetObject: function () { + return { + uniqueId: this.uniqueId, + parameters: this.widgetParameters, + isHidden: this.options.isHidden + }; + }, + + /** + * Show the current widget in an ui.dialog + */ + maximise: function () { + this.isMaximised = true; + + if (this.options.onMaximise) { + this.options.onMaximise(this.element); + } else { + this._maximiseImpl(); + } + + $('.widgetContent', this.element).trigger('widget:maximise'); + return this; + }, + + /** + * Reloads the widgets content with the currently set parameters + */ + reload: function (hideLoading, notJQueryUI, overrideParams) { + if (!notJQueryUI) { + piwikHelper.log('widget.reload() was called by jquery.ui, ignoring', arguments.callee.caller); + return; + } + + var self = this, currentWidget = this.element; + + $('.widgetContent', currentWidget).trigger('widget:reload'); + + function onWidgetLoadedReplaceElementWithContent(loadedContent) { + var $widgetContent = $('.widgetContent', currentWidget); + + $widgetContent.html(loadedContent); + + /* move widget icons into datatable top actions + var $buttons = currentWidget.find('.buttons .button'); + var $controls = currentWidget.find('.dataTableControls .dataTableAction').first(); + if ($buttons.length && $controls.length) { + $buttons.find('.button').addClass('dataTableAction'); + $buttons.insertBefore($controls); + }*/ + + if (currentWidget.parents('body').length) { + // there might be race conditions, eg widget might be just refreshed while whole dashboard is also + // removed from DOM + piwikHelper.compileVueEntryComponents($widgetContent); + } + $widgetContent.removeClass('loading'); + $widgetContent.trigger('widget:create', [self]); + + window.CoreHome.NotificationsStore.parseNotificationDivs(); + } + + // Reading segment from hash tag (standard case) or from the URL (when embedding dashboard) + ['segment'].forEach(function (paramName) { + var value = broadcast.getValueFromHash(paramName) || broadcast.getValueFromUrl(paramName); + if (value.length) { + self.widgetParameters[paramName] = value; + } + }); + + ['compareSegments', 'comparePeriods', 'compareDates'].forEach(function (paramName) { + var value = broadcast.getValueFromHash(paramName) || broadcast.getValueFromUrl(paramName); + if (value.length) { + self.widgetParameters[paramName] = value; + } else { + delete self.widgetParameters[paramName]; + } + }); + + if (!hideLoading) { + $('.widgetContent', currentWidget).addClass('loading'); + } + + var params = $.extend( + this.widgetParameters, + overrideParams || {}, + window.CoreHome.SearchFiltersPersistenceStore.getSearchFilters(this.uniqueId) + ); + + widgetsHelper.loadWidgetAjax(this.uniqueId, params, onWidgetLoadedReplaceElementWithContent, function (deferred, status) { + if (status == 'abort' || !deferred || deferred.status < 400 || deferred.status >= 600) { + return; + } + + var errorMessage; + $('.widgetContent', currentWidget).removeClass('loading'); + + + if (deferred.status === 429) { + errorMessage = `
    ${_pk_translate('General_ErrorRateLimit')}>', + ''])}
    `; + + if($('#loadingRateLimitError').html()) { + errorMessage = $('#loadingRateLimitError') + .html(); + } + } else { + var errorMessage = _pk_translate('General_ErrorRequest', ['', '']); + if ($('#loadingError').html()) { + errorMessage = $('#loadingError').html(); + } + } + + $('.widgetContent', currentWidget).html('
    ' + errorMessage + '
    '); + }); + + return this; + }, + + /** + * Update widget parameters + * + * @param {object} parameters + */ + setParameters: function (parameters) { + if (!this.isMaximised + && this.options.autoMaximiseVisualizations.indexOf(parameters.viewDataTable) !== -1 + ) { + this.maximise(); + } + for (var name in parameters) { + this.widgetParameters[name] = parameters[name]; + } + if (!this.isMaximised) { + this.options.onChange(); + } + + return this; + }, + + /** + * Get widget parameters + * + * @param {object} parameters + */ + getParameters: function () { + return $.extend({}, this.widgetParameters); + }, + + /** + * Creates the widget markup for the given uniqueId + * + * @param {String} uniqueId + */ + _createDashboardWidget: function (uniqueId) { + + var self = this; + + widgetsHelper.getWidgetNameFromUniqueId(uniqueId, function(widgetName) { + if (!widgetName) { + // when widget not found hide it. + $('[widgetId="' + uniqueId + '"]').hide(); + widgetName = _pk_translate('Dashboard_WidgetNotFound'); + } + + var title = self.options.title === null ? $('').text(widgetName) : self.options.title; + var emptyWidgetContent = require('piwik/UI/Dashboard').WidgetFactory.make(uniqueId, title); + self.element.html(emptyWidgetContent); + + var widgetElement = $('[id="' + uniqueId + '"]', self.element); + widgetElement + .on('mouseenter.dashboardWidget', function () { + if (!self.isMaximised) { + $(this).addClass('widgetHover'); + $('.widgetTop', this).addClass('widgetTopHover'); + } + }) + .on('mouseleave.dashboardWidget', function () { + if (!self.isMaximised) { + $(this).removeClass('widgetHover'); + $('.widgetTop', this).removeClass('widgetTopHover'); + } + }); + + if (self.options.isHidden) { + $('.widgetContent', widgetElement).toggleClass('hidden').closest('.widget').toggleClass('hiddenContent'); + } + + $('.button#close', widgetElement) + .on('click.dashboardWidget', function (ev) { + piwikHelper.modalConfirm('#confirm', {yes: function () { + if (self.options.onRemove) { + self.options.onRemove(self.element); + } else { + self.element.remove(); + self.options.onChange(); + } + }}); + }); + + $('.button#maximise', widgetElement) + .on('click.dashboardWidget', function (ev) { + if (self.options.onMaximise) { + self.options.onMaximise(self.element); + } else { + if ($('.widgetContent', $(this).parents('.widget')).hasClass('hidden')) { + self.showContent(); + } else { + self.maximise(); + } + } + }); + + $('.button#minimise', widgetElement) + .on('click.dashboardWidget', function (ev) { + if (self.options.onMinimise) { + self.options.onMinimise(self.element); + } else { + if (!self.isMaximised) { + self.hideContent(); + } else { + self.element.dialog("close"); + } + } + }); + + $('.button#refresh', widgetElement) + .on('click.dashboardWidget', function (ev) { + if (self.options.onRefresh) { + self.options.onRefresh(self.element); + } else { + self.reload(false, true); + } + }); + }); + }, + + /** + * Hide the widget content. Triggers the onChange event. + */ + hideContent: function () { + $('.widgetContent', this.element.find('.widget').addClass('hiddenContent')).addClass('hidden'); + this.options.isHidden = true; + this.options.onChange(); + }, + + /** + * Show the widget content. Triggers the onChange event. + */ + showContent: function () { + this.isMaximised = false; + this.options.isHidden = false; + this.element.find('.widget').removeClass('hiddenContent').find('.widgetContent').removeClass('hidden'); + this.element.find('.widget').find('div.piwik-graph').trigger('resizeGraph'); + this.options.onChange(); + $('.widgetContent', this.element).trigger('widget:minimise'); + }, + + /** + * Default maximise behavior. Will create a dialog that is 70% of the document's width, + * displaying the widget alone. + */ + _maximiseImpl: function () { + this.detachWidget(); + + var width = Math.floor($('body').width() * 0.7); + + var self = this; + this.element.dialog({ + title: '', + dialogClass: 'widgetoverlay', + modal: true, + width: width, + resizable: true, + autoOpen: true, + close: function (event, ui) { + self.isMaximised = false; + $('body').off('.dashboardWidget'); + $(this).dialog("destroy"); + $('[id="' + self.uniqueId + '-placeholder"]').replaceWith(this); + $(this).removeAttr('style'); + self.options.onChange(); + $(this).find('div.piwik-graph').trigger('resizeGraph'); + $('.widgetContent', self.element).trigger('widget:minimise'); + } + }); + this.element.find('div.piwik-graph').trigger('resizeGraph'); + // remove all previously shown tooltips as they might not be destroyed automatically + // see https://github.com/matomo-org/matomo/issues/17625 + $('.ui-tooltip').remove(); + + var currentWidget = this.element; + $('body').on('click.dashboardWidget', function (ev) { + if (/ui-widget-overlay/.test(ev.target.className)) { + $(currentWidget).dialog("close"); + } + }); + }, + + /** + * Detaches the widget from the DOM and replaces it with a placeholder element. + * The placeholder element will have the save dimensions as the widget and will have + * the widgetPlaceholder CSS class. + * + * @return {jQuery} the detached widget + */ + detachWidget: function () { + this.element.before('
    '); + var placeholder = $('[id="' + self.uniqueId + '-placeholder"]'); + + $('#' + this.uniqueId + '-placeholder').height(this.element.height()); + $('#' + this.uniqueId + '-placeholder').width(this.element.width() - 16); + + return this.element.detach(); + } + }); + +})(jQuery); diff --git a/plugins/Dashboard/javascripts/widgetMenu.js b/plugins/Dashboard/javascripts/widgetMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..75ebde698eb01449ca91a9cc561b6f7326992ac4 --- /dev/null +++ b/plugins/Dashboard/javascripts/widgetMenu.js @@ -0,0 +1,625 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +function widgetsHelper() { +} + +// a Promise for the first call to getAvailableWidgets. this should not be aborted, +// so any code that aborts all ajax requests should make sure this promise is resolved +// first. +widgetsHelper.firstGetAvailableWidgetsCall = null; + +/** + * Returns the available widgets fetched via AJAX (if not already done) + * + * @return {object} object containing available widgets + */ +widgetsHelper.getAvailableWidgets = function (callback) { + + function resetFirstGetAvailableWidgetsCall(promise) { + if (widgetsHelper.firstGetAvailableWidgetsCall === promise) { + widgetsHelper.firstGetAvailableWidgetsCall = null; + } + } + + if (!widgetsHelper.availableWidgets && widgetsHelper.firstGetAvailableWidgetsCall) { + return widgetsHelper.firstGetAvailableWidgetsCall.then(function () { + if (callback) { + callback(widgetsHelper.availableWidgets); + } + }); + } + + function mergeCategoriesAndSubCategories(availableWidgets) + { + var categorized = {}; + + $.each(availableWidgets, function (index, widget) { + var category = widget.category.name; + + if (!categorized[category]) { + categorized[category] = {'-': []}; + } + + var subcategory = '-'; + if (widget.subcategory && widget.subcategory.name) { + subcategory = widget.subcategory.name; + } + + if (!categorized[category][subcategory]) { + categorized[category][subcategory] = []; + } + + categorized[category][subcategory].push(widget); + }); + + var moved = {}; + + $.each(categorized, function (category, widgets) { + $.each(widgets, function (subcategory, subwidgets) { + if (!subwidgets.length) { + return; + } + + var categoryToUse = category; + if (subwidgets.length >= 3 && subcategory !== '-') { + categoryToUse = category + ' - ' + subcategory; + } + + if (!moved[categoryToUse]) { + moved[categoryToUse] = []; + } + + $.each(subwidgets, function (index, widget) { + moved[categoryToUse].push(widget); + }); + }); + }); + + return moved; + } + + var promise = new Promise(function (resolve, reject) { + if (!widgetsHelper.availableWidgets) { + var ajaxRequest = new ajaxHelper(); + ajaxRequest._mixinDefaultGetParams = function (params) { + return params; + }; + ajaxRequest.addParams({ + module: 'API', + method: 'API.getWidgetMetadata', + filter_limit: '-1', + format: 'JSON', + deep: '1', + idSite: piwik.idSite || broadcast.getValueFromUrl('idSite') + }, 'get'); + ajaxRequest.setCallback( + function (data) { + widgetsHelper.availableWidgets = mergeCategoriesAndSubCategories(data); + + resetFirstGetAvailableWidgetsCall(promise); + resolve(); + } + ); + ajaxRequest.setErrorCallback(function (deferred, status) { + resetFirstGetAvailableWidgetsCall(promise); + + if (status == 'abort') { + reject(new Error('Loading widget metadata was aborted')); + return; + } + + if (!deferred || deferred.status < 400 || deferred.status >= 600) { + reject(new Error('Loading widget metadata failed')); + return; + } + + $('#loadingError').show(); + reject(new Error('Loading widget metadata failed')); + }); + ajaxRequest.send(); + return; + } + + resolve(); + }); + + if (!widgetsHelper.firstGetAvailableWidgetsCall) { + widgetsHelper.firstGetAvailableWidgetsCall = promise; + } + + return promise.then(function () { + if (callback) { + callback(widgetsHelper.availableWidgets); + } + }); +}; + +/** + * Clear the current collection of availableWidgets so that getAvailableWidgets() will load a fresh + * collection from the API. This is useful when we added/deleted a report and need a refresh. + */ +widgetsHelper.clearAvailableWidgets = function () { + delete widgetsHelper.availableWidgets; + widgetsHelper.firstGetAvailableWidgetsCall = null; +}; + +/** + * Determines the complete widget object by its unique id and sends it to callback method + * + * @param {string} uniqueId + * @param {function} callback + */ +widgetsHelper.getWidgetObjectFromUniqueId = function (uniqueId, callback) { + widgetsHelper.getAvailableWidgets(function(widgets){ + callback(widgetsHelper.findWidgetObjectFromUniqueId(uniqueId, widgets)); + }); +}; + +widgetsHelper.findWidgetObjectFromUniqueId = function (uniqueId, widgets) { + if (!widgets) { + return false; + } + + for (var widgetCategory in widgets) { + var widgetInCategory = widgets[widgetCategory]; + for (var i in widgetInCategory) { + if (widgetInCategory[i]["uniqueId"] == uniqueId) { + return widgetInCategory[i]; + } + } + } + + return false; +}; + +/** + * Determines the name of a widget by its unique id and sends it to the callback + * + * @param {string} uniqueId unique id of the widget + * @param {function} callback + * @return {string} + */ +widgetsHelper.getWidgetNameFromUniqueId = function (uniqueId, callback) { + this.getWidgetObjectFromUniqueId(uniqueId, function(widget) { + if (widget == false) { + callback(false); + } + callback(widget["name"]); + }); +}; + +/** + * Sends an ajax request to query for the widget's html + * + * @param {string} widgetUniqueId unique id of the widget + * @param {object} widgetParameters parameters to be used for loading the widget + * @param {function} onWidgetLoadedCallback callback to be executed after widget is loaded + * @return {object} + */ +widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWidgetLoadedCallback, onWidgetErrorCallback) { + var disableLink = broadcast.getValueFromUrl('disableLink'); + widgetParameters['disableLink'] = disableLink.length || $('body#standalone').length; + + widgetParameters['widget'] = 1; + + var widgetRequest = { + aborted: false, + ajaxRequest: null, + abort: function () { + this.aborted = true; + if (this.ajaxRequest) { + this.ajaxRequest.abort(); + } + } + }; + + function loadLegacyWidget() { + if (widgetRequest.aborted) { + return; + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(widgetParameters, 'get'); + ajaxRequest.setCallback(function () { + if (widgetRequest.aborted) { + return; + } + + onWidgetLoadedCallback.apply(this, arguments); + }); + if (onWidgetErrorCallback) { + ajaxRequest.setErrorCallback(function () { + if (widgetRequest.aborted) { + return; + } + + onWidgetErrorCallback.apply(this, arguments); + }); + } + ajaxRequest.setFormat('html'); + ajaxRequest.send(); + widgetRequest.ajaxRequest = ajaxRequest; + } + + var metadataReady = widgetsHelper.availableWidgets + ? Promise.resolve() + : widgetsHelper.getAvailableWidgets(); + + metadataReady.then(function () { + if (widgetRequest.aborted) { + return; + } + + var clientWidget = widgetsHelper.findWidgetObjectFromUniqueId( + widgetUniqueId, + widgetsHelper.availableWidgets + ); + + if (clientWidget && clientWidget.clientComponent) { + clientWidget = $.extend(true, {}, clientWidget); + clientWidget.parameters = $.extend({}, clientWidget.parameters, widgetParameters); + + var html = '
    '; + onWidgetLoadedCallback(html); + return; + } + + loadLegacyWidget(); + }).catch(function () { + loadLegacyWidget(); + }); + + return widgetRequest; +}; + +(function ($, require) { + var exports = require('piwik/UI/Dashboard'); + + /** + * Singleton instance that creates widget elements. Normally not needed even + * when embedding/re-using dashboard widgets, but it can be useful when creating + * elements with the same look and feel as dashboard widgets, but different + * behavior (such as the widget preview in the dashboard manager control). + * + * @constructor + */ + var WidgetFactory = function () { + // empty + }; + + /** + * Creates an HTML element for displaying a widget. + * + * @param {string} uniqueId unique id of the widget + * @param {string} widgetName name of the widget + * @return {Element} the empty widget + */ + WidgetFactory.prototype.make = function (uniqueId, widgetName) { + var $result = this.getWidgetTemplate().clone(); + $result.attr('id', uniqueId).find('.widgetName').append(widgetName); + return $result; + }; + + /** + * Returns the base widget template element. The template is stored in the + * element with id == 'widgetTemplate'. + * + * @return {Element} the widget template + */ + WidgetFactory.prototype.getWidgetTemplate = function () { + if (!this.widgetTemplate) { + this.widgetTemplate = $('#widgetTemplate').find('>.widget').detach(); + } + return this.widgetTemplate; + }; + + exports.WidgetFactory = new WidgetFactory(); +})(jQuery, require); + +/** + * widgetPreview jQuery Extension + * + * Converts a dom element to a widget preview + * Widget preview contains a categorylist, a widgetlist and a preview + */ +(function ($) { + $.extend({ + widgetPreview: new function () { + + /** + * Default settings for widgetPreview + * @type {object} + */ + var defaultSettings = { + /** + * handler called after a widget preview is loaded in preview element + * @type {function} + */ + onPreviewLoaded: function () {}, + /** + * handler called on click on element in widgetlist or widget header + * @type {function} + */ + onSelect: function () {}, + /** + * callback used to determine if a widget is available or not + * unavailable widgets aren't selectable in widgetlist + * @type {function} + */ + isWidgetAvailable: function (widgetUniqueId) { return true; }, + /** + * should the lists and preview be reset on widget selection? + * @type {boolean} + */ + resetOnSelect: false, + /** + * css classes for various elements + * @type {string} + */ + baseClass: 'widgetpreview-base', + categorylistClass: 'widgetpreview-categorylist', + widgetlistClass: 'widgetpreview-widgetlist', + widgetpreviewClass: 'widgetpreview-preview', + choosenClass: 'widgetpreview-choosen', + unavailableClass: 'widgetpreview-unavailable' + }; + + /** + * Returns the div to show category list in + * - if element doesn't exist, it will be created and added + * - if element already exists, its content will be removed + * + * @return {$} category list element + */ + function createWidgetCategoryList(widgetPreview, availableWidgets) { + + var settings = widgetPreview.settings; + + if (!$('.' + settings.categorylistClass, widgetPreview).length) { + $(widgetPreview).append(''); + } else { + $('.' + settings.categorylistClass, widgetPreview).empty(); + } + + for (var widgetCategory in availableWidgets) { + $('.' + settings.categorylistClass, widgetPreview).append($('
  • ').text(widgetCategory)); + } + + return $('.' + settings.categorylistClass, widgetPreview); + } + + /** + * Returns the div to show widget list in + * - if element doesn't exist, it will be created and added + * - if element already exists, its content will be removed + * + * @return {$} widget list element + */ + function createWidgetList(widgetPreview) { + var settings = widgetPreview.settings; + + if (!$('.' + settings.widgetlistClass, widgetPreview).length) { + $(widgetPreview).append(''); + } else { + $('.' + settings.widgetlistClass + ' li', widgetPreview).off('mouseover'); + $('.' + settings.widgetlistClass + ' li', widgetPreview).off('click'); + $('.' + settings.widgetlistClass, widgetPreview).empty(); + } + + if ($('.' + settings.categorylistClass + ' .' + settings.choosenClass, widgetPreview).length) { + var position = $('.' + settings.categorylistClass + ' .' + settings.choosenClass, widgetPreview).position().top - + $('.' + settings.categorylistClass, widgetPreview).position().top; + + $('.' + settings.widgetlistClass, widgetPreview).css({ + top: position, + marginBottom: position + 10 + }); + } + + return $('.' + settings.widgetlistClass, widgetPreview); + } + + /** + * Display the given widgets in a widget list + * + * @param {object} widgets widgets to be displayed + * @return {void} + */ + function showWidgetList(widgets, widgetPreview) { + var settings = widgetPreview.settings; + + var widgetList = createWidgetList(widgetPreview), + widgetPreviewTimer; + + for (var j = 0; j < widgets.length; j++) { + var widgetName = widgets[j]["name"]; + var widgetUniqueId = widgets[j]["uniqueId"]; + var widgetCategoryId = widgets[j].category ? widgets[j].category.id : null; + var widgetClass = ''; + if (!settings.isWidgetAvailable(widgetUniqueId) && widgetCategoryId !== 'General_KpiMetric') { + widgetClass += ' ' + settings.unavailableClass; + } + + widgetName = piwikHelper.escape(piwikHelper.htmlEntities(widgetName)); + + widgetList.append('
  • ' + widgetName + '
  • '); + } + + // delay widget preview a few milliseconds + $('li', widgetList).on('mouseenter', function () { + var that = this, + widgetUniqueId = $(this).attr('uniqueid'); + clearTimeout(widgetPreview); + widgetPreviewTimer = setTimeout(function () { + $('li', widgetList).removeClass(settings.choosenClass); + $(that).addClass(settings.choosenClass); + + showPreview(widgetUniqueId, widgetPreview); + }, 400); + }); + + // clear timeout after mouse has left + $('li:not(.' + settings.unavailableClass + ')', widgetList).on('mouseleave', function () { + clearTimeout(widgetPreview); + }); + + $('li', widgetList).on('click', function () { + if (!$('.widgetLoading', widgetPreview).length) { + settings.onSelect($(this).attr('uniqueid')); + if (settings.resetOnSelect) { + resetWidgetPreview(widgetPreview); + } + } + return false; + }); + } + + /** + * Returns the div to show widget preview in + * - if element doesn't exist, it will be created and added + * - if element already exists, its content will be removed + * + * @return {$} preview element + */ + function createPreviewElement(widgetPreview) { + var settings = widgetPreview.settings; + + if (!$('.' + settings.widgetpreviewClass, widgetPreview).length) { + $(widgetPreview).append('
    '); + } else { + $('.' + settings.widgetpreviewClass + ' .widgetTop', widgetPreview).off('click'); + $('.' + settings.widgetpreviewClass, widgetPreview).empty(); + } + + return $('.' + settings.widgetpreviewClass, widgetPreview); + } + + /** + * Show widget with the given uniqueId in preview + * + * @param {string} widgetUniqueId unique id of widget to display + * @param widgetPreview + * @return {void} + */ + function showPreview(widgetUniqueId, widgetPreview) { + // do not reload id widget already displayed + if ($('[id="' + widgetUniqueId + '"]', widgetPreview).length) return; + + var settings = widgetPreview.settings; + + var previewElement = createPreviewElement(widgetPreview); + + widgetsHelper.getWidgetObjectFromUniqueId(widgetUniqueId, function(widget) { + var widgetParameters = widget['parameters']; + + var emptyWidgetHtml = require('piwik/UI/Dashboard').WidgetFactory.make( + widgetUniqueId, + $('') + .attr('title', _pk_translate("Dashboard_AddPreviewedWidget")) + .text(_pk_translate('Dashboard_WidgetPreview')) + ); + previewElement.html(emptyWidgetHtml); + + var onWidgetLoadedCallback = function (response) { + var widgetElement = previewElement.children('.widget').first(); + $('.widgetContent', widgetElement).html($(response)); + piwikHelper.compileVueEntryComponents($('.widgetContent', widgetElement)); + // `widget:create` handlers should only rely on `.element`, the widget's + // root jQuery node. Dashboard widgets pass their full plugin instance; + // previews pass `{ element }` so handlers scope to this preview instead + // of grabbing the first matching node in the document. + $('.widgetContent', widgetElement).trigger('widget:create', [{ element: widgetElement }]); + settings.onPreviewLoaded(widgetUniqueId, widgetElement); + $('.' + settings.widgetpreviewClass + ' .widgetTop', widgetPreview).on('click', function () { + settings.onSelect(widgetUniqueId); + if (settings.resetOnSelect) { + resetWidgetPreview(widgetPreview); + } + return false; + }); + }; + + // abort previous sent request + if (widgetPreview.widgetAjaxRequest) { + widgetPreview.widgetAjaxRequest.abort(); + } + + widgetPreview.widgetAjaxRequest = widgetsHelper.loadWidgetAjax(widgetUniqueId, widgetParameters, onWidgetLoadedCallback); + }); + } + + /** + * Reset function + * + * @return {void} + */ + function resetWidgetPreview(widgetPreview) { + var settings = widgetPreview.settings; + + $('.' + settings.categorylistClass + ' li', widgetPreview).removeClass(settings.choosenClass); + createWidgetList(widgetPreview); + createPreviewElement(widgetPreview); + } + + /** + * Constructor + * + * @param {object} userSettings Settings to be used + * @return {void} + */ + this.construct = function (userSettings) { + + if (userSettings == 'reset') { + resetWidgetPreview(this); + return; + } + + this.widgetAjaxRequest = null; + + $(this).addClass('widgetpreview-base'); + + this.settings = jQuery.extend({}, defaultSettings, userSettings); + + // set onSelect callback + if (typeof this.settings.onSelect == 'function') { + this.onSelect = this.settings.onSelect; + } + + // set onPreviewLoaded callback + if (typeof this.settings.onPreviewLoaded == 'function') { + this.onPreviewLoaded = this.settings.onPreviewLoaded; + } + + var self = this; + widgetsHelper.getAvailableWidgets(function (availableWidgets) { + + var categoryList = createWidgetCategoryList(self, availableWidgets); + + $('li', categoryList).on('mouseover', function () { + var category = $(this).text(); + var widgets = availableWidgets[category]; + $('li', categoryList).removeClass(self.settings.choosenClass); + $(this).addClass(self.settings.choosenClass); + showWidgetList(widgets, self); + createPreviewElement(self); // empty preview + }); + }); + + }; + } + }); + + /** + * Makes widgetPreview available with $().widgetPreview() + */ + $.fn.extend({ + widgetPreview: $.widgetPreview.construct + }) +})(jQuery); diff --git a/plugins/Dashboard/lang/am.json b/plugins/Dashboard/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..fdb3422af2b252e9a37de36566af5afa0fccf329 --- /dev/null +++ b/plugins/Dashboard/lang/am.json @@ -0,0 +1,11 @@ +{ + "Dashboard": { + "AddPreviewedWidget": "ቅድመ እይታ የተሰጠውን widget ወደ ዳሽቦርዱ ጨምር", + "Dashboard": "ዳሽቦርድ", + "DeleteWidgetConfirm": "እርግጠኛ ነህ ይህ widget ከዳሽቦርድ ይሰረዝ?", + "LoadingWidget": "widget በመጫን ላይ, እባክዎ ይጠበቁ…", + "SelectWidget": "ወደ ዳሽቦርድ የሚጨመረውን widget ምረጥ", + "WidgetNotFound": "Widget አልተገኘም", + "WidgetPreview": "የWidget ቅድመእይታ" + } +} diff --git a/plugins/Dashboard/lang/ar.json b/plugins/Dashboard/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..52ba87ca99b15139c230dfc7fb93249daba3a9e6 --- /dev/null +++ b/plugins/Dashboard/lang/ar.json @@ -0,0 +1,37 @@ +{ + "Dashboard": { + "AddAWidget": "أضف عنصر واجهة", + "AddPreviewedWidget": "إضغط لتضيف عنصر واجهة إلى لوحة المعلومات", + "ChangeDashboardLayout": "تغيير تخطيط لوحة المعلومات", + "CopyDashboardToUser": "إنسخ لوحة المعلومات إلى مستخدِم", + "CreateNewDashboard": "إنشاء لوحة معلومات جديدة", + "Dashboard": "لوحة المعلومات", + "DashboardCopied": "تم نسخ لوحة المعلومات الحالية إلى المستخدم المحدَّد بنجاح.", + "DashboardEmptyNotification": "اللوحة الرئيسية فارغة تماماً. ابدأ بإضافة بعد التطبيقات أو قم بتنضيد اللوحة الرئيسية إلى الإعدادات الافتراضية.", + "DashboardName": "اسم اللوحة:", + "DashboardOf": "لوحة معلومات %s", + "DefaultDashboard": "اللوحة الافتراضية - تستخدم المجموعة الافتراضية من التطبيقات وتخطيط الأعمدة.", + "DeleteWidgetConfirm": "هل ترغب حقاً في حذف لوحة الأداة هذه من لوحة التحكم?", + "EmptyDashboard": "لوحة المعلومات فارغة - إختر عناصر التحكم التي تفضّلها", + "LoadingWidget": "جاري تحميل الأداة، الرجاء الانتظار…", + "ManageDashboard": "إدارة اللوحة", + "Maximise": "تكبير", + "Minimise": "تصغير", + "NotUndo": "لن يمكنك التراجع عن هذه العملية.", + "PluginDescription": "لوحة معلومات تحليلات الويب الخاصة بك. خصّص لوحة معلوماتك بإضافة عناصر واجهة جديدة وسحبها وإسقاطها أينما شئت ، بإمكانك أيضاً تغيير تخطيط أعمدة لوحة المعلومات. يمكن لكل مستخدم إدارة لوحة معلومات يخصصها كما يشاء.", + "RemoveDashboard": "إزالة اللوحة", + "RemoveDashboardConfirm": "هل أنت متأكد من رغبتك حذف لوحة المعلومات \"%s\" ؟", + "RemoveDefaultDashboardNotPossible": "لايمكن حذف لوحة المعلومات الافتراضية", + "RenameDashboard": "إعادة تسمية اللوحة", + "ResetDashboard": "أعد ضبط لوحة المعلومات", + "ResetDashboardConfirm": "هل ترغب حقاً في تنضيد تخطيط اللوحة الرئيسية ومجموعة التطبيقات المعروضة فيها إلى الافتراضية؟", + "SelectDashboardLayout": "فضلاً إختر التخطيط الجديد للوحة المعلومات", + "SelectWidget": "اختر لوحة الأداة لإضافتها للوحة التحكم", + "SetAsDefaultWidgets": "اضبط كمجموعة التطبيقات الافتراضية", + "SetAsDefaultWidgetsConfirm": "هل أنت متأكد أنك تريد جعل عناصر الواجهة المختارة حالياً وتخطيط لوحة المعلومات هذا القالب الافتراضي للوحة المعلومات ؟", + "SetAsDefaultWidgetsConfirmHelp": "سيتم استخدام مجموعة التطبيقات هذه ومخطط أعمدة اللوحة عند إنشاء لوحات جديدة أو عند استخدام خاصية \"%s\".", + "TopLinkTooltip": "عرض تقارير إحصاءات الويب لـ %s", + "WidgetNotFound": "لم يتم العثور على الأداة", + "WidgetPreview": "معاينة لوحة الأداة" + } +} diff --git a/plugins/Dashboard/lang/az.json b/plugins/Dashboard/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..fd1bef7ce4300cf8108f6628ce835d70b3e43695 --- /dev/null +++ b/plugins/Dashboard/lang/az.json @@ -0,0 +1,6 @@ +{ + "Dashboard": { + "AddAWidget": "Mini-proqramı bu lövhəyə əlavə et", + "Dashboard": "Lövhə" + } +} diff --git a/plugins/Dashboard/lang/be.json b/plugins/Dashboard/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..9684c24f1d47639b9df39e4149d655cf08f806fd --- /dev/null +++ b/plugins/Dashboard/lang/be.json @@ -0,0 +1,12 @@ +{ + "Dashboard": { + "AddPreviewedWidget": "Дадаць предпросмотренный віджэт на інформпанель", + "Dashboard": "Інфармацыйная панэль", + "DeleteWidgetConfirm": "Вы ўпэўнены, што жадаеце выдаліць гэты віджэт з інфармацыйнай панэлі?", + "LoadingWidget": "Загрузка віджэта, калі ласка пачакайце…", + "Maximise": "Максімалізаваць", + "SelectWidget": "Абярыце віджэт для дадання на інформпанель", + "WidgetNotFound": "Віджэт не знойдзены", + "WidgetPreview": "Прадпрагляд віджэта" + } +} diff --git a/plugins/Dashboard/lang/bg.json b/plugins/Dashboard/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..668cc7a5ac38717413e1c6b4b1771cefc612d29b --- /dev/null +++ b/plugins/Dashboard/lang/bg.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Добави нов уиджет", + "AddPreviewedWidget": "Добави уиджета на таблото", + "ChangeDashboardLayout": "Сменете изледа на таблото", + "CopyDashboardToUser": "Копиране на табло при потребител", + "CreateNewDashboard": "Направете ново табло", + "Dashboard": "Табло", + "DashboardCategoryHelp": "Това е страница на таблото. Таблата за управление са колекция от уиджети на Matomo, които добавяте сами, за да отговарят на Вашите специфични нужди. Смесете и съпоставете които и да е от уиджетите на Matomo, за да получите данните %1$s*, от които*%2$s се нуждаете с един поглед.", + "DashboardCopied": "Настоящото табло е успешно копирано при избрания потребител.", + "DashboardEmptyNotification": "Вашето табло не съдържа никакви уиджети. Започнете с добавяне на някои уиджети или просто нулирайте таблото, за да изберете уиджетите по подразбиране.", + "DashboardName": "Име на табло:", + "DashboardOf": "Табло за %s", + "DefaultDashboard": "Табло по подразбиране - Използвайте уиджетите по подразбиране и изгледа на колоните", + "DeleteWidgetConfirm": "Наистина ли искате да изтриете този уиджет от таблото?", + "EmptyDashboard": "Изпразнете таблото - Изберете любимите си уиджети", + "LoadingWidget": "Зарежда уиджета, почакайте…", + "ManageDashboard": "Управлявате таблото", + "Maximise": "Максимизиране", + "Minimise": "Минимизиране", + "NotUndo": "Вие няма да можете да отмените тази операция.", + "PluginDescription": "Вашето табло за уеб анализи. Настройте Вашия панел добавяйки нови уиджети, които можете да влачите, също така променяйки оформлението на колоните. Всеки потребител поотделно може да настрой своето табло.", + "RemoveDashboard": "Премахнете таблото", + "RemoveDashboardConfirm": "Сигурни ли сте че искате да премахнете таблото \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Таблото по подразбиране не може да бъде премахнато", + "RenameDashboard": "Сменете името на таблото", + "ResetDashboard": "Зареди отново таблото", + "ResetDashboardConfirm": "Сигурни ли сте, че искате да заредите отново плана на таблото с избраните по подразбиране уиджети?", + "SelectDashboardLayout": "Моля, изберете Вашият нов изглед на табло", + "SelectWidget": "Изберете уиджет, който да добавите на таблото", + "SetAsDefaultWidgets": "Поставете по подразбиране избора на уиджети", + "SetAsDefaultWidgetsConfirm": "Сигурни ли сте, че искате да настройте настоящата селекция на уиджети и оформление на таблото по подразбиране?", + "SetAsDefaultWidgetsConfirmHelp": "Селекцията на уиджетии колоните на изгледа на таблото ще бъдат използвани, когато потребителят създаде ново табло, или когато \"%s\" функция е използвана.", + "TopLinkTooltip": "Вижте доклада за уеб анализи за %s.", + "TopMenuTitle": "Анализ", + "WidgetNotFound": "Несъществуващ уиджет", + "WidgetPreview": "Преглед на уиджета" + } +} diff --git a/plugins/Dashboard/lang/bn.json b/plugins/Dashboard/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..343b1457b160d407293df18bd2047b04a6b7cfe8 --- /dev/null +++ b/plugins/Dashboard/lang/bn.json @@ -0,0 +1,9 @@ +{ + "Dashboard": { + "CreateNewDashboard": "নতুন ড্যাশবোর্ড তৈরি করুন", + "Dashboard": "ড্যাশবোর্ড", + "DashboardName": "ড্যাশবোর্ডের নাম:", + "RemoveDashboard": "ড্যাশবোর্ড মুছে ফেলুন", + "RenameDashboard": "ড্যাশবোর্ডের নাম পরিবর্তন" + } +} diff --git a/plugins/Dashboard/lang/bs.json b/plugins/Dashboard/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..597a46ab128d2cfeef1d71696a0f433ff1bd3860 --- /dev/null +++ b/plugins/Dashboard/lang/bs.json @@ -0,0 +1,7 @@ +{ + "Dashboard": { + "Dashboard": "Kontrolna ploča", + "Maximise": "Maksimiziraj", + "Minimise": "Minimiziraj" + } +} diff --git a/plugins/Dashboard/lang/ca.json b/plugins/Dashboard/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..8809f3db5fa378431fa864669ce2e5cbd5cf2b86 --- /dev/null +++ b/plugins/Dashboard/lang/ca.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Afegiu un giny a aquest tauler de control", + "AddPreviewedWidget": "Afegeix el giny al tauler", + "ChangeDashboardLayout": "Canviar el disseny del tauler", + "CopyDashboardToUser": "Copiar tauler a l'usuari", + "CreateNewDashboard": "Crear un nou tauler", + "Dashboard": "Tauler", + "DashboardCategoryHelp": "Aquesta és una pàgina de tauler. Els taulers de control són una col·lecció de ginys de Matomo per afegir tu mateix per adaptar-los a les teves necessitats específiques. Barreja i combina qualsevol dels widgets de Matomo per obtenir les dades que %1$s*tu*%2$s necessites veure d'un cop d'ull.", + "DashboardCopied": "El tauler de control actual s'ha copiat correctament a l'usuari seleccionat.", + "DashboardEmptyNotification": "El vostre tauler no conté cap giny. Comenceu per afegir alguns ginys o restablieu el tauler a la selecció de ginys per defecte.", + "DashboardName": "Nom del tauler:", + "DashboardOf": "Tauler de %s", + "DefaultDashboard": "Tauler per defecte - Utilitzant la selecció de ginys i l'estructura de columnes per defecte", + "DeleteWidgetConfirm": "Realment voleu esborrar aquest giny?", + "EmptyDashboard": "Tauler buit - Seleccioneu els vostres ginys preferits", + "LoadingWidget": "S'està carregant el giny…", + "ManageDashboard": "Administreu aquest tauler de control", + "Maximise": "Maximitza", + "Minimise": "Minimitza", + "NotUndo": "No podreu desfer aquesta operació.", + "PluginDescription": "El teu taulell d'analítica web. Personalitza el teu taulell afegint nous ginys, arrossega i solta per a moure'ls i canvia l'estructura de columnes. Cada usuari pot tenir el seu taulell personalitzat.", + "RemoveDashboard": "Elimina el tauler", + "RemoveDashboardConfirm": "Esteu segurs que voleu eliminar el tauler \"%s\"?", + "RemoveDefaultDashboardNotPossible": "El tauler predeterminat no es pot eliminar", + "RenameDashboard": "Reanomena el tauler", + "ResetDashboard": "Reinicialitza el tauler", + "ResetDashboardConfirm": "Esteu segurs que voleu reiniciar la disposició del vostre tauler a la selecció de ginys per defecte?", + "SelectDashboardLayout": "Seleccioneu la nova disposició del vostre tauler", + "SelectWidget": "Escolliu el giny que voleu afegir a la consola", + "SetAsDefaultWidgets": "Estableix com a selecció de widgets per defecte", + "SetAsDefaultWidgetsConfirm": "Esteu segurs que voleu definir la selecció de ginys i la disposició del tauler actuals per a la plantilla de tauler per defecte?", + "SetAsDefaultWidgetsConfirmHelp": "Aquesta selecció de ginys i la disposició de columnes del tauler es faran servir quan qualsevol usuari crei un nou tauler o quan s'utiltiza la funció \"%s\".", + "TopLinkTooltip": "Mostra els informes d'analítica web per %s.", + "TopMenuTitle": "Analítica", + "WidgetNotFound": "Aquest giny no s'ha trobat", + "WidgetPreview": "Previsualitza el giny" + } +} diff --git a/plugins/Dashboard/lang/cs.json b/plugins/Dashboard/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..f5a9589879cb710f39cc80577fe01c9d13d70209 --- /dev/null +++ b/plugins/Dashboard/lang/cs.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Přidat widget", + "AddPreviewedWidget": "Přidat widget v náhledu na nástěnku", + "ChangeDashboardLayout": "Změnit rozvržení nástěnky", + "CopyDashboardToUser": "Zkopírovat přehled uživateli", + "CreateNewDashboard": "Vytvořit novou nástěnku", + "Dashboard": "Nástěnka", + "DashboardCategoryHelp": "Toto je stránka s ovládacím panelem. Ovládací panely jsou sbírkou widgetů Matomo, které si sami přidáváte podle svých specifických potřeb. Můžete kombinovat libovolné widgety Matomo a získat tak na první pohled data, která %1$s*vy*%2$s potřebujete.", + "DashboardCopied": "Přehled byl úspěšně zkopírován vybranému uživateli.", + "DashboardEmptyNotification": "Vaše nástěnka neobsahuje žádné widgety. Nějaké přidejte, nebo obnovte výchozí výběr widgetů na nástěnce.", + "DashboardName": "Jméno nástěnky:", + "DashboardOf": "Nástěnka %s", + "DefaultDashboard": "Výchozí nástěnka - používáte výchozí rozvržení nástěnky", + "DeleteWidgetConfirm": "Opravdu chcete odstranit tento widget z nástěnky?", + "EmptyDashboard": "Prázdná nástěnka - vyberte si své oblíbené widgety", + "LoadingWidget": "Načítání widgetu, chvíli strpení…", + "ManageDashboard": "Správa nástěnky", + "Maximise": "Maximalizovat", + "Minimise": "Minimalizovat", + "NotUndo": "Tuto operaci nemůžete vrátit zpět.", + "PluginDescription": "Vaše nástěnka analýzy webu. Přizpůsobte si vaši nástěnku přidáním nových widgetů, jejich přesunutím nebo změnou rozvržení sloupců. Každý uživatel může přizpůsobit svou vlastní nástěnku.", + "RemoveDashboard": "Odstranit nástěnku", + "RemoveDashboardConfirm": "Opravdu chcete odstranit nástěnku: %s?", + "RemoveDefaultDashboardNotPossible": "Výchozí nástěnka nemůže být odstraněna", + "RenameDashboard": "Přejmenovat nástěnku", + "ResetDashboard": "Obnovit nástěnku", + "ResetDashboardConfirm": "Opravdu chcete obnovit nastavení nástěnky do továrního stavu?", + "SelectDashboardLayout": "Prosím vyberte nové rozvržení nástěnky", + "SelectWidget": "Zvolte widget pro přidání na nástěnku", + "SetAsDefaultWidgets": "Nastavit výchozí výběr widgetů", + "SetAsDefaultWidgetsConfirm": "Opravdu chcete nastavit toto rozvržení widgetů jako výchozí?", + "SetAsDefaultWidgetsConfirmHelp": "Tyto widgety a jejich rozvržení budou použity vždy, když někdo vytvoří novou nástěnku nebo použije vlastnost %s.", + "TopLinkTooltip": "Zobraz analytické hlášení webu pro %s.", + "TopMenuTitle": "Analytika", + "WidgetNotFound": "Widget nenalezen", + "WidgetPreview": "Náhled widgetu" + } +} diff --git a/plugins/Dashboard/lang/cy.json b/plugins/Dashboard/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..a619d63aa20a5ba818780e765b5a1e01bde3a992 --- /dev/null +++ b/plugins/Dashboard/lang/cy.json @@ -0,0 +1,5 @@ +{ + "Dashboard": { + "Dashboard": "Dangosfwrdd" + } +} diff --git a/plugins/Dashboard/lang/da.json b/plugins/Dashboard/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..9ec54b7e9a2f63af88944152c14357d3ad2e52eb --- /dev/null +++ b/plugins/Dashboard/lang/da.json @@ -0,0 +1,37 @@ +{ + "Dashboard": { + "AddAWidget": "Tilføj modul", + "AddPreviewedWidget": "Tilføj forhåndsvist modul til kontrolpanelet", + "ChangeDashboardLayout": "Skift udformning på kontrolpanel", + "CopyDashboardToUser": "Kopier kontrolpanel til bruger", + "CreateNewDashboard": "Opret nyt kontrolpanel", + "Dashboard": "Kontrolpanel", + "DashboardCopied": "Aktuelt kontrolpanel blev succesfuldt kopieret til den valgte bruger.", + "DashboardEmptyNotification": "Kontrolpanelet indeholder ikke nogen moduler. Start med at tilføje nogle moduler eller nulstille kontrolpanel til standard.", + "DashboardName": "Kontrolpanel navn:", + "DashboardOf": "Kontrolpanel af %s", + "DefaultDashboard": "Standard kontrolpanel - Bruger standard moduler og udformning", + "DeleteWidgetConfirm": "Bekræft fjernelse af modul fra kontrolpanelet?", + "EmptyDashboard": "Tomt kontrolpanel - Vælg favorit moduler", + "LoadingWidget": "Indlæser modul, vent…", + "ManageDashboard": "Administrer kontrolpanel", + "Maximise": "Maksimer", + "Minimise": "Minimer", + "NotUndo": "Handlingen kan ikke fortrydes.", + "PluginDescription": "Webanalyse kontrolpanel. Tilpas dit kontrolpanel ved at tilføje nye moduler, træk og slip dem rundt, og ændre kontrolpanelets kolonnelayout. Hver bruger kan styre deres eget brugerdefinerede kontrolpanel.", + "RemoveDashboard": "Fjern kontrolpanel", + "RemoveDashboardConfirm": "Bekræft fjernelse af kontrolpanel \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Standard kontrolpanel kan ikke fjernes", + "RenameDashboard": "Omdøb kontrolpanel", + "ResetDashboard": "Nulstil kontrolpanel", + "ResetDashboardConfirm": "Nulstil kontrolpanel til standard?", + "SelectDashboardLayout": "Vælg ny udformning på kontrolpanel", + "SelectWidget": "Vælg modul, som skal tilføjes kontrolpanelet", + "SetAsDefaultWidgets": "Indstil som standard modul valg", + "SetAsDefaultWidgetsConfirm": "Skal de nuværende modul udvælgelser og kontrolpanel udformning bruges som standard kontrolpanel skabelon?", + "SetAsDefaultWidgetsConfirmHelp": "Modul udvælgelse og kontrolpanelets udformning vil blive brugt når en bruger opretter et nyt kontrolpanel, eller når \"%s\" -funktionen bruges.", + "TopLinkTooltip": "Vis webanalyse-rapporter for %s.", + "WidgetNotFound": "Fandt ikke modul", + "WidgetPreview": "Forhåndsvisning af modul" + } +} diff --git a/plugins/Dashboard/lang/de.json b/plugins/Dashboard/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..99808d4e95c545bba4e4595e484cc54afb915597 --- /dev/null +++ b/plugins/Dashboard/lang/de.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Widget zu diesem Dashboard hinzufügen", + "AddPreviewedWidget": "Widget aus der Vorschau zum Dashboard hinzufügen", + "ChangeDashboardLayout": "Dashboardlayout verändern", + "CopyDashboardToUser": "Dashboard zu Benutzer kopieren", + "CreateNewDashboard": "Neues Dashboard erstellen", + "Dashboard": "Dashboard", + "DashboardCategoryHelp": "Dies ist eine Dashboard Seite. Dashboards sind Sammlungen von Matomo Widgets welche Sie für Ihre persönlichen Bedürfnisse anpassen können. Mixen und kombinieren Sie beliebig Matomo Widgets um die Daten zu erhalten, die %1$s*Sie*%2$s auf einen Blick benötigen.", + "DashboardCopied": "Aktuelles Dashboard wurde erfolgreich zum ausgewählten Benutzer kopiert.", + "DashboardEmptyNotification": "Ihr Dashboard enthält keine Widgets. Sie können jetzt Widgets hinzufügen oder das Dashboard auf die Standard Widgetauswahl zurücksetzen.", + "DashboardName": "Dashboard Name:", + "DashboardOf": "Dashboard von %s", + "DefaultDashboard": "Standard Dashboard - Verwendet Standardwidgets und Spaltenlayout", + "DeleteWidgetConfirm": "Wollen Sie wirklich dieses Widget vom Dashboard entfernen?", + "EmptyDashboard": "Leeres Dashboard - Wählen Sie Ihre bevorzugten Widgets", + "LoadingWidget": "Lade Widget, bitte warten…", + "ManageDashboard": "Verwalten Sie dieses Dashboard", + "Maximise": "Vergrößern", + "Minimise": "Minimieren", + "NotUndo": "Sie können diese Aktion nicht rückgängig machen.", + "PluginDescription": "Ihr Web Analyse Dashboard. Personalisieren Sie Ihr Dashboard, in dem Sie neue Widgets hinzufügen, per Drag-and-drop herumschieben, und das Dashboard Spaltenlayout ändern. Jeder Benutzer kann sein eigenes personalisiertes Dashboard verwalten.", + "RemoveDashboard": "Dashboard entfernen", + "RemoveDashboardConfirm": "Sind Sie sicher, dass Sie das Dashboard \"%s\" löschen möchten?", + "RemoveDefaultDashboardNotPossible": "Das Standard-Dashboard kann nicht entfernt werden", + "RenameDashboard": "Dashboard umbenennen", + "ResetDashboard": "Dashboard zurücksetzen", + "ResetDashboardConfirm": "Wollen Sie wirklich das Dashboardlayout auf die Standardeinstellungen zurücksetzen?", + "SelectDashboardLayout": "Bitte wählen Sie ein neues Dashboardlayout", + "SelectWidget": "Wählen Sie hier das Widget aus, welches zum Dashboard hinzugefügt werden soll", + "SetAsDefaultWidgets": "Als Standard Widgetauswahl speichern", + "SetAsDefaultWidgetsConfirm": "Sind Sie sicher, dass Sie die aktuelle Widgetauswahl und das Layout als das Standard Dashboardvorlage speichern möchten?", + "SetAsDefaultWidgetsConfirmHelp": "Diese Widgetauswahl und das aktuelle Spaltenlayout werden verwendet, wenn ein neues Dashboard erstellt wird oder wenn \"%s\" verwendet wird.", + "TopLinkTooltip": "Zeige Webanalytik Berichte für %s.", + "TopMenuTitle": "Analysen", + "WidgetNotFound": "Widget nicht gefunden", + "WidgetPreview": "Widget-Vorschau" + } +} diff --git a/plugins/Dashboard/lang/dv.json b/plugins/Dashboard/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Dashboard/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Dashboard/lang/el.json b/plugins/Dashboard/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..04604162c7372d26d58cc41f133037cf162c2e10 --- /dev/null +++ b/plugins/Dashboard/lang/el.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Προσθήκη μικροεφαρμογής στον πίνακα", + "AddPreviewedWidget": "Πατήστε για να προσθέσετε τη μικροεφαρμογή στον κεντρικό πίνακα", + "ChangeDashboardLayout": "Αλλαγή εμφάνισης κεντρικού πίνακα", + "CopyDashboardToUser": "Αντιγραφή κεντρικού πίνακα στο χρήστη", + "CreateNewDashboard": "Δημιουργία νέου κεντρικού πίνακα", + "Dashboard": "Κεντρικός Πίνακας", + "DashboardCategoryHelp": "Αυτή είναι μια σελίδα ταμπλό. Τα ταμπλό είναι μια συλλογή από γραφικά συστατικά μέρη του Matomo που προσθέτετε οι ίδιοι ώστε να σας εξυπηρετούν. Ανακατέψτε και τοποθετήστε οποιαδήποτε από τα γραφικά συστατικά μέρη του Matomo για να έχετε τα δεδομένα που %1$s*εσείς*%2$s χρειάζεστε με μια ματιά.", + "DashboardCopied": "Ο τρέχων κεντρικός πίνακας αντιγράφτηκε επιτυχώς στον επιλεγμένο χρήστη.", + "DashboardEmptyNotification": "Ο Κεντρικός σας Πίνακας δεν περιέχει μικροεφαρμογές. Ξεκινήστε βάζοντας ορισμένες ή απλά επαναφέρτε τον κεντρικό πίνακα στην προεπιλεγμένη διάταξη μικροεφαρμογών.", + "DashboardName": "Ονομασία κεντρικού πίνακα:", + "DashboardOf": "Κεντρικός πίνακας του %s", + "DefaultDashboard": "Προεπιλεγμένος Κεντρικός Πίνακας - Χρήση επιλογής προεπιλεγμένων μικροεφαρμογών και διάταξης στηλών", + "DeleteWidgetConfirm": "Είστε σίγουρος ότι θέλετε να αφαιρέσετε αυτή τη μικροεφαρμογή από τον κεντρικό πίνακα;", + "EmptyDashboard": "Άδειασμα κεντρικού πίνακα - Επιλέξτε τις αγαπημένες μικροεφαρμογές σας", + "ExportThisDashboard": "Εξαγωγή της εργαλειοθήκης", + "LoadingWidget": "Φόρτωση μικροεφαρμογής, περιμένετε…", + "ManageDashboard": "Διαχείριση Κεντρικού πίνακα", + "Maximise": "Μεγιστοποίηση", + "Minimise": "Ελαχιστοποίηση", + "NotUndo": "Αυτή η λειτουργεία δεν είναι αναστρέψιμη.", + "PluginDescription": "Ο κεντρικός πίνακας Αναλυτικών Ιστού. Προσαρμόστε τον πίνακα με την προσθήκη νέων γραφικών συστατικών, σύρτε και αφήστε τα όπου θέλετε και αλλάξτε την διάταξη στήλης του πίνακα. Κάθε χρήστης μπορεί να διαχειρίζεται τον δικό του πίνακα.", + "RemoveDashboard": "Απομάκρυνση κεντρικού πίνακα", + "RemoveDashboardConfirm": "Θέλετε, σίγουρα, να απομακρύνετε τον Κεντρικό Πίνακα «%s»;", + "RemoveDefaultDashboardNotPossible": "Ο προκαθορισμένος πίνακας εργαλείων δεν είναι δυνατό να αφαιρεθεί", + "RenameDashboard": "Μετονομασία κεντρικού πίνακα", + "ResetDashboard": "Επαναφορά κεντρικού πίνακα", + "ResetDashboardConfirm": "Θέλετε σίγουρα να επαναφέρετε τον κεντρικό σας πίνακα στις προεπιλεγμένες Μικροεφαρμογές;", + "SelectDashboardLayout": "Επιλέξτε τη νέα εμφάνιση κεντρικού πίνακα", + "SelectWidget": "Επιλέξτε τη μικροεφαρμογή για προσθήκη στον κεντρικό πίνακα", + "SetAsDefaultWidgets": "Ορισμός ως προεπιλεγμένης ομάδας μικροεφαρμογών", + "SetAsDefaultWidgetsConfirm": "Θέλετε, σίγουρα, να ορίσετε την τρέχουσα επιλογή μικροεφαρμογών και διάταξης κεντρικού πίνακα ως το προεπιλεγμένο πρότυπο κεντρικού πίνακα;", + "SetAsDefaultWidgetsConfirmHelp": "Αυτή η επιλογή μικροεφαρμογών και διάταξης στηλών κεντρικού πίνακα θα χρησιμοποιηθεί όταν κάθε χρήστης δημιουργεί έναν νέο κεντρικό πίνακα ή όταν το χαρακτηριστικό «%s» χρησιμοποιείται.", + "TopLinkTooltip": "Προβολή αναφορών Στατιστικών Ιστού για %s.", + "TopMenuTitle": "Αναλυτικά", + "WidgetNotFound": "Η Μικροεφαρμογή δεν βρέθηκε", + "WidgetPreview": "Προεπισκόπηση Μικροεφαρμογής" + } +} diff --git a/plugins/Dashboard/lang/en.json b/plugins/Dashboard/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..afd3aaba7408f1b27c4ed40c59d20d103439648c --- /dev/null +++ b/plugins/Dashboard/lang/en.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Add widget to this dashboard", + "AddPreviewedWidget": "Click to add widget to the dashboard", + "ChangeDashboardLayout": "Change dashboard layout", + "CopyDashboardToUser": "Copy dashboard to user", + "CreateNewDashboard": "Create new dashboard", + "Dashboard": "Dashboard", + "DashboardCopied": "Current dashboard successfully copied to selected user.", + "DashboardEmptyNotification": "Your Dashboard does not contain any widgets. Start by adding some widgets or just reset the dashboard to the default widget selection.", + "DashboardName": "Dashboard name:", + "DashboardOf": "Dashboard of %s", + "DefaultDashboard": "Default dashboard - Using default widgets selection and columns layout", + "DeleteWidgetConfirm": "Are you sure you want to delete this widget from the dashboard?", + "EmptyDashboard": "Empty dashboard - Pick your favorite widgets", + "LoadingWidget": "Loading widget, please wait…", + "ManageDashboard": "Manage Dashboard", + "ExportThisDashboard": "Export dashboard", + "Maximise": "Maximise", + "Minimise": "Minimise", + "NotUndo": "You will not be able to undo this operation.", + "PluginDescription": "Your Web Analytics Dashboard. Customise your dashboard by adding new widgets, drag and drop them around, and change the dashboard column layout. Each user can manage their own custom dashboard.", + "RemoveDashboard": "Remove dashboard", + "RemoveDashboardConfirm": "Are you sure you want to remove the dashboard \"%s\"?", + "RemoveDefaultDashboardNotPossible": "The default dashboard cannot be removed", + "RenameDashboard": "Rename dashboard", + "ResetDashboard": "Reset dashboard", + "ResetDashboardConfirm": "Are you sure you want to reset your dashboard layout to the default Widgets selection?", + "SelectDashboardLayout": "Please select your new dashboard layout", + "SelectWidget": "Select the widget to add in the dashboard", + "SetAsDefaultWidgets": "Set as default widgets selection", + "SetAsDefaultWidgetsConfirm": "Are you sure you want to set the current widgets selection and dashboard layout as the default dashboard template?", + "SetAsDefaultWidgetsConfirmHelp": "This widgets selection and dashboard columns layout will be used when any user creates a new dashboard, or when \"%s\" feature is used.", + "TopLinkTooltip": "View Web Analytics reports for %s.", + "TopMenuTitle": "Analytics", + "WidgetNotFound": "Widget not found", + "WidgetPreview": "Widget preview", + "DashboardCategoryHelp": "This is a dashboard page. Dashboards are a collection of Matomo's widgets that you add yourself to suit your specific needs. Mix and match any of Matomo's widgets to get the data %1$s*you*%2$s need at a glance." + } +} diff --git a/plugins/Dashboard/lang/eo.json b/plugins/Dashboard/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..c60e1890c8212ebb8c5fdaba3bf99a1629e19623 --- /dev/null +++ b/plugins/Dashboard/lang/eo.json @@ -0,0 +1,5 @@ +{ + "Dashboard": { + "Dashboard": "Panelo" + } +} diff --git a/plugins/Dashboard/lang/es-ar.json b/plugins/Dashboard/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..f388743b9a34c18abccea8a171142f45003e618c --- /dev/null +++ b/plugins/Dashboard/lang/es-ar.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Agregá un widget", + "AddPreviewedWidget": "Hacé clic para agregar el widget al panel", + "ChangeDashboardLayout": "Cambiar el diseño del panel", + "CopyDashboardToUser": "Copiar panel al usuario", + "CreateNewDashboard": "Crear nuevo panel", + "Dashboard": "Panel", + "DashboardCategoryHelp": "Esta es una página de panel. Los paneles son una colección de widgets de Matomo que agregas vos mismo para cubrir tus necesidades específicas. Mezclá y combiná cualquiera de los widgets de Matomo para obtener los datos que %1$sVOS%2$s necesitás de un saque.", + "DashboardCopied": "Se copió exitosamente el panel actual al usuario seleccionado.", + "DashboardEmptyNotification": "Tu panel no contiene ningún widget. Comenzá agregando alguno o simplemente reiniciá el panel a la selección predeterminada de widgets.", + "DashboardName": "Nombre del panel:", + "DashboardOf": "Panel de %s", + "DefaultDashboard": "Panel predeterminado - Usando la selección de widgets y diseño de columnas predeterminado", + "DeleteWidgetConfirm": "¿Estás seguro que querés eliminar este widget del panel?", + "EmptyDashboard": "Panel vacío - Elegí tus widgets favoritos", + "LoadingWidget": "Cargando widget. Por favor, esperá…", + "ManageDashboard": "Administrar panel", + "Maximise": "Maximizar", + "Minimise": "Minimizar", + "NotUndo": "No podrás deshacer esta operación.", + "PluginDescription": "Tu panel de análisis web. Personalizá tu panel agregando nuevos widgets, arrastrándolos y soltándolos, y cambiando el diseño de las columnas del panel. Cada usuario puede administrar su propio panel personalizado.", + "RemoveDashboard": "Quitar panel", + "RemoveDashboardConfirm": "¿Estás seguro que querés eliminar el panel \"%s\"?", + "RemoveDefaultDashboardNotPossible": "No se puede quitar el panel predeterminado", + "RenameDashboard": "Renombrar el panel", + "ResetDashboard": "Reiniciar panel", + "ResetDashboardConfirm": "¿Estás seguro que querés restablecer el diseño de tu panel a la selección predeterminada de widgets?", + "SelectDashboardLayout": "Por favor, seleccioná tu nuevo diseño de panel", + "SelectWidget": "Seleccioná el widget para agregar en el panel", + "SetAsDefaultWidgets": "Establecer como selección predeterminada de widgets", + "SetAsDefaultWidgetsConfirm": "¿Estás seguro que querés establecer la selección actual de widgets y el diseño del panel como la plantilla predeterminada?", + "SetAsDefaultWidgetsConfirmHelp": "Esta selección de widgets y diseño de columnas del panel será usada cuando un usuario cree un nuevo panel o cuando se use la función \"%s\".", + "TopLinkTooltip": "Ver informes de análisis web para %s.", + "TopMenuTitle": "Análisis", + "WidgetNotFound": "Widget no encontrado", + "WidgetPreview": "Previsualización del Widget" + } +} diff --git a/plugins/Dashboard/lang/es.json b/plugins/Dashboard/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..36a739cece6038a6cd2cdeebf9738a7f8440378a --- /dev/null +++ b/plugins/Dashboard/lang/es.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Añadir widget a este tablero", + "AddPreviewedWidget": "Añade el módulo al panel de control", + "ChangeDashboardLayout": "Modificar distribución del tablero", + "CopyDashboardToUser": "Copiar tablero al usuario", + "CreateNewDashboard": "Crear nuevo tablero", + "Dashboard": "Tablero", + "DashboardCategoryHelp": "Esta es una página de tablero. Los paneles son una colección de widgets de Matomo que agrega usted mismo para satisfacer sus necesidades específicas. Mezcle y combine cualquiera de los widgets de Matomo para obtener los datos que %1$s*usted*%2$s necesita de un vistazo.", + "DashboardCopied": "Tablero actual copiado con éxito al usuario seleccionado.", + "DashboardEmptyNotification": "Su panel de control no contiene ningún módulo. Comience añadiendo alguno o restablezca el panel de control con la selección de módulos por defecto.", + "DashboardName": "Nombre del tablero:", + "DashboardOf": "Tablero de %s", + "DefaultDashboard": "Panel de control por defecto - Usando la selección por defecto de módulos y la disposición de columnas", + "DeleteWidgetConfirm": "¿Está seguro que desea borrar este módulo de su panel de control?", + "EmptyDashboard": "Panel de control vacío - Elija sus módulos preferidos", + "ExportThisDashboard": "Exportar tablero", + "LoadingWidget": "Cargando módulo, por favor espere…", + "ManageDashboard": "Gestionar tablero", + "Maximise": "Maximizar", + "Minimise": "Minimizar", + "NotUndo": "No podrá deshacer esta operación.", + "PluginDescription": "Su panel de control. Personalice su tablero añadiendo nuevos módulos arrastrándolos y soltándolos; cambie la disposición de las columnas del panel de control. Cada usuario puede administrar su propio panel de control personalizado.", + "RemoveDashboard": "Elimina el tablero", + "RemoveDashboardConfirm": "¿Está seguro que desea eliminar el tablero \"%s\"?", + "RemoveDefaultDashboardNotPossible": "El tablero predeterminado no puede ser eliminado", + "RenameDashboard": "Renombrar el tablero", + "ResetDashboard": "Reinicia el tablero", + "ResetDashboardConfirm": "¿Realmente desea restablecer la distribución del panel de control y regresar a la selección de módulos por defecto ?", + "SelectDashboardLayout": "Por favor seleccione su nueva distribución de tablero", + "SelectWidget": "Selecciona el módulo para añadirlo al panel de control", + "SetAsDefaultWidgets": "Establecer como selección de módulos por defecto", + "SetAsDefaultWidgetsConfirm": "¿Está seguro que desea establecer esta selección y disposición de tablero como la plantilla de tablero por defecto?", + "SetAsDefaultWidgetsConfirmHelp": "Esta selección de módulos y disposición de columnas del panel de control será usada cuando un usuario cree un nuevo panel de control o cuando se use la función \"%s\".", + "TopLinkTooltip": "Ver los reportes de Análisis de internet para %s.", + "TopMenuTitle": "Analíticas", + "WidgetNotFound": "Reproductor no encontrado", + "WidgetPreview": "Previsualización del reproductor" + } +} diff --git a/plugins/Dashboard/lang/et.json b/plugins/Dashboard/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..5c1fe9f2ad0eb751928066f82e65ecb0343ab9e6 --- /dev/null +++ b/plugins/Dashboard/lang/et.json @@ -0,0 +1,28 @@ +{ + "Dashboard": { + "AddAWidget": "Lisa vidin", + "AddPreviewedWidget": "Vali et lisada vidin töölauale", + "ChangeDashboardLayout": "Muuda töölaua paigutust", + "CopyDashboardToUser": "Kopeeri töölaud kasutajale", + "CreateNewDashboard": "Loo uus töölaud", + "Dashboard": "Töölaud", + "DashboardName": "Töölaua nimi:", + "DashboardOf": "%s töölaud", + "DefaultDashboard": "Vaikimisi töölaud - kasutab vaikimisi vidinaid ja paigutust", + "DeleteWidgetConfirm": "Oled sa kindel selle mooduli kustutamises oma töölaualt?", + "EmptyDashboard": "Tühi töölaud - vali oma lemmik vidinad", + "LoadingWidget": "Laen moodulit, palun oota…", + "ManageDashboard": "Halda töölauda", + "Maximise": "Maksimeeri", + "Minimise": "Minimeeri", + "NotUndo": "Sul ei ole võimalik antud tegevust tagasi võtta.", + "RemoveDashboard": "Eemalda töölaud", + "RenameDashboard": "Nimeta töölaud ümber", + "ResetDashboard": "Algseadista töölaud", + "SelectWidget": "Vali töölauale lisatav moodul", + "SetAsDefaultWidgets": "Määra vaikimisi valitud vidinateks", + "TopLinkTooltip": "Kuva veebianalüütika raport lehele %s.", + "WidgetNotFound": "Moodulit ei leitud", + "WidgetPreview": "Vidina eelvaade" + } +} diff --git a/plugins/Dashboard/lang/eu.json b/plugins/Dashboard/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..5c9c9273696ecf01b867c523cf9ca9fef80dd1c9 --- /dev/null +++ b/plugins/Dashboard/lang/eu.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Gehitu widget-a panel honetara", + "AddPreviewedWidget": "Egin klik widget-a panelera gehitzeko", + "ChangeDashboardLayout": "Aldatu panelaren diseinua", + "CopyDashboardToUser": "Kopiatu panela erabiltzailera", + "CreateNewDashboard": "Sortu panel berria", + "Dashboard": "Panela", + "DashboardCategoryHelp": "Hau panelaren orri bat da. Panelak zuk zure beharrak asetzeko Matomoren widget-ak gehituz sortutako bildumak dira. Erabili nahi dituzun Matomo widget-ak %1$s*zuk*%2$s behar dituzun datuak begi kolpean ikusteko.", + "DashboardCopied": "Uneko panela behar bezala kopiatu da hautatutako erabiltzailera.", + "DashboardEmptyNotification": "Zure panelak ez dauka widget-ik. Gehitu widget batzuk edo berrezarri panelaren widget hautapen lehenetsia.", + "DashboardName": "Panelaren izena:", + "DashboardOf": "%s erabiltzailearen panela", + "DefaultDashboard": "Panel lehenetsia - Widget hautapen eta zutabe diseinu lehenetsia erabiltzen du", + "DeleteWidgetConfirm": "Ziur zaude widget hau paneletik ezabatu nahi duzula?", + "EmptyDashboard": "Panel hutsa - Aukeratu zure gogoko widget-ak", + "LoadingWidget": "Widget-a kargatzen, itxaron mesedez…", + "ManageDashboard": "Kudeatu panela", + "Maximise": "Maximizatu", + "Minimise": "Minimizatu", + "NotUndo": "Ezingo duzu eragiketa hau desegin.", + "PluginDescription": "Zure web analitika panela. Pertsonaliza ezazu zure panela widget berriak gehituz, arrastatu eta jaregin itzazu batetik bestera eta aldatu panelaren zutabe diseinua. Erabiltzaile bakoitzak bere panel pertsonalizatua kudeatu dezake.", + "RemoveDashboard": "Kendu panela", + "RemoveDashboardConfirm": "Ziur zaude \"%s\" panela kendu nahi duzula?", + "RemoveDefaultDashboardNotPossible": "Panel lehenetsia ezin da kendu", + "RenameDashboard": "Berrizendatu panela", + "ResetDashboard": "Berrezarri panela", + "ResetDashboardConfirm": "Ziur zaude panelaren diseinua Widget hautapen lehenetsira berrezarri nahi duzula?", + "SelectDashboardLayout": "Hautatu zure panel berriaren diseinua", + "SelectWidget": "Hautatu panelean gehitzeko widget-a", + "SetAsDefaultWidgets": "Ezarri widget hautapen lehenetsi bezala", + "SetAsDefaultWidgetsConfirm": "Ziur zaude uneko widget hautapena eta panelaren diseinua panel-txantiloi lehenetsi bezala ezarri nahi dituzula?", + "SetAsDefaultWidgetsConfirmHelp": "Widget hautapen eta panelaren zutabe-diseinu hau erabiliko da edozein erabiltzailek panel berri bat sortzen duenean edo \"%s\" ezaugarria erabiltzen denean.", + "TopLinkTooltip": "Ikusi %s(r)en web analitika txostenak.", + "TopMenuTitle": "Analitikak", + "WidgetNotFound": "Ez da widget-a aurkitu", + "WidgetPreview": "Widget-aren aurrebista" + } +} diff --git a/plugins/Dashboard/lang/fa.json b/plugins/Dashboard/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..30ad78c28147a7e4084039f9341316320ab52d43 --- /dev/null +++ b/plugins/Dashboard/lang/fa.json @@ -0,0 +1,37 @@ +{ + "Dashboard": { + "AddAWidget": "اضافه کردن یک ابزار", + "AddPreviewedWidget": "برای اضافه کردن یک ابزار به داشبورد اینجا کلیک کنید", + "ChangeDashboardLayout": "تغییر طرح داشبورد", + "CopyDashboardToUser": "اضافه کردن داشبورد به کاربر", + "CreateNewDashboard": "ایجاد داشبورد جدید", + "Dashboard": "داشبورد", + "DashboardCopied": "همین داشبورد با موفقیت به کاربر انتخاب شده کپی شد.", + "DashboardEmptyNotification": "صفحه شما هر ویدجت نیست. شروع با اضافه کردن برخی از ویدجت و یا تنظیم مجدد داشبورد به انتخاب پیش فرض ویجت.", + "DashboardName": "نام داشبورد:", + "DashboardOf": "داشبورد %s", + "DefaultDashboard": "داشبورد پیش فرض - با استفاده از ابزارهای پیش فرض و طرح ستونی", + "DeleteWidgetConfirm": "آیا شما مطمئن هستید که می خواهید این ابزار را از داشبورد حذف کنید؟", + "EmptyDashboard": "داشبورد خالی - ویدجت های مورد علاقه خود را انتخاب کنید", + "LoadingWidget": "در حال بارگزاری ابزار، لطفا منتظر بمانید…", + "ManageDashboard": "مدیریت داشبورد", + "Maximise": "ماکسمیاز", + "Minimise": "حداقل", + "NotUndo": "شما قادر نخواهد بود برای خنثیسازی این عملیات است.", + "PluginDescription": "داشبورد داده‌های تحلیلی سایت شما. داشبورد خود را با افزودن ابزارک‌ها جدید، برداشتن و گذاشتن در مکان‌های مختلف، و تغییر چیدمان ستون‌های داشبورد سفارشی کنید. هر کاربر می‌تواند داشبورد سفارشی خود را مدیریت کند.", + "RemoveDashboard": "حذف داشبورد", + "RemoveDashboardConfirm": "آیا شما مطمئن هستید که می خواهید این داشبورد %s را پاک کنید؟", + "RemoveDefaultDashboardNotPossible": "داشبورد پیشفرض قابل حذف نیست", + "RenameDashboard": "تغییر نام داشبورد", + "ResetDashboard": "تنظیم مجدد داشبورد", + "ResetDashboardConfirm": "آیا شما واقعا طرح داشبورد خود را به تنظیم مجدد به طور پیش فرض انتخاب ابزارک می خواهم؟", + "SelectDashboardLayout": "لطفا طرح داشبورد جدید خود را انتخاب کنید", + "SelectWidget": "یک ابزار برای اضافه کردن در داشبورد انتخاب کنید", + "SetAsDefaultWidgets": "قرار دادن به عنوان پیش فرض انتخاب ویدجت", + "SetAsDefaultWidgetsConfirm": "آیا شما مطمئن هستید که می خواهید به مجموعه فعلی انتخاب ویدجت ها و طرح داشبورد به عنوان قالب پیش فرض داشبورد؟", + "SetAsDefaultWidgetsConfirmHelp": "این ابزارک‌ها و چیدمان ستون‌های داشبورد برای هر کاربر که داشبورد جدید می‌سازد و یا وقتی ویژگی \"%s\" به کار برده می‌شود، استفاده خواهد شد.", + "TopLinkTooltip": "گزارش های داده های آماری %s را ببینید.", + "WidgetNotFound": "ابزار پیدا نشد", + "WidgetPreview": "پیش نمایش ابزار" + } +} diff --git a/plugins/Dashboard/lang/fi.json b/plugins/Dashboard/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..d842c42da2bb920a1d4fa35463424582613487b0 --- /dev/null +++ b/plugins/Dashboard/lang/fi.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Lisää vimpain", + "AddPreviewedWidget": "Napsauta lisätäksesi vimpaimen työpöydälle", + "ChangeDashboardLayout": "Vaihda työpöydän asettelua", + "CopyDashboardToUser": "Kopioi työpöytä käyttäjälle", + "CreateNewDashboard": "Luo uusi työpöytä", + "Dashboard": "Työpöytä", + "DashboardCategoryHelp": "Tämä on työpöydän sivu. Työpöydät ovat kokoelmia Matomon vimpaimia, joita lisäät itsellesi omiin tarpeisiisi. Määritä oma valikoima vimpaimia saadaksesi ne tiedot, joita %1$s*sinä*%2$s tarvitset muodostaaksesi yleiskuvan.", + "DashboardCopied": "Nykyinen työpöytä on kopioitu onnistuneesti valitulle käyttäjälle.", + "DashboardEmptyNotification": "Työpöytäsi on tyhjä. Voit aloittaa lisäämällä vimpaimia tai palauttamalla työpöydän oletusasetuksiin.", + "DashboardName": "Työpöydän nimi:", + "DashboardOf": "Työpöytä: %s", + "DefaultDashboard": "Oletustyöpöytä - käytössä on oletusvalikoima ja -asettelu", + "DeleteWidgetConfirm": "Haluatko varmasti poistaa vimpaimen työpöydältä?", + "EmptyDashboard": "Tyhjä työpöytä - valitse vimpaimia", + "LoadingWidget": "Ladataan vimpainta, odota…", + "ManageDashboard": "Muokkaa työpöytää", + "Maximise": "Suurenna", + "Minimise": "Pienennä", + "NotUndo": "Et voi perua tätä operaatiota.", + "PluginDescription": "Web-analytiikkojen työpöytä. Muokkaa työpöytääsi lisäämällä uusia vimpaimia, siirtämällä vimpaimia eri kohtiin, ja vaihtamalla työpöydän sisältöä. Jokainen käyttäjä voi hallita omaa työpöytäänsä.", + "RemoveDashboard": "Poista työpöytä", + "RemoveDashboardConfirm": "Haluatko varmasti poistaa työpöydän \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Oletustyöpöytää ei voi poistaa", + "RenameDashboard": "Nimeä työpöytä uudelleen", + "ResetDashboard": "Palauta työpöytä oletusasetuksiin", + "ResetDashboardConfirm": "Haluatko varmasti palauttaa työpöydän asettelun takaisin oletusasetuksiin?", + "SelectDashboardLayout": "Valitse uusi työpöydän asettelu", + "SelectWidget": "Valitse työpöydälle lisättävä vimpain", + "SetAsDefaultWidgets": "Aseta oletusvalikoimaksi", + "SetAsDefaultWidgetsConfirm": "Haluatko varmasti tallentaa nykyisen työpöydän oletusasetukseksi?", + "SetAsDefaultWidgetsConfirmHelp": "Tätä työpöytää käytetään kun kuka tahansa käyttäjistä luo uuden työpöydän tai kun ominaisuutta \"%s\" käytetään.", + "TopLinkTooltip": "Näytä verkkoanalyysiraportit %s:lle.", + "TopMenuTitle": "Analytiikka", + "WidgetNotFound": "Vimpainta ei löydy", + "WidgetPreview": "Vimpaimen esikatselu" + } +} diff --git a/plugins/Dashboard/lang/fr.json b/plugins/Dashboard/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..0ceefa23f8a1fa3d44a7aa60d777d73f666c5630 --- /dev/null +++ b/plugins/Dashboard/lang/fr.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Ajouter un gadget", + "AddPreviewedWidget": "Ajouter le widget visualisé au tableau de bord", + "ChangeDashboardLayout": "Modifier la mise en page du tableau de bord", + "CopyDashboardToUser": "Copier le tableau de bord vers l'utilisateur", + "CreateNewDashboard": "Créer un nouveau tableau de bord", + "Dashboard": "Tableau de bord", + "DashboardCategoryHelp": "Ceci est une page de tableau de bord. Les tableaux de bord sont une collection de gadgets Matomo que vous agencez vous-même en fonction de vos besoins. Combinez et disposez les widgets de Matomo pour obtenir les données dont %1$svous%2$s avez besoin en un coup d'œil.", + "DashboardCopied": "Tableau de bord actuel copié avec succès vers l'utilisateur sélectionné.", + "DashboardEmptyNotification": "Votre tableau de bord ne contient aucun gadget. Commencez par ajouter quelques gadgets ou réinitialisez le tableau de bord à la sélection de gadgets par défaut.", + "DashboardName": "Nom du tableau de bord :", + "DashboardOf": "Tableau de bord de %s", + "DefaultDashboard": "Tableau de bord par défaut - Utilisant la sélection de gadgets par défaut et la disposition des colonnes", + "DeleteWidgetConfirm": "Êtes-vous sûr(e) de vouloir supprimer ce widget du tableau de bord ?", + "EmptyDashboard": "Tableau de bord vide - Sélectionnez vos gadgets favoris", + "ExportThisDashboard": "Exporter le tableau de bord", + "LoadingWidget": "Chargement du widget, patientez svp…", + "ManageDashboard": "Gérer le tableau de bord", + "Maximise": "Maximiser", + "Minimise": "Minimiser", + "NotUndo": "Vous ne pourrez pas annuler cette action.", + "PluginDescription": "Votre tableau de bord, personnalisez le en ajoutant des widgets, en les déplaçant et en modifiant la disposition des colonnes. Chaque utilisateur possède son propre tableau de bord.", + "RemoveDashboard": "Supprimer le tableau de bord", + "RemoveDashboardConfirm": "Êtes-vous sûr(e) de vouloir supprimer le tableau de bord \"%s\" ?", + "RemoveDefaultDashboardNotPossible": "Le tableau de bord par défaut ne peut pas être supprimé", + "RenameDashboard": "Renommer le tableau de bord", + "ResetDashboard": "Restaurer le tableau de bord", + "ResetDashboardConfirm": "Voulez-vous vraiment restaurer votre tableau de bord avec les widgets par défaut ?", + "SelectDashboardLayout": "Veuillez sélectionner votre nouvelle mise en page pour le tableau de bord", + "SelectWidget": "Sélectionnez le widget à ajouter au tableau de bord", + "SetAsDefaultWidgets": "Définir comme sélection de gadgets par défaut", + "SetAsDefaultWidgetsConfirm": "Êtes-vous sûr(e) de vouloir définir la sélection de gadgets actuelle et la disposition actuelle du tableau de bord comme modèle par défaut ?", + "SetAsDefaultWidgetsConfirmHelp": "Cette sélection de gadgets et cette disposition du tableau de bord seront utilisées à chaque création de tableau de bord par n'importe quel utilisateur ou quand la fonctionnalité \"%s\" est utilisée.", + "TopLinkTooltip": "Afficher les rapport de statistiques web pour %s.", + "TopMenuTitle": "Statistiques", + "WidgetNotFound": "Widget introuvable", + "WidgetPreview": "Prévisualisation du Widget" + } +} diff --git a/plugins/Dashboard/lang/ga.json b/plugins/Dashboard/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..6543db1eb4ba9823531a9be637aa48575a20c9a6 --- /dev/null +++ b/plugins/Dashboard/lang/ga.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Cuir giuirléid leis an deais seo", + "AddPreviewedWidget": "Cliceáil chun giuirléid a chur leis an deais", + "ChangeDashboardLayout": "Athraigh leagan amach an deais", + "CopyDashboardToUser": "Cóipeáil an painéal chuig an úsáideoir", + "CreateNewDashboard": "Cruthaigh painéal nua", + "Dashboard": "Deais", + "DashboardCategoryHelp": "Is leathanach deais é seo. Is éard atá i ndeischláir ná bailiúchán de ghiuirléidí Matomo a chuireann tú féin leis chun freastal ar do shainriachtanais. Measc agus meaitseáil aon cheann de ghiuirléidí Matomo chun na sonraí %1$s* a theastaíonn uait*%2$s a fháil go sracfhéachaint.", + "DashboardCopied": "D'éirigh le cóipeáil an deais reatha chuig an úsáideoir roghnaithe.", + "DashboardEmptyNotification": "Níl aon ghiuirléid ar do Phainéal. Tosaigh trí roinnt giuirléidí a chur leis nó díreach an painéal a athshocrú go dtí an rogha réamhshocraithe giuirléidí.", + "DashboardName": "Ainm an deais:", + "DashboardOf": "Painéal %s", + "DefaultDashboard": "Deais réamhshocraithe - Ag baint úsáide as roghnú giuirléidí réamhshocraithe agus leagan amach na gcolún", + "DeleteWidgetConfirm": "An bhfuil tú cinnte gur mhaith leat an ghiuirléid seo a scriosadh ón deais?", + "EmptyDashboard": "Deais folamh - Roghnaigh na giuirléidí is fearr leat", + "ExportThisDashboard": "Easpórtáil Painéal Rialaithe", + "LoadingWidget": "Giuirléid á lódáil, fan go fóill…", + "ManageDashboard": "Bainistigh Painéal Rialaithe", + "Maximise": "Uasmhéadaigh", + "Minimise": "Íoslaghdaigh", + "NotUndo": "Ní bheidh tú in ann an oibríocht seo a chealú.", + "PluginDescription": "Do Phainéal Analytics Gréasáin. Saincheap do dheais trí ghiuirléidí nua a chur leis, tarraing agus scaoil thart iad, agus athraigh leagan amach colúin an deais. Is féidir le gach úsáideoir a dheais saincheaptha féin a bhainistiú.", + "RemoveDashboard": "Bain deais", + "RemoveDashboardConfirm": "An bhfuil tú cinnte gur mhaith leat an painéal \"%s\" a bhaint?", + "RemoveDefaultDashboardNotPossible": "Ní féidir an painéal réamhshocraithe a bhaint", + "RenameDashboard": "Athainmnigh an painéal", + "ResetDashboard": "Athshocraigh an painéal", + "ResetDashboardConfirm": "An bhfuil tú cinnte gur mian leat leagan amach do dheais a athshocrú go dtí an rogha réamhshocraithe Giuirléidí?", + "SelectDashboardLayout": "Roghnaigh do leagan amach nua deais", + "SelectWidget": "Roghnaigh an ghiuirléid le cur leis an deais", + "SetAsDefaultWidgets": "Socraigh mar rogha réamhshocraithe giuirléidí", + "SetAsDefaultWidgetsConfirm": "An bhfuil tú cinnte gur mian leat rogha na ngiuirléidí reatha agus leagan amach an deais a shocrú mar an teimpléad deais réamhshocraithe?", + "SetAsDefaultWidgetsConfirmHelp": "Úsáidfear an rogha giuirléidí seo agus leagan amach na gcolún ar an deais nuair a chruthaíonn aon úsáideoir deais nua, nó nuair a úsáidtear an ghné \"%s\".", + "TopLinkTooltip": "Féach ar thuairiscí Web Analytics le haghaidh %s.", + "TopMenuTitle": "Anailísíocht", + "WidgetNotFound": "Giuirléid gan aimsiú", + "WidgetPreview": "Réamhamharc giuirléid" + } +} diff --git a/plugins/Dashboard/lang/gl.json b/plugins/Dashboard/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..86a94e2ff6ddf97bf817decd08133ece85b93f82 --- /dev/null +++ b/plugins/Dashboard/lang/gl.json @@ -0,0 +1,13 @@ +{ + "Dashboard": { + "AddAWidget": "Engadir un trebello", + "Dashboard": "Tableiro", + "DeleteWidgetConfirm": "¿Está seguro de que desexa borrar este widget desde el tablón?", + "LoadingWidget": "Cargando widget, agarde por favor…", + "Maximise": "Maximizar", + "Minimise": "Minimizar", + "NotUndo": "Non lle será posíbel desfacer esta operación.", + "SelectWidget": "Seleccione un widget para engadilo no tablón", + "WidgetNotFound": "Trebello non atopado" + } +} diff --git a/plugins/Dashboard/lang/gu.json b/plugins/Dashboard/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..f36026d6b67f578680636a3f5f381a4e56a40bbd --- /dev/null +++ b/plugins/Dashboard/lang/gu.json @@ -0,0 +1,38 @@ +{ + "Dashboard": { + "AddAWidget": "વિજેટ ઉમેરો", + "AddPreviewedWidget": "ડેશબોર્ડમાં વિજેટ ઉમેરવા માટે ક્લિક કરો", + "ChangeDashboardLayout": "ડેશબોર્ડ લેઆઉટ બદલો", + "CopyDashboardToUser": "ડૅશબોર્ડને વપરાશકર્તા પર કૉપિ કરો", + "CreateNewDashboard": "નવું ડેશબોર્ડ બનાવો", + "Dashboard": "ડેશબોર્ડ", + "DashboardCategoryHelp": "આ ડેશબોર્ડ પેજ છે. ડેશબોર્ડ એ Matomo ના વિજેટ્સનો સંગ્રહ છે જે તમે તમારી ચોક્કસ જરૂરિયાતોને અનુરૂપ તમારી જાતને ઉમેરો છો. %1$s*તમને*%2$s ને એક નજરમાં જોઈતો ડેટા મેળવવા માટે Matomo ના કોઈપણ વિજેટ્સને મિક્સ અને મેચ કરો.", + "DashboardCopied": "વર્તમાન ડેશબોર્ડ પસંદ કરેલ વપરાશકર્તા પર સફળતાપૂર્વક કૉપિ કર્યું.", + "DashboardEmptyNotification": "તમારા ડેશબોર્ડમાં કોઈપણ વિજેટ્સ નથી. કેટલાક વિજેટ્સ ઉમેરીને પ્રારંભ કરો અથવા ફક્ત ડેશબોર્ડને ડિફોલ્ટ વિજેટ પસંદગી પર ફરીથી સેટ કરો.", + "DashboardName": "ડેશબોર્ડ નામ:", + "DashboardOf": "%s નું ડેશબોર્ડ", + "DefaultDashboard": "ડિફૉલ્ટ ડેશબોર્ડ - ડિફૉલ્ટ વિજેટ્સ પસંદગી અને કૉલમ લેઆઉટનો ઉપયોગ કરીને", + "DeleteWidgetConfirm": "શું તમે ખરેખર આ વિજેટને ડેશબોર્ડમાંથી કાઢી નાખવા માંગો છો?", + "EmptyDashboard": "ખાલી ડેશબોર્ડ - તમારા મનપસંદ વિજેટ્સ પસંદ કરો", + "LoadingWidget": "વિજેટ લોડ કરી રહ્યા છે, કૃપા કરીને રાહ જુઓ…", + "ManageDashboard": "ડેશબોર્ડ મેનેજ કરો", + "Maximise": "મહત્તમ કરો", + "Minimise": "નાનું કરો", + "NotUndo": "તમે આ ઓપરેશનને પૂર્વવત્ કરી શકશો નહીં.", + "PluginDescription": "તમારું વેબ એનાલિટિક્સ ડેશબોર્ડ. નવા વિજેટ્સ ઉમેરીને તમારા ડેશબોર્ડને કસ્ટમાઇઝ કરો, તેમને આસપાસ ખેંચો અને છોડો અને ડેશબોર્ડ કૉલમ લેઆઉટ બદલો. દરેક વપરાશકર્તા તેમના પોતાના કસ્ટમ ડેશબોર્ડનું સંચાલન કરી શકે છે.", + "RemoveDashboard": "ડેશબોર્ડ દૂર કરો", + "RemoveDashboardConfirm": "શું તમે ખરેખર ડેશબોર્ડ \"%s\" દૂર કરવા માંગો છો?", + "RemoveDefaultDashboardNotPossible": "ડિફૉલ્ટ ડેશબોર્ડ દૂર કરી શકાતું નથી", + "RenameDashboard": "ડેશબોર્ડનું નામ બદલો", + "ResetDashboard": "ડેશબોર્ડ રીસેટ કરો", + "ResetDashboardConfirm": "શું તમે ખરેખર તમારા ડેશબોર્ડ લેઆઉટને ડિફોલ્ટ વિજેટ્સ પસંદગી પર રીસેટ કરવા માંગો છો?", + "SelectDashboardLayout": "કૃપા કરીને તમારું નવું ડેશબોર્ડ લેઆઉટ પસંદ કરો", + "SelectWidget": "ડેશબોર્ડમાં ઉમેરવા માટે વિજેટ પસંદ કરો", + "SetAsDefaultWidgets": "ડિફૉલ્ટ વિજેટ્સ પસંદગી તરીકે સેટ કરો", + "SetAsDefaultWidgetsConfirm": "શું તમે ખરેખર વર્તમાન વિજેટ્સ પસંદગી અને ડેશબોર્ડ લેઆઉટને ડિફોલ્ટ ડેશબોર્ડ નમૂના તરીકે સેટ કરવા માંગો છો?", + "SetAsDefaultWidgetsConfirmHelp": "આ વિજેટ્સ પસંદગી અને ડેશબોર્ડ કૉલમ લેઆઉટનો ઉપયોગ જ્યારે કોઈપણ વપરાશકર્તા નવું ડેશબોર્ડ બનાવે છે, અથવા જ્યારે \"%s\" સુવિધાનો ઉપયોગ કરવામાં આવે ત્યારે કરવામાં આવશે.", + "TopLinkTooltip": "%s માટે વેબ ઍનલિટિક્સ રિપોર્ટ્સ જુઓ.", + "WidgetNotFound": "વિજેટ મળ્યું નથી", + "WidgetPreview": "વિજેટ પ્રીવ્યૂ" + } +} diff --git a/plugins/Dashboard/lang/he.json b/plugins/Dashboard/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..5cb7bdbcb5cd92667d128a3e6d1195c7e47182cf --- /dev/null +++ b/plugins/Dashboard/lang/he.json @@ -0,0 +1,17 @@ +{ + "Dashboard": { + "AddAWidget": "הוספת וידג'ט", + "AddPreviewedWidget": "הוספת היישומון המוצג לפאנל הסקירה", + "CreateNewDashboard": "יצירת לוח בקרה חדש", + "Dashboard": "פאנל סקירה", + "DashboardName": "שם לוח בקרה:", + "DeleteWidgetConfirm": "למחוק את היישומון מפאנל הסקירה? 100%%?", + "LoadingWidget": "טוען יישומון, נא להמתין…", + "ManageDashboard": "ניהול לוח בקרה", + "Maximise": "הגדלה", + "RenameDashboard": "שינוי שם לוח בקרה", + "SelectWidget": "בחירת היישומון להוספה לפאנל הסקירה", + "WidgetNotFound": "יישומון לא נמצא", + "WidgetPreview": "הצגת יישומון" + } +} diff --git a/plugins/Dashboard/lang/hi.json b/plugins/Dashboard/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..d564d2dd85938a0890372f7622c88f195ba70b22 --- /dev/null +++ b/plugins/Dashboard/lang/hi.json @@ -0,0 +1,35 @@ +{ + "Dashboard": { + "AddAWidget": "एक विजेट जोड़ें", + "AddPreviewedWidget": "डैशबोर्ड विजेट जोड़ने के लिए क्लिक करें", + "ChangeDashboardLayout": "डैशबोर्ड अभिन्यास बदलें", + "CopyDashboardToUser": "उपयोगकर्ता के लिए डैशबोर्ड कॉपी करें", + "CreateNewDashboard": "नया डैशबोर्ड बनाएँ", + "Dashboard": "डैशबोर्ड", + "DashboardCopied": "वर्तमान डैशबोर्ड सफलतापूर्वक चयनित उपयोगकर्ता के लिए नकल की.", + "DashboardEmptyNotification": "अपने डैशबोर्ड पर कोई भी विगेट्स शामिल नहीं है. कुछ विगेट्स जोड़ने से शुरू करो या बस डिफ़ॉल्ट विजेट चयन करने के लिए डैशबोर्ड रीसेट करें.", + "DashboardName": "डैशबोर्ड नाम:", + "DashboardOf": "%s से डैशबोर्ड", + "DefaultDashboard": "डिफ़ॉल्ट डैशबोर्ड - डिफ़ॉल्ट विगेट्स चयन और कॉलम लेआउट का उपयोग करना", + "DeleteWidgetConfirm": "आप सुनिश्चित कर रहे हैं आप डैशबोर्ड से इस विजेट को हटाना चाहते हैं?", + "EmptyDashboard": "खाली डैशबोर्ड - अपने पसंदीदा विगेट्स उठाओ", + "LoadingWidget": "विजेट लोड हो रहा है, कृपया प्रतीक्षा करें …", + "ManageDashboard": "डैशबोर्ड प्रबंधन", + "Maximise": "अधिकतम", + "Minimise": "कम से कम", + "NotUndo": "आप इस कार्रवाई को पूर्ववत् करने सक्षम नहीं होंगे.", + "RemoveDashboard": "डैशबोर्ड को हटाएँ", + "RemoveDashboardConfirm": "आप सुनिश्चित कर रहे हैं, आप \"%s\" डैशबोर्ड को हटाना चाहते हैं?", + "RenameDashboard": "डैशबोर्ड का नाम बदलें", + "ResetDashboard": "डैशबोर्ड पुनःआरंभ करें", + "ResetDashboardConfirm": "आप सुनिश्चित कर रहे हैं आप डिफ़ॉल्ट विजेट चयन करने के लिए अपने डैशबोर्ड अभिन्यास पुनःआरंभ करना चाहते हैं?", + "SelectDashboardLayout": "अपना नया डैशबोर्ड अभिन्यास का चयन करें", + "SelectWidget": "डैशबोर्ड में जोड़ने के लिए विजेट का चयन करें", + "SetAsDefaultWidgets": "डिफ़ॉल्ट विगेट्स चयन के रूप में सेट करें", + "SetAsDefaultWidgetsConfirm": "आप सुनिश्चित कर रहे हैं, आप डिफ़ॉल्ट डैशबोर्ड टेम्पलेट के रूप में मौजूदा विगेट्स चयन और डैशबोर्ड अभिन्यास सेट करना चाहते हैं?", + "SetAsDefaultWidgetsConfirmHelp": "इस विगेट्स चयन और डैशबोर्ड कॉलम अभिन्यास का उपयोग किया जाएगा जब कोई उपयोगकर्ता एक नया डैशबोर्ड बनाता है, या जब \"%s\" सुविधा का उपयोग करता है।", + "TopLinkTooltip": "%s के लिए वेब विश्लेषिकी रिपोर्ट देखें", + "WidgetNotFound": "विजेट नहीं मिला", + "WidgetPreview": "विजेट पूर्वावलोकन" + } +} diff --git a/plugins/Dashboard/lang/hr.json b/plugins/Dashboard/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..615eb1a7eacd7543f8b87a78135e5d4343a28087 --- /dev/null +++ b/plugins/Dashboard/lang/hr.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Dodaj widget u nadzornu ploču", + "AddPreviewedWidget": "Klikni za dodavanje widgeta u nadzornu ploču", + "ChangeDashboardLayout": "Promijeni raspored nadzorne ploče", + "CopyDashboardToUser": "Kopiraj nadzornu ploču korisniku", + "CreateNewDashboard": "Stvori novu nadzornu ploču", + "Dashboard": "Nadzorna ploča", + "DashboardCategoryHelp": "Ovo je stranica nadzorne ploče. Nadzorne ploče su zbirka Matomovih widgeta koje ti dodaješ kako bi odgovarali tvojim specifičnim potrebama. Kombiniraj bilo koje od Matomovih widgeta kako bi se na prvi pogled prikazali podaci koje %1$s*ti*%2$s trebaš.", + "DashboardCopied": "Trenutačna nadzorna ploča je uspješno kopirana odabranom korisniku.", + "DashboardEmptyNotification": "Tvoja nadzorna ploča ne sadrži widgete. Započni dodavanjem nekoliko widgeta ili jednostavno resetiraj nadzornu ploču na zadani odabir widgeta.", + "DashboardName": "Ime nadzorne ploče:", + "DashboardOf": "Nadzorna ploča od %s", + "DefaultDashboard": "Zadana nadzorna ploča – Korištenje zadanog odabira widgeta i rasporeda stupaca", + "DeleteWidgetConfirm": "Stvarno želiš izbrisati ovaj widget iz nadzorne ploče?", + "EmptyDashboard": "Prazna nadzorna ploča – Odaberi svoje omiljene widgete", + "ExportThisDashboard": "Izvezi ovu nadzornu ploču", + "LoadingWidget": "Učitavanje widgeta, molimo pričekajte…", + "ManageDashboard": "Upravljaj ovom nadzornom pločom", + "Maximise": "Uvečaj", + "Minimise": "Smanji", + "NotUndo": "Ovu radnju nećeš moći poništiti.", + "PluginDescription": "Tvoja nadzorna ploča za web analitiku. Prilagodi svoju nadzornu ploču dodavanjem novih widgeta, povlačenjem i ispuštanjem i mijenjenjem rasporeda stupaca nadzorne ploče. Svaki korisnik može upravljati vlastitom prilagođenom nadzornom pločom.", + "RemoveDashboard": "Ukloni nadzornu ploču", + "RemoveDashboardConfirm": "Stvarno želiš ukloniti nadzornu ploču „%s”?", + "RemoveDefaultDashboardNotPossible": "Zadana nadzorna ploča se ne može ukloniti", + "RenameDashboard": "Preimenuj nadzornu ploču", + "ResetDashboard": "Resetiraj nadzornu ploču", + "ResetDashboardConfirm": "Stvarno želiš resetirati raspored nadzorne ploče na zadani odabir widgeta?", + "SelectDashboardLayout": "Odaberi novi raspored nadzorne ploče", + "SelectWidget": "Odaberi widget za dodavanje u nadzornu ploču", + "SetAsDefaultWidgets": "Postavi kao zadani odabir widgeta", + "SetAsDefaultWidgetsConfirm": "Stvarno želiš postaviti trenutačni odabir widgeta i raspored nadzorne ploče kao zadani predložak nadzorne ploče?", + "SetAsDefaultWidgetsConfirmHelp": "Ovaj odabir widgeta i raspored stupaca nadzorne ploče koristit će se kada bilo koji korisnik stvori novu nadzornu ploču ili kada se koristi funkcija „%s”.", + "TopLinkTooltip": "Prikaži izvještaje web analitike za %s.", + "WidgetNotFound": "Widget nije pronađen", + "WidgetPreview": "Predpregled widgeta" + } +} diff --git a/plugins/Dashboard/lang/hu.json b/plugins/Dashboard/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..9442c51a9a3a1496ea2e3e1b326d5b8fdee0d99d --- /dev/null +++ b/plugins/Dashboard/lang/hu.json @@ -0,0 +1,13 @@ +{ + "Dashboard": { + "AddPreviewedWidget": "Add hozzá a megtekintett modult a vezérlőpultodhoz", + "Dashboard": "Vezérlőpult", + "DeleteWidgetConfirm": "Biztosan törölni akarod ezt a modult a vezérlőpultról?", + "LoadingWidget": "Modul betöltése folyamatban. Kérjük, várj…", + "Maximise": "Maximalizál", + "Minimise": "Minimalizál", + "SelectWidget": "Válaszd ki melyik modult szeretnéd hozzáadni a vezérlőpulthoz!", + "WidgetNotFound": "A modul nem található", + "WidgetPreview": "Modul előnézete" + } +} diff --git a/plugins/Dashboard/lang/hy.json b/plugins/Dashboard/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Dashboard/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Dashboard/lang/id.json b/plugins/Dashboard/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..a5e3f5e49e01e0b817656a8886f69edea366c1c9 --- /dev/null +++ b/plugins/Dashboard/lang/id.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Tambahkan widget", + "AddPreviewedWidget": "Klik untuk menambahkan widget ke dasbor", + "ChangeDashboardLayout": "Ubah tata letak dasbor", + "CopyDashboardToUser": "Salin dasbor ke pengguna", + "CreateNewDashboard": "Buat dasbor baru", + "Dashboard": "Dasbor", + "DashboardCategoryHelp": "Ini adalah halaman dasbor. Dasbor adalah kumpulan widget Matomo yang Anda tambahkan sendiri sesuai dengan kebutuhan spesifik Anda. Campur dan cocokkan widget Matomo untuk mendapatkan data %1$s*Anda*%2$s yang diperlukan dengan sekilas.", + "DashboardCopied": "Dasbor berikut ini berhasil disalin ke pengguna terpilih.", + "DashboardEmptyNotification": "Dasbor Anda tidak berisi widget apa pun. Mulailah dengan menambahkan beberapa widget atau cukup setel ulang dasbor ke pilihan widget bawaan.", + "DashboardName": "Nama dasbor:", + "DashboardOf": "Dasbor %s", + "DefaultDashboard": "Dasbor bawaan. Menggunakan pemilihan widget dan tata letak kolom bawaan", + "DeleteWidgetConfirm": "Anda yakin ingin menghapus widget ini dari dasbor?", + "EmptyDashboard": "Dasbor kosong. Pilih widget favorit Anda", + "LoadingWidget": "Memuat widget, harap tunggu…", + "ManageDashboard": "Kelola dasbor", + "Maximise": "Maksimalkan", + "Minimise": "Minimalkan", + "NotUndo": "Anda tak dapat mengembalikan tidakan ini.", + "PluginDescription": "Dasbor Analisis Web Anda. Sesuaikan dasbor Anda dengan menambahkan widget baru, seret dan lepas, dan ubah tata letak kolom dasbor. Setiap pengguna dapat mengelola dasbor mereka sendiri.", + "RemoveDashboard": "Hapus dasbor", + "RemoveDashboardConfirm": "Yakin ingin menghapus dasbor \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Dasbor bawaan tidak dapat dihapus", + "RenameDashboard": "Ganti nama dasbor", + "ResetDashboard": "Setel ulang dasbor", + "ResetDashboardConfirm": "Yakin ingin mengatur ulang tata letak dasbor ke pemilihan Widget bawaan?", + "SelectDashboardLayout": "Silakan pilih tata letak dasbor baru Anda", + "SelectWidget": "Pilih widget untuk ditambahkan di dasbor", + "SetAsDefaultWidgets": "Tetapkan sebagai pilihan widget asali", + "SetAsDefaultWidgetsConfirm": "Apakah Anda yakin ingin menyetel pemilihan widget dan tata letak dasbor saat ini sebagai templat dasbor bawaan?", + "SetAsDefaultWidgetsConfirmHelp": "Pemilihan widget dan tata letak kolom dasbor ini akan digunakan saat pengguna membuat dasbor baru atau saat fitur \"%s\" digunakan.", + "TopLinkTooltip": "Lihat laporan Analisis Web untuk %s.", + "TopMenuTitle": "Analitik", + "WidgetNotFound": "Widget tidak ditemukan", + "WidgetPreview": "Pratinjau widget" + } +} diff --git a/plugins/Dashboard/lang/is.json b/plugins/Dashboard/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..229822abfe4e79542d33bb5b1bc5765d75f5b5bc --- /dev/null +++ b/plugins/Dashboard/lang/is.json @@ -0,0 +1,12 @@ +{ + "Dashboard": { + "AddPreviewedWidget": "Bæta forskoðuðum aukahlut á skjáborðið", + "Dashboard": "Skjáborð", + "DeleteWidgetConfirm": "Ertu viss um að þú viljir eyða þessum aukahlut af skjáborðinu?", + "LoadingWidget": "Hleð inn aukahlut, vinsamlegast bíðið…", + "Maximise": "Hámarka", + "SelectWidget": "Veldu aukahlutinn til að bæta við skjáborðið", + "WidgetNotFound": "Aukahlutur fannst ekki", + "WidgetPreview": "Forskoðun á aukahlut" + } +} diff --git a/plugins/Dashboard/lang/it.json b/plugins/Dashboard/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..3bb00de27cccd3693b9d76aaf81daec694d474e1 --- /dev/null +++ b/plugins/Dashboard/lang/it.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Aggiungi widget a questa dashboard", + "AddPreviewedWidget": "Clicca per aggiungere un widget alla dashboard", + "ChangeDashboardLayout": "Cambia il layout della dashboard", + "CopyDashboardToUser": "Copia la dashboard a un utente", + "CreateNewDashboard": "Crea una nuova dasboard", + "Dashboard": "Dashboard", + "DashboardCategoryHelp": "Questa è una pagina della dashboard. Le dashboard sono una raccolta di widget di Matomo che aggiungi tu stesso per soddisfare le tue esigenze specifiche. Mescola e abbina qualsiasi widget di Matomo per ottenere i dati di cui %1$s\"tu\"%2$s hai bisogno a colpo d'occhio.", + "DashboardCopied": "La dashboard corrente è stata copiata con successo all'utente selezionato.", + "DashboardEmptyNotification": "La tua dashboard non contiene widget. Inizia con l'aggiunta di alcuni widget o semplicemente resetta il pannello alla selezione widget predefinita.", + "DashboardName": "Nome della dashboard:", + "DashboardOf": "Dashboard di %s", + "DefaultDashboard": "Dashboard predefinita - Utilizza la selezione di widget e le colonne di layout predefinite", + "DeleteWidgetConfirm": "Sei sicuro di voler eliminare il widget dalla dashboard?", + "EmptyDashboard": "Dashboard vuota - Scegli i tuoi widget preferiti", + "LoadingWidget": "Caricamento widget, attendere…", + "ManageDashboard": "Gestisci questa dashboard", + "Maximise": "Espandi", + "Minimise": "Minimizza", + "NotUndo": "Non sarà possibile annullare questa operazione.", + "PluginDescription": "La tua Dashboard delle Statistiche Web. Personalizzala aggiungendo nuovi widgets, trascinandoli e spostandoli e cambiando l'aspetto delle colonne. Ciascun utente può gestire una propria dashboard.", + "RemoveDashboard": "Rimuovi la dashboard", + "RemoveDashboardConfirm": "Sei sicuro di voler rimuovere la dashboard \"%s\"?", + "RemoveDefaultDashboardNotPossible": "La dashboard predefinita non può essere eliminata", + "RenameDashboard": "Rinomina dashboard", + "ResetDashboard": "Reimposta la dashboard", + "ResetDashboardConfirm": "Sei sicuro di voler ripristinare il layout della dashboard alla selezione di widget predefinita?", + "SelectDashboardLayout": "Si prega di selezionare il layout della nuova dashboard", + "SelectWidget": "Seleziona il widget da aggiungere nella dashboard", + "SetAsDefaultWidgets": "Imposta come selezione predefinita di widget", + "SetAsDefaultWidgetsConfirm": "Sei sicuro di voler impostare la selezione corrente di widget e il layout del pannello come modello predefinito?", + "SetAsDefaultWidgetsConfirmHelp": "Questa selezione di widget e il layout colonne saranno utilizzate quando un utente creerà una nuova dashboard o quando verrà utilizzata la funzione \"%s\".", + "TopLinkTooltip": "Visualizza report Web Analytics per %s.", + "TopMenuTitle": "Statistiche", + "WidgetNotFound": "Widget non trovato", + "WidgetPreview": "Anteprima del widget" + } +} diff --git a/plugins/Dashboard/lang/ja.json b/plugins/Dashboard/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..e46424891c7e3f39280c69ed30b9e8f6d0c8b228 --- /dev/null +++ b/plugins/Dashboard/lang/ja.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "ウィジェットを追加", + "AddPreviewedWidget": "クリックしてウィジェットをダッシュボードに追加します", + "ChangeDashboardLayout": "ダッシュボードのレイアウトを変更", + "CopyDashboardToUser": "ダッシュボードをユーザーへコピーする", + "CreateNewDashboard": "新しいダッシュボードを作成する", + "Dashboard": "ダッシュボード", + "DashboardCategoryHelp": "これはダッシュボードページです。 ダッシュボードは、特定のニーズに合わせて自分で追加するMatomoのウィジェットのコレクションです。 Matomoのウィジェットを組み合わせて、必要なデータを%1$s一目で%2$s確認できます。", + "DashboardCopied": "現在のダッシュボードは選択したユーザーへコピーされました。", + "DashboardEmptyNotification": "ダッシュボードにウィジェットがありません。ダッシュボードにウィジェットを追加するか、ダッシュボードをデフォルトのウィジェットにリセットしてから始めてください。", + "DashboardName": "ダッシュボード名:", + "DashboardOf": "%s のダッシュボード", + "DefaultDashboard": "デフォルトのダッシュボード - デフォルトのウィジェットとカラムレイアウトを使用します", + "DeleteWidgetConfirm": "本当にこのウィジェットをダッシュボードから削除しますか?", + "EmptyDashboard": "空のダッシュボード - お好みのウィジェットを選択してください", + "LoadingWidget": "ウィジェットを読み込んでいます、お待ちください…", + "ManageDashboard": "ダッシュボードの管理", + "Maximise": "最大化", + "Minimise": "最小化", + "NotUndo": "この操作は取り消せません。", + "PluginDescription": "ユーザーの Web Analytics のダッシュ ボード。新しいウィジェットを追加することによって、ダッシュ ボードをカスタマイズ、ドラッグ アンドドロップしでダッシュ ボードのカラムのレイアウトを変更します。各ユーザーは、独自のカスタム ダッシュ ボードを管理できます。", + "RemoveDashboard": "ダッシュボードの削除", + "RemoveDashboardConfirm": "本当にダッシュボード \"%s\" を削除しますか?", + "RemoveDefaultDashboardNotPossible": "既定のダッシュ ボードを削除できません", + "RenameDashboard": "ダッシュボードの名前を変更する", + "ResetDashboard": "ダッシュボードのリセット", + "ResetDashboardConfirm": "ダッシュボードのレイアウトをリセットしてデフォルトのウィジェットに戻しますか?", + "SelectDashboardLayout": "新しいダッシュボードのレイアウトを選択してください", + "SelectWidget": "ダッシュボードに追加するウィジェットを選択してください", + "SetAsDefaultWidgets": "デフォルトのウィジェット選択として設定", + "SetAsDefaultWidgetsConfirm": "現在のウィジェットとダッシュボードのレイアウトをデフォルトのテンプレートとして設定しますか?", + "SetAsDefaultWidgetsConfirmHelp": "これらのウィジェットとダッシュボードのレイアウトは、新規ダッシュボードの作成時か、あるいは %s 機能の使用時に使用されます。", + "TopLinkTooltip": "%s のウェブ解析レポートを表示します。", + "TopMenuTitle": "アナリティクス", + "WidgetNotFound": "ウィジェットが見つかりませんでした", + "WidgetPreview": "ウィジェットのプレビュー" + } +} diff --git a/plugins/Dashboard/lang/ka.json b/plugins/Dashboard/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..b62c71d9fe48bc5e39ff21db60d087e30562c235 --- /dev/null +++ b/plugins/Dashboard/lang/ka.json @@ -0,0 +1,12 @@ +{ + "Dashboard": { + "AddPreviewedWidget": "დაამატეთ ნანახი ვიჯეტი საინფორმაციო დაფას", + "Dashboard": "საინფორმაციო დაფა", + "DeleteWidgetConfirm": "დარწმუნებული ხართ, რომ გსურთ ვიჯეტის წაშლა ინსრუმენტთა პანელიდან?", + "LoadingWidget": "ვიჯეტი იტვირთება, გთხოვთ დაიცადოთ…", + "Maximise": "მაქსიმალურად გადიდება", + "SelectWidget": "შეარჩიეთ საინფორმაციო დაფაზე დასამატებელი ვიჯეტი", + "WidgetNotFound": "ვიჯეტი არ მოიძებნა", + "WidgetPreview": "ვიჯეტის ნახვა" + } +} diff --git a/plugins/Dashboard/lang/ko.json b/plugins/Dashboard/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..92de455645398c8906b7a1ce47c01703b9e0631a --- /dev/null +++ b/plugins/Dashboard/lang/ko.json @@ -0,0 +1,38 @@ +{ + "Dashboard": { + "AddAWidget": "위젯 추가", + "AddPreviewedWidget": "대시보드에 미리보기된 위젯 추가", + "ChangeDashboardLayout": "대시보드 레이아웃 변경", + "CopyDashboardToUser": "대시보드를 사용자에게 복사", + "CreateNewDashboard": "새로운 대시보드 만들기", + "Dashboard": "대시보드", + "DashboardCategoryHelp": "이것은 대시보드 페이지입니다. 대시보드는 특정 요구 사항에 맞게 추가하는 Matomo의 위젯 모음입니다. Matomo의 위젯을 조합하여 %1$s*사용자*%2$s에게 필요한 데이터를 한 눈에 볼 수 있습니다.", + "DashboardCopied": "대시보드가 선택한 사용자에게 성공적으로 복사되었습니다.", + "DashboardEmptyNotification": "대시보드에 위젯이 하나도 없습니다. 대시 보드 위젯을 추가하거나 대시 보드를 기본 위젯으로 재설정하세요.", + "DashboardName": "대시보드 이름:", + "DashboardOf": "%s의 대시보드", + "DefaultDashboard": "기본 대시보드 - 기본 위젯으로 구성된 레이아웃을 사용합니다", + "DeleteWidgetConfirm": "대시보드에서 이 위젯을 삭제하시겠습니까?", + "EmptyDashboard": "빈 대시보드 - 원하는 위젯을 선택하세요", + "LoadingWidget": "위젯 로딩중, 잠시만 기다리세요…", + "ManageDashboard": "대시보드 관리", + "Maximise": "최대화", + "Minimise": "최소화", + "NotUndo": "이 작업은 취소할 수 없습니다.", + "PluginDescription": "당신의 사이트를 분석한 대시보드입니다. 드래그 앤 드롭으로 새로운 위젯을 추가하거나 대시보드 레이아웃을 바꾸는 기능을 통해 당신만의 대시보드를 꾸며보세요. 모든 사용자들은 자신만의 대시보드를 가질 수 있습니다.", + "RemoveDashboard": "대시보드 제거", + "RemoveDashboardConfirm": "\"%s\" 대시보드를 제거하시겠습니까?", + "RemoveDefaultDashboardNotPossible": "기본 대시보드는 제거할 수 없습니다", + "RenameDashboard": "대시보드 이름 변경", + "ResetDashboard": "대시보드 재설정", + "ResetDashboardConfirm": "대시보드의 레이아웃을 재설정하여 기본 위젯으로 복원하시겠습니까?", + "SelectDashboardLayout": "새 대시보드의 레이아웃을 선택하세요", + "SelectWidget": "대시보드에 추가할 위젯 선택", + "SetAsDefaultWidgets": "기본 위젯으로 설정", + "SetAsDefaultWidgetsConfirm": "현재 위젯과 대시보드의 레이아웃을 기본 템플릿으로 설정하시겠습니까?", + "SetAsDefaultWidgetsConfirmHelp": "위젯과 대시보드의 레이아웃은 새로운 대시보드를 만들거나 %s 기능을 사용할 때 사용됩니다.", + "TopLinkTooltip": "%s에 대한 웹 로그 분석 보고서를 볼 수 있습니다.", + "WidgetNotFound": "위젯을 찾지 못함", + "WidgetPreview": "위젯 미리보기" + } +} diff --git a/plugins/Dashboard/lang/ku.json b/plugins/Dashboard/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..f4e1c8e9ad0b5918fda4b1dbb8bec32718682baa --- /dev/null +++ b/plugins/Dashboard/lang/ku.json @@ -0,0 +1,29 @@ +{ + "Dashboard": { + "AddAWidget": "زیادکردنی ویجیت", + "AddPreviewedWidget": "بۆ زیادکردنی ویجیت بۆ داشبۆرد کرتە بکە", + "ChangeDashboardLayout": "گۆڕینی چوارچێوەی داشبۆرد", + "CopyDashboardToUser": "ڕوونووسکردنی داشبۆرد بۆ بەکارهێنەر", + "CreateNewDashboard": "دروستکردنی داشبۆردی نوێ", + "Dashboard": "داشبۆرد", + "DashboardCopied": "ئەم داشبۆردە بەسەرکەوتوویی بۆ بەکارهێنەر ڕوونووسکرا.", + "DashboardEmptyNotification": "داشبۆردەکەت هیچ ویجیتێک لەخۆ ناگرێ. بە زێدەکردنی هەندێ ویجتەوە دەست پێ بکە یان هەر داشبۆردەکە ڕێکبخەرەوە بۆ هەڵبژاردنی ویجتی بنەڕەتی.", + "DashboardName": "ناوی داشبۆرد", + "DashboardOf": "داشبۆردی %s", + "DefaultDashboard": "داشبۆردی بنەڕەت - بەکارهێنانی بنەڕەتی هەڵبژاردنەکانی ویجت و چوارچێوەی ستوونەکان", + "DeleteWidgetConfirm": "دڵنیای دەتەوێ ئەم ویجتە لە داشبۆرد بسڕیتەوە؟", + "EmptyDashboard": "داشبۆردی چۆڵ - ویجتە دڵخوازەکانت هەڵبژێرە", + "LoadingWidget": "بارگرتنی ویجت، تکایە بوەستە…", + "ManageDashboard": "بەڕێوەبەریی داشبۆرد", + "Maximise": "زۆرینەکردن", + "Minimise": "کەمینەکردن", + "NotUndo": "لەداهاتوودا ناتوانی ئەم کردارە هەڵوەشێنیتەوە.", + "RemoveDashboard": "لابردنی داشبۆرد", + "RemoveDashboardConfirm": "دڵنیای دەتەوێ داشبۆردەکە لاببەی \"%s\"؟", + "RemoveDefaultDashboardNotPossible": "داشبۆردی بنەڕەتی لاناچێ", + "RenameDashboard": "ناونانەوەی داشبۆرد", + "ResetDashboard": "ڕێکخستنەوەی داشبۆرد", + "ResetDashboardConfirm": "دڵنیای دەتەوێ چوارچێوەی داشبۆردەکەت ڕێک بخەیتەوە بۆ هەڵبژاردنە بنەڕەتەکانی ویجت؟", + "SelectDashboardLayout": "تکایە چواچێوەی نوێی داشبۆردەکەت هەڵبژێرە" + } +} diff --git a/plugins/Dashboard/lang/lb.json b/plugins/Dashboard/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Dashboard/lang/lb.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Dashboard/lang/lt.json b/plugins/Dashboard/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..12f1502d941dc7dae9975eacb497e607b6e00cae --- /dev/null +++ b/plugins/Dashboard/lang/lt.json @@ -0,0 +1,37 @@ +{ + "Dashboard": { + "AddAWidget": "Pridėti valdiklį", + "AddPreviewedWidget": "Spustelėkite, norėdami pridėti valdiklį į skydelį", + "ChangeDashboardLayout": "Pakeisti skydelio išdėstymą", + "CopyDashboardToUser": "Kopijuoti skydelį naudotojui", + "CreateNewDashboard": "Sukurti naują skydelį", + "Dashboard": "Skydelis", + "DashboardCopied": "Esamas skydelis sėkmingai nukopijuotas pasirinktam naudotojui.", + "DashboardEmptyNotification": "Jūsų skydelyje nėra jokių valdiklių. Pradėkite, pridėdami kelis valdiklius arba, tiesiog, atstatykite skydelį į numatytąjį valdiklių pasirinkimą.", + "DashboardName": "Skydelio pavadinimas:", + "DashboardOf": "%s skydelis", + "DefaultDashboard": "Numatytasis skydelis - Naudojamas numatytųjų valdiklių pasirinkimo ir stulpelių išdėstymas", + "DeleteWidgetConfirm": "Ar tikrai norite pašalinti šį valdiklį iš skydelio?", + "EmptyDashboard": "Tuščias skydelis - Pasirinkite savo mėgstamus valdiklius", + "LoadingWidget": "Įkeliamas valdiklis, palaukite…", + "ManageDashboard": "Tvarkyti skydelį", + "Maximise": "Išskleisti", + "Minimise": "Suskleisti", + "NotUndo": "Jūs negalėsite atšaukti šios operacijos.", + "PluginDescription": "Jūsų Saityno Analitikos Skydelis. Tinkinkite savo skydelį, pridėdami naujų valdiklių, vilkdami juos iš vienos vietos į kitą ir keisdami skydelio stulpelių išdėstymą. Kiekvienas naudotojas gali tvarkyti savo asmeninį pasirinktiną skydelį.", + "RemoveDashboard": "Šalinti skydelį", + "RemoveDashboardConfirm": "Ar tikrai norite pašalinti skydelį \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Numatytasis skydelis negali būti pašalintas", + "RenameDashboard": "Pervadinti skydelį", + "ResetDashboard": "Atstatyti skydelį", + "ResetDashboardConfirm": "Ar tikrai norite atstatyti savo skydelio išdėstymą į numatytąjį valdiklių pasirinkimą?", + "SelectDashboardLayout": "Pasirinkite savo naujojo skydelio išdėstymą", + "SelectWidget": "Pasirinkite valdiklį, kurį norite pridėti į skydelį", + "SetAsDefaultWidgets": "Nustatyti kaip numatytąjį valdiklių pasirinkimą", + "SetAsDefaultWidgetsConfirm": "Ar tikrai norite nustatyti esamą valdiklių pasirinkimą ir skydelio išdėstymą kaip numatytąjį skydelio šabloną?", + "SetAsDefaultWidgetsConfirmHelp": "Šis valdiklių pasirinkimas ir skydelio stulpelių išdėstymas bus naudojami, kai bet kuris naudotojas sukurs naują skydelį arba kai bus naudojama \"%s\" ypatybė.", + "TopLinkTooltip": "Rodyti saityno analitikos ataskaitas, skirtas %s.", + "WidgetNotFound": "Valdiklis nerastas", + "WidgetPreview": "Valdiklio peržiūra" + } +} diff --git a/plugins/Dashboard/lang/lv.json b/plugins/Dashboard/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..575dc5490a12b09ccf5a9095fcb450b947f3663e --- /dev/null +++ b/plugins/Dashboard/lang/lv.json @@ -0,0 +1,38 @@ +{ + "Dashboard": { + "AddAWidget": "Pievienot logrīku", + "AddPreviewedWidget": "Pievienot apskatīto logdaļu panelim", + "ChangeDashboardLayout": "Izmainīt paneļa izkārtojumu", + "CopyDashboardToUser": "Kopēt paneli uz lietotāju", + "CreateNewDashboard": "Izveidot jaunu paneli", + "Dashboard": "Panelis", + "DashboardCategoryHelp": "Šī ir paneļu lapa. Paneļi ir Matomo logrīku kolekcija, ko jūs pats izveidojat, lai tā atbilstu jūsu konkrētajām vajadzībām. Apvienojiet jebkurus Matomo logrīkus, lai iegūtu %1$s*jums*%2$s nepieciešamos datus ar vienu acu uzmetienu.", + "DashboardCopied": "Panelis sekmīgi kopēts atlasītajam lietotājam.", + "DashboardEmptyNotification": "Jūsu panelī nav logrīku. Sāciet ar dažu logrīku pievienošanu vai vienkārši atiestatiet paneli uz noklusējuma logrīka atlasi.", + "DashboardName": "Paneļa vārds:", + "DashboardOf": "%s panelis", + "DefaultDashboard": "Noklusējuma panelis - ar noklusējuma logrīku izvēli un kolonnu izkārtojumu", + "DeleteWidgetConfirm": "Vai tiešām vēlaties dzēst šo logdaļu no paneļa?", + "EmptyDashboard": "Tukšs panelis - izvēlieties savus iecienītākos logrīkus", + "LoadingWidget": "Notiek logdaļas ielāde, lūdzu uzgaidiet…", + "ManageDashboard": "Pārvaldīt paneli", + "Maximise": "Izvērst", + "Minimise": "Samazināt", + "NotUndo": "Jūs nevarēsiet atgriezt šo operāciju.", + "PluginDescription": "Jūsu tīmekļa analīzes panelis. Pielāgojiet savu paneli, pievienojot jaunus logrīkus, pārvietojot tos vai mainot paneļa kolonnu izkārtojumu. Katrs lietotājs var pārvaldīt savu pielāgoto paneli.", + "RemoveDashboard": "Noņemt paneli", + "RemoveDashboardConfirm": "Vai Jūs tiešām vēlaties noņemt paneli \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Noklusējuma paneli nevar noņemt", + "RenameDashboard": "Pārsaukt paneli", + "ResetDashboard": "Atiestatīt paneli", + "ResetDashboardConfirm": "Vai tiešām vēlaties atiestatīt savu paneļa izkārtojumu uz noklusējuma logrīku izvēli?", + "SelectDashboardLayout": "Lūdzu izvēlieties Jūsu jaunā paneļa izkārtojumu", + "SelectWidget": "Izvēlieties to logdaļu, kuru vēlaties pievienot panelim", + "SetAsDefaultWidgets": "Iestatīt kā noklusējuma logrīku izvēli", + "SetAsDefaultWidgetsConfirm": "Vai tiešām vēlaties iestatīt pašreizējo logrīku izvēli un paneļa izkārtojumu kā noklusējuma paneļa veidni?", + "SetAsDefaultWidgetsConfirmHelp": "Šī logrīku izvēle un paneļa kolonnu izkārtojums tiks izmantots, kad lietotājs izveidos jaunu paneli vai kad tiks pielietota \"%s\" funkcija.", + "TopLinkTooltip": "Skatīt tīmekļa analīzes ziņojumus par %s.", + "WidgetNotFound": "Logdaļa nav atrasta", + "WidgetPreview": "Logdaļas priekšstatījums" + } +} diff --git a/plugins/Dashboard/lang/ms.json b/plugins/Dashboard/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..8f17d37b1860cdc7955f86940e7d1c0aa9e2c80a --- /dev/null +++ b/plugins/Dashboard/lang/ms.json @@ -0,0 +1,12 @@ +{ + "Dashboard": { + "AddAWidget": "Tambah widget", + "AddPreviewedWidget": "Klik untuk menambah widget pada papan pemuka", + "ChangeDashboardLayout": "Tukar susun atur papan pemuka", + "CopyDashboardToUser": "Salin papan pemuka kepada pengguna", + "CreateNewDashboard": "Buat papan pemuka baharu", + "Dashboard": "Papan Pemuka", + "DashboardCopied": "Papan pemuka semasa berjaya disalin ke pengguna terpilih.", + "RenameDashboard": "Ubah nama papan pemuka" + } +} diff --git a/plugins/Dashboard/lang/nb.json b/plugins/Dashboard/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..baf772a421b5e6369aa6430f975b7ce99bd37b95 --- /dev/null +++ b/plugins/Dashboard/lang/nb.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Legg til miniprogram", + "AddPreviewedWidget": "Klikk for å legge til miniprogram i oversikt", + "ChangeDashboardLayout": "Tilpass oversiktspanel", + "CopyDashboardToUser": "Kopier oversiktspanel til bruker", + "CreateNewDashboard": "Ny oversikt", + "Dashboard": "Oversikt", + "DashboardCategoryHelp": "Dette er en oversiktsside. Oversikten er en samling Matomo-miniprogrammer du kan legge til selv for å tilpasse bruken dine behov. Bland fritt for å få dataen %1$s*du*%2$s trenger.", + "DashboardCopied": "Gjeldende oversikt ble kopiert til valgt bruker.", + "DashboardEmptyNotification": "Din oversikt inneholder ikke noen miniprogrammer. Start ved å legge til noen miniprogrammer eller bare gjenopprett oversikten til å bruke de forvalgte miniprogrammene.", + "DashboardName": "Navn på oversikt:", + "DashboardOf": "Oversikt for %s", + "DefaultDashboard": "Standard oversikt – Bruker standardwidgetene og -kolonnelayout", + "DeleteWidgetConfirm": "Er du sikker på at du vil fjerne dette elementet fra din oversikt?", + "EmptyDashboard": "Tøm oversikt — Velg dine favorittminiprogrammer", + "LoadingWidget": "Laster element, vennligst vent …", + "ManageDashboard": "Administrer oversikt", + "Maximise": "Maksimer", + "Minimise": "Minimer", + "NotUndo": "Du vil ikke være i stand til å angre denne handlingen.", + "PluginDescription": "Din vevstatistikkoversikt. Tilpass din oversikt ved å legge til nye miniprogrammer, dra og slipp dem der du vil ha dem, og endre oversiktens kolonneoppsett. Hver bruker kan administrere sine egne tilpassede oversikter.", + "RemoveDashboard": "Fjern oversikt", + "RemoveDashboardConfirm": "Er du sikker på at du vil fjerne oversikten «%s»?", + "RemoveDefaultDashboardNotPossible": "Standardoversikten kan ikke slettes", + "RenameDashboard": "Gi nytt navn på oversiktspanelet", + "ResetDashboard": "Nullstill oversikt", + "ResetDashboardConfirm": "Gjennopprett oversiktsoppsettet til bruk av de forvalgte miniprogrammene?", + "SelectDashboardLayout": "Vennligst veld ny oversiktslayout", + "SelectWidget": "Velg miniprogram å legge til i oversiktspanelet", + "SetAsDefaultWidgets": "Bruk som forvalgt miniprogramsutvalg", + "SetAsDefaultWidgetsConfirm": "Sett dette miniprogramsutvalget og kolonneoppsettet som forvalgt mal?", + "SetAsDefaultWidgetsConfirmHelp": "Dette widgetutvalget og kolonnelayouten vil bli brukt når brukere lager nye oversikter, eller når funksjonen \"%s\" brukes.", + "TopLinkTooltip": "Vis webstatistikkrapporter for %s.", + "TopMenuTitle": "Analyse", + "WidgetNotFound": "Finner ikke element", + "WidgetPreview": "Forhåndsvisning av element" + } +} diff --git a/plugins/Dashboard/lang/nl.json b/plugins/Dashboard/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..3ec245e87b09c3a650b4d02395755a0076d60430 --- /dev/null +++ b/plugins/Dashboard/lang/nl.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Voeg widget toe aan dit dashboard", + "AddPreviewedWidget": "Klik om het widget aan het dashboard toe te voegen", + "ChangeDashboardLayout": "Wijzig de dashboard lay-out", + "CopyDashboardToUser": "Dashboard naar gebruiker kopiëren", + "CreateNewDashboard": "Nieuw dashboard aanmaken", + "Dashboard": "Dashboard", + "DashboardCategoryHelp": "Dit is een dashboardpagina. Dashboards zijn een verzameling van Matomo-widgets die u zelf toevoegt volgens uw specifieke behoeften. Kies en schik alle Matomo-widgets zodat u in één oogopslag de %1$s-gegevens krijgt die *u* %2$s nodig heeft.", + "DashboardCopied": "Huidig dashboard succesvol naar geselecteerde gebruiker gekopieerd.", + "DashboardEmptyNotification": "Uw dashboard bevat geen widgets. Start met het toevoegen van nieuwe widgets of reset het dashboard naar de standaard widget selectie.", + "DashboardName": "Naam dashboard:", + "DashboardOf": "Dashboard van %s", + "DefaultDashboard": "Standaard dashboard - Gebruik de standaard widgets in een standaard lay-out", + "DeleteWidgetConfirm": "Weet u zeker dat u dit widget van het dashboard wilt verwijderen?", + "EmptyDashboard": "Maak het dashboard leeg - kies uw favoriete widgets", + "LoadingWidget": "Bezig widget te laden, even geduld aub…", + "ManageDashboard": "Beheer dit dashboard", + "Maximise": "Maximaliseren", + "Minimise": "Minimaliseren", + "NotUndo": "Je kunt deze verwijdering niet ongedaan maken.", + "PluginDescription": "Je Web Analytics Dashboard. Pas je dashboard aan door nieuwe widgets toe te voegen, versleep ze, en wijzig de dashboard kolomweergave. Elke gebruiker kan een eigen aangepaste dashboard beheren.", + "RemoveDashboard": "Dashboard verwijderen", + "RemoveDashboardConfirm": "Weet u zeker dat u het dashboard \"%s\" wilt verwijderen?", + "RemoveDefaultDashboardNotPossible": "Het standaard dashboard kan niet worden verwijderd", + "RenameDashboard": "Dashboard hernoemen", + "ResetDashboard": "Reset dashboard", + "ResetDashboardConfirm": "Wilt u echt uw dashboard lay-out terugzetten naar de standaard Widgets selectie?", + "SelectDashboardLayout": "Selecteer je nieuwe dashboard lay-out", + "SelectWidget": "Selecteer een widget om toe te voegen aan het dashboard", + "SetAsDefaultWidgets": "Stel in als standaard widgetselectie", + "SetAsDefaultWidgetsConfirm": "Weet je zeker dat je de huidige selectie van widgets en dashboard lay-out wilt toepassen als het nieuwe standaard dashboard sjabloon?", + "SetAsDefaultWidgetsConfirmHelp": "Deze selectie van widgets en dashboard lay-out zal gebruikt worden om nieuwe dashboards te maken voor nieuwe gebruikers, of wanneer de \"%s\" functie gebruikt is.", + "TopLinkTooltip": "Bekijk web analyse rapporten voor %s.", + "TopMenuTitle": "Analytics", + "WidgetNotFound": "Widget niet gevonden", + "WidgetPreview": "Widget voorbeeld" + } +} diff --git a/plugins/Dashboard/lang/nn.json b/plugins/Dashboard/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..0998c7917fddbf7d7ff931302fab331d6baa65e6 --- /dev/null +++ b/plugins/Dashboard/lang/nn.json @@ -0,0 +1,32 @@ +{ + "Dashboard": { + "AddAWidget": "Legg til småprogram", + "AddPreviewedWidget": "Klikk for å leggja småprogram til styringspanelet", + "ChangeDashboardLayout": "Endre oppsett på styringspanelet", + "CreateNewDashboard": "Opprett nytt styringspanel", + "Dashboard": "Styringspanel", + "DashboardEmptyNotification": "Styringspanelet ditt inneheld ingen småprogram. Start med å leggja til nokre småprogram, eller nullstill panelet til standardutvalet.", + "DashboardName": "Namn på styringspanel:", + "DashboardOf": "Styringspanelet til %s", + "DefaultDashboard": "Standard styringspanel - Bruker standard småprogramutval og kolonneutforming", + "DeleteWidgetConfirm": "Er du sikker på at du vil fjerna dette småprogrammet frå styringspanelet ditt?", + "EmptyDashboard": "Tomt styringspanel - Vel dine favorittsmåprogram", + "LoadingWidget": "Lastar småprogram, vent…", + "ManageDashboard": "Handsam styringspanel", + "Maximise": "Maksimer", + "Minimise": "Minimer", + "NotUndo": "Du kan ikkje angra denne handlinga.", + "RemoveDashboard": "Fjern styringspanel", + "RemoveDashboardConfirm": "Er du sikker på at du vil fjerna styringspanelet \"%s\"?", + "RenameDashboard": "Lag nytt namn på styringspanel", + "ResetDashboard": "Nullstill styringspanelet", + "ResetDashboardConfirm": "Vil du verkeleg nullstilla styringspanelet ditt?", + "SelectDashboardLayout": "Vel ditt nye oppsett av styringspanelet", + "SelectWidget": "Vel det småprogrammet du vil legga til styringspanelet", + "SetAsDefaultWidgets": "Sett som standard småprogramutval", + "SetAsDefaultWidgetsConfirm": "Er du sikker på at du vil setja dette utvalet av småprogram og utforming som standardmal for styringspanel?", + "SetAsDefaultWidgetsConfirmHelp": "Dette utvalet av småprogram og kolonneutforminga vil bli brukt når ein brukar opprettar eit nytt styringspanel, eller når \"%s\"-funksjonen er brukt.", + "WidgetNotFound": "Fann ikkje småprogrammet", + "WidgetPreview": "Småprogramvisning" + } +} diff --git a/plugins/Dashboard/lang/pl.json b/plugins/Dashboard/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..3cb59d731e7d6e8e25c2853832a7dfafb9d910d2 --- /dev/null +++ b/plugins/Dashboard/lang/pl.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Dodaj widget do tego pulpitu", + "AddPreviewedWidget": "Kliknij, aby dodać widżet do pulpitu", + "ChangeDashboardLayout": "Zmień układ pulpitu", + "CopyDashboardToUser": "Kopiuj pulpit do użytkownika", + "CreateNewDashboard": "Utwórz nowy pulpit", + "Dashboard": "Pulpit", + "DashboardCategoryHelp": "Strona pulpitu. Pulpity są kolekcją widżetów Matomo, które możesz samodzielnie dodawać, w zależności od potrzeb. Miksuj i dobieraj widżety Matomo, żeby mieć dane, których %1$s\"Ty\"%2$s potrzebujesz, pod ręką.", + "DashboardCopied": "Bieżący pulpit został pomyślnie skopiowany do wybranego użytkownika.", + "DashboardEmptyNotification": "Twój pulpit nie zawiera żadnych widżetów. Rozpocznij pracę od dodania kilku widżetów, lub po prostu zresetuj pulpit, aby przywrócić domyślny zestaw widżetów.", + "DashboardName": "Nazwa pulpitu:", + "DashboardOf": "Pulpit %s", + "DefaultDashboard": "Domyślny pulpit - Używa domyślnego zestawu widżetów i układu kolumn", + "DeleteWidgetConfirm": "Czy jesteś pewien, że chcesz usunąć ten widżet z pulpitu?", + "EmptyDashboard": "Pusty pulpit - wybierz swoje ulubione widżety", + "ExportThisDashboard": "Eksportuj Pulpit", + "LoadingWidget": "Ładowanie widżetu, proszę czekać…", + "ManageDashboard": "Zarządzaj Pulpitem", + "Maximise": "Maksymalizuj", + "Minimise": "Minimalizuj", + "NotUndo": "Nie będziesz mógł cofnąć tej operacji.", + "PluginDescription": "Twój pulpit statystyk. Dostosuj swój pulpit poprzez dodanie nowych widżetów, przeciągaj i opuszczaj je w dowolnych miejscach, zmień także układ kolumn. Każdy użytkownik może zarządzać swoim pulpitem.", + "RemoveDashboard": "Usuń pulpit", + "RemoveDashboardConfirm": "Czy jesteś pewny, że chcesz usunąć pulpit \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Nie można usunąć domyślnego pulpitu", + "RenameDashboard": "Zmień nawę pulpitu", + "ResetDashboard": "Resetuj pulpit", + "ResetDashboardConfirm": "Czy na pewno chcesz zresetować układ pulpitu?", + "SelectDashboardLayout": "Wybierz układ swojego nowego pulpitu", + "SelectWidget": "Wybierz widżet, który chcesz dodać do pulpitu", + "SetAsDefaultWidgets": "Ustaw jako domyślny zestaw widżetów", + "SetAsDefaultWidgetsConfirm": "Czy na pewno chcesz zapisać obecny układ elementów i zestaw widżetów na bieżącym panelu jako domyślny szablon pulpitu?", + "SetAsDefaultWidgetsConfirmHelp": "Ten zestaw widżetów i układ kolumn będzie używany, gdy jakikolwiek użytkownik stworzy nowy pulpit, albo gdy funkcja \"%s\" zostanie użyta.", + "TopLinkTooltip": "Podgląd raportów Web Analytics dla %s.", + "TopMenuTitle": "Statystyki", + "WidgetNotFound": "Widżet nie został znaleziony", + "WidgetPreview": "Podgląd widżetu" + } +} diff --git a/plugins/Dashboard/lang/pt-br.json b/plugins/Dashboard/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..0f01495da2d8367ff55bd25c49715f7de32ee8b5 --- /dev/null +++ b/plugins/Dashboard/lang/pt-br.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Adicionar um widget", + "AddPreviewedWidget": "Adicionar o widget visualizado no preview ao painel", + "ChangeDashboardLayout": "Alterar o leiaute do painel de controle", + "CopyDashboardToUser": "Copiar o painel para um usuário", + "CreateNewDashboard": "Criar um novo painel", + "Dashboard": "Painel de Controle", + "DashboardCategoryHelp": "Essa é uma página de painel de controle. Paineis de controle são uma coleção de widgets da Matomo que você mesmo adiciona para adequar as necessidades específicas. Misture e combine quaisquer widgets da Matomo para obter os dados %1$s*você*%2$s precisa de uma olhada.", + "DashboardCopied": "Painel atual copiado com êxito para o usuário selecionado.", + "DashboardEmptyNotification": "Seu Painel não contém widgets. Comece por adicionar alguns widgets ou apenas voltar o painel para a seleção widget padrão.", + "DashboardName": "Nome do painel:", + "DashboardOf": "Painel de %s", + "DefaultDashboard": "Painel padrão - Usando seleção padrão widgets e layout de colunas", + "DeleteWidgetConfirm": "Tem certeza que deseja excluir este widget do seu Painel?", + "EmptyDashboard": "Painel vazio - Escolha os seus widgets favoritos", + "ExportThisDashboard": "Exportar painel", + "LoadingWidget": "Carregando widget, aguarde…", + "ManageDashboard": "Gerenciar painel", + "Maximise": "Maximizar", + "Minimise": "Minimizar", + "NotUndo": "Você não será capaz de desfazer esta operação.", + "PluginDescription": "Seu Painel de Análise da Rede. Personalize seu painel adicionando novos widgets, arrastando para suas posições, e mudando o leiaute das colunas do painel. Cada usuário pode gerenciar seus próprios painéis personalizados.", + "RemoveDashboard": "Remover painel", + "RemoveDashboardConfirm": "Tem certeza de que deseja remover o painel \"%s\"?", + "RemoveDefaultDashboardNotPossible": "O painel padrão não pode ser removido", + "RenameDashboard": "Renomear painel", + "ResetDashboard": "Reiniciar o painel", + "ResetDashboardConfirm": "Tem certeza de que deseja redefinir o layout do painel para a seleção Widgets padrão?", + "SelectDashboardLayout": "Por favor selecione o novo leiaute do painel", + "SelectWidget": "Selecione o widget a adicionar ao painel", + "SetAsDefaultWidgets": "Definir como seleção de widgets padrão", + "SetAsDefaultWidgetsConfirm": "Tem a certeza que pretende definir a seleção widgets do atual e layout de painel como o modelo de painel padrão?", + "SetAsDefaultWidgetsConfirmHelp": "Esta seleção widgets e layout de colunas do painel será usado quando qualquer usuário cria um novo painel, ou quando o recurso \"%s\" é usado.", + "TopLinkTooltip": "Ver relatórios de Análise Web para %s.", + "TopMenuTitle": "Analíticos", + "WidgetNotFound": "Widget não encontrado", + "WidgetPreview": "Visualização do Widget" + } +} diff --git a/plugins/Dashboard/lang/pt.json b/plugins/Dashboard/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..b3053dd82e105915d9f2dc37f94707c5b3e90250 --- /dev/null +++ b/plugins/Dashboard/lang/pt.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Adicionar um widget", + "AddPreviewedWidget": "Clique para adicionar o widget ao painel", + "ChangeDashboardLayout": "Alterar a disposição do painel", + "CopyDashboardToUser": "Copiar o painel para o utilizador", + "CreateNewDashboard": "Criar um novo painel", + "Dashboard": "Painel", + "DashboardCategoryHelp": "Esta é página de dashboard. Dashboards são uma coleção de widgets do Matomo que pode adicionar para se adaptarem às suas necessidades específicas. Misture e organize qualquer um dos widgets to Matomo para obter as informações que %1$s*você*%2$s precisa.", + "DashboardCopied": "O painel atual foi copiado com sucesso para o utilizador selecionado.", + "DashboardEmptyNotification": "O seu painel não contém quaisquer widgets. Comece por adicionar alguns widgets ou simplesmente reponha o painel para a seleção predefinida de widgets.", + "DashboardName": "Nome do painel:", + "DashboardOf": "Painel de %s", + "DefaultDashboard": "Painel predefinido - Utilizando a seleção predefinida de widgets e disposição em colunas", + "DeleteWidgetConfirm": "Tem a certeza que deseja excluir este widget do seu painel?", + "EmptyDashboard": "Painel vazio - Escolha os seus widgets favoritos", + "LoadingWidget": "A carregar o widget, por favor aguarde…", + "ManageDashboard": "Gerir painel", + "Maximise": "Maximizar", + "Minimise": "Minimizar", + "NotUndo": "Não poderá anular esta operação.", + "PluginDescription": "O seu painel de Web Analytics. Personalize o seu painel adicionando novos widgets, movendo widgets e alterando o layout das colunas do painel. Cada utilizador pode gerir o seu próprio painel personalizado.", + "RemoveDashboard": "Remover painel", + "RemoveDashboardConfirm": "Tem a certeza que pretende remover o painel \"%s\"?", + "RemoveDefaultDashboardNotPossible": "O painel predefinido não pode ser removido", + "RenameDashboard": "Renomear painel", + "ResetDashboard": "Repor painel", + "ResetDashboardConfirm": "Tem a certeza que pretende repor a disposição do seu painel para a seleção de Widgets predefinida?", + "SelectDashboardLayout": "Por favor selecione a sua nova disposição de painel", + "SelectWidget": "Selecione o widget a adicionar ao painel", + "SetAsDefaultWidgets": "Definir como a seleção de widgets predefinida", + "SetAsDefaultWidgetsConfirm": "Tem a certeza que pretende definir a seleção de widgets e a disposição do painel atual como o modelo de painel predefinido?", + "SetAsDefaultWidgetsConfirmHelp": "Esta seleção de widgets e disposição colunas do painel será utilizada quando qualquer utilizador criar um novo painel ou quando a funcionalidade \"%s\" for utilizada.", + "TopLinkTooltip": "Ver relatórios de Web Analytics para %s.", + "TopMenuTitle": "Análise", + "WidgetNotFound": "Widget não encontrado", + "WidgetPreview": "Pré-visualização do widget" + } +} diff --git a/plugins/Dashboard/lang/ro.json b/plugins/Dashboard/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..701ee1e7c649b2d4898d1b94f755c76241d7317d --- /dev/null +++ b/plugins/Dashboard/lang/ro.json @@ -0,0 +1,38 @@ +{ + "Dashboard": { + "AddAWidget": "Adaugă un widget", + "AddPreviewedWidget": "Dă click ca să adaugi un widget la panoul de control", + "ChangeDashboardLayout": "Schimbă aspectul panoului de control", + "CopyDashboardToUser": "Crează o copie a panoului de control pentru utilizator", + "CreateNewDashboard": "Creează un panou de control nou", + "Dashboard": "Panou de control", + "DashboardCategoryHelp": "Aceasta este o pagină de bord. Tablourile de bord sunt o colecție de widget-uri Matomo pe care le adăugați pentru a se potrivi nevoilor dvs. specifice. Amestecați și potriviți oricare dintre widgeturile Matomo pentru a obține dintr-o privire datele %1$s de care *tu*%2$s ai nevoie.", + "DashboardCopied": "Panoul de control copiat cu succes pentru utilizatorul selectat.", + "DashboardEmptyNotification": "Panoul tău de control nu conţine niciun widget. Începi prin a adăuga câteva widget-uri sau doar resetează panoul de control către selecţia implicită de widget-uri.", + "DashboardName": "Numele panoului de control:", + "DashboardOf": "Panoul de control %s", + "DefaultDashboard": "Panou de control implicit - Foloseşte widget-urile selectate şi coloanele implicite", + "DeleteWidgetConfirm": "Esti sigur că doreşti să ştergi widget-ul din panoul de control?", + "EmptyDashboard": "Panou de control gol - Alege widget-urile preferate", + "LoadingWidget": "Se incarca dispozitivul, asteapta…", + "ManageDashboard": "Administreză panou de control", + "Maximise": "Maximizează", + "Minimise": "Minimizează", + "NotUndo": "Nu o să poţi revoca acestă operaţie.", + "PluginDescription": "Tabloul de bord Web Analytics. Personalizați tabloul de bord adăugând widget-uri noi, trageți-le și aruncați-le aici și modificați aspectul coloanei de bord. Fiecare utilizator își poate gestiona propriul tablou de bord personalizat.", + "RemoveDashboard": "Şterge panoul de control", + "RemoveDashboardConfirm": "Eşti sigur(ă) că vrei să ştergi panoul de control \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Tabloul de bord implicit nu poate fi şters", + "RenameDashboard": "Redenumeşte panoul de control", + "ResetDashboard": "Resetează panoul de control", + "ResetDashboardConfirm": "Eşti sigur(ă) că vrei să resetezi aspectul panoului de control la selecţia implicită de Widget-uri?", + "SelectDashboardLayout": "Te rog selectează aspectul noului panou de control", + "SelectWidget": "Selectează widget-ul ce doreşti să-l adaugi în panoul de control", + "SetAsDefaultWidgets": "Stabileşte ca implicită selecţia de widget-uri", + "SetAsDefaultWidgetsConfirm": "Eşti sigur(ă) că vrei să stabileşti selecţia curentă de widget-uri şi aspectul panoului de control ca fiind şablonul implicit al panoului de control?", + "SetAsDefaultWidgetsConfirmHelp": "Această selecţie de widget-uri şi aspectul coloanelor panoului de control va fi folosită când un utilizator creează un panou de control nou sau când este folosită caracteristica \"%s\".", + "TopLinkTooltip": "Vizualizează rapoarte web analitică pentru %s.", + "WidgetNotFound": "Dispozitivul nu exista", + "WidgetPreview": "Previzualizează widget-ul" + } +} diff --git a/plugins/Dashboard/lang/ru.json b/plugins/Dashboard/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..7a0277c43bcb36b3f8639807937bc8bcde8e379b --- /dev/null +++ b/plugins/Dashboard/lang/ru.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Добавить виджет", + "AddPreviewedWidget": "Добавить виджет на панель инструментов", + "ChangeDashboardLayout": "Выберите вид панели инструментов", + "CopyDashboardToUser": "Копировать панель управления пользователю", + "CreateNewDashboard": "Создать новую панель управления", + "Dashboard": "Панель управления", + "DashboardCategoryHelp": "Это страница панели управления. Панели управления представляют собой набор виджетов Matomo, которые вы добавляете самостоятельно в соответствии со своими потребностями. Смешивайте и подбирайте любые виджеты Matomo, чтобы с первого взгляда получить нужные %1$s*вам*%2$s данные.", + "DashboardCopied": "Текущая панель управления успешно скопирована выбранному пользователю.", + "DashboardEmptyNotification": "Ваша панель управления не содержит ни одного виджета. Добавьте какие-нибудь виджеты или просто сбросьте панель управления к настройкам по умолчанию.", + "DashboardName": "Имя панели управления:", + "DashboardOf": "Панель управления %s", + "DefaultDashboard": "Панель управления по умолчанию — использует набор виджетов и расположение колонок по умолчанию", + "DeleteWidgetConfirm": "Вы уверены, что хотите удалить этот виджет с панели управления?", + "EmptyDashboard": "Панель управления пустая — выберите ваши любимые виджеты", + "LoadingWidget": "Загрузка виджета, пожалуйста, подождите…", + "ManageDashboard": "Настройки панели управления", + "Maximise": "Развернуть", + "Minimise": "Свернуть", + "NotUndo": "Данная операция не обратима.", + "PluginDescription": "Ваша Панель управления веб-аналитикой. Настройте свою панель управления, добавив новые виджеты, перемещайте их и изменяйте расположение столбцов в панели управления. Каждый пользователь может управлять своей собственной панелью управления.", + "RemoveDashboard": "Удалить панель управления", + "RemoveDashboardConfirm": "Вы уверены, что хотите удалить эту панель управления: «%s»?", + "RemoveDefaultDashboardNotPossible": "Панель управления по умолчанию не может быть удалена", + "RenameDashboard": "Переименовать панель управления", + "ResetDashboard": "Сбросить панель управления", + "ResetDashboardConfirm": "Вы уверены, что хотите сбросить вид панели управления к виду по умолчанию?", + "SelectDashboardLayout": "Пожалуйста, выберите ваш новый вид панели управления", + "SelectWidget": "Выберите виджет для добавления на панель инструментов", + "SetAsDefaultWidgets": "Установить набор виджетов по умолчанию", + "SetAsDefaultWidgetsConfirm": "Вы уверены, что хотите установить текущий набор виджетов панели как набор виджетов по умолчанию?", + "SetAsDefaultWidgetsConfirmHelp": "Выбор виджетов и расположение колонок панели управления будет использоваться, когда пользователь создаёт новую панель управления или когда используется «%s».", + "TopLinkTooltip": "Посмотреть отчёт веб-аналитики для %s.", + "TopMenuTitle": "Аналитика", + "WidgetNotFound": "Виджет не найден", + "WidgetPreview": "Предпросмотр виджета" + } +} diff --git a/plugins/Dashboard/lang/si.json b/plugins/Dashboard/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Dashboard/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Dashboard/lang/sk.json b/plugins/Dashboard/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..d1d4f7e60fca7b428666084a1e5d7a4bfae15a9b --- /dev/null +++ b/plugins/Dashboard/lang/sk.json @@ -0,0 +1,37 @@ +{ + "Dashboard": { + "AddAWidget": "Pridať widget", + "AddPreviewedWidget": "Pridať prezeranú miniaplikáciu na nástenku", + "ChangeDashboardLayout": "Zmeniť rozvrhnutie nástenky", + "CopyDashboardToUser": "Skopírovať nástenku pre používateľa", + "CreateNewDashboard": "Vytvoriť novú nástenku", + "Dashboard": "Nástenka", + "DashboardCopied": "Aktuálna nástenka bola úspešne skopírovaná pre vybraného používateľa.", + "DashboardEmptyNotification": "Vaša nástenka neobsahuje žiadne miniaplikácie. Začnite pridaním nejakých miniaplikácií alebo iba resetujte nástenku na výber prednastavených miniaplikácií.", + "DashboardName": "Názov panelu:", + "DashboardOf": "Nástenka %s", + "DefaultDashboard": "Prednastavený panel - používa výber prednastavených miniaplikácií a stĺpcové rozloženie", + "DeleteWidgetConfirm": "Ste si istý, že chcete odstrániť miniaplikáciu z panela?", + "EmptyDashboard": "Prázdna nástenka - vyberte si svoje preferované miniaplikácie", + "LoadingWidget": "Načítavanie miniaplikácie, prosím čakajte…", + "ManageDashboard": "Spravovať nástenku", + "Maximise": "Maximalizovať", + "Minimise": "Minimalizovať", + "NotUndo": "Túto operáciu nebudete schopný vrátiť späť.", + "PluginDescription": "Vaša nástenka webovej analýzy. Prispôsobte si svoj panel pridaním nových miniaplikácií, popresúvajte ich, a zmeňte rozvrhnutie stĺpcov panela. Každý používateľ môže spravovať svoju vlastnú nástenku.", + "RemoveDashboard": "Odstrániť nástenku", + "RemoveDashboardConfirm": "Naozaj chcete odstrániť panel \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Štandardnú nástenku nie je možné odstrániť.", + "RenameDashboard": "Premenovať nástenku", + "ResetDashboard": "Resetovať nástenku", + "ResetDashboardConfirm": "Naozaj chcete resetovať rozvrhnutie vašej nástenky na výber prednastavených miniaplikácií?", + "SelectDashboardLayout": "Prosím vyberte si svoje nové rozvrhnutie nástenky", + "SelectWidget": "Vyberte miniaplikáciu, ktorá sa pridá na nástenku", + "SetAsDefaultWidgets": "Nastaviť ako výber prednastavených miniaplikácií", + "SetAsDefaultWidgetsConfirm": "Naozaj chcete nastaviť aktuálny výber miniaplikácií a rozvrhnutie nástenky ako prednastavenú šablónu panela?", + "SetAsDefaultWidgetsConfirmHelp": "Tento výber miniaplikácií a stĺpcové rozvrhnutie nástenky bude použitý ak hociktorý užívateľ vytvorí nový panel, alebo keď je použitá črta \"%s\" .", + "TopLinkTooltip": "Prezeranie reportov webovej analytiky pre %s.", + "WidgetNotFound": "Miniaplikácia nebola nájdená", + "WidgetPreview": "Náhľad na miniaplikáciu" + } +} diff --git a/plugins/Dashboard/lang/sl.json b/plugins/Dashboard/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..f260717ca0ca69e2e8d248e2fe928a5cb540fe6e --- /dev/null +++ b/plugins/Dashboard/lang/sl.json @@ -0,0 +1,20 @@ +{ + "Dashboard": { + "AddAWidget": "Dodaj gradnik", + "AddPreviewedWidget": "Kliknite, če želite dodati gradnik na kontrolno ploščo", + "ChangeDashboardLayout": "Spremeni postavitev nadzorne plošče", + "Dashboard": "Nadzorna Plošča", + "DashboardOf": "Nadzorna plošča od %s", + "DeleteWidgetConfirm": "Ste prepričani da želite izbrisati ta gradnik s kontrolne plošče?", + "LoadingWidget": "Nalagam gradnik…", + "Maximise": "Povečaj", + "Minimise": "Pomanjšaj", + "NotUndo": "Te operacije ne bo mogoče razveljaviti.", + "ResetDashboard": "Ponastavi kontrolno ploščo", + "ResetDashboardConfirm": "Ste prepričani da želite ponastaviti postavitev kontrolne plošče na privzeti izbor gradnikov?", + "SelectDashboardLayout": "Prosimo, izberite vašo novo postavitev nadzorne plošče", + "SelectWidget": "Izberite gradnik, ki ga želite dodati na kontrolno ploščo.", + "WidgetNotFound": "Gradnik ni bil najden", + "WidgetPreview": "Predogled gradnika" + } +} diff --git a/plugins/Dashboard/lang/sq.json b/plugins/Dashboard/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..38f67641f93b3ad5b5ce7c8accac3745b39c3f82 --- /dev/null +++ b/plugins/Dashboard/lang/sq.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Shtoje widget-in te ky pult", + "AddPreviewedWidget": "Që widget-i të shtohet te pulti, klikojeni", + "ChangeDashboardLayout": "Ndryshoni skemë pulti", + "CopyDashboardToUser": "Kopjoje pultin te përdoruesi", + "CreateNewDashboard": "Krijoni pult të ri", + "Dashboard": "Pulti", + "DashboardCategoryHelp": "Kjo është një faqe pulti. Pultet janë një koleksion widget-esh Matomo që mund t’i shtoni ju vetë, sipas nevojave tuaja. Zgjidhni dhe merrni çfarëdo widget-esh të Matomo-s, që të merrni me një vështrim të dhënat që %1$s*ju*%2$s keni nevojë.", + "DashboardCopied": "Pulti i tanishëm u kopjua me sukses te përdoruesi i përzgjedhur.", + "DashboardEmptyNotification": "Pulti juaj s’përmban ndonjë “widget”. Fillojani duke shtuar ndonjë të tillë, ose thjesht rikthejeni pultin te përzgjedhja parazgjedhje për widget-et.", + "DashboardName": "Emër pulti:", + "DashboardOf": "Pulti i %s", + "DefaultDashboard": "Pulti parazgjedhje - Përdorim përzgjedhjeje widget-esh parazgjedhje dhe skema shtyllash", + "DeleteWidgetConfirm": "Jeni i sigurt se doni të fshihet ky “widget” prej pultit?", + "EmptyDashboard": "Pult i zbrazët - Zgjidhni widget-et tuaj të parapëlqyer", + "ExportThisDashboard": "Eksportoje pultin", + "LoadingWidget": "Po ngarkohet widget-i, ju lutemi, prisni…", + "ManageDashboard": "Administroni Pult", + "Maximise": "Maksimizoje", + "Minimise": "Minimizoje", + "NotUndo": "S’do të jeni në gjendje ta zhbëni këtë veprim.", + "PluginDescription": "Pulti Juaj i Analizave Web. Përshtateni pultin tuaj duke shtuar widget-e të rinj, merrini dhe vërini atje ku doni dhe ndryshoni skemën e shtyllave të pultit. Çdo përdorues mund ta administrojë pultin e vet ashtu si do.", + "RemoveDashboard": "Hiqe pultin", + "RemoveDashboardConfirm": "Jeni i sigurt se doni të hiqet pulti “%s”?", + "RemoveDefaultDashboardNotPossible": "Pulti parazgjedhje s’mund të hiqet", + "RenameDashboard": "Riemërtoni pultin", + "ResetDashboard": "Riktheje pultin te parazgjedhjet", + "ResetDashboardConfirm": "Doni vërtet ta riktheni skemën e pultit tuaj te përzgjedhja parazgjedhje për Widget-et?", + "SelectDashboardLayout": "Ju lutemi, përzgjidhni skemën e re për pultin tuaj", + "SelectWidget": "Përzgjidhni widget-in që doni të shtohet te pulti", + "SetAsDefaultWidgets": "Caktojeni si përzgjedhje widget-esh parazgjedhje", + "SetAsDefaultWidgetsConfirm": "Jeni i sigurt se doni të caktohet përzgjedhja e tanishme e widget-eve dhe e skemës për shtyllat si gjedhja parazgjedhje e pultit?", + "SetAsDefaultWidgetsConfirmHelp": "Kjo përzgjedhje widget-sh dhe shtyllash për pultin do të përdoret kur cilido përdorues krijon një pult të ri, ose kur përdoret veçoria “%s”.", + "TopLinkTooltip": "Shihni raporte Analizash Web për %s.", + "TopMenuTitle": "Statistika", + "WidgetNotFound": "S’u gjet widget-i", + "WidgetPreview": "Paraparje widget-i" + } +} diff --git a/plugins/Dashboard/lang/sr.json b/plugins/Dashboard/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..c30b16ca6204e2eb0a0828c9aeb5bff4f69734a0 --- /dev/null +++ b/plugins/Dashboard/lang/sr.json @@ -0,0 +1,37 @@ +{ + "Dashboard": { + "AddAWidget": "Dodaj vidžet", + "AddPreviewedWidget": "Dodaj pregledani vidžet na Konzolu", + "ChangeDashboardLayout": "Promeni izgled konzole", + "CopyDashboardToUser": "Kopiraj Konzolu korisniku", + "CreateNewDashboard": "Nova konzola", + "Dashboard": "Konzola", + "DashboardCopied": "Konzola je uspešno iskopirana izabranim korisnicima.", + "DashboardEmptyNotification": "Vaša konzola nema nijedan vidžet. Počnite sa dodavanjem vidžeta ili jednostavno resetujte konzolu na podrazumevane vidžete.", + "DashboardName": "Naziv konzole:", + "DashboardOf": "Konzola za %s", + "DefaultDashboard": "Podrazumevana konzola - prikaz podrazumevanih vidžeta i rasporeda kolona", + "DeleteWidgetConfirm": "Da li ste sigurni da želite da uklonite ovaj vidžet sa Konzole?", + "EmptyDashboard": "Prazna konzola - izaberite omiljene vidžete", + "LoadingWidget": "Učitavanje vidžeta, molimo sačekajte…", + "ManageDashboard": "Upravljanje konzolom", + "Maximise": "Povećaj", + "Minimise": "Smanji", + "NotUndo": "Nećete moći da poništite ovu operaciju", + "PluginDescription": "Vaša konzola za veb analitiku. Prilagodite je svojim potrebama dodavanjem novih vidžeta, prevlačenjem istih i menjanjem rasporeda. Svaki korisnik može da upravlja svojom konzolom.", + "RemoveDashboard": "Ukloni konzolu", + "RemoveDashboardConfirm": "Da li ste sigurni da želite da uklonite konzolu \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Ne možete ukloniti podrazumevanu konzolu", + "RenameDashboard": "Promena naziva", + "ResetDashboard": "Resetuj konzolu", + "ResetDashboardConfirm": "Da li ste sigurni da želite da resetujete konzolu na podrazumevane vidžete?", + "SelectDashboardLayout": "Molimo vas da izaberete novi izgled konzole", + "SelectWidget": "Izaberite vidžet koji želite da dodate na Konzolu", + "SetAsDefaultWidgets": "Sačuvaj kao podrazumevane vidžete", + "SetAsDefaultWidgetsConfirm": "Da li ste sigurni da trenutno prikazane vidžete i raspored kolona želite da sačuvate kao podrazumevane?", + "SetAsDefaultWidgetsConfirmHelp": "Ovi vidžeti i raspored kolona će biti upotrebljeni kada neki korisnik kreira novu konzolu ili kada se klikne na \"%s\"", + "TopLinkTooltip": "Prikaži izveštaj za %s.", + "WidgetNotFound": "Vidžet nije pronađen", + "WidgetPreview": "Prikaz vidžeta" + } +} diff --git a/plugins/Dashboard/lang/sv.json b/plugins/Dashboard/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..8e81f3fa61f87ab448818f43ee33be94fe4e6593 --- /dev/null +++ b/plugins/Dashboard/lang/sv.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Lägg widget till denna instrumentpanel", + "AddPreviewedWidget": "Klicka för att lägga till widget i instrumentpanelen", + "ChangeDashboardLayout": "Ändra instrumentpanelens layout", + "CopyDashboardToUser": "Kopiera instrumentpanel till användare", + "CreateNewDashboard": "Skapa ny instrumentpanel", + "Dashboard": "Instrumentpanel", + "DashboardCategoryHelp": "Det här är en instrumentpanel-sida. En instrumentpanel är en samling med Matomos widgets som du själv lägger till för att passa specifika behov. Blanda och matcha vilka som helst av Matomos widgets för att få den information som %1$s*du*%2$s vill få överblick av.", + "DashboardCopied": "Aktuell instrumentpanel har kopierats till vald användare.", + "DashboardEmptyNotification": "Instrumentpanelen innehåller inga widgets. Börja med att lägga till widgets eller återställ instrumentpanelen till standardutseendet.", + "DashboardName": "Instrumentpanelens namn:", + "DashboardOf": "Instrumentpanel för %s", + "DefaultDashboard": "Standardinstrumentpanel - Använder standardval för widgets och kolumnlayout", + "DeleteWidgetConfirm": "Är du säker på att du vill ta bort denna widget från instrumentpanelen?", + "EmptyDashboard": "Tom instrumentpanel - Välj dina favoritwidgets", + "ExportThisDashboard": "Exportera instrumentpanel", + "LoadingWidget": "Laddar widget, vänligen vänta…", + "ManageDashboard": "Hantera instrumentpanel", + "Maximise": "Maximera", + "Minimise": "Minimera", + "NotUndo": "Du kommer inte kunna ångra den här åtgärden.", + "PluginDescription": "Din instrumentpanel för webbanalys. Anpassa din instrumentpanel genom att lägga till nya widgets, flytta runt dem och ändra kolumnindelning. Varje användare kan anpassa sin egen instrumentpanel.", + "RemoveDashboard": "Ta bort instrumentpanel", + "RemoveDashboardConfirm": "Vill du verkligen radera instrumentpanelen \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Standardinstrumentpanelen kan inte tas bort", + "RenameDashboard": "Byt namn på instrumentpanel", + "ResetDashboard": "Återställ instrumentpanel", + "ResetDashboardConfirm": "Vill du verkligen återställa instrumentpanelens layout för widgets?", + "SelectDashboardLayout": "Vänligen välj din nya instrumentpanels layout", + "SelectWidget": "Välj widget som ska läggas till i instrumentpanelen", + "SetAsDefaultWidgets": "Ställ in som standardval för widgets", + "SetAsDefaultWidgetsConfirm": "Är du säker på att du vill använda nuvarande val av widgets och layout som standardval för instrumentpaneler?", + "SetAsDefaultWidgetsConfirmHelp": "Det här valet av widgets och kolumnlayout kommer att användas när användare skapar en ny instrumentpanel, eller när \"%s\" används.", + "TopLinkTooltip": "Visa webbanalysrapport för %s.", + "TopMenuTitle": "Analys", + "WidgetNotFound": "Widgeten hittades inte", + "WidgetPreview": "Widget förhandsgranskning" + } +} diff --git a/plugins/Dashboard/lang/ta.json b/plugins/Dashboard/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..1fa473ba7985783491dd29780c9e2f68a84c535b --- /dev/null +++ b/plugins/Dashboard/lang/ta.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "இந்த டாஷ்போர்டில் விட்ஜெட்டைச் சேர்க்கவும்", + "AddPreviewedWidget": "டாஷ்போர்டில் விட்ஜெட்டைச் சேர்க்க கிளிக் செய்க", + "ChangeDashboardLayout": "டாஷ்போர்டு தளவமைப்பை மாற்ற", + "CopyDashboardToUser": "டாஷ்போர்டை பயனருக்கு நகலெடுக்க", + "CreateNewDashboard": "புதிய டாஷ்போர்டை உருவாக்க", + "Dashboard": "டாஷ்போர்டு", + "DashboardCategoryHelp": "இது ஒரு முகப்புப்பெட்டி பக்கம். முகப்புப்பெட்டிகள் உங்கள் குறிப்பிட்ட தேவைகளுக்கு ஏற்ப நீங்கள் கூட்டும் Matomo நிரல்பலகைகளின் தொகுப்பு. Matomo நிரல்பலகைகளை ஏதேனும் ஒரு வகையில் கலவை செய்து ஒரு பார்வையில் %1$s*நீங்கள்*%2$s தேவைப்படும் தகவல்களை பெறுங்கள்.", + "DashboardCopied": "தற்போதைய டாஷ்போர்டு தேர்ந்தெடுக்கப்பட்ட பயனருக்கு வெற்றிகரமாக நகலெடுக்கப்பட்டது.", + "DashboardEmptyNotification": "உங்களது முகப்புப்பலகையில் எந்தவொரு விட்ஜெட்டும் இல்லை. சில விட்ஜெட்களைச் சேர்ப்பதன் மூலம் தொடங்கவும் அல்லது இயல்புநிலை விட்ஜெட் தேர்வுக்கு டாஷ்போர்டை மீட்டமைக்கவும்.", + "DashboardName": "முகப்புப்பெட்டி பெயர்:", + "DashboardOf": "%s -ன் முகப்புப்பலகை", + "DefaultDashboard": "இயல்புநிலை டாஷ்போர்டு - இயல்புநிலை விட்ஜெட்கள் தேர்வு மற்றும் நெடுவரிசை தளவமைப்பைப் பயன்படுத்துதல்", + "DeleteWidgetConfirm": "டாஷ்போர்டிலிருந்து இந்த விட்ஜெட்டை நீக்க விரும்புகிறீர்களா?", + "EmptyDashboard": "வெற்று டாஷ்போர்டு- உங்களுக்கு பிடித்த விட்ஜெட்டுகளைத் தேர்ந்தெடுங்கள்", + "ExportThisDashboard": "இந்த முகப்புபலகையை ஏற்றுமதி செய்", + "LoadingWidget": "விட்ஜெட் ஏற்றப்படுகிறது. தயவு செய்து காத்திருக்கவும்…", + "ManageDashboard": "இந்த முகப்புபலகையை நிர்வகி", + "Maximise": "பெருப்பிக்க", + "Minimise": "சிறியதாக்க", + "NotUndo": "உங்களால் இந்தச் செயற்பாட்டை மீளப் பின்நோக்கி நகர்த்த முடியாது.", + "PluginDescription": "உங்கள் வலை பகுப்பாய்வு டாஷ்போர்டு. விட்ஜெட்களைச் சேர்ப்பதன் மூலமும், அவற்றை இழுத்து விட்டும், டாஷ்போர்டு நெடுவரிசை அமைப்பை மாற்றியும் உங்கள் டாஷ்போர்டைத் தனிப்பயனாக்கவும். ஒவ்வொரு பயனரும் தங்களது சொந்த தனிப்பயன் டாஷ்போர்டை நிர்வகிக்கலாம்.", + "RemoveDashboard": "டாஷ்போர்டை அகற்று", + "RemoveDashboardConfirm": "\"%s\"டாஷ்போர்டை அகற்ற விரும்புகிறீர்களா?", + "RemoveDefaultDashboardNotPossible": "இயல்புநிலை டாஷ்போர்டை அகற்ற முடியாது", + "RenameDashboard": "டாஷ்போர்டை மறுபெயரிடுங்கள்", + "ResetDashboard": "டாஷ்போர்டை மீட்டமைக்க", + "ResetDashboardConfirm": "உங்கள் டாஷ்போர்டு தளவமைப்பை இயல்புநிலை விட்ஜெட்டுகள் தேர்வுக்கு மீட்டமைக்க விரும்புகிறீர்களா?", + "SelectDashboardLayout": "உங்கள் புதிய டாஷ்போர்டு தளவமைப்பைத் தேர்ந்தெடுக்கவும்", + "SelectWidget": "டாஷ்போர்டில் சேர்க்க விட்ஜெட்டைத் தேர்ந்தெடுக்கவும்", + "SetAsDefaultWidgets": "இயல்புநிலை விட்ஜெட் தேர்வாக அமைக்க", + "SetAsDefaultWidgetsConfirm": "தற்போதைய விட்ஜெட்டுகள் தேர்வு மற்றும் டாஷ்போர்டு தளவமைப்பை இயல்புநிலை தளவமைப்பு வார்ப்புருவாக அமைக்க விரும்புகிறீர்களா?", + "SetAsDefaultWidgetsConfirmHelp": "எந்தவொரு பயனரும் புதிய டாஷ்போர்டை உருவாக்கும்போது அல்லது \" %s \" அம்சம் பயன்படுத்தப்படும்போது இந்த விட்ஜெட்கள் தேர்வு மற்றும் டாஷ்போர்டு நெடுவரிசை தளவமைப்பு பயன்படுத்தப்படும்.", + "TopLinkTooltip": "வலை பகுப்பாய்வு அறிக்கைகளை %s க்கு பார்வையிடவும்.", + "TopMenuTitle": "பகுப்பாய்வு", + "WidgetNotFound": "விட்ஜெட் கிடைக்கவில்லை", + "WidgetPreview": "விட்ஜெட்டை முன்னோட்டமிடுக" + } +} diff --git a/plugins/Dashboard/lang/te.json b/plugins/Dashboard/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..35d69a63048080d1f96c676bd058882475a763aa --- /dev/null +++ b/plugins/Dashboard/lang/te.json @@ -0,0 +1,7 @@ +{ + "Dashboard": { + "Dashboard": "డాష్‌బోర్డు", + "LoadingWidget": "విడ్జెట్ లోడవుతూండి, దయచేసి వేచివుండండి…", + "WidgetPreview": "విడ్జెట్ మునుజూపు" + } +} diff --git a/plugins/Dashboard/lang/th.json b/plugins/Dashboard/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..df8a23df533f5687c53a4537b87e5f80aaa34930 --- /dev/null +++ b/plugins/Dashboard/lang/th.json @@ -0,0 +1,28 @@ +{ + "Dashboard": { + "AddAWidget": "เพิ่มวิดเจ็ท", + "AddPreviewedWidget": "เพิ่มวิดเจ็ทตัวอย่างในแผงควบคุม", + "ChangeDashboardLayout": "เปลี่ยนเค้าโครงแผงควบคุม", + "CopyDashboardToUser": "คัดลอกกระดานหลักไปไว้ที่ผู้ใช้งาน", + "CreateNewDashboard": "สร้างหน้ากระดานหลักใหม่", + "Dashboard": "แผงควบคุม", + "DashboardCopied": "กระดานหลักปัจจุบันได้ถูกคัดลอกไปยังผู้ใช้งานที่เลือกเสร็จเรียบร้อยแล้ว", + "DashboardName": "ชื่อกระดานหลัก", + "DashboardOf": "กระดานหลักของ %s", + "DeleteWidgetConfirm": "คุณแน่ใจไหมที่จะลบวิดเจ็ทนี้ออกจากแผงควบคุม?", + "LoadingWidget": "กำลังโหลดวิดเจ็ท กรุณารอ…", + "ManageDashboard": "จัดการกระดานหลัก", + "Maximise": "มากที่สุด", + "Minimise": "น้อยที่สุด", + "NotUndo": "คุณจะไม่สามารถย้อนกลับไปดำเนินการในส่วนนี้ได้อีก", + "RemoveDashboard": "ลบกระดานหลักออก", + "RemoveDashboardConfirm": "คุณแน่ใจแล้วหรือว่าต้องการลบกระดานหลัก \"%s\" นี้ทิ้งไป?", + "RenameDashboard": "เปลี่ยนชื่อกระดานหลัก", + "ResetDashboard": "รีเซ็ตแผงควบคุมใหม่", + "ResetDashboardConfirm": "แน่ใจหรือไม่ ที่จะต้องการรีเซ็ตรูปแบบแผงควบคุมของคุณ เพื่อเริ่มต้นการเลือกวิดเจ็ตใหม่?", + "SelectDashboardLayout": "กรุณาเลือกรูปแบบแผงควบคุมใหม่ของคุณ", + "SelectWidget": "เลือกวิดเจ็ทเพื่อเพิ่มในแผงควบคุม", + "WidgetNotFound": "ไม่พบวิดเจ็ท", + "WidgetPreview": "ตัวอย่าง Widget" + } +} diff --git a/plugins/Dashboard/lang/tl.json b/plugins/Dashboard/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..7d695812c6bd40f2532d453a14faad588630e8e0 --- /dev/null +++ b/plugins/Dashboard/lang/tl.json @@ -0,0 +1,35 @@ +{ + "Dashboard": { + "AddAWidget": "Magdagdag ng widget", + "AddPreviewedWidget": "I-click upang magdagdag ng widget sa dashboard", + "ChangeDashboardLayout": "Baguhin ang layout ng dashboard", + "CopyDashboardToUser": "Kopyahin ang dashboard sa user", + "CreateNewDashboard": "Lumikha ng bagong dashboard", + "Dashboard": "Dashboard", + "DashboardCopied": "Matagumpay na nakopya ang kasalukuyang dashboard sa napiling user.", + "DashboardEmptyNotification": "Ang iyong dashboard ay hindi nag lalaman ng kahit anong widgets. Umpisahan sa pagdag-dag ng mga widget o i-rest ang dashboard sa pangunahing widget selection.", + "DashboardName": "Pangalan ng dashboard:", + "DashboardOf": "Dashboard ng %s", + "DefaultDashboard": "Default dashboard - Paggamit ng default na pagpili ng widget at mga hanay ng layout", + "DeleteWidgetConfirm": "Sigurado ka bang gusto mong burahin itong widget mula sa dashboard?", + "EmptyDashboard": "Alisin ang laman ng dashboard - Pumili ng iyong mga paboritong widget", + "LoadingWidget": "Ni-loload ang widget mangyaring maghintay sandali…", + "ManageDashboard": "pamahalaan ang dashboard", + "Maximise": "I-maximise", + "Minimise": "I-minimise", + "NotUndo": "Hindi mo magagawang i-undo ang operasyon na ito.", + "RemoveDashboard": "Alisin dashboard", + "RemoveDashboardConfirm": "Sigurado ko bang gusto mong tanggalin ang dashboard \"%s\"?", + "RenameDashboard": "Palitan ang pangalan ng dashboard", + "ResetDashboard": "I-reset ang dashboard", + "ResetDashboardConfirm": "Sigurado ka bang gusto mong i-reset ang iyong layout dashboard sa default na pagpipilian Widget?", + "SelectDashboardLayout": "Mangyaring piliin ang iyong bagong layout dashboard.", + "SelectWidget": "Pumili ng widget na idadagdag sa inyong dashboard.", + "SetAsDefaultWidgets": "Itakda ang default na pagpipilian ng mga widget bilang.", + "SetAsDefaultWidgetsConfirm": "Sigurado ka bang gusto mong e-set ang kasalukuyang mga widget at layout ng dashboard ng template bilang default dashboard?", + "SetAsDefaultWidgetsConfirmHelp": "Ang mga widget na pagpilian at dashboard column layout ay magagamit kapag ang sinumang user na guwama ng bagong dashboard o kapag ang \"%s\" feature ay ginamit.", + "TopLinkTooltip": "Tignan ang Ulat ng Web Analytics para sa %s.", + "WidgetNotFound": "Hindi nakita ang Widget", + "WidgetPreview": "preview ng widget" + } +} diff --git a/plugins/Dashboard/lang/tr.json b/plugins/Dashboard/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..03e53c11b13ecd33ad25e0acc24f86281159743c --- /dev/null +++ b/plugins/Dashboard/lang/tr.json @@ -0,0 +1,40 @@ +{ + "Dashboard": { + "AddAWidget": "Bu panoya bir pano bileşeni ekle", + "AddPreviewedWidget": "Bileşeni panoya eklemek için tıklayın", + "ChangeDashboardLayout": "Pano düzenini değiştir", + "CopyDashboardToUser": "Panoyu kullanıcıya kopyala", + "CreateNewDashboard": "Pano ekle", + "Dashboard": "Pano", + "DashboardCategoryHelp": "Pano bölümü. Panolar, gereksinimlerinize göre Matomo pano bileşenlerini ekleyerek oluşturabileceğiniz görünümlerdir. %1$s\"Sizin\"%2$s gerek duyduğunuz verileri bir bakışta görebileceğiniz şekilde bir araya getirir.", + "DashboardCopied": "Geçerli pano seçilmiş kullanıcıya kopyalandı.", + "DashboardEmptyNotification": "Panoda henüz bir bileşen yok. Başlamak için bazı pano bileşenleri ekleyin ya da panoyu varsayılan pano bileşeni seçimlerine sıfırlayın.", + "DashboardName": "Pano adı:", + "DashboardOf": "%s panosu", + "DefaultDashboard": "Varsayılan pano - Varsayılan pano bileşeni seçimlerini ve sütun düzenini kullanır", + "DeleteWidgetConfirm": "Bu bileşeni panodan silmek istediğinize emin misiniz?", + "EmptyDashboard": "Boş pano - Kullanmak istediğiniz pano bileşenlerini seçin", + "ExportThisDashboard": "Panoyu dışa aktar", + "LoadingWidget": "Lütfen pano bileşeni yüklenirken bekleyin…", + "ManageDashboard": "Pano yönetimi", + "Maximise": "Büyüt", + "Minimise": "Küçült", + "NotUndo": "Bu işlem geri alınamaz.", + "PluginDescription": "Site istatistikleri panonuz. Panonuzu, yeni pano bileşenleri ekleyerek, var olan pano bileşenlerini sürükleyip bırakarak ve sütun düzenini değiştirerek isteğinize göre düzenleyebilirsiniz. Her kullanıcı kendine özel bir pano oluşturabilir.", + "RemoveDashboard": "Panoyu sil", + "RemoveDashboardConfirm": "\"%s\" panosunu silmek istediğinize emin misiniz?", + "RemoveDefaultDashboardNotPossible": "Varsayılan pano silinemez", + "RenameDashboard": "Panoyu yeniden adlandırın", + "ResetDashboard": "Panoyu sıfırla", + "ResetDashboardConfirm": "Pano düzeninizi varsayılan pano bileşeni seçimlerine sıfırlamak istediğinize emin misiniz?", + "SelectDashboardLayout": "Lütfen yeni pano düzenini seçin", + "SelectWidget": "Panoya eklemek istesiğiniz pano bileşenini seçin", + "SetAsDefaultWidgets": "Varsayılan pano bileşeni seçimi olarak ata", + "SetAsDefaultWidgetsConfirm": "Geçerli pano bileşeni seçimi ve pano düzenini varsayılan pano teması olarak atamak istediğinize emin misiniz?", + "SetAsDefaultWidgetsConfirmHelp": "Başka bir kullanıcı yeni bir pano eklediğinde ya da \"%s\" özelliği kullanıldığında, varsayılan olarak bu pano bileşeni seçimi ve sütun düzeni kullanılır.", + "TopLinkTooltip": "%s için site istatistikleri raporunu görüntüle.", + "TopMenuTitle": "İstatistikler", + "WidgetNotFound": "Pano bileşeni bulunamadı", + "WidgetPreview": "Pano bileşeni ön izlemesi" + } +} diff --git a/plugins/Dashboard/lang/tzm.json b/plugins/Dashboard/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Dashboard/lang/tzm.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Dashboard/lang/uk.json b/plugins/Dashboard/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..63e64288e6f71889b20c4c8d383456361b056c6b --- /dev/null +++ b/plugins/Dashboard/lang/uk.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "Додати віджет", + "AddPreviewedWidget": "Додати віджет на панель інструментів", + "ChangeDashboardLayout": "Виберіть вид панелі інструментів", + "CopyDashboardToUser": "Копіювати панель інструментів користувачеві", + "CreateNewDashboard": "Створити нову панель інструментів", + "Dashboard": "Панель інструментів", + "DashboardCategoryHelp": "Це сторінка панелі керування. Панелі керування являють собою набір віджетів Matomo, які ви додаєте самі, щоб задовольнити свої конкретні потреби. Змішуйте і поєднуйте будь-які віджети Matomo, щоб отримати дані, які потрібні %1$s*вам*%2$s з першого погляду.", + "DashboardCopied": "Поточна панель інструментів успішно скопійовано вибраному користувачеві.", + "DashboardEmptyNotification": "Ваша панель інструментів не містить жодного елемента. Додайте якісь віджети або просто скиньте панель інструментів до налаштувань за замовчуванням.", + "DashboardName": "Ім'я панелі інструментів:", + "DashboardOf": "Панель інструментів %s", + "DefaultDashboard": "Панель інструментів за замовчуванням використовує набір віджетів та розташування колонок за замовчуванням", + "DeleteWidgetConfirm": "Ви впевнені, що хочете видалити цей віджет з інформаційної панелі?", + "EmptyDashboard": "Порожня панель інструментів - виберіть ваші улюблені віджети", + "LoadingWidget": "Завантаження віджета, будь ласка, зачекайте…", + "ManageDashboard": "Налаштування панелі інструментів", + "Maximise": "Розгорнути", + "Minimise": "Згорнути", + "NotUndo": "Дана операція не зворотна.", + "PluginDescription": "Ваша панель керування вебаналітикою. Налаштуйте свою панель керування, додавайте нові віджети, переміщуйте їх і змінюйте розташування стовпців панелі керування. Кожен користувач може керувати своєю власною панеллю керування.", + "RemoveDashboard": "Видалити панель інструментів", + "RemoveDashboardConfirm": "Ви впевнені, що хочете видалити цю панель інструментів: '%s'?", + "RemoveDefaultDashboardNotPossible": "Панель управління за замовчуванням не може бути видалена", + "RenameDashboard": "Перейменувати панель інструментів", + "ResetDashboard": "Скинути панель інструментів", + "ResetDashboardConfirm": "Ви впевнені, що хочете скинути вид панелі інструментів до виду за замовчуванням?", + "SelectDashboardLayout": "Будь ласка, виберіть ваш новий вид панелі інструментів", + "SelectWidget": "Виберіть віджет для додавання на панелі інструментів", + "SetAsDefaultWidgets": "Встановити набір віджетів за замовчуванням", + "SetAsDefaultWidgetsConfirm": "Ви впевнені, що хочете встановити поточний набір віджетів панелі інструментів як набір віджетів за замовчуванням?", + "SetAsDefaultWidgetsConfirmHelp": "Вибір віджетів і розташування колонок панелі інструментів буде використовуватися, коли користувач створює нову панель інструментів або коли використовується '%s'.", + "TopLinkTooltip": "Подивитися звіт вебаналітики для %s.", + "TopMenuTitle": "Аналітика", + "WidgetNotFound": "Віджет не найден", + "WidgetPreview": "Попередній перегляд віджету" + } +} diff --git a/plugins/Dashboard/lang/ur.json b/plugins/Dashboard/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..10e02b0bd2ebc9a876df6c10f649b4c684c821a9 --- /dev/null +++ b/plugins/Dashboard/lang/ur.json @@ -0,0 +1,5 @@ +{ + "Dashboard": { + "Dashboard": "ڈیش بورڈ" + } +} diff --git a/plugins/Dashboard/lang/vi.json b/plugins/Dashboard/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..b60a0e3c476e3c52874de1ff78b3ca86b5b9a84d --- /dev/null +++ b/plugins/Dashboard/lang/vi.json @@ -0,0 +1,37 @@ +{ + "Dashboard": { + "AddAWidget": "Thêm Widget", + "AddPreviewedWidget": "Bấm vào đây để thêm widget vào bảng điều khiển", + "ChangeDashboardLayout": "Thay đổi cách bố trí bảng điều khiển", + "CopyDashboardToUser": "Sao chép bảng điều khiển người sử dụng", + "CreateNewDashboard": "Tạo bảng điều khiển mới", + "Dashboard": "Bảng điều khiển", + "DashboardCopied": "Bảng điều khiển hiện tại đã sao chép thành công để cho người sử dụng lựa chọn.", + "DashboardEmptyNotification": "Bảng điều khiển của bạn không có widget nào. Bắt đầu bằng cách thêm các widget hoặc chỉ cần cài đặt lại bảng điều khiển với lựa chọn widget mặc định.", + "DashboardName": "Tên bảng điều khiển:", + "DashboardOf": "Bảng điều khiển của %s", + "DefaultDashboard": "Bảng điều khiển mặc định - Sử dụng các lựa chọn widget mặc định và cột bố trí.", + "DeleteWidgetConfirm": "Bạn có chắc chắn rằng muốn xóa widget này từ bảng điều khiển?", + "EmptyDashboard": "Bảng điều khiển trống - Chọn các widget yêu thích của bạn.", + "LoadingWidget": "Đang tải widget, vui lòng đợi …", + "ManageDashboard": "Quản lý bảng điều khiển", + "Maximise": "Phóng to", + "Minimise": "Thu nhỏ", + "NotUndo": "Bạn sẽ không thể khôi phục hành động này", + "PluginDescription": "Bảng điều khiển của bạn. Có thể tùy biến bằng cách thêm widget, kéo thả hoặc thêm các trường dữ liệu. Mỗi người dùng có thể tùy biến bảng điều khiến cho riêng mình.", + "RemoveDashboard": "Loại bỏ bảng điều khiển", + "RemoveDashboardConfirm": "Bạn có chắc chắn muốn loại bỏ bảng điều khiển \"%s\"?", + "RemoveDefaultDashboardNotPossible": "Các bảng điều khiển mặc định không thể bị gỡ bỏ", + "RenameDashboard": "Đổi tên bảng điều khiển", + "ResetDashboard": "Đặt bảng điều khiển về mặc định", + "ResetDashboardConfirm": "Bạn có chắc chắn muốn thiết lập lại bố trí bảng điều khiển của bạn cho thiết lập các Widget mặc định?", + "SelectDashboardLayout": "Vui lòng chọn bố trí bảng điều khiển mới của bạn", + "SelectWidget": "Chọn widget để thêm vào trong bảng điều khiển", + "SetAsDefaultWidgets": "Thiết lập như lựa chọn widget mặc định.", + "SetAsDefaultWidgetsConfirm": "Bạn có chắc chắn muốn thiết lập các lựa chọn widget hiện tại và bố trí bảng điều khiển như mẫu bảng điều khiển mặc định không?", + "SetAsDefaultWidgetsConfirmHelp": "Các lựa chọn Widget và bố trí các cột bảng điều khiển này sẽ được sử dụng khi bất kỳ người dùng tạo ra một bảng điều khiển mới, hoặc khi \"%s\" tính năng được sử dụng.", + "TopLinkTooltip": "Xem báo cáo Web Analytics cho %s.", + "WidgetNotFound": "Không tìm thấy Widget", + "WidgetPreview": "Xem trước Widget" + } +} diff --git a/plugins/Dashboard/lang/zh-cn.json b/plugins/Dashboard/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..4a0c2df57d1930cb29d6294a5f81ab6996c65b2b --- /dev/null +++ b/plugins/Dashboard/lang/zh-cn.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "向报表面板增加小工具", + "AddPreviewedWidget": "添加小工具到报表面板", + "ChangeDashboardLayout": "修改面板布局", + "CopyDashboardToUser": "复制面板给用户", + "CreateNewDashboard": "创建新的面板", + "Dashboard": "报表面板", + "DashboardCategoryHelp": "面板这是一个面板页面。面板是Matomo的部件的集合,你可以自己添加以满足你的特定需求。混合和匹配任何Matomo的部件,以获得数据%1$s*你*%2$s 需要,一目了然。", + "DashboardCopied": "当前面板成功复制到所选用户。", + "DashboardEmptyNotification": "您的面板没有任何小工具,请增加一些小工具或重置为默认的小工具选择。", + "DashboardName": "面板名称:", + "DashboardOf": "%s报表", + "DefaultDashboard": "默认面板——使用默认的小工具和布局", + "DeleteWidgetConfirm": "您确定要从报表面板中删除这个小工具吗?", + "EmptyDashboard": "空白面板——请选择您喜欢的小工具", + "LoadingWidget": "正在加载小部件,请稍候…", + "ManageDashboard": "管理面板", + "Maximise": "最大化", + "Minimise": "最小化", + "NotUndo": "此操作不可撤消。", + "PluginDescription": "您的 Web 分析面板。如果需要自定义,您可以添加新的小工具,拖动这些小工具,和更改面板的列数和布局。每个用户都可以独立定制属于他自己的面板。", + "RemoveDashboard": "删除面板", + "RemoveDashboardConfirm": "确认要删除面板“%s”吗?", + "RemoveDefaultDashboardNotPossible": "默认的面板无法删除", + "RenameDashboard": "重命名面板", + "ResetDashboard": "重置面板", + "ResetDashboardConfirm": "确认重置面板布局为默认的小工具列表?", + "SelectDashboardLayout": "请选择新报表布局", + "SelectWidget": "选择要加入面板的小工具", + "SetAsDefaultWidgets": "设置为默认小工具列表", + "SetAsDefaultWidgetsConfirm": "您确定要设置当前的小工具列表和布局为默认的面板模板吗?", + "SetAsDefaultWidgetsConfirmHelp": "用户创建新面板时,或者“%s”功能启用时,将使用当前小工具列表和面板布局。", + "TopLinkTooltip": "查看%s的统计报表。", + "TopMenuTitle": "分析", + "WidgetNotFound": "找不到小工具", + "WidgetPreview": "小工具预览" + } +} diff --git a/plugins/Dashboard/lang/zh-tw.json b/plugins/Dashboard/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..6cbb4cadf5da46d32c819b96a2523d6528b62490 --- /dev/null +++ b/plugins/Dashboard/lang/zh-tw.json @@ -0,0 +1,39 @@ +{ + "Dashboard": { + "AddAWidget": "新增小工具", + "AddPreviewedWidget": "將小工具加入展示板", + "ChangeDashboardLayout": "改變展示板版面", + "CopyDashboardToUser": "將展示板複製給使用者", + "CreateNewDashboard": "新增展示板", + "Dashboard": "展示板", + "DashboardCategoryHelp": "這裡是展示板網頁。展示板整合了你新增的 Matomo 小工具以符合你的需求。挑選、排列任何 %1$s*你*%2$s 想要的 Matomo 小工具,來快速展示你要的資料。", + "DashboardCopied": "已成功將目前的展示板複製給選擇的使用者。", + "DashboardEmptyNotification": "你的展示板還沒有任何小工具。開始添加一些小工具,或是直接重置展示板,使用預選的小工具。", + "DashboardName": "展示板名稱:", + "DashboardOf": "%s 的展示板", + "DefaultDashboard": "預設展示板 - 使用預選小工具以及欄位版面", + "DeleteWidgetConfirm": "你確定要從展示板中刪除這個小工具嗎?", + "EmptyDashboard": "空白展示板 - 依你的喜好選擇小工具", + "LoadingWidget": "小工具載入中,請稍候…", + "ManageDashboard": "展示板管理", + "Maximise": "最大化", + "Minimise": "最小化", + "NotUndo": "你將無法復原此操作。", + "PluginDescription": "你的網頁分析展示板。透過添加小工具來自定你的展示板,只需拖曳並放置即可改變展示板的欄位版面。每位使用者可以管理他們自己的展示板。", + "RemoveDashboard": "移除展示板", + "RemoveDashboardConfirm": "你確定要移除展示板:%s?", + "RemoveDefaultDashboardNotPossible": "無法移除預設展示板", + "RenameDashboard": "重新命名展示板", + "ResetDashboard": "重置展示板", + "ResetDashboardConfirm": "你確定要重置你的展示板還原為預選小工具版面?", + "SelectDashboardLayout": "請選擇你新展示板的版面", + "SelectWidget": "選取欲增加至展示板的小工具", + "SetAsDefaultWidgets": "設定為預選小工具", + "SetAsDefaultWidgetsConfirm": "你確定要將目前選擇的小工具和展示板版面設定為預設的展示板範本?", + "SetAsDefaultWidgetsConfirmHelp": "這個小工具和展示板版面將於使用者建立的新展示板,或是使用 %s 功能時套用。", + "TopLinkTooltip": "查看 %s 的網頁分析報表。", + "TopMenuTitle": "分析", + "WidgetNotFound": "找不到小工具", + "WidgetPreview": "小工具預覽" + } +} diff --git a/plugins/Dashboard/stylesheets/dashboard.less b/plugins/Dashboard/stylesheets/dashboard.less new file mode 100644 index 0000000000000000000000000000000000000000..e8dd15e61b07336abeb8706a2d104095154b4eb4 --- /dev/null +++ b/plugins/Dashboard/stylesheets/dashboard.less @@ -0,0 +1,454 @@ +#dashboard { + margin: 0 -8px; + + #dashboardWidgetsArea { + margin-left: auto; + margin-right: auto; + } + + #dashboardWidgetsArea > .col { + min-height: 100px; + // Customize Bootstrap gutter between columns + padding-right: 8px; + padding-left: 8px; + + >.sortable { + padding: 5px 0 13px 0; + } + } +} + +body#standalone { + background-color: @theme-color-widget-exported-background-base; + + .top_controls { + z-index: 999; + background-color: @theme-color-widget-exported-background-base; + } + #dashboard { + margin: -10px -6px 0; + } +} + +.hover { + border: 2px dashed #E3E3E3; +} + +.widgetHover { + border: 1px solid #aba494; +} + +.widget .entityContainer { + width: 100%; +} + +.widget .sparkline { + margin-left: 5px; +} + +// Overriding some dataTable css for better dashboard display +.widget .dataTableWrapper { + width: 100% !important; + table * { + // Because of Bootstrap + box-sizing: content-box; + } +} + +.ui-confirm { + display: none; + color: @theme-color-text-light; + cursor: default; + font-size: 12px !important; + border-radius: 4px; + padding: 20px 10px; + min-height: 0 !important; + + textarea { + background-color: @theme-color-background-contrast; + } +} + +.ui-confirm p { + margin-top: 16px; + font-size: 14px; + +} +.ui-confirm h2 { + text-align: center; + font-weight: bold; + padding: 0; +} + +.ui-dialog .ui-dialog-buttonpane { + text-align: center; + border: none; +} + +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: none; +} + +.ui-dialog-buttonset input[type=button], .ui-dialog-buttonset button { + color: #fff !important; + border: 0 !important; + font-size: 12px !important; + padding: 5px 20px !important; + border-radius: 3px; + cursor: pointer; + display: inline-block; + margin: 0 8px 3px 8px !important; +} + +.ui-dialog .ui-button-text { + padding: 0 !important; +} + +#Piwik_Popover_Wrapper { + position: absolute; + z-index: 1001; + inset: 0; + padding: 15px; + display: flex; + flex-direction: column; + pointer-events: none; +} + +.ui-widget-overlay { + opacity: 0.6; + background: none #000; + position: fixed; + z-index: 1000; + + pointer-events: auto; +} + +.ui-dialog { + z-index: 1001; +} + +.ui-dialog--responsive { + display: flex; + flex-direction: column; + + position: relative; + top: 0 !important; + left: 0 !important; + margin: auto; + width: 100%; + max-width: 100%; + height: min-content !important; + + pointer-events: auto; + + @media (min-width: 570px) { + width: min-content; + min-width: 540px; + } +} + +.menu { + display: none; +} + +.widgetLoading, .widgetLoadingError, .dimensionLoadingError { + padding: 10px; + font-size: 10pt; +} + +.widgetLoadingError { + .alert.alert-danger { + border: 0; + } +} + +.widgetLoadingError, .dimensionLoadingError { + color: @theme-color-brand; + text-align: left; + a { + color: @theme-color-brand; + text-decoration: underline; + } +} + +#closeMenuIcon { + float: right; + margin: 3px; + cursor: pointer; +} + +.menuClear { + clear: both; + height: 30px; +} + +.dashboardSettings > span { + background: url(plugins/Morpheus/images/sort_subtable_desc.png) right center no-repeat; + padding-right: 20px; + display: block; +} + +.dashboardSettings ul.submenu { + display: none; + float: left; + margin: 0; + padding: 0; + list-style: none; +} + +.dashboardSettings.expanded ul.submenu { + display: block; + list-style: none; + z-index: 1020; /* More than .jqplot-seriespicker-popover (1010) */ +} + +.dashboardSettings.expanded .dropdown { + min-width: 240px; + padding: 8px; +} + +.dashboardSettings ul.submenu > li { + clear: both; + list-style: none; +} + +#changeDashboardLayout h2 { + margin-bottom: 20px; +} + +#columnPreview { + clear: both; + width: 400px; + margin: auto; +} + +#columnPreview > div { + margin: 5px; + float: left; + opacity: 0.4; + cursor: pointer; + filter: Alpha(opacity=40); +} + +#columnPreview > div:hover, #columnPreview > div.choosen { + opacity: 1; + filter: Alpha(opacity=100); +} + +#columnPreview div div { + height: 120px; + float: left; +} + +#columnPreview div div span { + background-color: #ddd; + width: 100%; + height: 100%; + display: block; + border: 2px dotted #555; + margin: 0 1px; +} + +#columnPreview div.choosen div span, #columnPreview div:hover div span { + border-style: solid; +} + +#columnPreview .width-100 { + width: 120px; +} +#columnPreview .width-75 { + width: 90px; +} +#columnPreview .width-67 { + width: 80.4px; +} +#columnPreview .width-50 { + width: 60px; +} +#columnPreview .width-40 { + width: 48px; +} +#columnPreview .width-33 { + width: 40px; +} +#columnPreview .width-30 { + width: 36px; +} +#columnPreview .width-25 { + width: 30px; +} + +/** + * Layout for widget previews + */ + +.widgetpreview-base { + clear: both; +} + +ul.widgetpreview-widgetlist, +ul.widgetpreview-categorylist { + color: @theme-color-text-light; + list-style: none; + font-size: 11px; + line-height: 20px; + float: left; + margin-right: 20px; +} + +ul.widgetpreview-categorylist { + cursor: pointer; +} + +ul.widgetpreview-categorylist li, +ul.widgetpreview-widgetlist li { + line-height: 20px; + padding: 0 25px 0 5px; + border-radius: 2px; +} + +.widgetpreview-categorylist li.widgetpreview-choosen { + color: @theme-color-text-contrast; +} + +.widgetpreview-base li.widgetpreview-unavailable { + color: @theme-color-text-disabled; + cursor: default; +} + +ul.widgetpreview-widgetlist { + cursor: pointer; + position: relative; + top: 0; +} + +div.widgetpreview-preview { + width: 500px; + float: left; +} + +.dashboardSettings .submenu button { + display: block; + width: 100%; + box-sizing: border-box; + padding: 8px; + margin: 0; + background: transparent; + border: 0; + border-radius: 8px; + font-family: inherit; + font-size: 14px; + line-height: 20px; + font-weight: normal; + color: @theme-color-text-lighter; + text-align: left; + cursor: pointer; + text-decoration: none; + appearance: none; + white-space: nowrap; +} + +.dashboardSettings .submenu button:focus { + outline: none; +} + +.dashboardSettings .submenu button:focus-visible { + outline: 2px solid @theme-color-focus-ring; + outline-offset: -2px; +} + +.dashboardSettings .submenu button:not([disabled]):hover, +.dashboardSettings .submenu button:not([disabled]):focus-visible { + background: @theme-color-background-base; + color: @theme-color-text; +} + +.dashboardSettings .submenu button[disabled], +.dashboardSettings .submenu button[disabled]:hover { + color: @theme-color-text-disabled; + cursor: default; +} + +.dashboardSettings .submenu .addWidget-button .icon { + margin-right: 8px; + font-size: inherit; + line-height: inherit; +} + +.widgetPlaceholder { + border: 1px dashed #bbb6ad; +} + +#newDashboardName, #createDashboardName { + width: 200px; +} + +#newDashboardNameInput, #createDashboardNameInput { + margin: 20px 0 0 100px; + text-align: left; +} + +#createDashboardNameInput input { + margin-bottom: 10px; +} + +.popoverSubMessage { + text-align: center; + padding: 10px 0 5px 0; +} + +#copyDashboardToUserConfirm .inputs { + width: 400px; + margin: 32px auto 0; + + label { + margin-top: 18px; + display: inline-block; + } +} + + +#dashboardWidgetsArea { + margin-top: -5px; +} + +@media all and (max-width: 749px) { + #dashboardWidgetsArea { + padding-right: 7px; + } + + .col.width-75, + .col.width-67, + .col.width-50, + .col.width-40, + .col.width-33, + .col.width-30, + .col.width-25 { + width: 100%; + .widget { + margin-right: 0; + } + } + +} + +.ui-dialog .widget { + .button#close,.button#maximise { + display:none; + } +} + +.ui-button, .ui-dialog-titlebar-close { + .btn; + color: @theme-color-brand-contrast !important; + background-color: @color-red-matomo !important; + &:hover { + background: none; + background-color: transparent; + } +} + +[vue-entry="Dashboard.DashboardSettings"] { + // Display its children (.piwikSelector) as direct children of .top_controls + display: contents; +} diff --git a/plugins/Dashboard/stylesheets/standalone.css b/plugins/Dashboard/stylesheets/standalone.css new file mode 100644 index 0000000000000000000000000000000000000000..149521905831fd9509f34948db25980c449cb14f --- /dev/null +++ b/plugins/Dashboard/stylesheets/standalone.css @@ -0,0 +1,63 @@ + +body { + padding-left: 7px; +} + +#standalone strong { + font-weight: 700; +} + +#dashboard { + margin: 30px -6px 0 -12px; + width: 100%; + padding-top: 8px; +} + +#Dashboard { + z-index: 5; + font-size: 14px; + cursor: pointer; + float: left; + position: relative; +} + +#Dashboard > ul { + list-style: square inside none; + background: #f7f7f7; + border: 1px solid #e4e5e4; + padding: 5px 10px 6px 10px; + border-radius: 4px; + color: #444; + height: 18px; +} + +#Dashboard:hover ul { + background: #f1f0eb; + border-color: #a9a399; +} + +#Dashboard > ul > li { + list-style: square inside none; + float: left; + margin-right: 15px; +} + +#Dashboard a { + color: #444; + text-decoration: none; + font-weight: normal; + display: inline-block; +} + +#Dashboard > ul > li:hover, +#Dashboard > ul > li:hover a { + color: #e87500; +} + +.jqplot-seriespicker-popover { + top: 0; +} + +#ajaxLoading { + margin: 40px 0 -30px 0; +} diff --git a/plugins/Dashboard/stylesheets/widget.less b/plugins/Dashboard/stylesheets/widget.less new file mode 100644 index 0000000000000000000000000000000000000000..517292caa21d82c8186cd61ba6661441858a5d6a --- /dev/null +++ b/plugins/Dashboard/stylesheets/widget.less @@ -0,0 +1,161 @@ +.widget { + .font-default(13px, 18px); + background: @theme-color-widget-background; + border-radius: 8px; + position: relative; + border: 1px solid @theme-color-background-lowContrast !important; + + h2 { + font-weight: normal; + border-bottom: none; + margin: 0; + padding: 12px 10px 7px; + } + h3 { + font-weight: normal; + font-size: 15px; + margin: 0; + color: @theme-color-text; + text-shadow: none; + padding: 22px 15px 16px 12px !important; + } + p { + margin-left: 10px; + } + + &.default { + margin-left: 0; + margin-right: 0; + .widgetTop { + cursor: default !important; + } + } + + .widgetTop { + cursor: move; + font-size: 10pt; + font-weight: normal; + border-radius: 8px; + background: @theme-color-widget-title-background; + h3 { + .font-default(20px, 24px); + color: @theme-color-widget-title-text; + padding: 20px !important; + } + .button { + margin: 16px 8px 0 0; + opacity: 0.8; + cursor: pointer; + float: right; + } + .buttons { + right: 8px; + float: right; + position: absolute; + padding-left: 50px; + display: none; + top: 7px; + color: @theme-color-widget-title-text; + background: -moz-linear-gradient(left, rgba(255,255,255,0) 0%, @theme-color-widget-title-background 45px); + background: -webkit-linear-gradient(left,rgba(255,255,255,0) 0%, @theme-color-widget-title-background 45px); + background: linear-gradient(to right, rgba(255,255,255,0) 0%, @theme-color-widget-title-background 45px); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='@{theme-color-widget-title-background}',GradientType=1 ); + } + + &:hover .buttons { + display:block; + } + } + + .widgetText { + padding: 10px; + } + + .pk-emptyDataTable { + .font-default(13px, 16px); + text-transform: none; + } + + .widgetContent { + .widgetBody { + padding: 5px 12px 0; + } + + /** We do not want to show a material-card in a widget which is already a card */ + .card { + box-shadow: 0 0; + padding: 0 10px; + border: none; + .card-content { + padding: 0; + } + } + .jqplot-graph { + margin-top: 6px; + } + .sparkline { + padding: 0 10px; + } + } + + .widgetContent.hidden { + position: absolute; + top: -5000px; + height: 1000px; + overflow: hidden; + } + .widgetContent.loading { + opacity: 0.5; + background: url(plugins/Morpheus/images/loading-blue.gif) no-repeat top right; + } + + .widgetLoading { + cursor: wait; + text-align: center; + } + + .widgetNameOffScreen { + overflow: hidden; + width:1px; + height:1px; + } + + .card { + margin: 0; + border-radius: 8px; + } + + .datatableHeaderMessage, .datatableFooterMessage { + padding-left: 12px; + } +} + +.widget.hiddenContent .widgetTop.widgetTopHover { + .button#minimise,.button#refresh { + display:none; + } +} + +.widget table.dataTable tr td { + background-color: @theme-color-widget-background; +} + +.dataTable table.dataTable tr td { + background-color: @theme-color-widget-background; +} + +.bar-graph-colors[data-name=grid-background] { + color: @theme-color-widget-background !important; +} + +.widgetoverlay { + .widget, .ui-dialog-content { + position: static; + } +} + +.ui-dialog .widget .widgetTop .buttons { + right: 30px; + top: 2px; + display: block; +} diff --git a/plugins/Dashboard/templates/_header.twig b/plugins/Dashboard/templates/_header.twig new file mode 100644 index 0000000000000000000000000000000000000000..c6bfc81f022d06f7fdf1a25b7942b0645ac178ab --- /dev/null +++ b/plugins/Dashboard/templates/_header.twig @@ -0,0 +1,14 @@ +{# This header is for loading the dashboard in stand alone mode #} + + + + + + + + {{ 'Dashboard_Dashboard'|translate }} - {{ 'CoreHome_WebAnalyticsReports'|translate }} + +{% include "_jsGlobalVariables.twig" %} +{% include "_jsCssIncludes.twig" %} + + diff --git a/plugins/Dashboard/templates/_widgetFactoryTemplate.twig b/plugins/Dashboard/templates/_widgetFactoryTemplate.twig new file mode 100644 index 0000000000000000000000000000000000000000..4c24631dcc36570a3b80457e4411f00b5354018b --- /dev/null +++ b/plugins/Dashboard/templates/_widgetFactoryTemplate.twig @@ -0,0 +1,28 @@ + diff --git a/plugins/Dashboard/templates/embeddedIndex.twig b/plugins/Dashboard/templates/embeddedIndex.twig new file mode 100644 index 0000000000000000000000000000000000000000..d32d970f4ae481e4d209ab890b9f19a8b53339a7 --- /dev/null +++ b/plugins/Dashboard/templates/embeddedIndex.twig @@ -0,0 +1,113 @@ +
    +
    +

    {{ 'Dashboard_DeleteWidgetConfirm'|translate }}

    + + +
    + +
    +

    {{ 'Dashboard_SetAsDefaultWidgetsConfirm'|translate }}

    + {% set resetDashboard %}{{ 'Dashboard_ResetDashboard'|translate }}{% endset %} +
    {{ 'Dashboard_SetAsDefaultWidgetsConfirmHelp'|translate(resetDashboard) }}
    + + +
    + +
    +

    {{ 'Dashboard_ResetDashboardConfirm'|translate }}

    + + +
    + +
    +

    {{ 'Dashboard_DashboardEmptyNotification'|translate }}

    + + +
    + +
    +

    {{ 'Dashboard_SelectDashboardLayout'|translate }}

    + +
    + {% for layout in availableLayouts %} +
    + {% for column in layout %} +
    + {% endfor %} +
    + {% endfor %} +
    +
    + + +
    + +
    +

    {{ 'Dashboard_RenameDashboard'|translate }}

    + +
    + + +
    + + +
    + + {% if hasSomeAdminAccess %} +
    +

    {{ 'Dashboard_CopyDashboardToUser'|translate }}

    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    + {% endif %} + +
    +

    {{ 'Dashboard_CreateNewDashboard'|translate }}

    + +
    +

    + + +

    +

    + +

    +

    + +

    +
    + + +
    + +
    +

    {{ 'Dashboard_RemoveDashboardConfirm'|translate('')|raw }}

    + +
    {{ 'Dashboard_NotUndo'|translate(resetDashboard) }}
    + + +
    + + {% include "@Dashboard/_widgetFactoryTemplate.twig" %} + +
    +
    diff --git a/plugins/Dashboard/templates/index.twig b/plugins/Dashboard/templates/index.twig new file mode 100644 index 0000000000000000000000000000000000000000..bbaaa1e589c0dce24cb9ef537d278b90f4185a2e --- /dev/null +++ b/plugins/Dashboard/templates/index.twig @@ -0,0 +1,23 @@ +{% include "@Dashboard/_header.twig" %} +
    + {% include "@CoreHome/_periodSelect.twig" %} + {{ postEvent("Template.nextToCalendar") }} +
    + {% if dashboards|length %} +
    + +
    + {% endif %} +
    +{% import 'ajaxMacros.twig' as ajax %} +{{ ajax.loadingDiv }} +{% include "@Dashboard/embeddedIndex.twig" %} + + \ No newline at end of file diff --git a/plugins/Dashboard/tests/Integration/APITest.php b/plugins/Dashboard/tests/Integration/APITest.php new file mode 100644 index 0000000000000000000000000000000000000000..5b7ff50cfe6b9b40a6bc5ac6babee93782e45723 --- /dev/null +++ b/plugins/Dashboard/tests/Integration/APITest.php @@ -0,0 +1,327 @@ +addUser('eva', '12345.3m2', 'eva@example.com'); + + $this->model = new Model(); + $this->api = API::getInstance(); + } + + public function testGetDashboardsNoParamsNoDashboardShouldReturnDefault() + { + $result = $this->api->getDashboards(); + $this->assertCount(1, $result); + } + + public function testGetDashboardsNoDefaultShouldReturnEmpty() + { + $result = $this->api->getDashboards('', false); + $this->assertCount(0, $result); + } + + public function testGetDashboardsShouldReturnOwnDashboardsForSuperUser() + { + $layout = '[[{"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}}]]'; + $this->model->createNewDashboardForUser('eva', 'any name', $layout); + + FakeAccess::$superUser = true; + FakeAccess::$identity = 'eva'; + + $result = $this->api->getDashboards('eva', false); + + $expected = [ + 'name' => 'any name', + 'id' => 1, + 'widgets' => [ + ['module' => 'Live', 'action' => 'widget'], + ], + ]; + + $this->assertCount(1, $result); + $this->assertEquals([$expected], $result); + } + + public function testGetDashboardsShouldReturnForeignDashboardsForSuperUser() + { + $layout = '[[{"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}}]]'; + $this->model->createNewDashboardForUser('peter', 'any name', $layout); + + FakeAccess::$superUser = true; + FakeAccess::$identity = 'eva'; + + $result = $this->api->getDashboards('peter', false); + $this->assertCount(1, $result); + } + + + public function testGetDashboardsShouldReturnOwnDashboardsForUser() + { + $layout = '[[{"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}}]]'; + $this->model->createNewDashboardForUser('eva', 'any name', $layout); + + FakeAccess::$superUser = false; + FakeAccess::$identity = 'eva'; + + $result = $this->api->getDashboards('eva', false); + $this->assertCount(1, $result); + } + + public function testGetDashboardsShouldNotReturnForeignDashboardsForNonSuperUser() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('General_ExceptionCheckUserHasSuperUserAccessOrIsTheUser'); + + FakeAccess::$superUser = false; + FakeAccess::$identity = 'eva'; + + $this->api->getDashboards('peter', false); + } + + public function testCreateNewDashboardForOtherUserDoesNotWorkForNonSuperUser() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('General_ExceptionCheckUserHasSuperUserAccessOrIsTheUser'); + + FakeAccess::$superUser = false; + + $layout = '[[{"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}}]]'; + $this->api->createNewDashboardForUser('eva', 'name', $layout); + } + + public function testCreateNewDashboardForAnonymousDoesNotWork() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('General_YouMustBeLoggedIn'); + + FakeAccess::$superUser = false; + FakeAccess::$identity = 'anonymous'; + + $layout = '[[{"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}}]]'; + $this->api->createNewDashboardForUser('anonymous', 'name', $layout); + } + + public function testCreateNewDashboardForAnonymousDoesNotWorkForSuperUser() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('This method can\'t be performed for anonymous user'); + + FakeAccess::$superUser = true; + FakeAccess::$identity = 'eva'; + + $layout = '[[{"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}}]]'; + $this->api->createNewDashboardForUser('anonymous', 'name', $layout); + } + + public function testCreateNewDashboardForUserHimself() + { + FakeAccess::$superUser = false; + FakeAccess::$identity = 'eva'; + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertEmpty($dashboards); + + $this->api->createNewDashboardForUser('eva', 'name', $addDefaultWidgets = true); + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertCount(1, $dashboards); + } + + public function testCreateNewDashboardForOtherUser() + { + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertEmpty($dashboards); + $this->api->createNewDashboardForUser('eva', 'name', $addDefaultWidgets = true); + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertCount(1, $dashboards); + } + + public function testCopyDashboardToUser() + { + $layout = '[[{"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}}]]'; + $id = $this->model->createNewDashboardForUser('superUserLogin', 'any name', $layout); + + $this->assertNotEmpty($id); + + $newId = $this->api->copyDashboardToUser($id, 'eva', 'new name'); + $dashboards = $this->model->getAllDashboardsForUser('eva'); + + $this->assertCount(1, $dashboards); + $dashboard = end($dashboards); + $this->assertEquals($dashboard['iddashboard'], $newId); + $this->assertEquals($dashboard['name'], 'new name'); + $this->assertEquals($dashboard['layout'], $layout); + } + + public function testCopyDashboardToUserFails() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Dashboard not found'); + + $this->api->copyDashboardToUser(5, 'eva', 'new name'); + } + + public function testRemoveDashboardForAnonymousDoesntWork() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('General_YouMustBeLoggedIn'); + + FakeAccess::$superUser = false; + FakeAccess::$identity = 'anonymous'; + + $this->api->removeDashboard(1, 'anonymous'); + } + + public function testRemoveDashboardForAnonymousDoesntWorkForSuperUser() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('This method can\'t be performed for anonymous user'); + + FakeAccess::$superUser = true; + FakeAccess::$identity = 'eva'; + + $this->api->removeDashboard(1, 'anonymous'); + } + + public function testRemoveDashboardForUserHimself() + { + FakeAccess::$superUser = false; + FakeAccess::$identity = 'eva'; + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertEmpty($dashboards); + + // first dashboard shouldn't be removed + $this->api->createNewDashboardForUser('eva', 'name', $addDefaultWidgets = true); + $id = $this->api->createNewDashboardForUser('eva', 'new name', $addDefaultWidgets = true); + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertCount(2, $dashboards); + + $this->api->removeDashboard($id); + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertCount(1, $dashboards); + } + + public function testRemoveDashboardForOtherUser() + { + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertEmpty($dashboards); + + // first dashboard shouldn't be removed + $this->api->createNewDashboardForUser('eva', 'name', $addDefaultWidgets = true); + $id = $this->api->createNewDashboardForUser('eva', 'new name', $addDefaultWidgets = true); + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertCount(2, $dashboards); + + $this->api->removeDashboard($id, 'eva'); + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertCount(1, $dashboards); + } + + public function testRemoveDashboardAllowsRemovingFirst() + { + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertEmpty($dashboards); + + // we allow removing first dashboard for an automation use case, so tested here + // but if another dashboard isn't immediately added, it can cause problems. + $id = $this->api->createNewDashboardForUser('eva', 'name', $addDefaultWidgets = true); + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertCount(1, $dashboards); + + $this->api->removeDashboard($id, 'eva'); + + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertEmpty($dashboards); + } + + public function testResetDashboardForAnonymousDoesntWork() + { + FakeAccess::$superUser = false; + FakeAccess::$identity = 'anonymous'; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('General_YouMustBeLoggedIn'); + + $this->api->resetDashboardLayout(1, 'anonymous'); + } + + public function testResetDashboardForAnonymousDoesntWorkForSuperUser() + { + FakeAccess::$superUser = true; + FakeAccess::$identity = 'eva'; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('This method can\'t be performed for anonymous user'); + + $this->api->resetDashboardLayout(1, 'anonymous'); + } + + public function testResetDashboard() + { + $db = new Dashboard(); + $dashboards = $this->model->getAllDashboardsForUser('eva'); + $this->assertEmpty($dashboards); + + $id = $this->api->createNewDashboardForUser('eva', 'name', false); + + $dashboard = $db->getLayoutForUser('eva', $id); + $this->assertEquals('{}', $dashboard); + + $this->api->resetDashboardLayout($id, 'eva'); + + $dashboard = $db->getLayoutForUser('eva', $id); + $this->assertEquals($db->getDefaultLayout(), $dashboard); + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess(), + ); + } +} diff --git a/plugins/Dashboard/tests/UI/DashboardManager_spec.js b/plugins/Dashboard/tests/UI/DashboardManager_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..1a5597a8f239e4f4ca66acfe83ae7546a45b03ca --- /dev/null +++ b/plugins/Dashboard/tests/UI/DashboardManager_spec.js @@ -0,0 +1,159 @@ +/*! + * Matomo - free/libre analytics platform + * + * Dashboard manager screenshot tests. + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe("DashboardManager", function () { + const managerSelector = '.dashboard-manager,.dashboard-manager .dropdown'; + const modalSelector = '.modal.open.add-widget-modal'; + + const generalParams = 'idSite=1&period=day&date=2012-01-01'; + const url = '?module=CoreHome&action=index&' + generalParams + '#?' + generalParams + '&category=Dashboard_Dashboard&subcategory=1'; + + it("should load correctly", async function() { + await page.goto(url); + await page.waitForNetworkIdle(); + + expect(await page.screenshotSelector(managerSelector)).to.matchImage('loaded'); + }); + + it("should expand when clicked", async function() { + await page.click('.dashboard-manager .title'); + + expect(await page.screenshotSelector(managerSelector)).to.matchImage('expanded'); + }); + + it("should show widget for a category when category label hovered", async function() { + await page.click('.dashboard-manager .addWidget-button'); + await page.waitForSelector(modalSelector); + await page.waitForSelector(modalSelector + ' .widgetpreview-categorylist>li button'); + + visitors = await page.jQuery(modalSelector + ' .widgetpreview-categorylist>li button:contains(Visitors):first'); + await visitors.hover(); + await visitors.click(); + + await page.waitForNetworkIdle(); + await page.waitForSelector(modalSelector + ' .widgetpreview-widgetlist>li button', { visible: true }); + + expect(await page.screenshotSelector(modalSelector, false)).to.matchImage('widget_list_shown'); + }); + + it("should load a widget preview when a widget is hovered", async function() { + await page.waitForSelector(modalSelector + ' .widgetpreview-widgetlist>li button', { visible: true }); + + vot = await page.jQuery(modalSelector + ' .widgetpreview-widgetlist>li button:contains(Visits Over Time)'); + await vot.hover(); + + await page.waitForNetworkIdle(); + await page.waitForSelector(modalSelector + ' .widgetpreview-widgetlist>li.widgetpreview-choosen > button > .widgetpreview-add-hint', { visible: true }); + + expect(await page.screenshotSelector(modalSelector)).to.matchImage('widget_preview'); + }); + + it("should add the widget to the dashboard and keep the modal open when a widget is selected", async function() { + const widgetsCountBefore = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + + vot = await page.jQuery(modalSelector + ' .widgetpreview-widgetlist>li button:contains(Visits Over Time)'); + await vot.click(); + + await page.waitForNetworkIdle(); + await page.waitForFunction( + (before) => $('#dashboardWidgetsArea .widget').length > before, + {}, + widgetsCountBefore, + ); + + const widgetsCountAfter = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCountAfter).to.equal(widgetsCountBefore + 1); + + const modalStillOpen = await page.evaluate(() => !!document.querySelector('.modal.open.add-widget-modal')); + expect(modalStillOpen).to.equal(true); + + // close manually so the next test starts clean + await page.click(modalSelector + ' .btn-close'); + await page.waitForFunction(() => !document.querySelector('.modal.open.add-widget-modal')); + }); + + it("should create new dashboard with new default widget selection when create dashboard process completed", async function() { + // check that widget count on currently loaded dashboard is correct, so we are able to say if + // number of widgets has changed after creating and loading the new one + const widgetsCountBefore = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCountBefore).to.equal(2); + + await page.click('.dashboard-manager .title'); + await page.click('button[data-action="createDashboard"]'); + await page.waitForSelector('#createDashboardName', { visible: true }); + + // try to type the text a few times, as it sometimes doesn't get the full value + var name = 'newdash2'; + for (var i=0; i<5; i++) { + await page.evaluate(function() { + $('#createDashboardName').val(''); + }); + await page.type('#createDashboardName', name); + await page.waitForTimeout(500); // sometimes the text doesn't seem to type fast enough + + var value = await page.evaluate(function() { + return $('#createDashboardName').attr('value'); + }); + + if (value === name) { + break; + } + } + + button = await page.jQuery('.modal.open .modal-footer a:contains(Ok)'); + // ensure tour widget always shows the same state + testEnvironment.completeAllChallenges = 1; + testEnvironment.save(); + + await button.click(); + + await page.mouse.move(-10, -10); + await page.waitForNetworkIdle(); + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + await page.waitForTimeout(100); // wait for widgets to render fully + + // check new dashboard was loaded, which should have 10 widgets + const widgetsCount = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCount).to.equal(10); + + // check dashboard has been added to menu, causing menu to switch from listing to selector + expect(await page.screenshotSelector('#secondNavBar .menuTab.active')).to.matchImage('create_new'); + }); + + + it("should load widgets on smaller screen", async function(){ + await page.webpage.setViewport({ width: 815, height: 512 }); + await page.waitForTimeout(500); + expect(await page.screenshotSelector('.top_controls, #dashboard')).to.matchImage('small_screen'); + }); + + it("should remove dashboard when remove dashboard process completed", async function() { + await page.click('.dashboard-manager .title'); + await page.click('button[data-action="removeDashboard"]'); + button = await page.jQuery('.modal.open .modal-footer a:contains(Yes)'); + await button.click(); + + await page.mouse.move(-10, -10); + await page.waitForTimeout(500); + await page.waitForNetworkIdle(); + + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + + // check initial dashboard was loaded again, which should have 2 widgets + // (1 default widget plus the one added in the "should add the widget…" test) + const widgetsCount = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + // should also count the widget we added in the previous test + expect(widgetsCount).to.equal(2); + + // check dashboard has been removed from menu, causing menu to switch from selector to listing + expect(await page.screenshotSelector('#secondNavBar .menuTab.active')).to.matchImage('removed'); + }); +}); diff --git a/plugins/Dashboard/tests/UI/Dashboard_spec.js b/plugins/Dashboard/tests/UI/Dashboard_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..01b1be159d2e1da5fa6e68cfefc9dd8ae8c3f0f4 --- /dev/null +++ b/plugins/Dashboard/tests/UI/Dashboard_spec.js @@ -0,0 +1,97 @@ +/*! + * Matomo - free/libre analytics platform + * + * Dashboard screenshot tests. + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +describe("Dashboard", function () { + + var generalParams = 'idSite=1&period=year&date=2012-08-09', + urlBase = 'module=CoreHome&action=index&' + generalParams; + + before(async function () { + testEnvironment.queryParamOverride = { + forceNowValue: testEnvironment.forcedNowTimestamp, + visitorId: testEnvironment.forcedIdVisitor, + realtimeWindow: 'false' + }; + testEnvironment.completeNoChallenge = true; + testEnvironment.pluginsToLoad = ['CustomDirPlugin']; + testEnvironment.save(); + }); + + after(function () { + delete testEnvironment.queryParamOverride; + delete testEnvironment.completeNoChallenge; + }); + + it("should load dashboard2 correctly", async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=Dashboard_Dashboard&subcategory=2"); + await page.waitForNetworkIdle(); + await page.evaluate(function () { + // Prevent random sizing error eg. http://builds-artifacts.matomo.org/ui-tests.master/2301.1/screenshot-diffs/diffviewer.html + $("[widgetid=widgetActionsgetOutlinks] .widgetContent").text('Displays different at random -> hidden'); + }); + + pageWrap = await page.$('.pageWrap'); + expect(await pageWrap.screenshot()).to.matchImage('dashboard2'); + }); + + it("should load dashboard3 correctly", async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=Dashboard_Dashboard&subcategory=3"); + await page.waitForNetworkIdle(); + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + + pageWrap = await page.$('.pageWrap'); + expect(await pageWrap.screenshot()).to.matchImage('dashboard3'); + }); + + it("should load dashboard4 correctly", async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=Dashboard_Dashboard&subcategory=4"); + await page.waitForNetworkIdle(); + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + await page.evaluate(() => { // give table headers constant width so the screenshot stays the same + $('.dataTableScroller').css('overflow-x', 'scroll'); + $('[widgetid="widgetRssWidgetrssChangelog"] .widgetContent').html( + '
    ' + ); + }); + await page.waitForTimeout(100); + pageWrap = await page.$('.pageWrap'); + expect(await pageWrap.screenshot()).to.matchImage('dashboard4'); + }); + + it("should load dashboard5 correctly", async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=Dashboard_Dashboard&subcategory=5"); + await page.waitForNetworkIdle(); + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + + pageWrap = await page.$('.pageWrap'); + // A report label (e.g. "Provence-Alpes-Côte-d'Azur, France" in the Region widget) sits right on the + // CSS text-overflow:ellipsis truncation boundary. Headless Chrome's sub-pixel text layout is not fully + // deterministic, so it occasionally truncates one character earlier ("Fr…" vs "Fra…"). Allow a tiny + // difference so this purely cosmetic variance doesn't make the test flaky. + expect(await pageWrap.screenshot()).to.matchImage({ + imageName: 'dashboard5', + comparisonThreshold: 0.001 + }); + }); + + it("should display dashboard correctly on a mobile phone", async function () { + await page.webpage.setViewport({ + width: 480, + height: 320 + }); + await page.goto("?" + urlBase + "#?" + generalParams + "&category=Dashboard_Dashboard&subcategory=1"); + await page.waitForNetworkIdle(); + + expect(await page.screenshotSelector('.top_controls, #dashboard')).to.matchImage('dashboard1_mobile'); + }); +}); diff --git a/plugins/Dashboard/tests/UI/WidgetizedDashboard_spec.js b/plugins/Dashboard/tests/UI/WidgetizedDashboard_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b32bba563f52ef14e99e8dad23e1f2ea8979f4f4 --- /dev/null +++ b/plugins/Dashboard/tests/UI/WidgetizedDashboard_spec.js @@ -0,0 +1,378 @@ +/*! + * Matomo - free/libre analytics platform + * + * Dashboard screenshot tests. + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe("WidgetizedDashboard", function () { + var url = "?module=Widgetize&action=iframe&idSite=1&period=year&date=2012-08-09&moduleToWidgetize=Dashboard&" + + "actionToWidgetize=index&idDashboard=1"; + + var removeAllExtraDashboards = async function() { + var dashboards = await testEnvironment.callController("Dashboard.getAllDashboards", {}); + dashboards = (dashboards || []).filter(function (dash) { + return parseInt(dash.iddashboard) > 5; + }); + + var removeDashboard = async function (i) { + if (i >= dashboards.length) { + return; + } + + console.log("Removing dashboard ID = " + dashboards[i].iddashboard); + await testEnvironment.callApi("Dashboard.removeDashboard", {idDashboard: dashboards[i].iddashboard}); + await removeDashboard(i + 1); + }; + + await removeDashboard(0); + }; + + var clickDashboardMenuItem = async function (item) { + await page.waitForSelector('.dashboard-manager .title', { visible: true }); + await page.click('.dashboard-manager .title'); + await page.waitForSelector('button[data-action="' + item + '"]', { visible: true }); + await page.click('button[data-action="' + item + '"]'); + } + + var setup = async function() { + // make sure live widget doesn't refresh constantly for UI tests + testEnvironment.overrideConfig('General', 'live_widget_refresh_after_seconds', 1000000); + // ensure tour widget always shows the same state + testEnvironment.completeAllChallenges = 1; + testEnvironment.testUseMockAuth = 1; + testEnvironment.save(); + + // save empty layout for dashboard ID = 1 + var layout = [ + [ + { + uniqueId: "widgetVisitsSummarygetEvolutionGraphforceView1viewDataTablegraphEvolution", + parameters: {module: "VisitsSummary", action: "getEvolutionGraph", columns: "nb_visits"} + } + ], + [], + [] + ]; + + await testEnvironment.callController("Dashboard.saveLayout", {name: 'D4', layout: JSON.stringify(layout), idDashboard: 1, idSite: 2}); + await testEnvironment.callController("Dashboard.saveLayoutAsDefault", {layout: 0}); + await removeAllExtraDashboards(); + }; + + before(setup); + after(setup); + + it("should load correctly", async function() { + await page.goto(url); + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('loaded'); + }); + + it("should move a widget when widget is drag & dropped", async function() { + var widget = await page.$('.widgetTop'); + await widget.hover(); + await page.mouse.down(); + + var col2 = await page.jQuery('#dashboardWidgetsArea > .col:eq(2)'); + await col2.hover(); + await page.mouse.up(); + await page.waitForNetworkIdle(); + await page.waitForTimeout(100); + await page.mouse.move(-10, -10); + + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('widget_move'); + }); + + it("should refresh widget when widget refresh icon clicked", async function() { + var widget = await page.$('.widgetTop'); + await widget.hover(); + + await page.click('.button#refresh'); + await page.mouse.move(-10, -10); + + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('widget_refresh'); + }); + + it("should minimise widget when widget minimise icon clicked", async function() { + var widget = await page.$('.widgetTop'); + await widget.hover(); + await page.click('.button#minimise'); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('widget_minimised'); + }); + + it("should unminimise widget when widget maximise icon is clicked after being minimised", async function() { + var widget = await page.$('.widgetTop'); + await widget.hover(); + await page.click('.button#maximise'); + await page.mouse.move(-10, -10); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('widget_unminimise'); + }); + + it("should maximise widget when widget maximise icon is clicked", async function() { + var widget = await page.$('.widgetTop'); + await widget.hover(); + await page.click('.button#maximise'); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('widget_maximise'); + }); + + it("should close maximise dialog when minimise icon is clicked", async function() { + var widget = await page.$('.widgetTop'); + await widget.hover(); + await page.click('.button#minimise'); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('widget_unmaximise'); + }); + + it("should add a widget when a widget is selected in the dashboard manager", async function() { + const modalSelector = '.modal.open.add-widget-modal'; + + await page.click('.dashboard-manager .title'); + await page.waitForTimeout(50); + await page.click('.dashboard-manager .addWidget-button'); + await page.waitForSelector(modalSelector); + await page.waitForSelector(modalSelector + ' .widgetpreview-categorylist>li button'); + + var behaviour = await page.jQuery(modalSelector + ' .widgetpreview-categorylist>li button:contains(Behaviour):first'); + await behaviour.hover(); + await behaviour.click(); + + var pages = await page.jQuery(modalSelector + ' .widgetpreview-widgetlist>li button:contains(Pages):first'); + await pages.hover(); + // Wait for the hover timer in WidgetsList.vue to fire and mark the row as the + // chosen widget — that's the signal the next click will select rather than just + // preview. Reading the rendered state avoids hard-coding the timer duration. + await page.waitForSelector(modalSelector + ' .widgetpreview-widgetlist>li.widgetpreview-choosen'); + await pages.click(); + + await page.waitForNetworkIdle(); + + // The modal now stays open after a widget is selected so the user can add more + // widgets in the same session; close it before screenshotting the dashboard. + await page.click(modalSelector + ' .btn-close'); + await page.waitForFunction(() => !document.querySelector('.modal.open.add-widget-modal')); + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('widget_add_widget'); + }); + + it("should open row evolution", async function() { + var row = await page.jQuery('#dashboardWidgetsArea .dataTable tbody td:contains(thankyou)'); + await row.hover(); + var icon = await page.waitForSelector('#dashboardWidgetsArea .dataTable tbody a.actionRowEvolution'); + await icon.click(); + await page.waitForSelector('.ui-dialog'); + await page.waitForNetworkIdle(); + var dialog = await page.$('.ui-dialog'); + expect(await dialog.screenshot()).to.matchImage('rowevolution'); + }); + + it("should remove widget when remove widget icon is clicked", async function() { + await page.click('.ui-dialog-titlebar-close'); // close row evolution + + var widget = '#dashboardWidgetsArea [id="widgetActionsgetPageUrls"]'; + + var titlebar = await page.$(widget + ' .widgetTop'); + await titlebar.hover(); + + var icon = await page.$(widget + ' .button#close'); + await icon.click(); + + var button = await page.jQuery('.modal.open .modal-footer a:contains(Yes)'); + await button.click(); + + // wait until widget was removed and modal closed + await page.waitForFunction((widget) => $(widget).length === 0); + await page.waitForTimeout(250); + + // check that one widget remains + const widgetsCount = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCount).to.equal(1); + }); + + it("should change dashboard layout when new layout is selected", async function() { + await clickDashboardMenuItem('showChangeDashboardLayoutDialog'); + await (await page.waitForSelector('.modal.open div[layout="50-50"]')).click(); + var button = await page.jQuery('.modal.open .modal-footer a:contains(Save)'); + await button.click(); + await page.mouse.move(-10, -10); + await page.waitForTimeout(250); // animation + await page.evaluate(() => $('.evolution-annotations').css('display','none')); + expect(await page.screenshot({ fullPage: true })).to.matchImage('change_layout'); + }); + + it("should rename dashboard when dashboard rename process completed", async function() { + await clickDashboardMenuItem('renameDashboard'); + await page.waitForSelector('.modal.open'); + + await page.evaluate(() => $('#newDashboardName').val('newname')); + await page.waitForTimeout(250); + var button = await page.jQuery('.modal.open .modal-footer a:contains(Save)'); + await button.click(); + await page.waitForFunction(() => $('#Dashboard_embeddedIndex_1').text().trim() === 'newname'); + await page.waitForTimeout(100); // wait for javascript to finish everything + }); + + it("should copy dashboard successfully when copy dashboard process completed", async function() { + await clickDashboardMenuItem('copyDashboardToUser'); + await page.waitForSelector('.modal.open'); + + await page.evaluate(function () { + $('#copyDashboardName').val(''); + }); + await page.type('#copyDashboardName', 'new 💩'); + await page.waitForSelector('#copyDashboardUser [value="superUserLogin"]'); + await page.select('#copyDashboardUser', 'superUserLogin'); + var button = await page.jQuery('.modal.open .modal-footer a:contains(Ok)'); + await button.click(); + await page.waitForFunction("$('.ui-confirm :contains(\"Current dashboard successfully copied to selected user.\").length > 0')"); + + await page.goto(url.replace("idDashboard=1", "idDashboard=6")); + await page.waitForNetworkIdle(); + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + + const dashboardCount = await page.evaluate(() => $('#Dashboard ul li').length); + expect(dashboardCount).to.equal(6); + + const widgetsCount = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCount).to.equal(1); + + const dashboardName = await page.evaluate(() => $('#Dashboard_embeddedIndex_6').text().trim()); + expect(dashboardName).to.equal('new 💩'); + }); + + it("should reset dashboard when reset dashboard process completed", async function() { + await clickDashboardMenuItem('resetDashboard'); + await page.waitForSelector('.modal.open'); + var button = await page.jQuery('.modal.open .modal-footer a:contains(Yes)'); + await button.click(); + await page.waitForNetworkIdle(); + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + + // after resetting dashboard should have 10 widgets + const widgetsCount = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCount).to.equal(10); + }); + + it("should remove dashboard when remove dashboard process completed", async function() { + await clickDashboardMenuItem('removeDashboard'); + await page.waitForSelector('.modal.open'); + var button = await page.jQuery('.modal.open .modal-footer a:contains(Yes)'); + await button.click(); + await page.waitForTimeout(200); + await page.waitForNetworkIdle(); + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + + // dashboard should be removed from list, so 5 remaining dashboards + const dashboardCount = await page.evaluate(() => $('#Dashboard ul li').length); + expect(dashboardCount).to.equal(5); + const dashboardNames = await page.evaluate(() => $('#Dashboard ul').text().trim()); + expect(dashboardNames).to.not.contain('new 💩'); + + // first dashboard should be loaded, which contains 1 widget + const activeDashboardName = await page.evaluate(() => $('#Dashboard ul li.active').text().trim()); + expect(activeDashboardName).to.equal('newname'); + const widgetsCount = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCount).to.equal(1); + }); + + it("should not fail when default widget selection changed", async function() { + await page.goto(url); + await clickDashboardMenuItem('setAsDefaultWidgets'); + var button = await page.jQuery('.modal.open .modal-footer a:contains(Yes)'); + await button.click(); + await page.waitForTimeout(200); + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('default_widget_selection_changed'); + }); + + it("should create new dashboard with new default widget selection when create dashboard process completed", async function() { + await clickDashboardMenuItem('createDashboard'); + await page.waitForSelector('#createDashboardName', { visible: true }); + + // try to type the text a few times, as it sometimes doesn't get the full value + var name = 'newdash2'; + for (var i=0; i<5; i++) { + await page.evaluate(function() { + $('#createDashboardName').val(''); + }); + await page.type('#createDashboardName', name); + await page.waitForTimeout(500); // sometimes the text doesn't seem to type fast enough + + var value = await page.evaluate(function() { + return $('#createDashboardName').prop('value'); + }); + + if (value === name) { + break; + } + } + + var button = await page.jQuery('.modal.open .modal-footer a:contains(Ok)'); + await button.click(); + await page.waitForSelector('.widget'); + await page.waitForNetworkIdle(); + + // dashboard should be added to list, so 6 dashboards available + const dashboardCount = await page.evaluate(() => $('#Dashboard ul li').length); + expect(dashboardCount).to.equal(6); + const dashboardNames = await page.evaluate(() => $('#Dashboard ul').text().trim()); + expect(dashboardNames).to.contain(name); + + // new dashboard should be loaded, which contains 1 widget + const activeDashboardName = await page.evaluate(() => $('#Dashboard ul li.active').text().trim()); + expect(activeDashboardName).to.equal(name); + const widgetsCount = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCount).to.equal(1); + }); + + it("should load segmented dashboard", async function() { + await removeAllExtraDashboards(); + await page.goto(url + '&segment=' + encodeURIComponent("browserCode==FF")); + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('segmented'); + }); + + it("should load correctly with token_auth", async function() { + testEnvironment.testUseMockAuth = 0; + testEnvironment.save(); + + var tokenAuth = "a4ca4238a0b923820dcc509a6f75849f"; + await page.goto(url.replace("idDashboard=5", "idDashboard=1") + '&token_auth=' + tokenAuth); + + // list of dashboard should be hidden + expect(await page.$('#Dashboard')).to.be.not.ok; + + // should show one widget on dashboard + const widgetsCount = await page.evaluate(() => $('#dashboardWidgetsArea .widget').length); + expect(widgetsCount).to.equal(1); + }); + + it("should fail to load with invalid token_auth", async function() { + testEnvironment.testUseMockAuth = 0; + testEnvironment.save(); + + var tokenAuth = "anyInvalidToken"; + await page.goto(url.replace("idDashboard=5", "idDashboard=1") + '&token_auth=' + tokenAuth); + + // should show login page with error message + expect(await page.$('#loginPage')).to.be.ok; + const errorMessage = await page.evaluate(() => $('.message_container').text()); + expect(errorMessage).to.contain('You must be logged in to access this functionality.'); + }); +}); diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png new file mode 100644 index 0000000000000000000000000000000000000000..e3bb6780747513188ecb44a6cebf2a782a0e5036 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png new file mode 100644 index 0000000000000000000000000000000000000000..cf8105961de26c1d9707cd35ad16d7e8be344491 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_loaded.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_loaded.png new file mode 100644 index 0000000000000000000000000000000000000000..d48fdcbe44f4baeb741d1879d4bdde1f89ab0d8f Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_loaded.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png new file mode 100644 index 0000000000000000000000000000000000000000..26bbccbc856161cb5b844cae10c1f1e492581fd4 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_small_screen.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_small_screen.png new file mode 100644 index 0000000000000000000000000000000000000000..1db8bb9e324c20be023b40750b4923df9bb516e9 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_small_screen.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png new file mode 100644 index 0000000000000000000000000000000000000000..7e19bc9e8bf34a8b163b524ee1d182aa2bc1a091 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..4719fac7698023c36c91602ef189ab86338674b8 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_selected.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..e0cd664bee74117c61e1d9f776f645db30030b5f Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_selected.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard1_mobile.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard1_mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..09af3b96a2365daeb9e66bdb78769d87d9d1fa92 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard1_mobile.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard2.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard2.png new file mode 100644 index 0000000000000000000000000000000000000000..47f1481f7ba33fd534a9fae271608f74bfd84317 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard2.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard3.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard3.png new file mode 100644 index 0000000000000000000000000000000000000000..897aff325dee089b3af27d6c4dd04d1f05e51401 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard3.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard4.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard4.png new file mode 100644 index 0000000000000000000000000000000000000000..d14c0bc33a0f8498b57a95a7414a4edf64b0d3e3 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard4.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard5.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard5.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8e23ebc1f6c437b92e93f7aaa2e7a0dc4c44a3 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_dashboard5.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_change_layout.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_change_layout.png new file mode 100644 index 0000000000000000000000000000000000000000..2791757b1f8d5cd4a059c3955828d3b3599382b7 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_change_layout.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_default_widget_selection_changed.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_default_widget_selection_changed.png new file mode 100644 index 0000000000000000000000000000000000000000..2229092150758892d7da6663d287e9b5a4bf7bf8 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_default_widget_selection_changed.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_loaded.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_loaded.png new file mode 100644 index 0000000000000000000000000000000000000000..f03d1f908fdd26f5d62af7e13ba81c4d809ff8b5 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_loaded.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_rowevolution.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_rowevolution.png new file mode 100644 index 0000000000000000000000000000000000000000..860f360fbf443d803bf4cd909c6f109798f9536d Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_rowevolution.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_segmented.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_segmented.png new file mode 100644 index 0000000000000000000000000000000000000000..43e8307532db6e7fe028cf2dd8aee5079f23fd10 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_segmented.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_add_widget.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_add_widget.png new file mode 100644 index 0000000000000000000000000000000000000000..aba2c1c7912a72680a648b5591a6ef94e44608bf Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_add_widget.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_maximise.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_maximise.png new file mode 100644 index 0000000000000000000000000000000000000000..567633ae92ef9418151b8eb760fb3c7e518b784c Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_maximise.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_minimised.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_minimised.png new file mode 100644 index 0000000000000000000000000000000000000000..dd11f7c425a51d0d94065397f7bea631ca24402b Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_minimised.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_move.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_move.png new file mode 100644 index 0000000000000000000000000000000000000000..59a867d6f95f6609c333dfe3b1fee001946011f4 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_move.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_refresh.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..59a867d6f95f6609c333dfe3b1fee001946011f4 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_refresh.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_unmaximise.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_unmaximise.png new file mode 100644 index 0000000000000000000000000000000000000000..053ff922af39c5e219f0ef5a0dfd7288831f29ca Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_unmaximise.png differ diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_unminimise.png b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_unminimise.png new file mode 100644 index 0000000000000000000000000000000000000000..59a867d6f95f6609c333dfe3b1fee001946011f4 Binary files /dev/null and b/plugins/Dashboard/tests/UI/expected-screenshots/WidgetizedDashboard_widget_unminimise.png differ diff --git a/plugins/Dashboard/vue/dist/Dashboard.umd.js b/plugins/Dashboard/vue/dist/Dashboard.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..67353afa27c29b85760ba2887185b2e9bd34f005 --- /dev/null +++ b/plugins/Dashboard/vue/dist/Dashboard.umd.js @@ -0,0 +1,1164 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("vue")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", ], factory); + else if(typeof exports === 'object') + exports["Dashboard"] = factory(require("CoreHome"), require("vue")); + else + root["Dashboard"] = factory(root["CoreHome"], root["Vue"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/Dashboard/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "DashboardStore", function() { return /* reexport */ Dashboard_store; }); +__webpack_require__.d(__webpack_exports__, "Dashboard", function() { return /* reexport */ Dashboard; }); +__webpack_require__.d(__webpack_exports__, "DashboardSettings", function() { return /* reexport */ DashboardSettings; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/Dashboard/Dashboard.store.ts +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + +class Dashboard_store_DashboardStore { + constructor() { + _defineProperty(this, "privateState", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["reactive"])({ + dashboards: [] + })); + _defineProperty(this, "state", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(() => Object(external_commonjs_vue_commonjs2_vue_root_Vue_["readonly"])(this.privateState))); + _defineProperty(this, "dashboards", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(() => this.state.value.dashboards)); + _defineProperty(this, "dashboardsPromise", null); + } + getDashboard(dashboardId) { + return this.getAllDashboards().then(dashboards => dashboards.find(b => parseInt(`${b.id}`, 10) === parseInt(`${dashboardId}`, 10))); + } + getDashboardLayout(dashboardId) { + return external_CoreHome_["AjaxHelper"].fetch({ + module: 'Dashboard', + action: 'getDashboardLayout', + idDashboard: dashboardId + }, { + withTokenInUrl: true + }); + } + reloadAllDashboards() { + this.dashboardsPromise = null; + return this.getAllDashboards(); + } + getAllDashboards() { + if (!this.dashboardsPromise) { + this.dashboardsPromise = external_CoreHome_["AjaxHelper"].fetch({ + method: 'Dashboard.getDashboards', + filter_limit: '-1' + }).then(response => { + if (response) { + this.privateState.dashboards = response; + } + return this.dashboards.value; + }); + } + return this.dashboardsPromise; + } +} +/* harmony default export */ var Dashboard_store = (new Dashboard_store_DashboardStore()); +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/Dashboard/Dashboard.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + + +const { + $ +} = window; +function renderDashboard(dashboardId, dashboard, layout) { + const $settings = $('.dashboardSettings'); + $settings.show(); + window.initTopControls(); + // Embed dashboard / exported as widget + if (!$('#topBars').length) { + $settings.after($('#Dashboard')); + $('#Dashboard ul li').removeClass('active'); + $(`#Dashboard_embeddedIndex_${dashboardId}`).addClass('active'); + } + window.widgetsHelper.getAvailableWidgets(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $('#dashboardWidgetsArea').off('dashboardempty', window.showEmptyDashboardNotification).on('dashboardempty', window.showEmptyDashboardNotification).dashboard({ + idDashboard: dashboardId, + layout, + name: dashboard ? dashboard.name : '' + }); + const divElements = $('#columnPreview').find('>div'); + divElements.each(function eachPreview() { + const width = []; + $('div', this).each(function eachDiv() { + width.push(this.className.replace(/width-/, '')); + }); + $(this).attr('layout', width.join('-')); + }); + divElements.off('click.renderDashboard'); + divElements.on('click.renderDashboard', function onRenderDashboard() { + divElements.removeClass('choosen'); + $(this).addClass('choosen'); + }); +} +function fetchDashboard(dashboardId) { + return new Promise(resolve => setTimeout(resolve)).then(() => Promise.resolve(window.widgetsHelper.firstGetAvailableWidgetsCall)).then(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dashboardElement = $('#dashboardWidgetsArea'); + dashboardElement.dashboard('destroyWidgets'); + dashboardElement.empty(); + return Promise.all([Dashboard_store.getDashboard(dashboardId), Dashboard_store.getDashboardLayout(dashboardId)]); + }).then(([dashboard, layout]) => new Promise(resolve => { + $(() => { + renderDashboard(dashboardId, dashboard, layout); + resolve(); + }); + })); +} +function clearDashboard() { + $('.top_controls .dashboard-manager').hide(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $('#dashboardWidgetsArea').dashboard('destroy'); +} +function onLocationChange(parsed) { + if (parsed.module !== 'Widgetize' && parsed.category !== 'Dashboard_Dashboard') { + // we remove the dashboard only if we no longer show a dashboard. + clearDashboard(); + } +} +function onLoadDashboard(idDashboard) { + fetchDashboard(idDashboard); +} +/* harmony default export */ var Dashboard = ({ + mounted(el, binding) { + fetchDashboard(binding.value.idDashboard); + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["watch"])(() => external_CoreHome_["MatomoUrl"].parsed.value, parsed => { + onLocationChange(parsed); + }); + // ensure to not bind the event multiple times + external_CoreHome_["Matomo"].off('Dashboard.loadDashboard', onLoadDashboard); + external_CoreHome_["Matomo"].on('Dashboard.loadDashboard', onLoadDashboard); + }, + unmounted() { + onLocationChange(external_CoreHome_["MatomoUrl"].parsed.value); + external_CoreHome_["Matomo"].off('Dashboard.loadDashboard', onLoadDashboard); + } +}); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.vue?vue&type=template&id=859f193a + +const _hoisted_1 = ["title"]; +const _hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + class: "icon icon-dashboard-customize" +}, null, -1); +const _hoisted_3 = { + class: "dropdown positionInViewport" +}; +const _hoisted_4 = { + class: "submenu" +}; +const _hoisted_5 = ["onClick", "disabled", "title", "data-action"]; +const _hoisted_6 = ["onClick", "disabled", "title", "data-action"]; +const _hoisted_7 = { + class: "addWidget" +}; +const _hoisted_8 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + class: "icon icon-add1" +}, null, -1); +function render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_AddWidgetModal = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("AddWidgetModal"); + const _directive_tooltips = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("tooltips"); + const _directive_expand_on_click = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("expand-on-click"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", { + ref: "root", + class: "dashboard-manager piwikSelector borderedControl piwikTopControl dashboardSettings", + onClick: _cache[2] || (_cache[2] = $event => _ctx.onOpen()), + onFocusout: _cache[3] || (_cache[3] = (...args) => _ctx.onFocusOut && _ctx.onFocusOut(...args)) + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("button", { + type: "button", + class: "title", + title: _ctx.translate('Dashboard_ManageDashboard'), + tabindex: "4", + ref: "expander" + }, [_hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Dashboard_ManageDashboard')), 1)], 8, _hoisted_1)), [[_directive_tooltips]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("ul", _hoisted_4, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.generalActions, (title, actionName) => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("li", { + key: actionName + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + type: "button", + tabindex: "4", + onClick: $event => _ctx.onClickAction($event, actionName), + class: "generalAction", + disabled: _ctx.isActionDisabled[actionName] ? 'disabled' : undefined, + title: _ctx.actionTooltips[actionName] || undefined, + "data-action": actionName + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate(title)), 9, _hoisted_5)]); + }), 128)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + type: "button", + tabindex: "4", + class: "exportDashboard", + "data-action": "exportDashboard", + onClick: _cache[0] || (_cache[0] = $event => _ctx.onClickExportDashboard()) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Dashboard_ExportThisDashboard')), 1)]), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.dashboardActions, (title, actionName) => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("li", { + key: actionName + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + type: "button", + tabindex: "4", + onClick: $event => _ctx.onClickAction($event, actionName), + disabled: _ctx.isActionDisabled[actionName] ? 'disabled' : undefined, + title: _ctx.actionTooltips[actionName] || undefined, + "data-action": actionName + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate(title)), 9, _hoisted_6)]); + }), 128)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", _hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + type: "button", + tabindex: "4", + class: "addWidget-button", + onClick: _cache[1] || (_cache[1] = $event => _ctx.openAddWidget()) + }, [_hoisted_8, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Dashboard_AddAWidget')), 1)])])])])), [[_directive_tooltips, { + show: false + }]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_AddWidgetModal, { + onSelect: _ctx.onWidgetSelected + }, null, 8, ["onSelect"])], 32)), [[_directive_expand_on_click, { + expander: 'expander', + onExpand: _ctx.onExpand, + onClosed: _ctx.onClosed + }]]); +} +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.vue?vue&type=template&id=859f193a + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.vue?vue&type=template&id=e54103e6 + +const AddWidgetModalvue_type_template_id_e54103e6_hoisted_1 = ["aria-label"]; +const AddWidgetModalvue_type_template_id_e54103e6_hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("i", { + class: "icon-close" +}, null, -1); +const AddWidgetModalvue_type_template_id_e54103e6_hoisted_3 = [AddWidgetModalvue_type_template_id_e54103e6_hoisted_2]; +const AddWidgetModalvue_type_template_id_e54103e6_hoisted_4 = { + class: "add-widget-modal-title" +}; +const AddWidgetModalvue_type_template_id_e54103e6_hoisted_5 = { + class: "add-widget-modal-body widgetpreview-base" +}; +const AddWidgetModalvue_type_template_id_e54103e6_hoisted_6 = { + class: "add-widget-modal-categories" +}; +const AddWidgetModalvue_type_template_id_e54103e6_hoisted_7 = { + class: "add-widget-modal-widgets" +}; +const AddWidgetModalvue_type_template_id_e54103e6_hoisted_8 = { + class: "add-widget-modal-preview" +}; +function AddWidgetModalvue_type_template_id_e54103e6_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_category_list = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("category-list"); + const _component_widgets_list = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("widgets-list"); + const _component_widget_preview = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("widget-preview"); + const _component_matomo_modal = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("matomo-modal"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_matomo_modal, { + modelValue: _ctx.isOpen, + "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => _ctx.isOpen = $event), + classes: "add-widget-modal", + "content-class": "add-widget-modal-content", + "aria-label": _ctx.translate('Dashboard_AddAWidget'), + onClosed: _ctx.onClosed + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + type: "button", + class: "btn-close modal-close", + "aria-label": _ctx.translate('General_Close'), + onClick: _cache[0] || (_cache[0] = (...args) => _ctx.close && _ctx.close(...args)) + }, AddWidgetModalvue_type_template_id_e54103e6_hoisted_3, 8, AddWidgetModalvue_type_template_id_e54103e6_hoisted_1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", AddWidgetModalvue_type_template_id_e54103e6_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Dashboard_AddAWidget')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", AddWidgetModalvue_type_template_id_e54103e6_hoisted_5, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", AddWidgetModalvue_type_template_id_e54103e6_hoisted_6, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_category_list, { + categories: _ctx.categoryNames, + "chosen-category": _ctx.chosenCategory, + "onUpdate:chosenCategory": _ctx.onCategoryChosen, + onConfirm: _ctx.focusWidgetList + }, null, 8, ["categories", "chosen-category", "onUpdate:chosenCategory", "onConfirm"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", AddWidgetModalvue_type_template_id_e54103e6_hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_widgets_list, { + ref: "widgetsList", + widgets: _ctx.widgetsInCategory, + "chosen-widget-id": _ctx.hoveredWidgetId, + "added-widgets": _ctx.addedWidgetIds, + "existing-widget-ids": _ctx.existingWidgetIds, + onHover: _ctx.onWidgetHover, + onSelect: _ctx.onSelect + }, null, 8, ["widgets", "chosen-widget-id", "added-widgets", "existing-widget-ids", "onHover", "onSelect"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", AddWidgetModalvue_type_template_id_e54103e6_hoisted_8, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_widget_preview, { + widget: _ctx.previewWidget, + onSelect: _ctx.onSelect + }, null, 8, ["widget", "onSelect"])])])]), + _: 1 + }, 8, ["modelValue", "aria-label", "onClosed"]); +} +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.vue?vue&type=template&id=e54103e6 + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.vue?vue&type=template&id=91b874c0 + +const CategoryListvue_type_template_id_91b874c0_hoisted_1 = { + class: "widgetpreview-base widgetpreview-categorylist" +}; +const CategoryListvue_type_template_id_91b874c0_hoisted_2 = ["onMouseover", "onClick", "onFocus", "onKeydown"]; +function CategoryListvue_type_template_id_91b874c0_render(_ctx, _cache, $props, $setup, $data, $options) { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("ul", CategoryListvue_type_template_id_91b874c0_hoisted_1, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.categories, category => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("li", { + key: category, + class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])([{ + 'widgetpreview-choosen': category === _ctx.chosenCategory + }, "category-list-item"]) + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + type: "button", + class: "category-button-item", + onMouseover: $event => _ctx.selectCategory(category), + onClick: $event => _ctx.selectCategory(category), + onFocus: $event => _ctx.selectCategory(category), + onKeydown: [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withKeys"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.confirmCategory(category), ["prevent"]), ["enter"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withKeys"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.confirmCategory(category), ["prevent"]), ["space"])] + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(category), 41, CategoryListvue_type_template_id_91b874c0_hoisted_2)], 2); + }), 128))]); +} +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.vue?vue&type=template&id=91b874c0 + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.vue?vue&type=script&lang=ts + +/* harmony default export */ var CategoryListvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + name: 'CategoryList', + props: { + categories: { + type: Array, + required: true + }, + chosenCategory: { + type: String, + default: null + } + }, + emits: ['update:chosenCategory', 'confirm'], + methods: { + selectCategory(category) { + this.$emit('update:chosenCategory', category); + }, + // Keyboard-only: selecting via Enter/Space also signals "advance focus to + // the widgets list", so a keyboard user isn't stranded on the category + // they just confirmed. Mouse/touch paths intentionally don't emit this. + confirmCategory(category) { + this.selectCategory(category); + this.$emit('confirm'); + } + } +})); +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.vue + + + +CategoryListvue_type_script_lang_ts.render = CategoryListvue_type_template_id_91b874c0_render + +/* harmony default export */ var CategoryList = (CategoryListvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.vue?vue&type=template&id=840d3c14 + +const WidgetsListvue_type_template_id_840d3c14_hoisted_1 = { + ref: "list", + class: "widgetpreview-base widgetpreview-widgetlist" +}; +const WidgetsListvue_type_template_id_840d3c14_hoisted_2 = ["uniqueid"]; +const WidgetsListvue_type_template_id_840d3c14_hoisted_3 = ["onMouseenter", "onMouseleave", "onFocus", "onBlur", "onClick", "onKeydown"]; +const WidgetsListvue_type_template_id_840d3c14_hoisted_4 = { + class: "widgetpreview-widgetname" +}; +const WidgetsListvue_type_template_id_840d3c14_hoisted_5 = { + class: "widgetpreview-add-hint", + "aria-hidden": "true" +}; +const WidgetsListvue_type_template_id_840d3c14_hoisted_6 = { + key: 0, + class: "icon-ok widgetpreview-add-check" +}; +const WidgetsListvue_type_template_id_840d3c14_hoisted_7 = { + key: 1, + class: "widgetpreview-add-plus" +}; +function WidgetsListvue_type_template_id_840d3c14_render(_ctx, _cache, $props, $setup, $data, $options) { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("ul", WidgetsListvue_type_template_id_840d3c14_hoisted_1, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.widgets, widget => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("li", { + key: widget.uniqueId, + uniqueid: widget.uniqueId, + class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])([{ + 'widgetpreview-choosen': widget.uniqueId === _ctx.chosenWidgetId, + 'widgetpreview-unavailable': _ctx.isUnavailable(widget) + }, "widget-list-item"]) + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + type: "button", + class: "widget-button-item", + onMouseenter: $event => _ctx.onMouseEnter(widget), + onMouseleave: $event => _ctx.onMouseLeave(widget), + onFocus: $event => _ctx.onMouseEnter(widget), + onBlur: $event => _ctx.onMouseLeave(widget), + onClick: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.onRowClick(widget), ["prevent"]), + onKeydown: [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withKeys"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.onActivate(widget), ["prevent"]), ["enter"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withKeys"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.onActivate(widget), ["prevent"]), ["space"])] + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", WidgetsListvue_type_template_id_840d3c14_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(widget.name), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", WidgetsListvue_type_template_id_840d3c14_hoisted_5, [_ctx.isJustAdded(widget) ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("i", WidgetsListvue_type_template_id_840d3c14_hoisted_6)) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", WidgetsListvue_type_template_id_840d3c14_hoisted_7, "+")), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate(_ctx.isJustAdded(widget) ? 'General_Added' : 'General_Add')), 1)])], 40, WidgetsListvue_type_template_id_840d3c14_hoisted_3)], 10, WidgetsListvue_type_template_id_840d3c14_hoisted_2); + }), 128))], 512); +} +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.vue?vue&type=template&id=840d3c14 + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.vue?vue&type=script&lang=ts + + +const HOVER_DELAY_MS = 400; +const KPI_METRIC_CATEGORY_ID = 'General_KpiMetric'; +function hasHoverCapablePointer() { + return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(any-hover: hover)').matches; +} +/* harmony default export */ var WidgetsListvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + name: 'WidgetsList', + props: { + widgets: { + type: Array, + required: true + }, + chosenWidgetId: { + type: String, + default: null + }, + addedWidgets: { + type: Object, + default: () => new Set() + }, + existingWidgetIds: { + type: Object, + default: () => new Set() + } + }, + emits: ['hover', 'select'], + data() { + return { + hoverTimer: null, + // Cached once: any hover-capable pointer gets desktop-like click-to-add + // behaviour. Only pure no-hover environments use preview-first double-tap. + supportsHover: hasHoverCapablePointer(), + // The row most recently added in this session. Drives the transient green + // check in the add hint; cleared as soon as the hover moves elsewhere (see + // the chosenWidgetId watcher) so re-hovering an added row shows "+" again. + justAddedId: null + }; + }, + watch: { + chosenWidgetId(newId) { + // Revert the green check the moment the preview/hover moves off the + // just-added row. The add hint is only visible on the chosen row, so + // tying the reset to chosenWidgetId matches what the user actually sees. + if (newId !== this.justAddedId) { + this.justAddedId = null; + } + } + }, + methods: { + translate: external_CoreHome_["translate"], + isRepeatableWidget(widget) { + var _widget$category; + return ((_widget$category = widget.category) === null || _widget$category === void 0 ? void 0 : _widget$category.id) === KPI_METRIC_CATEGORY_ID; + }, + isJustAdded(widget) { + return !!widget.uniqueId && widget.uniqueId === this.justAddedId; + }, + isUnavailable(widget) { + if (!widget.uniqueId) { + return false; + } + if (this.addedWidgets.has(widget.uniqueId)) { + return true; + } + if (this.isRepeatableWidget(widget)) { + return false; + } + return this.existingWidgetIds.has(widget.uniqueId); + }, + onMouseEnter(widget) { + if (!widget.uniqueId) { + return; + } + this.clearHoverTimer(); + const { + uniqueId + } = widget; + this.hoverTimer = window.setTimeout(() => { + this.hoverTimer = null; + this.$emit('hover', uniqueId); + }, HOVER_DELAY_MS); + }, + onMouseLeave(widget) { + // Matches the original jQuery widget menu: leaving an *unavailable* row keeps the + // preview timer running so the user still gets a preview, while leaving any other + // row cancels the pending preview. + if (this.isUnavailable(widget)) { + return; + } + this.clearHoverTimer(); + }, + onRowClick(widget) { + if (!widget.uniqueId) { + return; + } + // Rows flagged as `widgetpreview-unavailable` (already on the dashboard, or + // added earlier in this modal session) stay clickable — the class is a + // visual hint, not a hard block. Matches 5.x-dev's widgetMenu.js behaviour + // where the click handler ignores the unavailable class. + this.clearHoverTimer(); + // Touch / non-hover devices: first tap previews; second tap on the same row adds. + if (!this.supportsHover && widget.uniqueId !== this.chosenWidgetId) { + this.$emit('hover', widget.uniqueId); + return; + } + this.justAddedId = widget.uniqueId; + this.$emit('select', widget.uniqueId); + }, + // Keyboard activation (Enter / Space). Bypasses the touch double-tap branch in + // onRowClick on purpose — a keypress is not a touch interaction, so a focused + // row should add immediately even when supportsHover is false. + onActivate(widget) { + if (!widget.uniqueId) { + return; + } + this.clearHoverTimer(); + this.justAddedId = widget.uniqueId; + this.$emit('select', widget.uniqueId); + }, + focusFirst() { + const list = this.$refs.list; + const first = list === null || list === void 0 ? void 0 : list.querySelector('li button'); + if (first instanceof HTMLElement) { + first.focus(); + } + }, + clearHoverTimer() { + if (this.hoverTimer !== null) { + window.clearTimeout(this.hoverTimer); + this.hoverTimer = null; + } + } + }, + beforeUnmount() { + this.clearHoverTimer(); + } +})); +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.vue + + + +WidgetsListvue_type_script_lang_ts.render = WidgetsListvue_type_template_id_840d3c14_render + +/* harmony default export */ var WidgetsList = (WidgetsListvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.vue?vue&type=template&id=307f9336 + +const WidgetPreviewvue_type_template_id_307f9336_hoisted_1 = { + class: "widgetpreview-preview" +}; +const WidgetPreviewvue_type_template_id_307f9336_hoisted_2 = { + key: 0, + class: "widget" +}; +const WidgetPreviewvue_type_template_id_307f9336_hoisted_3 = ["title"]; +const WidgetPreviewvue_type_template_id_307f9336_hoisted_4 = { + class: "widgetName" +}; +const WidgetPreviewvue_type_template_id_307f9336_hoisted_5 = { + class: "widgetContent" +}; +function WidgetPreviewvue_type_template_id_307f9336_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_Widget = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Widget"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", WidgetPreviewvue_type_template_id_307f9336_hoisted_1, [_ctx.previewWidget ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", WidgetPreviewvue_type_template_id_307f9336_hoisted_2, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", { + class: "widgetTop", + title: _ctx.translate('Dashboard_AddPreviewedWidget'), + role: "button", + tabindex: "0", + onClick: _cache[0] || (_cache[0] = $event => _ctx.$emit('select', _ctx.previewWidget.uniqueId)), + onKeydown: [_cache[1] || (_cache[1] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withKeys"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.$emit('select', _ctx.previewWidget.uniqueId), ["prevent"]), ["enter"])), _cache[2] || (_cache[2] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withKeys"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.$emit('select', _ctx.previewWidget.uniqueId), ["prevent"]), ["space"]))] + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", WidgetPreviewvue_type_template_id_307f9336_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Dashboard_WidgetPreview')), 1)], 40, WidgetPreviewvue_type_template_id_307f9336_hoisted_3), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", WidgetPreviewvue_type_template_id_307f9336_hoisted_5, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_Widget, { + key: _ctx.previewWidget.uniqueId, + widget: _ctx.previewWidget, + widgetized: true, + "suppress-notifications": true + }, null, 8, ["widget"]))])])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)]); +} +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.vue?vue&type=template&id=307f9336 + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.vue?vue&type=script&lang=ts + + +/* harmony default export */ var WidgetPreviewvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + name: 'WidgetPreview', + components: { + Widget: external_CoreHome_["Widget"] + }, + props: { + widget: { + type: Object, + default: null + } + }, + emits: ['select'], + computed: { + previewWidget() { + if (!this.widget) { + return null; + } + const result = Object.assign(Object.assign({}, this.widget), {}, { + parameters: this.getPreviewParameters(this.widget.parameters) + }); + if (this.isContainerWidget(result)) { + result.widgets = this.getPreviewChildren(result); + } + return result; + } + }, + mounted() { + external_CoreHome_["Matomo"].on('widget:loaded', this.onWidgetLoaded); + }, + unmounted() { + external_CoreHome_["Matomo"].off('widget:loaded', this.onWidgetLoaded); + }, + methods: { + translate: external_CoreHome_["translate"], + shouldDisableLink() { + // disableLink is only forced for Widgetize embeds and the standalone body + // matching widgetMenu.js. + const urlFlag = external_CoreHome_["Matomo"].broadcast.getValueFromUrl('disableLink'); + if (urlFlag && urlFlag.length) { + return true; + } + return !!document.querySelector('body#standalone'); + }, + getPreviewParameters(parameters = {}) { + // Force widget=1 so previews render in widgetized layout, matching legacy widgetMenu.js. + // showtitle=0 suppresses the server-rendered

    + return Object.assign(Object.assign({}, parameters), {}, { + widget: '1', + showtitle: '0' + }, this.shouldDisableLink() ? { + disableLink: '1' + } : {}); + }, + isContainerWidget(widget) { + return !!widget.isContainer && Array.isArray(widget.widgets); + }, + getPreviewChildren(widget) { + var _widget$parameters; + // Child widgets need the same widgetized parameters as the container preview. + // Without this, nested widgets render as non-widgetized and may show titles again. + const containerId = (_widget$parameters = widget.parameters) === null || _widget$parameters === void 0 ? void 0 : _widget$parameters.containerId; + return widget.widgets.map(child => Object.assign(Object.assign({}, child), {}, { + parameters: Object.assign(Object.assign({}, child.parameters), {}, { + widget: '1' + }, containerId ? { + containerId + } : {}) + })); + }, + onWidgetLoaded(payload) { + var _payload$parameters, _payload$element; + // Only re-fire widget:create for the top-level preview; + // nested container loads emit widget:loaded too. + if (!this.widget || ((_payload$parameters = payload.parameters) === null || _payload$parameters === void 0 ? void 0 : _payload$parameters.uniqueId) !== this.widget.uniqueId) { + return; + } + const root = this.$el; + const loadedElement = payload === null || payload === void 0 || (_payload$element = payload.element) === null || _payload$element === void 0 ? void 0 : _payload$element[0]; + if (!root || !loadedElement || !root.contains(loadedElement)) { + return; + } + const widget = root.querySelector('.widget'); + const widgetContent = widget === null || widget === void 0 ? void 0 : widget.querySelector('.widgetContent'); + if (!widget || !widgetContent) { + return; + } + window.$(widgetContent).trigger('widget:create', [{ + element: window.$(widget) + }]); + } + } +})); +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.vue + + + +WidgetPreviewvue_type_script_lang_ts.render = WidgetPreviewvue_type_template_id_307f9336_render + +/* harmony default export */ var WidgetPreview = (WidgetPreviewvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.vue?vue&type=script&lang=ts + + + + + +const OPEN_EVENT = 'Dashboard.AddWidget.open'; +/* harmony default export */ var AddWidgetModalvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + name: 'AddWidgetModal', + components: { + MatomoModal: external_CoreHome_["MatomoModal"], + CategoryList: CategoryList, + WidgetsList: WidgetsList, + WidgetPreview: WidgetPreview + }, + emits: ['select'], + data() { + return { + isOpen: false, + chosenCategory: null, + hoveredWidgetId: null, + addedWidgetIds: new Set(), + existingWidgetIds: new Set() + }; + }, + computed: { + widgets() { + return external_CoreHome_["WidgetsStore"].widgets.value || {}; + }, + categoryNames() { + return Object.keys(this.widgets); + }, + widgetsInCategory() { + if (!this.chosenCategory) { + return []; + } + return this.widgets[this.chosenCategory] || []; + }, + widgetsById() { + // Rebuilds only when `WidgetsStore.widgets` mutates; `previewWidget` and + // `onSelect` both consult this in O(1) instead of scanning every category. + return new Map(Object.values(this.widgets).flat().filter(w => !!w.uniqueId).map(w => [w.uniqueId, w])); + }, + previewWidget() { + if (!this.hoveredWidgetId) { + return null; + } + return this.widgetsById.get(this.hoveredWidgetId) || null; + } + }, + methods: { + translate: external_CoreHome_["translate"], + open() { + // Snapshot the dashboard's currently-placed widget IDs once per open so + // WidgetsList can flag them as unavailable without hitting the DOM on + // every render. Refreshed each open; within-session additions are tracked + // separately via `addedWidgetIds`. + const ids = new Set(); + document.querySelectorAll('#dashboardWidgetsArea [widgetId]').forEach(el => { + const id = el.getAttribute('widgetId'); + if (id) { + ids.add(id); + } + }); + this.existingWidgetIds = ids; + this.isOpen = true; + }, + close() { + this.isOpen = false; + }, + onClosed() { + this.chosenCategory = null; + this.hoveredWidgetId = null; + this.addedWidgetIds = new Set(); + this.existingWidgetIds = new Set(); + }, + onCategoryChosen(category) { + if (this.chosenCategory === category) { + return; + } + this.chosenCategory = category; + this.hoveredWidgetId = null; + }, + async focusWidgetList() { + // Wait for the widgets list to re-render under the newly chosen category + // before focusing — the prior li elements may have unmounted. + await this.$nextTick(); + const widgetsList = this.$refs.widgetsList; + if (widgetsList && typeof widgetsList.focusFirst === 'function') { + widgetsList.focusFirst(); + } + }, + onWidgetHover(uniqueId) { + this.hoveredWidgetId = uniqueId; + }, + onSelect(uniqueId) { + const widget = this.widgetsById.get(uniqueId); + if (widget) { + // Keep the modal open so the user can add more widgets in one session; + // the added row is greyed out via the `added-widgets` set on WidgetsList. + this.addedWidgetIds.add(uniqueId); + this.$emit('select', widget); + return; + } + // WidgetsStore drives both the list and the lookup; a miss here means the cache + // was unexpectedly cleared between render and click. Close anyway so the modal + // cannot block follow-up interactions. + console.warn(`Could not resolve dashboard widget "${uniqueId}" from cached metadata.`); + this.close(); + } + }, + mounted() { + external_CoreHome_["Matomo"].on(OPEN_EVENT, this.open); + }, + unmounted() { + external_CoreHome_["Matomo"].off(OPEN_EVENT, this.open); + } +})); +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.vue + + + +AddWidgetModalvue_type_script_lang_ts.render = AddWidgetModalvue_type_template_id_e54103e6_render + +/* harmony default export */ var AddWidgetModal = (AddWidgetModalvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.vue?vue&type=script&lang=ts + + + +const { + $: DashboardSettingsvue_type_script_lang_ts_$ +} = window; +const DASHBOARD_EXPORT_STORAGE_KEY = 'scheduledReports.dashboardExportId'; +/* harmony default export */ var DashboardSettingsvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + name: 'DashboardSettings', + components: { + AddWidgetModal: AddWidgetModal + }, + directives: { + ExpandOnClick: external_CoreHome_["ExpandOnClick"], + Tooltips: external_CoreHome_["Tooltips"] + }, + data() { + return { + isActionDisabled: {}, + actionTooltips: {} + }; + }, + setup() { + const root = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["ref"])(null); + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["onMounted"])(() => { + external_CoreHome_["Matomo"].postEvent('Dashboard.DashboardSettings.mounted', root.value); + DashboardSettingsvue_type_script_lang_ts_$(root.value).hide(); // hide dashboard-manager initially (shown manually by Dashboard.ts) + }); + return { + root + }; + }, + computed: { + isUserNotAnonymous() { + return !!external_CoreHome_["Matomo"].userLogin && external_CoreHome_["Matomo"].userLogin !== 'anonymous'; + }, + isSuperUser() { + return this.isUserNotAnonymous && external_CoreHome_["Matomo"].hasSuperUserAccess; + }, + isUserHasSomeAdminAccess() { + return this.isUserNotAnonymous && external_CoreHome_["Matomo"].userHasSomeAdminAccess; + }, + dashboardActions() { + const result = { + resetDashboard: 'Dashboard_ResetDashboard', + showChangeDashboardLayoutDialog: 'Dashboard_ChangeDashboardLayout' + }; + if (this.isUserNotAnonymous) { + result.renameDashboard = 'Dashboard_RenameDashboard'; + result.removeDashboard = 'Dashboard_RemoveDashboard'; + } + if (this.isSuperUser) { + result.setAsDefaultWidgets = 'Dashboard_SetAsDefaultWidgets'; + } + if (this.isUserHasSomeAdminAccess) { + result.copyDashboardToUser = 'Dashboard_CopyDashboardToUser'; + } + return result; + }, + generalActions() { + const result = {}; + if (this.isUserNotAnonymous) { + result.createDashboard = 'Dashboard_CreateNewDashboard'; + } + return result; + } + }, + methods: { + onClickAction(event, action) { + if (event.target.getAttribute('disabled')) { + return; + } + window[action](); + }, + onOpen() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (DashboardSettingsvue_type_script_lang_ts_$('#dashboardWidgetsArea').dashboard('isDefaultDashboard')) { + this.isActionDisabled.removeDashboard = true; + this.actionTooltips.removeDashboard = Object(external_CoreHome_["translate"])('Dashboard_RemoveDefaultDashboardNotPossible'); + } else { + this.isActionDisabled.removeDashboard = false; + this.actionTooltips.removeDashboard = undefined; + } + }, + onExpand(event) { + // Clicks triggered via keyboard (Enter/Space on the button) have detail === 0, + // mouse clicks have detail >= 1. Only shift focus into the menu for keyboard opens. + if (event.detail !== 0) { + return; + } + this.$nextTick(() => { + const firstAction = this.$refs.root.querySelector('.submenu button:not([disabled])'); + if (firstAction) { + firstAction.focus(); + } + }); + }, + onFocusOut(event) { + const root = this.$refs.root; + const newTarget = event.relatedTarget; + if (newTarget && root.contains(newTarget)) { + return; + } + root.classList.remove('expanded'); + }, + onClosed(event) { + // Return focus to the trigger when the dropdown was dismissed via the Escape + // key, so keyboard users keep their place. Enter/Space activation of buttons + // produces a MouseEvent (synthetic click) and is handled by the browser + // leaving focus on the activated element. + if (!(event instanceof KeyboardEvent)) { + return; + } + const expander = this.$refs.expander; + if (expander) { + expander.focus(); + } + }, + openAddWidget() { + // close the dashboard-manager dropdown when opening the modal + this.$refs.root.classList.remove('expanded'); + external_CoreHome_["Matomo"].postEvent('Dashboard.AddWidget.open'); + }, + onWidgetSelected(widget) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + DashboardSettingsvue_type_script_lang_ts_$('#dashboardWidgetsArea').dashboard('addWidget', widget.uniqueId, 1, widget.parameters, true, false); + }, + redirectToCreateScheduledReports() { + const query = Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value); + delete query.category; + delete query.subcategory; + delete query.idDashboard; + query.module = 'ScheduledReports'; + query.action = 'index'; + const hash = Object.assign({}, external_CoreHome_["MatomoUrl"].hashParsed.value); + delete hash.category; + delete hash.subcategory; + delete hash.idDashboard; + external_CoreHome_["MatomoUrl"].updateUrl(query, hash); + }, + redirectToLoginPage() { + const loginQuery = { + module: external_CoreHome_["Matomo"].getLoginModule() + }; + external_CoreHome_["MatomoUrl"].updateUrl(loginQuery); + }, + onClickExportDashboard() { + if (typeof sessionStorage !== 'undefined') { + sessionStorage.removeItem(DASHBOARD_EXPORT_STORAGE_KEY); + } + if (this.isUserNotAnonymous) { + const dashboardId = this.getCurrentDashboardId(); + if (dashboardId !== null && typeof sessionStorage !== 'undefined') { + sessionStorage.setItem(DASHBOARD_EXPORT_STORAGE_KEY, String(dashboardId)); + } + this.redirectToCreateScheduledReports(); + return; + } + // We do not persist dashboard id when user is anonymous + this.redirectToLoginPage(); + }, + normalizeDashboardId(value) { + const candidate = Array.isArray(value) ? value[0] : value; + if (candidate === null || candidate === undefined) { + return null; + } + const normalized = String(candidate).trim(); + if (!/^[1-9]\d*$/.test(normalized)) { + return null; + } + return Number(normalized); + }, + getCurrentDashboardId() { + const fromSubcategory = this.normalizeDashboardId(external_CoreHome_["MatomoUrl"].getSearchParam('subcategory')); + if (fromSubcategory !== null) { + return fromSubcategory; + } + const fromQueryIdDashboard = this.normalizeDashboardId(external_CoreHome_["MatomoUrl"].urlParsed.value.idDashboard); + if (fromQueryIdDashboard !== null) { + return fromQueryIdDashboard; + } + return this.normalizeDashboardId(external_CoreHome_["MatomoUrl"].hashParsed.value.idDashboard); + } + } +})); +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.vue + + + +DashboardSettingsvue_type_script_lang_ts.render = render + +/* harmony default export */ var DashboardSettings = (DashboardSettingsvue_type_script_lang_ts); +// CONCATENATED MODULE: ./plugins/Dashboard/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=Dashboard.umd.js.map \ No newline at end of file diff --git a/plugins/Dashboard/vue/dist/Dashboard.umd.min.js b/plugins/Dashboard/vue/dist/Dashboard.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..f19dad5e99c7ac435b2b9608c3636990d925dffa --- /dev/null +++ b/plugins/Dashboard/vue/dist/Dashboard.umd.min.js @@ -0,0 +1,20 @@ +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue")):"function"===typeof define&&define.amd?define(["CoreHome"],t):"object"===typeof exports?exports["Dashboard"]=t(require("CoreHome"),require("vue")):e["Dashboard"]=t(e["CoreHome"],e["Vue"])})("undefined"!==typeof self?self:this,(function(e,t){return function(e){var t={};function o(i){if(t[i])return t[i].exports;var s=t[i]={i:i,l:!1,exports:{}};return e[i].call(s.exports,s,s.exports,o),s.l=!0,s.exports}return o.m=e,o.c=t,o.d=function(e,t,i){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},o.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(o.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)o.d(i,s,function(t){return e[t]}.bind(null,s));return i},o.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="plugins/Dashboard/vue/dist/",o(o.s="fae3")}({"19dc":function(t,o){t.exports=e},"8bbf":function(e,o){e.exports=t},fae3:function(e,t,o){"use strict";if(o.r(t),o.d(t,"DashboardStore",(function(){return c})),o.d(t,"Dashboard",(function(){return p})),o.d(t,"DashboardSettings",(function(){return he})),"undefined"!==typeof window){var i=window.document.currentScript,s=i&&i.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);s&&(o.p=s[1])}var a=o("8bbf"),n=o("19dc");function d(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e} +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */class r{constructor(){d(this,"privateState",Object(a["reactive"])({dashboards:[]})),d(this,"state",Object(a["computed"])(()=>Object(a["readonly"])(this.privateState))),d(this,"dashboards",Object(a["computed"])(()=>this.state.value.dashboards)),d(this,"dashboardsPromise",null)}getDashboard(e){return this.getAllDashboards().then(t=>t.find(t=>parseInt(""+t.id,10)===parseInt(""+e,10)))}getDashboardLayout(e){return n["AjaxHelper"].fetch({module:"Dashboard",action:"getDashboardLayout",idDashboard:e},{withTokenInUrl:!0})}reloadAllDashboards(){return this.dashboardsPromise=null,this.getAllDashboards()}getAllDashboards(){return this.dashboardsPromise||(this.dashboardsPromise=n["AjaxHelper"].fetch({method:"Dashboard.getDashboards",filter_limit:"-1"}).then(e=>(e&&(this.privateState.dashboards=e),this.dashboards.value))),this.dashboardsPromise}}var c=new r; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */const{$:l}=window;function u(e,t,o){const i=l(".dashboardSettings");i.show(),window.initTopControls(),l("#topBars").length||(i.after(l("#Dashboard")),l("#Dashboard ul li").removeClass("active"),l("#Dashboard_embeddedIndex_"+e).addClass("active")),window.widgetsHelper.getAvailableWidgets(),l("#dashboardWidgetsArea").off("dashboardempty",window.showEmptyDashboardNotification).on("dashboardempty",window.showEmptyDashboardNotification).dashboard({idDashboard:e,layout:o,name:t?t.name:""});const s=l("#columnPreview").find(">div");s.each((function(){const e=[];l("div",this).each((function(){e.push(this.className.replace(/width-/,""))})),l(this).attr("layout",e.join("-"))})),s.off("click.renderDashboard"),s.on("click.renderDashboard",(function(){s.removeClass("choosen"),l(this).addClass("choosen")}))}function b(e){return new Promise(e=>setTimeout(e)).then(()=>Promise.resolve(window.widgetsHelper.firstGetAvailableWidgetsCall)).then(()=>{const t=l("#dashboardWidgetsArea");return t.dashboard("destroyWidgets"),t.empty(),Promise.all([c.getDashboard(e),c.getDashboardLayout(e)])}).then(([t,o])=>new Promise(i=>{l(()=>{u(e,t,o),i()})}))}function h(){l(".top_controls .dashboard-manager").hide(),l("#dashboardWidgetsArea").dashboard("destroy")}function g(e){"Widgetize"!==e.module&&"Dashboard_Dashboard"!==e.category&&h()}function m(e){b(e)}var p={mounted(e,t){b(t.value.idDashboard),Object(a["watch"])(()=>n["MatomoUrl"].parsed.value,e=>{g(e)}),n["Matomo"].off("Dashboard.loadDashboard",m),n["Matomo"].on("Dashboard.loadDashboard",m)},unmounted(){g(n["MatomoUrl"].parsed.value),n["Matomo"].off("Dashboard.loadDashboard",m)}};const w=["title"],v=Object(a["createElementVNode"])("span",{class:"icon icon-dashboard-customize"},null,-1),f={class:"dropdown positionInViewport"},j={class:"submenu"},O=["onClick","disabled","title","data-action"],y=["onClick","disabled","title","data-action"],D={class:"addWidget"},C=Object(a["createElementVNode"])("span",{class:"icon icon-add1"},null,-1);function k(e,t,o,i,s,n){const d=Object(a["resolveComponent"])("AddWidgetModal"),r=Object(a["resolveDirective"])("tooltips"),c=Object(a["resolveDirective"])("expand-on-click");return Object(a["withDirectives"])((Object(a["openBlock"])(),Object(a["createElementBlock"])("div",{ref:"root",class:"dashboard-manager piwikSelector borderedControl piwikTopControl dashboardSettings",onClick:t[2]||(t[2]=t=>e.onOpen()),onFocusout:t[3]||(t[3]=(...t)=>e.onFocusOut&&e.onFocusOut(...t))},[Object(a["withDirectives"])((Object(a["openBlock"])(),Object(a["createElementBlock"])("button",{type:"button",class:"title",title:e.translate("Dashboard_ManageDashboard"),tabindex:"4",ref:"expander"},[v,Object(a["createTextVNode"])(" "+Object(a["toDisplayString"])(e.translate("Dashboard_ManageDashboard")),1)],8,w)),[[r]]),Object(a["withDirectives"])((Object(a["openBlock"])(),Object(a["createElementBlock"])("div",f,[Object(a["createElementVNode"])("ul",j,[(Object(a["openBlock"])(!0),Object(a["createElementBlock"])(a["Fragment"],null,Object(a["renderList"])(e.generalActions,(t,o)=>(Object(a["openBlock"])(),Object(a["createElementBlock"])("li",{key:o},[Object(a["createElementVNode"])("button",{type:"button",tabindex:"4",onClick:t=>e.onClickAction(t,o),class:"generalAction",disabled:e.isActionDisabled[o]?"disabled":void 0,title:e.actionTooltips[o]||void 0,"data-action":o},Object(a["toDisplayString"])(e.translate(t)),9,O)]))),128)),Object(a["createElementVNode"])("li",null,[Object(a["createElementVNode"])("button",{type:"button",tabindex:"4",class:"exportDashboard","data-action":"exportDashboard",onClick:t[0]||(t[0]=t=>e.onClickExportDashboard())},Object(a["toDisplayString"])(e.translate("Dashboard_ExportThisDashboard")),1)]),(Object(a["openBlock"])(!0),Object(a["createElementBlock"])(a["Fragment"],null,Object(a["renderList"])(e.dashboardActions,(t,o)=>(Object(a["openBlock"])(),Object(a["createElementBlock"])("li",{key:o},[Object(a["createElementVNode"])("button",{type:"button",tabindex:"4",onClick:t=>e.onClickAction(t,o),disabled:e.isActionDisabled[o]?"disabled":void 0,title:e.actionTooltips[o]||void 0,"data-action":o},Object(a["toDisplayString"])(e.translate(t)),9,y)]))),128)),Object(a["createElementVNode"])("li",D,[Object(a["createElementVNode"])("button",{type:"button",tabindex:"4",class:"addWidget-button",onClick:t[1]||(t[1]=t=>e.openAddWidget())},[C,Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.translate("Dashboard_AddAWidget")),1)])])])])),[[r,{show:!1}]]),Object(a["createVNode"])(d,{onSelect:e.onWidgetSelected},null,8,["onSelect"])],32)),[[c,{expander:"expander",onExpand:e.onExpand,onClosed:e.onClosed}]])}const W=["aria-label"],A=Object(a["createElementVNode"])("i",{class:"icon-close"},null,-1),I=[A],S={class:"add-widget-modal-title"},M={class:"add-widget-modal-body widgetpreview-base"},E={class:"add-widget-modal-categories"},x={class:"add-widget-modal-widgets"},N={class:"add-widget-modal-preview"};function B(e,t,o,i,s,n){const d=Object(a["resolveComponent"])("category-list"),r=Object(a["resolveComponent"])("widgets-list"),c=Object(a["resolveComponent"])("widget-preview"),l=Object(a["resolveComponent"])("matomo-modal");return Object(a["openBlock"])(),Object(a["createBlock"])(l,{modelValue:e.isOpen,"onUpdate:modelValue":t[1]||(t[1]=t=>e.isOpen=t),classes:"add-widget-modal","content-class":"add-widget-modal-content","aria-label":e.translate("Dashboard_AddAWidget"),onClosed:e.onClosed},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("button",{type:"button",class:"btn-close modal-close","aria-label":e.translate("General_Close"),onClick:t[0]||(t[0]=(...t)=>e.close&&e.close(...t))},I,8,W),Object(a["createElementVNode"])("h3",S,Object(a["toDisplayString"])(e.translate("Dashboard_AddAWidget")),1),Object(a["createElementVNode"])("div",M,[Object(a["createElementVNode"])("div",E,[Object(a["createVNode"])(d,{categories:e.categoryNames,"chosen-category":e.chosenCategory,"onUpdate:chosenCategory":e.onCategoryChosen,onConfirm:e.focusWidgetList},null,8,["categories","chosen-category","onUpdate:chosenCategory","onConfirm"])]),Object(a["createElementVNode"])("div",x,[Object(a["createVNode"])(r,{ref:"widgetsList",widgets:e.widgetsInCategory,"chosen-widget-id":e.hoveredWidgetId,"added-widgets":e.addedWidgetIds,"existing-widget-ids":e.existingWidgetIds,onHover:e.onWidgetHover,onSelect:e.onSelect},null,8,["widgets","chosen-widget-id","added-widgets","existing-widget-ids","onHover","onSelect"])]),Object(a["createElementVNode"])("div",N,[Object(a["createVNode"])(c,{widget:e.previewWidget,onSelect:e.onSelect},null,8,["widget","onSelect"])])])]),_:1},8,["modelValue","aria-label","onClosed"])}const T={class:"widgetpreview-base widgetpreview-categorylist"},q=["onMouseover","onClick","onFocus","onKeydown"];function V(e,t,o,i,s,n){return Object(a["openBlock"])(),Object(a["createElementBlock"])("ul",T,[(Object(a["openBlock"])(!0),Object(a["createElementBlock"])(a["Fragment"],null,Object(a["renderList"])(e.categories,t=>(Object(a["openBlock"])(),Object(a["createElementBlock"])("li",{key:t,class:Object(a["normalizeClass"])([{"widgetpreview-choosen":t===e.chosenCategory},"category-list-item"])},[Object(a["createElementVNode"])("button",{type:"button",class:"category-button-item",onMouseover:o=>e.selectCategory(t),onClick:o=>e.selectCategory(t),onFocus:o=>e.selectCategory(t),onKeydown:[Object(a["withKeys"])(Object(a["withModifiers"])(o=>e.confirmCategory(t),["prevent"]),["enter"]),Object(a["withKeys"])(Object(a["withModifiers"])(o=>e.confirmCategory(t),["prevent"]),["space"])]},Object(a["toDisplayString"])(t),41,q)],2))),128))])}var L=Object(a["defineComponent"])({name:"CategoryList",props:{categories:{type:Array,required:!0},chosenCategory:{type:String,default:null}},emits:["update:chosenCategory","confirm"],methods:{selectCategory(e){this.$emit("update:chosenCategory",e)},confirmCategory(e){this.selectCategory(e),this.$emit("confirm")}}});L.render=V;var P=L;const U={ref:"list",class:"widgetpreview-base widgetpreview-widgetlist"},_=["uniqueid"],$=["onMouseenter","onMouseleave","onFocus","onBlur","onClick","onKeydown"],H={class:"widgetpreview-widgetname"},F={class:"widgetpreview-add-hint","aria-hidden":"true"},K={key:0,class:"icon-ok widgetpreview-add-check"},R={key:1,class:"widgetpreview-add-plus"};function z(e,t,o,i,s,n){return Object(a["openBlock"])(),Object(a["createElementBlock"])("ul",U,[(Object(a["openBlock"])(!0),Object(a["createElementBlock"])(a["Fragment"],null,Object(a["renderList"])(e.widgets,t=>(Object(a["openBlock"])(),Object(a["createElementBlock"])("li",{key:t.uniqueId,uniqueid:t.uniqueId,class:Object(a["normalizeClass"])([{"widgetpreview-choosen":t.uniqueId===e.chosenWidgetId,"widgetpreview-unavailable":e.isUnavailable(t)},"widget-list-item"])},[Object(a["createElementVNode"])("button",{type:"button",class:"widget-button-item",onMouseenter:o=>e.onMouseEnter(t),onMouseleave:o=>e.onMouseLeave(t),onFocus:o=>e.onMouseEnter(t),onBlur:o=>e.onMouseLeave(t),onClick:Object(a["withModifiers"])(o=>e.onRowClick(t),["prevent"]),onKeydown:[Object(a["withKeys"])(Object(a["withModifiers"])(o=>e.onActivate(t),["prevent"]),["enter"]),Object(a["withKeys"])(Object(a["withModifiers"])(o=>e.onActivate(t),["prevent"]),["space"])]},[Object(a["createElementVNode"])("span",H,Object(a["toDisplayString"])(t.name),1),Object(a["createElementVNode"])("span",F,[e.isJustAdded(t)?(Object(a["openBlock"])(),Object(a["createElementBlock"])("i",K)):(Object(a["openBlock"])(),Object(a["createElementBlock"])("span",R,"+")),Object(a["createTextVNode"])(" "+Object(a["toDisplayString"])(e.translate(e.isJustAdded(t)?"General_Added":"General_Add")),1)])],40,$)],10,_))),128))],512)}const G=400,J="General_KpiMetric";function Q(){return"undefined"!==typeof window&&"function"===typeof window.matchMedia&&window.matchMedia("(any-hover: hover)").matches}var X=Object(a["defineComponent"])({name:"WidgetsList",props:{widgets:{type:Array,required:!0},chosenWidgetId:{type:String,default:null},addedWidgets:{type:Object,default:()=>new Set},existingWidgetIds:{type:Object,default:()=>new Set}},emits:["hover","select"],data(){return{hoverTimer:null,supportsHover:Q(),justAddedId:null}},watch:{chosenWidgetId(e){e!==this.justAddedId&&(this.justAddedId=null)}},methods:{translate:n["translate"],isRepeatableWidget(e){var t;return(null===(t=e.category)||void 0===t?void 0:t.id)===J},isJustAdded(e){return!!e.uniqueId&&e.uniqueId===this.justAddedId},isUnavailable(e){return!!e.uniqueId&&(!!this.addedWidgets.has(e.uniqueId)||!this.isRepeatableWidget(e)&&this.existingWidgetIds.has(e.uniqueId))},onMouseEnter(e){if(!e.uniqueId)return;this.clearHoverTimer();const{uniqueId:t}=e;this.hoverTimer=window.setTimeout(()=>{this.hoverTimer=null,this.$emit("hover",t)},G)},onMouseLeave(e){this.isUnavailable(e)||this.clearHoverTimer()},onRowClick(e){e.uniqueId&&(this.clearHoverTimer(),this.supportsHover||e.uniqueId===this.chosenWidgetId?(this.justAddedId=e.uniqueId,this.$emit("select",e.uniqueId)):this.$emit("hover",e.uniqueId))},onActivate(e){e.uniqueId&&(this.clearHoverTimer(),this.justAddedId=e.uniqueId,this.$emit("select",e.uniqueId))},focusFirst(){const e=this.$refs.list,t=null===e||void 0===e?void 0:e.querySelector("li button");t instanceof HTMLElement&&t.focus()},clearHoverTimer(){null!==this.hoverTimer&&(window.clearTimeout(this.hoverTimer),this.hoverTimer=null)}},beforeUnmount(){this.clearHoverTimer()}});X.render=z;var Y=X;const Z={class:"widgetpreview-preview"},ee={key:0,class:"widget"},te=["title"],oe={class:"widgetName"},ie={class:"widgetContent"};function se(e,t,o,i,s,n){const d=Object(a["resolveComponent"])("Widget");return Object(a["openBlock"])(),Object(a["createElementBlock"])("div",Z,[e.previewWidget?(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",ee,[Object(a["createElementVNode"])("div",{class:"widgetTop",title:e.translate("Dashboard_AddPreviewedWidget"),role:"button",tabindex:"0",onClick:t[0]||(t[0]=t=>e.$emit("select",e.previewWidget.uniqueId)),onKeydown:[t[1]||(t[1]=Object(a["withKeys"])(Object(a["withModifiers"])(t=>e.$emit("select",e.previewWidget.uniqueId),["prevent"]),["enter"])),t[2]||(t[2]=Object(a["withKeys"])(Object(a["withModifiers"])(t=>e.$emit("select",e.previewWidget.uniqueId),["prevent"]),["space"]))]},[Object(a["createElementVNode"])("h3",oe,Object(a["toDisplayString"])(e.translate("Dashboard_WidgetPreview")),1)],40,te),Object(a["createElementVNode"])("div",ie,[(Object(a["openBlock"])(),Object(a["createBlock"])(d,{key:e.previewWidget.uniqueId,widget:e.previewWidget,widgetized:!0,"suppress-notifications":!0},null,8,["widget"]))])])):Object(a["createCommentVNode"])("",!0)])}var ae=Object(a["defineComponent"])({name:"WidgetPreview",components:{Widget:n["Widget"]},props:{widget:{type:Object,default:null}},emits:["select"],computed:{previewWidget(){if(!this.widget)return null;const e=Object.assign(Object.assign({},this.widget),{},{parameters:this.getPreviewParameters(this.widget.parameters)});return this.isContainerWidget(e)&&(e.widgets=this.getPreviewChildren(e)),e}},mounted(){n["Matomo"].on("widget:loaded",this.onWidgetLoaded)},unmounted(){n["Matomo"].off("widget:loaded",this.onWidgetLoaded)},methods:{translate:n["translate"],shouldDisableLink(){const e=n["Matomo"].broadcast.getValueFromUrl("disableLink");return!(!e||!e.length)||!!document.querySelector("body#standalone")},getPreviewParameters(e={}){return Object.assign(Object.assign({},e),{},{widget:"1",showtitle:"0"},this.shouldDisableLink()?{disableLink:"1"}:{})},isContainerWidget(e){return!!e.isContainer&&Array.isArray(e.widgets)},getPreviewChildren(e){var t;const o=null===(t=e.parameters)||void 0===t?void 0:t.containerId;return e.widgets.map(e=>Object.assign(Object.assign({},e),{},{parameters:Object.assign(Object.assign({},e.parameters),{},{widget:"1"},o?{containerId:o}:{})}))},onWidgetLoaded(e){var t,o;if(!this.widget||(null===(t=e.parameters)||void 0===t?void 0:t.uniqueId)!==this.widget.uniqueId)return;const i=this.$el,s=null===e||void 0===e||null===(o=e.element)||void 0===o?void 0:o[0];if(!i||!s||!i.contains(s))return;const a=i.querySelector(".widget"),n=null===a||void 0===a?void 0:a.querySelector(".widgetContent");a&&n&&window.$(n).trigger("widget:create",[{element:window.$(a)}])}}});ae.render=se;var ne=ae;const de="Dashboard.AddWidget.open";var re=Object(a["defineComponent"])({name:"AddWidgetModal",components:{MatomoModal:n["MatomoModal"],CategoryList:P,WidgetsList:Y,WidgetPreview:ne},emits:["select"],data(){return{isOpen:!1,chosenCategory:null,hoveredWidgetId:null,addedWidgetIds:new Set,existingWidgetIds:new Set}},computed:{widgets(){return n["WidgetsStore"].widgets.value||{}},categoryNames(){return Object.keys(this.widgets)},widgetsInCategory(){return this.chosenCategory&&this.widgets[this.chosenCategory]||[]},widgetsById(){return new Map(Object.values(this.widgets).flat().filter(e=>!!e.uniqueId).map(e=>[e.uniqueId,e]))},previewWidget(){return this.hoveredWidgetId&&this.widgetsById.get(this.hoveredWidgetId)||null}},methods:{translate:n["translate"],open(){const e=new Set;document.querySelectorAll("#dashboardWidgetsArea [widgetId]").forEach(t=>{const o=t.getAttribute("widgetId");o&&e.add(o)}),this.existingWidgetIds=e,this.isOpen=!0},close(){this.isOpen=!1},onClosed(){this.chosenCategory=null,this.hoveredWidgetId=null,this.addedWidgetIds=new Set,this.existingWidgetIds=new Set},onCategoryChosen(e){this.chosenCategory!==e&&(this.chosenCategory=e,this.hoveredWidgetId=null)},async focusWidgetList(){await this.$nextTick();const e=this.$refs.widgetsList;e&&"function"===typeof e.focusFirst&&e.focusFirst()},onWidgetHover(e){this.hoveredWidgetId=e},onSelect(e){const t=this.widgetsById.get(e);if(t)return this.addedWidgetIds.add(e),void this.$emit("select",t);console.warn(`Could not resolve dashboard widget "${e}" from cached metadata.`),this.close()}},mounted(){n["Matomo"].on(de,this.open)},unmounted(){n["Matomo"].off(de,this.open)}});re.render=B;var ce=re;const{$:le}=window,ue="scheduledReports.dashboardExportId";var be=Object(a["defineComponent"])({name:"DashboardSettings",components:{AddWidgetModal:ce},directives:{ExpandOnClick:n["ExpandOnClick"],Tooltips:n["Tooltips"]},data(){return{isActionDisabled:{},actionTooltips:{}}},setup(){const e=Object(a["ref"])(null);return Object(a["onMounted"])(()=>{n["Matomo"].postEvent("Dashboard.DashboardSettings.mounted",e.value),le(e.value).hide()}),{root:e}},computed:{isUserNotAnonymous(){return!!n["Matomo"].userLogin&&"anonymous"!==n["Matomo"].userLogin},isSuperUser(){return this.isUserNotAnonymous&&n["Matomo"].hasSuperUserAccess},isUserHasSomeAdminAccess(){return this.isUserNotAnonymous&&n["Matomo"].userHasSomeAdminAccess},dashboardActions(){const e={resetDashboard:"Dashboard_ResetDashboard",showChangeDashboardLayoutDialog:"Dashboard_ChangeDashboardLayout"};return this.isUserNotAnonymous&&(e.renameDashboard="Dashboard_RenameDashboard",e.removeDashboard="Dashboard_RemoveDashboard"),this.isSuperUser&&(e.setAsDefaultWidgets="Dashboard_SetAsDefaultWidgets"),this.isUserHasSomeAdminAccess&&(e.copyDashboardToUser="Dashboard_CopyDashboardToUser"),e},generalActions(){const e={};return this.isUserNotAnonymous&&(e.createDashboard="Dashboard_CreateNewDashboard"),e}},methods:{onClickAction(e,t){e.target.getAttribute("disabled")||window[t]()},onOpen(){le("#dashboardWidgetsArea").dashboard("isDefaultDashboard")?(this.isActionDisabled.removeDashboard=!0,this.actionTooltips.removeDashboard=Object(n["translate"])("Dashboard_RemoveDefaultDashboardNotPossible")):(this.isActionDisabled.removeDashboard=!1,this.actionTooltips.removeDashboard=void 0)},onExpand(e){0===e.detail&&this.$nextTick(()=>{const e=this.$refs.root.querySelector(".submenu button:not([disabled])");e&&e.focus()})},onFocusOut(e){const t=this.$refs.root,o=e.relatedTarget;o&&t.contains(o)||t.classList.remove("expanded")},onClosed(e){if(!(e instanceof KeyboardEvent))return;const t=this.$refs.expander;t&&t.focus()},openAddWidget(){this.$refs.root.classList.remove("expanded"),n["Matomo"].postEvent("Dashboard.AddWidget.open")},onWidgetSelected(e){le("#dashboardWidgetsArea").dashboard("addWidget",e.uniqueId,1,e.parameters,!0,!1)},redirectToCreateScheduledReports(){const e=Object.assign({},n["MatomoUrl"].urlParsed.value);delete e.category,delete e.subcategory,delete e.idDashboard,e.module="ScheduledReports",e.action="index";const t=Object.assign({},n["MatomoUrl"].hashParsed.value);delete t.category,delete t.subcategory,delete t.idDashboard,n["MatomoUrl"].updateUrl(e,t)},redirectToLoginPage(){const e={module:n["Matomo"].getLoginModule()};n["MatomoUrl"].updateUrl(e)},onClickExportDashboard(){if("undefined"!==typeof sessionStorage&&sessionStorage.removeItem(ue),this.isUserNotAnonymous){const e=this.getCurrentDashboardId();return null!==e&&"undefined"!==typeof sessionStorage&&sessionStorage.setItem(ue,String(e)),void this.redirectToCreateScheduledReports()}this.redirectToLoginPage()},normalizeDashboardId(e){const t=Array.isArray(e)?e[0]:e;if(null===t||void 0===t)return null;const o=String(t).trim();return/^[1-9]\d*$/.test(o)?Number(o):null},getCurrentDashboardId(){const e=this.normalizeDashboardId(n["MatomoUrl"].getSearchParam("subcategory"));if(null!==e)return e;const t=this.normalizeDashboardId(n["MatomoUrl"].urlParsed.value.idDashboard);return null!==t?t:this.normalizeDashboardId(n["MatomoUrl"].hashParsed.value.idDashboard)}}});be.render=k;var he=be; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */}})})); +//# sourceMappingURL=Dashboard.umd.min.js.map \ No newline at end of file diff --git a/plugins/Dashboard/vue/dist/umd.metadata.json b/plugins/Dashboard/vue/dist/umd.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..9ecfcc0456204cf8e8e80416ebc95ef42d4fbd28 --- /dev/null +++ b/plugins/Dashboard/vue/dist/umd.metadata.json @@ -0,0 +1,5 @@ +{ + "dependsOn": [ + "CoreHome" + ] +} \ No newline at end of file diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.less b/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.less new file mode 100644 index 0000000000000000000000000000000000000000..e66f14916708b9d604928c746b7386791bd53609 --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.less @@ -0,0 +1,232 @@ +@add-widget-padding: 16px; +@add-widget-border: 1px; +@add-widget-space-or-radius: 8px; +@add-widget-categories: 200px + @add-widget-space-or-radius + @add-widget-border; +@add-widget-widgets: 270px + @add-widget-padding + @add-widget-border; +@add-widget-preview: 407px; +@add-widget-height: 600px; +@add-widget-item-height: 32px; + + +// 60px comes from padding (32px on the left and 28px on the right) +.modal.add-widget-modal { + width: 1000px; + max-width: 90%; + height: @add-widget-height; + max-height: 90vh; + border-radius: @add-widget-space-or-radius; + + .widgetpreview-categorylist .widgetpreview-choosen, + .widgetpreview-widgetlist .widgetpreview-choosen { + background-color: @theme-color-background-tinyContrast; + + .widget-button-item, + .category-button-item { + color: @theme-color-text; + } + } + .widgetpreview-widgetlist .widgetpreview-unavailable .widget-button-item { + color: @theme-color-text-disabled; + } + // Hovering an already-added row restores the hovered/chosen text colour, so the + // row reads as focusable rather than staying greyed while it's under the cursor. + // Two classes give this higher specificity than the disabled rule above, which + // a same-element hovered+added row would otherwise win. + .widgetpreview-widgetlist .widgetpreview-choosen.widgetpreview-unavailable .widget-button-item { + color: @theme-color-text; + } +} + +.add-widget-modal { + .btn-close.modal-close { + top: 4px; + right: 4px; + width: 40px; + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + padding: 14px; + color: @theme-color-text-lighter; + } + + .add-widget-modal-content { + background: @theme-color-background-contrast; + display: flex; + flex-direction: column; + overflow-x: auto; + overflow-y: hidden; + min-width: 0; + padding: 32px 28px 0 32px; + -webkit-overflow-scrolling: touch; + } + + .add-widget-modal-title { + margin-bottom: 32px; + text-align: left; + flex: none; + font-size: 20px; + font-weight: 600; + color: @theme-color-text; + } + + .add-widget-modal-body { + display: grid; + grid-template-columns: @add-widget-categories @add-widget-widgets @add-widget-preview; + gap: @add-widget-padding; + flex: 1 1 auto; + min-height: 0; + width: max-content; + min-width: 100%; + } + + .add-widget-modal-categories, + .add-widget-modal-widgets, + .add-widget-modal-preview { + min-width: 0; + min-height: 0; + } + + .add-widget-modal-categories, + .add-widget-modal-widgets { + overflow-y: auto; + overscroll-behavior: contain; + border-right: @add-widget-border solid @theme-color-background-tinyContrast; + } + .add-widget-modal-categories { + padding-right: @add-widget-space-or-radius; + } + .add-widget-modal-widgets { + padding-right: @add-widget-padding; + } + .add-widget-modal-preview { + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + align-self: start; + max-height: 100%; + } + + // The preview widget hosts arbitrary reports; datatables and other content + // can be intrinsically wider than the 500px column. Box it in so the widget + // border stays flush with the column edges and any overflow scrolls inside + // the inner report rather than stretching the column. + .widgetpreview-preview .widget { + width: 100%; + box-sizing: border-box; + overflow: hidden; + } + + // Re-apply the global `.widget div.dataTable…` cell-level padding rules + // (CoreHome/stylesheets/dataTable/_dataTable.less:194 and :498) with higher + // specificity scoped to the preview pane, so they always take precedence + // over any layout shrinks the preview column imposes. + .widgetpreview-preview .widget .card-content div.dataTable:not(.isComparing) table.dataTable th, + .widgetpreview-preview .widget .card-content div.dataTable:not(.isComparing) table.dataTable tbody > tr > td:not(.cellSubDataTable):first-child { + padding-left: 30px !important; + } + + .add-widget-modal-preview .select-wrapper.cohorts-metric-picker { + min-width: 16rem; + } + + .widgetpreview-categorylist, + .widgetpreview-widgetlist { + width: 100%; + float: none; + margin: 0; + padding: 0; + } + + .widgetpreview-widgetlist { + font-weight: normal; + } + + .widgetpreview-preview { + float: none; + width: 100%; + margin-top: 0; + } + + .widgetTop { + cursor: pointer; + } + + // Overrides the global `ul.widgetpreview-widgetlist li { padding: 0 25px 0 5px }` + .category-list-item, + .widget-list-item { + border-radius: @add-widget-space-or-radius; + padding: 0; + } + + .widget-button-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: @add-widget-space-or-radius; + } + + .category-button-item, + .widget-button-item { + width: 100%; + min-height: @add-widget-item-height; + border: 0; + background: transparent; + box-sizing: border-box; + color: @theme-color-text-lighter; + text-align: left; + font: inherit; + cursor: inherit; + font-size: 14px; + line-height: 20px; + padding: 4px @add-widget-space-or-radius; + white-space: normal; + } + + .widgetpreview-widgetlist .widgetpreview-widgetname { + flex: 1 1 auto; + min-width: 0; + white-space: normal; + } + + // Keep the focus ring for keyboard users but suppress it after a mouse click, + // otherwise the row button keeps a dashed outline after selection and leaks + // into screenshot tests. + .widgetpreview-categorylist .category-button-item:focus:not(:focus-visible), + .widgetpreview-widgetlist .widget-button-item:focus:not(:focus-visible) { + outline: none; + } + + .widgetpreview-categorylist .category-button-item:focus-visible, + .widgetpreview-widgetlist .widget-button-item:focus-visible { + outline: 2px solid @theme-color-focus-ring; + outline-offset: -2px; + border-radius: @add-widget-space-or-radius; + } + + + .widgetpreview-choosen::after { + display: none; + } + + .widgetpreview-widgetlist .widgetpreview-add-hint { + flex: 0 0 auto; + padding: 0 6px; + color: @theme-color-text-lighter; + opacity: 0; + transition: opacity 80ms linear; + font-size: 12px; + line-height: 16px; + } + + .widgetpreview-widgetlist .widgetpreview-choosen .widgetpreview-add-hint { + opacity: 1; + } + + // Transient "added" confirmation: the leading "+" becomes a green check on the + // row just added, reverting once the hover moves elsewhere (see WidgetsList). + .widgetpreview-widgetlist .widgetpreview-add-check { + color: @color-green-matomo; + } +} diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.spec.ts b/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2cb44d02300a608ea84ea2aff7c8e93464d2388 --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.spec.ts @@ -0,0 +1,153 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { ref } from 'vue'; +import { shallowMount } from '@vue/test-utils'; + +jest.useFakeTimers(); + +const mockMatomo = { on: jest.fn(), off: jest.fn() }; + +const mockWidgets = ref>({}); + +jest.mock('CoreHome', () => ({ + Matomo: mockMatomo, + translate: (key: string) => key, + WidgetsStore: { + widgets: mockWidgets, + }, + WidgetType: {}, + MatomoModal: { template: '
    ' }, + Widget: { template: '
    ' }, +}), { virtual: true }); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const AddWidgetModal = require('./AddWidgetModal.vue').default; + +describe('Dashboard/AddWidgetModal', () => { + function mountComponent() { + return shallowMount(AddWidgetModal as any); + } + + function getHandler(eventName: string) { + return mockMatomo.on.mock.calls.find((call) => call[0] === eventName)?.[1]; + } + + beforeEach(() => { + jest.clearAllMocks(); + mockWidgets.value = {}; + }); + + it('opens when the Matomo open event fires', () => { + const wrapper = mountComponent(); + + getHandler('Dashboard.AddWidget.open')!(); + expect((wrapper.vm as any).isOpen).toBe(true); + }); + + it('snapshots the dashboard widget IDs on open', () => { + const dashboardArea = document.createElement('div'); + dashboardArea.id = 'dashboardWidgetsArea'; + ['widget.one', 'widget.two'].forEach((id) => { + const el = document.createElement('div'); + el.setAttribute('widgetId', id); + dashboardArea.appendChild(el); + }); + document.body.appendChild(dashboardArea); + + try { + const wrapper = mountComponent(); + getHandler('Dashboard.AddWidget.open')!(); + + const ids = (wrapper.vm as any).existingWidgetIds as Set; + expect(Array.from(ids).sort()).toEqual(['widget.one', 'widget.two']); + } finally { + document.body.removeChild(dashboardArea); + } + }); + + it('exposes category names directly from the WidgetsStore', () => { + mockWidgets.value = { + Visitors: [{ uniqueId: 'Widget.unique' }], + Goals: [], + }; + const wrapper = mountComponent(); + + expect((wrapper.vm as any).categoryNames).toEqual(['Visitors', 'Goals']); + }); + + it('returns the widgets of the chosen category', () => { + const widget = { uniqueId: 'Widget.unique', name: 'My Widget' }; + mockWidgets.value = { Visitors: [widget] }; + const wrapper = mountComponent(); + + (wrapper.vm as any).onCategoryChosen('Visitors'); + + expect((wrapper.vm as any).widgetsInCategory).toEqual([widget]); + expect((wrapper.vm as any).hoveredWidgetId).toBeNull(); + }); + + it('updates the preview when a widget hover is emitted', () => { + const widget = { uniqueId: 'Widget.unique', name: 'My Widget' }; + mockWidgets.value = { Visitors: [widget] }; + const wrapper = mountComponent(); + + (wrapper.vm as any).onWidgetHover('Widget.unique'); + + expect((wrapper.vm as any).previewWidget).toEqual(widget); + }); + + it('emits select and keeps the modal open when a widget is chosen', () => { + const widget = { uniqueId: 'Widget.unique', parameters: { foo: 'bar' } }; + mockWidgets.value = { Visitors: [widget] }; + const wrapper = mountComponent(); + (wrapper.vm as any).isOpen = true; + + (wrapper.vm as any).onSelect('Widget.unique'); + + expect(wrapper.emitted().select).toEqual([[widget]]); + expect((wrapper.vm as any).isOpen).toBe(true); + expect((wrapper.vm as any).addedWidgetIds.has('Widget.unique')).toBe(true); + }); + + it('closes without emitting select when the widget is missing from the store', () => { + const wrapper = mountComponent(); + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + (wrapper.vm as any).onSelect('Widget.missing'); + + expect(wrapper.emitted().select).toBeUndefined(); + expect((wrapper.vm as any).isOpen).toBe(false); + expect(warnSpy).toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); + + it('resets the chosen category, hover and added widgets when the modal closes', () => { + const wrapper = mountComponent(); + const vm = wrapper.vm as any; + vm.chosenCategory = 'Visitors'; + vm.hoveredWidgetId = 'Widget.unique'; + vm.addedWidgetIds = new Set(['Widget.unique']); + vm.existingWidgetIds = new Set(['Widget.placed']); + + vm.onClosed(); + + expect(vm.chosenCategory).toBeNull(); + expect(vm.hoveredWidgetId).toBeNull(); + expect(vm.addedWidgetIds.size).toBe(0); + expect(vm.existingWidgetIds.size).toBe(0); + }); + + it('unregisters Matomo listeners on unmount', () => { + const wrapper = mountComponent(); + + wrapper.unmount(); + + expect(mockMatomo.off).toHaveBeenCalledWith('Dashboard.AddWidget.open', expect.any(Function)); + }); +}); diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.vue b/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..d8d315dbefa6c6d98b5db15d5f71bfc70d78f394 --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/AddWidgetModal.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.spec.ts b/plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..89aaecbe91d9aebeda235bb2e0d21fa6216f8140 --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.spec.ts @@ -0,0 +1,102 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { mount } from '@vue/test-utils'; + +jest.mock('CoreHome', () => ({}), { virtual: true }); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const CategoryList = require('./CategoryList.vue').default; + +describe('Dashboard/AddWidgetModal/CategoryList', () => { + const longCategory = 'A very long category name that should wrap onto another line in the modal'; + + it('renders one
  • per category and applies the chosen class', () => { + const wrapper = mount(CategoryList as any, { + props: { + categories: [ + 'Visitors', + longCategory, + 'Live', + ], + chosenCategory: longCategory, + }, + }); + + const items = wrapper.findAll('li'); + const buttons = wrapper.findAll('li button'); + expect(items).toHaveLength(3); + expect(buttons).toHaveLength(3); + expect(items[0].classes()).not.toContain('widgetpreview-choosen'); + expect(items[1].classes()).toContain('widgetpreview-choosen'); + expect(items[2].classes()).not.toContain('widgetpreview-choosen'); + expect(buttons.map((i) => i.text())).toEqual(['Visitors', longCategory, 'Live']); + }); + + it('renders the full category text for long labels', () => { + const wrapper = mount(CategoryList as any, { + props: { + categories: [longCategory], + chosenCategory: longCategory, + }, + }); + + expect(wrapper.find('li button').text()).toBe(longCategory); + }); + + it('emits update:chosenCategory on mouseover', async () => { + const wrapper = mount(CategoryList as any, { + props: { + categories: [ + 'Visitors', + 'Goals', + ], + chosenCategory: null, + }, + }); + + await wrapper.findAll('li button')[1].trigger('mouseover'); + + expect(wrapper.emitted()['update:chosenCategory']).toEqual([['Goals']]); + }); + + describe('keyboard', () => { + it('emits update:chosenCategory and confirm on Enter', async () => { + const wrapper = mount(CategoryList as any, { + props: { categories: ['Visitors', 'Goals'], chosenCategory: null }, + }); + + await wrapper.findAll('li button')[1].trigger('keydown', { key: 'Enter' }); + + expect(wrapper.emitted()['update:chosenCategory']).toEqual([['Goals']]); + expect(wrapper.emitted().confirm).toEqual([[]]); + }); + + it('emits update:chosenCategory and confirm on Space', async () => { + const wrapper = mount(CategoryList as any, { + props: { categories: ['Visitors', 'Goals'], chosenCategory: null }, + }); + + await wrapper.findAll('li button')[0].trigger('keydown', { key: ' ' }); + + expect(wrapper.emitted()['update:chosenCategory']).toEqual([['Visitors']]); + expect(wrapper.emitted().confirm).toEqual([[]]); + }); + + it('does not emit confirm on mouse / focus paths', async () => { + const wrapper = mount(CategoryList as any, { + props: { categories: ['Visitors'], chosenCategory: null }, + }); + + await wrapper.findAll('li button')[0].trigger('mouseover'); + await wrapper.findAll('li button')[0].trigger('click'); + await wrapper.findAll('li button')[0].trigger('focus'); + + expect(wrapper.emitted().confirm).toBeUndefined(); + }); + }); +}); diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.vue b/plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.vue new file mode 100644 index 0000000000000000000000000000000000000000..f69dcafa29183eced0c62b7d1f12142671e87b91 --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/CategoryList.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.spec.ts b/plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..088000aaae3306be1100bfef051d19f1b198e9c8 --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.spec.ts @@ -0,0 +1,224 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { mount } from '@vue/test-utils'; + +const mockBroadcast = { getValueFromUrl: jest.fn((_param: string) => '') }; +const mockMatomo = { on: jest.fn(), off: jest.fn(), broadcast: mockBroadcast }; + +jest.mock('CoreHome', () => ({ + Matomo: mockMatomo, + translate: (key: string) => key, + Widget: { template: '
    ' }, + WidgetType: {}, +}), { virtual: true }); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const WidgetPreview = require('./WidgetPreview.vue').default; + +describe('Dashboard/AddWidgetModal/WidgetPreview', () => { + function getHandler(eventName: string) { + return mockMatomo.on.mock.calls.find((call) => call[0] === eventName)?.[1]; + } + + function mountComponent() { + return mount(WidgetPreview as any, { + props: { widget: { uniqueId: 'W1' } }, + attachTo: document.body, + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + mockBroadcast.getValueFromUrl.mockReturnValue(''); + // Make sure no previous test leaked a body#standalone marker. + document.body.removeAttribute('id'); + }); + + afterEach(() => { + document.body.removeAttribute('id'); + }); + + it('registers the widget:loaded handler on mount and removes it on unmount', () => { + const wrapper = mountComponent(); + + expect(mockMatomo.on).toHaveBeenCalledWith('widget:loaded', expect.any(Function)); + + wrapper.unmount(); + + expect(mockMatomo.off).toHaveBeenCalledWith('widget:loaded', expect.any(Function)); + }); + + it('forces widget=1 and showtitle=0 and does NOT set disableLink on a normal dashboard page', () => { + // Regression guards: + // - `widget=1` matches legacy widgetMenu.js so the server renders in widgetized layout. + // - `showtitle=0` suppresses the bare

    the server would otherwise emit via + // _dataTable.twig `showOnlyTitleWithoutCard`, which duplicated the modal's own + // "Widget preview" header above the preview pane. + // - `disableLink` is only forced on Widgetize embeds or body#standalone (legacy + // parity). On a normal dashboard page the preview must match the widget the user + // actually adds, so links stay enabled (disableLink absent / 0). + const incoming = { uniqueId: 'W1', parameters: { module: 'X', action: 'y' } }; + const wrapper = mount(WidgetPreview as any, { + props: { widget: incoming }, + attachTo: document.body, + }); + + const { previewWidget } = wrapper.vm as unknown as { + previewWidget: { uniqueId: string; parameters: Record }; + }; + expect(previewWidget.parameters).toEqual({ + module: 'X', + action: 'y', + widget: '1', + showtitle: '0', + }); + expect(previewWidget.parameters).not.toHaveProperty('disableLink'); + // Guard against mutating the prop: the dashboard's persisted metadata must + // not pick up the preview-only flags when the user actually adds the widget. + expect(incoming.parameters).toEqual({ module: 'X', action: 'y' }); + + wrapper.unmount(); + }); + + it('forces disableLink=1 when the URL already carries disableLink (Widgetize embed)', () => { + mockBroadcast.getValueFromUrl.mockImplementation((param: string) => ( + param === 'disableLink' ? '1' : '' + )); + const wrapper = mount(WidgetPreview as any, { + props: { widget: { uniqueId: 'W1', parameters: {} } }, + attachTo: document.body, + }); + + const { previewWidget } = wrapper.vm as unknown as { + previewWidget: { parameters: Record }; + }; + expect(previewWidget.parameters.disableLink).toBe('1'); + expect(mockBroadcast.getValueFromUrl).toHaveBeenCalledWith('disableLink'); + + wrapper.unmount(); + }); + + it('propagates widget=1 and the parent containerId into child widgets for container previews', () => { + // Regression for "widget preview not showing sub/child widgets correctly": + // container widgets must hand each child the widgetized flag and the + // parent's containerId so nested AJAX renders match the legacy widgetMenu.js + // path. The non-container shape is covered by the parameter-shape test above. + const incoming = { + uniqueId: 'GoalsOverview', + isContainer: true, + parameters: { containerId: 'GoalsOverview' }, + widgets: [ + { uniqueId: 'GoalsOverview_child1', parameters: { module: 'Goals', action: 'a' } }, + { uniqueId: 'GoalsOverview_child2', parameters: { module: 'Goals', action: 'b' } }, + ], + }; + const wrapper = mount(WidgetPreview as any, { + props: { widget: incoming }, + attachTo: document.body, + }); + + const { previewWidget } = wrapper.vm as unknown as { + previewWidget: { + widgets: Array<{ parameters: Record }>; + }; + }; + expect(previewWidget.widgets).toHaveLength(2); + expect(previewWidget.widgets[0].parameters).toEqual({ + module: 'Goals', + action: 'a', + widget: '1', + containerId: 'GoalsOverview', + }); + expect(previewWidget.widgets[1].parameters).toEqual({ + module: 'Goals', + action: 'b', + widget: '1', + containerId: 'GoalsOverview', + }); + // The original child widgets must not be mutated — the dashboard's + // persisted metadata is shared with the catalog. + expect(incoming.widgets[0].parameters).toEqual({ module: 'Goals', action: 'a' }); + expect(incoming.widgets[1].parameters).toEqual({ module: 'Goals', action: 'b' }); + + wrapper.unmount(); + }); + + it('forces disableLink=1 when the page is body#standalone', () => { + document.body.id = 'standalone'; + const wrapper = mount(WidgetPreview as any, { + props: { widget: { uniqueId: 'W1', parameters: {} } }, + attachTo: document.body, + }); + + const { previewWidget } = wrapper.vm as unknown as { + previewWidget: { parameters: Record }; + }; + expect(previewWidget.parameters.disableLink).toBe('1'); + + wrapper.unmount(); + }); + + it('triggers widget:create on the preview .widgetContent for the previewed widget', () => { + const wrapper = mountComponent(); + const root = wrapper.element as HTMLElement; + const widgetEl = root.querySelector('.widget') as HTMLElement; + const widgetContent = root.querySelector('.widgetContent') as HTMLElement; + const created: Array<{ element: JQuery }> = []; + (window as any).$(widgetContent).on('widget:create', (_evt: Event, w: { element: JQuery }) => { + created.push(w); + }); + + getHandler('widget:loaded')!({ + parameters: { uniqueId: 'W1' }, + element: (window as any).$(widgetContent), + }); + + expect(created).toHaveLength(1); + expect(created[0].element[0]).toBe(widgetEl); + + wrapper.unmount(); + }); + + it('ignores widget:loaded events whose element is outside the preview', () => { + const wrapper = mountComponent(); + const root = wrapper.element as HTMLElement; + const widgetContent = root.querySelector('.widgetContent') as HTMLElement; + const created = jest.fn(); + (window as any).$(widgetContent).on('widget:create', created); + + const detached = document.createElement('div'); + document.body.appendChild(detached); + + getHandler('widget:loaded')!({ + parameters: { uniqueId: 'W1' }, + element: (window as any).$(detached), + }); + + expect(created).not.toHaveBeenCalled(); + + document.body.removeChild(detached); + wrapper.unmount(); + }); + + it('ignores widget:loaded events whose uniqueId does not match the previewed widget', () => { + const wrapper = mountComponent(); + const root = wrapper.element as HTMLElement; + const widgetContent = root.querySelector('.widgetContent') as HTMLElement; + const created = jest.fn(); + (window as any).$(widgetContent).on('widget:create', created); + + getHandler('widget:loaded')!({ + parameters: { uniqueId: 'NestedSubWidget' }, + element: (window as any).$(widgetContent), + }); + + expect(created).not.toHaveBeenCalled(); + + wrapper.unmount(); + }); +}); diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.vue b/plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.vue new file mode 100644 index 0000000000000000000000000000000000000000..2926bd058fa7e6911e7af3b1bab21491904f6d47 --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/WidgetPreview.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.spec.ts b/plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ae292d9942c9ba13590b8daf71478a693e335bf --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.spec.ts @@ -0,0 +1,367 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { mount } from '@vue/test-utils'; + +jest.mock('CoreHome', () => ({ + translate: (key: string) => key, +}), { virtual: true }); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const WidgetsList = require('./WidgetsList.vue').default; + +const widgetVisits = { + uniqueId: 'widgetVisits', + name: 'Visits Over Time', + parameters: {}, + category: { id: 'Visitors_VisitsOverTime' }, +}; +const widgetKpi = { + uniqueId: 'widgetKpi', + name: 'KPI', + parameters: {}, + category: { id: 'General_KpiMetric' }, +}; +const widgetBlocked = { + uniqueId: 'widgetBlocked', + name: 'Already On Dashboard', + parameters: {}, + category: { id: 'Visitors' }, +}; +const widgetLongName = { + uniqueId: 'widgetLongName', + name: 'This is a very long widget name that should wrap instead of truncating in the modal', + parameters: {}, + category: { id: 'Visitors' }, +}; + +describe('Dashboard/AddWidgetModal/WidgetsList', () => { + let originalMatchMedia: typeof window.matchMedia; + + // Scoped helper for the two `supportsHover` initialization tests. Other tests + // override `wrapper.vm.supportsHover` directly after mount and don't need this. + const setAnyHoverMatch = (matches: boolean) => { + Object.defineProperty(window, 'matchMedia', { + configurable: true, + writable: true, + value: jest.fn().mockImplementation((query: string) => ({ + matches: query === '(any-hover: hover)' ? matches : false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + }; + + beforeEach(() => { + jest.useFakeTimers(); + originalMatchMedia = window.matchMedia; + }); + + afterEach(() => { + jest.useRealTimers(); + Object.defineProperty(window, 'matchMedia', { + configurable: true, + writable: true, + value: originalMatchMedia, + }); + }); + + it('renders unavailable class for widgets in existingWidgetIds, except for KPI metrics', () => { + const wrapper = mount(WidgetsList as any, { + props: { + widgets: [widgetVisits, widgetKpi, widgetBlocked], + existingWidgetIds: new Set(['widgetKpi', 'widgetBlocked']), + }, + }); + + const items = wrapper.findAll('li'); + const buttons = wrapper.findAll('li button'); + expect(items[0].classes()).not.toContain('widgetpreview-unavailable'); + expect(items[1].classes()).not.toContain('widgetpreview-unavailable'); + expect(items[2].classes()).toContain('widgetpreview-unavailable'); + expect(buttons).toHaveLength(3); + }); + + it('emits hover with a 400ms debounce', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + await wrapper.findAll('li button')[0].trigger('mouseenter'); + expect(wrapper.emitted().hover).toBeUndefined(); + + jest.advanceTimersByTime(399); + expect(wrapper.emitted().hover).toBeUndefined(); + + jest.advanceTimersByTime(1); + expect(wrapper.emitted().hover).toEqual([['widgetVisits']]); + }); + + it('cancels the debounced hover on mouseleave', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + await wrapper.findAll('li button')[0].trigger('mouseenter'); + await wrapper.findAll('li button')[0].trigger('mouseleave'); + + jest.advanceTimersByTime(500); + expect(wrapper.emitted().hover).toBeUndefined(); + }); + + it('emits select on click', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + (wrapper.vm as unknown as { supportsHover: boolean }).supportsHover = true; + + await wrapper.findAll('li button')[0].trigger('click'); + expect(wrapper.emitted().select).toEqual([['widgetVisits']]); + }); + + it('treats any-hover environments as hover-capable at initialization', () => { + setAnyHoverMatch(true); + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + expect((wrapper.vm as unknown as { supportsHover: boolean }).supportsHover).toBe(true); + expect(window.matchMedia).toHaveBeenCalledWith('(any-hover: hover)'); + }); + + it('still emits select when clicking a widget already on the dashboard', async () => { + const wrapper = mount(WidgetsList as any, { + props: { + widgets: [widgetBlocked], + existingWidgetIds: new Set(['widgetBlocked']), + }, + }); + (wrapper.vm as unknown as { supportsHover: boolean }).supportsHover = true; + + expect(wrapper.findAll('li')[0].classes()).toContain('widgetpreview-unavailable'); + + await wrapper.findAll('li button')[0].trigger('click'); + expect(wrapper.emitted().select).toEqual([['widgetBlocked']]); + }); + + it('still emits select when clicking a widget added earlier in the session', async () => { + const wrapper = mount(WidgetsList as any, { + props: { + widgets: [widgetVisits], + addedWidgets: new Set(['widgetVisits']), + }, + }); + (wrapper.vm as unknown as { supportsHover: boolean }).supportsHover = true; + + expect(wrapper.findAll('li')[0].classes()).toContain('widgetpreview-unavailable'); + + await wrapper.findAll('li button')[0].trigger('click'); + expect(wrapper.emitted().select).toEqual([['widgetVisits']]); + }); + + it('renders the add hint with a "+" symbol and the shared translation key', () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + const hint = wrapper.find('.widgetpreview-add-hint'); + expect(hint.find('.widgetpreview-add-plus').exists()).toBe(true); + expect(hint.find('.icon-ok').exists()).toBe(false); + expect(hint.text()).toBe('+ General_Add'); + }); + + it('swaps the "+" for a green check on the just-added row and reverts when the hover moves', async () => { + const wrapper = mount(WidgetsList as any, { + props: { + widgets: [widgetVisits, widgetKpi], + chosenWidgetId: 'widgetVisits', + }, + }); + (wrapper.vm as unknown as { supportsHover: boolean }).supportsHover = true; + + await wrapper.findAll('li button')[0].trigger('click'); + expect(wrapper.emitted().select).toEqual([['widgetVisits']]); + + let hint = wrapper.findAll('li')[0].find('.widgetpreview-add-hint'); + expect(hint.find('.icon-ok').exists()).toBe(true); + expect(hint.find('.widgetpreview-add-plus').exists()).toBe(false); + expect(hint.text()).toBe('General_Added'); + + // Hovering another widget reverts the check and label back to "+ Add". + await wrapper.setProps({ chosenWidgetId: 'widgetKpi' }); + + hint = wrapper.findAll('li')[0].find('.widgetpreview-add-hint'); + expect(hint.find('.icon-ok').exists()).toBe(false); + expect(hint.find('.widgetpreview-add-plus').exists()).toBe(true); + expect(hint.text()).toBe('+ General_Add'); + }); + + it('renders the full widget name text for long labels', () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetLongName] }, + }); + + expect(wrapper.find('.widgetpreview-widgetname').text()).toBe(widgetLongName.name); + }); + + describe('keyboard navigation', () => { + it('emits hover with the same 400ms debounce on focus', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + await wrapper.findAll('li button')[0].trigger('focus'); + expect(wrapper.emitted().hover).toBeUndefined(); + + jest.advanceTimersByTime(399); + expect(wrapper.emitted().hover).toBeUndefined(); + + jest.advanceTimersByTime(1); + expect(wrapper.emitted().hover).toEqual([['widgetVisits']]); + }); + + it('cancels the debounced hover on blur', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + await wrapper.findAll('li button')[0].trigger('focus'); + await wrapper.findAll('li button')[0].trigger('blur'); + + jest.advanceTimersByTime(500); + expect(wrapper.emitted().hover).toBeUndefined(); + }); + + it('keeps the pending preview alive on blur for unavailable rows', async () => { + // Mirrors the mouseleave carve-out: leaving an unavailable row should still + // let the user see the preview rather than swallowing it. + const wrapper = mount(WidgetsList as any, { + props: { + widgets: [widgetBlocked], + existingWidgetIds: new Set(['widgetBlocked']), + }, + }); + + await wrapper.findAll('li button')[0].trigger('focus'); + await wrapper.findAll('li button')[0].trigger('blur'); + + jest.advanceTimersByTime(400); + expect(wrapper.emitted().hover).toEqual([['widgetBlocked']]); + }); + + it('emits select on Enter and clears any pending preview', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + await wrapper.findAll('li button')[0].trigger('focus'); + await wrapper.findAll('li button')[0].trigger('keydown', { key: 'Enter' }); + + expect(wrapper.emitted().select).toEqual([['widgetVisits']]); + jest.advanceTimersByTime(500); + expect(wrapper.emitted().hover).toBeUndefined(); + }); + + it('emits select on Space', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + await wrapper.findAll('li button')[0].trigger('keydown', { key: ' ' }); + expect(wrapper.emitted().select).toEqual([['widgetVisits']]); + }); + + it('emits select on Enter immediately on no-hover devices, bypassing the touch double-tap branch', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + (wrapper.vm as unknown as { supportsHover: boolean }).supportsHover = false; + + await wrapper.findAll('li button')[0].trigger('keydown', { key: 'Enter' }); + + expect(wrapper.emitted().select).toEqual([['widgetVisits']]); + expect(wrapper.emitted().hover).toBeUndefined(); + }); + + it('focusFirst() focuses the first widget row', () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits, widgetKpi] }, + attachTo: document.body, + }); + + (wrapper.vm as unknown as { focusFirst: () => void }).focusFirst(); + + expect(document.activeElement).toBe(wrapper.findAll('li button')[0].element); + wrapper.unmount(); + }); + + it('focusFirst() is a no-op when the widget list is empty', () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [] }, + attachTo: document.body, + }); + + expect(() => { + (wrapper.vm as unknown as { focusFirst: () => void }).focusFirst(); + }).not.toThrow(); + wrapper.unmount(); + }); + }); + + describe('on touch / no-hover devices', () => { + it('emits hover on the first tap of a new row instead of select', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + (wrapper.vm as unknown as { supportsHover: boolean }).supportsHover = false; + + await wrapper.findAll('li button')[0].trigger('click'); + + expect(wrapper.emitted().hover).toEqual([['widgetVisits']]); + expect(wrapper.emitted().select).toBeUndefined(); + }); + + it('treats no-hover environments as touch-like at initialization', () => { + setAnyHoverMatch(false); + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits] }, + }); + + expect((wrapper.vm as unknown as { supportsHover: boolean }).supportsHover).toBe(false); + expect(window.matchMedia).toHaveBeenCalledWith('(any-hover: hover)'); + }); + + it('emits select on the second tap of the already-chosen row', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits], chosenWidgetId: 'widgetVisits' }, + }); + (wrapper.vm as unknown as { supportsHover: boolean }).supportsHover = false; + + await wrapper.findAll('li button')[0].trigger('click'); + + expect(wrapper.emitted().select).toEqual([['widgetVisits']]); + expect(wrapper.emitted().hover).toBeUndefined(); + }); + + it('emits hover (not select) when tapping a different row than the chosen one', async () => { + const wrapper = mount(WidgetsList as any, { + props: { widgets: [widgetVisits, widgetKpi], chosenWidgetId: 'widgetVisits' }, + }); + (wrapper.vm as unknown as { supportsHover: boolean }).supportsHover = false; + + await wrapper.findAll('li button')[1].trigger('click'); + + expect(wrapper.emitted().hover).toEqual([['widgetKpi']]); + expect(wrapper.emitted().select).toBeUndefined(); + }); + }); +}); diff --git a/plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.vue b/plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.vue new file mode 100644 index 0000000000000000000000000000000000000000..00d0f824d8dd5a60ce7d62440217b675f6768626 --- /dev/null +++ b/plugins/Dashboard/vue/src/AddWidgetModal/WidgetsList.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/plugins/Dashboard/vue/src/Dashboard/Dashboard.store.ts b/plugins/Dashboard/vue/src/Dashboard/Dashboard.store.ts new file mode 100644 index 0000000000000000000000000000000000000000..b12f15661afe36923efd4d3e25141d108f61b18c --- /dev/null +++ b/plugins/Dashboard/vue/src/Dashboard/Dashboard.store.ts @@ -0,0 +1,76 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { + computed, + readonly, + reactive, + DeepReadonly, +} from 'vue'; +import { AjaxHelper } from 'CoreHome'; +import { DashboardLayout, Dashboard } from '../types'; + +interface DashboardStoreState { + dashboards: Dashboard[]; +} + +class DashboardStore { + private privateState = reactive({ + dashboards: [], + }); + + readonly state = computed(() => readonly(this.privateState)); + + readonly dashboards = computed(() => this.state.value.dashboards); + + private dashboardsPromise: Promise>|null = null; + + getDashboard(dashboardId: string|number) { + return this.getAllDashboards().then( + (dashboards) => dashboards.find( + (b) => parseInt(`${b.id}`, 10) === parseInt(`${dashboardId}`, 10), + ), + ); + } + + getDashboardLayout(dashboardId: string|number): Promise { + return AjaxHelper.fetch( + { + module: 'Dashboard', + action: 'getDashboardLayout', + idDashboard: dashboardId, + }, + { + withTokenInUrl: true, + }, + ); + } + + reloadAllDashboards(): ReturnType { + this.dashboardsPromise = null; + return this.getAllDashboards(); + } + + getAllDashboards(): Promise> { + if (!this.dashboardsPromise) { + this.dashboardsPromise = AjaxHelper.fetch({ + method: 'Dashboard.getDashboards', + filter_limit: '-1', + }).then((response) => { + if (response) { + this.privateState.dashboards = response; + } + + return this.dashboards.value; + }); + } + + return this.dashboardsPromise; + } +} + +export default new DashboardStore(); diff --git a/plugins/Dashboard/vue/src/Dashboard/Dashboard.ts b/plugins/Dashboard/vue/src/Dashboard/Dashboard.ts new file mode 100644 index 0000000000000000000000000000000000000000..2111d62e0ef20366911d5999d6565c7bf6282d62 --- /dev/null +++ b/plugins/Dashboard/vue/src/Dashboard/Dashboard.ts @@ -0,0 +1,120 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { DirectiveBinding, watch } from 'vue'; +import { Matomo, MatomoUrl } from 'CoreHome'; +import DashboardStore from './Dashboard.store'; +import { Dashboard, DashboardLayout } from '../types'; + +interface DashboardDirectiveArgs { + idDashboard: string|number; + layout?: unknown; +} + +const { $ } = window; + +function renderDashboard( + dashboardId: string|number, + dashboard: Dashboard, + layout: DashboardLayout, +) { + const $settings = $('.dashboardSettings'); + + $settings.show(); + window.initTopControls(); + + // Embed dashboard / exported as widget + if (!$('#topBars').length) { + $settings.after($('#Dashboard')); + $('#Dashboard ul li').removeClass('active'); + $(`#Dashboard_embeddedIndex_${dashboardId}`).addClass('active'); + } + + window.widgetsHelper.getAvailableWidgets(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ($('#dashboardWidgetsArea') as any) + .off('dashboardempty', window.showEmptyDashboardNotification) + .on('dashboardempty', window.showEmptyDashboardNotification) + .dashboard({ + idDashboard: dashboardId, + layout, + name: dashboard ? dashboard.name : '', + }); + + const divElements = $('#columnPreview').find('>div'); + divElements.each(function eachPreview() { + const width: string[] = []; + $('div', this).each(function eachDiv() { + width.push(this.className.replace(/width-/, '')); + }); + $(this).attr('layout', width.join('-')); + }); + + divElements.off('click.renderDashboard'); + divElements.on('click.renderDashboard', function onRenderDashboard() { + divElements.removeClass('choosen'); + $(this).addClass('choosen'); + }); +} + +function fetchDashboard(dashboardId: string|number) { + return new Promise((resolve) => setTimeout(resolve)).then( + () => Promise.resolve(window.widgetsHelper.firstGetAvailableWidgetsCall), + ).then(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dashboardElement = $('#dashboardWidgetsArea') as any; + dashboardElement.dashboard('destroyWidgets'); + dashboardElement.empty(); + + return Promise.all([ + DashboardStore.getDashboard(dashboardId), + DashboardStore.getDashboardLayout(dashboardId), + ]); + }).then(([dashboard, layout]) => new Promise((resolve) => { + $(() => { + renderDashboard(dashboardId, dashboard as Dashboard, layout as DashboardLayout); + resolve(); + }); + })); +} + +function clearDashboard() { + $('.top_controls .dashboard-manager').hide(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ($('#dashboardWidgetsArea') as any).dashboard('destroy'); +} + +function onLocationChange(parsed: (typeof MatomoUrl)['urlParsed']['value']) { + if (parsed.module !== 'Widgetize' && parsed.category !== 'Dashboard_Dashboard') { + // we remove the dashboard only if we no longer show a dashboard. + clearDashboard(); + } +} + +function onLoadDashboard(idDashboard: string|number) { + fetchDashboard(idDashboard); +} + +export default { + mounted(el: HTMLElement, binding: DirectiveBinding): void { + fetchDashboard(binding.value.idDashboard); + + watch(() => MatomoUrl.parsed.value, (parsed) => { + onLocationChange(parsed); + }); + + // ensure to not bind the event multiple times + Matomo.off('Dashboard.loadDashboard', onLoadDashboard); + Matomo.on('Dashboard.loadDashboard', onLoadDashboard); + }, + unmounted(): void { + onLocationChange(MatomoUrl.parsed.value); + + Matomo.off('Dashboard.loadDashboard', onLoadDashboard); + }, +}; diff --git a/plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.spec.ts b/plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca4a26d2149bbadfb3e40961052e0b8815d3fc51 --- /dev/null +++ b/plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.spec.ts @@ -0,0 +1,364 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { shallowMount } from '@vue/test-utils'; + +type PlainObject = Record; + +const mockRootJQuery = { + hide: jest.fn(), + dashboard: jest.fn(), + find: jest.fn(() => ({ length: 0 })), +}; + +const mockDollar = jest.fn(() => mockRootJQuery); + +const testWindow = window as any; +testWindow.$ = mockDollar; +testWindow.jQuery = mockDollar; +testWindow.widgetsHelper = { + getWidgetObjectFromUniqueId: jest.fn(), +}; + +const mockUpdateUrl = jest.fn(); +const mockGetSearchParam = jest.fn(); +const mockGetLoginModule = jest.fn(() => 'Login'); +const DASHBOARD_EXPORT_STORAGE_KEY = 'scheduledReports.dashboardExportId'; + +const mockMatomo = { + userLogin: 'admin', + hasSuperUserAccess: false, + userHasSomeAdminAccess: false, + postEvent: jest.fn(), + on: jest.fn(), + off: jest.fn(), + getLoginModule: mockGetLoginModule, +}; + +const mockMatomoUrl = { + urlParsed: { value: {} as PlainObject }, + hashParsed: { value: {} as PlainObject }, + getSearchParam: mockGetSearchParam, + updateUrl: mockUpdateUrl, +}; + +jest.mock('CoreHome', () => ({ + Matomo: mockMatomo, + MatomoUrl: mockMatomoUrl, + translate: (key: string) => key, + ExpandOnClick: {}, + Tooltips: {}, + WidgetType: {}, +}), { virtual: true }); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const DashboardSettings = require('./DashboardSettings.vue').default; + +describe('Dashboard/DashboardSettings export navigation', () => { + function mountComponent() { + return shallowMount(DashboardSettings as any, { + global: { + mocks: { + translate: (key: string) => key, + }, + }, + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + sessionStorage.clear(); + + mockMatomo.userLogin = 'admin'; + mockMatomoUrl.urlParsed.value = {} as PlainObject; + mockMatomoUrl.hashParsed.value = {} as PlainObject; + mockGetSearchParam.mockReturnValue(''); + mockGetLoginModule.mockReturnValue('Login'); + }); + + describe('#getCurrentDashboardId()', () => { + it('returns null when no dashboard id can be resolved', () => { + mockGetSearchParam.mockReturnValue(''); + const wrapper = mountComponent(); + + expect((wrapper.vm as any).getCurrentDashboardId()).toBeNull(); + }); + + it('returns a number when subcategory is numeric', () => { + mockGetSearchParam.mockReturnValue('7'); + const wrapper = mountComponent(); + + expect((wrapper.vm as any).getCurrentDashboardId()).toBe(7); + }); + + it('falls back to query idDashboard when subcategory is missing', () => { + mockGetSearchParam.mockReturnValue(''); + mockMatomoUrl.urlParsed.value = { idDashboard: '9' }; + const wrapper = mountComponent(); + + expect((wrapper.vm as any).getCurrentDashboardId()).toBe(9); + }); + + it('falls back to hash idDashboard when subcategory and query idDashboard are missing', () => { + mockGetSearchParam.mockReturnValue(''); + mockMatomoUrl.urlParsed.value = {}; + mockMatomoUrl.hashParsed.value = { idDashboard: '13' }; + const wrapper = mountComponent(); + + expect((wrapper.vm as any).getCurrentDashboardId()).toBe(13); + }); + + it('prefers subcategory over idDashboard', () => { + mockGetSearchParam.mockReturnValue('7'); + mockMatomoUrl.urlParsed.value = { idDashboard: '9' }; + mockMatomoUrl.hashParsed.value = { idDashboard: '13' }; + const wrapper = mountComponent(); + + expect((wrapper.vm as any).getCurrentDashboardId()).toBe(7); + }); + + it('falls back to query idDashboard when subcategory is invalid', () => { + mockGetSearchParam.mockReturnValue('foo'); + mockMatomoUrl.urlParsed.value = { idDashboard: '9' }; + const wrapper = mountComponent(); + + expect((wrapper.vm as any).getCurrentDashboardId()).toBe(9); + }); + + it.each(['', 'foo', '1.5', '0', '-1'])( + 'returns null for invalid dashboard id value "%s"', + (invalidValue) => { + mockGetSearchParam.mockReturnValue(invalidValue); + mockMatomoUrl.urlParsed.value = {}; + mockMatomoUrl.hashParsed.value = {}; + const wrapper = mountComponent(); + + expect((wrapper.vm as any).getCurrentDashboardId()).toBeNull(); + }, + ); + }); + + describe('#normalizeDashboardId()', () => { + it.each([ + ['1', 1], + [1, 1], + ['001', null], + [' 7 ', 7], + [['8'], 8], + [[], null], + ['1.5', null], + ['0', null], + ['-3', null], + ['foo', null], + [null, null], + [undefined, null], + ])('normalizes %p to %p', (input, expected) => { + const wrapper = mountComponent(); + + expect((wrapper.vm as any).normalizeDashboardId(input)).toBe(expected); + }); + }); + + describe('#onClickExportDashboard()', () => { + it('stores a valid query dashboard id and redirects authenticated users', () => { + mockMatomo.userLogin = 'admin'; + mockGetSearchParam.mockReturnValue(''); + mockMatomoUrl.urlParsed.value = { idDashboard: '11' }; + const wrapper = mountComponent(); + const vm = wrapper.vm as any; + + const redirectToCreateScheduledReportsSpy = jest.spyOn(vm, 'redirectToCreateScheduledReports'); + + vm.onClickExportDashboard(); + + expect(redirectToCreateScheduledReportsSpy).toHaveBeenCalledTimes(1); + expect(redirectToCreateScheduledReportsSpy).toHaveBeenCalledWith(); + expect(sessionStorage.getItem(DASHBOARD_EXPORT_STORAGE_KEY)).toBe('11'); + }); + + it('stores a valid hash dashboard id and redirects authenticated users', () => { + mockMatomo.userLogin = 'admin'; + mockGetSearchParam.mockReturnValue(''); + mockMatomoUrl.urlParsed.value = {}; + mockMatomoUrl.hashParsed.value = { idDashboard: '17' }; + const wrapper = mountComponent(); + const vm = wrapper.vm as any; + + const redirectToCreateScheduledReportsSpy = jest.spyOn(vm, 'redirectToCreateScheduledReports'); + + vm.onClickExportDashboard(); + + expect(redirectToCreateScheduledReportsSpy).toHaveBeenCalledTimes(1); + expect(redirectToCreateScheduledReportsSpy).toHaveBeenCalledWith(); + expect(sessionStorage.getItem(DASHBOARD_EXPORT_STORAGE_KEY)).toBe('17'); + }); + + it('does not store a dashboard id when all candidates are invalid', () => { + mockMatomo.userLogin = 'admin'; + mockGetSearchParam.mockReturnValue('foo'); + mockMatomoUrl.urlParsed.value = { idDashboard: '0' }; + mockMatomoUrl.hashParsed.value = { idDashboard: 'x' }; + const wrapper = mountComponent(); + const vm = wrapper.vm as any; + + const redirectToCreateScheduledReportsSpy = jest.spyOn(vm, 'redirectToCreateScheduledReports'); + + vm.onClickExportDashboard(); + + expect(redirectToCreateScheduledReportsSpy).toHaveBeenCalledTimes(1); + expect(redirectToCreateScheduledReportsSpy).toHaveBeenCalledWith(); + expect(sessionStorage.getItem(DASHBOARD_EXPORT_STORAGE_KEY)).toBeNull(); + }); + }); + + describe('#openAddWidget()', () => { + it('closes the expanded dropdown and posts the add-widget open event', () => { + const wrapper = mountComponent(); + const root = wrapper.element as HTMLElement; + root.classList.add('expanded'); + + (wrapper.vm as any).openAddWidget(); + + expect(root.classList.contains('expanded')).toBe(false); + expect(mockMatomo.postEvent).toHaveBeenCalledWith('Dashboard.AddWidget.open'); + }); + }); + + describe('#getCurrentDashboardId() legacy invalid subcategory', () => { + it('returns null when subcategory and fallbacks are not numeric', () => { + mockGetSearchParam.mockReturnValue('foo'); + mockMatomoUrl.urlParsed.value = {}; + mockMatomoUrl.hashParsed.value = {}; + const wrapper = mountComponent(); + + expect((wrapper.vm as any).getCurrentDashboardId()).toBeNull(); + }); + }); + + describe('#redirectToCreateScheduledReports()', () => { + it('removes dashboard-specific params and redirects to ScheduledReports without dashboard id', () => { + mockMatomoUrl.urlParsed.value = { + module: 'Dashboard', + action: 'embeddedIndex', + category: 'General_Dashboard', + subcategory: '12', + idDashboard: '12', + idSite: '1', + }; + mockMatomoUrl.hashParsed.value = { + category: 'General_Dashboard', + subcategory: '12', + idDashboard: '12', + period: 'day', + date: 'today', + }; + + const wrapper = mountComponent(); + (wrapper.vm as any).redirectToCreateScheduledReports(); + + expect(mockUpdateUrl).toHaveBeenCalledTimes(1); + expect(mockUpdateUrl).toHaveBeenCalledWith( + { + module: 'ScheduledReports', + action: 'index', + idSite: '1', + }, + { + period: 'day', + date: 'today', + }, + ); + }); + + it('does not include idDashboard when redirecting without dashboard state in the URL', () => { + mockMatomoUrl.urlParsed.value = { + module: 'Dashboard', + subcategory: '12', + idSite: '1', + }; + mockMatomoUrl.hashParsed.value = { + period: 'week', + }; + + const wrapper = mountComponent(); + (wrapper.vm as any).redirectToCreateScheduledReports(null); + + expect(mockUpdateUrl).toHaveBeenCalledTimes(1); + expect(mockUpdateUrl).toHaveBeenCalledWith( + { + module: 'ScheduledReports', + action: 'index', + idSite: '1', + }, + { + period: 'week', + }, + ); + }); + }); + + describe('#redirectToLoginPage()', () => { + it('uses Matomo.getLoginModule for redirect', () => { + mockGetLoginModule.mockReturnValue('CustomLogin'); + + const wrapper = mountComponent(); + (wrapper.vm as any).redirectToLoginPage(); + + expect(mockGetLoginModule).toHaveBeenCalledTimes(1); + expect(mockUpdateUrl).toHaveBeenCalledTimes(1); + expect(mockUpdateUrl).toHaveBeenCalledWith({ module: 'CustomLogin' }); + }); + }); + + describe('#onClickExportDashboard() existing behavior', () => { + it('redirects authenticated users to create scheduled report with dashboard id', () => { + mockMatomo.userLogin = 'admin'; + const wrapper = mountComponent(); + const vm = wrapper.vm as any; + + const redirectToCreateScheduledReportsSpy = jest.spyOn(vm, 'redirectToCreateScheduledReports'); + const redirectToLoginPageSpy = jest.spyOn(vm, 'redirectToLoginPage'); + jest.spyOn(vm, 'getCurrentDashboardId').mockReturnValue(3); + + vm.onClickExportDashboard(); + + expect(redirectToCreateScheduledReportsSpy).toHaveBeenCalledTimes(1); + expect(redirectToCreateScheduledReportsSpy).toHaveBeenCalledWith(); + expect(sessionStorage.getItem(DASHBOARD_EXPORT_STORAGE_KEY)).toBe('3'); + expect(redirectToLoginPageSpy).not.toHaveBeenCalled(); + }); + + it('redirects anonymous users to login page without preserving the dashboard id in session', () => { + mockMatomo.userLogin = 'anonymous'; + sessionStorage.setItem(DASHBOARD_EXPORT_STORAGE_KEY, '99'); + const wrapper = mountComponent(); + const vm = wrapper.vm as any; + + const redirectToCreateScheduledReportsSpy = jest.spyOn(vm, 'redirectToCreateScheduledReports'); + const redirectToLoginPageSpy = jest.spyOn(vm, 'redirectToLoginPage'); + + vm.onClickExportDashboard(); + + expect(redirectToCreateScheduledReportsSpy).not.toHaveBeenCalled(); + expect(redirectToLoginPageSpy).toHaveBeenCalledTimes(1); + expect(sessionStorage.getItem(DASHBOARD_EXPORT_STORAGE_KEY)).toBeNull(); + }); + }); + + describe('export menu click', () => { + it('triggers export navigation method when export menu item is clicked', async () => { + mockMatomo.userLogin = 'admin'; + const wrapper = mountComponent(); + const vm = wrapper.vm as any; + const onClickExportDashboardSpy = jest.spyOn(vm, 'onClickExportDashboard'); + + await wrapper.find('.exportDashboard').trigger('click'); + + expect(onClickExportDashboardSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.vue b/plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.vue new file mode 100644 index 0000000000000000000000000000000000000000..6b376a5bf2f3b5ffdbaf40de5a23030e74332d59 --- /dev/null +++ b/plugins/Dashboard/vue/src/DashboardSettings/DashboardSettings.vue @@ -0,0 +1,329 @@ + + + + + diff --git a/plugins/Dashboard/vue/src/index.ts b/plugins/Dashboard/vue/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e65674a0ea6db3aae87b1551d2ef091beabe05d0 --- /dev/null +++ b/plugins/Dashboard/vue/src/index.ts @@ -0,0 +1,10 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +export { default as DashboardStore } from './Dashboard/Dashboard.store'; +export { default as Dashboard } from './Dashboard/Dashboard'; +export { default as DashboardSettings } from './DashboardSettings/DashboardSettings.vue'; diff --git a/plugins/Dashboard/vue/src/types.ts b/plugins/Dashboard/vue/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ffaf781c004814c0c111641062e25f5d4de5a28 --- /dev/null +++ b/plugins/Dashboard/vue/src/types.ts @@ -0,0 +1,28 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { WidgetType } from 'CoreHome'; + +interface WidgetRef { + module: string; + action: string; +} + +export interface Dashboard { + id: string|number; + name: string; + widgets: WidgetRef[]; +} + +interface DashboardLayoutConfig { + layout: string; +} + +export interface DashboardLayout { + columns: WidgetType[][]; + config: DashboardLayoutConfig; +} diff --git a/plugins/DeviceDetectorCache/.github/workflows/matomo-ai-checklist.yml b/plugins/DeviceDetectorCache/.github/workflows/matomo-ai-checklist.yml new file mode 100644 index 0000000000000000000000000000000000000000..cc2c022da6f944b9683668c262330ca060d15f8b --- /dev/null +++ b/plugins/DeviceDetectorCache/.github/workflows/matomo-ai-checklist.yml @@ -0,0 +1,32 @@ +# Action for running tests + +name: AI Checklist + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + actions: read + checks: none + contents: none + deployments: none + issues: none + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: none + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + AiChecklist: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + steps: + - name: Run tests + uses: matomo-org/github-action-checklist-gate@main diff --git a/plugins/DeviceDetectorCache/.github/workflows/matomo-tests.yml b/plugins/DeviceDetectorCache/.github/workflows/matomo-tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..cae01543ce8799e1f501142a7fff3972e8b6907f --- /dev/null +++ b/plugins/DeviceDetectorCache/.github/workflows/matomo-tests.yml @@ -0,0 +1,57 @@ +# Action for running tests +# This file has been automatically created. +# To recreate it you can run this command +# ./console generate:test-action --plugin="DeviceDetectorCache" --php-versions="matomo5_min_php,matomo5_max_php" --schedule-cron="15 3 * * 6" + +name: Plugin DeviceDetectorCache Tests + +on: + pull_request: + types: [opened, synchronize] + push: + branches: + - '**.x-dev' + workflow_dispatch: + schedule: + - cron: "15 3 * * 6" + +permissions: + actions: read + checks: none + contents: read + deployments: none + issues: read + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: none + +concurrency: + group: php-${{ github.ref }} + cancel-in-progress: true + +jobs: + PluginTests: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: [ 'matomo5_min_php', 'matomo5_max_php' ] + target: ['minimum_required_matomo', 'maximum_supported_matomo'] + steps: + - uses: actions/checkout@v3 + with: + lfs: true + persist-credentials: false + - name: Install package ripgrep + run: sudo apt-get install ripgrep + - name: Run tests + uses: matomo-org/github-action-tests@main + with: + plugin-name: 'DeviceDetectorCache' + php-version: ${{ matrix.php }} + test-type: 'PluginTests' + matomo-test-branch: ${{ matrix.target }} + artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} + upload-artifacts: ${{ matrix.php == 'matomo5_min_php' && matrix.target == 'maximum_supported_matomo' }} diff --git a/plugins/DeviceDetectorCache/.github/workflows/phpcs.yml b/plugins/DeviceDetectorCache/.github/workflows/phpcs.yml new file mode 100644 index 0000000000000000000000000000000000000000..e41da57b0ab423e26baae89ab121279b6f439f2f --- /dev/null +++ b/plugins/DeviceDetectorCache/.github/workflows/phpcs.yml @@ -0,0 +1,43 @@ +name: PHPCS check + +on: pull_request + +permissions: + actions: read + checks: read + contents: read + deployments: none + issues: read + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: read + +jobs: + phpcs: + name: PHPCS + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + lfs: false + persist-credentials: false + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: cs2pr + - name: Install dependencies + run: + composer init --name=matomo/devicedetectorcache --quiet; + composer --no-plugins config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true -n; + composer config repositories.matomo-coding-standards vcs https://github.com/matomo-org/matomo-coding-standards -n; + composer require matomo-org/matomo-coding-standards:dev-master; + composer install --dev --prefer-dist --no-progress --no-suggest + - name: Check PHP code styles + id: phpcs + run: ./vendor/bin/phpcs --report-full --standard=phpcs.xml --report-checkstyle=./phpcs-report.xml + - name: Show PHPCS results in PR + if: ${{ always() && steps.phpcs.outcome == 'failure' }} + run: cs2pr ./phpcs-report.xml --prepend-filename diff --git a/plugins/DeviceDetectorCache/.gitignore b/plugins/DeviceDetectorCache/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4c0a2a6b98cf71c03d91748d13363c04c374ce13 --- /dev/null +++ b/plugins/DeviceDetectorCache/.gitignore @@ -0,0 +1,2 @@ +tests/System/processed +tests/Integration/testcache \ No newline at end of file diff --git a/plugins/DeviceDetectorCache/CHANGELOG.md b/plugins/DeviceDetectorCache/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..a45c0b9c198e65345a0635ca3f3ff99ac0183abc --- /dev/null +++ b/plugins/DeviceDetectorCache/CHANGELOG.md @@ -0,0 +1,49 @@ +## Changelog + +5.0.3 +- Added plugin category for Marketplace + +5.0.2 +- Fixes errors due to incorrect default values + +5.0.1 +- Fixes errors from undefined indexes + +5.0.0 +- Compatibility with Matomo 5.X + +4.3.0 +- Compatibility with Matomo 4.12, and Adjustments for client hints support + +4.2.3 +- Fixed parsers not being set when cache is present + +4.2.2 +- Compatibility with Matomo 4 + +4.2.1 +- Compatibility with PHP DI 6 + +4.2.0 +- Performance improvements + +4.1.0 +- Changed how this command works. It now requires a configuration. + +4.0.0 +- Compatibility with Matomo 4.X + +3.1.0 +- Changed how this command works. It now requires a configuration. + +3.0.3 +- Add more cache entries + +3.0.2 +- Add more cache entries + +3.0.1 +- Add more cache entries + +3.0.0 +- Initial version diff --git a/plugins/DeviceDetectorCache/CachedEntry.php b/plugins/DeviceDetectorCache/CachedEntry.php new file mode 100644 index 0000000000000000000000000000000000000000..5291e71af161df3c36ac2ef82a106c0659fe4918 --- /dev/null +++ b/plugins/DeviceDetectorCache/CachedEntry.php @@ -0,0 +1,192 @@ +bot = $values['bot'] ?? null; + $this->brand = $values['brand'] ?? ''; + $this->client = $values['client'] ?? null; + $this->device = $values['device'] ?? null; + $this->model = $values['model'] ?? ''; + $this->os = $values['os'] ?? null; + + // Or cached entries only use the useragents, so if we have some client hints provided, + // We use some special parsers, which use the cached user agent result and parses it again using client hints + if (!empty($clientHints) && !empty($values['client']['type']) && $values['client']['type'] === 'browser') { + $browserParser = new CachedBrowserParser($userAgent, $clientHints); + $browserParser->setCachedResult($this->client); + $this->client = $browserParser->parse(); + } + + if (!empty($clientHints)) { + $osParser = new CachedOperatingSystemParser($userAgent, $clientHints); + $osParser->setCachedResult($this->os); + $this->os = $osParser->parse(); + } + } + + public static function getCached(string $userAgent): ?array + { + // we check if file exists and include the file here directly as it needs to be kind of atomic... + // if we only checked if file exists, and then choose to use cached entry which would then include the file, + // then there's a risk that between the file_exists and the include the cache file was removed + $path = self::getCachePath($userAgent); + $exists = file_exists($path); + if ($exists) { + $values = @include($path); + if (!empty($values) && is_array($values) && isset($values['os'])) { + return $values; + } + } + + return null; + } + + public static function writeToCache(string $userAgent): ?string + { + if (self::getCached($userAgent)) { + return null; // already cached + } + + if (empty(self::$customCache)) { + self::$customCache = StaticContainer::get('DeviceDetector\Cache\Cache'); + } + + // we don't use device detector factory because this way we can cache the cache instance and + // lower memory since the factory would store an instance of every user agent in a static variable + $deviceDetector = new DeviceDetector($userAgent); + $deviceDetector->discardBotInformation(); + $deviceDetector->setCache(self::$customCache); + $deviceDetector->parse(); + + $outputArray = [ + 'bot' => $deviceDetector->getBot(), + 'brand' => $deviceDetector->getBrandName(), + 'client' => $deviceDetector->getClient(), + 'device' => $deviceDetector->getDevice(), + 'model' => $deviceDetector->getModel(), + 'os' => $deviceDetector->getOs() + ]; + $outputPath = self::getCachePath($userAgent, true); + $content = " $time) { + if ($numFilesDeleted > $numFilesToDelete) { + break; + } else { + Filesystem::deleteFileIfExists($file); + $numFilesDeleted++; + } + } + } +} diff --git a/plugins/DeviceDetectorCache/Commands/WarmDeviceDetectorCache.php b/plugins/DeviceDetectorCache/Commands/WarmDeviceDetectorCache.php new file mode 100644 index 0000000000000000000000000000000000000000..336d9c4972db34ff43999ca238900fb359a164f5 --- /dev/null +++ b/plugins/DeviceDetectorCache/Commands/WarmDeviceDetectorCache.php @@ -0,0 +1,189 @@ +config = StaticContainer::get(Configuration::class); + } + + protected function configure() + { + $this->setName(self::COMMAND_NAME); + $this->setDescription('Cached device detector information based on access log'); + } + + private function printupdate($count) + { + if ($this->getOutput()->isVerbose()) { + $mem = round(memory_get_peak_usage() / 1024 / 1024); + $now = Date::now()->getDatetime(); + $this->getOutput()->writeln("Count: " . $count . ' Mem:' . $mem . 'MB Date: ' . $now); + } + } + + private function log($message) + { + if ($this->getOutput()->isVerbose()) { + $mem = round(memory_get_peak_usage() / 1024 / 1024); + $now = Date::now()->getDatetime(); + $this->getOutput()->writeln($message . ' Mem:' . $mem . 'MB Date: ' . $now); + } + } + + protected function doExecute(): int + { + $userAgents = []; + + $output = $this->getOutput(); + $regex = $this->config->getAccessLogRegex(); + $numEntriesToCache = $this->config->getNumEntriesToCache(); + $matchEntry = $this->config->getRegexMatchEntry(); + $path = $this->config->getAccessLogPath(); + $path = trim($path); + + $this->log('caching up to ' . $numEntriesToCache . ' entries'); + $this->log('reading from file ' . $path); + $this->log('used regex ' . $regex . ' with index ' . $matchEntry); + + if (empty($numEntriesToCache)) { + $output->writeln('No entries are supposed to be cached. Stopping command'); + return self::SUCCESS; + } + + if (!file_exists($path)) { + throw new \Exception('Configured access log path does not exist: "' . $path . '"'); + } + + $count = 0; + $numLinesToProcess = StaticContainer::get('DeviceDetectorCacheNumLinesToScan'); + $numLinesProcessed = 0; + $handle = fopen($path, "r"); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $numLinesProcessed++; + if ($numLinesProcessed >= $numLinesToProcess) { + break;// we read max 5M lines to prevent in running for too long time + } + if (empty($line)) { + continue; + } + preg_match($regex, $line, $matches); + if ( + !empty($matches[$matchEntry]) + && strlen($matches[$matchEntry]) > 5 + && strlen($matches[$matchEntry]) < 700 + ) { + $useragent = $matches[$matchEntry]; + if (!isset($userAgents[$useragent])) { + $userAgents[$useragent] = 1; + $count = count($userAgents); + if ($count % 10000 === 0) { + $this->printupdate($count); + } + } else { + $userAgents[$useragent] = $userAgents[$useragent] + 1; + } + } + $line = null; + unset($line); + $matches = null; + unset($matches); + if ($numLinesProcessed % 10 === 0) { + usleep(300); // slightly slow down disk usage to avoid running eg into some EBS limit + } + + if ($numLinesProcessed % 1000 === 0) { + usleep(10000); // every 10K lines sleep for a 10ms to not max out CPU as much + } + } + + fclose($handle); + } else { + throw new \Exception('Error opening file. Maybe no read permission? Path: ' . $path); + } + + $this->log("parsed file: " . $numLinesProcessed . " lines"); + $this->printupdate($count); + + arsort($userAgents, SORT_NATURAL); + + if (empty($userAgents)) { + $output->writeln('No user agents found'); + return self::SUCCESS; + } + + $this->log($count . ' user agents found'); + $this->log("writing files"); + + $i = 0; + $numRequestsDetected = 0; + $ignoreUserAgentsWithLessRequestsThan = StaticContainer::get('DeviceDetectorCacheIgnoreUserAgentsWithLessThanXRequests'); + + foreach ($userAgents as $agent => $val) { + if ($i >= $numEntriesToCache) { + $output->writeln('stopping because number of configured entries were cached'); + break; + } + if ($val < $ignoreUserAgentsWithLessRequestsThan) { + $output->writeln('stopping because remaining user agents have only few requests'); + // we don't cache user agents that happened less than 9 times or less as it's so rare it's not really worth caching it and we rather do it on demand + break; + } + $i++; + $numRequestsDetected += $val; // useful to detect hit ratio + + if ($i % 5000 === 0) { + $this->printupdate( + 'written files so far: ' . $i . ' detecting that many requests: ' . $numRequestsDetected + ); + } + if ($i <= 10) { + $this->log('Found user agent ' . $agent . ' count: ' . $val); + } + CachedEntry::writeToCache($agent, []); + // sleep 2ms to let CPU do something else + // this will make things about 10m slower for 200K entries but at least sudden CPU increase for instance + // can be prevented when there are only few CPUs available + // note: roughly per minute we write around 5K entries + usleep(2000); + } + $output->writeln('Written ' . $i . ' cache entries to file.'); + $output->writeln('The hit ratio will be roughly ' . Piwik::getPercentageSafe($numRequestsDetected, $numLinesToProcess) . '%'); + + $numCacheFilesExist = CachedEntry::getNumEntriesInCacheDir(); + $output->writeln($numCacheFilesExist . ' cached files exist'); + + if ($numCacheFilesExist > $numEntriesToCache) { + $numEntriesToDelete = $numCacheFilesExist - $numEntriesToCache; + $output->writeln('Need to delete ' . $numEntriesToDelete . ' files'); + CachedEntry::deleteLeastAccessedFiles($numEntriesToDelete); + $output->writeln('done deleting files'); + } + + return self::SUCCESS; + } +} diff --git a/plugins/DeviceDetectorCache/Configuration.php b/plugins/DeviceDetectorCache/Configuration.php new file mode 100644 index 0000000000000000000000000000000000000000..c95cb1a56ffaaf36b7f1e53b28d4d63f09d96907 --- /dev/null +++ b/plugins/DeviceDetectorCache/Configuration.php @@ -0,0 +1,110 @@ +getConfig(); + + if (empty($config->DeviceDetectorCache)) { + $config->DeviceDetectorCache = []; + } + + $cache = $config->DeviceDetectorCache; + + // we make sure to set a value only if none has been configured yet, eg in common config. + if (empty($cache[self::KEY_NUM_ENTRIES_TO_CACHE])) { + $cache[self::KEY_NUM_ENTRIES_TO_CACHE] = self::DEFAULT_NUM_ENTRIES_TO_CACHE; + } + if (empty($cache[self::KEY_ACCESS_LOG_PATH])) { + $cache[self::KEY_ACCESS_LOG_PATH] = self::DEFAULT_ACCESS_LOG_PATH; + } + if (empty($cache[self::KEY_ACCESS_LOG_REGEX])) { + $cache[self::KEY_ACCESS_LOG_REGEX] = self::DEFAULT_ACCESS_LOG_REGEX; + } + if (empty($cache[self::KEY_ACCESS_LOG_REGEX_MATCH_ENTRY])) { + $cache[self::KEY_ACCESS_LOG_REGEX_MATCH_ENTRY] = self::DEFAULT_ACCESS_LOG_REGEX_MATCH_ENTRY; + } + + $config->DeviceDetectorCache = $cache; + + $config->forceSave(); + } + + public function uninstall() + { + $config = $this->getConfig(); + $config->DeviceDetectorCache = []; + $config->forceSave(); + } + + /** + * @return string + */ + public function getAccessLogPath() + { + return $this->getConfigValue(self::KEY_ACCESS_LOG_PATH, self::DEFAULT_ACCESS_LOG_PATH); + } + + /** + * @return string + */ + public function getAccessLogRegex() + { + return $this->getConfigValue(self::KEY_ACCESS_LOG_REGEX, self::DEFAULT_ACCESS_LOG_REGEX); + } + + /** + * @return string + */ + public function getRegexMatchEntry() + { + return (int)$this->getConfigValue(self::KEY_ACCESS_LOG_REGEX_MATCH_ENTRY, self::DEFAULT_ACCESS_LOG_REGEX_MATCH_ENTRY); + } + + /** + * @return string + */ + public function getNumEntriesToCache() + { + return (int)$this->getConfigValue(self::KEY_NUM_ENTRIES_TO_CACHE, self::DEFAULT_NUM_ENTRIES_TO_CACHE); + } + + private function getConfig() + { + return Config::getInstance(); + } + + private function getConfigValue($name, $default) + { + $config = $this->getConfig(); + $attribution = $config->DeviceDetectorCache; + if (isset($attribution[$name])) { + return $attribution[$name]; + } + return $default; + } +} diff --git a/plugins/DeviceDetectorCache/DeviceDetector/CachedBrowserParser.php b/plugins/DeviceDetectorCache/DeviceDetector/CachedBrowserParser.php new file mode 100644 index 0000000000000000000000000000000000000000..d4e92417102ec9b79368ba9164eae6b4431915d5 --- /dev/null +++ b/plugins/DeviceDetectorCache/DeviceDetector/CachedBrowserParser.php @@ -0,0 +1,32 @@ +cachedUaResult = $result; + } + + protected function parseBrowserFromUserAgent(): array + { + if (!empty($this->cachedUaResult)) { + return $this->cachedUaResult; + } + + return parent::parseBrowserFromUserAgent(); + } +} diff --git a/plugins/DeviceDetectorCache/DeviceDetector/CachedOperatingSystemParser.php b/plugins/DeviceDetectorCache/DeviceDetector/CachedOperatingSystemParser.php new file mode 100644 index 0000000000000000000000000000000000000000..6c4c0d990a928362acee55a00ad6f5d5d785485b --- /dev/null +++ b/plugins/DeviceDetectorCache/DeviceDetector/CachedOperatingSystemParser.php @@ -0,0 +1,32 @@ +cachedUaResult = $result; + } + + protected function parseOsFromUserAgent(): array + { + if (!empty($this->cachedUaResult)) { + return $this->cachedUaResult; + } + + return parent::parseOsFromUserAgent(); + } +} diff --git a/plugins/DeviceDetectorCache/DeviceDetectorCache.php b/plugins/DeviceDetectorCache/DeviceDetectorCache.php new file mode 100644 index 0000000000000000000000000000000000000000..ed23fb6af0ed8b97d5b0e02420af19305889005f --- /dev/null +++ b/plugins/DeviceDetectorCache/DeviceDetectorCache.php @@ -0,0 +1,24 @@ +install(); + } +} diff --git a/plugins/DeviceDetectorCache/Factory.php b/plugins/DeviceDetectorCache/Factory.php new file mode 100644 index 0000000000000000000000000000000000000000..6d1eadb3816e74eb9b7b9aa499a792121400ab30 --- /dev/null +++ b/plugins/DeviceDetectorCache/Factory.php @@ -0,0 +1,25 @@ + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugins/DeviceDetectorCache/README.md b/plugins/DeviceDetectorCache/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dd3b04b05f582ea5f81bad81745613edc5068fb8 --- /dev/null +++ b/plugins/DeviceDetectorCache/README.md @@ -0,0 +1,58 @@ +# Matomo DeviceDetectorCache Plugin + +[![Build Status](https://github.com/matomo-org/plugin-DeviceDetectorCache/actions/workflows/matomo-tests.yml/badge.svg?branch=4.x-dev)](https://github.com/matomo-org/plugin-DeviceDetectorCache/actions/workflows/matomo-tests.yml) + +## Description + +Makes device detection in Matomo faster by having cached entries for many commonly used user agents. + +By default, Matomo runs thousands of regular expressions for each tracking request to detect what Browser, Device, Operating system, ... is being used and to detect if a user agent is a bot or not. + +This plugin changes this by first looking if a cached result exists for the particular user agent and if so, directly loads the result from the file system. + +We recommend this plugin only if you have a very high traffic website (> 200M requests a month). Depending on your server it may save you a few ms per tracking request (say 5ms which may be say 10% of the total tracking request time). + +If you have not that much traffic, the overhead might not be worth it. + +### How to set it up + +#### Config setup + +Configure these values in your `config/config.ini.php` + +``` +[DeviceDetectorCache] +access_log_path = "/var/log/httpd/access_log" # The path to your access log file. This command needs to have read permission for this file +access_log_regex = "/^(\S+) (\S+) (\S+) \[([^:]+):(\d+:\d+:\d+) ([^\]]+)\] \"(\S+) (.*?) (\S+)\" (\S+) (\S+) \"([^\"]*)\" \"([^\"]*)\"$/" # the regex used to extract the user agent +regex_match_entry = 14 # defines which subpattern of the abovce regex matches the user agent +num_cache_entries = 200000 # how many user agents should be cached. This value basically depends on your memory and disk space. Likely there is no need to change this +``` + +#### Testing if it works + +Run this command to see if it works: + +``` +php /path/to/matomo/console device-detector-cache:warm-cache -vvv +``` + +It should show how many user agents were detected and should print the top 10 most commonly found user agents if things are configured correctly. + +Cached files will be stored in `/tmp/devicecache/`. Make sure there is write access for this folder. Every time this command runs previously created cache entries will be deleted. + +#### Set up a cronjob + +If above test goes well you need to set up a cronjob that runs regularly (eg every few hours or days) to update the cached entries based on the access log. + +The cronjob needs to look like for example like this: + +``` +0 8 * * * php /path/to/matomo/console device-detector-cache:warm-cache +``` + +If you have multiple servers, you need to set up the command on every server that processes tracking requests. + +## Credits + +* [PHP Device Detector library](https://github.com/matomo-org/device-detector/) +* [Matomo - Open Source Web Analytics](https://matomo.org) diff --git a/plugins/DeviceDetectorCache/config/config.php b/plugins/DeviceDetectorCache/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..bc83b0329b7702e2680e1d527c12daa0863d1aa8 --- /dev/null +++ b/plugins/DeviceDetectorCache/config/config.php @@ -0,0 +1,8 @@ + 9, + 'DeviceDetectorCacheNumLinesToScan' => 5000000, + \Piwik\DeviceDetector\DeviceDetectorFactory::class + => Piwik\DI::autowire(\Piwik\Plugins\DeviceDetectorCache\Factory::class), +]; diff --git a/plugins/DeviceDetectorCache/config/test.php b/plugins/DeviceDetectorCache/config/test.php new file mode 100644 index 0000000000000000000000000000000000000000..a812bf567780a714b1d3fad3d8c61bc006ff5bfc --- /dev/null +++ b/plugins/DeviceDetectorCache/config/test.php @@ -0,0 +1,12 @@ + 0, + \Piwik\DeviceDetector\DeviceDetectorFactory::class => Piwik\DI::decorate(function ($previous) { + CachedEntry::setCacheDir(PIWIK_DOCUMENT_ROOT . '/tmp/devicecachetests/'); + + return $previous; + }) +]; diff --git a/plugins/DeviceDetectorCache/config/tracker.php b/plugins/DeviceDetectorCache/config/tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..0b67a5fe4742a02757d939368cc05d4c246502f8 --- /dev/null +++ b/plugins/DeviceDetectorCache/config/tracker.php @@ -0,0 +1,3 @@ + + + + Matomo Coding Standard for DeviceDetectorCache plugin + + + + . + + tests/javascript/* + */vendor/* + + + + + + + + tests/* + + + + + Updates/* + + + + + tests/* + + + + + tests/* + + \ No newline at end of file diff --git a/plugins/DeviceDetectorCache/plugin.json b/plugins/DeviceDetectorCache/plugin.json new file mode 100644 index 0000000000000000000000000000000000000000..1ead9b7d91431980306fcc6b3feeb4e92b1c534c --- /dev/null +++ b/plugins/DeviceDetectorCache/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "DeviceDetectorCache", + "description": "Makes tracking faster by detecting many devices, operating systems, bots, and browsers from a cache.", + "version": "5.0.3", + "theme": false, + "require": { + "matomo": ">=5.0.0-b1,<6.0.0-b1" + }, + "authors": [ + { + "name": "Matomo", + "email": "hello@matomo.org", + "homepage": "https://matomo.org" + } + ], + "support": { + "issues": "https://github.com/matomo-org/plugin-DeviceDetectorCache/issues" + }, + "homepage": "https://matomo.org", + "license": "GPL v3+", + "keywords": ["device", "detector", "tracking", "performance"], + "category": "database" +} diff --git a/plugins/DeviceDetectorCache/pull_request_template.md b/plugins/DeviceDetectorCache/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..211089bb4ec77dcc05db341c8d1be45bae811eea --- /dev/null +++ b/plugins/DeviceDetectorCache/pull_request_template.md @@ -0,0 +1,29 @@ +## Description + + +## Issue No + + +## Steps to Replicate the Issue +1. +2. +3. + + + +## Checklist +- [✔/✖] Tested locally or on demo2/demo3? +- [✔/✖/NA] New test case added/updated? +- [✔/✖/NA] Are all newly added texts included via translation? +- [✔/✖/NA] Are text sanitized properly? (Eg use of v-text v/s v-html for vue) +- [✔/✖/NA] Version bumped? +- [✔/✖/NA] I have understood, reviewed, and tested all AI outputs before use +- [✔/✖/NA] All AI instructions respect security, IP, and privacy rules +- [✔/✖/NA] Documentation updated? diff --git a/plugins/DeviceDetectorCache/tests/Fixtures/SimpleFixtureTrackFewVisits.php b/plugins/DeviceDetectorCache/tests/Fixtures/SimpleFixtureTrackFewVisits.php new file mode 100644 index 0000000000000000000000000000000000000000..bea2210ec6fca8b1c1fcffae6b0384f2349e9e24 --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/Fixtures/SimpleFixtureTrackFewVisits.php @@ -0,0 +1,113 @@ +setupCache(); + $this->setUpWebsite(); + $this->trackFirstVisit(); + DeviceDetectorFactory::clearInstancesCache(); + } + + /** + * Write device data for the wrong useragent - means we have valid device data (so the OS etc fields + * in the API output won't just be "Unknown") but we can distinguish between data read from the cache + * and data that was parsed directly from the useragent string. + */ + private function setupCache() + { + $expected = [ + 'bot' => null, + 'brand' => 'Fly', + 'client' => [ + 'type' => 'browser', + 'name' => 'Microsoft Edge', + 'short_name' => 'PS', + 'version' => '33.0', + 'engine' => 'Gecko', + 'engine_version' => '', + ], + 'device' => 1, + 'model' => 'iPhone', + 'os' => [ + 'name' => 'GNU/Linux', + 'short_name' => 'LIN', + 'version' => '10.10', + 'platform' => '', + ], + ]; + //user agent 2 should detect an iphone even though it is not as it's read from cache + CachedEntry::setCacheDir(PIWIK_DOCUMENT_ROOT . '/tmp/devicecachetests/'); + DeviceDetectorCacheFactoryTest::writeFakeFile($expected, $this->userAgent2); + } + + public function tearDown(): void + { + CachedEntry::clearCacheDir(); + Filesystem::unlinkRecursive(CachedEntry::getCacheDir(), true); + } + + private function setUpWebsite() + { + if (!self::siteCreated($this->idSite)) { + $idSite = self::createWebsite($this->dateTime, $ecommerce = 1); + $this->assertSame($this->idSite, $idSite); + } + } + + protected function trackFirstVisit() + { + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + $t->setIp('56.11.55.73'); + $t->setUserAgent($this->userAgent2); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime()); + $t->setUrl('http://example.com/sub/page'); + self::checkResponse($t->doTrackPageView('Viewing homepage')); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.2)->getDatetime()); + $t->setUrl('http://example.com/?search=this is a site search query'); + self::checkResponse($t->doTrackPageView('Site search query')); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.3)->getDatetime()); + $t->addEcommerceItem($sku = 'SKU_ID2', $name = 'A durable item', $category = 'Best seller', $price = 321); + self::checkResponse($t->doTrackEcommerceCartUpdate($grandTotal = 33 * 77)); + + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + $t->setIp('56.11.55.74'); + $t->setUserAgent($this->userAgent1); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime()); + $t->setUrl('http://example.com/sub/page2'); + self::checkResponse($t->doTrackPageView('Viewing homepage2')); + } +} diff --git a/plugins/DeviceDetectorCache/tests/Integration/CachedEntryTest.php b/plugins/DeviceDetectorCache/tests/Integration/CachedEntryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c2e29043f6ab8efdf3c42afedbf2360e40747da8 --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/Integration/CachedEntryTest.php @@ -0,0 +1,265 @@ +assertIsObject($instance); + $this->assertInstanceOf(CachedEntry::class, $instance); + } + + public function testConstructSomeValues() + { + $values = [ + 'bot' => 'testBot', + 'brand' => 'testBrand', + 'client' => 'testClient', + 'device' => 2, + 'model' => 'testModel', + 'os' => 'testOs', + ]; + + $instance = new CachedEntry('', [], $values); + $this->assertIsObject($instance); + $this->assertInstanceOf(CachedEntry::class, $instance); + $this->assertSame($values['bot'], $instance->getBot()); + $this->assertSame($values['brand'], $instance->getBrandName()); + $this->assertSame($values['client'], $instance->getClient()); + $this->assertSame($values['device'], $instance->getDevice()); + $this->assertSame($values['model'], $instance->getModel()); + $this->assertSame($values['os'], $instance->getOs()); + } + + public function testConstruct() + { + $userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Edg/97.0.1072.66"; + + $values = [ + 'bot' => null, + 'brand' => '', + 'client' => [ + 'type' => 'browser', + 'name' => 'Microsoft Edge', + 'version' => '97.0.1072.66' + ], + 'device' => 1, + 'model' => '', + 'os' => [ + 'name' => 'Windows', + 'version' => '10', + ], + ]; + + $clientHints = [ + 'HTTP_SEC_CH_UA_PLATFORM' => '"Windows"', + 'HTTP_SEC_CH_UA' => '" Not A;Brand";v="99", "Chromium";v="95", "Microsoft Edge";v="95"', + 'HTTP_SEC_CH_UA_MOBILE' => "?0", + 'HTTP_SEC_CH_UA_FULL_VERSION' => '"98.0.0.1"', + 'HTTP_SEC_CH_UA_PLATFORM_VERSION' => '"14.0.0"', + 'HTTP_SEC_CH_UA_ARCH' => "x86", + 'HTTP_SEC_CH_UA_BITNESS' => "64", + 'HTTP_SEC_CH_UA_MODEL' => "" + ]; + + $expectedClient = [ + 'type' => 'browser', + 'name' => 'Microsoft Edge', + 'short_name' => 'PS', + 'version' => '98.0', + 'engine' => '', + 'engine_version' => '', + 'family' => 'Internet Explorer', + ]; + + $expectedOs = [ + 'name' => 'Windows', + 'short_name' => 'WIN', + 'version' => '11', + 'platform' => 'x64', + 'family' => 'Windows', + ]; + + $instance = new CachedEntry($userAgent, $clientHints, $values); + $this->assertIsObject($instance); + $this->assertInstanceOf(CachedEntry::class, $instance); + $this->assertSame(null, $instance->getBot()); + $this->assertSame($values['brand'], $instance->getBrandName()); + $this->assertSame($expectedClient, $instance->getClient()); + $this->assertSame($values['device'], $instance->getDevice()); + $this->assertSame($values['model'], $instance->getModel()); + $this->assertSame($expectedOs, $instance->getOs()); + } + + public function testConstructDefault() + { + $userAgent = "unknown"; + + $clientHints = [ + 'HTTP_SEC_CH_UA_PLATFORM' => '"Windows"', + 'HTTP_SEC_CH_UA' => '" Not A;Brand";v="99", "Chromium";v="95", "Microsoft Edge";v="95"', + 'HTTP_SEC_CH_UA_MOBILE' => "?0", + 'HTTP_SEC_CH_UA_FULL_VERSION' => '"98.0.0.1"', + 'HTTP_SEC_CH_UA_PLATFORM_VERSION' => '"14.0.0"', + 'HTTP_SEC_CH_UA_ARCH' => "x86", + 'HTTP_SEC_CH_UA_BITNESS' => "64", + 'HTTP_SEC_CH_UA_MODEL' => "" + ]; + + $expectedOs = [ + 'name' => 'Windows', + 'short_name' => 'WIN', + 'version' => '11', + 'platform' => 'x64', + 'family' => 'Windows', + ]; + + + $instance = new CachedEntry($userAgent, $clientHints, []); + $this->assertIsObject($instance); + $this->assertInstanceOf(CachedEntry::class, $instance); + $this->assertSame(null, $instance->getBot()); + $this->assertSame('', $instance->getBrandName()); + $this->assertSame(null, $instance->getClient()); + $this->assertSame(null, $instance->getDevice()); + $this->assertSame('', $instance->getModel()); + $this->assertSame($expectedOs, $instance->getOs()); + } + + public function testGetNumCacheFiles_noneCached() + { + $this->assertEquals(0, CachedEntry::getNumEntriesInCacheDir()); + } + + public function testGetNumCacheFiles() + { + CachedEntry::writeToCache('foo', []); + $this->assertEquals(1, CachedEntry::getNumEntriesInCacheDir()); + CachedEntry::writeToCache('bar', []); + $this->assertEquals(2, CachedEntry::getNumEntriesInCacheDir()); + CachedEntry::writeToCache('baz', []); + $this->assertEquals(3, CachedEntry::getNumEntriesInCacheDir()); + } + + public function testGetCached_noEntry() + { + $this->assertNull(CachedEntry::getCached('foo', [])); + } + + public function test_writeToCache_GetCached() + { + CachedEntry::writeToCache('foo', []); + $cacheEntry = CachedEntry::getCached('foo', []); + $this->assertEquals( + [ + 'bot', 'brand', 'client', 'device', 'model', 'os' + ], + array_keys($cacheEntry) + ); + } + + public function test_getCachePath() + { + $path1 = CachedEntry::getCachePath('foo'); + $path2 = CachedEntry::getCachePath('bar'); + + $path1 = str_replace(PIWIK_DOCUMENT_ROOT, '', $path1); + $path2 = str_replace(PIWIK_DOCUMENT_ROOT, '', $path2); + + $this->assertEquals('/tmp/devicecachetests/acb/acbd18db4cc2f85cedef654fccc4a4d8.php', $path1); + $this->assertEquals('/tmp/devicecachetests/37b/37b51d194a7513e45b56f6524f2d51f2.php', $path2); + } + + public function test_getCacheDir() + { + $path1 = CachedEntry::getCacheDir(); + + // because we may delete files we want to make sure it starts with the matomo tmp path + $this->assertSame(0, strpos($path1, PIWIK_DOCUMENT_ROOT . '/tmp/')); + } + + public function test_deleteLeastAccessedFiles_nothingToDelete() + { + $filePath = CachedEntry::writeToCache('file', []); + $this->assertFileExists($filePath); + + CachedEntry::deleteLeastAccessedFiles(-1); + CachedEntry::deleteLeastAccessedFiles(0); + + $this->assertFileExists($filePath); + } + + public function test_deleteLeastAccessedFiles_deletesOnlyOldest() + { + $filePath1 = CachedEntry::writeToCache('file', []); + sleep(1); // otherwise without sleep the sorting won't work properly + $filePath2 = CachedEntry::writeToCache('bar', []); + sleep(1); + $filePath3 = CachedEntry::writeToCache('baz', []); + sleep(1); + $filePath4 = CachedEntry::writeToCache('foo', []); + sleep(1); + + CachedEntry::deleteLeastAccessedFiles(2); + + $this->assertFileNotExists($filePath1); + $this->assertFileNotExists($filePath2); + $this->assertFileExists($filePath3); + $this->assertFileExists($filePath4); + } + + public function test_deleteLeastAccessedFiles_deletesOnlyOldest2() + { + $filePath1 = CachedEntry::writeToCache('file', []); + sleep(1); + $filePath2 = CachedEntry::writeToCache('bar', []); + sleep(1); + $filePath3 = CachedEntry::writeToCache('baz', []); + sleep(1); + $filePath4 = CachedEntry::writeToCache('foo', []); + sleep(1); + + touch($filePath1); + sleep(1); + touch($filePath3); + + CachedEntry::deleteLeastAccessedFiles(2); + + $this->assertFileNotExists($filePath2); + $this->assertFileNotExists($filePath4); + $this->assertFileExists($filePath1); + $this->assertFileExists($filePath3); + } +} diff --git a/plugins/DeviceDetectorCache/tests/Integration/ConfigurationTest.php b/plugins/DeviceDetectorCache/tests/Integration/ConfigurationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8c5470beb28fe6b095f717c77341fecda4562ff8 --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/Integration/ConfigurationTest.php @@ -0,0 +1,101 @@ +configuration = new Configuration(); + $this->configuration->install(); + } + + public function test_shouldInstallConfig() + { + $this->configuration->install(); + + $configs = Config::getInstance()->DeviceDetectorCache; + $this->assertEquals([ + 'num_cache_entries' => '200000', + 'access_log_path' => '/var/log/httpd/access_log', + 'access_log_regex' => Configuration::DEFAULT_ACCESS_LOG_REGEX, + 'regex_match_entry' => 14, + ], $configs); + } + + public function test_getRegexMatchEntry() + { + $this->assertSame(Configuration::DEFAULT_ACCESS_LOG_REGEX_MATCH_ENTRY, $this->configuration->getRegexMatchEntry()); + } + + public function test_getRegexMatchEntry_customValue() + { + Config::getInstance()->DeviceDetectorCache = [ + Configuration::KEY_ACCESS_LOG_REGEX_MATCH_ENTRY => '5', + ]; + $this->assertEquals(5, $this->configuration->getRegexMatchEntry()); + } + + public function test_getAccessLogPath() + { + $this->assertSame(Configuration::DEFAULT_ACCESS_LOG_PATH, $this->configuration->getAccessLogPath()); + } + + public function test_getAccessLogPath_customValue() + { + Config::getInstance()->DeviceDetectorCache = [ + Configuration::KEY_ACCESS_LOG_PATH => '/var/log/foo', + ]; + $this->assertEquals('/var/log/foo', $this->configuration->getAccessLogPath()); + } + + public function test_getNumEntriesToCache() + { + $this->assertSame(Configuration::DEFAULT_NUM_ENTRIES_TO_CACHE, $this->configuration->getNumEntriesToCache()); + } + + public function test_getNumEntriesToCache_customValue() + { + Config::getInstance()->DeviceDetectorCache = [ + Configuration::KEY_NUM_ENTRIES_TO_CACHE => '145', + ]; + $this->assertEquals(145, $this->configuration->getNumEntriesToCache()); + } + + public function test_getAccessLogRegex() + { + $this->assertSame(Configuration::DEFAULT_ACCESS_LOG_REGEX, $this->configuration->getAccessLogRegex()); + } + + public function test_getAccessLogRegex_customValue() + { + Config::getInstance()->DeviceDetectorCache = [ + Configuration::KEY_ACCESS_LOG_REGEX => '(.*)', + ]; + $this->assertEquals('(.*)', $this->configuration->getAccessLogRegex()); + } +} diff --git a/plugins/DeviceDetectorCache/tests/Integration/WarmDeviceDetectorCacheTest.php b/plugins/DeviceDetectorCache/tests/Integration/WarmDeviceDetectorCacheTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8b3f760c1208a692e998b0b84518f3c9dcc60464 --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/Integration/WarmDeviceDetectorCacheTest.php @@ -0,0 +1,207 @@ +DeviceDetectorCache; + $d[Configuration::KEY_ACCESS_LOG_PATH] = $file; + $config->DeviceDetectorCache = $d; + } + + private function setCountProcessNumEntries($numEntries) + { + $config = \Piwik\Config::getInstance(); + $d = $config->DeviceDetectorCache; + $d[Configuration::KEY_NUM_ENTRIES_TO_CACHE] = $numEntries; + $config->DeviceDetectorCache = $d; + } + + public function testWritesUserAgentsToFile() + { + $testFile = __DIR__ . '/files/useragents1.csv'; + $this->setAccessLogFile($testFile); + + $this->applicationTester->run([ + 'command' => WarmDeviceDetectorCache::COMMAND_NAME, + ]); + + $userAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko', + ]; + + $this->assertStringContainsString("Written 3 cache entries to file", $this->applicationTester->getDisplay()); + foreach ($userAgents as $userAgent) { + $this->assertUserAgentWrittenToFile($userAgent); + } + } + + public function testInputFileDoesntExist() + { + $testFile = __DIR__ . '/files/notarealfile.csv'; + $this->setAccessLogFile($testFile); + + $this->applicationTester->run([ + 'command' => WarmDeviceDetectorCache::COMMAND_NAME, + ]); + + $this->assertStringContainsString("Configured access log path does not exist", $this->applicationTester->getDisplay()); + } + + public function testDoesNotClearExistingFilesFromCacheByDefault() + { + $userAgent = 'Mozilla/5.0 (Linux; Android 8.0.0; SAMSUNG SM-G930F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.4 Chrome/67.0.3396.87 Mobile Safari/537.36'; + $cacheFilePath = CachedEntry::getCachePath($userAgent, true); + + file_put_contents($cacheFilePath, " 'testresult');", LOCK_EX); + + $testFile = __DIR__ . '/files/useragents1.csv'; + $this->setAccessLogFile($testFile); + + $this->applicationTester->run([ + 'command' => WarmDeviceDetectorCache::COMMAND_NAME, + ]); + + $this->assertFileExists($cacheFilePath); + } + + public function testDoesClearExistingFilesFromCacheByDefaultWhenTooManyEntriesExist() + { + // was last accessed + $userAgentKept = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko'; + $userAgentDeleted = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'; + $cacheFilePathKept = CachedEntry::getCachePath($userAgentKept, true); + $cacheFilePathDeleted = CachedEntry::getCachePath($userAgentDeleted, true); + + $this->setCountProcessNumEntries(3); + + $testFile = __DIR__ . '/files/useragents1.csv'; + $this->setAccessLogFile($testFile); + + //cache 3 entries + $this->applicationTester->run([ + 'command' => WarmDeviceDetectorCache::COMMAND_NAME, + ]); + $this->assertFileExists($cacheFilePathKept); + $this->assertFileExists($cacheFilePathDeleted); + + $this->assertEquals(3, CachedEntry::getNumEntriesInCacheDir()); + + $this->setCountProcessNumEntries(1); + + // now we run again and it should delete 2 of the entries + $this->applicationTester->run([ + 'command' => WarmDeviceDetectorCache::COMMAND_NAME, + ]); + // should now have deleted 2 files + $this->assertEquals(1, CachedEntry::getNumEntriesInCacheDir()); + + $this->assertFileExists($cacheFilePathKept); + $this->assertFileNotExists($cacheFilePathDeleted); + } + + public function testDoesntProcessAllRowsWhenCounterSet() + { + $testFile = __DIR__ . '/files/useragents1.csv'; + $this->setAccessLogFile($testFile); + $this->setCountProcessNumEntries(2); + + $this->applicationTester->run([ + 'command' => WarmDeviceDetectorCache::COMMAND_NAME, + ]); + + $userAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko', + ]; + + $this->assertStringContainsString("Written 2 cache entries to file", $this->applicationTester->getDisplay()); + $this->assertUserAgentWrittenToFile($userAgents[0]); + $this->assertUserAgentWrittenToFile($userAgents[1]); + $this->assertUserAgentNotWrittenToFile($userAgents[2]); + } + + public function testVeryLongUserAgent() + { + $testFile = __DIR__ . '/files/useragentsverylong.csv'; + $this->setAccessLogFile($testFile); + + $this->applicationTester->run([ + 'command' => WarmDeviceDetectorCache::COMMAND_NAME, + ]); + + $userAgentStr = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36 QBCore/3.53.1159.400 QQBrowser/9.0.2524.400 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 MicroMessenger/6.5.2.501 NetType/WIFI WindowsWechat AndHereIsAVeryLongStringJustToMakeSureThatWeHitOver500CharactersWithThisRidiculouslyLongUserAgentThatIsLikelyToBreakOurCodeBecauseWeTruncateTheUserAgentAt500CharactersThatIsAnAwfulLotOfCharactersImStillGoing"; + + $this->assertStringContainsString("Written 1 cache entries to file", $this->applicationTester->getDisplay()); + $this->assertUserAgentWrittenToFile($userAgentStr); + } + + private function assertUserAgentNotWrittenToFile($userAgent) + { + $expectedFilePath = CachedEntry::getCachePath($userAgent); + $this->assertFileNotExists($expectedFilePath); + } + + private function assertUserAgentWrittenToFile($userAgent) + { + $expectedFilePath = CachedEntry::getCachePath($userAgent); + $this->assertFileExists($expectedFilePath); + + DeviceDetectorFactory::clearInstancesCache(); + $cacheFactory = new Factory(); + $deviceDetectionFromFile = $cacheFactory->makeInstance($userAgent); + + DeviceDetectorFactory::clearInstancesCache(); + $parsingFactory = new DeviceDetectorFactory(); + $deviceDetectionParsed = $parsingFactory->makeInstance($userAgent); + + $this->assertInstanceOf(CachedEntry::class, $deviceDetectionFromFile); + $this->assertInstanceOf("\DeviceDetector\DeviceDetector", $deviceDetectionParsed); + $this->assertEquals($deviceDetectionParsed->getBot(), $deviceDetectionFromFile->getBot()); + $this->assertEquals($deviceDetectionParsed->getBrand(), $deviceDetectionFromFile->getBrand()); + $this->assertEquals($deviceDetectionParsed->getClient(), $deviceDetectionFromFile->getClient()); + $this->assertEquals($deviceDetectionParsed->getDevice(), $deviceDetectionFromFile->getDevice()); + $this->assertEquals($deviceDetectionParsed->getModel(), $deviceDetectionFromFile->getModel()); + $this->assertEquals($deviceDetectionParsed->getOs(), $deviceDetectionFromFile->getOs()); + } +} diff --git a/plugins/DeviceDetectorCache/tests/Integration/files/useragents1.csv b/plugins/DeviceDetectorCache/tests/Integration/files/useragents1.csv new file mode 100644 index 0000000000000000000000000000000000000000..33c20f0db37ec781eff1962bd866c6d9d0a40c6e --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/Integration/files/useragents1.csv @@ -0,0 +1,9 @@ +test.matomo 10.10.10.10 - - [04/Aug/2020:13:24:45 +0000] "GET /matomo.php? HTTP/1.1" 200 50 "https://matomo.org/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" 239127 +test.matomo 10.10.10.10 - - [04/Aug/2020:13:24:45 +0000] "GET /matomo.php? HTTP/1.1" 200 50 "https://matomo.org/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" 239127 +test.matomo 10.10.10.10 - - [04/Aug/2020:13:24:45 +0000] "GET /matomo.php? HTTP/1.1" 200 50 "https://matomo.org/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" 239127 +foo not matching any regex + +test.matomo 10.10.10.10 - - [04/Aug/2020:13:24:45 +0000] "GET /matomo.php? HTTP/1.1" 200 50 "https://matomo.org/" "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 239127 +test.matomo 10.10.10.10 - - [04/Aug/2020:13:24:45 +0000] "GET /matomo.php? HTTP/1.1" 200 50 "https://matomo.org/" "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 239127 + +test.matomo 10.10.10.10 - - [04/Aug/2020:13:24:45 +0000] "GET /matomo.php? HTTP/1.1" 200 50 "https://matomo.org/" "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko" 239127 diff --git a/plugins/DeviceDetectorCache/tests/Integration/files/useragentsverylong.csv b/plugins/DeviceDetectorCache/tests/Integration/files/useragentsverylong.csv new file mode 100644 index 0000000000000000000000000000000000000000..309b26df7c76d8ab121308921c05c02e9f873b73 --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/Integration/files/useragentsverylong.csv @@ -0,0 +1 @@ +test.matomo 10.10.10.10 - - [04/Aug/2020:13:24:45 +0000] "GET /matomo.php? HTTP/1.1" 200 50 "https://matomo.org/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36 QBCore/3.53.1159.400 QQBrowser/9.0.2524.400 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 MicroMessenger/6.5.2.501 NetType/WIFI WindowsWechat AndHereIsAVeryLongStringJustToMakeSureThatWeHitOver500CharactersWithThisRidiculouslyLongUserAgentThatIsLikelyToBreakOurCodeBecauseWeTruncateTheUserAgentAt500CharactersThatIsAnAwfulLotOfCharactersImStillGoing" 239127 diff --git a/plugins/DeviceDetectorCache/tests/System/CheckDirectDependencyUseCommandTest.php b/plugins/DeviceDetectorCache/tests/System/CheckDirectDependencyUseCommandTest.php new file mode 100644 index 0000000000000000000000000000000000000000..40d644ca7660598bb317b26596bd5f02240d0e61 --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/System/CheckDirectDependencyUseCommandTest.php @@ -0,0 +1,53 @@ +markTestSkipped('tests:check-direct-dependency-use is not available in this version'); + } + + $pluginName = 'DeviceDetectorCache'; + + $checkDirectDependencyUse = new CheckDirectDependencyUse(); + + $console = new \Piwik\Console(self::$fixture->piwikEnvironment); + $console->addCommands([$checkDirectDependencyUse]); + $command = $console->find('tests:check-direct-dependency-use'); + $arguments = [ + 'command' => 'tests:check-direct-dependency-use', + '--plugin' => $pluginName, + '--grep-vendor', + ]; + + $inputObject = new ArrayInput($arguments); + $command->run($inputObject, new NullOutput()); + + $this->assertEquals([ + 'Symfony\Component\Console' => [ + 'DeviceDetectorCache/tests/System/CheckDirectDependencyUseCommandTest.php' + ], + 'DeviceDetector' => [ + 'DeviceDetectorCache/CachedEntry.php', + 'DeviceDetectorCache/DeviceDetector/CachedBrowserParser.php', + 'DeviceDetectorCache/DeviceDetector/CachedOperatingSystemParser.php', + ] + ], $checkDirectDependencyUse->usesFoundList[$pluginName]); + } +} diff --git a/plugins/DeviceDetectorCache/tests/System/DeviceDetectorCacheTest.php b/plugins/DeviceDetectorCache/tests/System/DeviceDetectorCacheTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0866d9007d9ffa19b4fedc9bc16586b869cd6999 --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/System/DeviceDetectorCacheTest.php @@ -0,0 +1,80 @@ +runApiTests($api, $params); + } + + public function getApiForTesting() + { + $api = [ + 'Live.getLastVisitsDetails', + ]; + + $columnsToHide = []; + + if (version_compare(Version::VERSION, '5.2.0-alpha', '<')) { + // In Matomo 5.2 referrer columns had been added to ecommerce actions. For tests with older Matomo releases we therefor ignore those columns + $columnsToHide = array_merge($columnsToHide, ['referrerType', 'referrerName', 'referrerKeyword']); + } + + if (version_compare(Version::VERSION, '5.5.0-b1', '<')) { + // In Matomo 5.5 ai referrer had been added + $columnsToHide = array_merge($columnsToHide, ['referrerAIAssistantUrl', 'referrerAIAssistantIcon']); + } + + $apiToTest = []; + $apiToTest[] = [ + $api, + [ + 'idSite' => 1, + 'date' => self::$fixture->dateTime, + 'periods' => ['day'], + 'testSuffix' => '', + 'xmlFieldsToRemove' => $columnsToHide, + ], + ]; + + return $apiToTest; + } + + public static function getOutputPrefix() + { + return ''; + } + + public static function getPathToTestDirectory() + { + return dirname(__FILE__); + } +} + +DeviceDetectorCacheTest::$fixture = new SimpleFixtureTrackFewVisits(); diff --git a/plugins/DeviceDetectorCache/tests/System/expected/test___Live.getLastVisitsDetails_day.xml b/plugins/DeviceDetectorCache/tests/System/expected/test___Live.getLastVisitsDetails_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..87aa078b0d2199f7be33b63b7bed827e65a1d479 --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/System/expected/test___Live.getLastVisitsDetails_day.xml @@ -0,0 +1,307 @@ + + + + 1 + 1 + 56.11.55.73 + + d6c0d9b7448ddef6 + + + action + http://example.com/sub/page + Viewing homepage + 2 + + + 1 + + 721 + 12 min 1s + 1 + Viewing homepage + http://example.com/sub/page + + plugins/Morpheus/images/action.svg + + 0 M + + + search + + + + + 2 + + this is a site search query + + + 2 + plugins/Morpheus/images/search.png + plugins/Morpheus/images/search.svg + Site Search + this is a site search query + + 0 M + + + ecommerceAbandonedCart + 2541 + 1 + + + direct + + + + + SKU_ID2 + A durable item + Best seller + 321 + 1 + + Best seller + + + + plugins/Morpheus/images/ecommerceAbandonedCart.png + plugins/Morpheus/images/ecommerceAbandonedCart.svg + Abandoned Cart + $2,541 revenue - 1 items: A durable item) + + + + 0 + USD + $ + + + + + Piwik test + + + + + + + + new + + 0 + + 1 + abandonedCart + plugins/Morpheus/images/ecommerceAbandonedCart.svg + 0 + 0 + 0 + + 721 + 12 min 1s + 1 + 2 + 2 + direct + Direct Entry + + + + + + + + + + + fr + French + Smartphone + plugins/Morpheus/icons/dist/devices/smartphone.png + Fly + iPhone + GNU/Linux 10.10 + GNU/Linux + plugins/Morpheus/icons/dist/os/LIN.png + LIN + 10.10 + Gecko + Gecko (Firefox) + Microsoft Edge 33.0 + Microsoft Edge + plugins/Morpheus/icons/dist/browsers/PS.png + PS + 33.0 + 0 + 0 + 0 + 2541 + 1 + 1 + 0 + Europe + eur + France + fr + plugins/Morpheus/icons/dist/flags/fr.png + + + + France + + + 12:34:06 + 12 + 0 + 0 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + + + 1 + 2 + 56.11.55.74 + + 3fc0c55f8936fec4 + + + action + http://example.com/sub/page2 + Viewing homepage2 + 8 + + + 3 + + 0 + 0s + 1 + Viewing homepage2 + http://example.com/sub/page2 + + plugins/Morpheus/images/action.svg + + 0 M + + + 0 + USD + $ + + + + + Piwik test + + + + + + + + new + + 0 + + 1 + none + + 0 + 0 + 0 + + 0 + 0s + 0 + 1 + 1 + direct + Direct Entry + + + + + + + + + + + fr + French + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Apple + Generic Desktop + Mac 10.14 + Mac + plugins/Morpheus/icons/dist/os/MAC.png + MAC + 10.14 + Blink + Blink (Chrome, Opera) + Chrome 74.0 + Chrome + plugins/Morpheus/icons/dist/browsers/CH.png + CH + 74.0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/Morpheus/icons/dist/flags/fr.png + + + + France + + + 12:34:06 + 12 + 0 + 0 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + + \ No newline at end of file diff --git a/plugins/DeviceDetectorCache/tests/Unit/DeviceDetectorCacheFactoryTest.php b/plugins/DeviceDetectorCache/tests/Unit/DeviceDetectorCacheFactoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c2cb5d43f4c9878f9b1526f2b4079a0c3c231e3f --- /dev/null +++ b/plugins/DeviceDetectorCache/tests/Unit/DeviceDetectorCacheFactoryTest.php @@ -0,0 +1,194 @@ + null, + 'brand' => 'Cooper', + 'client' => [ + 'type' => 'browser', + 'name' => 'Microsoft Edge', + ], + 'device' => 1, + 'model' => 'iPhone', + 'os' => [ + 'name' => 'Linux', + ], + ]; + + self::writeFakeFile($expected, $userAgent); + + $factory = new Factory(); + $deviceDetection = $factory->makeInstance($userAgent, []); + $this->assertInstanceOf(CachedEntry::class, $deviceDetection); + $this->assertEquals(null, $deviceDetection->getBot()); + $this->assertEquals('Cooper', $deviceDetection->getBrandName()); + $this->assertEquals($expected['client'], $deviceDetection->getClient()); + $this->assertEquals(1, $deviceDetection->getDevice()); + $this->assertEquals('iPhone', $deviceDetection->getModel()); + $this->assertEquals($expected['os'], $deviceDetection->getOs()); + } + + public function testGetInstanceFromCacheWithClientHints() + { + $userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Edg/97.0.1072.66"; + $clientHints = [ + 'HTTP_SEC_CH_UA_PLATFORM' => '"Windows"', + 'HTTP_SEC_CH_UA' => '" Not A;Brand";v="99", "Chromium";v="95", "Microsoft Edge";v="95"', + 'HTTP_SEC_CH_UA_MOBILE' => "?0", + 'HTTP_SEC_CH_UA_FULL_VERSION' => '"98.0.0.1"', + 'HTTP_SEC_CH_UA_PLATFORM_VERSION' => '"14.0.0"', + 'HTTP_SEC_CH_UA_ARCH' => "x86", + 'HTTP_SEC_CH_UA_BITNESS' => "64", + 'HTTP_SEC_CH_UA_MODEL' => "" + ]; + $expected = [ + 'bot' => null, + 'brand' => '', + 'client' => [ + 'type' => 'browser', + 'name' => 'Microsoft Edge', + 'version' => '97.0.1072.66' + ], + 'device' => 1, + 'model' => '', + 'os' => [ + 'name' => 'Windows', + 'version' => '10', + ], + ]; + + self::writeFakeFile($expected, $userAgent); + + $factory = new Factory(); + + // not using client hints will return the cached entry + $deviceDetection = $factory->makeInstance($userAgent, []); + $this->assertInstanceOf(CachedEntry::class, $deviceDetection); + $this->assertEquals(null, $deviceDetection->getBot()); + $this->assertEquals($expected['brand'], $deviceDetection->getBrandName()); + $this->assertEquals($expected['client'], $deviceDetection->getClient()); + $this->assertEquals(1, $deviceDetection->getDevice()); + $this->assertEquals($expected['model'], $deviceDetection->getModel()); + $this->assertEquals($expected['os'], $deviceDetection->getOs()); + + // using client hints will overwrite some value if needed + $deviceDetection = $factory->makeInstance($userAgent, $clientHints); + $expected['os'] = [ + 'name' => 'Windows', + 'version' => '11', + 'short_name' => 'WIN', + 'platform' => 'x64', + 'family' => 'Windows', + ]; + $expected['client'] = [ + 'type' => 'browser', + 'name' => 'Microsoft Edge', + 'version' => '98.0', + 'short_name' => 'PS', + // empty because cached result doesn't contain it and engine won't be detected through client hints only + 'engine' => '', + 'engine_version' => '', + 'family' => 'Internet Explorer', + ]; + + if (version_compare('4.11.0', Version::VERSION, '>=')) { + // On older versions client hints were not supported and thus the result differed. + $expected['os'] = [ + 'name' => 'Windows', + 'version' => '10', + ]; + $expected['client'] = [ + 'type' => 'browser', + 'name' => 'Microsoft Edge', + 'version' => '97.0.1072.66' + ]; + } + + $this->assertInstanceOf(CachedEntry::class, $deviceDetection); + $this->assertEquals(null, $deviceDetection->getBot()); + $this->assertEquals($expected['brand'], $deviceDetection->getBrandName()); + $this->assertEquals($expected['client'], $deviceDetection->getClient()); + $this->assertEquals(1, $deviceDetection->getDevice()); + $this->assertEquals($expected['model'], $deviceDetection->getModel()); + $this->assertEquals($expected['os'], $deviceDetection->getOs()); + } + + public function testGetInstanceFromDeviceDetector() + { + $userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0"; + $expected = [ + 'client' => [ + 'type' => 'browser', + 'name' => 'Firefox', + 'short_name' => 'FF', + 'version' => '33.0', + 'engine' => 'Gecko', + 'engine_version' => '33.0', + ], + 'os' => [ + 'name' => 'Mac', + 'short_name' => 'MAC', + 'version' => '10.10', + 'platform' => '', + ], + ]; + + $factory = new Factory(); + $deviceDetection = $factory->makeInstance($userAgent, []); + $this->assertInstanceOf("\DeviceDetector\DeviceDetector", $deviceDetection); + $this->assertEquals(null, $deviceDetection->getBot()); + $this->assertEquals('AP', $deviceDetection->getBrand()); + $client = $deviceDetection->getClient(); + unset($client['family']); // newer version of DD might return the family. We ignore it here, to allow tests with older Matomo versions + $this->assertEquals($expected['client'], $client); + $this->assertEquals(0, $deviceDetection->getDevice()); + $this->assertEquals('', $deviceDetection->getModel()); + $os = $deviceDetection->getOs(); + unset($os['family']); // newer version of DD might return the family. We ignore it here, to allow tests with older Matomo versions + $this->assertEquals($expected['os'], $os); + } + + public static function writeFakeFile($expected, $userAgent) + { + $filePath = CachedEntry::getCachePath($userAgent, true); + $content = "getDataTable($name); + $dataTable->queueFilter('ReplaceColumnNames'); + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + return $dataTable; + } + + /** + * Returns metrics for detected browser plugins on the requested site. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Browser plugin metrics with visit percentages. + */ + public function getPlugin($idSite, string $period, string $date, $segment = false) + { + // fetch all archive data required + $dataTable = $this->getDataTable(Archiver::PLUGIN_RECORD_NAME, $idSite, $period, $date, $segment); + $browserVersions = $this->getDataTable(DDArchiver::BROWSER_VERSION_RECORD_NAME, $idSite, $period, $date, $segment); + $archive = Archive::build($idSite, $period, $date, $segment); + $visitsSums = $archive->getDataTableFromNumeric('nb_visits'); + + if ($dataTable instanceof DataTable\Map) { + $dataTable->multiFilter( + [ + $browserVersions instanceof DataTable\Map ? $browserVersions : null, + $visitsSums instanceof DataTable\Map ? $visitsSums : null, + ], + function (DataTable $pluginTable, ?DataTable $browserVersionsTable, ?DataTable $visitsTable): void { + $this->addVisitsPercentProcessedMetric($pluginTable, $browserVersionsTable, $visitsTable); + } + ); + } else { + $this->addVisitsPercentProcessedMetric( + $dataTable, + $browserVersions instanceof DataTable ? $browserVersions : null, + $visitsSums instanceof DataTable ? $visitsSums : null + ); + } + + $dataTable->queueFilter('ColumnCallbackAddMetadata', ['label', 'logo', __NAMESPACE__ . '\getPluginsLogo']); + $dataTable->queueFilter('ColumnCallbackReplace', ['label', 'ucfirst']); + $dataTable->queueFilter('RangeCheck', ['nb_visits_percentage', 0, 1]); + + return $dataTable; + } + + private function addVisitsPercentProcessedMetric( + DataTable $table, + ?DataTable $browserVersions, + ?DataTable $visits + ): void { + // Calculate percentage, but ignore IE users because plugin detection doesn't work on IE + $ieVisits = 0; + + $browserVersionsToExclude = [ + 'IE;10.0', + 'IE;9.0', + 'IE;8.0', + 'IE;7.0', + 'IE;6.0', + ]; + foreach ($browserVersionsToExclude as $browserVersionToExclude) { + $ieStats = $browserVersions ? $browserVersions->getRowFromLabel($browserVersionToExclude) : false; + if ($ieStats !== false) { + $ieVisits += $ieStats->getColumn(Metrics::INDEX_NB_VISITS); + } + } + + if (!$visits || $visits->getRowsCount() == 0) { + $visitsSumTotal = 0; + } else { + $visitsSumTotal = (float)$visits->getFirstRow()->getColumn('nb_visits'); + } + + $visitsSum = $visitsSumTotal - $ieVisits; + + $extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME); + $extraProcessedMetrics = is_array($extraProcessedMetrics) ? $extraProcessedMetrics : []; + $extraProcessedMetrics[] = new VisitsPercent($visitsSum); + $table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics); + } +} diff --git a/plugins/DevicePlugins/Archiver.php b/plugins/DevicePlugins/Archiver.php new file mode 100644 index 0000000000000000000000000000000000000000..9fa8221648f4c919da2dee49097b888dad3b9a87 --- /dev/null +++ b/plugins/DevicePlugins/Archiver.php @@ -0,0 +1,22 @@ +getName()]); + + $metric = $dimensionMetricFactory->createCustomMetric('nb_visits_with_' . $this->getMetricId(), $name, 'sum(%s)'); + $metric->setType(Dimension::TYPE_NUMBER); + $metricsList->addMetric($metric); + } +} diff --git a/plugins/DevicePlugins/Columns/Plugin.php b/plugins/DevicePlugins/Columns/Plugin.php new file mode 100644 index 0000000000000000000000000000000000000000..443d00bf8ce70892b739c972be2025ab5ab60c7e --- /dev/null +++ b/plugins/DevicePlugins/Columns/Plugin.php @@ -0,0 +1,17 @@ +getParams()); + } +} diff --git a/plugins/DevicePlugins/Columns/PluginFlash.php b/plugins/DevicePlugins/Columns/PluginFlash.php new file mode 100644 index 0000000000000000000000000000000000000000..16dd7035cddf2462c06a896c1946ef2bceec298e --- /dev/null +++ b/plugins/DevicePlugins/Columns/PluginFlash.php @@ -0,0 +1,37 @@ +getParams()); + } +} diff --git a/plugins/DevicePlugins/Columns/PluginJava.php b/plugins/DevicePlugins/Columns/PluginJava.php new file mode 100644 index 0000000000000000000000000000000000000000..b500dbca406fc4f1b0616bfea92a398ada913356 --- /dev/null +++ b/plugins/DevicePlugins/Columns/PluginJava.php @@ -0,0 +1,37 @@ +getParams()); + } +} diff --git a/plugins/DevicePlugins/Columns/PluginPdf.php b/plugins/DevicePlugins/Columns/PluginPdf.php new file mode 100644 index 0000000000000000000000000000000000000000..69bafdc9b56e814d7ea61c38fb3a24bddc8bf4a6 --- /dev/null +++ b/plugins/DevicePlugins/Columns/PluginPdf.php @@ -0,0 +1,37 @@ +getParams()); + } +} diff --git a/plugins/DevicePlugins/Columns/PluginQuickTime.php b/plugins/DevicePlugins/Columns/PluginQuickTime.php new file mode 100644 index 0000000000000000000000000000000000000000..5293a36bbfb2f622fd816383c9624d1805727e53 --- /dev/null +++ b/plugins/DevicePlugins/Columns/PluginQuickTime.php @@ -0,0 +1,37 @@ +getParams()); + } +} diff --git a/plugins/DevicePlugins/Columns/PluginRealPlayer.php b/plugins/DevicePlugins/Columns/PluginRealPlayer.php new file mode 100644 index 0000000000000000000000000000000000000000..8a4bdb7f416dea1d1612cefe336c7515d113a60a --- /dev/null +++ b/plugins/DevicePlugins/Columns/PluginRealPlayer.php @@ -0,0 +1,37 @@ +getParams()); + } +} diff --git a/plugins/DevicePlugins/Columns/PluginSilverlight.php b/plugins/DevicePlugins/Columns/PluginSilverlight.php new file mode 100644 index 0000000000000000000000000000000000000000..bee11a6cb105ec2ec39e71b8999afc302bbdf586 --- /dev/null +++ b/plugins/DevicePlugins/Columns/PluginSilverlight.php @@ -0,0 +1,37 @@ +getParams()); + } +} diff --git a/plugins/DevicePlugins/Columns/PluginWindowsMedia.php b/plugins/DevicePlugins/Columns/PluginWindowsMedia.php new file mode 100644 index 0000000000000000000000000000000000000000..477b33dc9594466d65513f755f865f51f29c0c8f --- /dev/null +++ b/plugins/DevicePlugins/Columns/PluginWindowsMedia.php @@ -0,0 +1,37 @@ +getParams()); + } +} diff --git a/plugins/DevicePlugins/DevicePlugins.php b/plugins/DevicePlugins/DevicePlugins.php new file mode 100644 index 0000000000000000000000000000000000000000..9c3183696f0c7b3859ab1e0664be4c7df1bd75d2 --- /dev/null +++ b/plugins/DevicePlugins/DevicePlugins.php @@ -0,0 +1,77 @@ + 'addMetricTranslations', + ); + } + + public function addMetricTranslations(&$translations) + { + $metrics = array( + 'nb_visits_percentage' => Piwik::translate('General_ColumnPercentageVisits'), + ); + + $translations = array_merge($translations, $metrics); + } + + + /** + * Returns all available DevicePlugins Columns + * + * @return Columns\DevicePluginColumn[] + * @throws \Exception + */ + public static function getAllPluginColumns() + { + $cacheId = CacheId::pluginAware('DevicePluginColumns'); + $cache = Cache::getTransientCache(); + $removedDimensions = Dimension::getRemovedDimensions(); + + if (!$cache->contains($cacheId)) { + $instances = []; + + foreach (self::getAllDevicePluginsColumnClasses() as $className) { + if (!in_array($className, $removedDimensions)) { + $instances[] = new $className(); + } + } + + $cache->save($cacheId, $instances); + } + + return $cache->fetch($cacheId); + } + + /** + * Returns class names of all DevicePlugins Column classes. + * + * @return string[] + * @api + */ + protected static function getAllDevicePluginsColumnClasses() + { + return Plugin\Manager::getInstance()->findMultipleComponents('Columns', 'Piwik\Plugins\DevicePlugins\Columns\DevicePluginColumn'); + } +} diff --git a/plugins/DevicePlugins/RecordBuilders/DevicePlugins.php b/plugins/DevicePlugins/RecordBuilders/DevicePlugins.php new file mode 100644 index 0000000000000000000000000000000000000000..74d02fe9a458f15fa1e18e65af108d2df5699186 --- /dev/null +++ b/plugins/DevicePlugins/RecordBuilders/DevicePlugins.php @@ -0,0 +1,62 @@ +maxRowsInTable = PiwikConfig::getInstance()->General['datatable_archiving_maximum_rows_standard']; + $this->columnToSortByBeforeTruncation = Metrics::INDEX_NB_VISITS; + } + + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + return [ + Record::make(Record::TYPE_BLOB, Archiver::PLUGIN_RECORD_NAME), + ]; + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + $selects = []; + $columns = \Piwik\Plugins\DevicePlugins\DevicePlugins::getAllPluginColumns(); + + foreach ($columns as $column) { + $selects[] = sprintf( + "sum(case log_visit.%s when 1 then 1 else 0 end) as %s", + $column->getColumnName(), + substr($column->getColumnName(), 7) // remove leading `config_` + ); + } + + $logAggregator = $archiveProcessor->getLogAggregator(); + $query = $logAggregator->queryVisitsByDimension(array(), false, $selects, $metrics = array()); + $data = $query->fetch(); + + $cleanRow = LogAggregator::makeArrayOneColumn($data, Metrics::INDEX_NB_VISITS); + $table = DataTable::makeFromIndexedArray($cleanRow); + + return [ + Archiver::PLUGIN_RECORD_NAME => $table, + ]; + } +} diff --git a/plugins/DevicePlugins/Reports/Base.php b/plugins/DevicePlugins/Reports/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..533e7c22d983258624260d75c2bd97e345cf3491 --- /dev/null +++ b/plugins/DevicePlugins/Reports/Base.php @@ -0,0 +1,33 @@ +categoryId = 'General_Visitors'; + } + + protected function getBasicDevicePluginsDisplayProperties(ViewDataTable $view) + { + $view->config->show_search = false; + $view->config->show_exclude_low_population = false; + + $view->requestConfig->filter_limit = 5; + + if ($view->isViewDataTableId(Graph::ID)) { + $view->config->max_graph_elements = 5; + } + } +} diff --git a/plugins/DevicePlugins/Reports/GetPlugin.php b/plugins/DevicePlugins/Reports/GetPlugin.php new file mode 100644 index 0000000000000000000000000000000000000000..6e72a6d31b827431e87e4821a553cab6c4188c10 --- /dev/null +++ b/plugins/DevicePlugins/Reports/GetPlugin.php @@ -0,0 +1,71 @@ +dimension = new Plugin(); + $this->name = Piwik::translate('DevicePlugins_WidgetPlugins'); + $this->documentation = Piwik::translate('DevicePlugins_WidgetPluginsDocumentation', '
    '); + $this->metrics = array('nb_visits'); + $this->constantRowsCount = true; + $this->processedMetrics = array('nb_visits_percentage'); + $this->order = 13; + + $this->subcategoryId = 'DevicesDetection_Software'; + } + + public function getMetricsDocumentation() + { + $documentation = parent::getMetricsDocumentation(); + + $documentation['nb_visits_percentage'] = Piwik::translate('DevicePlugins_ColumnPercentageVisitsDocumentation'); + + return $documentation; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $this->getBasicDevicePluginsDisplayProperties($view); + + $view->config->addTranslations(array( + 'nb_visits_percentage' => + str_replace(' ', ' ', Piwik::translate('General_ColumnPercentageVisits')), + )); + + $view->config->show_offset_information = false; + $view->config->show_pagination_control = false; + $view->config->show_all_views_icons = false; + $view->config->show_table_all_columns = false; + $view->config->show_totals_row = false; + $view->config->columns_to_display = array('label', 'nb_visits_percentage', 'nb_visits'); + $view->config->show_footer_message = Piwik::translate('DevicePlugins_PluginDetectionDoesNotWorkInIE'); + + if (!$view->isViewDataTableId(Evolution::ID)) { + $view->config->show_limit_control = false; + } + + $view->requestConfig->filter_sort_column = 'nb_visits_percentage'; + $view->requestConfig->filter_sort_order = 'desc'; + $view->requestConfig->filter_limit = count(DevicePlugins::getAllPluginColumns()); + $view->requestConfig->totals = 0; + } +} diff --git a/plugins/DevicePlugins/VisitorDetails.php b/plugins/DevicePlugins/VisitorDetails.php new file mode 100644 index 0000000000000000000000000000000000000000..bd80885afb0311a2af649c916000ce690d29e451 --- /dev/null +++ b/plugins/DevicePlugins/VisitorDetails.php @@ -0,0 +1,63 @@ +getPlugins(); + $visitor['pluginsIcons'] = $this->getPluginIcons(); + } + + protected function getPlugins() + { + $plugins = array(); + $columns = DevicePlugins::getAllPluginColumns(); + + foreach ($columns as $column) { + $plugins[] = $column->getColumnName(); + } + + $pluginShortNames = array(); + + foreach ($plugins as $plugin) { + if (array_key_exists($plugin, $this->details) && $this->details[$plugin] == 1) { + $pluginShortName = substr($plugin, 7); + $pluginShortNames[] = $pluginShortName; + } + } + + return implode(self::DELIMITER_PLUGIN_NAME, $pluginShortNames); + } + + protected function getPluginIcons() + { + $pluginNames = $this->getPlugins(); + if (!empty($pluginNames)) { + $pluginNames = explode(self::DELIMITER_PLUGIN_NAME, $pluginNames); + $pluginIcons = array(); + + foreach ($pluginNames as $plugin) { + $pluginIcons[] = array("pluginIcon" => getPluginsLogo($plugin), "pluginName" => $plugin); + } + + return $pluginIcons; + } + + return null; + } +} diff --git a/plugins/DevicePlugins/config/config.php b/plugins/DevicePlugins/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/DevicePlugins/config/config.php @@ -0,0 +1,3 @@ +getColumnName(), 7) && $column->columnIcon) { + return $column->columnIcon; + } + } + + return false; +} diff --git a/plugins/DevicePlugins/lang/am.json b/plugins/DevicePlugins/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..99d007fc7243ce27350c734edabade99b40ae749 --- /dev/null +++ b/plugins/DevicePlugins/lang/am.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "የተሰኪዎች ዝርዝር" + } +} diff --git a/plugins/DevicePlugins/lang/ar.json b/plugins/DevicePlugins/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..77adf08d72bc9160a4e25178cb4386d942d3d3b1 --- /dev/null +++ b/plugins/DevicePlugins/lang/ar.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "قائمة الإضافات" + } +} diff --git a/plugins/DevicePlugins/lang/az.json b/plugins/DevicePlugins/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/az.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/be.json b/plugins/DevicePlugins/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..3b1526c984db8e25647ee511420fdd97dd7b5998 --- /dev/null +++ b/plugins/DevicePlugins/lang/be.json @@ -0,0 +1,6 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "Спіс плагінаў", + "WidgetPluginsDocumentation": "Гэтая справаздача паказвае, якія плагіны были ўключаны ў браўзэраў Вашых наведвальнікаў. Гэтая інфармацыя можа мець важнае значэнне для выбару правільнага спосабу дастаўкі кантэнту." + } +} diff --git a/plugins/DevicePlugins/lang/bg.json b/plugins/DevicePlugins/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..9b6ce39cd11a55293abc315149364269640198f1 --- /dev/null +++ b/plugins/DevicePlugins/lang/bg.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s без активирани плъгини", + "BrowserWithPluginsEnabled": "%1$s с активирани плъгини %2$s", + "PluginDescription": "Отчита списъка с плъгини, които се поддържат в браузърите на посетителите.", + "PluginDetectionDoesNotWorkInIE": "Забележка: Откриването на плъгини не работи в Internet Explorer преди версия 11. Този отчет се базира само на браузъри, различни от IE, и по-нови версии на IE.", + "WidgetPlugins": "Плъгини за браузър", + "WidgetPluginsDocumentation": "Този отчет показва кои плъгини за браузъри са активирали Вашите посетители. Тази информация може да е важна за избора на правилния начин за предоставяне на Вашето съдържание." + } +} diff --git a/plugins/DevicePlugins/lang/bn.json b/plugins/DevicePlugins/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/bn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/bs.json b/plugins/DevicePlugins/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/bs.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/ca.json b/plugins/DevicePlugins/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..64fb35ed5db0c4cb443508e6ad18bbadfab08651 --- /dev/null +++ b/plugins/DevicePlugins/lang/ca.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s sense connectors activats", + "BrowserWithPluginsEnabled": "%1$s amb els connectors %2$s activats", + "ColumnPercentageVisitsDocumentation": "El percentatge de visites totals on s'ha detectat el connector. Una visita pot incloure diversos connectors. El valor s'arrodoneix a nombres enters.", + "PluginDescription": "Informa de la llista de connectors compatibles amb els navegadors dels visitants.", + "PluginDetectionDoesNotWorkInIE": "Nota: la detecció de connectors no funciona a Internet Explorer abans de l'11. Aquest informe només es basa en navegadors que no són IE i en versions més noves d'IE.", + "WidgetPlugins": "Llistat de connectors", + "WidgetPluginsDocumentation": "Aquest informe mostra quins connectors tenen els vostres visitants activades. Aquesta informació pot ser important per determinar la forma correcta de mostrar el contingut." + } +} diff --git a/plugins/DevicePlugins/lang/cs.json b/plugins/DevicePlugins/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..2caf3c5eeff59bf3ebdfa0852380a9b5fb2a5bdd --- /dev/null +++ b/plugins/DevicePlugins/lang/cs.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s bez povolených zásuvných modulů", + "BrowserWithPluginsEnabled": "%1$s s povolenými zásuvnými moduly %2$s", + "ColumnPercentageVisitsDocumentation": "Procento z celkového počtu návštěv, u kterých byl zásuvný modul zjištěn. Jedna návštěva může zahrnovat více zásuvných modulů. Hodnota se zaokrouhluje na celá čísla směrem dolů.", + "PluginDescription": "Hlásí podporované zásuvné moduly v prohlížečích návštěvníků.", + "PluginDetectionDoesNotWorkInIE": "Poznámka: Detekce zásuvných modulů nepracuje v prohlížeči Interet Explorer před verzí 11. Toto hlášení je založeno na datech z ostatních prohlížečů a novějších verzích IE.", + "WidgetPlugins": "Seznam zásuvných modulů", + "WidgetPluginsDocumentation": "Toto hlášení zobrazuje zásuvné moduly, které měli vaši návštěvníci povoleny. Tato informace může být důležitá při rozhodování o tom, jakým způsobem prezentovat obsah." + } +} diff --git a/plugins/DevicePlugins/lang/cy.json b/plugins/DevicePlugins/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/da.json b/plugins/DevicePlugins/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce8a10cabd38dd2bea5a713b4b679549d30f959 --- /dev/null +++ b/plugins/DevicePlugins/lang/da.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s med ingen aktiverede udvidelsesmoduler", + "BrowserWithPluginsEnabled": "%1$s med udvidelsesmoduler %2$s aktiveret", + "PluginDescription": "Viseren liste over udvidelser, der understøttes i besøgendes browsere.", + "PluginDetectionDoesNotWorkInIE": "Bemærk: Plugins detektering virker ikke i Internet Explorer før version 11. Denne rapport er kun baseret på ikke-IE browsere og nyere versioner af IE.", + "WidgetPlugins": "Udvidelsesmoduler", + "WidgetPluginsDocumentation": "Rapporten viser, hvilke browserudvidelser de besøgende havde aktiveret. Oplysningerne kan være vigtigt for at vælge den rigtige måde at levere indholdet på." + } +} diff --git a/plugins/DevicePlugins/lang/de.json b/plugins/DevicePlugins/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..ff7530660e3f81a56d118efe1ee815b5ad017478 --- /dev/null +++ b/plugins/DevicePlugins/lang/de.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s mit keinen aktivierten Plugins", + "BrowserWithPluginsEnabled": "%1$s mit den Plugins %2$s aktiviert", + "ColumnPercentageVisitsDocumentation": "Der Prozentsatz alle Besuche, bei denen das Plugin erkannt wurde. Ein Besuch kann mehrere Plugins umfassen. Der Wert wird auf ganze Zahlen abgerundet.", + "PluginDescription": "Zeigt die Liste der Plugins an, welche in den Browsern der Benutzer unterstützt werden.", + "PluginDetectionDoesNotWorkInIE": "Hinweis: Die Erkennung von Plugins funktioniert nicht im Internet Explorer vor Version 11. Diese Statistik beruht nur auf Nicht-IE Browsern sowie neueren Version des IE.", + "WidgetPlugins": "Browser-Plugins", + "WidgetPluginsDocumentation": "Dieser Bericht zeigt Ihnen, welche Plugins Ihre Besucher in Ihren Browser aktiviert haben. Diese Informationen kann Ihnen dabei helfen, die beste Art zu finden, Ihre Inhalte auszuliefern." + } +} diff --git a/plugins/DevicePlugins/lang/dv.json b/plugins/DevicePlugins/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/el.json b/plugins/DevicePlugins/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..29871f0c6501e27c376629291b3f1d37dc23067c --- /dev/null +++ b/plugins/DevicePlugins/lang/el.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s με ανενεργά πρόσθετα", + "BrowserWithPluginsEnabled": "%1$s με %2$s ενεργά πρόσθετα", + "ColumnPercentageVisitsDocumentation": "Το ποσοστό των συνολικών επισκέψεων όπου το πρόσθετο εντοπίστηκε. Μια επίσκεψη ενδέχεται να περιλαμβάνει πολλά πρόσθετα. Η τιμή στρογγυλεύεται προς τα κάτω σε ακέραιους αριθμούς.", + "PluginDescription": "Αναφέρει τη λίστα με τα πρόσθετα που υποστηρίζουν τα προγράμματα πλοήγησης των επισκεπτών.", + "PluginDetectionDoesNotWorkInIE": "Σημείωση: Ο εντοπισμός προσθέτων δεν λειτουργεί σε προγενέστερη έκδοση της 11 του Internet Explorer. Η αναφορά αυτή βασίζεται μόνο σε προγράμματα πλοήγησης που δεν είναι ο IE και σε νεότερες εκδόσεις του IE.", + "WidgetPlugins": "Λίστα προσθέτων προγραμμάτων πλοήγησης", + "WidgetPluginsDocumentation": "Αυτή η αναφορά δείχνει ποια πρόσθετα του προγράμματος πλοήγησης έχουν ενεργά οι επισκέπτες σας. Αυτή η πληροφορία ίσως είναι σημαντική για την επιλογή του πιο σωστού τρόπου απόδοσης του περιεχομένου σας." + } +} diff --git a/plugins/DevicePlugins/lang/en.json b/plugins/DevicePlugins/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..2770b44ba1e41851f8d198afe9ce0bbbef42941b --- /dev/null +++ b/plugins/DevicePlugins/lang/en.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s with no plugins enabled", + "BrowserWithPluginsEnabled": "%1$s with plugins %2$s enabled", + "PluginDescription": "Reports the list of plugins that are supported in visitors browsers.", + "PluginDetectionDoesNotWorkInIE": "Note: Plugins detection doesn't work in Internet Explorer before 11. This report is only based on non-IE browsers and newer versions of IE.", + "WidgetPlugins": "Browser Plugins", + "WidgetPluginsDocumentation": "This report shows which browser plugins your visitors had enabled. This information might be important for choosing the right way to deliver your content.", + "ColumnPercentageVisitsDocumentation": "The percentage of total visits where the plugin was detected. A visit may include multiple plugins. The value is rounded down to whole numbers." + } +} \ No newline at end of file diff --git a/plugins/DevicePlugins/lang/eo.json b/plugins/DevicePlugins/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/eo.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/es-ar.json b/plugins/DevicePlugins/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..dfb42fa2142a933fe47b02532a29eb14a3af80dc --- /dev/null +++ b/plugins/DevicePlugins/lang/es-ar.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s sin plugins habilitados", + "BrowserWithPluginsEnabled": "%1$s con plugins %2$s habilitados", + "PluginDescription": "Informa la lista de plugins que están soportados en los navegadores de los visitantes.", + "PluginDetectionDoesNotWorkInIE": "Nota: la detección de plugins no funciona en versiones de Internet Explorer anteriores a la 11. Este informe sólo está basado en Internet Explorer 11 y en otros navegadores.", + "WidgetPlugins": "Plugins del navegador", + "WidgetPluginsDocumentation": "Este informe muestra qué plugins de navegador tus visitantes tienen habilitados. Esta información puede ser importante para elegir el método eficiente para ofrecer tu contenido." + } +} diff --git a/plugins/DevicePlugins/lang/es.json b/plugins/DevicePlugins/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..3cfdf0e6e69e6f11d8d9f3e249c1c2b4324b3213 --- /dev/null +++ b/plugins/DevicePlugins/lang/es.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s sin complementos habilitados", + "BrowserWithPluginsEnabled": "%1$s con los complementos %2$s habilitados", + "ColumnPercentageVisitsDocumentation": "El porcentaje de las visitas totales donde se detectó el plugin. Una visita puede incluir múltiples plugins. El valor se redondea a números enteros.", + "PluginDescription": "Informa la lista de complementos que están admitidos en los navegadores de internet de los visitantes.", + "PluginDetectionDoesNotWorkInIE": "Nota: La detección de complementos no funciona en Internet Explorer antes de la versión 11. Este informe está basado solamente en navegadores no-IE y nuevas versiones de IE.", + "WidgetPlugins": "Lista de Complementos", + "WidgetPluginsDocumentation": "Este informe muestra que extensiones del navegador sus visitantes han habilitado. Esta información puede ser importante sea para elegir el método eficiente de enviar su contenido." + } +} diff --git a/plugins/DevicePlugins/lang/et.json b/plugins/DevicePlugins/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..88e14d74b2c477ef190c9769841f52729b66b189 --- /dev/null +++ b/plugins/DevicePlugins/lang/et.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "Sirviku lisatarkvarad" + } +} diff --git a/plugins/DevicePlugins/lang/eu.json b/plugins/DevicePlugins/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..e0e0b92df74f8f40ae5cdc80690aedaa8de42fe4 --- /dev/null +++ b/plugins/DevicePlugins/lang/eu.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s pluginik gaitu gabe", + "BrowserWithPluginsEnabled": "%1$s %2$s plugin gaituta", + "PluginDescription": "Bisitarien nabigatzaileek onartzen dituzten pluginen berri ematen du.", + "PluginDetectionDoesNotWorkInIE": "Oharra: Pluginen detekzioak ez du funtzionatzen Internet Explorer-en 11 bertsioa arte. Txosten hau IE ez diren nabigatzaileetan eta IEren bertsio berriagoetan oinarritzen da soilik.", + "WidgetPlugins": "Nabigatzaile-pluginak", + "WidgetPluginsDocumentation": "Txosten honek erakusten du zure bisitariek zein nabigatzaile-plugin zituzten gaituta. Informazio hau garrantzitsua izan daiteke zure edukia banatzeko modu egokia aurkitzeko." + } +} diff --git a/plugins/DevicePlugins/lang/fa.json b/plugins/DevicePlugins/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..84d558ab8b43029d204f5cbeb9af45cb8cb0b9ee --- /dev/null +++ b/plugins/DevicePlugins/lang/fa.json @@ -0,0 +1,8 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s با هیچ پلاگین را فعال کنید", + "BrowserWithPluginsEnabled": "%1$s با پلاگین ها%2$s فعال کنید", + "WidgetPlugins": "لیست افزونه ها", + "WidgetPluginsDocumentation": "این گزارش نشان می دهد که پلاگین مرورگر بازدید کننده خود را فعال کرده بود. این اطلاعات ممکن است مهم برای انتخاب راه درست برای ارائه محتوای خود را." + } +} diff --git a/plugins/DevicePlugins/lang/fi.json b/plugins/DevicePlugins/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..1646d35d4a8f3c7b3f44cb3306c232f6b4454ee7 --- /dev/null +++ b/plugins/DevicePlugins/lang/fi.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s ilman liitännäisiä aktivoitu", + "BrowserWithPluginsEnabled": "%1$s liitännäisillä %2$s on aktivoitu", + "ColumnPercentageVisitsDocumentation": "Niiden käyntien prosenttiosuus kokonaismäärästä, joilla liitännäinen havaittiin. Käynti voi sisältää useita liitännäisiä. Arvo pyöristetään alaspäin kokonaislukuun.", + "PluginDescription": "Lista liitännäisistä, joita kävijöiden selaimet tukevat.", + "PluginDetectionDoesNotWorkInIE": "Huom. Liitännäisten tunnistus ei toimi Internet Explorerissa ennen versiota 11. Tässä raportissa on mukana vain uudet IE:t ja muut selaimet.", + "WidgetPlugins": "Selainliitännäiset", + "WidgetPluginsDocumentation": "Tämä raportti näyttää, mitä selainliitännäisiä kävijöilläsi oli käytössä. Tästä tiedosta voi olla hyötyä, kun joudut valitsemaan, miten tietoa esitetään ja välitetään kävijöille." + } +} diff --git a/plugins/DevicePlugins/lang/fr.json b/plugins/DevicePlugins/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..fb92cf9e45206adb9b45944ab18c817900b8dd9f --- /dev/null +++ b/plugins/DevicePlugins/lang/fr.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s avec aucun plugin activé", + "BrowserWithPluginsEnabled": "%1$s avec les plugins %2$s activés", + "ColumnPercentageVisitsDocumentation": "Pourcentage du nombre total de visites où le plugin a été détecté. Une visite peut inclure plusieurs plugins. La valeur est arrondie à l’entier inférieur.", + "PluginDescription": "Rapporte la liste des composants supportés par les navigateurs des visiteurs.", + "PluginDetectionDoesNotWorkInIE": "Note : la détections de plugins ne fonctionne pas avec Internet Explorer d'une version inférieure à 11. Ce Rapport est basé uniquement sur les utilsateurs non IE et les versions récentes d'IE.", + "WidgetPlugins": "Liste de Plugins", + "WidgetPluginsDocumentation": "Ce rapport montre quels plugins du navigateur vos visiteurs ont activés. Cette information peut être importante pour choisir le bon moyen de délivrer le contenu." + } +} diff --git a/plugins/DevicePlugins/lang/ga.json b/plugins/DevicePlugins/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..4227b67b4f212b02d0247210f32e14de61f1cf00 --- /dev/null +++ b/plugins/DevicePlugins/lang/ga.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s gan forlíontán ar bith cumasaithe", + "BrowserWithPluginsEnabled": "%1$s le forlíontáin %2$s cumasaithe", + "ColumnPercentageVisitsDocumentation": "Céatadán na gcuairteanna iomlána inar braitheadh an breiseán. Féadfaidh cuairt breiseáin iolracha a bheith san áireamh. Déantar an luach a shlánú síos go huimhreacha iomlána.", + "PluginDescription": "Tuairiscíonn sé liosta na bhforlíontán a dtacaítear leo i mbrabhsálaithe cuairteoirí.", + "PluginDetectionDoesNotWorkInIE": "Nóta: Ní oibríonn braiteadh breiseán in Internet Explorer roimh 11. Níl an tuarascáil seo bunaithe ach ar bhrabhsálaithe neamh-IE agus ar leaganacha níos nuaí de IE.", + "WidgetPlugins": "Breiseáin Brabhsálaithe", + "WidgetPluginsDocumentation": "Léiríonn an tuarascáil seo na forlíontáin bhrabhsálaí a bhí cumasaithe ag do chuairteoirí. D’fhéadfadh an fhaisnéis seo a bheith tábhachtach chun an bealach ceart a roghnú chun d’inneachar a sheachadadh." + } +} diff --git a/plugins/DevicePlugins/lang/gl.json b/plugins/DevicePlugins/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..0eeb90acedf01d48009b48f1ea0a7aa253706c22 --- /dev/null +++ b/plugins/DevicePlugins/lang/gl.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s sen complementos activos", + "BrowserWithPluginsEnabled": "%1$s cos complementos %2$s activos", + "PluginDescription": "Informa da lista de complementos compatibles nos navegadores dos visitantes.", + "PluginDetectionDoesNotWorkInIE": "Nota: a detección dos complementos non funciona en Internet Explorer antes do 11. Este informe só está baseado en navegadores que non son de IE e en versións máis recentes de IE.", + "WidgetPlugins": "Lista de plugins", + "WidgetPluginsDocumentation": "Este informe mostra que complementos do navegador activaron os teus visitantes. Esta información pode ser importante para escollela forma correcta de entregalo teu contido." + } +} diff --git a/plugins/DevicePlugins/lang/gu.json b/plugins/DevicePlugins/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..f57321bae5ff178f42889b14d37976d56e773180 --- /dev/null +++ b/plugins/DevicePlugins/lang/gu.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s કોઈ પ્લગઈન્સ સક્ષમ કર્યા વિના", + "BrowserWithPluginsEnabled": "%1$s પ્લગઇન્સ સાથે %2$s સક્ષમ", + "PluginDescription": "મુલાકાતીઓ બ્રાઉઝર્સમાં સમર્થિત પ્લગિન્સની સૂચિની જાણ કરે છે.", + "PluginDetectionDoesNotWorkInIE": "નોંધ: ઈન્ટરનેટ એક્સપ્લોરર 11પહેલા પ્લગઈન્સ ડિટેક્શન કામ કરતું નથી. આ રિપોર્ટ ફક્ત નોન-IE બ્રાઉઝર્સ અને IE ના નવા વર્ઝન પર આધારિત છે.", + "WidgetPlugins": "બ્રાઉઝર પ્લગઈન્સ", + "WidgetPluginsDocumentation": "આ રિપોર્ટ બતાવે છે કે તમારા મુલાકાતીઓએ કયા બ્રાઉઝર પ્લગિન્સને સક્ષમ કર્યા છે. આ માહિતી તમારી સામગ્રીને પહોંચાડવાની યોગ્ય રીત પસંદ કરવા માટે મહત્વપૂર્ણ હોઈ શકે છે." + } +} diff --git a/plugins/DevicePlugins/lang/he.json b/plugins/DevicePlugins/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..e19e5264a75786177d9d0043d7eaacfb1100ead5 --- /dev/null +++ b/plugins/DevicePlugins/lang/he.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s ללא תוספים פעילים" + } +} diff --git a/plugins/DevicePlugins/lang/hi.json b/plugins/DevicePlugins/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..7a87fe8a40c9176804c04a91d8c8be41c0d12f14 --- /dev/null +++ b/plugins/DevicePlugins/lang/hi.json @@ -0,0 +1,8 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s के साथ प्लगइन सक्रिय नहीं", + "BrowserWithPluginsEnabled": "प्लगिन %2$s से %1$s सक्षम", + "PluginDescription": "आगंतुकों ब्राउज़रों में समर्थन कर रहे हैं कि प्लग इन की सूची रिपोर्टें।", + "WidgetPlugins": "ब्राउज़र प्लगइन्स" + } +} diff --git a/plugins/DevicePlugins/lang/hr.json b/plugins/DevicePlugins/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..c1e99647447808a11e73b21944e39ad0dd47d834 --- /dev/null +++ b/plugins/DevicePlugins/lang/hr.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s s nijednim aktiviranim dodatkom", + "BrowserWithPluginsEnabled": "%1$s s aktiviranim dodacima %2$s", + "ColumnPercentageVisitsDocumentation": "Postotak ukupnog broja posjeta u kojima je otkriven dodatak. Posjet može uključivati više dodataka. Vrijednost se zaokružuje na cijele brojeve.", + "PluginDescription": "Prikazuje popis dodataka koji su podržani u preglednicima posjetitelja.", + "PluginDetectionDoesNotWorkInIE": "Napomena: Otkrivanje dodataka ne radi u Internet Exploreru starijem od verzije 11. Ovaj se izvještaj temelji samo na preglednicima koji nisu IE i novijim IE verzijama.", + "WidgetPlugins": "Dodaci preglednika", + "WidgetPluginsDocumentation": "Ovaj izvještaj prikazuje koje su dodatke preglednika tvoji posjetitelji aktivirali. Ove informacije mogu biti važne za biranje pravog načina isporuke tvog sadržaja." + } +} diff --git a/plugins/DevicePlugins/lang/hu.json b/plugins/DevicePlugins/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..66123a021f8e631919a53f99b060a22778a363b7 --- /dev/null +++ b/plugins/DevicePlugins/lang/hu.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "Bővítmények listája" + } +} diff --git a/plugins/DevicePlugins/lang/hy.json b/plugins/DevicePlugins/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/id.json b/plugins/DevicePlugins/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..f550d9d836a444ce5e34361350f0448efc2031c8 --- /dev/null +++ b/plugins/DevicePlugins/lang/id.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s tanpa pengaya yang diaktifkan", + "BrowserWithPluginsEnabled": "%1$s dengan pengaya %2$s yang diaktifkan", + "PluginDescription": "Melaporkan daftar pengaya yang didukung di peramban pengunjung.", + "PluginDetectionDoesNotWorkInIE": "Catatan: Deteksi pengaya tidak berfungsi di Internet Explorer sebelum versi 11. Laporan ini hanya didasarkan pada peramban non-IE dan versi IE yang lebih baru.", + "WidgetPlugins": "Pengaya Peramban", + "WidgetPluginsDocumentation": "Laporan ini menunjukkan pengaya peramban mana yang diaktifkan oleh pengunjung Anda. Informasi ini mungkin penting untuk memilih cara yang tepat untuk menyampaikan konten Anda." + } +} diff --git a/plugins/DevicePlugins/lang/is.json b/plugins/DevicePlugins/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..504fe010d1a197e249bc6c8548b401a885066bed --- /dev/null +++ b/plugins/DevicePlugins/lang/is.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "Listi yfir íbætur" + } +} diff --git a/plugins/DevicePlugins/lang/it.json b/plugins/DevicePlugins/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..dd8c4ad4cbeaf1c747bd40c3164cdfe918c081da --- /dev/null +++ b/plugins/DevicePlugins/lang/it.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s con nessun plugin abilitato", + "BrowserWithPluginsEnabled": "%1$s con plugin %2$s abilitati", + "ColumnPercentageVisitsDocumentation": "Percentuale delle visite totali in cui è stato rilevato il plugin. Una visita può includere più plugin. Il valore è arrotondato per difetto a numeri interi.", + "PluginDescription": "Restituisce l'elenco dei plugin supportati dal browser dei visitatori.", + "PluginDetectionDoesNotWorkInIE": "Nota: L'individuazione dei plugin non funziona con versioni di Internet Explorer precedenti alla 11. Questo report è basato solamente su browser diversi da IE e con versioni recenti di IE.", + "WidgetPlugins": "Plugin Browser", + "WidgetPluginsDocumentation": "Questo report mostra quali plugin del browser i visitatori avevano abilitato. Questa informazione potrebbe essere importante per la scelta del giusto modo di inviare i tuoi contenuti." + } +} diff --git a/plugins/DevicePlugins/lang/ja.json b/plugins/DevicePlugins/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..02a0613ae96f104510fafe1c87782b421e05df19 --- /dev/null +++ b/plugins/DevicePlugins/lang/ja.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s のプラグインが有効ではありません", + "BrowserWithPluginsEnabled": "%1$s のプラグイン %2$s は有効", + "PluginDescription": "ビジターのブラウザでサポートされているプラグインの一覧をレポートします。", + "PluginDetectionDoesNotWorkInIE": "注:プラグインの検出は Internet Explorer 11 より前では機能しません。このレポートは IE 以外のブラウザーと IE の新しいバージョンにのみ基づいています。", + "WidgetPlugins": "ブラウザプラグイン", + "WidgetPluginsDocumentation": "ビジターが利用しているブラウザのプラグインについてのリポートです。コンテンツの最適な表示方法を選択するために重要な情報です。" + } +} diff --git a/plugins/DevicePlugins/lang/ka.json b/plugins/DevicePlugins/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..9272f87d21e1477961fa66160719af9e95ff7299 --- /dev/null +++ b/plugins/DevicePlugins/lang/ka.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "პლაგინების სია" + } +} diff --git a/plugins/DevicePlugins/lang/ko.json b/plugins/DevicePlugins/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..7e0a8c0ec41ac32c0e9fdbdb417d5035800bb1dc --- /dev/null +++ b/plugins/DevicePlugins/lang/ko.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "플러그인이 활성화되지 않은 %1$s", + "BrowserWithPluginsEnabled": "플러그인 %2$s가 활성화된 %1$s", + "PluginDescription": "방문자 브라우저에서 지원되는 플러그인 목록을 보고합니다.", + "PluginDetectionDoesNotWorkInIE": "참고: Internet Explorer 11 이전 버전에서는 플러그인 검색이 작동하지 않습니다. 이 보고서는 IE 브라우저가 아닌것과 새로운 버전의 IE에 기반합니다.", + "WidgetPlugins": "플러그인 목록", + "WidgetPluginsDocumentation": "방문자가 사용하는 브라우저의 플러그인에 대한 보고서입니다. 컨텐츠에 대한 최적의 표시 방법을 선택하는 데 중요한 정보입니다." + } +} diff --git a/plugins/DevicePlugins/lang/ku.json b/plugins/DevicePlugins/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/ku.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/lb.json b/plugins/DevicePlugins/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/lb.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/lt.json b/plugins/DevicePlugins/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..d2d5a70ba5e0f23c3f31e540a7b3f28196c22c1d --- /dev/null +++ b/plugins/DevicePlugins/lang/lt.json @@ -0,0 +1,8 @@ +{ + "DevicePlugins": { + "PluginDescription": "Praneša lankytojų naršyklėse palaikomų papildinių sąrašą.", + "PluginDetectionDoesNotWorkInIE": "Pastaba: Papildinių aptikimas neveikia naršyklėse, kurios senesnės nei Internet Explorer 11. Ši ataskaita yra paremta tik ne IE naršyklėmis ir naujesnėmis IE naršyklės versijomis.", + "WidgetPlugins": "Naršyklės papildiniai", + "WidgetPluginsDocumentation": "Ši ataskaita parodo kuriuos naršyklės papildinius jūsų lankytojai buvo įjungę. Ši informacija gali būti svarbi, pasirenkant teisingą savo turinio pristatymo būdą." + } +} diff --git a/plugins/DevicePlugins/lang/lv.json b/plugins/DevicePlugins/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..975bba1c4094e297d57979978f036800566c2866 --- /dev/null +++ b/plugins/DevicePlugins/lang/lv.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s bez iespējotiem spraudņiem", + "BrowserWithPluginsEnabled": "%1$s ar iespējotiem spraudņiem %2$s", + "PluginDescription": "Ziņo par spraudņiem, kas tiek atbalstīti apmeklētāju pārlūkos.", + "PluginDetectionDoesNotWorkInIE": "Piezīme: spraudņu noteikšana nedarbojas Internet Explorer pārlūkā pirms 11. versijas. Šis ziņojums ir balstīts tikai uz ne-IE pārlūkiem un jaunākām IE versijām.", + "WidgetPlugins": "Spraudņu saraksts", + "WidgetPluginsDocumentation": "Šajā ziņojumā redzami pārlūku spraudņi, kuri bija ieslēgti apmeklētāju pārlūkos. Šī informācija ir svarīga, lai izvēlētos vislabāko veidu kā piegādāt saturu apmeklētājiem." + } +} diff --git a/plugins/DevicePlugins/lang/ms.json b/plugins/DevicePlugins/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..8efccce8dbfe4b3867c07f2bfd6ea46534ec0027 --- /dev/null +++ b/plugins/DevicePlugins/lang/ms.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s tanpa pemalam didayakan", + "BrowserWithPluginsEnabled": "%1$s dengan pemalam %2$s didayakan", + "PluginDescription": "Melaporkan senarai pemalam yang disokong dalam pelayar pelawat.", + "PluginDetectionDoesNotWorkInIE": "Nota: Pengesanan pemalam tidak berfungsi dalam Internet Explorer sebelum 11. Laporan ini hanya berdasarkan pelayar bukan IE dan versi IE yang lebih baharu.", + "WidgetPlugins": "Pemalam Penyemak Imbas", + "WidgetPluginsDocumentation": "Laporan ini menunjukkan pemalam penyemak imbas yang telah didayakan oleh pelawat anda. Maklumat ini mungkin penting untuk memilih cara yang betul untuk menyampaikan kandungan anda." + } +} diff --git a/plugins/DevicePlugins/lang/nb.json b/plugins/DevicePlugins/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..1931ad490600e9edf8acb2e3d9e864911f083c4f --- /dev/null +++ b/plugins/DevicePlugins/lang/nb.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s uten noen aktiverte utvidelser", + "BrowserWithPluginsEnabled": "%1$s med utvidelsene %2$s aktivert", + "PluginDescription": "Rapporter med lister av utvidelser som er støttet i besøkeres nettlesere.", + "PluginDetectionDoesNotWorkInIE": "Merk. Utvidelsesgjenkjenning fungerer ikke i Internet Explorer før versjon 11. Denne rapporten baserer seg kun på ikke-IE-nettlesere og nyere versjoner av IE.", + "WidgetPlugins": "Liste over utvidelser", + "WidgetPluginsDocumentation": "Denne rapporten viser hvilke utvidelser som dine besøkere har aktivert. Denne informasjonen kan være nyttig for velge riktig måte å presentere ditt innhold." + } +} diff --git a/plugins/DevicePlugins/lang/nl.json b/plugins/DevicePlugins/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..19bcedd7f011feb299e18e9865ee7c6211cea37b --- /dev/null +++ b/plugins/DevicePlugins/lang/nl.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s zonder plugins ingeschakeld", + "BrowserWithPluginsEnabled": "%1$s met plugins %2$s ingeschakeld", + "ColumnPercentageVisitsDocumentation": "Het percentage totaal aantal bezoeken waarbij de plugin werd gedetecteerd. Een bezoek kan meerdere plugins bevatten. De waarde wordt afgerond naar hele getallen.", + "PluginDescription": "Rapporteert de lijst van plugins die door browsers van bezoekers worden ondersteund.", + "PluginDetectionDoesNotWorkInIE": "Opmerking: plugin detectie werkt niet in Internet Explorer-versies lager dan 11. Dit rapport is alleen gebaseerd op nieuwere versies van IE en andere browsers dan IE.", + "WidgetPlugins": "Geïnstalleerde plugins", + "WidgetPluginsDocumentation": "Dit rapport laat zien welke browserplugins uw bezoekers haden geïnstalleerd. Deze informatie kan van belang zijn voor het kiezen van de juiste manier om uw content aan te bieden." + } +} diff --git a/plugins/DevicePlugins/lang/nn.json b/plugins/DevicePlugins/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..9329e3c0fb43d32c2391fd1bbc3775b2c4aa4a72 --- /dev/null +++ b/plugins/DevicePlugins/lang/nn.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "Liste over innstikk" + } +} diff --git a/plugins/DevicePlugins/lang/pl.json b/plugins/DevicePlugins/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..4fb8f9705ab8cbf634d3c9cec7960f5d0e245d6c --- /dev/null +++ b/plugins/DevicePlugins/lang/pl.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s bez aktywnych wtyczek", + "BrowserWithPluginsEnabled": "%1$s z aktywnymi wtyczkami: %2$s", + "PluginDescription": "Zwraca listę wtyczek wspieranych przez przeglądarki odwiedzających.", + "PluginDetectionDoesNotWorkInIE": "NOTKA: Wykrywanie wtyczek nie działa w starszych niż 11 wersjach Internet Explorer'a. Ten raport bazuje na danych z nowszych wersji IE i pozostałych przeglądarek.", + "WidgetPlugins": "Lista wtyczek", + "WidgetPluginsDocumentation": "Ten raport pokazuje, które wtyczki były aktywne w przeglądarkach Twoich odwiedzających. To może być ważna informacja, która pomoże zdecydować w jaki sposób zwracać treści." + } +} diff --git a/plugins/DevicePlugins/lang/pt-br.json b/plugins/DevicePlugins/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..4d32629f74fdec91c66fe9c75982eb52c019a68c --- /dev/null +++ b/plugins/DevicePlugins/lang/pt-br.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s sem plugins habilitados", + "BrowserWithPluginsEnabled": "%1$s com os plugins %2$s habilitados", + "ColumnPercentageVisitsDocumentation": "A porcentagem do total de visitas em que o plugin foi detectado. Uma visita pode incluir vários plugins. O valor é arredondado para baixo, para números inteiros.", + "PluginDescription": "Informa a lista de plugins que são suportados nos navegadores dos visitantes.", + "PluginDetectionDoesNotWorkInIE": "Obs: A detecção de plugins não funciona no Internet Explorer anterior ao 11. Este relatório se baseia apenas em navegadores não-IE e versões mais recentes do IE.", + "WidgetPlugins": "Plugins do navegador", + "WidgetPluginsDocumentation": "Este relatório mostra quais plugins de navegador seus visitantes tinham habilitados. Esta informação pode ser importante para escolher a maneira correta para entregar o seu conteúdo." + } +} diff --git a/plugins/DevicePlugins/lang/pt.json b/plugins/DevicePlugins/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..e36dd646396f8791700bc5193bc9c796c36b6bdb --- /dev/null +++ b/plugins/DevicePlugins/lang/pt.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s sem plugins ativos", + "BrowserWithPluginsEnabled": "%1$s com plugins %2$s ativos", + "ColumnPercentageVisitsDocumentation": "A porcentagem de visitas totais onde o plugin foi detectado. Uma visita pode incluir vários plugins. O valor é arredondado para números inteiros.", + "PluginDescription": "Reporta a lista de plugins que são suportados nos navegadores dos visitantes.", + "PluginDetectionDoesNotWorkInIE": "Nota: A deteção de plugins não funciona no Internet Explorer anterior ao 11. Este relatório é baseado apenas nos outros navegadores e novas versões do IE.", + "WidgetPlugins": "Lista de plugins", + "WidgetPluginsDocumentation": "Este relatório mostra quais os plugins que os navegadores dos seus visitantes tinham ativados. Esta informação pode ser importante para a escolha do caminho certo para distribuir o seu conteúdo." + } +} diff --git a/plugins/DevicePlugins/lang/ro.json b/plugins/DevicePlugins/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..46696b535fcf63e551dfa366720ff4a351b68806 --- /dev/null +++ b/plugins/DevicePlugins/lang/ro.json @@ -0,0 +1,8 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s fără plugin-uri activate", + "BrowserWithPluginsEnabled": "%1$s cu plugin-uri %2$s activate", + "WidgetPlugins": "Lista pluginurilor", + "WidgetPluginsDocumentation": "Acest raport arată ce plugin-uri de browser-ul au activat vizitatorii. Aceste informație ar putea fi importanta pentru a alege modul corect de a livra conținut." + } +} diff --git a/plugins/DevicePlugins/lang/ru.json b/plugins/DevicePlugins/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..a108049f6ee23cb6f1da8995e3c118b7ec47a79f --- /dev/null +++ b/plugins/DevicePlugins/lang/ru.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s без активированных плагинов", + "BrowserWithPluginsEnabled": "%1$s с плагинами %2$s включен", + "PluginDescription": "Сообщает список плагинов, доступных в браузерах посетителей.", + "PluginDetectionDoesNotWorkInIE": "Внимание: Определение плагинов не работает в Internet Explorer до 11. Этот отчет базируется только на не-IE браузерах и новейших версиях IE.", + "WidgetPlugins": "Список плагинов", + "WidgetPluginsDocumentation": "Этот отчет показывается какие плагины посетители используют в своих браузерах. Эта информация может быть важна для того, чтобы посетители смогли видеть ваш контент должным образом." + } +} diff --git a/plugins/DevicePlugins/lang/si.json b/plugins/DevicePlugins/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/sk.json b/plugins/DevicePlugins/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..cf85b37009fddd8b19e7af47a20659c31ffaf00b --- /dev/null +++ b/plugins/DevicePlugins/lang/sk.json @@ -0,0 +1,7 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s bez žiadnych aktivovaných modulov", + "BrowserWithPluginsEnabled": "%1$s s modulmi %2$s aktivovanými", + "WidgetPlugins": "Moduly prehliadača" + } +} diff --git a/plugins/DevicePlugins/lang/sl.json b/plugins/DevicePlugins/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..84f03ab65787d722b132ac5ac25085a215b7abcf --- /dev/null +++ b/plugins/DevicePlugins/lang/sl.json @@ -0,0 +1,7 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s brez omogočenih vtičnikov", + "BrowserWithPluginsEnabled": "%1$s z omogočenimi vtičniki %2$s", + "WidgetPlugins": "Seznam Vtičnikov" + } +} diff --git a/plugins/DevicePlugins/lang/sq.json b/plugins/DevicePlugins/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..0b56f93cde0ab72d32d2173222380554af05eec9 --- /dev/null +++ b/plugins/DevicePlugins/lang/sq.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s pa shtojca të aktivizuara", + "BrowserWithPluginsEnabled": "%1$s me shtojca %2$s të aktivizuara", + "ColumnPercentageVisitsDocumentation": "Përqindja e vizitave në të cilat u pikas shtojca. Një vizitë mund të përfshijë shtojca të shumta. Vlera përafrohet në numra të plotë.", + "PluginDescription": "Raporton listën e shtojcave që mbulohen nga shfletuesit e përdoruesve.", + "PluginDetectionDoesNotWorkInIE": "Shënim: Zbulimi i shtojcave nuk funksionon nën Internet Explorer më të hershëm se 11. Ky raport bazohet vetëm në shfletues jo-IE dhe versione IE më të rinj.", + "WidgetPlugins": "Shtojca Shfletuesish", + "WidgetPluginsDocumentation": "Ky raport tregon se cilat shtojca shfletuesi kanë të aktivizuara vizitorët tuaj. Ky informacion mund të jetë i vlefshëm për zgjedhjen e mënyrës më të përshtatshme për ofrimin e lëndës suaj." + } +} diff --git a/plugins/DevicePlugins/lang/sr.json b/plugins/DevicePlugins/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..c7512b404dd0a76fd50638c042dab3907032baad --- /dev/null +++ b/plugins/DevicePlugins/lang/sr.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s sa isključenim dodacima", + "BrowserWithPluginsEnabled": "%1$s sa uključenim dodacima %2$s", + "PluginDescription": "Prikazuje spisak dodataka koji su podržani u brauzerima posetilaca.", + "PluginDetectionDoesNotWorkInIE": "Pažnja: detekcija dodataka ne radi za Internet Eksplorer pre verzije 11. Ovaj izveštaj se odnosi samo na brauzere koji nisu Internet Ekslorer i na nove verzije dotičnog", + "WidgetPlugins": "Lista dodataka", + "WidgetPluginsDocumentation": "Ovaj izveštaj prikazuje koje dodatke za brauzere vaši posetioci imaju uključene. Ova informacija može biti od značaja prilikom odabira pravog načina prikaza sadržaja na vašem sajtu." + } +} diff --git a/plugins/DevicePlugins/lang/sv.json b/plugins/DevicePlugins/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..a5a88e00b112e575a1562be21cef15a42ee7134a --- /dev/null +++ b/plugins/DevicePlugins/lang/sv.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s med inga plugins är aktiverad", + "BrowserWithPluginsEnabled": "%1$s med plugins %2$s är aktiverad", + "ColumnPercentageVisitsDocumentation": "Den totala andelen besök där insticksprogram upptäcktes. Ett besök kan omfatta flera insticksprogram. Värdet är avrundat nedåt till hela siffror.", + "PluginDescription": "Visar en lista av plugins som stöds i besökarnas webbläsare.", + "PluginDetectionDoesNotWorkInIE": "Notering: Detektering av tillägg fungerar inte i Internet Explorer med versioner före 11. Denna rapport baseras endast på icke-IE webbläsare och nyare versioner av IE.", + "WidgetPlugins": "Lista över plugins", + "WidgetPluginsDocumentation": "Denna rapport visar vilka plugins i webbläsaren som besökarna hade aktiverat. Denna information kan vara viktig för att välja rätt sätt att leverera ditt innehåll." + } +} diff --git a/plugins/DevicePlugins/lang/ta.json b/plugins/DevicePlugins/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..162d459af335645bbbed7135e628f1f6be4c4f29 --- /dev/null +++ b/plugins/DevicePlugins/lang/ta.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s எந்த செருகுநிரல்களும் இயக்கப்படாது", + "BrowserWithPluginsEnabled": "%1$s செருகுநிரல்களுடன் %2$s இயக்கப்பட்டது", + "ColumnPercentageVisitsDocumentation": "சொருகு கண்டறியப்பட்ட மொத்த வருகைகளின் சதவீதம். ஒரு வருகை பல செருகுநிரல்களையும் கொண்டிருக்கலாம். மதிப்பு முழு எண்களுக்கு மேல் மாற்றப்படுகிறது.", + "PluginDescription": "பார்வையாளர்கள் உலாவிகளில் ஆதரிக்கப்படும் செருகுநிரல்களின் பட்டியலைப் புகாரளிக்கும்.", + "PluginDetectionDoesNotWorkInIE": "குறிப்பு: செருகுநிரல்களைக் கண்டறிதல் 11 க்கு முன் Internet Explorer இல் வேலை செய்யாது. இந்த அறிக்கை IE அல்லாத உலாவிகள் மற்றும் IE இன் புதிய பதிப்புகளின் அடிப்படையில் மட்டுமே.", + "WidgetPlugins": "உலாவி செருகுநிரல்கள்", + "WidgetPluginsDocumentation": "உங்கள் பார்வையாளர்கள் எந்த உலாவி செருகுநிரல்களை இயக்கியுள்ளனர் என்பதை இந்த அறிக்கை காட்டுகிறது. உங்கள் உள்ளடக்கத்தை வழங்குவதற்கான சரியான வழியைத் தேர்ந்தெடுப்பதற்கு இந்தத் தகவல் முக்கியமானதாக இருக்கலாம்." + } +} diff --git a/plugins/DevicePlugins/lang/te.json b/plugins/DevicePlugins/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/te.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/th.json b/plugins/DevicePlugins/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..4a3c7e497271dc8abc098e923a582b0cbce26c05 --- /dev/null +++ b/plugins/DevicePlugins/lang/th.json @@ -0,0 +1,5 @@ +{ + "DevicePlugins": { + "WidgetPlugins": "รายการปลั้กอิน" + } +} diff --git a/plugins/DevicePlugins/lang/tl.json b/plugins/DevicePlugins/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..8f75ef87d216a163300824c425f3a857f1592698 --- /dev/null +++ b/plugins/DevicePlugins/lang/tl.json @@ -0,0 +1,8 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s na may plugins na hindi pinapagana", + "BrowserWithPluginsEnabled": "%1$s na may mga plugin %2$s na naka-enable", + "WidgetPlugins": "Browser Plugins", + "WidgetPluginsDocumentation": "Ang ulat na ito ay ipinapakita kung anong browser plugis ang gumagana sa iyong bisita. Ang impormasyon na ito may maaring mahalaga sa pagpili kung paanu ihahatid ang nilalaman nito." + } +} diff --git a/plugins/DevicePlugins/lang/tr.json b/plugins/DevicePlugins/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..cf12b9fe8a8867169ef423946276e2a240e6ed91 --- /dev/null +++ b/plugins/DevicePlugins/lang/tr.json @@ -0,0 +1,11 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s eklentiler etkinleştirilmemiş olarak", + "BrowserWithPluginsEnabled": "%1$s, %2$s eklentileri etkinleştirilmiş olarak", + "ColumnPercentageVisitsDocumentation": "Eklentinin algılandığı toplam ziyaretlerin yüzdesi. Bir ziyaret birden fazla eklenti tarafından işlenebilir. Değer tam sayılara yuvarlanır.", + "PluginDescription": "Ziyaretçilerin tarayıcıları tarafından desteklenen eklentilerin listesini görüntüler.", + "PluginDetectionDoesNotWorkInIE": "Not: Eklenti algılaması Internet Explorer 11 öncesindeki sürümleri desteklemez. Bu raporda yalnızca IE dışındaki tarayıcılar ve yeni IE tarayıcıları için veriler bulunur.", + "WidgetPlugins": "Tarayıcı eklentileri", + "WidgetPluginsDocumentation": "Bu rapor ziyaretçilerin tarayıcılarında hangi eklentilerin etkin olduğu bilgisini içerir. Bu bilgiler içeriğinizi doğru şekilde sunmanız için önemli olabilir." + } +} diff --git a/plugins/DevicePlugins/lang/tzm.json b/plugins/DevicePlugins/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/tzm.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/uk.json b/plugins/DevicePlugins/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..8a6c4b0c8fda45640b3a6d5b4538b480d41a1f7e --- /dev/null +++ b/plugins/DevicePlugins/lang/uk.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s без активованих плагінів", + "BrowserWithPluginsEnabled": "%1$s з доповненнями %2$s ввімкнено", + "PluginDescription": "Повідомляє список плагінів, доступних в браузерах відвідувачів.", + "PluginDetectionDoesNotWorkInIE": "Увага: Визначення плагінів не працює в Internet Explorer до 11. Цей звіт базується тільки на не-IE браузерах і новітніх версіях IE.", + "WidgetPlugins": "Список плагінів", + "WidgetPluginsDocumentation": "Цей звіт показується які плагіни відвідувачі використовують в своїх браузерах. Ця інформація може бути важлива для того, щоб відвідувачі змогли побачити ваш контент належним чином." + } +} diff --git a/plugins/DevicePlugins/lang/ur.json b/plugins/DevicePlugins/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicePlugins/lang/ur.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicePlugins/lang/vi.json b/plugins/DevicePlugins/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..99dc69dd02c789eff4ac262dae7c738f93e0f85f --- /dev/null +++ b/plugins/DevicePlugins/lang/vi.json @@ -0,0 +1,8 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s không có plugin nào được kích hoạt", + "BrowserWithPluginsEnabled": "%1$s với plugin %2$s đã kích hoạt", + "WidgetPlugins": "Các Plugin trình duyệt", + "WidgetPluginsDocumentation": "Báo cáo này cho thấy các plugin trình duyệt mà khách truy cập của bạn đã kích hoạt. Thông tin này có thể là quan trọng cho việc lựa chọn cách đúng để cung cấp nội dung của bạn." + } +} diff --git a/plugins/DevicePlugins/lang/zh-cn.json b/plugins/DevicePlugins/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..18c2b0506aa29cda5d3b7eda31fafadcb0d567c8 --- /dev/null +++ b/plugins/DevicePlugins/lang/zh-cn.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s 没有启用任何插件", + "BrowserWithPluginsEnabled": "%1$s 启用了插件%2$s", + "PluginDescription": "报告访客浏览器支持的插件列表。", + "PluginDetectionDoesNotWorkInIE": "注意:插件检测无法在 Internet Explorer 11 之前的版本运行。此报表仅用于非 IE 浏览器或者 IE 的新版本。", + "WidgetPlugins": "浏览器插件", + "WidgetPluginsDocumentation": "此报表显示访客浏览器已启用的插件。这些信息可能对选择发布您内容的合适方式至关重要。" + } +} diff --git a/plugins/DevicePlugins/lang/zh-tw.json b/plugins/DevicePlugins/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..7b5651670e160acd7d189f20ca552099d1c2c245 --- /dev/null +++ b/plugins/DevicePlugins/lang/zh-tw.json @@ -0,0 +1,10 @@ +{ + "DevicePlugins": { + "BrowserWithNoPluginsEnabled": "%1$s 沒有啟用任何外掛", + "BrowserWithPluginsEnabled": "%1$s 啟用外掛 %2$s", + "PluginDescription": "顯示訪客的瀏覽器中所有啟用的外掛程式。", + "PluginDetectionDoesNotWorkInIE": "注意:外掛偵測功能無法在比 Internet Explorer 11 舊的版本中運作。這份報表僅基於非 IE 瀏覽器或新版的 IE 來統計。", + "WidgetPlugins": "瀏覽器外掛清單", + "WidgetPluginsDocumentation": "這份報表顯示訪客所使用的瀏覽器中已啟用的外掛。這些資訊對你以正確的方式提供內容來說可能很重要。" + } +} diff --git a/plugins/DevicePlugins/tests/Integration/GetPluginApiTest.php b/plugins/DevicePlugins/tests/Integration/GetPluginApiTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cb34368b449d042cd3c9bd5f51a8326abe7ec0a8 --- /dev/null +++ b/plugins/DevicePlugins/tests/Integration/GetPluginApiTest.php @@ -0,0 +1,72 @@ +trackVisit(1, '2024-01-16 10:00:00'); + $this->trackVisit(1, '2024-01-17 10:00:00'); + $this->trackVisit(2, '2024-01-16 11:00:00'); + $this->trackVisit(2, '2024-01-17 11:00:00'); + } + + public function testGetPluginShouldSupportMultiSiteAndMultiPeriodRequests(): void + { + $result = API::getInstance()->getPlugin('1,2', 'day', '2024-01-16,2024-01-17'); + + self::assertInstanceOf(DataTable\Map::class, $result); + + $result->applyQueuedFilters(); + + foreach ($result->getDataTables() as $siteTable) { + self::assertInstanceOf(DataTable\Map::class, $siteTable); + + foreach ($siteTable->getDataTables() as $periodTable) { + self::assertInstanceOf(DataTable::class, $periodTable); + self::assertNotEmpty( + array_filter( + $periodTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME) ?: [], + function ($metric) { + return $metric instanceof VisitsPercent; + } + ) + ); + self::assertGreaterThan(0, $periodTable->getRowsCount()); + } + } + } + + private function trackVisit(int $idSite, string $dateTime): void + { + $tracker = Fixture::getTracker($idSite, $dateTime, true, true); + $tracker->setIp(sprintf('10.0.0.%d', $idSite)); + $tracker->setPlugins(true, false, true, false, true); + $tracker->setUrl(sprintf('https://example.org/site-%d', $idSite)); + Fixture::checkResponse($tracker->doTrackPageView('Plugin Report Visit')); + } +} diff --git a/plugins/DevicesDetection/API.php b/plugins/DevicesDetection/API.php new file mode 100644 index 0000000000000000000000000000000000000000..0565a45b268ac6c8e91d0075b1564648607a37ab --- /dev/null +++ b/plugins/DevicesDetection/API.php @@ -0,0 +1,395 @@ +getDataTable($name); + $dataTable->queueFilter('ReplaceColumnNames'); + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + return $dataTable; + } + + /** + * Returns visit information grouped by device type. + * + * This report always includes every device type Matomo can detect, even if a type has zero visits. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map The device type report with segment metadata for each device type. + */ + public function getType($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable('DevicesDetection_types', $idSite, $period, $date, $segment); + // ensure all device types are in the list + $this->ensureDefaultRowsInTable($dataTable); + + $mapping = AbstractDeviceParser::getAvailableDeviceTypeNames(); + $dataTable->filter('AddSegmentByLabelMapping', ['deviceType', $mapping]); + $dataTable->filter('ColumnCallbackAddMetadata', ['label', 'logo', __NAMESPACE__ . '\getDeviceTypeLogo']); + $dataTable->filter('GroupBy', ['label', __NAMESPACE__ . '\getDeviceTypeLabel']); + return $dataTable; + } + + /** + * @param DataTable|DataTable\Map $dataTable + */ + protected function ensureDefaultRowsInTable($dataTable): void + { + $requiredRows = array_fill(0, count(AbstractDeviceParser::getAvailableDeviceTypes()), Metrics::INDEX_NB_VISITS); + + $dataTable->filter(function (DataTable $table) use ($requiredRows) { + if ($table->getRowsCount() == 0) { + return; + } + foreach ($requiredRows as $requiredRow => $key) { + $row = $table->getRowFromLabel((string)$requiredRow); + if (empty($row)) { + $table->addRowsFromSimpleArray([ + ['label' => $requiredRow, $key => 0], + ]); + } + } + }); + } + + /** + * Returns visit information grouped by device brand. + * + * Most device brand information is only available for non-desktop devices. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map The device brand report with segment metadata for each detected brand. + */ + public function getBrand($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable('DevicesDetection_brands', $idSite, $period, $date, $segment); + $dataTable->filter('GroupBy', ['label', __NAMESPACE__ . '\getDeviceBrandLabel']); + $dataTable->filter('ColumnCallbackAddMetadata', ['label', 'logo', __NAMESPACE__ . '\getBrandLogo']); + $dataTable->filter('AddSegmentByLabel', ['deviceBrand']); + return $dataTable; + } + + /** + * Returns visit information grouped by device model. + * + * This report is unavailable when device model detection is disabled by compliance policy. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map The device model report with segment metadata for each brand/model pair. + */ + public function getModel($idSite, string $period, string $date, $segment = false) + { + Piwik::checkUserHasViewAccess($idSite); + $idSites = Site::getIdSitesFromIdSitesString($idSite); + + // filter sites where model report disabled by compliance + $idSitesFiltered = array_filter($idSites, function ($idSite) { + return !DevicesDetection::isDeviceModelDetectionDisabledByCompliancePolicy((int)$idSite); + }); + + // show an error only if none of the requested sites is left + if (count($idSitesFiltered) === 0 && count($idSites) !== count($idSitesFiltered)) { + throw new Exception(Piwik::translate('DevicesDetection_DeviceModelReportDisabledByCompliancePolicy')); + } + + $dataTable = $this->getDataTable('DevicesDetection_models', $idSitesFiltered, $period, $date, $segment); + + $dataTable->filter(function (DataTable $table) { + foreach ($table->getRowsWithoutSummaryRow() as $row) { + /** @var string $label */ + $label = $row->getColumn('label'); + + if (strpos($label, ';') !== false) { + [$brand, $model] = explode(';', $label, 2); + $brand = getDeviceBrandLabel($brand); + } else { + $brand = ''; + $model = $label; + } + + $segment = sprintf('deviceBrand==%s;deviceModel==%s', urlencode($brand), urlencode($model)); + + $row->setMetadata('segment', $segment); + } + }); + + $dataTable->filter('GroupBy', ['label', __NAMESPACE__ . '\getModelName']); + return $dataTable; + } + + /** + * Returns visit information grouped by operating system family. + * + * For legacy archives, this report falls back to operating system version data when needed. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map The operating system family report with grouped family labels and logos. + */ + public function getOsFamilies($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable('DevicesDetection_os', $idSite, $period, $date, $segment); + + // handle legacy archives + if ($dataTable instanceof DataTable\Map || !$dataTable->getRowsCount()) { + $versionDataTable = $this->getDataTable('DevicesDetection_osVersions', $idSite, $period, $date, $segment); + $dataTable = $this->mergeDataTables($dataTable, $versionDataTable); + } + + $dataTable->filter('GroupBy', ['label', __NAMESPACE__ . '\getOSFamilyFullName']); + $dataTable->filter('ColumnCallbackAddMetadata', ['label', 'logo', __NAMESPACE__ . '\getOsFamilyLogo']); + return $dataTable; + } + + + /** + * That methods handles the fallback to version datatables to calculate those without versions. + * + * Unlike DevicesDetection plugin now, the UserSettings plugin did not store archives holding the os and browser data without + * their version number. The "version-less" reports were always generated out of the "version-containing" archives . + * For big archives (month/year) that meant that some of the data was truncated, due to the datatable entry limit. + * To avoid that data loss / inaccuracy in the future, DevicesDetection plugin will also store archives without the version. + * For data archived before DevicesDetection plugin was enabled, those archives do not exist, so we try to calculate + * them here from the "version-containing" reports if possible. + * + * @template T of DataTable|DataTable\Map + * + * @param T $dataTable + * @param T $dataTable2 + * @return T + */ + protected function mergeDataTables(DataTable\DataTableInterface $dataTable, DataTable\DataTableInterface $dataTable2) + { + if ($dataTable instanceof DataTable\Map && $dataTable2 instanceof DataTable\Map) { + $dataTables = $dataTable->getDataTables(); + + foreach ($dataTables as $label => $table) { + $versionDataTables = $dataTable2->getDataTables(); + + if (!array_key_exists($label, $versionDataTables)) { + continue; + } + $newDataTable = $this->mergeDataTables($table, $versionDataTables[$label]); + $dataTable->addTable($newDataTable, $label); + } + } elseif (!$dataTable->getRowsCount() && $dataTable2->getRowsCount()) { + $dataTable2->filter('GroupBy', ['label', function ($label) { + if (preg_match("/(.+) [0-9]+(?:\.[0-9]+)?$/", $label, $matches)) { + return $matches[1]; // should match for browsers + } + if (strpos($label, ';')) { + return substr($label, 0, 3); // should match for os + } + return $label; + }]); + return $dataTable2; + } + + return $dataTable; + } + + /** + * Returns visit information grouped by operating system version. + * + * Each row includes segment metadata for the operating system code and version. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map The operating system version report with segment metadata and logos. + */ + public function getOsVersions($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable('DevicesDetection_osVersions', $idSite, $period, $date, $segment); + + $segments = ['operatingSystemCode', 'operatingSystemVersion']; + $dataTable->filter('AddSegmentByLabel', [$segments, Archiver::BROWSER_SEPARATOR]); + $dataTable->filter('ColumnCallbackAddMetadata', ['label', 'logo', __NAMESPACE__ . '\getOsLogo']); + // use GroupBy filter to avoid duplicate rows if old (UserSettings) and new (DevicesDetection) reports were combined + $dataTable->filter('GroupBy', ['label', __NAMESPACE__ . '\getOsFullName']); + return $dataTable; + } + + /** + * Returns visit information grouped by browser family without version numbers. + * + * For legacy archives, this report falls back to browser version data when needed. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map The browser family report with grouped browser names and logos. + */ + public function getBrowsers($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable('DevicesDetection_browsers', $idSite, $period, $date, $segment); + $availableBrowsers = BrowserParser::getAvailableBrowsers(); + $dataTable->filter('AddSegmentValue', [function ($label) use ($availableBrowsers) { + if (!array_key_exists($label, $availableBrowsers) && $label !== 'UNK') { + return false; + } + return $label; + }]); + + // handle legacy archives + if ($dataTable instanceof DataTable\Map || !$dataTable->getRowsCount()) { + $versionDataTable = $this->getDataTable('DevicesDetection_browserVersions', $idSite, $period, $date, $segment); + $dataTable = $this->mergeDataTables($dataTable, $versionDataTable); + } + + $dataTable->filter('GroupBy', ['label', __NAMESPACE__ . '\getBrowserName']); + $dataTable->filter('ColumnCallbackAddMetadata', ['label', 'logo', __NAMESPACE__ . '\getBrowserFamilyLogo']); + return $dataTable; + } + + /** + * Returns visit information grouped by browser version. + * + * Each row includes segment metadata for the browser code and detected version. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map The browser version report with rewritten labels and logos. + */ + public function getBrowserVersions($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable('DevicesDetection_browserVersions', $idSite, $period, $date, $segment); + + $segments = ['browserCode', 'browserVersion']; + $dataTable->filter('AddSegmentByLabel', [$segments, Archiver::BROWSER_SEPARATOR]); + $dataTable->filter('ColumnCallbackAddMetadata', ['label', 'logo', __NAMESPACE__ . '\getBrowserLogo']); + $dataTable->filter('ColumnCallbackReplace', ['label', __NAMESPACE__ . '\getBrowserNameWithVersion']); + return $dataTable; + } + + /** + * Returns visit information grouped by browser engine. + * + * Row labels are normalized to the detected browser engine names. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map The browser engine report with grouped engine names. + */ + public function getBrowserEngines($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable('DevicesDetection_browserEngines', $idSite, $period, $date, $segment); + $dataTable->filter('AddSegmentValue'); + // use GroupBy filter to avoid duplicate rows if old (UserSettings) and new (DevicesDetection) reports were combined + $dataTable->filter('GroupBy', ['label', __NAMESPACE__ . '\getBrowserEngineName']); + return $dataTable; + } +} diff --git a/plugins/DevicesDetection/Archiver.php b/plugins/DevicesDetection/Archiver.php new file mode 100644 index 0000000000000000000000000000000000000000..89a967e406f8fc6ea5167c50b4ac0e2ebec3f107 --- /dev/null +++ b/plugins/DevicesDetection/Archiver.php @@ -0,0 +1,32 @@ +makeInstance($userAgent, $clientHints); + } +} diff --git a/plugins/DevicesDetection/Columns/BrowserEngine.php b/plugins/DevicesDetection/Columns/BrowserEngine.php new file mode 100644 index 0000000000000000000000000000000000000000..e9d32faa9523df74b20101854b9dc79de48478f3 --- /dev/null +++ b/plugins/DevicesDetection/Columns/BrowserEngine.php @@ -0,0 +1,49 @@ +getUAParser($request->getUserAgent(), $request->getClientHints()); + + $aBrowserInfo = $parser->getClient(); + + if (!empty($aBrowserInfo['engine'])) { + return $aBrowserInfo['engine']; + } + + return ''; + } +} diff --git a/plugins/DevicesDetection/Columns/BrowserName.php b/plugins/DevicesDetection/Columns/BrowserName.php new file mode 100644 index 0000000000000000000000000000000000000000..8486a37155eaf5773ad427b460ad38c84183e283 --- /dev/null +++ b/plugins/DevicesDetection/Columns/BrowserName.php @@ -0,0 +1,90 @@ +setName('DevicesDetection_BrowserCode'); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + + $segment = new Segment(); + $segment->setSegment('browserName'); + $segment->setName('DevicesDetection_ColumnBrowser'); + $segment->setAcceptedValues('FireFox, Internet Explorer, Chrome, Safari, Opera etc.'); + $segment->setNeedsMostFrequentValues(false); + $segment->setSqlFilterValue(function ($val) { + $browsers = Browser::getAvailableBrowsers(); + $browsers = array_map(function ($val) { + return mb_strtolower($val); + }, $browsers); + $result = array_search(mb_strtolower($val), $browsers); + + if ($result === false) { + $result = 'UNK'; + } + + return $result; + }); + $segment->setSuggestedValuesCallback(function ($idSite, $maxValuesToReturn, $table) { + $browserList = Browser::getAvailableBrowsers(); + return $this->sortStaticListByUsage($browserList, $table, 'browserCode', $maxValuesToReturn); + }); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + public function formatValue($value, $idSite, Formatter $formatter) + { + return \Piwik\Plugins\DevicesDetection\getBrowserName($value); + } + + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + $parser = $this->getUAParser($request->getUserAgent(), $request->getClientHints()); + + $aBrowserInfo = $parser->getClient(); + + if (!empty($aBrowserInfo['short_name'])) { + return $aBrowserInfo['short_name']; + } elseif (!empty($aBrowserInfo['name'])) { + return $aBrowserInfo['name']; + } + + return 'UNK'; + } +} diff --git a/plugins/DevicesDetection/Columns/BrowserVersion.php b/plugins/DevicesDetection/Columns/BrowserVersion.php new file mode 100644 index 0000000000000000000000000000000000000000..e0ed0d7eec41f86ff381d7fef83b57a42459e2c9 --- /dev/null +++ b/plugins/DevicesDetection/Columns/BrowserVersion.php @@ -0,0 +1,47 @@ +getUAParser($request->getUserAgent(), $request->getClientHints()); + + $aBrowserInfo = $parser->getClient(); + + if (!empty($aBrowserInfo['version'])) { + $version = substr($aBrowserInfo['version'], 0, 20); + if (DevicesDetection::shouldOnlyStoreMajorVersions($request->getIdSiteIfExists())) { + return explode('.', $version, 2)[0]; + } + return $version; + } + + return ''; + } +} diff --git a/plugins/DevicesDetection/Columns/ClientType.php b/plugins/DevicesDetection/Columns/ClientType.php new file mode 100644 index 0000000000000000000000000000000000000000..78a4793a0c08ed680061a8aee33b15e192e3143d --- /dev/null +++ b/plugins/DevicesDetection/Columns/ClientType.php @@ -0,0 +1,65 @@ +acceptValues = $clientTypeList; + } + + public function formatValue($value, $idSite, Formatter $formatter) + { + return \Piwik\Plugins\DevicesDetection\getClientTypeLabel($value); + } + + public function getEnumColumnValues() + { + return \Piwik\Plugins\DevicesDetection\getClientTypeMapping(); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + $parser = $this->getUAParser($request->getUserAgent(), $request->getClientHints()); + + $clientTypes = \Piwik\Plugins\DevicesDetection\getClientTypeMapping(); + + return array_search($parser->getClient('type'), $clientTypes) ?: null; + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } +} diff --git a/plugins/DevicesDetection/Columns/DeviceBrand.php b/plugins/DevicesDetection/Columns/DeviceBrand.php new file mode 100644 index 0000000000000000000000000000000000000000..b36320ea7fc79daeaf344582d29b9ff3ad62b068 --- /dev/null +++ b/plugins/DevicesDetection/Columns/DeviceBrand.php @@ -0,0 +1,72 @@ +acceptValues = $brandList; + + $this->sqlFilter = function ($brand) use ($brandList, $brands) { + if ($brand == Piwik::translate('General_Unknown')) { + return ''; + } + $index = array_search(trim(urldecode($brand)), $brands); + if ($index === false) { + throw new \Exception("deviceBrand segment must be one of: $brandList"); + } + return $index; + }; + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + $parser = $this->getUAParser($request->getUserAgent(), $request->getClientHints()); + + return $parser->getBrand(); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } +} diff --git a/plugins/DevicesDetection/Columns/DeviceModel.php b/plugins/DevicesDetection/Columns/DeviceModel.php new file mode 100644 index 0000000000000000000000000000000000000000..391eb05f203078212b0b0a24b981b1df6fc868f5 --- /dev/null +++ b/plugins/DevicesDetection/Columns/DeviceModel.php @@ -0,0 +1,65 @@ +getUAParser($request->getUserAgent(), $request->getClientHints()); + $genericDevice = ''; + + $deviceType = $parser->getDeviceName(); + if (!empty($deviceType)) { + $genericDevice = 'generic ' . $deviceType; + } elseif ($parser->isMobile()) { + $genericDevice = 'generic mobile'; + } + + // in privacy compliance mode, we can only detect/return generic device type, but not the model + if (DevicesDetection::isDeviceModelDetectionDisabledByCompliancePolicy($request->getIdSiteIfExists())) { + return $genericDevice; + } + + $model = $parser->getModel(); + + if (!empty($model)) { + return mb_substr($model, 0, 100); + } + + return $genericDevice; + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } +} diff --git a/plugins/DevicesDetection/Columns/DeviceType.php b/plugins/DevicesDetection/Columns/DeviceType.php new file mode 100644 index 0000000000000000000000000000000000000000..f890576560af54c69408c2c91ddda56c32d6ef26 --- /dev/null +++ b/plugins/DevicesDetection/Columns/DeviceType.php @@ -0,0 +1,65 @@ +acceptValues = $deviceTypeList; + } + + public function formatValue($value, $idSite, Formatter $formatter) + { + return \Piwik\Plugins\DevicesDetection\getDeviceTypeLabel($value); + } + + public function getEnumColumnValues() + { + $values = DeviceParser::getAvailableDeviceTypes(); + return array_flip($values); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + $parser = $this->getUAParser($request->getUserAgent(), $request->getClientHints()); + + return $parser->getDevice(); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } +} diff --git a/plugins/DevicesDetection/Columns/Os.php b/plugins/DevicesDetection/Columns/Os.php new file mode 100644 index 0000000000000000000000000000000000000000..bc963cb454b734fc3d9d149b48ebe4aea4355939 --- /dev/null +++ b/plugins/DevicesDetection/Columns/Os.php @@ -0,0 +1,95 @@ +setName('DevicesDetection_OperatingSystemCode'); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + + $segment = new Segment(); + $segment->setSegment('operatingSystemName'); + $segment->setName('DevicesDetection_ColumnOperatingSystem'); + $segment->setAcceptedValues('Windows, Linux, Mac, Android, iOS etc.'); + $segment->setNeedsMostFrequentValues(false); + $segment->setSqlFilterValue(function ($val) { + $oss = OperatingSystem::getAvailableOperatingSystems(); + $oss = array_map(function ($val) { + return mb_strtolower($val); + }, $oss); + $result = array_search(mb_strtolower($val), $oss); + + if ($result === false) { + $result = 'UNK'; + } + + return $result; + }); + $segment->setSuggestedValuesCallback(function ($idSite, $maxValuesToReturn, $table) { + return $this->sortStaticListByUsage( + OperatingSystem::getAvailableOperatingSystems(), + $table, + 'operatingSystemCode', + $maxValuesToReturn + ); + }); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + public function formatValue($value, $idSite, Formatter $formatter) + { + return \Piwik\Plugins\DevicesDetection\getOSFamilyFullName($value); + } + + public function getName() + { + return Piwik::translate('DevicesDetection_OperatingSystemFamily'); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + $parser = $this->getUAParser($request->getUserAgent(), $request->getClientHints()); + + if ($parser->isBot()) { + $os = Settings::OS_BOT; + } else { + $os = $parser->getOS(); + $os = $os['short_name'] ?? 'UNK'; + } + + return $os; + } +} diff --git a/plugins/DevicesDetection/Columns/OsVersion.php b/plugins/DevicesDetection/Columns/OsVersion.php new file mode 100644 index 0000000000000000000000000000000000000000..5ae8dbc56eedf08e297af36483c887fb45e87078 --- /dev/null +++ b/plugins/DevicesDetection/Columns/OsVersion.php @@ -0,0 +1,42 @@ +getUAParser($request->getUserAgent(), $request->getClientHints()); + + $osVersion = $parser->getOs('version'); + + if (DevicesDetection::shouldOnlyStoreMajorVersions($request->getIdSiteIfExists())) { + return explode('.', $osVersion, 2)[0]; + } + return $osVersion; + } +} diff --git a/plugins/DevicesDetection/Controller.php b/plugins/DevicesDetection/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..77346efd03a364d7588e9c17cf137b2a12324708 --- /dev/null +++ b/plugins/DevicesDetection/Controller.php @@ -0,0 +1,126 @@ +setBasicVariablesView($view); + ControllerAdmin::setBasicVariablesAdminView($view); + + $userAgent = Request::fromPost()->getStringParameter('ua', $_SERVER['HTTP_USER_AGENT']); + $clientHints = Common::getRequestVar('clienthints', '', 'json'); + + $uaParser = new DeviceDetector($userAgent, is_array($clientHints) ? ClientHints::factory($clientHints) : null); + $uaParser->parse(); + + $view->userAgent = $userAgent; + $view->clientHints = $clientHints; + $view->bot_info = $uaParser->getBot(); + $view->browser_name = $uaParser->getClient('name'); + $view->browser_short_name = $uaParser->getClient('short_name'); + $view->browser_version = $uaParser->getClient('version'); + $view->browser_logo = getBrowserLogo($uaParser->getClient('short_name')); + $view->browser_family = \DeviceDetector\Parser\Client\Browser::getBrowserFamily($uaParser->getClient('name')); + $view->browser_family_logo = getBrowserFamilyLogo($view->browser_family); + $view->os_name = $uaParser->getOs('name'); + $view->os_logo = getOsLogo($uaParser->getOs('short_name')); + $view->os_short_name = $uaParser->getOs('short_name'); + $view->os_family = \DeviceDetector\Parser\OperatingSystem::getOsFamily($uaParser->getOs('name')); + $view->os_family_logo = getOsFamilyLogo($view->os_family); + $view->os_version = $uaParser->getOs('version'); + $view->device_type = getDeviceTypeLabel($uaParser->getDeviceName()); + $view->device_type_logo = getDeviceTypeLogo($uaParser->getDevice()); + $view->device_model = $uaParser->getModel(); + $view->device_brand = getDeviceBrandLabel($uaParser->getBrand()); + $view->device_brand_logo = getBrandLogo($view->device_brand); + + return $view->render(); + } + + public function showList() + { + Piwik::checkUserHasSomeAdminAccess(); + + $view = new View('@DevicesDetection/list'); + + $type = Common::getRequestVar('type', 'brands', 'string'); + + $list = array(); + + switch ($type) { + case 'brands': + $availableBrands = \DeviceDetector\Parser\Device\AbstractDeviceParser::$deviceBrands; + + foreach ($availableBrands as $short => $name) { + if ($name != 'Unknown') { + $list[$name] = getBrandLogo($name); + } + } + break; + + case 'browsers': + $availableBrowsers = \DeviceDetector\Parser\Client\Browser::getAvailableBrowsers(); + + foreach ($availableBrowsers as $short => $name) { + $list[$name] = getBrowserLogo($short); + } + break; + + case 'browserfamilies': + $availableBrowserFamilies = \DeviceDetector\Parser\Client\Browser::getAvailableBrowserFamilies(); + + foreach ($availableBrowserFamilies as $name => $browsers) { + $list[$name] = getBrowserFamilyLogo($name); + } + break; + + case 'os': + $availableOSs = \DeviceDetector\Parser\OperatingSystem::getAvailableOperatingSystems(); + + foreach ($availableOSs as $short => $name) { + $list[$name] = getOsLogo($short); + } + break; + + case 'osfamilies': + $osFamilies = \DeviceDetector\Parser\OperatingSystem::getAvailableOperatingSystemFamilies(); + + foreach ($osFamilies as $name => $oss) { + $list[$name] = getOsFamilyLogo($name); + } + break; + + case 'devicetypes': + $deviceTypes = \DeviceDetector\Parser\Device\AbstractDeviceParser::getAvailableDeviceTypes(); + + foreach ($deviceTypes as $name => $id) { + $list[$name] = getDeviceTypeLogo($id); + } + break; + } + + $view->itemList = $list; + + return $view->render(); + } +} diff --git a/plugins/DevicesDetection/DevicesDetection.php b/plugins/DevicesDetection/DevicesDetection.php new file mode 100644 index 0000000000000000000000000000000000000000..952aef6ad2124b28df03f0cc018bd5b71c9aaf8d --- /dev/null +++ b/plugins/DevicesDetection/DevicesDetection.php @@ -0,0 +1,92 @@ + 'getClientSideTranslationKeys', + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'Segment.filterSegments' => 'filterSegments', + ]; + } + + public function getClientSideTranslationKeys(&$translations) + { + $translations[] = 'DevicesDetection_UserAgent'; + $translations[] = 'General_Refresh'; + $translations[] = 'DevicesDetection_BotDetected'; + $translations[] = 'DevicesDetection_ColumnOperatingSystem'; + $translations[] = 'Mobile_ShowAll'; + $translations[] = 'CorePluginsAdmin_Version'; + $translations[] = 'DevicesDetection_OperatingSystemFamily'; + $translations[] = 'DevicesDetection_ColumnBrowser'; + $translations[] = 'DevicesDetection_BrowserFamily'; + $translations[] = 'DevicesDetection_Device'; + $translations[] = 'DevicesDetection_dataTableLabelTypes'; + $translations[] = 'DevicesDetection_dataTableLabelBrands'; + $translations[] = 'DevicesDetection_dataTableLabelModels'; + $translations[] = 'General_Close'; + $translations[] = 'DevicesDetection_DeviceDetection'; + $translations[] = 'DevicesDetection_ClientHints'; + $translations[] = 'DevicesDetection_ConsiderClientHints'; + $translations[] = 'DevicesDetection_ClientHintsNotSupported'; + } + + public function getStylesheetFiles(&$files) + { + $files[] = 'plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.less'; + } + + public static function shouldOnlyStoreMajorVersions(?int $idsite = null): bool + { + $cache = TrackerCache::getCacheWebsiteAttributes($idsite); + $cacheKey = OnlyMajorVersions::class; + return (($cache[$cacheKey] ?? false) === true); + } + + /** + * Check if compliance policy disables device model detection + * + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException + */ + public static function isDeviceModelDetectionDisabledByCompliancePolicy(?int $idSite = null): bool + { + $cache = TrackerCache::getCacheWebsiteAttributes($idSite); + $cacheKey = DeviceModelDetectionDisabled::class; + return (($cache[$cacheKey] ?? false) === true); + } + + public function filterSegments(SegmentsList &$list, array $idSites) + { + $limitSegmentsSettingEnabled = false; + if (empty($idSites)) { + $limitSegmentsSettingEnabled = LimitSegments::getInstance()->getValue(); + } else { + foreach ($idSites as $idsite) { + $limitSegmentsSettingEnabled |= LimitSegments::getInstance($idsite)->getValue(); + } + } + if ($limitSegmentsSettingEnabled) { + $list->remove('General_Visitors', 'deviceModel'); + } + } +} diff --git a/plugins/DevicesDetection/Menu.php b/plugins/DevicesDetection/Menu.php new file mode 100644 index 0000000000000000000000000000000000000000..9c1a54fc01cec94f2fa1dba524b4eee709c6515e --- /dev/null +++ b/plugins/DevicesDetection/Menu.php @@ -0,0 +1,27 @@ +addDiagnosticItem( + 'DevicesDetection_DeviceDetection', + $this->urlForAction('detection'), + $order = 40 + ); + } + } +} diff --git a/plugins/DevicesDetection/ProfileSummary/DevicesSummary.php b/plugins/DevicesDetection/ProfileSummary/DevicesSummary.php new file mode 100644 index 0000000000000000000000000000000000000000..d4bffdf5e0c14b5bba617e3f4a50f97ebc827e88 --- /dev/null +++ b/plugins/DevicesDetection/ProfileSummary/DevicesSummary.php @@ -0,0 +1,50 @@ +profile['devices'])) { + return ''; + } + + $view = new View('@DevicesDetection/_profileSummary.twig'); + $view->visitorData = $this->profile; + return $view->render(); + } + + /** + * @inheritdoc + */ + public function getOrder() + { + return 50; + } +} diff --git a/plugins/DevicesDetection/RecordBuilders/Base.php b/plugins/DevicesDetection/RecordBuilders/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..fcb3c750436247b057f08c9cf532b4e669140b31 --- /dev/null +++ b/plugins/DevicesDetection/RecordBuilders/Base.php @@ -0,0 +1,100 @@ +recordName = $recordName; + $this->labelSql = $labelSql; + + $this->maxRowsInTable = PiwikConfig::getInstance()->General['datatable_archiving_maximum_rows_standard']; + $this->maxRowsInSubtable = $this->maxRowsInTable; + $this->columnToSortByBeforeTruncation = Metrics::INDEX_NB_VISITS; + $this->enrichWithConversionMetrics = $enrichWithConversionMetrics; + } + + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + return [ + Record::make(Record::TYPE_BLOB, $this->recordName), + ]; + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + $logAggregator = $archiveProcessor->getLogAggregator(); + + $report = new DataTable(); + + $query = $logAggregator->queryVisitsByDimension(['label' => $this->labelSql]); + while ($row = $query->fetch()) { + $columns = [ + Metrics::INDEX_NB_UNIQ_VISITORS => $row[Metrics::INDEX_NB_UNIQ_VISITORS], + Metrics::INDEX_NB_VISITS => $row[Metrics::INDEX_NB_VISITS], + Metrics::INDEX_NB_ACTIONS => $row[Metrics::INDEX_NB_ACTIONS], + Metrics::INDEX_NB_USERS => $row[Metrics::INDEX_NB_USERS], + Metrics::INDEX_MAX_ACTIONS => $row[Metrics::INDEX_MAX_ACTIONS], + Metrics::INDEX_SUM_VISIT_LENGTH => $row[Metrics::INDEX_SUM_VISIT_LENGTH], + Metrics::INDEX_BOUNCE_COUNT => $row[Metrics::INDEX_BOUNCE_COUNT], + Metrics::INDEX_NB_VISITS_CONVERTED => $row[Metrics::INDEX_NB_VISITS_CONVERTED], + ]; + + $report->sumRowWithLabel($row['label'] ?? '', $columns); + } + + if ($this->enrichWithConversionMetrics) { + $labelSql = str_replace('log_visit.', 'log_conversion.', $this->labelSql); + + $query = $logAggregator->queryConversionsByDimension(['label' => $labelSql]); + while ($conversionRow = $query->fetch()) { + $label = $conversionRow['label'] ?? ''; + + $idGoal = (int) $conversionRow['idgoal']; + $columns = [ + Metrics::INDEX_GOALS => [ + $idGoal => Metrics::makeGoalColumnsRow($idGoal, $conversionRow), + ], + ]; + + $report->sumRowWithLabel($label, $columns); + } + + $report->filter(DataTable\Filter\EnrichRecordWithGoalMetricSums::class); + } + + return [$this->recordName => $report]; + } +} diff --git a/plugins/DevicesDetection/RecordBuilders/BrowserEngines.php b/plugins/DevicesDetection/RecordBuilders/BrowserEngines.php new file mode 100644 index 0000000000000000000000000000000000000000..f552e1110225449e86ddaa1638f3a5cdd504f7b6 --- /dev/null +++ b/plugins/DevicesDetection/RecordBuilders/BrowserEngines.php @@ -0,0 +1,20 @@ +categoryId = 'General_Visitors'; + } +} diff --git a/plugins/DevicesDetection/Reports/GetBrand.php b/plugins/DevicesDetection/Reports/GetBrand.php new file mode 100644 index 0000000000000000000000000000000000000000..48a7ffabd8fa382442e219882b200fa6b93d0a13 --- /dev/null +++ b/plugins/DevicesDetection/Reports/GetBrand.php @@ -0,0 +1,35 @@ +dimension = new DeviceBrand(); + $this->name = Piwik::translate('DevicesDetection_DeviceBrand'); + $this->documentation = Piwik::translate('DevicesDetection_DeviceBrandReportDocumentation'); + $this->order = 4; + $this->hasGoalMetrics = true; + $this->subcategoryId = 'DevicesDetection_Devices'; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_search = true; + $view->config->show_exclude_low_population = false; + $view->config->addTranslation('label', Piwik::translate("DevicesDetection_dataTableLabelBrands")); + } +} diff --git a/plugins/DevicesDetection/Reports/GetBrowserEngines.php b/plugins/DevicesDetection/Reports/GetBrowserEngines.php new file mode 100644 index 0000000000000000000000000000000000000000..3ba876c7aa9024f8b45a6a9eb10b81b94b27f170 --- /dev/null +++ b/plugins/DevicesDetection/Reports/GetBrowserEngines.php @@ -0,0 +1,40 @@ +dimension = new BrowserEngine(); + $this->name = Piwik::translate('DevicesDetection_BrowserEngines'); + $this->documentation = Piwik::translate('DevicesDetection_BrowserEngineDocumentation', '
    '); + $this->order = 10; + + $this->subcategoryId = 'DevicesDetection_Software'; + } + + public function getDefaultTypeViewDataTable() + { + return Pie::ID; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_search = false; + $view->config->show_exclude_low_population = false; + } +} diff --git a/plugins/DevicesDetection/Reports/GetBrowserVersions.php b/plugins/DevicesDetection/Reports/GetBrowserVersions.php new file mode 100644 index 0000000000000000000000000000000000000000..7b423b9208ea00b0f9d89a2f0aa3d86476ce64c7 --- /dev/null +++ b/plugins/DevicesDetection/Reports/GetBrowserVersions.php @@ -0,0 +1,41 @@ +dimension = new BrowserVersion(); + $this->name = Piwik::translate('DevicesDetection_BrowserVersion'); + $this->documentation = Piwik::translate('DevicesDetection_WidgetBrowserVersionsDocumentation'); + $this->order = 6; + $this->subcategoryId = 'DevicesDetection_Software'; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_search = true; + $view->config->show_exclude_low_population = false; + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('DevicesDetection', 'getBrowsers'), + ); + } +} diff --git a/plugins/DevicesDetection/Reports/GetBrowsers.php b/plugins/DevicesDetection/Reports/GetBrowsers.php new file mode 100644 index 0000000000000000000000000000000000000000..aebc36b41f1b3b420bba9a39dbd0157ce9063aa5 --- /dev/null +++ b/plugins/DevicesDetection/Reports/GetBrowsers.php @@ -0,0 +1,45 @@ +dimension = new BrowserName(); + $this->name = Piwik::translate('DevicesDetection_Browsers'); + $this->documentation = Piwik::translate('DevicesDetection_WidgetBrowsersDocumentation'); + $this->order = 5; + $this->subcategoryId = 'DevicesDetection_Software'; + $this->hasGoalMetrics = true; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $view->config->title = $this->name; + $view->config->show_search = true; + $view->config->show_exclude_low_population = false; + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('DevicesDetection', 'getBrowserVersions'), + ); + } +} diff --git a/plugins/DevicesDetection/Reports/GetModel.php b/plugins/DevicesDetection/Reports/GetModel.php new file mode 100644 index 0000000000000000000000000000000000000000..8dabb00e50b580bf591e49a596b90e09b212a338 --- /dev/null +++ b/plugins/DevicesDetection/Reports/GetModel.php @@ -0,0 +1,43 @@ +dimension = new DeviceModel(); + $this->name = Piwik::translate('DevicesDetection_DeviceModel'); + $this->documentation = Piwik::translate('DevicesDetection_DeviceModelReportDocumentation'); + $this->order = 2; + $this->hasGoalMetrics = true; + $this->subcategoryId = 'DevicesDetection_Devices'; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_search = true; + $view->config->show_exclude_low_population = false; + $view->config->addTranslation('label', Piwik::translate("DevicesDetection_dataTableLabelModels")); + } + + public function isEnabled() + { + // Metadata visibility is global-only here, so check the policy state directly. + return !PolicyManager::isPolicyActive(CnilPolicy::class, $idSite = null); + } +} diff --git a/plugins/DevicesDetection/Reports/GetOsFamilies.php b/plugins/DevicesDetection/Reports/GetOsFamilies.php new file mode 100644 index 0000000000000000000000000000000000000000..e4881441e81e7debb478c600b37f96c63dcf67a4 --- /dev/null +++ b/plugins/DevicesDetection/Reports/GetOsFamilies.php @@ -0,0 +1,43 @@ +dimension = new Os(); + $this->name = Piwik::translate('DevicesDetection_OperatingSystemFamilies'); + $this->documentation = Piwik::translate('DevicesDetection_OperatingSystemFamiliesReportDocumentation'); + $this->order = 8; + + $this->subcategoryId = 'DevicesDetection_Software'; + } + + public function configureView(ViewDataTable $view) + { + $view->config->title = $this->name; + $view->config->show_search = false; + $view->config->show_exclude_low_population = false; + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('DevicesDetection', 'getOsVersions'), + ); + } +} diff --git a/plugins/DevicesDetection/Reports/GetOsVersions.php b/plugins/DevicesDetection/Reports/GetOsVersions.php new file mode 100644 index 0000000000000000000000000000000000000000..53cb214463c104ab99339d122ae02ada2ca68297 --- /dev/null +++ b/plugins/DevicesDetection/Reports/GetOsVersions.php @@ -0,0 +1,46 @@ +dimension = new OsVersion(); + $this->name = Piwik::translate('DevicesDetection_OperatingSystemVersions'); + $this->documentation = Piwik::translate('DevicesDetection_OperatingSystemVersionsReportDocumentation'); + $this->order = 2; + + $this->subcategoryId = 'DevicesDetection_Software'; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $view->config->title = $this->name; + $view->config->show_search = true; + $view->config->show_exclude_low_population = false; + $view->config->addTranslation('label', Piwik::translate("DevicesDetection_dataTableLabelSystemVersion")); + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('DevicesDetection', 'getOsFamilies'), + ); + } +} diff --git a/plugins/DevicesDetection/Reports/GetType.php b/plugins/DevicesDetection/Reports/GetType.php new file mode 100644 index 0000000000000000000000000000000000000000..46b308ea43b9b14cdc4a8b8dbc08d0d06b041ac0 --- /dev/null +++ b/plugins/DevicesDetection/Reports/GetType.php @@ -0,0 +1,37 @@ +dimension = new DeviceType(); + $this->name = Piwik::translate('DevicesDetection_DeviceType'); + $this->documentation = Piwik::translate('DevicesDetection_DeviceTypeReportDocumentation'); + $this->order = 0; + $this->hasGoalMetrics = true; + $this->subcategoryId = 'DevicesDetection_Devices'; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $view->config->show_search = true; + $view->config->show_exclude_low_population = false; + $view->config->addTranslation('label', Piwik::translate("DevicesDetection_dataTableLabelTypes")); + } +} diff --git a/plugins/DevicesDetection/Settings/DeviceModelDetectionDisabled.php b/plugins/DevicesDetection/Settings/DeviceModelDetectionDisabled.php new file mode 100644 index 0000000000000000000000000000000000000000..a4ad353117471b3d803e5c2de7664601fef24aab --- /dev/null +++ b/plugins/DevicesDetection/Settings/DeviceModelDetectionDisabled.php @@ -0,0 +1,27 @@ + true, + ]; + } +} diff --git a/plugins/DevicesDetection/Settings/OnlyMajorVersions.php b/plugins/DevicesDetection/Settings/OnlyMajorVersions.php new file mode 100644 index 0000000000000000000000000000000000000000..687301a0f7f19ff45e2599ac0750d604f05c33ba --- /dev/null +++ b/plugins/DevicesDetection/Settings/OnlyMajorVersions.php @@ -0,0 +1,81 @@ + + * @implements SettingValueInterface + */ +class OnlyMajorVersions implements + PolicyComparisonInterface, + SettingValueInterface +{ + /** @use PolicyComparisonTrait */ + use PolicyComparisonTrait; + + /** @var bool */ + private $value; + + protected function __construct(bool $value) + { + $this->value = $value; + } + + protected static function compareStrictness($value1, $value2) + { + return $value1 || $value2; + } + + public static function getTitle(): string + { + return Piwik::translate("DevicesDetection_OnlyMajorVersionsSettingTitle"); + } + + public static function getInstance(?int $idSite = null): self + { + $values = self::getPolicyRequiredValues($idSite); + + $strictest = (bool) self::getStrictestValueFromArray($values); + return new self($strictest); + } + + public static function getInlineHelp(): string + { + return ''; + } + + public static function getPolicyRequirements(): array + { + return [ + CnilPolicy::class => true, + ]; + } + + public static function isCompliant(string $policy, ?int $idSite = null): bool + { + $policyValues = self::getPolicyRequirements(); + if (!array_key_exists($policy, $policyValues)) { + return true; + } + + $currentValue = self::getInstance($idSite)->getValue(); + + return $currentValue === $policyValues[$policy]; + } + + public function getValue() + { + return $this->value; + } + + public static function getComplianceRequirementNote(?int $idSite = null): string + { + return Piwik::translate("DevicesDetection_OnlyMajorVersionsSettingRequirementNote"); + } +} diff --git a/plugins/DevicesDetection/Updates/1.14.php b/plugins/DevicesDetection/Updates/1.14.php new file mode 100644 index 0000000000000000000000000000000000000000..3b168a8dbd9886d843b31bafb3800143455005db --- /dev/null +++ b/plugins/DevicesDetection/Updates/1.14.php @@ -0,0 +1,47 @@ +migration = $factory; + } + + public function getMigrations(Updater $updater) + { + return array( + $this->migration->db->changeColumnTypes('log_visit', array( + 'config_os_version' => 'VARCHAR( 100 ) DEFAULT NULL', + 'config_device_type' => 'VARCHAR( 100 ) DEFAULT NULL', + )), + ); + } + + public static function isMajorUpdate() + { + return true; + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrations(__FILE__, $this->getMigrations($updater)); + } +} diff --git a/plugins/DevicesDetection/VisitorDetails.php b/plugins/DevicesDetection/VisitorDetails.php new file mode 100644 index 0000000000000000000000000000000000000000..af7e14bae290301bff936ee74b576713735f4915 --- /dev/null +++ b/plugins/DevicesDetection/VisitorDetails.php @@ -0,0 +1,187 @@ +getDeviceType(); + $visitor['deviceTypeIcon'] = $this->getDeviceTypeIcon(); + $visitor['deviceBrand'] = $this->getDeviceBrand(); + $visitor['deviceModel'] = $this->getDeviceModel(); + $visitor['operatingSystem'] = $this->getOperatingSystem(); + $visitor['operatingSystemName'] = $this->getOperatingSystemName(); + $visitor['operatingSystemIcon'] = $this->getOperatingSystemIcon(); + $visitor['operatingSystemCode'] = $this->getOperatingSystemCode(); + $visitor['operatingSystemVersion'] = $this->getOperatingSystemVersion(); + $visitor['browserFamily'] = $this->getBrowserEngine(); + $visitor['browserFamilyDescription'] = $this->getBrowserEngineDescription(); + $visitor['browser'] = $this->getBrowser(); + $visitor['browserName'] = $this->getBrowserName(); + $visitor['browserIcon'] = $this->getBrowserIcon(); + $visitor['browserCode'] = $this->getBrowserCode(); + $visitor['browserVersion'] = $this->getBrowserVersion(); + } + + protected function getDeviceType() + { + return getDeviceTypeLabel($this->details['config_device_type']); + } + + protected function getDeviceTypeIcon() + { + return getDeviceTypeLogo($this->details['config_device_type']); + } + + protected function getDeviceBrand() + { + return getDeviceBrandLabel($this->details['config_device_brand']); + } + + protected function getDeviceModel() + { + return getModelName($this->details['config_device_model']); + } + + protected function getOperatingSystemCode() + { + return $this->details['config_os']; + } + + protected function getOperatingSystem() + { + return getOsFullName($this->details['config_os'] . ";" . $this->details['config_os_version']); + } + + protected function getOperatingSystemName() + { + return getOsFullName($this->details['config_os']); + } + + protected function getOperatingSystemVersion() + { + return $this->details['config_os_version']; + } + + protected function getOperatingSystemIcon() + { + return getOsLogo($this->details['config_os']); + } + + protected function getBrowserEngineDescription() + { + return getBrowserEngineName($this->getBrowserEngine()); + } + + protected function getBrowserEngine() + { + return $this->details['config_browser_engine']; + } + + protected function getBrowserCode() + { + return $this->details['config_browser_name']; + } + + protected function getBrowserVersion() + { + return $this->details['config_browser_version']; + } + + protected function getBrowser() + { + return getBrowserNameWithVersion($this->details['config_browser_name'] . ";" . $this->details['config_browser_version']); + } + + protected function getBrowserName() + { + return getBrowserName($this->details['config_browser_name']); + } + + protected function getBrowserIcon() + { + return getBrowserLogo($this->details['config_browser_name'] . ";" . $this->details['config_browser_version']); + } + + + private $devices = array(); + + public function initProfile($visits, &$profile) + { + $this->devices = array(); + } + + public function handleProfileVisit($visit, &$profile) + { + $deviceType = $visit->getColumn('deviceType'); + $deviceTypeIcon = $visit->getColumn('deviceTypeIcon'); + $deviceBrand = $visit->getColumn('deviceBrand'); + $deviceModel = $visit->getColumn('deviceModel'); + if ($deviceBrand == Piwik::translate('General_Unknown')) { + $deviceName = $deviceModel; + } else { + $deviceName = trim($deviceBrand . " " . $deviceModel); + } + + if (!isset($this->devices[$deviceType])) { + $this->devices[$deviceType] = array( + 'count' => 0, + 'icon' => $deviceTypeIcon, + 'devices' => array(), + ); + } + + ++$this->devices[$deviceType]['count']; + + if (!isset($this->devices[$deviceType]['devices'][$deviceName])) { + $this->devices[$deviceType]['devices'][$deviceName] = 0; + } + + ++$this->devices[$deviceType]['devices'][$deviceName]; + } + + public function finalizeProfile($visits, &$profile) + { + $devices = $this->devices; + uksort($this->devices, function ($a, $b) use ($devices) { + $cmp = strcmp($devices[$b]['count'], $devices[$a]['count']); + if (0 == $cmp) { + $cmp = strcmp($a, $b); + } + return $cmp; + }); + + $devices = []; + + foreach ($this->devices as $type => $devicesData) { + $typeDevices = []; + foreach ($devicesData['devices'] as $name => $count) { + $typeDevices[] = [ + 'name' => $name, + 'count' => $count, + ]; + } + $devices[] = [ + 'type' => $type, + 'count' => $devicesData['count'], + 'icon' => $devicesData['icon'], + 'devices' => $typeDevices, + ]; + } + + $profile['devices'] = $devices; + } +} diff --git a/plugins/DevicesDetection/config/config.php b/plugins/DevicesDetection/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/DevicesDetection/config/config.php @@ -0,0 +1,3 @@ + $family) { + if (in_array($label, $family)) { + return $name; + } + } + return Piwik::translate('General_Unknown'); +} + +function getBrowserFamilyLogo($label) +{ + $browserFamilies = BrowserParser::getAvailableBrowserFamilies(); + if (!empty($label) && in_array($label, BrowserParser::getAvailableBrowsers())) { + return getBrowserLogo($label); + } + if (!empty($label) && array_key_exists($label, $browserFamilies)) { + return getBrowserLogo($browserFamilies[$label][0]); + } + return getBrowserLogo($label); +} + +function getBrowserNameWithVersion($label) +{ + $pos = strrpos($label, ';'); + $short = substr($label, 0, $pos); + $ver = substr($label, $pos + 1); + $browsers = BrowserParser::getAvailableBrowsers(); + if ($short && array_key_exists($short, $browsers)) { + return trim(ucfirst($browsers[$short]) . ' ' . $ver); + } elseif (strlen($short) > 2 && $short !== 'UNK') { + return trim($short . ' ' . $ver); + } + return Piwik::translate('General_Unknown'); +} + +function getBrowserName($label) +{ + // Return early if the label is empty so that we don't try to manipulate an empty value + if (empty($label)) { + return Piwik::translate('General_Unknown'); + } + + $short = substr($label, 0, 2); + $browsers = BrowserParser::getAvailableBrowsers(); + + if ($short && array_key_exists($short, $browsers)) { + return trim(ucfirst($browsers[$short])); + } elseif (strlen($label) > 2 && strpos($label, 'UNK') === false) { + return $label; + } + + return Piwik::translate('General_Unknown'); +} + +/** + * Returns the path to the logo for the given browser + * + * First try to find a logo for the given short code + * If none can be found try to find a logo for the browser family + * Return unknown logo otherwise + * + * @param string $short Shortcode or name of browser + * + * @return string path to image + */ +function getBrowserLogo($short) +{ + $path = 'plugins/Morpheus/icons/dist/browsers/%s.png'; + + // If name is given instead of short code, try to find matching shortcode + if (!empty($short) && strlen($short) > 2) { + if (in_array($short, BrowserParser::getAvailableBrowsers())) { + $flippedBrowsers = array_flip(BrowserParser::getAvailableBrowsers()); + $short = $flippedBrowsers[$short]; + } else { + $short = substr($short, 0, 2); + } + } + + if (empty($short)) { + return sprintf($path, 'UNK'); + } + + $family = getBrowserFamilyFullName($short); + + $browserFamilies = BrowserParser::getAvailableBrowserFamilies(); + + if ( + array_key_exists($short, BrowserParser::getAvailableBrowsers()) && + file_exists(PIWIK_INCLUDE_PATH . '/' . sprintf($path, $short)) + ) { + return sprintf($path, $short); + } + + if (array_key_exists($family, $browserFamilies)) { + foreach ($browserFamilies[$family] as $browserShort) { + if (file_exists(PIWIK_INCLUDE_PATH . '/' . sprintf($path, $browserShort))) { + return sprintf($path, $browserShort); + } + } + } + + return sprintf($path, 'UNK'); +} + +function getDeviceBrandLabel($label) +{ + if (array_key_exists($label, DeviceParser::$deviceBrands)) { + return ucfirst(DeviceParser::$deviceBrands[$label]); + } + + return Piwik::translate('General_Unknown'); +} + +function getClientTypeMapping() +{ + return [ + 1 => 'browser', + 2 => 'library', + 3 => 'feed reader', + 4 => 'mediaplayer', + 5 => 'mobile app', + 6 => 'pim', + ]; +} + +function getClientTypeLabel($label) +{ + $translations = [ + 'browser' => 'DevicesDetection_ColumnBrowser', + 'library' => 'DevicesDetection_Library', + 'feed reader' => 'DevicesDetection_FeedReader', + 'mediaplayer' => 'DevicesDetection_MediaPlayer', + 'mobile app' => 'DevicesDetection_MobileApp', + 'pim' => 'DevicesDetection_Pim', + ]; + + $clientTypes = getClientTypeMapping(); + + if ( + is_numeric($label) && + array_key_exists($label, $clientTypes) && + isset($translations[$clientTypes[$label]]) + ) { + return Piwik::translate($translations[$clientTypes[$label]]); + } elseif (isset($translations[$label])) { + return Piwik::translate($translations[$label]); + } + + return Piwik::translate('General_Unknown'); +} + +function getDeviceTypeLabel($label) +{ + $translations = [ + 'desktop' => 'General_Desktop', + 'smartphone' => 'DevicesDetection_Smartphone', + 'tablet' => 'DevicesDetection_Tablet', + 'phablet' => 'DevicesDetection_Phablet', + 'feature phone' => 'DevicesDetection_FeaturePhone', + 'console' => 'DevicesDetection_Console', + 'tv' => 'DevicesDetection_TV', + 'car browser' => 'DevicesDetection_CarBrowser', + 'smart display' => 'DevicesDetection_SmartDisplay', + 'camera' => 'DevicesDetection_Camera', + 'portable media player' => 'DevicesDetection_PortableMediaPlayer', + 'smart speaker' => 'DevicesDetection_SmartSpeaker', + 'wearable' => 'DevicesDetection_Wearable', + 'peripheral' => 'DevicesDetection_Peripheral', + ]; + + $deviceTypes = DeviceParser::getAvailableDeviceTypes(); + + if ( + is_numeric($label) && + in_array($label, $deviceTypes) && + isset($translations[array_search($label, $deviceTypes)]) + ) { + return Piwik::translate($translations[array_search($label, $deviceTypes)]); + } elseif (isset($translations[$label])) { + return Piwik::translate($translations[$label]); + } + + return Piwik::translate('General_Unknown'); +} + +function getDeviceTypeLogo($label) +{ + if (is_numeric($label) && in_array($label, DeviceParser::getAvailableDeviceTypes())) { + $label = array_search($label, DeviceParser::getAvailableDeviceTypes()); + $label = strtolower($label); + $label = str_replace(' ', '_', $label); + } else { + $label = 'unknown'; + } + + return 'plugins/Morpheus/icons/dist/devices/' . $label . '.png'; +} + +function getModelName($label) +{ + if (strpos($label, ';') !== false) { + [$brand, $model] = explode(';', $label, 2); + } else { + $brand = null; + $model = $label; + } + if ($brand) { + $brand = getDeviceBrandLabel($brand); + if ($brand == Piwik::translate('General_Unknown')) { + $brand = null; + } + } + if (!$model) { + $model = Piwik::translate('General_Unknown'); + } else { + if (strpos($model, 'generic ') === 0) { + $model = substr($model, 8); + if ($model == 'mobile') { + $model = Piwik::translate( + 'DevicesDetection_GenericDevice', + Piwik::translate('DevicesDetection_MobileDevice') + ); + } else { + $model = Piwik::translate('DevicesDetection_GenericDevice', getDeviceTypeLabel($model)); + } + } + } + if (empty($brand)) { + return $model; + } + return $brand . ' - ' . $model; +} + +function getOSFamilyFullName($label) +{ + if ($label == Settings::OS_BOT) { + return 'Bot'; + } + $label = OperatingSystemParser::getOsFamily(_mapLegacyOsShortCodes($label)); + + if ($label == 'unknown') { + $label = Piwik::translate('General_Unknown'); + } elseif ($label == 'Gaming Console') { + $label = Piwik::translate('DevicesDetection_Console'); + } + + if ($label !== null) { + return $label; + } + return Piwik::translate('General_Unknown'); +} + +function getOsFamilyLogo($label) +{ + $label = _mapLegacyOsShortCodes($label); + $osFamilies = OperatingSystemParser::getAvailableOperatingSystemFamilies(); + if (!empty($label) && array_key_exists($label, $osFamilies)) { + return getOsLogo($osFamilies[$label][0]); + } + return getOsLogo($label); +} + +function getOsFullName($label) +{ + // Return early if the label is empty so that we don't try to manipulate an empty value + if (empty($label)) { + return Piwik::translate('General_Unknown'); + } + + if (substr($label, 0, 3) == Settings::OS_BOT) { + return 'Bot'; + } + if (!empty($label) && $label != ';') { + $os = substr($label, 0, 3); + $ver = substr($label, 4, 15); + $name = OperatingSystemParser::getNameFromId(_mapLegacyOsShortCodes($os), $ver); + if (!empty($name)) { + return $name; + } + } + return Piwik::translate('General_Unknown'); +} + +function _mapLegacyOsShortCodes($shortCode): string +{ + $legacyShortCodes = [ + 'IPA' => 'IOS', // iPad => iOS + 'IPH' => 'IOS', // iPhone => iOS + 'IPD' => 'IOS', // iPod => iOS + 'WIU' => 'WII', // WiiU => Nintendo + '3DS' => 'NDS', // Nintendo 3DS => Nintendo Mobile + 'DSI' => 'NDS', // Nintendo DSi => Nintendo Mobile + 'PSV' => 'PSP', // PlayStation Vita => PlayStation Portable + 'MAE' => 'SMG', // Maemo => MeeGo + 'W10' => 'WIN', + 'W2K' => 'WIN', + 'W31' => 'WIN', + 'WI7' => 'WIN', + 'WI8' => 'WIN', + 'W81' => 'WIN', + 'W95' => 'WIN', + 'W98' => 'WIN', + 'WME' => 'WIN', + 'WNT' => 'WIN', + 'WS3' => 'WIN', + 'WVI' => 'WIN', + 'WXP' => 'WIN', + //'VMS' => '', // OpenVMS => ?? + ]; + return ($shortCode && array_key_exists( + $shortCode, + $legacyShortCodes + )) ? $legacyShortCodes[$shortCode] : (string)$shortCode; +} + +/** + * Returns the path to the logo for the given OS + * + * First try to find a logo for the given short code + * If none can be found try to find a logo for the os family + * Return unknown logo otherwise + * + * @param string $short Shortcode or name of OS + * + * @return string path to image + */ +function getOsLogo($short) +{ + $path = 'plugins/Morpheus/icons/dist/os/%s.png'; + + $short = _mapLegacyOsShortCodes($short); + + // If name is given instead of short code, try to find matching shortcode + if (strlen($short) > 3) { + if (in_array($short, OperatingSystemParser::getAvailableOperatingSystems())) { + $short = array_search($short, OperatingSystemParser::getAvailableOperatingSystems()); + } else { + $short = substr($short, 0, 3); + } + } + + $family = getOSFamilyFullName($short); + $osFamilies = OperatingSystemParser::getAvailableOperatingSystemFamilies(); + + if ( + !empty($short) && + array_key_exists($short, OperatingSystemParser::getAvailableOperatingSystems()) && + file_exists(PIWIK_INCLUDE_PATH . '/' . sprintf($path, $short)) + ) { + return sprintf($path, $short); + } elseif ( + !empty($family) && + array_key_exists($family, $osFamilies) && + file_exists(PIWIK_INCLUDE_PATH . '/' . sprintf($path, $osFamilies[$family][0])) + ) { + return sprintf($path, $osFamilies[$family][0]); + } + return sprintf($path, 'UNK'); +} + +/** + * Returns the display name for a browser engine + * + * @param $engineName + * + * @return string + */ +function getBrowserEngineName($engineName) +{ + /* + * Map legacy types to engines + */ + $oldTypeMapping = [ + 'ie' => 'Trident', + 'gecko' => 'Gecko', + 'khtml' => 'KHTML', + 'webkit' => 'WebKit', + 'opera' => 'Presto', + 'unknown' => '', + ]; + if (array_key_exists($engineName, $oldTypeMapping)) { + $engineName = $oldTypeMapping[$engineName]; + } + + $displayNames = [ + 'Trident' => 'Trident (IE)', + 'Gecko' => 'Gecko (Firefox)', + 'KHTML' => 'KHTML (Konqueror)', + 'Presto' => 'Presto (Opera)', + 'WebKit' => 'WebKit (Safari)', + 'Blink' => 'Blink (Chrome, Opera)', + ]; + + if (!empty($engineName)) { + if (!empty($displayNames[$engineName])) { + return $displayNames[$engineName]; + } + return $engineName; + } + return Piwik::translate('General_Unknown'); +} diff --git a/plugins/DevicesDetection/lang/am.json b/plugins/DevicesDetection/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..7a89b8600434220e371b50984c48f3a4dab835f5 --- /dev/null +++ b/plugins/DevicesDetection/lang/am.json @@ -0,0 +1,9 @@ +{ + "DevicesDetection": { + "BrowserEngine": "ማሰሺያ", + "BrowserFamily": "የማሰሻ ቤተሰብ", + "Browsers": "ማሰሻዎች", + "ColumnBrowser": "ማሰሺያ", + "WidgetBrowsers": "የጎብኚ ማሰሻዎች" + } +} diff --git a/plugins/DevicesDetection/lang/ar.json b/plugins/DevicesDetection/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..7c62789d9e61bcdf63e56806ee64a246e7c1b40b --- /dev/null +++ b/plugins/DevicesDetection/lang/ar.json @@ -0,0 +1,44 @@ +{ + "DevicesDetection": { + "BrowserEngine": "محرّك المتصفح", + "BrowserEngineDocumentation": "يظهر هذا التقرير متصفحات زوارك مقسمة حسب محركاتها. %s أكثر المعلومات أهمية لمطوري الوب هي نوع محرك التصيير الذي يستخدمه الزوار. إن المسميات تحوي أسماء المحركات متبوعة بأكثر المتصفحات شعبية الذي يستخدم هذا المحرك بين قوسين.", + "BrowserEngines": "محرّكات المتصفح", + "BrowserFamily": "عائلة المتصفح", + "BrowserVersion": "إصدار المتصفح", + "BrowserVersions": "إصدارات المتصفح", + "Browsers": "المتصفحات", + "Camera": "الكاميرا", + "CarBrowser": "متصفح سيارة", + "ColumnBrowser": "المتصفح", + "ColumnOperatingSystem": "نظام التشغيل", + "ColumnOperatingSystemVersion": "إصدار نظام التشغيل", + "Console": "وحدة التحكُّم", + "Device": "الجهاز", + "DeviceBrand": "صانع الجهاز", + "DeviceDetection": "اكتشاف الجهاز", + "DeviceModel": "طراز الجهاز", + "DeviceType": "نوع الجهاز", + "Devices": "الأجهزة", + "DevicesDetection": "أجهزة الزائر", + "FeaturePhone": "هاتف مميزات", + "OperatingSystemFamilies": "عوائل أنظمة التشغيل", + "OperatingSystemFamily": "عائلة نظام التشغيل", + "OperatingSystemVersions": "إصدارات نظام التشغيل", + "OperatingSystems": "أنظمة التشغيل", + "Phablet": "هاتف لوحي", + "PluginDescription": "يزودك بمعلومات تفصيلية عن أجهزة الزائر ، مثل العلامة التجارية (الصانع) والطراز (إصدار الجهاز) و نوع الجهاز (تلفاز، وحدة متخصصة ، هاتف ذكي ، حاسب مكتبي ...الخ) والمزيد من المعلومات.", + "PortableMediaPlayer": "مشغل وسائط محمول", + "SmartDisplay": "شاشة ذكية", + "Smartphone": "هاتف ذكي", + "Software": "برنامج", + "TV": "تلفاز", + "Tablet": "لوحي", + "UserAgent": "وكيل-المستخدم", + "WidgetBrowserVersionsDocumentation": "يحوي هذا التقرير معلومات عن ماهية نوع المستعرض الذي استخدمه زوارك. يُذكَر كل إصدار من المستعرض لوحده.", + "WidgetBrowsers": "متصفحات الزوار", + "dataTableLabelBrands": "العلامة", + "dataTableLabelModels": "الطراز", + "dataTableLabelSystemVersion": "إصدار نظام التشغيل", + "dataTableLabelTypes": "النوع" + } +} diff --git a/plugins/DevicesDetection/lang/az.json b/plugins/DevicesDetection/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/az.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/be.json b/plugins/DevicesDetection/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..e8765821e95ae292ee067b8d024f72adfb32b84d --- /dev/null +++ b/plugins/DevicesDetection/lang/be.json @@ -0,0 +1,15 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Браўзэр", + "BrowserEngineDocumentation": "Гэты графік паказвае браўзэры наведвальнікаў, разбітыя па сем'ях. %s Найбольш важная інфармацыю для вэб-распрацоўнікаў у тым, што я ны могуць даведацца аб тыпах вэб-рэндэрынгу сваіх наведвальнікаў. Пазнакі ўтрымліваюць імёны рухавікоў найбольш распаўсюджаныя адзначаныя ў браўзэрам дужках.", + "BrowserFamily": "Сямейства браўзэраў", + "BrowserVersion": "Версія браўзэра", + "Browsers": "Па браўзарах", + "ColumnBrowser": "Браўзэр", + "ColumnOperatingSystem": "Аперацыйная сістэма", + "OperatingSystems": "Па аперацыйных сістэмах", + "WidgetBrowserVersionsDocumentation": "Гэтая справаздача змяшчае інфармацыю аб тым, які браўзэр выкарыстоўвалі наведвальнікі. Кожная версія браўзэра пералічана асобна.", + "WidgetBrowsers": "Браўзары карыстачоў", + "dataTableLabelTypes": "Тып" + } +} diff --git a/plugins/DevicesDetection/lang/bg.json b/plugins/DevicesDetection/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..87c532ff4ab23316411ce728f32f41fab9e9804e --- /dev/null +++ b/plugins/DevicesDetection/lang/bg.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Този потребителски агент е разпознат като %1$s. Посещенията с този потребителски агент няма да се проследяват в Matomo по подразбиране.", + "BrowserCode": "Код на браузъра", + "BrowserEngine": "Браузър и версия", + "BrowserEngineDocumentation": "Тази таблица показва браузърите на вашите потребители ,разделени по фамилии. %s Най-важната информация за уеб разработчиците е какъв тип технология за обработка са използвали посетителите. Етикета показва имената на технологиите, следвани от браузера, който е бил използван, поставен в скоби.", + "BrowserEngines": "Браузърни енджини", + "BrowserFamily": "Фамилия браузъри", + "BrowserVersion": "Версия на браузъра", + "BrowserVersions": "Версии на браузъра", + "Browsers": "Браузъри", + "Camera": "Камера", + "CarBrowser": "Браузър, който се използва в кола", + "ClientHints": "Клиентски съвети", + "ClientHintsNotSupported": "Вашият браузър не поддържа клиентски съвети.", + "ClientType": "Тип клиент", + "ClientTypes": "Типове клиенти", + "ColumnBrowser": "Браузър", + "ColumnOperatingSystem": "Операционна система и версия", + "ColumnOperatingSystemVersion": "Версия на операционната система", + "ConsiderClientHints": "Обмислете съветите на клиента", + "Console": "Конзола", + "Device": "Устройство", + "DeviceBrand": "Марка на устройството", + "DeviceBrandReportDocumentation": "Този отчет показва марките/производителите на устройствата, които посетителите Ви са използвали. В повечето случаи тази информация е достъпна само за устройства, които не са настолни компютри.", + "DeviceBrands": "Марки на устройства", + "DeviceDetection": "Определяне на устройство", + "DeviceModel": "Модел на устройството", + "DeviceModelReportDocumentation": "Този отчет показва устройствата, които посетителите Ви използват. Всеки модел се показва в комбинация с марката на устройството, тъй като някои имена на модели се използват от множество марки.", + "DeviceModels": "Модели на устройства", + "DeviceType": "Вид устройство", + "DeviceTypeReportDocumentation": "Този отчет показва типовете устройства, които посетителите Ви са използвали. Този отчет винаги ще показва всички типове устройства, които Matomo може да открие, дори ако няма посещения с конкретен тип.", + "DeviceTypes": "Типове устройства", + "Devices": "Устройства", + "DevicesDetection": "Устройства, които използва посетителят", + "FeaturePhone": "Несмарт телефон", + "FeedReader": "Четец на емисии", + "GenericDevice": "Общ %s", + "Library": "Библиотека", + "MediaPlayer": "Медиен плейър", + "MobileApp": "Мобилно приложение", + "MobileDevice": "Мобилно устройство", + "OperatingSystemCode": "Код на операционната система", + "OperatingSystemFamilies": "Вид операционна система", + "OperatingSystemFamiliesReportDocumentation": "Този отчет Ви показва операционните системи, които посетителите Ви използват, групирани по семейство операционни системи. Семейството операционни системи се състои от различни версии или дистрибуции.", + "OperatingSystemFamily": "Семейство на операционната система", + "OperatingSystemVersions": "Версия на операционната система", + "OperatingSystemVersionsReportDocumentation": "Този отчет Ви показва операционните системи, които използват Вашите посетители. Всяка версия и дистрибуция се показват отделно.", + "OperatingSystems": "Операционни системи", + "Peripheral": "Периферно устройство", + "Phablet": "Фаблет", + "Pim": "PIM", + "PluginDescription": "Предоставя разширена информация за потребителски устройства, като марка (производител), модел (версия на устройството), тип устройство (телевизор, конзоли, смартфони, настолни компютри и др.) и др.", + "PortableMediaPlayer": "Преносим медиен плейър", + "SmartDisplay": "„Умен“ дисплей", + "SmartSpeaker": "Смарт говорител", + "Smartphone": "Смартфон", + "Software": "Софтуер", + "TV": "ТВ", + "Tablet": "Таблет", + "UserAgent": "Потребителски агент", + "Wearable": "Устройство за носене (часовник, гривна и т.н.)", + "WidgetBrowserVersionsDocumentation": "Този отчет показва информация, за това какъв браузър са използвали вашите потребители. Всеки браузер е показан поотделно.", + "WidgetBrowsers": "Браузъри на посетителите", + "WidgetBrowsersDocumentation": "Този отчет съдържа информация за това какъв тип браузър са използвали Вашите посетители.", + "XVisitsFromDevices": "%1$s посещения от %2$s устройства", + "dataTableLabelBrands": "Марка", + "dataTableLabelModels": "Модел", + "dataTableLabelSystemVersion": "Версия на операционната система", + "dataTableLabelTypes": "Тип" + } +} diff --git a/plugins/DevicesDetection/lang/bn.json b/plugins/DevicesDetection/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/bn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/bs.json b/plugins/DevicesDetection/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/bs.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/ca.json b/plugins/DevicesDetection/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..dad3b8223c99fe5d95f1e55c50d547041ae194c0 --- /dev/null +++ b/plugins/DevicesDetection/lang/ca.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Aquest agent d'usuari es detecta com a %1$s. Les visites d'aquest agent d'usuari no seran seguides a Matomo de manera predeterminada.", + "BrowserCode": "Codi de navegador", + "BrowserEngine": "Navegador", + "BrowserEngineDocumentation": "Aquest gràfic mostra els navegadors dels vostres visitants dividits en famílies. %s La informació més important per als desenvolupadors web es quin tipus de sistema de renderització estan utilitzant els seus visitants. Les etiquetiquetes contenen els noms dels sistemes, seguit pel navegador més comú utilitzant aquest sistema.", + "BrowserEngines": "motors de navegació", + "BrowserFamily": "Família del navegador", + "BrowserVersion": "Versió del navegador", + "BrowserVersions": "versions del navegador", + "Browsers": "Navegadors", + "Camera": "càmera", + "CarBrowser": "navegador de cotxe", + "ClientHints": "Consells per al client", + "ClientHintsNotSupported": "El vostre navegador no admet suggeriments de client.", + "ClientType": "Tipus de client", + "ClientTypes": "Tipus de clients", + "ColumnBrowser": "Navegador", + "ColumnOperatingSystem": "Sistema operatiu", + "ColumnOperatingSystemVersion": "Versió del sistema operatiu", + "ConsiderClientHints": "Tingueu en compte els consells del client", + "Console": "Consola", + "Device": "Dispositiu", + "DeviceBrand": "Marca del dispositiu", + "DeviceBrandReportDocumentation": "Aquest informe mostra les marques/fabricants dels dispositius que han utilitzat els vostres visitants. En la majoria dels casos aquesta informació només està disponible per a dispositius que no són d'escriptori.", + "DeviceBrands": "Marques de dispositius", + "DeviceDetection": "Detecció de dispositius", + "DeviceModel": "Model del dispositiu", + "DeviceModelDetectionDisabled": "Detecció del model de dispositiu desactivada", + "DeviceModelDetectionDisabledRequirementNote": "La detecció del model de dispositiu i l'informe del model de dispositiu han d'estar desactivats.", + "DeviceModelReportDisabledByCompliancePolicy": "L'informe del model de dispositiu està desactivat per la política de compliment.", + "DeviceModelReportDocumentation": "Aquest informe mostra els dispositius que fan servir els vostres visitants. Cada model es mostra combinat amb la marca del dispositiu ja que alguns noms de models són utilitzats per diverses marques.", + "DeviceModels": "Models de dispositiu", + "DeviceType": "Tipus de dispositiu", + "DeviceTypeReportDocumentation": "Aquest informe mostra els tipus de dispositius que han utilitzat els vostres visitants. Aquest informe mostrarà sempre tots els tipus de dispositius que Matomo pugui detectar, encara que no hi hagi hagut visites amb un tipus específic.", + "DeviceTypes": "Tipus de dispositius", + "Devices": "Dispositius", + "DevicesDetection": "Dispositius del visitant", + "FeaturePhone": "Característiques del telèfon", + "FeedReader": "Lector de notícies", + "GenericDevice": "Genèric %s", + "Library": "Biblioteca", + "MediaPlayer": "Reproductor multimèdia", + "MobileApp": "Aplicació mòbil", + "MobileDevice": "Dispositiu mòbil", + "OnlyMajorVersionsSettingRequirementNote": "Només s'emmagatzemen les versions principals del sistema operatiu i del navegador.", + "OnlyMajorVersionsSettingTitle": "Només versions principals", + "OperatingSystemCode": "Codi de sistema operatiu", + "OperatingSystemFamilies": "Famílies del sistema operatiu", + "OperatingSystemFamiliesReportDocumentation": "Aquest informe us mostra els sistemes operatius que utilitzen els vostres visitants agrupats per família de sistemes operatius. Una família de sistemes operatius consta de diferents versions o distribucions.", + "OperatingSystemFamily": "Família del Sistema Operatiu", + "OperatingSystemVersions": "Versions del sistema operatiu", + "OperatingSystemVersionsReportDocumentation": "Aquest informe us mostra els sistemes operatius que utilitzen els vostres visitants. Cada versió i distribució es mostra per separat.", + "OperatingSystems": "Sistemes operatius", + "Peripheral": "Perifèric", + "Phablet": "Taulèfon", + "Pim": "PIM", + "PluginDescription": "Proporciona informació extesa sobre els dispositius dels usuaris, tals com marca (fabricant), model (versió del dispositiu), tipus de dispositiu (tv, consola, telèfon intel·ligent, d'escriptori, etc.) i més.", + "PortableMediaPlayer": "Reproductor de medis portàtils", + "SmartDisplay": "Pantalla intel·ligent", + "SmartSpeaker": "Altaveu intel·ligent", + "Smartphone": "Telèfon intel·ligent", + "Software": "Software", + "TV": "Televisió", + "Tablet": "Tauleta", + "UserAgent": "Agent d'usuari", + "Wearable": "Portàtil", + "WidgetBrowserVersionsDocumentation": "Aquest informe conté informació sobre quin tipus de navegador està utilitzant els vostres visitants. Cada versió del navegador es llista per separat.", + "WidgetBrowsers": "Navegadors", + "WidgetBrowsersDocumentation": "Aquest informe conté informació sobre quin tipus de navegador estaven utilitzant els vostres visitants.", + "XVisitsFromDevices": "%1$s visites provenen de %2$s dispositius", + "dataTableLabelBrands": "Marca", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Versió del sistema operatiu", + "dataTableLabelTypes": "Tipus" + } +} diff --git a/plugins/DevicesDetection/lang/cs.json b/plugins/DevicesDetection/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..61f68298912bd972618710224b9999f4a16856cd --- /dev/null +++ b/plugins/DevicesDetection/lang/cs.json @@ -0,0 +1,71 @@ +{ + "DevicesDetection": { + "BrowserCode": "Kód prohlížeče", + "BrowserEngine": "Vykreslovací jádro", + "BrowserEngineDocumentation": "Tento graf zobrazuje prohlížeče vašich návštěvníků odle prodin prohlížečů. %s Nejdůležitější informací pro webové vývojáře je to, jaká vykreslovací jádra návštěvníci používají. Název vykreslovacího jádra je v popisku následován v závorkách nejčastějším prohlížečem, který ho používá.", + "BrowserEngines": "Vykreslovací jádra", + "BrowserFamily": "Rodina Web prohlížeče", + "BrowserVersion": "Verze prohlížeče", + "BrowserVersions": "Verze prohlížečů", + "Browsers": "Web prohlížeče", + "Camera": "Fotoaparát", + "CarBrowser": "Prohlížeč v autě", + "ClientHints": "Tipy pro klienty", + "ClientHintsNotSupported": "Váš prohlížeč nepodporuje tipy pro klienty.", + "ClientType": "Typ klienta", + "ClientTypes": "Typy klientů", + "ColumnBrowser": "Web prohlížeč", + "ColumnOperatingSystem": "Operační systém", + "ColumnOperatingSystemVersion": "Verze operačního systému", + "ConsiderClientHints": "Zvažte tipy pro klienty", + "Console": "Konzole", + "Device": "Zařízení", + "DeviceBrand": "Značka zařízení", + "DeviceBrandReportDocumentation": "Tento report zobrazuje značky/výrobce zařízení, která návštěvníci používali. Ve většině případů jsou tyto informace k dispozici pouze pro jiná než stolní zařízení.", + "DeviceBrands": "Značky zařízení", + "DeviceDetection": "Detekce zařízení", + "DeviceModel": "Model zařízení", + "DeviceModelReportDocumentation": "Tento report zobrazuje zařízení, která vaši návštěvníci používají. Každý model je zobrazen v kombinaci se značkou zařízení, protože některé názvy modelů používá více značek.", + "DeviceModels": "Modely zařízení", + "DeviceType": "Typ zařízení", + "DeviceTypeReportDocumentation": "Tento report zobrazuje typy zařízení, která návštěvníci používali. Tento report vždy zobrazuje všechny typy zařízení, která je Matomo schopno detekovat, a to i v případě, že s určitým typem nebyly zaznamenány žádné návštěvy.", + "DeviceTypes": "Typy zařízení", + "Devices": "Zařízení", + "DevicesDetection": "Zařízení návštěvníků", + "FeaturePhone": "Ukázkový telefon", + "FeedReader": "Čtečka kanálu", + "GenericDevice": "Generické %s", + "Library": "Knihovna", + "MediaPlayer": "Přehrávač médií", + "MobileApp": "Mobilní aplikace", + "MobileDevice": "Mobilní zařízení", + "OperatingSystemCode": "Kód operačního systému", + "OperatingSystemFamilies": "Rodiny operačních systémů", + "OperatingSystemFamiliesReportDocumentation": "Tento report zobrazuje operační systémy, které návštěvníci používají, seskupené podle rodiny operačních systémů. Rodina operačních systémů se skládá z různých verzí nebo distribucí.", + "OperatingSystemFamily": "Operační systém", + "OperatingSystemVersions": "Verze operačního systému", + "OperatingSystemVersionsReportDocumentation": "Tento report zobrazuje operační systémy, které návštěvníci používají. Každá verze a distribuce je zobrazena zvlášť.", + "OperatingSystems": "Operační systémy", + "Peripheral": "Periferní", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Poskytuje rozšířené informace o zařízení návštěvníků, jako je značka (výrobce), model (verze zařízení), typ zařízení (tv, konzole, chytré telefony, stolní PC, aj.) a více.", + "PortableMediaPlayer": "Přenosný přehrávač médií", + "SmartDisplay": "Chytrý displej", + "SmartSpeaker": "Chytrý reproduktor", + "Smartphone": "Chytrý telefon", + "Software": "Software", + "TV": "Tv", + "Tablet": "Tablet", + "UserAgent": "User-Agent", + "Wearable": "Wearable", + "WidgetBrowserVersionsDocumentation": "Toto hlášení obsahuje informace o tom, jaký druh prohlížeče vaši návštěvníci použili. Každá verze je počítána samostatně.", + "WidgetBrowsers": "Web prohlížeče návštěvníků", + "WidgetBrowsersDocumentation": "Tento report obsahuje informace o tom, jaký prohlížeč návštěvníci používali.", + "XVisitsFromDevices": "%1$s návštěv ze zařízení %2$s", + "dataTableLabelBrands": "Značka", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Verze operačního systému", + "dataTableLabelTypes": "Typ" + } +} diff --git a/plugins/DevicesDetection/lang/cy.json b/plugins/DevicesDetection/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/da.json b/plugins/DevicesDetection/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..52860522b208cb6cd1c72a48d153d6be3053f2f9 --- /dev/null +++ b/plugins/DevicesDetection/lang/da.json @@ -0,0 +1,53 @@ +{ + "DevicesDetection": { + "BrowserCode": "Browserkode", + "BrowserEngine": "Browser", + "BrowserEngineDocumentation": "Diagrammet viser de besøgendes browsere opdelt i grupper. %s De vigtigste oplysninger for web-udviklere er, hvilken slags rendering motor de besøgende bruger. Etiketterne indeholder navnene på motorerne, efterfulgt af de mest almindelige browsere, der benytter motoren i parentes.", + "BrowserEngines": "Browsermotorer", + "BrowserFamily": "Browsertype", + "BrowserVersion": "Browserversion", + "BrowserVersions": "Browserversioner", + "Browsers": "Browsere", + "Camera": "Kamera", + "CarBrowser": "Bilbrowser", + "ColumnBrowser": "Browser", + "ColumnOperatingSystem": "Operativsystem", + "ColumnOperatingSystemVersion": "Operativsystemversion", + "Console": "Konsol", + "Device": "Enhed", + "DeviceBrand": "Enhedsmærke", + "DeviceBrands": "Enhedsmærker", + "DeviceDetection": "Detektering af enheder", + "DeviceModel": "Enhedsmodel", + "DeviceModels": "Enhedsmodeller", + "DeviceType": "Enhedstypen", + "DeviceTypes": "Enhedstyper", + "Devices": "Enheder", + "DevicesDetection": "Besøgendes enheder", + "FeaturePhone": "Telefonfunktioner", + "GenericDevice": "Uspecificeret %s", + "MobileApp": "Mobilapp", + "MobileDevice": "Mobil enhed", + "OperatingSystemCode": "Operativsystemkode", + "OperatingSystemFamilies": "Operativsystem familier", + "OperatingSystemFamily": "Operativsystemsfamilie", + "OperatingSystemVersions": "Operativsystemversioner", + "OperatingSystems": "Operativsystemer", + "Phablet": "Phablet", + "PluginDescription": "Giver udvidet information om brugerens enheder, såsom mærke (fremstiller), model (enhedsversion), enhedstype (tv, konsoller, smartphones, desktop, osv) m. m.", + "PortableMediaPlayer": "Bærbar medieafspiller", + "SmartDisplay": "Smart skærm", + "Smartphone": "Smartphone", + "Software": "Software", + "TV": "TV", + "Tablet": "Tablet", + "UserAgent": "Brugeragent", + "WidgetBrowserVersionsDocumentation": "Rapporten indeholder oplysninger om hvilken browser de besøgende bruger. Hver version er angivet særskilt.", + "WidgetBrowsers": "Besøgendes browser", + "XVisitsFromDevices": "%1$s besøg fra %2$s enheder", + "dataTableLabelBrands": "Mærke", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Operativsystemversion", + "dataTableLabelTypes": "Type" + } +} diff --git a/plugins/DevicesDetection/lang/de.json b/plugins/DevicesDetection/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..30a673f0d4e46ec72f465cb19b3ce1cb032e0493 --- /dev/null +++ b/plugins/DevicesDetection/lang/de.json @@ -0,0 +1,76 @@ +{ + "DevicesDetection": { + "BotDetected": "Dieser User-Agent wird als %1$s erkannt. Besuche mit diesem Benutzer-Agenten werden in Matomo standardmäßig nicht protokolliert.", + "BrowserCode": "Browser-Code", + "BrowserEngine": "Browser-Engine", + "BrowserEngineDocumentation": "Dieser Bericht zeigt Ihnen die Browser Ihrer Besucher aufgeteilt nach Browserfamilien. %s Die wichtigste Information für Webentwickler ist, welche Rendering-Engine ihre Besucher verwenden. Die Beschriftungen enthalten den Namen der Engine und in Klammern den Namen des bekanntesten Browsers, der diese verwendet.", + "BrowserEngines": "Browser-Engines", + "BrowserFamily": "Browserfamilie", + "BrowserVersion": "Browser-Version", + "BrowserVersions": "Browser-Versionen", + "Browsers": "Browser", + "Camera": "Digitalkamera", + "CarBrowser": "PKW-Browser", + "ClientHints": "Kundenhinweise", + "ClientHintsNotSupported": "Ihr Browser unterstützt keine Kundenhinweise.", + "ClientType": "Kundentyp", + "ClientTypes": "Kundentypen", + "ColumnBrowser": "Browser", + "ColumnOperatingSystem": "Betriebssystem", + "ColumnOperatingSystemVersion": "Betriebssystem-Version", + "ConsiderClientHints": "Kundenhinweise berücksichtigen", + "Console": "Spielekonsole", + "Device": "Gerät", + "DeviceBrand": "Gerätemarke", + "DeviceBrandReportDocumentation": "Dieser Bericht zeigt die Marke / Hersteller der Geräte, welche Ihre Besucher verwenden. In den meisten Fällen ist diese Information für Desktopgeräte nicht verfügbar.", + "DeviceBrands": "Gerätemarken", + "DeviceDetection": "Geräteerkennung", + "DeviceModel": "Gerätemodell", + "DeviceModelDetectionDisabled": "Erkennung des Gerätemodells deaktiviert", + "DeviceModelDetectionDisabledRequirementNote": "Die Erkennung des Gerätemodells und der Bericht über das Gerätemodell müssen deaktiviert werden.", + "DeviceModelReportDisabledByCompliancePolicy": "Der Bericht zum Gerätemodell ist durch die Compliance-Richtlinie deaktiviert.", + "DeviceModelReportDocumentation": "Dieser Bericht zeigt die Geräte, welche Ihre Besucher verwenden. Jedes Modell wird kombiniert mit der Gerätemarke angezeigt, weil einige Namen von mehreren Marken verwendet werden.", + "DeviceModels": "Gerätemodelle", + "DeviceType": "Gerätetyp", + "DeviceTypeReportDocumentation": "Dieser Bericht zeigt die Arten der Geräte, welche Ihre Besucher verwenden. Dieser Bericht wird immer alle Gerätetypen anzeigen, welche Matomo in der Lage ist, zu erkennen, auch wenn keine Besuche eines spezifischen Typs registriert wurden.", + "DeviceTypes": "Gerätetypen", + "Devices": "Geräte", + "DevicesDetection": "Geräte der Besucher", + "FeaturePhone": "Feature-Phone", + "FeedReader": "Feed Leser", + "GenericDevice": "Allgemeines %s", + "Library": "Bibliothek", + "MediaPlayer": "Medienplayer", + "MobileApp": "Mobil-App", + "MobileDevice": "Mobilgerät", + "OnlyMajorVersionsSettingTitle": "Nur Hauptversionen", + "OperatingSystemCode": "Betriebssystem-Code", + "OperatingSystemFamilies": "Betriebssystem-Familien", + "OperatingSystemFamiliesReportDocumentation": "Dieser Bericht zeigt Ihnen das Betriebssystem welches Ihre Besucher verwenden, gegliedert nach Betriebssystemfamilie. Eine Betriebssystemfamilie besteht aus verschiedenen Versionen oder Distributionen.", + "OperatingSystemFamily": "Betriebssystem-Familie", + "OperatingSystemVersions": "Betriebssystem-Versionen", + "OperatingSystemVersionsReportDocumentation": "Dieser Bericht zeigt Ihnen das Betriebssystem, welches Ihre Besucher verwenden. Jede Version und jede Distribution wird separat aufgeführt.", + "OperatingSystems": "Betriebssysteme", + "Peripheral": "Peripheriegeräte", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Unterstützt erweiterte Informationen über Benutzergeräte, wie Marke (Hersteller), Modell (Geräteversion), Gerätetyp (Fernseher, Konsolen, Smartphones, Desktops, usw.) und mehr.", + "PortableMediaPlayer": "Portable Mediengeräte", + "SmartDisplay": "Smart Display", + "SmartSpeaker": "Smarter Lautsprecher", + "Smartphone": "Smartphone", + "Software": "Software", + "TV": "TV", + "Tablet": "Tablet", + "UserAgent": "User-Agent", + "Wearable": "Tragbare Geräte", + "WidgetBrowserVersionsDocumentation": "Dieser Bericht enthält Informationen über die Browser Ihrer Besucher. Jede Version eines Browsers wird separat aufgelistet.", + "WidgetBrowsers": "Besucher-Browser", + "WidgetBrowsersDocumentation": "Dieser Bericht enthält Informationen darüber, welche Art von Browser Ihre Besucher verwenden.", + "XVisitsFromDevices": "%1$s Besuche von %2$s Geräten", + "dataTableLabelBrands": "Marke", + "dataTableLabelModels": "Modell", + "dataTableLabelSystemVersion": "Betriebssystem-Version", + "dataTableLabelTypes": "Typ" + } +} diff --git a/plugins/DevicesDetection/lang/dv.json b/plugins/DevicesDetection/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/el.json b/plugins/DevicesDetection/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..05447249ff6993e00f4dd78a0d1745faf8e5a656 --- /dev/null +++ b/plugins/DevicesDetection/lang/el.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Αυτό το πρόγραμμα-πελάτης εντοπίζεται ως %1$s. Οι επισκέψεις με αυτό το πρόγραμμα-πελάτη εξ' ορισμού δε θα ιχνηλατούνται από το Matomo.", + "BrowserCode": "Κωδικός φυλλομετρητή", + "BrowserEngine": "Μηχανή προγράμματος πλοήγησης", + "BrowserEngineDocumentation": "Η αναφορά αυτή δείχνει τα προγράμματα πλοήγησης των επισκεπτών σας ταξινομημένα με την εσωτερική τους μηχανή. %s Η πιο κρίσιμη πληροφορία για τους προγραμματιστές ιστοσελίδων είναι το είδος της μηχανής απεικόνισης που χρησιμοποιούν τα προγράμματα πλοήγησης των επισκεπτών τους. Οι ετικέτες περιέχουν τα ονόματα των μηχανών ακολουθούμενα σε αγκύλες από τα πιο συχνά χρησιμοποιούμενα προγράμματα πλοήγησης που χρησιμοποιούν αυτή τη μηχανή.", + "BrowserEngines": "Μηχανές προγράμματος πλοήγησης", + "BrowserFamily": "Οικογένεια προγραμμάτων πλοήγησης", + "BrowserVersion": "Έκδοση φυλλομετρητή", + "BrowserVersions": "Εκδόσεις προγραμμάτων περιήγησης", + "Browsers": "Προγράμματα πλοήγησης", + "Camera": "Κάμερα", + "CarBrowser": "Πρόγραμμα περιήγησης αυτοκινήτου", + "ClientHints": "Στοιχεία Πελάτη", + "ClientHintsNotSupported": "Το πρόγραμμα πλοήγησής σας δεν υποστηρίζει στοιχεία πελάτη.", + "ClientType": "Τύπος πελάτη", + "ClientTypes": "Τύποι πελατών", + "ColumnBrowser": "Φυλλομετρητής", + "ColumnOperatingSystem": "Λειτουργικό σύστημα", + "ColumnOperatingSystemVersion": "Έκδοση του λειτουργικού συστήματος", + "ConsiderClientHints": "Λάβετε υπόψη τα Στοιχεία Πελάτη", + "Console": "Κονσόλα", + "Device": "Συσκευή", + "DeviceBrand": "Μάρκα συσκευής", + "DeviceBrandReportDocumentation": "Αυτή η αναφορά δείχνει τις μάρκες / κατασκευαστές των συσκευών που οι επισκέπτες σας χρησιμοποιούν. Στις περισσότερες περιπτώσεις, η πληροφορία είναι διαθέσιμη μόνο για μη επιτραπέζιες συσκευές.", + "DeviceBrands": "Μάρκες συσκευής", + "DeviceDetection": "Εντοπισμός συσκευής", + "DeviceModel": "Μοντέλο συσκευής", + "DeviceModelDetectionDisabled": "Εντοπισμός μοντέλου συσκευής απενεργοποιημένος", + "DeviceModelDetectionDisabledRequirementNote": "Ο εντοπισμός μοντέλου συσκευής και οι αναφορές μοντέλων συσκευής πρέπει να είναι ανενεργές.", + "DeviceModelReportDisabledByCompliancePolicy": "Οι αναφορές μοντέλων συσκευής είναι απενεργοποιημένες για λόγους συμμόρφωσης.", + "DeviceModelReportDocumentation": "Αυτή η αναφορά δείχνει τις συσκευές που χρησιμοποιούν οι επισκέπτες σας. Κάθε μοντέλο συνδυάζεται με την μάρκα της συσκευής, καθώς ορισμένα ονόματα μοντέλων χρησιμοποιούνται από διαφορετικές μάρκες.", + "DeviceModels": "Μοντέλα συσκευής", + "DeviceType": "Τύπος συσκευής", + "DeviceTypeReportDocumentation": "Αυτή η αναφορά δείχνει τους τύπους συσκευών που χρησιμοποιούν οι επισκέπτες σας. Η αναφορά θα δείχνει όλους τους τύπους συσκευών που μπορεί το Matomo να εντοπίζει, ακόμη και αν δεν υπήρχαν επισκέψεις για ένα συγκεκριμένο τύπο.", + "DeviceTypes": "Τύποι συσκευής", + "Devices": "Συσκευές", + "DevicesDetection": "Συσκευές επισκεπτών", + "FeaturePhone": "Τηλέφωνο", + "FeedReader": "Αναγνώστης Ροών", + "GenericDevice": "Γενικό %s", + "Library": "Βιβλιοθήκη", + "MediaPlayer": "Συσκευή Αναπαραγωγής", + "MobileApp": "Εφαρμογή για κινητά", + "MobileDevice": "Συσκευή κινητής", + "OnlyMajorVersionsSettingRequirementNote": "Αποθηκεύονται μόνο κύριες εκδόσεις λειτουργικού συστήματος και φυλλομετρητή.", + "OnlyMajorVersionsSettingTitle": "Μόνο κύριες εκδόσεις", + "OperatingSystemCode": "Κωδικός λειτουργικού συστήματος", + "OperatingSystemFamilies": "Οικογένειες Λειτουργικών Συστημάτων", + "OperatingSystemFamiliesReportDocumentation": "Αυτή η αναφορά δείχνει τα λειτουργικά συστήματα που χρησιμοποιούν οι επισκέπτες σας, ομαδοποιημένα ανά οικογένεια λειτουργικών συστημάτων. Μια οικογένεια λειτουργικού συστήματος αποτελείται από διαφορετικές εκδόσεις ή διανομές.", + "OperatingSystemFamily": "Οικογένεια Λειτουργικού Συστήματος", + "OperatingSystemVersions": "Εκδόσεις Λειτουργικών Συστημάτων", + "OperatingSystemVersionsReportDocumentation": "Αυτή η αναφορά δείχνει τα λειτουργικά συστήματα που χρησιμοποιούν οι επισκέπτες σας. Κάθε έκδοση και κατανομή εμφανίζεται ξεχωριστά.", + "OperatingSystems": "Λειτουργικά συστήματα", + "Peripheral": "Περιφερειακά", + "Phablet": "Φάβλετ", + "Pim": "PIM", + "PluginDescription": "Παρέχει εκτεταμένες πληροφορίες σχετικά με τις συσκευές χρήστη, όπως Μάρκα (κατασκευαστής), Μοντέλο (έκδοση συσκευής), τύπος συσκευής (τηλεόραση, κονσόλες, έξυπνα τηλέφωνα, υπολογιστές, κτλ.) και περισσότερα.", + "PortableMediaPlayer": "Φορητή συσκευή αναπαραγωγής", + "SmartDisplay": "Έξυπνη οθόνη", + "SmartSpeaker": "Έξυπνος εκφωνητής", + "Smartphone": "Έξυπνο κινητό", + "Software": "Λογισμικό", + "TV": "Τηλεόραση", + "Tablet": "Ταμπλέτα", + "UserAgent": "Πρόγραμμα πελάτη χρήστη", + "Wearable": "Φορέσιμος", + "WidgetBrowserVersionsDocumentation": "Αυτή η αναφορά περιέχει πληροφορίες για το φυλλομετρητή που χρησιμοποιούσαν οι επισκέπτες. Κάθε έκδοση φυλλομετρητή κατατάσσεται ξεχωριστά.", + "WidgetBrowsers": "Φυλλομετρητές επισκεπτών", + "WidgetBrowsersDocumentation": "Αυτή η αναφορά περιέχει πληροφορίες για το είδος του φυλλομετρητή που χρησιμοποιούσαν οι επισκέπτες.", + "XVisitsFromDevices": "%1$s επισκέψεις από %2$s συσκευές", + "dataTableLabelBrands": "Μάρκα", + "dataTableLabelModels": "Μοντέλο", + "dataTableLabelSystemVersion": "Έκδοση λειτουργικού συστήματος", + "dataTableLabelTypes": "Τύπος" + } +} diff --git a/plugins/DevicesDetection/lang/en.json b/plugins/DevicesDetection/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..687f53ffb54c58cd1b0aa33011b219f3b4ea0f99 --- /dev/null +++ b/plugins/DevicesDetection/lang/en.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "This User-Agent is detected as %1$s. Visits with this user agent will not be tracked in Matomo by default.", + "BrowserEngine": "Browser engine", + "BrowserEngineDocumentation": "This report shows your visitors' browsers broken down into browser engines. %s The most important information for web developers is what kind of rendering engine their visitors are using. The labels contain the names of the engines followed by the most common browser using that engine in brackets.", + "BrowserEngines": "Browser engines", + "BrowserFamily": "Browser family", + "Browsers": "Browsers", + "BrowserVersion": "Browser version", + "BrowserVersions": "Browser versions", + "Camera": "Camera", + "CarBrowser": "Car browser", + "Software": "Software", + "ColumnBrowser": "Browser", + "BrowserCode": "Browser code", + "ColumnOperatingSystem": "Operating system", + "ColumnOperatingSystemVersion": "Operating system version", + "Console": "Console", + "dataTableLabelBrands": "Brand", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Operating System version", + "dataTableLabelTypes": "Type", + "ClientType": "Client type", + "ClientTypes": "Client types", + "ClientHints": "Client Hints", + "ClientHintsNotSupported": "Your Browser does not support client hints.", + "ConsiderClientHints": "Consider Client Hints", + "Device": "Device", + "DeviceBrand": "Device brand", + "DeviceBrands": "Device brands", + "DeviceBrandReportDocumentation": "This report shows the brands \/ manufacturers of the devices your visitors were using. In most cases this information is only available for non-desktop devices.", + "DeviceDetection": "Device detection", + "DeviceModel": "Device model", + "DeviceModels": "Device models", + "DeviceModelReportDocumentation": "This report shows the devices your visitors are using. Each model is displayed combined with the device brand as some model names are used by multiple brands.", + "DevicesDetection": "Visitor Devices", + "DeviceType": "Device type", + "DeviceTypes": "Device types", + "DeviceTypeReportDocumentation": "This report shows the types of devices your visitors were using. This report will always show all device types Matomo is able to detect, even if there were no visits with a specific type.", + "FeaturePhone": "Feature phone", + "OnlyMajorVersionsSettingTitle": "Only Major versions", + "OnlyMajorVersionsSettingRequirementNote": "Only Major OS and browser versions are stored.", + "OperatingSystemFamilies": "Operating System families", + "OperatingSystemFamily": "Operating system family", + "OperatingSystemFamiliesReportDocumentation": "This report shows you the operating systems your visitors are using grouped by operating system family. An operating system family consists of different versions or distributions.", + "OperatingSystemCode": "Operating system code", + "OperatingSystems": "Operating systems", + "OperatingSystemVersions": "Operating System versions", + "OperatingSystemVersionsReportDocumentation": "This report shows you the operating systems your visitors are using. Each version and distribution is shown separately.", + "PluginDescription": "Provides extended information about user devices, such as Brand (manufacturer), Model (device version), device type (tv, consoles, smart phones, desktop, etc) and more.", + "SmartDisplay": "Smart display", + "Smartphone": "Smartphone", + "SmartSpeaker": "Smart speaker", + "PortableMediaPlayer": "Portable media player", + "Devices": "Devices", + "GenericDevice": "Generic %s", + "MobileDevice": "Mobile device", + "Tablet": "Tablet", + "Peripheral": "Peripheral", + "Phablet": "Phablet", + "TV": "Tv", + "UserAgent": "User-Agent", + "Library": "Library", + "FeedReader": "Feed Reader", + "MobileApp": "Mobile App", + "Pim": "PIM", + "MediaPlayer": "Media Player", + "XVisitsFromDevices": "%1$s visits from %2$s devices", + "Wearable": "Wearable", + "WidgetBrowsers": "Visitor Browser", + "WidgetBrowsersDocumentation": "This report contains information about what kind of browser your visitors were using.", + "WidgetBrowserVersionsDocumentation": "This report contains information about what kind of browser your visitors were using. Each browser version is listed separately.", + "DeviceModelDetectionDisabled": "Device model detection disabled", + "DeviceModelDetectionDisabledRequirementNote": "Device model detection and device model report must be disabled.", + "DeviceModelReportDisabledByCompliancePolicy": "Device model report is disabled by compliance policy." + } +} diff --git a/plugins/DevicesDetection/lang/eo.json b/plugins/DevicesDetection/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/eo.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/es-ar.json b/plugins/DevicesDetection/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..360e3322a7b2ee396e2fd713a4383e1254aa5bee --- /dev/null +++ b/plugins/DevicesDetection/lang/es-ar.json @@ -0,0 +1,69 @@ +{ + "DevicesDetection": { + "BotDetected": "Este agente de usuario es detectado como %1$s. Las visitas con esta cadena de agente de usuario no serán rastreadas por Matomo predeterminadamente.", + "BrowserCode": "Código del navegador web", + "BrowserEngine": "Motor del navegador", + "BrowserEngineDocumentation": "Este informe muestra los navegadores web de tus visitantes divididos en sus respectivos motores. %s La información más importante para los desarrolladores de sitios web es saber qué motor de renderizado están usando sus visitantes. Las etiquetas contienen los nombres de los motores seguidos por el navegador web más usual utilizando dicho motor entre corchetes.", + "BrowserEngines": "Motores de navegador", + "BrowserFamily": "Familia del navegador", + "BrowserVersion": "Versión del navegador web", + "BrowserVersions": "Versiones del navegador", + "Browsers": "Navegadores web", + "Camera": "Cámara", + "CarBrowser": "Navegador web de auto", + "ClientType": "Tipo de cliente", + "ClientTypes": "Tipos de clientes", + "ColumnBrowser": "Navegador web", + "ColumnOperatingSystem": "Sistema operativo", + "ColumnOperatingSystemVersion": "Versión del sistema operativo", + "Console": "Consola", + "Device": "Dispositivo", + "DeviceBrand": "Marca del dispositivo", + "DeviceBrandReportDocumentation": "Este informe muestra las marcas/fabricantes de los dispositivos que tus visitantes están usando. En la mayoría de los casos, esta información sólo está disponible para dispositivos que no sean de escritorio.", + "DeviceBrands": "Marcas del dispositivo", + "DeviceDetection": "Detección del dispositivo", + "DeviceModel": "Modelo del dispositivo", + "DeviceModelReportDocumentation": "Este informe muestra los dispositivos que tus visitantes están usando. Cada modelo se muestra combinado con la marca de dispositivo, ya que algunos nombres de modelos son usandos por múltiples marcas.", + "DeviceModels": "Modelos del dispositivo", + "DeviceType": "Tipo de dispositivo", + "DeviceTypeReportDocumentation": "Este informe muestra los tipos de dispositivos que tus visitantes están usando. Este informe siempre mostrará todos los tipos de dispositivos que Matomo sea capaz de detectar, incluso si no hubo visitas con un tipo específico.", + "DeviceTypes": "Tipos de dispositivo", + "Devices": "Dispositivo", + "DevicesDetection": "Dispositivos del visitante", + "FeaturePhone": "Teléfono celular", + "FeedReader": "Lector de fuente web", + "GenericDevice": "%s genérico", + "Library": "Biblioteca", + "MediaPlayer": "Reproductor de medios", + "MobileApp": "Aplicación móvil", + "MobileDevice": "Dispositivo móvil", + "OperatingSystemCode": "Código del sistema operativo", + "OperatingSystemFamilies": "Familias de sistema operativo", + "OperatingSystemFamiliesReportDocumentation": "Este informe muestra los sistemas operativos que tus visitantes están usando, agrupados por familia de sistema operativo. Una familia de sistema operativo consiste en diferentes versiones o distribuciones.", + "OperatingSystemFamily": "Familia de sistema operativo", + "OperatingSystemVersions": "Versiones del sistema operativo", + "OperatingSystemVersionsReportDocumentation": "Este informe muestra los sistemas operativos que tus visitantes están usando. Cada versión y distribución se muestra separadamente.", + "OperatingSystems": "Sistemas operativos", + "Peripheral": "Periférico", + "Phablet": "Fableta", + "Pim": "PIM", + "PluginDescription": "Provee información extendida acerca de los dispositivos del usuario como \"Marca\" (fabricante), \"Modelo\" (versión del dispositivo), \"Tipo de dispositivo\" (televisor inteligente, consolas de videojuegos, teléfonos inteligentes, computadoras de escritorio, etc) y más.", + "PortableMediaPlayer": "Reproductor de medios portátil", + "SmartDisplay": "Televisor inteligente", + "SmartSpeaker": "Parlante inteligente", + "Smartphone": "Teléfono inteligente", + "Software": "Software", + "TV": "Televisor", + "Tablet": "Tableta", + "UserAgent": "Agente de usuario", + "Wearable": "Prendas electrónicas", + "WidgetBrowserVersionsDocumentation": "Este informe contiene información acerca de qué tipo de navegador web están usando tus visitantes. Cada versión de navegador web se muestra separadamente.", + "WidgetBrowsers": "Navegador web de los visitantes", + "WidgetBrowsersDocumentation": "Este informe contiene información de qué tipo de navegador tus visitantes están usando.", + "XVisitsFromDevices": "%1$s visitas desde %2$s dispositivos", + "dataTableLabelBrands": "Marca", + "dataTableLabelModels": "Modelo", + "dataTableLabelSystemVersion": "Versión del sistema operativo", + "dataTableLabelTypes": "Tipo" + } +} diff --git a/plugins/DevicesDetection/lang/es.json b/plugins/DevicesDetection/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..d8fd52095272d3e190c3cf8669ec96d0db43708b --- /dev/null +++ b/plugins/DevicesDetection/lang/es.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Este agente-de-usuario es detectado como %1$s. Los visitantes con este tipo de agente de usuario no será monitoreado de manera predeterminado por Matomo.", + "BrowserCode": "Navegador de código", + "BrowserEngine": "Motor del navegador", + "BrowserEngineDocumentation": "Este informe muestra los navegadores de sus visitantes desglosados en los motores de los navegadores. %s La información más importante para los desarrolladores de internet es que clase de motor de renderizado sus visitantes están utilizando. Las etiquetas contienen los nombres de los motores seguido por el navegador más común utilizando dicho motor entre paréntesis.", + "BrowserEngines": "Motores de navegador", + "BrowserFamily": "Familia de navegador", + "BrowserVersion": "Versión del navegador", + "BrowserVersions": "Versiones de navegadores", + "Browsers": "Navegadores", + "Camera": "Cámara", + "CarBrowser": "Navegador de coche", + "ClientHints": "Consejos para el cliente", + "ClientHintsNotSupported": "Su navegador no admite sugerencias de clientes.", + "ClientType": "Tipo de cliente", + "ClientTypes": "tipos de clientes", + "ColumnBrowser": "Navegador", + "ColumnOperatingSystem": "Sistemas operativos", + "ColumnOperatingSystemVersion": "Versión del sistema operativo", + "ConsiderClientHints": "Considere las sugerencias del cliente", + "Console": "Consola", + "Device": "Dispositivo", + "DeviceBrand": "Marca del dispositivo", + "DeviceBrandReportDocumentation": "Este informe muestra las marcas/fabricantes de los dispositivos que usaban sus visitantes. En la mayoría de los casos, esta información solo está disponible para dispositivos que no son de escritorio.", + "DeviceBrands": "Marcas de dispositivos", + "DeviceDetection": "Detección de dispositivos", + "DeviceModel": "Modelo del dispositivo", + "DeviceModelDetectionDisabled": "Detección de modelo del dispositivo deshabilitado", + "DeviceModelDetectionDisabledRequirementNote": "La detección del modelo del dispositivo y el informe del modelo del dispositivo deben estar inhabilitados.", + "DeviceModelReportDisabledByCompliancePolicy": "El informe del modelo de dispositivo está inhabilitado por la directiva de cumplimiento.", + "DeviceModelReportDocumentation": "Este informe muestra los dispositivos que utilizan sus visitantes. Cada modelo se muestra combinado con la marca del dispositivo, ya que varias marcas utilizan algunos nombres de modelos.", + "DeviceModels": "Modelos de dispositivos", + "DeviceType": "Tipo de dispositivo", + "DeviceTypeReportDocumentation": "Este informe muestra los tipos de dispositivos que usaban sus visitantes. Este informe siempre mostrará todos los tipos de dispositivos que Matomo puede detectar, incluso si no hubo visitas con un tipo específico.", + "DeviceTypes": "Tipos de dispositivos", + "Devices": "Dispositivos", + "DevicesDetection": "Dispositivos del visitante", + "FeaturePhone": "Característica del teléfono", + "FeedReader": "Lector de noticias", + "GenericDevice": "Genérico %s", + "Library": "Librería", + "MediaPlayer": "Reproductor multimedia", + "MobileApp": "Aplicación móvil", + "MobileDevice": "Dispositivos móvil", + "OnlyMajorVersionsSettingRequirementNote": "Sólo se almacenan las versiones mayores de los sistemas operativos y navegadores.", + "OnlyMajorVersionsSettingTitle": "Sólo versiones mayores", + "OperatingSystemCode": "Código sistema operativo", + "OperatingSystemFamilies": "Familias de sistemas operativos", + "OperatingSystemFamiliesReportDocumentation": "Este informe le muestra los sistemas operativos que utilizan sus visitantes agrupados por familia de sistemas operativos. Una familia de sistemas operativos consta de diferentes versiones o distribuciones.", + "OperatingSystemFamily": "Familia de sistema operativo", + "OperatingSystemVersions": "Versiones de sistema operativo", + "OperatingSystemVersionsReportDocumentation": "Este informe le muestra los sistemas operativos que utilizan sus visitantes. Cada versión y distribución se muestra por separado.", + "OperatingSystems": "Sistemas operativos", + "Peripheral": "Periférico", + "Phablet": "Tabléfono", + "Pim": "Gestión de la información del producto (PIM)", + "PluginDescription": "Provee información extendida acerca de los dispositivos de los usuarios, tales como Marca (Fabricante), Modelo (versión del dispositivo), tipo de dispositivo (tv, consolas, teléfonos inteligentes, de escritorio, etc.) y más.", + "PortableMediaPlayer": "Reproductor de medios portátil", + "SmartDisplay": "Pantalla inteligente", + "SmartSpeaker": "Bocina inteligente", + "Smartphone": "Teléfono inteligente", + "Software": "Software", + "TV": "Televisión", + "Tablet": "Tableta", + "UserAgent": "Agente de usuario", + "Wearable": "Utilizable", + "WidgetBrowserVersionsDocumentation": "Este informe contiene información acerca de qué tipo de navegador el visitante está utilizando. Cada versión de navegador está alistado separadamente.", + "WidgetBrowsers": "Navegador del visitante", + "WidgetBrowsersDocumentation": "Este informe contiene información sobre qué tipo de navegador estaban usando sus visitantes.", + "XVisitsFromDevices": "%1$s visitas desde %2$s dispositivos", + "dataTableLabelBrands": "Marca", + "dataTableLabelModels": "Modelo", + "dataTableLabelSystemVersion": "Versión del sistema operativo", + "dataTableLabelTypes": "Tipo" + } +} diff --git a/plugins/DevicesDetection/lang/et.json b/plugins/DevicesDetection/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..8bc965763b1fc5488b36b440cabd9e2b01ec50f7 --- /dev/null +++ b/plugins/DevicesDetection/lang/et.json @@ -0,0 +1,40 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Veebisirvija", + "BrowserEngines": "Veebimootorid", + "BrowserFamily": "Sirvija tüüp", + "BrowserVersion": "Sirvija versioon", + "BrowserVersions": "Brauseri versioonid", + "Browsers": "Veebisirvijad", + "Camera": "Kaamera", + "CarBrowser": "Sõiduki sirvik", + "ColumnBrowser": "Veebisirvija", + "ColumnOperatingSystem": "Operatsioonisüsteem", + "ColumnOperatingSystemVersion": "Operatsioonisüsteemi versioon", + "Console": "Konsool", + "Device": "Seade", + "DeviceBrand": "Seadme bränd", + "DeviceDetection": "Seadme tuvastus", + "DeviceModel": "Seadme mudel", + "DeviceType": "Seadme tüüp", + "Devices": "Seadmed", + "DevicesDetection": "Külastajate seadmed", + "FeaturePhone": "Tark telefon", + "MobileApp": "Mobiilirakendus", + "OperatingSystemFamilies": "Operatsioonisüsteemide tüübid", + "OperatingSystemFamily": "OP-süsteemi tüüp", + "OperatingSystemVersions": "Operatsioonisüsteemide versioonid", + "OperatingSystems": "Operatsioonisüsteemid", + "SmartDisplay": "Nutikas kuvar", + "Smartphone": "Nutitelefon", + "Software": "Tarkvara", + "TV": "Televiisor", + "Tablet": "Tahvelarvuti", + "UserAgent": "Veebisirvija tüüp", + "WidgetBrowsers": "Külastajate veebisirvijad", + "dataTableLabelBrands": "Bränd", + "dataTableLabelModels": "Mudel", + "dataTableLabelSystemVersion": "Operatsioonisüsteemi versioon", + "dataTableLabelTypes": "Tüüp" + } +} diff --git a/plugins/DevicesDetection/lang/eu.json b/plugins/DevicesDetection/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..079610f19fde1695ddd450f2c4d2bb4e9c767cca --- /dev/null +++ b/plugins/DevicesDetection/lang/eu.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Erabiltzaile-agente hau %1$s bezala detektatu da. Erabiltzaile-agente hau duten bisiten jarraipenik ez du egiten Matomok modu lehenetsian.", + "BrowserCode": "Nabigatzaile-kodea", + "BrowserEngine": "Nabigatzaile-motorra", + "BrowserEngineDocumentation": "Txosten honek bisitarien nabigatzaileak erakusten ditu, nabigatzaile-motorraren arabera sailkatuta. %s Web garatzaileentzako informaziorik garrantzitsuena bisitariek erabiltzen duten errendatze-motorra da. Etiketek motorraren izena daukate eta ondoren, parentesi artean, motor hori erabiltzen duen nabigatzaile ohikoena.", + "BrowserEngines": "Nabigatzaile-motorrak", + "BrowserFamily": "Nabigatzaile-familia", + "BrowserVersion": "Nabigatzailearen bertsioa", + "BrowserVersions": "Nabigatzaileen bertsioak", + "Browsers": "Nabigatzaileak", + "Camera": "Kamera", + "CarBrowser": "Autoko nabigatzailea", + "ClientHints": "Bezeroaren aztarnak", + "ClientHintsNotSupported": "Zure nabigatzaileak ez ditu onartzen bezeroaren aztarnak.", + "ClientType": "Bezero-mota", + "ClientTypes": "Bezero-motak", + "ColumnBrowser": "Nabigatzailea", + "ColumnOperatingSystem": "Sistema eragilea", + "ColumnOperatingSystemVersion": "Sistema eragilearen bertsioa", + "ConsiderClientHints": "Kontuan izan bezeroaren aztarnak", + "Console": "Kontsola", + "Device": "Gailua", + "DeviceBrand": "Gailuaren marka", + "DeviceBrandReportDocumentation": "Txosten honek bisitariek erabiltzen dituzten gailuen marka / fabrikatzaileak erakusten ditu. Kasu gehienetan informazio hau soilik mahaigainekoak ez diren gailuetan dago eskuragarri.", + "DeviceBrands": "Gailuen markak", + "DeviceDetection": "Gailuen detekzioa", + "DeviceModel": "Gailuaren modeloa", + "DeviceModelReportDocumentation": "Txosten honek bisitariek erabiltzen dituzten gailuak erakusten ditu. Modelo bakoitza gailuaren markarekin batera bistaratzen da, modelo-izen batzuk hainbat markak erabiltzen dituztelako.", + "DeviceModels": "Gailuen modeloak", + "DeviceType": "Gailu-mota", + "DeviceTypeReportDocumentation": "Txosten honek bisitariek erabiltzen dituzten gailu-motak erakusten ditu. Txosten honek beti erakutsiko ditu Matomok detektatu ditzakeen gailu-mota guztiak, mota zehatz horretako bisitarik ez badago ere.", + "DeviceTypes": "Gailu-motak", + "Devices": "Gailuak", + "DevicesDetection": "Bisitarien gailuak", + "FeaturePhone": "Oinarrizko telefonoa", + "FeedReader": "Jario irakurlea", + "GenericDevice": "%s generikoa", + "Library": "Liburutegia", + "MediaPlayer": "Multimedia erreproduzigailua", + "MobileApp": "Mugikorreko aplikazioa", + "MobileDevice": "Gailu mugikorra", + "OperatingSystemCode": "Sistema eragilearen kodea", + "OperatingSystemFamilies": "Sistema eragileen familiak", + "OperatingSystemFamiliesReportDocumentation": "Txosten honek erakusten du zure bisitariek zein sistema eragile erabiltzen duten, sistema eragileen familiaka taldekatuta. Sistema eragileen familia batean hainbat bertsio eta banaketa daude.", + "OperatingSystemFamily": "Sistema eragileen familia", + "OperatingSystemVersions": "Sistema eragileen bertsioak", + "OperatingSystemVersionsReportDocumentation": "Txosten honek erakusten du zure bisitariek zein sistema eragile erabiltzen duten. Bertsio eta banaketa bakoitza bereizita erakusten da.", + "OperatingSystems": "Sistema eragileak", + "Peripheral": "Periferikoa", + "Phablet": "Phableta", + "Pim": "PIM", + "PluginDescription": "Erabiltzaileen gailuei buruzko informazio gehigarria eskaintzen du: marka (fabrikatzailea), modeloa (gailuaren bertsioa), gailu-mota (telebista, kontsolak, telefono adimendunak, mahaigainekoak eta abar) eta gehiago.", + "PortableMediaPlayer": "Multimedia erreproduzigailu eramangarria", + "SmartDisplay": "Pantaila adimendunak", + "SmartSpeaker": "Bozgorailu adimenduna", + "Smartphone": "Telefono adimenduna", + "Software": "Softwarea", + "TV": "TB", + "Tablet": "Tableta", + "UserAgent": "Erabiltzaile-agentea", + "Wearable": "Janztekoa", + "WidgetBrowserVersionsDocumentation": "Txosten honetan zure bisitariek erabiltzen duten nabigatzaile motaren inguruko informazioa dago. Nabigatzaileen bertsioak bereizita zerrendatzen dira.", + "WidgetBrowsers": "Bisitarien nabigatzaileak", + "WidgetBrowsersDocumentation": "Txosten honetan zure bisitariek erabiltzen duten nabigatzaile motaren inguruko informazioa dago.", + "XVisitsFromDevices": "%1$s bisita %2$s gailutatik", + "dataTableLabelBrands": "Marka", + "dataTableLabelModels": "Modeloa", + "dataTableLabelSystemVersion": "Sistema eragilearen bertsioa", + "dataTableLabelTypes": "Mota" + } +} diff --git a/plugins/DevicesDetection/lang/fa.json b/plugins/DevicesDetection/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..03b15bc9b93c0e666cb35c4f46286d0bddd297ba --- /dev/null +++ b/plugins/DevicesDetection/lang/fa.json @@ -0,0 +1,37 @@ +{ + "DevicesDetection": { + "BrowserEngine": "مرورگر", + "BrowserFamily": "خانواده مرورگر", + "BrowserVersion": "نسخه مرورگر", + "BrowserVersions": "نسخه مرورگر", + "Browsers": "مرورگر ها", + "Camera": "دوربین", + "CarBrowser": "مرورگر خودرو", + "ColumnBrowser": "مرورگر", + "ColumnOperatingSystem": "سیستم عامل", + "Console": "کنسول", + "Device": "وسیله", + "DeviceBrand": "مارک وسیله", + "DeviceDetection": "تشخیص وسیله", + "DeviceModel": "مدل وسیله", + "DeviceType": "نوع وسیله", + "Devices": "وسایل", + "DevicesDetection": "وسایل بازدیده کنندگان", + "FeaturePhone": "گوشی", + "OperatingSystemFamilies": "خانواده سیستم عامل", + "OperatingSystemFamily": "خانواده سیستم عامل", + "OperatingSystemVersions": "نسخه سیستم عامل", + "OperatingSystems": "سیستم عامل", + "SmartDisplay": "صفحه نمایش هوشمند", + "Smartphone": "تلفن هوشمند", + "TV": "تلویزیون", + "Tablet": "تبلت", + "UserAgent": "کاربر-نمایندگی", + "WidgetBrowserVersionsDocumentation": "این گزارش حاوی اطلاعات در مورد چه نوع مرورگر بازدید کنندگان خود را با استفاده از. هر مرورگر بطور جداگانه ذکر شده است.", + "WidgetBrowsers": "مرورگر بازدید کننده", + "dataTableLabelBrands": "مارک", + "dataTableLabelModels": "مدل", + "dataTableLabelSystemVersion": "نسخه سیستم عامل", + "dataTableLabelTypes": "نوع" + } +} diff --git a/plugins/DevicesDetection/lang/fi.json b/plugins/DevicesDetection/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..e9b02c764089ad3c1e803b28c06e63bfe16b919f --- /dev/null +++ b/plugins/DevicesDetection/lang/fi.json @@ -0,0 +1,60 @@ +{ + "DevicesDetection": { + "BotDetected": "Tämä kävijäagentti havaittiin olevan %1$s. Tällä käyttäjäagentilla tehtäviä käyntejä ei seurata oletuksena Matomossa.", + "BrowserCode": "Selainkoodi", + "BrowserEngine": "Selainmoottori", + "BrowserEngineDocumentation": "Tämä raportti näyttää käyttäjien selaimet jaoteltuna selainmoottorien mukaan. %s Tärkein tieto verkkosivujen kehittäjille on käyttäjien selaimen taustamoottori. Tässä esitetään taustamoottorin nimi ja nimen perässä suluissa moottoria yleisimmin hyödyntävät selaimet.", + "BrowserEngines": "Selainmoottorit", + "BrowserFamily": "Selainperhe", + "BrowserVersion": "Selaimen versio", + "BrowserVersions": "Selainversiot", + "Browsers": "Selaimet", + "Camera": "Kamera", + "CarBrowser": "Auton selain", + "ColumnBrowser": "Selain", + "ColumnOperatingSystem": "Käyttöjärjestelmä", + "ColumnOperatingSystemVersion": "Käyttöjärjestelmän versio", + "Console": "Konsoli", + "Device": "Laite", + "DeviceBrand": "Laitteen merkki", + "DeviceBrands": "Laitemerkit", + "DeviceDetection": "Laitteen jäljitys", + "DeviceModel": "Laitteen malli", + "DeviceModels": "Laitemallit", + "DeviceType": "Laitteen tyyppi", + "DeviceTypes": "Laitetyypit", + "Devices": "Laitteet", + "DevicesDetection": "Kävijän laitteet", + "FeaturePhone": "Peruspuhelin", + "FeedReader": "Syötteenlukija", + "GenericDevice": "Ylenen %s", + "Library": "Kirjasto", + "MediaPlayer": "Mediasoitin", + "MobileApp": "Mobiilisovellus", + "MobileDevice": "Mobiililaite", + "OperatingSystemCode": "Käyttöjärjestelmäkoodi", + "OperatingSystemFamilies": "Käyttöjärjestelmäperheet", + "OperatingSystemFamily": "Käyttöjärjestelmä", + "OperatingSystemVersions": "Käyttöjärjestelmäversiot", + "OperatingSystems": "Käyttöjärjestelmä", + "Phablet": "Phabletti", + "PluginDescription": "Tarjoaa laajempaa tietoa käyttäjän laitteesta, kuten merkin (valmistajan), mallin (laiteversion), laitetyypin (tv, pelikonsolit, älypuhelimet, työpöytä jne.) sekä paljon muuta.", + "PortableMediaPlayer": "Kannettava mediasoitin", + "SmartDisplay": "Älynäyttö", + "SmartSpeaker": "Älykaiutin", + "Smartphone": "Älypuhelin", + "Software": "Ohjelmisto", + "TV": "TV", + "Tablet": "Tabletti", + "UserAgent": "Käyttäjäagentti", + "Wearable": "Puettava", + "WidgetBrowserVersionsDocumentation": "Tämä raportti näyttää, mitä selaimia kävijöilläsi oli. Jokainen selaimen versio näytetään erikseen.", + "WidgetBrowsers": "Kävijöiden selaimet", + "WidgetBrowsersDocumentation": "Tämä raportti sisältää tietoa kävijöiden käyttämistä selaimista.", + "XVisitsFromDevices": "Käyntien määrä: %1$s, laitteelta tyyppiä %2$s", + "dataTableLabelBrands": "Merkki", + "dataTableLabelModels": "Malli", + "dataTableLabelSystemVersion": "Käyttöjärjestelmäversio", + "dataTableLabelTypes": "Tyyppi" + } +} diff --git a/plugins/DevicesDetection/lang/fr.json b/plugins/DevicesDetection/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..33d568ca5f934a2e07500ea87e02d8dbf03bfef2 --- /dev/null +++ b/plugins/DevicesDetection/lang/fr.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Ce User-Agent est détecté comme %1$s. Les visites avec ce user agent ne seront pas traquées par défaut dans Matomo.", + "BrowserCode": "Code navigateur", + "BrowserEngine": "Moteur de rendu HTML", + "BrowserEngineDocumentation": "Ce rapport affiche les navigateurs de vos visiteurs divisés par moteurs. %s L'information la plus importante pour des développeurs web est quelle sorte de moteur de rendu utilisent les visiteurs. Les labels contiennent le nom du moteur suivi par le navigateur le plus commun utilisant ce moteur entre parenthèses.", + "BrowserEngines": "Moteurs de rendu HTML", + "BrowserFamily": "Famille de navigateurs", + "BrowserVersion": "Version du navigateur", + "BrowserVersions": "Versions du navigateur", + "Browsers": "Navigateurs", + "Camera": "Appareil photo", + "CarBrowser": "Navigateur de voiture", + "ClientHints": "Module astuces", + "ClientHintsNotSupported": "Votre navigateur ne fournit pas automatiquement certaines informations sur votre appareil, ce qui peut limiter l’affichage ou l’adaptation du contenu.", + "ClientType": "Type de client", + "ClientTypes": "Types de client", + "ColumnBrowser": "Navigateur", + "ColumnOperatingSystem": "Système d'exploitation", + "ColumnOperatingSystemVersion": "Version du système d'exploitation", + "ConsiderClientHints": "Considérer les Conseils du client", + "Console": "Console", + "Device": "Périphérique", + "DeviceBrand": "Marque du périphérique", + "DeviceBrandReportDocumentation": "Ce rapport indique les marques / fabricants des périphériques que vos visiteurs ont utilisés. Dans la plupart des cas, ces informations ne sont disponibles que pour les appareils autres que les ordinateurs de bureau.", + "DeviceBrands": "Marques de périphérique", + "DeviceDetection": "Détection du périphérique", + "DeviceModel": "Modèle du périphérique", + "DeviceModelDetectionDisabled": "Détection du modèle d'appareil désactivée", + "DeviceModelDetectionDisabledRequirementNote": "La détection et le rapport du modèle de périphérique doivent être désactivés.", + "DeviceModelReportDisabledByCompliancePolicy": "Le rapport sur le modèle de l'appareil est désactivé par la politique de conformité.", + "DeviceModelReportDocumentation": "Ce rapport indique les périphériques que vos visiteurs utilisent. Chaque modèle est affiché en associant la marque de l'appareil car certains noms de modèles sont utilisés par plusieurs marques.", + "DeviceModels": "Modèles de périphérique", + "DeviceType": "Type de périphérique", + "DeviceTypeReportDocumentation": "Ce rapport indique les types de périphériques que vos visiteurs ont utilisés. Ce rapport indiquera toujours tous les types de périphériques que Matomo est en mesure de détecter, même si aucun visiteur n'a utilisé un type particulier.", + "DeviceTypes": "Types de périphérique", + "Devices": "Périphériques", + "DevicesDetection": "Périphériques du visiteur", + "FeaturePhone": "Téléphone mobile basique", + "FeedReader": "Lecteur de flux", + "GenericDevice": "Générique %s", + "Library": "Bibliothèque", + "MediaPlayer": "Lecteur multimédia", + "MobileApp": "Application mobile", + "MobileDevice": "Périphérique mobile", + "OnlyMajorVersionsSettingRequirementNote": "Seules les versions majeures du système d'exploitation et du navigateur sont enregistrées.", + "OnlyMajorVersionsSettingTitle": "Uniquement les versions majeures", + "OperatingSystemCode": "Code système d'exploitation", + "OperatingSystemFamilies": "Familles de systèmes d'exploitations", + "OperatingSystemFamiliesReportDocumentation": "Ce rapport vous montre les systèmes d'exploitation que vos visiteurs utilisent, regroupés par famille de systèmes d'exploitation. Une famille de systèmes d'exploitation se compose de différentes versions ou distributions.", + "OperatingSystemFamily": "Famille de systèmes d'exploitations", + "OperatingSystemVersions": "Versions de système d'exploitation", + "OperatingSystemVersionsReportDocumentation": "Ce rapport vous montre les systèmes d'exploitation que vos visiteurs utilisent. Chaque version et distribution est présentée séparément.", + "OperatingSystems": "Système d'exploitation", + "Peripheral": "Périphérique", + "Phablet": "Phablette", + "Pim": "PIM", + "PluginDescription": "Fournit des informations détaillés à propos des périphériques de l'utilisateur telles que la Marque (fabriquant), le Modèle (version pérhip.), type de périph. (tv, consoles, téléphone intelligent, ordinateur, etc) et plus.", + "PortableMediaPlayer": "Baladeur multimédia", + "SmartDisplay": "Écran connecté", + "SmartSpeaker": "Haut-parleur intelligent", + "Smartphone": "Téléphone intelligent (smartphone)", + "Software": "Logiciel", + "TV": "Téléviseur", + "Tablet": "Tablette", + "UserAgent": "Agent Utilisateur (User-Agent)", + "Wearable": "Technologie portable", + "WidgetBrowserVersionsDocumentation": "Ce rapport contient des informations à propos de quel type de navigateur vos visiteurs utilisent. Chaque version du navigateur est listée séparément.", + "WidgetBrowsers": "Navigateurs du visiteur", + "WidgetBrowsersDocumentation": "Ce rapport contient des informations à propos des types de navigateurs que vos visiteurs utilisent.", + "XVisitsFromDevices": "%1$s visites de %2$s périphériques", + "dataTableLabelBrands": "Marque", + "dataTableLabelModels": "Modèle", + "dataTableLabelSystemVersion": "Version du système d'exploitation", + "dataTableLabelTypes": "Type" + } +} diff --git a/plugins/DevicesDetection/lang/ga.json b/plugins/DevicesDetection/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..ee5f54265f07655935d7ba39dd0a7d597a8306b2 --- /dev/null +++ b/plugins/DevicesDetection/lang/ga.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Braitear an Gníomhaire Úsáideora seo mar %1$s. Ní dhéanfar cuairteanna leis an ngníomhaire úsáideora seo a rianú in Matomo de réir réamhshocraithe.", + "BrowserCode": "Cóid brabhsálaithe", + "BrowserEngine": "Inneall brabhsálaí", + "BrowserEngineDocumentation": "Taispeánann an tuarascáil seo brabhsálaithe do chuairteoirí arna miondealú ina n-innill bhrabhsálaí. %s Is é an t-eolas is tábhachtaí d'fhorbróirí gréasáin ná an cineál inneall rindreála atá in úsáid ag a gcuairteoirí. Tá ainmneacha na n-inneall ar na lipéid agus an brabhsálaí is coitianta a úsáideann an t-inneall sin idir lúibíní ina dhiaidh sin.", + "BrowserEngines": "Innill brabhsálaithe", + "BrowserFamily": "Teaghlaigh brabhsálaithe", + "BrowserVersion": "Leagan brabhsálaithe", + "BrowserVersions": "Leaganacha brabhsálaí", + "Browsers": "Brabhsálaithe", + "Camera": "Leaganacha", + "CarBrowser": "Brabhsálaí gluaisteáin", + "ClientHints": "Leideanna Cliant", + "ClientHintsNotSupported": "Ní thacaíonn do Bhrabhsálaí le leideanna cliant.", + "ClientType": "Cineál cliant", + "ClientTypes": "Cineálacha cliant", + "ColumnBrowser": "Brabhsálaí", + "ColumnOperatingSystem": "Córas oibriúcháin", + "ColumnOperatingSystemVersion": "Leagan an chórais oibriúcháin", + "ConsiderClientHints": "Smaoinigh ar Leideanna Cliant", + "Console": "Consól", + "Device": "Gléas", + "DeviceBrand": "Branda gléas", + "DeviceBrandReportDocumentation": "Léiríonn an tuarascáil seo brandaí / déantúsóirí na bhfeistí a bhí in úsáid ag do chuairteoirí. I bhformhór na gcásanna níl an fhaisnéis seo ar fáil ach amháin le haghaidh gléasanna nach feistí deisce iad.", + "DeviceBrands": "Brandaí gléas", + "DeviceDetection": "Brath gléas", + "DeviceModel": "Múnla gléas", + "DeviceModelDetectionDisabled": "Brath samhail gléis díchumasaithe", + "DeviceModelDetectionDisabledRequirementNote": "Ní mór braiteadh samhail gléis agus tuarascáil samhail gléis a dhíchumasú.", + "DeviceModelReportDisabledByCompliancePolicy": "Tá tuarascáil mhúnla gléis díchumasaithe ag an mbeartas comhlíontachta.", + "DeviceModelReportDocumentation": "Léiríonn an tuarascáil seo na gléasanna atá in úsáid ag do chuairteoirí. Taispeántar gach samhail in éineacht le branda an fheiste mar go n-úsáideann brandaí iolracha roinnt ainmneacha samhlacha.", + "DeviceModels": "Samhlacha gléas", + "DeviceType": "Cineál gléas", + "DeviceTypeReportDocumentation": "Léiríonn an tuarascáil seo na cineálacha gléasanna a bhí in úsáid ag do chuairteoirí. Léireoidh an tuarascáil seo i gcónaí gach cineál feiste atá Matomo in ann a bhrath, fiú mura raibh aon chuairteanna le cineál sonrach.", + "DeviceTypes": "Cineálacha gléas", + "Devices": "Gléasanna", + "DevicesDetection": "Gléasanna do Chuairteoirí", + "FeaturePhone": "Fón gné", + "FeedReader": "Léitheoir Fothaí", + "GenericDevice": "Cineálach %s", + "Library": "Leabharlann", + "MediaPlayer": "Seinnteoir meáin", + "MobileApp": "Aip Soghluaiste", + "MobileDevice": "Gléas soghluaiste", + "OnlyMajorVersionsSettingRequirementNote": "Ní stóráiltear ach leaganacha móra de na córais oibriúcháin agus na brabhsálaithe.", + "OnlyMajorVersionsSettingTitle": "Leaganacha móra amháin", + "OperatingSystemCode": "Cóid córas oibriúcháin", + "OperatingSystemFamilies": "Teaghlaigh an Chórais Oibriúcháin", + "OperatingSystemFamiliesReportDocumentation": "Taispeánann an tuarascáil seo duit na córais oibriúcháin atá á n-úsáid ag do chuairteoirí agus iad grúpáilte de réir teaghlach an chórais oibriúcháin. Is éard atá i dteaghlach córais oibriúcháin ná leaganacha nó dáiltí éagsúla.", + "OperatingSystemFamily": "Córas oibriúcháin teaghlaigh", + "OperatingSystemVersions": "Leaganacha an Chórais Oibriúcháin", + "OperatingSystemVersionsReportDocumentation": "Taispeánann an tuarascáil seo duit na córais oibriúcháin atá in úsáid ag do chuairteoirí. Taispeántar gach leagan agus dáileadh ar leithligh.", + "OperatingSystems": "Córais oibriúcháin", + "Peripheral": "Imeallach", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Soláthraíonn sé faisnéis leathnaithe faoi fheistí úsáideoirí, mar shampla Brand (monaróir), Múnla (leagan feiste), cineál feiste (teilifís, consóil, fóin chliste, deasc, srl) agus níos mó.", + "PortableMediaPlayer": "Seinnteoir meáin soghluaiste", + "SmartDisplay": "Taispeáint cliste", + "SmartSpeaker": "Cainteoir cliste", + "Smartphone": "Fón cliste", + "Software": "Bogearraí", + "TV": "Teilifís", + "Tablet": "Táibléad", + "UserAgent": "Úsáideoir-Gníomhaire", + "Wearable": "Inchaite", + "WidgetBrowserVersionsDocumentation": "Tá faisnéis sa tuarascáil seo faoin gcineál brabhsálaí a bhí in úsáid ag do chuairteoirí. Tá gach leagan brabhsálaí liostaithe ar leithligh.", + "WidgetBrowsers": "Brabhsálaí Cuairteoirí", + "WidgetBrowsersDocumentation": "Tá faisnéis sa tuarascáil seo faoin gcineál brabhsálaí a bhí in úsáid ag do chuairteoirí.", + "XVisitsFromDevices": "%1$s cuairt ó ghléas %2$s", + "dataTableLabelBrands": "Branda", + "dataTableLabelModels": "Múnla", + "dataTableLabelSystemVersion": "Leagan an Chórais Oibriúcháin", + "dataTableLabelTypes": "Cineál" + } +} diff --git a/plugins/DevicesDetection/lang/gl.json b/plugins/DevicesDetection/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..3e9106f597def4736ed9d0d41361becffc0823e8 --- /dev/null +++ b/plugins/DevicesDetection/lang/gl.json @@ -0,0 +1,7 @@ +{ + "DevicesDetection": { + "Browsers": "Navegadores", + "OperatingSystems": "Sistemas Operativos", + "WidgetBrowsers": "Navegadores dos visitantes" + } +} diff --git a/plugins/DevicesDetection/lang/gu.json b/plugins/DevicesDetection/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..5bc2a27de3e66c1293a99d6b72ad3743cc7e0db0 --- /dev/null +++ b/plugins/DevicesDetection/lang/gu.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "આ વપરાશકર્તા-એજન્ટ %1$s તરીકે શોધાયેલ છે. આ વપરાશકર્તા એજન્ટ સાથેની મુલાકાતો ડિફૉલ્ટ રૂપે Matomo માં ટ્રૅક કરવામાં આવશે નહીં.", + "BrowserCode": "બ્રાઉઝર કોડ", + "BrowserEngine": "બ્રાઉઝર એન્જિન", + "BrowserEngineDocumentation": "આ રિપોર્ટ તમારા મુલાકાતીઓના બ્રાઉઝર્સને બ્રાઉઝર એન્જિનમાં વિભાજીત બતાવે છે. %s વેબ ડેવલપર્સ માટે સૌથી મહત્વપૂર્ણ માહિતી એ છે કે તેમના મુલાકાતીઓ કેવા પ્રકારનું રેન્ડરિંગ એન્જિન વાપરી રહ્યા છે. લેબલ્સમાં એન્જિનના નામ હોય છે જે પછી સૌથી સામાન્ય બ્રાઉઝર કૌંસમાં તે એન્જિનનો ઉપયોગ કરે છે.", + "BrowserEngines": "બ્રાઉઝર એન્જિન", + "BrowserFamily": "બ્રાઉઝર પરિવાર", + "BrowserVersion": "બ્રાઉઝર સંસ્કરણ", + "BrowserVersions": "બ્રાઉઝર આવૃત્તિઓ", + "Browsers": "બ્રાઉઝર્સ", + "Camera": "કેમેરા", + "CarBrowser": "કાર બ્રાઉઝર", + "ClientHints": "ક્લાયન્ટ સંકેતો", + "ClientHintsNotSupported": "તમારું બ્રાઉઝર ક્લાયંટના સંકેતોને સપોર્ટ કરતું નથી.", + "ClientType": "ક્લાયન્ટ પ્રકાર", + "ClientTypes": "ક્લાયંટ પ્રકારો", + "ColumnBrowser": "બ્રાઉઝર", + "ColumnOperatingSystem": "ઓપરેટિંગ સિસ્ટમ", + "ColumnOperatingSystemVersion": "ઓપરેટિંગ સિસ્ટમ સંસ્કરણ", + "ConsiderClientHints": "ક્લાયન્ટના સંકેતો ધ્યાનમાં લો", + "Console": "કન્સોલ", + "Device": "ડિવાઇસ", + "DeviceBrand": "ઉપકરણ બ્રાન્ડ", + "DeviceBrandReportDocumentation": "આ રિપોર્ટ તમારા મુલાકાતીઓ ઉપયોગ કરતા હતા તે ઉપકરણોના બ્રાન્ડ્સ/ઉત્પાદકો બતાવે છે. મોટાભાગના કિસ્સાઓમાં આ માહિતી ફક્ત નોન-ડેસ્કટોપ ઉપકરણો માટે જ ઉપલબ્ધ છે.", + "DeviceBrands": "ઉપકરણ બ્રાન્ડ્સ", + "DeviceDetection": "ડિવાઇસ શોધ", + "DeviceModel": "ડિવાઇસ મોડેલ", + "DeviceModelReportDocumentation": "આ રિપોર્ટ તમારા મુલાકાતીઓ ઉપયોગ કરી રહ્યાં છે તે ઉપકરણો બતાવે છે. દરેક મૉડલને ડિવાઇસની બ્રાન્ડ સાથે જોડીને પ્રદર્શિત કરવામાં આવે છે કારણ કે કેટલાક મૉડલના નામ બહુવિધ બ્રાન્ડ્સ દ્વારા ઉપયોગમાં લેવાય છે.", + "DeviceModels": "ડિવાઇસ મૉડલ્સ", + "DeviceType": "ડિવાઇસ પ્રકાર", + "DeviceTypeReportDocumentation": "આ રિપોર્ટ તમારા મુલાકાતીઓ ઉપયોગ કરતા હતા તે ડિવાઇસના પ્રકારો બતાવે છે. આ રિપોર્ટ હંમેશા બતાવશે કે Matomo તમામ પ્રકારના ડિવાઇસને શોધી શકે છે, પછી ભલે ત્યાં કોઈ ચોક્કસ પ્રકાર સાથે કોઈ મુલાકાત ન હોય.", + "DeviceTypes": "ડિવાઇસ પ્રકારો", + "Devices": "ડિવાઇસીસ", + "DevicesDetection": "વિઝિટર ડિવાઇસીસ", + "FeaturePhone": "ફીચર ફોન", + "FeedReader": "ફીડ રીડર", + "GenericDevice": "સામાન્ય %s", + "Library": "લાયબ્રેરી", + "MediaPlayer": "મીડિયા પ્લેયર", + "MobileApp": "મોબાઈલ એપ", + "MobileDevice": "મોબાઇલ ડિવાઇસ", + "OperatingSystemCode": "ઓપરેટિંગ સિસ્ટમ કોડ", + "OperatingSystemFamilies": "ઓપરેટિંગ સિસ્ટમ પરિવારો", + "OperatingSystemFamiliesReportDocumentation": "આ રિપોર્ટ તમને બતાવે છે કે તમારા મુલાકાતીઓ ઓપરેટિંગ સિસ્ટમ પરિવાર દ્વારા જૂથબદ્ધ ઉપયોગ કરી રહ્યાં છે. ઓપરેટિંગ સિસ્ટમ પરિવારમાં વિવિધ સંસ્કરણો અથવા વિતરણોનો સમાવેશ થાય છે.", + "OperatingSystemFamily": "ઓપરેટિંગ સિસ્ટમ પરિવાર", + "OperatingSystemVersions": "ઓપરેટિંગ સિસ્ટમ આવૃત્તિઓ", + "OperatingSystemVersionsReportDocumentation": "આ રિપોર્ટ તમને તમારા મુલાકાતીઓ ઉપયોગ કરી રહ્યાં છે તે ઓપરેટિંગ સિસ્ટમ્સ બતાવે છે. દરેક સંસ્કરણ અને વિતરણ અલગથી બતાવવામાં આવે છે.", + "OperatingSystems": "ઓપરેટિંગ સિસ્ટમ્સ", + "Peripheral": "પેરિફેરલ", + "Phablet": "ફેબલેટ", + "Pim": "PIM", + "PluginDescription": "વપરાશકર્તા ઉપકરણો વિશે વિસ્તૃત માહિતી પ્રદાન કરે છે, જેમ કે બ્રાન્ડ (ઉત્પાદક), મોડલ (ઉપકરણ સંસ્કરણ), ઉપકરણ પ્રકાર (ટીવી, કન્સોલ, સ્માર્ટ ફોન, ડેસ્કટોપ, વગેરે) અને વધુ.", + "PortableMediaPlayer": "પોર્ટેબલ મીડિયા પ્લેયર", + "SmartDisplay": "સ્માર્ટ ડિસ્પ્લે", + "SmartSpeaker": "સ્માર્ટ સ્પીકર", + "Smartphone": "સ્માર્ટફોન", + "Software": "સોફ્ટવેર", + "TV": "ટી.વી", + "Tablet": "ટેબ્લેટ", + "UserAgent": "વપરાશકર્તા એજન્ટ", + "Wearable": "પહેરી શકાય તેવું", + "WidgetBrowserVersionsDocumentation": "આ રિપોર્ટમાં તમારા મુલાકાતીઓ કેવા પ્રકારના બ્રાઉઝરનો ઉપયોગ કરતા હતા તે વિશેની માહિતી ધરાવે છે. દરેક બ્રાઉઝર સંસ્કરણ અલગથી સૂચિબદ્ધ છે.", + "WidgetBrowsers": "મુલાકાતી બ્રાઉઝર", + "WidgetBrowsersDocumentation": "આ રિપોર્ટમાં તમારા મુલાકાતીઓ કેવા પ્રકારના બ્રાઉઝરનો ઉપયોગ કરતા હતા તે વિશેની માહિતી ધરાવે છે.", + "XVisitsFromDevices": "%2$s ડિવાઇસીસ પરથી %1$s મુલાકાતો", + "dataTableLabelBrands": "બ્રાન્ડ", + "dataTableLabelModels": "મોડલ", + "dataTableLabelSystemVersion": "ઓપરેટિંગ સિસ્ટમ સંસ્કરણ", + "dataTableLabelTypes": "પ્રકાર" + } +} diff --git a/plugins/DevicesDetection/lang/he.json b/plugins/DevicesDetection/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..565132c35a15454c23ce4cefd3bb7e60cf73fec3 --- /dev/null +++ b/plugins/DevicesDetection/lang/he.json @@ -0,0 +1,9 @@ +{ + "DevicesDetection": { + "BrowserEngine": "דפדפן", + "BrowserFamily": "משפחת דפדפן", + "BrowserVersion": "גרסת דפדפן", + "Browsers": "דפדפן", + "ColumnBrowser": "דפדפן" + } +} diff --git a/plugins/DevicesDetection/lang/hi.json b/plugins/DevicesDetection/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..92e48c4858cf13d4e173881a00d46aa570909db8 --- /dev/null +++ b/plugins/DevicesDetection/lang/hi.json @@ -0,0 +1,28 @@ +{ + "DevicesDetection": { + "BotDetected": "इस उपयोगकर्ता-एजेंट को %1$s के रूप में पहचाना गया है। इस उपयोगकर्ता एजेंट के साथ विज़िट डिफ़ॉल्ट रूप से Matomo में ट्रैक नहीं की जाएंगी।", + "BrowserEngine": "ब्राउज़र", + "BrowserEngines": "ब्राउज़र इंजन", + "BrowserFamily": "ब्राउज़र परिवार", + "BrowserVersion": "ब्राउज़र संस्करण", + "BrowserVersions": "ब्राउज़र संस्करणों", + "Browsers": "ब्राउज़र्स", + "Camera": "कैमरा", + "CarBrowser": "कार ब्राउज़र", + "ColumnBrowser": "ब्राउज़र", + "ColumnOperatingSystem": "आपरेटिंग सिस्टम", + "ColumnOperatingSystemVersion": "आपरेटिंग सिस्टम version", + "Console": "कंसोल", + "Device": "न्यूनतम मूल्य", + "DeviceBrand": "डिवाइस ब्रांड", + "MobileApp": "मोबाइल एप्लिकेशन", + "OperatingSystemFamily": "आपरेटिंग सिस्टम परिवार", + "OperatingSystems": "आपरेटिंग सिस्टम", + "Software": "सॉफ्टवेयर", + "WidgetBrowsers": "आगंतुक ब्राउज़र", + "dataTableLabelBrands": "ब्रांड", + "dataTableLabelModels": "मॉडल", + "dataTableLabelSystemVersion": "ऑपरेटिंग सिस्टम संस्करण", + "dataTableLabelTypes": "प्रकार" + } +} diff --git a/plugins/DevicesDetection/lang/hr.json b/plugins/DevicesDetection/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..464f763a83f9e45219ee9c683681c93d46519fbb --- /dev/null +++ b/plugins/DevicesDetection/lang/hr.json @@ -0,0 +1,22 @@ +{ + "DevicesDetection": { + "BrowserVersion": "Verzija pretraživača", + "Browsers": "Pretraživači", + "ColumnOperatingSystem": "Operativni sustav", + "Device": "Uređaj", + "DeviceBrand": "Marka uređaja", + "DeviceBrands": "Marke uređaja", + "DeviceDetection": "Prepoznavanje uređaja", + "DeviceModel": "Model uređaja", + "DeviceModels": "Modeli uređaja", + "DeviceType": "Vrsta uređaja", + "DeviceTypes": "Vrste uređaja", + "DevicesDetection": "Uređaji posetitelja", + "MobileApp": "Mobilna aplikacija", + "UserAgent": "Korisnički agent", + "WidgetBrowserVersionsDocumentation": "Ovaj izvještaj sadrži informacije o vrsti preglednika koji su tvoji posjetitelji koristili. Svaka verzija preglednika je navedena zasebno.", + "WidgetBrowsers": "Pretraživać posjetitelja", + "WidgetBrowsersDocumentation": "Ovaj izvještaj sadrži informacije o vrsti preglednika koji su tvoji posjetitelji koristili.", + "dataTableLabelTypes": "Vrsta" + } +} diff --git a/plugins/DevicesDetection/lang/hu.json b/plugins/DevicesDetection/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..aa305a65ff2855c5583af6a24b99214039cb0490 --- /dev/null +++ b/plugins/DevicesDetection/lang/hu.json @@ -0,0 +1,13 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Böngésző", + "BrowserFamily": "Böngészőcsalád", + "Browsers": "Böngészők", + "ColumnBrowser": "Böngésző", + "ColumnOperatingSystem": "Operációs rendszer", + "MobileApp": "Mobil alkalmazás", + "OperatingSystems": "Operációs rendszerek", + "WidgetBrowsers": "Látogatók böngészői", + "dataTableLabelTypes": "Típus" + } +} diff --git a/plugins/DevicesDetection/lang/hy.json b/plugins/DevicesDetection/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/id.json b/plugins/DevicesDetection/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..524aad385e16a3194b0652e5a7ce913b924c5ecc --- /dev/null +++ b/plugins/DevicesDetection/lang/id.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Agen-Pengguna ini terdeteksi sebagai %1$s. Kunjungan dengan agen pengguna ini tidak akan dilacak di Matomo secara asali.", + "BrowserCode": "Kode peramban", + "BrowserEngine": "Mesin peramban", + "BrowserEngineDocumentation": "Di laporan ini dapat terlihat bahwa peramban web pengunjung Anda dipecah berdasarkan mesin perambannya. %s Informasi terpenting bagi pengembang web adalah jenis mesin rendering yang digunakan oleh pengunjung mereka. Label berisi nama-nama mesin diikuti oleh peramban paling umum yang menggunakan mesin itu dalam tanda kurung.", + "BrowserEngines": "Mesin peramban", + "BrowserFamily": "Keluarga Peramban", + "BrowserVersion": "Versi Peramban", + "BrowserVersions": "Versi peramba", + "Browsers": "Peramban", + "Camera": "Kamera", + "CarBrowser": "Peramban mobil", + "ClientHints": "Petunjuk Klien", + "ClientHintsNotSupported": "Peramban Anda tidak mendukung petunjuk klien.", + "ClientType": "Tipe klien", + "ClientTypes": "Tipe klien", + "ColumnBrowser": "Peramban", + "ColumnOperatingSystem": "Sistem Operasi", + "ColumnOperatingSystemVersion": "Versi sistem operasi", + "ConsiderClientHints": "Pertimbangkan Petunjuk Klien", + "Console": "Konsol", + "Device": "Perangkat", + "DeviceBrand": "Merek perangkat", + "DeviceBrandReportDocumentation": "Laporan ini menunjukkan merek/produsen perangkat yang digunakan oleh pengunjung Anda. Dalam kebanyakan kasus, informasi ini hanya tersedia untuk perangkat non-desktop.", + "DeviceBrands": "Merek perangkat", + "DeviceDetection": "Deteksi perangkat", + "DeviceModel": "Model perangkat", + "DeviceModelReportDocumentation": "Laporan ini menampilkan perangkat yang digunakan oleh pengunjung Anda. Setiap model ditampilkan secara digabung dengan merek perangkat karena beberapa nama model digunakan oleh beberapa merek.", + "DeviceModels": "Model perangkat", + "DeviceType": "Tipe perangkat", + "DeviceTypeReportDocumentation": "Laporan ini menunjukkan jenis perangkat yang digunakan oleh pengunjung Anda. Laporan ini akan selalu menampilkan semua jenis perangkat yang dapat dideteksi oleh Matomo, meskipun tidak ada kunjungan dengan jenis tertentu.", + "DeviceTypes": "Tipe perangkat", + "Devices": "Perangkat", + "DevicesDetection": "Perangkat pengunjung", + "FeaturePhone": "Fitur telepon", + "FeedReader": "Pembaca Umpan", + "GenericDevice": "Generik %s", + "Library": "Pustaka", + "MediaPlayer": "Pemutar Media", + "MobileApp": "Aplikasi Seluler", + "MobileDevice": "Perangkat seluler", + "OperatingSystemCode": "Kode sistem operasi", + "OperatingSystemFamilies": "Keluarga Sistem Operasi", + "OperatingSystemFamiliesReportDocumentation": "Laporan ini menunjukkan sistem operasi yang digunakan oleh pengunjung Anda yang dikelompokkan menurut keluarga sistem operasi. Keluarga sistem operasi terdiri atas versi atau distribusi yang berbeda.", + "OperatingSystemFamily": "Keluarga sistem operasi", + "OperatingSystemVersions": "Versi Sistem Operasi", + "OperatingSystemVersionsReportDocumentation": "Laporan ini menunjukkan sistem operasi yang digunakan oleh pengunjung Anda. Setiap versi dan distribusi ditampilkan secara terpisah.", + "OperatingSystems": "Sistem operasi", + "Peripheral": "Periferal", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Memberikan informasi tambahan tentang perangkat pengguna, seperti Merek (produsen), Model (versi perangkat), jenis perangkat (tv, konsol, ponsel pintar, desktop, dll.) dan masih banyak lagi.", + "PortableMediaPlayer": "Pemutar media portabel", + "SmartDisplay": "Tampilan pintar", + "SmartSpeaker": "Pengeras suara pintar", + "Smartphone": "Telepon pintar", + "Software": "Perangkat lunak", + "TV": "TV", + "Tablet": "Tablet", + "UserAgent": "Agen-Pengguna", + "Wearable": "Dapat dipakai", + "WidgetBrowserVersionsDocumentation": "Laporan ini berisi informasi tentang jenis peramban yang digunakan oleh pengunjung Anda. Setiap versi peramban terdaftar secara terpisah.", + "WidgetBrowsers": "Peramban Web Pengunjung", + "WidgetBrowsersDocumentation": "Laporan ini berisi informasi tentang jenis peramban yang digunakan oleh pengunjung Anda.", + "XVisitsFromDevices": "%1$s kunjungan dari %2$s perangkat", + "dataTableLabelBrands": "Merek", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Versi Sistem Operasi", + "dataTableLabelTypes": "Jenis" + } +} diff --git a/plugins/DevicesDetection/lang/is.json b/plugins/DevicesDetection/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..5f18931f91251d999029e1193bb9a74058d4c343 --- /dev/null +++ b/plugins/DevicesDetection/lang/is.json @@ -0,0 +1,12 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Vafri", + "BrowserFamily": "Vafrafjölskylda", + "Browsers": "Vafrar", + "ColumnBrowser": "Vafri", + "ColumnOperatingSystem": "Stýrikerfi", + "OperatingSystems": "Stýrikerfi", + "WidgetBrowsers": "Vafrar gesta", + "dataTableLabelTypes": "Tegund" + } +} diff --git a/plugins/DevicesDetection/lang/it.json b/plugins/DevicesDetection/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..3f93af6e076cdc750319ac63a1f5b84f870a9a13 --- /dev/null +++ b/plugins/DevicesDetection/lang/it.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Questo User-Agent è stato individuato come %1$s. Le visite che avvengono tramite questo user agent non verranno tracciate da Matomo.", + "BrowserCode": "Codice browser", + "BrowserEngine": "Motore browser", + "BrowserEngineDocumentation": "Questo report mostra i browser dei vostri visitatori suddivisi in famiglie di browser. %s Le informazioni più importanti per gli sviluppatori web sono che tipo di motore di rendering i loro visitatori stanno usando. Le etichette contengono i nomi dei motori seguiti dal browser più comune che utilizza quel motore, messo tra parentesi.", + "BrowserEngines": "Motori browser", + "BrowserFamily": "Famiglia del Browser", + "BrowserVersion": "Versione del browser", + "BrowserVersions": "Versioni browser", + "Browsers": "Browser", + "Camera": "Fotocamera", + "CarBrowser": "Browser in auto", + "ClientHints": "Client hints", + "ClientHintsNotSupported": "Il tuo browser non supporta le client hints.", + "ClientType": "Tipo di client", + "ClientTypes": "Tipi di client", + "ColumnBrowser": "Browser", + "ColumnOperatingSystem": "Sistema operativo", + "ColumnOperatingSystemVersion": "Versione sistema operativo", + "ConsiderClientHints": "Considera i client hints", + "Console": "Console", + "Device": "Dispositivo", + "DeviceBrand": "Marca dispositivo", + "DeviceBrandReportDocumentation": "Questo report mostra i marchi / produttori dei dispositivi utilizzati dai tuoi visitatori. Nella maggior parte dei casi queste informazioni sono disponibili solo per dispositivi non desktop.", + "DeviceBrands": "Marche dispositivi", + "DeviceDetection": "Rilevamento dispositivo", + "DeviceModel": "Modello dispositivo", + "DeviceModelDetectionDisabled": "Rilevamento del modello del dispositivo disabilitato", + "DeviceModelDetectionDisabledRequirementNote": "Il rilevamento del modello del dispositivo e il report del modello del dispositivo devono essere disattivati.", + "DeviceModelReportDisabledByCompliancePolicy": "Il report sul modello del dispositivo è disabilitato dai criteri di conformità.", + "DeviceModelReportDocumentation": "Questo report mostra i dispositivi utilizzati dai tuoi visitatori. Ogni modello viene visualizzato insieme alla marca del dispositivo poiché alcuni nomi di modello vengono utilizzati da più marchi.", + "DeviceModels": "Modelli dispositivi", + "DeviceType": "Tipo di dispositivo", + "DeviceTypeReportDocumentation": "Questo report mostra i tipi di dispositivi utilizzati dai tuoi visitatori. Esso mostrerà sempre tutti i tipi di dispositivi che Matomo è in grado di rilevare, anche se non ci sono state visite con un tipo specifico.", + "DeviceTypes": "Tipi di dispositivo", + "Devices": "Dispositivi", + "DevicesDetection": "Dispositivi visitatore", + "FeaturePhone": "Feature phone", + "FeedReader": "Lettore di Feed", + "GenericDevice": "Generico %s", + "Library": "Libreria", + "MediaPlayer": "Media Player", + "MobileApp": "App Mobile", + "MobileDevice": "Dispositivo mobile", + "OnlyMajorVersionsSettingRequirementNote": "Vengono memorizzate solo le versioni principali del sistema operativo e del browser.", + "OnlyMajorVersionsSettingTitle": "Solo versioni principali", + "OperatingSystemCode": "Codice sistema operativo", + "OperatingSystemFamilies": "Famiglie Sistema Operativo", + "OperatingSystemFamiliesReportDocumentation": "Questo report mostra i sistemi operativi utilizzati dai visitatori raggruppati per famiglia di sistemi operativi. Una famiglia di sistemi operativi è composta da diverse versioni o distribuzioni.", + "OperatingSystemFamily": "Famiglia sistema operativo", + "OperatingSystemVersions": "Versioni Sistema Operativo", + "OperatingSystemVersionsReportDocumentation": "Questo report mostra i sistemi operativi utilizzati dai tuoi visitatori. Ogni versione e distribuzione viene mostrata separatamente.", + "OperatingSystems": "Sistemi Operativi", + "Peripheral": "Periferica", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Fornisce informazioni dettagliate sui dispositivi degli utenti, come marca, modello, tipo di dispositivo (tv, console, smartphone, desktop, ecc.) e altro ancora.", + "PortableMediaPlayer": "Media player portatile", + "SmartDisplay": "Smart display", + "SmartSpeaker": "Smart speaker", + "Smartphone": "Smartphone", + "Software": "Software", + "TV": "Apparecchio TV", + "Tablet": "Tablet", + "UserAgent": "User-Agent", + "Wearable": "Indossabile", + "WidgetBrowserVersionsDocumentation": "Questo report contiene informazioni sul tipo di browser che i tuoi visitatori hanno utilizzato. Ciascuna versione del browser è elencata separatamente.", + "WidgetBrowsers": "Browser dei visitatori", + "WidgetBrowsersDocumentation": "Questo report contiene informazioni sul tipo di browser utilizzato dai tuoi visitatori.", + "XVisitsFromDevices": "%1$s visite da %2$s dispositivi", + "dataTableLabelBrands": "Marca", + "dataTableLabelModels": "Modello", + "dataTableLabelSystemVersion": "Versione Sistema Operativo", + "dataTableLabelTypes": "Tipo" + } +} diff --git a/plugins/DevicesDetection/lang/ja.json b/plugins/DevicesDetection/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..cb420c7b51d10472e572cc804055dd8963c3638a --- /dev/null +++ b/plugins/DevicesDetection/lang/ja.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "このユーザーエージェントは %1$s として検出されます。このユーザーエージェントによるビジットは、デフォルトでは Matomo で追跡されません。", + "BrowserCode": "ブラウザコード", + "BrowserEngine": "ブラウザ エンジン", + "BrowserEngineDocumentation": "このチャートは、ビジターのブラウザをブラウザファミリー別に表示します。 %s Web開発者にとって最も重要な情報は、ビジターが使用しているレンダリングエンジンの種類です。ラベルはエンジンの名前で、括弧内にそのエンジンを使う最も一般的なブラウザ名を表示しています。", + "BrowserEngines": "ブラウザ エンジン", + "BrowserFamily": "ブラウザファミリー", + "BrowserVersion": "ブラウザのバージョン", + "BrowserVersions": "ブラウザのバージョン", + "Browsers": "ブラウザ", + "Camera": "カメラ", + "CarBrowser": "車載ブラウザ", + "ClientHints": "クライアントのヒント", + "ClientHintsNotSupported": "お使いのブラウザはクライアント ヒントをサポートしていません。", + "ClientType": "クライアントタイプ", + "ClientTypes": "クライアントタイプ", + "ColumnBrowser": "ブラウザ", + "ColumnOperatingSystem": "オペレーティングシステム", + "ColumnOperatingSystemVersion": "オペレーティングシステムのバージョン", + "ConsiderClientHints": "クライアントのヒントを検討する", + "Console": "コンソール", + "Device": "デバイス", + "DeviceBrand": "デバイスブランド", + "DeviceBrandReportDocumentation": "このレポートには、ビジターが使用していたデバイスのブランド/メーカーが表示されます。 ほとんどの場合、この情報はデスクトップ以外のデバイスでのみ利用できます。", + "DeviceBrands": "デバイスブランド", + "DeviceDetection": "デバイス検出", + "DeviceModel": "デバイスのモデル", + "DeviceModelReportDocumentation": "このレポートには、ビジターが使用しているデバイスが表示されます。 一部のモデル名は複数のブランドで使用されているため、各モデルはデバイスブランドと組み合わせて表示されます。", + "DeviceModels": "デバイスモデル", + "DeviceType": "デバイスタイプ", + "DeviceTypeReportDocumentation": "このレポートには、ビジターが使用していたデバイスの種類が表示されます。 このレポートには、特定のタイプのビジットがなかった場合でも、Matomoが検出できるすべてのデバイスタイプが常に表示されます。", + "DeviceTypes": "デバイスタイプ", + "Devices": "デバイス", + "DevicesDetection": "ビジターのデバイス", + "FeaturePhone": "フィーチャーフォン", + "FeedReader": "フィードリーダー", + "GenericDevice": "ジェネリック %s", + "Library": "ライブラリ", + "MediaPlayer": "メディアプレーヤー", + "MobileApp": "モバイルアプリ", + "MobileDevice": "モバイルデバイス", + "OperatingSystemCode": "オペレーティングシステムコード", + "OperatingSystemFamilies": "オペレーティングシステムファミリー", + "OperatingSystemFamiliesReportDocumentation": "このレポートには、ビジターが使用しているオペレーティングシステムがオペレーティングシステムファミリごとにグループ化されて表示されます。 オペレーティングシステムファミリは、さまざまなバージョンまたはディストリビューションで構成されています。", + "OperatingSystemFamily": "オペレーティングシステムファミリー", + "OperatingSystemVersions": "オペレーティングシステムのバージョン", + "OperatingSystemVersionsReportDocumentation": "このレポートには、ビジターが使用しているオペレーティングシステムが表示されます。 各バージョンとディストリビューションは別々に表示されます。", + "OperatingSystems": "オペレーティングシステム", + "Peripheral": "周辺機器", + "Phablet": "ファブレット", + "Pim": "PIM", + "PluginDescription": "ブランド ( メーカー ) 、モデル ( デバイス バージョン ) 、デバイスの種類 ( テレビ、コンソール、スマート フォン、デスクトップ ) など、ユーザー デバイスに関する拡張情報を提供します。", + "PortableMediaPlayer": "ポータブルメディアプレーヤー", + "SmartDisplay": "スマートディスプレイ", + "SmartSpeaker": "スマートスピーカー", + "Smartphone": "スマートフォン", + "Software": "ソフトウェア", + "TV": "TV", + "Tablet": "タブレット", + "UserAgent": "ユーザーエージェント", + "Wearable": "ウェアラブル", + "WidgetBrowserVersionsDocumentation": "ビジターが使用しているブラウザについてのリポートです。各ブラウザのバージョンは別途記載されています。", + "WidgetBrowsers": "ブラウザ", + "WidgetBrowsersDocumentation": "このレポートには、ビジターが使用していたブラウザの種類に関する情報が含まれています。", + "XVisitsFromDevices": "%2$s デバイスからの %1$s ビジット", + "dataTableLabelBrands": "ブランド", + "dataTableLabelModels": "モデル", + "dataTableLabelSystemVersion": "オペレーティングシステムのバージョン", + "dataTableLabelTypes": "タイプ" + } +} diff --git a/plugins/DevicesDetection/lang/ka.json b/plugins/DevicesDetection/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..b0e92822478d9395ec9f057586537dced768caed --- /dev/null +++ b/plugins/DevicesDetection/lang/ka.json @@ -0,0 +1,12 @@ +{ + "DevicesDetection": { + "BrowserEngine": "ბრაუზერი", + "BrowserFamily": "ბრაუზერის ოჯახი", + "Browsers": "ბრაუზერები", + "ColumnBrowser": "ბრაუზერი", + "ColumnOperatingSystem": "ოპერაციული სისტემა", + "OperatingSystems": "ოპერაციული სიტემები", + "WidgetBrowsers": "ვიზიტორის ბრაუზერები", + "dataTableLabelTypes": "ტიპი" + } +} diff --git a/plugins/DevicesDetection/lang/ko.json b/plugins/DevicesDetection/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..c6c652881001936025b1224803cbb5b4a3aa8bb4 --- /dev/null +++ b/plugins/DevicesDetection/lang/ko.json @@ -0,0 +1,45 @@ +{ + "DevicesDetection": { + "BrowserEngine": "브라우저 엔진", + "BrowserEngineDocumentation": "이 차트는 방문자의 브라우저를 제품군별로 표시합니다. %s 웹개발자에게 가장 중요한 정보는 사용자의 렌더링 엔진 종류입니다. 엔진의 이름과 괄호 안에 일반적인 해당 브라우저 이름을 표시하고 있습니다.", + "BrowserEngines": "브라우저 엔진들", + "BrowserFamily": "브라우저 타입", + "BrowserVersion": "브라우저 버전", + "BrowserVersions": "브라우저 버전들", + "Browsers": "브라우저들", + "Camera": "카메라", + "CarBrowser": "자동차 브라우저", + "ColumnBrowser": "브라우저", + "ColumnOperatingSystem": "운영 체제", + "ColumnOperatingSystemVersion": "운영 체제 버전", + "Console": "콘솔", + "Device": "기기장치", + "DeviceBrand": "기기장치 브랜드", + "DeviceDetection": "기기장치 탐지", + "DeviceModel": "기기장치 모델", + "DeviceType": "기기장치 타입", + "Devices": "기기장치들", + "DevicesDetection": "방문자 기기장치들", + "FeaturePhone": "피처 폰", + "MobileApp": "모바일 앱", + "OperatingSystemFamilies": "운영 체제 제품군들", + "OperatingSystemFamily": "운영 체제 제품군", + "OperatingSystemVersions": "운영 체제 버전들", + "OperatingSystems": "운영체제", + "Phablet": "패블릿", + "PluginDescription": "유저의 기기장치에 관하여 좀 더 자세한 정보를 제공합니다. 이는 브랜드(생산자), 모델(기기장치 버전), 타입(텔레비전, 콘솔, 스마트폰, 데스크탑 컴퓨터 등) 외 여러 정보를 포함합니다.", + "PortableMediaPlayer": "휴대용 미디어 플레이어", + "SmartDisplay": "스마트 디스플레이", + "Smartphone": "스마트폰", + "Software": "소프트웨어", + "TV": "텔레비전", + "Tablet": "태블릿", + "UserAgent": "사용자 에이전트", + "WidgetBrowserVersionsDocumentation": "사용자가 어떤 브라우저를 사용하는지에 대한 보고서입니다. 각 브라우저의 버전은 별도로 기재되어 있습니다.", + "WidgetBrowsers": "방문자 브라우저", + "dataTableLabelBrands": "브랜드", + "dataTableLabelModels": "모델", + "dataTableLabelSystemVersion": "운영 체제 버전", + "dataTableLabelTypes": "유형" + } +} diff --git a/plugins/DevicesDetection/lang/ku.json b/plugins/DevicesDetection/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/ku.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/lb.json b/plugins/DevicesDetection/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..de68cf33373aa82dcf278eca4bf10e7bb4e44fe0 --- /dev/null +++ b/plugins/DevicesDetection/lang/lb.json @@ -0,0 +1,5 @@ +{ + "DevicesDetection": { + "Browsers": "Browseren" + } +} diff --git a/plugins/DevicesDetection/lang/lt.json b/plugins/DevicesDetection/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..4a06fd13a072b3cc67fbc15c52d5b3793a444c95 --- /dev/null +++ b/plugins/DevicesDetection/lang/lt.json @@ -0,0 +1,34 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Naršyklė", + "BrowserEngines": "Naršyklių moduliai", + "BrowserFamily": "Naršyklių šeimos", + "BrowserVersion": "Naršyklės versija", + "Browsers": "Naršyklės", + "CarBrowser": "Automobilio naršyklė", + "ColumnBrowser": "Naršyklė", + "ColumnOperatingSystem": "Operacinė sistema", + "ColumnOperatingSystemVersion": "Operacinės sistemos versija", + "Device": "Įrenginys", + "DeviceBrand": "Įrenginio prekės ženklas", + "DeviceModel": "Įrenginio modelis", + "DeviceType": "Įrenginio tipas", + "DeviceTypes": "Įrenginių tipai", + "Devices": "Įrenginiai", + "MobileApp": "Mobilioji programa", + "OperatingSystems": "Operacinės sistemos", + "Phablet": "Planšetofonas", + "Pim": "PIM", + "PortableMediaPlayer": "Nešiojamoji medijos leistuvė", + "SmartDisplay": "Belaidis monitorius", + "Smartphone": "Išmanusis telefonas", + "Software": "Programinė įranga", + "TV": "Televizorius", + "Tablet": "Planšetinis kompiuteris", + "WidgetBrowsers": "Lankytojų naršyklės", + "dataTableLabelBrands": "Prekės ženklas", + "dataTableLabelModels": "Modelis", + "dataTableLabelSystemVersion": "Operacinės sistemos versija", + "dataTableLabelTypes": "Tipas" + } +} diff --git a/plugins/DevicesDetection/lang/lv.json b/plugins/DevicesDetection/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..2ab07d2d7e232d4ec9e15f61b8f2d7f42711a5fe --- /dev/null +++ b/plugins/DevicesDetection/lang/lv.json @@ -0,0 +1,16 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Pārlūks", + "BrowserEngineDocumentation": "Šis grafiks rāda apmeklētāju pārlūkus izdalītus pa pārlūku ģimenēm. %s Vissvarīgākā informācija tīkla izstrādātājiem ir kādu atveidojuma dzinēju apmeklētāji izmanto. Etiķetes satur dzinēju nosaukumus, kuriem seko visbiežāk izmantotie dzinēji.", + "BrowserFamily": "Pārlūku ģimene", + "BrowserVersion": "Pārlūka versija", + "Browsers": "Pārlūki", + "ColumnBrowser": "Pārlūks", + "ColumnOperatingSystem": "Operētājsistēma", + "OperatingSystems": "Operētājsistēmas", + "UserAgent": "Lietotāja aģents", + "WidgetBrowserVersionsDocumentation": "Šis ir ziņojums par to, kādus pārlūkus izmanto apmeklētāji. Katra pārlūka versija ir izdalīta atsevišķi.", + "WidgetBrowsers": "Apmeklētāju pārlūki", + "dataTableLabelTypes": "Tips" + } +} diff --git a/plugins/DevicesDetection/lang/ms.json b/plugins/DevicesDetection/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/ms.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/nb.json b/plugins/DevicesDetection/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..d2385f3be0edd4640172aecc9fc1fd06189e3351 --- /dev/null +++ b/plugins/DevicesDetection/lang/nb.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Denne brukeragenten er oppdaget som %1$s. Besøk med denne brukeragenten vil ikke kunne spores i Matomo som forvalg.", + "BrowserCode": "Nettleserkode", + "BrowserEngine": "Nettlesermotor", + "BrowserEngineDocumentation": "Denne rapporten viser dine besøkeres nettlesere brutt ned etter nettlesermotorer. %s den viktigste informasjonen for web-utviklere er hva slags rendering-motor som besøkere bruker. Oppføringen inneholder navnene til motoren fulgt av den mest kjente nettleseren som bruker den motoren.", + "BrowserEngines": "Nettlesermotorer", + "BrowserFamily": "Nettleserfamilie", + "BrowserVersion": "Nettleserversjon", + "BrowserVersions": "Nettleser-versjoner", + "Browsers": "Nettlesere", + "Camera": "Kamera", + "CarBrowser": "Nettleser i bil", + "ClientHints": "Klienthint", + "ClientHintsNotSupported": "Nettleseren din støtter ikke klienthint.", + "ClientType": "Klienttype", + "ClientTypes": "Klienttyper", + "ColumnBrowser": "Nettleser", + "ColumnOperatingSystem": "Operativsystem", + "ColumnOperatingSystemVersion": "Operativsystemversjon", + "ConsiderClientHints": "Ta hensyn til klienthint", + "Console": "Konsoll", + "Device": "Enhet", + "DeviceBrand": "Enhetsmerke", + "DeviceBrandReportDocumentation": "Denne rapportene viser merkene/produsentene av enhetene dine besøkende bruker. I de fleste fall er denne infoen kun tilgjengelig for enheter som ikke er skrivebordsmaskiner.", + "DeviceBrands": "Enhetsmerker", + "DeviceDetection": "Enhetsdeteksjon", + "DeviceModel": "Enhetsmodell", + "DeviceModelReportDocumentation": "Denne rapporten viser enhetene dine besøkende bruker. Hver modell vises kombinert med enhetsmerket, siden noen modellnavn brukes av flere merker.", + "DeviceModels": "Enhetsmodeller", + "DeviceType": "Enhetstype", + "DeviceTypeReportDocumentation": "Denne rapporten viser hvilke typer enheter dine besøkende bruker. Denne rapporten vil alltid vise enhetstypene Matomo kan oppdage, selv om det ikke er noen besøk av angitt type.", + "DeviceTypes": "Enhetstyper", + "Devices": "Enheter", + "DevicesDetection": "Besøkendes enheter", + "FeaturePhone": "Smarttelefon", + "FeedReader": "Informasjonskanalleser", + "GenericDevice": "Generisk %s", + "Library": "Bibliotek", + "MediaPlayer": "Mediaspiller", + "MobileApp": "Mobil-app", + "MobileDevice": "Mobilenhet", + "OperatingSystemCode": "Operativsystemskode", + "OperatingSystemFamilies": "Operativsystem-familier", + "OperatingSystemFamiliesReportDocumentation": "Denne rapporten viser operativsystemene dine besøkende bruker, gruppert etter operativsystemsfamilie. En operativsystemsfamilie består av forskjellige versjoner eller distribusjoner.", + "OperatingSystemFamily": "Operativsystemfamilie", + "OperatingSystemVersions": "Operativsystem-versjoner", + "OperatingSystemVersionsReportDocumentation": "Denne rapporten viser deg operativsystemene dine besøkere bruker. Hver versjon og distribusjon vises for seg.", + "OperatingSystems": "Operativsystem", + "Peripheral": "Tilleggsutstyr", + "Phablet": "Phablet", + "Pim": "Mediaspiller", + "PluginDescription": "Gir utvidet informasjon om brukers enheter, som merke (produsent), modell (enhetsversjon), enhetstype (TV, konsoll, smarttelefon, osv) og mer.", + "PortableMediaPlayer": "Bærbar mediespiller", + "SmartDisplay": "Smart skjerm", + "SmartSpeaker": "Smart høyttaler", + "Smartphone": "Smarttelefon", + "Software": "Programvare", + "TV": "TV", + "Tablet": "Nettbrett", + "UserAgent": "Bruker-agent", + "Wearable": "Ikledbar", + "WidgetBrowserVersionsDocumentation": "Denne rapporten inneholder informasjon om hva slags nettleser som dine besøkere bruker. Alle nettleserversjoner er opplistet separat.", + "WidgetBrowsers": "Besøkendes nettlesere", + "WidgetBrowsersDocumentation": "Denne rapporten inneholder info om hva slags nettleser dine besøkende brukte.", + "XVisitsFromDevices": "%1$s besøk fra %2$s enheter", + "dataTableLabelBrands": "Merke", + "dataTableLabelModels": "Modell", + "dataTableLabelSystemVersion": "Operativsystem-versjon", + "dataTableLabelTypes": "Type" + } +} diff --git a/plugins/DevicesDetection/lang/nl.json b/plugins/DevicesDetection/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..0c7e222cf44a45638a68503aeb02652062ff717c --- /dev/null +++ b/plugins/DevicesDetection/lang/nl.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Deze User-Agent is gedetecteerd als %1$s. Bezoeken met deze User-Agent zullen standaard niet worden gevolgd in Matomo.", + "BrowserCode": "Browsercode", + "BrowserEngine": "Browser engine", + "BrowserEngineDocumentation": "Deze grafiek toont de browsers van uw bezoekers, verdeeld in browserfamilies. %s De belangrijkste informatie voor webontwikkelaars is wat voor soort rendering engine hun bezoekers gebruiken. De labels bevatten de namen van de engines, gevolgd door de meest voorkomende browsers die deze engine gebruiken tussen haakjes.", + "BrowserEngines": "Browser engines", + "BrowserFamily": "Browser per familie", + "BrowserVersion": "Browserversie", + "BrowserVersions": "Browserversies", + "Browsers": "Browsers", + "Camera": "Camera", + "CarBrowser": "Auto browser", + "ClientHints": "Klantentips", + "ClientHintsNotSupported": "Uw browser ondersteunt geen klantentips.", + "ClientType": "Clienttype", + "ClientTypes": "Clienttypes", + "ColumnBrowser": "Browser", + "ColumnOperatingSystem": "Besturingssysteem", + "ColumnOperatingSystemVersion": "Versie besturingssysteem", + "ConsiderClientHints": "Overweeg klantentips", + "Console": "Console", + "Device": "Apparaat", + "DeviceBrand": "Merk apparaat", + "DeviceBrandReportDocumentation": "Dit rapport toont de merken / fabrikanten van de apparaten die uw bezoekers gebruikten. In de meeste gevallen is deze informatie enkel beschikbaar voor niet-desktop apparaten.", + "DeviceBrands": "Merken apparaten", + "DeviceDetection": "Detectie apparaat", + "DeviceModel": "Model apparaat", + "DeviceModelReportDocumentation": "Dit rapport toont de apparaten die uw bezoekers gebruikten. Elk model is weergegeven in combinatie met het merk, aangezien sommige modelnamen door meerdere merken worden gebruikt.", + "DeviceModels": "Modellen apparaten", + "DeviceType": "Soort apparaat", + "DeviceTypeReportDocumentation": "Dit rapport toont de apparaattypes die uw bezoekers gebruikten. Het rapport toont steeds alle apparaattypes die Matomo kan herkennen, zelfs als er geen bezoeken met een specifiek type waren.", + "DeviceTypes": "Soorten apparaten", + "Devices": "Apparaten", + "DevicesDetection": "Apparaten bezoekers", + "FeaturePhone": "Functie telefoon", + "FeedReader": "Feedlezer", + "GenericDevice": "Algemeen %s", + "Library": "Bibliotheek", + "MediaPlayer": "Mediaspeler", + "MobileApp": "Mobiele app", + "MobileDevice": "Mobiel apparaat", + "OperatingSystemCode": "Besturingssysteem code", + "OperatingSystemFamilies": "Besturingssysteem families", + "OperatingSystemFamiliesReportDocumentation": "Dit rapport toont de besturingssystemen die uw bezoekers gebruikten, gegroepeerd volgens besturingssysteemfamilie. Een besturingssysteemfamilie bestaat uit verschillende versies en distributies.", + "OperatingSystemFamily": "Besturingssyteem familie", + "OperatingSystemVersions": "Besturingssysteem versies", + "OperatingSystemVersionsReportDocumentation": "Dit rapport toont de besturingssystemen die uw bezoekers gebruikten. Elke versie en distributie wordt afzonderlijk weergegeven.", + "OperatingSystems": "Besturingssystemen", + "Peripheral": "Randapparatuur", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Toont meer informatie over de gebruikte apparaten, zoals Merk (fabrikant), Model (apparaat versie), apparaat type (tv, console, smartphone, enz) en meer.", + "PortableMediaPlayer": "Draagbare mediaspeler", + "SmartDisplay": "Slim scherm", + "SmartSpeaker": "Slimme luidspreker", + "Smartphone": "Smartphone", + "Software": "Software", + "TV": "TV", + "Tablet": "Tablet", + "UserAgent": "Gebruikers-agent", + "Wearable": "Draagbaar", + "WidgetBrowserVersionsDocumentation": "Dit rapport bevat informatie over het type browsers dat door uw bezoekers werd gebruikt. Elke browserversie wordt apart vermeld.", + "WidgetBrowsers": "Browser bezoeker", + "WidgetBrowsersDocumentation": "Dit rapport biedt informatie over welk soort browser uw bezoekers gebruikten.", + "XVisitsFromDevices": "%1$s bezoeken van %2$s apparaten", + "dataTableLabelBrands": "Merk", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Versie besturingssysteem", + "dataTableLabelTypes": "Soort" + } +} diff --git a/plugins/DevicesDetection/lang/nn.json b/plugins/DevicesDetection/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..b72a2a5dd4ffd5e74b38d27e97457adb7e253c16 --- /dev/null +++ b/plugins/DevicesDetection/lang/nn.json @@ -0,0 +1,13 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Nettlesar", + "BrowserFamily": "Nettlesarfamilar", + "Browsers": "Nettlesarar", + "ColumnBrowser": "Nettlesar", + "ColumnOperatingSystem": "Operativsystem", + "OperatingSystems": "Operativsystem", + "UserAgent": "Brukaragent", + "WidgetBrowsers": "Nytta nettlesarar", + "dataTableLabelTypes": "Type" + } +} diff --git a/plugins/DevicesDetection/lang/pl.json b/plugins/DevicesDetection/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..f9b8322a8944014aec48f00932304726d906bab4 --- /dev/null +++ b/plugins/DevicesDetection/lang/pl.json @@ -0,0 +1,52 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Silnik przeglądarki", + "BrowserEngineDocumentation": "Ten raport pokazuje przeglądarki, z których korzystają odwiedzający podzielone pod względem silników. %s Najważniejszą tu informacją dla deweloperów jest typ silnika renderującego używanego przez ich odwiedzających. Etykiety zawierają nazwy silników poprzedzające podane w nawiasach najpopularniejsze przeglądarki z danym silnikiem.", + "BrowserEngines": "Silniki przeglądarek", + "BrowserFamily": "Rodzina przeglądarki", + "BrowserVersion": "Wersja przeglądarki", + "BrowserVersions": "Wersje przeglądarek", + "Browsers": "Przeglądarki", + "Camera": "Aparat fotograficzny", + "CarBrowser": "Przeglądarka samochodowa", + "ColumnBrowser": "Przeglądarka", + "ColumnOperatingSystem": "System operacyjny", + "ColumnOperatingSystemVersion": "Wersja systemu operacyjnego", + "Console": "Konsola", + "Device": "Urządzenie", + "DeviceBrand": "Marka urządzenia", + "DeviceBrands": "Marki urządzeń", + "DeviceDetection": "Wykrywanie urządzeń", + "DeviceModel": "Model urządzenia", + "DeviceModels": "Modele urządzeń", + "DeviceType": "Typ urządzenia", + "DeviceTypes": "Typy urządzeń", + "Devices": "Urządzenia", + "DevicesDetection": "Urządzenia gości", + "FeaturePhone": "Zaawansowany telefon", + "GenericDevice": "Nieznany %s", + "Library": "Biblioteka", + "MobileApp": "Aplikacja mobilna", + "MobileDevice": "Urządzenie przenośne", + "OperatingSystemFamilies": "Rodziny systemu operacyjnego", + "OperatingSystemFamily": "Rodzina systemu operacyjnego", + "OperatingSystemVersions": "Wersje systemu operacyjnego", + "OperatingSystems": "Systemy operacyjne", + "Phablet": "Phablet", + "PluginDescription": "Dostarcza poszerzonej informacji o urządzeniach użytkowników, takich jak Marka (producent), Model (wersja urządzenia), typ urządzenia (tv, konsola, smartfon, komputer, etc) i innych.", + "PortableMediaPlayer": "Odtwarzacz przenośny", + "SmartDisplay": "Inteligentny wyświetlacz", + "Smartphone": "Smartfon", + "Software": "Oprogramowanie", + "TV": "TV", + "Tablet": "Tablet", + "UserAgent": "User-Agent", + "WidgetBrowserVersionsDocumentation": "Ten raport zawiera informacje o rodzajach przeglądarek używanych przez Twoich odwiedzających. Każda wersja przeglądarki prezentowana jest osobno.", + "WidgetBrowsers": "Przeglądarki odwiedzających", + "XVisitsFromDevices": "%1$s wizyt z %2$s urządzeń", + "dataTableLabelBrands": "Marka", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Wersja systemu operacyjnego", + "dataTableLabelTypes": "Rodzaj" + } +} diff --git a/plugins/DevicesDetection/lang/pt-br.json b/plugins/DevicesDetection/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..6ce901b394125847d74a11155ee5a1d810090b87 --- /dev/null +++ b/plugins/DevicesDetection/lang/pt-br.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Este User-Agent é detectado como %1$s. Visitas com este user-agent não serão rastreadas no Matomo por padrão.", + "BrowserCode": "Código do navegador", + "BrowserEngine": "Navegador", + "BrowserEngineDocumentation": "Este gráfico mostra os navegadores dos seus visitantes divididos em famílias de navegadores. %s A informação mais importante para os desenvolvedores web é o tipo de motor de renderização que seus visitantes estão usando. As etiquetas contêm os nomes dos motores seguido pelo navegador mais comum usando esse motor entre parênteses.", + "BrowserEngines": "Mecanismos de navegação", + "BrowserFamily": "Família do Navegador", + "BrowserVersion": "Versão do Browser", + "BrowserVersions": "Versões de navegadores", + "Browsers": "Navegadores", + "Camera": "Câmera", + "CarBrowser": "Navegador de carro", + "ClientHints": "Dicas do cliente", + "ClientHintsNotSupported": "O seu navegador não suporta dicas de clientes.", + "ClientType": "Tipo Cliente", + "ClientTypes": "Tipos de clientes", + "ColumnBrowser": "Navegador", + "ColumnOperatingSystem": "Sistema Operacional", + "ColumnOperatingSystemVersion": "Versão do sistema operacional", + "ConsiderClientHints": "Considere as dicas do cliente", + "Console": "Console", + "Device": "Dispositivo", + "DeviceBrand": "Marca do dispositivo", + "DeviceBrandReportDocumentation": "Este relatório mostra as marcas / fabricantes dos dispositivos que os seus visitantes utilizaram. Na maioria dos casos, esta informação só está disponível para dispositivos que não sejam computadores fixos.", + "DeviceBrands": "Marcas do dispositivo", + "DeviceDetection": "Detecção de dispositivo", + "DeviceModel": "Modelo do dispositivo", + "DeviceModelDetectionDisabled": "Detecção do modelo do dispositivo desativada", + "DeviceModelDetectionDisabledRequirementNote": "A detecção e o relatório do modelo do dispositivo devem ser desativados.", + "DeviceModelReportDisabledByCompliancePolicy": "O relatório de modelo do dispositivo está desativado pela política de conformidade.", + "DeviceModelReportDocumentation": "Este relatório mostra os dispositivos que seus visitantes estão usando. Cada modelo é exibido combinado com a marca do dispositivo, pois alguns nomes de modelo são usados por várias marcas.", + "DeviceModels": "Modelos do dispositivo", + "DeviceType": "Tipo de dispositivo", + "DeviceTypeReportDocumentation": "Este relatório mostra os tipos de dispositivos que seus visitantes estavam usando. Este relatório sempre mostrará todos os tipos de dispositivos que o Matomo é capaz de detectar, mesmo se não houver visitas com um tipo específico.", + "DeviceTypes": "Tipos de dispositivo", + "Devices": "Dispositivos", + "DevicesDetection": "Dispositivos dos Visitantes", + "FeaturePhone": "Características do telefone", + "FeedReader": "Leitor de feeds", + "GenericDevice": "Genérico %s", + "Library": "Biblioteca", + "MediaPlayer": "Reprodutor de mídia", + "MobileApp": "Aplicativo móvel", + "MobileDevice": "Dispositivo Móvel", + "OnlyMajorVersionsSettingRequirementNote": "Apenas as versões principais de sistemas operacionais e navegadores são armazenadas.", + "OnlyMajorVersionsSettingTitle": "Apenas versões principais", + "OperatingSystemCode": "Código do sistema operacional", + "OperatingSystemFamilies": "Famílias de Sistemas Operacionais", + "OperatingSystemFamiliesReportDocumentation": "Este relatório mostra os sistemas operacionais que seus visitantes estão usando agrupados por família de sistema operacional. Uma família de sistema operacional consiste em diferentes versões ou distribuições.", + "OperatingSystemFamily": "Família de sistema operacional", + "OperatingSystemVersions": "Versões dos Sistemas Operacionais", + "OperatingSystemVersionsReportDocumentation": "Este relatório mostra os sistemas operacionais que seus visitantes estão usando. Cada versão e distribuição são mostradas separadamente.", + "OperatingSystems": "Sistemas Operacionais", + "Peripheral": "Periférico", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Fornece informações adicionais sobre os dispositivos dos usuários, tais como marca (fabricante), modelo (versão do dispositivo), tipo de dispositivo (tv, consoles, smartphones, desktop, etc) e muito mais.", + "PortableMediaPlayer": "Media player portátil", + "SmartDisplay": "Display inteligente", + "SmartSpeaker": "Alto-falante inteligente", + "Smartphone": "Smartphone", + "Software": "Programa", + "TV": "Tv", + "Tablet": "Tablet", + "UserAgent": "User-Agent", + "Wearable": "Vestível", + "WidgetBrowserVersionsDocumentation": "Este relatório contém informações sobre o tipo de navegador que os visitantes estavam usando. Cada versão do navegador é listada separadamente.", + "WidgetBrowsers": "Navegador dos Visitantes", + "WidgetBrowsersDocumentation": "Este relatório contém informações sobre que tipo de navegador seus visitantes estavam usando.", + "XVisitsFromDevices": "%1$s visitas de %2$s dispositivos", + "dataTableLabelBrands": "Marca", + "dataTableLabelModels": "Modelo", + "dataTableLabelSystemVersion": "Versão do Sistema Operacional", + "dataTableLabelTypes": "Tipo" + } +} diff --git a/plugins/DevicesDetection/lang/pt.json b/plugins/DevicesDetection/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..cbc6f19bce553553d073df655dd8adca7bc594d1 --- /dev/null +++ b/plugins/DevicesDetection/lang/pt.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Este User-Agent é detetado como %1$s. Por predefinição, as visitas com este agente de utilizador não serão acompanhadas pelo Matomo.", + "BrowserCode": "Código do navegador", + "BrowserEngine": "Motor do navegador", + "BrowserEngineDocumentation": "Este relatório mostra os navegadores dos seus visitantes divididos por famílias. %s A informação mais importante para os programadores web é o tipo de motor de renderização que os seus visitantes estão a utilizar. As etiquetas contêm os nomes dos motores seguido pelo navegador mais comum que utiliza esse motor entre parênteses.", + "BrowserEngines": "Motores de navegadores", + "BrowserFamily": "Família do navegador", + "BrowserVersion": "Versão do navegador", + "BrowserVersions": "Versões de navegadores", + "Browsers": "Navegadores", + "Camera": "Câmara", + "CarBrowser": "Navegador de automóvel", + "ClientHints": "Dicas do cliente", + "ClientHintsNotSupported": "O seu navegador não suporta dicas de clientes.", + "ClientType": "Tipo de cliente", + "ClientTypes": "Tipos de clientes", + "ColumnBrowser": "Navegador", + "ColumnOperatingSystem": "Sistema operativo", + "ColumnOperatingSystemVersion": "Versão do sistema operativo", + "ConsiderClientHints": "Considere as dicas do cliente", + "Console": "Consola", + "Device": "Dispositivo", + "DeviceBrand": "Marca do dispositivo", + "DeviceBrandReportDocumentation": "Este relatório mostra as marcas / fabricantes dos dispositivos que os seus visitantes utilizaram. Na maioria dos casos, esta informação só está disponível para dispositivos que não sejam computadores fixos.", + "DeviceBrands": "Marcas dos dispositivos", + "DeviceDetection": "Deteção do dispositivo", + "DeviceModel": "Modelo do dispositivo", + "DeviceModelReportDocumentation": "Este relatório mostra os dispositivos que os seus visitantes estão a utilizar. Cada modelo é mostrado em conjunto com a marca do aparelho, uma vez que alguns nomes de modelos são utilizados por várias marcas.", + "DeviceModels": "Modelos dos dispositivos", + "DeviceType": "Tipo do dispositivo", + "DeviceTypeReportDocumentation": "Este relatório mostra os tipos de dispositivos que os seus visitantes estavam a utilizar. Este relatório mostrará sempre todos os tipos de dispositivos que a Matomo é capaz de detetar, mesmo que não tenha existido visitas com um tipo específico.", + "DeviceTypes": "Tipos de dispositivos", + "Devices": "Dispositivos", + "DevicesDetection": "Dispositivos dos visitantes", + "FeaturePhone": "Telemóvel simples", + "FeedReader": "Leitor de feeds", + "GenericDevice": "%s genérico", + "Library": "Biblioteca", + "MediaPlayer": "Reprodutor de media", + "MobileApp": "Aplicação Móvel", + "MobileDevice": "Dispositivo móvel", + "OperatingSystemCode": "Código do sistema operativo", + "OperatingSystemFamilies": "Famílias de sistemas operativos", + "OperatingSystemFamiliesReportDocumentation": "Este relatório mostra os sistemas operativos que os seus visitantes estão a utilizar, agrupados por família de sistemas operativos. Uma família de sistemas operativos consiste em diferentes versões ou distribuições.", + "OperatingSystemFamily": "Família do sistema operativo", + "OperatingSystemVersions": "Versões de sistemas operativos", + "OperatingSystemVersionsReportDocumentation": "Este relatório mostra os sistemas operativos que os seus visitantes estão a utilizar. Cada versão e distribuição é mostrada separadamente.", + "OperatingSystems": "Sistemas operativos", + "Peripheral": "Periférico", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Fornece informações adicionais sobre os dispositivos dos utilizadores, tais como Marca (fabricante), Modelo (versão do dispositivo), tipo de dispositivo (tv, consolas, smartphones, computador, etc.), entre outras.", + "PortableMediaPlayer": "Media player portátil", + "SmartDisplay": "Ecrã inteligente", + "SmartSpeaker": "Coluna inteligente", + "Smartphone": "Smartphone", + "Software": "Software", + "TV": "Tv", + "Tablet": "Tablet", + "UserAgent": "User-Agent", + "Wearable": "Dispositivo utilizável", + "WidgetBrowserVersionsDocumentation": "Este relatório contém informações sobre o tipo de navegadores que os seus visitantes estavam a utilizar. Cada versão do navegador é listada separadamente.", + "WidgetBrowsers": "Navegador do visitante", + "WidgetBrowsersDocumentation": "Este relatório contém informações sobre o tipo de navegador que os seus visitantes estavam a utilizar.", + "XVisitsFromDevices": "%1$s visitas de %2$s dispositivos", + "dataTableLabelBrands": "Marca", + "dataTableLabelModels": "Modelo", + "dataTableLabelSystemVersion": "Versão do sistema operativo", + "dataTableLabelTypes": "Tipo" + } +} diff --git a/plugins/DevicesDetection/lang/ro.json b/plugins/DevicesDetection/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..0e55377f1f8de7c40d18038dd11fc715b6ac67be --- /dev/null +++ b/plugins/DevicesDetection/lang/ro.json @@ -0,0 +1,40 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Motorul browserului", + "BrowserEngineDocumentation": "Acest grafic arată browserele vizitatorilor dvs. defalcate în familii browser. %s Cele mai importante informații pentru dezvoltatori web este ce fel de motor de cautare au utilizat vizitatorii lor . Etichetele conțin numele motoarelor urmate de cele mai comune browsere folosind motorul în paranteze.", + "BrowserFamily": "Browser după familie", + "BrowserVersion": "Versiunea browser-ului", + "BrowserVersions": "Versiunile browserului", + "Browsers": "Browsere", + "Camera": "Camera", + "CarBrowser": "Browser de masina", + "ColumnBrowser": "Browser", + "ColumnOperatingSystem": "Sistem de operare", + "Console": "Consola", + "Device": "Dispozitiv", + "DeviceBrand": "Brandul dispozitivului", + "DeviceDetection": "Detectarea dispozitivului", + "DeviceModel": "Modelul dispozitivului", + "DeviceType": "Tipul dispozitivului", + "Devices": "Dispozitive", + "DevicesDetection": "Dispozitivele Utilizatorilor", + "FeaturePhone": "Caracteristica telefon", + "MobileApp": "Aplicație mobilă", + "OperatingSystemFamilies": "Familiile Sistemului de Operare", + "OperatingSystemFamily": "Sistem de operare după familie", + "OperatingSystemVersions": "Versiunile Sistemului de Operare", + "OperatingSystems": "Sisteme operare", + "SmartDisplay": "Display inteligent", + "Smartphone": "Smartphone", + "Software": "Software", + "TV": "Tv", + "Tablet": "Tabletă", + "UserAgent": "User-Agent", + "WidgetBrowserVersionsDocumentation": "Acest raport conține informații cu privire la ce fel de browser utilizeaza de vizitatori . Fiecare versiune de browser este listată separat.", + "WidgetBrowsers": "Browsere vizitatori", + "dataTableLabelBrands": "Brand", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Versiunea Sistemului de Operare", + "dataTableLabelTypes": "Tip" + } +} diff --git a/plugins/DevicesDetection/lang/ru.json b/plugins/DevicesDetection/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..e25e00abdad799d7f762628f758dfdb16cf75f1c --- /dev/null +++ b/plugins/DevicesDetection/lang/ru.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Этот User-Agent обнаружен как %1$s. Посещения с этим пользовательским агентом не будут отслеживаться в Matomo по умолчанию.", + "BrowserCode": "Код браузера", + "BrowserEngine": "Браузерный движок", + "BrowserEngineDocumentation": "Этот график показывается информацию о браузерах посетителей, разбитую по типу семейства. %s Наиболее важная информация для веб-разработчиков - на каком движке работают браузеры посетителей. Значки содержат имя движка в зависимости от наиболее популярного браузера, который работает на этом движке.", + "BrowserEngines": "Браузерные движки", + "BrowserFamily": "Семейство браузеров", + "BrowserVersion": "Версия браузера", + "BrowserVersions": "Версии браузеров", + "Browsers": "По браузерам", + "Camera": "Камера", + "CarBrowser": "Автомобильный браузер", + "ClientHints": "Подсказки клиента (Client Hints)", + "ClientHintsNotSupported": "Ваш браузер не поддерживает подсказки клиента (Client Hints).", + "ClientType": "Тип клиента", + "ClientTypes": "Типы клиентов", + "ColumnBrowser": "Браузер", + "ColumnOperatingSystem": "Операционная система", + "ColumnOperatingSystemVersion": "Версия операционной системы", + "ConsiderClientHints": "Учитывать подсказки клиентов (Client Hints)", + "Console": "Консоль", + "Device": "Устройство", + "DeviceBrand": "Фирма-производитель устройства", + "DeviceBrandReportDocumentation": "В этом отчете показаны бренды/производители устройств, которыми пользовались ваши посетители. В большинстве случаев эта информация доступна только для устройств, отличных от настольных.", + "DeviceBrands": "Фирмы-производители устройства", + "DeviceDetection": "Определение устройств", + "DeviceModel": "Модель устройства", + "DeviceModelReportDocumentation": "В этом отчете показаны устройства, которые используют ваши посетители. Каждая модель отображается вместе с маркой устройства, поскольку названия некоторых моделей используются несколькими марками.", + "DeviceModels": "Модели устройства", + "DeviceType": "Тип устройства", + "DeviceTypeReportDocumentation": "В этом отчете показаны типы устройств, которые использовали ваши посетители. В этом отчете всегда будут отображаться все типы устройств, которые Matomo может обнаружить, даже если не было посещений с определенным типом.", + "DeviceTypes": "Типы устройства", + "Devices": "Устройства", + "DevicesDetection": "Устройства посетителей", + "FeaturePhone": "Особенность телефона", + "FeedReader": "Читатель ленты новостей (RSS-агрегатор)", + "GenericDevice": "Общий %s", + "Library": "Библиотека", + "MediaPlayer": "Медиа плеер", + "MobileApp": "Мобильное приложение", + "MobileDevice": "Мобильное устройство", + "OperatingSystemCode": "Код операционной системы", + "OperatingSystemFamilies": "Семейства операционных систем", + "OperatingSystemFamiliesReportDocumentation": "В этом отчете показаны операционные системы, которые используют ваши посетители, сгруппированные по семейству операционных систем. Семейство операционных систем состоит из разных версий или дистрибутивов.", + "OperatingSystemFamily": "Тип операционной системы", + "OperatingSystemVersions": "Версии операционных систем", + "OperatingSystemVersionsReportDocumentation": "В этом отчете показаны операционные системы, которые используют ваши посетители. Каждая версия и дистрибутив показаны отдельно.", + "OperatingSystems": "По операционным системам", + "Peripheral": "Периферия", + "Phablet": "Фаблет", + "Pim": "PIM", + "PluginDescription": "Предоставляет расширенную информацию об устройствах пользователя: бренд (производитель), модель (версия устройства), тип устройства (тв, консоли, смартфоны, рабочая станция и др.) и др.", + "PortableMediaPlayer": "Портативный медиа-плеер", + "SmartDisplay": "Смарт-дисплей", + "SmartSpeaker": "Умная колонка", + "Smartphone": "Смартфон", + "Software": "Программы", + "TV": "ТВ", + "Tablet": "Планшет", + "UserAgent": "User-Agent", + "Wearable": "Носимое", + "WidgetBrowserVersionsDocumentation": "Этот отчёт содержит информацию по браузерам, которые используют ваши посетители. Каждая версия браузера отображается отдельно.", + "WidgetBrowsers": "Браузер посетителя", + "WidgetBrowsersDocumentation": "Этот отчет содержит информацию о том, какой браузер использовали ваши посетители.", + "XVisitsFromDevices": "%1$s посещений с %2$s устройств", + "dataTableLabelBrands": "Фирма-производитель", + "dataTableLabelModels": "Модель", + "dataTableLabelSystemVersion": "Версия операционной системы", + "dataTableLabelTypes": "Тип" + } +} diff --git a/plugins/DevicesDetection/lang/si.json b/plugins/DevicesDetection/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/sk.json b/plugins/DevicesDetection/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..70fc88a2f45336bb618960631741cb5275727d65 --- /dev/null +++ b/plugins/DevicesDetection/lang/sk.json @@ -0,0 +1,13 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Prehliadač", + "BrowserFamily": "Rodina prehliadača", + "Browsers": "Prehliadače", + "ColumnBrowser": "Prehliadač", + "ColumnOperatingSystem": "Operačný systém", + "MobileApp": "Mobilná aplikácia", + "OperatingSystems": "Operačný systém", + "WidgetBrowsers": "Prehliadače návštevníkov", + "dataTableLabelTypes": "Typ" + } +} diff --git a/plugins/DevicesDetection/lang/sl.json b/plugins/DevicesDetection/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..8206d878aa30f286d4fb11d3f0f2f9cbe64e35e6 --- /dev/null +++ b/plugins/DevicesDetection/lang/sl.json @@ -0,0 +1,20 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Brskalnik", + "BrowserEngines": "Pogoni brskalnikov", + "BrowserFamily": "Družina brskalnika", + "BrowserVersion": "Različica brskalnika", + "Browsers": "Brskalniki", + "ColumnBrowser": "Brskalnik", + "ColumnOperatingSystem": "Operacijski sistem", + "DeviceBrand": "Znamka naprave", + "DeviceModel": "Model naprave", + "DeviceType": "Tip naprave", + "Devices": "Naprave", + "DevicesDetection": "Naprave obiskovalcev", + "OperatingSystemFamilies": "Družine operacijskih sistemov", + "OperatingSystems": "Operacijski sistemi", + "WidgetBrowsers": "Brskalnik obiskovalca", + "dataTableLabelTypes": "Tip" + } +} diff --git a/plugins/DevicesDetection/lang/sq.json b/plugins/DevicesDetection/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..3fda8bded95c03ed36d80fca7aa2bafd2a97deab --- /dev/null +++ b/plugins/DevicesDetection/lang/sq.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Ky Agjent Përdoruesi është pikasur si %1$s. Si parazgjedhje, vizita me këtë agjent përdoruesi s’do të ndiqen në Matomo.", + "BrowserCode": "Kod shfletuesi", + "BrowserEngine": "Motor shfletuesi", + "BrowserEngineDocumentation": "Ky grafik tregon shfletuesit e vizitorëve tuaj, grupuar sipas familjesh shfletuesish. %s Informacioni më i rëndësishëm për programuesit web është lloji i mekanizmit vizatues që përdor shfletuesi. Etiketat përmbajnë emrat e mekanizmave, pasuar në kllapa nga shfletuesit më të zakonshëm që përdorin secilin prej tyre.", + "BrowserEngines": "Motorë shfletuesi", + "BrowserFamily": "Familje shfletuesi", + "BrowserVersion": "Version shfletuesi", + "BrowserVersions": "Versione shfletuesi", + "Browsers": "Shfletues", + "Camera": "Kamerë", + "CarBrowser": "Shfletues për automjete", + "ClientHints": "Ndihmëza Klienti", + "ClientHintsNotSupported": "Shfletuesi juaj nuk mbulon ndihmëza klienti.", + "ClientType": "Lloj klienti", + "ClientTypes": "Lloje klientësh", + "ColumnBrowser": "Shfletues", + "ColumnOperatingSystem": "Sistem operativ", + "ColumnOperatingSystemVersion": "Version sistemi operativ", + "ConsiderClientHints": "Merr Parasysh Ndihmëza Klienti", + "Console": "Konsolë", + "Device": "Pajisje", + "DeviceBrand": "Markë pajisjeje", + "DeviceBrandReportDocumentation": "Ky raport shfaq markat / prodhuesit e pajisjeve që përdornin vizitorët tuaj. Në shumicën e rasteve ky informacion mund të kihet vetëm për pajisje jo desktop.", + "DeviceBrands": "Marka pajisjesh", + "DeviceDetection": "Zbulim pajisjeje", + "DeviceModel": "Model pajisjeje", + "DeviceModelDetectionDisabled": "U çaktivizua pikasje modeli pajisjeje", + "DeviceModelDetectionDisabledRequirementNote": "Pikasje modeli pajisjeje dhe raportim modeli pajisjeje duhen çaktivizuar.", + "DeviceModelReportDisabledByCompliancePolicy": "Raportimi i modelit të pajisjes është çaktivizuar nga rregulla përputhjeje me rregullore dhe standarde.", + "DeviceModelReportDocumentation": "Ky raport shfaq pajisjet që përdorin vizitorët tuaj. Secili model shfaqet me markën e pajisjes, ngaqë disa emra modelesh përdoren nga shumë marka.", + "DeviceModels": "Modele pajisjesh", + "DeviceType": "Lloj pajisjeje", + "DeviceTypeReportDocumentation": "Ky raport shfaq llojet e pajisjeve që përdornin vizitorët tuaj. Ky raport do të shfaqë përherë krejt llojet e pajisjeve që është në gjendje të pikasë Matomo-ja, edhe pse s’pati vizita me një lloj të caktuar.", + "DeviceTypes": "Lloje pajisjesh", + "Devices": "Pajisje", + "DevicesDetection": "Pajisje Vizitorësh", + "FeaturePhone": "Telefon", + "FeedReader": "Lexues Prurjesh", + "GenericDevice": "%s elementare", + "Library": "Bibliotekë", + "MediaPlayer": "Lojtës Mediash", + "MobileApp": "Aplikacion Celulari", + "MobileDevice": "Pajisje celulare", + "OnlyMajorVersionsSettingRequirementNote": "Depozitohen vetëm versioni të rëndësishëm OS-esh dhe shfletuesish.", + "OnlyMajorVersionsSettingTitle": "Vetëm versioni të rëndësishëm", + "OperatingSystemCode": "Kod sistemi operativ", + "OperatingSystemFamilies": "Familje Sistemesh Operativë", + "OperatingSystemFamiliesReportDocumentation": "Ky raport shfaq sistemet operative që përdorin vizitorët tuaj, grupuar sipas familjeve të sistemit. Një familje sistemi operativ përbëhet nga versione ose shpërndarje të ndryshme.", + "OperatingSystemFamily": "Familje Sistemi Operativ", + "OperatingSystemVersions": "Versione Sistemesh Operativë", + "OperatingSystemVersionsReportDocumentation": "Ky raport ju shfaq sistemet operative që po përdorin vizitorët tuaj. Çdo version dhe shpërndarje shfaqet më vete.", + "OperatingSystems": "Sisteme operativë", + "Peripheral": "Periferike", + "Phablet": "Fablet", + "Pim": "PIM", + "PluginDescription": "Furnizon të dhëna të zgjeruara rreth pajisjesh përdoruesish, të tilla si Marka (prodhuesi), Modeli (version pajisjeje), lloj pajisjeje (tv, konsola, telefon të mençur, desktop, etj) dhe të tjera.", + "PortableMediaPlayer": "Lojtës mediash portativ", + "SmartDisplay": "Ekran i mençur", + "SmartSpeaker": "Altoparlant i mençur", + "Smartphone": "Telefon i mençur", + "Software": "Software", + "TV": "Tv", + "Tablet": "Tablet", + "UserAgent": "Agjent Përdoruesi", + "Wearable": "Portativ", + "WidgetBrowserVersionsDocumentation": "Ky raport përmban të dhëna rreth llojeve të shfletuesit që përdornin vizitorët tuaj. Secili version i një shfletuesi tregohet veçmas.", + "WidgetBrowsers": "Shfletues Vizitori", + "WidgetBrowsersDocumentation": "Ky raport përmban të dhëna rreth ç’lloje shfletuesish përdornin vizitorët tuaj.", + "XVisitsFromDevices": "%1$s vizita nga %2$s pajisje", + "dataTableLabelBrands": "Markë", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Version Sistemi Operativ", + "dataTableLabelTypes": "Lloj" + } +} diff --git a/plugins/DevicesDetection/lang/sr.json b/plugins/DevicesDetection/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..04f51d043de6491511616b30cbfe991bd9951316 --- /dev/null +++ b/plugins/DevicesDetection/lang/sr.json @@ -0,0 +1,45 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Brauzer", + "BrowserEngineDocumentation": "Ovaj grafikon prikazuje brauzere vaših posetilaca po porodicama. %s Najvažnija informacija za web dizajnere je koju vrstu endžina koriste njihovi posetioci. Naptisi sadrže imena endžina a u zagradi je najčešća lista brauzera koji koriste taj endžin.", + "BrowserEngines": "Endžini brauzera", + "BrowserFamily": "Porodica brauzera", + "BrowserVersion": "Verzija brauzera", + "BrowserVersions": "Verzije pretraživača", + "Browsers": "Brauzeri", + "Camera": "Kamera", + "CarBrowser": "Pretraživač za auto", + "ColumnBrowser": "Brauzer", + "ColumnOperatingSystem": "Operativni sistem", + "ColumnOperatingSystemVersion": "Verzija operativnog sistema", + "Console": "Konzola", + "Device": "Uređaj", + "DeviceBrand": "Marka uređaja", + "DeviceDetection": "Prepoznavanje uređaja", + "DeviceModel": "Model uređaja", + "DeviceType": "Tip uređaja", + "Devices": "Uređaji", + "DevicesDetection": "Uređaji posetilaca", + "FeaturePhone": "Telefon", + "MobileApp": "Mobilna aplikacija", + "OperatingSystemFamilies": "Porodice operativnih sistema", + "OperatingSystemFamily": "Porodica operativnih sistema", + "OperatingSystemVersions": "Verzije operativnog sistema", + "OperatingSystems": "Operativni sistemi", + "Phablet": "Fablet", + "PluginDescription": "Prikazuje dodatne informacije korisničkih uređaja poput marke (proizvođač), model (verzija uređaja), tip uređaja (TV, konzola, pametni telefon, desktop računar itd.) itd.", + "PortableMediaPlayer": "Portabilni media plejer", + "SmartDisplay": "Pametan displej", + "Smartphone": "Pametni telefon", + "Software": "Softver", + "TV": "TV", + "Tablet": "Tablet", + "UserAgent": "Korisnički agent", + "WidgetBrowserVersionsDocumentation": "Ovaj izveštaj prikazuje koje brauzere koriste vaši posetioci. Svaka verzija brauzera je prikazana posebno.", + "WidgetBrowsers": "Brauzeri posetilaca", + "dataTableLabelBrands": "Marka", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "Verzija operativnog sistema", + "dataTableLabelTypes": "Tip" + } +} diff --git a/plugins/DevicesDetection/lang/sv.json b/plugins/DevicesDetection/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..36c58f59f83e37368c44367c5d31ba825abe8b22 --- /dev/null +++ b/plugins/DevicesDetection/lang/sv.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Den här användaragenten är upptäckt som %1$s. Besök med denna användaragent kommer inte att spåras i Matomo som default.", + "BrowserCode": "Webbläsarkod", + "BrowserEngine": "Webbläsarmotor", + "BrowserEngineDocumentation": "Detta diagram visar besökarnas webbläsare uppdelat i webbläsarfamiljer. %s Den viktigaste informationen för webbutvecklare är vilken typ av renderingsmotor besökarna använder. Etiketterna innehåller namnen på de renderingsmotorn följt av den vanligaste webbläsaren med den motorn inom parentes.", + "BrowserEngines": "Webbläsarmotorer", + "BrowserFamily": "Webbläsarfamilj", + "BrowserVersion": "Webbläsarversion", + "BrowserVersions": "Webbläsarversioner", + "Browsers": "Webbläsare", + "Camera": "Kamera", + "CarBrowser": "Bilwebbläsare", + "ClientHints": "Tips för klienter", + "ClientHintsNotSupported": "Din webbläsare stöder inte klienttips.", + "ClientType": "Klienttyp", + "ClientTypes": "Klienttyper", + "ColumnBrowser": "Webbläsare", + "ColumnOperatingSystem": "Operativsystem", + "ColumnOperatingSystemVersion": "Operativsystemversion", + "ConsiderClientHints": "Överväg klienttips", + "Console": "Konsol", + "Device": "Enhet", + "DeviceBrand": "Typ av utrustning", + "DeviceBrandReportDocumentation": "Den här rapporten visar de varumärken / tillverkare av de enheter som dina besökare använde. I de flesta fall är denna information endast tillgänglig för enheter som inte är stationära.", + "DeviceBrands": "Typ av utrustning", + "DeviceDetection": "Utrustningsinformation", + "DeviceModel": "Modell", + "DeviceModelReportDocumentation": "Den här rapporten visar vilka enheter dina besökare använder. Varje modell visas i kombination med enhetens märke eftersom vissa modellnamn används av flera märken.", + "DeviceModels": "Enhetsmodell", + "DeviceType": "Utrustningstyp", + "DeviceTypeReportDocumentation": "Den här rapporten visar vilka typer av enheter dina besökare använde. Denna rapport visar alltid alla typer av enheter som Matomo har möjlighet att upptäcka, även om det inte fanns några besök med en viss typ av enhet.", + "DeviceTypes": "Enhetstyper", + "Devices": "Enheter", + "DevicesDetection": "Besöksutrustning", + "FeaturePhone": "Telefonmodell", + "FeedReader": "Matarläsare", + "GenericDevice": "Generell %s", + "Library": "Bibliotek", + "MediaPlayer": "Mediaspelare", + "MobileApp": "Mobilapp", + "MobileDevice": "Mobil enhet", + "OperatingSystemCode": "Operativsystemskod", + "OperatingSystemFamilies": "Operativsystemfamiljer", + "OperatingSystemFamiliesReportDocumentation": "Denna rapport visar vilka operativsystem dina besökare använder grupperat på operativsystemfamilj. En operativsystemfamilj består av olika versioner eller distributioner.", + "OperatingSystemFamily": "Operativsystemfamilj", + "OperatingSystemVersions": "Operativsystemversion", + "OperatingSystemVersionsReportDocumentation": "Denna rapport innehåller information om vilken typ av webbläsare dina besökare använde. Varje version och distribution av webbläsare är listad separat.", + "OperatingSystems": "Operativsystem", + "Peripheral": "Kringutrustning", + "Phablet": "Phablet", + "Pim": "PIM", + "PluginDescription": "Visar utökad information om användarenheter som t.ex. tillverkare, modell, typ av enhet mm.", + "PortableMediaPlayer": "Portabel mediaspelare", + "SmartDisplay": "Smart skärm", + "SmartSpeaker": "Smart högtalare", + "Smartphone": "Smartphone", + "Software": "Mjukvara", + "TV": "TV", + "Tablet": "Surfplatta", + "UserAgent": "User-Agent", + "Wearable": "Bärbar", + "WidgetBrowserVersionsDocumentation": "Denna rapport innehåller information om vilken typ av webbläsare dina besökare använde. Varje version av webbläsare är listad separat.", + "WidgetBrowsers": "Besökarens webbläsare", + "WidgetBrowsersDocumentation": "Denna rapport innehåller information om vilken typ av webbläsare dina besökare använde.", + "XVisitsFromDevices": "%1$s besök från %2$s enheter", + "dataTableLabelBrands": "Märke", + "dataTableLabelModels": "Modell", + "dataTableLabelSystemVersion": "Hanterar Systemets version", + "dataTableLabelTypes": "Typ" + } +} diff --git a/plugins/DevicesDetection/lang/ta.json b/plugins/DevicesDetection/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..194d2d8e108fb198c15fcbfef0eb08e6cad67026 --- /dev/null +++ b/plugins/DevicesDetection/lang/ta.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "இந்த பயனர் முகவர் %1$s என கண்டறியப்படுகிறது. இந்த பயனர் முகவருடனான வருகைகள் முன்னிருப்பாக மாடோமோவில் கண்காணிக்கப்படாது.", + "BrowserCode": "உலாவி குறியீடு", + "BrowserEngine": "உலாவி இயந்திரம்", + "BrowserEngineDocumentation": "இந்த அறிக்கை உங்கள் பார்வையாளர்களின் உலாவிகள் உலாவி இயந்திரங்களாக உடைக்கப்பட்டிருப்பதைக் காட்டுகிறது. %s வலை உருவாக்குநர்களுக்கான மிக முக்கியமான செய்தி என்னவென்றால், அவர்களின் பார்வையாளர்கள் எந்த வகையான வழங்குதல் எஞ்சின் பயன்படுத்துகிறார்கள் என்பதுதான். லேபிள்களில் என்சின்களின் பெயர்கள் உள்ளன, அதைத் தொடர்ந்து அந்த இயந்திரத்தை அடைப்புக்குறிக்குள் பயன்படுத்தும் பொதுவான உலாவி.", + "BrowserEngines": "உலாவி என்சின்கள்", + "BrowserFamily": "உலாவி குடும்பம்", + "BrowserVersion": "உலாவி பதிப்பு", + "BrowserVersions": "உலாவி பதிப்புகள்", + "Browsers": "உலாவிகள்", + "Camera": "கேமரா", + "CarBrowser": "கார் உலாவி", + "ClientHints": "கிளையன்ட் குறிப்புகள்", + "ClientHintsNotSupported": "உங்கள் உலாவி கிளையன்ட் குறிப்புகளை ஆதரிக்காது.", + "ClientType": "கிளையன்ட் வகை", + "ClientTypes": "கிளையன்ட் வகைகள்", + "ColumnBrowser": "உலாவி", + "ColumnOperatingSystem": "இயக்க முறைமை", + "ColumnOperatingSystemVersion": "இயக்க முறைமை பதிப்பு", + "ConsiderClientHints": "கிளையன்ட் குறிப்புகளைக் கவனியுங்கள்", + "Console": "கன்சோல்", + "Device": "சாதனம்", + "DeviceBrand": "சாதன சூட்டுக்குறி", + "DeviceBrandReportDocumentation": "இந்த அறிக்கை உங்கள் பார்வையாளர்கள் பயன்படுத்தும் சாதனங்களின் பிராண்டுகள் / உற்பத்தியாளர்களைக் காட்டுகிறது. பெரும்பாலான சந்தர்ப்பங்களில் இந்த செய்தி டெச்க்டாப் அல்லாத சாதனங்களுக்கு மட்டுமே கிடைக்கிறது.", + "DeviceBrands": "சாதன பிராண்டுகள்", + "DeviceDetection": "சாதனக் கண்டறிதல்", + "DeviceModel": "சாதன மாதிரி", + "DeviceModelReportDocumentation": "உங்கள் பார்வையாளர்கள் பயன்படுத்தும் சாதனங்களை இந்த அறிக்கை காட்டுகிறது. சில மாதிரி பெயர்கள் பல பிராண்டுகளால் பயன்படுத்தப்படுவதால் ஒவ்வொரு மாதிரியும் சாதன பிராண்டுடன் இணைந்து காட்டப்படும்.", + "DeviceModels": "சாதன மாதிரிகள்", + "DeviceType": "சாதன வகை", + "DeviceTypeReportDocumentation": "இந்த அறிக்கை உங்கள் பார்வையாளர்கள் பயன்படுத்தும் சாதனங்களின் வகைகளைக் காட்டுகிறது. ஒரு குறிப்பிட்ட வகையுடன் வருகைகள் இல்லாவிட்டாலும், மாடோமோ கண்டறியக்கூடிய அனைத்து சாதன வகைகளையும் இந்த அறிக்கை எப்போதும் காண்பிக்கும்.", + "DeviceTypes": "சாதன வகைகள்", + "Devices": "சாதனங்கள்", + "DevicesDetection": "பார்வையாளர் சாதனங்கள்", + "FeaturePhone": "அம்ச தொலைபேசி", + "FeedReader": "உணவளிக்கும் வாசகருக்கு", + "GenericDevice": "பொதுவான %s", + "Library": "நூலகம்", + "MediaPlayer": "மீடியா பிளேயர்", + "MobileApp": "மொபைல் பயன்பாடு", + "MobileDevice": "மொபைல் சாதனம்", + "OperatingSystemCode": "இயக்க முறைமை குறியீடு", + "OperatingSystemFamilies": "இயக்க முறைமை குடும்பங்கள்", + "OperatingSystemFamiliesReportDocumentation": "உங்கள் பார்வையாளர்கள் இயக்க முறைமை குடும்பத்தால் தொகுக்கப்பட்ட இயக்க முறைமைகளை இந்த அறிக்கை உங்களுக்குக் காட்டுகிறது. ஒரு இயக்க முறைமை குடும்பம் வெவ்வேறு பதிப்புகள் அல்லது விநியோகங்களைக் கொண்டுள்ளது.", + "OperatingSystemFamily": "இயக்க முறைமை குடும்பம்", + "OperatingSystemVersions": "இயக்க முறைமை பதிப்புகள்", + "OperatingSystemVersionsReportDocumentation": "உங்கள் பார்வையாளர்கள் பயன்படுத்தும் இயக்க முறைமைகளை இந்த அறிக்கை உங்களுக்குக் காட்டுகிறது. ஒவ்வொரு பதிப்பு மற்றும் வழங்கல் தனித்தனியாக காட்டப்பட்டுள்ளது.", + "OperatingSystems": "இயக்க முறைமைகள்", + "Peripheral": "புற", + "Phablet": "பேப்லெட்", + "Pim": "பிம்", + "PluginDescription": "சூட்டுக்குறி (உற்பத்தியாளர்), மாதிரி (சாதன பதிப்பு), சாதன வகை (டிவி, கன்சோல்கள், அறிவுள்ள போன்கள், டெச்க்டாப் போன்றவை) மற்றும் பல போன்ற பயனர் சாதனங்களைப் பற்றிய நீட்டிக்கப்பட்ட தகவல்களை வழங்குகிறது.", + "PortableMediaPlayer": "போர்ட்டபிள் மீடியா பிளேயர்", + "SmartDisplay": "அறிவுள்ள காட்சி", + "SmartSpeaker": "அறிவுள்ள ச்பீக்கர்", + "Smartphone": "ச்மார்ட்போன்", + "Software": "மென்பொருள்", + "TV": "டிவி", + "Tablet": "டேப்லெட்", + "UserAgent": "பயனர் முகவர்", + "Wearable": "அணியக்கூடிய", + "WidgetBrowserVersionsDocumentation": "இந்த அறிக்கையில் உங்கள் பார்வையாளர்கள் எந்த வகையான உலாவி பயன்படுத்துகிறார்கள் என்பது பற்றிய தகவல்கள் உள்ளன. ஒவ்வொரு உலாவி பதிப்பும் தனித்தனியாக பட்டியலிடப்பட்டுள்ளது.", + "WidgetBrowsers": "பார்வையாளர் உலாவி", + "WidgetBrowsersDocumentation": "இந்த அறிக்கையில் உங்கள் பார்வையாளர்கள் எந்த வகையான உலாவி பயன்படுத்துகிறார்கள் என்பது பற்றிய தகவல்கள் உள்ளன.", + "XVisitsFromDevices": "%2$s சாதனங்களிலிருந்து %1$s வருகைகள்", + "dataTableLabelBrands": "சூட்டுக்குறி", + "dataTableLabelModels": "மாதிரியுரு", + "dataTableLabelSystemVersion": "இயக்க முறைமை பதிப்பு", + "dataTableLabelTypes": "வகை" + } +} diff --git a/plugins/DevicesDetection/lang/te.json b/plugins/DevicesDetection/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..5b7f3c3402c355c6fa4f8dd2ccc3dd32ab60847b --- /dev/null +++ b/plugins/DevicesDetection/lang/te.json @@ -0,0 +1,14 @@ +{ + "DevicesDetection": { + "BrowserEngine": "విహారిణి", + "BrowserFamily": "విహారిణి కుటుంబం", + "BrowserVersion": "విహారిణి సంచిక", + "Browsers": "విహారిణులు", + "ColumnBrowser": "విహారిణి", + "ColumnOperatingSystem": "నిర్వాహక వ్యవస్థ", + "OperatingSystemFamily": "నిర్వాహక వ్యవస్థ కుటుంబం", + "OperatingSystems": "నిర్వాహక వ్యవస్థలు", + "WidgetBrowsers": "సందర్శకుల విహారిణులు", + "dataTableLabelTypes": "రకం" + } +} diff --git a/plugins/DevicesDetection/lang/th.json b/plugins/DevicesDetection/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..ce9674abde85c3d5605b77ab5758033899433ef4 --- /dev/null +++ b/plugins/DevicesDetection/lang/th.json @@ -0,0 +1,13 @@ +{ + "DevicesDetection": { + "BrowserEngine": "เบราว์เซอร์", + "BrowserFamily": "ตระกูลของเบราว์เซอร์", + "BrowserVersion": "รุ่นของโปรแกรมบราวเซอร์", + "Browsers": "เบราว์เซอร์", + "ColumnBrowser": "เบราว์เซอร์", + "ColumnOperatingSystem": "ระบบปฏิบัติการ", + "OperatingSystems": "ระบบปฏิบัติการ", + "WidgetBrowsers": "เบราว์เซอร์ของผู้เข้าชม", + "dataTableLabelTypes": "รูปแบบ" + } +} diff --git a/plugins/DevicesDetection/lang/tl.json b/plugins/DevicesDetection/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..1936b0210e25c068be55e13c141b0a6e72446318 --- /dev/null +++ b/plugins/DevicesDetection/lang/tl.json @@ -0,0 +1,38 @@ +{ + "DevicesDetection": { + "BotDetected": "Ang User-Agent na ito ay nakita bilang %1$s. Ang mga pagbisita sa user agent na ito ay hindi masusubaybayan sa Matomo bilang default.", + "BrowserEngine": "Browser", + "BrowserEngineDocumentation": "Ipinapakita ng chart na ito ang magkaka hiwa-hiwalay na pamilya ng browser na gamit ng iyong bisita. %s Ang pinakamahalagang impormasyon na kinakailangan ng web developers ay kung anong klaseng rendering engine ang ginagamit ng bisita. Ang mga label ay naglalaman ng mga pangalan ng mga engines na sinusundan ng mga pang-karaniwang browser na gumagamit ng parehas na engine alinsunod sa mga sumusunod na nasa brackets.", + "BrowserFamily": "Pamilya ng browser", + "BrowserVersion": "bersyon ng browser", + "BrowserVersions": "mga bersyon ng browser", + "Browsers": "Mga Browser", + "Camera": "Camera", + "CarBrowser": "Car browser", + "ColumnBrowser": "Browser", + "Console": "Console", + "Device": "Aparato", + "DeviceBrand": "Brand ng device", + "DeviceDetection": "Pag detect ng device", + "DeviceModel": "Modelo ng device", + "DeviceType": "Uri ng device", + "Devices": "Mga Device", + "DevicesDetection": "Device ng Bisita", + "FeaturePhone": "Feature phone", + "OperatingSystemFamilies": "Mga pamilya ng Operating System", + "OperatingSystemFamily": "Pamilya ng Operating system", + "OperatingSystemVersions": "Mga bersyon ng Operating System", + "OperatingSystems": "Operating systems", + "SmartDisplay": "Smart display", + "Smartphone": "Smartphone", + "TV": "Tv", + "Tablet": "Tableta", + "UserAgent": "User-Agent", + "WidgetBrowserVersionsDocumentation": "Ang ulat na ito ay naglalaman ng mga impormasyon tungkol sa kung anong browser ang gamit ng iyong bisita. Bawat browser bersyon ay magkakahiwalay na naka-lista.", + "WidgetBrowsers": "Browser ng Bisita", + "dataTableLabelBrands": "Tatak", + "dataTableLabelModels": "Modelo", + "dataTableLabelSystemVersion": "Bersyon ng Operating System", + "dataTableLabelTypes": "Uri" + } +} diff --git a/plugins/DevicesDetection/lang/tr.json b/plugins/DevicesDetection/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..c4505bef9dd422d9103a2905834628db0811cb00 --- /dev/null +++ b/plugins/DevicesDetection/lang/tr.json @@ -0,0 +1,77 @@ +{ + "DevicesDetection": { + "BotDetected": "Kullanıcı uygulaması %1$s olarak algılandı. Bu kullanıcı uygulaması ile yapılan ziyaretler Matomo tarafından varsayılan olarak izlenmeyecek.", + "BrowserCode": "Tarayıcı kodu", + "BrowserEngine": "Tarayıcı motoru", + "BrowserEngineDocumentation": "Bu rapor ziyaretçilerin tarayıcılarının tarayıcı motorlarına göre dağılımını görüntüler. %s Site tasarımcıları için en önemli bilgi ziyaretçilerin kullandığı görüntüleme motorudur. Etiketlerde motorların adı ve yanında köşeli parantez içinde bu motoru en çok kullanan tarayıcı görüntülenir.", + "BrowserEngines": "Tarayıcı motorları", + "BrowserFamily": "Tarayıcı ailesi", + "BrowserVersion": "Tarayıcı sürümü", + "BrowserVersions": "Tarayıcı sürümleri", + "Browsers": "Tarayıcılar", + "Camera": "Kamera", + "CarBrowser": "Araç tarayıcısı", + "ClientHints": "İstemci ipuçları", + "ClientHintsNotSupported": "Tarayıcınız istemci ipuçlarını desteklemiyor.", + "ClientType": "İstemci türü", + "ClientTypes": "İstemci türleri", + "ColumnBrowser": "Tarayıcı", + "ColumnOperatingSystem": "İşletim sistemi", + "ColumnOperatingSystemVersion": "İşletim sistemi sürümü", + "ConsiderClientHints": "İstemci ipuçları değerlendirilsin", + "Console": "Konsol", + "Device": "Aygıt", + "DeviceBrand": "Aygıt markası", + "DeviceBrandReportDocumentation": "Bu rapor, ziyaretçilerin kullandığı aygıtlarını markalarını / üreticilerini içerir. Çoğu durumda bu bilgiler yalnızca masaüstü olmayan aygıtlar için kullanılabilir.", + "DeviceBrands": "Aygıt markaları", + "DeviceDetection": "Aygıt algılama", + "DeviceModel": "Aygıt modeli", + "DeviceModelDetectionDisabled": "Aygıt modeli algılaması kapalı", + "DeviceModelDetectionDisabledRequirementNote": "Aygıt modeli algılaması ve aygıt modeli bildirimi kapalı olmalıdır.", + "DeviceModelReportDisabledByCompliancePolicy": "Aygıt modeli bildirimi uyumluluk ilkesi ile kapatılmış.", + "DeviceModelReportDocumentation": "Bu rapor, ziyaretçilerin kullandığı aygıtları içerir. Bazı model adları birden çok marka tarafından kullanıldığından, her model aygıt markasıyla birlikte görüntülenir.", + "DeviceModels": "Aygıt modelleri", + "DeviceType": "Aygıt türü", + "DeviceTypeReportDocumentation": "Bu rapor, ziyaretçilerin kullandığı aygıt türlerini içerir. Belirli bir aygıt türünde ziyaret olmasa da, Matomo tarafından algılanabilen tüm aygıt türleri her zaman görüntülenir.", + "DeviceTypes": "Aygıt türleri", + "Devices": "Aygıtlar", + "DevicesDetection": "Ziyaretçi aygıtları", + "FeaturePhone": "Özellikli telefon", + "FeedReader": "Akış okuyucu", + "GenericDevice": "Genel %s", + "Library": "Kitaplık", + "MediaPlayer": "Ortam oynatıcı", + "MobileApp": "Mobil uygulama", + "MobileDevice": "Mobil aygıt", + "OnlyMajorVersionsSettingRequirementNote": "Yalnızca büyük işletim sistemi ve tarayıcı sürümleri tutulur.", + "OnlyMajorVersionsSettingTitle": "Yalnızca büyük sürümler", + "OperatingSystemCode": "İşletim sistemi kodu", + "OperatingSystemFamilies": "İşletim sistemi aileleri", + "OperatingSystemFamiliesReportDocumentation": "Bu rapor, ziyaretçilerin kullandığı işletim sistemlerini, işletim sistemi ailesine göre gruplandırılmış olarak içerir. Bir işletim sisteminin farklı sürümleri ya da dağıtımları işletim sisteminin ailesini oluşturur.", + "OperatingSystemFamily": "İşletim sistemi ailesi", + "OperatingSystemVersions": "İşletim sistemi sürümleri", + "OperatingSystemVersionsReportDocumentation": "Bu rapor, ziyaretçilerin kullandığı işletim sistemlerini içerir. Her sürüm ve dağıtım ayrı görüntülenir.", + "OperatingSystems": "İşletim sistemleri", + "Peripheral": "Çevre", + "Phablet": "Phablet", + "Pim": "Kişisel bilgi yöneticisi", + "PluginDescription": "Marka (Üretici), model (aygıt sürümü), aygıt türü (tv, oyun konsolu, akıllı telefon, masaüstü gibi) özellikleri içeren kullanıcı aygıtları ile ilgili ek bilgileri içerir.", + "PortableMediaPlayer": "Taşınabilir ortam oynatıcısı", + "SmartDisplay": "Akıllı görüntüleyici", + "SmartSpeaker": "Akıllı hoparlör", + "Smartphone": "Akıllı telefon", + "Software": "Yazılım", + "TV": "TV", + "Tablet": "Tablet", + "UserAgent": "User-Agent", + "Wearable": "Giyilebilir", + "WidgetBrowserVersionsDocumentation": "Bu rapor ziyaretçilerin kullandığı tarayıcı türlerini içerir. Her tarayıcı sürümü ayrı olarak listelenmiştir.", + "WidgetBrowsers": "Ziyaretçi tarayıcısı", + "WidgetBrowsersDocumentation": "Bu rapor ziyaretçilerin kullandığı tarayıcı türlerini içerir.", + "XVisitsFromDevices": "%2$s aygıttan %1$s ziyaret", + "dataTableLabelBrands": "Marka", + "dataTableLabelModels": "Model", + "dataTableLabelSystemVersion": "İşletim sistemi sürümü", + "dataTableLabelTypes": "Tür" + } +} diff --git a/plugins/DevicesDetection/lang/tzm.json b/plugins/DevicesDetection/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..d91dc6bf4ea92a3e408d56912000455f016aa6c7 --- /dev/null +++ b/plugins/DevicesDetection/lang/tzm.json @@ -0,0 +1,5 @@ +{ + "DevicesDetection": { + "dataTableLabelTypes": "ⴰⵏⴰⵡ" + } +} diff --git a/plugins/DevicesDetection/lang/uk.json b/plugins/DevicesDetection/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..cf9d33f5f48b07336de54f899087782a3c159b07 --- /dev/null +++ b/plugins/DevicesDetection/lang/uk.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "Цей User-Agent виявлено як %1$s. Відвідування з цим агентом користувача не буде відстежено в Matomo за замовчуванням.", + "BrowserCode": "Код браузера", + "BrowserEngine": "Движок браузера", + "BrowserEngineDocumentation": "Цей графік показує відомості про браузери відвідувачів, розбиту на сімейства. %s Найважливіша інформація для веброзробників — на якому рушії працюють браузери відвідувачів. Мітки містять назву рушія залежно від найпопулярнішого браузера, який працює на ньому.", + "BrowserEngines": "Браузерні движки", + "BrowserFamily": "Сімейство браузерів", + "BrowserVersion": "Версія браузера", + "BrowserVersions": "Версії браузерів", + "Browsers": "По браузерам", + "Camera": "Камера", + "CarBrowser": "Автомобільний браузер", + "ClientHints": "Підказки для клієнта", + "ClientHintsNotSupported": "Ваш браузер не підтримує підказки клієнтів.", + "ClientType": "Тип клієнта", + "ClientTypes": "Типи клієнтів", + "ColumnBrowser": "Браузер", + "ColumnOperatingSystem": "Операційна система", + "ColumnOperatingSystemVersion": "Версія операційної системи", + "ConsiderClientHints": "Враховувати підказки клієнта", + "Console": "Консоль", + "Device": "Пристрій", + "DeviceBrand": "Фірма-виробник пристрою", + "DeviceBrandReportDocumentation": "Цей звіт показує бренди / виробники пристроїв, якими користувались ваші відвідувачі. У більшості випадків ця інформація доступна лише для ненастільних пристроїв.", + "DeviceBrands": "Виробник пристрою", + "DeviceDetection": "Визначення пристроїв", + "DeviceModel": "Модель пристрою", + "DeviceModelReportDocumentation": "Цей звіт показує пристрої, якими користуються ваші відвідувачі. Кожна модель відображається в поєднанні з брендом пристрою, оскільки деякі назви моделей використовуються кількома марками.", + "DeviceModels": "Модель пристрою", + "DeviceType": "Тип пристрою", + "DeviceTypeReportDocumentation": "Цей звіт показує типи пристроїв, якими користувалися ваші відвідувачі. У цьому звіті завжди відображатимуться всі типи пристроїв, які може виявити Matomo, навіть якщо не було відвідувань певного типу.", + "DeviceTypes": "Тип пристрою", + "Devices": "Пристрої", + "DevicesDetection": "Пристрої відвідувачів", + "FeaturePhone": "Особливість телефону", + "FeedReader": "Читач каналів", + "GenericDevice": "Загальний %s", + "Library": "Бібліотека", + "MediaPlayer": "Медіаплеєр", + "MobileApp": "Мобільний Застосунок", + "MobileDevice": "Мобільний пристрій", + "OperatingSystemCode": "Код операційної системи", + "OperatingSystemFamilies": "Сімейства операційних систем", + "OperatingSystemFamiliesReportDocumentation": "У цьому звіті показано операційні системи, які використовують ваші відвідувачі, згруповані за сімейством операційних систем. Сімейство операційних систем складається з різних версій або дистрибутивів.", + "OperatingSystemFamily": "Тип операційної системи", + "OperatingSystemVersions": "Версії операційних систем", + "OperatingSystemVersionsReportDocumentation": "Цей звіт показує операційні системи, якими користуються відвідувачі. Кожна версія та дистрибутив показані окремо.", + "OperatingSystems": "По операційним системам", + "Peripheral": "Периферійний", + "Phablet": "Фаблет", + "Pim": "PIM", + "PluginDescription": "Надає розширену інформацію про пристрої користувача: бренд (виробник), модель (версія пристрою), тип пристрою (тв, консолі, смартфони, робоча станція та ін.) та ін.", + "PortableMediaPlayer": "Портативний медіа-плеєр", + "SmartDisplay": "Смарт-дисплей", + "SmartSpeaker": "Розумна колонка", + "Smartphone": "Смартфон", + "Software": "Програми", + "TV": "ТВ", + "Tablet": "Планшет", + "UserAgent": "User-Agent", + "Wearable": "Можна носити", + "WidgetBrowserVersionsDocumentation": "Цей звіт містить інформацію по браузерам, які використовують ваші відвідувачі. Кожна версія браузера відображається окремо.", + "WidgetBrowsers": "Браузери відвідувачів", + "WidgetBrowsersDocumentation": "Цей звіт містить інформацію про те, який браузер використовували ваші відвідувачі.", + "XVisitsFromDevices": "%1$s візитів з %2$s пристроїв", + "dataTableLabelBrands": "Фірма виробник", + "dataTableLabelModels": "Модель", + "dataTableLabelSystemVersion": "Версія операційної системи", + "dataTableLabelTypes": "Тип" + } +} diff --git a/plugins/DevicesDetection/lang/ur.json b/plugins/DevicesDetection/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/DevicesDetection/lang/ur.json @@ -0,0 +1 @@ +{} diff --git a/plugins/DevicesDetection/lang/vi.json b/plugins/DevicesDetection/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..eeb424c94cd7d48f2ee7ea4a70059b49c446872a --- /dev/null +++ b/plugins/DevicesDetection/lang/vi.json @@ -0,0 +1,16 @@ +{ + "DevicesDetection": { + "BrowserEngine": "Trình duyệt", + "BrowserEngineDocumentation": "Biểu đồ này cho thấy các trình duyệt khách truy cập của bạn chia thành các họ trình duyệt. %s Thông tin quan trọng nhất đối với nhà phát triển web là loại công cụ rendering nào khách truy cập của họ đang sử dụng. Các nhãn chứa tên của các công cụ tiếp theo của trình duyệt phổ biến nhất đang sử dụng công cụ cho trong giấu ngoặc đơn.", + "BrowserFamily": "Họ trình duyệt", + "BrowserVersion": "Phiên bản trình duyệt", + "Browsers": "Các trình duyệt", + "ColumnBrowser": "Trình duyệt", + "ColumnOperatingSystem": "Hệ điều hành", + "OperatingSystemFamily": "Hệ điều hành gia đình", + "OperatingSystems": "Các hệ điều hành", + "WidgetBrowserVersionsDocumentation": "Báo cáo này chứa thông tin về loại trình duyệt mà khách truy cập của bạn sử dụng. Mỗi phiên bản trình duyệt được liệt kê một cách riêng biệt.", + "WidgetBrowsers": "Trình duyệt khách truy cập", + "dataTableLabelTypes": "Kiểu" + } +} diff --git a/plugins/DevicesDetection/lang/zh-cn.json b/plugins/DevicesDetection/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..8517ff55a7aab867e468145755ce869e63dac0b6 --- /dev/null +++ b/plugins/DevicesDetection/lang/zh-cn.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "此User-Agent被检测为%1$s。 默认情况下,Matomo中不会跟踪此用户代理的默认访问。", + "BrowserCode": "浏览器代码", + "BrowserEngine": "浏览器引擎", + "BrowserEngineDocumentation": "此报表展示访客使用的浏览器核心。%s对网站开发者来说最重要的就是访客使用何种渲染引擎。标签包括引擎名称以及在括号中标注的此引擎最常见浏览器。", + "BrowserEngines": "浏览器引擎", + "BrowserFamily": "浏览器家族", + "BrowserVersion": "浏览器版本", + "BrowserVersions": "浏览器版本", + "Browsers": "浏览器", + "Camera": "摄像头", + "CarBrowser": "车用浏览器", + "ClientHints": "客户端提示", + "ClientHintsNotSupported": "您的浏览器不支持客户端提示。", + "ClientType": "客户类型", + "ClientTypes": "客户类型", + "ColumnBrowser": "浏览器", + "ColumnOperatingSystem": "操作系统", + "ColumnOperatingSystemVersion": "操作系统版本", + "ConsiderClientHints": "考虑客户端提示", + "Console": "控制台", + "Device": "设备", + "DeviceBrand": "设备品牌", + "DeviceBrandReportDocumentation": "该报告展示了您的访问者所使用设备的品牌/制造商。在大多数情况下,这些信息只适用于非桌面设备。", + "DeviceBrands": "设备品牌", + "DeviceDetection": "设备检测", + "DeviceModel": "设备型号", + "DeviceModelReportDocumentation": "该报告展示了您的访问者正在使用的设备。由于某些型号的名称被多个品牌使用,所以设备型号与品牌一起显示。", + "DeviceModels": "设备型号", + "DeviceType": "设备类型", + "DeviceTypeReportDocumentation": "该报告展示了您的访问者所使用的设备类型。该报告将始终显示Matomo能够检测到的所有设备类型,即使没有特定类型的访问。", + "DeviceTypes": "设备类型", + "Devices": "设备", + "DevicesDetection": "访客设备", + "FeaturePhone": "功能手机", + "FeedReader": "提要阅读器", + "GenericDevice": "通用%s", + "Library": "库", + "MediaPlayer": "媒体播放器", + "MobileApp": "移动应用程序", + "MobileDevice": "移动设备", + "OperatingSystemCode": "操作系统代码", + "OperatingSystemFamilies": "操作系统家族", + "OperatingSystemFamiliesReportDocumentation": "该报告展示了您的访客正在使用的操作系统,并按操作系统系列进行分类.操作系统系列由不同版本或发行版组成。", + "OperatingSystemFamily": "操作系统家族", + "OperatingSystemVersions": "操作系统版本", + "OperatingSystemVersionsReportDocumentation": "该报告展示了您的访问者正在使用的操作系统。每个版本和发行版都会单独显示。", + "OperatingSystems": "操作系统", + "Peripheral": "外围设备", + "Phablet": "平板手机", + "Pim": "PIM", + "PluginDescription": "提供使用者设备的品牌(制造商)、型号(版本)、类型(电视、手机或电脑等)以及更多扩展信息。", + "PortableMediaPlayer": "便携媒体播放器", + "SmartDisplay": "智能显示器", + "SmartSpeaker": "智能音箱", + "Smartphone": "智能手机", + "Software": "软件", + "TV": "TV", + "Tablet": "平板电脑", + "UserAgent": "用户代理(User-Agent)", + "Wearable": "适合的", + "WidgetBrowserVersionsDocumentation": "此报表包含访客所用的浏览器信息,每个浏览器版本单独列出。", + "WidgetBrowsers": "访客浏览器", + "WidgetBrowsersDocumentation": "该报告包含您的访问者所使用的浏览器类型。", + "XVisitsFromDevices": "来自%2$s设备的%1$s次访问", + "dataTableLabelBrands": "品牌", + "dataTableLabelModels": "型号", + "dataTableLabelSystemVersion": "操作系统版本", + "dataTableLabelTypes": "类型" + } +} diff --git a/plugins/DevicesDetection/lang/zh-tw.json b/plugins/DevicesDetection/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..625d5dfa78596824819953ebdaa2e0128fdc4090 --- /dev/null +++ b/plugins/DevicesDetection/lang/zh-tw.json @@ -0,0 +1,72 @@ +{ + "DevicesDetection": { + "BotDetected": "此 User Agent 被判斷為 %1$s。 來自此 User Agent 的訪問預設將不會被 Matomo 記錄。", + "BrowserCode": "瀏覽器代號", + "BrowserEngine": "瀏覽器核心", + "BrowserEngineDocumentation": "這份報表顯示訪客所使用瀏覽器的核心。%s 對於網頁開發者來說了解使用者的瀏覽器核心是最重要事情。所有瀏覽器核心的名稱後方都會標示出該核心中最廣泛運用的瀏覽器。", + "BrowserEngines": "瀏覽器核心", + "BrowserFamily": "瀏覽器家族", + "BrowserVersion": "瀏覽器版本", + "BrowserVersions": "瀏覽器版本", + "Browsers": "使用瀏覽器", + "Camera": "相機", + "CarBrowser": "車用顯示器", + "ClientHints": "客戶端提示", + "ClientHintsNotSupported": "你的瀏覽器不支援客戶端提示。", + "ClientType": "使用者端類型", + "ClientTypes": "使用者端類型", + "ColumnBrowser": "瀏覽器", + "ColumnOperatingSystem": "作業系統", + "ColumnOperatingSystemVersion": "作業系統版本", + "ConsiderClientHints": "參考客戶端提示", + "Console": "遊戲主機", + "Device": "裝置", + "DeviceBrand": "裝置品牌", + "DeviceBrandReportDocumentation": "這份報表顯示你的訪客所使用的裝置品牌/製造商。通常,只有桌上型電腦以外的裝置才會顯示此資訊。", + "DeviceBrands": "裝置品牌", + "DeviceDetection": "裝置偵測", + "DeviceModel": "裝置型號", + "DeviceModelReportDocumentation": "這份報表顯示你的訪客所使用的裝置。每個裝置的型號與品牌會一起顯示,因為有些品牌使用了同名的型號。", + "DeviceModels": "裝置型號", + "DeviceType": "裝置類型", + "DeviceTypeReportDocumentation": "這份報表顯示你的訪客所使用的裝置。此報表會永遠顯示 Matomo 能偵測到的所有裝置類型,即使某些裝置沒有任何訪客使用過。", + "DeviceTypes": "裝置類型", + "Devices": "裝置", + "DevicesDetection": "訪客裝置", + "FeaturePhone": "功能型手機", + "FeedReader": "聚合器", + "GenericDevice": "通用 %s", + "Library": "函式庫", + "MediaPlayer": "媒體播放器", + "MobileApp": "行動應用程式", + "MobileDevice": "行動裝置", + "OperatingSystemCode": "作業系統代號", + "OperatingSystemFamilies": "作業系統家族", + "OperatingSystemFamiliesReportDocumentation": "這份報表顯示你的訪客所使用的作業系統,以作業系統家族分類。同一作業系統家族包含了不同的版本號碼或是發行版。", + "OperatingSystemFamily": "作業系統家族", + "OperatingSystemVersions": "作業系統版本", + "OperatingSystemVersionsReportDocumentation": "這份報表顯示你的訪客所使用的作業系統資訊。每個版本和發行版都會分開顯示。", + "OperatingSystems": "作業系統", + "Peripheral": "外接設備", + "Phablet": "平板手機", + "Pim": "個人資訊管理軟體", + "PluginDescription": "提供使用者裝置的更多資訊,如品牌(製造商)、型號(版本)、類型(電視、手機或電腦等等)及更多。", + "PortableMediaPlayer": "可攜式媒體播放器", + "SmartDisplay": "智慧型電視", + "SmartSpeaker": "智慧音箱", + "Smartphone": "智慧型手機", + "Software": "軟體", + "TV": "電視", + "Tablet": "平板電腦", + "UserAgent": "User Agent", + "Wearable": "穿戴式裝置", + "WidgetBrowserVersionsDocumentation": "這份報表包含訪客所使用的瀏覽器資訊。每個瀏覽器的版本都會分開排列。", + "WidgetBrowsers": "訪客瀏覽器", + "WidgetBrowsersDocumentation": "這份報表包含訪客所使用的瀏覽器種類。", + "XVisitsFromDevices": "來自%2$s裝置的%1$s個訪問數", + "dataTableLabelBrands": "品牌", + "dataTableLabelModels": "型號", + "dataTableLabelSystemVersion": "作業系統版本", + "dataTableLabelTypes": "類型" + } +} diff --git a/plugins/DevicesDetection/templates/_profileSummary.twig b/plugins/DevicesDetection/templates/_profileSummary.twig new file mode 100644 index 0000000000000000000000000000000000000000..b1243026845df837dc298b3ff9b7776c04c514f5 --- /dev/null +++ b/plugins/DevicesDetection/templates/_profileSummary.twig @@ -0,0 +1,21 @@ +{% if visitorData.devices is defined %} +
    +

    {{ 'DevicesDetection_Devices'|translate }}

    +
    + {%- for entry in visitorData.devices -%} +

    + + {% if entry.devices|length == 1 and 'General_Unknown'|translate in entry.devices[0].name %} + {{ 'DevicesDetection_XVisitsFromDevices'|translate('' ~ entry.count ~ '', '' ~ entry.type ~ '')|raw }} + {% else %} + {{ 'DevicesDetection_XVisitsFromDevices'|translate('' ~ entry.count ~ '', '' ~ entry.type ~ '')|raw }}: + {% for device in entry.devices -%} + {{ device.name }} ({{ device.count }}x){% if not loop.last %}, {% endif %} + {%- endfor -%} + + {% endif %} +

    + {%- endfor -%} +
    +
    +{% endif %} \ No newline at end of file diff --git a/plugins/DevicesDetection/templates/detection.twig b/plugins/DevicesDetection/templates/detection.twig new file mode 100644 index 0000000000000000000000000000000000000000..fbe384117fe0aea748f701a6ae5a08b30b51fb14 --- /dev/null +++ b/plugins/DevicesDetection/templates/detection.twig @@ -0,0 +1,29 @@ +{% extends 'admin.twig' %} + +{% set title %}{{ 'DevicesDetection_DeviceDetection'|translate }}{% endset %} + +{% block content %} + +
    + +{% endblock %} diff --git a/plugins/DevicesDetection/templates/list.twig b/plugins/DevicesDetection/templates/list.twig new file mode 100644 index 0000000000000000000000000000000000000000..061903047450f08bf6ae4d11d2590ae2200a64c1 --- /dev/null +++ b/plugins/DevicesDetection/templates/list.twig @@ -0,0 +1,7 @@ + + {% for name,image in itemList %} + + + + {% endfor %} +
    {{ name }}
    diff --git a/plugins/DevicesDetection/tests/Fixtures/MultiDeviceGoalConversions.php b/plugins/DevicesDetection/tests/Fixtures/MultiDeviceGoalConversions.php new file mode 100644 index 0000000000000000000000000000000000000000..019195149490a4397e89a2c200e7c41af08cd2dc --- /dev/null +++ b/plugins/DevicesDetection/tests/Fixtures/MultiDeviceGoalConversions.php @@ -0,0 +1,219 @@ +setUpWebsitesAndGoals(); + $this->trackSmartphoneVisits(); + $this->trackTabletVisits(); + $this->trackOtherVisits(); + $this->trackAdditionalSiteVisits(); + } + + public function tearDown(): void + { + // empty + } + + private function setUpWebsitesAndGoals() + { + if (!self::siteCreated($idSite = 1)) { + self::createWebsite($this->dateTime, $ecommerce = 1); + } + + if (!self::siteCreated($idSite = 2)) { + self::createWebsite($this->dateTime, $ecommerce = 0); + } + + if (!self::goalExists($idSite = 1, $idGoal = 1)) { + API::getInstance()->addGoal( + $this->idSite, + 'Goal 1 - Thank you', + 'title', + 'Thank you', + 'contains', + $caseSensitive = false, + $revenue = 10, + $allowMultipleConversions = 1 + ); + } + } + + private function trackSmartphoneVisits() + { + // first visit (with conversion) + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + + $t->setUserAgent('Mozilla/5.0 (Linux; Android 4.2.2; HTC Butterfly Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36'); + + $t->setUrl('http://example.org/index.htm'); + self::checkResponse($t->doTrackPageView('0')); + + $t->setForceVisitDateTime($this->getAdjustedDateTime(0.3)); + + self::checkResponse($t->doTrackGoal($this->idGoal, $revenue = 42.256)); + + // second visit (without conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(0.2), $defaultInit = true); + + $t->setUserAgent('Mozilla/5.0 (Linux; Android 4.2.2; HTC Butterfly Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36'); + + $t->setUrl('http://example.org/anypage.php'); + self::checkResponse($t->doTrackPageView('mine')); + + + // third visit (with conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(0.2), $defaultInit = true); + + $t->setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D167 iPhone6,1/N51AP Zite/2.6'); + + $t->setUrl('http://example.org/anypage.php'); + self::checkResponse($t->doTrackPageView('mine')); + + $t->setForceVisitDateTime($this->getAdjustedDateTime(1)); + + self::checkResponse($t->doTrackGoal($this->idGoal, $revenue = 0)); + } + + private function trackTabletVisits() + { + // first visit (with conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(1), $defaultInit = true); + + $t->setUserAgent('Mozilla/5.0 (iPad; CPU OS 6_0_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) CriOS/31.0.1650.18 Mobile/10A523 Safari/8536.25'); + + $t->setUrl('http://example.org/index.htm'); + self::checkResponse($t->doTrackPageView('0')); + + $t->setForceVisitDateTime($this->getAdjustedDateTime(1.6)); + + self::checkResponse($t->doTrackGoal($this->idGoal, $revenue = 42.256)); + + // second visit (without conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(0.6), $defaultInit = true); + + $t->setUserAgent('Mozilla/5.0 (Linux; Android 4.2.2; SM-T310 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.136 Safari/537.36'); + + $t->setUrl('http://example.org/index.htm'); + self::checkResponse($t->doTrackPageView('0')); + + + // third visit (with conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(1.6), $defaultInit = true); + + $t->setUserAgent('Mozilla/5.0 (Linux; U; Android 2.3;en-us; ViewSonic-ViewPad7e build/ERE27) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1'); + + $t->setUrl('http://example.org/anypage.php'); + self::checkResponse($t->doTrackPageView('mine')); + + $t->setForceVisitDateTime($this->getAdjustedDateTime(1.9)); + + self::checkResponse($t->doTrackGoal($this->idGoal, $revenue = 0)); + } + + private function trackOtherVisits() + { + // unknown device visit (with conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(1), $defaultInit = true); + + $t->setUserAgent('not detectable'); + + $t->setUrl('http://example.org/anypage.php'); + self::checkResponse($t->doTrackPageView('mine')); + + $t->setForceVisitDateTime($this->getAdjustedDateTime(4)); + + self::checkResponse($t->doTrackGoal($this->idGoal, $revenue = 42.256)); + + // tv visit (without conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(3), $defaultInit = true); + + $t->setUserAgent('WebKit/3.7.6, (CE-HTML/1.0 NETTV/3.3.0 NewB) PHILIPS-AVM-2013/2.19 (Philips, BDP5600, wired)'); + + $t->setUrl('http://example.org/anypage.php'); + self::checkResponse($t->doTrackPageView('mine')); + + + // feature phone visit (with conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(4), $defaultInit = true); + + $t->setUserAgent('Fly_DS123/Q03C_MAUI_Browser/MIDP2.0 Configuration/CLDC-1.1'); + + $t->setUrl('http://example.org/index.htm'); + self::checkResponse($t->doTrackPageView('0')); + + $t->setForceVisitDateTime($this->getAdjustedDateTime(4.2)); + + self::checkResponse($t->doTrackGoal($this->idGoal, $revenue = 0)); + + + // desktop visit (with conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(1.6), $defaultInit = true); + + $t->setUserAgent('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; Banca Caboto s.p.a.; rv:11.0) like Gecko'); + // The client hints below should change the OS to Windows 11 and browser to Edge 95.5.2 + $t->setClientHints('', 'Windows', '14.0.0', '" Not A;Brand";v="99", "Chromium";v="95", "Microsoft Edge";v="95"', '95.5.2'); + + $t->setUrl('http://example.org/index.htm'); + self::checkResponse($t->doTrackPageView('0')); + + $t->setForceVisitDateTime($this->getAdjustedDateTime(1.9)); + + self::checkResponse($t->doTrackGoal($this->idGoal, $revenue = 0)); + + // car browser visit (without conversion) + $t = self::getTracker($this->idSite, $this->getAdjustedDateTime(1.8), $defaultInit = true); + + $t->setUserAgent('Some Unknown UA'); + // The client hints below should change the OS to Android, browser to Chrome 95.5.2 and device type to car browser + $t->setClientHints( + 'UltraOcta-T8', + 'Android', + '14.0.0', + '" Not A;Brand";v="99", "Chromium";v="95"', + '95.5.2', + '"Tablet", "Automotive"' + ); + + $t->setUrl('http://example.org/index.htm'); + self::checkResponse($t->doTrackPageView('0')); + } + + private function trackAdditionalSiteVisits() + { + $t = self::getTracker($this->idSite2, $this->getAdjustedDateTime(2), $defaultInit = true); + + $t->setUserAgent('Mozilla/5.0 (Linux; Android 4.4.2; SM-G900F Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36'); + + $t->setUrl('http://example-two.org/index.htm'); + self::checkResponse($t->doTrackPageView('site-two')); + } + + + private function getAdjustedDateTime($addition) + { + return Date::factory($this->dateTime)->addHour($addition)->getDatetime(); + } +} diff --git a/plugins/DevicesDetection/tests/System/GoalReportForDevicesTest.php b/plugins/DevicesDetection/tests/System/GoalReportForDevicesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ec436d722747406c288484e63654ef833faf898f --- /dev/null +++ b/plugins/DevicesDetection/tests/System/GoalReportForDevicesTest.php @@ -0,0 +1,197 @@ + + */ + private function getModelLabelsForSiteRequest(string $idSite): array + { + /** @var DataTable|DataTable\Map $report */ + $report = Request::processRequest('DevicesDetection.getModel', [ + 'idSite' => $idSite, + 'period' => 'day', + 'date' => self::$fixture->dateTime, + 'flat' => '1', + ]); + + return array_values($report->getColumn('label')); + } + + /** + * @return list + */ + private function getAvailableReportsForSiteRequest(string $idSite): array + { + $reports = Request::processRequest('API.getReportMetadata', [ + 'idSite' => $idSite, + 'period' => 'day', + 'date' => self::$fixture->dateTime, + ]); + + return array_map(static function (array $report): string { + return $report['module'] . '.' . $report['action']; + }, $reports); + } + + public function getApiForTesting() + { + $idSite = self::$fixture->idSite; + $dateTime = self::$fixture->dateTime; + + return [ + ['DevicesDetection.getType', ['idSite' => $idSite, 'date' => $dateTime]], + ['DevicesDetection.getOsVersions', ['idSite' => $idSite, 'date' => $dateTime]], + ['DevicesDetection.getBrand', ['idSite' => $idSite, 'date' => $dateTime]], + ['DevicesDetection.getModel', ['idSite' => $idSite, 'date' => $dateTime]], + ]; + } + + /** + * @dataProvider getApiForTesting + */ + public function testApi($api, $params) + { + $this->runApiTests($api, $params); + } + + public function testGetModelDoesNotReturnDataWhenPolicyEnforced(): void + { + CnilPolicy::setActiveStatus(null, true); + + $this->runApiTests('DevicesDetection.getModel', [ + 'idSite' => self::$fixture->idSite, + 'date' => self::$fixture->dateTime, + 'testSuffix' => 'compliancePolicyEnforcedSystem', + ]); + + CnilPolicy::setActiveStatus(null, false); + } + + public function testGetModelReportMetadataHidesOnlyRelevantReportWhenPolicyEnabledGlobally(): void + { + CnilPolicy::setActiveStatus(null, true); + + try { + $availableReports = $this->getAvailableReportsForSiteRequest((string) self::$fixture->idSite); + + $this->assertContains('DevicesDetection.getType', $availableReports); + $this->assertContains('DevicesDetection.getBrand', $availableReports); + $this->assertContains('DevicesDetection.getOsVersions', $availableReports); + $this->assertNotContains('DevicesDetection.getModel', $availableReports); + } finally { + CnilPolicy::setActiveStatus(null, false); + } + } + + public function testGetModelReturnsOnlyAllowedSitesForSpecificSiteList(): void + { + $this->setSiteCompliancePolicy(self::$fixture->idSite, true); + + try { + $this->assertSame( + ['Samsung - Galaxy S5'], + $this->getModelLabelsForSiteRequest(self::$fixture->idSite . ',' . self::$fixture->idSite2) + ); + } finally { + $this->setSiteCompliancePolicy(self::$fixture->idSite, false); + } + } + + public function testGetModelReturnsOnlyAllowedSitesForAll(): void + { + $this->setSiteCompliancePolicy(self::$fixture->idSite, true); + + try { + $this->assertSame(['Samsung - Galaxy S5'], $this->getModelLabelsForSiteRequest('all')); + } finally { + $this->setSiteCompliancePolicy(self::$fixture->idSite, false); + } + } + + public function testGetModelReturnsErrorWhenSingleRequestedSiteIsDisallowed(): void + { + $this->setSiteCompliancePolicy(self::$fixture->idSite, true); + + try { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Device model report is disabled by compliance policy.'); + + $this->getModelLabelsForSiteRequest((string) self::$fixture->idSite); + } finally { + $this->setSiteCompliancePolicy(self::$fixture->idSite, false); + } + } + + public function testGetModelReturnsErrorWhenAllRequestedSitesAreDisallowedForSpecificSiteList(): void + { + $this->setSiteCompliancePolicy(self::$fixture->idSite, true); + $this->setSiteCompliancePolicy(self::$fixture->idSite2, true); + + try { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Device model report is disabled by compliance policy.'); + + $this->getModelLabelsForSiteRequest(self::$fixture->idSite . ',' . self::$fixture->idSite2); + } finally { + $this->setSiteCompliancePolicy(self::$fixture->idSite, false); + $this->setSiteCompliancePolicy(self::$fixture->idSite2, false); + } + } + + public function testGetModelReturnsErrorWhenAllRequestedSitesAreDisallowedForAll(): void + { + $this->setSiteCompliancePolicy(self::$fixture->idSite, true); + $this->setSiteCompliancePolicy(self::$fixture->idSite2, true); + + try { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Device model report is disabled by compliance policy.'); + + $this->getModelLabelsForSiteRequest('all'); + } finally { + $this->setSiteCompliancePolicy(self::$fixture->idSite, false); + $this->setSiteCompliancePolicy(self::$fixture->idSite2, false); + } + } +} + +GoalReportForDevicesTest::$fixture = new MultiDeviceGoalConversions(); diff --git a/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getBrand_day.xml b/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getBrand_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..999a7d15f97a01692d6715940cfe1fd8f151af25 --- /dev/null +++ b/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getBrand_day.xml @@ -0,0 +1,139 @@ + + + + + 2 + 4 + 2 + 0 + 1 + 6 + 4 + 2 + + + 2 + 2 + 52.26 + + + 2 + 52.26 + plugins/Morpheus/icons/dist/brand/Apple.png + deviceBrand==Apple + + + + 3 + 4 + 3 + 0 + 1 + 1087 + 4 + 2 + + + 2 + 2 + 52.26 + + + 2 + 52.26 + plugins/Morpheus/icons/dist/brand/unk.png + deviceBrand==Unknown + + + + 1 + 1 + 1 + 0 + 1 + 724 + 1 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + plugins/Morpheus/icons/dist/brand/Fly.png + deviceBrand==Fly + + + + 1 + 1 + 2 + 0 + 2 + 721 + 0 + 1 + + + 1 + 1 + 42.26 + + + 1 + 42.26 + plugins/Morpheus/icons/dist/brand/HTC.png + deviceBrand==HTC + + + + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + plugins/Morpheus/icons/dist/brand/Philips.png + deviceBrand==Philips + + + + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + plugins/Morpheus/icons/dist/brand/Samsung.png + deviceBrand==Samsung + + + + 1 + 1 + 1 + 0 + 1 + 1084 + 1 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + plugins/Morpheus/icons/dist/brand/ViewSonic.png + deviceBrand==ViewSonic + + \ No newline at end of file diff --git a/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getModel_day.xml b/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getModel_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..1ca3d68b9b2800f78e0b1e136d665e92362fbc5d --- /dev/null +++ b/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getModel_day.xml @@ -0,0 +1,186 @@ + + + + + 1 + 2 + 1 + 0 + 1 + 3 + 2 + 1 + + + 1 + 1 + 42.26 + + + 1 + 42.26 + deviceBrand==Apple;deviceModel==iPad + + + + 1 + 2 + 1 + 0 + 1 + 3 + 2 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + deviceBrand==Apple;deviceModel==iPhone+5S + + + + 1 + 2 + 1 + 0 + 1 + 3 + 2 + 1 + + + 1 + 1 + 42.26 + + + 1 + 42.26 + deviceBrand==Unknown;deviceModel== + + + + 1 + 1 + 1 + 0 + 1 + 724 + 1 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + deviceBrand==Fly;deviceModel==DS123 + + + + 1 + 1 + 1 + 0 + 1 + 1084 + 1 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + deviceBrand==Unknown;deviceModel==generic+desktop + + + + 1 + 1 + 2 + 0 + 2 + 721 + 0 + 1 + + + 1 + 1 + 42.26 + + + 1 + 42.26 + deviceBrand==HTC;deviceModel==Butterfly + + + + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + deviceBrand==Philips;deviceModel==Blu-ray+Player+%28BDP5600%29 + + + + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + deviceBrand==Samsung;deviceModel==Galaxy+Tab+3+8.0%22+WiFi + + + + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + deviceBrand==Unknown;deviceModel==UltraOcta-T8 + + + + 1 + 1 + 1 + 0 + 1 + 1084 + 1 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + deviceBrand==ViewSonic;deviceModel==ViewPad+7e + + \ No newline at end of file diff --git a/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getOsVersions_day.xml b/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getOsVersions_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..ad9428ce691a4f801563342b6ba2181b5bf86725 --- /dev/null +++ b/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getOsVersions_day.xml @@ -0,0 +1,107 @@ + + + + + 2 + 3 + 2 + 0 + 1 + 3 + 3 + 1 + operatingSystemCode==UNK;operatingSystemVersion==UNK + plugins/Morpheus/icons/dist/os/UNK.png + + + + 2 + 2 + 3 + 0 + 2 + 721 + 1 + 1 + operatingSystemCode==AND;operatingSystemVersion==4.2 + plugins/Morpheus/icons/dist/os/AND.png + + + + 1 + 2 + 1 + 0 + 1 + 3 + 2 + 1 + operatingSystemCode==IOS;operatingSystemVersion==6.0 + plugins/Morpheus/icons/dist/os/IOS.png + + + + 1 + 2 + 1 + 0 + 1 + 3 + 2 + 1 + operatingSystemCode==IOS;operatingSystemVersion==7.1 + plugins/Morpheus/icons/dist/os/IOS.png + + + + 1 + 1 + 1 + 0 + 1 + 1084 + 1 + 1 + operatingSystemCode==AND;operatingSystemVersion==2.3 + plugins/Morpheus/icons/dist/os/AND.png + + + + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + operatingSystemCode==AND;operatingSystemVersion==14.0 + plugins/Morpheus/icons/dist/os/AND.png + + + + 1 + 1 + 1 + 0 + 1 + 724 + 1 + 1 + operatingSystemCode==JME;operatingSystemVersion== + plugins/Morpheus/icons/dist/os/UNK.png + + + + 1 + 1 + 1 + 0 + 1 + 1084 + 1 + 1 + operatingSystemCode==WIN;operatingSystemVersion==11 + plugins/Morpheus/icons/dist/os/WIN.png + + \ No newline at end of file diff --git a/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getType_day.xml b/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getType_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..b289ae48a6b9a12556721180a56fe62d2eaf2d80 --- /dev/null +++ b/plugins/DevicesDetection/tests/System/expected/test___DevicesDetection.getType_day.xml @@ -0,0 +1,186 @@ + + + + + 3 + 4 + 3 + 0 + 1 + 1087 + 4 + 2 + + + 2 + 2 + 52.26 + + + 2 + 52.26 + deviceType==tablet + plugins/Morpheus/icons/dist/devices/tablet.png + + + + 2 + 3 + 3 + 0 + 2 + 724 + 2 + 2 + + + 2 + 2 + 52.26 + + + 2 + 52.26 + deviceType==smartphone + plugins/Morpheus/icons/dist/devices/smartphone.png + + + + 1 + 2 + 1 + 0 + 1 + 3 + 2 + 1 + + + 1 + 1 + 42.26 + + + 1 + 42.26 + plugins/Morpheus/icons/dist/devices/unknown.png + + + + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + deviceType==car+browser + plugins/Morpheus/icons/dist/devices/car_browser.png + + + + 1 + 1 + 1 + 0 + 1 + 1084 + 1 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + deviceType==desktop + plugins/Morpheus/icons/dist/devices/desktop.png + + + + 1 + 1 + 1 + 0 + 1 + 724 + 1 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + deviceType==feature+phone + plugins/Morpheus/icons/dist/devices/feature_phone.png + + + + 1 + 1 + 1 + 0 + 1 + 0 + 1 + 0 + deviceType==tv + plugins/Morpheus/icons/dist/devices/tv.png + + + + 0 + deviceType==camera + plugins/Morpheus/icons/dist/devices/camera.png + + + + 0 + deviceType==console + plugins/Morpheus/icons/dist/devices/console.png + + + + 0 + deviceType==peripheral + plugins/Morpheus/icons/dist/devices/peripheral.png + + + + 0 + deviceType==phablet + plugins/Morpheus/icons/dist/devices/phablet.png + + + + 0 + deviceType==portable+media+player + plugins/Morpheus/icons/dist/devices/portable_media_player.png + + + + 0 + deviceType==smart+display + plugins/Morpheus/icons/dist/devices/smart_display.png + + + + 0 + deviceType==smart+speaker + plugins/Morpheus/icons/dist/devices/smart_speaker.png + + + + 0 + deviceType==wearable + plugins/Morpheus/icons/dist/devices/wearable.png + + \ No newline at end of file diff --git a/plugins/DevicesDetection/tests/System/expected/test_compliancePolicyEnforcedSystem__DevicesDetection.getModel_day.xml b/plugins/DevicesDetection/tests/System/expected/test_compliancePolicyEnforcedSystem__DevicesDetection.getModel_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..dcf0d8faada89fd2b92cabe12fb66d34408ff64e --- /dev/null +++ b/plugins/DevicesDetection/tests/System/expected/test_compliancePolicyEnforcedSystem__DevicesDetection.getModel_day.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/plugins/DevicesDetection/vue/dist/DevicesDetection.umd.js b/plugins/DevicesDetection/vue/dist/DevicesDetection.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..0de3a9189e413f7e61029a55c1f63f1d7c6d50d1 --- /dev/null +++ b/plugins/DevicesDetection/vue/dist/DevicesDetection.umd.js @@ -0,0 +1,407 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("vue")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", ], factory); + else if(typeof exports === 'object') + exports["DevicesDetection"] = factory(require("CoreHome"), require("vue")); + else + root["DevicesDetection"] = factory(root["CoreHome"], root["Vue"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/DevicesDetection/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "DetectionPage", function() { return /* reexport */ DetectionPage; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.vue?vue&type=template&id=1e490552 + +const _hoisted_1 = { + class: "detectionPage" +}; +const _hoisted_2 = { + action: "", + method: "POST" +}; +const _hoisted_3 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_4 = { + key: 0, + class: "checkbox-container usech" +}; +const _hoisted_5 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_6 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_7 = ["value"]; +const _hoisted_8 = { + key: 0 +}; +const _hoisted_9 = { + key: 1 +}; +const _hoisted_10 = { + class: "detection" +}; +const _hoisted_11 = ["src"]; +const _hoisted_12 = ["src"]; +const _hoisted_13 = { + class: "detection" +}; +const _hoisted_14 = ["src"]; +const _hoisted_15 = ["src"]; +const _hoisted_16 = { + class: "detection" +}; +const _hoisted_17 = ["src"]; +const _hoisted_18 = ["src"]; +const _hoisted_19 = { + class: "ui-confirm", + id: "deviceDetectionItemList", + ref: "deviceDetectionItemList" +}; +const _hoisted_20 = ["innerHTML"]; +const _hoisted_21 = ["value"]; +function render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + const _directive_content_table = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("content-table"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ContentBlock, { + "content-title": _ctx.translate('DevicesDetection_DeviceDetection') + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("form", _hoisted_2, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_UserAgent')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("textarea", { + name: "ua", + "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => _ctx.userAgentText = $event) + }, null, 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vModelText"], _ctx.userAgentText]]), _hoisted_3, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_ClientHints')), 1), _ctx.isClientHintsSupported ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_4, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("label", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "checkbox", + id: "usech", + "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => _ctx.considerClientHints = $event), + onChange: _cache[2] || (_cache[2] = $event => _ctx.toggleClientHints()) + }, null, 544), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vModelCheckbox"], _ctx.considerClientHints]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_ConsiderClientHints')), 1)])])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.isClientHintsSupported && _ctx.considerClientHints ? Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("textarea", { + key: 1, + name: "clienthints", + style: { + "margin-top": "2em" + }, + "onUpdate:modelValue": _cache[3] || (_cache[3] = $event => _ctx.clientHintsText = $event) + }, null, 512)), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vModelText"], _ctx.clientHintsText]]) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + id: "noclienthints", + class: "alert alert-warning" + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_ClientHintsNotSupported')), 513), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], !_ctx.isClientHintsSupported]]), _hoisted_5, _hoisted_6, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "submit", + value: _ctx.translate('General_Refresh'), + class: "btn" + }, null, 8, _hoisted_7)]), _ctx.bot_info ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("h3", _hoisted_8, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_BotDetected', _ctx.bot_info.name)), 1)) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_9, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_ColumnOperatingSystem')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("table", _hoisted_10, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tbody", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Name')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("small", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" ("), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: "", + onClick: _cache[4] || (_cache[4] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.showList('os'), ["prevent"])) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Mobile_ShowAll')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(") ")])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", { + height: 16, + width: 16, + src: _ctx.os_logo + }, null, 8, _hoisted_11), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.os_name), 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('CorePluginsAdmin_Version')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.os_version), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_OperatingSystemFamily')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("small", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" ("), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: "", + onClick: _cache[5] || (_cache[5] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.showList('osfamilies'), ["prevent"])) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Mobile_ShowAll')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(") ")])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", { + height: 16, + width: 16, + src: _ctx.os_family_logo + }, null, 8, _hoisted_12), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.os_family), 1)])])])])), [[_directive_content_table]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_ColumnBrowser')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("table", _hoisted_13, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tbody", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Name')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("small", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" ("), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: "", + onClick: _cache[6] || (_cache[6] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.showList('browsers'), ["prevent"])) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Mobile_ShowAll')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(") ")])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", { + height: 16, + width: 16, + src: _ctx.browser_logo + }, null, 8, _hoisted_14), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.browser_name), 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('CorePluginsAdmin_Version')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.browser_version), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_BrowserFamily')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("small", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" ("), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: "", + onClick: _cache[7] || (_cache[7] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.showList('browserfamilies'), ["prevent"])) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Mobile_ShowAll')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(") ")])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", { + height: 16, + width: 16, + src: _ctx.browser_family_logo + }, null, 8, _hoisted_15), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.browser_family), 1)])])])])), [[_directive_content_table]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_Device')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("table", _hoisted_16, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tbody", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_dataTableLabelTypes')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("small", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" ("), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: "", + onClick: _cache[8] || (_cache[8] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.showList('devicetypes'), ["prevent"])) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Mobile_ShowAll')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(") ")])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", { + height: 16, + width: 16, + src: _ctx.device_type_logo + }, null, 8, _hoisted_17), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.device_type), 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_dataTableLabelBrands')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("small", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" ("), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: "", + onClick: _cache[9] || (_cache[9] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.showList('brands'), ["prevent"])) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Mobile_ShowAll')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(") ")])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", { + height: 16, + width: 16, + src: _ctx.device_brand_logo + }, null, 8, _hoisted_18), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.device_brand), 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('DevicesDetection_dataTableLabelModels')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.device_model), 1)])])])), [[_directive_content_table]])]))]), + _: 1 + }, 8, ["content-title"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_19, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", { + class: "itemList", + innerHTML: _ctx.$sanitize(_ctx.itemListHtml) + }, null, 8, _hoisted_20), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + role: "close", + type: "button", + value: _ctx.translate('General_Close') + }, null, 8, _hoisted_21)], 512)]); +} +// CONCATENATED MODULE: ./plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.vue?vue&type=template&id=1e490552 + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.vue?vue&type=script&lang=ts + + +function isClientHintsSupported() { + const nav = navigator; // eslint-disable-line @typescript-eslint/no-explicit-any + return nav.userAgentData && typeof nav.userAgentData.getHighEntropyValues === 'function'; +} +let clientHints = null; +function getDefaultClientHints() { + const nav = navigator; // eslint-disable-line @typescript-eslint/no-explicit-any + if (!isClientHintsSupported()) { + return Promise.resolve(null); + } + if (clientHints) { + return Promise.resolve(clientHints); + } + // Initialize with low entropy values that are always available + clientHints = { + brands: nav.userAgentData.brands, + platform: nav.userAgentData.platform + }; + // try to gather high entropy values + // currently this methods simply returns the requested values through a Promise + // In later versions it might require a user permission + return nav.userAgentData.getHighEntropyValues(['brands', 'model', 'platform', 'platformVersion', 'uaFullVersion', 'fullVersionList']).then(ua => { + clientHints = Object.assign({}, ua); + if (clientHints.fullVersionList) { + // if fullVersionList is available, brands and uaFullVersion isn't needed + delete clientHints.brands; + delete clientHints.uaFullVersion; + } + return clientHints; + }); +} +/* harmony default export */ var DetectionPagevue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + userAgent: { + type: String, + required: true + }, + bot_info: Object, + os_logo: String, + os_name: String, + os_version: String, + os_family_logo: String, + os_family: String, + browser_logo: String, + browser_name: String, + browser_version: String, + browser_family: String, + browser_family_logo: String, + device_type_logo: String, + device_type: String, + device_brand_logo: String, + device_brand: String, + device_model: String, + clientHintsChecked: Boolean + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"] + }, + directives: { + ContentTable: external_CoreHome_["ContentTable"] + }, + created() { + getDefaultClientHints().then(hints => { + this.defaultClientHints = hints; + this.toggleClientHints(); + }); + }, + data() { + return { + itemListHtml: '', + considerClientHints: !!this.clientHintsChecked, + clientHintsText: '', + userAgentText: this.userAgent, + defaultClientHints: null + }; + }, + methods: { + showList(type) { + external_CoreHome_["AjaxHelper"].fetch({ + module: 'DevicesDetection', + action: 'showList', + type + }, { + format: 'html' + }).then(response => { + this.itemListHtml = response; + external_CoreHome_["Matomo"].helper.modalConfirm(this.$refs.deviceDetectionItemList, undefined, { + fixedFooter: true + }); + }); + }, + toggleClientHints() { + if (this.considerClientHints && this.defaultClientHints !== null) { + this.clientHintsText = this.clientHintsText || JSON.stringify(this.defaultClientHints); + } else { + this.clientHintsText = ''; + } + } + }, + computed: { + isClientHintsSupported() { + return isClientHintsSupported(); + } + } +})); +// CONCATENATED MODULE: ./plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.vue + + + +DetectionPagevue_type_script_lang_ts.render = render + +/* harmony default export */ var DetectionPage = (DetectionPagevue_type_script_lang_ts); +// CONCATENATED MODULE: ./plugins/DevicesDetection/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/ + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=DevicesDetection.umd.js.map \ No newline at end of file diff --git a/plugins/DevicesDetection/vue/dist/DevicesDetection.umd.min.js b/plugins/DevicesDetection/vue/dist/DevicesDetection.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..5c0fce1349bb887735b7fdbb8e9e4e4918bc42ed --- /dev/null +++ b/plugins/DevicesDetection/vue/dist/DevicesDetection.umd.min.js @@ -0,0 +1,8 @@ +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue")):"function"===typeof define&&define.amd?define(["CoreHome"],t):"object"===typeof exports?exports["DevicesDetection"]=t(require("CoreHome"),require("vue")):e["DevicesDetection"]=t(e["CoreHome"],e["Vue"])})("undefined"!==typeof self?self:this,(function(e,t){return function(e){var t={};function n(l){if(t[l])return t[l].exports;var c=t[l]={i:l,l:!1,exports:{}};return e[l].call(c.exports,c,c.exports,n),c.l=!0,c.exports}return n.m=e,n.c=t,n.d=function(e,t,l){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:l})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var l=Object.create(null);if(n.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var c in e)n.d(l,c,function(t){return e[t]}.bind(null,c));return l},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="plugins/DevicesDetection/vue/dist/",n(n.s="fae3")}({"19dc":function(t,n){t.exports=e},"8bbf":function(e,n){e.exports=t},fae3:function(e,t,n){"use strict";if(n.r(t),n.d(t,"DetectionPage",(function(){return k})),"undefined"!==typeof window){var l=window.document.currentScript,c=l&&l.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);c&&(n.p=c[1])}var o=n("8bbf");const i={class:"detectionPage"},r={action:"",method:"POST"},a=Object(o["createElementVNode"])("br",null,null,-1),s={key:0,class:"checkbox-container usech"},d=Object(o["createElementVNode"])("br",null,null,-1),b=Object(o["createElementVNode"])("br",null,null,-1),u=["value"],j={key:0},m={key:1},O={class:"detection"},p=["src"],g=["src"],V={class:"detection"},f=["src"],N=["src"],h={class:"detection"},D=["src"],v=["src"],y={class:"ui-confirm",id:"deviceDetectionItemList",ref:"deviceDetectionItemList"},E=["innerHTML"],_=["value"];function S(e,t,n,l,c,S){const w=Object(o["resolveComponent"])("ContentBlock"),x=Object(o["resolveDirective"])("content-table");return Object(o["openBlock"])(),Object(o["createElementBlock"])("div",i,[Object(o["createVNode"])(w,{"content-title":e.translate("DevicesDetection_DeviceDetection")},{default:Object(o["withCtx"])(()=>[Object(o["createElementVNode"])("form",r,[Object(o["createElementVNode"])("h3",null,Object(o["toDisplayString"])(e.translate("DevicesDetection_UserAgent")),1),Object(o["withDirectives"])(Object(o["createElementVNode"])("textarea",{name:"ua","onUpdate:modelValue":t[0]||(t[0]=t=>e.userAgentText=t)},null,512),[[o["vModelText"],e.userAgentText]]),a,Object(o["createElementVNode"])("h3",null,Object(o["toDisplayString"])(e.translate("DevicesDetection_ClientHints")),1),e.isClientHintsSupported?(Object(o["openBlock"])(),Object(o["createElementBlock"])("span",s,[Object(o["createElementVNode"])("label",null,[Object(o["withDirectives"])(Object(o["createElementVNode"])("input",{type:"checkbox",id:"usech","onUpdate:modelValue":t[1]||(t[1]=t=>e.considerClientHints=t),onChange:t[2]||(t[2]=t=>e.toggleClientHints())},null,544),[[o["vModelCheckbox"],e.considerClientHints]]),Object(o["createElementVNode"])("span",null,Object(o["toDisplayString"])(e.translate("DevicesDetection_ConsiderClientHints")),1)])])):Object(o["createCommentVNode"])("",!0),e.isClientHintsSupported&&e.considerClientHints?Object(o["withDirectives"])((Object(o["openBlock"])(),Object(o["createElementBlock"])("textarea",{key:1,name:"clienthints",style:{"margin-top":"2em"},"onUpdate:modelValue":t[3]||(t[3]=t=>e.clientHintsText=t)},null,512)),[[o["vModelText"],e.clientHintsText]]):Object(o["createCommentVNode"])("",!0),Object(o["withDirectives"])(Object(o["createElementVNode"])("span",{id:"noclienthints",class:"alert alert-warning"},Object(o["toDisplayString"])(e.translate("DevicesDetection_ClientHintsNotSupported")),513),[[o["vShow"],!e.isClientHintsSupported]]),d,b,Object(o["createElementVNode"])("input",{type:"submit",value:e.translate("General_Refresh"),class:"btn"},null,8,u)]),e.bot_info?(Object(o["openBlock"])(),Object(o["createElementBlock"])("h3",j,Object(o["toDisplayString"])(e.translate("DevicesDetection_BotDetected",e.bot_info.name)),1)):(Object(o["openBlock"])(),Object(o["createElementBlock"])("div",m,[Object(o["createElementVNode"])("h3",null,Object(o["toDisplayString"])(e.translate("DevicesDetection_ColumnOperatingSystem")),1),Object(o["withDirectives"])((Object(o["openBlock"])(),Object(o["createElementBlock"])("table",O,[Object(o["createElementVNode"])("tbody",null,[Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,[Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.translate("General_Name"))+" ",1),Object(o["createElementVNode"])("small",null,[Object(o["createTextVNode"])(" ("),Object(o["createElementVNode"])("a",{href:"",onClick:t[4]||(t[4]=Object(o["withModifiers"])(t=>e.showList("os"),["prevent"]))},Object(o["toDisplayString"])(e.translate("Mobile_ShowAll")),1),Object(o["createTextVNode"])(") ")])]),Object(o["createElementVNode"])("td",null,[Object(o["createElementVNode"])("img",{height:16,width:16,src:e.os_logo},null,8,p),Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.os_name),1)])]),Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,Object(o["toDisplayString"])(e.translate("CorePluginsAdmin_Version")),1),Object(o["createElementVNode"])("td",null,Object(o["toDisplayString"])(e.os_version),1)]),Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,[Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.translate("DevicesDetection_OperatingSystemFamily"))+" ",1),Object(o["createElementVNode"])("small",null,[Object(o["createTextVNode"])(" ("),Object(o["createElementVNode"])("a",{href:"",onClick:t[5]||(t[5]=Object(o["withModifiers"])(t=>e.showList("osfamilies"),["prevent"]))},Object(o["toDisplayString"])(e.translate("Mobile_ShowAll")),1),Object(o["createTextVNode"])(") ")])]),Object(o["createElementVNode"])("td",null,[Object(o["createElementVNode"])("img",{height:16,width:16,src:e.os_family_logo},null,8,g),Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.os_family),1)])])])])),[[x]]),Object(o["createElementVNode"])("h3",null,Object(o["toDisplayString"])(e.translate("DevicesDetection_ColumnBrowser")),1),Object(o["withDirectives"])((Object(o["openBlock"])(),Object(o["createElementBlock"])("table",V,[Object(o["createElementVNode"])("tbody",null,[Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,[Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.translate("General_Name"))+" ",1),Object(o["createElementVNode"])("small",null,[Object(o["createTextVNode"])(" ("),Object(o["createElementVNode"])("a",{href:"",onClick:t[6]||(t[6]=Object(o["withModifiers"])(t=>e.showList("browsers"),["prevent"]))},Object(o["toDisplayString"])(e.translate("Mobile_ShowAll")),1),Object(o["createTextVNode"])(") ")])]),Object(o["createElementVNode"])("td",null,[Object(o["createElementVNode"])("img",{height:16,width:16,src:e.browser_logo},null,8,f),Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.browser_name),1)])]),Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,Object(o["toDisplayString"])(e.translate("CorePluginsAdmin_Version")),1),Object(o["createElementVNode"])("td",null,Object(o["toDisplayString"])(e.browser_version),1)]),Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,[Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.translate("DevicesDetection_BrowserFamily"))+" ",1),Object(o["createElementVNode"])("small",null,[Object(o["createTextVNode"])(" ("),Object(o["createElementVNode"])("a",{href:"",onClick:t[7]||(t[7]=Object(o["withModifiers"])(t=>e.showList("browserfamilies"),["prevent"]))},Object(o["toDisplayString"])(e.translate("Mobile_ShowAll")),1),Object(o["createTextVNode"])(") ")])]),Object(o["createElementVNode"])("td",null,[Object(o["createElementVNode"])("img",{height:16,width:16,src:e.browser_family_logo},null,8,N),Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.browser_family),1)])])])])),[[x]]),Object(o["createElementVNode"])("h3",null,Object(o["toDisplayString"])(e.translate("DevicesDetection_Device")),1),Object(o["withDirectives"])((Object(o["openBlock"])(),Object(o["createElementBlock"])("table",h,[Object(o["createElementVNode"])("tbody",null,[Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,[Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.translate("DevicesDetection_dataTableLabelTypes"))+" ",1),Object(o["createElementVNode"])("small",null,[Object(o["createTextVNode"])(" ("),Object(o["createElementVNode"])("a",{href:"",onClick:t[8]||(t[8]=Object(o["withModifiers"])(t=>e.showList("devicetypes"),["prevent"]))},Object(o["toDisplayString"])(e.translate("Mobile_ShowAll")),1),Object(o["createTextVNode"])(") ")])]),Object(o["createElementVNode"])("td",null,[Object(o["createElementVNode"])("img",{height:16,width:16,src:e.device_type_logo},null,8,D),Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.device_type),1)])]),Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,[Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.translate("DevicesDetection_dataTableLabelBrands"))+" ",1),Object(o["createElementVNode"])("small",null,[Object(o["createTextVNode"])(" ("),Object(o["createElementVNode"])("a",{href:"",onClick:t[9]||(t[9]=Object(o["withModifiers"])(t=>e.showList("brands"),["prevent"]))},Object(o["toDisplayString"])(e.translate("Mobile_ShowAll")),1),Object(o["createTextVNode"])(") ")])]),Object(o["createElementVNode"])("td",null,[Object(o["createElementVNode"])("img",{height:16,width:16,src:e.device_brand_logo},null,8,v),Object(o["createTextVNode"])(Object(o["toDisplayString"])(e.device_brand),1)])]),Object(o["createElementVNode"])("tr",null,[Object(o["createElementVNode"])("td",null,Object(o["toDisplayString"])(e.translate("DevicesDetection_dataTableLabelModels")),1),Object(o["createElementVNode"])("td",null,Object(o["toDisplayString"])(e.device_model),1)])])])),[[x]])]))]),_:1},8,["content-title"]),Object(o["createElementVNode"])("div",y,[Object(o["createElementVNode"])("div",{class:"itemList",innerHTML:e.$sanitize(e.itemListHtml)},null,8,E),Object(o["createElementVNode"])("input",{role:"close",type:"button",value:e.translate("General_Close")},null,8,_)],512)])}var w=n("19dc");function x(){const e=navigator;return e.userAgentData&&"function"===typeof e.userAgentData.getHighEntropyValues}let C=null;function T(){const e=navigator;return x()?C?Promise.resolve(C):(C={brands:e.userAgentData.brands,platform:e.userAgentData.platform},e.userAgentData.getHighEntropyValues(["brands","model","platform","platformVersion","uaFullVersion","fullVersionList"]).then(e=>(C=Object.assign({},e),C.fullVersionList&&(delete C.brands,delete C.uaFullVersion),C))):Promise.resolve(null)}var H=Object(o["defineComponent"])({props:{userAgent:{type:String,required:!0},bot_info:Object,os_logo:String,os_name:String,os_version:String,os_family_logo:String,os_family:String,browser_logo:String,browser_name:String,browser_version:String,browser_family:String,browser_family_logo:String,device_type_logo:String,device_type:String,device_brand_logo:String,device_brand:String,device_model:String,clientHintsChecked:Boolean},components:{ContentBlock:w["ContentBlock"]},directives:{ContentTable:w["ContentTable"]},created(){T().then(e=>{this.defaultClientHints=e,this.toggleClientHints()})},data(){return{itemListHtml:"",considerClientHints:!!this.clientHintsChecked,clientHintsText:"",userAgentText:this.userAgent,defaultClientHints:null}},methods:{showList(e){w["AjaxHelper"].fetch({module:"DevicesDetection",action:"showList",type:e},{format:"html"}).then(e=>{this.itemListHtml=e,w["Matomo"].helper.modalConfirm(this.$refs.deviceDetectionItemList,void 0,{fixedFooter:!0})})},toggleClientHints(){this.considerClientHints&&null!==this.defaultClientHints?this.clientHintsText=this.clientHintsText||JSON.stringify(this.defaultClientHints):this.clientHintsText=""}},computed:{isClientHintsSupported(){return x()}}});H.render=S;var k=H; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/}})})); +//# sourceMappingURL=DevicesDetection.umd.min.js.map \ No newline at end of file diff --git a/plugins/DevicesDetection/vue/dist/umd.metadata.json b/plugins/DevicesDetection/vue/dist/umd.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..9ecfcc0456204cf8e8e80416ebc95ef42d4fbd28 --- /dev/null +++ b/plugins/DevicesDetection/vue/dist/umd.metadata.json @@ -0,0 +1,5 @@ +{ + "dependsOn": [ + "CoreHome" + ] +} \ No newline at end of file diff --git a/plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.less b/plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.less new file mode 100644 index 0000000000000000000000000000000000000000..a0b39533421f13f5278162e7cec417221ab1be43 --- /dev/null +++ b/plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.less @@ -0,0 +1,22 @@ +.detectionPage { + textarea { + width: 700px; + display: block; + } + + .detection { + padding-top: 10px; + } + + .detection td { + width: 50%; + } + + .detection td img { + margin-right: 10px; + } + + #noclienthints { + display: inline-block; + } +} diff --git a/plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.vue b/plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..fb8b83bfea1d07299b4ae08ac24a220f86c75584 --- /dev/null +++ b/plugins/DevicesDetection/vue/src/DetectionPage/DetectionPage.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/plugins/DevicesDetection/vue/src/index.ts b/plugins/DevicesDetection/vue/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..04d6ce3165d63450de64f9ddc2566761d5b36b6f --- /dev/null +++ b/plugins/DevicesDetection/vue/src/index.ts @@ -0,0 +1,8 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/ + +export { default as DetectionPage } from './DetectionPage/DetectionPage.vue'; diff --git a/plugins/Diagnostics/Commands/AnalyzeArchiveTable.php b/plugins/Diagnostics/Commands/AnalyzeArchiveTable.php new file mode 100644 index 0000000000000000000000000000000000000000..7283515586c4baf325cc2a6b7ce8c49435ecced3 --- /dev/null +++ b/plugins/Diagnostics/Commands/AnalyzeArchiveTable.php @@ -0,0 +1,91 @@ +setName('diagnostics:analyze-archive-table'); + $this->setDescription('Analyze an archive table and display human readable information about what is stored. ' + . 'This command can be used to diagnose issues like bloated archive tables.'); + $this->addRequiredArgument('table-date', "The table's associated date, eg, 2015_01 or 2015_02"); + } + + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + $tableDate = $input->getArgument('table-date'); + + $output->writeln("Statistics for the archive_numeric_$tableDate and archive_blob_$tableDate tables:"); + $output->writeln(""); + + $archiveTableDao = StaticContainer::get('Piwik\DataAccess\ArchiveTableDao'); + $rows = $archiveTableDao->getArchiveTableAnalysis($tableDate); + + // process labels + $periodIdsToLabels = array_flip(Piwik::$idPeriods); + foreach ($rows as $key => &$row) { + list($idSite, $date1, $date2, $period) = explode('.', $key); + + $periodLabel = isset($periodIdsToLabels[$period]) ? $periodIdsToLabels[$period] : "Unknown Period ($period)"; + $row['label'] = $periodLabel . "[" . $date1 . " - " . $date2 . "] idSite = " . $idSite; + } + + $headers = array('Group', '# Archives', '# Invalidated', '# Temporary', '# Error', '# Segment', + '# Numeric Rows', '# Blob Rows', '# Blob Data'); + + // display all rows + $this->renderTable($headers, $rows); + + // display summary + $totalArchives = 0; + $totalInvalidated = 0; + $totalTemporary = 0; + $totalError = 0; + $totalSegment = 0; + $totalBlobLength = 0; + + foreach ($rows as $row) { + $totalArchives += (int) $row['count_archives']; + $totalInvalidated += (int) $row['count_invalidated_archives']; + $totalTemporary += (int) $row['count_temporary_archives']; + $totalError += (int) $row['count_error_archives']; + $totalSegment += (int) $row['count_segment_archives']; + + if (isset($row['sum_blob_length'])) { + $totalBlobLength += (int) $row['sum_blob_length']; + } + } + + $formatter = new Formatter(); + + $output->writeln(""); + $output->writeln("Total # Archives: $totalArchives"); + $output->writeln("Total # Invalidated Archives: $totalInvalidated"); + $output->writeln("Total # Temporary Archives: $totalTemporary"); + $output->writeln("Total # Error Archives: $totalError"); + $output->writeln("Total # Segment Archives: $totalSegment"); + $output->writeln("Total Size of Blobs: " . $formatter->getPrettySizeFromBytes($totalBlobLength) . ""); + $output->writeln(""); + + return self::SUCCESS; + } +} diff --git a/plugins/Diagnostics/Commands/ArchivingConfig.php b/plugins/Diagnostics/Commands/ArchivingConfig.php new file mode 100644 index 0000000000000000000000000000000000000000..5b9771214076cc45b543d2bb6aaf867e92a6deaa --- /dev/null +++ b/plugins/Diagnostics/Commands/ArchivingConfig.php @@ -0,0 +1,116 @@ +setName('diagnostics:archiving-config'); + $this->addNoValueOption( + 'json', + null, + "If supplied, the command will return data in json format" + ); + $this->setDescription('Show configuration settings that can affect archiving performance'); + } + + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + + $metrics = $this->getArchivingConfig(); + + if ($input->getOption('json')) { + $output->write(json_encode($metrics)); + } else { + $headers = ['Section', 'Setting', 'Value']; + $this->renderTable($headers, $metrics); + } + + return self::SUCCESS; + } + + /** + * Retrieve various data statistics useful for diagnosing archiving performance + * + * @return array + */ + public function getArchivingConfig(): array + { + $configs = [ + 'database' => [ + 'enable_segment_first_table_join_prefix', + 'enable_first_table_join_prefix', + ], + 'general' => [ + 'browser_archiving_disabled_enforce', + 'enable_processing_unique_visitors_day', + 'enable_processing_unique_visitors_week', + 'enable_processing_unique_visitors_month', + 'enable_processing_unique_visitors_year', + 'enable_processing_unique_visitors_range', + 'enable_processing_unique_visitors_multiple_sites', + 'process_new_segments_from', + 'time_before_today_archive_considered_outdated', + 'time_before_week_archive_considered_outdated', + 'time_before_month_archive_considered_outdated', + 'time_before_year_archive_considered_outdated', + 'time_before_range_archive_considered_outdated', + 'enable_browser_archiving_triggering', + 'archiving_range_force_on_browser_request', + 'archiving_custom_ranges[]', + 'archiving_query_max_execution_time', + 'archiving_ranking_query_row_limit', + 'disable_archiving_segment_for_plugins', + 'disable_archive_actions_goals', + 'datatable_archiving_maximum_rows_referrers', + 'datatable_archiving_maximum_rows_subtable_referrers', + 'datatable_archiving_maximum_rows_userid_users', + 'datatable_archiving_maximum_rows_custom_dimensions', + 'datatable_archiving_maximum_rows_subtable_custom_dimensions', + 'datatable_archiving_maximum_rows_actions', + 'datatable_archiving_maximum_rows_subtable_actions', + 'datatable_archiving_maximum_rows_site_search', + 'datatable_archiving_maximum_rows_events', + 'datatable_archiving_maximum_rows_subtable_events', + 'datatable_archiving_maximum_rows_products', + 'datatable_archiving_maximum_rows_standard', + ], + ]; + + $data = []; + foreach ($configs as $section => $sectionConfigs) { + foreach ($sectionConfigs as $setting) { + switch ($section) { + case 'general': + $value = GeneralConfig::getConfigValue($setting); + break; + case 'database': + $value = DatabaseConfig::getConfigValue($setting); + break; + default: + $value = Config::getInstance()->{$section}[$setting] ?? ''; + } + $data[] = ['Section' => $section, 'Setting' => $setting, 'Value' => $value]; + } + } + return $data; + } +} diff --git a/plugins/Diagnostics/Commands/ArchivingInstanceStatistics.php b/plugins/Diagnostics/Commands/ArchivingInstanceStatistics.php new file mode 100644 index 0000000000000000000000000000000000000000..98ca57ba6ef16e9ba24a7e459e434b813a00196c --- /dev/null +++ b/plugins/Diagnostics/Commands/ArchivingInstanceStatistics.php @@ -0,0 +1,79 @@ +setName('diagnostics:archiving-instance-statistics'); + $this->addNoValueOption( + 'json', + null, + "If supplied, the command will return data in json format" + ); + $this->setDescription('Show data statistics which can affect archiving performance'); + } + + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + + $metrics = $this->getArchivingInstanceStatistics(); + + if ($input->getOption('json')) { + $output->write(json_encode($metrics)); + } else { + $headers = ['Statistic Name', 'Value']; + $this->renderTable($headers, $metrics); + } + + return self::SUCCESS; + } + + /** + * Retrieve various data statistics useful for diagnosing archiving performance + * + * @return array + */ + public function getArchivingInstanceStatistics(): array + { + $stats = []; + $stats[] = ['Site Count', (int) Db::fetchOne("SELECT COUNT(*) FROM " . Common::prefixTable("site"))]; + $stats[] = ['Segment Count', (int) Db::fetchOne("SELECT COUNT(*) FROM " . Common::prefixTable("segment"))]; + $stats[] = ['Database Version', defined('PIWIK_TEST_MODE') ? 'mysql-version-redacted' : Db::get()->getServerVersion()]; + $stats[] = ['Last full Month Hits', (int) Db::fetchOne( + "SELECT COUNT(*) FROM " . Common::prefixTable("log_link_visit_action") . " WHERE server_time >= ? AND server_time <= ?", + [ + Date::now()->setDay(1)->subMonth(1)->setTime('00:00:00')->toString('Y-m-d H:i:s'), + Date::now()->setDay(1)->subDay(1)->setTime('23:59:59')->toString('Y-m-d H:i:s'), + ] + ), + ]; + $stats[] = ['Last 12 Month Hits', (int) Db::fetchOne( + "SELECT COUNT(*) FROM " . Common::prefixTable("log_link_visit_action") . " WHERE server_time >= ? AND server_time <= ?", + [ + Date::now()->setDay(1)->subMonth(12)->setTime('00:00:00')->toString('Y-m-d H:i:s'), + Date::now()->setDay(1)->subDay(1)->setTime('23:59:59')->toString('Y-m-d H:i:s'), + ] + ), + ]; + return $stats; + } +} diff --git a/plugins/Diagnostics/Commands/ArchivingMetrics.php b/plugins/Diagnostics/Commands/ArchivingMetrics.php new file mode 100644 index 0000000000000000000000000000000000000000..d1634686294c2a35514acd6d853a50682b838260 --- /dev/null +++ b/plugins/Diagnostics/Commands/ArchivingMetrics.php @@ -0,0 +1,72 @@ +setName('diagnostics:archiving-metrics'); + $this->addNoValueOption( + 'json', + null, + "If supplied, the command will return data in json format" + ); + $this->setDescription('Show metrics describing the current archiving status'); + } + + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + + $metrics = $this->getMetrics(); + + if ($input->getOption('json')) { + $output->write(json_encode($metrics)); + } else { + $headers = ['Metric', 'Value']; + $this->renderTable($headers, $metrics); + } + + return self::SUCCESS; + } + + /** + * Get an archiving metrics array from the diagnostics class + * + * @return array + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException + */ + public function getMetrics(): array + { + $metrics = []; + $informational = new ArchiveInvalidationsInformational(StaticContainer::get(Translator::class)); + $diags[] = $informational->execute(); + if (is_array($diags)) { + foreach (reset($diags) as $diag) { + $items = $diag->getItems(); + if (count($items) > 0) { + $metrics[] = [$diag->getLabel(), reset($items)->getComment()]; + } + } + } + return $metrics; + } +} diff --git a/plugins/Diagnostics/Commands/ArchivingQueue.php b/plugins/Diagnostics/Commands/ArchivingQueue.php new file mode 100644 index 0000000000000000000000000000000000000000..410f19ec82be79da8ae9d067d132e29e6abd2177 --- /dev/null +++ b/plugins/Diagnostics/Commands/ArchivingQueue.php @@ -0,0 +1,49 @@ +setName('diagnostics:archiving-queue'); + $this->addNoValueOption( + 'json', + null, + "If supplied, the command will return table data in json format" + ); + $this->setDescription('Show the current state of the archive invalidations queue as a table'); + } + + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + + $archiveTableDao = StaticContainer::get('Piwik\DataAccess\ArchiveTableDao'); + + if ($input->getOption('json')) { + $queue = $archiveTableDao->getInvalidationQueueData(); + $output->write(json_encode($queue)); + } else { + $headers = ['Invalidation', 'Segment', 'Site', 'Period', 'Date', 'Time Queued', 'Waiting', 'Started', 'Processing', 'Status']; + $queue = $archiveTableDao->getInvalidationQueueData(true); + $this->renderTable($headers, $queue); + } + + return self::SUCCESS; + } +} diff --git a/plugins/Diagnostics/Commands/ArchivingStatus.php b/plugins/Diagnostics/Commands/ArchivingStatus.php new file mode 100644 index 0000000000000000000000000000000000000000..d64deeac60ee77225bc163d3cd601ca14891b1f2 --- /dev/null +++ b/plugins/Diagnostics/Commands/ArchivingStatus.php @@ -0,0 +1,109 @@ +setName('diagnostics:archiving-status'); + $this->addNoValueOption( + 'with-stats', + null, + "If supplied, the command will include instance statistics such as monthly hits and site count" + ); + $this->addOptionalValueOption( + 'email', + null, + "If supplied, the command will email the output to the supplied email address" + ); + $this->setDescription(''); + } + + protected function doExecute(): int + { + $input = $this->getInput(); + + // If using email option then buffer output + if ($input->getOption('email')) { + $output = new ConsoleCommandBufferedOutput(); + $this->setOutput($output); + } else { + $output = $this->getOutput(); + } + + // Queue + $this->outputSectionHeader($output, 'Invalidation Queue'); + $archiveTableDao = StaticContainer::get('Piwik\DataAccess\ArchiveTableDao'); + $headers = ['Invalidation', 'Segment', 'Site', 'Period', 'Date', 'Time Queued', 'Waiting', 'Started', 'Processing', 'Status']; + $queue = $archiveTableDao->getInvalidationQueueData(true); + $this->renderTable($headers, $queue); + + // Metrics + $this->outputSectionHeader($output, 'Archiving Metrics'); + $am = new ArchivingMetrics(); + $this->renderTable(['Metric', 'Value'], $am->getMetrics()); + + // Optional instance stats + if ($input->getOption('with-stats')) { + $this->outputSectionHeader($output, 'Instance Statistics'); + $ais = new ArchivingInstanceStatistics(); + $this->renderTable(['Statistic Name', 'Value'], $ais->getArchivingInstanceStatistics()); + } + + // Config + $this->outputSectionHeader($output, 'Archiving Configuration Settings'); + $am = new ArchivingConfig(); + $this->renderTable(['Section', 'Setting', 'Value'], $am->getArchivingConfig()); + + if ($input->getOption('email')) { + $address = $input->getOption('email'); + $content = 'This email was sent via the Matomo diagnostic:archiving-status command'; + $content .= '
    ';
    +            $content .= $output->fetch();
    +            $content .= '
    '; + $mail = new Mail(); + $mail->setDefaultFromPiwik(); + $mail->addTo($address); + $mail->setSubject('Matomo Archiving Diagnostics'); + $mail->setWrappedHtmlBody($content); + $output = new ConsoleCommandConsoleOutput(); + $this->setOutput($output); + try { + $mail->send(); + $output->writeln("Archiving diagnostic email successfully sent to " . $address); + } catch (\Exception $e) { + $output->writeln("Failed to send email to " . $address . ", error: " . $e->getMessage()); + return self::FAILURE; + } + } + + return self::SUCCESS; + } + + /** + * Output a styled header string + * + * @param mixed $output + */ + private function outputSectionHeader($output, string $title): void + { + $output->writeln("\n" . $title . ""); + } +} diff --git a/plugins/Diagnostics/Commands/Run.php b/plugins/Diagnostics/Commands/Run.php new file mode 100644 index 0000000000000000000000000000000000000000..bdf55ddb6191cc642e2cdf443e651c5e0c7a79e4 --- /dev/null +++ b/plugins/Diagnostics/Commands/Run.php @@ -0,0 +1,94 @@ +setName('diagnostics:run') + ->setDescription('Run diagnostics to check that Piwik is installed and runs correctly') + ->addNoValueOption('all', null, 'Show all diagnostics, including those that passed with success'); + } + + protected function doExecute(): int + { + // Replace this with dependency injection once available + /** @var DiagnosticService $diagnosticService */ + $diagnosticService = StaticContainer::get('Piwik\Plugins\Diagnostics\DiagnosticService'); + + $input = $this->getInput(); + $output = $this->getOutput(); + $showAll = $input->getOption('all'); + + $report = $diagnosticService->runDiagnostics(); + + foreach ($report->getAllResults() as $result) { + $items = $result->getItems(); + + if (! $showAll && ($result->getStatus() === DiagnosticResult::STATUS_OK)) { + continue; + } + + if (count($items) === 1) { + $output->writeln($result->getLabel() . ': ' . $this->formatItem($items[0])); + continue; + } + + $output->writeln($result->getLabel() . ':'); + foreach ($items as $item) { + $output->writeln("\t- " . $this->formatItem($item)); + } + } + + if ($report->hasWarnings()) { + $output->writeln(sprintf('%d warnings detected', $report->getWarningCount())); + } + if ($report->hasErrors()) { + $output->writeln(sprintf('%d errors detected', $report->getErrorCount())); + return self::FAILURE; + } + + if (!$report->hasWarnings() && !$report->hasErrors()) { + $output->writeln(sprintf('%s', Piwik::translate('Installation_SystemCheckSummaryNoProblems'))); + } + + return self::SUCCESS; + } + + private function formatItem(DiagnosticResultItem $item) + { + if ($item->getStatus() === DiagnosticResult::STATUS_ERROR) { + $tag = 'error'; + } elseif ($item->getStatus() === DiagnosticResult::STATUS_WARNING) { + $tag = 'comment'; + } else { + $tag = 'info'; + } + + return sprintf( + '<%s>%s %s', + $tag, + strtoupper($item->getStatus()), + preg_replace('%]*>%sim', '', preg_replace('/\/i', "\n", $item->getComment())), + $tag + ); + } +} diff --git a/plugins/Diagnostics/Commands/UnexpectedFiles.php b/plugins/Diagnostics/Commands/UnexpectedFiles.php new file mode 100644 index 0000000000000000000000000000000000000000..69f6847d1efc4b694a0033f7eeb83cab7d94b394 --- /dev/null +++ b/plugins/Diagnostics/Commands/UnexpectedFiles.php @@ -0,0 +1,134 @@ +setName('diagnostics:unexpected-files') + ->setDescription('Show a list of unexpected files found in the Matomo installation directory and optionally delete them.') + ->addNoValueOption('delete', null, 'Delete all the unexpected files'); + } + + /** + * Execute the command + */ + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + + // Prevent running in development mode + if (Development::isEnabled()) { + $output->writeln("Aborting - this command cannot be used in development mode as it requires a release manifest file"); + return 1; + } + + // Prevent running if there is no release manifest file + $manifest = PIWIK_INCLUDE_PATH . '/config/manifest.inc.php'; + if (!file_exists($manifest)) { + $output->writeln("Release manifest file '" . $manifest . "' not found."); + $output->writeln("Aborting - this command can only be used when a release manifest file is present."); + return 1; + } + + + $delete = $input->getOption('delete'); + if ($delete) { + $output->writeln("Preparing to delete all unexpected files from the Matomo installation directory"); + + if (!$this->askForDeleteConfirmation()) { + $output->writeln("Aborted - no files were deleted"); + return 1; + } + + return $this->runUnexpectedFiles(true); + } + + return $this->runUnexpectedFiles(false); + } + + /** + * Handle unexpected files command options + */ + private function runUnexpectedFiles(bool $delete = false): int + { + $output = $this->getOutput(); + + // A list of files that should never be deleted under any circumstances, this acts as a backup safety check + // for the FileIntegrity class which should already be excluding these files. + $excludedFiles = [ + '/^config\/config.ini.php$/', // main config file + '/^config\/common.config.ini.php$/', // multi-tenant config file + '/\.htaccess$/', // apache directory access rules + '/^config\/config.php$/', // DI customisation + '/^misc\/.*$/', // everything in the misc/ directory (geo databases, multi-tenant, etc) + ]; + + $files = FileIntegrity::getUnexpectedFilesList(); + $fails = 0; + + foreach ($files as $f) { + foreach ($excludedFiles as $ef) { + if (preg_match($ef, $f)) { + continue 2; + } + } + + $fileName = realpath($f); + + if ($delete) { + if (Filesystem::deleteFileIfExists($fileName)) { + $output->writeln("Deleted unexpected file '" . $fileName); + } else { + $output->writeln("Failed to delete unexpected file '" . $fileName); + $fails++; + } + } else { + $output->writeln($fileName); + } + } + if ($delete && $fails) { + $output->writeln("Failed to delete " . $fails . " unexpected files"); + return 1; + } + return 0; + } + + /** + * Interact with the user to confirm the deletion + */ + private function askForDeleteConfirmation(): bool + { + if (!$this->getInput()->isInteractive()) { + return true; + } + + return $this->askForConfirmation( + 'You are about to delete files. This action cannot be undone, are you sure you want to continue? (Y/N)', + false + ); + } +} diff --git a/plugins/Diagnostics/ConfigReader.php b/plugins/Diagnostics/ConfigReader.php new file mode 100644 index 0000000000000000000000000000000000000000..3c28f1bd9b8e45781308c5739f4c1b578d16326f --- /dev/null +++ b/plugins/Diagnostics/ConfigReader.php @@ -0,0 +1,206 @@ +settings = $settings; + $this->iniReader = $iniReader; + } + + public function getConfigValuesFromFiles() + { + $ini = $this->settings->getIniFileChain(); + $descriptions = $this->iniReader->readComments($this->settings->getPathGlobal()); + + $copy = array(); + foreach ($ini->getAll() as $category => $values) { + if ($this->shouldSkipCategory($category)) { + continue; + } + + $local = $this->getFromLocalConfig($category); + if (empty($local)) { + $local = array(); + } + + $global = $this->getFromGlobalConfig($category); + if (empty($global)) { + $global = array(); + } + + $copy[$category] = array(); + foreach ($values as $key => $value) { + $newValue = $value; + if ($this->isKeyAPassword($key)) { + $newValue = $this->getMaskedPassword(); + } + + $defaultValue = null; + if (array_key_exists($key, $global)) { + $defaultValue = $global[$key]; + } + + $description = ''; + if (!empty($descriptions[$category][$key])) { + $description = trim($descriptions[$category][$key]); + } + + $copy[$category][$key] = array( + 'value' => $newValue, + 'description' => $description, + 'isCustomValue' => array_key_exists($key, $local), + 'defaultValue' => $defaultValue, + ); + } + } + + return $copy; + } + + private function shouldSkipCategory($category) + { + $category = strtolower($category); + if ($category === 'database' || $category === 'database_reader') { + return true; + } + + $developmentOnlySections = array('database_tests', 'tests', 'debugtests'); + + return !Development::isEnabled() && in_array($category, $developmentOnlySections); + } + + public function getFromGlobalConfig($name) + { + return $this->settings->getIniFileChain()->getFrom($this->settings->getPathGlobal(), $name); + } + + public function getFromLocalConfig($name) + { + return $this->settings->getIniFileChain()->getFrom($this->settings->getPathLocal(), $name); + } + + private function getMaskedPassword() + { + return '******'; + } + + private function isKeyAPassword($key) + { + $key = strtolower($key); + $passwordFields = array( + 'password', 'secret', 'apikey', 'privatekey', 'admin_pass', 'md5', 'sha1', + ); + foreach ($passwordFields as $value) { + if (strpos($key, $value) !== false) { + return true; + } + } + + if ($key === 'salt') { + return true; + } + + return false; + } + + /** + * Adds config values that can be used to overwrite a plugin system setting and adds a description + default value + * for already existing configured config values that overwrite a plugin system setting. + * + * @param array $configValues + * @param \Piwik\Settings\Plugin\SystemSettings[] $systemSettings + * @return array + */ + public function addConfigValuesFromSystemSettings($configValues, $systemSettings) + { + foreach ($systemSettings as $pluginSetting) { + $pluginName = $pluginSetting->getPluginName(); + + if (empty($pluginName)) { + continue; + } + + if (!array_key_exists($pluginName, $configValues)) { + $configValues[$pluginName] = array(); + } + + foreach ($pluginSetting->getSettingsWritableByCurrentUser() as $setting) { + $name = $setting->getName(); + + $configSection = $pluginName; + + if ($setting instanceof PiwikSettings\Plugin\SystemConfigSetting) { + $configSection = $setting->getConfigSectionName(); + + if ($this->shouldSkipCategory($configSection)) { + continue; + } + } + + $config = $setting->configureField(); + + $description = ''; + if (!empty($config->description)) { + $description .= $config->description . ' '; + } + + if (!empty($config->inlineHelp)) { + $description .= $config->inlineHelp; + } + + if (isset($configValues[$configSection][$name])) { + $configValues[$configSection][$name]['defaultValue'] = $setting->getDefaultValue(); + $configValues[$configSection][$name]['description'] = trim($description); + } else { + $configValues[$configSection][$name] = array( + 'value' => $setting->getValue() != $setting->getDefaultValue() ? $setting->getValue() : null, + 'description' => trim($description), + 'isCustomValue' => $setting->getValue() != $setting->getDefaultValue(), + 'defaultValue' => $setting->getDefaultValue(), + ); + } + + if ( + !empty($configValues[$configSection][$name]['value']) + && $config->uiControl === PiwikSettings\FieldConfig::UI_CONTROL_PASSWORD + ) { + $configValues[$configSection][$name]['value'] = $this->getMaskedPassword(); + } + } + + if (empty($configValues[$pluginName])) { + unset($configValues[$pluginName]); + } + } + + return $configValues; + } +} diff --git a/plugins/Diagnostics/Controller.php b/plugins/Diagnostics/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..e29b2a2508f6757a9c009a76aee0e6550552cbff --- /dev/null +++ b/plugins/Diagnostics/Controller.php @@ -0,0 +1,68 @@ +configReader = $configReader; + parent::__construct(); + } + + public function configfile() + { + Piwik::checkUserHasSuperUserAccess(); + + $settings = new SettingsProvider(\Piwik\Plugin\Manager::getInstance()); + $allSettings = $settings->getAllSystemSettings(); + + $configValues = $this->configReader->getConfigValuesFromFiles(); + $configValues = $this->configReader->addConfigValuesFromSystemSettings($configValues, $allSettings); + $configValues = $this->sortConfigValues($configValues); + $configValues = array_filter($configValues); + + return $this->renderTemplate('configfile', array( + 'allConfigValues' => $configValues, + 'configFilePath' => Config::getInstance()->getLocalPath(), + )); + } + + private function sortConfigValues($configValues) + { + // we sort by sections alphabetically + uksort($configValues, function ($section1, $section2) { + return strcasecmp($section1, $section2); + }); + + foreach ($configValues as $category => &$settings) { + // we sort keys alphabetically but list the ones that are changed first + uksort($settings, function ($setting1, $setting2) use ($settings) { + if ($settings[$setting1]['isCustomValue'] && !$settings[$setting2]['isCustomValue']) { + return -1; + } elseif (!$settings[$setting1]['isCustomValue'] && $settings[$setting2]['isCustomValue']) { + return 1; + } + return strcasecmp($setting1, $setting2); + }); + } + + return $configValues; + } +} diff --git a/plugins/Diagnostics/Diagnostic/AbstractPrivateDirectories.php b/plugins/Diagnostics/Diagnostic/AbstractPrivateDirectories.php new file mode 100644 index 0000000000000000000000000000000000000000..f5c5278e0cc718f860ebf6781a57a3807ea8ae01 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/AbstractPrivateDirectories.php @@ -0,0 +1,168 @@ +translator = $translator; + } + + public function execute() + { + if (!SettingsPiwik::isMatomoInstalled()) { + return []; + } + + $label = $this->translator->translate($this->labelKey); + + $baseUrl = SettingsPiwik::getPiwikUrl(); + if (!Common::stringEndsWith($baseUrl, '/')) { + $baseUrl .= '/'; + } + + $manualCheck = $this->translator->translate('Diagnostics_PrivateDirectoryManualCheck'); + + $testUrls = []; + foreach ($this->privatePaths as $path) { + if (!file_exists($path)) { + continue; + } + $testUrls[$path] = $baseUrl . $path; + } + + $isInternetEnabled = SettingsPiwik::isInternetEnabled(); + if (!$isInternetEnabled) { + $testUrlsList = $this->getUrlList($testUrls); + + $unknown = $this->translator->translate('Diagnostics_PrivateDirectoryInternetDisabled') . ' ' . $manualCheck + . $testUrlsList; + $results[] = DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $unknown); + return $results; + } + + $result = new DiagnosticResult($label); + if (Config::getInstance()->General['enable_required_directories_diagnostic'] == 0) { + $result->addItem( + new DiagnosticResultItem( + DiagnosticResult::STATUS_WARNING, + $this->translator->translate('Diagnostics_EnableRequiredDirectoriesDiagnostic') + ) + ); + return [$result]; + } + + $atLeastOneIsAccessible = $this->computeAccessiblePaths($result, $baseUrl, $testUrls); + + if ($atLeastOneIsAccessible) { + $this->addError($result); + } else { + $result->addItem(new DiagnosticResultItem(DiagnosticResult::STATUS_OK, $this->translator->translate('Diagnostics_AllPrivateDirectoriesAreInaccessible'))); + } + + return [$result]; + } + + private function getUrlList(array $testUrls) + { + $testUrlsList = ''; + foreach ($testUrls as $testUrl) { + $testUrlsList .= '
    ' . Common::sanitizeInputValue($testUrl); + } + return $testUrlsList; + } + + protected function isAccessible(DiagnosticResult $result, $testUrl, $publicIfResponseEquals, $publicIfResponseContains) + { + try { + $response = Http::sendHttpRequest($testUrl, $timeout = 2, null, null, null, false, false, true); + $status = $response['status']; + + if ($status >= 400 && $status < 500) { + return false; + } elseif ($status >= 300 && $status < 400) { + // follow the redirect + $response = Http::sendHttpRequest($testUrl, $timeout = 5, null, null, 5, false, false, true); + $isResolvedRedirectProtected = $response['status'] >= 400 && $response['status'] < 500; + if ($isResolvedRedirectProtected) { + // eg someone redirect from http to https or the other way around + return false; + } + + // we check for content if possible as they may redirect these files eg to /home or something else + if (!$publicIfResponseContains || !$publicIfResponseEquals) { + // it may or may not be an issue depending where they redirect to + // TODO ideally we make this more clear maybe? + $result->addItem(new DiagnosticResultItem(DiagnosticResult::STATUS_WARNING, $testUrl)); + return true; + } + + if (trim($response['data']) === $publicIfResponseEquals) { + // we assume it is publicly accessible because either the exact expected content is returned or because we don't check for content match + $result->addItem(new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, $testUrl)); + return true; + } elseif (strpos($response['data'], $publicIfResponseContains) !== false) { + // we assume it is publicly accessible because a unique content is included in the response or because we don't check for content contains + $result->addItem(new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, $testUrl)); + return true; + } + // in other cases we assume it's not publicly accessible because we didn't get any expected output in the response + // so it seems like they redirect eg to the homepage or another page + } else { + // we assume the file is accessible publicly + $result->addItem(new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, $testUrl)); + return true; + } + } catch (\Exception $e) { + $error = $e->getMessage(); + $result->addItem( + new DiagnosticResultItem( + DiagnosticResult::STATUS_WARNING, + Piwik::translate( + 'Diagnostics_PrivateDirectoryCantCheckUrl', + [Common::sanitizeInputValue($testUrl), Common::sanitizeInputValue($error)] + ) + ) + ); + } + return false; + } + + protected function computeAccessiblePaths(DiagnosticResult &$result, $baseUrl, array $testUrls): bool + { + $atLeastOneIsAccessible = false; + foreach ($testUrls as $path => $testUrl) { + if ($this->isAccessible($result, $testUrl, '', '')) { + $atLeastOneIsAccessible = true; + } + } + return $atLeastOneIsAccessible; + } + + abstract protected function addError(DiagnosticResult &$result); +} diff --git a/plugins/Diagnostics/Diagnostic/ArchiveInvalidationsInformational.php b/plugins/Diagnostics/Diagnostic/ArchiveInvalidationsInformational.php new file mode 100644 index 0000000000000000000000000000000000000000..ee76ea1aed90ed10e3f030247178a6e0a5f7f5bd --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/ArchiveInvalidationsInformational.php @@ -0,0 +1,118 @@ +translator = $translator; + } + + public function execute() + { + $results = []; + + if (SettingsPiwik::isMatomoInstalled()) { + $invalidationCounts = $this->getInvalidationCounts(); + $results[] = DiagnosticResult::informationalResult('Total Invalidation Count', $invalidationCounts['all'] ?? '0'); + $results[] = DiagnosticResult::informationalResult('In Progress Invalidation Count', $invalidationCounts[ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS] ?? '0'); + $results[] = DiagnosticResult::informationalResult('Scheduled Invalidation Count', $invalidationCounts[ArchiveInvalidator::INVALIDATION_STATUS_QUEUED] ?? '0'); + + $minMaxes = $this->getInvalidationMinMaxes(); + $results[] = DiagnosticResult::informationalResult('Earliest invalidation ts_started', $minMaxes['min_ts_started']); + $results[] = DiagnosticResult::informationalResult('Latest invalidation ts_started', $minMaxes['max_ts_started']); + $results[] = DiagnosticResult::informationalResult('Earliest invalidation ts_invalidated', $minMaxes['min_ts_invalidated']); + $results[] = DiagnosticResult::informationalResult('Latest invalidation ts_invalidated', $minMaxes['max_ts_invalidated']); + + $invalidationTypes = $this->getInvalidationTypes(); + $results[] = DiagnosticResult::informationalResult('Number of segment invalidations', $invalidationTypes['count_segment']); + $results[] = DiagnosticResult::informationalResult('Number of plugin invalidations', $invalidationTypes['count_plugin']); + $results[] = DiagnosticResult::informationalResult('List of plugins being invalidated', implode(', ', $invalidationTypes['plugins'])); + } + + return $results; + } + + public function getInvalidationCounts() + { + $table = Common::prefixTable('archive_invalidations'); + $sql = "SELECT COUNT(*) as `count`, status FROM `$table` GROUP BY status"; + + $rows = Db::fetchAll($sql); + + $result = []; + foreach ($rows as $row) { + $result[$row['status']] = $row['count']; + } + $result['all'] = array_sum($result); + return $result; + } + + public function getInvalidationMinMaxes() + { + $sql = "SELECT MIN(ts_started) as min_ts_started, MAX(ts_started) as max_ts_started, MIN(ts_invalidated) as min_ts_invalidated, MAX(ts_invalidated) as max_ts_invalidated FROM " + . Common::prefixTable('archive_invalidations'); + $row = Db::fetchRow($sql); + return $row; + } + + public function getInvalidationTypes() + { + $table = Common::prefixTable('archive_invalidations'); + + $pluginSql = 'IF(INSTR(`name`, \'.\') > 0, SUBSTRING_INDEX(`name`, \'.\', -1), NULL)'; + $nonPluginDoneFlagSql = 'IF(INSTR(`name`, \'.\') > 0, SUBSTRING_INDEX(`name`, \'.\', 1), `name`)'; + + $sql = "SELECT COUNT(*) as `count`, $pluginSql AS plugin, CHAR_LENGTH($nonPluginDoneFlagSql) > 32 AS is_segment_archive + FROM `$table` + GROUP BY plugin, is_segment_archive"; + + $result = [ + 'count_segment' => 0, + 'count_plugin' => 0, + 'plugins' => [], + ]; + + $rows = Db::fetchAll($sql); + foreach ($rows as $row) { + if (!empty($row['is_segment_archive'])) { + $result['count_segment'] += $row['count']; + } + + if (!empty($row['plugin'])) { + $result['count_plugin'] += $row['count']; + } + + $result['plugins'][] = $row['plugin']; + } + + $result['plugins'] = array_unique($result['plugins']); + $result['plugins'] = array_filter($result['plugins']); + $result['plugins'] = array_values($result['plugins']); + sort($result['plugins']); + + return $result; + } +} diff --git a/plugins/Diagnostics/Diagnostic/ConfigInformational.php b/plugins/Diagnostics/Diagnostic/ConfigInformational.php new file mode 100644 index 0000000000000000000000000000000000000000..10250518b97ccd5dd6034fcd90c85e7f733b32d9 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/ConfigInformational.php @@ -0,0 +1,84 @@ +translator = $translator; + } + + public function execute() + { + $results = []; + + if (SettingsPiwik::isMatomoInstalled()) { + $results[] = DiagnosticResult::informationalResult('Browser Segment Archiving Enabled', Rules::isBrowserArchivingAvailableForSegments()); + $results[] = DiagnosticResult::informationalResult('Development Mode Enabled', Development::isEnabled()); + $results[] = DiagnosticResult::informationalResult('Internet Enabled', SettingsPiwik::isInternetEnabled()); + $results[] = DiagnosticResult::informationalResult('Multi Server Environment', SettingsPiwik::isMultiServerEnvironment()); + $results[] = DiagnosticResult::informationalResult('Auto Update Enabled', SettingsPiwik::isAutoUpdateEnabled()); + $results[] = DiagnosticResult::informationalResult('Custom User Path', PIWIK_USER_PATH != PIWIK_DOCUMENT_ROOT); + $results[] = DiagnosticResult::informationalResult('Custom Include Path', PIWIK_INCLUDE_PATH != PIWIK_DOCUMENT_ROOT); + $results[] = DiagnosticResult::informationalResult('Release Channel', Config::getInstance()->General['release_channel']); + + $pluginsActivated = array(); + $pluginsDeactivated = array(); + $pluginsInvalid = array(); + $plugins = Manager::getInstance()->loadAllPluginsAndGetTheirInfo(); + foreach ($plugins as $pluginName => $plugin) { + $string = $pluginName; + if ( + !empty($plugin['info']['version']) + && !empty($plugin['uninstallable']) + && (!defined('PIWIK_TEST_MODE') || !PIWIK_TEST_MODE) + ) { + // we only want to show versions for plugins not shipped with core + // in tests we don't show version numbers to not always needing to update the screenshot + $string .= ' ' . $plugin['info']['version']; + } + if (!empty($plugin['activated'])) { + $pluginsActivated[] = $string; + } else { + $pluginsDeactivated[] = $string; + } + if (!empty($plugin['invalid'])) { + $pluginsInvalid[] = $string; + } + } + + $results[] = DiagnosticResult::informationalResult('Plugins Activated', implode(', ', $pluginsActivated)); + $results[] = DiagnosticResult::informationalResult('Plugins Deactivated', implode(', ', $pluginsDeactivated)); + $results[] = DiagnosticResult::informationalResult('Plugins Invalid', implode(', ', $pluginsInvalid)); + + if (!empty($GLOBALS['MATOMO_PLUGIN_DIRS'])) { + $results[] = DiagnosticResult::informationalResult('Custom Plugins Directories', json_encode($GLOBALS['MATOMO_PLUGIN_DIRS'])); + } + } + + return $results; + } +} diff --git a/plugins/Diagnostics/Diagnostic/CronArchivingCheck.php b/plugins/Diagnostics/Diagnostic/CronArchivingCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..4d7a84852c9c949b90842d048a1a219d46059280 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/CronArchivingCheck.php @@ -0,0 +1,92 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckCronArchiveProcess') . ' (' . + $this->translator->translate('Installation_FasterReportLoading') . ')'; + + if (SettingsPiwik::isMatomoInstalled()) { + $isBrowserTriggerEnabled = Rules::isBrowserTriggerEnabled(); + if ($isBrowserTriggerEnabled) { + $comment = $this->translator->translate('Diagnostics_BrowserTriggeredArchivingEnabled', [ + Url::getExternalLinkTag('https://matomo.org/docs/setup-auto-archiving/'), '']); + $result[] = DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment); + + $archiveLastStarted = Option::get(CronArchive::OPTION_ARCHIVING_STARTED_TS); + $thirtySixHoursAgoInSeconds = 36 * 3600; + if ($archiveLastStarted && $archiveLastStarted > (time() - $thirtySixHoursAgoInSeconds)) { + // auto archive was used recently... if they maybe only once ran core:archive then eventually it will correct + // itself and no longer show this + $formatter = new Formatter(); + $lastStarted = $formatter->getPrettyTimeFromSeconds(time() - $archiveLastStarted, true); + $label = $this->translator->translate('Diagnostics_BrowserAndAutoArchivingEnabledLabel'); + $comment = $this->translator->translate('Diagnostics_BrowserAndAutoArchivingEnabledComment', [ + Url::getExternalLinkTag('https://matomo.org/docs/setup-auto-archiving/'), '', $lastStarted]); + $result[] = DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment); + } + } + } + + $comment = ''; + + $process = new CliMulti(); + if ($process->supportsAsync()) { + $comment .= $this->translator->translate('General_Ok'); + $status = DiagnosticResult::STATUS_OK; + } else { + $reasons = CliMulti\Process::isSupportedWithReason(); + if (empty($reasons)) { + $reasonText = $this->translator->translate('General_Unknown'); + } else { + $reasonText = implode(', ', $reasons); + } + $comment .= $this->translator->translate('Installation_NotSupported') + . ' ' . $this->translator->translate('Goals_Optional') + . ' (' . $this->translator->translate('General_Reasons') . ': ' . $reasonText . ')' + . ' ' . $this->translator->translate( + 'General_LearnMore', + [Url::getExternalLinkTag('https://matomo.org/faq/troubleshooting/how-to-make-the-diagnostic-managing-processes-via-cli-to-display-ok/'), ''] + ); + $status = DiagnosticResult::STATUS_INFORMATIONAL; + } + + $label = $this->translator->translate('Installation_SystemCheckCronArchiveProcess') . ' - ' + . $this->translator->translate('Installation_SystemCheckCronArchiveProcessCLI'); + $result[] = DiagnosticResult::singleResult($label, $status, $comment); + + return $result; + } +} diff --git a/plugins/Diagnostics/Diagnostic/CronArchivingLastRunCheck.php b/plugins/Diagnostics/Diagnostic/CronArchivingLastRunCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..e146f9bb329879bbdb59c94128b8d82e15aec5f5 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/CronArchivingLastRunCheck.php @@ -0,0 +1,124 @@ +translator = $translator; + } + + public function execute() + { + if (!SettingsPiwik::isMatomoInstalled()) { + return []; + } + + $label = $this->translator->translate('Diagnostics_CronArchivingLastRunCheck'); + $commandToRerun = '' . $this->getArchivingCommand() . ''; + $coreArchiveShort = 'core:archive'; + $mailto = 'MAILTO'; + + // check cron archiving has been enabled + $isBrowserTriggerDisabled = !Rules::isBrowserTriggerEnabled(); + if (!$isBrowserTriggerDisabled) { + return []; + } + + // check archiving has been run + $lastRunTime = (int)Option::get(CronArchive::OPTION_ARCHIVING_FINISHED_TS); + if (empty($lastRunTime)) { + $comment = $this->translator->translate('Diagnostics_CronArchivingHasNotRun') + . '

    ' . $this->translator->translate( + 'Diagnostics_CronArchivingRunDetails', + [$coreArchiveShort, $mailto, $commandToRerun, Url::getExternalLinkTag('https://matomo.org/docs/setup-auto-archiving/'), ''] + ); + return [DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_ERROR, $comment)]; + } + + $lastRunTimePretty = Date::factory($lastRunTime)->getLocalized(DateTimeFormatProvider::DATETIME_FORMAT_LONG); + + $diffTime = self::getTimeSinceLastSuccessfulRun($lastRunTime); + + $formatter = new Formatter(); + $diffTimePretty = $formatter->getPrettyTimeFromSeconds($diffTime); + + $errorComment = $this->translator->translate('Diagnostics_CronArchivingHasNotRunInAWhile', [$lastRunTimePretty, $diffTimePretty]) + . '

    ' . + $this->translator->translate( + 'Diagnostics_CronArchivingRunDetails', + [$coreArchiveShort, $mailto, $commandToRerun, + Url::getExternalLinkTag('https://matomo.org/docs/setup-auto-archiving/'), + '', + ] + ); + + // check archiving has been run recently + if ($diffTime > self::SECONDS_IN_DAY * 2) { + $result = DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_ERROR, $errorComment); + } elseif ($diffTime > self::SECONDS_IN_DAY) { + $result = DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $errorComment); + } else { + $comment = $this->translator->translate('Diagnostics_CronArchivingRanSuccessfullyXAgo', $diffTimePretty); + $result = DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK, $comment); + } + + return [$result]; + } + + private function getArchivingCommand() + { + if (Url::isValidHost()) { + $domain = Config::getHostname($checkIfTrusted = true); + + return PIWIK_INCLUDE_PATH . '/console --matomo-domain=' . $domain . ' core:archive'; + } + + return PIWIK_INCLUDE_PATH . '/console core:archive'; + } + + public static function getTimeSinceLastSuccessfulRun($lastRunTime = null) + { + if (empty($lastRunTime)) { + $lastRunTime = (int)Option::get(CronArchive::OPTION_ARCHIVING_FINISHED_TS); + } + + if (empty($lastRunTime)) { + return null; + } + + $now = Date::now()->getTimestamp(); + $diffTime = $now - $lastRunTime; + + return $diffTime; + } +} diff --git a/plugins/Diagnostics/Diagnostic/DatabaseAbilitiesCheck.php b/plugins/Diagnostics/Diagnostic/DatabaseAbilitiesCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..ba8326e29375ec5f891b95a83dd42658e34cf353 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/DatabaseAbilitiesCheck.php @@ -0,0 +1,219 @@ +translator = $translator; + } + + public function execute() + { + if (!SettingsPiwik::isMatomoInstalled()) { + // Skip the diagnostic if Matomo is being installed + return []; + } + + $result = new DiagnosticResult($this->translator->translate('Installation_DatabaseAbilities')); + + $result->addItem($this->checkUtf8mb4Charset()); + $result->addItem($this->checkCollation()); + + if (Config::getInstance()->General['enable_load_data_infile']) { + $result->addItem($this->checkLoadDataInfile()); + } + + $result->addItem($this->checkTemporaryTables()); + $result->addItem($this->checkTransactionLevel()); + + $databaseVersion = Db::fetchOne('SELECT VERSION();'); + + if (strpos(strtolower($databaseVersion), 'mariadb') !== false && Config\DatabaseConfig::getConfigValue('schema') !== 'Mariadb') { + $comment = $this->translator->translate('Diagnostics_MariaDbNotConfigured'); + $result->addItem(new DiagnosticResultItem(DiagnosticResult::STATUS_INFORMATIONAL, $comment)); + } + + return [$result]; + } + + protected function checkUtf8mb4Charset() + { + $dbSettings = new Db\Settings(); + $charset = $dbSettings->getUsedCharset(); + + if (DbHelper::getDefaultCharset() === 'utf8mb4' && $charset === 'utf8mb4') { + return new DiagnosticResultItem(DiagnosticResult::STATUS_OK, 'UTF8mb4 charset'); + } + + if (DbHelper::getDefaultCharset() === 'utf8mb4') { + return new DiagnosticResultItem( + DiagnosticResult::STATUS_WARNING, + 'UTF8mb4 charset

    ' . + $this->translator->translate('Diagnostics_DatabaseUtf8mb4CharsetAvailableButNotUsed', '' . PIWIK_INCLUDE_PATH . '/console core:convert-to-utf8mb4') . + '

    ' . + $this->translator->translate('Diagnostics_DatabaseUtf8Requirement', ['�', + Url::getExternalLinkTag('https://matomo.org/faq/how-to-update/how-to-convert-the-database-to-utf8mb4-charset/'), '']) . + '
    ' + ); + } + + return new DiagnosticResultItem( + DiagnosticResult::STATUS_WARNING, + 'UTF8mb4 charset

    ' . + $this->translator->translate('Diagnostics_DatabaseUtf8mb4CharsetRecommended') . + '

    ' . + $this->translator->translate('Diagnostics_DatabaseUtf8Requirement', ['�', + Url::getExternalLinkTag('https://matomo.org/faq/how-to-update/how-to-convert-the-database-to-utf8mb4-charset/'), '']) . + '
    ' + ); + } + + protected function checkCollation(): DiagnosticResultItem + { + $dbSettings = new Db\Settings(); + $collation = $dbSettings->getUsedCollation(); + + if ('' !== $collation) { + return new DiagnosticResultItem(DiagnosticResult::STATUS_OK, 'Connection collation'); + } + + $collationConnection = Db::get()->fetchOne('SELECT @@collation_connection'); + $collationCharset = DbHelper::getDefaultCollationForCharset($dbSettings->getUsedCharset()); + + $message = sprintf( + 'Connection collation

    %s

    %s
    ', + $this->translator->translate('Diagnostics_DatabaseCollationNotConfigured'), + $this->translator->translate('Diagnostics_DatabaseCollationConnection', [$collationConnection]) + ); + + if ('' !== $collationCharset) { + $message .= $this->translator->translate('Diagnostics_DatabaseCollationCharset', [$collationCharset]) . '
    '; + } + + return new DiagnosticResultItem(DiagnosticResult::STATUS_WARNING, $message); + } + + protected function checkLoadDataInfile() + { + $optionTable = Common::prefixTable('option'); + $testOptionNames = array('test_system_check1', 'test_system_check2'); + + $loadDataInfile = false; + $errorMessage = null; + try { + $loadDataInfile = Db\BatchInsert::tableInsertBatch( + $optionTable, + array('option_name', 'option_value'), + array( + array($testOptionNames[0], '1'), + array($testOptionNames[1], '2'), + ), + $throwException = true, + $charset = 'latin1' + ); + } catch (\Exception $ex) { + $errorMessage = str_replace("\n", "
    ", $ex->getMessage()); + } + + // delete the temporary rows that were created + Db::exec("DELETE FROM `$optionTable` WHERE option_name IN ('" . implode("','", $testOptionNames) . "')"); + + if ($loadDataInfile) { + return new DiagnosticResultItem(DiagnosticResult::STATUS_OK, 'LOAD DATA INFILE'); + } + + $comment = sprintf( + 'LOAD DATA INFILE
    %s
    %s', + $this->translator->translate('Installation_LoadDataInfileUnavailableHelp', array( + 'LOAD DATA INFILE', + 'FILE', + )), + $this->translator->translate('Installation_LoadDataInfileRecommended') + ); + + if ($errorMessage) { + $comment .= sprintf( + '
    %s: %s
    %s', + $this->translator->translate('General_Error'), + $errorMessage, + 'Troubleshooting: ' . Url::getExternalLinkTag('https://matomo.org/faq/troubleshooting/faq_194') . 'FAQ on matomo.org' + ); + } + + return new DiagnosticResultItem(DiagnosticResult::STATUS_WARNING, $comment); + } + + protected function checkTemporaryTables() + { + $status = DiagnosticResult::STATUS_OK; + $comment = 'CREATE TEMPORARY TABLES'; + + try { + // create a temporary table + Db::exec("CREATE TEMPORARY TABLE `piwik_test_table_temp` ( + id INT, + val VARCHAR(5) NULL, + PRIMARY KEY (id) + )"); + + // insert an entry into the new temporary table + Db::exec('INSERT INTO `piwik_test_table_temp` (`id`, `val`) VALUES ("1", "val1");'); + + for ($i = 0; $i < 5; $i++) { + // try reading the entry a few times to ensure it doesn't fail, which might be possible when using load balanced databases + $result = Db::fetchRow('SELECT * FROM `piwik_test_table_temp` WHERE `id` = 1'); + + if (empty($result)) { + throw new \Exception('read failed'); + } + } + } catch (\Exception $e) { + $status = DiagnosticResult::STATUS_ERROR; + $comment .= '
    ' . $this->translator->translate('Diagnostics_MysqlTemporaryTablesWarning'); + $comment .= '
    Troubleshooting: ' . Url::getExternalLinkTag('https://matomo.org/faq/how-to-install/faq_23484/') . 'FAQ on matomo.org'; + } + + return new DiagnosticResultItem($status, $comment); + } + + protected function checkTransactionLevel() + { + $status = DiagnosticResult::STATUS_OK; + $comment = 'Changing transaction isolation level'; + + $level = new Db\TransactionLevel(Db::getReader()); + if (!$level->setTransactionLevelForNonLockingReads()) { + $status = DiagnosticResult::STATUS_WARNING; + $comment .= '
    ' . $this->translator->translate('Diagnostics_MysqlTransactionLevel'); + } else { + $level->restorePreviousStatus(); + } + + return new DiagnosticResultItem($status, $comment); + } +} diff --git a/plugins/Diagnostics/Diagnostic/DatabaseInformational.php b/plugins/Diagnostics/Diagnostic/DatabaseInformational.php new file mode 100644 index 0000000000000000000000000000000000000000..1dd4eb6c522bd4c6245065de1a579c5a00589b03 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/DatabaseInformational.php @@ -0,0 +1,76 @@ +translator = $translator; + } + + public function execute() + { + $results = []; + + if (SettingsPiwik::isMatomoInstalled()) { + $dbConfig = Config::getInstance()->database; + $results[] = DiagnosticResult::informationalResult('DB Prefix', $dbConfig['tables_prefix']); + $results[] = DiagnosticResult::informationalResult('DB Charset', $dbConfig['charset']); + $results[] = DiagnosticResult::informationalResult('DB Collation', $dbConfig['collation']); + $results[] = DiagnosticResult::informationalResult('DB Adapter', $dbConfig['adapter']); + $results[] = DiagnosticResult::informationalResult('MySQL Version', $this->getServerVersion()); + $results[] = DiagnosticResult::informationalResult('Num Tables', $this->getNumMatomoTables()); + } + + return $results; + } + + private function getServerVErsion() + { + try { + return Db::get()->getServerVersion(); + } catch (\Exception $e) { + return $e->getMessage(); + } + } + + private function getNumMatomoTables() + { + $prefix = Common::prefixTable(''); + + $results = null; + try { + $results = Db::get()->fetchAll('show tables like "' . $prefix . '%"'); + } catch (\Exception $e) { + return $e->getMessage(); + } + + if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) { + return '89'; // fails randomly in tests as it is sometimes eg 89 and sometimes 90 depending on the time + } + + return count($results); + } +} diff --git a/plugins/Diagnostics/Diagnostic/DbAdapterCheck.php b/plugins/Diagnostics/Diagnostic/DbAdapterCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..219d3904b90b6fcc005f257c4aa09258124ee9de --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/DbAdapterCheck.php @@ -0,0 +1,107 @@ +translator = $translator; + } + + public function execute() + { + $results = array(); + $results[] = $this->checkPdo(); + $results = array_merge($results, $this->checkDbAdapters()); + + return $results; + } + + private function checkPdo() + { + $label = 'PDO ' . $this->translator->translate('Installation_Extension'); + + if (extension_loaded('PDO')) { + $status = DiagnosticResult::STATUS_OK; + } else { + $status = DiagnosticResult::STATUS_WARNING; + } + + return DiagnosticResult::singleResult($label, $status); + } + + private function checkDbAdapters() + { + $results = array(); + $adapters = Adapter::getAdapters(); + + foreach ($adapters as $adapter) { + $label = $adapter . ' ' . $this->translator->translate('Installation_Extension'); + + $results[] = DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK); + } + + if (empty($adapters)) { + $label = $this->translator->translate('Installation_SystemCheckDatabaseExtensions'); + $comment = $this->translator->translate('Installation_SystemCheckDatabaseHelp'); + + $result = DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_ERROR, $comment); + $result->setLongErrorMessage($this->getLongErrorMessage()); + + $results[] = $result; + } + + return $results; + } + + private function getLongErrorMessage() + { + $message = '

    '; + + if (SettingsServer::isWindows()) { + $message .= $this->translator->translate( + 'Installation_SystemCheckWinPdoAndMysqliHelp', + array('

    extension=php_mysqli.dll
    extension=php_pdo.dll
    extension=php_pdo_mysql.dll
    ') + ); + } else { + $message .= $this->translator->translate( + 'Installation_SystemCheckPdoAndMysqliHelp', + array( + '

    --with-mysqli
    --with-pdo-mysql

    ', + '

    extension=mysqli.so
    extension=pdo.so
    extension=pdo_mysql.so
    ', + ) + ); + } + + $message .= $this->translator->translate('Installation_RestartWebServer') . '

    '; + $message .= $this->translator->translate('Installation_SystemCheckPhpPdoAndMysqli', array( + '', + '', + '', + '', + )); + $message .= '

    '; + + return $message; + } +} diff --git a/plugins/Diagnostics/Diagnostic/DbMaxPacket.php b/plugins/Diagnostics/Diagnostic/DbMaxPacket.php new file mode 100644 index 0000000000000000000000000000000000000000..6628fed8d21e523e1d45faff1cab75f6ba355305 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/DbMaxPacket.php @@ -0,0 +1,60 @@ +translator = $translator; + } + + public function execute() + { + if (!SettingsPiwik::isMatomoInstalled()) { + return array(); // only possible to perform check once we have DB connection + } + + $maxPacketBytes = Db::fetchRow("SHOW VARIABLES LIKE 'max_allowed_packet'"); + + $status = DiagnosticResult::STATUS_OK; + $label = $this->translator->translate('Diagnostics_MysqlMaxPacketSize'); + $comment = ''; + + $minSize = self::MIN_VALUE_MAX_PACKET_MB * 1000 * 1000; // not using 1024 just in case... this amount be good enough + if (!empty($maxPacketBytes['Value']) && $maxPacketBytes['Value'] < $minSize) { + $status = DiagnosticResult::STATUS_WARNING; + $formatter = new Formatter\Html(); + $pretty = $formatter->getPrettySizeFromBytes($maxPacketBytes['Value'], 'M', $precision = 1); + $configured = str_replace(array(' M', ' M'), 'MB', $pretty); + $comment = Piwik::translate( + 'Diagnostics_MysqlMaxPacketSizeWarning', + [ + Url::getExternalLinkTag('https://dev.mysql.com/doc/refman/en/packet-too-large.html'), + '', + '64MB', + $configured, + ] + ); + } + + return array(DiagnosticResult::singleResult($label, $status, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/DbOverSSLCheck.php b/plugins/Diagnostics/Diagnostic/DbOverSSLCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..91c322721a5152f20470c7528c59516ae6db95b7 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/DbOverSSLCheck.php @@ -0,0 +1,62 @@ +translator = $translator; + } + + public function execute() + { + $enable_ssl = Config::getInstance()->database['enable_ssl']; + if (!$enable_ssl) { + return array(); + } + + $label = $this->translator->translate('Installation_SystemCheckDatabaseSSL'); + + $cipher = Db::fetchRow("show status like 'Ssl_cipher'"); + if (!empty($cipher['Value'])) { + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK, $this->translator->translate('Installation_SystemCheckDatabaseSSLCipher') . ': ' . $cipher['Value'])); + } + + //no cipher, not working + $comment = sprintf($this->translator->translate('Installation_SystemCheckDatabaseSSLNotWorking'), "enable_ssl") . "
    "; + + // test ssl support + $ssl_support = Db::fetchRow("SHOW VARIABLES LIKE 'have_ssl'"); + if (!empty($ssl_support['Value'])) { + switch ($ssl_support['Value']) { + case 'YES': + $comment .= $this->translator->translate('Installation_SystemCheckDatabaseSSLOn'); + break; + case 'DISABLED': + $comment .= $this->translator->translate('Installation_SystemCheckDatabaseSSLDisabled'); + break; + case 'NO': + $comment .= $this->translator->translate('Installation_SystemCheckDatabaseSSLNo'); + break; + } + } + + $comment .= '
    ' . Url::getExternalLinkTag('https://matomo.org/faq/') . 'FAQ on matomo.org'; + + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/DbReaderCheck.php b/plugins/Diagnostics/Diagnostic/DbReaderCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..1b085367aa92da6cd0af2b702d230274c5c2cb0e --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/DbReaderCheck.php @@ -0,0 +1,55 @@ +translator = $translator; + } + + public function execute() + { + if (!SettingsPiwik::isMatomoInstalled()) { + // Skip the diagnostic if Matomo is being installed + return []; + } + + if (!Db::hasReaderConfigured()) { + // only show an entry when reader is actually configured + return []; + } + + $label = $this->translator->translate('Diagnostics_DatabaseReaderConnection'); + + try { + Db::getReader(); + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK, '')); + } catch (\Exception $e) { + } + + $comment = Piwik::translate('Installation_CannotConnectToDb'); + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/Diagnostic.php b/plugins/Diagnostics/Diagnostic/Diagnostic.php new file mode 100644 index 0000000000000000000000000000000000000000..54f8f285c3a232ad184f34fa61272f7a5baecdca --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/Diagnostic.php @@ -0,0 +1,45 @@ +label = $label; + } + + /** + * @param string $label + * @param string $status + * @param string $comment + * @return DiagnosticResult + */ + public static function singleResult($label, $status, $comment = '') + { + $result = new self($label); + $result->addItem(new DiagnosticResultItem($status, $comment)); + return $result; + } + + /** + * @param string $label + * @param string $comment + * @param bool $escapeComment + * @return DiagnosticResult + */ + public static function informationalResult($label, $comment = '', $escapeComment = true) + { + if ($comment === true) { + $comment = '1'; + } elseif ($comment === false) { + $comment = '0'; + } + + if ($escapeComment) { + $comment = Common::sanitizeInputValue($comment); + } + + return self::singleResult($label, self::STATUS_INFORMATIONAL, $comment); + } + + /** + * @return string + */ + public function getLabel() + { + return $this->label; + } + + /** + * @return DiagnosticResultItem[] + */ + public function getItems() + { + return $this->items; + } + + public function addItem(DiagnosticResultItem $item) + { + $this->items[] = $item; + } + + /** + * @param DiagnosticResultItem[] $items + */ + public function setItems(array $items) + { + $this->items = $items; + } + + /** + * @return string + */ + public function getLongErrorMessage() + { + return $this->longErrorMessage; + } + + /** + * @param string $longErrorMessage + */ + public function setLongErrorMessage($longErrorMessage) + { + $this->longErrorMessage = $longErrorMessage; + } + + /** + * Returns the worst status of the items. + * + * @return string One of the `STATUS_*` constants. + */ + public function getStatus() + { + $status = self::STATUS_OK; + + foreach ($this->getItems() as $item) { + if ($item->getStatus() === self::STATUS_ERROR) { + return self::STATUS_ERROR; + } + + if ($item->getStatus() === self::STATUS_WARNING) { + $status = self::STATUS_WARNING; + } + } + + return $status; + } + + public function jsonSerialize(): array + { + return [ + 'label' => $this->label, + 'longErrorMessage' => $this->longErrorMessage, + 'items' => $this->items, + ]; + } +} diff --git a/plugins/Diagnostics/Diagnostic/DiagnosticResultItem.php b/plugins/Diagnostics/Diagnostic/DiagnosticResultItem.php new file mode 100644 index 0000000000000000000000000000000000000000..40a554b1574b784573c4e2f83ee13956d1ef5689 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/DiagnosticResultItem.php @@ -0,0 +1,58 @@ +status = $status; + $this->comment = $comment; + } + + /** + * @return string + */ + public function getStatus() + { + return $this->status; + } + + /** + * @return string + */ + public function getComment() + { + return $this->comment; + } + + public function jsonSerialize(): array + { + return [ + 'status' => $this->status, + 'comment' => $this->comment, + ]; + } +} diff --git a/plugins/Diagnostics/Diagnostic/FileIntegrityCheck.php b/plugins/Diagnostics/Diagnostic/FileIntegrityCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..44c07f266e1b2e90405effe680b74eed3605a5ae --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/FileIntegrityCheck.php @@ -0,0 +1,57 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckFileIntegrity'); + + if (Development::isEnabled()) { + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, '(Disabled in development mode)')); + } + + list($ok, $messages) = FileIntegrity::getFileIntegrityInformation(); + + if ($ok) { + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK, implode('
    ', $messages))); + } + + $comment = $this->translator->translate('General_FileIntegrityWarning'); + + // Keep only the 20 first lines else it becomes unmanageable + if (count($messages) > 20) { + $messages = array_slice($messages, 0, 20); + $messages[] = '...'; + } + $comment .= '

    '
    +            . implode("\n", $messages) . '
    '; + + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/ForceSSLCheck.php b/plugins/Diagnostics/Diagnostic/ForceSSLCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..64c90f042e41741370a6f8fd231bf17ab7c45201 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/ForceSSLCheck.php @@ -0,0 +1,64 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('General_ForcedSSL'); + + // special handling during install + if (!SettingsPiwik::isMatomoInstalled()) { + if (ProxyHttp::isHttps()) { + return []; + } + + $view = new View('@Diagnostics/force_ssl_link'); + $view->link = 'https://' . Url::getCurrentHost() . Url::getCurrentScriptName(false) . Url::getCurrentQueryString(); + $message = $view->render(); + return [DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $message)]; + } + + $forceSSLEnabled = (Config::getInstance()->General['force_ssl'] == 1); + + if ($forceSSLEnabled) { + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK)); + } + + $comment = $this->translator->translate('General_ForceSSLRecommended', ['force_ssl = 1', 'General']); + + if (!ProxyHttp::isHttps()) { + $comment .= '

    ' . $this->translator->translate('General_NotPossibleWithoutHttps'); + } + + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/GdExtensionCheck.php b/plugins/Diagnostics/Diagnostic/GdExtensionCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..36a7f3e31026867fa327d6978eb01c536793acfb --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/GdExtensionCheck.php @@ -0,0 +1,46 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckGDFreeType'); + + if (SettingsServer::isGdExtensionEnabled()) { + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK)); + } + + $comment = sprintf( + '%s
    %s', + $this->translator->translate('Installation_SystemCheckGDFreeType'), + $this->translator->translate('Installation_SystemCheckGDHelp') + ); + + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/HttpClientCheck.php b/plugins/Diagnostics/Diagnostic/HttpClientCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..1ed6de929553350c00bfe10ddddb0f03c688c497 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/HttpClientCheck.php @@ -0,0 +1,51 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckOpenURL'); + + $httpMethod = Http::getTransportMethod(); + + if ($httpMethod) { + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK, $httpMethod)); + } + + $canAutoUpdate = Filechecks::canAutoUpdate(); + + $comment = $this->translator->translate('Installation_SystemCheckOpenURLHelp'); + + if (! $canAutoUpdate) { + $comment .= '
    ' . $this->translator->translate('Installation_SystemCheckAutoUpdateHelp'); + } + + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/MatomoInformational.php b/plugins/Diagnostics/Diagnostic/MatomoInformational.php new file mode 100644 index 0000000000000000000000000000000000000000..cf5b29fc8977ffef9e1e1dc8c16d9a08353d115f --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/MatomoInformational.php @@ -0,0 +1,62 @@ +translator = $translator; + } + + public function execute() + { + $results = []; + + $results[] = DiagnosticResult::informationalResult('Matomo Version', Version::VERSION); + + if (SettingsPiwik::isMatomoInstalled()) { + $results[] = DiagnosticResult::informationalResult('Matomo Update History', Option::get(Updater::OPTION_KEY_MATOMO_UPDATE_HISTORY)); + $results[] = DiagnosticResult::informationalResult('Matomo Install Version', $this->getInstallVersion()); + $results[] = DiagnosticResult::informationalResult('Latest Available Version', Option::get(\Piwik\Plugins\CoreUpdater\Updater::OPTION_LATEST_VERSION)); + $results[] = DiagnosticResult::informationalResult('Is Git Deployment', SettingsPiwik::isGitDeployment()); + } + + return $results; + } + + private function getInstallVersion() + { + try { + $version = DbHelper::getInstallVersion(); + if (empty($version)) { + $version = 'Unknown - pre 3.8.'; + } + return $version; + } catch (\Exception $e) { + return $e->getMessage(); + } + } +} diff --git a/plugins/Diagnostics/Diagnostic/MemoryLimitCheck.php b/plugins/Diagnostics/Diagnostic/MemoryLimitCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..da5f13f645cb8eb44424ed1c2eaef2e36c6c6da8 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/MemoryLimitCheck.php @@ -0,0 +1,61 @@ +translator = $translator; + $this->minimumMemoryLimit = $minimumMemoryLimit; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckMemoryLimit'); + + SettingsServer::raiseMemoryLimitIfNecessary(); + + $memoryLimit = SettingsServer::getMemoryLimitValue(); + $comment = $memoryLimit . 'M'; + + if (false === $memoryLimit) { + $status = DiagnosticResult::STATUS_OK; + $comment = $this->translator->translate('Installation_SystemCheckMemoryNoMemoryLimitSet'); + } elseif ($memoryLimit >= $this->minimumMemoryLimit) { + $status = DiagnosticResult::STATUS_OK; + } else { + $status = DiagnosticResult::STATUS_WARNING; + $comment .= sprintf( + '
    %s
    %s', + $this->translator->translate('Installation_SystemCheckMemoryLimitHelp'), + $this->translator->translate('Installation_RestartWebServer') + ); + } + + return array(DiagnosticResult::singleResult($label, $status, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/NfsDiskCheck.php b/plugins/Diagnostics/Diagnostic/NfsDiskCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..3b991d6d6802c467bff2613bf7c70fc1addde27c --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/NfsDiskCheck.php @@ -0,0 +1,56 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_Filesystem'); + + if (! Filesystem::checkIfFileSystemIsNFS()) { + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK)); + } + + if (!SettingsPiwik::isMatomoInstalled()) { + $help = 'Installation_NfsFilesystemWarningSuffixInstall'; + } else { + $help = 'Installation_NfsFilesystemWarningSuffixAdmin'; + } + + $comment = sprintf( + '%s
    %s', + $this->translator->translate('Installation_NfsFilesystemWarning'), + $this->translator->translate($help) + ); + + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/PHPBinaryCheck.php b/plugins/Diagnostics/Diagnostic/PHPBinaryCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..f150ba283f080fe20035617b8c2c6d77d472dfd4 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/PHPBinaryCheck.php @@ -0,0 +1,43 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_PhpBinaryCheck'); + + if (PHP_INT_SIZE === 8) { + $status = DiagnosticResult::STATUS_OK; + $comment = ""; + } else { + $status = DiagnosticResult::STATUS_WARNING; + $comment = $this->translator->translate('Installation_PhpBinaryCheckHelp'); + } + + return array(DiagnosticResult::singleResult($label, $status, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/PageSpeedCheck.php b/plugins/Diagnostics/Diagnostic/PageSpeedCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..4d1c2ef1ec6f83b66f84946919dd872a43dcb9fd --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/PageSpeedCheck.php @@ -0,0 +1,91 @@ +translator = $translator; + $this->logger = $logger; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckPageSpeedDisabled'); + + if (! $this->isPageSpeedEnabled()) { + return [DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK)]; + } + + $comment = $this->translator->translate('Installation_SystemCheckPageSpeedWarning', [ + '(eg. Apache, Nginx or IIS)', + ]); + + return [DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)]; + } + + private function isPageSpeedEnabled() + { + try { + $matomoUrl = SettingsPiwik::getPiwikUrl(); + } catch (\Exception $e) { + $matomoUrl = Url::getCurrentUrlWithoutQueryString(); + } + + if (empty($matomoUrl)) { + // skip this check if we can't determine the matomo url (e.g. on command line) + return false; + } + + $url = $matomoUrl . '?module=Installation&action=getEmptyPageForSystemCheck'; + + try { + $page = Http::sendHttpRequest( + $url, + $timeout = 1, + $userAgent = null, + $destinationPath = null, + $followDepth = 0, + $acceptLanguage = false, + $byteRange = false, + // Return headers + $getExtendedInfo = true + ); + } catch (\Exception $e) { + $this->logger->info('Unable to test if mod_pagespeed is enabled: the request to {url} failed', [ + 'url' => $url, + ]); + // If the test failed, we assume Page speed is not enabled + return false; + } + + return isset($page['headers']['X-Mod-Pagespeed']) || isset($page['headers']['X-Page-Speed']); + } +} diff --git a/plugins/Diagnostics/Diagnostic/PhpExtensionsCheck.php b/plugins/Diagnostics/Diagnostic/PhpExtensionsCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..2882cac88e54a821325e446d8ddb5e7cf381ecef --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/PhpExtensionsCheck.php @@ -0,0 +1,84 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckExtensions'); + + $result = new DiagnosticResult($label); + $longErrorMessage = ''; + + $requiredExtensions = $this->getRequiredExtensions(); + + foreach ($requiredExtensions as $extension) { + if (! extension_loaded($extension)) { + $status = DiagnosticResult::STATUS_ERROR; + $comment = $extension . ': ' . $this->translator->translate('Installation_RestartWebServer'); + $longErrorMessage .= '

    ' . $this->getHelpMessage($extension) . '

    '; + } else { + $status = DiagnosticResult::STATUS_OK; + $comment = $extension; + } + + $result->addItem(new DiagnosticResultItem($status, $comment)); + } + + $result->setLongErrorMessage($longErrorMessage); + + return array($result); + } + + /** + * @return string[] + */ + private function getRequiredExtensions() + { + $requiredExtensions = array( + 'zlib', + 'json', + 'filter', + 'hash', + 'session', + ); + + return $requiredExtensions; + } + + private function getHelpMessage($missingExtension) + { + $messages = array( + 'zlib' => 'Installation_SystemCheckZlibHelp', + 'json' => 'Installation_SystemCheckWarnJsonHelp', + 'filter' => 'Installation_SystemCheckFilterHelp', + 'hash' => 'Installation_SystemCheckHashHelp', + 'session' => 'Installation_SystemCheckSessionHelp', + ); + + return $this->translator->translate($messages[$missingExtension]); + } +} diff --git a/plugins/Diagnostics/Diagnostic/PhpFunctionsCheck.php b/plugins/Diagnostics/Diagnostic/PhpFunctionsCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..5e4005547c7ba56b61ecb1866250e16cf231b11c --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/PhpFunctionsCheck.php @@ -0,0 +1,114 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckFunctions'); + + $result = new DiagnosticResult($label); + + foreach ($this->getRequiredFunctions() as $function) { + if (! self::functionExists($function)) { + $status = DiagnosticResult::STATUS_ERROR; + $comment = sprintf( + '%s

    %s
    %s
    ', + $function, + $this->getHelpMessage($function), + $this->translator->translate('Installation_RestartWebServer') + ); + } else { + $status = DiagnosticResult::STATUS_OK; + $comment = $function; + } + + $result->addItem(new DiagnosticResultItem($status, $comment)); + } + + return array($result); + } + + /** + * @return string[] + */ + private function getRequiredFunctions() + { + return array( + 'debug_backtrace', + 'escapeshellarg', + 'eval', + 'hash', + 'gzcompress', + 'gzuncompress', + 'pack', + ); + } + + /** + * Tests if a function exists. Also handles the case where a function is disabled via Suhosin. + * + * @param string $function + * @return bool + */ + public static function functionExists($function) + { + // eval() is a language construct + if ($function == 'eval') { + // does not check suhosin.executor.eval.whitelist (or blacklist) + if (extension_loaded('suhosin')) { + return @ini_get("suhosin.executor.disable_eval") != "1"; + } + return true; + } + + $exists = function_exists($function); + + if (extension_loaded('suhosin')) { + $blacklist = @ini_get("suhosin.executor.func.blacklist"); + if (!empty($blacklist)) { + $blacklistFunctions = array_map('strtolower', array_map('trim', explode(',', $blacklist))); + return $exists && !in_array($function, $blacklistFunctions); + } + } + + return $exists; + } + + private function getHelpMessage($missingFunction) + { + $messages = array( + 'debug_backtrace' => 'Installation_SystemCheckDebugBacktraceHelp', + 'eval' => 'Installation_SystemCheckEvalHelp', + 'hash' => 'Installation_SystemCheckHashHelp', + 'gzcompress' => 'Installation_SystemCheckGzcompressHelp', + 'gzuncompress' => 'Installation_SystemCheckGzuncompressHelp', + 'pack' => 'Installation_SystemCheckPackHelp', + ); + + return $this->translator->translate($messages[$missingFunction]); + } +} diff --git a/plugins/Diagnostics/Diagnostic/PhpInformational.php b/plugins/Diagnostics/Diagnostic/PhpInformational.php new file mode 100644 index 0000000000000000000000000000000000000000..470d49008e8859d12445421a4351df0abefd9636 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/PhpInformational.php @@ -0,0 +1,113 @@ +translator = $translator; + } + + public function execute() + { + $results = []; + + if (defined('PHP_OS') && PHP_OS) { + $results[] = DiagnosticResult::informationalResult('PHP_OS', PHP_OS); + } + if (SettingsPiwik::isMatomoInstalled() && defined('PHP_BINARY') && PHP_BINARY) { + $results[] = DiagnosticResult::informationalResult('PHP_BINARY', PHP_BINARY); + } + + $isGlobalConfigIniAccessible = true; // Assume true if not installed yet + // Only attempt to check file accessibility if the config setting allows it + $disableFileAccessCheck = (GeneralConfig::getConfigValue('enable_required_directories_diagnostic') == 0); + if (!$disableFileAccessCheck) { + if (SettingsPiwik::isMatomoInstalled()) { + $rpd = new RequiredPrivateDirectories($this->translator); + $isGlobalConfigIniAccessible = $rpd->isGlobalConfigIniAccessible(); + } + } + + if (strpos(strtolower(php_sapi_name()), 'fpm-fcgi') !== false && $isGlobalConfigIniAccessible && !$disableFileAccessCheck) { + // Using PHP-FPM and private files are accessible + $comment = php_sapi_name() . "

    "; + + if (!empty($_SERVER['SERVER_SOFTWARE'])) { + if (strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'apache') !== false) { + $comment .= $this->translator->translate('Diagnostics_PHPFPMWarningApache', [ + 'ProxyPass /config !', 'mod_proxy_fcgi.c', 'ProxyPassMatch']); + } elseif (strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'nginx') !== false) { + $comment .= $this->translator->translate('Diagnostics_PHPFPMWarningNginx', [ + '', '']); + } else { + $comment .= $this->translator->translate('Diagnostics_PHPFPMWarningGeneric'); + } + } else { + $comment .= $this->translator->translate('Diagnostics_PHPFPMWarningGeneric'); + } + $results[] = DiagnosticResult::singleResult('PHP SAPI', DiagnosticResult::STATUS_WARNING, $comment); + } else { + $results[] = DiagnosticResult::informationalResult('PHP SAPI', php_sapi_name()); + } + + if (SettingsPiwik::isMatomoInstalled()) { + $cliPhp = new CliPhp(); + $binary = $cliPhp->findPhpBinary(); + if (!empty($binary)) { + $binary = basename($binary); + $rows[] = array( + 'name' => 'PHP Found Binary', + 'value' => $binary, + ); + } + $results[] = DiagnosticResult::informationalResult('Timezone Version', timezone_version_get()); + } + $results[] = DiagnosticResult::informationalResult('PHP Timezone', date_default_timezone_get()); + $results[] = DiagnosticResult::informationalResult('PHP Time', time()); + $results[] = DiagnosticResult::informationalResult('PHP Datetime', Date::now()->getDatetime()); + + $disabled_functions = ini_get('disable_functions'); + $disabled_functions = implode(", ", explode(",", $disabled_functions)); + if (!empty($disabled_functions)) { + $results[] = DiagnosticResult::informationalResult('PHP Disabled functions', $disabled_functions); + } + + foreach (['max_execution_time', 'post_max_size', 'max_input_vars', 'zlib.output_compression'] as $iniSetting) { + $results[] = DiagnosticResult::informationalResult('PHP INI ' . $iniSetting, @ini_get($iniSetting)); + } + + if (function_exists('curl_version')) { + $curl_version = curl_version(); + $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version']; + $results[] = DiagnosticResult::informationalResult('Curl Version', $curl_version); + } + $suhosin_installed = ( extension_loaded('suhosin') || ( defined('SUHOSIN_PATCH') && constant('SUHOSIN_PATCH') ) ); + + $results[] = DiagnosticResult::informationalResult('Suhosin Installed', $suhosin_installed); + + return $results; + } +} diff --git a/plugins/Diagnostics/Diagnostic/PhpSettingsCheck.php b/plugins/Diagnostics/Diagnostic/PhpSettingsCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..0a86dc5cfa9c7e597c8f3f0e2ee99759346b5241 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/PhpSettingsCheck.php @@ -0,0 +1,70 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckSettings'); + + $result = new DiagnosticResult($label); + + foreach ($this->getRequiredSettings() as $setting) { + if (!$setting->check()) { + $status = $setting->getErrorResult(); + $comment = sprintf( + '%s

    %s
    %s
    ', + $setting, + $this->translator->translate('Installation_SystemCheckPhpSetting', array($setting)), + $this->translator->translate('Installation_RestartWebServer') + ); + } else { + $status = DiagnosticResult::STATUS_OK; + $comment = $setting; + } + + $result->addItem(new DiagnosticResultItem($status, $comment)); + } + + return array($result); + } + + /** + * @return RequiredPhpSetting[] + */ + private function getRequiredSettings() + { + $requiredSettings[] = new RequiredPhpSetting('session.auto_start', 0); + + $maxExecutionTime = new RequiredPhpSetting('max_execution_time', 0); + $maxExecutionTime->addRequiredValue(-1, '='); + $maxExecutionTime->addRequiredValue(30, '>='); + $maxExecutionTime->setErrorResult(DiagnosticResult::STATUS_WARNING); + $requiredSettings[] = $maxExecutionTime; + + return $requiredSettings; + } +} diff --git a/plugins/Diagnostics/Diagnostic/PhpVersionCheck.php b/plugins/Diagnostics/Diagnostic/PhpVersionCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..581b570176d4b912bb02e94eeb0962c198c1a2f7 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/PhpVersionCheck.php @@ -0,0 +1,60 @@ +translator = $translator; + } + + public function execute() + { + global $piwik_minimumPHPVersion; + + $actualVersion = PHP_VERSION; + + $label = sprintf( + '%s >= %s', + $this->translator->translate('Installation_SystemCheckPhp'), + $piwik_minimumPHPVersion + ); + + if ($this->isPhpVersionValid($piwik_minimumPHPVersion, $actualVersion)) { + $status = DiagnosticResult::STATUS_OK; + $comment = $actualVersion; + } else { + $status = DiagnosticResult::STATUS_ERROR; + $comment = sprintf( + '%s: %s', + $this->translator->translate('General_Error'), + $this->translator->translate('General_Required', array($label)) + ); + } + + return array(DiagnosticResult::singleResult($label, $status, $comment)); + } + + private function isPhpVersionValid($requiredVersion, $actualVersion) + { + return version_compare($requiredVersion, $actualVersion) <= 0; + } +} diff --git a/plugins/Diagnostics/Diagnostic/RecommendedExtensionsCheck.php b/plugins/Diagnostics/Diagnostic/RecommendedExtensionsCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..f266d5cdd077cbe59854f7f785f64fea3b3be9d1 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/RecommendedExtensionsCheck.php @@ -0,0 +1,81 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckOtherExtensions'); + + $result = new DiagnosticResult($label); + + $loadedExtensions = @get_loaded_extensions(); + $loadedExtensions = array_map(function ($extension) { + return strtolower($extension); + }, $loadedExtensions); + + foreach ($this->getRecommendedExtensions() as $extension) { + if (! in_array(strtolower($extension), $loadedExtensions)) { + $status = DiagnosticResult::STATUS_WARNING; + $comment = $extension . '
    ' . $this->getHelpMessage($extension); + } else { + $status = DiagnosticResult::STATUS_OK; + $comment = $extension; + } + + $result->addItem(new DiagnosticResultItem($status, $comment)); + } + + return array($result); + } + + /** + * @return string[] + */ + private function getRecommendedExtensions() + { + return array( + 'json', + 'libxml', + 'dom', + 'SimpleXML', + 'openssl', + ); + } + + private function getHelpMessage($missingExtension) + { + $messages = array( + 'json' => 'Installation_SystemCheckWarnJsonHelp', + 'libxml' => 'Installation_SystemCheckWarnLibXmlHelp', + 'dom' => 'Installation_SystemCheckWarnDomHelp', + 'SimpleXML' => 'Installation_SystemCheckWarnSimpleXMLHelp', + 'openssl' => 'Installation_SystemCheckWarnOpensslHelp', + ); + + return $this->translator->translate($messages[$missingExtension]); + } +} diff --git a/plugins/Diagnostics/Diagnostic/RecommendedFunctionsCheck.php b/plugins/Diagnostics/Diagnostic/RecommendedFunctionsCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..04fe426b0674bb04cb15c6302bc7156f4b37cd7b --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/RecommendedFunctionsCheck.php @@ -0,0 +1,85 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckOtherFunctions'); + + $result = new DiagnosticResult($label); + + foreach ($this->getRecommendedFunctions() as $function) { + if (! PhpFunctionsCheck::functionExists($function)) { + $status = DiagnosticResult::STATUS_WARNING; + $comment = $function . '
    ' . $this->getHelpMessage($function); + } else { + $status = DiagnosticResult::STATUS_OK; + $comment = $function; + } + + $result->addItem(new DiagnosticResultItem($status, $comment)); + } + + return array($result); + } + + /** + * @return string[] + */ + private function getRecommendedFunctions() + { + return array( + 'shell_exec', + 'set_time_limit', + 'mail', + 'parse_ini_file', + 'glob', + 'gzopen', + 'md5_file', + 'hash_file', + ); + } + + private function getHelpMessage($function) + { + $messages = array( + 'shell_exec' => 'Installation_SystemCheckShellExecHelp', + 'set_time_limit' => 'Installation_SystemCheckTimeLimitHelp', + 'mail' => 'Installation_SystemCheckMailHelp', + 'parse_ini_file' => 'Installation_SystemCheckParseIniFileHelp', + 'glob' => 'Installation_SystemCheckGlobHelp', + 'gzopen' => 'Installation_SystemCheckZlibHelp', + ); + + $translation_params = [ + 'shell_exec' => [Url::getExternalLinkTag('https://matomo.org/faq/troubleshooting/how-to-make-the-diagnostic-managing-processes-via-cli-to-display-ok/'), ''], + ]; + + return $this->translator->translate($messages[$function], $translation_params[$function] ?? []); + } +} diff --git a/plugins/Diagnostics/Diagnostic/RecommendedPrivateDirectories.php b/plugins/Diagnostics/Diagnostic/RecommendedPrivateDirectories.php new file mode 100644 index 0000000000000000000000000000000000000000..7a631b733d05240b5123efd8fa8600ffce1a031b --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/RecommendedPrivateDirectories.php @@ -0,0 +1,39 @@ +addItem(new DiagnosticResultItem( + DiagnosticResult::STATUS_INFORMATIONAL, + $this->translator->translate('Diagnostics_UrlsAccessibleViaBrowser') . ' ' . + $this->translator->translate('General_ReadThisToLearnMore', [ + Url::getExternalLinkTag('https://matomo.org/faq/troubleshooting/how-do-i-fix-the-error-private-directories-are-accessible/'), + '', + ]) + )); + } +} diff --git a/plugins/Diagnostics/Diagnostic/ReportInformational.php b/plugins/Diagnostics/Diagnostic/ReportInformational.php new file mode 100644 index 0000000000000000000000000000000000000000..28b91fe756c6e32f608e43e7d59080d3889b03f6 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/ReportInformational.php @@ -0,0 +1,94 @@ +translator = $translator; + } + + public function execute() + { + $results = []; + + if (SettingsPiwik::isMatomoInstalled()) { + $results[] = DiagnosticResult::informationalResult('Had visits in last 1 day', $this->hadVisitsInLastDays(1)); + $results[] = DiagnosticResult::informationalResult('Had visits in last 3 days', $this->hadVisitsInLastDays(3)); + $results[] = DiagnosticResult::informationalResult('Had visits in last 5 days', $this->hadVisitsInLastDays(5)); + $archiveStart = Option::get(CronArchive::OPTION_ARCHIVING_STARTED_TS); + $results[] = DiagnosticResult::informationalResult('Archive Time Last Started', ($archiveStart ? date("Y-m-d H:i:s", $archiveStart) : '-')); + $archiveEnd = Option::get(CronArchive::OPTION_ARCHIVING_FINISHED_TS); + $results[] = DiagnosticResult::informationalResult('Archive Time Last Finished', ($archiveEnd ? date("Y-m-d H:i:s", $archiveEnd) : '-')); + } + + return $results; + } + + private function hadVisitsInLastDays($numDays) + { + $table = Common::prefixTable('log_visit'); + $time = Date::now()->subDay($numDays)->getDatetime(); + + try { + $idSites = $this->getImplodedIdSitesSecure(); + $row = Db::fetchOne('SELECT idsite from `' . $table . '` where idsite in (' . $idSites . ') and visit_last_action_time > ? LIMIT 1', $time); + } catch (\Exception $e) { + $row = null; + } + + if ($numDays === 1 && defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) { + return '0'; // fails randomly in tests + } + + if (!empty($row)) { + return '1'; + } + return '0'; + } + + private function getImplodedIdSitesSecure() + { + if (empty($this->idSiteCache)) { + $idSites = null; + /* + * Performed as super user to ensure we are able to fetch all available site ids. + */ + Access::doAsSuperUser(function () use (&$idSites) { + $idSites = Site::getIdSitesFromIdSitesString('all'); + }); + $idSites = array_map('intval', $idSites); + $this->idSiteCache = implode(',', $idSites); + } + + return $this->idSiteCache; + } +} diff --git a/plugins/Diagnostics/Diagnostic/RequiredPhpSetting.php b/plugins/Diagnostics/Diagnostic/RequiredPhpSetting.php new file mode 100644 index 0000000000000000000000000000000000000000..d43ab00df1d85caf37a6d6f69f4c887302185388 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/RequiredPhpSetting.php @@ -0,0 +1,107 @@ +setting = $setting; + $this->addRequiredValue($requiredValue, $operator); + } + + /** + * @param int $requiredValue + * @param string $operator + * + * @return $this + */ + public function addRequiredValue($requiredValue, $operator) + { + if (!is_int($requiredValue)) { + throw new \InvalidArgumentException('Required value must be an integer.'); + } + + $this->requiredValues[] = array( + 'requiredValue' => $requiredValue, + 'operator' => $operator, + 'isValid' => null, + ); + + return $this; + } + + /** + * @param $errorResult + * + * @return $this + */ + public function setErrorResult($errorResult) + { + if ($errorResult !== DiagnosticResult::STATUS_WARNING && $errorResult !== DiagnosticResult::STATUS_ERROR) { + throw new \InvalidArgumentException('Error result must be either DiagnosticResult::STATUS_WARNING or DiagnosticResult::STATUS_ERROR.'); + } + + $this->errorResult = $errorResult; + + return $this; + } + + /** + * @return string + */ + public function getErrorResult() + { + return $this->errorResult; + } + + /** + * Checks required values against php.ini value. + * + * @return bool + */ + public function check() + { + $currentValue = (int) ini_get($this->setting); + + $return = false; + foreach ($this->requiredValues as $key => $requiredValue) { + $this->requiredValues[$key]['isValid'] = version_compare($currentValue, $requiredValue['requiredValue'], $requiredValue['operator']); + + if ($this->requiredValues[$key]['isValid']) { + $return = true; + } + } + + return $return; + } + + public function __toString(): string + { + $checks = array(); + foreach ($this->requiredValues as $requiredValue) { + $checks[] = $requiredValue['operator'] . ' ' . $requiredValue['requiredValue']; + } + + return $this->setting . ' ' . implode(' OR ', $checks); + } + + public function jsonSerialize(): string + { + return $this->__toString(); + } +} diff --git a/plugins/Diagnostics/Diagnostic/RequiredPrivateDirectories.php b/plugins/Diagnostics/Diagnostic/RequiredPrivateDirectories.php new file mode 100644 index 0000000000000000000000000000000000000000..64df3855a74657a6f90de01b6e06fb32be1feff4 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/RequiredPrivateDirectories.php @@ -0,0 +1,53 @@ +translator->translate('Diagnostics_PrivateDirectoryIsAccessible'); + if ($this->configIniAccessible) { + $pathIsAccessible .= '

    ' . $this->translator->translate('Diagnostics_ConfigIniAccessible'); + } + $pathIsAccessible .= '

    ' . Url::getExternalLinkTag('https://matomo.org/faq/troubleshooting/how-do-i-fix-the-error-private-directories-are-accessible/') . $this->translator->translate('General_ReadThisToLearnMore', ['', '']) . ''; + $result->setLongErrorMessage($pathIsAccessible); + } + + protected function computeAccessiblePaths(DiagnosticResult &$result, $baseUrl, array $testUrls): bool + { + $this->configIniAccessible = $this->isAccessible($result, $baseUrl . 'config/config.ini.php', ';', 'trusted_hosts[]'); + $atLeastOneIsAccessible = parent::computeAccessiblePaths($result, $baseUrl, $testUrls); + return $this->configIniAccessible || $atLeastOneIsAccessible; + } + + public function isGlobalConfigIniAccessible() + { + $baseUrl = SettingsPiwik::getPiwikUrl(); + if (!Common::stringEndsWith($baseUrl, '/')) { + $baseUrl .= '/'; + } + return $this->isAccessible(new DiagnosticResult(''), $baseUrl . 'config/global.ini.php', ';', 'trusted_hosts[]'); + } +} diff --git a/plugins/Diagnostics/Diagnostic/ServerInformational.php b/plugins/Diagnostics/Diagnostic/ServerInformational.php new file mode 100644 index 0000000000000000000000000000000000000000..50521b0301d201570ccedb3f6398d0becf369147 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/ServerInformational.php @@ -0,0 +1,61 @@ +translator = $translator; + } + + public function execute() + { + $results = []; + + if (!empty($_SERVER['SERVER_SOFTWARE'])) { + $isGlobalConfigIniAccessible = true; // Assume true if not installed yet + + // Only attempt to check file accessibility if the config setting allows it + $disableFileAccessCheck = (GeneralConfig::getConfigValue('enable_required_directories_diagnostic') == 0); + + if (!$disableFileAccessCheck) { + if (SettingsPiwik::isMatomoInstalled()) { + $rpd = new RequiredPrivateDirectories($this->translator); + $isGlobalConfigIniAccessible = $rpd->isGlobalConfigIniAccessible(); + } + } + + if (strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'nginx') !== false && $isGlobalConfigIniAccessible && !$disableFileAccessCheck) { + $comment = $_SERVER['SERVER_SOFTWARE'] . "

    "; + $comment .= $this->translator->translate('Diagnostics_HtaccessWarningNginx', [ + '', '']); + + $results[] = DiagnosticResult::singleResult('Server Info', DiagnosticResult::STATUS_WARNING, $comment); + } else { + $results[] = DiagnosticResult::informationalResult('Server Info', $_SERVER['SERVER_SOFTWARE']); + } + } + + return $results; + } +} diff --git a/plugins/Diagnostics/Diagnostic/TimezoneCheck.php b/plugins/Diagnostics/Diagnostic/TimezoneCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..b15779d7849b3cb8bf191bdf954e9eb6aaed0085 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/TimezoneCheck.php @@ -0,0 +1,47 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('SitesManager_Timezone'); + + if (SettingsServer::isTimezoneSupportEnabled()) { + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK)); + } + + $comment = sprintf( + '%s
    %s.', + $this->translator->translate('SitesManager_AdvancedTimezoneSupportNotFound'), + Url::getExternalLinkTag('https://php.net/manual/en/datetime.installation.php') . 'Timezone PHP documentation' + ); + + return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/TrackerCheck.php b/plugins/Diagnostics/Diagnostic/TrackerCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..174b0bf3ebced7c411d944a0ebac5bd82559f543 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/TrackerCheck.php @@ -0,0 +1,51 @@ +translator = $translator; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckTracker'); + + $trackerStatus = Common::getRequestVar('trackerStatus', 0, 'int'); + + if ($trackerStatus == 0) { + $status = DiagnosticResult::STATUS_OK; + $comment = ''; + } else { + $status = DiagnosticResult::STATUS_WARNING; + $comment = sprintf( + '%s
    %s
    %s', + $trackerStatus, + $this->translator->translate('Installation_SystemCheckTrackerHelp'), + $this->translator->translate('Installation_RestartWebServer') + ); + } + + return array(DiagnosticResult::singleResult($label, $status, $comment)); + } +} diff --git a/plugins/Diagnostics/Diagnostic/UserInformational.php b/plugins/Diagnostics/Diagnostic/UserInformational.php new file mode 100644 index 0000000000000000000000000000000000000000..87155eb07be01f14d15cf5cc7bd09745ca2b4684 --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/UserInformational.php @@ -0,0 +1,42 @@ +translator = $translator; + } + + public function execute() + { + $results = []; + + if (!empty($_SERVER['HTTP_USER_AGENT'])) { + $results[] = DiagnosticResult::informationalResult('User Agent', $_SERVER['HTTP_USER_AGENT']); + } + + $results[] = DiagnosticResult::informationalResult('Browser Language', Common::getBrowserLanguage()); + + return $results; + } +} diff --git a/plugins/Diagnostics/Diagnostic/WriteAccessCheck.php b/plugins/Diagnostics/Diagnostic/WriteAccessCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..f1692ef3aef050c53eebf7506749bf2682fe794a --- /dev/null +++ b/plugins/Diagnostics/Diagnostic/WriteAccessCheck.php @@ -0,0 +1,103 @@ +translator = $translator; + $this->tmpPath = $tmpPath; + } + + public function execute() + { + $label = $this->translator->translate('Installation_SystemCheckWriteDirs'); + + $result = new DiagnosticResult($label); + + $directories = Filechecks::checkDirectoriesWritable($this->getDirectories()); + + $error = false; + foreach ($directories as $directory => $isWritable) { + if ($isWritable) { + $status = DiagnosticResult::STATUS_OK; + } else { + $status = DiagnosticResult::STATUS_ERROR; + $error = true; + } + + $result->addItem(new DiagnosticResultItem($status, $directory)); + } + + if ($error) { + $longErrorMessage = $this->translator->translate('Installation_SystemCheckWriteDirsHelp'); + $longErrorMessage .= '
      '; + foreach ($directories as $directory => $isWritable) { + if (! $isWritable) { + $longErrorMessage .= sprintf('
    • chmod a+w %s
    • ', $directory); + } + } + $longErrorMessage .= '
    '; + $result->setLongErrorMessage($longErrorMessage); + } + + return array($result); + } + + /** + * @return string[] + */ + private function getDirectories() + { + $directoriesToCheck = array( + $this->tmpPath, + $this->tmpPath . '/assets/', + $this->tmpPath . '/cache/', + $this->tmpPath . '/climulti/', + $this->tmpPath . '/latest/', + $this->tmpPath . '/logs/', + $this->tmpPath . '/logos/', + $this->tmpPath . '/sessions/', + $this->tmpPath . '/tcpdf/', + StaticContainer::get('path.tmp.templates') + , + ); + + if (! DbHelper::isInstalled()) { + // at install, need /config to be writable (so we can create config.ini.php) + $directoriesToCheck[] = PIWIK_USER_PATH . '/config/'; + } + + return $directoriesToCheck; + } +} diff --git a/plugins/Diagnostics/DiagnosticReport.php b/plugins/Diagnostics/DiagnosticReport.php new file mode 100644 index 0000000000000000000000000000000000000000..971e0b500ab18d8717398002a0d7e5fa692f80ee --- /dev/null +++ b/plugins/Diagnostics/DiagnosticReport.php @@ -0,0 +1,132 @@ +mandatoryDiagnosticResults = $mandatoryDiagnosticResults; + $this->optionalDiagnosticResults = $optionalDiagnosticResults; + $this->informationalResults = $informationalResults; + + $this->computeErrorAndWarningCount(); + } + + /** + * @return bool + */ + public function hasErrors() + { + return $this->getErrorCount() > 0; + } + + /** + * @return bool + */ + public function hasWarnings() + { + return $this->getWarningCount() > 0; + } + + /** + * @return int + */ + public function getErrorCount() + { + return $this->errorCount; + } + + /** + * @return int + */ + public function getWarningCount() + { + return $this->warningCount; + } + + /** + * @return DiagnosticResult[] + */ + public function getAllResults() + { + return array_merge($this->mandatoryDiagnosticResults, $this->optionalDiagnosticResults, $this->informationalResults); + } + + /** + * @return DiagnosticResult[] + */ + public function getMandatoryDiagnosticResults() + { + return $this->mandatoryDiagnosticResults; + } + + /** + * @return DiagnosticResult[] + */ + public function getOptionalDiagnosticResults() + { + return $this->optionalDiagnosticResults; + } + + public function getInformationalResults() + { + return $this->informationalResults; + } + + private function computeErrorAndWarningCount() + { + foreach ($this->getAllResults() as $result) { + switch ($result->getStatus()) { + case DiagnosticResult::STATUS_ERROR: + $this->errorCount++; + break; + case DiagnosticResult::STATUS_WARNING: + $this->warningCount++; + break; + } + } + } +} diff --git a/plugins/Diagnostics/DiagnosticService.php b/plugins/Diagnostics/DiagnosticService.php new file mode 100644 index 0000000000000000000000000000000000000000..08e2f1a2b934e582f1eaf996be7c4994d73e337b --- /dev/null +++ b/plugins/Diagnostics/DiagnosticService.php @@ -0,0 +1,82 @@ +mandatoryDiagnostics = $this->removeDisabledDiagnostics($mandatoryDiagnostics, $disabledDiagnostics); + $this->optionalDiagnostics = $this->removeDisabledDiagnostics($optionalDiagnostics, $disabledDiagnostics); + $this->informationDiagnostics = $this->removeDisabledDiagnostics($informationDiagnostics, $disabledDiagnostics); + } + + /** + * @return DiagnosticReport + */ + public function runDiagnostics() + { + return new DiagnosticReport( + $this->run($this->mandatoryDiagnostics), + $this->run($this->optionalDiagnostics), + $this->run($this->informationDiagnostics) + ); + } + + /** + * @param Diagnostic[] $diagnostics + * @return DiagnosticResult[] + */ + private function run(array $diagnostics) + { + $results = array(); + + foreach ($diagnostics as $diagnostic) { + $results = array_merge($results, $diagnostic->execute()); + } + + return $results; + } + + private function removeDisabledDiagnostics(array $diagnostics, array $disabledDiagnostics) + { + return array_filter($diagnostics, function (Diagnostic $diagnostic) use ($disabledDiagnostics) { + return ! in_array($diagnostic, $disabledDiagnostics, true); + }); + } +} diff --git a/plugins/Diagnostics/Diagnostics.php b/plugins/Diagnostics/Diagnostics.php new file mode 100644 index 0000000000000000000000000000000000000000..b1b1d80e70a08917902667474d95b053295c749c --- /dev/null +++ b/plugins/Diagnostics/Diagnostics.php @@ -0,0 +1,76 @@ + 'getClientSideTranslationKeys', + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'Visualization.onNoData' => ['function' => 'onNoData', 'before' => true], + ); + } + + public function getClientSideTranslationKeys(&$translations) + { + $translations[] = 'Diagnostics_ConfigFileTitle'; + $translations[] = 'Diagnostics_ConfigFileIntroduction'; + $translations[] = 'Diagnostics_HideUnchanged'; + $translations[] = 'Diagnostics_Sections'; + } + + public function getStylesheetFiles(&$stylesheets) + { + $stylesheets[] = "plugins/Diagnostics/stylesheets/configfile.less"; + } + + public function onNoData(View $dataTableView) + { + if (!Piwik::isUserHasSomeAdminAccess()) { + return; + } + + if (Rules::isBrowserTriggerEnabled()) { + return; + } + + $lastSuccessfulRun = CronArchivingLastRunCheck::getTimeSinceLastSuccessfulRun(); + if ($lastSuccessfulRun > CronArchivingLastRunCheck::SECONDS_IN_DAY) { + $content = Piwik::translate('Diagnostics_NoDataForReportArchivingNotRun', [ + Url::getExternalLinkTag('https://matomo.org/docs/setup-auto-archiving/'), + '', + ]); + + $notification = new Notification($content); + $notification->priority = Notification::PRIORITY_HIGH; + $notification->context = Notification::CONTEXT_INFO; + $notification->flags = Notification::FLAG_NO_CLEAR; + $notification->type = Notification::TYPE_TRANSIENT; + $notification->raw = true; + + $dataTableView->notifications[self::NO_DATA_ARCHIVING_NOT_RUN_NOTIFICATION_ID] = $notification; + } + } +} diff --git a/plugins/Diagnostics/Menu.php b/plugins/Diagnostics/Menu.php new file mode 100644 index 0000000000000000000000000000000000000000..8ecfa6ebf003bad1875dfa5659ac5f775113a1a2 --- /dev/null +++ b/plugins/Diagnostics/Menu.php @@ -0,0 +1,28 @@ +addDiagnosticItem('Diagnostics_ConfigFileTitle', $this->urlForAction('configfile'), $orderId = 30); + } + } +} diff --git a/plugins/Diagnostics/config/config.php b/plugins/Diagnostics/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..2872d1782ff9174c5bab6683e51d1cc3432baeff --- /dev/null +++ b/plugins/Diagnostics/config/config.php @@ -0,0 +1,61 @@ + array( + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\PhpVersionCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\DbAdapterCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\DbReaderCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\PhpExtensionsCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\PhpFunctionsCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\PhpSettingsCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\WriteAccessCheck'), + ), + // Diagnostics for recommended features + 'diagnostics.optional' => array( + Piwik\DI::get(RequiredPrivateDirectories::class), + Piwik\DI::get(RecommendedPrivateDirectories::class), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\FileIntegrityCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\PHPBinaryCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\TrackerCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\MemoryLimitCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\TimezoneCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\HttpClientCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\PageSpeedCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\GdExtensionCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\RecommendedExtensionsCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\RecommendedFunctionsCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\NfsDiskCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\CronArchivingCheck'), + Piwik\DI::get(CronArchivingLastRunCheck::class), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\DatabaseAbilitiesCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\DbOverSSLCheck'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\DbMaxPacket'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\ForceSSLCheck'), + ), + 'diagnostics.informational' => array( + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\MatomoInformational'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\PhpInformational'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\DatabaseInformational'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\ConfigInformational'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\ServerInformational'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\ReportInformational'), + Piwik\DI::get('Piwik\Plugins\Diagnostics\Diagnostic\UserInformational'), + Piwik\DI::get(\Piwik\Plugins\Diagnostics\Diagnostic\ArchiveInvalidationsInformational::class), + ), + // Allows other plugins to disable diagnostics that were previously registered + 'diagnostics.disabled' => array(), + + 'Piwik\Plugins\Diagnostics\DiagnosticService' => Piwik\DI::autowire() + ->constructor(Piwik\DI::get('diagnostics.required'), Piwik\DI::get('diagnostics.optional'), Piwik\DI::get('diagnostics.informational'), Piwik\DI::get('diagnostics.disabled')), + + 'Piwik\Plugins\Diagnostics\Diagnostic\MemoryLimitCheck' => Piwik\DI::autowire() + ->constructorParameter('minimumMemoryLimit', Piwik\DI::get('ini.General.minimum_memory_limit')), + + 'Piwik\Plugins\Diagnostics\Diagnostic\WriteAccessCheck' => Piwik\DI::autowire() + ->constructorParameter('tmpPath', Piwik\DI::get('path.tmp')), +); diff --git a/plugins/Diagnostics/config/tracker.php b/plugins/Diagnostics/config/tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/Diagnostics/config/tracker.php @@ -0,0 +1,3 @@ +%1$s", + "DatabaseCollationConnection": "La vostra intercalació de connexió utilitzada actualment és: %1$s", + "DatabaseCollationNotConfigured": "La vostra connexió de base de dades està configurada sense una intercalació explícita. Si us plau, actualitzeu [database] collation = '' al fitxer \"config/config.ini.php\" amb la intercalació que s'ha d'utilitzar, per assegurar-vos que totes les funcions de la base de dades funcionin com s'esperava.", + "DatabaseReaderConnection": "Connexió del lector de bases de dades", + "DatabaseUtf8Requirement": "Això és necessari per poder emmagatzemar caràcters UTF8 de 4 bytes. A menys que utf8mb4 estigui disponible els caràcters especials, com ara emojis, caràcters menys comuns d'idiomes asiàtics, diversos scripts històrics o símbols matemàtics es substituiran per %1$s. Podeu llegir més detalls sobre aquest tema a %2$saquest FAQ%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "La vostra base de dades admet el conjunt de caràcters utf8mb4, però les taules de la vostra base de dades encara no s'han convertit. Podeu fer-ho executant l'ordre %1$s o activant la conversió automàtica a la configuració general.", + "DatabaseUtf8mb4CharsetRecommended": "La vostra base de dades encara no és compatible amb el conjunt de caràcters utf8mb4.", + "EnableRequiredDirectoriesDiagnostic": "Aquesta comprovació s'ha omès perquè aquesta comprovació està desactivada a la configuració. Per habilitar aquesta comprovació, establiu [General] enable_required_directories_diagnostic = 1 al fitxer \"config/config.ini.php\".", + "HideUnchanged": "Si voleu veure només els valors canviats, podeu %1$samagar tots els valors sense canvis%2$s.", + "HtaccessWarningNginx": "Per garantir que no es pot accedir directament als fitxers sensibles, es recomana configurar el servidor web per restringir l'accés a determinats directoris. Per obtenir més informació, consulteu %1$sla configuració oficial del servidor nginx%2$s", + "MariaDbNotConfigured": "La versió de la vostra base de dades indica que potser utilitzeu un servidor MariaDb. Si aquest és el cas, assegureu-vos d'establir [database] schema = Mariadb al fitxer \"config/config.ini.php\", per assegurar-vos que totes les funcions de la base de dades funcionin com s'esperava.", + "MysqlMaxPacketSize": "Mida màxima del paquet", + "MysqlMaxPacketSizeWarning": "Es recomana configurar una mida '%1$smax_allowed_packet%2$s' a la vostra base de dades MySQL d'almenys %3$s. La configuració actualment és %4$s.", + "MysqlTemporaryTablesWarning": "És necessari el permís de MySQL CREATE TEMPORARY TABLES perquè el Matomo funcioni correctament.", + "MysqlTransactionLevel": "No s'admet el canvi del nivell d'aïllament de la transacció. L'arxivat seguirà funcionant, però pot ser més lent i es recomana canviar, per exemple, el `binlog_format` a `row` si és possible.", + "NoDataForReportArchivingNotRun": "L'arxivat dels vostres informes no s'ha executat recentment, %1$sobteniu més informació sobre com generar els vostres informes.%2$s", + "PHPFPMWarningApache": "PHP FPM ignorarà les regles .htaccess per als fitxers .php. Per assegurar que no es pot accedir directament als fitxers sensibles, es recomana excloure determinats directoris de ser gestionats per PHP FPM afegint la línia %1$s a la secció %2$s a la configuració d'hoste virtual d'Apache just a sobre de la línia %3$s.", + "PHPFPMWarningGeneric": "PHP FPM pot ignorar les regles .htaccess per als fitxers .php. Per assegurar que no es pot accedir directament als fitxers sensibles, es recomana configurar el servidor web per excloure el directori /config de ser gestionat per PHP FPM.", + "PHPFPMWarningNginx": "PHP FPM ignorarà les regles .htaccess per als fitxers .php. Per assegurar que no es pot accedir directament als fitxers sensibles, es recomana excloure determinats directoris de ser gestionats per PHP FPM. Per obtenir més informació, consulteu %1$sla configuració oficial del servidor nginx%2$s", + "PrivateDirectoryCantCheckUrl": "Comprovar l'URL %1$sha fallat: %2$s", + "PrivateDirectoryInternetDisabled": "No hem pogut comprovar si les URL següents són accessibles perquè les funcions d'Internet estan desactivades en aquest Matomo.", + "PrivateDirectoryIsAccessible": "Hem trobat que les URL anteriors són accessibles mitjançant el navegador i NO haurien de ser-ho. Permetre l'accés pot suposar un risc potencial de seguretat, ja que els continguts poden proporcionar informació sobre el vostre servidor i potencialment els vostres usuaris. Si us plau, restringeix-hi l'accés.", + "PrivateDirectoryManualCheck": "Si us plau obriu les URL manualment en un navegador per veure si hi podeu accedir. Si podeu, és possible que hàgiu de modificar la configuració del vostre servidor, ja que aquests fitxers/directoris no haurien de ser accessibles mitjançant un navegador des d'Internet o la intranet.", + "RecommendedPrivateDirectories": "Directoris privats recomanats", + "RequiredPrivateDirectories": "Directoris privats obligatoris", + "Sections": "Seccions", + "UrlsAccessibleViaBrowser": "Hem trobat que les URL anteriors són accessibles mitjançant el navegador, però recomanem que no ho siguin. Si és possible, restringeix-hi l'accés." + } +} diff --git a/plugins/Diagnostics/lang/cs.json b/plugins/Diagnostics/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..483416aafc03d4711b502d8820008eafa6ba8879 --- /dev/null +++ b/plugins/Diagnostics/lang/cs.json @@ -0,0 +1,7 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Konfigurační soubor", + "HideUnchanged": "Pokud chcete vidět pouze změněné hodnoty, můžete %1$snezměněné hodnoty skrýt%2$s.", + "Sections": "Sekce" + } +} diff --git a/plugins/Diagnostics/lang/cy.json b/plugins/Diagnostics/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/da.json b/plugins/Diagnostics/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..9103b0112ca641dd0e07354608c8634b0dd34e61 --- /dev/null +++ b/plugins/Diagnostics/lang/da.json @@ -0,0 +1,8 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Konfigurationsfil", + "MysqlMaxPacketSize": "Maks. pakkestørrelse", + "MysqlMaxPacketSizeWarning": "Det anbefales at sætte '%1$smax_allowed_packet%2$s' i din MySQL-konfiguration til mindst %3$s. Den nuværende værdi er %4$s.", + "Sections": "Sektioner" + } +} diff --git a/plugins/Diagnostics/lang/de.json b/plugins/Diagnostics/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..d0687b69a7dfaf7f577a3d5ecca05513b5ee9da4 --- /dev/null +++ b/plugins/Diagnostics/lang/de.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Alle privaten Verzeichnisse sind nicht aus dem Internet zugänglich.", + "BrowserAndAutoArchivingEnabledComment": "Sowohl Browser als auch Autoarchivierung scheinen aktiviert zu sein. Autoarchivierung startete zuletzt vor %3$s. Wenn %1$sAutoarchvierung%2$s aktiviert ist, sollten Sie Browserarchivierung in \"Allgemeine Einstellungen\" deaktivieren.", + "BrowserAndAutoArchivingEnabledLabel": "Browser- und Autoarchivierung aktiviert", + "BrowserTriggeredArchivingEnabled": "Für eine optimale Leistung und ein schnelles Matomo wird dringend empfohlen, einen Crontab einzurichten, um Ihre Berichte automatisch zu archivieren und das Auslösen durch den Browser in den Matomo-Einstellungen zu deaktivieren. %1$sErfahren Sie mehr.%2$s", + "ConfigFileIntroduction": "Hier können Sie die Matomo Konfiguration einsehen. Sollten Sie Matomo auf lastverteilten Systemen einsetzen, könnte diese Seite unterschiedlich sein, abhängig vom Server auf dem die Seite geladen wurde. Zeilen mit einer anderen Hintergrundfarbe sind geänderte Konfigurationswerte, die beispielsweise in der Datei %1$s definiert wurden.", + "ConfigFileTitle": "Konfigurationsdatei", + "ConfigIniAccessible": "Wir haben auch herausgefunden, dass Matomos Konfigurationsverzeichnis öffentlich zugänglich ist. Zwar können Angreifer die Konfigurationsdatei nicht lesen, jedoch sind Ihre MySQL Anmeldeinformationen und andere Informationen für jeden zugänglich, wenn Ihr Webserver aus irgend einem Grund aufhört, PHP Dateien auszuführen. Bitte prüfen Sie Ihre Webserverkonfiguration und verwehren Sie den Zugriff zu diesem Verzeichnis.", + "CronArchivingHasNotRun": "Die Archivierung wurde noch nicht erfolgreich abgeschlossen.", + "CronArchivingHasNotRunInAWhile": "Die Archivierung ist zuletzt am %1$s erfolgreich gelaufen, also vor %2$s.", + "CronArchivingLastRunCheck": "Letzter erfolgreicher Abschluss der Archivierung", + "CronArchivingRanSuccessfullyXAgo": "Der Archivierungsprozess wurde vor %1$s erfolgreich abgeschlossen.", + "CronArchivingRunDetails": "Bitte überprüfen Sie, ob Sie einen Crontab eingerichtet haben, der den %1$s Konsolenbefehl aufruft, und ob Sie einen %2$s konfiguriert haben, um Fehler per E-Mail zu erhalten, wenn die Archivierung fehlschlägt. Sie können auch versuchen, den Konsolenbefehl auszuführen, um Ihre Berichte manuell zu archivieren: %3$s. %4$s Mehr erfahren.%5$s", + "DatabaseCollationCharset": "Die Standard-Kollation für Ihren konfigurierten Zeichensatz lautet: %1$s", + "DatabaseCollationConnection": "Ihre derzeit verwendete Verbindungskollation lautet: %1$s", + "DatabaseCollationNotConfigured": "Ihre Datenbankverbindung ist ohne explizite Kollation konfiguriert. Bitte aktualisieren Sie [database] collation = '' in der Datei \"config/config.ini.php\" mit der zu verwendenden Kollation, um sicherzustellen, dass alle Datenbankfunktionen wie erwartet funktionieren.", + "DatabaseReaderConnection": "Datenbankleserecht-Verbindung", + "DatabaseUtf8Requirement": "Dies ist nötig, um 4-Byte UTF8 Zeichen speichern zu können. Wenn utf8mb4 nicht verfügbar ist, werden spezielle Zeichen wie Emojis, selten verwendete Zeichen asiatischer Sprachen, historische Schriftzeichen oder mathematische Symbole mit %1$s ersetzt. Sie können mehr über dieses Thema in %2$sdiesen FAQ%3$s lesen.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Ihre Datenbank unterstützt den utf8mb4 Zeichensatz, aber Ihre Datenbanktabellen wurden noch nicht konvertiert. Sie können dies nachholen, indem Sie den Befehl %1$s ausführen oder die automatische Konvertierung in den generellen Einstellungen aktivieren.", + "DatabaseUtf8mb4CharsetRecommended": "Ihre Datenbank unterstützt den utf8mb4 Zeichensatz noch nicht.", + "EnableRequiredDirectoriesDiagnostic": "Diese Prüfung wurde übersprungen, da diese Prüfung in der Konfiguration deaktiviert ist. Um diese Prüfung zu aktivieren, setzen Sie [General] enable_required_directories_diagnostic = 1 in der Datei \"config/config.ini.php\".", + "HideUnchanged": "Falls Sie nur die geänderten Werte einsehen möchten, können Sie %1$salle unveränderten Werte ausblenden%2$s.", + "HtaccessWarningNginx": "Um sicherzustellen, dass auf sensible Dateien nicht direkt zugegriffen werden kann, wird empfohlen, Ihren Webserver so zu konfigurieren, dass der Zugriff auf bestimmte Verzeichnisse eingeschränkt wird. Weitere Informationen finden Sie in der %1$s offiziellen nginx-Serverkonfiguration %2$s", + "MariaDbNotConfigured": "Ihre Datenbankversion deutet darauf hin, dass Sie möglicherweise einen MariaDb-Server verwenden. Wenn dies der Fall ist, stellen Sie bitte sicher, dass Sie [database] schema = Mariadb in der Datei \"config/config.ini.php\" setzen, um sicherzustellen, dass alle Datenbank-Features wie erwartet funktionieren.", + "MysqlMaxPacketSize": "Maximale Packetgröße", + "MysqlMaxPacketSizeWarning": "Es wird empfohlen die '%1$smax_allowed_packet%2$s' Größe in Ihrer MySQL Datenbank auf mindestens %3$s zu erhöhen. Aktuell ist %4$s eingestellt.", + "MysqlTemporaryTablesWarning": "Die MySQL Berechtigung CREATE TEMPORARY TABLES wird benötigt, damit Matomo korrekt funktioniert.", + "MysqlTransactionLevel": "Ändern von Transaction Isolation Levels wird nicht unterstützt. Archivierung wird trotzdem funktionieren, aber es wird eventuell langsamer sein und es wird empfohlen, zum Beispiel das `binlog_format` auf `row` zu ändern, wenn möglich.", + "NoDataForReportArchivingNotRun": "Die Archivierung Ihrer Berichte wurde in letzter Zeit nicht durchgeführt, %1$serfahren Sie mehr darüber, wie Sie Ihre Berichte erzeugen können.%2$s", + "PHPFPMWarningApache": "PHP FPM ignoriert die .htaccess-Regeln für .php-Dateien. Um sicherzustellen, dass auf sensible Dateien nicht direkt zugegriffen werden kann, wird empfohlen, bestimmte Verzeichnisse von der Bearbeitung durch \"PHP FPM\" auszuschließen, indem Sie die Zeile %1$s in den %2$s-Abschnitt in Ihrer Apache Virtual Host Config direkt über der %3$s Zeile einfügen.", + "PHPFPMWarningGeneric": "PHP FPM könnte die .htaccess-Regeln für .php-Dateien ignorieren. Um sicherzustellen, dass auf sensible Dateien nicht direkt zugegriffen werden kann, wird empfohlen, Ihren Webserver so zu konfigurieren, dass das Verzeichnis /config von der Bearbeitung durch \"PHP FPM\" ausgeschlossen wird.", + "PHPFPMWarningNginx": "PHP FPM ignoriert die .htaccess-Regeln für .php-Dateien. Um sicherzustellen, dass auf sensible Dateien nicht direkt zugegriffen werden kann, wird empfohlen, bestimmte Verzeichnisse von der Bearbeitung durch \"PHP FPM\" auszuschließen. Weitere Informationen finden Sie in der %1$s offiziellen nginx-Serverkonfiguration %2$s", + "PrivateDirectoryCantCheckUrl": "Überprüfung der URL %1$s ist fehlgeschlagen: %2$s", + "PrivateDirectoryInternetDisabled": "Wir konnten bei den folgenden URLs nicht prüfen, ob sie erreichbar sind, weil Internet Features aktuell auf dieser Matomo Installation deaktiviert sind.", + "PrivateDirectoryIsAccessible": "Wir haben festgestellt, dass auf die oben aufgelisteten URLs über den Browser zugegriffen werden kann, was aber NICHT so sein sollte. Der Zugriff kann ein mögliches Sicherheitsrisiko darstellen, weil der Inhalt Informationen über Ihren Server und potenziell über Ihre Benutzer enthält. Bitte beschränken Sie den Zugriff auf diese URLs.", + "PrivateDirectoryManualCheck": "Bitte öffnen Sie die URLs manuell in einem Browser um zu sehen, ob Sie Zugang dazu haben. Wenn Sie können, sollten Sie Ihre Serverkonfiguration so ändern, dass diese Dateien/Verzeichnisse nicht via Browser aus dem Internet oder Intranet verfügbar sind.", + "RecommendedPrivateDirectories": "Empfohlene private Verzeichnisse", + "RequiredPrivateDirectories": "Verzeichnisse, welche privat sein müssen", + "Sections": "Sektionen", + "UrlsAccessibleViaBrowser": "Wir haben die obenstehenden URLs gefunden, welche über den Browser erreichbar sind, aber wir empfehlen das zu unterbinden. Wenn möglich, schränken Sie bitte den Zugriff auf sie ein." + } +} diff --git a/plugins/Diagnostics/lang/dv.json b/plugins/Diagnostics/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/el.json b/plugins/Diagnostics/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..7522917bbc86a68cd2773496e51b32d121b5ce1a --- /dev/null +++ b/plugins/Diagnostics/lang/el.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Όλοι οι ιδιωτικοί κατάλογοι δεν είναι προσπελάσιμοι από το διαδίκτυο.", + "BrowserAndAutoArchivingEnabledComment": "Φαίνεται να είναι ενεργοποιημένη και η αυτόματη αρχειοθέτηση και η αρχειοθέτηση μέσω προγράμματος πλοήγησης. Η αυτόματη αρχειοθέτηση τελευταία ξεκίνησε πριν %3$s. Αν η %1$sαυτόματη αρχειοθέτηση%2$s είναι ενεργή, θα πρέπει να απενεργοποιήσετε την αρχειοθέτηση μέσω προγράμματος πλοήγησης στις \"Γενικές Ρυθμίσεις\".", + "BrowserAndAutoArchivingEnabledLabel": "Ενεργοποιήθηκε η Αυτόματη και η αρχειοθέτηση μέσω προγράμματος πλοήγησης", + "BrowserTriggeredArchivingEnabled": "Για βέλτιστη απόδοση και ένα γρήγορο Matomo, προτείνεται να παραμετροποιήσετε ένα crontab για την αυτόματη αρχειοθέτηση των αναφορών σας και να απενεργοποιήσετε την εκτέλεση μέσω του προγράμματος περιήγησης στις ρυθμίσεις του Matomo. %1$sΜάθετε περισσότερα.%2$s", + "ConfigFileIntroduction": "Εδώ μπορείτε να δείτε την παραμετροποίηση του Matomo. Αν εκτελείτε το Matomo σε περιβάλλον μοιραζόμενου φόρτου, η σελίδα ενδέχεται να είναι διαφορετικά ανάλογα με τον διακομιστή από τον οποίο φορτώνεται. Οι γραμμές με διαφορετικό χρώμα υποβάθρου αποτελούν τροποποιημένες τιμές παραμετροποίησης που έχουν οριστεί για παράδειγμα στο αρχείο %1$s.", + "ConfigFileTitle": "Αρχείο ρυθμίσεων", + "ConfigIniAccessible": "Βρέθηκε ότι ο φάκελος παραμετροποίησης του Matomo είναι δημόσια προσπελάσιμος. Καθώς ένας επιτιθέμενος δεν μπορεί να διαβάσει αυτή τη στιγμή την παραμετροποίηση, σε περίπτωση που ο διακομιστής σας ιστού σταματήσει να εκτελεί αρχεία PHP για κάποιο λόγο, τα διαπιστευτήρια για τη MySQL και άλλες πληροφορίες θα είναι διαθέσιμα σε οποιονδήποτε. Ελέγξτε την παραμετροποίηση του διακομιστή ιστού σας και απαγορεύστε την πρόσβαση σε αυτό τον κατάλογο.", + "CronArchivingHasNotRun": "Η αρχειοθέτηση δεν εκτελέστηκε ακόμη με επιτυχία.", + "CronArchivingHasNotRunInAWhile": "Η αρχειοθέτηση εκτελέστηκε με επιτυχία στις %1$s που είναι πριν από %2$s.", + "CronArchivingLastRunCheck": "Τελευταία Επιτυχής Εκτέλεση Αρχειοθέτησης", + "CronArchivingRanSuccessfullyXAgo": "Η διαδικασία αρχειοθέτησης ολοκληρώθηκε με επιτυχία πριν από %1$s.", + "CronArchivingRunDetails": "Ελέγξτε ότι έχετε ορίσει ένα crontab που καλεί την εντολή %1$s σε κονσόλα και ότι έχετε παραμετροποιήσει ένα %2$s να λαμβάνει τα σφάλματα με e-mail σε περίπτωση που η αρχειοθέτηση αποτύχει. Μπορείτε επίσης να εκτελείτε την εντολή στη γραμμή εντολών για την αρχειοθέτηση των αναφορών χειροκίνητα: %3$s. %4$sΔείτε περισσότερα.%5$s", + "DatabaseCollationCharset": "Η προκαθορισμένη συλλογή χαρακτήρων για το παραμετροποιημένο σύνολο χαρακτήρων σας είναι: %1$s", + "DatabaseCollationConnection": "Η τρέχουσα συλλογή χαρακτήρων που χρησιμοποιείται είναι: %1$s", + "DatabaseCollationNotConfigured": "Η σύνδεση με τη βάση δεδομένων σας δεν έχει παραμετροποιηθεί για χρήση συγκεκριμένης συλλογής χαρακτήρων. Παρακαλούμε ενημερώστε το [database] collation = '' στο αρχείο \"config/config.ini.php\" με τη συλλογή χαρακτήρων που θα χρησιμοποιηθεί, για να βεβαιωθείτε ότι όλα τα χαρακτηριστικά της βάσης δεδομένων θα λειτουργούν όπως πρέπει.", + "DatabaseReaderConnection": "Σύνδεση Αναγνώστη Βάσης Δεδομένων", + "DatabaseUtf8Requirement": "Αυτό απαιτείται για την αποθήκευση χαρακτήρων 4-byte UTF8. Εκτός αν η κωδικοποίηση utf8mb4 δεν είναι διαθέσιμη, ειδικοί χαρακτήρες όπως emoji, λιγότερο συχνοί χαρακτήρες ασιατικών γλωσσών όπως ιστορικοί χαρακτήρες ή μαθηματικά σύμβολα θα αντικαθίστανται από το %1$s. Μπορείτε να δείτε περισσότερες πληροφορίες για το θέμα αυτό στις %2$sΣυχνές Ερωτήσεις-Απαντήσεις%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Η βάση δεδομένων σας υποστηρίζει κωδικοποίηση χαρακτήρων utf8mb4, αλλά οι πίνακες στη βάση δεν έχουν ακόμη μετατραπεί. Μπορείτε να το κάνετε αυτό εκτελώντας την εντολή %1$s ή ενεργοποιώντας την αυτόματη μετατροπή στις Γενικές Ρυθμίσεις.", + "DatabaseUtf8mb4CharsetRecommended": "Η βάση δεδομένων σας δεν υποστηρίζει ακόμη κωδικοποίηση χαρακτήρων utf8mb4.", + "EnableRequiredDirectoriesDiagnostic": "Ο έλεγχος δεν έγινε, καθώς είναι απενεργοποιημένος στην ρύθμιση. Για να ενεργοποιηθεί, ορίστε στο τμήμα [General] το enable_required_directories_diagnostic = 1 στο αρχείο \"config/config.ini.php\".", + "HideUnchanged": "Αν επιθυμείτε να δείτε μόνο τις τιμές που άλλαξαν, μπορείτε να %1$sαποκρύψετε τις τιμές που παρέμειναν ίδιες%2$s.", + "HtaccessWarningNginx": "Για να βεβαιωθείτε ότι δε θα είναι άμεσα προσπελάσιμα ευαίσθητα αρχεία, προτείνεται να ρυθμίσετε τον διακομιστή ιστού σας ώστε να αποκλείει την πρόσβαση σε ορισμένους καταλόγους. Για περισσότερες πληροφορίες δείτε την %1$s επίσημη τεκμηρίωση για τις ρυθμίσεις του nginx %2$s", + "MariaDbNotConfigured": "Η έκδοση της βάσης δεδομένων σας δείχνει ότι μάλλον χρησιμοποιείτε ένα διακομιστή MariaDB. Αν είναι αυτή η περίπτωση, βεβαιωθείτε να ορίσετε το [database] schema = Mariadb στο αρχείο config/config.ini.php\" προκειμένου να λειτουργούν σωστά όλα τα χαρακτηριστικά της βάσης δεδομένων.", + "MysqlMaxPacketSize": "Μέγιστο Μέγεθος Πακέτου", + "MysqlMaxPacketSizeWarning": "Προτείνεται να ρυθμίσετε το μέγεθος του '%1$smax_allowed_packet%2$s' στη βάση MySQL τουλάχιστον σε %3$s. Αυτή τη στιγμή είναι ρυθμισμένο σε %4$s.", + "MysqlTemporaryTablesWarning": "Απαιτείται η άδεια CREATE TEMPORARY TABLES της MySQL για τη σωστή λειτουργία του Matomo.", + "MysqlTransactionLevel": "Η αλλαγή του επιπέδου απομόνωσης συναλλαγής δεν υποστηρίζεται. Η αρχειοθέτηση θα συνεχίσει να λειτουργεί αλλά ενδέχεται να είναι πιο αργή και προτείνεται για παράδειγμα η αλλαγή του `binlog_format` σε `row` αν αυτό είναι δυνατόν.", + "NoDataForReportArchivingNotRun": "Η αρχειοθέτηση των αναφορών σας δεν εκτελέστηκε επιτυχώς. %1$sΜάθετε περισσότερα για το πως να δημιουργείτε τις αναφορές σας.%2$s", + "PHPFPMWarningApache": "Το PHP FPM αγνοεί τους κανόνες στα αρχεία .htaccess για τα αρχεία .php. Για να βεβαιωθείτε ότι δε θα είναι άμεσα προσπελάσιμα ευαίσθητα αρχεία, προτείνεται να αποκλείσετε το PHP FPM να χειρίζεται ορισμένους καταλόγους προσθέτοντας τη γραμμή %1$s στο τμήμα %2$s στο αρχείο ρυθμίσεων του εικονικού διακομιστή ακριβώς πάνω από τη γραμμή %3$s.", + "PHPFPMWarningGeneric": "Το PHP FPM ενδέχεται να αγνοεί τους κανόνες στα αρχεία .htaccess για τα αρχεία .php. Για να βεβαιωθείτε ότι δε θα είναι άμεσα προσπελάσιμα ευαίσθητα αρχεία, προτείνεται να ρυθμίσετε τον διακομιστή ιστού σας να αποκλείει τον χειρισμό ορισμένων καταλόγων από το PHP FPM.", + "PHPFPMWarningNginx": "Το PHP FPM αγνοεί τους κανόνες στα αρχεία .htaccess για τα αρχεία .php. Για να βεβαιωθείτε ότι δε θα είναι άμεσα προσπελάσιμα ευαίσθητα αρχεία, προτείνεται να αποκλείσετε το PHP FPM να χειρίζεται ορισμένους καταλόγους. Για περισσότερες πληροφορίες δείτε την %1$s επίσημη τεκμηρίωση για τις ρυθμίσεις του nginx %2$s", + "PrivateDirectoryCantCheckUrl": "Ο έλεγχος της διεύθυνσης URL %1$sαπέτυχε: %2$s", + "PrivateDirectoryInternetDisabled": "Δεν ήταν δυνατός ο έλεγχος αν οι παρακάτω διευθύνσεις URL είναι προσπελάσιμες επειδή τα χαρακτηριστικά διαδικτύου είναι απενεργοποιημένα στο Matomo.", + "PrivateDirectoryIsAccessible": "Βρέθηκε ότι οι παρακάτω διευθύνσεις URL είναι προσπελάσιμες από το φυλλομετρητή, αλλά ΔΕ θα έπρεπε. Το να επιτρέπεται να είναι προσπελάσιμες αποτελούν ρίσκο ασφάλειας, καθώς τα περιεχόμενά τους ενδέχεται να περιέχουν πληροφορίες σχετικά με το διακομιστή και τους χρήστες σας. Αποκλείστε την πρόσβαση σε αυτές.", + "PrivateDirectoryManualCheck": "Δοκιμάστε να ανοίξετε τις διευθύνσεις URL χειροκίνητα στο φυλλομετρητή σας για να δείτε αν έχετε πρόσβαση. Αν μπορείτε, ενδέχεται να χρειαστεί να αλλάξετε την παραμετροποίηση του διακομιστή σας, καθώς αυτά τα αρχεία/κατάλογοι δεν πρέπει να είναι προσπελάσιμα από το φυλλομετρητή από το διαδίκτυο ή το εσωτερικό σας δίκτυο.", + "RecommendedPrivateDirectories": "Προτεινόμενοι Ιδιωτικοί Κατάλογοι", + "RequiredPrivateDirectories": "Απαιτούμενα Ιδιωτικοί Κατάλογοι", + "Sections": "Τμήματα", + "UrlsAccessibleViaBrowser": "Βρέθηκε ότι οι παραπάνω διευθύνσεις URL είναι προσπελάσιμες μέσω του φυλλομετρητή, αλλά συνιστούμε να μην είναι. Αν είναι δυνατόν, αποκλείστε την πρόσβαση σε αυτές." + } +} diff --git a/plugins/Diagnostics/lang/en.json b/plugins/Diagnostics/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..70855e531a459253670863f5cb5cf0dd2fecbf30 --- /dev/null +++ b/plugins/Diagnostics/lang/en.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Config file", + "MysqlTemporaryTablesWarning": "MySQL Permission CREATE TEMPORARY TABLES is required for Matomo to work properly.", + "MysqlTransactionLevel": "Changing transaction isolation level not supported. Archiving will still work but it may be slower and it is recommended to change for example the `binlog_format` to `row` if possible.", + "MysqlMaxPacketSize": "Max Packet Size", + "MysqlMaxPacketSizeWarning": "It is recommended to configure a '%1$smax_allowed_packet%2$s' size in your MySQL database of at least %3$s. Configured is currently %4$s.", + "MariaDbNotConfigured": "Your database version indicates you might be using a MariaDb server. If this is the case, please ensure to set [database] schema = Mariadb in the \"config/config.ini.php\" file, to ensure all database feature work as expected.", + "ConfigFileIntroduction": "Here you can view the Matomo configuration. If you are running Matomo in a load balanced environment, the page might be different depending on from which server this page is loaded. Rows with a different background color are changed config values that are specified for example in the %1$s file.", + "EnableRequiredDirectoriesDiagnostic": "This check was skipped as this check is disabled in the config. To enable this check set [General] enable_required_directories_diagnostic = 1 in the \"config/config.ini.php\" file.", + "HideUnchanged": "If you want to see only changed values you can %1$shide all unchanged values%2$s.", + "Sections": "Sections", + "BrowserAndAutoArchivingEnabledLabel": "Browser and Auto-archiving enabled", + "BrowserAndAutoArchivingEnabledComment": "It looks like both browser and auto archiving are enabled. Auto archiving last started %3$s ago. If %1$sauto archiving%2$s is enabled, you should disable browser archiving in \"General Settings\".", + "DatabaseCollationNotConfigured": "You database connection is configured without an explicit collation. Please update [database] collation = '' in the \"config/config.ini.php\" file with the collation to be used, to ensure all database features work as expected.", + "DatabaseCollationConnection": "Your currently used connection collation is: %1$s", + "DatabaseCollationCharset": "The default collation for your configured charset is: %1$s", + "DatabaseReaderConnection": "Database Reader Connection", + "DatabaseUtf8Requirement": "This is required to be able to store 4-byte UTF8 characters. Unless utf8mb4 is available special characters, such as emojis, less common characters of asian languages, various historic scripts or mathematical symbols will be replaced with %1$s. You can read more details about this topic in %2$sthis FAQ%3$s.", + "DatabaseUtf8mb4CharsetRecommended": "Your database doesn't support utf8mb4 charset yet.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Your database supports utf8mb4 charset, but your database tables have not been converted yet. You can do this by executing the command %1$s or activating the automatic conversion in General Settings.", + "CronArchivingLastRunCheck": "Last Successful Archiving Completion", + "CronArchivingHasNotRun": "Archiving has not yet run successfully.", + "CronArchivingHasNotRunInAWhile": "Archiving last ran successfully on %1$s which is %2$s ago.", + "CronArchivingRunDetails": "Please check that you have setup a crontab calling the %1$s console command, and that you have configured a %2$s to receive errors by email if archiving fails. You can also try to run the console command to archive your reports manually: %3$s. %4$sLearn more.%5$s", + "CronArchivingRanSuccessfullyXAgo": "The archiving process completed successfully %1$s ago.", + "BrowserTriggeredArchivingEnabled": "For optimal performance and a speedy Matomo, it is highly recommended to set up a crontab to automatically archive your reports, and to disable browser triggering in the Matomo settings. %1$sLearn more.%2$s", + "NoDataForReportArchivingNotRun": "The archiving of your reports hasn't been executed recently, %1$slearn more about how to generate your reports.%2$s", + "RecommendedPrivateDirectories": "Recommended Private Directories", + "RequiredPrivateDirectories": "Required Private Directories", + "PrivateDirectoryManualCheck": "Please open the URLs manually in a browser to see if you can access it. If you can, you might need to modify your server configuration as these files\/directories should not be accessible via a browser from the Internet or Intranet.", + "PrivateDirectoryInternetDisabled": "We couldn't check if the following URLs are accessible because internet features are disabled on this Matomo.", + "PrivateDirectoryIsAccessible": "We found that the above URLs are accessible via the browser, but they should NOT be. Allowing them to be accessed can pose a potential security risk since the contents can provide information about your server and potentially your users. Please restrict access to them.", + "PrivateDirectoryCantCheckUrl": "Checking the URL %1$s failed: %2$s", + "ConfigIniAccessible": "We also found that Matomo's config directory is publicly accessible. While attackers can't read the config now, if your webserver stops executing PHP files for some reason, your MySQL credentials and other information will be available to anyone. Please check your webserver config and deny access to this directory.", + "AllPrivateDirectoriesAreInaccessible": "All private directories are inaccessible from the internet.", + "UrlsAccessibleViaBrowser": "We found that the above URLs are accessible via the browser, but we recommend they should not be. If possible, please restrict access to them.", + "PHPFPMWarningApache": "PHP FPM will ignore .htaccess rules for .php files. To ensure that sensitive files cannot be accessed directly it is recommended to exclude certain directories from being handled by PHP FPM by adding the line %1$s to the %2$s section in your apache virtual host config just above the %3$s line.", + "PHPFPMWarningNginx": "PHP FPM will ignore .htaccess rules for .php files. To ensure that sensitive files cannot be accessed directly it is recommended to exclude certain directories from being handled by PHP FPM. For more information please see the %1$s official nginx server configuration %2$s", + "PHPFPMWarningGeneric": "PHP FPM may ignore .htaccess rules for .php files. To ensure that sensitive files cannot be accessed directly it is recommended to configure your web server to exclude the /config directory from being handled by PHP FPM.", + "HtaccessWarningNginx": "To ensure that sensitive files cannot be accessed directly it is recommended to configure your web server to restrict access to certain directories. For more information please see the %1$s official nginx server configuration %2$s" + } +} \ No newline at end of file diff --git a/plugins/Diagnostics/lang/eo.json b/plugins/Diagnostics/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/eo.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/es-ar.json b/plugins/Diagnostics/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..a5ed25d833d62c92bad46b960ca4361c7a37b426 --- /dev/null +++ b/plugins/Diagnostics/lang/es-ar.json @@ -0,0 +1,20 @@ +{ + "Diagnostics": { + "BrowserTriggeredArchivingEnabled": "Para un rendimiento óptimo y un Matomo rápido, es altamente recomendable establecer un crontab para archivar automáticamente tus informes, y deshabilitar el accionar del navegador web en la configuración de Matomo. %1$sAprendé más%2$s.", + "ConfigFileIntroduction": "Acá podés ver la configuración de Matomo. Si estás ejecutando Matomo en un entorno de carga balanceada, la página podría ser diferente dependiendo desde qué servidor se página esta página. Las filas con diferente fondo de color son valores de configuración cambiadas que están especificadas, por ejemplo, en el archivo %1$s.", + "ConfigFileTitle": "Archivo de configuración", + "CronArchivingHasNotRun": "El archivado todavía no se ejecutó exitosamente.", + "CronArchivingHasNotRunInAWhile": "El último archivado exitoso se ejecutó el %1$s, lo cual ya tiene %2$s.", + "CronArchivingLastRunCheck": "Último archivado exitoso", + "CronArchivingRanSuccessfullyXAgo": "El proceso de archivado se completó exitosamente hace %1$s.", + "CronArchivingRunDetails": "Por favor, revisá que hayás configurado un crontab llamando al comando de consola %1$s, y que configuraste un %2$s para recibir errores por correo electrónico si el archivado falla. También podés intentar ejecutar el comando de consola para archivar tus informes manualmente: %3$s. %4$sAprendé más.%5$s", + "DatabaseReaderConnection": "Conexión de lector de base de datos", + "HideUnchanged": "Sí sólo querés ver valores cambiados, podés %1$socultar todos los valores sin cambios%2$s.", + "MysqlMaxPacketSize": "Tamaño máximo del paquete", + "MysqlMaxPacketSizeWarning": "Se recomienda configurar el tamaño de \"%1$smax_allowed_packet%2$s\" en tu base de datos MySQL en al menos %3$s. La configuración actual es de %4$s.", + "MysqlTemporaryTablesWarning": "El permiso \"CREATE TEMPORARY TABLES\" de MySQL es requerido para que Matomo funcione correctamente.", + "MysqlTransactionLevel": "Cambiar el nivel de transacción de aislamiento no está soportado. El archivado aún funcionará pero podría ser más lento y es recomendado cambiar, por ejemplo, \"binlog_format\" a \"row\", de ser posible.", + "NoDataForReportArchivingNotRun": "El archivado de tus informes no fue ejecutado recientemente. %1$sAprendé más sobre cómo generar tus informes%2$s.", + "Sections": "Secciones" + } +} diff --git a/plugins/Diagnostics/lang/es.json b/plugins/Diagnostics/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..f5bcd2494836233971ee55f9dc965dc5c641564d --- /dev/null +++ b/plugins/Diagnostics/lang/es.json @@ -0,0 +1,28 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Todos los directorios privados son inaccesibles desde Internet.", + "BrowserTriggeredArchivingEnabled": "Para un rendimiento óptimo y un Matomo veloz, se recomienda configurar un crontab para archivar automáticamente sus informes y deshabilitar la activación del navegador en la configuración de Matomo. %1$sAprender más.%2$s", + "ConfigFileIntroduction": "Aquí puede ver la configuración de Matomo. Si está ejecutando Matomo en un ambiente balanceado puede que la página sea diferente sea por el servidor que la esté cargando. Las filas con un color de fondo diferente son valores de configuración modificados que son especificados por ejemplo en el archivo %1$s.", + "ConfigFileTitle": "Archivo de configuración", + "ConfigIniAccessible": "También descubrimos que el directorio config de Matomo es de acceso público. Mientras que los atacantes no pueden leer la configuración ahora, si su servidor web deja de ejecutar archivos PHP por alguna razón, sus credenciales MySQL y otra información estará disponible para cualquiera. Por favor, comprueba la configuración de tu servidor web y deniega el acceso a este directorio.", + "CronArchivingHasNotRun": "El archivado aún no se ha ejecutado correctamente.", + "CronArchivingHasNotRunInAWhile": "El último archivado se ejecutó exitosamente en %1$s el cual es %2$s.", + "CronArchivingLastRunCheck": "Último archivado completado con éxito", + "CronArchivingRanSuccessfullyXAgo": "El proceso de archivado se completó exitosamente hace %1$s.", + "CronArchivingRunDetails": "Verifique que haya configurado un crontab llamando al comando %1$s de la consola, y que haya configurado un %2$s para recibir los errores por correo electrónico si falla el archivado. También puede intentar ejecutar el comando de la consola para archivar sus informes manualmente: %3$s. %4$sPara saber más.%5$s", + "DatabaseReaderConnection": "Conexión con el lector de la base de datos", + "HideUnchanged": "Si desea ver solo los valores modificados puede %1$socultar todos los valores no modificados%2$s.", + "HtaccessWarningNginx": "Para garantizar que no se pueda acceder directamente a los archivos sensibles, se recomienda configurar el servidor web para restringir el acceso a determinados directorios. Para más información, consulte la %1$s configuración oficial del servidor nginx %2$s", + "MysqlMaxPacketSize": "Tamaño máximo del paquete", + "MysqlMaxPacketSizeWarning": "Es recomendable configurar un tamaño de al menos %3$sen '%1$smax_allowed_packet%2$s' en su base de datos MySQL. Actualmente está configurada en %4$s.", + "MysqlTemporaryTablesWarning": "Se necesita el permiso de MySQL CREATE TEMPORARY TABLES para que Matomo funcione correctamente.", + "MysqlTransactionLevel": "No se puede cambiar el nivel de aislamiento de la transacción. El archivado funcionará, pero puede ser más lento, y se recomienda cambiar por ejemplo de `binlog_format` a `row` si es posible.", + "NoDataForReportArchivingNotRun": "El archivado de sus informes no se han ejecutado ultimamente, %1$ssepa más cómo generar sus informes.%2$s", + "PHPFPMWarningApache": "PHP FPM ignorará las reglas .htaccess para archivos .php. Para asegurar que los archivos sensibles no puedan ser accedidos directamente se recomienda excluir ciertos directorios de ser manejados por PHP FPM agregando la línea %1$s a la sección %2$s en la configuración de su host virtual apache justo arriba de la línea %3$s.", + "PHPFPMWarningGeneric": "PHP FPM puede ignorar las reglas .htaccess para archivos .php. Para asegurar que los archivos sensibles no puedan ser accedidos directamente se recomienda configurar su servidor web para excluir el directorio /config de ser manejado por PHP FPM.", + "PHPFPMWarningNginx": "PHP FPM ignorará las reglas .htaccess para archivos .php. Para asegurar que archivos sensibles no puedan ser accedidos directamente se recomienda excluir ciertos directorios de ser manejados por PHP FPM. Para más información por favor vea la %1$s configuración oficial del servidor nginx %2$s", + "PrivateDirectoryCantCheckUrl": "Error al comprobar la URL %1$s: %2$s", + "Sections": "Secciones", + "UrlsAccessibleViaBrowser": "Hemos comprobado que las URL anteriores son accesibles a través del navegador, pero recomendamos que no lo sean. Si es posible, restrinja el acceso a las mismas." + } +} diff --git a/plugins/Diagnostics/lang/et.json b/plugins/Diagnostics/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..c45fc54d0ed2bb1a6731f6390ace31a8a8a93f41 --- /dev/null +++ b/plugins/Diagnostics/lang/et.json @@ -0,0 +1,6 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Seadistuste fail", + "Sections": "Sektsioonid" + } +} diff --git a/plugins/Diagnostics/lang/eu.json b/plugins/Diagnostics/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..5132a57a2fc1bfc3666ff32cff30044814a47722 --- /dev/null +++ b/plugins/Diagnostics/lang/eu.json @@ -0,0 +1,39 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Direktorio pribatu guztiak ezin dira atzitu Internetetik.", + "BrowserAndAutoArchivingEnabledComment": "Dirudienez, nabigatzailea eta artxibatze automatikoa, biak gaituta daude. Azken artxibatze automatikoa duela %3$s hasi zen. %1$sArtxibatze automatikoa%2$s gaituta badago, nabigatzean artxibatzea desgaitu beharko zenuke \"Ezarpen orokorretan\".", + "BrowserAndAutoArchivingEnabledLabel": "Nabigatzailea eta artxibatze automatikoa gaituta", + "BrowserTriggeredArchivingEnabled": "Errendimendua hobetzeko eta Matomo azkar exekutatzeko, guztiz gomendagarria da zure txostenak automatikoki artxibatzeko crontab bat konfiguratzea, eta artxibatzea nabigatzaileak abiaraztea desgaitzea Matomoren ezarpenetan. %1$sIkasi gehiago.%2$s", + "ConfigFileIntroduction": "Hemen Matomoren konfigurazioa ikus dezakezu. Matomo karga balantzea duen ingurune batean exekutatzen baduzu, orria desberdina izango da orria kargatzeko erabilitako zerbitzariaren arabera. Atzeko planoko kolore desberdina duten errenkadak aldatutako konfigurazio balioak dituztenak dira,adibidez %1$s fitxategian zehazten direnak.", + "ConfigFileTitle": "Konfigurazio-fitxategia", + "ConfigIniAccessible": "Matomoren konfigurazio direktorioa publikoki atzitu daitekeela ere aurkitu dugu. Erasotzaileek ezin dute konfigurazioa irakurri une honetan, baina zure web-zerbitzariak PHP fitxategiak exekutatzeari uzten badio, zure MySQL kredentzialak eta gainerako informazioa edonoren esku egongo dira. Egiaztatu zure web-zerbitzariaren konfigurazioa eta ukatu direktorio horretarako sarbidea.", + "CronArchivingHasNotRun": "Artxibatzea ez da arrakastaz exekutatu oraindik.", + "CronArchivingHasNotRunInAWhile": "Azken artxibatze arrakastatsuaren data: %1$s, hau da, duela %2$s.", + "CronArchivingLastRunCheck": "Arrakastaz osatutako azken artxibatzea", + "CronArchivingRanSuccessfullyXAgo": "Artxibatzeko prozesua arrakastaz burutu zen duela %1$s.", + "CronArchivingRunDetails": "Egiaztatu crontab bat konfiguratu duzula kontsolako %1$s komandoa exekutatuz, eta %2$s bat konfiguratu duzula artxibatzeak huts eginez gero erroreak e-posta bidez jasotzeko. Zure txostenak artxibatzeko kontsolako komandoa eskuz ere exekutatu dezakezu: %3$s. %4$sIkasi gehiago.%5$s", + "DatabaseReaderConnection": "Datu-basearen irakurgailuaren konexioa", + "DatabaseUtf8Requirement": "Hau beharrezkoa da 4 byteko UTF8 karaktereak gorde ahal izateko. utf8mb4 ez badago erabilgarri, emojiak, asiar hizkuntzetako karaktere ez horren ohikoak, zenbait idazkera historikoko karaktereak edo ikur matematikoak bezalako karaktere bereziak honekin ordezkatzen dira: %1$s. Gaiari buruzko informazio gehiago aurkituko duzu %2$sMEG%3$s honetan.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Zure datu-baseak utf8mb4 karaktere-jokoa onartzen du, baina zure datu-baseko taulak ez dira bihurtu oraindik. %1$s komandoa exekutatuz egin dezakezu hori edo Ezarpen orokorretan bihurketa automatikoa gaituz.", + "DatabaseUtf8mb4CharsetRecommended": "Zure datu-baseak ez du utf8mb4 karaktere-jokoa onartzen oraindik.", + "EnableRequiredDirectoriesDiagnostic": "Egiaztapen hau saltatu da konfigurazioan desgaituta dagoelako. Egiaztapen hau gaitzeko ezarri [General] enable_required_directories_diagnostic = 1 \"config/config.ini.php\" fitxategian.", + "HideUnchanged": "Aldatutako balioak soilik ikusi nahi badituzu, %1$saldatu gabeko balioak ezkutatu%2$s ditzakezu.", + "HtaccessWarningNginx": "Fitxategi sentikorrak zuzenean ezin direla zuzenean atzitu ziurtatzeko, gomendagarria da zure web-zerbitzaria konfiguratzea zenbait direktoriotarako sarbidea mugatzeko. Informazio gehiagorako ikusi %1$s nginx zerbitzariaren konfigurazio ofiziala %2$s", + "MysqlMaxPacketSize": "Gehienezko pakete-tamaina", + "MysqlMaxPacketSizeWarning": "Gomendagarria da zure MySQL datu-basean '%1$smax_allowed_packet%2$s' %3$s edo tamaina handiago batera ezartzea. Une honetan %4$s ezarrita dago.", + "MysqlTemporaryTablesWarning": "Matomok MySQLren CREATE TEMPORARY TABLES baimena behar du behar bezala funtzionatzeko.", + "MysqlTransactionLevel": "Ezin da aldatu transakzioaren isolatze-maila. Hala ere artxibatzeak funtzionatuko du, baina mantsoagoa izan daiteke, eta ahal izanez gero gomendagarria da adibidez `binlog_format``row` baliora aldatzea adibidez.", + "NoDataForReportArchivingNotRun": "Zure txostenen artxibatzea ez da exekutatu azkenaldian, %1$sikasi gehiago zure txostenak sortzeko moduari buruz.%2$s", + "PHPFPMWarningApache": "PHP FPM-k ez ikusia egingo die .php fitxategientzako .htaccess arauei. Fitxategi sentikorrak zuzenean ezin direla atzitu ziurtatzeko, gomendagarria da zenbait direktorio PHP FPM-k maneiatzetik baztertzea, zure Apache-ren ostalari birtualaren konfigurazioan %1$s lerroa gehituz %2$s atalean, justu %3$s lerroaren gainean.", + "PHPFPMWarningGeneric": "PHP FPM-k ez ikusia egin diezaieke .php fitxategientzako .htaccess arauei. Fitxategi sentikorrak zuzenean ezin direla atzitu ziurtatzeko, gomendagarria da zure web-zerbitzaria konfiguratzea /config direktorioa PHP FPM-k maneiatzetik baztertzeko.", + "PHPFPMWarningNginx": "PHP FPM-k ez ikusia egingo die .php fitxategientzako .htaccess arauei. Fitxategi sentikorrak zuzenean ezin direla atzitu ziurtatzeko, gomendagarria da zenbait direktorio PHP FPM-k maneiatzetik baztertzea. Informazio gehiagorako ikusi %1$s nginx zerbitzariaren konfigurazio ofiziala %2$s", + "PrivateDirectoryCantCheckUrl": "%1$s URLa egiaztatzeak huts egin du: %2$s", + "PrivateDirectoryInternetDisabled": "Ezin izan dugu egiaztatu ondorengo URLak atzigarri dauden, Matomon Interneteko ezaugarriak desgaituta daudelako.", + "PrivateDirectoryIsAccessible": "Goiko URLak nabigatzaile bidez atzitu daitezkeela aurkitu dugu, baina EZ luke horrela izan behar. Atzigarri izateak segurtasun arriskua izan dezake, edukiek zure zerbitzariari buruzko informazioa eman dezaketelako, behar bada baita zure erabiltzaileei buruzkoa ere. Mugatu horietarako sarbidea.", + "PrivateDirectoryManualCheck": "Ireki URLak eskuz nabigatzaile batean, atzitu ditzakezun ikusteko. Atzitu baditzakezu, behar bada zure zerbitzariaren konfigurazioa aldatu beharko duzu, fitxategi/karpeta horiek ez bailukete atzigarri egon behar nabigatzaile bidez Interneten edo Intranetean.", + "RecommendedPrivateDirectories": "Gomendatutako direktorio pribatuak", + "RequiredPrivateDirectories": "Beharrezko direktorio pribatuak", + "Sections": "Atalak", + "UrlsAccessibleViaBrowser": "Goiko URLa nabigatzaile bidez atzitu daitekeela aurkitu dugu, baina hala ez izatea gomendatzen dugu. Posible bada, mugatu sarbidea." + } +} diff --git a/plugins/Diagnostics/lang/fa.json b/plugins/Diagnostics/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/fa.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/fi.json b/plugins/Diagnostics/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..124bd715d470c81331750a8a5f8c6846b0cc0dfe --- /dev/null +++ b/plugins/Diagnostics/lang/fi.json @@ -0,0 +1,13 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Asetustiedosto", + "CronArchivingHasNotRun": "Arkistointia ei ole vielä suoritettu onnistuneesti.", + "CronArchivingHasNotRunInAWhile": "Arkistointi suoritettiin onnistuneesti viimeksi %1$s, eli %2$s sitten.", + "CronArchivingLastRunCheck": "Viimeisin onnistunut arkistointi", + "CronArchivingRanSuccessfullyXAgo": "Arkistointiprosessi suoritettiin onnistuneesti %1$s sitten.", + "DatabaseUtf8mb4CharsetRecommended": "Tietokantasi ei tue vielä utf8mb4-merkistöä.", + "MysqlMaxPacketSize": "Paketin enimmäiskoko", + "NoDataForReportArchivingNotRun": "Raporttiesi arkistointia ei ole suoritettu hetkeen. %1$sLue lisää kuinka luoda raportteja.%2$s", + "Sections": "Osiot" + } +} diff --git a/plugins/Diagnostics/lang/fr.json b/plugins/Diagnostics/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..bc99d1d56fb2df86881222258a4590c0fe90d47d --- /dev/null +++ b/plugins/Diagnostics/lang/fr.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Tous les répertoires privés sont inaccessibles depuis Internet.", + "BrowserAndAutoArchivingEnabledComment": "Il semble que le navigateur et l'archivage automatique soient activés. L'archivage automatique a commencé il y a %3$s. Si %1$sauto archivage%2$s est activé, vous devez désactiver l'archivage du navigateur dans \"Paramètres généraux\".", + "BrowserAndAutoArchivingEnabledLabel": "Navigateur et archivage automatique activés", + "BrowserTriggeredArchivingEnabled": "Pour des performances optimales et un Matomo rapide, il est vivement recommandé de mettre en place une entrée CRON pour archiver automatiquement vos rapports, et de désactiver le déclenchement depuis l'interface depuis les paramètres Matomo. %1$sApprenez-en plus.%2$s", + "ConfigFileIntroduction": "Ici vous pouvez visualiser la configuration de Matomo. Si vous exécutez Matomo depuis un environnement où la charge est répartie, la page peut être différente en fonction du serveur depuis lequel la page est chargée. Les lignes avec une couleur de fond différentes représentent les valeurs de configuration modifiées qui sont spécifiées par exemple dans le fichier %1$s.", + "ConfigFileTitle": "Fichier de configuration", + "ConfigIniAccessible": "Nous avons également constaté que le répertoire de configuration de Matomo est accessible au public. Bien que les attaquants ne puissent pas lire la configuration maintenant, si votre serveur Web cesse d'exécuter les fichiers PHP pour une raison quelconque, vos informations d'identification MySQL et d'autres informations seront disponibles pour tout le monde. Veuillez vérifier la configuration de votre serveur Web et refuser l'accès à ce répertoire.", + "CronArchivingHasNotRun": "L'archivage ne s'est pas encore exécuté avec succès.", + "CronArchivingHasNotRunInAWhile": "L'archivage s'est exécuté avec succès le %1$s soit il y a %2$s.", + "CronArchivingLastRunCheck": "Dernière exécution d'archivage réussie", + "CronArchivingRanSuccessfullyXAgo": "Le processus d'archivage s'est terminé avec succès il y a %1$s.", + "CronArchivingRunDetails": "Veuillez vérifier que vous avez une entrée dans la table de CRON en exécutant la commande %1$s depuis la console, et que vous avez configuré un %2$s afin de recevoir les erreurs par courriel si l'archivage échoue. Vous pouvez aussi essayer d'exécuter la commande manuellement depuis la console pour archiver vos rapports manuellement : %3$s. %4$sEn apprendre plus.%5$s", + "DatabaseCollationCharset": "La collation par défaut pour le charset que vous avez configuré est : %1$s", + "DatabaseCollationConnection": "La collation de connexion actuellement utilisée est : %1$s", + "DatabaseCollationNotConfigured": "Votre connexion à la base de données est configurée sans une collation explicite. Veuillez mettre à jour [database] collation = '' dans le fichier \"config/config.ini.php\" avec la collation à utiliser, afin de garantir que toutes les fonctionnalités de la base de données fonctionnent comme prévu.", + "DatabaseReaderConnection": "Connexion au lecteur de la base de données", + "DatabaseUtf8Requirement": "Ceci est nécessaire pour pouvoir stocker des caractères UTF8 de 4 octets. À moins que utf8mb4 ne soit disponible, les caractères spéciaux, tels que les emojis, les caractères moins courants des langues asiatiques, divers scripts historiques ou symboles mathématiques seront remplacés par %1$s. Vous pouvez lire plus de détails sur ce sujet dans %2$scette FAQ%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Votre base de données prend en charge le jeu de caractères utf8mb4, mais vos tables de base de données n'ont pas encore été converties. Vous pouvez le faire en exécutant la commande %1$s ou en activant la conversion automatique dans les paramètres généraux.", + "DatabaseUtf8mb4CharsetRecommended": "Votre base de données ne prend pas encore en charge le jeu de caractères utf8mb4.", + "EnableRequiredDirectoriesDiagnostic": "Cette vérification a été ignorée car elle est désactivée dans la configuration. Pour activer cette vérification, définissez [General] enable_required_directories_diagnostic = 1 dans le fichier \"config/config.ini.php.", + "HideUnchanged": "Si vous souhaitez afficher uniquement les valeurs modifiées, vous pouvez cliquer sur %1$smasquer toutes les valeurs inchangées%2$s.", + "HtaccessWarningNginx": "Pour garantir l'impossibilité d'accéder directement aux fichiers sensibles, il est recommandé de configurer votre serveur web afin de restreindre l'accès à certains répertoires. Pour plus d'informations, veuillez consulter la %1$s configuration officielle du serveur nginx %2$s", + "MariaDbNotConfigured": "Votre version de base de données indique que vous utilisez peut-être un serveur MariaDb. Si c'est le cas, veuillez vous assurer d'avoir défini [database] schema = Mariadb dans le fichier \"config/config.ini.php\", afin que toutes les fonctionnalités de la base de données fonctionnent comme prévu.", + "MysqlMaxPacketSize": "Taille maximale des paquets", + "MysqlMaxPacketSizeWarning": "Il est important de configurer une taille '%1$smax_allowed_packet%2$s' dans votre base de données MySQL d'au moins %3$s. %4$s configuré en ce moment.", + "MysqlTemporaryTablesWarning": "L'autorisation MySQL CREATE TEMPORARY TABLES est requise pour que Matomo fonctionne correctement.", + "MysqlTransactionLevel": "Modification du niveau d'isolement des transactions non prise en charge. L'archivage fonctionnera toujours mais il peut être plus lent et il est recommandé de changer par exemple le `binlog_format` en `row` si possible.", + "NoDataForReportArchivingNotRun": "L'archivage de votre rapport ne s'est pas exécuté correctement, %1$s apprenez-en plus sur comment générer vos rapports.%2$s", + "PHPFPMWarningApache": "PHP FPM ignorera les règles .htaccess pour les fichiers .php. Pour s'assurer que les fichiers sensibles ne sont pas accessibles directement, il est recommandé d'exclure certains répertoires de la gestion de PHP FPM en ajoutant la ligne %1$s à la section %2$s de la configuration de votre hôte virtuel Apache, juste au-dessus de la ligne %3$s.", + "PHPFPMWarningGeneric": "PHP FPM peut ignorer les règles .htaccess pour les fichiers .php. Pour s'assurer que les fichiers sensibles ne peuvent pas être accédés directement, il est recommandé de configurer votre serveur web pour exclure le répertoire /config de la gestion par PHP FPM.", + "PHPFPMWarningNginx": "PHP FPM ignorera les règles .htaccess pour les fichiers .php. Pour s'assurer que les fichiers sensibles ne sont pas accessibles directement, il est recommandé d'exclure certains répertoires de la gestion de PHP FPM. Pour plus d'informations, veuillez consulter la configuration officielle du serveur nginx %1$s %2$s", + "PrivateDirectoryCantCheckUrl": "La vérification de l'URL %1$s a échoué : %2$s", + "PrivateDirectoryInternetDisabled": "Nous n'avons pas pu vérifier si les URL suivantes sont accessibles car les fonctionnalités Internet sont désactivées sur ce Matomo.", + "PrivateDirectoryIsAccessible": "Nous n'avons pas pu vérifier si les URL suivantes sont accessibles car les fonctionnalités Internet sont désactivées sur ce Matomo.", + "PrivateDirectoryManualCheck": "Veuillez ouvrir les URL manuellement dans un navigateur pour voir si vous pouvez y accéder. Si vous le pouvez, vous devrez peut-être modifier la configuration de votre serveur car ces fichiers/répertoires ne devraient pas être accessibles via un navigateur à partir d'Internet ou d'un intranet.", + "RecommendedPrivateDirectories": "Répertoires privés recommandés", + "RequiredPrivateDirectories": "Répertoires privés requis", + "Sections": "Sections", + "UrlsAccessibleViaBrowser": "Nous avons constaté que les URL ci-dessus sont accessibles via le navigateur, mais nous vous déconseillons de l'être. Si possible, veuillez en restreindre l'accès." + } +} diff --git a/plugins/Diagnostics/lang/ga.json b/plugins/Diagnostics/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..d1d77e7ce96fae327343727cb06608db3fefff7f --- /dev/null +++ b/plugins/Diagnostics/lang/ga.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Tá gach eolaire príobháideach dorochtana ón idirlíon.", + "BrowserAndAutoArchivingEnabledComment": "Tá an chuma ar an scéal go bhfuil an brabhsálaí agus an chartlannú uathoibríoch cumasaithe. Cuireadh tús le cartlannú uathoibríoch %3$s ó shin. Má tá %1$sauto cartlannú%2$s cumasaithe, ba cheart duit cartlannú brabhsálaí a dhíchumasú i \"Socruithe Ginearálta\".", + "BrowserAndAutoArchivingEnabledLabel": "Brabhsálaí agus cartlannú Uathoibríoch cumasaithe", + "BrowserTriggeredArchivingEnabled": "Ar mhaithe le feidhmíocht optamach agus Matomo gasta, moltar go mór crontab a bhunú chun do thuairiscí a chartlannú go huathoibríoch, agus chun truicear brabhsálaí i socruithe Matomo a dhíchumasú. %1$sFoghlaim tuilleadh.%2$s", + "ConfigFileIntroduction": "Anseo is féidir leat breathnú ar chumraíocht Matomo. Má tá Matomo á rith agat i dtimpeallacht ualach-chothromaithe, seans go mbeidh an leathanach difriúil ag brath ar an bhfreastalaí a bhfuil an leathanach seo lódáilte aige. Athraítear sraitheanna a bhfuil dath cúlra eile orthu na luachanna cumraíochta atá sonraithe mar shampla sa chomhad %1$s.", + "ConfigFileTitle": "Comhad cumraíochta", + "ConfigIniAccessible": "Fuaireamar amach freisin go bhfuil eolaire cumraíochta Matomo inrochtana go poiblí. Cé nach féidir le hionsaitheoirí an cumraíocht a léamh anois, má stopann do fhreastalaí gréasáin comhaid PHP a fhorghníomhú ar chúis éigin, beidh do dhintiúir MySQL agus faisnéis eile ar fáil do dhuine ar bith. Seiceáil cumraíocht do fhreastalaí gréasáin agus diúltaigh rochtain ar an eolaire seo.", + "CronArchivingHasNotRun": "Níor éirigh go maith le cartlannú go rathúil.", + "CronArchivingHasNotRunInAWhile": "D'éirigh leis an gcartlann an uair dheireanach ar %1$s atá %2$s ó shin.", + "CronArchivingLastRunCheck": "Críochnú cartlainne rathúil deireanach", + "CronArchivingRanSuccessfullyXAgo": "D'éirigh leis an bpróiseas cartlainne a chríochnú %1$s ó shin.", + "CronArchivingRunDetails": "Cinntigh go bhfuil crontab socraithe agat a ghlaoigh an t-ordú consól %1$s, agus go bhfuil %2$s cumraithe agat chun earráidí a fháil trí ríomhphost má theipeann ar an gcartlann. Is féidir leat freisin triail a bhaint as ordú an chonsóil chun do thuairiscí a chartlannú de láimh: %3$s. %4$sFoghlaim tuilleadh.%5$s", + "DatabaseCollationCharset": "Is é an comhthiomsú réamhshocraithe le do thacar carachtair cumraithe ná: %1$s", + "DatabaseCollationConnection": "Is é an comhthiomsú naisc atá in úsáid agat faoi láthair ná: %1$s", + "DatabaseCollationNotConfigured": "Tá do nasc bunachar sonraí cumraithe gan comhthiomsú sainráite. Nuashonraigh [database] collation = '' sa chomhad \"config/config.ini.php\" leis an gcomhthiomsú a úsáidfear, lena chinntiú go n-oibríonn gnéithe an bhunachair shonraí go léir mar a bheifeá ag súil leis.", + "DatabaseReaderConnection": "Nasc Léitheoir Bunachar Sonraí", + "DatabaseUtf8Requirement": "Ní mór é seo a bheith in ann carachtair 4-bheart UTF8 a stóráil. Mura bhfuil utf8mb4 ar fáil cuirfear %1$s in ionad carachtair speisialta ar nós emojis, carachtair as teangacha Áiseacha nach bhfuil chomh coitianta, scripteanna stairiúla éagsúla nó siombailí matamaitice. Is féidir leat tuilleadh sonraí a léamh faoin ábhar seo i %2$s na Ceisteanna Coitianta seo%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Tacaíonn do bhunachar sonraí le tacar charn utf8mb4, ach níor tiontaíodh táblaí do bhunachar sonraí fós. Is féidir leat é seo a dhéanamh tríd an ordú %1$s a rith nó an tiontú uathoibríoch sna Socruithe Ginearálta a ghníomhachtú.", + "DatabaseUtf8mb4CharsetRecommended": "Ní thacaíonn do bhunachar sonraí le tacar carachtair utf8mb4 fós.", + "EnableRequiredDirectoriesDiagnostic": "Léiríodh an tseiceáil seo toisc go bhfuil an tseiceáil seo díchumasaithe sa chumraíocht. Chun an tseiceáil seo a chumasú, leag [Ginearálta] enable_required_directories_diagnostic = 1 sa chomhad \"config/config.ini.php\".", + "HideUnchanged": "Mura dteastaíonn uait ach luachanna athraithe a fheiceáil is féidir leat %1$s gach luach gan athrú %2$s a cheilt.", + "HtaccessWarningNginx": "Chun a chinntiú nach féidir teacht ar chomhaid íogair go díreach, moltar do fhreastalaí gréasáin a chumrú chun rochtain ar eolairí áirithe a shrianadh. Le haghaidh tuilleadh eolais, féach cumraíocht oifigiúil freastalaí nginx %1$s %2$s", + "MariaDbNotConfigured": "Léiríonn leagan do bhunachar sonraí go mb'fhéidir go bhfuil tú ag úsáid freastalaí MariaDb. Más é seo an cás, cinntigh le do thoil [database] schema = Mariadb a shocrú sa chomhad \"config/config.ini.php\", chun a chinntiú go n-oibreoidh gné an bhunachair shonraí go léir mar a bheifeá ag súil leis.", + "MysqlMaxPacketSize": "Méid Paicéad Uasta", + "MysqlMaxPacketSizeWarning": "Moltar méid '%1$smax_allowed_packet%2$s' a chumrú i do bhunachar sonraí MySQL de %3$s ar a laghad. Cumraithe %4$s faoi láthair.", + "MysqlTemporaryTablesWarning": "Teastaíonn cead MySQL CRUTHÚ TÁBLAÍ SEALADACHA le go n-oibreoidh Matomo i gceart.", + "MysqlTransactionLevel": "Ní thacaítear le leibhéal leithlisithe idirbhirt a athrú. Oibreoidh an chartlannú go fóill ach seans go mbeidh sé níos moille agus moltar an `bhionlog_format` a athrú go `row` más féidir.", + "NoDataForReportArchivingNotRun": "Níl cartlannú do thuairiscí curtha i gcrích le déanaí, %1$s tuilleadh faisnéise a fháil faoi conas do thuairiscí a ghiniúint.%2$s", + "PHPFPMWarningApache": "Déanfaidh PHP FPM neamhaird ar rialacha .htaccess do chomhaid .php. Chun a chinntiú nach féidir teacht ar chomhaid íogair go díreach moltar duit comhadlanna áirithe a eisiamh ó bheith á láimhseáil ag PHP FPM tríd an líne %1$s a chur leis an gcuid %2$s i do chumraíocht óstríomhaire apache díreach os cionn %3$s líne.", + "PHPFPMWarningGeneric": "Féadfaidh PHP FPM neamhaird a dhéanamh ar rialacha .htaccess do chomhaid .php. Chun a chinntiú nach féidir teacht ar chomhaid íogair go díreach, moltar do fhreastalaí gréasáin a chumrú chun an eolaire /config a eisiamh ó bheith á láimhseáil ag PHP FPM.", + "PHPFPMWarningNginx": "Déanfaidh PHP FPM neamhaird ar rialacha .htaccess do chomhaid .php. Chun a chinntiú nach féidir teacht ar chomhaid íogair go díreach, moltar gan eolairí áirithe a bheith á láimhseáil ag PHP FPM. Le haghaidh tuilleadh eolais, féach cumraíocht oifigiúil freastalaí nginx %1$s %2$s", + "PrivateDirectoryCantCheckUrl": "Theip ar URL %1$s a sheiceáil: %2$s", + "PrivateDirectoryInternetDisabled": "Níorbh fhéidir linn a sheiceáil an bhfuil rochtain ar na URLanna seo a leanas toisc go bhfuil gnéithe idirlín díchumasaithe ar an Matomo seo.", + "PrivateDirectoryIsAccessible": "Fuaireamar amach go bhfuil na URLanna thuas inrochtana tríd an mbrabhsálaí, ach níor cheart dóibh a bheith. D’fhéadfadh riosca slándála a bheith ann dá gceadófaí rochtain a fháil orthu toisc gur féidir leis an ábhar faisnéis a sholáthar faoi do fhreastalaí agus faoi d’úsáideoirí a d’fhéadfadh a bheith ann. Cuir srian le rochtain orthu.", + "PrivateDirectoryManualCheck": "Oscail na URLanna de láimh i mbrabhsálaí féachaint an féidir leat rochtain a fháil air. Más féidir leat, b'fhéidir go mbeadh ort cumraíocht do fhreastalaí a mhodhnú mar níor cheart go mbeadh na comhaid/eolaire seo inrochtana trí bhrabhsálaí ón Idirlíon nó ón Inlíon.", + "RecommendedPrivateDirectories": "Comhadlanna Príobháideacha Molta", + "RequiredPrivateDirectories": "Comhadlanna Príobháideacha Riachtanach", + "Sections": "Ailt", + "UrlsAccessibleViaBrowser": "Fuaireamar amach go bhfuil na URLanna thuas inrochtana tríd an mbrabhsálaí, ach molaimid nár cheart dóibh a bheith. Más féidir, cuir srian le rochtain orthu." + } +} diff --git a/plugins/Diagnostics/lang/gl.json b/plugins/Diagnostics/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/gl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/gu.json b/plugins/Diagnostics/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..de5f762baf2e11e0e9eef3b576768f9d060ca9b4 --- /dev/null +++ b/plugins/Diagnostics/lang/gu.json @@ -0,0 +1,39 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "ઇંટરનેટ પરથી બધી ખાનગી ડિરેક્ટરીઓ અણસુલભ છે.", + "BrowserAndAutoArchivingEnabledComment": "લાગે છે કે બ્રાઉઝર અને ઓટો આર્કાઇવિંગ બંને સક્ષમ છે. ઓટો આર્કાઇવિંગ છેલ્લે %3$s પહેલા શરૂ થયેલ છે. જો %1$sઓટો આર્કાઇવિંગ%2$s સક્ષમ છે, તો તમે \"જનરલ સેટિંગ્સ\"માં બ્રાઉઝર આર્કાઇવિંગને નિષ્ક્રિય કરવું જોઈએ.", + "BrowserAndAutoArchivingEnabledLabel": "બ્રાઉઝર અને ઓટો-આર્કાઇવિંગ સક્ષમ", + "BrowserTriggeredArchivingEnabled": "શ્રેષ્ઠ પ્રદર્શન અને ઝડપી Matomo માટે, તમારા અહેવાલોને આપમેળે આર્કાઇવ કરવા માટે અને Matomo સેટિંગ્સમાં બ્રાઉઝર ટ્રિગરિંગને અક્ષમ કરવા માટે ક્રોન્ટાબ સેટ કરવાની ખૂબ ભલામણ કરવામાં આવે છે. %1$sવધુ જાણો.%2$s", + "ConfigFileIntroduction": "અહીં તમે Matomo કોન્ફિગ્યુરેશન જોઈ શકો છો. જો તમે લોડ સંતુલિત વાતાવરણમાં માટોમો ચલાવી રહ્યા હોવ, તો આ પૃષ્ઠ કયા સર્વરથી લોડ થયું છે તેના આધારે પૃષ્ઠ અલગ હોઈ શકે છે. ભિન્ન પૃષ્ઠભૂમિ રંગ સાથેની પંક્તિઓ રૂપરેખા મૂલ્યો બદલાય છે જે ઉદાહરણ તરીકે %1$s ફાઇલમાં ઉલ્લેખિત છે.", + "ConfigFileTitle": "કોન્ફિગ ફાઇલ", + "ConfigIniAccessible": "અમે એ પણ જોયું કે Matomo ની કોન્ફિગ ડિરેક્ટરી સાર્વજનિક રીતે ઍક્સેસિબલ છે. જ્યારે હુમલાખોરો અત્યારે કોન્ફિગ વાંચી શકતા નથી, જો તમારું વેબસર્વર કોઈ કારણસર PHP ફાઇલોને એક્ઝિક્યુટ કરવાનું બંધ કરે, તો તમારા MySQL ઓળખપત્રો અને અન્ય માહિતી કોઈપણ માટે ઉપલબ્ધ રહેશે. કૃપા કરીને તમારી વેબસર્વર કોન્ફિગ તપાસો અને આ ડિરેક્ટરીની ઍક્સેસને નકારી કાઢો.", + "CronArchivingHasNotRun": "આર્કાઇવિંગ હજુ સુધી સફળતાપૂર્વક ચાલ્યું નથી.", + "CronArchivingHasNotRunInAWhile": "છેલ્લું આર્કાઇવિંગ %1$s ના રોજ સફળતાપૂર્વક ચાલ્યું જે %2$s પહેલા છે.", + "CronArchivingLastRunCheck": "છેલ્લું સફળ આર્કાઇવિંગ પૂર્ણ", + "CronArchivingRanSuccessfullyXAgo": "આર્કાઇવિંગ પ્રક્રિયા %1$s પહેલા સફળતાપૂર્વક પૂરી થઈ.", + "CronArchivingRunDetails": "કૃપા કરીને તપાસો કે તમે %1$s કન્સોલ કમાન્ડને કૉલ કરવા માટે ક્રોન્ટાબ સેટઅપ કર્યું છે અને જો આર્કાઇવિંગ નિષ્ફળ જાય તો ઇમેઇલ દ્વારા ભૂલો મેળવવા માટે તમે %2$s ને ગોઠવ્યું છે. તમે તમારા રિપોર્ટ્સને મેન્યુઅલી આર્કાઇવ કરવા માટે કન્સોલ આદેશ ચલાવવાનો પણ પ્રયાસ કરી શકો છો: %3$s. %4$sવધુ જાણો.%5$s", + "DatabaseReaderConnection": "ડેટાબેઝ રીડર કનેક્શન", + "DatabaseUtf8Requirement": "4-બાઇટ UTF8 અક્ષરો સંગ્રહિત કરવા માટે સક્ષમ થવા માટે આ જરૂરી છે. જ્યાં સુધી utf8mb4 ઉપલબ્ધ ન હોય ત્યાં સુધી વિશેષ અક્ષરો, જેમ કે ઇમોજીસ, એશિયન ભાષાઓના ઓછા સામાન્ય અક્ષરો, વિવિધ ઐતિહાસિક સ્ક્રિપ્ટો અથવા ગાણિતિક પ્રતીકોને %1$s સાથે બદલવામાં આવશે. તમે આ વિષય વિશે વધુ વિગતો %2$sthis FAQ%3$s માં વાંચી શકો છો.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "તમારો ડેટાબેઝ utf8mb4 અક્ષરસેટને સપોર્ટ કરે છે, પરંતુ તમારા ડેટાબેઝ કોષ્ટકો હજુ સુધી રૂપાંતરિત થયા નથી. તમે આ આદેશ %1$s ચલાવીને અથવા સામાન્ય સેટિંગ્સમાં સ્વચાલિત રૂપાંતરણને સક્રિય કરીને કરી શકો છો.", + "DatabaseUtf8mb4CharsetRecommended": "તમારો ડેટાબેઝ હજુ સુધી utf8mb4 અક્ષરસેટને સપોર્ટ કરતું નથી.", + "EnableRequiredDirectoriesDiagnostic": "આ ચેક અવગણવામાં આવ્યો હતો કારણ કે આ ચેક રૂપરેખામાં અક્ષમ છે. આ ચેકને સક્ષમ કરવા માટે \"config/config.ini.php\" ફાઈલમાં [General] enable_required_directories_diagnostic = 1 સેટ કરો.", + "HideUnchanged": "જો તમે ફક્ત બદલાયેલ મૂલ્યો જોવા માંગો છો, તો તમે %1$sબધા અપરિવર્તિત મૂલ્યો છુપાવી શકો છો%2$s.", + "HtaccessWarningNginx": "સંવેદનશીલ ફાઇલો સીધી રીતે એક્સેસ કરી શકાતી નથી તેની ખાતરી કરવા માટે અમુક ડિરેક્ટરીઓની ઍક્સેસને પ્રતિબંધિત કરવા માટે તમારા વેબ સર્વરને કન્ફિગર કરવાની ભલામણ કરવામાં આવે છે. વધુ માહિતી માટે કૃપા કરીને %1$s સત્તાવાર nginx સર્વર કન્ફિગરેશન %2$s જુઓ", + "MysqlMaxPacketSize": "મહત્તમ પેકેટ કદ", + "MysqlMaxPacketSizeWarning": "તમારા MySQL ડેટાબેઝમાં '%1$smax_allowed_packet%2$s' કદને ઓછામાં ઓછી %3$s કન્ફિગર કરવાની ભલામણ કરવામાં આવે છે. હાલમાં કન્ફિગર કરેલ છે %4$s.", + "MysqlTemporaryTablesWarning": "Matomo યોગ્ય રીતે કાર્ય કરે તે માટે CREATE TEMPORARY TABLES પરવાનગી MySQL માટે જરૂરી છે.", + "MysqlTransactionLevel": "ટ્રાન્સેક્શન આઇસોલેશન લેવલનો ફેરફાર કરવાનું સમર્થન નથી. આર્કાઇવિંગ હજુ પણ કામ કરશે પરંતુ તે હજુ પણ મંદ હોઈ શકે છે અને તેને ફેરફાર કરવાની ભલામણ કરવામાં આવે છે ઉદાહરણ તરીકે `binlog_format` ને `row` માં જો શક્ય હોય તો.", + "NoDataForReportArchivingNotRun": "તમારા અહેવાલોની આર્કાઇવિંગ તાજેતરમાં કરવામાં આવી નથી, %1$sતમારા અહેવાલો કેવી રીતે ઉત્પન્ન કરવા વિશે વધુ જાણો.%2$s", + "PHPFPMWarningApache": "PHP FPM .php ફાઇલો માટે .htaccess નિયમોને અવગણશે. સંવેદનશીલ ફાઇલો સીધી ઍક્સેસ ન થાય તેની ખાતરી કરવા માટે કેટલીક ડિરેક્ટરીઓને PHP FPM દ્વારા હેન્ડલ થવાથી બહાર રાખવા માટે તમારા અપાચે વર્ચ્યુઅલ હોસ્ટ કોન્ફિગમાં %2$s વિભાગમાં %1$s લાઇન ઉમેરવાની ભલામણ કરવામાં આવે છે, જે %3$s લાઇન ઉપર છે.", + "PHPFPMWarningGeneric": "PHP FPM .php ફાઇલો માટે .htaccess નિયમોને અવગણી શકે છે. સંવેદનશીલ ફાઈલો સીધી રીતે એક્સેસ કરી શકાતી નથી તેની ખાતરી કરવા માટે તમારા વેબ સર્વરને PHP FPM દ્વારા હેન્ડલ કરવામાં આવતી /config ડિરેક્ટરીને બાકાત રાખવા માટે રૂપરેખાંકિત કરવાની ભલામણ કરવામાં આવે છે.", + "PHPFPMWarningNginx": "PHP FPM .php ફાઇલો માટે .htaccess નિયમોને અવગણશે. સંવેદનશીલ ફાઇલો સીધી રીતે એક્સેસ કરી શકાતી નથી તેની ખાતરી કરવા માટે PHP FPM દ્વારા હેન્ડલ કરવામાં આવતી અમુક ડિરેક્ટરીઓને બાકાત રાખવાની ભલામણ કરવામાં આવે છે. વધુ માહિતી માટે કૃપા કરીને %1$s સત્તાવાર nginx સર્વર ગોઠવણી %2$s જુઓ", + "PrivateDirectoryCantCheckUrl": "URL %1$s તપાસવાનું નિષ્ફળ થયું: %2$s", + "PrivateDirectoryInternetDisabled": "આપણે નીચેના URL ઓ ઍક્સેસિબલ છે કે નહીં તે તપાસી શક્યા નથી કારણ કે આ માટોમો પર ઇન્ટરનેટ ફિચર્સ અક્ષમ છે.", + "PrivateDirectoryIsAccessible": "અમને જાણવા મળ્યું છે કે ઉપરોક્ત URL બ્રાઉઝર દ્વારા ઍક્સેસિબલ છે, પરંતુ તે ન હોવા જોઈએ. તેમને ઍક્સેસ કરવાની મંજૂરી આપવાથી સંભવિત સુરક્ષા જોખમ ઊભું થઈ શકે છે કારણ કે સામગ્રી તમારા સર્વર અને સંભવિત તમારા વપરાશકર્તાઓ વિશે માહિતી પ્રદાન કરી શકે છે. કૃપા કરીને તેમની ઍક્સેસને પ્રતિબંધિત કરો.", + "PrivateDirectoryManualCheck": "કૃપા કરીને URL ઓ ને મેન્યુઅલી બ્રાઉઝરમાં ખોલો અને જોવો કે તમે તેને પ્રવેશ-હક મેળવી શકો છો કે નહીં. જો તમે તેને પ્રવેશ-હક મેળવી શકો છો, તો તમારે તમારી સર્વર રૂપરેખાંકનને સુધારવું પડી શકે છે કારણ કે આ ફાઇલો / ડિરેક્ટરીઓ ઇન્ટરનેટ અથવા ઇન્ટ્રાનેટ દ્વારા બ્રાઉઝર મારફતે પ્રવેશ-હક મેળવી શકતી નથી.", + "RecommendedPrivateDirectories": "ભલામણ કરેલ ખાનગી ડિરેક્ટરીઓ", + "RequiredPrivateDirectories": "જરૂરી ખાનગી ડાયરેક્ટરીઓ", + "Sections": "વિભાગો", + "UrlsAccessibleViaBrowser": "અમને જણાયું છે કે ઉપરોક્ત URL બ્રાઉઝર દ્વારા ઍક્સેસિબલ છે, પરંતુ અમે ભલામણ કરીએ છીએ કે તે ન હોવા જોઈએ. જો શક્ય હોય તો, કૃપા કરીને તેમની ઍક્સેસને પ્રતિબંધિત કરો." + } +} diff --git a/plugins/Diagnostics/lang/he.json b/plugins/Diagnostics/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/he.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/hi.json b/plugins/Diagnostics/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/hi.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/hr.json b/plugins/Diagnostics/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/hr.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/hu.json b/plugins/Diagnostics/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/hu.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/hy.json b/plugins/Diagnostics/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/id.json b/plugins/Diagnostics/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..d6bf85b46e8d0a1b658e74e439ba2b74d6f0733d --- /dev/null +++ b/plugins/Diagnostics/lang/id.json @@ -0,0 +1,38 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Semua direktori pribadi tidak dapat diakses dari internet.", + "BrowserAndAutoArchivingEnabledComment": "Sepertinya peramban dan pengarsipan otomatis diaktifkan. Pengarsipan otomatis terakhir dimulai pada %3$s yang lalu. Jika %1$spengarsipan otomatis%2$s diaktifkan, Anda harus menonaktifkan pengarsipan peramban di \"Pengaturan Umum\".", + "BrowserAndAutoArchivingEnabledLabel": "Peramban dan Pengarsipan otomatis diaktifkan", + "BrowserTriggeredArchivingEnabled": "Untuk kinerja Matomo yang optimal dan cepat, sangat disarankan untuk menyiapkan crontab untuk mengarsipkan laporan Anda secara otomatis dan untuk menonaktifkan pemicu peramban di pengaturan Matomo. %1$sPelajari lebih lanjut.%2$s", + "ConfigFileIntroduction": "Konfigurasi Matomo dapat dilihat pada bagian ini. Jika Anda menggunakan perangkat lunak Matomo dalam lingkungan penyeimbangan beban, informasi pada halaman mungkin berbeda tergantung pada server tempat perangkat tersebut dipasang. Garis yang dimodifikasi sesuai dengan nilai konfigurasi yang ditentukan dalam sampel di berkas %1$s ditampilkan dalam warna latar belakang yang berbeda.", + "ConfigFileTitle": "Berkas konfigurasi", + "ConfigIniAccessible": "Kami juga menemukan bahwa direktori konfigurasi Matomo dapat diakses oleh publik. Meskipun penyerang tidak dapat membaca konfigurasi sekarang, jika server web Anda berhenti mengeksekusi berkas PHP karena suatu alasan, kredensial MySQL Anda dan informasi lainnya akan tersedia bagi siapa saja. Silakan periksa konfigurasi server web Anda dan tolak akses ke direktori ini.", + "CronArchivingHasNotRun": "Pengarsipan belum berhasil dijalankan.", + "CronArchivingHasNotRunInAWhile": "Pengarsipan terakhir berhasil dijalankan pada %1$s yaitu %2$s yang lalu.", + "CronArchivingLastRunCheck": "Penyelesaian Pengarsipan Terakhir yang Berhasil", + "CronArchivingRanSuccessfullyXAgo": "Proses pengarsipan berhasil diselesaikan %1$s lalu.", + "CronArchivingRunDetails": "Harap periksa apakah Anda telah menyiapkan crontab yang memanggil perintah konsol %1$s, dan Anda telah mengatur %2$s untuk menerima kesalahan melalui surel jika pengarsipan gagal. Anda juga dapat mencoba menjalankan perintah konsol untuk mengarsipkan laporan Anda secara manual: %3$s. %4$sPelajari lebih lanjut.%5$s", + "DatabaseReaderConnection": "Koneksi Pembaca Basis Data", + "DatabaseUtf8Requirement": "Fitur ini diperlukan untuk dapat menyimpan karakter UTF8 4 byte. Tanpa utf8mb4, %1$s digunakan sebagai pengganti karakter khusus, emotikon (emoji), karakter Asia yang kurang umum, berbagai simbol sejarah atau matematika. Untuk informasi terperinci, Anda dapat melihatnya di %2$sFAQ%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Basis data Anda mendukung pengodean karakter utf8mb4, tetapi tabel basis data Anda belum dikonversi. Anda dapat melakukannya dengan menjalankan perintah %1$s atau mengaktifkan konversi otomatis di Pengaturan Umum.", + "DatabaseUtf8mb4CharsetRecommended": "Basis data Anda belum mendukung pengodean karakter utf8mb4.", + "EnableRequiredDirectoriesDiagnostic": "Pemeriksaan ini dilewati karena dinonaktifkan di dalam pengaturan. Untuk mengaktifkannya, centang set [General] enable_required_directories_diagnostic = 1 dalam berkas \"config/config.ini.php\".", + "HideUnchanged": "Jika Anda hanya ingin melihat nilai yang sudah diubah, Anda dapat %1$smenyembunyikan semua nilai yang tidak berubah%2$s.", + "HtaccessWarningNginx": "Untuk memastikan bahwa berkas sensitif tidak dapat diakses secara langsung, disarankan untuk mengkonfigurasi server web Anda untuk membatasi akses ke direktori tertentu. Untuk informasi lebih lanjut, silakan lihat %1$s konfigurasi server nginx resmi %2$s", + "MysqlMaxPacketSize": "Ukuran Paket Maksimum", + "MysqlMaxPacketSizeWarning": "Disarankan untuk mengonfigurasi '%1$smax_allowed_packet%2$s' minimal ke %3$s di basis data MySQL Anda. Saat ini disetel ke %4$s.", + "MysqlTemporaryTablesWarning": "Izin MySQL CREATE TEMPORARY TABLES diperlukan agar Matomo berfungsi dengan baik.", + "MysqlTransactionLevel": "Mengubah tingkat isolasi transaksi tidak didukung. Pengarsipan akan tetap berfungsi, namun mungkin lebih lambat dan disarankan untuk mengubah, misalnya, `binlog_format` menjadi `baris` jika memungkinkan.", + "NoDataForReportArchivingNotRun": "Pengarsipan laporan Anda belum dijalankan baru-baru ini, %1$spelajari lebih lanjut tentang cara membuat laporan.%2$s", + "PHPFPMWarningApache": "PHP FPM akan mengabaikan aturan .htaccess untuk berkas .php. Untuk memastikan bahwa berkas sensitif tidak dapat diakses secara langsung, disarankan untuk mengecualikan direktori tertentu agar tidak ditangani oleh PHP FPM dengan menambahkan baris %1$s ke bagian %2$s di konfigurasi host virtual apache Anda tepat di atas baris %3$s.", + "PHPFPMWarningGeneric": "PHP FPM mungkin mengabaikan aturan .htaccess untuk berkas .php. Untuk memastikan bahwa berkas sensitif tidak dapat diakses secara langsung, disarankan untuk mengkonfigurasi server web Anda untuk mengecualikan direktori /config agar tidak ditangani oleh PHP FPM.", + "PHPFPMWarningNginx": "PHP FPM akan mengabaikan aturan .htaccess untuk berkas .php. Untuk memastikan bahwa berkas sensitif tidak dapat diakses secara langsung, disarankan untuk mengecualikan direktori tertentu agar tidak ditangani oleh PHP FPM. Untuk informasi lebih lanjut silakan lihat %1$s konfigurasi server nginx resmi %2$s", + "PrivateDirectoryInternetDisabled": "Kami tidak dapat memeriksa apakah URL berikut dapat diakses karena fitur internet dinonaktifkan di Matomo.", + "PrivateDirectoryIsAccessible": "Kami menemukan bahwa URL di atas dapat diakses melalui peramban, tetapi seharusnya TIDAK seperti itu. Mengizinkan mereka untuk diakses dapat menimbulkan potensi risiko keamanan karena isinya dapat memberikan informasi tentang server dan pengguna Anda. Harap batasi akses tersebut.", + "PrivateDirectoryManualCheck": "Silakan buka URL secara manual di peramban untuk melihat apakah Anda dapat mengaksesnya. Jika bisa, Anda mungkin perlu mengubah konfigurasi server Anda karena berkas/direktori ini tidak boleh diakses melalui peramban dari Internet atau Intranet.", + "RecommendedPrivateDirectories": "Direktori Pribadi yang Direkomendasikan", + "RequiredPrivateDirectories": "Direktori Pribadi yang Diperlukan", + "Sections": "Bagian", + "UrlsAccessibleViaBrowser": "Kami menemukan bahwa URL di atas dapat diakses melalui peramban, tetapi sebaiknya tidak. Jika memungkinkan, harap batasi akses tersebut." + } +} diff --git a/plugins/Diagnostics/lang/is.json b/plugins/Diagnostics/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/is.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/it.json b/plugins/Diagnostics/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..ec52021b8f44d2ff8da1b679a9dd00913930b71a --- /dev/null +++ b/plugins/Diagnostics/lang/it.json @@ -0,0 +1,39 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Tutte le directory private sono accessibili da Internet.", + "BrowserAndAutoArchivingEnabledComment": "Sembra che sia il browser che l'archiviazione automatica siano abilitati. L'archiviazione automatica è iniziata l'ultima volta %3$s fa. Se l'%1$sarchiviazione automatica%2$s è abilitata, è necessario disabilitarla nelle \"Impostazioni generali\" del browser.", + "BrowserAndAutoArchivingEnabledLabel": "Browser e Auto-archiviazione abilitati", + "BrowserTriggeredArchivingEnabled": "Per delle prestazioni ottimali e un Matomo veloce, è altamente raccomandato di impostare un crontab per archiviare automaticamente i tuoi report, e di disabilitare l'attivazione dal browser nelle impostazioni di Matomo. %1$sLeggi altro.%2$s", + "ConfigFileIntroduction": "Qui puoi vedere la configurazione di Matomo. Se si esegue Matomo in un ambiente con bilanciamento del carico, la pagina potrebbe essere diversa a seconda del server su cui viene caricata. Righe con un colore di sfondo diverso hanno valori di configurazione cambiati, specificati per esempio nel file %1$s.", + "ConfigFileTitle": "File di configurazione", + "ConfigIniAccessible": "Abbiamo anche riscontrato che la directory di configurazione di Matomo è accessibile pubblicamente. Sebbene gli aggressori non possano leggere la configurazione ora, se il tuo server web interrompe l'esecuzione dei file PHP per qualche motivo, le tue credenziali MySQL e altre informazioni saranno disponibili a chiunque. Controlla la configurazione del tuo server web e nega l'accesso a questa directory.", + "CronArchivingHasNotRun": "L'archiviazione non è ancora stata eseguita con successo.", + "CronArchivingHasNotRunInAWhile": "L'ultima archiviazione è stata eseguita con successo il %1$s, cioè %2$s fa.", + "CronArchivingLastRunCheck": "Ultima archiviazione completata con successo", + "CronArchivingRanSuccessfullyXAgo": "Il processo di archiviazione è stato completato con successo %1$s fa.", + "CronArchivingRunDetails": "Verifica di aver configurato un crontab per chiamare il comando di console %1$s, e di aver configurato un %2$s per ricevere gli errori via email se l'archiviazione fallisce. Puoi anche provare a eseguire il comando di console per archiviare manualmente i tuoi report: %3$s. %4$sPer saperne di più.%5$s", + "DatabaseReaderConnection": "Connessione al Lettore del Database", + "DatabaseUtf8Requirement": "Ciò è richiesto per conservare i caratteri UTF8 a 4-byte. Se il charset utf8mb4 non è disponibile, caratteri speciali come le emoji, i caratteri meno comuni delle lingue asiatiche, vari glifi antichi o i simboli matematici verranno sostituiti con %1$s. Puoi leggere più dettagli su questo argomento in %2$squeste FAQ%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Il tuo database supporta il charset utf8mb4, ma le tabelle del tuo database non sono ancora state convertite. Puoi farlo eseguendo il comando %1$s o attivando la conversione automatica nelle Impostazioni Generali.", + "DatabaseUtf8mb4CharsetRecommended": "Il tuo database non supporta ancora il charset UTF8mb4.", + "EnableRequiredDirectoriesDiagnostic": "Questo controllo è stato saltato perchè è disabilitato nelle configurazioni. Per abilitare questo controllo imposta [General] enable_required_directories_diagnostic = 1 nel file \"config/config.ini.php\".", + "HideUnchanged": "Se vuoi vedere soltanto i valori cambiati, puoi %1$snascondere tutti quelli che non sono cambiati%2$s.", + "HtaccessWarningNginx": "Per garantire che i file sensibili non siano accessibili direttamente, si consiglia di configurare il server web per limitare l'accesso a determinate directory. Per maggiori informazioni, consultare la %1$s configurazione ufficiale del server nginx %2$s", + "MysqlMaxPacketSize": "Dimensione Massima Pacchetto", + "MysqlMaxPacketSizeWarning": "Si raccomanda di impostare la dimensione '%1$smax_allowed_packet%2$s' nel tuo database MySQL ad almeno %3$s. Quella impostata al momento è %4$s.", + "MysqlTemporaryTablesWarning": "È richiesta da Matomo per funzionare correttamente l'autorizzazione CREATE TEMPORARY TABLES.", + "MysqlTransactionLevel": "La modifica del livello di isolamento delle transazioni non è supportata. L'archiviazione continuerà a funzionare ma potrebbe essere più lenta e, se possibile, si consiglia di cambiare ad esempio `binlog_format` in` row`.", + "NoDataForReportArchivingNotRun": "L'archiviazione dei tuoi report non è stata eseguita di recente, %1$sleggi di più su come generare i tuoi report.%2$s", + "PHPFPMWarningApache": "PHP FPM ignorerà le regole .htaccess per i file .php. Per garantire che i file sensibili non siano accessibili direttamente, si raccomanda di escludere alcune directory dalla gestione di PHP FPM, aggiungendo la riga %1$s alla sezione %2$s nella configurazione dell'host virtuale apache, appena sopra la riga %3$s.", + "PHPFPMWarningGeneric": "PHP FPM può ignorare le regole .htaccess per i file .php. Per garantire che i file sensibili non siano accessibili direttamente, si consiglia di configurare il server web in modo da escludere la directory /config dalla gestione di PHP FPM.", + "PHPFPMWarningNginx": "PHP FPM ignorerà le regole .htaccess per i file .php. Per garantire che i file sensibili non siano accessibili direttamente, si consiglia di escludere alcune directory dalla gestione di PHP FPM. Per ulteriori informazioni, consultare la %1$s configurazione ufficiale del server nginx %2$s", + "PrivateDirectoryCantCheckUrl": "Controllo dell'URL %1$s non riuscito: %2$s", + "PrivateDirectoryInternetDisabled": "Non è stato possibile verificare se i seguenti URL sono accessibili perché le funzionalità Internet sono disabilitate su questo Matomo.", + "PrivateDirectoryIsAccessible": "Abbiamo riscontrato che gli URL di cui sopra sono accessibili tramite il browser, ma NON dovrebbero esserlo. Consentire l'accesso può rappresentare un potenziale rischio per la sicurezza poiché i contenuti possono fornire informazioni sul tuo server e potenzialmente sui tuoi utenti. Si prega di limitare l'accesso a essi.", + "PrivateDirectoryManualCheck": "Si prega di aprire manualmente gli URL in un browser per vedere se è possibile accedervi. Se possibile, potrebbe essere necessario modificare la configurazione del server poiché questi file/directory non dovrebbero essere accessibili tramite un browser da Internet o Intranet.", + "RecommendedPrivateDirectories": "Directory private consigliate", + "RequiredPrivateDirectories": "Directory private richieste", + "Sections": "Sezioni", + "UrlsAccessibleViaBrowser": "Abbiamo riscontrato che i suddetti URL sono accessibili tramite il browser, ma raccomandiamo che non lo siano. Se possibile, vi preghiamo di limitarne l'accesso." + } +} diff --git a/plugins/Diagnostics/lang/ja.json b/plugins/Diagnostics/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..33da5c6fc4fa228648837d4deaf0bf5650c3219c --- /dev/null +++ b/plugins/Diagnostics/lang/ja.json @@ -0,0 +1,38 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "すべてのプライベート ディレクトリは、インターネットからアクセスできません。", + "BrowserAndAutoArchivingEnabledComment": "ブラウザと自動アーカイブの両方が有効になっているようです。 自動アーカイブは最後に %3$s より前に開始されました。 %1$s自動アーカイブ%2$sが有効になっている場合は、\"一般設定\" でブラウザのアーカイブを無効にする必要があります。", + "BrowserAndAutoArchivingEnabledLabel": "ブラウザと自動アーカイブが有効になっています", + "BrowserTriggeredArchivingEnabled": "最適なパフォーマンスと迅速な Matomo のために、レポートを自動的にアーカイブするように crontab をセットアップし、Matomo 設定でブラウザのトリガーを無効にすることを強くお勧めします。%1$s詳細%2$s", + "ConfigFileIntroduction": "ここで Matomo の設定を見ることができます。 負荷分散環境で Matomo を実行している場合、このページがどのサーバーからロードされるかによってページが異なる場合があります。 背景色が異なる行は、 %1$sファイルなどで指定されている構成値が変更されます。", + "ConfigFileTitle": "コンフィグファイル", + "ConfigIniAccessible": "また、Matomo の config ディレクトリが公開されていることもわかりました。 攻撃者は現在、構成を読み取ることはできませんが、Web サーバーが何らかの理由で PHP ファイルの実行を停止した場合、MySQL 資格情報やその他の情報は誰でも利用できるようになります。 Web サーバーの設定を確認し、このディレクトリへのアクセスを拒否してください。", + "CronArchivingHasNotRun": "アーカイブはまだ正常に実行されていません。", + "CronArchivingHasNotRunInAWhile": "アーカイブは、 %2$s 前の %1$s で正常に実行されました。", + "CronArchivingLastRunCheck": "最後に正常に完了したアーカイブ", + "CronArchivingRanSuccessfullyXAgo": "アーカイブプロセスは%1$s 前に正常に完了しました。", + "CronArchivingRunDetails": "%1$s コンソールコマンドを呼び出すcrontabを設定したこと、およびアーカイブが失敗した場合に電子メールでエラーを受信するように %2$s を設定したことを確認してください。 手動でレポートをアーカイブするために console コマンドを実行してみることもできます:%3$s。 %4$sもっと詳しく知る。%5$s", + "DatabaseReaderConnection": "データベースリーダー接続", + "DatabaseUtf8Requirement": "これは、4 バイトの UTF8 文字を格納できるようにするために必要です。 utf8mb4 が使用可能でない限り、絵文字などの特殊文字、アジア言語のあまり一般的でない文字、さまざまな歴史的なスクリプトまたは数学記号は %1$s に置き換えられます。 このトピックの詳細については、%2$sこの FAQ %3$sを参照してください。", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "データベースは utf8mb4 文字セットをサポートしていますが、データベース テーブルはまだ変換されていません。 これを行うには、コマンド %1$s を実行するか、一般設定で自動変換を有効にします。", + "DatabaseUtf8mb4CharsetRecommended": "データベースはまだ utf8mb4 文字セットをサポートしていません。", + "EnableRequiredDirectoriesDiagnostic": "このチェックは構成で無効になっているため、このチェックはスキップされました。 このチェックを有効にするには、\"config/config.ini.php\" ファイルで [General] enable_required_directories_diagnostic = 1 を設定します。", + "HideUnchanged": "変更された値だけを表示する場合は、変更されていない値%2$sをすべて非表示にすることができます%1$s。", + "HtaccessWarningNginx": "機密ファイルに直接アクセスできないようにするには、特定のディレクトリへのアクセスを制限するように Web サーバーを構成することをお勧めします。 詳細については、%1$s 公式の nginx サーバー構成 %2$s を参照してください", + "MysqlMaxPacketSize": "最大パケットサイズ", + "MysqlMaxPacketSizeWarning": "少なくとも %3$s の MySQL データベースに '%1$smax_allowed_packet%2$s' サイズを設定することをお勧めします。 設定は現在 %4$s です。", + "MysqlTemporaryTablesWarning": "Matomo が適切に動作するには、MySQL パーミッション CREATE TEMPORARY TABLES が必要です。", + "MysqlTransactionLevel": "トランザクション分離レベルの変更はサポートされていません。 アーカイブは引き続き機能しますが、遅くなる可能性があるため、可能であれば、たとえば `binlog_format` を `row` に変更することをお勧めします。", + "NoDataForReportArchivingNotRun": "レポートのアーカイブは最近実行されていません。 %1$sレポートの生成方法について詳しく学んでください。%2$s", + "PHPFPMWarningApache": "PHP FPM は、.php ファイルの .htaccess ルールを無視します。 機密ファイルに直接アクセスできないようにするために、Apache 仮想ホスト構成の %3$s 行のすぐ上にある %2$s セクションに行 %1$s を追加して、PHP FPM によって処理される特定のディレクトリを除外することをお勧めします。", + "PHPFPMWarningGeneric": "PHP FPM は、.php ファイルの .htaccess ルールを無視する場合があります。 機密ファイルに直接アクセスできないようにするには、PHP FPM による処理から /config ディレクトリを除外するように Web サーバーを構成することをお勧めします。", + "PHPFPMWarningNginx": "PHP FPM は、.php ファイルの .htaccess ルールを無視します。 機密ファイルに直接アクセスできないようにするには、特定のディレクトリを PHP FPM で処理しないようにすることをお勧めします。 詳細については、%1$s 公式の nginx サーバー構成 %2$s を参照してください", + "PrivateDirectoryInternetDisabled": "この Matomo ではインターネット機能が無効になっているため、次の URL にアクセスできるかどうかを確認できませんでした。", + "PrivateDirectoryIsAccessible": "上記の URL はブラウザ経由でアクセスできることがわかりましたが、推奨できません。 それらへのアクセスを許可すると、潜在的なセキュリティ リスクが生じる可能性があります。これは、コンテンツがサーバーおよび潜在的にユーザーに関する情報を提供する可能性があるためです。 それらへのアクセスを制限してください。", + "PrivateDirectoryManualCheck": "URL をブラウザーで手動で開いて、アクセスできるかどうかを確認してください。 可能であれば、サーバー構成を変更する必要があるかもしれません。これらのファイル/ディレクトリには、インターネットまたはイントラネットからブラウザー経由でアクセスできないようにする必要があります。", + "RecommendedPrivateDirectories": "推奨されるプライベート ディレクトリ", + "RequiredPrivateDirectories": "必要なプライベート ディレクトリ", + "Sections": "セクション", + "UrlsAccessibleViaBrowser": "上記の URL はブラウザからアクセスできることがわかりましたが、アクセスしないことをお勧めします。 可能であれば、それらへのアクセスを制限してください。" + } +} diff --git a/plugins/Diagnostics/lang/ka.json b/plugins/Diagnostics/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/ka.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/ko.json b/plugins/Diagnostics/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/ko.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/ku.json b/plugins/Diagnostics/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/ku.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/lb.json b/plugins/Diagnostics/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/lb.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/lt.json b/plugins/Diagnostics/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..d7569e9672a189188ef86a75d8ef087a66dcc778 --- /dev/null +++ b/plugins/Diagnostics/lang/lt.json @@ -0,0 +1,6 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Konfigūracijos failas", + "Sections": "Sekcijos" + } +} diff --git a/plugins/Diagnostics/lang/lv.json b/plugins/Diagnostics/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/lv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/ms.json b/plugins/Diagnostics/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/ms.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/nb.json b/plugins/Diagnostics/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..d02015fd479f636a036d66d2ba38a0cd61da3b86 --- /dev/null +++ b/plugins/Diagnostics/lang/nb.json @@ -0,0 +1,38 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Ingen private mapper kan nås fra Internett.", + "BrowserAndAutoArchivingEnabledComment": "Det ser ut til at både nettleser- og auto-arkivering er påskrudd. Auto-arkivering startet sist for %3$s siden. Hvis %1$sAuto-arkivering%2$s er påskrudd, bør du skru av nettleser-arkivering i «Generelle innstillinger».", + "BrowserAndAutoArchivingEnabledLabel": "Nettleser- og auto-arkivering påskrudd", + "BrowserTriggeredArchivingEnabled": "For optimal og rask Matomo-ytelse anbefales det sterkt å sette opp en crontab for å arkivere rapportene dine automatisk, og for å skru av nettleseren-utløsing i Matomo-innstillingene. %1$sLær mer.%2$s", + "ConfigFileIntroduction": "Her kan du sjekke ditt Matomo-oppsett. Hvis du kjører Matomo i et last-balansert miljø, kan siden være forskjellig avhengig av hvilken tjener den lastes inn fra. Rader med forskjellig bakgrunnsfarge er endrede oppsett som angis for eksempel i %1$s-filen.", + "ConfigFileTitle": "Konfigurasjonsfil", + "ConfigIniAccessible": "Oppsettsmappen i Matomo er også offentlig tilgjengelig. Angripere kan ikke lese oppsettet nå, men hvis vev-tjeneren din stopper å kjøre PHP-filer av noen grunn, vil dine MySQL-identitetsdetaljer og annen info bli tilgjengelig for andre. Sjekk ditt vev-tjeneroppsett og nekt tilgang til mappen.", + "CronArchivingHasNotRun": "Arkivering har ikke blitt fullført enda.", + "CronArchivingHasNotRunInAWhile": "Arkivering ble sist fullført %1$s, som er %2$s siden.", + "CronArchivingLastRunCheck": "Siste fullførte arkivering", + "CronArchivingRanSuccessfullyXAgo": "Arkiveringsprosessen ble fullført for %1$s siden.", + "CronArchivingRunDetails": "Sjekk at du har satt opp en crontab som kaller %1$s-konsoll-kommandoen, og at du har satt opp en %2$s for å motta feilmeldinger per e-post hvis arkivering misslykkes. Du kan også prøve å kjøre konsoll-kommandoen for å arkivere rapportene dine manuelt: %3$s. %4$sLær mer%5$s.", + "DatabaseReaderConnection": "Databaseleser-tilkobling", + "DatabaseUtf8Requirement": "Dette kreves for å kunne lagre 4-byte lange UTF-8-tegn. Med mindre utf8mb4 er tilgjengelig, vil spesialtegn som f.eks. emoji-er, mindre vanlige tegn i asiatiske språk, forskjellige historiske og matematiske symboler bli erstattet med %1$s. Du kan lese mer om dette i %2$sO-S-S%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Databasen din støtter utf8mb4-tegnsettet, men databasetabellene har ikke blitt konvertert enda. Du kan gjøre dette ved å kjøre kommandoen «%1$s» eller ved å skru på auto-konvertering i generelle innstillinger.", + "DatabaseUtf8mb4CharsetRecommended": "Databasen din støtter ikke utf8mb4-tegnsettet enda.", + "EnableRequiredDirectoriesDiagnostic": "Denne sjekken ble hoppet over siden den er skrudd av i oppsettet. Skru den på ved å sette [General] enable_required_directories_diagnostic = 1 i «config/config.ini.php»-filen.", + "HideUnchanged": "Hvis du kun vil se de endrede verdiene, kan du %1$sskjule alle uendrede verdier%2$s.", + "HtaccessWarningNginx": "For å forsikre at sensitive filer ikke kan nås direkte anbefales det å sette opp vev-tjeneren din slik at den begrenser tilgang til gitte mapper. Som eksempel kan du sjekke %1$sdet offisielle NGINX-tjeneroppsettet%2$s", + "MysqlMaxPacketSize": "Maksimal pakkestørrelse", + "MysqlMaxPacketSizeWarning": "Det anbefales å sette opp en '%1$smax_allowed_packet%2$s'-størrelse i din MySQL-database som minst %3$s. %4$s er nå valgt.", + "MysqlTemporaryTablesWarning": "MySQL-tilgangen «CREATE TEMPORARY TABLES» er påkrevd for at Matomo skal fungere riktig.", + "MysqlTransactionLevel": "Endring av isolasjonsnivå for transaksjon støttes ikke. Arkivering vil fremdeles virke, men det kan være tregere og det anbefales å endre for eksempel `binlog_format` til `row` hvis mulig.", + "NoDataForReportArchivingNotRun": "Arkivering av rapportene dine har ikke blitt gjort i det siste. %1$sLær mer om hvordan du genererer rapportene dine.%2$s", + "PHPFPMWarningApache": "PHP FPM vil ignorere .htaccess-regler for .php-filer. For å forsikre at sensitive filer ikke kan nås direkte anbefales det å utelukke noen mapper fra å bli håndtert av PHP FPM ved å legge til linjen %1$s til i %2$s-delen i ditt virtuelle vertstjeningsoppsett for Apache, akkurat over %3$s-linjen.", + "PHPFPMWarningGeneric": "PHP FPM kan ignorere .htaccess-regler for .php-filer. For å forsikre at sensitive filer ikke kan nås direkte anbefales det å sette opp din vev-tjener for å utelate /config-mappen fra å bli håndtert direkte fra PHP FPM.", + "PHPFPMWarningNginx": "PHP FPM vil ignorere .htaccess-regler for .php-filer. For å forsikre at sensitive filer ikke kan nås direkte anbefales det å utelukke gitte mapper fra å bli håndtert av PHP FPM. Mer informasjon er å finne i %1$soffisielt tjeneroppsett for NGNIX-tjener%2$s.", + "PrivateDirectoryInternetDisabled": "Kunne ikke sjekke om følgende nettadresser kan nås, fordi Internett-funksjoner er avskrudd på denne Matomo-en.", + "PrivateDirectoryIsAccessible": "Nettadressene ovenfor kan nås via nettleseren, men de bør IKKE være tilfelle. Å tillate dette kan være en sikkerhetsrisiko siden innholdet kan tilkjennegi info om din tjener og kanskje også brukerne dine. Begrens tilgangen til dem.", + "PrivateDirectoryManualCheck": "Åpne nettadressene manuelt i en nettleser for å finne ut hvem som har tilgang. Det kan det hende du må endre tjeneroppsettet hvis du har tilgang til det, siden disse filene/mappene ikke bør være tilgjengelige via en nettleser fra Internett eller intranett.", + "RecommendedPrivateDirectories": "Anbefalte private mapper", + "RequiredPrivateDirectories": "Påkrevde private mapper", + "Sections": "Seksjoner", + "UrlsAccessibleViaBrowser": "Nettadressene ovenfor kan nås fra nettleseren, men det anbefales at det forbys. Hvis mulig bør du begrense tilgangen til dem." + } +} diff --git a/plugins/Diagnostics/lang/nl.json b/plugins/Diagnostics/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..29a8d7c797dc368bba517be4e7c0571b632777bf --- /dev/null +++ b/plugins/Diagnostics/lang/nl.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Alle private mappen zijn niet bereikbaar vanaf het internet.", + "BrowserAndAutoArchivingEnabledComment": "Het lijkt erop dat zowel browser als automatisch archiveren zijn ingeschakeld. Automatische archivering is het laatst gestart %3$s geleden. Als %1$sautomatisch archiveren%2$s is ingeschakeld, moet u browser-archivering uitschakelen in de \"Algemene instellingen\".", + "BrowserAndAutoArchivingEnabledLabel": "Browser en automatische archivering ingeschakeld", + "BrowserTriggeredArchivingEnabled": "Voor optimale prestaties en een snelle Matomo, is het sterk aanbevolen om een crontab in te stellen om uw rapporten automatisch te archiveren, en om browser triggering uit te schakelen in de Matomo-instellingen. %1$sMeer informatie.%2$s", + "ConfigFileIntroduction": "Hier kunt u de Matomo configuratie bekijken. Als u Matomo in een load balanced omgeving draait, kan de pagina er anders uitzien afhankelijk vanaf welke server deze pagina wordt geladen. Rijen met een andere achtergrondkleur zijn gewijzigde configuratiewaarden die bijvoorbeeld in het bestand %1$s zijn gespecifieerd.", + "ConfigFileTitle": "Configuratiebestand", + "ConfigIniAccessible": "We hebben ook ontdekt dat Matomo's config-map publiekelijk bereikbaar is. Hoewel aanvallers de configuratie nu niet kunnen lezen, indien uw webserver om een of andere reden stopt met het uitvoeren van PHP-bestanden, zullen uw MySQL-gegevens en andere informatie voor iedereen beschikbaar komen. Controleer de configuratie van uw webserver en haal de toegang tot deze map weg.", + "CronArchivingHasNotRun": "Archivering is nog niet succesvol afgerond.", + "CronArchivingHasNotRunInAWhile": "Archivering is voor het laatst succesvol uitgevoerd op %1$s, dat is %2$s geleden.", + "CronArchivingLastRunCheck": "Recentst afgeronde succesvolle archivering", + "CronArchivingRanSuccessfullyXAgo": "Het archiveringsproces is succesvol afgerond %1$s geleden.", + "CronArchivingRunDetails": "Controleer of u een crontab heeft ingesteld om het console commando %1$s aan te roepen, en of u een %2$s heeft geconfigureerd om fouten per e-mail te ontvangen als het archiveren mislukt. U kunt ook proberen om manueel het console commando uit te voeren om uw rapporten te archiveren: %3$s. %4$sMeer informatie.%5$s", + "DatabaseCollationCharset": "De standaard collatie voor uw geconfigureerde charset is: %1$s", + "DatabaseCollationConnection": "Uw momenteel gebruikte verbindingscollatie is: %1$s", + "DatabaseCollationNotConfigured": "Uw databaseverbinding is geconfigureerd zonder expliciete sortering. Update [database] collation = '' in het bestand \"config/config.ini.php\" met de te gebruiken sortering, om zeker te zijn dat alle databasefuncties werken zoals verwacht.", + "DatabaseReaderConnection": "Database Reader-verbinding", + "DatabaseUtf8Requirement": "Dit is nodig om 4-byte UTF8-karakters te kunnen opslaan. Tenzij utf8mb4 beschikbaar is, worden speciale karakters, zoals emoji's, minder gebruikelijke tekens van Aziatische talen, diverse historische scripts of wiskundige symbolen vervangen door %1$s. Meer details over dit onderwerp kunt u lezen in %2$sdeze FAQ%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Uw database ondersteunt de utf8mb4 karakterset, maar uw databasetabellen zijn nog niet geconverteerd. U kunt dit doen door het commando %1$s uit te voeren of de automatische conversie te activeren in Algemene instellingen.", + "DatabaseUtf8mb4CharsetRecommended": "Uw database ondersteunt nog geen utf8mb4-karakterset.", + "EnableRequiredDirectoriesDiagnostic": "Deze controle werd overgeslagen omdat deze is uitgeschakeld in de config. Om deze controle in te schakelen stelt u '[General] enable_required_directories_diagnostic = 1' in het \"config/config.ini.php\" bestand in.", + "HideUnchanged": "Wanneer je alleen de gewijzigde waarden wil zien %1$sverberg alle ongewijzigde waarden%2$s.", + "HtaccessWarningNginx": "Om ervoor te zorgen dat gevoelige bestanden niet rechtstreeks bereikbaar zijn, is het raadzaam uw webserver te configureren om de toegang tot bepaalde mappen te beperken. Zie voor meer informatie de %1$sofficiële nginx server configuratie.%2$s", + "MariaDbNotConfigured": "Uw databaseversie geeft aan dat u mogelijk een MariaDb-server gebruikt. Als dit het geval is, zorg er dan voor dat u [database] schema = Mariadb instelt in het bestand \"config/config.ini.php\", om zeker te zijn dat alle databasefuncties werken zoals verwacht.", + "MysqlMaxPacketSize": "Maximale pakketgrootte", + "MysqlMaxPacketSizeWarning": "Het is aan te raden om de '%1$smax_allowed_packet%2$s' grootte in uw MySQL database te configureren op minimum %3$s. Momenteel is dit %4$s.", + "MysqlTemporaryTablesWarning": "Het MySQL-recht CREATE TEMPORARY TABLES is vereist voor een goede werking van Matomo.", + "MysqlTransactionLevel": "Aanpassen van transactie-isolatieniveau niet ondersteund. Archiveren zal nog steeds werken, maar die kan langzamer zijn en het is aanbevolen om bijvoorbeeld de `binlog_format` te wijzigen naar `row` indien mogelijk.", + "NoDataForReportArchivingNotRun": "De archivering van uw rapporten is niet recent uitgevoerd, %1$slees meer over het genereren van uw rapporten.%2$s", + "PHPFPMWarningApache": "PHP FPM zal .htaccess regels voor .php bestanden negeren. Om ervoor te zorgen dat gevoelige bestanden niet direct bereikbaar zijn, is het aan te raden bepaalde mappen uit te sluiten van behandeling door PHP FPM door de regel %1$s toe te voegen aan de sectie %2$s in de configuratie van je apache virtual host, net boven de regel %3$s.", + "PHPFPMWarningGeneric": "PHP FPM kan .htaccess-regels voor .php-bestanden negeren. Om ervoor te zorgen dat gevoelige bestanden niet rechtstreeks bereikbaar zijn, wordt aanbevolen uw webserver zo te configureren dat de map /config niet door PHP FPM kan worden verwerkt.", + "PHPFPMWarningNginx": "PHP FPM negeert .htaccess regels voor .php bestanden. Om ervoor te zorgen dat gevoelige bestanden niet rechtstreeks kunnen worden benaderd, is het aanbevolen om bepaalde mappen uit te sluiten van behandeling door PHP FPM. Voor meer informatie zie de %1$s officiële nginx server configuratie%2$s", + "PrivateDirectoryCantCheckUrl": "Het controleren van de URL %1$s is mislukt: %2$s", + "PrivateDirectoryInternetDisabled": "We konden niet controleren of de volgende URL's bereikbaar zijn omdat internetfuncties uitgeschakeld zijn op deze Matomo.", + "PrivateDirectoryIsAccessible": "We hebben vastgesteld dat de bovenstaande URL's bereikbaar zijn via de browser, maar dat zou NIET mogen. Deze bereikbaar maken kan een potentieel veiligheidsrisico vormen, omdat de inhoud informatie kan blootstellen over uw server en mogelijk over uw gebruikers. Gelieve de toegang ertoe te beperken.", + "PrivateDirectoryManualCheck": "Open de URL's handmatig in een browser om na te gaan of u er toegang toe heeft. Zo ja, dan moet u wellicht uw serverconfiguratie aanpassen, aangezien deze bestanden/mappen niet toegankelijk mogen zijn via een browser vanaf het internet of intranet.", + "RecommendedPrivateDirectories": "Aanbevolen private mappen", + "RequiredPrivateDirectories": "Vereiste private mappen", + "Sections": "Secties", + "UrlsAccessibleViaBrowser": "We hebben vastgesteld dat de bovenstaande URL's bereikbaar zijn via de browser, maar wij raden aan dat niet te doen. Indien mogelijk, gelieve de toegang ertoe te beperken." + } +} diff --git a/plugins/Diagnostics/lang/nn.json b/plugins/Diagnostics/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/nn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/pl.json b/plugins/Diagnostics/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..6c6b1fa4ddcf484a748d04af31b257317e80d926 --- /dev/null +++ b/plugins/Diagnostics/lang/pl.json @@ -0,0 +1,8 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Plik konfiguracyjny", + "DatabaseUtf8mb4CharsetRecommended": "Twoja baza danych nie wspiera jeszcze zestawu UTF8mb4.", + "HideUnchanged": "W celu zobaczenia tylko zmienionych wartości %1$sukryj niezmienione wartości%2$s.", + "Sections": "Sekcje" + } +} diff --git a/plugins/Diagnostics/lang/pt-br.json b/plugins/Diagnostics/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..44a63e29333128d5254e27c40c5faed4e31c006c --- /dev/null +++ b/plugins/Diagnostics/lang/pt-br.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Todos os diretórios privados são inacessíveis a partir da Internet.", + "BrowserAndAutoArchivingEnabledComment": "Parece que o navegador e o arquivamento automático estão ativados. O arquivamento automático começou pela última vez há %3$s. Se o %1$sauto arquivamento%2$s estiver habilitado, você deve desabilitar o arquivamento do navegador em 'Configurações gerais'.", + "BrowserAndAutoArchivingEnabledLabel": "Navegador e arquivamento automático ativados", + "BrowserTriggeredArchivingEnabled": "Para uma performance ótima e um Matomo veloz, é altamente recomendável definir um crontab para automaticamente arquivar seus relatórios, e desabilitar o acionamento de navegador nas configurações do Matomo. %1$sSaiba mais.%2$s", + "ConfigFileIntroduction": "Aqui você pode ver a configuração do Matomo. Se você estiver rodando o Matomo em um ambiente com balanceamento de carga, a página pode estar diferente dependendo de qual servidor esta página é carregada. Linhas com diferentes cores de fundo são valores de configuração alterados que estão especificados por exemplo no arquivo %1$s.", + "ConfigFileTitle": "Arquivo de configuração", + "ConfigIniAccessible": "Também descobrimos que o diretório de configuração do Matomo é acessível publicamente. Embora os invasores não possam ler a configuração agora, se seu servidor da Web parar de executar arquivos PHP por algum motivo, suas credenciais do MySQL e outras informações estarão disponíveis para qualquer pessoa. Verifique a configuração do seu servidor web e negue o acesso a este diretório.", + "CronArchivingHasNotRun": "O arquivamento ainda não foi realizado com sucesso.", + "CronArchivingHasNotRunInAWhile": "O último arquivamento realizado com sucesso foi em %1$s que é %2$s atrás.", + "CronArchivingLastRunCheck": "Última conclusão de arquivamento com sucesso", + "CronArchivingRanSuccessfullyXAgo": "O processo de arquivamento foi completado com sucesso %1$s atrás.", + "CronArchivingRunDetails": "Favor checar que há um agendamento crontab chamando o %1$sconsole de comandos, e que há um %2$s configurado para receber erros por email se o arquivamento falhar. Você também pode tentar rodar o console de comandos para arquivar seus relatórios manualmente: %3$s. %4$sSaiba mais.%5$s", + "DatabaseCollationCharset": "A ordenação padrão para o conjunto de caracteres configurado é: %1$s", + "DatabaseCollationConnection": "A ordenação de conexão atualmente em uso é: %1$s", + "DatabaseCollationNotConfigured": "Sua conexão com o banco de dados está configurada sem um *collation* explícito. Por favor, atualize [database] collation = '' no arquivo \"config/config.ini.php\" com o *collation* a ser utilizado, para garantir que todos os recursos do banco de dados funcionem conforme o esperado.", + "DatabaseReaderConnection": "Conexão do leitor de banco de dados", + "DatabaseUtf8Requirement": "Isso é necessário para poder armazenar caracteres UTF8 de 4 bytes. A menos que utf8mb4 esteja disponível, caracteres especiais, como emojis, caracteres menos comuns de idiomas asiáticos, vários scripts históricos ou símbolos matemáticos serão substituídos por %1$s. Você pode ler mais detalhes sobre este tópico em %2$sneste FAQ%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Seu banco de dados suporta o conjunto de caracteres utf8mb4, mas suas tabelas de banco de dados ainda não foram convertidas. Você pode fazer isso executando o comando %1$s ou ativando a conversão automática em Configurações Gerais.", + "DatabaseUtf8mb4CharsetRecommended": "Seu banco de dados ainda não suporta o conjunto de caracteres utf8mb4.", + "EnableRequiredDirectoriesDiagnostic": "Esta verificação foi ignorada porque esta verificação está desabilitada no arquivo config. Para habilitar esta verificação, configure [General] enable_required_directories_diagnostic = 1 no arquivo 'config/config.ini.php'.", + "HideUnchanged": "Se você quiser ver apenas valores alterados você pode %1$socultar todos os valores inalterados%2$s.", + "HtaccessWarningNginx": "Para garantir que os arquivos confidenciais não possam ser acessados diretamente, é recomendável configurar seu servidor web para restringir o acesso a determinados diretórios. Para obter mais informações, consulte a %1$sconfiguração oficial do servidor nginx %2$s", + "MariaDbNotConfigured": "A versão do seu banco de dados indica que você pode estar usando um servidor MariaDb. Se for esse o caso, certifique-se de definir [database] schema = Mariadb no arquivo \"config/config.ini.php\", para garantir que todos os recursos do banco de dados funcionem conforme o esperado.", + "MysqlMaxPacketSize": "Tamanho máximo de pacote", + "MysqlMaxPacketSizeWarning": "É recomendado configurar um tamanho máximo de pacote (%1$smax_allowed_packet%2$s) no seu banco de dados MySQL de pelo menos %3$s. Atualmente está configurado %4$s.", + "MysqlTemporaryTablesWarning": "A permissão CREATE TEMPORARY TABLES do MySQL é requerida para que o Matomo funcione corretamente.", + "MysqlTransactionLevel": "A alteração do nível de isolamento de transação não é suportada. O arquivamento ainda funcionará mas ele pode ser mais lento e ele é recomendado para alterar, por exemplo, o `binlog_format` para `row` se possível.", + "NoDataForReportArchivingNotRun": "O arquivamento de seus relatórios não foi executado recentemente. %1$sSaiba mais sobre como gerar seus relatórios.%2$s", + "PHPFPMWarningApache": "O PHP FPM irá ignorar as regras .htaccess para arquivos .php. Para garantir que os arquivos confidenciais não possam ser acessados diretamente, é recomendável excluir certos diretórios de serem manipulados pelo PHP FPM adicionando a linha %1$s à seção %2$s na configuração do host virtual do apache logo acima da linha %3$s.", + "PHPFPMWarningGeneric": "O PHP FPM pode ignorar as regras .htaccess para arquivos .php. Para garantir que os arquivos confidenciais não possam ser acessados diretamente, é recomendável configurar seu servidor web para excluir o diretório /config de ser manipulado pelo PHP FPM.", + "PHPFPMWarningNginx": "O PHP FPM irá ignorar as regras .htaccess para arquivos .php. Para garantir que os arquivos confidenciais não possam ser acessados diretamente, é recomendado excluir certos diretórios de serem manipulados pelo PHP FPM. Para obter mais informações, consulte a %1$s configuração oficial do servidor nginx%2$s", + "PrivateDirectoryCantCheckUrl": "A verificação da URL %1$s falhou: %2$s", + "PrivateDirectoryInternetDisabled": "Não foi possível verificar se as seguintes URLs estão acessíveis porque os recursos da Internet estão desabilitados neste Matomo.", + "PrivateDirectoryIsAccessible": "Descobrimos que as URLs acima são acessíveis pelo navegador, mas NÃO deveriam ser. Permitir que elas sejam acessadas pode representar um risco potencial de segurança, pois o conteúdo pode fornecer informações sobre seu servidor e, potencialmente, seus usuários. Por favor, restrinja o acesso a elas.", + "PrivateDirectoryManualCheck": "Abra os URLs manualmente em um navegador para ver se você pode acessá-los. Se puder, talvez seja necessário modificar a configuração do servidor, pois esses arquivos/diretórios não devem ser acessíveis por meio de um navegador da Internet ou Intranet.", + "RecommendedPrivateDirectories": "Diretórios Privados Recomendados", + "RequiredPrivateDirectories": "Diretórios Privados Necessários", + "Sections": "Seções", + "UrlsAccessibleViaBrowser": "Descobrimos que as URLs acima podem ser acessadas pelo navegador, mas recomendamos que não sejam. Se possível, restrinja o acesso a elas." + } +} diff --git a/plugins/Diagnostics/lang/pt.json b/plugins/Diagnostics/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..8661069f4e461aa794dcb82a1f14d0e36c5574f7 --- /dev/null +++ b/plugins/Diagnostics/lang/pt.json @@ -0,0 +1,38 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Todos os diretórios privados são inacessíveis pela Internet.", + "BrowserAndAutoArchivingEnabledComment": "Parece que o navegador e o arquivamento automático estão ativados. O arquivamento automático foi iniciado há %3$s. Se %1$sauto arquivamento%2$s estiver ativado, você deve desativar o arquivamento do navegador em \"Configurações gerais\".", + "BrowserAndAutoArchivingEnabledLabel": "Navegador e arquivamento automático ativados", + "BrowserTriggeredArchivingEnabled": "Para um Matomo rápido e com uma boa performance, é altamente recomendado que configure um crontab para arquivar automaticamente os seus relatórios e desativar a execução do arquivamento através do navegador nas definições do Matomo. %1$sSaber mais.%2$s", + "ConfigFileIntroduction": "Aqui, pode consultar a configuração do Matomo. Se estiver a executar o Matomo num ambiente distribuído, a página pode ser diferente mediante o servidor a partir do qual foi carregada. As linhas com uma cor de fundo diferente são valores alterados de configuração especificados, por exemplo, no ficheiro %1$s.", + "ConfigFileTitle": "Ficheiro de configuração", + "ConfigIniAccessible": "Também descobrimos que o diretório de configuração do Matomo é acessível publicamente. Embora os invasores não possam ler a configuração agora, se o servidor da Web parar de executar ficheiros de PHP por algum motivo, as suas credenciais do MySQL e outras informações estarão disponíveis para qualquer pessoa. Por favor, verifique a configuração do seu servidor web e negue o acesso a este diretório.", + "CronArchivingHasNotRun": "O arquivamento ainda não foi executado com sucesso.", + "CronArchivingHasNotRunInAWhile": "O arquivamento foi executado com sucesso pela última a %1$s há %2$s.", + "CronArchivingLastRunCheck": "Última conclusão do processo de arquivamento", + "CronArchivingRanSuccessfullyXAgo": "O processo de arquivamento concluído com sucesso há %1$s.", + "CronArchivingRunDetails": "Por favor confirme que configurou um crontab a invocar o comando da consola %1$s e que configurou um %2$s para receber erros por e-mail se o arquivamento falhar. Pode também tentar executar o comando na consola para arquivar os seus relatórios manualmente: %3$s. %4$sSaber mais.%5$s", + "DatabaseReaderConnection": "Ligação do leitor da base de dados", + "DatabaseUtf8Requirement": "Isto é necessário para armazenar carateres UTF8 de 4-bytes. A menos que o utf8mb4 esteja disponível, carateres especiais como emojis, carateres menos comuns de idiomas asiáticos, vários scripts históricos ou símbolos matemáticas, serão substituídos por %1$s. Pode ler mais informações sobre este tópico %2$snesta FAQ%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "A sua base de dados suporta o conjunto de caracteres utf8mb4, mas as suas tabelas da base de dados ainda não foram convertidas. Pode fazer isto executando o comando %1$s ou ativamente a conversão automática nas Definições gerais.", + "DatabaseUtf8mb4CharsetRecommended": "A sua base de dados não suporta ainda o conjuntos de carateres UTF8mb4.", + "EnableRequiredDirectoriesDiagnostic": "Esta verificação foi ignorada porque está desativada na configuração. Para ativar esta verificação, defina[General] enable_required_directories_diagnostic = 1 no ficheiro \"config/config.ini.php\".", + "HideUnchanged": "Se quiser ver apenas os valores alterados, pode %1$socultar todos os valores inalterados%2$s.", + "HtaccessWarningNginx": "Para garantir que ficheiros confidenciais não possam ser acedidos diretamente, é recomendável configurar o seu servidor da Web para restringir o acesso a determinados diretórios. Para obter mais informações, consulte a %1$s configuração oficial do servidor nginx %2$s", + "MysqlMaxPacketSize": "Tamanho máximo do pacote", + "MysqlMaxPacketSizeWarning": "É recomendável a configuração de um tamanho de pacote '%1$smax_allowed_packet%2$s' na sua base de dados MySQL de, pelo menos, %3$s. Atualmente está configurado como %4$s.", + "MysqlTemporaryTablesWarning": "A permissão MySQL CREATE TEMPORARY TABLES é necessária para que o Matomo funcione corretamente.", + "MysqlTransactionLevel": "A alteração do nível de isolamento da transação não é suportado. O arquivamento irá funcionar mas será mais lento e é recomendável que altere, por exemplo, o `binlog_format` para `row`, se for possível.", + "NoDataForReportArchivingNotRun": "O arquivamento dos seus relatórios não foi executado recentemente. %1$sSaber mais sobre como gerar os seus relatórios.%2$s", + "PHPFPMWarningApache": "O PHP FPM irá ignorar as regras .htaccess para ficheiros .php. Para garantir que ficheiros confidenciais não possam ser acedidos diretamente, é recomendável excluir certos diretórios a serem manipulados pelo PHP FPM adicionando a linha %1$s à seção %2$s na sua configuração de host virtual do apache logo acima de %3$s linha.", + "PHPFPMWarningGeneric": "PHP FPM pode ignorar regras .htaccess para ficheiros .php. Para garantir que ficheiros confidenciais não possam ser acessados diretamente, é recomendável configurar o seu servidor web para excluir o diretório /config de ser manipulado pelo PHP FPM.", + "PHPFPMWarningNginx": "O PHP FPM irá ignorar as regras .htaccess para ficheiros .php. Para garantir que ficheiros confidenciais não possam ser acedidos diretamente, é recomendável excluir certos diretórios a serem manipulados pelo PHP FPM. Para obter mais informações, consulte a configuração oficial do servidor nginx %1$s %2$s", + "PrivateDirectoryInternetDisabled": "Não foi possível verificar se os seguintes URLs estão acessíveis porque os recursos da Internet estão desativados neste Matomo.", + "PrivateDirectoryIsAccessible": "Descobrimos que as URL acima são acessíveis através do navegador, mas NÃO deveriam ser. Permitir o acesso às mesmas pode representar um potencial risco de segurança, uma vez que os conteúdos podem fornecer informações sobre o seu servidor e potencialmente sobre os seus utilizadores. Por favor, restrinja o acesso às mesmas.", + "PrivateDirectoryManualCheck": "Abra os URLs manualmente num navegador para ver se consegue acedê-los. Se puder, talvez seja necessário modificar a configuração do servidor, pois esses ficheiros/diretórios não devem ser acedidos por meio de um navegador da Internet ou Intranet.", + "RecommendedPrivateDirectories": "Diretórios privados recomendados", + "RequiredPrivateDirectories": "Diretórios Privados Necessários", + "Sections": "Secções", + "UrlsAccessibleViaBrowser": "Descobrimos que os URLs acima podem ser acessados por meio do navegador, mas recomendamos que não. Se possível, restrinja o acesso a eles." + } +} diff --git a/plugins/Diagnostics/lang/ro.json b/plugins/Diagnostics/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..5c903e5bc875b822a201278399d2288d398fa003 --- /dev/null +++ b/plugins/Diagnostics/lang/ro.json @@ -0,0 +1,7 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Fişier configurări", + "HideUnchanged": "Dacă vrei să vezi doar valorile modificate poţi ascunde %1$s toate valorile neschimbate %2$s.", + "Sections": "Secţiuni" + } +} diff --git a/plugins/Diagnostics/lang/ru.json b/plugins/Diagnostics/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..3902832c84bde53d09890c27a657951df30561de --- /dev/null +++ b/plugins/Diagnostics/lang/ru.json @@ -0,0 +1,14 @@ +{ + "Diagnostics": { + "ConfigFileIntroduction": "Здесь вы можете просмотреть конфигурацию Matomo. Если вы используете Matomo в среде с балансировкой нагрузки, страница может отличаться в зависимости от того, с какого сервера эта страница загружена. Строки с другим цветом фона изменяют значения конфигурации, указанные, например, в %1$s файле.", + "ConfigFileTitle": "Файл конфигурации", + "CronArchivingHasNotRun": "Архивация еще не прошла успешно.", + "CronArchivingLastRunCheck": "Последнее успешное завершение архивирования", + "HideUnchanged": "Если вы хотите видеть только измененные значения, вы можете %1$sспрятать все неизмененные значения%2$s.", + "MysqlMaxPacketSize": "Максимальный размер пакета", + "MysqlMaxPacketSizeWarning": "Для вашей базы данных рекомендовано установить размер '%1$smax_allowed_packet%2$s' минимум %3$s. Сейчас он - %4$s.", + "MysqlTemporaryTablesWarning": "Разрешение MySQL CREATE TEMPORARY TABLES требуется для правильной работы Matomo.", + "MysqlTransactionLevel": "Изменение уровня изоляции транзакции не поддерживается. Архивация все равно будет работать, но она может быть медленнее, поэтому рекомендуется, например, изменить `binlog_format` на` row` если возможно.", + "Sections": "Разделы" + } +} diff --git a/plugins/Diagnostics/lang/si.json b/plugins/Diagnostics/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/sk.json b/plugins/Diagnostics/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/sk.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/sl.json b/plugins/Diagnostics/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/sl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/sq.json b/plugins/Diagnostics/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..7c529725233e48bbf652fab707cbc677c195acb3 --- /dev/null +++ b/plugins/Diagnostics/lang/sq.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Krejt drejtoritë private s’mund të hapen që nga interneti.", + "BrowserAndAutoArchivingEnabledComment": "Duket se shfletuesi dhe vetëarkivimi janë aktivizuar. Vetëarkivimi së fundi filloi %3$s më parë. Nëse %1$svetëarkivimi%2$s është i aktivizuar, duhet të çaktivizoni arkivimin nga shfletuesi, te “Rregullime të Përgjithshme”.", + "BrowserAndAutoArchivingEnabledLabel": "Shfletim dhe Vetëarkivim të aktivizuar", + "BrowserTriggeredArchivingEnabled": "Për funksionimin më të mirë dhe një Matomo të shpejtë, këshillohet me forcë të ujdisni një akt crontab për arkivim automatik të raporteve tuaja dhe të çaktivizoni që nga rregullimet e Matomo-s ngacmimin e shfletuesit. %1$sMësoni më tepër.%2$s", + "ConfigFileIntroduction": "Këtu mund të shihni formësimin e Matomo-s. Nëse Matomo-n e xhironi në një mjedis me ngarkesë të balancuar, faqja mund të jetë ndryshe, varet se nga cili shërbyes ngarkohet faqja. Rreshtat me një ngjyrë tjetër për sfondin përfaqësojnë vlera formësimi që janë dhënë si shembull te kartela %1$s.", + "ConfigFileTitle": "Kartelë formësimesh", + "ConfigIniAccessible": "Pamë gjithashtu se drejtoria e formësimeve të Matomo-s është e përdorshme publikisht. Teksa agresorë s’mund të lexojnë formësimin tani, nëse shërbyesi juaj resht të ekzekutuara kartela PHP për ndonjë arsye, kredencialet tuaja MySQL dhe informacion tjetër do të jetë i passhëm për këdo. Ju lutemi, kontrolloni formësimin e shërbyesit tuaj dhe mos lejoni hyrje në këtë drejtori.", + "CronArchivingHasNotRun": "Arkivimi ende s’është xhiruar me sukses.", + "CronArchivingHasNotRunInAWhile": "Hera e fundit që arkivimi u bë me sukses qe më %1$s, që do të thotë %2$s më parë.", + "CronArchivingLastRunCheck": "Plotësimi i Fundit i Suksesshëm i Arkivimit", + "CronArchivingRanSuccessfullyXAgo": "Procesi i arkivimit u plotësua me sukses %1$s më parë.", + "CronArchivingRunDetails": "Ju lutemi, shihni të keni ujdisur një akt crontab për vënie në punë të urdhrit %1$s të konsolës, dhe se keni formësuar një %2$s për të marrë me email njoftime gabimesh, nëse arkivimi dështon. Mund të provoni edhe të xhironi urdhër konsole për arkivim dorazi të raporteve tuaja: %3$s. %4$sMësoni më tepër.%5$s", + "DatabaseCollationCharset": "“Collation” parazgjedhje për llojin e shkronjave formësuar nga ju është: %1$s", + "DatabaseCollationConnection": "“Collation” juaj i përdorur aktualisht për lidhjen është: %1$s", + "DatabaseCollationNotConfigured": "Lidhja e bazës suaj të të dhënave është formësuar pa një “collation” specifik. Ju lutemi, përditësoni [database] collation = '' te kartela “config/config.ini.php” me “collation” që duhet përdorur, që të garantohet se krejt veçoritë e bazës së të dhënave funksionojnë siç pritet.", + "DatabaseReaderConnection": "Lidhje Leximi Baze të Dhënash", + "DatabaseUtf8Requirement": "Kjo është e domosdoshme për të qenë në gjendje të depozitohen shenja UTF8 4-bajtshe. Po qe se utf8mb4 s’është e përdorshme, shenja speciale, bie fjala, emotikone, shenja më pak të rëndomta gjuhësh aziatike, skripte të ndryshëm historikë apo simbole matematike do të zëvendësohen me %1$s. Te %2$skëto PBR%3$s mund të lexoni më tepër hollësi rreth këtij subjekti.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Baza juaj e të dhënave e mbulon kodimin utf8mb4, por tabelat e bazës tuaj të të dhënave ende s’janë shndërruar. Këtë mund ta bëni duke ekzekutuar urdhrin %1$s, ose aktivizuar shndërrimin e automatizuar që nga Rregullimet e Përgjithshme.", + "DatabaseUtf8mb4CharsetRecommended": "Baza juaj e të dhënave s’e mbulon ende kodimin utf8mb4.", + "EnableRequiredDirectoriesDiagnostic": "Ky kontroll u anashkalua, ngaqë ky kontroll është çaktivizuar te formësimi. Që ta aktivizoni këtë kontroll, vini [General] enable_required_directories_diagnostic = 1 te kartela “config/config.ini.php”.", + "HideUnchanged": "Nëse dëshironi të shihni vetëm vlerat e ndryshuara, mund të %1$sfshihni krejt vlerat e pandryshuara%2$s.", + "HtaccessWarningNginx": "Që të garantohet se kartela rezervat s’mund të përdoren drejtpërsëdrejti, rekomandohet ta formësoni shërbyesin tuaj të kufizojë hyrjen në disa drejtori. Për më tepër hollësi, ju lutemi, shihni %1$s formësimin zyrtar të shërbyesit nginx %2$s", + "MariaDbNotConfigured": "Versioni juaj i bazës së të dhënave tregon se mund të jeni duke përdorur një shërbyes MariaDb. Nëse është kështu, ju lutemi, siguroni caktimin si [database] schema = Mariadb, te kartela “config/config.ini.php”, për të garantuar se krejt veçoritë e bazës së të dhënave funksionojnë siç pritet.", + "MysqlMaxPacketSize": "Maksimum Madhësie Paketi", + "MysqlMaxPacketSizeWarning": "Këshillohet të formësoni te baza juaj e të dhënave MySQL një madhësi '%1$smax_allowed_packet%2$s' të paktën %3$s. Formësimi i tanishëm për të është %4$s.", + "MysqlTemporaryTablesWarning": "Leja MySQL CREATE TEMPORARY TABLES është e domosdoshme që Matomo të funksionojë si duhet.", + "MysqlTransactionLevel": "Nuk mbulohet ndryshimi i shkallës së izolimit. Arkivimi do të funksionojë, por mund të jetë më i ngadaltë dhe rekomandohet të ndryshohet, për shembull `binlog_format` në `row`, në qoftë e mundur.", + "NoDataForReportArchivingNotRun": "Arkivimi i raporteve tuaja nuk ka ngjarë tani së fundi, %1$smësoni më tepër se si të prodhoni raportet tuaja.%2$s", + "PHPFPMWarningApache": "PHP FPM do të shpërfillë rregulla .htaccess për kartela .php. Që të garantohet se kartela rezervat s’mund të përdoren drejtpërsëdrejti, rekomandohet të përjashtoni disa drejtori nga të qenit të trajtueshme nga PHP FPM, duke shtuar rreshtin %1$s te ndarja %2$s në formësimin e strehës suaj virtuale Apache, mu sipër rreshtit %3$s.", + "PHPFPMWarningGeneric": "PHP FPM mund të shpërfillë rregulla .htaccess për kartela .php. Që të garantohet se kartela rezervat s’mund të përdoren drejtpërsëdrejti, rekomandohet ta formësoni shërbyesin tuaj të përjashtojë drejtorinë /config nga të qenët e trajtueshme prej PHP FPM.", + "PHPFPMWarningNginx": "PHP FPM mund të shpërfillë rregulla .htaccess për kartela .php. Që të garantohet se kartela rezervat s’mund të përdoren drejtpërsëdrejti, rekomandohet të përjashtohen disa drejtori nga të qenët e trajtueshme prej PHP FPM. Për më tepër hollësi, ju lutemi, shihni %1$s formësimin zyrtar të shërbyesit nginx %2$s", + "PrivateDirectoryCantCheckUrl": "Kontrolli i URL-së %1$s dështoi: %2$s", + "PrivateDirectoryInternetDisabled": "S’kontrolluam dot nëse URL-të vijuese janë të përdorshme, ngaqë veçoritë internet janë të çaktivizuara në këtë instancë Matomo.", + "PrivateDirectoryIsAccessible": "Na doli se URL-të më sipër janë të përdorshme që nga shfletuesi, por NUK duhet të jenë. Lënia e tyre të përdorshme mund të përbëjë një rrezik potencial sigurie, ngaqë lënda mund të furnizojë informacion mbi shërbyesin tuaj dhe, potencialisht, përdoruesit tuaj. Ju lutemi, kufizojeni hyrjen në to.", + "PrivateDirectoryManualCheck": "Ju lutemi, hapini URL-të dorazi në një shfletues, që të shihni nëse mund t’i përdorni. Nëse mundeni, mund të duhet të ndryshoni formësimin e e shërbyesit tuaj, ngaqë këto kartela/drejtori s’duhet të jenë të përdorshme që prej një shfletuesi nga Interneti ose Intraneti.", + "RecommendedPrivateDirectories": "Drejtori Private të Rekomanduara", + "RequiredPrivateDirectories": "Drejtori Private të Domosdoshme", + "Sections": "Ndarje", + "UrlsAccessibleViaBrowser": "Gjetëm se URL-të më sipër janë të përdorshme që nga shfletuesi, por rekomandojmë të mos jenë. Nëse mundet, ju lutemi, kufizoni hyrjen në to." + } +} diff --git a/plugins/Diagnostics/lang/sr.json b/plugins/Diagnostics/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..31e56505e07887a01744fe0b2c662062d56d6129 --- /dev/null +++ b/plugins/Diagnostics/lang/sr.json @@ -0,0 +1,7 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Datoteka sa podešavanjima", + "HideUnchanged": "Ukoliko želite da vidite samo izmenjene vrednosti, možete da %1$ssakrijete neizmenjene vrednosti%2$s.", + "Sections": "Sekcije" + } +} diff --git a/plugins/Diagnostics/lang/sv.json b/plugins/Diagnostics/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..3e04891aa0a3a53c6068091fd0d4d87b1fa4247c --- /dev/null +++ b/plugins/Diagnostics/lang/sv.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "Alla privata kataloger är oåtkomliga via internet.", + "BrowserAndAutoArchivingEnabledComment": "Det verkar som att både webbläsare och automatisk arkivering är aktiverade. Automatisk arkivering startade senast %3$s sedan. Om %1$sautomatisk arkivering%2$s är aktiverat bör du inaktivera webbläsararkivering i \"Allmänna inställningar\".", + "BrowserAndAutoArchivingEnabledLabel": "Webbläsare och automatisk arkivering aktiverat", + "BrowserTriggeredArchivingEnabled": "För optimal prestanda och en snabb Matomo rekommenderas det starkt att du ställer in en crontab för att automatiskt arkivera dina rapporter och inaktivera webbläsarutlösningen i Matomo-inställningarna. %1$sLäs mer.%2$s", + "ConfigFileIntroduction": "Här kan du se Matomo-konfigurationen. Om du kör Matomo i en belastningsbalanserad miljö kan sidan vara annorlunda beroende på vilken server denna sida laddas. Rader med en annan bakgrundsfärg är ändrade konfigurationsvärden som är specificerade till exempel i %1$s filen.", + "ConfigFileTitle": "Konfigurationsfil", + "ConfigIniAccessible": "Vi upptäckte också att Matomos konfigurationskatalog är öppet åtkomlig. Trots att attackerare inte kan läsa konfigurationen nu så finns risken att utifall din webbserver slutar exekvera PHP-filer (av någon anledning) kommer dina inloggningsuppgifter för MySQL och liknande vara tillgängligt för vem som helst. Vänligen kontrollera webbserverns konfiguration och begränsa åtkomsten till denna katalog.", + "CronArchivingHasNotRun": "Arkiveringen har ännu inte körts.", + "CronArchivingHasNotRunInAWhile": "Arkiveringen kördes senast framgångsrikt i %1$s vilken är %2$s sedan.", + "CronArchivingLastRunCheck": "Senaste avklarade arkiveringen", + "CronArchivingRanSuccessfullyXAgo": "Arkiveringsprocessen slutfördes framgångsrikt för %1$s sedan.", + "CronArchivingRunDetails": "Kontrollera att du har ställt in en crontab som anropar %1$s konsolkommandot och att du har konfigurerat en %2$s för att ta emot fel via e-post om arkivering misslyckas. Du kan också försöka köra konsolkommandot för att arkivera dina rapporter manuellt: %3$s. %4$sLäs mer.%5$s", + "DatabaseCollationCharset": "Standardkollationen för din konfigurerade teckenuppsättning är: %1$s", + "DatabaseCollationConnection": "Din nuvarande anslutnings kollation är: %1$s", + "DatabaseCollationNotConfigured": "Din databasanslutning är konfigurerad utan en uttrycklig kollation. Vänligen uppdatera [database] collation = '' i filen \"config/config.ini.php\" med den kollation som ska användas, för att säkerställa att alla databasfunktioner fungerar som förväntat.", + "DatabaseReaderConnection": "Anslutning av databasläsare", + "DatabaseUtf8Requirement": "Detta krävs för att kunna lagra 4-byte UTF8-tecken. Om inte utf8mb4 finns så kommer specialtecken, som emojis, mindre vanliga tecken på asiatiska språk, olika historiska skript eller matematiska symboler att ersättas med%1$s. Du kan läsa mer information om detta ämne i %2$sdenna FAQ%3$s.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Din databas stödjer utf8mb4-teckenkodning, men dina databastabeller har inte konverterats ännu. Du kan göra detta genom att utföra kommandot %1$s eller genom att aktivera den automatiska konverteringen i Allmänna inställningar.", + "DatabaseUtf8mb4CharsetRecommended": "Din databas stödjer inte utf8mb4-teckenkodning än.", + "EnableRequiredDirectoriesDiagnostic": "Den här kontrollen hoppades över eftersom den här kontrollen är inaktiverad i konfigurationen. För att aktivera den här kontrollen ställer du in [Allmänt] enable_required_directories_diagnostic = 1 i filen \"config/config.ini.php\".", + "HideUnchanged": "Om du vill se endast ändrade värden så kan du %1$s dölja alla oförändrade värden%2$s.", + "HtaccessWarningNginx": "För att säkerställa att känsliga filer inte kan nås direkt rekommenderas du att konfigurera din webbserver att begränsa åtkomsten till vissa kataloger. För mer information kan du läsa om den %1$s officiella nginx server konfigurationen %2$s", + "MariaDbNotConfigured": "Din databasversion indikerar att du nog använder en MariaDb-server. Om så är fallet så säkerställ att följande sätts i filen \"config/config.ini.php\" för att databasen ska fungera som förväntat: [database] schema = Mariadb", + "MysqlMaxPacketSize": "Max paketstorlek", + "MysqlMaxPacketSizeWarning": "Det rekommenderas att du konfigurerar en '%1$smax_allowed_packet%2$s'-storlek i din MySQL-databas med minst %3$s. Konfigurerad är för närvarande %4$s.", + "MysqlTemporaryTablesWarning": "MySQL-tillstånd CREATE TEMPORARY TABLES krävs för att Matomo ska fungera korrekt.", + "MysqlTransactionLevel": "Ändring av transaktionsisoleringsnivå stöds inte. Arkivering kommer fortfarande att fungera men det kan gå långsammare och det rekommenderas att ändra till exempel `binlog_format` till `row` om möjligt.", + "NoDataForReportArchivingNotRun": "Arkiveringen av dina rapporter har inte genomförts på senaste tiden, %1$släs mer om hur du genererar dina rapporter.%2$s", + "PHPFPMWarningApache": "PHP FPM kommer att ignorera regler i .htaccess för .php-filer. För att säkerställa att känsliga filer inte kan nås direkt rekommenderas det att exkludera dessa kataloger från att hanteras av PHP FPM genom att lägga till raden %1$s till %2$s-sektionen i din virtuella värd av Apache-konfiguration. Strax ovanför raden %3$s.", + "PHPFPMWarningGeneric": "PHP FPM kan möjligen ignorera regler från .htaccess för .php-filer. För att säkerställa att inte känsliga filer kan nås direkt rekommenderas du att konfigurera din webbserver att exkludera katalogen /config från att hanteras av PHP FPM.", + "PHPFPMWarningNginx": "PHP FPM kommer ignorera regler från .htaccess för .php-filer. För att säkerställa att inte känsliga filer kan nås direkt rekommenderas du att exkludera vissa kataloger från att hantera av PHP FPM. För mer information vänligen se mer i den %1$s officiiella nginx server konfigurationen %2$s", + "PrivateDirectoryCantCheckUrl": "Kollar URL:en %1$s misslyckades: %2$s", + "PrivateDirectoryInternetDisabled": "Vi kunde inte kontrollera om följande webbadresser är tillgängliga eftersom internetfunktionerna är inaktiverade på denna Matomo.", + "PrivateDirectoryIsAccessible": "Vi upptäckte att ovanstående URL:ar är åtkomliga via en webbläsare, vilket de INTE borde vara. Att låta dem vara det är en säkerhetsrisk eftersom det kan avslöja information om din server och eventuellt dina användare. Vänligen begränsa åtkomsten till detta.", + "PrivateDirectoryManualCheck": "Öppna URL:erna manuellt i en webbläsare för att se om du kan komma åt den. Om du kan det kan du behöva ändra din serverkonfiguration eftersom dessa filer/kataloger inte bör vara tillgängliga via en webbläsare från Internet eller intranätet.", + "RecommendedPrivateDirectories": "Rekommenderade privata kataloger", + "RequiredPrivateDirectories": "Obligatoriska privata kataloger", + "Sections": "Sektioner", + "UrlsAccessibleViaBrowser": "Vi upptäckte att ovanstående URL:ar är åtkomliga via en webbläsare, men vi rekommenderar att de inte är det. Om möjligt bör du begränsa åtkomsten till dem." + } +} diff --git a/plugins/Diagnostics/lang/ta.json b/plugins/Diagnostics/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..eae6fccd7178c41af50e962ac3fc18dec5ced321 --- /dev/null +++ b/plugins/Diagnostics/lang/ta.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "அனைத்து தனியார் கோப்பகங்களும் இணையத்திலிருந்து அணுக முடியாதவை.", + "BrowserAndAutoArchivingEnabledComment": "உலாவி மற்றும் தானியங்கி ஆர்கைவிங் இரண்டும் இயக்கப்பட்டுள்ளன. தானியங்கி ஆர்கைவிங் கடைசியாக %3$s முன்பு தொடங்கப்பட்டது. ஏனெனில் %1$sதானியங்கி ஆர்கைவிங்%2$s இயக்கப்பட்டிருந்தால், \"பொது அமைப்புகள்\" இல் உலாவி ஆர்கைவிஙை முடக்க வேண்டும்.", + "BrowserAndAutoArchivingEnabledLabel": "உலாவி மற்றும் தானாக காப்பகப்படுத்துதல் இயக்கப்பட்டது", + "BrowserTriggeredArchivingEnabled": "சிறந்த செயல்பாடுக்கும் விரைவான மாட்டோமோக்கும், உங்கள் அறிக்கைகளை தானாக காப்பகப்படுத்துவதற்கு ஒரு கிரோன்டாபை அமைக்குவது மிகவும் பரிந்துரைக்கப்படுகிறது, மற்றும் மாட்டோமோ அமைப்புகளில் உலாவி தூண்டுதலை முடக்குவது. %1$sமேலும் அறிய.%2$s", + "ConfigFileIntroduction": "இங்கே நீங்கள் மாடோமோ உள்ளமைவைக் காணலாம். சுமை சீரான சூழலில் நீங்கள் மாடோமோவை இயக்குகிறீர்கள் என்றால், இந்த பக்கம் எந்த சேவையகத்திலிருந்து ஏற்றப்பட்டுள்ளது என்பதைப் பொறுத்து பக்கம் வேறுபட்டிருக்கலாம். வேறு பின்னணி வண்ணத்துடன் கூடிய வரிசைகள் %1$s கோப்பில் குறிப்பிடப்பட்டுள்ள கட்டமைப்பு மதிப்புகள்.", + "ConfigFileTitle": "கட்டமைப்பு கோப்பு", + "ConfigIniAccessible": "மாடோமோவின் கட்டமைப்பு அடைவு பகிரங்கமாக அணுகக்கூடியது என்பதையும் நாங்கள் கண்டறிந்தோம். தாக்குபவர்களால் இப்போது உள்ளமைவைப் படிக்க முடியாது என்றாலும், உங்கள் வெப்சர்வர் சில காரணங்களால் பிஎச்பி கோப்புகளை இயக்குவதை நிறுத்தினால், உங்கள் MySQL நற்சான்றிதழ்கள் மற்றும் பிற தகவல்கள் யாருக்கும் கிடைக்கும். தயவுசெய்து உங்கள் வெப்சர்வர் கட்டமைப்பைச் சரிபார்த்து, இந்த கோப்பகத்திற்கான அணுகலை மறுக்கவும்.", + "CronArchivingHasNotRun": "காப்பகமானது இன்னும் வெற்றிகரமாக இயங்கவில்லை.", + "CronArchivingHasNotRunInAWhile": "காப்பகமானது கடைசியாக %1$s இல் வெற்றிகரமாக ஓடியது, இது %2$s க்கு முன்பு.", + "CronArchivingLastRunCheck": "கடைசியாக வெற்றிகரமான காப்பக நிறைவு", + "CronArchivingRanSuccessfullyXAgo": "காப்பகப்படுத்தும் செயல்முறை %1$s முன்பு வெற்றிகரமாக முடிந்தது.", + "CronArchivingRunDetails": "தயவுசெய்து நீங்கள் %1$s கன்சோல் கமாண்டை அழைக்கும் ஒரு crontab அமைத்துள்ளேன் என்பதை சரிபார்க்கவும், மற்றும் ஆர்கைவிங் தோல்வியடைந்தால் மின்னஞ்சல் மூலம் பிழைகளை பெறுவதற்கு நீங்கள் %2$s ஐ கட்டமைத்துள்ளேன் என்பதை சரிபார்க்கவும். உங்கள் அறிக்கைகளை கைமுறையாக ஆர்கைவ் செய்வதற்கு நீங்கள் கன்சோல் கமாண்டை இயக்க முயற்சி செய்யலாம்: %3$s. %4$sமேலும் அறிய.%5$s", + "DatabaseCollationCharset": "உங்கள் கட்டமைக்கப்பட்ட சார்செட்டுக்கான இயல்புநிலை சேகரிப்பு: %1$s ", + "DatabaseCollationConnection": "நீங்கள் தற்போது பயன்படுத்திய இணைப்பு தொகுப்பு: %1$s ", + "DatabaseCollationNotConfigured": "நீங்கள் தரவுத்தள இணைப்பு வெளிப்படையான கூட்டு இல்லாமல் கட்டமைக்கப்பட்டுள்ளது. எல்லா தரவுத்தள அம்சங்களும் எதிர்பார்த்தபடி செயல்படுவதை உறுதிசெய்ய, பயன்படுத்தப்பட வேண்டிய தொகுப்புடன் \"config/config.ini.php\" கோப்பில் <குறியீடு> [தரவுத்தளம்] கூட்டு = '' ஐப் புதுப்பிக்கவும்.", + "DatabaseReaderConnection": "தரவுத்தள வாசகர் இணைப்பு", + "DatabaseUtf8Requirement": "இது 4-பைட் யுடிஎஃப் 8 எழுத்துக்களை சேமிக்க வேண்டும். யுடிஎஃப் 8 எம்பி 4 எமோசிகள், ஆசிய மொழிகளின் குறைவான பொதுவான எழுத்துக்கள் போன்ற சிறப்பு எழுத்துக்கள் கிடைக்காவிட்டால், பல்வேறு வரலாற்று ச்கிரிப்ட்கள் அல்லது கணித சின்னங்கள் %1$s உடன் மாற்றப்படும். இந்த தலைப்பைப் பற்றிய கூடுதல் விவரங்களை %2$sthis கேள்விகள் %3$s இல் படிக்கலாம்.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "உங்கள் தரவுத்தளம் utf8mb4 சார்சட்டை ஆதரிக்கிறது, ஆனால் உங்கள் தரவுத்தள அட்டவணைகள் இன்னும் மாற்றப்படவில்லை. %1$s கட்டளையை இயக்குவதன் மூலமோ அல்லது பொது அமைப்புகளில் தானியங்கி மாற்றத்தைச் செயல்படுத்துவதன் மூலமோ இதைச் செய்யலாம்.", + "DatabaseUtf8mb4CharsetRecommended": "உங்கள் தரவுத்தளம் utf8mb4 சார்சனை இன்னும் ஆதரிக்கவில்லை.", + "EnableRequiredDirectoriesDiagnostic": "இந்த சோதனை கட்டமைப்பில் முடக்கப்பட்டுள்ளதால் இந்த சோதனை தவிர்க்கப்பட்டுள்ளது. \"config/config.ini.php\" கோப்பில் [பொது] enable_required_directories_diagnostic = 1 என்று கணம் செய்து இந்த சோதனையை இயக்கவும்.", + "HideUnchanged": "நீங்கள் மாற்றப்பட்ட மதிப்புகளை மட்டுமே காண விரும்பினால், நீங்கள் %1$sமாறாத மதிப்புகளை அனைத்தையும் மறைக்கலாம்%2$s.", + "HtaccessWarningNginx": "உணர்திறன் கோப்புகளை நேரடியாக அணுக முடியாது என்பதை உறுதிப்படுத்த, சில கோப்பகங்களுக்கான அணுகலைக் கட்டுப்படுத்த உங்கள் வலை சேவையகத்தை உள்ளமைக்க பரிந்துரைக்கப்படுகிறது. மேலும் தகவலுக்கு %1$s அதிகாரப்பூர்வ nginx சேவையக உள்ளமைவு %2$s ஐப் பார்க்கவும்", + "MariaDbNotConfigured": "உங்கள் தரவுத்தள பதிப்பு உங்கள் MariaDb சேவையகத்தைப் பயன்படுத்துவதாக குறிப்பிடுகிறது. இது உண்மையாக இருந்தால், அனைத்து தரவுத்தள நற்பொருத்தங்களும் எதிர்பார்க்கப்பட்டபடி வேலை செய்ய உறுதி செய்யும் \"config/config.ini.php\" கோப்பில் [database] schema = Mariadb என்று கணம் செய்ய தயவுசெய்து உறுதி செய்யவும்.", + "MysqlMaxPacketSize": "அதிகபட்ச பாக்கெட் அளவு", + "MysqlMaxPacketSizeWarning": "குறைந்தது%3$s இன் உங்கள் MySQL தரவுத்தளத்தில் '%1$smax_allowed_packet%2$s' அளவை உள்ளமைக்க பரிந்துரைக்கப்படுகிறது. கட்டமைக்கப்பட்ட தற்போது %4$s ஆகும்.", + "MysqlTemporaryTablesWarning": "MySQL இசைவு MATOMO சரியாக வேலை செய்ய தற்காலிக அட்டவணைகள் தேவை.", + "MysqlTransactionLevel": "பரிவர்த்தனை தனிமைப்படுத்தல் நிலை ஆதரிக்கப்படவில்லை. காப்பகமானது இன்னும் செயல்படும், ஆனால் அது மெதுவாக இருக்கலாம், எடுத்துக்காட்டாக `binlog_format` முடிந்தால் `வரி` க்கு மாற்றப் பரிந்துரைக்கப்படுகிறது.", + "NoDataForReportArchivingNotRun": "உங்கள் அறிக்கைகளின் காப்பகப்படுத்தல் அண்மையில் மேற்கொள்ளப்படவில்லை, %1$sஉங்கள் அறிக்கைகளை எவ்வாறு உருவாக்க என்பதைப் பற்றி மேலும் அறிய%2$s", + "PHPFPMWarningApache": "PHP FPM .php கோப்புகளுக்கான .htaccess விதிகளை புறக்கணிக்கும். உள்ளாட்சியான கோப்புகளை நேரடியாக அணுக முடியாமல் சில அடைவுகளை PHP FPM மூலம் கையாளப்படுத்தப்படுவதை விலக்க உங்கள் apache விருந்தோம்பி கட்டமைப்புக்குள் %2$s பிரிவில் %1$s என்ற வரியை %3$s வரிக்கு மேல் சேர்க்குவதை பரிந்துரைக்கின்றோம்.", + "PHPFPMWarningGeneric": "பிஎச்பி FPM புறக்கணிக்கலாம் .php கோப்புகளுக்கான .htaccess விதிகள். உணர்திறன் கோப்புகளை நேரடியாக அணுக முடியாது என்பதை உறுதிப்படுத்த, /கட்டமைப்பு கோப்பகத்தை பிஎச்பி FPM ஆல் கையாளப்படுவதிலிருந்து விலக்க உங்கள் வலை சேவையகத்தை உள்ளமைக்க பரிந்துரைக்கப்படுகிறது.", + "PHPFPMWarningNginx": "பிஎச்பி FPM புறக்கணிக்கும் .php கோப்புகளுக்கான .htaccess விதிகள். உணர்திறன் கோப்புகளை நேரடியாக அணுக முடியாது என்பதை உறுதிப்படுத்த, சில கோப்பகங்களை பிஎச்பி FPM ஆல் கையாளப்படுவதிலிருந்து விலக்க பரிந்துரைக்கப்படுகிறது. மேலும் தகவலுக்கு %1$s அதிகாரப்பூர்வ NGINX சேவையக உள்ளமைவு %2$s ஐப் பார்க்கவும்", + "PrivateDirectoryCantCheckUrl": "முகவரி %1$s ஐ சரிபார்க்கவும் தோல்வியுற்றது: %2$s", + "PrivateDirectoryInternetDisabled": "இந்த மாடோமோவில் இணைய நற்பொருத்தங்கள் முடக்கப்பட்டுள்ளதால் பின்வரும் முகவரி களை அணுக முடியுமா என்பதை எங்களால் சரிபார்க்க முடியவில்லை.", + "PrivateDirectoryIsAccessible": "மேலே உள்ள முகவரி கள் உலாவி வழியாக அணுகக்கூடியவை என்பதைக் கண்டறிந்தோம், ஆனால் அவை இருக்கக்கூடாது. அவற்றை அணுக அனுமதிப்பது சாத்தியமான பாதுகாப்பு அபாயத்தை ஏற்படுத்தும், ஏனெனில் உள்ளடக்கங்கள் உங்கள் சேவையகம் மற்றும் உங்கள் பயனர்களைப் பற்றிய தகவல்களை வழங்க முடியும். அவர்களுக்கான அணுகலை கட்டுப்படுத்தவும்.", + "PrivateDirectoryManualCheck": "நீங்கள் அதை அணுக முடியுமா என்று பார்க்க உலாவியில் முகவரி களை கைமுறையாக திறக்கவும். உங்களால் முடிந்தால், இந்த கோப்புகள்/கோப்பகங்கள் இணையம் அல்லது இன்ட்ராநெட்டிலிருந்து உலாவி வழியாக அணுக முடியாததால் உங்கள் சேவையக உள்ளமைவை மாற்ற வேண்டியிருக்கும்.", + "RecommendedPrivateDirectories": "பரிந்துரைக்கப்பட்ட தனியார் கோப்பகங்கள்", + "RequiredPrivateDirectories": "தேவையான தனியார் கோப்பகங்கள்", + "Sections": "பிரிவுகள்", + "UrlsAccessibleViaBrowser": "மேலே உள்ள முகவரி கள் உலாவி வழியாக அணுகக்கூடியவை என்பதைக் கண்டறிந்தோம், ஆனால் அவை இருக்கக்கூடாது என்று நாங்கள் பரிந்துரைக்கிறோம். முடிந்தால், தயவுசெய்து அவற்றை அணுகவும்." + } +} diff --git a/plugins/Diagnostics/lang/te.json b/plugins/Diagnostics/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/te.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/th.json b/plugins/Diagnostics/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/th.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/tl.json b/plugins/Diagnostics/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/tl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/tr.json b/plugins/Diagnostics/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..a4611d3e3a40f76fa1c0ff0c4f8fece78888d4e6 --- /dev/null +++ b/plugins/Diagnostics/lang/tr.json @@ -0,0 +1,43 @@ +{ + "Diagnostics": { + "AllPrivateDirectoriesAreInaccessible": "İnternet üzerinden hiç bir kişisel klasöre erişilemiyor.", + "BrowserAndAutoArchivingEnabledComment": "Hem tarayıcı hem de otomatik arşivleme açılmış gibi görünüyor. Otomatik arşivleme en son %3$s önce başlatılmış. %1$sOtomatik arşivleme%2$s açık ise, \"Genel ayarlar\" bölümünden tarayıcı arşivlemeyi kapatmalısınız.", + "BrowserAndAutoArchivingEnabledLabel": "Tarayıcı ve otomatik arşivleme açık", + "BrowserTriggeredArchivingEnabled": "En iyi başarım ve daha hızlı bir Matomo kullanımı için raporlarınızı otomatik olarak arşivleyecek bir zamanlanmış görev yapılandırmanız ve Matomo ayarlarından tarayıcı ile tetiklenen arşivleme özelliğini kapatmanız önemle önerilir. %1$sAyrıntılı bilgi almak için buraya tıklayabilirsiniz.%2$s", + "ConfigFileIntroduction": "Matomo yapılandırması bu bölümde görülebilir. Matomo yazılımını yük dengeleme yapılan bir ortamda kullanıyorsanız sayfadaki bilgiler yüklendiği sunucuya göre değişiklik gösterebilir. %1$s dosyasındaki örnek dosyada belirtilen yapılandırma değerlerine göre değiştirilmiş olan satırlar farklı arka plan renginde görüntülenir.", + "ConfigFileTitle": "Yapılandırma dosyası", + "ConfigIniAccessible": "Ayrıca Matomo yapılandırma klasörü de herkesin erişimine açık. Saldırganlar şu anda yapılandırma bilgilerini okuyamaz. Ancak site sunucusu herhangi bir nedenle PHP dosyalarını yürütmeyi durdurursa, herkes MySQL ve diğer yapılandırma bilgilerini görebilir. Lütfen sunucu yapılandırmasını denetleyerek ve bu klasöre erişimi engelleyin.", + "CronArchivingHasNotRun": "Henüz tamamlanmış bir arşivleme işlemi yapılmamış.", + "CronArchivingHasNotRunInAWhile": "Son tamamlanmış arşivleme işlemi %1$s tarihinde %2$s önce yapılmış.", + "CronArchivingLastRunCheck": "Son tamamlanmış arşivleme işlemi", + "CronArchivingRanSuccessfullyXAgo": "Arşivleme işlemi %1$s önce tamamlanmış.", + "CronArchivingRunDetails": "Lütfen %1$s komut satırı komutunu çağıran bir zamanlanmış görev yapılandırdığınızdan ve arşivleme işleminde sorun çıkarsa hataların alınabilmesi için bir %2$s yapılandırdığınızdan emin olun. Ayrıca raporları el ile arşivlemek için şu komut satırı komutunu çalıştırmayı deneyebilirsiniz: %3$s. %4$sAyrıntılı bilgi alın.%5$s", + "DatabaseCollationCharset": "Yapılandırdığınız karakter kümesi için varsayılan karşılaştırma: %1$s", + "DatabaseCollationConnection": "Şu anda kullandığınız bağlantı karşılaştırması: %1$s", + "DatabaseCollationNotConfigured": "Veri tabanı bağlantınız açık bir karşılaştırma olmadan yapılandırılmış. Lütfen tüm veri tabanı özelliklerinin beklenildiği gibi çalışmasını sağlamak için \"config/config.ini.php\" dosyasındaki [database] collation = '' kodunu kullanılacak karşılaştırmaya göre güncelleyin.", + "DatabaseReaderConnection": "Veri tabanı okuyucu bağlantısı", + "DatabaseUtf8Requirement": "Bu özellik 4 baytlık UTF8 karakterlerini kaydedebilmek için gereklidir. utf8mb4 kullanılabilir olmadan, özel karakterler, duygu ifadeleri (emojiler), asya dillerinde az kullanılan karakterler, çeşitli tarihsel ya da matematiksel simgeler yerine %1$s kullanılır. Ayrıntılı bilgi almak için %2$ssık sorulan sorular bölümüne%3$s bakabilirsiniz.", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "Veri tabanınız utf8mb4 karakter kümesini destekliyor ancak veri tabanı tablolarınız henüz dönüştürülmemiş. Bu işlemi %1$s komutunu yürüterek ya da genel ayarlar bölümünden otomatik dönüştürme seçeneğini açarak yapabilirsiniz.", + "DatabaseUtf8mb4CharsetRecommended": "Veri tabanınız henüz utf8mb4 karakter kümesini desteklemiyor.", + "EnableRequiredDirectoriesDiagnostic": "Yapılandırma ayarlarında kapatıldığından bu denetim atlandı. Bu denetimi açmak için \"config/config.ini.php\" dosyasında [General] enable_required_directories_diagnostic = 1 ayarını yapın.", + "HideUnchanged": "Yalnızca değiştirilmiş değerlerin görüntülenmesi için %1$sdeğiştirilmemiş tüm değerleri gizleyin%2$s.", + "HtaccessWarningNginx": "Hassas dosyalara doğrudan erişilemediğinden emin olmak için, site sunucunuzu belirli klasörlere erişimi engelleyecek şekilde yapılandırmanız önerilir. Ayrıntılı bilgi almak için %1$s resmi Nginx sunucu yapılandırmasına %2$s bakabilirsiniz", + "MariaDbNotConfigured": "Veri tabanı sürümünüz bir MariaDB sunucusu kullanıyor olabileceğinizi gösteriyor. Bu durumda, tüm veri tabanı özelliklerinin beklendiği gibi çalıştığından emin olmak için lütfen \"config/config.ini.php\" dosyasında [database] schema = Mariadb ayarının yapıldığından emin olun.", + "MysqlMaxPacketSize": "En büyük paket boyutu", + "MysqlMaxPacketSizeWarning": "MySQL veri tabanınız için '%1$smax_allowed_packet%2$s' değerini en az %3$s olarak ayarlamanız önerilir. Şu anda %4$s olarak ayarlanmış.", + "MysqlTemporaryTablesWarning": "Matomo uygulamasının düzgün çalışabilmesi için CREATE TEMPORARY TABLES MySQL izninin verilmiş olması gereklidir.", + "MysqlTransactionLevel": "İşlem yalıtma düzeyinin değiştirilmesi desteklenmiyor. Arşivleme çalışır ancak daha yavaş olabilir. Olabiliyorsa `binlog_format` seçeneğinin `row` olarak değiştirilmesi önerilir.", + "NoDataForReportArchivingNotRun": "Yakın zamanda raporlarınız arşivlenmemiş. %1$sRaporlarınızı üretmek hakkında ayrıntılı bilgi almak için buraya tıklayabilirsiniz.%2$s", + "PHPFPMWarningApache": "PHP FPM, .php dosyaları için .htaccess kurallarını yok sayar. Hassas dosyalara doğrudan erişilemediğinden emin olmak için, %3$s satırının hemen üstündeki apache sanal sunucu yapılandırmanızdaki %2$s bölümüne %1$s satırını ekleyerek, belirli klasörlerin PHP FPM tarafından işlenmesini engellemeniz önerilir.", + "PHPFPMWarningGeneric": "PHP FPM, .php dosyaları için .htaccess kurallarını yok sayabilir. Hassas dosyalara doğrudan erişilemediğinden emin olmak için, site sunucunuzu /config klasörünü PHP FPM tarafından işlenmesini engelleyecek şekilde yapılandırmanız önerilir.", + "PHPFPMWarningNginx": "PHP FPM, .php dosyaları için .htaccess kurallarını yok sayar. Hassas dosyalara doğrudan erişilemediğinden emin olmak için, belirli klasörlerin PHP FPM tarafından işlenmesini engellemeniz önerilir. Ayrıntılı bilgi almak için %1$s resmi Nginx sunucu yapılandırmasına %2$s bakabilirsiniz", + "PrivateDirectoryCantCheckUrl": "%1$s adresine tıklamak sonuç vermedi: %2$s", + "PrivateDirectoryInternetDisabled": "Bu Matomo kopyasında İnternet özellikleri kapatılmış olduğundan aşağıdaki adreslerin erişilebilir olup olmadığı denetlenemedi.", + "PrivateDirectoryIsAccessible": "Yukarıdaki adreslere tarayıcı ile erişilememesi gerektiği halde erişilebiliyor. Bu adreslerdeki içerikler, sunucunuz ve potansiyel olarak kullanıcılarınız hakkında bilgiler sağlayabilir. Bu durum potansiyel bir güvenlik riski oluşturabilir. Lütfen bu adreslere erişimi kısıtlayın.", + "PrivateDirectoryManualCheck": "Lütfen erişilip erişilemediğini görmek için adresleri el ile bir tarayıcıda açın. Yapabiliyorsanız sunucu yapılandırmasını değiştirerek, bu dosyalara/klasörlere İnternet ya da İntranet üzerinden bir tarayıcı ile erişilmesini engellemeniz gerekebilir.", + "RecommendedPrivateDirectories": "Önerilen gizli klasörler", + "RequiredPrivateDirectories": "Gerekli kişisel klasörler", + "Sections": "Bölümler", + "UrlsAccessibleViaBrowser": "Yukarıdaki adreslere tarayıcı üzerinden erişilebildiğini gördük, ancak bunun yapılamamasını öneririz. Yapabiliyorsanız, bu klasörelere erişme iznini kısıtlayın." + } +} diff --git a/plugins/Diagnostics/lang/tzm.json b/plugins/Diagnostics/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/tzm.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/uk.json b/plugins/Diagnostics/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..cff1bfb83ef479c57adc8f51d64b6f8680dfe3df --- /dev/null +++ b/plugins/Diagnostics/lang/uk.json @@ -0,0 +1,7 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Файл конфігурації", + "HideUnchanged": "Якщо ви хочете бачити тільки змінені значення, ви можете %1$sзаховати всі незмінені значення%2$s.", + "Sections": "Розділи" + } +} diff --git a/plugins/Diagnostics/lang/ur.json b/plugins/Diagnostics/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Diagnostics/lang/ur.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Diagnostics/lang/vi.json b/plugins/Diagnostics/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..e19dcb65f3696132010afad15f9eb051e0eb97cf --- /dev/null +++ b/plugins/Diagnostics/lang/vi.json @@ -0,0 +1,9 @@ +{ + "Diagnostics": { + "ConfigFileTitle": "Tập tin cấu hình", + "HideUnchanged": "Nếu bạn chỉ muốn xem các giá trị đã được thay đổi, bạn có thể %1$sẩn tất cả các giá trị không thay đổi%2$s.", + "MysqlMaxPacketSize": "Kích thước gói tối đa", + "MysqlMaxPacketSizeWarning": "Bạn nên định cấu hình kích thước '%1$smax_allowed_packet%2$s' trong cơ sở dữ liệu MySQL của bạn ít nhất %3$s. Cấu hình hiện tại là %4$s.", + "Sections": "Phân mục" + } +} diff --git a/plugins/Diagnostics/lang/zh-cn.json b/plugins/Diagnostics/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..d23d03bad9dbf984e823d2f33fc7b71cfcd619b6 --- /dev/null +++ b/plugins/Diagnostics/lang/zh-cn.json @@ -0,0 +1,23 @@ +{ + "Diagnostics": { + "BrowserTriggeredArchivingEnabled": "为了获得最佳性能和快速的Matomo,强烈建议您设置crontab以自动存档报告,并在Matomo设置中禁用浏览器触发。 %1$s了解更多。%2$s", + "ConfigFileIntroduction": "在这里,您可以查看Matomo配置。 如果您在负载平衡环境中运行Matomo,则页面可能会有所不同,具体取决于加载此页面的服务器。 具有不同背景颜色的行是更改的配置值,例如在%1$s文件中指定。", + "ConfigFileTitle": "配置文件", + "CronArchivingHasNotRun": "存档尚未成功运行。", + "CronArchivingHasNotRunInAWhile": "归档最后一次在%1$s上成功运行,这是%2$s之前。", + "CronArchivingLastRunCheck": "上次成功完成归档", + "CronArchivingRanSuccessfullyXAgo": "归档过程已成功完成%1$s之前。", + "CronArchivingRunDetails": "请检查您是否设置了调用%1$s控制台命令的crontab,并且如果存档失败,您已配置%2$s以通过电子邮件接收错误。 您还可以尝试运行console命令手动存档报告:%3$s。 %4$s了解更多。%5$s", + "DatabaseReaderConnection": "数据库读取器连接", + "DatabaseUtf8Requirement": "为了能够存储4字节UTF8字符,这是必需的。 除非utf8mb4可用,否则特殊字符(例如表情符号,亚洲语言的较不常用字符),各种历史性脚本或数学符号将替换为%1$s。您可以在FAQ%3$s中的%2$s中阅读有关此主题的更多详细信息。", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "您的数据库支持utf8mb4字符集,但是您的数据库表尚未转换。 您可以通过执行命令%1$s或在“常规设置”中激活自动转换来执行此操作。", + "DatabaseUtf8mb4CharsetRecommended": "你的数据库不支持 utf8mb4 字符集。", + "HideUnchanged": "如果只想看改变的值可以%1$s,隐藏所有为改变的值%2$s。", + "MysqlMaxPacketSize": "最大包大小", + "MysqlMaxPacketSizeWarning": "建议在您的MySQL数据库中配置至少%3$s的'%1$smax_allowed_packet%2$s'大小。 配置目前为%4$s。", + "MysqlTemporaryTablesWarning": "Matomo必须具备MySQL权限CREATE TEMPORARY TABLES才能正常工作。", + "MysqlTransactionLevel": "不支持更改事务隔离级别。 归档仍然可以,但是可能会比较慢,因此建议将“ binlog_format”更改为“ row”。", + "NoDataForReportArchivingNotRun": "最近没有执行您的报告存档,%1$s更多关于如何生成报告。%2$s", + "Sections": "部分" + } +} diff --git a/plugins/Diagnostics/lang/zh-tw.json b/plugins/Diagnostics/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..5c3af28b643e56935803967f773ca68456f31e56 --- /dev/null +++ b/plugins/Diagnostics/lang/zh-tw.json @@ -0,0 +1,24 @@ +{ + "Diagnostics": { + "BrowserAndAutoArchivingEnabledLabel": "瀏覽器與自動封存已經啟用", + "BrowserTriggeredArchivingEnabled": "為使 Matomo 能最佳化,我們強烈建議你設定 crontab 工作排程來自動彙整你的報告,且在 Matomo 一般設定內關閉瀏覽器的自動觸發彙整。%1$s 了解更多。%2$s", + "ConfigFileIntroduction": "你可以在這裡檢視 Matomo 組態設定。若你在負載平衡環境中執行 Matomo,此頁面可能依不同的伺服器而有所差異。不同顏色的欄位代表已修改過的設定值,如範例檔案 %1$s 內。", + "ConfigFileTitle": "設定檔", + "CronArchivingHasNotRun": "報表尚未成功彙整過。", + "CronArchivingHasNotRunInAWhile": "上次於 %1$s 時成功彙整了報表 ( %2$s 之前 )。", + "CronArchivingLastRunCheck": "上次成功彙整報表的時間", + "CronArchivingRanSuccessfullyXAgo": "報表彙整程序於 %1$s 前執行成功。", + "CronArchivingRunDetails": "請你檢查是否已經設定了 crontab 工作排程指令 %1$s,且已設定了 %2$s 以便在報表彙整失敗時可以收到信件。你也能試著執行此指令來手動彙整報表:%3$s。%4$s了解更多。%5$s", + "DatabaseReaderConnection": "資料庫讀取器連線", + "DatabaseUtf8Requirement": "必須啟用此設定才能儲存 4-byte UTF8 字元。特殊字元例如顏文字、不常用的亞洲語系字元、古代文字、數學符號等,會被取代為 %1$s,直到啟用 utf8mb4 支援。你可以在 %2$s這個 FAQ%3$s 內閱讀更多細節。", + "DatabaseUtf8mb4CharsetAvailableButNotUsed": "你的資料庫支援 utf8mb4 字符集,但是你的資料庫表格尚未轉換。你可以執行此指令 %1$s 或是在「一般設定」內啟用自動轉換。", + "DatabaseUtf8mb4CharsetRecommended": "你的資料庫尚未支援 utf8mb4 字符集。", + "HideUnchanged": "如果你只想看有變更過值的設定,你可以%1$s隱藏未修改的值%2$s。", + "MysqlMaxPacketSize": "最大封包大小", + "MysqlMaxPacketSizeWarning": "推薦將 MySQL 資料庫中的「%1$smax_allowed_packet%2$s」值至少設定為 %3$s。目前的設定為 %4$s。", + "MysqlTemporaryTablesWarning": "Matomo 需要 MySQL CREATE TEMPORARY TABLES 權限以正常運作。", + "MysqlTransactionLevel": "不支持更改事务隔离级别。 归档仍然可以,但是可能会比较慢,因此建议将“ binlog_format”更改为“ row”。", + "NoDataForReportArchivingNotRun": "你最近尚未執行彙整報表,%1$s 了解如何產生你的報表。%2$s", + "Sections": "分區" + } +} diff --git a/plugins/Diagnostics/plugin.json b/plugins/Diagnostics/plugin.json new file mode 100644 index 0000000000000000000000000000000000000000..bf6a0bed8a89e6a5561d0bcd8b8134d4df862d7c --- /dev/null +++ b/plugins/Diagnostics/plugin.json @@ -0,0 +1,3 @@ +{ + "description": "Performs diagnostics to check that Matomo is installed and runs correctly." +} \ No newline at end of file diff --git a/plugins/Diagnostics/stylesheets/configfile.less b/plugins/Diagnostics/stylesheets/configfile.less new file mode 100644 index 0000000000000000000000000000000000000000..ecccc4197cf2713ded0e1c1dcb5714ca65dcd307 --- /dev/null +++ b/plugins/Diagnostics/stylesheets/configfile.less @@ -0,0 +1,31 @@ +.diagnostics.configfile { + .custom-value { + background-color: @theme-color-background-tinyContrast; + } + + .defaultValue { + font-style: italic; + } + + .pre { + white-space: pre-wrap; + } + + td.name { + max-width: 330px; + word-wrap: break-word; + width: 25%; + } + + td.value { + word-wrap: break-word; + max-width: 400px; + width: 25%; + } + + td.description { + word-wrap: break-word; + word-break: break-word; + } + +} diff --git a/plugins/Diagnostics/templates/configfile.twig b/plugins/Diagnostics/templates/configfile.twig new file mode 100644 index 0000000000000000000000000000000000000000..903bfb5a05538e4d06e122a6e1d670afcbd00028 --- /dev/null +++ b/plugins/Diagnostics/templates/configfile.twig @@ -0,0 +1,13 @@ +{% extends 'admin.twig' %} + +{% macro humanReadableValue(value) %} + +{% endmacro %} + +{% block content %} +
    +{% endblock %} diff --git a/plugins/Diagnostics/templates/force_ssl_link.twig b/plugins/Diagnostics/templates/force_ssl_link.twig new file mode 100644 index 0000000000000000000000000000000000000000..8d7c63a8b4d4b0340233003322f64dae05da7858 --- /dev/null +++ b/plugins/Diagnostics/templates/force_ssl_link.twig @@ -0,0 +1 @@ +{{ 'General_UseSSLInstall'|translate('', '')|raw }} \ No newline at end of file diff --git a/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php b/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d46bdcd7373f53a2db1af0d84c07684791d13aa7 --- /dev/null +++ b/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php @@ -0,0 +1,70 @@ +get(self::$fixture->idSite, 'month', '2010-03-01'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'browserCode==FF'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'daysSinceFirstVisit==2'); + } + + public function testCommandOutputIsAsExpected() + { + $expected = <<applicationTester->run(array( + 'command' => 'diagnostics:analyze-archive-table', + 'table-date' => '2010_03', + )); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } +} + +AnalyzeArchiveTableTest::$fixture = new OneVisitorTwoVisits(); diff --git a/plugins/Diagnostics/tests/Integration/Commands/ArchivingConfigTest.php b/plugins/Diagnostics/tests/Integration/Commands/ArchivingConfigTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e1a1a08b865bbf0d15ab36259b2cb86c0f8cb633 --- /dev/null +++ b/plugins/Diagnostics/tests/Integration/Commands/ArchivingConfigTest.php @@ -0,0 +1,94 @@ +applicationTester->run([ + 'command' => 'diagnostics:archiving-config', + ]); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } + + public function testCommandOutputWithJsonOptionIsAsExpected() + { + $expected = '[["Total Invalidation Count","0"],["In Progress Invalidation Count","0"],["Scheduled Invalidation Count","0"],["Earliest invalidation ts_started",""],["Latest invalidation ts_started",""],["Earliest invalidation ts_invalidated",""],["Latest invalidation ts_invalidated",""],["Number of segment invalidations","0"],["Number of plugin invalidations","0"],["List of plugins being invalidated",""]]'; + $this->applicationTester->run([ + 'command' => 'diagnostics:archiving-metrics', + '--json' => true, + ]); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } +} + +ArchivingConfigTest::$fixture = new OneVisitorTwoVisits(); diff --git a/plugins/Diagnostics/tests/Integration/Commands/ArchivingInstanceStatisticsTest.php b/plugins/Diagnostics/tests/Integration/Commands/ArchivingInstanceStatisticsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2437d1beb49e7a95f9bb1055fd842169e77b676b --- /dev/null +++ b/plugins/Diagnostics/tests/Integration/Commands/ArchivingInstanceStatisticsTest.php @@ -0,0 +1,83 @@ +get(self::$fixture->idSite, 'month', '2010-02-01'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-02-01', 'browserCode==FF'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-02-01', 'daysSinceFirstVisit==2'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'browserCode==FF'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'daysSinceFirstVisit==2'); + + // Create a segment + SegmentEditorAPI::getInstance()->add('test segment', 'browserCode==IE', self::$fixture->idSite); + + Date::$now = strtotime('2010-04-07 06:00:00'); + } + + public function testCommandOutputIsAsExpected() + { + $expected = <<applicationTester->run([ + 'command' => 'diagnostics:archiving-instance-statistics', + ]); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } + + public function testCommandOutputWithJsonOptionIsAsExpected() + { + $expected = '[["Site Count",3],["Segment Count",1],["Database Version","mysql-version-redacted"],["Last full Month Hits",8],["Last 12 Month Hits",8]]'; + $this->applicationTester->run([ + 'command' => 'diagnostics:archiving-instance-statistics', + '--json' => true, + ]); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } +} + +ArchivingInstanceStatisticsTest::$fixture = new OneVisitorTwoVisits(); diff --git a/plugins/Diagnostics/tests/Integration/Commands/ArchivingMetricsTest.php b/plugins/Diagnostics/tests/Integration/Commands/ArchivingMetricsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1dfe8a939aa15c2bf27c3906061b246a2ea98c94 --- /dev/null +++ b/plugins/Diagnostics/tests/Integration/Commands/ArchivingMetricsTest.php @@ -0,0 +1,81 @@ +get(self::$fixture->idSite, 'month', '2010-03-01'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'browserCode==FF'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'daysSinceFirstVisit==2'); + + Date::$now = strtotime('2010-03-07 06:00:00'); + } + + public function testCommandOutputIsAsExpected() + { + $expected = <<applicationTester->run([ + 'command' => 'diagnostics:archiving-metrics', + ]); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } + + public function testCommandOutputWithJsonOptionIsAsExpected() + { + $expected = '[["Total Invalidation Count","4"],["In Progress Invalidation Count","0"],["Scheduled Invalidation Count","4"],["Earliest invalidation ts_started",""],["Latest invalidation ts_started",""],["Earliest invalidation ts_invalidated","2010-03-07 01:00:00"],["Latest invalidation ts_invalidated","2010-03-07 01:00:00"],["Number of segment invalidations","0"],["Number of plugin invalidations","0"],["List of plugins being invalidated",""]]'; + $this->applicationTester->run([ + 'command' => 'diagnostics:archiving-metrics', + '--json' => true, + ]); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } +} + +ArchivingMetricsTest::$fixture = new OneVisitorTwoVisits(); diff --git a/plugins/Diagnostics/tests/Integration/Commands/ArchivingQueueTest.php b/plugins/Diagnostics/tests/Integration/Commands/ArchivingQueueTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2566065d5fe19105adb3b4737e2e5cd9ae66b14f --- /dev/null +++ b/plugins/Diagnostics/tests/Integration/Commands/ArchivingQueueTest.php @@ -0,0 +1,75 @@ +get(self::$fixture->idSite, 'month', '2010-03-01'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'browserCode==FF'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'daysSinceFirstVisit==2'); + + Date::$now = strtotime('2010-03-07 06:00:00'); + } + + public function testCommandOutputIsAsExpected() + { + $expected = <<applicationTester->run([ + 'command' => 'diagnostics:archiving-queue', + ]); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } + + public function testCommandOutputWithJsonOptionIsAsExpected() + { + $expected = '[{"Invalidation":1,"Segment":null,"Site":1,"Period":"Day","Date":"2010-03-06","TimeQueued":"2010-03-07 01:00:00","Waiting":18000,"Started":null,"Processing":"","Status":"Queued"},{"Invalidation":2,"Segment":null,"Site":1,"Period":"Week","Date":"2010-03-01 - 2010-03-07","TimeQueued":"2010-03-07 01:00:00","Waiting":18000,"Started":null,"Processing":"","Status":"Queued"},{"Invalidation":3,"Segment":null,"Site":1,"Period":"Month","Date":"2010-03","TimeQueued":"2010-03-07 01:00:00","Waiting":18000,"Started":null,"Processing":"","Status":"Queued"},{"Invalidation":4,"Segment":null,"Site":1,"Period":"Year","Date":"2010","TimeQueued":"2010-03-07 01:00:00","Waiting":18000,"Started":null,"Processing":"","Status":"Queued"}]'; + $this->applicationTester->run(array( + 'command' => 'diagnostics:archiving-queue', + '--json' => true, + )); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } +} + +ArchivingQueueTest::$fixture = new OneVisitorTwoVisits(); diff --git a/plugins/Diagnostics/tests/Integration/Commands/ArchivingStatusTest.php b/plugins/Diagnostics/tests/Integration/Commands/ArchivingStatusTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2abb0c70369602afe13da6a663ddfb543645a44e --- /dev/null +++ b/plugins/Diagnostics/tests/Integration/Commands/ArchivingStatusTest.php @@ -0,0 +1,220 @@ +get(self::$fixture->idSite, 'month', '2010-02-01'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-02-01', 'browserCode==FF'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-02-01', 'daysSinceFirstVisit==2'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'browserCode==FF'); + VisitsSummaryAPI::getInstance()->get(self::$fixture->idSite, 'month', '2010-03-01', 'daysSinceFirstVisit==2'); + + // Create a segment + SegmentEditorAPI::getInstance()->add('test segment', 'browserCode==IE', self::$fixture->idSite); + + Date::$now = strtotime('2010-04-07 06:00:00'); + } + + public function testCommandOutputIsAsExpected() + { + $expected = <<applicationTester->run([ + 'command' => 'diagnostics:archiving-status', + ]); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } + + + public function testCommandOutputWithStatsOptionIsAsExpected() + { + $expected = <<applicationTester->run(array( + 'command' => 'diagnostics:archiving-status', + '--with-stats' => true, + )); + $actual = $this->applicationTester->getDisplay(); + + $this->assertStringMatchesFormat($expected, $actual); + } +} + +ArchivingStatusTest::$fixture = new OneVisitorTwoVisits(); diff --git a/plugins/Diagnostics/tests/Integration/ConfigReaderTest.php b/plugins/Diagnostics/tests/Integration/ConfigReaderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1f58cb538b13d96baad71f80a979d2d56652e36a --- /dev/null +++ b/plugins/Diagnostics/tests/Integration/ConfigReaderTest.php @@ -0,0 +1,272 @@ +configPath('global.ini.php'), $this->configPath('config.ini.php'), $this->configPath('common.config.ini.php')); + $this->configReader = new ConfigReader($settings, new IniReader()); + + FakeAccess::clearAccess($superUser = true); + } + + public function testGetConfigValuesFromFiles() + { + $fileConfig = $this->configReader->getConfigValuesFromFiles(); + + $expected = array ( + 'Category' => + array ( + 'key1' => + array ( + 'value' => 'value_overwritten', + 'description' => '', + 'isCustomValue' => true, + 'defaultValue' => 'value1', + ), + 'key2' => + array ( + 'value' => 'valueCommon', + 'description' => '', + 'isCustomValue' => false, + 'defaultValue' => 'value2', + ), + 'key3' => + array ( + 'value' => '${@piwik(crash))}', + 'description' => '', + 'isCustomValue' => false, + 'defaultValue' => null, + ), + ), + 'CategoryOnlyInGlobalFile' => + array ( + 'key3' => + array ( + 'value' => 'value3', + 'description' => 'test comment', + 'isCustomValue' => false, + 'defaultValue' => 'value3', + ), + 'key4' => + array ( + 'value' => 'value4', + 'description' => 'test comment 4', + 'isCustomValue' => false, + 'defaultValue' => 'value4', + ), + ), + 'TestArray' => + array ( + 'installed' => + array ( + 'value' => + array ( + 0 => 'plugin"1', + 1 => 'plugin2', + 2 => 'plugin3', + ), + 'description' => 'test comment 2 +with multiple lines', + 'isCustomValue' => true, + 'defaultValue' => + array ( + 0 => 'plugin1', + 1 => 'plugin4', + ), + ), + ), + 'TestArrayOnlyInGlobalFile' => + array ( + 'my_array' => + array ( + 'value' => + array ( + 0 => 'value1', + 1 => 'value2', + ), + 'description' => '', + 'isCustomValue' => false, + 'defaultValue' => + array ( + 0 => 'value1', + 1 => 'value2', + ), + ), + ), + 'GeneralSection' => + array ( + 'password' => + array ( + 'value' => '******', + 'description' => '', + 'isCustomValue' => true, + 'defaultValue' => null, + ), + 'login' => + array ( + 'value' => 'tes"t', + 'description' => '', + 'isCustomValue' => true, + 'defaultValue' => null, + ), + ), + 'TestOnlyInCommon' => + array ( + 'value' => + array ( + 'value' => 'commonValue', + 'description' => '', + 'isCustomValue' => false, + 'defaultValue' => null, + ), + ), + 'Tracker' => + array ( + 'commonConfigTracker' => + array ( + 'value' => 'commonConfigTrackerValue', + 'description' => '', + 'isCustomValue' => false, + 'defaultValue' => null, + ), + ), + ); + $this->assertEquals($expected, $fileConfig); + } + + public function testAddConfigValuesFromPluginSettings() + { + $settings = new SystemSettings(); + + $configValues = $this->configReader->addConfigValuesFromSystemSettings(array(), array($settings)); + + $expected = array ( + 'ExampleSettingsPlugin' => + array ( + 'metric' => + array ( + 'value' => null, + 'description' => 'Choose the metric that should be displayed in the browser tab', + 'isCustomValue' => false, + 'defaultValue' => 'nb_visits', + ), + 'browsers' => + array ( + 'value' => null, + 'description' => 'The value will be only displayed in the following browsers', + 'isCustomValue' => false, + 'defaultValue' => + array ( + 0 => 'firefox', + 1 => 'chromium', + 2 => 'safari', + ), + ), + 'description' => + array ( + 'value' => null, + 'description' => 'This description will be displayed next to the value', + 'isCustomValue' => false, + 'defaultValue' => 'This is the value: +Another line', + ), + 'password' => + array ( + 'value' => null, + 'description' => 'Password for the 3rd API where we fetch the value', + 'isCustomValue' => false, + 'defaultValue' => null, + ), + ), + ); + $this->assertEquals($expected, $configValues); + } + + public function testAddConfigValuesFromPluginSettingsShouldAddDescriptionAndDefaultValueForExistingConfigValues() + { + $settings = new SystemSettings(); + + $existing = array( + 'ExampleSettingsPlugin' => + array ( + 'metric' => + array ( + 'value' => null, + 'description' => '', + 'isCustomValue' => false, + 'defaultValue' => null, + ), + ), + ); + + $configValues = $this->configReader->addConfigValuesFromSystemSettings($existing, array($settings)); + + $this->assertSame('Choose the metric that should be displayed in the browser tab', $configValues['ExampleSettingsPlugin']['metric']['description']); + $this->assertSame('nb_visits', $configValues['ExampleSettingsPlugin']['metric']['defaultValue']); + } + + public function testAddConfigValuesFromPluginSettingsShouldMaskValueIfTypeIsPassword() + { + $settings = new SystemSettings(); + $settings->metric->configureField()->uiControl = FieldConfig::UI_CONTROL_PASSWORD; + + $existing = array( + 'ExampleSettingsPlugin' => + array ( + 'metric' => + array ( + 'value' => 'test', + 'description' => '', + 'isCustomValue' => false, + 'defaultValue' => null, + ), + ), + ); + + $configValues = $this->configReader->addConfigValuesFromSystemSettings($existing, array($settings)); + + $this->assertSame('******', $configValues['ExampleSettingsPlugin']['metric']['value']); + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess(), + ); + } + + private function configPath($file) + { + return PIWIK_INCLUDE_PATH . '/tests/resources/Config/' . $file; + } +} diff --git a/plugins/Diagnostics/tests/Integration/Diagnostic/ArchiveInvalidationsInformationalTest.php b/plugins/Diagnostics/tests/Integration/Diagnostic/ArchiveInvalidationsInformationalTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e3a2ebed3a40cdafbaecd3efb1e7a5f343d2c619 --- /dev/null +++ b/plugins/Diagnostics/tests/Integration/Diagnostic/ArchiveInvalidationsInformationalTest.php @@ -0,0 +1,86 @@ +insertInvalidations([ + ['idsite' => '1', 'name' => 'done', 'date1' => '2020-01-13', 'date2' => '2020-01-13', 'period' => '1', 'report' => '', 'ts_started' => '2020-01-13 02:00:00', 'ts_invalidated' => '2020-01-12 02:00:00', 'status' => 1], + ['idsite' => '1', 'name' => 'done.MyPlugin', 'date1' => '2020-01-13', 'date2' => '2020-01-19', 'period' => '2', 'report' => '', 'ts_started' => null, 'ts_invalidated' => '2020-01-12 06:00:00', 'status' => 0], + ['idsite' => '1', 'name' => 'done' . $segmentHash, 'date1' => '2020-01-01', 'date2' => '2020-01-31', 'period' => '3', 'report' => '', 'ts_started' => null, 'ts_invalidated' => '2020-01-12 04:00:00', 'status' => 0], + ['idsite' => '2', 'name' => 'done' . $segmentHash . '.MyPlugin', 'date1' => '2020-01-15', 'date2' => '2020-01-15', 'period' => '1', 'report' => '', 'ts_started' => '2020-01-14 02:00:00', 'ts_invalidated' => '2020-01-12 03:00:00', 'status' => 1], + ['idsite' => '2', 'name' => 'done.MyOtherPlugin', 'date1' => '2020-01-15', 'date2' => '2020-01-15', 'period' => '1', 'report' => '', 'ts_started' => null, 'ts_invalidated' => '2020-01-12 12:00:00', 'status' => 0], + ['idsite' => '3', 'name' => 'done' . $anotherSegmentHash, 'date1' => '2020-01-06', 'date2' => '2020-01-12', 'period' => '2', 'report' => '', 'ts_started' => null, 'ts_invalidated' => '2020-01-12 02:30:00', 'status' => 0], + ['idsite' => '3', 'name' => 'done' . $anotherSegmentHash . '.AThirdPlugin', 'date1' => '2020-01-01', 'date2' => '2020-01-31', 'period' => '3', 'report' => 'aReport', 'ts_started' => '2020-01-13 07:00:00', 'ts_invalidated' => '2020-01-11 02:00:00', 'status' => 1], + ['idsite' => '3', 'name' => 'done', 'date1' => '2020-02-06', 'date2' => '2020-02-06', 'period' => '1', 'report' => '', 'ts_started' => null, 'ts_invalidated' => '2020-01-10 02:00:00', 'status' => 0], + ['idsite' => '3', 'name' => 'done', 'date1' => '2020-01-16', 'date2' => '2020-01-16', 'period' => '1', 'report' => '', 'ts_started' => null, 'ts_invalidated' => '2020-01-20 02:00:00', 'status' => 0], + ['idsite' => '3', 'name' => 'done' . $segmentHash, 'date1' => '2020-01-16', 'date2' => '2020-01-16', 'period' => '1', 'report' => '', 'ts_started' => null, 'ts_invalidated' => '2020-01-12 02:00:00', 'status' => 0], + ]); + + $diagnostic = new ArchiveInvalidationsInformational(StaticContainer::get(Translator::class)); + + $counts = $diagnostic->getInvalidationCounts(); + $this->assertEquals([ + 0 => '7', + 1 => '3', + 'all' => 10, + ], $counts); + + $minMaxes = $diagnostic->getInvalidationMinMaxes(); + $this->assertEquals([ + 'min_ts_started' => '2020-01-13 02:00:00', + 'max_ts_started' => '2020-01-14 02:00:00', + 'min_ts_invalidated' => '2020-01-10 02:00:00', + 'max_ts_invalidated' => '2020-01-20 02:00:00', + ], $minMaxes); + + $types = $diagnostic->getInvalidationTypes(); + $this->assertEquals([ + 'count_segment' => 5, + 'count_plugin' => 4, + 'plugins' => [ + 'AThirdPlugin', + 'MyOtherPlugin', + 'MyPlugin', + ], + ], $types); + } + + + private function insertInvalidations(array $invalidations) + { + $table = Common::prefixTable('archive_invalidations'); + foreach ($invalidations as $invalidation) { + $sql = "INSERT INTO $table (name, idsite, date1, date2, period, report, status, ts_started, ts_invalidated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + Db::query($sql, [ + $invalidation['name'], + $invalidation['idsite'], + $invalidation['date1'], + $invalidation['date2'], + $invalidation['period'], + $invalidation['report'], + $invalidation['status'], + $invalidation['ts_started'], + $invalidation['ts_invalidated'], + ]); + } + } +} diff --git a/plugins/Diagnostics/tests/Mock/DiagnosticWithError.php b/plugins/Diagnostics/tests/Mock/DiagnosticWithError.php new file mode 100644 index 0000000000000000000000000000000000000000..fb0a2fb3d4eab37fa7ca85730ba56c623ef87eba --- /dev/null +++ b/plugins/Diagnostics/tests/Mock/DiagnosticWithError.php @@ -0,0 +1,23 @@ + { + $('#systemCheckInformational td').each(function () { + let html = $(this).html(); + html = html.replace(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/g, 'DATETIME'); + html = html.replaceAll(directory, '/path/matomo/'); + html = html.replaceAll(directory.replaceAll('/', '\\/'), '\\/path\\/matomo'); + $(this).html(html); + }); + // replace varying invalidation counts with 0 + $('#systemCheckInformational td:contains(nvalidation) + td').each(function () { + let text = $(this).text(); + if (text.match(/^[0-9]+$/)) { + $(this).html($(this).html().replace(/[0-9]+/, '0')); + } + }); + + }, PIWIK_INCLUDE_PATH); + expect(await content.screenshot()).to.matchImage('page'); + }); +}); diff --git a/plugins/Diagnostics/tests/UI/expected-screenshots/Diagnostics_page.png b/plugins/Diagnostics/tests/UI/expected-screenshots/Diagnostics_page.png new file mode 100644 index 0000000000000000000000000000000000000000..3a4e51575770286a389802f48c4196dab20d64d9 Binary files /dev/null and b/plugins/Diagnostics/tests/UI/expected-screenshots/Diagnostics_page.png differ diff --git a/plugins/Diagnostics/tests/Unit/Diagnostic/DiagnosticResultTest.php b/plugins/Diagnostics/tests/Unit/Diagnostic/DiagnosticResultTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0dc0d94fba97d1dc3c5bf0affdc13a770b65554b --- /dev/null +++ b/plugins/Diagnostics/tests/Unit/Diagnostic/DiagnosticResultTest.php @@ -0,0 +1,38 @@ +assertEquals(DiagnosticResult::STATUS_OK, $result->getStatus()); + + $result->addItem(new DiagnosticResultItem(DiagnosticResult::STATUS_WARNING)); + $this->assertEquals(DiagnosticResult::STATUS_WARNING, $result->getStatus()); + + $result->addItem(new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR)); + $this->assertEquals(DiagnosticResult::STATUS_ERROR, $result->getStatus()); + } + + public function testSingleResultShouldReturnAResultWithASingleItem() + { + $result = DiagnosticResult::singleResult('Label', DiagnosticResult::STATUS_ERROR); + + $this->assertInstanceOf('Piwik\Plugins\Diagnostics\Diagnostic\DiagnosticResult', $result); + $this->assertEquals('Label', $result->getLabel()); + $this->assertEquals(DiagnosticResult::STATUS_ERROR, $result->getStatus()); + } +} diff --git a/plugins/Diagnostics/tests/Unit/DiagnosticReportTest.php b/plugins/Diagnostics/tests/Unit/DiagnosticReportTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d7bb644815fd15b087d82b64dfd69e9614abd915 --- /dev/null +++ b/plugins/Diagnostics/tests/Unit/DiagnosticReportTest.php @@ -0,0 +1,51 @@ +assertEquals(1, $report->getErrorCount()); + $this->assertTrue($report->hasErrors()); + $this->assertEquals(1, $report->getWarningCount()); + $this->assertTrue($report->hasWarnings()); + + $report = new DiagnosticReport(array(), array(), array()); + + $this->assertEquals(0, $report->getErrorCount()); + $this->assertFalse($report->hasErrors()); + $this->assertEquals(0, $report->getWarningCount()); + $this->assertFalse($report->hasWarnings()); + } + + public function testGetAllResults() + { + $report = new DiagnosticReport( + array(DiagnosticResult::singleResult('Error', DiagnosticResult::STATUS_ERROR, 'Comment')), + array(DiagnosticResult::singleResult('Warning', DiagnosticResult::STATUS_WARNING, 'Comment')), + array(DiagnosticResult::informationalResult('Test', 'Comment')) + ); + + $this->assertCount(1, $report->getMandatoryDiagnosticResults()); + $this->assertCount(1, $report->getOptionalDiagnosticResults()); + $this->assertCount(1, $report->getInformationalResults()); + $this->assertCount(3, $report->getAllResults()); + } +} diff --git a/plugins/Diagnostics/tests/Unit/DiagnosticServiceTest.php b/plugins/Diagnostics/tests/Unit/DiagnosticServiceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1bc71def5aea86bce70bb108f4448ec4a1657636 --- /dev/null +++ b/plugins/Diagnostics/tests/Unit/DiagnosticServiceTest.php @@ -0,0 +1,46 @@ +runDiagnostics(); + + $results = $report->getAllResults(); + + $this->assertCount(3, $results); + $this->assertEquals('Error', $results[0]->getLabel()); + $this->assertEquals(DiagnosticResult::STATUS_ERROR, $results[0]->getStatus()); + $this->assertEquals('Warning', $results[1]->getLabel()); + $this->assertEquals(DiagnosticResult::STATUS_WARNING, $results[1]->getStatus()); + $this->assertEquals('Success', $results[2]->getLabel()); + $this->assertEquals(DiagnosticResult::STATUS_OK, $results[2]->getStatus()); + } +} diff --git a/plugins/Diagnostics/vue/dist/Diagnostics.umd.js b/plugins/Diagnostics/vue/dist/Diagnostics.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..07223751fcfa05ecd5ee188d1a3be73939d0ec33 --- /dev/null +++ b/plugins/Diagnostics/vue/dist/Diagnostics.umd.js @@ -0,0 +1,316 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("vue")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", ], factory); + else if(typeof exports === 'object') + exports["Diagnostics"] = factory(require("CoreHome"), require("vue")); + else + root["Diagnostics"] = factory(root["CoreHome"], root["Vue"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/Diagnostics/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "ConfigFile", function() { return /* reexport */ ConfigFile; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Diagnostics/vue/src/ConfigFile/ConfigFile.vue?vue&type=template&id=6e46bb32 + +const _hoisted_1 = ["innerHTML"]; +const _hoisted_2 = ["innerHTML"]; +const _hoisted_3 = ["href"]; +const _hoisted_4 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_5 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, null, -1); +const _hoisted_6 = { + class: "diagnostics configfile" +}; +const _hoisted_7 = { + colspan: "3" +}; +const _hoisted_8 = ["name"]; +const _hoisted_9 = { + class: "name" +}; +const _hoisted_10 = ["innerHTML"]; +const _hoisted_11 = { + class: "description" +}; +const _hoisted_12 = ["innerHTML"]; +const _hoisted_13 = { + key: 0 +}; +const _hoisted_14 = { + key: 0 +}; +const _hoisted_15 = ["innerHTML"]; +function render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_Passthrough = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Passthrough"); + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + const _directive_content_table = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("content-table"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, { + "content-title": _ctx.translate('Diagnostics_ConfigFileTitle'), + feature: "true" + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.configFileIntro), + style: { + "margin-right": "3.5px" + } + }, null, 8, _hoisted_1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.translate('Diagnostics_HideUnchanged', '', '')), + onClick: _cache[0] || (_cache[0] = $event => _ctx.onHideUnchanged($event)) + }, null, 8, _hoisted_2)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Diagnostics_Sections')), 1), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.allConfigValues, (values, category) => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_Passthrough, { + key: category + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: `#${category}` + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(category), 9, _hoisted_3), _hoisted_4]), + _: 2 + }, 1024); + }), 128)), _hoisted_5, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("table", _hoisted_6, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tbody", null, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.allConfigValues, (configValues, category) => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_Passthrough, { + key: category + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", _hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + name: category + }, null, 8, _hoisted_8), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(category), 1)])]), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(configValues, (configEntry, key) => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("tr", { + key: key, + class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])({ + 'custom-value': configEntry.isCustomValue + }) + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", _hoisted_9, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(`${key}${configEntry.value !== null && (configEntry.value instanceof Array || typeof configEntry.value === 'object') ? '[]' : ''}`), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", { + class: "value", + innerHTML: _ctx.$sanitize(_ctx.humanReadableValue(configEntry.value)) + }, null, 8, _hoisted_10), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", _hoisted_11, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(configEntry.description) + }, null, 8, _hoisted_12), (configEntry.isCustomValue || configEntry.value === null) && configEntry.defaultValue !== null ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_13, [configEntry.description ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("br", _hoisted_14)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Default')) + ": ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + class: "defaultValue", + innerHTML: _ctx.$sanitize(_ctx.humanReadableValue(configEntry.defaultValue)) + }, null, 8, _hoisted_15)])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)])], 2)), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], configEntry.isCustomValue || !_ctx.hideGlobalConfigValues]]); + }), 128))]), + _: 2 + }, 1024); + }), 128))])])), [[_directive_content_table]])]), + _: 1 + }, 8, ["content-title"]); +} +// CONCATENATED MODULE: ./plugins/Diagnostics/vue/src/ConfigFile/ConfigFile.vue?vue&type=template&id=6e46bb32 + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Diagnostics/vue/src/ConfigFile/ConfigFile.vue?vue&type=script&lang=ts + + +/* harmony default export */ var ConfigFilevue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + allConfigValues: { + type: Object, + required: true + }, + configFilePath: { + type: String, + required: true + } + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"], + Passthrough: external_CoreHome_["Passthrough"] + }, + directives: { + ContentTable: external_CoreHome_["ContentTable"] + }, + data() { + return { + hideGlobalConfigValues: false + }; + }, + methods: { + humanReadableValue(value) { + if (value === false) { + return 'false'; + } + if (value === true) { + return 'true'; + } + if (value === null) { + return ''; + } + if (value === '') { + return '\'\''; + } + if (typeof value === 'object' && Object.keys(value).length === 0) { + return '[]'; + } + if (typeof value === 'object' && Object.keys(value).length > 0) { + return `
    ${JSON.stringify(value, null, 4)}
    `; + } + return external_CoreHome_["Matomo"].helper.htmlEntities(`${value}`); + }, + onHideUnchanged(event) { + if (event.target.tagName !== 'A') { + return; + } + this.hideGlobalConfigValues = !this.hideGlobalConfigValues; + } + }, + computed: { + configFileIntro() { + return Object(external_CoreHome_["translate"])('Diagnostics_ConfigFileIntroduction', `"${external_CoreHome_["Matomo"].helper.htmlEntities(this.configFilePath)}"`); + } + } +})); +// CONCATENATED MODULE: ./plugins/Diagnostics/vue/src/ConfigFile/ConfigFile.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/Diagnostics/vue/src/ConfigFile/ConfigFile.vue + + + +ConfigFilevue_type_script_lang_ts.render = render + +/* harmony default export */ var ConfigFile = (ConfigFilevue_type_script_lang_ts); +// CONCATENATED MODULE: ./plugins/Diagnostics/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/ + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=Diagnostics.umd.js.map \ No newline at end of file diff --git a/plugins/Diagnostics/vue/dist/Diagnostics.umd.min.js b/plugins/Diagnostics/vue/dist/Diagnostics.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..d58c42c9c978a33b8214cf0c49b7d8738ea32251 --- /dev/null +++ b/plugins/Diagnostics/vue/dist/Diagnostics.umd.min.js @@ -0,0 +1,8 @@ +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue")):"function"===typeof define&&define.amd?define(["CoreHome"],t):"object"===typeof exports?exports["Diagnostics"]=t(require("CoreHome"),require("vue")):e["Diagnostics"]=t(e["CoreHome"],e["Vue"])})("undefined"!==typeof self?self:this,(function(e,t){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var l=t[o]={i:o,l:!1,exports:{}};return e[o].call(l.exports,l,l.exports,n),l.l=!0,l.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var l in e)n.d(o,l,function(t){return e[t]}.bind(null,l));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="plugins/Diagnostics/vue/dist/",n(n.s="fae3")}({"19dc":function(t,n){t.exports=e},"8bbf":function(e,n){e.exports=t},fae3:function(e,t,n){"use strict";if(n.r(t),n.d(t,"ConfigFile",(function(){return k})),"undefined"!==typeof window){var o=window.document.currentScript,l=o&&o.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);l&&(n.p=l[1])}var c=n("8bbf");const r=["innerHTML"],a=["innerHTML"],i=["href"],u=Object(c["createElementVNode"])("br",null,null,-1),s=Object(c["createElementVNode"])("p",null,null,-1),b={class:"diagnostics configfile"},d={colspan:"3"},f=["name"],j={class:"name"},p=["innerHTML"],O={class:"description"},m=["innerHTML"],g={key:0},h={key:0},y=["innerHTML"];function V(e,t,n,o,l,V){const v=Object(c["resolveComponent"])("Passthrough"),C=Object(c["resolveComponent"])("ContentBlock"),k=Object(c["resolveDirective"])("content-table");return Object(c["openBlock"])(),Object(c["createBlock"])(C,{"content-title":e.translate("Diagnostics_ConfigFileTitle"),feature:"true"},{default:Object(c["withCtx"])(()=>[Object(c["createElementVNode"])("p",null,[Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(e.configFileIntro),style:{"margin-right":"3.5px"}},null,8,r),Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(e.translate("Diagnostics_HideUnchanged","","")),onClick:t[0]||(t[0]=t=>e.onHideUnchanged(t))},null,8,a)]),Object(c["createElementVNode"])("h3",null,Object(c["toDisplayString"])(e.translate("Diagnostics_Sections")),1),(Object(c["openBlock"])(!0),Object(c["createElementBlock"])(c["Fragment"],null,Object(c["renderList"])(e.allConfigValues,(e,t)=>(Object(c["openBlock"])(),Object(c["createBlock"])(v,{key:t},{default:Object(c["withCtx"])(()=>[Object(c["createElementVNode"])("a",{href:"#"+t},Object(c["toDisplayString"])(t),9,i),u]),_:2},1024))),128)),s,Object(c["withDirectives"])((Object(c["openBlock"])(),Object(c["createElementBlock"])("table",b,[Object(c["createElementVNode"])("tbody",null,[(Object(c["openBlock"])(!0),Object(c["createElementBlock"])(c["Fragment"],null,Object(c["renderList"])(e.allConfigValues,(t,n)=>(Object(c["openBlock"])(),Object(c["createBlock"])(v,{key:n},{default:Object(c["withCtx"])(()=>[Object(c["createElementVNode"])("tr",null,[Object(c["createElementVNode"])("td",d,[Object(c["createElementVNode"])("a",{name:n},null,8,f),Object(c["createElementVNode"])("h3",null,Object(c["toDisplayString"])(n),1)])]),(Object(c["openBlock"])(!0),Object(c["createElementBlock"])(c["Fragment"],null,Object(c["renderList"])(t,(t,n)=>Object(c["withDirectives"])((Object(c["openBlock"])(),Object(c["createElementBlock"])("tr",{key:n,class:Object(c["normalizeClass"])({"custom-value":t.isCustomValue})},[Object(c["createElementVNode"])("td",j,Object(c["toDisplayString"])(`${n}${null!==t.value&&(t.value instanceof Array||"object"===typeof t.value)?"[]":""}`),1),Object(c["createElementVNode"])("td",{class:"value",innerHTML:e.$sanitize(e.humanReadableValue(t.value))},null,8,p),Object(c["createElementVNode"])("td",O,[Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(t.description)},null,8,m),!t.isCustomValue&&null!==t.value||null===t.defaultValue?Object(c["createCommentVNode"])("",!0):(Object(c["openBlock"])(),Object(c["createElementBlock"])("span",g,[t.description?(Object(c["openBlock"])(),Object(c["createElementBlock"])("br",h)):Object(c["createCommentVNode"])("",!0),Object(c["createTextVNode"])(" "+Object(c["toDisplayString"])(e.translate("General_Default"))+": ",1),Object(c["createElementVNode"])("span",{class:"defaultValue",innerHTML:e.$sanitize(e.humanReadableValue(t.defaultValue))},null,8,y)]))])],2)),[[c["vShow"],t.isCustomValue||!e.hideGlobalConfigValues]])),128))]),_:2},1024))),128))])])),[[k]])]),_:1},8,["content-title"])}var v=n("19dc"),C=Object(c["defineComponent"])({props:{allConfigValues:{type:Object,required:!0},configFilePath:{type:String,required:!0}},components:{ContentBlock:v["ContentBlock"],Passthrough:v["Passthrough"]},directives:{ContentTable:v["ContentTable"]},data(){return{hideGlobalConfigValues:!1}},methods:{humanReadableValue(e){return!1===e?"false":!0===e?"true":null===e?"":""===e?"''":"object"===typeof e&&0===Object.keys(e).length?"[]":"object"===typeof e&&Object.keys(e).length>0?`
    ${JSON.stringify(e,null,4)}
    `:v["Matomo"].helper.htmlEntities(""+e)},onHideUnchanged(e){"A"===e.target.tagName&&(this.hideGlobalConfigValues=!this.hideGlobalConfigValues)}},computed:{configFileIntro(){return Object(v["translate"])("Diagnostics_ConfigFileIntroduction",`"${v["Matomo"].helper.htmlEntities(this.configFilePath)}"`)}}});C.render=V;var k=C; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/}})})); +//# sourceMappingURL=Diagnostics.umd.min.js.map \ No newline at end of file diff --git a/plugins/Diagnostics/vue/dist/umd.metadata.json b/plugins/Diagnostics/vue/dist/umd.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..9ecfcc0456204cf8e8e80416ebc95ef42d4fbd28 --- /dev/null +++ b/plugins/Diagnostics/vue/dist/umd.metadata.json @@ -0,0 +1,5 @@ +{ + "dependsOn": [ + "CoreHome" + ] +} \ No newline at end of file diff --git a/plugins/Diagnostics/vue/src/ConfigFile/ConfigFile.vue b/plugins/Diagnostics/vue/src/ConfigFile/ConfigFile.vue new file mode 100644 index 0000000000000000000000000000000000000000..a5ec688588253101cb0cf9bb25d061c3a9e26f1f --- /dev/null +++ b/plugins/Diagnostics/vue/src/ConfigFile/ConfigFile.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/plugins/Diagnostics/vue/src/index.ts b/plugins/Diagnostics/vue/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8e375299dbbad5d5a76ac88339cda88f9cb4c65 --- /dev/null +++ b/plugins/Diagnostics/vue/src/index.ts @@ -0,0 +1,8 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/ + +export { default as ConfigFile } from './ConfigFile/ConfigFile.vue'; diff --git a/plugins/Ecommerce/Categories/EcommerceCategory.php b/plugins/Ecommerce/Categories/EcommerceCategory.php new file mode 100644 index 0000000000000000000000000000000000000000..dccf8d8a291a5d8c0b1c06c983f1732f3df54567 --- /dev/null +++ b/plugins/Ecommerce/Categories/EcommerceCategory.php @@ -0,0 +1,19 @@ +' . Piwik::translate('Ecommerce_EcommerceLogSubcategoryHelp1') . '

    ' + . '

    ' . Piwik::translate('Ecommerce_EcommerceLogSubcategoryHelp2') . '

    '; + } +} diff --git a/plugins/Ecommerce/Categories/EcommerceOverviewSubcategory.php b/plugins/Ecommerce/Categories/EcommerceOverviewSubcategory.php new file mode 100644 index 0000000000000000000000000000000000000000..16884efa4b0f400e2dba181cdbe7f11e0a88de44 --- /dev/null +++ b/plugins/Ecommerce/Categories/EcommerceOverviewSubcategory.php @@ -0,0 +1,29 @@ +' . Piwik::translate('Ecommerce_EcommerceOverviewSubcategoryHelp1') . '

    ' + . '

    ' . Piwik::translate('Ecommerce_EcommerceOverviewSubcategoryHelp2') . '

    ' + . '

    ' . Url::getExternalLinkTag('https://matomo.org/docs/ecommerce-analytics/', null, null, 'App.Ecommerce.Overview') + . Piwik::translate('Ecommerce_EcommerceOverviewSubcategoryHelp3') . '

    '; + } +} diff --git a/plugins/Ecommerce/Categories/ProductSubcategory.php b/plugins/Ecommerce/Categories/ProductSubcategory.php new file mode 100644 index 0000000000000000000000000000000000000000..fa3b07a7f0810410901e69ed1571901ef9e7cc44 --- /dev/null +++ b/plugins/Ecommerce/Categories/ProductSubcategory.php @@ -0,0 +1,25 @@ +' . Piwik::translate('Ecommerce_ProductSubcategoryHelp') . '

    '; + } +} diff --git a/plugins/Ecommerce/Categories/SalesSubcategory.php b/plugins/Ecommerce/Categories/SalesSubcategory.php new file mode 100644 index 0000000000000000000000000000000000000000..d1a1aa4db0a4c86adcb713b8baac957b63fa6904 --- /dev/null +++ b/plugins/Ecommerce/Categories/SalesSubcategory.php @@ -0,0 +1,26 @@ +' . Piwik::translate('Ecommerce_SalesSubcategoryHelp1') . '

    ' + . '

    ' . Piwik::translate('Ecommerce_SalesSubcategoryHelp2') . '

    '; + } +} diff --git a/plugins/Ecommerce/Columns/BaseConversion.php b/plugins/Ecommerce/Columns/BaseConversion.php new file mode 100644 index 0000000000000000000000000000000000000000..a4e0d1bcca6d7a0e9453ec4a8a92fed954566c51 --- /dev/null +++ b/plugins/Ecommerce/Columns/BaseConversion.php @@ -0,0 +1,48 @@ + GoalManager::MAX_ALLOWED_REVENUE) { + StaticContainer::get(LoggerInterface::class)->debug( + "Ecommerce value ({$revenue}) exceeds the allowed maximum of " . GoalManager::MAX_ALLOWED_REVENUE . " and was rejected (treated as not set)." + ); + + return false; + } + + if (round($revenue) == $revenue) { + return $revenue; + } + + $value = round($revenue, GoalManager::REVENUE_PRECISION); + + return $value; + } +} diff --git a/plugins/Ecommerce/Columns/Items.php b/plugins/Ecommerce/Columns/Items.php new file mode 100644 index 0000000000000000000000000000000000000000..6ee1f7490fed1dd45c0f2d2419b59e58dd05189d --- /dev/null +++ b/plugins/Ecommerce/Columns/Items.php @@ -0,0 +1,18 @@ +createMetric(ArchivedMetric::AGGREGATION_UNIQUE); + $metricsList->addMetric($metric); + } + + public function getDbDiscriminator() + { + return new Discriminator($this->dbTableName, 'idgoal', GoalManager::IDGOAL_ORDER); + } +} diff --git a/plugins/Ecommerce/Columns/ProductCategory.php b/plugins/Ecommerce/Columns/ProductCategory.php new file mode 100644 index 0000000000000000000000000000000000000000..beb8ba4e41cf721de70cc2e59ad0ff96dcc70fc6 --- /dev/null +++ b/plugins/Ecommerce/Columns/ProductCategory.php @@ -0,0 +1,80 @@ +getProductCategorySegments(self::PRODUCT_CATEGORY_COUNT); + + // add individual productCategoryN segments for use as a union (these segments are not available through the UI/API) + foreach ($individualProductCategorySegments as $i => $productCategoryName) { + $productCategoryColumnName = 'idaction_category'; + if ($i > 0) { + $productCategoryColumnName .= $i + 1; + } + + $segment = new Segment(); + $segment->setCategory($this->category); + $segment->setType('dimension'); + $segment->setName($this->getName() . ' ' . ($i + 1)); + $segment->setSegment($productCategoryName); + $segment->setSqlFilter([TableLogAction::class, 'getOptimizedIdActionSqlMatch']); + $segment->setSqlSegment('log_conversion_item.' . $productCategoryColumnName); + $segment->setIsInternal(true); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + // add a union of these individual columns as productCategory + $segment = new Segment(); + $segment->setCategory($this->category); + $segment->setType('dimension'); + $segment->setSegment('productCategory'); + $segment->setName($this->getName()); + $segment->setUnionOfSegments($individualProductCategorySegments); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + private function getProductCategorySegments($categoryCount) + { + $result = []; + for ($i = 0; $i < $categoryCount; ++$i) { + $segmentName = 'productCategory' . ($i + 1); + $result[] = $segmentName; + } + return $result; + } +} diff --git a/plugins/Ecommerce/Columns/ProductName.php b/plugins/Ecommerce/Columns/ProductName.php new file mode 100644 index 0000000000000000000000000000000000000000..e1ac70ab5ce52fd570864561b1fd8d971d562ac5 --- /dev/null +++ b/plugins/Ecommerce/Columns/ProductName.php @@ -0,0 +1,38 @@ +categoryNumber; + } + + public function configureSegments(SegmentsList $segmentsList, DimensionSegmentFactory $dimensionSegmentFactory) + { + $individualProductCategorySegments = $this->getProductCategorySegments(ProductCategory::PRODUCT_CATEGORY_COUNT); + + // add individual productCategoryN segments for use as a union (these segments are not available through the UI/API) + foreach ($individualProductCategorySegments as $i => $productCategoryName) { + $productCategoryColumnName = 'idaction_product_cat'; + if ($i > 0) { + $productCategoryColumnName .= $i + 1; + } + + $segment = new Segment(); + $segment->setCategory($this->category); + $segment->setType('dimension'); + $segment->setName(Piwik::translate('Ecommerce_ViewedProductCategory') . ' ' . ($i + 1)); + $segment->setSegment($productCategoryName); + $segment->setSqlFilter([TableLogAction::class, 'getOptimizedIdActionSqlMatch']); + $segment->setSqlSegment('log_link_visit_action.' . $productCategoryColumnName); + $segment->setIsInternal(true); + $segment->setSuggestedValuesCallback(function ($idSite, $maxValuesToReturn, $table) { + $values = []; + foreach ($table->getRows() as $row) { + foreach ($row->getColumn('actionDetails') as $actionRow) { + if (isset($actionRow['productViewCategories'])) { + $values = array_merge($values, $actionRow['productViewCategories']); + } + } + } + return $values; + }); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + // add a union of these individual columns as productCategory + $segment = new Segment(); + $segment->setCategory($this->category); + $segment->setType('dimension'); + $segment->setSegment('productViewCategory'); + $segment->setName(Piwik::translate('Ecommerce_ViewedProductCategory')); + $segment->setUnionOfSegments($individualProductCategorySegments); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + private function getProductCategorySegments($categoryCount) + { + $result = []; + for ($i = 0; $i < $categoryCount; ++$i) { + $segmentName = 'productViewCategory' . ($i + 1); + $result[] = $segmentName; + } + return $result; + } + + public function getDbColumnJoin() + { + return new ActionNameJoin(); + } + + public function getDbDiscriminator() + { + return new Discriminator('log_action', 'type', Action::TYPE_ECOMMERCE_ITEM_CATEGORY); + } + + public function onLookupAction(Request $request, Action $action) + { + if ($request->hasParam('_pkc')) { + $categories = Common::unsanitizeInputValue($request->getParam('_pkc')); + $categories = $this->handleCategoryParam($categories); + + return $categories[$this->categoryNumber - 1] ?? false; + } + + // fall back to custom variables (might happen if old logs are replayed) + if (Manager::getInstance()->isPluginActivated('CustomVariables')) { + $customVariables = CustomVariablesRequestProcessor::getCustomVariablesInPageScope($request); + if (isset($customVariables['custom_var_k5']) && $customVariables['custom_var_k5'] === '_pkc') { + $categories = $this->handleCategoryParam($customVariables['custom_var_v5'] ?? ''); + + return $categories[$this->categoryNumber - 1] ?? false; + } + } + + return parent::onLookupAction($request, $action); + } + + protected function handleCategoryParam($categories) + { + if (0 === strpos($categories, '["')) { + $categories = array_values(array_filter((array) @\json_decode($categories, true))); + } else { + $categories = [$categories]; + } + + return $categories; + } + + public function getActionId() + { + return Action::TYPE_ECOMMERCE_ITEM_CATEGORY; + } +} diff --git a/plugins/Ecommerce/Columns/ProductViewCategory2.php b/plugins/Ecommerce/Columns/ProductViewCategory2.php new file mode 100644 index 0000000000000000000000000000000000000000..9a8e407ad9c7aca34343b6cd73dcb2f339ee839b --- /dev/null +++ b/plugins/Ecommerce/Columns/ProductViewCategory2.php @@ -0,0 +1,24 @@ +hasParam('_pkn')) { + return Common::unsanitizeInputValue($request->getParam('_pkn')); + } + + // fall back to custom variables (might happen if old logs are replayed) + if (Manager::getInstance()->isPluginActivated('CustomVariables')) { + $customVariables = CustomVariablesRequestProcessor::getCustomVariablesInPageScope($request); + if (isset($customVariables['custom_var_k4']) && $customVariables['custom_var_k4'] === '_pkn') { + return $customVariables['custom_var_v4'] ?? false; + } + } + + return parent::onLookupAction($request, $action); + } + + public function getActionId() + { + return Action::TYPE_ECOMMERCE_ITEM_NAME; + } +} diff --git a/plugins/Ecommerce/Columns/ProductViewPrice.php b/plugins/Ecommerce/Columns/ProductViewPrice.php new file mode 100644 index 0000000000000000000000000000000000000000..26dbb0229c9b48affb3ef2e893f3fc6aa193fd2e --- /dev/null +++ b/plugins/Ecommerce/Columns/ProductViewPrice.php @@ -0,0 +1,45 @@ +getParam('_pkp'); + if (is_numeric($price)) { + return $price; + } + + // fall back to custom variables (might happen if old logs are replayed) + if (Manager::getInstance()->isPluginActivated('CustomVariables')) { + $customVariables = CustomVariablesRequestProcessor::getCustomVariablesInPageScope($request); + if (isset($customVariables['custom_var_k2']) && $customVariables['custom_var_k2'] === '_pkp') { + return $customVariables['custom_var_v2'] ?? false; + } + } + + return parent::onNewAction($request, $visitor, $action); + } +} diff --git a/plugins/Ecommerce/Columns/ProductViewSku.php b/plugins/Ecommerce/Columns/ProductViewSku.php new file mode 100644 index 0000000000000000000000000000000000000000..dbdbdd0533b100a86b8f8294570d6410b63f9adb --- /dev/null +++ b/plugins/Ecommerce/Columns/ProductViewSku.php @@ -0,0 +1,63 @@ +hasParam('_pks')) { + return Common::unsanitizeInputValue($request->getParam('_pks')); + } + + // fall back to custom variables (might happen if old logs are replayed) + if (Manager::getInstance()->isPluginActivated('CustomVariables')) { + $customVariables = CustomVariablesRequestProcessor::getCustomVariablesInPageScope($request); + if (isset($customVariables['custom_var_k3']) && $customVariables['custom_var_k3'] === '_pks') { + return $customVariables['custom_var_v3'] ?? false; + } + } + + return parent::onLookupAction($request, $action); + } + + public function getActionId() + { + return Action::TYPE_ECOMMERCE_ITEM_SKU; + } +} diff --git a/plugins/Ecommerce/Columns/Revenue.php b/plugins/Ecommerce/Columns/Revenue.php new file mode 100644 index 0000000000000000000000000000000000000000..3ce7b78671d1bd09a5cc2147fa00031270288c28 --- /dev/null +++ b/plugins/Ecommerce/Columns/Revenue.php @@ -0,0 +1,134 @@ +dbTableName, 'idgoal', GoalManager::IDGOAL_ORDER); + } + + + public function configureSegments(SegmentsList $segmentsList, DimensionSegmentFactory $dimensionSegmentFactory) + { + //new Segment revenue on order + $segment = new Segment(); + $segment->setCategory($this->category); + $segment->setName(Piwik::translate('Ecommerce_OrderRevenue')); + $segment->setSegment('revenueOrder'); + $segment->setSqlSegment('log_conversion.idvisit'); + $segment->setSqlFilter(function ($valueToMatch, $sqlField, $matchType) { + return $this->getRevenueQuery($valueToMatch, $sqlField, $matchType, 0); + }); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + + //new Segment revenue left in cart + $segment = new Segment(); + $segment->setCategory($this->category); + $segment->setName(Piwik::translate('Ecommerce_RevenueLeftInCart')); + $segment->setSegment('revenueAbandonedCart'); + $segment->setSqlSegment('log_conversion.idvisit'); + $segment->setSqlFilter(function ($valueToMatch, $sqlField, $matchType) { + return $this->getRevenueQuery($valueToMatch, $sqlField, $matchType, -1); + }); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + /** + * revenue sql filter + * @param $valueToMatch + * @param $sqlField + * @param $matchType + * @return array + */ + private function getRevenueQuery($valueToMatch, $sqlField, $matchType, $idGoal) + { + //supported operator type + $supportType = [ + SegmentExpression::MATCH_EQUAL, + SegmentExpression::MATCH_GREATER_OR_EQUAL, + SegmentExpression::MATCH_LESS_OR_EQUAL, + SegmentExpression::MATCH_GREATER, + SegmentExpression::MATCH_LESS, + ]; + + if (!in_array($matchType, $supportType)) { + throw new \Exception("This match type $matchType is not available for action-segments."); + } + + //to fit mysql operator + if ($matchType === SegmentExpression::MATCH_EQUAL) { + $matchType = "="; + } + + $table = Common::prefixTable($this->dbTableName); + $sql = " SELECT idvisit from {$table} WHERE (idgoal = {$idGoal} and revenue {$matchType} ?) "; + return [ + 'SQL' => $sql, + 'bind' => (float)$valueToMatch, + ]; + } + + + /** + * @param Action|null $action + * + * @return mixed|false + */ + public function onGoalConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager) + { + $defaultRevenue = $goalManager->getGoalColumn('revenue'); + $revenue = $request->getGoalRevenue($defaultRevenue); + + return $this->roundRevenueIfNeeded($revenue); + } + + /** + * @param Action|null $action + * + * @return mixed|false + */ + public function onEcommerceOrderConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager) + { + $defaultRevenue = 0; + $revenue = $request->getGoalRevenue($defaultRevenue); + + return $this->roundRevenueIfNeeded($revenue); + } + + /** + * @param Action|null $action + * + * @return mixed|false + */ + public function onEcommerceCartUpdateConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager) + { + return $this->onEcommerceOrderConversion($request, $visitor, $action, $goalManager); + } +} diff --git a/plugins/Ecommerce/Columns/RevenueDiscount.php b/plugins/Ecommerce/Columns/RevenueDiscount.php new file mode 100644 index 0000000000000000000000000000000000000000..cb244867cb386a5c1a3a9d1c5310b353c31e8cab --- /dev/null +++ b/plugins/Ecommerce/Columns/RevenueDiscount.php @@ -0,0 +1,33 @@ +roundRevenueIfNeeded($request->getParam('ec_dt')); + } +} diff --git a/plugins/Ecommerce/Columns/RevenueShipping.php b/plugins/Ecommerce/Columns/RevenueShipping.php new file mode 100644 index 0000000000000000000000000000000000000000..241a281882f28a43d21d8f8593f06438bb4e110b --- /dev/null +++ b/plugins/Ecommerce/Columns/RevenueShipping.php @@ -0,0 +1,33 @@ +roundRevenueIfNeeded($request->getParam('ec_sh')); + } +} diff --git a/plugins/Ecommerce/Columns/RevenueSubtotal.php b/plugins/Ecommerce/Columns/RevenueSubtotal.php new file mode 100644 index 0000000000000000000000000000000000000000..ac34063cb175c340b62ca0ad4ad21b081b1a0b89 --- /dev/null +++ b/plugins/Ecommerce/Columns/RevenueSubtotal.php @@ -0,0 +1,33 @@ +roundRevenueIfNeeded($request->getParam('ec_st')); + } +} diff --git a/plugins/Ecommerce/Columns/RevenueTax.php b/plugins/Ecommerce/Columns/RevenueTax.php new file mode 100644 index 0000000000000000000000000000000000000000..b56f6bdc2a62cf3bef81da39640e40d70215213c --- /dev/null +++ b/plugins/Ecommerce/Columns/RevenueTax.php @@ -0,0 +1,33 @@ +roundRevenueIfNeeded($request->getParam('ec_tx')); + } +} diff --git a/plugins/Ecommerce/Controller.php b/plugins/Ecommerce/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..268bc8c6bcb56d78018293ffebd862d6944bdc39 --- /dev/null +++ b/plugins/Ecommerce/Controller.php @@ -0,0 +1,150 @@ +renderSharedSparklineView( + Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER, + $allowMultiple = true, + [ + 'nb_conversions' => Piwik::translate('General_EcommerceOrders'), + 'nb_visits_converted' => Piwik::translate('General_NVisits'), + 'conversion_rate' => Piwik::translate('Goals_ConversionRate', Piwik::translate('General_EcommerceOrders')), + 'revenue' => Piwik::translate('General_TotalRevenue'), + 'items' => Piwik::translate('General_PurchasedProducts'), + 'avg_order_revenue' => Piwik::translate('General_AverageOrderValue'), + ], + [ + ['items', 40], + ['avg_order_revenue', 50], + ] + ); + + $content .= $this->renderSharedSparklineView( + Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART, + $allowMultiple = false, + [ + 'nb_conversions' => Piwik::translate('General_AbandonedCarts'), + 'conversion_rate' => Piwik::translate('Goals_ConversionRate', Piwik::translate('Goals_AbandonedCart')), + 'revenue' => Piwik::translate('Ecommerce_RevenueLeftInCart'), + ] + ); + + return $content; + } + + /** + * Renders Ecommerce sparklines using the shared CoreVisualizations implementation. + * + * @param array $translations + * @param array, 1: int}> $extraSparklineMetrics + */ + private function renderSharedSparklineView( + string $idGoal, + bool $allowMultiple, + array $translations, + array $extraSparklineMetrics = [] + ): string { + return \Piwik\Context::executeWithQueryParameters([ + 'idSite' => $this->idSite, + 'idGoal' => $idGoal, + 'allow_multiple' => (int) $allowMultiple, + 'only_summary' => 0, + ], function () use ($idGoal, $translations, $extraSparklineMetrics) { + /** @var Sparklines $view */ + $view = ViewDataTableFactory::build(Sparklines::ID, 'Goals.get', 'Ecommerce.' . __METHOD__, true); + $view->config->show_title = false; + $view->config->custom_parameters = [ + 'idGoal' => $idGoal, + ]; + $view->config->addTranslations($translations); + + foreach ($extraSparklineMetrics as [$columns, $order]) { + $view->config->addSparklineMetric($columns, $order); + } + + return $view->render(); + }); + } + + public function getConversionsOverview() + { + $view = new View('@Ecommerce/conversionOverview'); + $idGoal = Common::getRequestVar('idGoal', null, 'string'); + $period = Common::getRequestVar('period', null, 'string'); + $segment = Common::getRequestVar('segment', '', 'string'); + $date = Common::getRequestVar('date', '', 'string'); + + $goalMetrics = Request::processRequest('Goals.get', [ + 'idGoal' => $idGoal, + 'idSite' => $this->idSite, + 'date' => $date, + 'period' => $period, + 'segment' => Common::unsanitizeInputValue($segment), + 'filter_limit' => '-1', + ], $default = []); + + $dataRow = $goalMetrics->getFirstRow(); + + $view->visitorLogEnabled = Manager::getInstance()->isPluginActivated('Live') + && Live::isVisitorLogEnabled($this->idSite); + $view->idSite = $this->idSite; + $view->idGoal = $idGoal; + + if ($dataRow) { + $view->revenue = $dataRow->getColumn('revenue'); + $view->revenue_subtotal = $dataRow->getColumn('revenue_subtotal'); + $view->revenue_tax = $dataRow->getColumn('revenue_tax'); + $view->revenue_shipping = $dataRow->getColumn('revenue_shipping'); + $view->revenue_discount = $dataRow->getColumn('revenue_discount'); + } + + return $view->render(); + } + + public function getEcommerceLog($fetch = false) + { + $saveGET = $_GET; + $originalQuery = $_SERVER['QUERY_STRING']; + + if (!empty($_GET['segment'])) { + $_GET['segment'] = $_GET['segment'] . ';' . 'visitEcommerceStatus!=none'; + } else { + $_GET['segment'] = 'visitEcommerceStatus!=none'; + } + $_SERVER['QUERY_STRING'] = Http::buildQuery($_GET); + + $_GET['widget'] = 1; + $output = FrontController::getInstance()->dispatch('Live', 'getVisitorLog', [$fetch]); + $_GET = $saveGET; + $_SERVER['QUERY_STRING'] = $originalQuery; + + return $output; + } +} diff --git a/plugins/Ecommerce/Ecommerce.php b/plugins/Ecommerce/Ecommerce.php new file mode 100644 index 0000000000000000000000000000000000000000..8ab18f2125b85c836790b7500d1f55dd77394238 --- /dev/null +++ b/plugins/Ecommerce/Ecommerce.php @@ -0,0 +1,105 @@ + 'getClientSideTranslationKeys', + 'Metric.addComputedMetrics' => 'addComputedMetrics', + 'Actions.getCustomActionDimensionFieldsAndJoins' => 'provideActionDimensionFields', + 'Segment.filterSegments' => 'filterSegments', + ]; + } + + public function getClientSideTranslationKeys(&$translations) + { + $translations[] = 'Goals_ConversionsOverview'; + $translations[] = 'General_ColumnRevenue'; + $translations[] = 'General_Subtotal'; + $translations[] = 'General_Tax'; + $translations[] = 'General_Shipping'; + $translations[] = 'General_Discount'; + $translations[] = 'Live_RowActionTooltipWithDimension'; + $translations[] = 'General_Goal'; + } + + public function provideActionDimensionFields(&$fields, &$joins) + { + $fields[] = 'log_link_visit_action.product_price as productViewPrice'; + $fields[] = 'log_action_productview_name.name as productViewName'; + $fields[] = 'log_action_productview_sku.name as productViewSku'; + $joins[] = 'LEFT JOIN ' . Common::prefixTable('log_action') . ' AS log_action_productview_name + ON log_link_visit_action.idaction_product_name = log_action_productview_name.idaction'; + $joins[] = 'LEFT JOIN ' . Common::prefixTable('log_action') . ' AS log_action_productview_sku + ON log_link_visit_action.idaction_product_sku = log_action_productview_sku.idaction'; + + for ($i = 1; $i <= ProductCategory::PRODUCT_CATEGORY_COUNT; $i++) { + $suffix = $i > 1 ? $i : ''; + $fields[] = "log_action_productview_category$i.name as productViewCategory$i"; + $joins[] = "LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_productview_category$i + ON log_link_visit_action.idaction_product_cat$suffix = log_action_productview_category$i.idaction"; + } + } + + public function addComputedMetrics(MetricsList $list, ComputedMetricFactory $computedMetricFactory) + { + $category = 'Goals_Ecommerce'; + + $metrics = $list->getMetrics(); + foreach ($metrics as $metric) { + if ($metric instanceof ArchivedMetric && $metric->getDimension()) { + $metricName = $metric->getName(); + if ( + $metric->getDbTableName() === 'log_conversion' + && $metricName !== 'nb_uniq_orders' + && strpos($metricName, ArchivedMetric::AGGREGATION_SUM_PREFIX) === 0 + && $metric->getCategoryId() === $category + ) { + $metric = $computedMetricFactory->createComputedMetric($metric->getName(), 'nb_uniq_orders', ComputedMetric::AGGREGATION_AVG); + $list->addMetric($metric); + } + } + } + } + + public function filterSegments(SegmentsList &$list, array $idSites) + { + $limitSegmentsSettingEnabled = false; + if (empty($idSites)) { + $limitSegmentsSettingEnabled = LimitSegments::getInstance()->getValue(); + } else { + foreach ($idSites as $idsite) { + $limitSegmentsSettingEnabled |= LimitSegments::getInstance($idsite)->getValue(); + } + } + if ($limitSegmentsSettingEnabled) { + $list->remove('Goals_Ecommerce', 'orderId'); + $list->remove('Goals_Ecommerce', 'revenueOrder'); + $list->remove('Goals_Ecommerce', 'productPrice'); + $list->remove('Goals_Ecommerce', 'productName'); + $list->remove('Goals_Ecommerce', 'productSku'); + } + } +} diff --git a/plugins/Ecommerce/ProfileSummary/EcommerceSummary.php b/plugins/Ecommerce/ProfileSummary/EcommerceSummary.php new file mode 100644 index 0000000000000000000000000000000000000000..5e232674840b2165822886728f9298411c7c0487 --- /dev/null +++ b/plugins/Ecommerce/ProfileSummary/EcommerceSummary.php @@ -0,0 +1,52 @@ +profile['totalEcommerceRevenue'])) { + return ''; + } + + $view = new View('@Ecommerce/_profileSummary.twig'); + $view->idSite = Common::getRequestVar('idSite', null, 'int'); + $view->visitorData = $this->profile; + return $view->render(); + } + + /** + * @inheritdoc + */ + public function getOrder() + { + return 20; + } +} diff --git a/plugins/Ecommerce/Reports/Base.php b/plugins/Ecommerce/Reports/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..87dd0796780f33fb4e47a83434af3dfe29276678 --- /dev/null +++ b/plugins/Ecommerce/Reports/Base.php @@ -0,0 +1,74 @@ +module = 'Goals'; + $this->categoryId = 'Goals_Ecommerce'; + $this->onlineGuideUrl = Url::addCampaignParametersToMatomoLink('https://matomo.org/docs/ecommerce-analytics/'); + } + + public function isEnabled() + { + $idSite = Common::getRequestVar('idSite', false, 'int'); + + if (empty($idSite)) { + return false; + } + + return $this->isEcommerceEnabled($idSite); + } + + public function checkIsEnabled() + { + if (!$this->isEnabled()) { + $message = Piwik::translate('General_ExceptionReportNotEnabled'); + + if (Piwik::hasUserSuperUserAccess()) { + $message .= ' Most likely Ecommerce is not enabled for the requested site.'; + } + + throw new \Exception($message); + } + } + + public function configureReportMetadata(&$availableReports, $infos) + { + if ($this->isEcommerceEnabledByInfos($infos)) { + $availableReports[] = $this->buildReportMetadata(); + } + } + + private function isEcommerceEnabledByInfos(array $infos): bool + { + $idSite = $infos['idSite']; + + if (empty($idSite) || !is_numeric($idSite)) { + return false; + } + + return $this->isEcommerceEnabled((int) $idSite); + } + + private function isEcommerceEnabled(int $idSite): bool + { + $site = new Site($idSite); + return $site->isEcommerceEnabled(); + } +} diff --git a/plugins/Ecommerce/Reports/BaseItem.php b/plugins/Ecommerce/Reports/BaseItem.php new file mode 100644 index 0000000000000000000000000000000000000000..742781507f6da6bf266f63509937fb0703c88ecb --- /dev/null +++ b/plugins/Ecommerce/Reports/BaseItem.php @@ -0,0 +1,173 @@ +processedMetrics = array( + new AveragePrice(), + new AverageQuantity(), + new ProductConversionRate(), + ); + $this->metrics = array( + 'revenue', 'quantity', 'orders', 'nb_visits', + ); + } + + public function getMetrics() + { + $metrics = parent::getMetrics(); + $metrics['revenue'] = Piwik::translate('General_ProductRevenue'); + $metrics['orders'] = Piwik::translate('General_UniquePurchases'); + return $metrics; + } + + public function getMetricsDocumentation() + { + if ($this->isAbandonedCart(false)) { + return [ + 'revenue' => Piwik::translate('Goals_ColumnRevenueLeftInCartDocumentation'), + 'quantity' => Piwik::translate('Goals_ColumnQuantityLeftInCartDocumentation', $this->name), + 'avg_price' => Piwik::translate('Goals_ColumnAveragePriceDocumentation', $this->name), + 'avg_quantity' => Piwik::translate('Goals_ColumnAverageQuantityDocumentation', $this->name), + 'nb_visits' => Piwik::translate('Goals_ColumnVisitsProductDocumentation', $this->name), + 'abandoned_carts' => Piwik::translate('Goals_ColumnAbandonedCartsDocumentation', $this->name), + ]; + } else { + return [ + 'revenue' => Piwik::translate( + 'Goals_ColumnRevenueDocumentation', + Piwik::translate('Goals_DocumentationRevenueGeneratedByProductSales') + ), + 'quantity' => Piwik::translate('Goals_ColumnQuantityDocumentation', $this->name), + 'orders' => Piwik::translate('Goals_ColumnOrdersDocumentation', $this->name), + 'avg_price' => Piwik::translate('Goals_ColumnAveragePriceDocumentation', $this->name), + 'avg_quantity' => Piwik::translate('Goals_ColumnAverageQuantityDocumentation', $this->name), + 'nb_visits' => Piwik::translate('Goals_ColumnVisitsProductDocumentation', $this->name), + 'conversion_rate' => Piwik::translate('Goals_ColumnConversionRateProductDocumentation', $this->name), + ]; + } + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + $widgetsList->addToContainerWidget('Products', $factory->createWidget()); + } + + public function configureView(ViewDataTable $view) + { + $idSite = Common::getRequestVar('idSite'); + + $view->config->show_ecommerce = true; + $view->config->show_table = false; + $view->config->show_all_views_icons = false; + $view->config->show_exclude_low_population = false; + $view->config->show_table_all_columns = false; + + if (!($view instanceof Evolution)) { + $moneyColumns = array('revenue'); + $formatter = array(new Formatter(), 'getPrettyMoney'); + $view->config->filters[] = array('ColumnCallbackReplace', array($moneyColumns, $formatter, array($idSite))); + } + + $view->requestConfig->filter_limit = 10; + $view->requestConfig->filter_sort_column = 'revenue'; + $view->requestConfig->filter_sort_order = 'desc'; + + $view->config->custom_parameters['isFooterExpandedInDashboard'] = true; + + // set columns/translations which differ based on viewDataTable TODO: shouldn't have to do this check... + // amount of reports should be dynamic, but metadata should be static + $columns = array_merge($this->getMetrics(), $this->getProcessedMetrics()); + $columnsOrdered = array('label', 'revenue', 'quantity', 'orders', 'avg_price', 'avg_quantity', + 'nb_visits', 'conversion_rate'); + + // handle old case where viewDataTable is set to ecommerceOrder/ecommerceAbandonedCart. in this case, we + // set abandonedCarts accordingly and remove the ecommerceOrder/ecommerceAbandonedCart as viewDataTable. + $viewDataTable = Common::getRequestVar('viewDataTable', ''); + if ($viewDataTable == 'ecommerceOrder') { + $view->config->custom_parameters['viewDataTable'] = 'table'; + $abandonedCart = false; + } elseif ($viewDataTable == 'ecommerceAbandonedCart') { + $view->config->custom_parameters['viewDataTable'] = 'table'; + $abandonedCart = true; + } else { + $abandonedCart = $this->isAbandonedCart($fetchIfNotSet = true); + } + + if ($abandonedCart) { + $columns['abandoned_carts'] = Piwik::translate('General_AbandonedCarts'); + $columns['revenue'] = Piwik::translate('Goals_LeftInCart', $columns['revenue']); + $columns['quantity'] = Piwik::translate('Goals_LeftInCart', $columns['quantity']); + $columns['avg_quantity'] = Piwik::translate('Goals_LeftInCart', $columns['avg_quantity']); + unset($columns['orders']); + unset($columns['conversion_rate']); + + $columnsOrdered = array('label', 'revenue', 'quantity', 'avg_price', 'avg_quantity', 'nb_visits', + 'abandoned_carts'); + + $view->config->custom_parameters['abandonedCarts'] = '1'; + } else { + $view->config->custom_parameters['abandonedCarts'] = '0'; + } + + $view->requestConfig->request_parameters_to_modify['abandonedCarts'] = $view->config->custom_parameters['abandonedCarts']; + + $translations = array_merge(array('label' => $this->name), $columns); + + $view->config->addTranslations($translations); + $view->config->columns_to_display = $columnsOrdered; + } + + private function isAbandonedCart($fetchIfNotSet) + { + $abandonedCarts = Common::getRequestVar('abandonedCarts', '', 'string'); + + if ($abandonedCarts === '') { + if ($fetchIfNotSet) { + $idSite = Common::getRequestVar('idSite', 0, 'int'); + $period = Common::getRequestVar('period', '', 'string'); + $date = Common::getRequestVar('date', '', 'string'); + + $conversion = new Conversions(); + $conversions = $conversion->getConversionForGoal(Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER, $idSite, $period, $date); + $cartNbConversions = $conversion->getConversionForGoal(Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART, $idSite, $period, $date); + $preloadAbandonedCart = $cartNbConversions !== false && $conversions == 0; + + if ($preloadAbandonedCart) { + $abandonedCarts = '1'; + } else { + $abandonedCarts = '0'; + } + } else { + $abandonedCarts = '0'; + } + } + + return $abandonedCarts == '1'; + } +} diff --git a/plugins/Ecommerce/Reports/GetDaysToConversionAbandonedCart.php b/plugins/Ecommerce/Reports/GetDaysToConversionAbandonedCart.php new file mode 100644 index 0000000000000000000000000000000000000000..19322d4f6ba49075ab8f27928fa2eca7bf8b5667 --- /dev/null +++ b/plugins/Ecommerce/Reports/GetDaysToConversionAbandonedCart.php @@ -0,0 +1,31 @@ +action = 'getDaysToConversion'; + $this->name = Piwik::translate('General_AbandonedCarts') . ' - ' . Piwik::translate('Goals_DaysToConv'); + $this->dimension = new DaysToConversion(); + $this->constantRowsCount = true; + $this->processedMetrics = []; + $this->metrics = array('nb_conversions'); + $this->order = 25; + + $this->parameters = array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART); + } +} diff --git a/plugins/Ecommerce/Reports/GetDaysToConversionEcommerceOrder.php b/plugins/Ecommerce/Reports/GetDaysToConversionEcommerceOrder.php new file mode 100644 index 0000000000000000000000000000000000000000..233880e8ecff3a6f4479f1186256309bf3ace2ed --- /dev/null +++ b/plugins/Ecommerce/Reports/GetDaysToConversionEcommerceOrder.php @@ -0,0 +1,31 @@ +action = 'getDaysToConversion'; + $this->name = Piwik::translate('General_EcommerceOrders') . ' - ' . Piwik::translate('Goals_DaysToConv'); + $this->dimension = new DaysToConversion(); + $this->constantRowsCount = true; + $this->processedMetrics = []; + $this->metrics = array('nb_conversions'); + $this->order = 12; + + $this->parameters = array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER); + } +} diff --git a/plugins/Ecommerce/Reports/GetEcommerceAbandonedCart.php b/plugins/Ecommerce/Reports/GetEcommerceAbandonedCart.php new file mode 100644 index 0000000000000000000000000000000000000000..dd5252d103cf6b4a3c607e84d1d2b8d3fd965bbf --- /dev/null +++ b/plugins/Ecommerce/Reports/GetEcommerceAbandonedCart.php @@ -0,0 +1,38 @@ +action = 'get'; + $this->name = Piwik::translate('General_AbandonedCarts'); + $this->processedMetrics = array('avg_order_revenue'); + $this->order = 15; + $this->metrics = array('nb_conversions', 'conversion_rate', 'revenue', 'items'); + + $this->parameters = array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART); + } + + public function getMetrics() + { + $metrics = parent::getMetrics(); + + $metrics['nb_conversions'] = Piwik::translate('General_AbandonedCarts'); + $metrics['revenue'] = Piwik::translate('Goals_LeftInCart', Piwik::translate('General_ColumnRevenue')); + $metrics['items'] = Piwik::translate('Goals_LeftInCart', Piwik::translate('Goals_Products')); + + return $metrics; + } +} diff --git a/plugins/Ecommerce/Reports/GetEcommerceOrder.php b/plugins/Ecommerce/Reports/GetEcommerceOrder.php new file mode 100644 index 0000000000000000000000000000000000000000..6b0e073d3b74b2766ef7c941db8aae0cfe066bec --- /dev/null +++ b/plugins/Ecommerce/Reports/GetEcommerceOrder.php @@ -0,0 +1,46 @@ +action = 'get'; + $this->name = Piwik::translate('General_EcommerceOrders'); + $this->processedMetrics = array('avg_order_revenue'); + $this->order = 10; + $this->metrics = array( + 'nb_conversions', + 'nb_visits_converted', + 'conversion_rate', + 'revenue', + 'revenue_subtotal', + 'revenue_tax', + 'revenue_shipping', + 'revenue_discount', + ); + + $this->parameters = array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER); + } + + public function getMetrics() + { + $metrics = parent::getMetrics(); + + $metrics['nb_conversions'] = Piwik::translate('General_EcommerceOrders'); + $metrics['items'] = Piwik::translate('General_PurchasedProducts'); + + return $metrics; + } +} diff --git a/plugins/Ecommerce/Reports/GetItemsCategory.php b/plugins/Ecommerce/Reports/GetItemsCategory.php new file mode 100644 index 0000000000000000000000000000000000000000..b37f80d2d264783fb73413b6123688e00db0cdd5 --- /dev/null +++ b/plugins/Ecommerce/Reports/GetItemsCategory.php @@ -0,0 +1,27 @@ +name = Piwik::translate('Goals_ProductCategory'); + $this->dimension = new ProductCategory(); + $this->order = 32; + + $this->subcategoryId = 'Goals_Products'; + } +} diff --git a/plugins/Ecommerce/Reports/GetItemsName.php b/plugins/Ecommerce/Reports/GetItemsName.php new file mode 100644 index 0000000000000000000000000000000000000000..3651caca8fec447df15edaa447dd470b1e2e1e29 --- /dev/null +++ b/plugins/Ecommerce/Reports/GetItemsName.php @@ -0,0 +1,27 @@ +name = Piwik::translate('Goals_ProductName'); + $this->dimension = new ProductName(); + $this->order = 30; + + $this->subcategoryId = 'Goals_Products'; + } +} diff --git a/plugins/Ecommerce/Reports/GetItemsSku.php b/plugins/Ecommerce/Reports/GetItemsSku.php new file mode 100644 index 0000000000000000000000000000000000000000..0cdb3ff78edc2b1235e9a9deed16252adb3df8fd --- /dev/null +++ b/plugins/Ecommerce/Reports/GetItemsSku.php @@ -0,0 +1,27 @@ +name = Piwik::translate('Goals_ProductSKU'); + $this->dimension = new ProductSku(); + $this->order = 31; + + $this->subcategoryId = 'Goals_Products'; + } +} diff --git a/plugins/Ecommerce/Reports/GetVisitsUntilConversionAbandonedCart.php b/plugins/Ecommerce/Reports/GetVisitsUntilConversionAbandonedCart.php new file mode 100644 index 0000000000000000000000000000000000000000..edce71c6a432aa0bad54911db03f6b48d618bb92 --- /dev/null +++ b/plugins/Ecommerce/Reports/GetVisitsUntilConversionAbandonedCart.php @@ -0,0 +1,31 @@ +action = 'getVisitsUntilConversion'; + $this->name = Piwik::translate('General_AbandonedCarts') . ' - ' . Piwik::translate('Goals_VisitsUntilConv'); + $this->dimension = new VisitsUntilConversion(); + $this->constantRowsCount = true; + $this->processedMetrics = []; + $this->metrics = array('nb_conversions'); + $this->order = 20; + + $this->parameters = array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART); + } +} diff --git a/plugins/Ecommerce/Reports/GetVisitsUntilConversionEcommerceOrder.php b/plugins/Ecommerce/Reports/GetVisitsUntilConversionEcommerceOrder.php new file mode 100644 index 0000000000000000000000000000000000000000..e1a9b4f3b403b430cca064d18b250e3a89455df7 --- /dev/null +++ b/plugins/Ecommerce/Reports/GetVisitsUntilConversionEcommerceOrder.php @@ -0,0 +1,31 @@ +action = 'getVisitsUntilConversion'; + $this->name = Piwik::translate('General_EcommerceOrders') . ' - ' . Piwik::translate('Goals_VisitsUntilConv'); + $this->dimension = new VisitsUntilConversion(); + $this->constantRowsCount = true; + $this->processedMetrics = array(); + $this->metrics = array('nb_conversions'); + $this->order = 11; + + $this->parameters = array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER); + } +} diff --git a/plugins/Ecommerce/Settings/EcommerceRestricted.php b/plugins/Ecommerce/Settings/EcommerceRestricted.php new file mode 100644 index 0000000000000000000000000000000000000000..00ceb9c40ffe35fa32e64a7486ca3cae50066335 --- /dev/null +++ b/plugins/Ecommerce/Settings/EcommerceRestricted.php @@ -0,0 +1,107 @@ +getValue() === false) { + return Piwik::translate('Ecommerce_EcommercePolicySettingNonCompliantNote'); + } + + $manageUrl = 'index.php' . Url::getCurrentQueryStringWithParametersModified(self::getManageParams($idSite)); + + return Piwik::translate('Ecommerce_EcommercePolicySettingRequirementNote', ['', '']); + } + + private static function getManageParams(?int $idSite): array + { + $params = [ + 'module' => 'SitesManager', + 'action' => 'index', + ]; + + if ($idSite !== null) { + $params['idSite'] = $idSite; + } + + return $params; + } + + public static function isCompliant(string $policy, ?int $idSite = null): bool + { + $policyValues = static::getPolicyRequirements(); + if (!array_key_exists($policy, $policyValues)) { + return true; + } + + $currentValue = self::getInstance($idSite)->getValue(); + + $idSites = self::getIdSitesToCheck($idSite); + + return $currentValue === $policyValues[$policy] || !self::hasEcommerceEnabledSite($idSites); + } + + public static function getPolicyRequirements(): array + { + return [ + CnilPolicy::class => true, + ]; + } + + private static function getIdSitesToCheck(?int $idSite): array + { + if ($idSite === null) { + return Site::getIdSitesFromIdSitesString('all'); + } + + $ids = Site::getIdSitesFromIdSitesString((string) $idSite); + + return empty($ids) ? [$idSite] : $ids; + } + + private static function hasEcommerceEnabledSite(array $idSites): bool + { + foreach ($idSites as $siteId) { + if (Site::isEcommerceEnabledFor((int) $siteId)) { + return true; + } + } + + return false; + } + + private static function getCompliantMessage(?int $idSite, array $idSites): string + { + if ($idSite !== null && count($idSites) === 1) { + return Piwik::translate('Ecommerce_EcommercePolicySettingCompliantSingle'); + } + + return Piwik::translate('Ecommerce_EcommercePolicySettingCompliantAll'); + } +} diff --git a/plugins/Ecommerce/Settings/OrderIdAnonymization.php b/plugins/Ecommerce/Settings/OrderIdAnonymization.php new file mode 100644 index 0000000000000000000000000000000000000000..b35e3f0088f2d3f1aa78c37942b6aebeeec3b183 --- /dev/null +++ b/plugins/Ecommerce/Settings/OrderIdAnonymization.php @@ -0,0 +1,102 @@ + + * @implements PolicyComparisonInterface + * @implements SettingValueInterface + */ +class OrderIdAnonymization implements + CustomSettingInterface, + PolicyComparisonInterface, + SettingValueInterface +{ + /** @use PolicyComparisonTrait */ + use PolicyComparisonTrait; + + /** @use CustomGetterTrait */ + use CustomGetterTrait; + + /** @var bool */ + private $value; + + private function __construct(bool $value) + { + $this->value = $value; + } + + public function getValue() + { + return $this->value; + } + + protected static function getCustomSettingName(): string + { + return 'anonymizeOrderId'; + } + + public static function getCustomValue(?int $idSite = null) + { + return (new Config($idSite))->getFromOption(self::getCustomSettingName(), $allowPolicyComplianceOverride = false); + } + + public static function getTitle(): string + { + return Piwik::translate('Ecommerce_OrderIdAnonymizationSettingTitle'); + } + + public static function getComplianceRequirementNote(?int $idSite = null): string + { + return Piwik::translate('Ecommerce_OrderIdAnonymizationSettingRequirementNote'); + } + + public static function getInlineHelp(): string + { + return ''; + } + + public static function getPolicyRequirements(): array + { + return [ + CnilPolicy::class => true, + ]; + } + + public static function getInstance(?int $idSite = null): self + { + $values = self::getPolicyRequiredValues($idSite); + $values['system'] = (bool) self::getCustomValue(); + $values['measurable'] = is_null($idSite) ? null : (bool) self::getCustomValue($idSite); + + $strictest = (bool) self::getStrictestValueFromArray($values); + return new self($strictest); + } + + public static function isCompliant(string $policy, ?int $idSite = null): bool + { + $policyValues = self::getPolicyRequirements(); + + if (!array_key_exists($policy, $policyValues)) { + return true; + } + + $currentValue = self::getInstance($idSite)->getValue(); + + return $currentValue === $policyValues[$policy]; + } + + protected static function compareStrictness($value1, $value2) + { + return ($value1 || $value2); + } +} diff --git a/plugins/Ecommerce/Tracker/EcommerceRequestProcessor.php b/plugins/Ecommerce/Tracker/EcommerceRequestProcessor.php new file mode 100644 index 0000000000000000000000000000000000000000..6e4de639403c85a0fcab4ecef8d22feab8527333 --- /dev/null +++ b/plugins/Ecommerce/Tracker/EcommerceRequestProcessor.php @@ -0,0 +1,93 @@ +goalManager = $goalManager; + } + + public function processRequestParams(VisitProperties $visitProperties, Request $request) + { + $isGoalAnOrder = $this->isRequestForAnOrder($request); + $request->setMetadata('Ecommerce', 'isGoalAnOrder', $isGoalAnOrder); + + $isRequestEcommerce = $this->isRequestEcommerce($request); + $request->setMetadata('Ecommerce', 'isRequestEcommerce', $isRequestEcommerce); + + if ($isRequestEcommerce) { + // Mark the visit as Converted only if it is an order (not for a Cart update) + $idGoal = GoalManager::IDGOAL_CART; + if ($isGoalAnOrder) { + $idGoal = GoalManager::IDGOAL_ORDER; + $request->setMetadata('Goals', 'visitIsConverted', true); + } + + $request->setMetadata('Goals', 'goalsConverted', array(array('idgoal' => $idGoal))); + + $request->setMetadata('Actions', 'action', null); // don't track actions when tracking ecommerce orders + } + + return false; + } + + public function afterRequestProcessed(VisitProperties $visitProperties, Request $request) + { + $goalsConverted = $request->getMetadata('Goals', 'goalsConverted'); + if (!empty($goalsConverted)) { + $isThereExistingCartInVisit = $this->goalManager->detectIsThereExistingCartInVisit( + $visitProperties->getProperties() + ); + $request->setMetadata('Goals', 'isThereExistingCartInVisit', $isThereExistingCartInVisit); + } + + return false; + } + + private function isRequestForAnOrder(Request $request) + { + $orderId = $request->getParam('ec_id'); + return !empty($orderId); + } + + private function isRequestEcommerce(Request $request) + { + $idGoal = $request->getParam('idgoal'); + return 0 == $idGoal; + } +} diff --git a/plugins/Ecommerce/VisitorDetails.php b/plugins/Ecommerce/VisitorDetails.php new file mode 100644 index 0000000000000000000000000000000000000000..c711884415234ca8274d63a787b42da88a6d0722 --- /dev/null +++ b/plugins/Ecommerce/VisitorDetails.php @@ -0,0 +1,347 @@ + 0, + 'lifeTimeConversions' => 0, + 'lifeTimeEcommerceItems' => 0); + + public function extendVisitorDetails(&$visitor) + { + if (Site::isEcommerceEnabledFor($visitor['idSite'])) { + $ecommerceMetrics = $this->queryEcommerceConversionsVisitorLifeTimeMetricsForVisitor( + $visitor['idSite'], + $visitor['visitorId'] + ); + $visitor['totalEcommerceRevenue'] = $ecommerceMetrics['totalEcommerceRevenue']; + $visitor['totalEcommerceConversions'] = $ecommerceMetrics['totalEcommerceConversions']; + $visitor['totalEcommerceItems'] = $ecommerceMetrics['totalEcommerceItems']; + + $visitor['totalAbandonedCartsRevenue'] = $ecommerceMetrics['totalAbandonedCartsRevenue']; + $visitor['totalAbandonedCarts'] = $ecommerceMetrics['totalAbandonedCarts']; + $visitor['totalAbandonedCartsItems'] = $ecommerceMetrics['totalAbandonedCartsItems']; + } + } + + public function extendActionDetails(&$action, $nextAction, $visitorDetails) + { + if (empty($action['productViewName'])) { + unset($action['productViewName']); + } + if (empty($action['productViewSku'])) { + unset($action['productViewSku']); + } + if (empty($action['productViewPrice'])) { + unset($action['productViewPrice']); + } + + $categories = []; + for ($i = 1; $i <= ProductCategory::PRODUCT_CATEGORY_COUNT; $i++) { + if (!empty($action['productViewCategory' . $i])) { + $categories[] = $action['productViewCategory' . $i]; + } + + unset($action['productViewCategory' . $i]); + } + if (!empty($categories)) { + $action['productViewCategories'] = $categories; + } + } + + public function renderActionTooltip($action, $visitInfo) + { + if ( + !isset($action['productViewName']) && !isset($action['productViewSku']) && + !isset($action['productViewPrice']) && !isset($action['productViewCategories']) + ) { + return []; + } + + $view = new View('@Ecommerce/_actionTooltip'); + $view->sendHeadersWhenRendering = false; + $view->action = $action; + $view->visitInfo = $visitInfo; + return [[ 15, $view->render() ]]; + } + + public function provideActionsForVisitIds(&$actions, $idVisits) + { + $ecommerceDetails = $this->queryEcommerceConversionsForVisits($idVisits); + + // use while / array_shift combination instead of foreach to save memory + while (is_array($ecommerceDetails) && count($ecommerceDetails)) { + $ecommerceDetail = array_shift($ecommerceDetails); + + $idVisit = $ecommerceDetail['idvisit']; + + unset($ecommerceDetail['idvisit']); + + if ($ecommerceDetail['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) { + unset($ecommerceDetail['orderId']); + unset($ecommerceDetail['revenueSubTotal']); + unset($ecommerceDetail['revenueTax']); + unset($ecommerceDetail['revenueShipping']); + unset($ecommerceDetail['revenueDiscount']); + } + + $ecommerceDetail['referrerType'] = $this->getReferrerType($ecommerceDetail['referrerType']); + + // 25.00 => 25 + foreach ($ecommerceDetail as $column => $value) { + if (strpos($column, 'revenue') !== false) { + if (!is_numeric($value)) { + $ecommerceDetail[$column] = 0; + } elseif ($value == round($value)) { + $ecommerceDetail[$column] = round($value); + } + } + } + + $idOrder = isset($ecommerceDetail['orderId']) ? $ecommerceDetail['orderId'] : GoalManager::ITEM_IDORDER_ABANDONED_CART; + + $itemsDetails = $this->queryEcommerceItemsForOrder($idVisit, $idOrder); + foreach ($itemsDetails as &$detail) { + if ($detail['price'] == round($detail['price'])) { + $detail['price'] = round($detail['price']); + } + } + $ecommerceDetail['itemDetails'] = $itemsDetails; + + $actions[$idVisit][] = $ecommerceDetail; + } + } + + /** + * @param $idSite + * @param $idVisitor + * @return array + * @throws \Exception + */ + protected function queryEcommerceConversionsVisitorLifeTimeMetricsForVisitor($idSite, $idVisitor) + { + $sql = $this->getSqlEcommerceConversionsLifeTimeMetricsForIdGoal(); + $lifeTimeStats = $this->getDb()->fetchAll($sql, array($idSite, @Common::hex2bin($idVisitor))); + + $defaultStats = array_fill_keys([GoalManager::IDGOAL_CART, GoalManager::IDGOAL_ORDER], self::DEFAULT_LIFETIME_STAT); + + $lifeTimeStatsByGoal = array_reduce($lifeTimeStats, function ($carry, $statRow) { + $idgoal = $statRow['idgoal']; + $carry[$idgoal] = array_merge($carry[$idgoal], $statRow); + return $carry; + }, $defaultStats); + + $ecommerceOrders = $lifeTimeStatsByGoal[GoalManager::IDGOAL_ORDER]; + $abandonedCarts = $lifeTimeStatsByGoal[GoalManager::IDGOAL_CART]; + + return array( + 'totalEcommerceRevenue' => $ecommerceOrders['lifeTimeRevenue'], + 'totalEcommerceConversions' => $ecommerceOrders['lifeTimeConversions'], + 'totalEcommerceItems' => $ecommerceOrders['lifeTimeEcommerceItems'], + 'totalAbandonedCartsRevenue' => $abandonedCarts['lifeTimeRevenue'], + 'totalAbandonedCarts' => $abandonedCarts['lifeTimeConversions'], + 'totalAbandonedCartsItems' => $abandonedCarts['lifeTimeEcommerceItems'], + ); + } + + + /** + * Returns and SQL string that queries for `lifeTimeRevenue`, `lifeTimeConversions`, and `lifeTimeEcommerceItems` grouped by + * `idgoal` for abandoned carts and orders. + * @return string + */ + protected function getSqlEcommerceConversionsLifeTimeMetricsForIdGoal() + { + $sql = "SELECT + idgoal, + COALESCE(SUM(" . LogAggregator::getSqlRevenue('revenue') . "), 0) as lifeTimeRevenue, + COUNT(*) as lifeTimeConversions, + COALESCE(SUM(" . LogAggregator::getSqlRevenue('items') . "), 0) as lifeTimeEcommerceItems + FROM " . Common::prefixTable('log_visit') . " AS log_visit + STRAIGHT_JOIN " . Common::prefixTable('log_conversion') . " AS log_conversion + ON log_visit.idvisit = log_conversion.idvisit + WHERE + log_visit.idsite = ? + AND log_visit.idvisitor = ? + AND log_conversion.idgoal IN ( " . GoalManager::IDGOAL_CART . ", " . GoalManager::IDGOAL_ORDER . " ) + GROUP BY idgoal + "; + return $sql; + } + + /** + * @param $idVisit + * @param $limit + * @return array + * @throws \Exception + */ + protected function queryEcommerceConversionsForVisits($idVisits) + { + $sql = "SELECT + log_conversion.idvisit, + case idgoal when " . GoalManager::IDGOAL_CART + . " then '" . Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART + . "' else '" . Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER . "' end as type, + idorder as orderId, + " . LogAggregator::getSqlRevenue('revenue') . " as revenue, + " . LogAggregator::getSqlRevenue('revenue_subtotal') . " as revenueSubTotal, + " . LogAggregator::getSqlRevenue('revenue_tax') . " as revenueTax, + " . LogAggregator::getSqlRevenue('revenue_shipping') . " as revenueShipping, + " . LogAggregator::getSqlRevenue('revenue_discount') . " as revenueDiscount, + items as items, + log_conversion.server_time as serverTimePretty, + log_conversion.idlink_va, + log_link_visit_action.idpageview, + log_conversion.referer_type as referrerType, + log_conversion.referer_name as referrerName, + log_conversion.referer_keyword as referrerKeyword + FROM " . Common::prefixTable('log_conversion') . " AS log_conversion + LEFT JOIN " . Common::prefixTable('log_link_visit_action') . " AS log_link_visit_action + ON log_link_visit_action.idlink_va = log_conversion.idlink_va + WHERE log_conversion.idvisit IN ('" . implode("','", $idVisits) . "') + AND idgoal <= " . GoalManager::IDGOAL_ORDER . " + ORDER BY log_conversion.idvisit, log_conversion.server_time ASC"; + + $sql = DbHelper::addMaxExecutionTimeHintToQuery($sql, $this->getLiveQueryMaxExecutionTime()); + + try { + $ecommerceDetails = $this->getDb()->fetchAll($sql); + } catch (\Exception $e) { + $now = Date::now(); + Model::handleMaxExecutionTimeError($this->getDb(), $e, '', $now, $now, null, 0, ['sql' => $sql]); + throw $e; + } + + return $ecommerceDetails; + } + + /** + * @param $idVisit + * @param $idOrder + * @param $actionsLimit + * @return array + * @throws \Exception + */ + protected function queryEcommerceItemsForOrder($idVisit, $idOrder) + { + $categorySelects = []; + $categoryJoins = []; + for ($i = 0; $i < self::CATEGORY_COUNT; ++$i) { + $suffix = $i === 0 ? '' : $i; + $column = $i === 0 ? 'idaction_category' : ('idaction_category' . ($i + 1)); + $categorySelects[] = 'log_action_category' . $suffix . '.name as itemCategory' . $suffix; + $categoryJoins[] = 'LEFT JOIN ' . Common::prefixTable('log_action') . " AS log_action_category$suffix + ON $column = log_action_category$suffix.idaction"; + } + $categorySelects = implode(',', $categorySelects); + $categoryJoins = implode("\n", $categoryJoins); + + $sql = "SELECT + log_action_sku.name as itemSKU, + log_action_name.name as itemName, + $categorySelects, + " . LogAggregator::getSqlRevenue('price') . " as price, + quantity as quantity + FROM " . Common::prefixTable('log_conversion_item') . " + INNER JOIN " . Common::prefixTable('log_action') . " AS log_action_sku + ON idaction_sku = log_action_sku.idaction + LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_name + ON idaction_name = log_action_name.idaction + $categoryJoins + WHERE idvisit = ? + AND idorder = ? + AND deleted = 0 + "; + + $bind = array($idVisit, $idOrder); + + $itemsDetails = $this->getDb()->fetchAll($sql, $bind); + + // create categories array for each item + foreach ($itemsDetails as &$item) { + $categories = []; + for ($i = 0; $i < self::CATEGORY_COUNT; ++$i) { + $suffix = $i === 0 ? '' : $i; + if (empty($item['itemCategory' . $suffix])) { + continue; + } + + $categories[] = trim($item['itemCategory' . $suffix]); + } + $item['categories'] = array_filter($categories); + + // remove itemCategotyN properties, except 'itemCategory' property for BC + for ($i = 1; $i < self::CATEGORY_COUNT; ++$i) { + unset($item['itemCategory' . $i]); + } + } + + return $itemsDetails; + } + + public function initProfile($visits, &$profile) + { + if (Site::isEcommerceEnabledFor($visits->getFirstRow()->getColumn('idSite'))) { + $profile['totalEcommerceRevenue'] = 0; + $profile['totalEcommerceConversions'] = 0; + $profile['totalEcommerceItems'] = 0; + $profile['totalAbandonedCarts'] = 0; + $profile['totalAbandonedCartsRevenue'] = 0; + $profile['totalAbandonedCartsItems'] = 0; + } + } + + public function finalizeProfile($visits, &$profile) + { + $lastVisit = $visits->getLastRow(); + if ($lastVisit && Site::isEcommerceEnabledFor($lastVisit->getColumn('idSite'))) { + $profile['totalEcommerceRevenue'] = $lastVisit->getColumn('totalEcommerceRevenue'); + $profile['totalEcommerceConversions'] = $lastVisit->getColumn('totalEcommerceConversions'); + $profile['totalEcommerceItems'] = $lastVisit->getColumn('totalEcommerceItems'); + $profile['totalAbandonedCartsRevenue'] = $lastVisit->getColumn('totalAbandonedCartsRevenue'); + $profile['totalAbandonedCarts'] = $lastVisit->getColumn('totalAbandonedCarts'); + $profile['totalAbandonedCartsItems'] = $lastVisit->getColumn('totalAbandonedCartsItems'); + } + } + + protected function getReferrerType($referrerTypeId) + { + try { + $referrerType = getReferrerTypeFromShortName($referrerTypeId); + } catch (\Exception $e) { + $referrerType = ''; + } + + return $referrerType; + } + + private function getLiveQueryMaxExecutionTime() + { + return Config::getInstance()->General['live_query_max_execution_time']; + } +} diff --git a/plugins/Ecommerce/Widgets/GetEcommerceLog.php b/plugins/Ecommerce/Widgets/GetEcommerceLog.php new file mode 100644 index 0000000000000000000000000000000000000000..b53cabf114e48d842826bbacc33df9b3b3faf457 --- /dev/null +++ b/plugins/Ecommerce/Widgets/GetEcommerceLog.php @@ -0,0 +1,39 @@ +setCategoryId('Goals_Ecommerce'); + $config->setSubcategoryId('Goals_EcommerceLog'); + $config->setName('Goals_EcommerceLog'); + + $idSite = Common::getRequestVar('idSite', 0, 'int'); + if (empty($idSite)) { + $config->disable(); + return; + } + + $site = new Site($idSite); + $config->setIsEnabled($site->isEcommerceEnabled()); + + if (!Manager::getInstance()->isPluginActivated('Live') || !Live::isVisitorLogEnabled($idSite)) { + $config->disable(); + } + } +} diff --git a/plugins/Ecommerce/Widgets/ProductsByDimension.php b/plugins/Ecommerce/Widgets/ProductsByDimension.php new file mode 100644 index 0000000000000000000000000000000000000000..3b0f2f7a959ae1bfd453690c5af27e5a1ac6f582 --- /dev/null +++ b/plugins/Ecommerce/Widgets/ProductsByDimension.php @@ -0,0 +1,35 @@ +isEcommerceEnabled(); + } +} diff --git a/plugins/Ecommerce/config/config.php b/plugins/Ecommerce/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/Ecommerce/config/config.php @@ -0,0 +1,3 @@ + +

    {{ 'Goals_Ecommerce'|translate }}

    +
    +

    + {{ 'Ecommerce_VisitorProfileLTV'|translate( "" ~ visitorData.totalEcommerceRevenue|money(idSite) ~ "")|raw }} + {{ 'Ecommerce_VisitorProfileItemsAndOrders'|translate("" ~ visitorData.totalEcommerceItems ~ "", "" ~ visitorData.totalEcommerceConversions ~ "")|raw }} +

    +

    + {%- if visitorData.totalAbandonedCarts|default(0) > 0 %} + {{ 'Ecommerce_VisitorProfileAbandonedCartSummary'|translate('' ~ visitorData.totalAbandonedCarts ~ '', '' ~ visitorData.totalAbandonedCartsItems ~ '', '' ~ visitorData.totalAbandonedCartsRevenue|money(idSite) ~ '')|raw }} + {%- endif -%} +

    +
    +

    \ No newline at end of file diff --git a/plugins/Ecommerce/templates/conversionOverview.twig b/plugins/Ecommerce/templates/conversionOverview.twig new file mode 100644 index 0000000000000000000000000000000000000000..a82ca0a8ceafeecde96dd2e438cf43e311295949 --- /dev/null +++ b/plugins/Ecommerce/templates/conversionOverview.twig @@ -0,0 +1,10 @@ +
    diff --git a/plugins/Ecommerce/tests/Fixtures/AbandonedCartWithoutConversions.php b/plugins/Ecommerce/tests/Fixtures/AbandonedCartWithoutConversions.php new file mode 100644 index 0000000000000000000000000000000000000000..19cfdd50716e3b3fc8f8430f819a51d9decfc66a --- /dev/null +++ b/plugins/Ecommerce/tests/Fixtures/AbandonedCartWithoutConversions.php @@ -0,0 +1,68 @@ +setUpWebsitesAndGoals(); + $this->trackVisits(); + } + + private function setUpWebsitesAndGoals() + { + if (!self::siteCreated($this->idSite)) { + $this->idSite = self::createWebsite($this->dateTime, $ecommerce = 1, 'test site'); + } + + if (!self::goalExists($this->idSite, $this->idGoalStandard)) { + GoalsAPI::getInstance()->addGoal( + $this->idSite, + 'title match, triggered NEVER', + 'title', + 'saldkfjaslkdfjsalkdjf', + 'contains', + $caseSensitive = false, + $revenue = 10, + $allowMultipleConversions = true + ); + } + } + + private function trackVisits() + { + // visit without ecommerce + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + $t->setUrl('http://piwik.net/here/we/go'); + $t->setForceVisitDateTime(Date::factory($this->dateTime)->getDatetime()); + self::checkResponse($t->doTrackPageView('one page visit')); + + // visit with abandoned cart + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + $t->setUrl('http://piwik.net/here/we/go'); + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(1)->getDatetime()); + self::checkResponse($t->doTrackPageView('one page visit')); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(1.1)->getDatetime()); + $t->addEcommerceItem($sku = 'SKU IN ABANDONED CART ONE', $name = 'PRODUCT ONE LEFT in cart', $category = '', $price = 500.11111112, $quantity = 2); + self::checkResponse($t->doTrackEcommerceCartUpdate($grandTotal = 1000)); + } +} diff --git a/plugins/Ecommerce/tests/Integration/EcommerceRestrictedTest.php b/plugins/Ecommerce/tests/Integration/EcommerceRestrictedTest.php new file mode 100644 index 0000000000000000000000000000000000000000..15ef90bdc9e086b3d66880ce68f1ce2cf2c1fc3b --- /dev/null +++ b/plugins/Ecommerce/tests/Integration/EcommerceRestrictedTest.php @@ -0,0 +1,88 @@ +ecommerceSite = Fixture::createWebsite('2024-01-01 00:00:00', $ecommerce = 1); + $this->nonEcommerceSite = Fixture::createWebsite('2024-01-02 00:00:00', $ecommerce = 0); + } + + public function tearDown(): void + { + CnilPolicy::setActiveStatus(null, false); + parent::tearDown(); + } + + public function testReturnsCompliantMessageWhenEcommerceDisabled(): void + { + $note = EcommerceRestricted::getComplianceRequirementNote($this->nonEcommerceSite); + + $this->assertEquals('Ecommerce_EcommercePolicySettingCompliantSingle', $note); + } + + public function testReturnsNonCompliantMessageWhenPolicyNotEnforced(): void + { + CnilPolicy::setActiveStatus(null, false); + $note = EcommerceRestricted::getComplianceRequirementNote($this->ecommerceSite); + + $this->assertEquals('Ecommerce_EcommercePolicySettingNonCompliantNote', $note); + } + + public function testReturnsRequirementNoteWithLinkWhenPolicyEnforced(): void + { + CnilPolicy::setActiveStatus(null, true); + $_GET = []; + + $note = EcommerceRestricted::getComplianceRequirementNote($this->ecommerceSite); + + $this->assertEquals('Ecommerce_EcommercePolicySettingRequirementNote', $note); + } + + public function testIsCompliantWhenEcommerceDisabled(): void + { + CnilPolicy::setActiveStatus(null, true); + + $this->assertTrue( + EcommerceRestricted::isCompliant(CnilPolicy::class, $this->nonEcommerceSite) + ); + } + + public function testIsCompliantWhenEcommerceEnabledAndPolicyEnforced(): void + { + CnilPolicy::setActiveStatus(null, true); + + $this->assertTrue( + EcommerceRestricted::isCompliant(CnilPolicy::class, $this->ecommerceSite) + ); + } + + public function testIsCompliantWhenEcommerceEnabledAndPolicyNotEnforced(): void + { + CnilPolicy::setActiveStatus(null, false); + + $this->assertFalse( + EcommerceRestricted::isCompliant(CnilPolicy::class, $this->ecommerceSite) + ); + } +} diff --git a/plugins/Ecommerce/tests/Integration/SegmentsFilterTest.php b/plugins/Ecommerce/tests/Integration/SegmentsFilterTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d50bd5e19f3394dc9027ca6920a1b52459ae57fa --- /dev/null +++ b/plugins/Ecommerce/tests/Integration/SegmentsFilterTest.php @@ -0,0 +1,78 @@ +idSite = Fixture::createWebsite('2023-01-01 00:00:00', $ecommerce = 1); + } + + public function tearDown(): void + { + CnilPolicy::setActiveStatus(null, false); + unset($_GET['idSite'], $_POST['idSite']); + Cache::getTransientCache()->flushAll(); + + parent::tearDown(); + } + + public function testSegmentsNotFilteredWhenPolicyDisabled(): void + { + CnilPolicy::setActiveStatus(null, false); + + $segmentsList = $this->getSegmentsListForSite(); + + $this->assertNotNull($segmentsList->getSegment('orderId')); + $this->assertNotNull($segmentsList->getSegment('revenueOrder')); + $this->assertNotNull($segmentsList->getSegment('productPrice')); + $this->assertNotNull($segmentsList->getSegment('productName')); + $this->assertNotNull($segmentsList->getSegment('productSku')); + } + + public function testSegmentsFilteredWhenPolicyEnabled(): void + { + CnilPolicy::setActiveStatus(null, true); + + $segmentsList = $this->getSegmentsListForSite(); + + $this->assertNull($segmentsList->getSegment('orderId')); + $this->assertNull($segmentsList->getSegment('revenueOrder')); + $this->assertNull($segmentsList->getSegment('productPrice')); + $this->assertNull($segmentsList->getSegment('productName')); + $this->assertNull($segmentsList->getSegment('productSku')); + } + + private function getSegmentsListForSite(): SegmentsList + { + Cache::getTransientCache()->flushAll(); + + $_GET['idSite'] = $this->idSite; + + return SegmentsList::get(); + } +} diff --git a/plugins/Ecommerce/tests/System/AbandonedCartWithoutConversionsTest.php b/plugins/Ecommerce/tests/System/AbandonedCartWithoutConversionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9e4d52d42f2e474a072e7c8a3de000fd0290c132 --- /dev/null +++ b/plugins/Ecommerce/tests/System/AbandonedCartWithoutConversionsTest.php @@ -0,0 +1,73 @@ +runApiTests($api, $params); + } + + public function getApiForTesting() + { + $idSite = self::$fixture->idSite; + $dateTime = self::$fixture->dateTime; + + $api = ['Goals']; + $goalItemApi = ['Goals.getItemsSku', 'Goals.getItemsName', 'Goals.getItemsCategory']; + + return [ + [ + $api, [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART, + ], + ], + [ + $goalItemApi, [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'testSuffix' => '_AbandonedCarts', + 'otherRequestParameters' => [ + 'abandonedCarts' => 1, + ], + ], + ], + ]; + } + + public static function getOutputPrefix() + { + return 'abandonedCartWithoutConversions'; + } + + public static function getPathToTestDirectory() + { + return dirname(__FILE__); + } +} + +AbandonedCartWithoutConversionsTest::$fixture = new AbandonedCartWithoutConversions(); diff --git a/plugins/Ecommerce/tests/System/EcommerceEvolutionComparisonTest.php b/plugins/Ecommerce/tests/System/EcommerceEvolutionComparisonTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ec9b537b751a69a365745f63a899eb2155ce1fc4 --- /dev/null +++ b/plugins/Ecommerce/tests/System/EcommerceEvolutionComparisonTest.php @@ -0,0 +1,127 @@ + Overview "Evolution over the period" + * chart shows $0 Total Revenue for both the main and comparison series when + * "Compare to previous period" is enabled. + * + * The chart issues a Goals.get request shaped by JqplotGraph\Evolution and + * EvolutionPeriodSelector::setDatePeriods. When isComparing() is true, the + * chart reads every series (including the originally selected period) from + * each row's comparison subtable, so populating that subtable correctly is + * what makes the chart display real revenue values. + * + * @group EcommerceEvolutionComparisonTest + * @group Plugins + */ +class EcommerceEvolutionComparisonTest extends SystemTestCase +{ + public static $fixture = null; // initialized below class definition + + public function testEvolutionComparisonReturnsRevenueForMainSeries() + { + $idSite = self::$fixture->idSite; + $mainDate = '2011-04-05,2011-04-05'; + $compareDate = '2011-04-04,2011-04-04'; + + $result = Request::processRequest('Goals.get', [ + 'idSite' => $idSite, + 'period' => 'day', + 'date' => $mainDate, + 'idGoal' => 'ecommerceOrder', + 'columns' => 'revenue', + 'showAllGoalSpecificMetrics' => 1, + 'format_metrics' => 0, + 'compareDates' => [$compareDate], + 'comparePeriods' => ['day'], + 'compareSegments' => [''], + 'compare' => 1, + ]); + + $tables = $result->getDataTables(); + $rows = reset($tables)->getFirstRow()->getComparisons()->getRows(); + + $mainRevenue = $rows[0]->getColumn('revenue'); + $this->assertIsNumeric($mainRevenue, 'Main-series revenue must be numeric, not a formatted currency string'); + $this->assertGreaterThan(0, $mainRevenue, 'Main-series revenue must reflect the day\'s tracked orders'); + + // The comparison row may legitimately be 0 (the fixture has no orders on 2011-04-04), + // but it must still be numeric — the original bug formatted it into a currency string too. + $this->assertIsNumeric($rows[1]->getColumn('revenue'), 'Comparison-series revenue must be numeric, not a formatted currency string'); + } + + public function testEvolutionComparisonReturnsConversionRateAsRawQuotient() + { + $idSite = self::$fixture->idSite; + $mainDate = '2011-04-05,2011-04-05'; + $compareDate = '2011-04-04,2011-04-04'; + + $result = Request::processRequest('Goals.get', [ + 'idSite' => $idSite, + 'period' => 'day', + 'date' => $mainDate, + 'idGoal' => 'ecommerceOrder', + 'columns' => 'conversion_rate', + 'showAllGoalSpecificMetrics' => 1, + 'format_metrics' => 0, + 'compareDates' => [$compareDate], + 'comparePeriods' => ['day'], + 'compareSegments' => [''], + 'compare' => 1, + ]); + + $tables = $result->getDataTables(); + $rows = reset($tables)->getFirstRow()->getComparisons()->getRows(); + + $mainConversionRate = $rows[0]->getColumn('conversion_rate'); + $this->assertIsNumeric( + $mainConversionRate, + 'Main-series conversion_rate must be a raw numeric quotient (e.g. 0.0984), not a formatted "x%" string — ' + . 'the evolution chart\'s Chart.php multiplies the raw quotient by 100 itself, so a pre-formatted string ' + . 'would render as 984%. Actual value: ' . var_export($mainConversionRate, true) + ); + $this->assertGreaterThan(0, $mainConversionRate, 'Fixture tracked conversions on 2011-04-05'); + $this->assertLessThan(1, $mainConversionRate, 'A conversion-rate quotient is always in [0, 1]'); + + $comparisonConversionRate = $rows[1]->getColumn('conversion_rate'); + $this->assertIsNumeric( + $comparisonConversionRate, + 'Comparison-series conversion_rate must also be numeric, not a "0%" / "x%" string. Actual: ' + . var_export($comparisonConversionRate, true) + ); + } + + public function testGoalsGetReportRegistersConversionRateAsProcessedMetric() + { + // The Ecommerce Overview sparklines call formatSparklineMetricValue, which only formats + // conversion_rate if it's registered as a ProcessedMetric instance on the Goals.get report. + // Pre-DEV-20289, the queued Goals formatter coincidentally pre-formatted the value so the + // sparkline displayed correctly. With format_metrics=0 honoured, the sparkline now depends + // on this registration being present — otherwise it renders the raw quotient (e.g. "0.1" + // instead of "9.84%"). + $report = ReportsProvider::factory('Goals', 'get'); + $processedMetrics = $report->getProcessedMetricsById(); + + $this->assertArrayHasKey( + 'conversion_rate', + $processedMetrics, + 'Goals.get must register conversion_rate as a ProcessedMetric instance so sparkline + compare formatting works' + ); + } +} + +EcommerceEvolutionComparisonTest::$fixture = new TwoSitesEcommerceOrderWithItems(); diff --git a/plugins/Ecommerce/tests/System/EcommerceOrderWithItemsTest.php b/plugins/Ecommerce/tests/System/EcommerceOrderWithItemsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8dd375508a18c96e939b14c48445168bbb2b0daa --- /dev/null +++ b/plugins/Ecommerce/tests/System/EcommerceOrderWithItemsTest.php @@ -0,0 +1,601 @@ +runApiTests($api, $params); + } + + public function getApiForTesting() + { + $idSite = self::$fixture->idSite; + $idSite2 = self::$fixture->idSite2; + $dateTime = self::$fixture->dateTime; + + $dayApi = [ + 'VisitsSummary.get', + 'VisitTime', + 'CustomVariables.getCustomVariables', + 'Live.getLastVisitsDetails', + 'UserCountry', + 'API.getProcessedReport', + 'Goals.get', + 'Goals.getConversions', + 'Goals.getItemsSku', + 'Goals.getItemsName', + 'Goals.getItemsCategory', + ]; + + $goalWeekApi = ['Goals.get', 'Goals.getItemsSku', 'Goals.getItemsName', 'Goals.getItemsCategory']; + + $goalItemApi = ['Goals.getItemsSku', 'Goals.getItemsName', 'Goals.getItemsCategory']; + + $processedReportApi = ['API.getProcessedReport']; + + $apiWithSegments = [ + 'Goals.getItemsSku', + 'Goals.getItemsName', + 'Goals.getItemsCategory', + ]; + + // Normal standard goal + $apiWithSegments_visitConvertedGoal = array_merge($apiWithSegments, ['Goals.get', 'VisitsSummary.get']); + return array_merge( + [ + // Segment: This will match the first visit of the fixture only + [ + $apiWithSegments, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'otherRequestParameters' => ['_leavePiwikCoreVariables' => 1], + 'segment' => 'pageUrl=@example.org%2Findex.htm', + 'testSuffix' => '_SegmentPageUrlContains', + ], + ], + + // Goals.get for Ecommerce, with Page Title segment + [ + 'Goals.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER, + 'segment' => 'pageTitle==Looking%20at%20product%20page', + 'testSuffix' => '_EcommerceOrderGoal_SegmentPageUrlContains', + ], + ], + + // Segment: This will match the first visit of the fixture only + [ + $apiWithSegments, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'otherRequestParameters' => ['_leavePiwikCoreVariables' => 1], + 'segment' => 'countryCode==fr', + 'testSuffix' => '_SegmentCountryIsFr', + ], + ], + + // day tests + [ + $dayApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'otherRequestParameters' => ['_leavePiwikCoreVariables' => 1], + ], + ], + + // goals API week tests + [$goalWeekApi, ['idSite' => $idSite, 'date' => $dateTime, 'periods' => ['week']]], + + // abandoned carts tests + [ + $goalItemApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'testSuffix' => '_AbandonedCarts', + 'otherRequestParameters' => [ + 'abandonedCarts' => 1, + ], + ], + ], + + // multiple periods tests + [ + $goalItemApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'setDateLastN' => true, + 'testSuffix' => 'multipleDates', + ], + ], + + // multiple periods & multiple websites tests + [ + $goalItemApi, + [ + 'idSite' => sprintf("%u,%u", $idSite, $idSite2), + 'date' => $dateTime, + 'periods' => ['day'], + 'setDateLastN' => true, + 'testSuffix' => 'multipleDates_andMultipleWebsites', + ], + ], + + // test metadata products + [ + $processedReportApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'apiModule' => 'Goals', + 'apiAction' => 'getItemsSku', + 'testSuffix' => '_Metadata_ItemsSku', + ], + ], + [ + $processedReportApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'apiModule' => 'Goals', + 'apiAction' => 'getItemsCategory', + 'testSuffix' => '_Metadata_ItemsCategory', + ], + ], + + // test metadata Goals.get for Ecommerce orders & Carts + [ + $processedReportApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'apiModule' => 'Goals', + 'apiAction' => 'get', + 'idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER, + 'testSuffix' => '_Metadata_Goals.Get_Order', + ], + ], + [ + $processedReportApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'apiModule' => 'Goals', + 'apiAction' => 'get', + 'idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART, + 'testSuffix' => '_Metadata_Goals.Get_AbandonedCart', + ], + ], + + // normal standard goal test + [ + $processedReportApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'apiModule' => 'Goals', + 'apiAction' => 'get', + 'idGoal' => self::$fixture->idGoalStandard, + 'testSuffix' => '_Metadata_Goals.Get_NormalGoal', + ], + ], + + // non-existant goal test + [ + $processedReportApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'apiModule' => 'Goals', + 'apiAction' => 'get', + 'idGoal' => 'FAKE IDGOAL', + 'testSuffix' => '_Metadata_Goals.Get_NotExistingGoal', + ], + ], + + // While we're at it, test for a standard Metadata report with zero entries + [ + $processedReportApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'apiModule' => 'VisitTime', + 'apiAction' => 'getVisitInformationPerServerTime', + 'testSuffix' => '_Metadata_VisitTime.getVisitInformationPerServerTime', + ], + ], + + // Standard non metadata Goals.get + // test Goals.get with idGoal=ecommerceOrder and ecommerceAbandonedCart + [ + 'Goals.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART, + 'testSuffix' => '_GoalAbandonedCart', + ], + ], + [ + 'Goals.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER, + 'testSuffix' => '_GoalOrder', + ], + ], + [ + 'Goals.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'idGoal' => 1, + 'testSuffix' => '_GoalMatchTitle', + ], + ], + [ + 'Goals.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'idGoal' => '', + 'testSuffix' => '_GoalOverall', + ], + ], + + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'segment' => 'visitEcommerceStatus==none', + 'testSuffix' => '_SegmentNoEcommerce', + ], + ], + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'testSuffix' => '_SegmentOrderedSomething', + 'segment' => 'visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart', + ], + ], + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'testSuffix' => '_SegmentAbandonedCart', + 'segment' => 'visitEcommerceStatus==abandonedCart,visitEcommerceStatus==orderedThenAbandonedCart', + ], + ], + + // test segment visitConvertedGoalId + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'testSuffix' => '_SegmentConvertedGoalId1', + 'segment' => "visitConvertedGoalId==" . self::$fixture->idGoalStandard, + ], + ], + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'testSuffix' => '_SegmentDidNotConvertGoalId1', + 'segment' => "visitConvertedGoalId!=" . self::$fixture->idGoalStandard, + ], + ], + + // test segment visitorType + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['week'], + 'segment' => 'visitorType==new', + 'testSuffix' => '_SegmentNewVisitors', + ], + ], + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['week'], + 'segment' => 'visitorType==returning', + 'testSuffix' => '_SegmentReturningVisitors', + ], + ], + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['week'], + 'segment' => 'visitorType==returningCustomer', + 'testSuffix' => '_SegmentReturningCustomers', + ], + ], + + // test segment visitConvertedGoalId with Ecommerce APIs + [ + $apiWithSegments_visitConvertedGoal, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['week'], + 'segment' => 'visitConvertedGoalId==1;visitConvertedGoalId!=2', + 'testSuffix' => '_SegmentVisitHasConvertedGoal', + ], + ], + + // Different segment will yield same result, so we keep same testSuffix + [ + $apiWithSegments_visitConvertedGoal, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['week'], + 'segment' => 'visitConvertedGoalId==1;visitConvertedGoalId!=2;countryCode!=xx;deviceType!=tv', + 'testSuffix' => '_SegmentVisitHasConvertedGoal', + ], + ], + + // testing a segment on log_conversion matching no visit + [ + $apiWithSegments_visitConvertedGoal, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['week'], + 'segment' => 'visitConvertedGoalId==666', + 'testSuffix' => '_SegmentNoVisit_HaveConvertedNonExistingGoal', + ], + ], + + // test segment visitEcommerceStatus and visitConvertedGoalId + [ + $apiWithSegments_visitConvertedGoal, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['week'], + 'segment' => 'visitEcommerceStatus!=ordered;visitConvertedGoalId==1', + 'testSuffix' => '_SegmentVisitHasNotOrderedAndConvertedGoal', + ], + ], + + // test segment pageTitle + [ + 'VisitsSummary.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day'], + 'segment' => 'pageTitle==incredible title!', + 'testSuffix' => '_SegmentPageTitleMatch', + ], + ], + + // test Live! output is OK also for the visit that just bought something (other visits leave an abandoned cart) + [ + 'Live.getLastVisitsDetails', + [ + 'idSite' => $idSite, + 'date' => Date::factory($dateTime)->addHour(30.65)->getDatetime(), + 'periods' => ['day'], + 'testSuffix' => '_LiveEcommerceStatusOrdered', + ], + ], + + // test API.get method + [ + 'API.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['day', 'week'], + 'otherRequestParameters' => [ + 'columns' => 'nb_pageviews,nb_visits,avg_time_on_site,nb_visits_converted', + ], + 'testSuffix' => '_API_get', + ], + ], + + // Website2 + [ + $goalWeekApi, + [ + 'idSite' => $idSite2, + 'date' => $dateTime, + 'periods' => ['week'], + 'testSuffix' => '_Website2', + ], + ], + + // see https://github.com/piwik/piwik/issues/7851 make sure avg_order_revenue is calculated correct + // even if only this column is given + [ + 'Goals.get', + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => ['week'], + 'idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER, + 'otherRequestParameters' => [ + 'columns' => 'avg_order_revenue', + ], + 'testSuffix' => '_AvgOrderRevenue', + ], + ], + + // product category segment + [ + array_merge(['VisitsSummary.get'], $goalItemApi), + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => 'week', + 'testSuffix' => '_productCategorySegment', + 'segment' => 'productCategory==Tools', + ], + ], + + [ + array_merge(['VisitsSummary.get'], $goalItemApi), + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => 'week', + 'testSuffix' => '_productNameSegment', + 'segment' => 'productName=@' . urlencode(urlencode('bought day after')), + ], + ], + + [ + array_merge(['VisitsSummary.get'], $goalItemApi), + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => 'week', + 'testSuffix' => '_productSkuSegment', + 'segment' => 'productSku==' . urlencode(urlencode('SKU VERY nice indeed')), + ], + ], + + [ + $goalItemApi, + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => 'week', + 'testSuffix' => '_productSkuSegmentSorted', + 'otherRequestParameters' => [ + 'filter_sort_column' => 'nb_visits', + ], + 'segment' => 'productSku==' . urlencode(urlencode('SKU VERY nice indeed')), + ], + ], + + // deleted sku will be deleted + [ + array_merge(['VisitsSummary.get'], $goalItemApi), + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => 'week', + 'testSuffix' => '_productSkuSegmentDeleted', + 'segment' => 'productSku==' . urlencode(urlencode('SKU WILL BE DELETED')), + ], + ], + + [ + array_merge(['VisitsSummary.get'], $goalItemApi), + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => 'week', + 'testSuffix' => '_productPrice', + 'segment' => 'productPrice>500', + ], + ], + [ + ['Live.getLastVisitsDetails', 'Goals.get'], + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => 'day', + 'testSuffix' => '_SegmentRevenueOrder', + 'segment' => 'revenueOrder>500', + ], + ], + [ + ['Live.getLastVisitsDetails', 'Goals.get'], + [ + 'idSite' => $idSite, + 'date' => $dateTime, + 'periods' => 'day', + 'testSuffix' => '_SegmentCartRevenueOrder', + 'segment' => 'revenueAbandonedCart>100', + ], + ], + + + ], + self::getApiForTestingScheduledReports($dateTime, 'week') + ); + } + + public static function getOutputPrefix() + { + return 'ecommerceOrderWithItems'; + } + + public static function getPathToTestDirectory() + { + return dirname(__FILE__); + } +} + +EcommerceOrderWithItemsTest::$fixture = new TwoSitesEcommerceOrderWithItems(); diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsCategory_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..e13dd2149eb958049145bd57723ed33012606726 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsCategory_day.xml @@ -0,0 +1,13 @@ + + + + + 1000.22 + 2 + 1 + 500.11 + 2 + 0% + productCategory==Product+Category+not+defined + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..e13dd2149eb958049145bd57723ed33012606726 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsCategory_week.xml @@ -0,0 +1,13 @@ + + + + + 1000.22 + 2 + 1 + 500.11 + 2 + 0% + productCategory==Product+Category+not+defined + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsName_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..592f71b16f31f3a18dbec2fa501ddd650dcec6f8 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsName_day.xml @@ -0,0 +1,13 @@ + + + + + 1000.22 + 2 + 1 + 500.11 + 2 + 0% + productName==PRODUCT+ONE+LEFT+in+cart + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..592f71b16f31f3a18dbec2fa501ddd650dcec6f8 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsName_week.xml @@ -0,0 +1,13 @@ + + + + + 1000.22 + 2 + 1 + 500.11 + 2 + 0% + productName==PRODUCT+ONE+LEFT+in+cart + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsSku_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..451f00e07877e2260bf119c5922b0436d07aacae --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsSku_day.xml @@ -0,0 +1,13 @@ + + + + + 1000.22 + 2 + 1 + 500.11 + 2 + 0% + productSku==SKU+IN+ABANDONED+CART+ONE + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..451f00e07877e2260bf119c5922b0436d07aacae --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions_AbandonedCarts__Goals.getItemsSku_week.xml @@ -0,0 +1,13 @@ + + + + + 1000.22 + 2 + 1 + 500.11 + 2 + 0% + productSku==SKU+IN+ABANDONED+CART+ONE + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getDaysToConversion_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getDaysToConversion_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..324df351356f164573ae83a12ab717085dd97554 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getDaysToConversion_day.xml @@ -0,0 +1,59 @@ + + + + + 1 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getDaysToConversion_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getDaysToConversion_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..324df351356f164573ae83a12ab717085dd97554 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getDaysToConversion_week.xml @@ -0,0 +1,59 @@ + + + + + 1 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getGoal.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getGoal.xml new file mode 100644 index 0000000000000000000000000000000000000000..d504ab9828b4b7e558999bb7b758c6e4eb47d89a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getGoal.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getGoals.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getGoals.xml new file mode 100644 index 0000000000000000000000000000000000000000..f69709ee98385022b6baa92f695c41e9130058d2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getGoals.xml @@ -0,0 +1,17 @@ + + + + 1 + 1 + title match, triggered NEVER + + title + saldkfjaslkdfjsalkdjf + contains + 0 + 1 + 10 + 0 + 0 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsCategory_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsCategory_day.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsCategory_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsName_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsName_day.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsName_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsSku_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsSku_day.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getItemsSku_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getMetrics_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getMetrics_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..4aff50748c6972c3d71277984965a1defd0a638c --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getMetrics_day.xml @@ -0,0 +1,9 @@ + + + 1 + 1 + 1000 + 2 + 1000 + 50% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getMetrics_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getMetrics_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..4aff50748c6972c3d71277984965a1defd0a638c --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getMetrics_week.xml @@ -0,0 +1,9 @@ + + + 1 + 1 + 1000 + 2 + 1000 + 50% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getVisitsUntilConversion_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getVisitsUntilConversion_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..017afd233ca9dc9ded4dbf0077aa1afe4cb20e9b --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getVisitsUntilConversion_day.xml @@ -0,0 +1,55 @@ + + + + + 1 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getVisitsUntilConversion_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getVisitsUntilConversion_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..017afd233ca9dc9ded4dbf0077aa1afe4cb20e9b --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.getVisitsUntilConversion_week.xml @@ -0,0 +1,55 @@ + + + + + 1 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..b5c424c2acde11c0d8fc24e43eb400cb97c555bc --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.get_day.xml @@ -0,0 +1,21 @@ + + + 1 + 1 + 1000 + 2 + 1000 + 50% + 1 + 1 + 1000 + 2 + 1000 + 50% + 0 + 0 + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..b5c424c2acde11c0d8fc24e43eb400cb97c555bc --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_abandonedCartWithoutConversions__Goals.get_week.xml @@ -0,0 +1,21 @@ + + + 1 + 1 + 1000 + 2 + 1000 + 50% + 1 + 1 + 1000 + 2 + 1000 + 50% + 0 + 0 + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_API_get__API.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_API_get__API.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..ebf215db8ead3c1a9524e21ff43e0df13c33f4ec --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_API_get__API.get_day.xml @@ -0,0 +1,7 @@ + + + 3 + 2 + 13 + 1801 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_API_get__API.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_API_get__API.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..e37996312bbb331e9272ba8cd88f782f7efbba99 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_API_get__API.get_week.xml @@ -0,0 +1,7 @@ + + + 5 + 4 + 16 + 1370 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..466de4fa8421600d9eb279e9a70b6a169bb18bdf --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_day.xml @@ -0,0 +1,95 @@ + + + + + 4000 + 4 + 2 + 1 + 2 + 2 + 1000 + 2 + 100% + productCategory==Category+TWO+LEFT+in+cart + + + + 4000 + 4 + 2 + 1 + 2 + 2 + 1000 + 2 + 100% + productCategory==second+category + + + + 1000.22 + 2 + 3 + 1 + 5 + 2 + 500.11 + 1 + 66.67% + productCategory==Product+Category+not+defined + + + + 20 + 2 + 3 + 2 + 6 + 2 + 10 + 1 + 66.67% + productCategory==Electronics+%26+Cameras + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+1 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+2 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+4 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+5 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..bcc003ddadea44df96739c86d9bf928525b7248b --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_week.xml @@ -0,0 +1,95 @@ + + + + + 6000 + 6 + 3 + 3 + 2 + 3 + 1000 + 2 + 100% + productCategory==Category+TWO+LEFT+in+cart + + + + 6000 + 6 + 3 + 3 + 2 + 3 + 1000 + 2 + 100% + productCategory==second+category + + + + 1500.33 + 3 + 4 + 7 + 2 + 3 + 500.11 + 1 + 75% + productCategory==Product+Category+not+defined + + + + 30 + 3 + 3 + 6 + 2 + 3 + 10 + 1 + 100% + productCategory==Electronics+%26+Cameras + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+1 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+2 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+4 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+5 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..603868d20faf32522af01ff18855e90a47d73116 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_day.xml @@ -0,0 +1,49 @@ + + + + + 4000 + 4 + 2 + 1 + 2 + 2 + 1000 + 2 + 100% + productName==PRODUCT+TWO+LEFT+in+cart + + + + 1000.22 + 2 + 2 + 500.11 + 1 + 0% + productName==PRODUCT+ONE+LEFT+in+cart + + + + 20 + 2 + 2 + 1 + 4 + 2 + 10 + 1 + 100% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 2 + 1 + 5 + 832.78 + 0 + 0% + productName==PRODUCT+name + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..62c30210e710970451a0dede93115dd441381929 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_week.xml @@ -0,0 +1,49 @@ + + + + + 6000 + 6 + 3 + 3 + 2 + 3 + 1000 + 2 + 100% + productName==PRODUCT+TWO+LEFT+in+cart + + + + 1500.33 + 3 + 3 + 500.11 + 1 + 0% + productName==PRODUCT+ONE+LEFT+in+cart + + + + 30 + 3 + 3 + 6 + 2 + 3 + 10 + 1 + 100% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 2 + 5 + 1 + 832.78 + 0 + 0% + productName==PRODUCT+name + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..d64eaed8ce186303a6fef6952b9977b24c3f58bd --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_day.xml @@ -0,0 +1,49 @@ + + + + + 4000 + 4 + 2 + 1 + 2 + 2 + 1000 + 2 + 100% + productSku==SKU+IN+ABANDONED+CART+TWO + + + + 1000.22 + 2 + 2 + 500.11 + 1 + 0% + productSku==SKU+IN+ABANDONED+CART+ONE + + + + 20 + 2 + 3 + 1 + 8 + 2 + 10 + 1 + 66.67% + productSku==SKU+VERY+nice+indeed + + + + 1 + 1 + 1 + 1111.11 + 0 + 0% + productSku==SKU2 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..8ca085fe0b66f517d1abbdd2435108b14758c15b --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_week.xml @@ -0,0 +1,49 @@ + + + + + 6000 + 6 + 3 + 3 + 2 + 3 + 1000 + 2 + 100% + productSku==SKU+IN+ABANDONED+CART+TWO + + + + 1500.33 + 3 + 3 + 500.11 + 1 + 0% + productSku==SKU+IN+ABANDONED+CART+ONE + + + + 30 + 3 + 4 + 10 + 2 + 3 + 10 + 1 + 75% + productSku==SKU+VERY+nice+indeed + + + + 1 + 1 + 1 + 1111.11 + 0 + 0% + productSku==SKU2 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AvgOrderRevenue__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AvgOrderRevenue__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..345702c9a6e10de19e25993d0a3a5681ea22aa56 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_AvgOrderRevenue__Goals.get_week.xml @@ -0,0 +1,6 @@ + + + 3337.78 + 0 + 3337.78 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_EcommerceOrderGoal_SegmentPageUrlContains__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_EcommerceOrderGoal_SegmentPageUrlContains__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..324e0a70bd220482a418ecb6eff412c4cd87796d --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_EcommerceOrderGoal_SegmentPageUrlContains__Goals.get_day.xml @@ -0,0 +1,33 @@ + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + 1555.56 + 100% + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0% + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + 1555.56 + 100% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_EcommerceOrderGoal_SegmentPageUrlContains__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_EcommerceOrderGoal_SegmentPageUrlContains__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..324e0a70bd220482a418ecb6eff412c4cd87796d --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_EcommerceOrderGoal_SegmentPageUrlContains__Goals.get_week.xml @@ -0,0 +1,33 @@ + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + 1555.56 + 100% + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0% + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + 1555.56 + 100% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..72c57859f35f129033ba175eac7f03ebc599bbc8 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_day.xml @@ -0,0 +1,21 @@ + + + 2 + 2 + 5020.22 + 8 + 2510.11 + 66.67% + 0 + 0 + 0 + 0 + 0 + 0% + 2 + 2 + 5020.22 + 8 + 2510.11 + 100% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..b174b63a6f728523a9a9bb7e5f9d5505b8406b94 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_week.xml @@ -0,0 +1,21 @@ + + + 3 + 3 + 7530.33 + 12 + 2510.11 + 60% + 0 + 0 + 0 + 0 + 0 + 0% + 3 + 3 + 7530.33 + 12 + 2510.11 + 75% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..bbfbfc601c16c8c7f1290f4b6b31cd28b07f03c7 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_day.xml @@ -0,0 +1,15 @@ + + + 1 + 1 + 10 + 33.33% + 1 + 1 + 10 + 100% + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..9c1f47244abe53bd93d138b3d33a1868f02370e1 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_week.xml @@ -0,0 +1,15 @@ + + + 1 + 1 + 10 + 20% + 1 + 1 + 10 + 100% + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..028c95e9471110ff8956e8803b721cd00a0f9917 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_day.xml @@ -0,0 +1,33 @@ + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + 1555.56 + 33.33% + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0% + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + 1555.56 + 50% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..2f03fafa0562c90f41dbfce203e1ca3184e0a8a2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_week.xml @@ -0,0 +1,33 @@ + + + 4 + 2 + 13351.11 + 2700 + 531 + 120.11 + 686 + 12 + 3337.78 + 40% + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0% + 4 + 2 + 13351.11 + 2700 + 531 + 120.11 + 686 + 12 + 3337.78 + 50% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..08432554eec70c97ac422dde3cec11b72c2d7de1 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_day.xml @@ -0,0 +1,15 @@ + + + 3 + 2 + 3121.11 + 66.67% + 1 + 1 + 10 + 100% + 2 + 1 + 3111.11 + 50% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6c6dacbd815b4e441eb3d68163af16a32faf7fd0 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_week.xml @@ -0,0 +1,15 @@ + + + 5 + 4 + 13361.11 + 80% + 1 + 1 + 10 + 100% + 4 + 3 + 13351.11 + 75% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_LiveEcommerceStatusOrdered__Live.getLastVisitsDetails_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_LiveEcommerceStatusOrdered__Live.getLastVisitsDetails_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..2a4504364249ffc98a526416db2bf50aaa5fdaa7 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_LiveEcommerceStatusOrdered__Live.getLastVisitsDetails_day.xml @@ -0,0 +1,417 @@ + + + + 1 + 5 + 156.5.3.2 + + c7eabd04b45e68b3 + + + ecommerceOrder + 666 + 240 + 200 + 20 + 20 + 20 + 2 + + + direct + + + + + TRIPOD SKU + TRIPOD - bought day after + Tools + 100 + 2 + + Tools + + + + plugins/Morpheus/images/ecommerceOrder.png + plugins/Morpheus/images/ecommerceOrder.svg + Ordered (666) + $240 revenue - 2 items: TRIPOD - bought day after) + + + + ecommerceOrder + 777 + 10000 + 0 + 0 + 0 + 0 + 0 + + + direct + + + + + plugins/Morpheus/images/ecommerceOrder.png + plugins/Morpheus/images/ecommerceOrder.svg + Ordered (777) + $10,000 revenue - 0 items: ) + + + + 0 + USD + $ + + + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + + + + + + + + returningCustomer + plugins/Live/images/returningVisitor.png + 1 + plugins/Morpheus/images/goal.svg + 5 + ordered + plugins/Morpheus/images/ecommerceOrder.svg + 1 + 110520 + 0 + 0 + 363 + 6 min 3s + 0 + 0 + 0 + direct + Direct Entry + + + + + + + + + + + pl + Polish + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Unknown + Generic Desktop + Windows XP + Windows + plugins/Morpheus/icons/dist/os/WIN.png + WIN + XP + Gecko + Gecko (Firefox) + Firefox 3.6 + Firefox + plugins/Morpheus/icons/dist/browsers/FF.png + FF + 3.6 + 13351.11 + 4 + 12 + 7530.33 + 3 + 12 + 0 + Europe + eur + Poland + pl + plugins/Morpheus/icons/dist/flags/pl.png + + + + Poland + + + 12:34:06 + 12 + 0 + 15480 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + VisitorName + Great name! + + + ValueIsZero + 0 + + + VisitorType + NewLoggedOut + + + + + 1 + 4 + 156.5.3.2 + + c7eabd04b45e68b3 + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 14 + 999 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 1 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 15 + 333 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 2 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 16 + PRODUCT TWO LEFT in cart + SKU IN ABANDONED CART TWO + + 361 + 6 min 1s + 3 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Category TWO LEFT in cart + second category + + 0 M + + + ecommerceAbandonedCart + 2510.11 + 4 + + + direct + + + + + SKU VERY nice indeed + PRODUCT THREE LEFT in cart + Electronics & Cameras + 10 + 1 + + Electronics & Cameras + + + + SKU IN ABANDONED CART ONE + PRODUCT ONE LEFT in cart + + 500.11 + 1 + + + + + SKU IN ABANDONED CART TWO + PRODUCT TWO LEFT in cart + Category TWO LEFT in cart + 1000 + 2 + + Category TWO LEFT in cart + second category + + + + plugins/Morpheus/images/ecommerceAbandonedCart.png + plugins/Morpheus/images/ecommerceAbandonedCart.svg + Abandoned Cart + $2,510.11 revenue - 4 items: PRODUCT THREE LEFT in cart, PRODUCT ONE LEFT in cart, PRODUCT TWO LEFT in cart) + + + + 0 + USD + $ + + + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + + + + + + + + returningCustomer + plugins/Live/images/returningVisitor.png + 1 + plugins/Morpheus/images/goal.svg + 4 + orderedThenAbandonedCart + plugins/Morpheus/images/ecommerceOrder.svg + 1 + 95040 + 0 + 0 + 1081 + 18 min 1s + 0 + 3 + 3 + direct + Direct Entry + + + + + + + + + + + pl + Polish + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Unknown + Generic Desktop + Windows XP + Windows + plugins/Morpheus/icons/dist/os/WIN.png + WIN + XP + Gecko + Gecko (Firefox) + Firefox 3.6 + Firefox + plugins/Morpheus/icons/dist/browsers/FF.png + FF + 3.6 + 13351.11 + 4 + 12 + 7530.33 + 3 + 12 + 0 + Europe + eur + Poland + pl + plugins/Morpheus/icons/dist/flags/pl.png + + + + Poland + + + 12:34:06 + 12 + 0 + 68040 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + VisitorName + Great name! + + + ValueIsZero + 0 + + + VisitorType + NewLoggedOut + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_AbandonedCart__API.getProcessedReport_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_AbandonedCart__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c8ca133daffed787377cb3b7d4825d9d8cb68412 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_AbandonedCart__API.getProcessedReport_day.xml @@ -0,0 +1,54 @@ + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + Tuesday, April 5, 2011 + + Ecommerce + Abandoned Carts + Goals + get + + ecommerceAbandonedCart + + https://matomo.org/docs/ecommerce-analytics/ + + Abandoned Carts + Conversion Rate + Revenue left in cart + Products left in cart + + + The percentage of visits that triggered a conversion. The conversion rate is calculated using the number of visits that converted at least one goal. Visits converting multiple goals are only counted once in the conversion rate. + + + Average Order Value + + + number + percent + money + number + money + + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=get&idGoal=ecommerceAbandonedCart&period=day&date=2011-03-07,2011-04-05 + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=get&idGoal=ecommerceAbandonedCart&period=day&date=2011-03-07,2011-04-05 + Goals_get_idGoal--ecommerceAbandonedCart + + + Abandoned Carts + Conversion Rate + Revenue left in cart + Products left in cart + Average Order Value + + + 2 + $5,020.22 + 8 + $2,510.11 + 66.67% + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_NormalGoal__API.getProcessedReport_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_NormalGoal__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..587575d40c446525202f3e595ed47bd0290ed748 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_NormalGoal__API.getProcessedReport_day.xml @@ -0,0 +1,52 @@ + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + Tuesday, April 5, 2011 + + Goals + Goal title match, triggered ONCE + Goals + get + + 1 + + This report gives an overview of how well your visitors convert a specific goal. + https://matomo.org/docs/tracking-goals-web-analytics/ + + Conversions + Visits with Conversions + Revenue + + + Number of visits where at least one goal was successfully converted. + The percentage of visits that triggered a conversion. The conversion rate is calculated using the number of visits that converted at least one goal. Visits converting multiple goals are only counted once in the conversion rate. + + + Conversion Rate + + + number + number + money + percent + + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=get&idGoal=1&period=day&date=2011-03-07,2011-04-05 + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=get&idGoal=1&period=day&date=2011-03-07,2011-04-05 + Goals_get_idGoal--1 + + + Conversions + Visits with Conversions + Revenue + Conversion Rate + + + 1 + 1 + $10 + 33.33% + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_NotExistingGoal__API.getProcessedReport_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_NotExistingGoal__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..1625ba026c9b14333c1516fd1e1713c7da408df4 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_NotExistingGoal__API.getProcessedReport_day.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_Order__API.getProcessedReport_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_Order__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..3d9524062f3e2afdd4bd24be76ac3d3119a630e3 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_Goals.Get_Order__API.getProcessedReport_day.xml @@ -0,0 +1,74 @@ + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + Tuesday, April 5, 2011 + + Ecommerce + Ecommerce Orders + Goals + get + + ecommerceOrder + + https://matomo.org/docs/ecommerce-analytics/ + + Ecommerce Orders + Visits with Conversions + Conversion Rate + Revenue + Subtotal + Tax + Shipping + Discount + Purchased Products + + + Number of visits where at least one goal was successfully converted. + The percentage of visits that triggered a conversion. The conversion rate is calculated using the number of visits that converted at least one goal. Visits converting multiple goals are only counted once in the conversion rate. + + + Average Order Value + + + number + number + percent + money + money + money + money + money + money + + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=get&idGoal=ecommerceOrder&period=day&date=2011-03-07,2011-04-05 + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=get&idGoal=ecommerceOrder&period=day&date=2011-03-07,2011-04-05 + Goals_get_idGoal--ecommerceOrder + + + Ecommerce Orders + Visits with Conversions + Conversion Rate + Revenue + Subtotal + Tax + Shipping + Discount + Purchased Products + Average Order Value + + + 2 + 1 + $3,111.11 + $2,500 + $511 + $100.11 + $666 + 10 + $1,555.56 + 33.33% + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_ItemsCategory__API.getProcessedReport_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_ItemsCategory__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..435880555144bb2bcd8a7d0d548cfaa4e1b7dfa7 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_ItemsCategory__API.getProcessedReport_day.xml @@ -0,0 +1,173 @@ + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + Tuesday, April 5, 2011 + + Ecommerce + Products + Product Category + Goals + getItemsCategory + Product Category + https://matomo.org/docs/ecommerce-analytics/ + + Product Revenue + Quantity + Unique Purchases + Visits + + + The total revenue generated by Product sales. Excludes tax, shipping and discount. + Quantity is the total number of products sold for each Product Category. + The total number of Ecommerce orders which contained this Product Category at least once. + The average price for this Product Category. + The average quantity of this Product Category sold in Ecommerce orders. + The number of visits on the Product/Category page. This is also used to process the Product Category conversion rate. This metric is in the report if Ecommerce view tracking was setup on Product/Category pages. + The Product Category conversion rate is the number of orders containing this product divided by number of visits on the product page. + + + Average Price + Average Quantity + Product Conversion Rate + + + money + number + number + number + money + number + percent + + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=getItemsCategory&period=day&date=2011-04-05 + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=getItemsCategory&period=day&date=2011-03-07,2011-04-05 + Goals_getItemsCategory + + + + Product Revenue + Quantity + Unique Purchases + Visits + Average Price + Average Quantity + Product Conversion Rate + + + + + $2,500 + 3 + 2 + 3 + $1,000 + 1.5 + 66.67% + + + + $1,000 + 2 + 1 + 1 + $500 + 2 + 100% + + + + $1,000 + 2 + 1 + 1 + $500 + 2 + 100% + + + + $1,000 + 2 + 1 + 1 + $500 + 2 + 100% + + + + $1,000 + 2 + 1 + 1 + $500 + 2 + 100% + + + + $611.22 + 7 + 2 + 3 + $55.61 + 3.5 + 66.67% + + + + 2 + $0 + 0 + 0% + $0 + 0 + 0 + + + + 2 + $0 + 0 + 0% + $0 + 0 + 0 + + + + + productCategory==Electronics+%26+Cameras + + + productCategory==Multiple+Category+1 + + + productCategory==Multiple+Category+2 + + + productCategory==Multiple+Category+4 + + + productCategory==Multiple+Category+5 + + + productCategory==Product+Category+not+defined + + + productCategory==Category+TWO+LEFT+in+cart + + + productCategory==second+category + + + + 7111.22 + 18 + 4111.22 + 8 + 14 + 9 + 19 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_ItemsSku__API.getProcessedReport_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_ItemsSku__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..36a9607cfbc5f6f0bdade7d6601724f2db065c77 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_ItemsSku__API.getProcessedReport_day.xml @@ -0,0 +1,121 @@ + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + Tuesday, April 5, 2011 + + Ecommerce + Products + Product SKU + Goals + getItemsSku + Product SKU + https://matomo.org/docs/ecommerce-analytics/ + + Product Revenue + Quantity + Unique Purchases + Visits + + + The total revenue generated by Product sales. Excludes tax, shipping and discount. + Quantity is the total number of products sold for each Product SKU. + The total number of Ecommerce orders which contained this Product SKU at least once. + The average price for this Product SKU. + The average quantity of this Product SKU sold in Ecommerce orders. + The number of visits on the Product/Category page. This is also used to process the Product SKU conversion rate. This metric is in the report if Ecommerce view tracking was setup on Product/Category pages. + The Product SKU conversion rate is the number of orders containing this product divided by number of visits on the product page. + + + Average Price + Average Quantity + Product Conversion Rate + + + money + number + number + number + money + number + percent + + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=getItemsSku&period=day&date=2011-04-05 + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Goals&apiAction=getItemsSku&period=day&date=2011-03-07,2011-04-05 + Goals_getItemsSku + + + + Product Revenue + Quantity + Unique Purchases + Visits + Average Price + Average Quantity + Product Conversion Rate + + + + + $1,500 + 1 + 1 + 1 + $1,500 + 1 + 100% + + + + $1,011.22 + 3 + 2 + 3 + $255.61 + 1.5 + 66.67% + + + + $600 + 6 + 1 + $100 + 6 + 0% + 0 + + + + 2 + $0 + 0 + 0% + $0 + 0 + 0 + + + + + productSku==SKU2 + + + productSku==SKU+VERY+nice+indeed + + + productSku==ANOTHER+SKU+HERE + + + productSku==SKU+IN+ABANDONED+CART+TWO + + + + 3111.22 + 10 + 2111.22 + 4 + 6 + 3 + 11 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_VisitTime.getVisitInformationPerServerTime__API.getProcessedReport_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_VisitTime.getVisitInformationPerServerTime__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..a06eeba1ccb8c843bcb159a810cd5f7fc3a692b1 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Metadata_VisitTime.getVisitInformationPerServerTime__API.getProcessedReport_day.xml @@ -0,0 +1,448 @@ + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + Tuesday, April 5, 2011 + + Visitors + Times + Visits per hour in the site's timezone + VisitTime + getVisitInformationPerServerTime + Site time — hour (start of visit) + This graph shows what time it was in the <strong> site's time zone </strong> during the visits. + + Visits + Unique visitors + Actions + Users + + + If a visitor comes to your website for the first time or if they visit a page more than 30 minutes after their last page view, this will be recorded as a new visit. + The number of unduplicated visitors coming to your website. Every user is only counted once, even if they visit the website multiple times a day. + The number of actions performed by your visitors. Actions can be page views, internal site searches, downloads or outlinks. + The number of users logged in your website. It is the number of unique active users that have a User ID set (via the Tracking code function 'setUserId'). + The average number of actions (page views, site searches, downloads or outlinks) that were performed during the visits. + The average duration of a visit. + The percentage of visits that only had a single pageview. This means, that the visitor left the website directly from the entrance page. + The percentage of visits that triggered a conversion. The conversion rate is calculated using the number of visits that converted at least one goal. Visits converting multiple goals are only counted once in the conversion rate. + + + Actions per Visit + Avg. Time on Website + Bounce Rate + + + number + number + number + number + number + duration_s + percent + percent + + 1 + + Conversions + Revenue + + + Revenue per Visit + + + money + number + percent + money + + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=VisitTime&apiAction=getVisitInformationPerServerTime&period=day&date=2011-04-05 + VisitTime_getVisitInformationPerServerTime + + + + Visits + Unique visitors + Actions + Users + Actions per Visit + Avg. Time on Website + Bounce Rate + Revenue + + + + + 1 + 1 + 4 + 0 + $10 + 4 + 00:12:02 + 0% + + + + 1 + 1 + 6 + 0 + 6 + 01:06:01 + 0% + $0 + + + + $3,111.11 + 0 + 0 + 00:00:00 + 0% + 0 + 0 + 0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 1 + 1 + 3 + 0 + $0 + 3 + 00:12:01 + 0% + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + 0 + 0 + 0 + 0 + 0 + 00:00:00 + 0% + $0 + + + + + visitStartServerHour==0 + + + visitStartServerHour==1 + + + visitStartServerHour==2 + + + visitStartServerHour==3 + + + visitStartServerHour==4 + + + visitStartServerHour==5 + + + visitStartServerHour==6 + + + visitStartServerHour==7 + + + visitStartServerHour==8 + + + visitStartServerHour==9 + + + visitStartServerHour==10 + + + visitStartServerHour==11 + + + visitStartServerHour==12 + + + visitStartServerHour==13 + + + visitStartServerHour==14 + + + visitStartServerHour==15 + + + visitStartServerHour==16 + + + visitStartServerHour==17 + + + visitStartServerHour==18 + + + visitStartServerHour==19 + + + visitStartServerHour==20 + + + visitStartServerHour==21 + + + visitStartServerHour==22 + + + visitStartServerHour==23 + + + + 3 + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + 4.3 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentAbandonedCart__VisitsSummary.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentAbandonedCart__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ecf6934746b019c8b7a6c903d049561a1d4c7fe --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentAbandonedCart__VisitsSummary.get_day.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 2 + 9 + 1 + 0 + 4682 + 6 + 0% + 4.5 + 2341 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCartRevenueOrder__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCartRevenueOrder__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..0401f9f52e1a0aaacc9a1dedd141162fef6f8e63 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCartRevenueOrder__Goals.get_day.xml @@ -0,0 +1,15 @@ + + + 2 + 1 + 3111.11 + 50% + 0 + 0 + 0 + 0% + 2 + 1 + 3111.11 + 50% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCartRevenueOrder__Live.getLastVisitsDetails_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCartRevenueOrder__Live.getLastVisitsDetails_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..27d2ed1255673e69fc44a5e16fe97119e30f25c9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCartRevenueOrder__Live.getLastVisitsDetails_day.xml @@ -0,0 +1,634 @@ + + + + 1 + 3 + 156.5.3.2 + + c7eabd04b45e68b3 + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 11 + 999 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 1 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 12 + 333 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 2 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 13 + PRODUCT TWO LEFT in cart + SKU IN ABANDONED CART TWO + + 361 + 6 min 1s + 3 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Category TWO LEFT in cart + second category + + 0 M + + + ecommerceAbandonedCart + 2510.11 + 4 + + + direct + + + + + SKU VERY nice indeed + PRODUCT THREE LEFT in cart + Electronics & Cameras + 10 + 1 + + Electronics & Cameras + + + + SKU IN ABANDONED CART ONE + PRODUCT ONE LEFT in cart + + 500.11 + 1 + + + + + SKU IN ABANDONED CART TWO + PRODUCT TWO LEFT in cart + Category TWO LEFT in cart + 1000 + 2 + + Category TWO LEFT in cart + second category + + + + plugins/Morpheus/images/ecommerceAbandonedCart.png + plugins/Morpheus/images/ecommerceAbandonedCart.svg + Abandoned Cart + $2,510.11 revenue - 4 items: PRODUCT THREE LEFT in cart, PRODUCT ONE LEFT in cart, PRODUCT TWO LEFT in cart) + + + + 0 + USD + $ + + + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + + + + + + + + returning + plugins/Live/images/returningVisitor.png + 0 + + 3 + abandonedCart + plugins/Morpheus/images/ecommerceAbandonedCart.svg + 0 + 27000 + 0 + 21240 + 721 + 12 min 1s + 0 + 3 + 3 + direct + Direct Entry + + + + + + + + + + + pl + Polish + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Unknown + Generic Desktop + Windows XP + Windows + plugins/Morpheus/icons/dist/os/WIN.png + WIN + XP + Gecko + Gecko (Firefox) + Firefox 3.6 + Firefox + plugins/Morpheus/icons/dist/browsers/FF.png + FF + 3.6 + 13351.11 + 4 + 12 + 7530.33 + 3 + 12 + 0 + Europe + eur + Poland + pl + plugins/Morpheus/icons/dist/flags/pl.png + + + + Poland + + + 12:34:06 + 12 + 0 + 21240 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + VisitorName + Great name! + + + ValueIsZero + 0 + + + VisitorType + NewLoggedOut + + + + + 1 + 2 + 156.5.3.2 + + c7eabd04b45e68b3 + + + action + http://example.org/index.htm + Looking at Electronics & Cameras page with a page level custom variable + 2 + + + 5 + + 360 + 6 min 0s + 1 + Looking at Electronics & Cameras page with a page level custom variable + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + action + http://example.org/index.htm + Looking at Electronics & Cameras page again + 2 + + + 6 + + 360 + 6 min 0s + 2 + Looking at Electronics & Cameras page again + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + action + http://example.org/index.htm + Looking at product page + 2 + + + 7 + 666 + PRODUCT name + SKU VERY nice indeed + + 720 + 12 min 0s + 3 + Looking at product page + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + ecommerceOrder + 937nsjusu 3894 + 1111.11 + 1000 + 111 + 0.11 + 666 + 8 + + + direct + + + + + SKU VERY nice indeed + PRODUCT name + Multiple Category 1 + 500 + 2 + + Multiple Category 1 + Multiple Category 2 + Electronics & Cameras + Multiple Category 4 + Multiple Category 5 + + + + ANOTHER SKU HERE + PRODUCT name BIS + + 100 + 6 + + + + + plugins/Morpheus/images/ecommerceOrder.png + plugins/Morpheus/images/ecommerceOrder.svg + Ordered (937nsjusu 3894) + $1,111.11 revenue - 8 items: PRODUCT name, PRODUCT name BIS) + + + + ecommerceOrder + 1037nsjusu4s3894 + 2000 + 1500 + 400 + 100 + 0 + 2 + + + direct + + + + + SKU2 + Canon SLR + Electronics & Cameras + 1500 + 1 + + Electronics & Cameras + + + + SKU VERY nice indeed + PRODUCT name + + 11.22 + 1 + + + + + plugins/Morpheus/images/ecommerceOrder.png + plugins/Morpheus/images/ecommerceOrder.svg + Ordered (1037nsjusu4s3894) + $2,000 revenue - 2 items: Canon SLR, PRODUCT name) + + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 8 + 999 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 4 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 9 + 333 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 5 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 10 + PRODUCT TWO LEFT in cart + SKU IN ABANDONED CART TWO + + 361 + 6 min 1s + 6 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Category TWO LEFT in cart + second category + + 0 M + + + ecommerceAbandonedCart + 2510.11 + 4 + + + direct + + + + + SKU VERY nice indeed + PRODUCT THREE LEFT in cart + Electronics & Cameras + 10 + 1 + + Electronics & Cameras + + + + SKU IN ABANDONED CART ONE + PRODUCT ONE LEFT in cart + + 500.11 + 1 + + + + + SKU IN ABANDONED CART TWO + PRODUCT TWO LEFT in cart + Category TWO LEFT in cart + 1000 + 2 + + Category TWO LEFT in cart + second category + + + + plugins/Morpheus/images/ecommerceAbandonedCart.png + plugins/Morpheus/images/ecommerceAbandonedCart.svg + Abandoned Cart + $2,510.11 revenue - 4 items: PRODUCT THREE LEFT in cart, PRODUCT ONE LEFT in cart, PRODUCT TWO LEFT in cart) + + + + 0 + USD + $ + + + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + + + + + + + + returning + plugins/Live/images/returningVisitor.png + 1 + plugins/Morpheus/images/goal.svg + 2 + orderedThenAbandonedCart + plugins/Morpheus/images/ecommerceOrder.svg + 0 + 5760 + 0 + 0 + 3961 + 1 hours 6 min + 0 + 6 + 6 + direct + Direct Entry + + + + + + + + + + + pl + Polish + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Unknown + Generic Desktop + Windows XP + Windows + plugins/Morpheus/icons/dist/os/WIN.png + WIN + XP + Gecko + Gecko (Firefox) + Firefox 3.6 + Firefox + plugins/Morpheus/icons/dist/browsers/FF.png + FF + 3.6 + 13351.11 + 4 + 12 + 7530.33 + 3 + 12 + 0 + Europe + eur + Poland + pl + plugins/Morpheus/icons/dist/flags/pl.png + + + + Poland + + + 12:34:06 + 12 + 0 + 5760 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + VisitorName + Great name! + + + ValueIsZero + 0 + + + VisitorType + NewLoggedOut + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentConvertedGoalId1__VisitsSummary.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentConvertedGoalId1__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..b33de3470dd90c8d217013509372015245cce8b2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentConvertedGoalId1__VisitsSummary.get_day.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 4 + 1 + 0 + 722 + 4 + 0% + 4 + 722 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentConvertedGoalId1__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentConvertedGoalId1__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..b33de3470dd90c8d217013509372015245cce8b2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentConvertedGoalId1__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 4 + 1 + 0 + 722 + 4 + 0% + 4 + 722 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsCategory_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..5b82befc68cea991a5d47c7287f2c3b9b806d72e --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsCategory_day.xml @@ -0,0 +1,63 @@ + + + + + 2 + 2 + 3 + 888.56 + 0 + 0% + productCategory==Electronics+%26+Cameras + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+1 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+2 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+4 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+5 + + + + 1 + 1 + 1 + 888 + 0 + 0% + productCategory==Product+Category+not+defined + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..4cb08823638f75977ac3765157880a2f1a55cb86 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsCategory_week.xml @@ -0,0 +1,63 @@ + + + + + 2 + 3 + 2 + 888.56 + 0 + 0% + productCategory==Electronics+%26+Cameras + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+1 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+2 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+4 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+5 + + + + 1 + 1 + 1 + 888 + 0 + 0% + productCategory==Product+Category+not+defined + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsName_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..34ea36454e7954cb6e4747c0bcae77f8c31bc6a0 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsName_day.xml @@ -0,0 +1,13 @@ + + + + + 1 + 1 + 4 + 888.37 + 0 + 0% + productName==PRODUCT+name + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..ecaaed1d4e343a65271c2504cd2032a6d6d5abfa --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsName_week.xml @@ -0,0 +1,13 @@ + + + + + 1 + 4 + 1 + 888.37 + 0 + 0% + productName==PRODUCT+name + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsSku_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..3985245252123bc8e9dc38df7aff865cfe7b1148 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsSku_day.xml @@ -0,0 +1,23 @@ + + + + + 1 + 1 + 1 + 1111.11 + 0 + 0% + productSku==SKU2 + + + + 1 + 1 + 3 + 777 + 0 + 0% + productSku==SKU+VERY+nice+indeed + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..e502e9a0ae35e73136d12a337434863eef9d83e9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.getItemsSku_week.xml @@ -0,0 +1,23 @@ + + + + + 1 + 1 + 1 + 1111.11 + 0 + 0% + productSku==SKU2 + + + + 1 + 3 + 1 + 777 + 0 + 0% + productSku==SKU+VERY+nice+indeed + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..6306af1f2b352056abc37241d929a54c5994b589 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.get_day.xml @@ -0,0 +1,15 @@ + + + 1 + 1 + 10 + 100% + 1 + 1 + 10 + 100% + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6306af1f2b352056abc37241d929a54c5994b589 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentCountryIsFr__Goals.get_week.xml @@ -0,0 +1,15 @@ + + + 1 + 1 + 10 + 100% + 1 + 1 + 10 + 100% + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentDidNotConvertGoalId1__VisitsSummary.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentDidNotConvertGoalId1__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ecf6934746b019c8b7a6c903d049561a1d4c7fe --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentDidNotConvertGoalId1__VisitsSummary.get_day.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 2 + 9 + 1 + 0 + 4682 + 6 + 0% + 4.5 + 2341 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNewVisitors__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNewVisitors__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..b33de3470dd90c8d217013509372015245cce8b2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNewVisitors__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 4 + 1 + 0 + 722 + 4 + 0% + 4 + 722 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoEcommerce__VisitsSummary.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoEcommerce__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..b33de3470dd90c8d217013509372015245cce8b2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoEcommerce__VisitsSummary.get_day.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 4 + 1 + 0 + 722 + 4 + 0% + 4 + 722 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsCategory_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsName_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.getItemsSku_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..36db013c1a9aafbc3ed08f31d7f0fd697f02a236 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__Goals.get_week.xml @@ -0,0 +1,15 @@ + + + 0 + 0 + 0 + 0% + 0 + 0 + 0 + 0% + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..32b66284bedbe458384d8a08a7b9a61b9d01f775 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentNoVisit_HaveConvertedNonExistingGoal__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0% + 0 + 0 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentOrderedSomething__VisitsSummary.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentOrderedSomething__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..44b18e613c2372108d964340f7ec8a0b8c9c9718 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentOrderedSomething__VisitsSummary.get_day.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 6 + 1 + 0 + 3961 + 6 + 0% + 6 + 3961 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageTitleMatch__VisitsSummary.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageTitleMatch__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..b33de3470dd90c8d217013509372015245cce8b2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageTitleMatch__VisitsSummary.get_day.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 4 + 1 + 0 + 722 + 4 + 0% + 4 + 722 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsCategory_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..4affcda1cab56f2dacdccc2926c3e58a53bd8bad --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsCategory_day.xml @@ -0,0 +1,101 @@ + + + + + 2500 + 3 + 2 + 3 + 2 + 6 + 1000 + 1.5 + 66.67% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+5 + + + + 611.22 + 7 + 2 + 3 + 1 + 5 + 55.61 + 3.5 + 66.67% + productCategory==Product+Category+not+defined + + + + 2 + 1 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 2 + 1 + 2 + 0 + 0 + 0% + productCategory==second+category + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..04c1014b81ff5b355a980f51ff3d92aceb692c3d --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsCategory_week.xml @@ -0,0 +1,101 @@ + + + + + 2500 + 3 + 2 + 3 + 6 + 2 + 1000 + 1.5 + 66.67% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+5 + + + + 611.22 + 7 + 2 + 4 + 7 + 2 + 55.61 + 3.5 + 50% + productCategory==Product+Category+not+defined + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==second+category + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsName_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..d58fdf35cb40b85e953a16a3f3863d0ea1da703f --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsName_day.xml @@ -0,0 +1,56 @@ + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 1011.22 + 3 + 2 + 2 + 1 + 5 + 255.61 + 1.5 + 100% + productName==PRODUCT+name + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + + + 2 + 1 + 4 + 666 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 2 + 1 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..057fbf993fa49406168bc648cf050b173ba9173c --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsName_week.xml @@ -0,0 +1,56 @@ + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 1011.22 + 3 + 2 + 2 + 5 + 1 + 255.61 + 1.5 + 100% + productName==PRODUCT+name + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + + + 3 + 6 + 2 + 1332 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsSku_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..177b4f375b50541da97fe6ff7cd89efa6f7887db --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsSku_day.xml @@ -0,0 +1,49 @@ + + + + + 1500 + 1 + 1 + 1 + 1 + 1 + 1500 + 1 + 100% + productSku==SKU2 + + + + 1011.22 + 3 + 2 + 3 + 1 + 8 + 255.61 + 1.5 + 66.67% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 2 + 1 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..d94d560b8e2b3e3a58bb6b511e1e4bdcb8f59fc9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentPageUrlContains__Goals.getItemsSku_week.xml @@ -0,0 +1,49 @@ + + + + + 1500 + 1 + 1 + 1 + 1 + 1 + 1500 + 1 + 100% + productSku==SKU2 + + + + 1011.22 + 3 + 2 + 4 + 10 + 2 + 255.61 + 1.5 + 50% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 3 + 3 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentReturningCustomers__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentReturningCustomers__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..db58c5b88118c6b84aa53c348bdb04a944ed5b03 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentReturningCustomers__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 2 + 3 + 2 + 1 + 1444 + 3 + 50% + 1.5 + 722 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentReturningVisitors__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentReturningVisitors__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ecf6934746b019c8b7a6c903d049561a1d4c7fe --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentReturningVisitors__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 2 + 9 + 1 + 0 + 4682 + 6 + 0% + 4.5 + 2341 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentRevenueOrder__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentRevenueOrder__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1ad070cccdd62958f4a2ee758b086e98fc6c35a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentRevenueOrder__Goals.get_day.xml @@ -0,0 +1,15 @@ + + + 2 + 1 + 3111.11 + 100% + 0 + 0 + 0 + 0% + 2 + 1 + 3111.11 + 100% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentRevenueOrder__Live.getLastVisitsDetails_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentRevenueOrder__Live.getLastVisitsDetails_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..13a18e0c0313e283f5b19923fb3b1a9c5cbd4b3a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentRevenueOrder__Live.getLastVisitsDetails_day.xml @@ -0,0 +1,396 @@ + + + + 1 + 2 + 156.5.3.2 + + c7eabd04b45e68b3 + + + action + http://example.org/index.htm + Looking at Electronics & Cameras page with a page level custom variable + 2 + + + 5 + + 360 + 6 min 0s + 1 + Looking at Electronics & Cameras page with a page level custom variable + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + action + http://example.org/index.htm + Looking at Electronics & Cameras page again + 2 + + + 6 + + 360 + 6 min 0s + 2 + Looking at Electronics & Cameras page again + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + action + http://example.org/index.htm + Looking at product page + 2 + + + 7 + 666 + PRODUCT name + SKU VERY nice indeed + + 720 + 12 min 0s + 3 + Looking at product page + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + ecommerceOrder + 937nsjusu 3894 + 1111.11 + 1000 + 111 + 0.11 + 666 + 8 + + + direct + + + + + SKU VERY nice indeed + PRODUCT name + Multiple Category 1 + 500 + 2 + + Multiple Category 1 + Multiple Category 2 + Electronics & Cameras + Multiple Category 4 + Multiple Category 5 + + + + ANOTHER SKU HERE + PRODUCT name BIS + + 100 + 6 + + + + + plugins/Morpheus/images/ecommerceOrder.png + plugins/Morpheus/images/ecommerceOrder.svg + Ordered (937nsjusu 3894) + $1,111.11 revenue - 8 items: PRODUCT name, PRODUCT name BIS) + + + + ecommerceOrder + 1037nsjusu4s3894 + 2000 + 1500 + 400 + 100 + 0 + 2 + + + direct + + + + + SKU2 + Canon SLR + Electronics & Cameras + 1500 + 1 + + Electronics & Cameras + + + + SKU VERY nice indeed + PRODUCT name + + 11.22 + 1 + + + + + plugins/Morpheus/images/ecommerceOrder.png + plugins/Morpheus/images/ecommerceOrder.svg + Ordered (1037nsjusu4s3894) + $2,000 revenue - 2 items: Canon SLR, PRODUCT name) + + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 8 + 999 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 4 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 9 + 333 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 5 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 10 + PRODUCT TWO LEFT in cart + SKU IN ABANDONED CART TWO + + 361 + 6 min 1s + 6 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Category TWO LEFT in cart + second category + + 0 M + + + ecommerceAbandonedCart + 2510.11 + 4 + + + direct + + + + + SKU VERY nice indeed + PRODUCT THREE LEFT in cart + Electronics & Cameras + 10 + 1 + + Electronics & Cameras + + + + SKU IN ABANDONED CART ONE + PRODUCT ONE LEFT in cart + + 500.11 + 1 + + + + + SKU IN ABANDONED CART TWO + PRODUCT TWO LEFT in cart + Category TWO LEFT in cart + 1000 + 2 + + Category TWO LEFT in cart + second category + + + + plugins/Morpheus/images/ecommerceAbandonedCart.png + plugins/Morpheus/images/ecommerceAbandonedCart.svg + Abandoned Cart + $2,510.11 revenue - 4 items: PRODUCT THREE LEFT in cart, PRODUCT ONE LEFT in cart, PRODUCT TWO LEFT in cart) + + + + 0 + USD + $ + + + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + + + + + + + + returning + plugins/Live/images/returningVisitor.png + 1 + plugins/Morpheus/images/goal.svg + 2 + orderedThenAbandonedCart + plugins/Morpheus/images/ecommerceOrder.svg + 0 + 5760 + 0 + 0 + 3961 + 1 hours 6 min + 0 + 6 + 6 + direct + Direct Entry + + + + + + + + + + + pl + Polish + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Unknown + Generic Desktop + Windows XP + Windows + plugins/Morpheus/icons/dist/os/WIN.png + WIN + XP + Gecko + Gecko (Firefox) + Firefox 3.6 + Firefox + plugins/Morpheus/icons/dist/browsers/FF.png + FF + 3.6 + 13351.11 + 4 + 12 + 7530.33 + 3 + 12 + 0 + Europe + eur + Poland + pl + plugins/Morpheus/icons/dist/flags/pl.png + + + + Poland + + + 12:34:06 + 12 + 0 + 5760 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + VisitorName + Great name! + + + ValueIsZero + 0 + + + VisitorType + NewLoggedOut + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..4cb08823638f75977ac3765157880a2f1a55cb86 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsCategory_week.xml @@ -0,0 +1,63 @@ + + + + + 2 + 3 + 2 + 888.56 + 0 + 0% + productCategory==Electronics+%26+Cameras + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+1 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+2 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+4 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+5 + + + + 1 + 1 + 1 + 888 + 0 + 0% + productCategory==Product+Category+not+defined + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..ecaaed1d4e343a65271c2504cd2032a6d6d5abfa --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsName_week.xml @@ -0,0 +1,13 @@ + + + + + 1 + 4 + 1 + 888.37 + 0 + 0% + productName==PRODUCT+name + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..e502e9a0ae35e73136d12a337434863eef9d83e9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.getItemsSku_week.xml @@ -0,0 +1,23 @@ + + + + + 1 + 1 + 1 + 1111.11 + 0 + 0% + productSku==SKU2 + + + + 1 + 3 + 1 + 777 + 0 + 0% + productSku==SKU+VERY+nice+indeed + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6306af1f2b352056abc37241d929a54c5994b589 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__Goals.get_week.xml @@ -0,0 +1,15 @@ + + + 1 + 1 + 10 + 100% + 1 + 1 + 10 + 100% + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..b33de3470dd90c8d217013509372015245cce8b2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasConvertedGoal__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 4 + 1 + 0 + 722 + 4 + 0% + 4 + 722 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..4cb08823638f75977ac3765157880a2f1a55cb86 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsCategory_week.xml @@ -0,0 +1,63 @@ + + + + + 2 + 3 + 2 + 888.56 + 0 + 0% + productCategory==Electronics+%26+Cameras + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+1 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+2 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+4 + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Multiple+Category+5 + + + + 1 + 1 + 1 + 888 + 0 + 0% + productCategory==Product+Category+not+defined + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..ecaaed1d4e343a65271c2504cd2032a6d6d5abfa --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsName_week.xml @@ -0,0 +1,13 @@ + + + + + 1 + 4 + 1 + 888.37 + 0 + 0% + productName==PRODUCT+name + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..e502e9a0ae35e73136d12a337434863eef9d83e9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.getItemsSku_week.xml @@ -0,0 +1,23 @@ + + + + + 1 + 1 + 1 + 1111.11 + 0 + 0% + productSku==SKU2 + + + + 1 + 3 + 1 + 777 + 0 + 0% + productSku==SKU+VERY+nice+indeed + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6306af1f2b352056abc37241d929a54c5994b589 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__Goals.get_week.xml @@ -0,0 +1,15 @@ + + + 1 + 1 + 10 + 100% + 1 + 1 + 10 + 100% + 0 + 0 + 0 + 0% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..b33de3470dd90c8d217013509372015245cce8b2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_SegmentVisitHasNotOrderedAndConvertedGoal__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 4 + 1 + 0 + 722 + 4 + 0% + 4 + 722 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsCategory_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsName_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.getItemsSku_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..0750637f7a553fcf2af12f368aeaabd9f211915c --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_Website2__Goals.get_week.xml @@ -0,0 +1,15 @@ + + + 0 + 1 + 0 + 50% + 0 + 0 + 0 + 0% + 0 + 1 + 0 + 100% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__API.getProcessedReport_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..aed4c3f4cfae444de31a4c5b4d73a9d65e9b3f95 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__API.getProcessedReport_day.xml @@ -0,0 +1,146 @@ + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + Tuesday, April 5, 2011 + + Visitors + Locations + Country + UserCountry + getCountry + Country + Shows which country your visitors connected from when accessing your website. + + Visits + Unique visitors + Actions + Users + + + If a visitor comes to your website for the first time or if they visit a page more than 30 minutes after their last page view, this will be recorded as a new visit. + The number of unduplicated visitors coming to your website. Every user is only counted once, even if they visit the website multiple times a day. + The number of actions performed by your visitors. Actions can be page views, internal site searches, downloads or outlinks. + The number of users logged in your website. It is the number of unique active users that have a User ID set (via the Tracking code function 'setUserId'). + The average number of actions (page views, site searches, downloads or outlinks) that were performed during the visits. + The average duration of a visit. + The percentage of visits that only had a single pageview. This means, that the visitor left the website directly from the entrance page. + The percentage of visits that triggered a conversion. The conversion rate is calculated using the number of visits that converted at least one goal. Visits converting multiple goals are only counted once in the conversion rate. + + + Actions per Visit + Avg. Time on Website + Bounce Rate + + + number + number + number + number + number + duration_s + percent + percent + + + Conversions + Revenue + + + Revenue per Visit + + + money + number + percent + money + + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=UserCountry&apiAction=getCountry&period=day&date=2011-04-05 + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=UserCountry&apiAction=getCountry&period=day&date=2011-03-07,2011-04-05 + UserCountry_getCountry + + + + Visits + Unique visitors + Actions + Users + Actions per Visit + Avg. Time on Website + Bounce Rate + Revenue + + + + + 1 + 2 + 9 + 0 + $3,111.11 + 4.5 + 00:39:01 + 0% + + + + 1 + 1 + 4 + 0 + $10 + 4 + 00:12:02 + 0% + + + + + pl + plugins/Morpheus/icons/dist/flags/pl.png + countryCode==pl + 16 + + + fr + plugins/Morpheus/icons/dist/flags/fr.png + countryCode==fr + 16 + + + + 2 + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + 4.3 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__CustomVariables.getCustomVariables_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__CustomVariables.getCustomVariables_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..5cdfa1a27e1df14b3602c2062843d70c28a1be2f --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__CustomVariables.getCustomVariables_day.xml @@ -0,0 +1,230 @@ + + + + + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + + + visit + 4 + + + customVariableName==ValueIsZero + + + + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + + + + + + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + + + visit + 5 + + + customVariableName==VisitorType + + + + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + + + + + + 2 + 9 + 0 + 6 + 4682 + 0 + 1 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 2 + 3111.11 + + + visit + 3 + + + customVariableName==VisitorName + + + + 2 + 9 + 0 + 6 + 4682 + 0 + 1 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 2 + 3111.11 + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..4affcda1cab56f2dacdccc2926c3e58a53bd8bad --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_day.xml @@ -0,0 +1,101 @@ + + + + + 2500 + 3 + 2 + 3 + 2 + 6 + 1000 + 1.5 + 66.67% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+5 + + + + 611.22 + 7 + 2 + 3 + 1 + 5 + 55.61 + 3.5 + 66.67% + productCategory==Product+Category+not+defined + + + + 2 + 1 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 2 + 1 + 2 + 0 + 0 + 0% + productCategory==second+category + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..24c02a3147fec08ca237ce32ee3b7047d08c0d91 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_week.xml @@ -0,0 +1,111 @@ + + + + + 2500 + 3 + 2 + 3 + 6 + 2 + 1000 + 1.5 + 66.67% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+5 + + + + 611.22 + 7 + 2 + 4 + 7 + 2 + 55.61 + 3.5 + 50% + productCategory==Product+Category+not+defined + + + + 200 + 2 + 1 + 100 + 2 + 0% + productCategory==Tools + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==second+category + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsName_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..d58fdf35cb40b85e953a16a3f3863d0ea1da703f --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsName_day.xml @@ -0,0 +1,56 @@ + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 1011.22 + 3 + 2 + 2 + 1 + 5 + 255.61 + 1.5 + 100% + productName==PRODUCT+name + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + + + 2 + 1 + 4 + 666 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 2 + 1 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..5b1000ca6e2cec0cb945540ac3566f880c9d0d32 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsName_week.xml @@ -0,0 +1,66 @@ + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 1011.22 + 3 + 2 + 2 + 5 + 1 + 255.61 + 1.5 + 100% + productName==PRODUCT+name + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + + + 200 + 2 + 1 + 100 + 2 + 0% + productName==TRIPOD+-+bought+day+after + + + + 3 + 6 + 2 + 1332 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..177b4f375b50541da97fe6ff7cd89efa6f7887db --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_day.xml @@ -0,0 +1,49 @@ + + + + + 1500 + 1 + 1 + 1 + 1 + 1 + 1500 + 1 + 100% + productSku==SKU2 + + + + 1011.22 + 3 + 2 + 3 + 1 + 8 + 255.61 + 1.5 + 66.67% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 2 + 1 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..b1c114f8c178105befe475bd3635702bbeefe428 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_week.xml @@ -0,0 +1,59 @@ + + + + + 1500 + 1 + 1 + 1 + 1 + 1 + 1500 + 1 + 100% + productSku==SKU2 + + + + 1011.22 + 3 + 2 + 4 + 10 + 2 + 255.61 + 1.5 + 50% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 200 + 2 + 1 + 100 + 2 + 0% + productSku==TRIPOD+SKU + + + + 3 + 3 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..08432554eec70c97ac422dde3cec11b72c2d7de1 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.get_day.xml @@ -0,0 +1,15 @@ + + + 3 + 2 + 3121.11 + 66.67% + 1 + 1 + 10 + 100% + 2 + 1 + 3111.11 + 50% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6c6dacbd815b4e441eb3d68163af16a32faf7fd0 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Goals.get_week.xml @@ -0,0 +1,15 @@ + + + 5 + 4 + 13361.11 + 80% + 1 + 1 + 10 + 100% + 4 + 3 + 13351.11 + 75% + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Live.getLastVisitsDetails_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Live.getLastVisitsDetails_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..5885fd2b044aa9fe56680e5315392bb068e5feae --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__Live.getLastVisitsDetails_day.xml @@ -0,0 +1,870 @@ + + + + 1 + 3 + 156.5.3.2 + + c7eabd04b45e68b3 + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 11 + 999 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 1 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 12 + 333 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 2 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 13 + PRODUCT TWO LEFT in cart + SKU IN ABANDONED CART TWO + + 361 + 6 min 1s + 3 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Category TWO LEFT in cart + second category + + 0 M + + + ecommerceAbandonedCart + 2510.11 + 4 + + + direct + + + + + SKU VERY nice indeed + PRODUCT THREE LEFT in cart + Electronics & Cameras + 10 + 1 + + Electronics & Cameras + + + + SKU IN ABANDONED CART ONE + PRODUCT ONE LEFT in cart + + 500.11 + 1 + + + + + SKU IN ABANDONED CART TWO + PRODUCT TWO LEFT in cart + Category TWO LEFT in cart + 1000 + 2 + + Category TWO LEFT in cart + second category + + + + plugins/Morpheus/images/ecommerceAbandonedCart.png + plugins/Morpheus/images/ecommerceAbandonedCart.svg + Abandoned Cart + $2,510.11 revenue - 4 items: PRODUCT THREE LEFT in cart, PRODUCT ONE LEFT in cart, PRODUCT TWO LEFT in cart) + + + + 0 + USD + $ + + + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + + + + + + + + returning + plugins/Live/images/returningVisitor.png + 0 + + 3 + abandonedCart + plugins/Morpheus/images/ecommerceAbandonedCart.svg + 0 + 27000 + 0 + 21240 + 721 + 12 min 1s + 0 + 3 + 3 + direct + Direct Entry + + + + + + + + + + + pl + Polish + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Unknown + Generic Desktop + Windows XP + Windows + plugins/Morpheus/icons/dist/os/WIN.png + WIN + XP + Gecko + Gecko (Firefox) + Firefox 3.6 + Firefox + plugins/Morpheus/icons/dist/browsers/FF.png + FF + 3.6 + 13351.11 + 4 + 12 + 7530.33 + 3 + 12 + 0 + Europe + eur + Poland + pl + plugins/Morpheus/icons/dist/flags/pl.png + + + + Poland + + + 12:34:06 + 12 + 0 + 21240 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + VisitorName + Great name! + + + ValueIsZero + 0 + + + VisitorType + NewLoggedOut + + + + + 1 + 2 + 156.5.3.2 + + c7eabd04b45e68b3 + + + action + http://example.org/index.htm + Looking at Electronics & Cameras page with a page level custom variable + 2 + + + 5 + + 360 + 6 min 0s + 1 + Looking at Electronics & Cameras page with a page level custom variable + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + action + http://example.org/index.htm + Looking at Electronics & Cameras page again + 2 + + + 6 + + 360 + 6 min 0s + 2 + Looking at Electronics & Cameras page again + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + action + http://example.org/index.htm + Looking at product page + 2 + + + 7 + 666 + PRODUCT name + SKU VERY nice indeed + + 720 + 12 min 0s + 3 + Looking at product page + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + ecommerceOrder + 937nsjusu 3894 + 1111.11 + 1000 + 111 + 0.11 + 666 + 8 + + + direct + + + + + SKU VERY nice indeed + PRODUCT name + Multiple Category 1 + 500 + 2 + + Multiple Category 1 + Multiple Category 2 + Electronics & Cameras + Multiple Category 4 + Multiple Category 5 + + + + ANOTHER SKU HERE + PRODUCT name BIS + + 100 + 6 + + + + + plugins/Morpheus/images/ecommerceOrder.png + plugins/Morpheus/images/ecommerceOrder.svg + Ordered (937nsjusu 3894) + $1,111.11 revenue - 8 items: PRODUCT name, PRODUCT name BIS) + + + + ecommerceOrder + 1037nsjusu4s3894 + 2000 + 1500 + 400 + 100 + 0 + 2 + + + direct + + + + + SKU2 + Canon SLR + Electronics & Cameras + 1500 + 1 + + Electronics & Cameras + + + + SKU VERY nice indeed + PRODUCT name + + 11.22 + 1 + + + + + plugins/Morpheus/images/ecommerceOrder.png + plugins/Morpheus/images/ecommerceOrder.svg + Ordered (1037nsjusu4s3894) + $2,000 revenue - 2 items: Canon SLR, PRODUCT name) + + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 8 + 999 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 4 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 9 + 333 + PRODUCT THREE LEFT in cart + SKU VERY nice indeed + + 180 + 3 min 0s + 5 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + View product left in cart + 2 + + + 10 + PRODUCT TWO LEFT in cart + SKU IN ABANDONED CART TWO + + 361 + 6 min 1s + 6 + View product left in cart + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Category TWO LEFT in cart + second category + + 0 M + + + ecommerceAbandonedCart + 2510.11 + 4 + + + direct + + + + + SKU VERY nice indeed + PRODUCT THREE LEFT in cart + Electronics & Cameras + 10 + 1 + + Electronics & Cameras + + + + SKU IN ABANDONED CART ONE + PRODUCT ONE LEFT in cart + + 500.11 + 1 + + + + + SKU IN ABANDONED CART TWO + PRODUCT TWO LEFT in cart + Category TWO LEFT in cart + 1000 + 2 + + Category TWO LEFT in cart + second category + + + + plugins/Morpheus/images/ecommerceAbandonedCart.png + plugins/Morpheus/images/ecommerceAbandonedCart.svg + Abandoned Cart + $2,510.11 revenue - 4 items: PRODUCT THREE LEFT in cart, PRODUCT ONE LEFT in cart, PRODUCT TWO LEFT in cart) + + + + 0 + USD + $ + + + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + + + + + + + + returning + plugins/Live/images/returningVisitor.png + 1 + plugins/Morpheus/images/goal.svg + 2 + orderedThenAbandonedCart + plugins/Morpheus/images/ecommerceOrder.svg + 0 + 5760 + 0 + 0 + 3961 + 1 hours 6 min + 0 + 6 + 6 + direct + Direct Entry + + + + + + + + + + + pl + Polish + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Unknown + Generic Desktop + Windows XP + Windows + plugins/Morpheus/icons/dist/os/WIN.png + WIN + XP + Gecko + Gecko (Firefox) + Firefox 3.6 + Firefox + plugins/Morpheus/icons/dist/browsers/FF.png + FF + 3.6 + 13351.11 + 4 + 12 + 7530.33 + 3 + 12 + 0 + Europe + eur + Poland + pl + plugins/Morpheus/icons/dist/flags/pl.png + + + + Poland + + + 12:34:06 + 12 + 0 + 5760 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + VisitorName + Great name! + + + ValueIsZero + 0 + + + VisitorType + NewLoggedOut + + + + + 1 + 1 + 156.5.3.2 + + e16cf2bbaeea2c88 + + + action + http://example.org/index.htm + incredible title! + 2 + + + 1 + 1111.11111 + PRODUCT name + SKU2 + + 360 + 6 min 0s + 1 + incredible title! + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + goal + title match, triggered ONCE + 1 + + 10 + 1 + + http://example.org/index.htm + direct + + + plugins/Morpheus/images/goal.png + plugins/Morpheus/images/goal.svg + Goal conversion + title match, triggered ONCE ($10 revenue) + + + + action + http://example.org/index.htm + Another Product page + 2 + + + 2 + 666 + PRODUCT name + SKU VERY nice indeed + + 360 + 6 min 0s + 2 + Another Product page + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Electronics & Cameras + + 0 M + + + action + http://example.org/index.htm + Another Product page with no category + 2 + + + 3 + 888 + PRODUCT name + SKU VERY nice indeed + + 1 + 1s + 3 + Another Product page with no category + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + 0 M + + + action + http://example.org/index.htm + Another Product page with multiple categories + 2 + + + 4 + PRODUCT name + SKU VERY nice indeed + + 0 + 0s + 4 + Another Product page with multiple categories + http://example.org/index.htm + + plugins/Morpheus/images/action.svg + + + Multiple Category 1 + Multiple Category 2 + Electronics & Cameras + Multiple Category 4 + Multiple Category 5 + + 0 M + + + 1 + USD + $ + + + + + A very long name for a very tiny test site, that also reaches the 90 character limit fully + + + + + + + + new + + 1 + plugins/Morpheus/images/goal.svg + 1 + none + + 0 + 0 + 0 + + 722 + 12 min 2s + 0 + 4 + 4 + direct + Direct Entry + + + + + + + + + + + fr + French + Desktop + plugins/Morpheus/icons/dist/devices/desktop.png + Unknown + Generic Desktop + Windows XP + Windows + plugins/Morpheus/icons/dist/os/WIN.png + WIN + XP + Gecko + Gecko (Firefox) + Firefox 3.6 + Firefox + plugins/Morpheus/icons/dist/browsers/FF.png + FF + 3.6 + 13351.11 + 4 + 12 + 7530.33 + 3 + 12 + 0 + Europe + eur + France + fr + plugins/Morpheus/icons/dist/flags/fr.png + + + + France + + + 12:34:06 + 12 + 0 + 0 + 1024x768 + cookie, flash, java + + + plugins/Morpheus/icons/dist/plugins/cookie.png + cookie + + + plugins/Morpheus/icons/dist/plugins/flash.png + flash + + + plugins/Morpheus/icons/dist/plugins/java.png + java + + + + + ValueIsZero + 0 + + + VisitorType + NewLoggedOut + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getCity_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getCity_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..be348ac9f8f7f1b188e8226b5786eedce4e72b49 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getCity_day.xml @@ -0,0 +1,46 @@ + + + + + 2 + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + Unknown + xx + xx + xx + Unknown + Unknown + plugins/Morpheus/icons/dist/flags/xx.png + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getContinent_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getContinent_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..15c520645b6705d725d86a533b3a80f59b5a1c40 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getContinent_day.xml @@ -0,0 +1,40 @@ + + + + + 2 + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + Europe + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getCountry_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getCountry_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..1ff99e5dfcf1777ffe3b3fee8b5edbf29dbce2d7 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getCountry_day.xml @@ -0,0 +1,62 @@ + + + + + 1 + 2 + 9 + 0 + 6 + 4682 + 0 + 1 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 2 + 3111.11 + pl + plugins/Morpheus/icons/dist/flags/pl.png + countryCode==pl + 16 + + + + 1 + 1 + 4 + 0 + 4 + 722 + 0 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + fr + plugins/Morpheus/icons/dist/flags/fr.png + countryCode==fr + 16 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getNumberOfDistinctCountries_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getNumberOfDistinctCountries_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..13002b20179a7f01ad26b5afab050d0539adedf7 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getNumberOfDistinctCountries_day.xml @@ -0,0 +1,2 @@ + +2 \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getRegion_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getRegion_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..13e63d57a17438b6d9f324583d81eb76e301f98c --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__UserCountry.getRegion_day.xml @@ -0,0 +1,44 @@ + + + + + 2 + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + + + 2 + 2 + 5020.22 + 8 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 1 + 1 + 10 + + + 3 + 3121.11 + xx + xx + Unknown + Unknown + plugins/Morpheus/icons/dist/flags/xx.png + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getByDayOfWeek_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getByDayOfWeek_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c5562822e6134d50a221fe1677797676ee290d9b --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getByDayOfWeek_day.xml @@ -0,0 +1,44 @@ + + + + + 0 + 1 + + + + 3 + 1 + 13 + 0 + 5404 + 0 + 2 + 2 + + + + 0 + 3 + + + + 0 + 4 + + + + 0 + 5 + + + + 0 + 6 + + + + 0 + 7 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getVisitInformationPerLocalTime_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getVisitInformationPerLocalTime_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..e2104925d0c97b38defe49d856d7841c633b8140 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getVisitInformationPerLocalTime_day.xml @@ -0,0 +1,291 @@ + + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==0 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==1 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==2 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==3 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==4 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==5 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==6 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==7 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==8 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==9 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==10 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==11 + + + + 1 + 3 + 13 + 0 + 6 + 5404 + 0 + 2 + visitLocalHour==12 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==13 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==14 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==15 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==16 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==17 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==18 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==19 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==20 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==21 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==22 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitLocalHour==23 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getVisitInformationPerServerTime_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getVisitInformationPerServerTime_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..ec32e9844cc35f3f0ebd3f5af621a2bfec77e5a2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitTime.getVisitInformationPerServerTime_day.xml @@ -0,0 +1,323 @@ + + + + + 1 + 1 + 4 + 0 + 4 + 722 + 0 + 1 + + + 1 + 1 + 10 + + + 1 + 10 + visitStartServerHour==0 + + + + 1 + 1 + 6 + 0 + 6 + 3961 + 0 + 1 + visitStartServerHour==1 + + + + + + 1 + 1 + 2510.11 + 4 + + + 2 + 1 + 3111.11 + 2500 + 511 + 100.11 + 666 + 10 + + + 2 + 3111.11 + 0 + visitStartServerHour==2 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==3 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==4 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==5 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==6 + + + + 1 + 1 + 3 + 0 + 3 + 721 + 0 + 0 + + + 1 + 1 + 2510.11 + 4 + + + 0 + 0 + visitStartServerHour==7 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==8 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==9 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==10 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==11 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==12 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==13 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==14 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==15 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==16 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==17 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==18 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==19 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==20 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==21 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==22 + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + visitStartServerHour==23 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitsSummary.get_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..996a413e26cf73341478eb36c477a3d1d44e71b1 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems__VisitsSummary.get_day.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 3 + 13 + 2 + 0 + 5404 + 6 + 0% + 4.3 + 1801 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6b4c8503b0370a8fa6ad4a98c2c0ee989517c273 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsCategory_week.xml @@ -0,0 +1,13 @@ + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productCategory==Tools + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..d596711d61ced4a01aa2cc8ae42b90a280774d4a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsName_week.xml @@ -0,0 +1,13 @@ + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productName==TRIPOD+-+bought+day+after + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..ea2bf20d548c6d8ff3d8ca437511f0cddc426f99 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__Goals.getItemsSku_week.xml @@ -0,0 +1,13 @@ + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productSku==TRIPOD+SKU + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..a42ba7134f0e6ae6ab0a5c2ed4bf37712427ee6a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productCategorySegment__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 0 + 1 + 1 + 363 + 0 + 100% + 0 + 363 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6b4c8503b0370a8fa6ad4a98c2c0ee989517c273 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsCategory_week.xml @@ -0,0 +1,13 @@ + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productCategory==Tools + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..d596711d61ced4a01aa2cc8ae42b90a280774d4a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsName_week.xml @@ -0,0 +1,13 @@ + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productName==TRIPOD+-+bought+day+after + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..ea2bf20d548c6d8ff3d8ca437511f0cddc426f99 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__Goals.getItemsSku_week.xml @@ -0,0 +1,13 @@ + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productSku==TRIPOD+SKU + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..a42ba7134f0e6ae6ab0a5c2ed4bf37712427ee6a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productNameSegment__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 1 + 0 + 1 + 1 + 363 + 0 + 100% + 0 + 363 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6e3455a8ec3f7c4dbc9b275dd08085855253a6e0 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsCategory_week.xml @@ -0,0 +1,89 @@ + + + + + 2500 + 3 + 2 + 1 + 3 + 1 + 1000 + 1.5 + 200% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+5 + + + + 611.22 + 7 + 2 + 3 + 6 + 2 + 55.61 + 3.5 + 66.67% + productCategory==Product+Category+not+defined + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==second+category + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..18b4292b3cc7183289e8d108e437885ad334286e --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsName_week.xml @@ -0,0 +1,56 @@ + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 1011.22 + 3 + 2 + 1 + 1 + 1 + 255.61 + 1.5 + 200% + productName==PRODUCT+name + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + + + 3 + 6 + 2 + 1332 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6dd64fce77976155b00f15b13036d014257dab9e --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__Goals.getItemsSku_week.xml @@ -0,0 +1,46 @@ + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productSku==SKU2 + + + + 1011.22 + 3 + 2 + 3 + 7 + 2 + 255.61 + 1.5 + 66.67% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 3 + 3 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..f0d70174e25cde6386ce4a62a0a100ca67968369 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productPrice__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 3 + 12 + 2 + 0 + 5763 + 6 + 0% + 4 + 1921 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsCategory_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsName_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__Goals.getItemsSku_week.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..32b66284bedbe458384d8a08a7b9a61b9d01f775 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentDeleted__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0% + 0 + 0 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..2957cbae993136636624f88bed1503907bb514a3 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsCategory_week.xml @@ -0,0 +1,89 @@ + + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 611.22 + 7 + 2 + 3 + 6 + 2 + 55.61 + 3.5 + 66.67% + productCategory==Product+Category+not+defined + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==second+category + + + + 2500 + 3 + 2 + 1 + 3 + 1 + 1000 + 1.5 + 200% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+5 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..d1feaa218f61ba455c73db8816035bf6628eb7c2 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsName_week.xml @@ -0,0 +1,56 @@ + + + + + 3 + 6 + 2 + 1332 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + + + 1011.22 + 3 + 2 + 1 + 1 + 1 + 255.61 + 1.5 + 200% + productName==PRODUCT+name + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..d75c926a49884180a2818119089d9620b998dfe5 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegmentSorted__Goals.getItemsSku_week.xml @@ -0,0 +1,46 @@ + + + + + 3 + 3 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + + + 1011.22 + 3 + 2 + 3 + 7 + 2 + 255.61 + 1.5 + 66.67% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productSku==SKU2 + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsCategory_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6e3455a8ec3f7c4dbc9b275dd08085855253a6e0 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsCategory_week.xml @@ -0,0 +1,89 @@ + + + + + 2500 + 3 + 2 + 1 + 3 + 1 + 1000 + 1.5 + 200% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 500 + 2 + 0% + productCategory==Multiple+Category+5 + + + + 611.22 + 7 + 2 + 3 + 6 + 2 + 55.61 + 3.5 + 66.67% + productCategory==Product+Category+not+defined + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productCategory==second+category + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsName_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..18b4292b3cc7183289e8d108e437885ad334286e --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsName_week.xml @@ -0,0 +1,56 @@ + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 1011.22 + 3 + 2 + 1 + 1 + 1 + 255.61 + 1.5 + 200% + productName==PRODUCT+name + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + + + 3 + 6 + 2 + 1332 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 3 + 3 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsSku_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6dd64fce77976155b00f15b13036d014257dab9e --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__Goals.getItemsSku_week.xml @@ -0,0 +1,46 @@ + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productSku==SKU2 + + + + 1011.22 + 3 + 2 + 3 + 7 + 2 + 255.61 + 1.5 + 66.67% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 3 + 3 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__VisitsSummary.get_week.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__VisitsSummary.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..f0d70174e25cde6386ce4a62a0a100ca67968369 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_productSkuSegment__VisitsSummary.get_week.xml @@ -0,0 +1,14 @@ + + + 1 + 0 + 3 + 12 + 2 + 0 + 5763 + 6 + 0% + 4 + 1921 + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_graph__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_graph__ScheduledReports.generateReport_week.original.html new file mode 100644 index 0000000000000000000000000000000000000000..c4cfaa7260c94b5e607d682e737e7e8d44cf7c3a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_graph__ScheduledReports.generateReport_week.original.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + +
    + + Matomo, free/libre analytics platform + + + + Dashboard + +
    + +
    + +

    + Hello, +

    + +

    + Please find below your day report for A very long name for a very tiny test site, that also reaches the 90 character limit fully. +
    Mail Test report (previous default) +
    Date range: week April 4 – 10, 2011 +
    Sent from http://example.com/piwik/tests/PHPUnit/proxy/. +

    + + + +

    + Page titles +

    + + + + +

    + + Back to top ↑ +

    + +
    + +

    + Powered by + Matomo Analytics +
    + The leading open analytics platform that respects your privacy. +

    + +

    To unsubscribe from this report please follow this link: Unsubscribe.

    + +
    + + diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_overEach__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_overEach__ScheduledReports.generateReport_week.original.html new file mode 100644 index 0000000000000000000000000000000000000000..2c4e2063cb99a9d219c40f3db3176227f8891b8a --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_overEach__ScheduledReports.generateReport_week.original.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + +
    + + Matomo, free/libre analytics platform + + + + Dashboard + +
    + +
    + +

    + Hello, +

    + +

    + Please find below your week report for A very long name for a very tiny test site, that also reaches the 90 character limit fully. +
    Mail Test report (each in period) +
    Date range: week April 4 – 10, 2011 +
    Sent from http://example.com/piwik/tests/PHPUnit/proxy/. +

    + + + +

    + Page titles +

    + + + + +

    + + Back to top ↑ +

    + +
    + +

    + Powered by + Matomo Analytics +
    + The leading open analytics platform that respects your privacy. +

    + +

    To unsubscribe from this report please follow this link: Unsubscribe.

    + +
    + + diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_prevCustomN__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_prevCustomN__ScheduledReports.generateReport_week.original.html new file mode 100644 index 0000000000000000000000000000000000000000..7df9128b62b16f32b019141d0570c47a0331b217 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_row_evolution_prevCustomN__ScheduledReports.generateReport_week.original.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + +
    + + Matomo, free/libre analytics platform + + + + Dashboard + +
    + +
    + +

    + Hello, +

    + +

    + Please find below your day report for A very long name for a very tiny test site, that also reaches the 90 character limit fully. +
    Mail Test report (previous10) +
    Date range: week April 4 – 10, 2011 +
    Sent from http://example.com/piwik/tests/PHPUnit/proxy/. +

    + + + +

    + Page titles +

    + + + + +

    + + Back to top ↑ +

    + +
    + +

    + Powered by + Matomo Analytics +
    + The leading open analytics platform that respects your privacy. +

    + +

    To unsubscribe from this report please follow this link: Unsubscribe.

    + +
    + + diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html new file mode 100644 index 0000000000000000000000000000000000000000..26085172f9448ab2ddbbcea0ef078e5bf5a051bd --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html @@ -0,0 +1,8178 @@ + + + + + + + + + + + + + + + + + +
    + + Matomo, free/libre analytics platform + + + + Dashboard + +
    + +
    + +

    + Hello, +

    + +

    + Please find below your day report for A very long name for a very tiny test site, that also reaches the 90 character limit fully. +
    Mail Test report +
    Date range: week April 4 – 10, 2011 +
    Sent from http://example.com/piwik/tests/PHPUnit/proxy/. +

    + + +

    + Report list +

    + + +

    + All Websites dashboard +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Website   + +  Visits   + +  Actions   + +  Pageviews   + +  Hits   + +  AI Chatbots Requests   + +  Revenue   + +  Conversions   + +  Ecommerce Orders   + +  Product Revenue   +
    + A very long name for a very tiny test site, that also reaches the 90 character limit fully + 5 + + 16 + + 16 + + 16 + + 0 + + $13,361.11 + + 5 + + 4 + + $13,351.11 +
    + Piwik test + 2 + + 1 + + 1 + + 1 + + 0 + + $0 + + 0 + + 0 + + $0 +
    +

    + + Back to top ↑ +

    + +

    + Visits Summary +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Unique visitors + 1 +
    + Visits + 5 +
    + Actions + 16 +
    + Maximum actions in one visit + 6 +
    + Actions per Visit + 3.2 +
    + Avg. Visit Duration (in seconds) + 00:22:50 +
    + Bounce Rate + 20% +
    +

    + + Back to top ↑ +

    + +

    + Country +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Country   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Poland + 4 + + 12 + + 3 + + 00:25:32 + + 25% + + $13,351.11 +
    + +   + France + 1 + + 4 + + 4 + + 00:12:02 + + 0% + + $10 +
    +

    + + Back to top ↑ +

    + +

    + Continent +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Continent   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + Europe + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Region +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Region   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Unknown + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Web-browser language +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Language   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Polish + 4 + + 12 + + 3 + + 00:25:32 + + 25% + + 75% +
    + French + 1 + + 4 + + 4 + + 00:12:02 + + 0% + + 100% +
    +

    + + Back to top ↑ +

    + +

    + City +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  City   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Unknown + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Language code +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Language   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Polish (pl) + 4 + + 12 + + 3 + + 00:25:32 + + 25% + + 75% +
    + French (fr) + 1 + + 4 + + 4 + + 00:12:02 + + 0% + + 100% +
    +

    + + Back to top ↑ +

    + +

    + Device type +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Device type   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Desktop + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Device model +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Device model   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + Generic Desktop + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Device brand +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Device brand   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Unknown + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Screen Resolution +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Resolution   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + 1024x768 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Operating System versions +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Operating system version   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + +   + Windows XP + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Browsers +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Browser   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Firefox + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Browser version +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Browser version   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + +   + Firefox 3.6 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Configurations +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Configuration   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Windows / Firefox / 1024x768 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Operating System families +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Operating system family   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + +   + Windows + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Browser engines +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Browser engine   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Gecko (Firefox) + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Browser Plugins +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Plugin   + +  Visits   + +  % Visits   +
    + +   + Cookie + 5 + + 100% +
    + +   + Flash + 5 + + 100% +
    + +   + Java + 5 + + 100% +
    + +   + Pdf + 0 + + 0% +
    + +   + Quicktime + 0 + + 0% +
    + +   + Realplayer + 0 + + 0% +
    + +   + Silverlight + 0 + + 0% +
    + +   + Windowsmedia + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Visits per local time +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Local time — hour (start of visit)   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + 00 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 01 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 02 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 03 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 04 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 05 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 06 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 07 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 08 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 09 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 10 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 11 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 12 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    + 13 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 14 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 15 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 16 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 17 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 18 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 19 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 20 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 21 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 22 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 23 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Visits per hour in the site's timezone +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Site time — hour (start of visit)   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + 00 + 1 + + 4 + + 4 + + 00:12:02 + + 0% + + $10 +
    + 01 + 1 + + 6 + + 6 + + 01:06:01 + + 0% + + $0 +
    + 02 + 1 + + 3 + + 3 + + 00:18:01 + + 0% + + $3,111.11 +
    + 03 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 04 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 05 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 06 + 1 + + 0 + + 0 + + 00:06:03 + + 100% + + $10,240 +
    + 07 + 1 + + 3 + + 3 + + 00:12:01 + + 0% + + $0 +
    + 08 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 09 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 10 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 11 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 12 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 13 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 14 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 15 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 16 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 17 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 18 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 19 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 20 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 21 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 22 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 23 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    +

    + + Back to top ↑ +

    + +

    + Visits by day of the week +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Day of the week   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Monday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + Tuesday + 3 + + 13 + + 4.3 + + 00:30:01 + + 0% + + 66.67% +
    + Wednesday + 2 + + 3 + + 1.5 + + 00:12:02 + + 50% + + 100% +
    + Thursday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + Friday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + Saturday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + Sunday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    +

    + + Back to top ↑ +

    + +

    + User IDs +

    + + There is no data for this report. + +

    + Custom Variables +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Custom Variable name   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + ValueIsZero - 0 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    + VisitorType - NewLoggedOut + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    + VisitorName - Great name! + 4 + + 12 + + 3 + + 00:25:32 + + 25% + + $13,351.11 +
    +

    + + Back to top ↑ +

    + +

    + Actions - Main metrics +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Pageviews + 16 +
    + Unique Pageviews + 4 +
    + Downloads + 0 +
    + Unique Downloads + 0 +
    + Outlinks + 0 +
    + Unique Outlinks + 0 +
    + Searches + 0 +
    + Unique Keywords + 0 +
    + Hits + 16 +
    +

    + + Back to top ↑ +

    + +

    + Bandwidth - Main metrics +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Bytes transferred overall + 0 +
    + Bytes transferred pageviews + 0 +
    + Bytes transferred downloads + 0 +
    +

    + + Back to top ↑ +

    + +

    + Page URLs +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    +  Page URL   + +  Pageviews   + +  Unique Pageviews   + +  Avg. time on page   + +  Bounce Rate   + +  Exit rate   +
    + + /index.htm + + 16 + + 4 + + 00:03:23 + + 0% + + 100% +
    +

    + + Back to top ↑ +

    + +

    + Entry pages +

    + + + +
    +
    + + + + + + + + + + + + + + + + + +
    +  Entry Page URL   + +  Entrances   + +  Bounces   + +  Bounce Rate   +
    + + /index.htm + + 4 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Entry page titles +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Entry Page title   + +  Entrances   + +  Bounces   + +  Bounce Rate   +
    + View product left in cart + 1 + + 0 + + 0% +
    + incredible title! + 1 + + 0 + + 0% +
    + Looking at Electronics & Cameras page with a page level custom variable + 1 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Exit pages +

    + + + +
    +
    + + + + + + + + + + + + + + + + + +
    +  Exit Page URL   + +  Exits   + +  Unique Pageviews   + +  Exit rate   +
    + + /index.htm + + 4 + + 4 + + 100% +
    +

    + + Back to top ↑ +

    + +

    + Exit page titles +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Exit Page Title   + +  Exits   + +  Unique Pageviews   + +  Exit rate   +
    + View product left in cart + 3 + + 3 + + 100% +
    + Another Product page with multiple categories + 1 + + 1 + + 100% +
    +

    + + Back to top ↑ +

    + +

    + Page titles +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Page Title   + +  Pageviews   + +  Unique Pageviews   + +  Avg. time on page   + +  Bounce Rate   + +  Exit rate   +
    + View product left in cart + 9 + + 3 + + 00:02:00 + + 0% + + 100% +
    + Another Product page + 1 + + 1 + + 00:06:00 + + 0% + + 0% +
    + Another Product page with multiple categories + 1 + + 1 + + 00:00:00 + + 0% + + 100% +
    + Another Product page with no category + 1 + + 1 + + 00:00:01 + + 0% + + 0% +
    + incredible title! + 1 + + 1 + + 00:06:00 + + 0% + + 0% +
    + Looking at Electronics & Cameras page again + 1 + + 1 + + 00:06:00 + + 0% + + 0% +
    + Looking at Electronics & Cameras page with a page level custom variable + 1 + + 1 + + 00:06:00 + + 0% + + 0% +
    + Looking at product page + 1 + + 1 + + 00:12:00 + + 0% + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Site Search Keywords +

    + + There is no data for this report. + +

    + Pages Following a Site Search +

    + + There is no data for this report. + +

    + Search Keywords with No Results +

    + + There is no data for this report. + +

    + Page Titles Following a Site Search +

    + + There is no data for this report. + +

    + Search Categories +

    + + There is no data for this report. + + + + There is no data for this report. + +

    + Downloads +

    + + There is no data for this report. + +

    + Event Categories +

    + + There is no data for this report. + +

    + Event Actions +

    + + There is no data for this report. + +

    + Event Names +

    + + There is no data for this report. + +

    + Content Name +

    + + There is no data for this report. + +

    + Content Piece +

    + + There is no data for this report. + +

    + Length of visits +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visit duration   + +  Visits   +
    + 0–10s + 0 +
    + 11–30s + 0 +
    + 31–60s + 0 +
    + 1-2 min + 0 +
    + 2-4 min + 0 +
    + 4-7 min + 1 +
    + 7-10 min + 0 +
    + 10-15 min + 2 +
    + 15-30 min + 1 +
    + 30+ min + 1 +
    +

    + + Back to top ↑ +

    + +

    + Pages per visit +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Pages per visit   + +  Visits   +
    + 1 page + 0 +
    + 2 pages + 0 +
    + 3 pages + 2 +
    + 4 pages + 1 +
    + 5 pages + 0 +
    + 6-7 pages + 1 +
    + 8-10 pages + 0 +
    + 11-14 pages + 0 +
    + 15-20 pages + 0 +
    + 21+ pages + 0 +
    +

    + + Back to top ↑ +

    + +

    + Visits by visit number +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits by visit number   + +  Visits   + +  % Visits   +
    + 1 visit + 1 + + 20% +
    + 2 visits + 1 + + 20% +
    + 3 visits + 1 + + 20% +
    + 4 visits + 1 + + 20% +
    + 5 visits + 1 + + 20% +
    + 6 visits + 0 + + 0% +
    + 7 visits + 0 + + 0% +
    + 8 visits + 0 + + 0% +
    + 9-14 visits + 0 + + 0% +
    + 15-25 visits + 0 + + 0% +
    + 26-50 visits + 0 + + 0% +
    + 51-100 visits + 0 + + 0% +
    + 101-200 visits + 0 + + 0% +
    + 201+ visits + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Visits by days since last visit +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days since last visit   + +  Visits   +
    + New visits + 1 +
    + 0 days + 2 +
    + 1 day + 0 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + Returning Visits +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Unique new visitors + 1 +
    + New Users + 0 +
    + New Visits + 1 +
    + Actions by New Visits + 4 +
    + Max_actions_new + 4 +
    + Bounce Rate for New Visits + 0% +
    + Avg. Actions per New Visit + 4 +
    + Avg. Duration of a New Visit (in sec) + 00:12:02 +
    + Unique returning visitors + 1 +
    + Returning Users + 0 +
    + Returning Visits + 4 +
    + Actions by Returning Visits + 12 +
    + Maximum actions in one returning visit + 6 +
    + Bounce Rate for Returning Visits + 25% +
    + Avg. Actions per Returning Visit + 3 +
    + Avg. Duration of a Returning Visit (in sec) + 00:25:32 +
    +

    + + Back to top ↑ +

    + +

    + Performance overview +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Avg. network time + 0s +
    + Avg. server time + 0s +
    + Avg. transfer time + 0s +
    + Avg. DOM processing time + 0s +
    + Avg. DOM completion time + 0s +
    + Avg. on load time + 0s +
    + Avg. page load time + 0s +
    +

    + + Back to top ↑ +

    + +

    + Referrers Overview +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Visitors from Search Engines + 0 +
    + Visitors from Social Networks + 0 +
    + Visitors from AI Assistants + 0 +
    + Visitors from Direct Entry + 5 +
    + Visitors from Websites + 0 +
    + Visitors from Campaigns + 0 +
    + Distinct search engines + 0 +
    + Distinct social networks + 0 +
    + Distinct AI assistants + 0 +
    + Distinct keywords + 0 +
    + Distinct websites + 0 +
    + Distinct campaigns + 0 +
    + Percent of Visitors from Direct Entry + 100% +
    + Percent of Visitors from Search Engines + 0% +
    + Percent of Visitors from AI Assistants + 0% +
    + Percent of Visitors from Campaigns + 0% +
    + Percent of Visitors from Social Networks + 0% +
    + Percent of Visitors from Websites + 0% +
    +

    + + Back to top ↑ +

    + +

    + Channel Type +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Channel Type   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + Direct Entry + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + All Channels +

    + + There is no data for this report. + +

    + Keywords +

    + + There is no data for this report. + +

    + Search Engines +

    + + There is no data for this report. + +

    + Websites +

    + + There is no data for this report. + +

    + Social Networks +

    + + There is no data for this report. + +

    + AI Assistants +

    + + There is no data for this report. + +

    + Campaigns +

    + + There is no data for this report. + +

    + Ecommerce Orders +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Ecommerce Orders + 4 +
    + Visits with Conversions + 2 +
    + Revenue + $13,351.11 +
    + Subtotal + $2,700 +
    + Tax + $531 +
    + Shipping + $120.11 +
    + Discount + $686 +
    + Purchased Products + 12 +
    + Average Order Value + $3,337.78 +
    + Conversion Rate + 40% +
    +

    + + Back to top ↑ +

    + +

    + Ecommerce Orders - Visits to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   +
    + 1 visit + 0 +
    + 2 visits + 2 +
    + 3 visits + 0 +
    + 4 visits + 0 +
    + 5 visits + 2 +
    + 6 visits + 0 +
    + 7 visits + 0 +
    + 8 visits + 0 +
    + 9-14 visits + 0 +
    + 15-25 visits + 0 +
    + 26-50 visits + 0 +
    + 51-100 visits + 0 +
    + 101+ visits + 0 +
    +

    + + Back to top ↑ +

    + +

    + Ecommerce Orders - Days to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   +
    + 0 days + 2 +
    + 1 day + 2 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + Abandoned Carts +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Abandoned Carts + 3 +
    + Revenue left in cart + $7,530.33 +
    + Products left in cart + 12 +
    + Average Order Value + $2,510.11 +
    + Conversion Rate + 60% +
    +

    + + Back to top ↑ +

    + +

    + Abandoned Carts - Visits to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   +
    + 1 visit + 0 +
    + 2 visits + 1 +
    + 3 visits + 1 +
    + 4 visits + 1 +
    + 5 visits + 0 +
    + 6 visits + 0 +
    + 7 visits + 0 +
    + 8 visits + 0 +
    + 9-14 visits + 0 +
    + 15-25 visits + 0 +
    + 26-50 visits + 0 +
    + 51-100 visits + 0 +
    + 101+ visits + 0 +
    +

    + + Back to top ↑ +

    + +

    + Abandoned Carts - Days to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   +
    + 0 days + 2 +
    + 1 day + 1 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + Product Name +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Product Name   + +  Product Revenue   + +  Quantity   + +  Unique Purchases   + +  Visits   + +  Average Price   + +  Average Quantity   + +  Product Conversion Rate   +
    + Canon SLR + $1,500 + + 1 + + 1 + + 0 + + $1,500 + + 1 + + 0% +
    + PRODUCT name + $1,011.22 + + 3 + + 2 + + 2 + + $255.61 + + 1.5 + + 100% +
    + PRODUCT name BIS + $600 + + 6 + + 1 + + 0 + + $100 + + 6 + + 0% +
    + TRIPOD - bought day after + $200 + + 2 + + 1 + + 0 + + $100 + + 2 + + 0% +
    + PRODUCT THREE LEFT in cart + $0 + + 0 + + 0 + + 3 + + $1,332 + + 0 + + 0% +
    + PRODUCT TWO LEFT in cart + $0 + + 0 + + 0 + + 3 + + $0 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Product SKU +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Product SKU   + +  Product Revenue   + +  Quantity   + +  Unique Purchases   + +  Visits   + +  Average Price   + +  Average Quantity   + +  Product Conversion Rate   +
    + SKU2 + $1,500 + + 1 + + 1 + + 1 + + $1,500 + + 1 + + 100% +
    + SKU VERY nice indeed + $1,011.22 + + 3 + + 2 + + 4 + + $255.61 + + 1.5 + + 50% +
    + ANOTHER SKU HERE + $600 + + 6 + + 1 + + 0 + + $100 + + 6 + + 0% +
    + TRIPOD SKU + $200 + + 2 + + 1 + + 0 + + $100 + + 2 + + 0% +
    + SKU IN ABANDONED CART TWO + $0 + + 0 + + 0 + + 3 + + $0 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Product Category +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Product Category   + +  Product Revenue   + +  Quantity   + +  Unique Purchases   + +  Visits   + +  Average Price   + +  Average Quantity   + +  Product Conversion Rate   +
    + Electronics & Cameras + $2,500 + + 3 + + 2 + + 3 + + $1,000 + + 1.5 + + 66.67% +
    + Multiple Category 1 + $1,000 + + 2 + + 1 + + 1 + + $500 + + 2 + + 100% +
    + Multiple Category 2 + $1,000 + + 2 + + 1 + + 1 + + $500 + + 2 + + 100% +
    + Multiple Category 4 + $1,000 + + 2 + + 1 + + 1 + + $500 + + 2 + + 100% +
    + Multiple Category 5 + $1,000 + + 2 + + 1 + + 1 + + $500 + + 2 + + 100% +
    + Product Category not defined + $611.22 + + 7 + + 2 + + 4 + + $55.61 + + 3.5 + + 50% +
    + Tools + $200 + + 2 + + 1 + + 0 + + $100 + + 2 + + 0% +
    + Category TWO LEFT in cart + $0 + + 0 + + 0 + + 3 + + $0 + + 0 + + 0% +
    + second category + $0 + + 0 + + 0 + + 3 + + $0 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Goals +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Conversions + 5 +
    + Visits with Conversions + 4 +
    + Revenue + $13,361.11 +
    + Conversion Rate + 80% +
    +

    + + Back to top ↑ +

    + +

    + Visits to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   + +  Revenue   +
    + 1 visit + 1 + + $0 +
    + 2 visits + 0 + + $0 +
    + 3 visits + 0 + + $0 +
    + 4 visits + 0 + + $0 +
    + 5 visits + 0 + + $0 +
    + 6 visits + 0 + + $0 +
    + 7 visits + 0 + + $0 +
    + 8 visits + 0 + + $0 +
    + 9-14 visits + 0 + + $0 +
    + 15-25 visits + 0 + + $0 +
    + 26-50 visits + 0 + + $0 +
    + 51-100 visits + 0 + + $0 +
    + 101+ visits + 0 + + $0 +
    +

    + + Back to top ↑ +

    + +

    + Days to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   + +  Revenue   +
    + 0 days + 1 + + $0 +
    + 1 day + 0 + + $0 +
    + 2 days + 0 + + $0 +
    + 3 days + 0 + + $0 +
    + 4 days + 0 + + $0 +
    + 5 days + 0 + + $0 +
    + 6 days + 0 + + $0 +
    + 7 days + 0 + + $0 +
    + 8-14 days + 0 + + $0 +
    + 15-30 days + 0 + + $0 +
    + 31-60 days + 0 + + $0 +
    + 61-120 days + 0 + + $0 +
    + 121-364 days + 0 + + $0 +
    + 365+ days + 0 + + $0 +
    +

    + + Back to top ↑ +

    + +

    + Goals Overview +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Conversions + 4 +
    + Visits with Conversions + 2 +
    + Revenue + $13,351.11 +
    + Conversion Rate + 40% +
    +

    + + Back to top ↑ +

    + +

    + Goals Overview - Visits to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   +
    + 1 visit + 1 +
    + 2 visits + 0 +
    + 3 visits + 0 +
    + 4 visits + 0 +
    + 5 visits + 0 +
    + 6 visits + 0 +
    + 7 visits + 0 +
    + 8 visits + 0 +
    + 9-14 visits + 0 +
    + 15-25 visits + 0 +
    + 26-50 visits + 0 +
    + 51-100 visits + 0 +
    + 101+ visits + 0 +
    +

    + + Back to top ↑ +

    + +

    + Goals Overview - Days to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   +
    + 0 days + 1 +
    + 1 day + 0 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + Goal title match, triggered ONCE +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Conversions + 1 +
    + Visits with Conversions + 1 +
    + Revenue + $10 +
    + Conversion Rate + 20% +
    +

    + + Back to top ↑ +

    + +

    + title match, triggered ONCE - Visits to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   +
    + 1 visit + 1 +
    + 2 visits + 0 +
    + 3 visits + 0 +
    + 4 visits + 0 +
    + 5 visits + 0 +
    + 6 visits + 0 +
    + 7 visits + 0 +
    + 8 visits + 0 +
    + 9-14 visits + 0 +
    + 15-25 visits + 0 +
    + 26-50 visits + 0 +
    + 51-100 visits + 0 +
    + 101+ visits + 0 +
    +

    + + Back to top ↑ +

    + +

    + title match, triggered ONCE - Days to Conversion +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   +
    + 0 days + 1 +
    + 1 day + 0 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + AI Chatbots Overview +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Requests + 0 +
    + Acquired visits + 0 +
    + Not found requests + 0 +
    + Unique AI chatbots + 0 +
    + Server error (5xx) requests + 0 +
    + Click-through rate + 0 +
    + Unique page URLs + 0 +
    + Unique document URLs + 0 +
    +

    + + Back to top ↑ +

    + +

    + AI Chatbots +

    + + There is no data for this report. + +

    + Pages +

    + + There is no data for this report. + +

    + Documents +

    + + There is no data for this report. + +

    + Broken Pages and Documents +

    + + There is no data for this report. + +

    + Human-Favoured Pages +

    + + There is no data for this report. + +

    + AI-Favoured Pages +

    + + There is no data for this report. + +

    + AI Agent Visits +

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Unique AI Agent Visitors + 0 +
    + AI Agent Users + 0 +
    + AI Agent Visits + 0 +
    + Actions by AI Agent Visits + 0 +
    + Maximum Actions in AI Agent Visit + 0 +
    + Bounce Rate for AI Agent Visits + 0% +
    + Avg. Actions per AI Agent Visit + 0 +
    + Avg. Duration of an AI Agent Visit (in sec) + 00:00:00 +
    + Unique Human Visitors + 1 +
    + Human Users + 0 +
    + Human Visits + 5 +
    + Actions by Human Visits + 16 +
    + Maximum Actions in one Human Visit + 6 +
    + Bounce Rate for Human Visits + 20% +
    + Avg. Actions per Human Visit + 3.2 +
    + Avg. Duration of a Human Visit (in sec) + 00:22:50 +
    +

    + + Back to top ↑ +

    + +

    + Data tables +

    + + There is no data for this report. + +

    + Temperatures evolution over time +

    + + There is no data for this report. + +

    + Pie graph +

    + + There is no data for this report. + +

    + Advanced tag cloud: with logos and links +

    + + There is no data for this report. + +
    + +

    + Powered by + Matomo Analytics +
    + The leading open analytics platform that respects your privacy. +

    + +

    To unsubscribe from this report please follow this link: Unsubscribe.

    + +
    + + diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html new file mode 100644 index 0000000000000000000000000000000000000000..26b0f6383fc15209bfd8933ccdfac153b9b0e4de --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html @@ -0,0 +1,7765 @@ + + + + + + + + + + + + + + + + + +
    + + Matomo, free/libre analytics platform + + + + Dashboard + +
    + +
    + +

    + Hello, +

    + +

    + Please find below your day report for A very long name for a very tiny test site, that also reaches the 90 character limit fully. +
    Mail Test report with <long> déscription and some 🚀 $peciäl characters $%&-~°^'"@#*ß§²³ +
    Date range: week April 4 – 10, 2011 +
    Sent from http://example.com/piwik/tests/PHPUnit/proxy/. +

    + + +

    + Report list +

    + + +

    + All Websites dashboard +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Website   + +  Visits   + +  Actions   + +  Pageviews   + +  Hits   + +  AI Chatbots Requests   + +  Revenue   + +  Conversions   + +  Ecommerce Orders   + +  Product Revenue   +
    + A very long name for a very tiny test site, that also reaches the 90 character limit fully + 5 + + 16 + + 16 + + 16 + + 0 + + $13,361.11 + + 5 + + 4 + + $13,351.11 +
    + Piwik test + 2 + + 1 + + 1 + + 1 + + 0 + + $0 + + 0 + + 0 + + $0 +
    +

    + + Back to top ↑ +

    + +

    + Visits Summary +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Unique visitors + 1 +
    + Visits + 5 +
    + Actions + 16 +
    + Maximum actions in one visit + 6 +
    + Actions per Visit + 3.2 +
    + Avg. Visit Duration (in seconds) + 00:22:50 +
    + Bounce Rate + 20% +
    +

    + + Back to top ↑ +

    + +

    + Country +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Country   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Poland + 4 + + 12 + + 3 + + 00:25:32 + + 25% + + $13,351.11 +
    + +   + France + 1 + + 4 + + 4 + + 00:12:02 + + 0% + + $10 +
    +

    + + Back to top ↑ +

    + +

    + Continent +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Continent   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + Europe + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Region +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Region   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Unknown + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Web-browser language +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Language   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Polish + 4 + + 12 + + 3 + + 00:25:32 + + 25% + + 75% +
    + French + 1 + + 4 + + 4 + + 00:12:02 + + 0% + + 100% +
    +

    + + Back to top ↑ +

    + +

    + City +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  City   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Unknown + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Language code +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Language   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Polish (pl) + 4 + + 12 + + 3 + + 00:25:32 + + 25% + + 75% +
    + French (fr) + 1 + + 4 + + 4 + + 00:12:02 + + 0% + + 100% +
    +

    + + Back to top ↑ +

    + +

    + Device type +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Device type   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Desktop + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Device model +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Device model   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + Generic Desktop + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Device brand +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Device brand   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Unknown + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Screen Resolution +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Resolution   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + 1024x768 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Operating System versions +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Operating system version   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + +   + Windows XP + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Browsers +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Browser   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + +   + Firefox + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + Browser version +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Browser version   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + +   + Firefox 3.6 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Configurations +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Configuration   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Windows / Firefox / 1024x768 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Operating System families +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Operating system family   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + +   + Windows + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Browser engines +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Browser engine   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Gecko (Firefox) + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    +

    + + Back to top ↑ +

    + +

    + Browser Plugins +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Plugin   + +  Visits   + +  % Visits   +
    + +   + Cookie + 5 + + 100% +
    + +   + Flash + 5 + + 100% +
    + +   + Java + 5 + + 100% +
    + +   + Pdf + 0 + + 0% +
    + +   + Quicktime + 0 + + 0% +
    + +   + Realplayer + 0 + + 0% +
    + +   + Silverlight + 0 + + 0% +
    + +   + Windowsmedia + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Visits per local time +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Local time — hour (start of visit)   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + 00 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 01 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 02 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 03 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 04 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 05 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 06 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 07 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 08 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 09 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 10 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 11 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 12 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + 80% +
    + 13 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 14 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 15 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 16 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 17 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 18 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 19 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 20 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 21 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 22 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + 23 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Visits per hour in the site's timezone +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Site time — hour (start of visit)   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + 00 + 1 + + 4 + + 4 + + 00:12:02 + + 0% + + $10 +
    + 01 + 1 + + 6 + + 6 + + 01:06:01 + + 0% + + $0 +
    + 02 + 1 + + 3 + + 3 + + 00:18:01 + + 0% + + $3,111.11 +
    + 03 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 04 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 05 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 06 + 1 + + 0 + + 0 + + 00:06:03 + + 100% + + $10,240 +
    + 07 + 1 + + 3 + + 3 + + 00:12:01 + + 0% + + $0 +
    + 08 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 09 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 10 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 11 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 12 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 13 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 14 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 15 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 16 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 17 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 18 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 19 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 20 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 21 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 22 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    + 23 + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + $0 +
    +

    + + Back to top ↑ +

    + +

    + Visits by day of the week +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Day of the week   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Conversion Rate   +
    + Monday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + Tuesday + 3 + + 13 + + 4.3 + + 00:30:01 + + 0% + + 66.67% +
    + Wednesday + 2 + + 3 + + 1.5 + + 00:12:02 + + 50% + + 100% +
    + Thursday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + Friday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + Saturday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    + Sunday + 0 + + 0 + + 0 + + 00:00:00 + + 0% + + 0% +
    +

    + + Back to top ↑ +

    + +

    + User IDs +

    + + There is no data for this report. + +

    + Custom Variables +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Custom Variable name   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + ValueIsZero - 0 + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    + VisitorType - NewLoggedOut + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    + VisitorName - Great name! + 4 + + 12 + + 3 + + 00:25:32 + + 25% + + $13,351.11 +
    +

    + + Back to top ↑ +

    + +

    + Actions - Main metrics +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Pageviews + 16 +
    + Unique Pageviews + 4 +
    + Downloads + 0 +
    + Unique Downloads + 0 +
    + Outlinks + 0 +
    + Unique Outlinks + 0 +
    + Searches + 0 +
    + Unique Keywords + 0 +
    + Hits + 16 +
    +

    + + Back to top ↑ +

    + +

    + Bandwidth - Main metrics +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Bytes transferred overall + 0 +
    + Bytes transferred pageviews + 0 +
    + Bytes transferred downloads + 0 +
    +

    + + Back to top ↑ +

    + +

    + Page URLs +

    + + + + + + + + + + + + + + + + + + + + + + + +
    +  Page URL   + +  Pageviews   + +  Unique Pageviews   + +  Avg. time on page   + +  Bounce Rate   + +  Exit rate   +
    + + /index.htm + + 16 + + 4 + + 00:03:23 + + 0% + + 100% +
    +

    + + Back to top ↑ +

    + +

    + Entry pages +

    + + + + + + + + + + + + + + + + + + + +
    +  Entry Page URL   + +  Entrances   + +  Bounces   + +  Bounce Rate   +
    + + /index.htm + + 4 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Entry page titles +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Entry Page title   + +  Entrances   + +  Bounces   + +  Bounce Rate   +
    + View product left in cart + 1 + + 0 + + 0% +
    + incredible title! + 1 + + 0 + + 0% +
    + Looking at Electronics & Cameras page with a page level custom variable + 1 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Exit pages +

    + + + + + + + + + + + + + + + + + + + +
    +  Exit Page URL   + +  Exits   + +  Unique Pageviews   + +  Exit rate   +
    + + /index.htm + + 4 + + 4 + + 100% +
    +

    + + Back to top ↑ +

    + +

    + Exit page titles +

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Exit Page Title   + +  Exits   + +  Unique Pageviews   + +  Exit rate   +
    + View product left in cart + 3 + + 3 + + 100% +
    + Another Product page with multiple categories + 1 + + 1 + + 100% +
    +

    + + Back to top ↑ +

    + +

    + Page titles +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Page Title   + +  Pageviews   + +  Unique Pageviews   + +  Avg. time on page   + +  Bounce Rate   + +  Exit rate   +
    + View product left in cart + 9 + + 3 + + 00:02:00 + + 0% + + 100% +
    + Another Product page + 1 + + 1 + + 00:06:00 + + 0% + + 0% +
    + Another Product page with multiple categories + 1 + + 1 + + 00:00:00 + + 0% + + 100% +
    + Another Product page with no category + 1 + + 1 + + 00:00:01 + + 0% + + 0% +
    + incredible title! + 1 + + 1 + + 00:06:00 + + 0% + + 0% +
    + Looking at Electronics & Cameras page again + 1 + + 1 + + 00:06:00 + + 0% + + 0% +
    + Looking at Electronics & Cameras page with a page level custom variable + 1 + + 1 + + 00:06:00 + + 0% + + 0% +
    + Looking at product page + 1 + + 1 + + 00:12:00 + + 0% + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Site Search Keywords +

    + + There is no data for this report. + +

    + Pages Following a Site Search +

    + + There is no data for this report. + +

    + Search Keywords with No Results +

    + + There is no data for this report. + +

    + Page Titles Following a Site Search +

    + + There is no data for this report. + +

    + Search Categories +

    + + There is no data for this report. + + + + There is no data for this report. + +

    + Downloads +

    + + There is no data for this report. + +

    + Event Categories +

    + + There is no data for this report. + +

    + Event Actions +

    + + There is no data for this report. + +

    + Event Names +

    + + There is no data for this report. + +

    + Content Name +

    + + There is no data for this report. + +

    + Content Piece +

    + + There is no data for this report. + +

    + Length of visits +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visit duration   + +  Visits   +
    + 0–10s + 0 +
    + 11–30s + 0 +
    + 31–60s + 0 +
    + 1-2 min + 0 +
    + 2-4 min + 0 +
    + 4-7 min + 1 +
    + 7-10 min + 0 +
    + 10-15 min + 2 +
    + 15-30 min + 1 +
    + 30+ min + 1 +
    +

    + + Back to top ↑ +

    + +

    + Pages per visit +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Pages per visit   + +  Visits   +
    + 1 page + 0 +
    + 2 pages + 0 +
    + 3 pages + 2 +
    + 4 pages + 1 +
    + 5 pages + 0 +
    + 6-7 pages + 1 +
    + 8-10 pages + 0 +
    + 11-14 pages + 0 +
    + 15-20 pages + 0 +
    + 21+ pages + 0 +
    +

    + + Back to top ↑ +

    + +

    + Visits by visit number +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits by visit number   + +  Visits   + +  % Visits   +
    + 1 visit + 1 + + 20% +
    + 2 visits + 1 + + 20% +
    + 3 visits + 1 + + 20% +
    + 4 visits + 1 + + 20% +
    + 5 visits + 1 + + 20% +
    + 6 visits + 0 + + 0% +
    + 7 visits + 0 + + 0% +
    + 8 visits + 0 + + 0% +
    + 9-14 visits + 0 + + 0% +
    + 15-25 visits + 0 + + 0% +
    + 26-50 visits + 0 + + 0% +
    + 51-100 visits + 0 + + 0% +
    + 101-200 visits + 0 + + 0% +
    + 201+ visits + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Visits by days since last visit +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days since last visit   + +  Visits   +
    + New visits + 1 +
    + 0 days + 2 +
    + 1 day + 0 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + Returning Visits +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Unique new visitors + 1 +
    + New Users + 0 +
    + New Visits + 1 +
    + Actions by New Visits + 4 +
    + Max_actions_new + 4 +
    + Bounce Rate for New Visits + 0% +
    + Avg. Actions per New Visit + 4 +
    + Avg. Duration of a New Visit (in sec) + 00:12:02 +
    + Unique returning visitors + 1 +
    + Returning Users + 0 +
    + Returning Visits + 4 +
    + Actions by Returning Visits + 12 +
    + Maximum actions in one returning visit + 6 +
    + Bounce Rate for Returning Visits + 25% +
    + Avg. Actions per Returning Visit + 3 +
    + Avg. Duration of a Returning Visit (in sec) + 00:25:32 +
    +

    + + Back to top ↑ +

    + +

    + Performance overview +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Avg. network time + 0s +
    + Avg. server time + 0s +
    + Avg. transfer time + 0s +
    + Avg. DOM processing time + 0s +
    + Avg. DOM completion time + 0s +
    + Avg. on load time + 0s +
    + Avg. page load time + 0s +
    +

    + + Back to top ↑ +

    + +

    + Referrers Overview +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Visitors from Search Engines + 0 +
    + Visitors from Social Networks + 0 +
    + Visitors from AI Assistants + 0 +
    + Visitors from Direct Entry + 5 +
    + Visitors from Websites + 0 +
    + Visitors from Campaigns + 0 +
    + Distinct search engines + 0 +
    + Distinct social networks + 0 +
    + Distinct AI assistants + 0 +
    + Distinct keywords + 0 +
    + Distinct websites + 0 +
    + Distinct campaigns + 0 +
    + Percent of Visitors from Direct Entry + 100% +
    + Percent of Visitors from Search Engines + 0% +
    + Percent of Visitors from AI Assistants + 0% +
    + Percent of Visitors from Campaigns + 0% +
    + Percent of Visitors from Social Networks + 0% +
    + Percent of Visitors from Websites + 0% +
    +

    + + Back to top ↑ +

    + +

    + Channel Type +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Channel Type   + +  Visits   + +  Actions   + +  Actions per Visit   + +  Avg. Time on Website   + +  Bounce Rate   + +  Revenue   +
    + Direct Entry + 5 + + 16 + + 3.2 + + 00:22:50 + + 20% + + $13,361.11 +
    +

    + + Back to top ↑ +

    + +

    + All Channels +

    + + There is no data for this report. + +

    + Keywords +

    + + There is no data for this report. + +

    + Search Engines +

    + + There is no data for this report. + +

    + Websites +

    + + There is no data for this report. + +

    + Social Networks +

    + + There is no data for this report. + +

    + AI Assistants +

    + + There is no data for this report. + +

    + Campaigns +

    + + There is no data for this report. + +

    + Ecommerce Orders +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Ecommerce Orders + 4 +
    + Visits with Conversions + 2 +
    + Revenue + $13,351.11 +
    + Subtotal + $2,700 +
    + Tax + $531 +
    + Shipping + $120.11 +
    + Discount + $686 +
    + Purchased Products + 12 +
    + Average Order Value + $3,337.78 +
    + Conversion Rate + 40% +
    +

    + + Back to top ↑ +

    + +

    + Ecommerce Orders - Visits to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   +
    + 1 visit + 0 +
    + 2 visits + 2 +
    + 3 visits + 0 +
    + 4 visits + 0 +
    + 5 visits + 2 +
    + 6 visits + 0 +
    + 7 visits + 0 +
    + 8 visits + 0 +
    + 9-14 visits + 0 +
    + 15-25 visits + 0 +
    + 26-50 visits + 0 +
    + 51-100 visits + 0 +
    + 101+ visits + 0 +
    +

    + + Back to top ↑ +

    + +

    + Ecommerce Orders - Days to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   +
    + 0 days + 2 +
    + 1 day + 2 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + Abandoned Carts +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Abandoned Carts + 3 +
    + Revenue left in cart + $7,530.33 +
    + Products left in cart + 12 +
    + Average Order Value + $2,510.11 +
    + Conversion Rate + 60% +
    +

    + + Back to top ↑ +

    + +

    + Abandoned Carts - Visits to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   +
    + 1 visit + 0 +
    + 2 visits + 1 +
    + 3 visits + 1 +
    + 4 visits + 1 +
    + 5 visits + 0 +
    + 6 visits + 0 +
    + 7 visits + 0 +
    + 8 visits + 0 +
    + 9-14 visits + 0 +
    + 15-25 visits + 0 +
    + 26-50 visits + 0 +
    + 51-100 visits + 0 +
    + 101+ visits + 0 +
    +

    + + Back to top ↑ +

    + +

    + Abandoned Carts - Days to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   +
    + 0 days + 2 +
    + 1 day + 1 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + Product Name +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Product Name   + +  Product Revenue   + +  Quantity   + +  Unique Purchases   + +  Visits   + +  Average Price   + +  Average Quantity   + +  Product Conversion Rate   +
    + Canon SLR + $1,500 + + 1 + + 1 + + 0 + + $1,500 + + 1 + + 0% +
    + PRODUCT name + $1,011.22 + + 3 + + 2 + + 2 + + $255.61 + + 1.5 + + 100% +
    + PRODUCT name BIS + $600 + + 6 + + 1 + + 0 + + $100 + + 6 + + 0% +
    + TRIPOD - bought day after + $200 + + 2 + + 1 + + 0 + + $100 + + 2 + + 0% +
    + PRODUCT THREE LEFT in cart + $0 + + 0 + + 0 + + 3 + + $1,332 + + 0 + + 0% +
    + PRODUCT TWO LEFT in cart + $0 + + 0 + + 0 + + 3 + + $0 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Product SKU +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Product SKU   + +  Product Revenue   + +  Quantity   + +  Unique Purchases   + +  Visits   + +  Average Price   + +  Average Quantity   + +  Product Conversion Rate   +
    + SKU2 + $1,500 + + 1 + + 1 + + 1 + + $1,500 + + 1 + + 100% +
    + SKU VERY nice indeed + $1,011.22 + + 3 + + 2 + + 4 + + $255.61 + + 1.5 + + 50% +
    + ANOTHER SKU HERE + $600 + + 6 + + 1 + + 0 + + $100 + + 6 + + 0% +
    + TRIPOD SKU + $200 + + 2 + + 1 + + 0 + + $100 + + 2 + + 0% +
    + SKU IN ABANDONED CART TWO + $0 + + 0 + + 0 + + 3 + + $0 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Product Category +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Product Category   + +  Product Revenue   + +  Quantity   + +  Unique Purchases   + +  Visits   + +  Average Price   + +  Average Quantity   + +  Product Conversion Rate   +
    + Electronics & Cameras + $2,500 + + 3 + + 2 + + 3 + + $1,000 + + 1.5 + + 66.67% +
    + Multiple Category 1 + $1,000 + + 2 + + 1 + + 1 + + $500 + + 2 + + 100% +
    + Multiple Category 2 + $1,000 + + 2 + + 1 + + 1 + + $500 + + 2 + + 100% +
    + Multiple Category 4 + $1,000 + + 2 + + 1 + + 1 + + $500 + + 2 + + 100% +
    + Multiple Category 5 + $1,000 + + 2 + + 1 + + 1 + + $500 + + 2 + + 100% +
    + Product Category not defined + $611.22 + + 7 + + 2 + + 4 + + $55.61 + + 3.5 + + 50% +
    + Tools + $200 + + 2 + + 1 + + 0 + + $100 + + 2 + + 0% +
    + Category TWO LEFT in cart + $0 + + 0 + + 0 + + 3 + + $0 + + 0 + + 0% +
    + second category + $0 + + 0 + + 0 + + 3 + + $0 + + 0 + + 0% +
    +

    + + Back to top ↑ +

    + +

    + Goals +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Conversions + 5 +
    + Visits with Conversions + 4 +
    + Revenue + $13,361.11 +
    + Conversion Rate + 80% +
    +

    + + Back to top ↑ +

    + +

    + Visits to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   + +  Revenue   +
    + 1 visit + 1 + + $0 +
    + 2 visits + 0 + + $0 +
    + 3 visits + 0 + + $0 +
    + 4 visits + 0 + + $0 +
    + 5 visits + 0 + + $0 +
    + 6 visits + 0 + + $0 +
    + 7 visits + 0 + + $0 +
    + 8 visits + 0 + + $0 +
    + 9-14 visits + 0 + + $0 +
    + 15-25 visits + 0 + + $0 +
    + 26-50 visits + 0 + + $0 +
    + 51-100 visits + 0 + + $0 +
    + 101+ visits + 0 + + $0 +
    +

    + + Back to top ↑ +

    + +

    + Days to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   + +  Revenue   +
    + 0 days + 1 + + $0 +
    + 1 day + 0 + + $0 +
    + 2 days + 0 + + $0 +
    + 3 days + 0 + + $0 +
    + 4 days + 0 + + $0 +
    + 5 days + 0 + + $0 +
    + 6 days + 0 + + $0 +
    + 7 days + 0 + + $0 +
    + 8-14 days + 0 + + $0 +
    + 15-30 days + 0 + + $0 +
    + 31-60 days + 0 + + $0 +
    + 61-120 days + 0 + + $0 +
    + 121-364 days + 0 + + $0 +
    + 365+ days + 0 + + $0 +
    +

    + + Back to top ↑ +

    + +

    + Goals Overview +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Conversions + 4 +
    + Visits with Conversions + 2 +
    + Revenue + $13,351.11 +
    + Conversion Rate + 40% +
    +

    + + Back to top ↑ +

    + +

    + Goals Overview - Visits to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   +
    + 1 visit + 1 +
    + 2 visits + 0 +
    + 3 visits + 0 +
    + 4 visits + 0 +
    + 5 visits + 0 +
    + 6 visits + 0 +
    + 7 visits + 0 +
    + 8 visits + 0 +
    + 9-14 visits + 0 +
    + 15-25 visits + 0 +
    + 26-50 visits + 0 +
    + 51-100 visits + 0 +
    + 101+ visits + 0 +
    +

    + + Back to top ↑ +

    + +

    + Goals Overview - Days to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   +
    + 0 days + 1 +
    + 1 day + 0 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + Goal title match, triggered ONCE +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Conversions + 1 +
    + Visits with Conversions + 1 +
    + Revenue + $10 +
    + Conversion Rate + 20% +
    +

    + + Back to top ↑ +

    + +

    + title match, triggered ONCE - Visits to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Visits to Conversion   + +  Conversions   +
    + 1 visit + 1 +
    + 2 visits + 0 +
    + 3 visits + 0 +
    + 4 visits + 0 +
    + 5 visits + 0 +
    + 6 visits + 0 +
    + 7 visits + 0 +
    + 8 visits + 0 +
    + 9-14 visits + 0 +
    + 15-25 visits + 0 +
    + 26-50 visits + 0 +
    + 51-100 visits + 0 +
    + 101+ visits + 0 +
    +

    + + Back to top ↑ +

    + +

    + title match, triggered ONCE - Days to Conversion +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Days to Conversion   + +  Conversions   +
    + 0 days + 1 +
    + 1 day + 0 +
    + 2 days + 0 +
    + 3 days + 0 +
    + 4 days + 0 +
    + 5 days + 0 +
    + 6 days + 0 +
    + 7 days + 0 +
    + 8-14 days + 0 +
    + 15-30 days + 0 +
    + 31-60 days + 0 +
    + 61-120 days + 0 +
    + 121-364 days + 0 +
    + 365+ days + 0 +
    +

    + + Back to top ↑ +

    + +

    + AI Chatbots Overview +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Requests + 0 +
    + Acquired visits + 0 +
    + Not found requests + 0 +
    + Unique AI chatbots + 0 +
    + Server error (5xx) requests + 0 +
    + Click-through rate + 0 +
    + Unique page URLs + 0 +
    + Unique document URLs + 0 +
    +

    + + Back to top ↑ +

    + +

    + AI Chatbots +

    + + There is no data for this report. + +

    + Pages +

    + + There is no data for this report. + +

    + Documents +

    + + There is no data for this report. + +

    + Broken Pages and Documents +

    + + There is no data for this report. + +

    + Human-Favoured Pages +

    + + There is no data for this report. + +

    + AI-Favoured Pages +

    + + There is no data for this report. + +

    + AI Agent Visits +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +  Name   + +  Value   +
    + Unique AI Agent Visitors + 0 +
    + AI Agent Users + 0 +
    + AI Agent Visits + 0 +
    + Actions by AI Agent Visits + 0 +
    + Maximum Actions in AI Agent Visit + 0 +
    + Bounce Rate for AI Agent Visits + 0% +
    + Avg. Actions per AI Agent Visit + 0 +
    + Avg. Duration of an AI Agent Visit (in sec) + 00:00:00 +
    + Unique Human Visitors + 1 +
    + Human Users + 0 +
    + Human Visits + 5 +
    + Actions by Human Visits + 16 +
    + Maximum Actions in one Human Visit + 6 +
    + Bounce Rate for Human Visits + 20% +
    + Avg. Actions per Human Visit + 3.2 +
    + Avg. Duration of a Human Visit (in sec) + 00:22:50 +
    +

    + + Back to top ↑ +

    + +

    + Data tables +

    + + There is no data for this report. + +

    + Temperatures evolution over time +

    + + There is no data for this report. + +

    + Pie graph +

    + + There is no data for this report. + +

    + Advanced tag cloud: with logos and links +

    + + There is no data for this report. + +
    + +

    + Powered by + Matomo Analytics +
    + The leading open analytics platform that respects your privacy. +

    + +

    To unsubscribe from this report please follow this link: Unsubscribe.

    + +
    + + diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv new file mode 100644 index 0000000000000000000000000000000000000000..88e2a8e00fa54f21e7c42953a793bba6ebc1f181 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv @@ -0,0 +1,588 @@ +All Websites dashboard +label,nb_visits,nb_actions,nb_pageviews,hits,ai_chatbots_requests,revenue,nb_conversions,orders,ecommerce_revenue,visits_evolution,actions_evolution,pageviews_evolution,hits_evolution,ai_chatbots_requests_evolution,revenue_evolution,nb_conversions_evolution,orders_evolution,ecommerce_revenue_evolution +"A very long name for a very tiny test site, that also reaches the 90 character limit fully",5,16,16,16,0,"$13,361.11",5,4,"$13,351.11",100%,100%,100%,100%,0%,100%,100%,100%,100% +Piwik test,2,1,1,1,0,$0,0,0,$0,100%,100%,100%,100%,0%,0%,0%,0,0 + +Visits Summary +nb_uniq_visitors,nb_visits,nb_actions,max_actions,nb_actions_per_visit,avg_time_on_site,bounce_rate +1,5,16,6,3.2,00:22:50,20% + +Country +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Poland,4,12,"$13,351.11",3,00:25:32,25% +France,1,4,$10,4,00:12:02,0% + +Continent +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Europe,5,16,"$13,361.11",3.2,00:22:50,20% + +Region +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Unknown,5,16,"$13,361.11",3.2,00:22:50,20% + +Web-browser language +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +Polish,4,12,75%,3,00:25:32,25% +French,1,4,100%,4,00:12:02,0% + +City +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Unknown,5,16,"$13,361.11",3.2,00:22:50,20% + +Language code +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +Polish (pl),4,12,75%,3,00:25:32,25% +French (fr),1,4,100%,4,00:12:02,0% + +Device type +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Desktop,5,16,"$13,361.11",3.2,00:22:50,20% + +Device model +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Generic Desktop,5,16,"$13,361.11",3.2,00:22:50,20% + +Device brand +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Unknown,5,16,"$13,361.11",3.2,00:22:50,20% + +Screen Resolution +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +1024x768,5,16,80%,3.2,00:22:50,20% + +Operating System versions +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +Windows XP,5,16,80%,3.2,00:22:50,20% + +Browsers +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Firefox,5,16,"$13,361.11",3.2,00:22:50,20% + +Browser version +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +Firefox 3.6,5,16,80%,3.2,00:22:50,20% + +Configurations +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +Windows / Firefox / 1024x768,5,16,80%,3.2,00:22:50,20% + +Operating System families +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +Windows,5,16,80%,3.2,00:22:50,20% + +Browser engines +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +Gecko (Firefox),5,16,80%,3.2,00:22:50,20% + +Browser Plugins +label,nb_visits,nb_visits_percentage +Cookie,5,100% +Flash,5,100% +Java,5,100% +Pdf,0,0% +Quicktime,0,0% +Realplayer,0,0% +Silverlight,0,0% +Windowsmedia,0,0% + +Visits per local time +label,nb_visits,nb_actions,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate +00,0,0,0%,0,00:00:00,0% +01,0,0,0%,0,00:00:00,0% +02,0,0,0%,0,00:00:00,0% +03,0,0,0%,0,00:00:00,0% +04,0,0,0%,0,00:00:00,0% +05,0,0,0%,0,00:00:00,0% +06,0,0,0%,0,00:00:00,0% +07,0,0,0%,0,00:00:00,0% +08,0,0,0%,0,00:00:00,0% +09,0,0,0%,0,00:00:00,0% +10,0,0,0%,0,00:00:00,0% +11,0,0,0%,0,00:00:00,0% +12,5,16,80%,3.2,00:22:50,20% +13,0,0,0%,0,00:00:00,0% +14,0,0,0%,0,00:00:00,0% +15,0,0,0%,0,00:00:00,0% +16,0,0,0%,0,00:00:00,0% +17,0,0,0%,0,00:00:00,0% +18,0,0,0%,0,00:00:00,0% +19,0,0,0%,0,00:00:00,0% +20,0,0,0%,0,00:00:00,0% +21,0,0,0%,0,00:00:00,0% +22,0,0,0%,0,00:00:00,0% +23,0,0,0%,0,00:00:00,0% + +Visits per hour in the site's timezone +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +00,1,4,$10,4,00:12:02,0% +01,1,6,$0,6,01:06:01,0% +02,1,3,"$3,111.11",3,00:18:01,0% +03,0,0,$0,0,00:00:00,0% +04,0,0,$0,0,00:00:00,0% +05,0,0,$0,0,00:00:00,0% +06,1,0,"$10,240",0,00:06:03,100% +07,1,3,$0,3,00:12:01,0% +08,0,0,$0,0,00:00:00,0% +09,0,0,$0,0,00:00:00,0% +10,0,0,$0,0,00:00:00,0% +11,0,0,$0,0,00:00:00,0% +12,0,0,$0,0,00:00:00,0% +13,0,0,$0,0,00:00:00,0% +14,0,0,$0,0,00:00:00,0% +15,0,0,$0,0,00:00:00,0% +16,0,0,$0,0,00:00:00,0% +17,0,0,$0,0,00:00:00,0% +18,0,0,$0,0,00:00:00,0% +19,0,0,$0,0,00:00:00,0% +20,0,0,$0,0,00:00:00,0% +21,0,0,$0,0,00:00:00,0% +22,0,0,$0,0,00:00:00,0% +23,0,0,$0,0,00:00:00,0% + +Visits by day of the week +label,nb_visits,conversion_rate,nb_actions_per_visit,avg_time_on_site,bounce_rate,nb_actions +Monday,0,0%,0,00:00:00,0%,0 +Tuesday,3,66.67%,4.3,00:30:01,0%,13 +Wednesday,2,100%,1.5,00:12:02,50%,3 +Thursday,0,0%,0,00:00:00,0%,0 +Friday,0,0%,0,00:00:00,0%,0 +Saturday,0,0%,0,00:00:00,0%,0 +Sunday,0,0%,0,00:00:00,0%,0 + +User IDs +No data available + +Custom Variables +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +ValueIsZero - 0,5,16,"$13,361.11",3.2,00:22:50,20% +VisitorType - NewLoggedOut,5,16,"$13,361.11",3.2,00:22:50,20% +VisitorName - Great name!,4,12,"$13,351.11",3,00:25:32,25% + +Actions - Main metrics +nb_pageviews,nb_uniq_pageviews,nb_downloads,nb_uniq_downloads,nb_outlinks,nb_uniq_outlinks,nb_searches,nb_keywords,hits +16,4,0,0,0,0,0,0,16 + +Bandwidth - Main metrics +nb_total_overall_bandwidth,nb_total_pageview_bandwidth,nb_total_download_bandwidth +0,0,0 + +Page URLs +label,nb_visits,nb_hits,bounce_rate,avg_time_on_page,exit_rate +/index.htm,4,16,0%,00:03:23,100% + +Entry pages +label,entry_nb_visits,entry_bounce_count,bounce_rate +/index.htm,4,0,0% + +Entry page titles +label,entry_nb_visits,entry_bounce_count,bounce_rate +View product left in cart,1,0,0% +incredible title!,1,0,0% +Looking at Electronics & Cameras page with a page level custom variable,1,0,0% + +Exit pages +label,nb_visits,exit_nb_visits,exit_rate +/index.htm,4,4,100% + +Exit page titles +label,nb_visits,exit_nb_visits,exit_rate +View product left in cart,3,3,100% +Another Product page with multiple categories,1,1,100% + +Page titles +label,nb_visits,nb_hits,bounce_rate,avg_time_on_page,exit_rate +View product left in cart,3,9,0%,00:02:00,100% +Another Product page,1,1,0%,00:06:00,0% +Another Product page with multiple categories,1,1,0%,00:00:00,100% +Another Product page with no category,1,1,0%,00:00:01,0% +incredible title!,1,1,0%,00:06:00,0% +Looking at Electronics & Cameras page again,1,1,0%,00:06:00,0% +Looking at Electronics & Cameras page with a page level custom variable,1,1,0%,00:06:00,0% +Looking at product page,1,1,0%,00:12:00,0% + +Site Search Keywords +No data available + +Pages Following a Site Search +No data available + +Search Keywords with No Results +No data available + +Page Titles Following a Site Search +No data available + +Search Categories +No data available + +Outlinks +No data available + +Downloads +No data available + +Event Categories +No data available + +Event Actions +No data available + +Event Names +No data available + +Content Name +No data available + +Content Piece +No data available + +Length of visits +label,nb_visits +0–10s,0 +11–30s,0 +31–60s,0 +1-2 min,0 +2-4 min,0 +4-7 min,1 +7-10 min,0 +10-15 min,2 +15-30 min,1 +30+ min,1 + +Pages per visit +label,nb_visits +1 page,0 +2 pages,0 +3 pages,2 +4 pages,1 +5 pages,0 +6-7 pages,1 +8-10 pages,0 +11-14 pages,0 +15-20 pages,0 +21+ pages,0 + +Visits by visit number +label,nb_visits,nb_visits_percentage +1 visit,1,20% +2 visits,1,20% +3 visits,1,20% +4 visits,1,20% +5 visits,1,20% +6 visits,0,0% +7 visits,0,0% +8 visits,0,0% +9-14 visits,0,0% +15-25 visits,0,0% +26-50 visits,0,0% +51-100 visits,0,0% +101-200 visits,0,0% +201+ visits,0,0% + +Visits by days since last visit +label,nb_visits +New visits,1 +0 days,2 +1 day,0 +2 days,0 +3 days,0 +4 days,0 +5 days,0 +6 days,0 +7 days,0 +8-14 days,0 +15-30 days,0 +31-60 days,0 +61-120 days,0 +121-364 days,0 +365+ days,0 + +Returning Visits +nb_uniq_visitors_new,nb_users_new,nb_visits_new,nb_actions_new,max_actions_new,bounce_rate_new,nb_actions_per_visit_new,avg_time_on_site_new,nb_uniq_visitors_returning,nb_users_returning,nb_visits_returning,nb_actions_returning,max_actions_returning,bounce_rate_returning,nb_actions_per_visit_returning,avg_time_on_site_returning +1,0,1,4,4,0%,4,00:12:02,1,0,4,12,6,25%,3,00:25:32 + +Performance overview +avg_time_network,avg_time_server,avg_time_transfer,avg_time_dom_processing,avg_time_dom_completion,avg_time_on_load,avg_page_load_time +0s,0s,0s,0s,0s,0s,0s + +Referrers Overview +Referrers_visitorsFromSearchEngines,Referrers_visitorsFromSocialNetworks,Referrers_visitorsFromAIAssistants,Referrers_visitorsFromDirectEntry,Referrers_visitorsFromWebsites,Referrers_visitorsFromCampaigns,Referrers_distinctSearchEngines,Referrers_distinctSocialNetworks,Referrers_distinctAIAssistants,Referrers_distinctKeywords,Referrers_distinctWebsites,Referrers_distinctCampaigns,Referrers_visitorsFromDirectEntry_percent,Referrers_visitorsFromSearchEngines_percent,Referrers_visitorsFromAIAssistants_percent,Referrers_visitorsFromCampaigns_percent,Referrers_visitorsFromSocialNetworks_percent,Referrers_visitorsFromWebsites_percent +0,0,0,5,0,0,0,0,0,0,0,0,100%,0%,0%,0%,0%,0% + +Channel Type +label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate +Direct Entry,5,16,"$13,361.11",3.2,00:22:50,20% + +All Channels +No data available + +Keywords +No data available + +Search Engines +No data available + +Websites +No data available + +Social Networks +No data available + +AI Assistants +No data available + +Campaigns +No data available + +Ecommerce Orders +nb_conversions,nb_visits_converted,revenue,revenue_subtotal,revenue_tax,revenue_shipping,revenue_discount,items,avg_order_revenue,conversion_rate +4,2,"$13,351.11","$2,700",$531,$120.11,$686,12,"$3,337.78",40% + +Ecommerce Orders - Visits to Conversion +label,nb_conversions +1 visit,0 +2 visits,2 +3 visits,0 +4 visits,0 +5 visits,2 +6 visits,0 +7 visits,0 +8 visits,0 +9-14 visits,0 +15-25 visits,0 +26-50 visits,0 +51-100 visits,0 +101+ visits,0 + +Ecommerce Orders - Days to Conversion +label,nb_conversions +0 days,2 +1 day,2 +2 days,0 +3 days,0 +4 days,0 +5 days,0 +6 days,0 +7 days,0 +8-14 days,0 +15-30 days,0 +31-60 days,0 +61-120 days,0 +121-364 days,0 +365+ days,0 + +Abandoned Carts +nb_conversions,revenue,items,avg_order_revenue,conversion_rate +3,"$7,530.33",12,"$2,510.11",60% + +Abandoned Carts - Visits to Conversion +label,nb_conversions +1 visit,0 +2 visits,1 +3 visits,1 +4 visits,1 +5 visits,0 +6 visits,0 +7 visits,0 +8 visits,0 +9-14 visits,0 +15-25 visits,0 +26-50 visits,0 +51-100 visits,0 +101+ visits,0 + +Abandoned Carts - Days to Conversion +label,nb_conversions +0 days,2 +1 day,1 +2 days,0 +3 days,0 +4 days,0 +5 days,0 +6 days,0 +7 days,0 +8-14 days,0 +15-30 days,0 +31-60 days,0 +61-120 days,0 +121-364 days,0 +365+ days,0 + +Product Name +label,revenue,quantity,orders,avg_price,avg_quantity,conversion_rate,nb_visits +Canon SLR,"$1,500",1,1,"$1,500",1,0%,0 +PRODUCT name,"$1,011.22",3,2,$255.61,1.5,100%,2 +PRODUCT name BIS,$600,6,1,$100,6,0%,0 +TRIPOD - bought day after,$200,2,1,$100,2,0%,0 +PRODUCT THREE LEFT in cart,$0,0,0,"$1,332",0,0%,3 +PRODUCT TWO LEFT in cart,$0,0,0,$0,0,0%,3 + +Product SKU +label,revenue,quantity,orders,nb_visits,avg_price,avg_quantity,conversion_rate +SKU2,"$1,500",1,1,1,"$1,500",1,100% +SKU VERY nice indeed,"$1,011.22",3,2,4,$255.61,1.5,50% +ANOTHER SKU HERE,$600,6,1,0,$100,6,0% +TRIPOD SKU,$200,2,1,0,$100,2,0% +SKU IN ABANDONED CART TWO,$0,0,0,3,$0,0,0% + +Product Category +label,revenue,quantity,orders,nb_visits,avg_price,avg_quantity,conversion_rate +Electronics & Cameras,"$2,500",3,2,3,"$1,000",1.5,66.67% +Multiple Category 1,"$1,000",2,1,1,$500,2,100% +Multiple Category 2,"$1,000",2,1,1,$500,2,100% +Multiple Category 4,"$1,000",2,1,1,$500,2,100% +Multiple Category 5,"$1,000",2,1,1,$500,2,100% +Product Category not defined,$611.22,7,2,4,$55.61,3.5,50% +Tools,$200,2,1,0,$100,2,0% +Category TWO LEFT in cart,$0,0,0,3,$0,0,0% +second category,$0,0,0,3,$0,0,0% + +Goals +nb_conversions,nb_visits_converted,revenue,conversion_rate +5,4,"$13,361.11",80% + +Visits to Conversion +label,nb_conversions,revenue +1 visit,1,$0 +2 visits,0,$0 +3 visits,0,$0 +4 visits,0,$0 +5 visits,0,$0 +6 visits,0,$0 +7 visits,0,$0 +8 visits,0,$0 +9-14 visits,0,$0 +15-25 visits,0,$0 +26-50 visits,0,$0 +51-100 visits,0,$0 +101+ visits,0,$0 + +Days to Conversion +label,nb_conversions,revenue +0 days,1,$0 +1 day,0,$0 +2 days,0,$0 +3 days,0,$0 +4 days,0,$0 +5 days,0,$0 +6 days,0,$0 +7 days,0,$0 +8-14 days,0,$0 +15-30 days,0,$0 +31-60 days,0,$0 +61-120 days,0,$0 +121-364 days,0,$0 +365+ days,0,$0 + +Goals Overview +nb_conversions,nb_visits_converted,revenue,conversion_rate +4,2,"$13,351.11",40% + +Goals Overview - Visits to Conversion +label,nb_conversions +1 visit,1 +2 visits,0 +3 visits,0 +4 visits,0 +5 visits,0 +6 visits,0 +7 visits,0 +8 visits,0 +9-14 visits,0 +15-25 visits,0 +26-50 visits,0 +51-100 visits,0 +101+ visits,0 + +Goals Overview - Days to Conversion +label,nb_conversions +0 days,1 +1 day,0 +2 days,0 +3 days,0 +4 days,0 +5 days,0 +6 days,0 +7 days,0 +8-14 days,0 +15-30 days,0 +31-60 days,0 +61-120 days,0 +121-364 days,0 +365+ days,0 + +"Goal title match, triggered ONCE" +nb_conversions,nb_visits_converted,revenue,conversion_rate +1,1,$10,20% + +"title match, triggered ONCE - Visits to Conversion" +label,nb_conversions +1 visit,1 +2 visits,0 +3 visits,0 +4 visits,0 +5 visits,0 +6 visits,0 +7 visits,0 +8 visits,0 +9-14 visits,0 +15-25 visits,0 +26-50 visits,0 +51-100 visits,0 +101+ visits,0 + +"title match, triggered ONCE - Days to Conversion" +label,nb_conversions +0 days,1 +1 day,0 +2 days,0 +3 days,0 +4 days,0 +5 days,0 +6 days,0 +7 days,0 +8-14 days,0 +15-30 days,0 +31-60 days,0 +61-120 days,0 +121-364 days,0 +365+ days,0 + +AI Chatbots Overview +BotTracking_AIChatbotsRequests,BotTracking_AIChatbotsAcquiredVisits,BotTracking_AIChatbotsNotFoundRequests,BotTracking_AIChatbotsUniqueChatbots,BotTracking_AIChatbotsServerErrorRequests,BotTracking_AIChatbotsClickThroughRate,BotTracking_AIChatbotsUniquePageUrls,BotTracking_AIChatbotsUniqueDocumentUrls +0,0,0,0,0,0,0,0 + +AI Chatbots +No data available + +Pages +No data available + +Documents +No data available + +Broken Pages and Documents +No data available + +Human-Favoured Pages +No data available + +AI-Favoured Pages +No data available + +AI Agent Visits +nb_uniq_visitors_ai_agent,nb_users_ai_agent,nb_visits_ai_agent,nb_actions_ai_agent,max_actions_ai_agent,bounce_rate_ai_agent,nb_actions_per_visit_ai_agent,avg_time_on_site_ai_agent,nb_uniq_visitors_human,nb_users_human,nb_visits_human,nb_actions_human,max_actions_human,bounce_rate_human,nb_actions_per_visit_human,avg_time_on_site_human +0,0,0,0,0,0%,0,00:00:00,1,0,5,16,6,20%,3.2,00:22:50 + +Data tables +No data available + +Temperatures evolution over time +No data available + +Pie graph +No data available + +Advanced tag cloud: with logos and links +No data available + diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fa5d0eeff3b503a50c1f963722bd4ae8a7f23667 Binary files /dev/null and b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf differ diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_tsv__ScheduledReports.generateReport_week.original.tsv b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_tsv__ScheduledReports.generateReport_week.original.tsv new file mode 100644 index 0000000000000000000000000000000000000000..c1d2339fb6c1598c05bb082bc568c2d95effecb5 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_tsv__ScheduledReports.generateReport_week.original.tsv @@ -0,0 +1,588 @@ +All Websites dashboard +label nb_visits nb_actions nb_pageviews hits ai_chatbots_requests revenue nb_conversions orders ecommerce_revenue visits_evolution actions_evolution pageviews_evolution hits_evolution ai_chatbots_requests_evolution revenue_evolution nb_conversions_evolution orders_evolution ecommerce_revenue_evolution +"A very long name for a very tiny test site, that also reaches the 90 character limit fully" 5 16 16 16 0 "$13,361.11" 5 4 "$13,351.11" 100% 100% 100% 100% 0% 100% 100% 100% 100% +Piwik test 2 1 1 1 0 $0 0 0 $0 100% 100% 100% 100% 0% 0% 0% 0 0 + +Visits Summary +nb_uniq_visitors nb_visits nb_actions max_actions nb_actions_per_visit avg_time_on_site bounce_rate +1 5 16 6 3.2 00:22:50 20% + +Country +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Poland 4 12 "$13,351.11" 3 00:25:32 25% +France 1 4 $10 4 00:12:02 0% + +Continent +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Europe 5 16 "$13,361.11" 3.2 00:22:50 20% + +Region +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Unknown 5 16 "$13,361.11" 3.2 00:22:50 20% + +Web-browser language +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +Polish 4 12 75% 3 00:25:32 25% +French 1 4 100% 4 00:12:02 0% + +City +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Unknown 5 16 "$13,361.11" 3.2 00:22:50 20% + +Language code +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +Polish (pl) 4 12 75% 3 00:25:32 25% +French (fr) 1 4 100% 4 00:12:02 0% + +Device type +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Desktop 5 16 "$13,361.11" 3.2 00:22:50 20% + +Device model +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Generic Desktop 5 16 "$13,361.11" 3.2 00:22:50 20% + +Device brand +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Unknown 5 16 "$13,361.11" 3.2 00:22:50 20% + +Screen Resolution +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +1024x768 5 16 80% 3.2 00:22:50 20% + +Operating System versions +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +Windows XP 5 16 80% 3.2 00:22:50 20% + +Browsers +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Firefox 5 16 "$13,361.11" 3.2 00:22:50 20% + +Browser version +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +Firefox 3.6 5 16 80% 3.2 00:22:50 20% + +Configurations +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +Windows / Firefox / 1024x768 5 16 80% 3.2 00:22:50 20% + +Operating System families +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +Windows 5 16 80% 3.2 00:22:50 20% + +Browser engines +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +Gecko (Firefox) 5 16 80% 3.2 00:22:50 20% + +Browser Plugins +label nb_visits nb_visits_percentage +Cookie 5 100% +Flash 5 100% +Java 5 100% +Pdf 0 0% +Quicktime 0 0% +Realplayer 0 0% +Silverlight 0 0% +Windowsmedia 0 0% + +Visits per local time +label nb_visits nb_actions conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate +00 0 0 0% 0 00:00:00 0% +01 0 0 0% 0 00:00:00 0% +02 0 0 0% 0 00:00:00 0% +03 0 0 0% 0 00:00:00 0% +04 0 0 0% 0 00:00:00 0% +05 0 0 0% 0 00:00:00 0% +06 0 0 0% 0 00:00:00 0% +07 0 0 0% 0 00:00:00 0% +08 0 0 0% 0 00:00:00 0% +09 0 0 0% 0 00:00:00 0% +10 0 0 0% 0 00:00:00 0% +11 0 0 0% 0 00:00:00 0% +12 5 16 80% 3.2 00:22:50 20% +13 0 0 0% 0 00:00:00 0% +14 0 0 0% 0 00:00:00 0% +15 0 0 0% 0 00:00:00 0% +16 0 0 0% 0 00:00:00 0% +17 0 0 0% 0 00:00:00 0% +18 0 0 0% 0 00:00:00 0% +19 0 0 0% 0 00:00:00 0% +20 0 0 0% 0 00:00:00 0% +21 0 0 0% 0 00:00:00 0% +22 0 0 0% 0 00:00:00 0% +23 0 0 0% 0 00:00:00 0% + +Visits per hour in the site's timezone +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +00 1 4 $10 4 00:12:02 0% +01 1 6 $0 6 01:06:01 0% +02 1 3 "$3,111.11" 3 00:18:01 0% +03 0 0 $0 0 00:00:00 0% +04 0 0 $0 0 00:00:00 0% +05 0 0 $0 0 00:00:00 0% +06 1 0 "$10,240" 0 00:06:03 100% +07 1 3 $0 3 00:12:01 0% +08 0 0 $0 0 00:00:00 0% +09 0 0 $0 0 00:00:00 0% +10 0 0 $0 0 00:00:00 0% +11 0 0 $0 0 00:00:00 0% +12 0 0 $0 0 00:00:00 0% +13 0 0 $0 0 00:00:00 0% +14 0 0 $0 0 00:00:00 0% +15 0 0 $0 0 00:00:00 0% +16 0 0 $0 0 00:00:00 0% +17 0 0 $0 0 00:00:00 0% +18 0 0 $0 0 00:00:00 0% +19 0 0 $0 0 00:00:00 0% +20 0 0 $0 0 00:00:00 0% +21 0 0 $0 0 00:00:00 0% +22 0 0 $0 0 00:00:00 0% +23 0 0 $0 0 00:00:00 0% + +Visits by day of the week +label nb_visits conversion_rate nb_actions_per_visit avg_time_on_site bounce_rate nb_actions +Monday 0 0% 0 00:00:00 0% 0 +Tuesday 3 66.67% 4.3 00:30:01 0% 13 +Wednesday 2 100% 1.5 00:12:02 50% 3 +Thursday 0 0% 0 00:00:00 0% 0 +Friday 0 0% 0 00:00:00 0% 0 +Saturday 0 0% 0 00:00:00 0% 0 +Sunday 0 0% 0 00:00:00 0% 0 + +User IDs +No data available + +Custom Variables +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +ValueIsZero - 0 5 16 "$13,361.11" 3.2 00:22:50 20% +VisitorType - NewLoggedOut 5 16 "$13,361.11" 3.2 00:22:50 20% +VisitorName - Great name! 4 12 "$13,351.11" 3 00:25:32 25% + +Actions - Main metrics +nb_pageviews nb_uniq_pageviews nb_downloads nb_uniq_downloads nb_outlinks nb_uniq_outlinks nb_searches nb_keywords hits +16 4 0 0 0 0 0 0 16 + +Bandwidth - Main metrics +nb_total_overall_bandwidth nb_total_pageview_bandwidth nb_total_download_bandwidth +0 0 0 + +Page URLs +label nb_visits nb_hits bounce_rate avg_time_on_page exit_rate +/index.htm 4 16 0% 00:03:23 100% + +Entry pages +label entry_nb_visits entry_bounce_count bounce_rate +/index.htm 4 0 0% + +Entry page titles +label entry_nb_visits entry_bounce_count bounce_rate +View product left in cart 1 0 0% +incredible title! 1 0 0% +Looking at Electronics & Cameras page with a page level custom variable 1 0 0% + +Exit pages +label nb_visits exit_nb_visits exit_rate +/index.htm 4 4 100% + +Exit page titles +label nb_visits exit_nb_visits exit_rate +View product left in cart 3 3 100% +Another Product page with multiple categories 1 1 100% + +Page titles +label nb_visits nb_hits bounce_rate avg_time_on_page exit_rate +View product left in cart 3 9 0% 00:02:00 100% +Another Product page 1 1 0% 00:06:00 0% +Another Product page with multiple categories 1 1 0% 00:00:00 100% +Another Product page with no category 1 1 0% 00:00:01 0% +incredible title! 1 1 0% 00:06:00 0% +Looking at Electronics & Cameras page again 1 1 0% 00:06:00 0% +Looking at Electronics & Cameras page with a page level custom variable 1 1 0% 00:06:00 0% +Looking at product page 1 1 0% 00:12:00 0% + +Site Search Keywords +No data available + +Pages Following a Site Search +No data available + +Search Keywords with No Results +No data available + +Page Titles Following a Site Search +No data available + +Search Categories +No data available + +Outlinks +No data available + +Downloads +No data available + +Event Categories +No data available + +Event Actions +No data available + +Event Names +No data available + +Content Name +No data available + +Content Piece +No data available + +Length of visits +label nb_visits +0–10s 0 +11–30s 0 +31–60s 0 +1-2 min 0 +2-4 min 0 +4-7 min 1 +7-10 min 0 +10-15 min 2 +15-30 min 1 +30+ min 1 + +Pages per visit +label nb_visits +1 page 0 +2 pages 0 +3 pages 2 +4 pages 1 +5 pages 0 +6-7 pages 1 +8-10 pages 0 +11-14 pages 0 +15-20 pages 0 +21+ pages 0 + +Visits by visit number +label nb_visits nb_visits_percentage +1 visit 1 20% +2 visits 1 20% +3 visits 1 20% +4 visits 1 20% +5 visits 1 20% +6 visits 0 0% +7 visits 0 0% +8 visits 0 0% +9-14 visits 0 0% +15-25 visits 0 0% +26-50 visits 0 0% +51-100 visits 0 0% +101-200 visits 0 0% +201+ visits 0 0% + +Visits by days since last visit +label nb_visits +New visits 1 +0 days 2 +1 day 0 +2 days 0 +3 days 0 +4 days 0 +5 days 0 +6 days 0 +7 days 0 +8-14 days 0 +15-30 days 0 +31-60 days 0 +61-120 days 0 +121-364 days 0 +365+ days 0 + +Returning Visits +nb_uniq_visitors_new nb_users_new nb_visits_new nb_actions_new max_actions_new bounce_rate_new nb_actions_per_visit_new avg_time_on_site_new nb_uniq_visitors_returning nb_users_returning nb_visits_returning nb_actions_returning max_actions_returning bounce_rate_returning nb_actions_per_visit_returning avg_time_on_site_returning +1 0 1 4 4 0% 4 00:12:02 1 0 4 12 6 25% 3 00:25:32 + +Performance overview +avg_time_network avg_time_server avg_time_transfer avg_time_dom_processing avg_time_dom_completion avg_time_on_load avg_page_load_time +0s 0s 0s 0s 0s 0s 0s + +Referrers Overview +Referrers_visitorsFromSearchEngines Referrers_visitorsFromSocialNetworks Referrers_visitorsFromAIAssistants Referrers_visitorsFromDirectEntry Referrers_visitorsFromWebsites Referrers_visitorsFromCampaigns Referrers_distinctSearchEngines Referrers_distinctSocialNetworks Referrers_distinctAIAssistants Referrers_distinctKeywords Referrers_distinctWebsites Referrers_distinctCampaigns Referrers_visitorsFromDirectEntry_percent Referrers_visitorsFromSearchEngines_percent Referrers_visitorsFromAIAssistants_percent Referrers_visitorsFromCampaigns_percent Referrers_visitorsFromSocialNetworks_percent Referrers_visitorsFromWebsites_percent +0 0 0 5 0 0 0 0 0 0 0 0 100% 0% 0% 0% 0% 0% + +Channel Type +label nb_visits nb_actions revenue nb_actions_per_visit avg_time_on_site bounce_rate +Direct Entry 5 16 "$13,361.11" 3.2 00:22:50 20% + +All Channels +No data available + +Keywords +No data available + +Search Engines +No data available + +Websites +No data available + +Social Networks +No data available + +AI Assistants +No data available + +Campaigns +No data available + +Ecommerce Orders +nb_conversions nb_visits_converted revenue revenue_subtotal revenue_tax revenue_shipping revenue_discount items avg_order_revenue conversion_rate +4 2 "$13,351.11" "$2,700" $531 $120.11 $686 12 "$3,337.78" 40% + +Ecommerce Orders - Visits to Conversion +label nb_conversions +1 visit 0 +2 visits 2 +3 visits 0 +4 visits 0 +5 visits 2 +6 visits 0 +7 visits 0 +8 visits 0 +9-14 visits 0 +15-25 visits 0 +26-50 visits 0 +51-100 visits 0 +101+ visits 0 + +Ecommerce Orders - Days to Conversion +label nb_conversions +0 days 2 +1 day 2 +2 days 0 +3 days 0 +4 days 0 +5 days 0 +6 days 0 +7 days 0 +8-14 days 0 +15-30 days 0 +31-60 days 0 +61-120 days 0 +121-364 days 0 +365+ days 0 + +Abandoned Carts +nb_conversions revenue items avg_order_revenue conversion_rate +3 "$7,530.33" 12 "$2,510.11" 60% + +Abandoned Carts - Visits to Conversion +label nb_conversions +1 visit 0 +2 visits 1 +3 visits 1 +4 visits 1 +5 visits 0 +6 visits 0 +7 visits 0 +8 visits 0 +9-14 visits 0 +15-25 visits 0 +26-50 visits 0 +51-100 visits 0 +101+ visits 0 + +Abandoned Carts - Days to Conversion +label nb_conversions +0 days 2 +1 day 1 +2 days 0 +3 days 0 +4 days 0 +5 days 0 +6 days 0 +7 days 0 +8-14 days 0 +15-30 days 0 +31-60 days 0 +61-120 days 0 +121-364 days 0 +365+ days 0 + +Product Name +label revenue quantity orders avg_price avg_quantity conversion_rate nb_visits +Canon SLR "$1,500" 1 1 "$1,500" 1 0% 0 +PRODUCT name "$1,011.22" 3 2 $255.61 1.5 100% 2 +PRODUCT name BIS $600 6 1 $100 6 0% 0 +TRIPOD - bought day after $200 2 1 $100 2 0% 0 +PRODUCT THREE LEFT in cart $0 0 0 "$1,332" 0 0% 3 +PRODUCT TWO LEFT in cart $0 0 0 $0 0 0% 3 + +Product SKU +label revenue quantity orders nb_visits avg_price avg_quantity conversion_rate +SKU2 "$1,500" 1 1 1 "$1,500" 1 100% +SKU VERY nice indeed "$1,011.22" 3 2 4 $255.61 1.5 50% +ANOTHER SKU HERE $600 6 1 0 $100 6 0% +TRIPOD SKU $200 2 1 0 $100 2 0% +SKU IN ABANDONED CART TWO $0 0 0 3 $0 0 0% + +Product Category +label revenue quantity orders nb_visits avg_price avg_quantity conversion_rate +Electronics & Cameras "$2,500" 3 2 3 "$1,000" 1.5 66.67% +Multiple Category 1 "$1,000" 2 1 1 $500 2 100% +Multiple Category 2 "$1,000" 2 1 1 $500 2 100% +Multiple Category 4 "$1,000" 2 1 1 $500 2 100% +Multiple Category 5 "$1,000" 2 1 1 $500 2 100% +Product Category not defined $611.22 7 2 4 $55.61 3.5 50% +Tools $200 2 1 0 $100 2 0% +Category TWO LEFT in cart $0 0 0 3 $0 0 0% +second category $0 0 0 3 $0 0 0% + +Goals +nb_conversions nb_visits_converted revenue conversion_rate +5 4 "$13,361.11" 80% + +Visits to Conversion +label nb_conversions revenue +1 visit 1 $0 +2 visits 0 $0 +3 visits 0 $0 +4 visits 0 $0 +5 visits 0 $0 +6 visits 0 $0 +7 visits 0 $0 +8 visits 0 $0 +9-14 visits 0 $0 +15-25 visits 0 $0 +26-50 visits 0 $0 +51-100 visits 0 $0 +101+ visits 0 $0 + +Days to Conversion +label nb_conversions revenue +0 days 1 $0 +1 day 0 $0 +2 days 0 $0 +3 days 0 $0 +4 days 0 $0 +5 days 0 $0 +6 days 0 $0 +7 days 0 $0 +8-14 days 0 $0 +15-30 days 0 $0 +31-60 days 0 $0 +61-120 days 0 $0 +121-364 days 0 $0 +365+ days 0 $0 + +Goals Overview +nb_conversions nb_visits_converted revenue conversion_rate +4 2 "$13,351.11" 40% + +Goals Overview - Visits to Conversion +label nb_conversions +1 visit 1 +2 visits 0 +3 visits 0 +4 visits 0 +5 visits 0 +6 visits 0 +7 visits 0 +8 visits 0 +9-14 visits 0 +15-25 visits 0 +26-50 visits 0 +51-100 visits 0 +101+ visits 0 + +Goals Overview - Days to Conversion +label nb_conversions +0 days 1 +1 day 0 +2 days 0 +3 days 0 +4 days 0 +5 days 0 +6 days 0 +7 days 0 +8-14 days 0 +15-30 days 0 +31-60 days 0 +61-120 days 0 +121-364 days 0 +365+ days 0 + +"Goal title match, triggered ONCE" +nb_conversions nb_visits_converted revenue conversion_rate +1 1 $10 20% + +"title match, triggered ONCE - Visits to Conversion" +label nb_conversions +1 visit 1 +2 visits 0 +3 visits 0 +4 visits 0 +5 visits 0 +6 visits 0 +7 visits 0 +8 visits 0 +9-14 visits 0 +15-25 visits 0 +26-50 visits 0 +51-100 visits 0 +101+ visits 0 + +"title match, triggered ONCE - Days to Conversion" +label nb_conversions +0 days 1 +1 day 0 +2 days 0 +3 days 0 +4 days 0 +5 days 0 +6 days 0 +7 days 0 +8-14 days 0 +15-30 days 0 +31-60 days 0 +61-120 days 0 +121-364 days 0 +365+ days 0 + +AI Chatbots Overview +BotTracking_AIChatbotsRequests BotTracking_AIChatbotsAcquiredVisits BotTracking_AIChatbotsNotFoundRequests BotTracking_AIChatbotsUniqueChatbots BotTracking_AIChatbotsServerErrorRequests BotTracking_AIChatbotsClickThroughRate BotTracking_AIChatbotsUniquePageUrls BotTracking_AIChatbotsUniqueDocumentUrls +0 0 0 0 0 0 0 0 + +AI Chatbots +No data available + +Pages +No data available + +Documents +No data available + +Broken Pages and Documents +No data available + +Human-Favoured Pages +No data available + +AI-Favoured Pages +No data available + +AI Agent Visits +nb_uniq_visitors_ai_agent nb_users_ai_agent nb_visits_ai_agent nb_actions_ai_agent max_actions_ai_agent bounce_rate_ai_agent nb_actions_per_visit_ai_agent avg_time_on_site_ai_agent nb_uniq_visitors_human nb_users_human nb_visits_human nb_actions_human max_actions_human bounce_rate_human nb_actions_per_visit_human avg_time_on_site_human +0 0 0 0 0 0% 0 00:00:00 1 0 5 16 6 20% 3.2 00:22:50 + +Data tables +No data available + +Temperatures evolution over time +No data available + +Pie graph +No data available + +Advanced tag cloud: with logos and links +No data available + diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_via_sms_all_sites__ScheduledReports.generateReport_week.original.sms.txt b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_via_sms_all_sites__ScheduledReports.generateReport_week.original.sms.txt new file mode 100644 index 0000000000000000000000000000000000000000..b4ed253ff0bf7bdf56cb7598b91f6104559bf086 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_via_sms_all_sites__ScheduledReports.generateReport_week.original.sms.txt @@ -0,0 +1 @@ +week April 4 – 10, 2011. A very long name for a very tiny test site, that also reaches the 90 character limit fully: 5 Visits (+100%), 16 Actions (+100%), Revenue: $130 (+100%), 5 Goal conversions (+100%), Product Revenue: $130 (+100%), 4 Ecommerce Orders (+100%). Piwik test: 2 Visits (+100%), 1 Actions (+100%) \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_via_sms_one_site__ScheduledReports.generateReport_week.original.sms.txt b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_via_sms_one_site__ScheduledReports.generateReport_week.original.sms.txt new file mode 100644 index 0000000000000000000000000000000000000000..d94f0eb0ca51a768626c365c68494de7df029b04 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_via_sms_one_site__ScheduledReports.generateReport_week.original.sms.txt @@ -0,0 +1 @@ +week April 4 – 10, 2011. 5 Visits (+100%), 16 Actions (+100%), Revenue: $130 (+100%), 5 Goal conversions (+100%), Product Revenue: $130 (+100%), 4 Ecommerce Orders (+100%) \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsCategory_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..21c16a613c9c01fd5571c620a8f64a11a036e375 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsCategory_day.xml @@ -0,0 +1,150 @@ + + + + + + 2500 + 3 + 2 + 3 + 2 + 6 + 1000 + 1.5 + 66.67% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+5 + + + + 611.22 + 7 + 2 + 3 + 1 + 5 + 55.61 + 3.5 + 66.67% + productCategory==Product+Category+not+defined + + + + 2 + 1 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 2 + 1 + 2 + 0 + 0 + 0% + productCategory==second+category + + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productCategory==Tools + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 1 + 1 + 2 + 666 + 0 + 0% + productCategory==Product+Category+not+defined + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==second+category + + + + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsName_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..bdfd71bb125b2268a5aad800c336e536485d054c --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsName_day.xml @@ -0,0 +1,95 @@ + + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 1011.22 + 3 + 2 + 2 + 1 + 5 + 255.61 + 1.5 + 100% + productName==PRODUCT+name + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + + + 2 + 1 + 4 + 666 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 2 + 1 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productName==TRIPOD+-+bought+day+after + + + + 1 + 1 + 2 + 666 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 1 + 1 + 1 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + + + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsSku_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..4964e2addd4d35086498f69794a470282b75e86f --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates__Goals.getItemsSku_day.xml @@ -0,0 +1,88 @@ + + + + + + 1500 + 1 + 1 + 1 + 1 + 1 + 1500 + 1 + 100% + productSku==SKU2 + + + + 1011.22 + 3 + 2 + 3 + 1 + 8 + 255.61 + 1.5 + 66.67% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 2 + 1 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productSku==TRIPOD+SKU + + + + 1 + 1 + 1 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + + + 1 + 1 + 2 + 666 + 0 + 0% + productSku==SKU+VERY+nice+indeed + + + + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsCategory_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..3c5157a826c7431b8bb09d06fc78d4a561906c53 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsCategory_day.xml @@ -0,0 +1,161 @@ + + + + + + + 2500 + 3 + 2 + 3 + 2 + 6 + 1000 + 1.5 + 66.67% + productCategory==Electronics+%26+Cameras + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+1 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+2 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+4 + + + + 1000 + 2 + 1 + 1 + 1 + 1 + 500 + 2 + 100% + productCategory==Multiple+Category+5 + + + + 611.22 + 7 + 2 + 3 + 1 + 5 + 55.61 + 3.5 + 66.67% + productCategory==Product+Category+not+defined + + + + 2 + 1 + 2 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 2 + 1 + 2 + 0 + 0 + 0% + productCategory==second+category + + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productCategory==Tools + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==Category+TWO+LEFT+in+cart + + + + 1 + 1 + 2 + 666 + 0 + 0% + productCategory==Product+Category+not+defined + + + + 1 + 1 + 1 + 0 + 0 + 0% + productCategory==second+category + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsName_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ddeb90a0429c59c49f8caf017bb2bb8435b5afe --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsName_day.xml @@ -0,0 +1,106 @@ + + + + + + + 1500 + 1 + 1 + 1500 + 1 + 0% + productName==Canon+SLR + + + + 1011.22 + 3 + 2 + 2 + 1 + 5 + 255.61 + 1.5 + 100% + productName==PRODUCT+name + + + + 600 + 6 + 1 + 100 + 6 + 0% + productName==PRODUCT+name+BIS + + + + 2 + 1 + 4 + 666 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 2 + 1 + 2 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productName==TRIPOD+-+bought+day+after + + + + 1 + 1 + 2 + 666 + 0 + 0% + productName==PRODUCT+THREE+LEFT+in+cart + + + + 1 + 1 + 1 + 0 + 0 + 0% + productName==PRODUCT+TWO+LEFT+in+cart + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsSku_day.xml b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..1947e3c20a8141cb0722417a3bc3123db7f078f8 --- /dev/null +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItemsmultipleDates_andMultipleWebsites__Goals.getItemsSku_day.xml @@ -0,0 +1,99 @@ + + + + + + + 1500 + 1 + 1 + 1 + 1 + 1 + 1500 + 1 + 100% + productSku==SKU2 + + + + 1011.22 + 3 + 2 + 3 + 1 + 8 + 255.61 + 1.5 + 66.67% + productSku==SKU+VERY+nice+indeed + + + + 600 + 6 + 1 + 100 + 6 + 0% + productSku==ANOTHER+SKU+HERE + + + + 2 + 1 + 2 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + + + + + 200 + 2 + 1 + 100 + 2 + 0% + productSku==TRIPOD+SKU + + + + 1 + 1 + 1 + 0 + 0 + 0% + productSku==SKU+IN+ABANDONED+CART+TWO + + + + 1 + 1 + 2 + 666 + 0 + 0% + productSku==SKU+VERY+nice+indeed + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/Ecommerce/tests/Unit/Columns/BaseConversionTest.php b/plugins/Ecommerce/tests/Unit/Columns/BaseConversionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2b10618e9875d62722c805090c50effcf86cb843 --- /dev/null +++ b/plugins/Ecommerce/tests/Unit/Columns/BaseConversionTest.php @@ -0,0 +1,67 @@ +roundRevenueIfNeeded($revenue); + } + }; + } + + /** + * @dataProvider getInRangeRevenueData + */ + public function testRoundRevenueIfNeededKeepsValuesWithinCap($revenue, $expected) + { + $this->assertEquals($expected, $this->makeConversion()->exposeRoundRevenueIfNeeded($revenue)); + } + + public function getInRangeRevenueData() + { + return [ + 'decimal is rounded' => [25.559, 25.56], + 'integer kept' => [40, 40], + 'negative kept' => [-50, -50], + 'exactly at the cap' => [GoalManager::MAX_ALLOWED_REVENUE, GoalManager::MAX_ALLOWED_REVENUE], + ]; + } + + /** + * @dataProvider getOutOfRangeRevenueData + */ + public function testRoundRevenueIfNeededRejectsValuesAboveCap($revenue) + { + $this->assertFalse($this->makeConversion()->exposeRoundRevenueIfNeeded($revenue)); + } + + public function getOutOfRangeRevenueData() + { + return [ + 'false is unchanged' => [false], + 'just above the cap' => [GoalManager::MAX_ALLOWED_REVENUE + 1], + 'negative above the cap' => [-GoalManager::MAX_ALLOWED_REVENUE - 1], + 'overflow value' => ['1e308'], + 'negative overflow value' => ['-1e308'], + ]; + } +} diff --git a/plugins/Ecommerce/vue/dist/Ecommerce.umd.js b/plugins/Ecommerce/vue/dist/Ecommerce.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..6c0b37e3432c5d1f9efd86117da7d6de48424eea --- /dev/null +++ b/plugins/Ecommerce/vue/dist/Ecommerce.umd.js @@ -0,0 +1,252 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("vue")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", ], factory); + else if(typeof exports === 'object') + exports["Ecommerce"] = factory(require("CoreHome"), require("vue")); + else + root["Ecommerce"] = factory(root["CoreHome"], root["Vue"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/Ecommerce/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "ConversionOverview", function() { return /* reexport */ ConversionOverview; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Ecommerce/vue/src/ConversionOverview/ConversionOverview.vue?vue&type=template&id=786e300a + +const _hoisted_1 = { + class: "ulGoalTopElements" +}; +const _hoisted_2 = ["innerHTML"]; +const _hoisted_3 = { + key: 0 +}; +const _hoisted_4 = ["innerHTML"]; +const _hoisted_5 = { + key: 1 +}; +const _hoisted_6 = ["innerHTML"]; +const _hoisted_7 = { + key: 2 +}; +const _hoisted_8 = ["innerHTML"]; +const _hoisted_9 = { + key: 3 +}; +const _hoisted_10 = ["innerHTML"]; +const _hoisted_11 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + class: "icon-visitor-profile rowActionIcon" +}, "  ", -1); +const _hoisted_12 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", { + style: { + "clear": "left" + } +}, null, -1); +function render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, { + "content-title": _ctx.translate('Goals_ConversionsOverview') + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("ul", _hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_ColumnRevenue')) + ": ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.revenue) + }, null, 8, _hoisted_2), _ctx.revenue_subtotal ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(", " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Subtotal')) + ": ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.revenue_subtotal) + }, null, 8, _hoisted_4)])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.revenue_tax ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_5, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(", " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Tax')) + ": ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.revenue_tax) + }, null, 8, _hoisted_6)])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.revenue_shipping ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(", " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Shipping')) + ": ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.revenue_shipping) + }, null, 8, _hoisted_8)])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.revenue_shipping ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_9, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(", " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Discount')) + ": ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.revenue_discount) + }, null, 8, _hoisted_10)])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)])]), _ctx.visitorLogEnabled ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("a", { + key: 0, + href: "", + class: "segmentedlog", + onClick: _cache[0] || (_cache[0] = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.showSegmentedVisitorLog(), ["prevent"])) + }, [_hoisted_11, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Live_RowActionTooltipWithDimension', _ctx.translate('General_Goal'))), 1)])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _hoisted_12]), + _: 1 + }, 8, ["content-title"]); +} +// CONCATENATED MODULE: ./plugins/Ecommerce/vue/src/ConversionOverview/ConversionOverview.vue?vue&type=template&id=786e300a + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/Ecommerce/vue/src/ConversionOverview/ConversionOverview.vue?vue&type=script&lang=ts + + +/* harmony default export */ var ConversionOverviewvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + idGoal: { + type: [String, Number], + required: true + }, + visitorLogEnabled: Boolean, + revenue: String, + revenue_subtotal: String, + revenue_tax: String, + revenue_shipping: String, + revenue_discount: String + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"] + }, + methods: { + showSegmentedVisitorLog() { + window.SegmentedVisitorLog.show('Goals.getMetrics', `visitConvertedGoalId==${this.idGoal}`, {}); + } + } +})); +// CONCATENATED MODULE: ./plugins/Ecommerce/vue/src/ConversionOverview/ConversionOverview.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/Ecommerce/vue/src/ConversionOverview/ConversionOverview.vue + + + +ConversionOverviewvue_type_script_lang_ts.render = render + +/* harmony default export */ var ConversionOverview = (ConversionOverviewvue_type_script_lang_ts); +// CONCATENATED MODULE: ./plugins/Ecommerce/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/ + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=Ecommerce.umd.js.map \ No newline at end of file diff --git a/plugins/Ecommerce/vue/dist/Ecommerce.umd.min.js b/plugins/Ecommerce/vue/dist/Ecommerce.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..27f25f590adeaca5b5900a1e04e5674046d1d7d6 --- /dev/null +++ b/plugins/Ecommerce/vue/dist/Ecommerce.umd.min.js @@ -0,0 +1,8 @@ +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue")):"function"===typeof define&&define.amd?define(["CoreHome"],t):"object"===typeof exports?exports["Ecommerce"]=t(require("CoreHome"),require("vue")):e["Ecommerce"]=t(e["CoreHome"],e["Vue"])})("undefined"!==typeof self?self:this,(function(e,t){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="plugins/Ecommerce/vue/dist/",n(n.s="fae3")}({"19dc":function(t,n){t.exports=e},"8bbf":function(e,n){e.exports=t},fae3:function(e,t,n){"use strict";if(n.r(t),n.d(t,"ConversionOverview",(function(){return _})),"undefined"!==typeof window){var o=window.document.currentScript,r=o&&o.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);r&&(n.p=r[1])}var c=n("8bbf");const i={class:"ulGoalTopElements"},l=["innerHTML"],a={key:0},s=["innerHTML"],u={key:1},p=["innerHTML"],d={key:2},b=["innerHTML"],f={key:3},m=["innerHTML"],j=Object(c["createElementVNode"])("span",{class:"icon-visitor-profile rowActionIcon"},"  ",-1),O=Object(c["createElementVNode"])("br",{style:{clear:"left"}},null,-1);function v(e,t,n,o,r,v){const g=Object(c["resolveComponent"])("ContentBlock");return Object(c["openBlock"])(),Object(c["createBlock"])(g,{"content-title":e.translate("Goals_ConversionsOverview")},{default:Object(c["withCtx"])(()=>[Object(c["createElementVNode"])("ul",i,[Object(c["createElementVNode"])("li",null,[Object(c["createTextVNode"])(Object(c["toDisplayString"])(e.translate("General_ColumnRevenue"))+": ",1),Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(e.revenue)},null,8,l),e.revenue_subtotal?(Object(c["openBlock"])(),Object(c["createElementBlock"])("span",a,[Object(c["createTextVNode"])(", "+Object(c["toDisplayString"])(e.translate("General_Subtotal"))+": ",1),Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(e.revenue_subtotal)},null,8,s)])):Object(c["createCommentVNode"])("",!0),e.revenue_tax?(Object(c["openBlock"])(),Object(c["createElementBlock"])("span",u,[Object(c["createTextVNode"])(", "+Object(c["toDisplayString"])(e.translate("General_Tax"))+": ",1),Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(e.revenue_tax)},null,8,p)])):Object(c["createCommentVNode"])("",!0),e.revenue_shipping?(Object(c["openBlock"])(),Object(c["createElementBlock"])("span",d,[Object(c["createTextVNode"])(", "+Object(c["toDisplayString"])(e.translate("General_Shipping"))+": ",1),Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(e.revenue_shipping)},null,8,b)])):Object(c["createCommentVNode"])("",!0),e.revenue_shipping?(Object(c["openBlock"])(),Object(c["createElementBlock"])("span",f,[Object(c["createTextVNode"])(", "+Object(c["toDisplayString"])(e.translate("General_Discount"))+": ",1),Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(e.revenue_discount)},null,8,m)])):Object(c["createCommentVNode"])("",!0)])]),e.visitorLogEnabled?(Object(c["openBlock"])(),Object(c["createElementBlock"])("a",{key:0,href:"",class:"segmentedlog",onClick:t[0]||(t[0]=Object(c["withModifiers"])(t=>e.showSegmentedVisitorLog(),["prevent"]))},[j,Object(c["createTextVNode"])(Object(c["toDisplayString"])(e.translate("Live_RowActionTooltipWithDimension",e.translate("General_Goal"))),1)])):Object(c["createCommentVNode"])("",!0),O]),_:1},8,["content-title"])}var g=n("19dc"),y=Object(c["defineComponent"])({props:{idGoal:{type:[String,Number],required:!0},visitorLogEnabled:Boolean,revenue:String,revenue_subtotal:String,revenue_tax:String,revenue_shipping:String,revenue_discount:String},components:{ContentBlock:g["ContentBlock"]},methods:{showSegmentedVisitorLog(){window.SegmentedVisitorLog.show("Goals.getMetrics","visitConvertedGoalId=="+this.idGoal,{})}}});y.render=v;var _=y; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/}})})); +//# sourceMappingURL=Ecommerce.umd.min.js.map \ No newline at end of file diff --git a/plugins/Ecommerce/vue/dist/umd.metadata.json b/plugins/Ecommerce/vue/dist/umd.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..9ecfcc0456204cf8e8e80416ebc95ef42d4fbd28 --- /dev/null +++ b/plugins/Ecommerce/vue/dist/umd.metadata.json @@ -0,0 +1,5 @@ +{ + "dependsOn": [ + "CoreHome" + ] +} \ No newline at end of file diff --git a/plugins/Ecommerce/vue/src/ConversionOverview/ConversionOverview.vue b/plugins/Ecommerce/vue/src/ConversionOverview/ConversionOverview.vue new file mode 100644 index 0000000000000000000000000000000000000000..b3bfebfef67f8bc172ed67e1c154821759e93ce3 --- /dev/null +++ b/plugins/Ecommerce/vue/src/ConversionOverview/ConversionOverview.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/plugins/Ecommerce/vue/src/index.ts b/plugins/Ecommerce/vue/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..92e7791a92da619bbc385a6ffa6dfecd27658526 --- /dev/null +++ b/plugins/Ecommerce/vue/src/index.ts @@ -0,0 +1,8 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +*/ + +export { default as ConversionOverview } from './ConversionOverview/ConversionOverview.vue'; diff --git a/plugins/Events/API.php b/plugins/Events/API.php new file mode 100644 index 0000000000000000000000000000000000000000..ad60335e5ad631532cfb25900d145cf46b6edb61 --- /dev/null +++ b/plugins/Events/API.php @@ -0,0 +1,421 @@ +An event is defined by an event category (Videos, Music, Games...), + * an event action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...), + * and an optional event name (a movie name, a song title, etc.) and an optional numeric value. + * + *
    This API exposes the following Custom Events reports: `getCategory` lists the top Event Categories, + * `getAction` lists the top Event Actions, `getName` lists the top Event Names. + * + *
    These Events report define the following metrics: nb_uniq_visitors, nb_visits, nb_events. + * If you define values for your events, you can expect to see the following metrics: nb_events_with_value, + * sum_event_value, min_event_value, max_event_value, avg_event_value + * + *
    The Events.get* reports can be used with an optional `&secondaryDimension` parameter. + * Secondary dimension is the dimension used in the sub-table of the Event report you are requesting. + * + *
    Here are the possible values of `secondaryDimension`:
      + *
    • For `Events.getCategory` you can set `secondaryDimension` to `eventAction` or `eventName`.
    • + *
    • For `Events.getAction` you can set `secondaryDimension` to `eventName` or `eventCategory`.
    • + *
    • For `Events.getName` you can set `secondaryDimension` to `eventAction` or `eventCategory`.
    • + *
    + * + *
    For example, to request all Custom Events Categories, and for each, the top Event actions, + * you would request: `method=Events.getCategory&secondaryDimension=eventAction&flat=1`. + * You may also omit `&flat=1` in which case, to get top Event actions for one Event category, + * use `method=Events.getActionFromCategoryId` passing it the `&idSubtable=` of this Event category. + * + * @method static \Piwik\Plugins\Events\API getInstance() + */ +class API extends \Piwik\Plugin\API +{ + /** + * @var array + */ + protected $defaultMappingApiToSecondaryDimension = array( + 'getCategory' => 'eventAction', + 'getAction' => 'eventName', + 'getName' => 'eventAction', + ); + + /** + * @var array> + */ + protected $mappingApiToRecord = array( + 'getCategory' => + array( + 'eventAction' => Archiver::EVENTS_CATEGORY_ACTION_RECORD_NAME, + 'eventName' => Archiver::EVENTS_CATEGORY_NAME_RECORD_NAME, + ), + 'getAction' => + array( + 'eventName' => Archiver::EVENTS_ACTION_NAME_RECORD_NAME, + 'eventCategory' => Archiver::EVENTS_ACTION_CATEGORY_RECORD_NAME, + ), + 'getName' => + array( + 'eventAction' => Archiver::EVENTS_NAME_ACTION_RECORD_NAME, + 'eventCategory' => Archiver::EVENTS_NAME_CATEGORY_RECORD_NAME, + ), + 'getActionFromCategoryId' => Archiver::EVENTS_CATEGORY_ACTION_RECORD_NAME, + 'getNameFromCategoryId' => Archiver::EVENTS_CATEGORY_NAME_RECORD_NAME, + 'getCategoryFromActionId' => Archiver::EVENTS_ACTION_CATEGORY_RECORD_NAME, + 'getNameFromActionId' => Archiver::EVENTS_ACTION_NAME_RECORD_NAME, + 'getActionFromNameId' => Archiver::EVENTS_NAME_ACTION_RECORD_NAME, + 'getCategoryFromNameId' => Archiver::EVENTS_NAME_CATEGORY_RECORD_NAME, + ); + + /** + * @ignore + * @param string $apiMethod + * @param string|false $secondaryDimension + * @return string|false + */ + public function getActionToLoadSubtables($apiMethod, $secondaryDimension = false) + { + $recordName = $this->getRecordNameForAction($apiMethod, $secondaryDimension); + $apiMethod = array_search($recordName, $this->mappingApiToRecord); + return $apiMethod; + } + + /** + * @ignore + * @param string $apiMethod + * @return string|false + */ + public function getDefaultSecondaryDimension($apiMethod) + { + if (isset($this->defaultMappingApiToSecondaryDimension[$apiMethod])) { + return $this->defaultMappingApiToSecondaryDimension[$apiMethod]; + } + return false; + } + + /** + * @param string $apiMethod + * @param string|false $secondaryDimension + * @return string + */ + protected function getRecordNameForAction($apiMethod, $secondaryDimension = false) + { + if (empty($secondaryDimension)) { + $secondaryDimension = $this->getDefaultSecondaryDimension($apiMethod); + } + $record = $this->mappingApiToRecord[$apiMethod]; + if (!is_array($record)) { + return $record; + } + // when secondaryDimension is incorrectly set + if (empty($record[$secondaryDimension])) { + return key($record); + } + return $record[$secondaryDimension]; + } + + /** + * @ignore + * @param string $apiMethod + * @return string[]|false + */ + public function getSecondaryDimensions($apiMethod) + { + $records = $this->mappingApiToRecord[$apiMethod]; + if (!is_array($records)) { + return false; + } + return array_keys($records); + } + + /** + * @param string|false $secondaryDimension + */ + protected function checkSecondaryDimension(string $apiMethod, $secondaryDimension): void + { + if (empty($secondaryDimension)) { + return; + } + + $isSecondaryDimensionValid = + isset($this->mappingApiToRecord[$apiMethod]) + && isset($this->mappingApiToRecord[$apiMethod][$secondaryDimension]); + + if (!$isSecondaryDimensionValid) { + throw new \Exception( + "Secondary dimension '$secondaryDimension' is not valid for the API $apiMethod. " . + "Use one of: " . implode(", ", $this->getSecondaryDimensions($apiMethod) ?: []) + ); + } + } + + /** + * @param int|string|int[] $idSite + * @param 'day'|'week'|'month'|'year'|'range' $period + * @param string|null|false $segment + * @param int|null|false $idSubtable + * @param string|false $secondaryDimension + * @return DataTable|DataTable\Map + */ + protected function getDataTable(string $name, $idSite, string $period, string $date, $segment, bool $expanded = false, $idSubtable = null, $secondaryDimension = false, bool $flat = false) + { + Piwik::checkUserHasViewAccess($idSite); + $this->checkSecondaryDimension($name, $secondaryDimension); + $recordName = $this->getRecordNameForAction($name, $secondaryDimension); + + $dataTable = Archive::createDataTableFromArchive($recordName, $idSite, $period, $date, $segment ?: '', $expanded, $flat, $idSubtable); + + $dataTable->filter(function ($dataTable) { + $dataTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, [ + Metrics::INDEX_EVENT_MIN_EVENT_VALUE => 'min', + Metrics::INDEX_EVENT_MAX_EVENT_VALUE => 'max', + ]); + }); + + if ($flat) { + $dataTable->filterSubtables('Piwik\Plugins\Events\DataTable\Filter\ReplaceEventNameNotSet'); + } else { + $dataTable->filter('AddSegmentValue', array(function ($label) { + if ($label === Archiver::EVENT_NAME_NOT_SET) { + return false; + } + + return $label; + })); + } + + $dataTable->filter('Piwik\Plugins\Events\DataTable\Filter\ReplaceEventNameNotSet'); + + return $dataTable; + } + + /** + * Returns event metrics grouped by event category. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @param bool $expanded Whether subtables should be expanded in the response. + * @param 'eventAction'|'eventName'|false $secondaryDimension Optional secondary dimension for subtable rows. + * @param bool $flat Whether subtable rows should be flattened into a single table. + * @return DataTable|DataTable\Map Event category metrics. + */ + public function getCategory($idSite, string $period, string $date, $segment = false, bool $expanded = false, $secondaryDimension = false, bool $flat = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, false, $secondaryDimension, $flat); + } + + /** + * Returns event metrics grouped by event action. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @param bool $expanded Whether subtables should be expanded in the response. + * @param 'eventName'|'eventCategory'|false $secondaryDimension Optional secondary dimension for subtable rows. + * @param bool $flat Whether subtable rows should be flattened into a single table. + * @return DataTable|DataTable\Map Event action metrics. + */ + public function getAction($idSite, string $period, string $date, $segment = false, bool $expanded = false, $secondaryDimension = false, bool $flat = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, false, $secondaryDimension, $flat); + } + + /** + * Returns event metrics grouped by event name. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @param bool $expanded Whether subtables should be expanded in the response. + * @param 'eventAction'|'eventCategory'|false $secondaryDimension Optional secondary dimension for subtable rows. + * @param bool $flat Whether subtable rows should be flattened into a single table. + * @return DataTable|DataTable\Map Event name metrics. + */ + public function getName($idSite, string $period, string $date, $segment = false, bool $expanded = false, $secondaryDimension = false, bool $flat = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, false, $secondaryDimension, $flat); + } + + /** + * Returns event actions for one event category row. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param int $idSubtable Subtable ID for the event category row to expand. + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Event action metrics for the selected category. + */ + public function getActionFromCategoryId($idSite, string $period, string $date, $idSubtable, $segment = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, false, $idSubtable); + } + + /** + * Returns event names for one event category row. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param int $idSubtable Subtable ID for the event category row to expand. + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Event name metrics for the selected category. + */ + public function getNameFromCategoryId($idSite, string $period, string $date, $idSubtable, $segment = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, false, $idSubtable); + } + + /** + * Returns event categories for one event action row. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param int $idSubtable Subtable ID for the event action row to expand. + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Event category metrics for the selected action. + */ + public function getCategoryFromActionId($idSite, string $period, string $date, $idSubtable, $segment = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, false, $idSubtable); + } + + /** + * Returns event names for one event action row. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param int $idSubtable Subtable ID for the event action row to expand. + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Event name metrics for the selected action. + */ + public function getNameFromActionId($idSite, string $period, string $date, $idSubtable, $segment = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, false, $idSubtable); + } + + /** + * Returns event actions for one event name row. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param int $idSubtable Subtable ID for the event name row to expand. + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Event action metrics for the selected name. + */ + public function getActionFromNameId($idSite, string $period, string $date, $idSubtable, $segment = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, false, $idSubtable); + } + + /** + * Returns event categories for one event name row. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param int $idSubtable Subtable ID for the event name row to expand. + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Event category metrics for the selected name. + */ + public function getCategoryFromNameId($idSite, string $period, string $date, $idSubtable, $segment = false) + { + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, false, $idSubtable); + } +} diff --git a/plugins/Events/Actions/ActionEvent.php b/plugins/Events/Actions/ActionEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..4964192719f0817d9953413409f3fe9d9d09c34c --- /dev/null +++ b/plugins/Events/Actions/ActionEvent.php @@ -0,0 +1,91 @@ +getParam('url'); + + $this->setActionUrl($url); + $this->eventValue = self::getEventValue($request); + } + + public static function shouldHandle(Request $request) + { + $eventCategory = $request->getParam('e_c'); + $eventAction = $request->getParam('e_a'); + + return (strlen($eventCategory) > 0 && strlen($eventAction) > 0); + } + + public static function getEventValue(Request $request) + { + return trim($request->getParam('e_v')); + } + + public function getEventAction() + { + return $this->request->getParam('e_a'); + } + + public function getEventCategory() + { + return $this->request->getParam('e_c'); + } + + public function getEventName() + { + return $this->request->getParam('e_n'); + } + + public function getCustomFloatValue() + { + return $this->eventValue; + } + + protected function getActionsToLookup() + { + $actionUrl = false; + + $url = $this->getActionUrl(); + + if (!empty($url)) { + // normalize urls by stripping protocol and www + $url = Tracker\PageUrl::normalizeUrl($url); + $actionUrl = array($url['url'], $this->getActionType(), $url['prefixId']); + } + + return array('idaction_url' => $actionUrl); + } + + public function writeDebugInfo() + { + $write = parent::writeDebugInfo(); + if ($write) { + Common::printDebug("Event Value = " . $this->getCustomFloatValue()); + } + return $write; + } +} diff --git a/plugins/Events/Archiver.php b/plugins/Events/Archiver.php new file mode 100644 index 0000000000000000000000000000000000000000..d5b90fa6b86ab09d3741b956085a792c4ca3ebf9 --- /dev/null +++ b/plugins/Events/Archiver.php @@ -0,0 +1,65 @@ + Actions X + - Top Category > Names X + - Top Actions > Categories X + - Top Actions > Names X + - Top Names > Actions X + - Top Names > Categories X + + UI + - Overview at the top (graph + Sparklines) + - Below show the left menu, defaults to Top Event Category + + Not MVP: + - On hover on any row: Show % of total events + - Add min value metric, max value metric in tooltip + - List event scope Custom Variables Names > Custom variables values > Event Names > Event Actions + - List event scope Custom Variables Value > Event Category > Event Names > Event Actions + + NOTES: + - For a given Name, Category is often constant + + */ +class Archiver extends \Piwik\Plugin\Archiver +{ + public const EVENTS_CATEGORY_ACTION_RECORD_NAME = 'Events_category_action'; + public const EVENTS_CATEGORY_NAME_RECORD_NAME = 'Events_category_name'; + public const EVENTS_ACTION_CATEGORY_RECORD_NAME = 'Events_action_category'; + public const EVENTS_ACTION_NAME_RECORD_NAME = 'Events_action_name'; + public const EVENTS_NAME_ACTION_RECORD_NAME = 'Events_name_action'; + public const EVENTS_NAME_CATEGORY_RECORD_NAME = 'Events_name_category'; + public const EVENT_NAME_NOT_SET = 'Piwik_EventNameNotSet'; +} diff --git a/plugins/Events/Categories/EventsCategory.php b/plugins/Events/Categories/EventsCategory.php new file mode 100644 index 0000000000000000000000000000000000000000..0fb0ff9e2da42063403f9b57274f26108b4091d3 --- /dev/null +++ b/plugins/Events/Categories/EventsCategory.php @@ -0,0 +1,19 @@ +' . Piwik::translate('Events_EventsSubcategoryHelp1') . '

    ' + . '

    ' . Url::getExternalLinkTag('https://matomo.org/docs/event-tracking/', null, null, 'App.Events.getCategory') + . Piwik::translate('Events_EventsSubcategoryHelp2') . '

    '; + } +} diff --git a/plugins/Events/Columns/EventAction.php b/plugins/Events/Columns/EventAction.php new file mode 100644 index 0000000000000000000000000000000000000000..fd4f8ae4ee9aed15969e98c3e04b342275f909da --- /dev/null +++ b/plugins/Events/Columns/EventAction.php @@ -0,0 +1,63 @@ +getActionId()); + } + + public function getActionId() + { + return Action::TYPE_EVENT_ACTION; + } + + public function onLookupAction(Request $request, Action $action) + { + if (!($action instanceof ActionEvent)) { + return false; + } + + $eventAction = $action->getEventAction(); + $eventAction = trim($eventAction); + + if (strlen($eventAction) > 0) { + return $eventAction; + } + + throw new InvalidRequestParameterException('Param `e_a` must not be empty or filled with whitespaces'); + } +} diff --git a/plugins/Events/Columns/EventCategory.php b/plugins/Events/Columns/EventCategory.php new file mode 100644 index 0000000000000000000000000000000000000000..14910148b32449183d9a3026dec63aa1f474f256 --- /dev/null +++ b/plugins/Events/Columns/EventCategory.php @@ -0,0 +1,63 @@ +getActionId()); + } + + public function getActionId() + { + return Action::TYPE_EVENT_CATEGORY; + } + + public function onLookupAction(Request $request, Action $action) + { + if (!($action instanceof ActionEvent)) { + return false; + } + + $eventCategory = $action->getEventCategory(); + $eventCategory = trim($eventCategory); + + if (strlen($eventCategory) > 0) { + return $eventCategory; + } + + throw new InvalidRequestParameterException('Param `e_c` must not be empty or filled with whitespaces'); + } +} diff --git a/plugins/Events/Columns/EventName.php b/plugins/Events/Columns/EventName.php new file mode 100644 index 0000000000000000000000000000000000000000..6bcc9dffbe08eb5d8712c4c4c665552cb08d5c94 --- /dev/null +++ b/plugins/Events/Columns/EventName.php @@ -0,0 +1,62 @@ +getActionId()); + } + + public function getActionId() + { + return Action::TYPE_EVENT_NAME; + } + + public function onLookupAction(Request $request, Action $action) + { + if (!($action instanceof ActionEvent)) { + return false; + } + + $eventName = $action->getEventName(); + $eventName = trim($eventName); + + if (strlen($eventName) > 0) { + return $eventName; + } + + return false; + } +} diff --git a/plugins/Events/Columns/EventUrl.php b/plugins/Events/Columns/EventUrl.php new file mode 100644 index 0000000000000000000000000000000000000000..8cb22c0e2f8a20a68a011dd5abda6e2044699684 --- /dev/null +++ b/plugins/Events/Columns/EventUrl.php @@ -0,0 +1,43 @@ +createMetric(ArchivedMetric::AGGREGATION_SUM); + $metricsList->addMetric($metric1); + + $metric2 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX); + $metric2->setDocumentation(Piwik::translate('Events_MaxValueDocumentation')); + $metricsList->addMetric($metric2); + + $metric4 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MIN); + $metric4->setDocumentation(Piwik::translate('Events_MinValueDocumentation')); + $metricsList->addMetric($metric4); + + $metric3 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_COUNT_WITH_NUMERIC_VALUE); + $metric3->setName('events_with_event_value'); + $metric3->setTranslatedName(Piwik::translate('Events_EventsWithValue')); + $metric3->setDocumentation(Piwik::translate('Events_EventsWithValueDocumentation')); + $metricsList->addMetric($metric3); + + $metric = $dimensionMetricFactory->createComputedMetric($metric1->getName(), $metric3->getName(), ComputedMetric::AGGREGATION_AVG); + $metric->setName('avg_event_value'); + $metric->setTranslatedName(Piwik::translate('Events_AvgValue')); + $metricsList->addMetric($metric); + } +} diff --git a/plugins/Events/Columns/Metrics/AverageEventValue.php b/plugins/Events/Columns/Metrics/AverageEventValue.php new file mode 100644 index 0000000000000000000000000000000000000000..3bc1cd5612370204d73c5fc30f467e1467e63c6a --- /dev/null +++ b/plugins/Events/Columns/Metrics/AverageEventValue.php @@ -0,0 +1,53 @@ +getMetric($row, 'sum_event_value'); + $eventsWithValue = $this->getMetric($row, 'nb_events_with_value'); + + return Piwik::getQuotientSafe($sumEventValue, $eventsWithValue, $precision = 2); + } + + public function getDependentMetrics() + { + return array('sum_event_value', 'nb_events_with_value'); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Events/Columns/TotalEvents.php b/plugins/Events/Columns/TotalEvents.php new file mode 100644 index 0000000000000000000000000000000000000000..79361dbd625066d43b8d91ac2f61acaf144c0920 --- /dev/null +++ b/plugins/Events/Columns/TotalEvents.php @@ -0,0 +1,65 @@ +0'); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + if ($this->isEventAction($action)) { + return 1; + } + + return 0; + } + + /** + * @param Action|null $action + * @return string|false + */ + public function onExistingVisit(Request $request, Visitor $visitor, $action) + { + if ($this->isEventAction($action)) { + return 'visit_total_events + 1'; + } + + return false; + } + + /** + * @param Action|null $action + * @return bool + */ + private function isEventAction($action) + { + return ($action && $action->getActionType() == Action::TYPE_EVENT); + } +} diff --git a/plugins/Events/DataTable/Filter/ReplaceEventNameNotSet.php b/plugins/Events/DataTable/Filter/ReplaceEventNameNotSet.php new file mode 100644 index 0000000000000000000000000000000000000000..4e4f7df1865cdd53c38a747b33eec9dd63a60e26 --- /dev/null +++ b/plugins/Events/DataTable/Filter/ReplaceEventNameNotSet.php @@ -0,0 +1,37 @@ +getRowFromLabel(Archiver::EVENT_NAME_NOT_SET); + if ($row) { + $row->setColumn('label', Piwik::translate('General_NotDefined', Piwik::translate('Events_EventName'))); + } + } +} diff --git a/plugins/Events/Events.php b/plugins/Events/Events.php new file mode 100644 index 0000000000000000000000000000000000000000..83555887bab1a9b6c1cecfab1698bdf1f4d836c5 --- /dev/null +++ b/plugins/Events/Events.php @@ -0,0 +1,272 @@ + 'addMetricDocumentationTranslations', + 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations', + 'Metrics.getDefaultMetricSemanticTypes' => 'addMetricSemanticTypes', + 'ViewDataTable.configure' => 'configureViewDataTable', + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'Actions.getCustomActionDimensionFieldsAndJoins' => 'provideActionDimensionFields', + ); + } + + public function addMetricTranslations(&$translations) + { + $translations = array_merge($translations, $this->getMetricTranslations()); + } + + public function addMetricDocumentationTranslations(&$translations) + { + $translations = array_merge($translations, $this->getMetricDocumentation()); + } + + public function addMetricSemanticTypes(array &$types): void + { + $metricTypes = array( + 'nb_events' => Dimension::TYPE_NUMBER, + 'sum_event_value' => Dimension::TYPE_NUMBER, + 'min_event_value' => Dimension::TYPE_NUMBER, + 'max_event_value' => Dimension::TYPE_NUMBER, + 'nb_events_with_value' => Dimension::TYPE_NUMBER, + ); + $types = array_merge($types, $metricTypes); + } + + public function getMetricDocumentation() + { + $documentation = array( + 'nb_events' => 'Events_TotalEventsDocumentation', + 'sum_event_value' => 'Events_TotalValueDocumentation', + 'min_event_value' => 'Events_MinValueDocumentation', + 'max_event_value' => 'Events_MaxValueDocumentation', + 'avg_event_value' => 'Events_AvgValueDocumentation', + 'nb_events_with_value' => 'Events_EventsWithValueDocumentation', + ); + $documentation = array_map(array('\\Piwik\\Piwik', 'translate'), $documentation); + return $documentation; + } + + public function getMetricTranslations() + { + $metrics = array( + 'nb_events' => 'Events_Events', + 'sum_event_value' => 'Events_EventValue', + 'min_event_value' => 'Events_MinValue', + 'max_event_value' => 'Events_MaxValue', + 'avg_event_value' => 'Events_AvgValue', + 'nb_events_with_value' => 'Events_EventsWithValue', + ); + $metrics = array_map(array('\\Piwik\\Piwik', 'translate'), $metrics); + return $metrics; + } + + public $metadataDimensions = array( + 'eventCategory' => array('Events_EventCategory', 'log_link_visit_action.idaction_event_category'), + 'eventAction' => array('Events_EventAction', 'log_link_visit_action.idaction_event_action'), + 'eventName' => array('Events_EventName', 'log_link_visit_action.idaction_name'), + ); + + public function getDimensionLabel($dimension) + { + return Piwik::translate($this->metadataDimensions[$dimension][0]); + } + /** + * @return array + */ + public static function getLabelTranslations() + { + return array( + 'getCategory' => array('Events_EventCategories', 'Events_EventCategory'), + 'getAction' => array('Events_EventActions', 'Events_EventAction'), + 'getName' => array('Events_EventNames', 'Events_EventName'), + ); + } + + /** + * Given getCategory, returns "Event Categories" + * + * @param $apiMethod + * @return string + */ + public function getReportTitleTranslation($apiMethod) + { + return $this->getTranslation($apiMethod, $index = 0); + } + + /** + * Given getCategory, returns "Event Category" + * + * @param $apiMethod + * @return string + */ + public function getColumnTranslation($apiMethod) + { + return $this->getTranslation($apiMethod, $index = 1); + } + + protected function getTranslation($apiMethod, $index) + { + $labels = $this->getLabelTranslations(); + foreach ($labels as $action => $translations) { + // Events.getActionFromCategoryId returns translation for Events.getAction + if (strpos($apiMethod, $action) === 0) { + $columnLabel = $translations[$index]; + return Piwik::translate($columnLabel); + } + } + throw new \Exception("Translation not found for report $apiMethod"); + } + + public function configureViewDataTable(ViewDataTable $view) + { + if ($view->requestConfig->getApiModuleToRequest() != 'Events') { + return; + } + + // eg. 'Events.getCategory' + $apiMethod = $view->requestConfig->getApiMethodToRequest(); + + $secondaryDimension = $this->getSecondaryDimensionFromRequest(); + $view->config->subtable_controller_action = API::getInstance()->getActionToLoadSubtables($apiMethod, $secondaryDimension); + + $pivotBy = Common::getRequestVar('pivotBy', false); + if (empty($pivotBy)) { + $view->config->columns_to_display = array('label', 'nb_events', 'sum_event_value'); + } + + $view->config->show_flatten_table = true; + $view->requestConfig->filter_sort_column = 'nb_events'; + + if ($view->isViewDataTableId(AllColumns::ID)) { + $view->config->filters[] = function (DataTable $table) use ($view) { + $columsToDisplay = array('label'); + + $columns = $table->getColumns(); + if (in_array('nb_visits', $columns)) { + $columsToDisplay[] = 'nb_visits'; + } + + if (in_array('nb_uniq_visitors', $columns)) { + $columsToDisplay[] = 'nb_uniq_visitors'; + } + + $view->config->columns_to_display = array_merge($columsToDisplay, array('nb_events', 'sum_event_value', 'avg_event_value', 'min_event_value', 'max_event_value')); + + if (!in_array($view->requestConfig->filter_sort_column, $view->config->columns_to_display)) { + $view->requestConfig->filter_sort_column = 'nb_events'; + } + }; + $view->config->show_pivot_by_subtable = false; + } + + $labelTranslation = $this->getColumnTranslation($apiMethod); + $view->config->addTranslation('label', $labelTranslation); + $view->config->addTranslations($this->getMetricTranslations()); + $this->addRelatedReports($view, $secondaryDimension); + $this->addTooltipEventValue($view); + + $subtableReport = ReportsProvider::factory('Events', $view->config->subtable_controller_action); + $view->config->pivot_by_dimension = $subtableReport->getDimension()->getId(); + $view->config->pivot_by_column = 'nb_events'; + } + + private function addRelatedReports($view, $secondaryDimension) + { + if (empty($secondaryDimension)) { + // eg. Row Evolution + return; + } + + $apiMethod = $view->requestConfig->getApiMethodToRequest(); + $secondaryDimensions = API::getInstance()->getSecondaryDimensions($apiMethod); + + if (empty($secondaryDimensions)) { + return; + } + + $dimensions = []; + foreach ($secondaryDimensions as $dimension) { + $dimensions[$dimension] = $this->getDimensionLabel($dimension); + } + + $view->config->setSecondaryDimensions($dimensions, $secondaryDimension); + } + + private function addTooltipEventValue(ViewDataTable $view) + { + // Creates the tooltip message for Event Value column + $tooltipCallback = function ($hits, $min, $max, $avg) { + if (!$hits) { + return false; + } + + $avg = $avg ?: 0; + + $msgEventMinMax = Piwik::translate("Events_EventValueTooltip", array($hits, "
    ", $min, $max)); + $msgEventAvg = Piwik::translate("Events_AvgEventValue", $avg); + return $msgEventMinMax . "
    " . $msgEventAvg; + }; + + // Add tooltip metadata column to the DataTable + $view->config->filters[] = array('ColumnCallbackAddMetadata', + array( + array( + 'nb_events', + 'min_event_value', + 'max_event_value', + 'avg_event_value', + ), + 'sum_event_value_tooltip', + $tooltipCallback, + ), + ); + } + + /** + * @return mixed + */ + public function getSecondaryDimensionFromRequest() + { + return Common::getRequestVar('secondaryDimension', false, 'string'); + } + + public function getStylesheetFiles(&$stylesheets) + { + $stylesheets[] = "plugins/Events/stylesheets/datatable.less"; + } + + public function provideActionDimensionFields(&$fields, &$joins) + { + $fields[] = 'log_action_event_category.type AS eventType'; + $fields[] = 'log_action_event_category.name AS eventCategory'; + $fields[] = 'log_action_event_action.name as eventAction'; + $joins[] = 'LEFT JOIN ' . Common::prefixTable('log_action') . ' AS log_action_event_action + ON log_link_visit_action.idaction_event_action = log_action_event_action.idaction'; + $joins[] = 'LEFT JOIN ' . Common::prefixTable('log_action') . ' AS log_action_event_category + ON log_link_visit_action.idaction_event_category = log_action_event_category.idaction'; + } +} diff --git a/plugins/Events/RecordBuilders/EventReports.php b/plugins/Events/RecordBuilders/EventReports.php new file mode 100644 index 0000000000000000000000000000000000000000..be7039ac65a644bd9b638ca2962f95f296586357 --- /dev/null +++ b/plugins/Events/RecordBuilders/EventReports.php @@ -0,0 +1,217 @@ +columnToSortByBeforeTruncation = Metrics::INDEX_NB_VISITS; + $this->columnAggregationOps = [ + Metrics::INDEX_EVENT_MIN_EVENT_VALUE => 'min', + Metrics::INDEX_EVENT_MAX_EVENT_VALUE => 'max', + ]; + } + + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + $idSite = $archiveProcessor->getParams()->getSite()->getId(); + $maximumRowsInDataTable = GeneralConfig::getConfigValue('datatable_archiving_maximum_rows_events', $idSite); + $maximumRowsInSubDataTable = GeneralConfig::getConfigValue('datatable_archiving_maximum_rows_subtable_events', $idSite); + + /** @var Record[] $records */ + $records = [ + Record::make(Record::TYPE_BLOB, Archiver::EVENTS_CATEGORY_ACTION_RECORD_NAME), + Record::make(Record::TYPE_BLOB, Archiver::EVENTS_CATEGORY_NAME_RECORD_NAME), + Record::make(Record::TYPE_BLOB, Archiver::EVENTS_ACTION_CATEGORY_RECORD_NAME), + Record::make(Record::TYPE_BLOB, Archiver::EVENTS_ACTION_NAME_RECORD_NAME), + Record::make(Record::TYPE_BLOB, Archiver::EVENTS_NAME_ACTION_RECORD_NAME), + Record::make(Record::TYPE_BLOB, Archiver::EVENTS_NAME_CATEGORY_RECORD_NAME), + ]; + + foreach ($records as $record) { + $record->setMaxRowsInTable($maximumRowsInDataTable) + ->setMaxRowsInSubtable($maximumRowsInSubDataTable) + ->setBlobColumnAggregationOps($this->columnAggregationOps); + } + + return $records; + } + + protected function getRecordToDimensions() + { + return [ + Archiver::EVENTS_CATEGORY_ACTION_RECORD_NAME => ["eventCategory", "eventAction"], + Archiver::EVENTS_CATEGORY_NAME_RECORD_NAME => ["eventCategory", "eventName"], + Archiver::EVENTS_ACTION_NAME_RECORD_NAME => ["eventAction", "eventName"], + Archiver::EVENTS_ACTION_CATEGORY_RECORD_NAME => ["eventAction", "eventCategory"], + Archiver::EVENTS_NAME_ACTION_RECORD_NAME => ["eventName", "eventAction"], + Archiver::EVENTS_NAME_CATEGORY_RECORD_NAME => ["eventName", "eventCategory"], + ]; + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + $logAggregator = $archiveProcessor->getLogAggregator(); + + $reports = []; + + $select = " + log_action_event_category.name as eventCategory, + log_action_event_action.name as eventAction, + log_action_event_name.name as eventName, + + count(distinct log_link_visit_action.idvisit) as `" . Metrics::INDEX_NB_VISITS . "`, + count(distinct log_link_visit_action.idvisitor) as `" . Metrics::INDEX_NB_UNIQ_VISITORS . "`, + count(*) as `" . Metrics::INDEX_EVENT_NB_HITS . "`, + + sum( + case when " . Action::DB_COLUMN_CUSTOM_FLOAT . " is null + then 0 + else " . Action::DB_COLUMN_CUSTOM_FLOAT . " + end + ) as `" . Metrics::INDEX_EVENT_SUM_EVENT_VALUE . "`, + sum( case when " . Action::DB_COLUMN_CUSTOM_FLOAT . " is null then 0 else 1 end ) + as `" . Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE . "`, + min(" . Action::DB_COLUMN_CUSTOM_FLOAT . ") as `" . Metrics::INDEX_EVENT_MIN_EVENT_VALUE . "`, + max(" . Action::DB_COLUMN_CUSTOM_FLOAT . ") as `" . Metrics::INDEX_EVENT_MAX_EVENT_VALUE . "` + "; + + $from = [ + "log_link_visit_action", + [ + "table" => "log_action", + "tableAlias" => "log_action_event_category", + "joinOn" => "log_link_visit_action.idaction_event_category = log_action_event_category.idaction", + ], + [ + "table" => "log_action", + "tableAlias" => "log_action_event_action", + "joinOn" => "log_link_visit_action.idaction_event_action = log_action_event_action.idaction", + ], + [ + "table" => "log_action", + "tableAlias" => "log_action_event_name", + "joinOn" => "log_link_visit_action.idaction_name = log_action_event_name.idaction", + ], + ]; + + $where = $logAggregator->getWhereStatement('log_link_visit_action', 'server_time'); + $where .= " AND log_link_visit_action.idaction_event_category IS NOT NULL"; + + $groupBy = "log_link_visit_action.idaction_event_category, + log_link_visit_action.idaction_event_action, + log_link_visit_action.idaction_name"; + + $orderBy = "`" . Metrics::INDEX_NB_VISITS . "` DESC, `eventName`"; + + $rankingQueryLimit = ArchivingHelper::getRankingQueryLimit(); + $rankingQuery = null; + if ($rankingQueryLimit > 0) { + $rankingQuery = new RankingQuery($rankingQueryLimit); + $rankingQuery->addLabelColumn(['eventCategory', 'eventAction', 'eventName']); + $rankingQuery->addColumn([Metrics::INDEX_NB_UNIQ_VISITORS]); + $rankingQuery->addColumn([Metrics::INDEX_EVENT_NB_HITS, Metrics::INDEX_NB_VISITS, Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE], 'sum'); + $rankingQuery->addColumn(Metrics::INDEX_EVENT_SUM_EVENT_VALUE, 'sum'); + $rankingQuery->addColumn(Metrics::INDEX_EVENT_MIN_EVENT_VALUE, 'min'); + $rankingQuery->addColumn(Metrics::INDEX_EVENT_MAX_EVENT_VALUE, 'max'); + } + + $this->archiveDayQueryProcess($reports, $logAggregator, $select, $from, $where, $groupBy, $orderBy, $rankingQuery); + + return $reports; + } + + protected function archiveDayQueryProcess( + array &$reports, + LogAggregator $logAggregator, + string $select, + array $from, + string $where, + string $groupBy, + string $orderBy, + ?RankingQuery $rankingQuery = null + ): void { + // get query with segmentation + $query = $logAggregator->generateQuery($select, $from, $where, $groupBy, $orderBy); + + // apply ranking query + if ($rankingQuery) { + $query['sql'] = $rankingQuery->generateRankingQuery($query['sql']); + } + + // get result + $resultSet = $logAggregator->getDb()->query($query['sql'], $query['bind']); + + if ($resultSet === false) { + return; + } + + while ($row = $resultSet->fetch()) { + $this->aggregateEventRow($reports, $row); + } + } + + protected function aggregateEventRow(array &$reports, array $row): void + { + foreach ($this->getRecordToDimensions() as $record => $dimensions) { + if (empty($reports[$record])) { + $reports[$record] = new DataTable(); + } + + /** @var DataTable $table */ + $table = $reports[$record]; + + $mainDimension = $dimensions[0]; + $mainLabel = $row[$mainDimension]; + + // Event name is optional + if ( + $mainDimension == 'eventName' + && empty($mainLabel) + ) { + $mainLabel = Archiver::EVENT_NAME_NOT_SET; + } + + $columns = [ + Metrics::INDEX_NB_UNIQ_VISITORS => $row[Metrics::INDEX_NB_UNIQ_VISITORS] ?? 0, + Metrics::INDEX_NB_VISITS => $row[Metrics::INDEX_NB_VISITS] ?? 0, + Metrics::INDEX_EVENT_NB_HITS => $row[Metrics::INDEX_EVENT_NB_HITS] ?? 0, + Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE => $row[Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE] ?? 0, + Metrics::INDEX_EVENT_SUM_EVENT_VALUE => round($row[Metrics::INDEX_EVENT_SUM_EVENT_VALUE] ?? 0, 2), + Metrics::INDEX_EVENT_MIN_EVENT_VALUE => is_numeric($row[Metrics::INDEX_EVENT_MIN_EVENT_VALUE]) ? round($row[Metrics::INDEX_EVENT_MIN_EVENT_VALUE], 2) : null, + Metrics::INDEX_EVENT_MAX_EVENT_VALUE => is_numeric($row[Metrics::INDEX_EVENT_MAX_EVENT_VALUE]) ? round($row[Metrics::INDEX_EVENT_MAX_EVENT_VALUE], 2) : null, + ]; + + $topLevelRow = $table->sumRowWithLabel($mainLabel, $columns, $this->columnAggregationOps); + + $subDimension = $dimensions[1]; + $subLabel = $row[$subDimension]; + if (empty($subLabel)) { + continue; + } + + $topLevelRow->sumRowWithLabelToSubtable($subLabel, $columns, $this->columnAggregationOps); + } + } +} diff --git a/plugins/Events/Reports/Base.php b/plugins/Events/Reports/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..05bb5cc11a3e60924e6fbdfad6dbf779f770c60e --- /dev/null +++ b/plugins/Events/Reports/Base.php @@ -0,0 +1,89 @@ +categoryId = 'General_Actions'; + $this->subcategoryId = 'Events_Events'; + $this->onlineGuideUrl = Url::addCampaignParametersToMatomoLink('https://matomo.org/docs/event-tracking/'); + + $this->processedMetrics = array( + new AverageEventValue(), + ); + } + + public function getMetricsDocumentation() + { + $documentation = parent::getMetricsDocumentation(); + + $documentation['nb_visits'] = Piwik::translate('Events_ColumnNbVisitsDocumentation'); + $documentation['nb_uniq_visitors'] = Piwik::translate('Events_ColumnNbUniqVisitorsDocumentation'); + + return $documentation; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + if (!$this->isSubtableReport) { + $widget = $factory->createWidget()->setParameters(array( + 'secondaryDimension' => API::getInstance()->getDefaultSecondaryDimension($this->action), + )); + + $widgetsList->addToContainerWidget('Events', $widget); + } + } + + public function configureView(ViewDataTable $view) + { + if (Common::getRequestVar('secondaryDimension', '', 'string')) { + foreach (['pivotBy', 'pivotByColumn'] as $property) { + $index = array_search($property, $view->requestConfig->overridableProperties); + if ($index) { + unset($view->requestConfig->overridableProperties[$index]); + } + } + $view->requestConfig->overridableProperties = array_values($view->requestConfig->overridableProperties); + } + + if (property_exists($view->config, 'selectable_columns')) { + $view->config->selectable_columns = ['nb_events', 'nb_visits', 'sum_event_value', 'nb_events_with_value']; + } + + $this->configureFooterMessage($view); + } + + protected function configureFooterMessage(ViewDataTable $view) + { + if ($this->isSubtableReport) { + // no footer message for subtables + return; + } + + $view->config->filters[] = function (DataTable $dataTable) use ($view) { + $out = ''; + EventDispatcher::getInstance()->postEvent('Template.afterEventsReport', [&$out, $dataTable]); + $view->config->show_footer_message = $out; + }; + } +} diff --git a/plugins/Events/Reports/GetAction.php b/plugins/Events/Reports/GetAction.php new file mode 100644 index 0000000000000000000000000000000000000000..090a437cca6718f7f78e65f30ad2006c7b4f699b --- /dev/null +++ b/plugins/Events/Reports/GetAction.php @@ -0,0 +1,32 @@ +dimension = new EventAction(); + $this->name = Piwik::translate('Events_EventActions'); + $this->documentation = Piwik::translate('Events_EventActionsReportDocumentation'); + $this->metrics = ['nb_events', 'nb_visits', 'nb_uniq_visitors', 'sum_event_value', 'min_event_value', 'max_event_value', 'nb_events_with_value']; + if (Common::getRequestVar('secondaryDimension', false) == 'eventCategory') { + $this->actionToLoadSubTables = 'getCategoryFromNameId'; + } else { + $this->actionToLoadSubTables = 'getNameFromActionId'; + } + $this->order = 1; + } +} diff --git a/plugins/Events/Reports/GetActionFromCategoryId.php b/plugins/Events/Reports/GetActionFromCategoryId.php new file mode 100644 index 0000000000000000000000000000000000000000..b1e3275fa66985d7021b1d32984a6e715bb278f5 --- /dev/null +++ b/plugins/Events/Reports/GetActionFromCategoryId.php @@ -0,0 +1,28 @@ +dimension = new EventAction(); + $this->name = Piwik::translate('Events_EventActions'); + $this->isSubtableReport = true; + } +} diff --git a/plugins/Events/Reports/GetActionFromNameId.php b/plugins/Events/Reports/GetActionFromNameId.php new file mode 100644 index 0000000000000000000000000000000000000000..7c6d47f741b5330796009860c9b882caf4f39fec --- /dev/null +++ b/plugins/Events/Reports/GetActionFromNameId.php @@ -0,0 +1,28 @@ +dimension = new EventAction(); + $this->name = Piwik::translate('Events_EventActions'); + $this->isSubtableReport = true; + } +} diff --git a/plugins/Events/Reports/GetCategory.php b/plugins/Events/Reports/GetCategory.php new file mode 100644 index 0000000000000000000000000000000000000000..e02a9c88d6ba30b56a46ce8671cc1069dc0207da --- /dev/null +++ b/plugins/Events/Reports/GetCategory.php @@ -0,0 +1,32 @@ +dimension = new EventCategory(); + $this->name = Piwik::translate('Events_EventCategories'); + $this->documentation = Piwik::translate('Events_EventCategoriesReportDocumentation'); + $this->metrics = ['nb_events', 'nb_visits', 'nb_uniq_visitors', 'sum_event_value', 'min_event_value', 'max_event_value', 'nb_events_with_value']; + if (Common::getRequestVar('secondaryDimension', false) == 'eventName') { + $this->actionToLoadSubTables = 'getNameFromCategoryId'; + } else { + $this->actionToLoadSubTables = 'getActionFromCategoryId'; + } + $this->order = 0; + } +} diff --git a/plugins/Events/Reports/GetCategoryFromActionId.php b/plugins/Events/Reports/GetCategoryFromActionId.php new file mode 100644 index 0000000000000000000000000000000000000000..7f38bf6138f178ee435ccd86603bc3ded34bb8de --- /dev/null +++ b/plugins/Events/Reports/GetCategoryFromActionId.php @@ -0,0 +1,28 @@ +dimension = new EventCategory(); + $this->name = Piwik::translate('Events_EventCategories'); + $this->isSubtableReport = true; + } +} diff --git a/plugins/Events/Reports/GetCategoryFromNameId.php b/plugins/Events/Reports/GetCategoryFromNameId.php new file mode 100644 index 0000000000000000000000000000000000000000..8d68f1d57d023f352addd0a092e57d542e43ba33 --- /dev/null +++ b/plugins/Events/Reports/GetCategoryFromNameId.php @@ -0,0 +1,28 @@ +dimension = new EventCategory(); + $this->name = Piwik::translate('Events_EventCategories'); + $this->isSubtableReport = true; + } +} diff --git a/plugins/Events/Reports/GetName.php b/plugins/Events/Reports/GetName.php new file mode 100644 index 0000000000000000000000000000000000000000..433129c380cd68078ad5a8b051517d4402937d12 --- /dev/null +++ b/plugins/Events/Reports/GetName.php @@ -0,0 +1,32 @@ +dimension = new EventName(); + $this->name = Piwik::translate('Events_EventNames'); + $this->documentation = Piwik::translate('Events_EventNamesReportDocumentation'); + $this->metrics = ['nb_events', 'nb_visits', 'nb_uniq_visitors', 'sum_event_value', 'min_event_value', 'max_event_value', 'nb_events_with_value']; + if (Common::getRequestVar('secondaryDimension', false) == 'eventCategory') { + $this->actionToLoadSubTables = 'getCategoryFromNameId'; + } else { + $this->actionToLoadSubTables = 'getActionFromNameId'; + } + $this->order = 2; + } +} diff --git a/plugins/Events/Reports/GetNameFromActionId.php b/plugins/Events/Reports/GetNameFromActionId.php new file mode 100644 index 0000000000000000000000000000000000000000..1670b0b703bce5b9594876554f8665859fa9e73b --- /dev/null +++ b/plugins/Events/Reports/GetNameFromActionId.php @@ -0,0 +1,28 @@ +dimension = new EventName(); + $this->name = Piwik::translate('Events_Names'); + $this->isSubtableReport = true; + } +} diff --git a/plugins/Events/Reports/GetNameFromCategoryId.php b/plugins/Events/Reports/GetNameFromCategoryId.php new file mode 100644 index 0000000000000000000000000000000000000000..aa2e9746cb3c98a91c03edb552e882af329e2504 --- /dev/null +++ b/plugins/Events/Reports/GetNameFromCategoryId.php @@ -0,0 +1,29 @@ +categoryId = 'Events_Events'; + $this->processedMetrics = []; + + $this->dimension = new EventName(); + $this->name = Piwik::translate('Events_EventNames'); + $this->isSubtableReport = true; + } +} diff --git a/plugins/Events/VisitorDetails.php b/plugins/Events/VisitorDetails.php new file mode 100644 index 0000000000000000000000000000000000000000..f15515f2f4221d61618a13158e8ca1281a4e943a --- /dev/null +++ b/plugins/Events/VisitorDetails.php @@ -0,0 +1,88 @@ + 0) { + $action['eventName'] = $action['pageTitle']; + } + + if (isset($action['custom_float']) && strlen($action['custom_float']) > 0) { + $action['eventValue'] = round($action['custom_float'], self::EVENT_VALUE_PRECISION); + } + + unset($action['pageTitle']); + unset($action['custom_float']); + } else { + unset($action['eventCategory']); + unset($action['eventAction']); + } + unset($action['eventType']); + } + + public function extendVisitorDetails(&$visitor) + { + $visitor['events'] = $this->details['visit_total_events']; + } + + public function renderAction($action, $previousAction, $visitorDetails) + { + if (empty($action['type']) || $action['type'] != 'event') { + return ''; + } + + $view = new View('@Events/_actionEvent.twig'); + $view->sendHeadersWhenRendering = false; + $view->action = $action; + $view->previousAction = $previousAction; + $view->visitInfo = $visitorDetails; + return $view->render(); + } + + + public function initProfile($visits, &$profile) + { + $profile['totalEvents'] = 0; + } + + public function handleProfileAction($action, &$profile) + { + if (empty($action['type']) || $action['type'] != 'event') { + return; + } + $profile['totalEvents']++; + } +} diff --git a/plugins/Events/Widgets/EventsByDimension.php b/plugins/Events/Widgets/EventsByDimension.php new file mode 100644 index 0000000000000000000000000000000000000000..a0b4383102bcc11cd0734d349ce8acace29cb633 --- /dev/null +++ b/plugins/Events/Widgets/EventsByDimension.php @@ -0,0 +1,21 @@ + +
    + {% if action.pageTitle|default(false) is not empty %} + {{ action.pageTitle|rawSafeDecoded }} + {% endif %} + + {{ action.eventCategory|rawSafeDecoded }} + - {{ action.eventAction|rawSafeDecoded }} {% if action.eventName is defined %}- {{ action.eventName|rawSafeDecoded }}{% endif %} {% if action.eventValue is defined %}[{{ action.eventValue }}]{% endif %} + {% if action.url is not empty %} + {% if previousAction.url|default(false) == action.url %} + {# For events, do not show (url) if the Event URL is the same as the URL last displayed #} + {% else %} + {% if action.url|trim|lower starts with 'javascript:' or + action.url|trim|lower starts with 'vbscript:' or + action.url|trim|lower starts with 'data:' %} + {{ action.url }} + {% else %} + + {{ action.url|replace({'http://': '', 'https://': ''}) }} + + {% endif %} + {% endif %} + {% endif %} +
    +
  • diff --git a/plugins/Events/tests/UI/Events_spec.js b/plugins/Events/tests/UI/Events_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ee247bfc0daf9188b859dc463d064ffbf93b6ba4 --- /dev/null +++ b/plugins/Events/tests/UI/Events_spec.js @@ -0,0 +1,31 @@ +describe("Events", function () { + var generalParams = 'idSite=1&period=year&date=2012-08-09', + urlBaseGeneric = 'module=CoreHome&action=index&', + urlBase = urlBaseGeneric + generalParams; + + it('should load the Events > index page correctly', async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=Events_Events"); + await page.mouse.move(-10, -10); + + expect(await page.screenshotSelector('.pageWrap,.dataTable')).to.matchImage('overview'); + }); + + it("should show report flattened", async function() { + await page.click('.dropdownConfigureIcon'); + await page.mouse.move(-10, -10); + await page.waitForTimeout(250); // rendering + await page.click('.dataTableFlatten'); + await page.mouse.move(-10, -10); + await page.waitForNetworkIdle(); + + expect(await page.screenshotSelector('.theWidgetContent')).to.matchImage('flattened'); + }); + + it("should show another secondary dimension", async function() { + await page.evaluate(() => $('.datatableRelatedReports li span:contains("Event Name")').click()); + await page.waitForTimeout(50); + await page.waitForNetworkIdle(); + + expect(await page.screenshotSelector('.theWidgetContent')).to.matchImage('secondary_switched'); + }); +}); diff --git a/plugins/Events/tests/UI/expected-screenshots/Events_flattened.png b/plugins/Events/tests/UI/expected-screenshots/Events_flattened.png new file mode 100644 index 0000000000000000000000000000000000000000..cb4cb6118a6cd6388d4b950f7cc72e188bb7d83c Binary files /dev/null and b/plugins/Events/tests/UI/expected-screenshots/Events_flattened.png differ diff --git a/plugins/Events/tests/UI/expected-screenshots/Events_overview.png b/plugins/Events/tests/UI/expected-screenshots/Events_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..24ffbebdf17302f0feb80dfbff43514539adb86a Binary files /dev/null and b/plugins/Events/tests/UI/expected-screenshots/Events_overview.png differ diff --git a/plugins/Events/tests/UI/expected-screenshots/Events_secondary_switched.png b/plugins/Events/tests/UI/expected-screenshots/Events_secondary_switched.png new file mode 100644 index 0000000000000000000000000000000000000000..db453fa61dfff0ee453f107a43037419a82b219d Binary files /dev/null and b/plugins/Events/tests/UI/expected-screenshots/Events_secondary_switched.png differ diff --git a/plugins/ExampleAPI/API.php b/plugins/ExampleAPI/API.php new file mode 100644 index 0000000000000000000000000000000000000000..efed3e1b8aa4d4d53316d442fd1dea105bc9befa --- /dev/null +++ b/plugins/ExampleAPI/API.php @@ -0,0 +1,145 @@ +source code in in the file plugins/ExampleAPI/API.php for more documentation. + * @method static \Piwik\Plugins\ExampleAPI\API getInstance() + */ +class API extends \Piwik\Plugin\API +{ + /** + * Defines if the parameters passed to the public API methods in this class will be automatically sanitized or not. + * When setting this value to false, please ensure to handle all input parameters with care. + * Especially parameters provided in an unspecific type, as string or array might contain harmful content. + * If those values are persisted and displayed in the UI somewhere, ensure to sanitize/escape them for output. + * + * @var bool + */ + protected $autoSanitizeInputParams = true; + + /** + * Get Matomo version + */ + public function getMatomoVersion(): string + { + Piwik::checkUserHasSomeViewAccess(); + return Version::VERSION; + } + + /** + * Get Answer to Life + */ + public function getAnswerToLife(): int + { + return 42; + } + + /** + * Returns a custom object. + * API format conversion will fail for this custom object. + * If used internally, the data structure can be returned untouched by using + * the API parameter 'format=original' + * + * @return MagicObject Will return a standard Matomo error when called from the Web APIs + */ + public function getObject(): MagicObject + { + return new MagicObject(); + } + + /** + * Sums two floats and returns the result. + * The parameters are set automatically from the GET request + * when the API function is called. You can also use default values + * as shown in this example. + */ + public function getSum(float $a = 0, float $b = 0): float + { + return $a + $b; + } + + /** + * Returns null value + * + * @return null + */ + public function getNull() + { + return null; + } + + /** + * Get array of descriptive text + * When called from the Web API, you see that simple arrays like this one + * are automatically converted in the various formats (xml, csv, etc.) + * + * @return array + */ + public function getDescriptionArray(): array + { + return array('piwik', 'free/libre', 'web analytics', 'free', 'Strong message: Свободный Тибет'); + } + + /** + * Returns a custom data table. + * This data table will be converted to all available formats + * when requested in the API request. + */ + public function getCompetitionDatatable(): DataTable + { + $dataTable = new DataTable(); + + $row1 = new Row(); + $row1->setColumns(array('name' => 'piwik', 'license' => 'GPL')); + + // Rows Metadata is useful to store non stats data for example (logos, urls, etc.) + // When printed out, they are simply merged with columns + $row1->setMetadata('logo', 'logo.png'); + $dataTable->addRow($row1); + + $dataTable->addRowFromSimpleArray(array('name' => 'google analytics', 'license' => 'commercial')); + + return $dataTable; + } + + /** + * Get more information on the Answer to Life... + */ + public function getMoreInformationAnswerToLife(): string + { + return "Check http://en.wikipedia.org/wiki/The_Answer_to_Life,_the_Universe,_and_Everything"; + } + + /** + * Returns a Multidimensional Array + * Only supported in JSON + * + * @return array + */ + public function getMultiArray(): array + { + $return = array( + 'Limitation' => array( + "Multi dimensional arrays is only supported by format=JSON", + "Known limitation", + ), + 'Second Dimension' => array(true, false, 1, 0, 152, 'test', array(42 => 'end')), + ); + return $return; + } +} diff --git a/plugins/ExampleAPI/ExampleAPI.php b/plugins/ExampleAPI/ExampleAPI.php new file mode 100644 index 0000000000000000000000000000000000000000..d17cf7d1db468871c6b9d86bf53950af9c1bf8cb --- /dev/null +++ b/plugins/ExampleAPI/ExampleAPI.php @@ -0,0 +1,18 @@ +setName('examplecommand:helloworld'); + $this->setDescription('ExampleCommandDescription'); + $this->addRequiredValueOption('name', null, 'Your name:'); + } + + /** + * Interact with the user. + * + * This method is executed before the InputDefinition is validated. + * This means that this is the only place where the command can + * interactively ask for values of missing required arguments. + */ + protected function doInteract(): void + { + } + + /** + * Initializes the command after the input has been bound and before the input + * is validated. + * + * This is mainly useful when a lot of commands extends one main command + * where some things need to be initialized based on the input arguments and options. + */ + protected function doInitialize(): void + { + } + + /** + * The actual task is defined in this method. Here you can access any option or argument that was defined on the + * command line via $this->getInput() and write anything to the console via $this->getOutput(). + * In case anything went wrong during the execution you should throw an exception to make sure the user will get a + * useful error message and to make sure the command does not exit with the status code 0. + * + * Ideally, the actual command is quite short as it acts like a controller. It should only receive the input values, + * execute the task by calling a method of another class and output any useful information. + * + * Execute the command like: ./console examplecommand:helloworld --name="The Matomo Team" + */ + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + + $name = $input->getOption('name'); + + $message = sprintf('HelloWorld: %s', $name); + + $output->writeln($message); + + return self::SUCCESS; + } +} diff --git a/plugins/ExampleCommand/ExampleCommand.php b/plugins/ExampleCommand/ExampleCommand.php new file mode 100644 index 0000000000000000000000000000000000000000..e8f1a97fe91b8c3e44e46caee0a90ccffc0293cf --- /dev/null +++ b/plugins/ExampleCommand/ExampleCommand.php @@ -0,0 +1,14 @@ +tablePrefixed = Common::prefixTable($this->table); + } + + public function install() + { + DbHelper::createTable($this->table, " + `group` VARCHAR(30) NOT NULL, + `is_admin` TINYINT(1) NOT NULL, + PRIMARY KEY (`group`)"); + } + + public function uninstall() + { + Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed)); + } + + private function getDb() + { + return Db::get(); + } + + public function getAllRecords() + { + return $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed); + } + + public function addGroupInformation($group, $isAdmin) + { + $columns = array( + 'group' => $group, + 'is_admin' => $isAdmin, + ); + + $bind = array_values($columns); + $placeholder = Common::getSqlStringFieldsArray($columns); + + $sql = sprintf( + 'INSERT INTO `%s` (`%s`) VALUES(%s)', + $this->tablePrefixed, + implode('`,`', array_keys($columns)), + $placeholder + ); + + $db = $this->getDb(); + + $db->query($sql, $bind); + } +} diff --git a/plugins/ExampleLogTables/Dao/CustomUserLog.php b/plugins/ExampleLogTables/Dao/CustomUserLog.php new file mode 100644 index 0000000000000000000000000000000000000000..8b95688ae411b012302145bc557f2d1a8a4417dc --- /dev/null +++ b/plugins/ExampleLogTables/Dao/CustomUserLog.php @@ -0,0 +1,72 @@ +tablePrefixed = Common::prefixTable($this->table); + } + + public function install() + { + DbHelper::createTable($this->table, " + `user_id` VARCHAR(191) NOT NULL, + `gender` VARCHAR(30) NOT NULL, + `group` VARCHAR(30) NOT NULL, + PRIMARY KEY (user_id)"); + } + + public function uninstall() + { + Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed)); + } + + private function getDb() + { + return Db::get(); + } + + public function getAllRecords() + { + return $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed); + } + + public function addUserInformation($userId, $group, $gender) + { + $columns = array( + 'user_id' => $userId, + 'group' => $group, + 'gender' => $gender, + ); + + $bind = array_values($columns); + $placeholder = Common::getSqlStringFieldsArray($columns); + + $sql = sprintf( + 'INSERT INTO `%s` (`%s`) VALUES(%s)', + $this->tablePrefixed, + implode('`,`', array_keys($columns)), + $placeholder + ); + + $db = $this->getDb(); + + $db->query($sql, $bind); + } +} diff --git a/plugins/ExampleLogTables/ExampleLogTables.php b/plugins/ExampleLogTables/ExampleLogTables.php new file mode 100644 index 0000000000000000000000000000000000000000..a86ed1164bc54a68fd423a7f23e0166f4a0e1c32 --- /dev/null +++ b/plugins/ExampleLogTables/ExampleLogTables.php @@ -0,0 +1,44 @@ + 'getTablesInstalled', + ]; + } + + public function install() + { + // Install custom log table [disabled as example only] + + // $userLog = new CustomUserLog(); + // $userLog->install(); + + // $userLog = new CustomGroupLog(); + // $userLog->install(); + } + + /** + * Register the new tables, so Matomo knows about them. + * + * @param array $allTablesInstalled + */ + public function getTablesInstalled(&$allTablesInstalled) + { + $allTablesInstalled[] = Common::prefixTable('log_group'); + $allTablesInstalled[] = Common::prefixTable('log_custom'); + } +} diff --git a/plugins/ExampleLogTables/Tracker/LogTable/CustomGroupLog.php b/plugins/ExampleLogTables/Tracker/LogTable/CustomGroupLog.php new file mode 100644 index 0000000000000000000000000000000000000000..e5ad3ea7132d901bb2cca5f7794817f19bf09a1a --- /dev/null +++ b/plugins/ExampleLogTables/Tracker/LogTable/CustomGroupLog.php @@ -0,0 +1,35 @@ + 'group']; + } +} diff --git a/plugins/ExampleLogTables/Tracker/LogTable/CustomUserLog.php b/plugins/ExampleLogTables/Tracker/LogTable/CustomUserLog.php new file mode 100644 index 0000000000000000000000000000000000000000..4fd50ee62227b6dde1d3ba65325c24ed9f715b48 --- /dev/null +++ b/plugins/ExampleLogTables/Tracker/LogTable/CustomUserLog.php @@ -0,0 +1,35 @@ + 'user_id']; + } +} diff --git a/plugins/ExampleLogTables/config/config.php b/plugins/ExampleLogTables/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/ExampleLogTables/config/config.php @@ -0,0 +1,3 @@ +=5.0.0-b1,<6.0.0-b1" + }, + "authors": [ + { + "name": "Matomo", + "email": "", + "homepage": "" + } + ], + "support": { + "email": "", + "issues": "", + "forum": "", + "irc": "", + "wiki": "", + "source": "", + "docs": "", + "rss": "" + }, + "homepage": "", + "license": "GPL v3+", + "keywords": [] +} diff --git a/plugins/ExampleLogTables/tests/Fixtures/VisitsWithUserIdAndCustomData.php b/plugins/ExampleLogTables/tests/Fixtures/VisitsWithUserIdAndCustomData.php new file mode 100644 index 0000000000000000000000000000000000000000..1c14f73c77d593410d40140f06041c5679e311a9 --- /dev/null +++ b/plugins/ExampleLogTables/tests/Fixtures/VisitsWithUserIdAndCustomData.php @@ -0,0 +1,113 @@ +dateTime); + } + + // set up database tables + $userLog = new CustomUserLog(); + $userLog->install(); + $groupLog = new CustomGroupLog(); + $groupLog->install(); + + $this->trackVisits(); + $this->insertCustomUserLogData(); + $this->insertCustomGroupLogData(); + } + + private function trackVisits() + { + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + $t->setTokenAuth(self::getTokenAuth()); + $t->enableBulkTracking(); + + foreach (array('user1', 'user2', 'user3', 'user4', false) as $key => $userId) { + for ($numVisits = 0; $numVisits < ($key + 1) * 10; $numVisits++) { + $visitDateTime = Date::factory($this->dateTime)->addHour($numVisits)->getDatetime(); + $t->setForceVisitDateTime($visitDateTime); + $t->setUserId($userId); + $t->setVisitorId(str_pad($numVisits . $key, 16, 'a')); + $t->setCountry(self::$countryCodes[$numVisits % count(self::$countryCodes)]); + + if ($numVisits % 5 == 0) { + $t->doTrackSiteSearch('some search term' . $numVisits); + } + + if ($numVisits % 4 == 0) { + $t->doTrackEvent('Event action ' . $numVisits, 'event cat ' . $numVisits); + } + + if ($numVisits % 7 == 0) { + $t->doTrackContentInteraction('click', 'slider ' . $numVisits % 4); + } + + if ($numVisits % 7 == 4) { + $t->doTrackAction('http://out.link', 'outlink'); + } + + if ($numVisits % 5 == 3) { + $t->setEcommerceView('SKU VERY nice indeed ' . ($numVisits % 3), 'PRODUCT name ' . ($numVisits % 4), 'category ' . ($numVisits % 5), $numVisits * 2.79); + } + + $t->setForceNewVisit(); + $t->setUrl('http://example.org/my/dir/page' . ($numVisits % 4)); + + $visitDateTime = Date::factory($this->dateTime)->addHour($numVisits + 6)->getDatetime(); + $t->setForceVisitDateTime($visitDateTime); + + if ($numVisits % 7 == 0) { + $t->doTrackAction('http://example.org/download.pdf', 'download'); + } + + self::assertTrue($t->doTrackPageView('incredible title ' . ($numVisits % 3))); + + if ($numVisits % 9 == 0) { + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour($numVisits + 6.1)->getDatetime()); + $t->addEcommerceItem('SKU VERY nice indeed ' . ($numVisits % 3), 'PRODUCT name ' . ($numVisits % 4), 'category ' . ($numVisits % 5), $numVisits * 2.79); + self::assertTrue($t->doTrackEcommerceCartUpdate($numVisits * 17)); + } + } + } + + self::checkBulkTrackingResponse($t->doBulkTrack()); + } + + private function insertCustomUserLogData() + { + $customLog = new CustomUserLog(); + $customLog->addUserInformation('user1', 'admin', 'men'); + $customLog->addUserInformation('user2', 'user', 'women'); + $customLog->addUserInformation('user3', 'admin', 'women'); + $customLog->addUserInformation('user4', '', 'men'); + } + + private function insertCustomGroupLogData() + { + $customGroup = new CustomGroupLog(); + $customGroup->addGroupInformation('admin', 1); + $customGroup->addGroupInformation('user', 0); + } +} diff --git a/plugins/ExampleLogTables/tests/System/CustomLogTablesTest.php b/plugins/ExampleLogTables/tests/System/CustomLogTablesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d678c8ccf5257e29d1ddeaa542dd1eddc4256c6d --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/CustomLogTablesTest.php @@ -0,0 +1,141 @@ +runApiTests($api, $params); + } + + /** + * @dataProvider getSegmentsToTest + */ + public function testNoApiReturnsError($segment) + { + self::expectNotToPerformAssertions(); + + $dateTime = self::$fixture->dateTime; + $idSite1 = self::$fixture->idSite; + + $params = [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'format' => 'JSON', + 'segment' => $segment, + 'testSuffix' => '', + ]; + + $testConfig = new ApiTestConfig($params); + $testRequests = $this->getTestRequestsCollection('all', $testConfig, 'all'); + + foreach ($testRequests->getRequestUrls() as $apiId => $requestUrl) { + $response = Response::loadFromApi($params, $requestUrl, false); + $decoded = json_decode($response->getResponseText(), true); + + if (is_array($decoded) && isset($decoded['result']) && $decoded['result'] == 'error') { + $this->fail('API returned an error when requesting ' . http_build_query($requestUrl) . "\nMessage: " . $decoded['message']); + } + } + } + + public function getSegmentsToTest() + { + return [ + ['attrgender==men'], + ['isadmin==1'], + ]; + } + + public function getApiForTesting() + { + $dateTime = self::$fixture->dateTime; + $idSite1 = self::$fixture->idSite; + + $result = [ + [[ + 'Actions.get', + 'UserId.getUsers', + 'VisitsSummary.get', + ], [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'segment' => 'attrgender==men', + 'testSuffix' => '_men'], + ], + [[ + 'Actions.get', + 'UserId.getUsers', + 'VisitsSummary.get', + ], [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'segment' => 'attrgender==women', + 'testSuffix' => '_women'], + ], + [[ + 'Actions.get', + 'UserId.getUsers', + 'VisitsSummary.get', + ], [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'segment' => 'isadmin==1', + 'testSuffix' => '_admin'], + ], + [[ + 'Actions.get', + 'UserId.getUsers', + 'VisitsSummary.get', + ], [ + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => 'month', + 'setDateLastN' => false, + 'testSuffix' => '_all'], + ], + ]; + + return $result; + } + + public static function getOutputPrefix() + { + return 'ExampleLogTables'; + } + + public static function getPathToTestDirectory() + { + return dirname(__FILE__); + } +} + +CustomLogTablesTest::$fixture = new VisitsWithUserIdAndCustomData(); diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__Actions.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__Actions.get_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..e7f403a1d79cbf7b2405c6665a503fa0c9858f81 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__Actions.get_month.xml @@ -0,0 +1,12 @@ + + + 45 + 45 + 7 + 7 + 0 + 0 + 8 + 6 + 78 + \ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__UserId.getUsers_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__UserId.getUsers_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..9c0f56c790d408cc8a049eb1f858a2903a92108a --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__UserId.getUsers_month.xml @@ -0,0 +1,29 @@ + + + + + 33 + 53 + 4 + 741 + 16 + 0 + 2 + 2 + + userId==user3 + + + + 13 + 18 + 2 + 727 + 8 + 0 + 2 + 2 + + userId==user1 + + \ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__VisitsSummary.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__VisitsSummary.get_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..c7cc556f4261170e004e2a166d4fe069f835b7f2 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_admin__VisitsSummary.get_month.xml @@ -0,0 +1,14 @@ + + + 2 + 2 + 46 + 71 + 0 + 24 + 1468 + 4 + 52% + 1.5 + 32 + \ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__Actions.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__Actions.get_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..dcac56923a0ee92d28b6f5a4498e302aff63a010 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__Actions.get_month.xml @@ -0,0 +1,12 @@ + + + 171 + 171 + 24 + 24 + 0 + 0 + 30 + 10 + 288 + \ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__UserId.getUsers_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__UserId.getUsers_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..f148858d2d0f0fcf1d87755beaa343f9f7f15dac --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__UserId.getUsers_month.xml @@ -0,0 +1,55 @@ + + + + + 43 + 70 + 4 + 1108 + 20 + 0 + 3 + 3 + + userId==user4 + + + + 33 + 53 + 4 + 741 + 16 + 0 + 2 + 2 + + userId==user3 + + + + 23 + 35 + 2 + 735 + 11 + 0 + 2 + 2 + + userId==user2 + + + + 13 + 18 + 2 + 727 + 8 + 0 + 2 + 2 + + userId==user1 + + \ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__VisitsSummary.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__VisitsSummary.get_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..da350cbd36fa6cca2bb6502af62bd4884b803757 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_all__VisitsSummary.get_month.xml @@ -0,0 +1,14 @@ + + + 54 + 4 + 191 + 264 + 0 + 120 + 5492 + 4 + 63% + 1.4 + 29 + \ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__Actions.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__Actions.get_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..b4945d81a96c34418ef2fa5f46003afa1ac81744 --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__Actions.get_month.xml @@ -0,0 +1,12 @@ + + + 57 + 57 + 8 + 8 + 0 + 0 + 10 + 8 + 96 + \ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__UserId.getUsers_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__UserId.getUsers_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..e9b843b1b8c8c302b561698620d43c4753b27a8c --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__UserId.getUsers_month.xml @@ -0,0 +1,29 @@ + + + + + 43 + 70 + 4 + 1108 + 20 + 0 + 3 + 3 + + userId==user4 + + + + 13 + 18 + 2 + 727 + 8 + 0 + 2 + 2 + + userId==user1 + + \ No newline at end of file diff --git a/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__VisitsSummary.get_month.xml b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__VisitsSummary.get_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..4f68a6e78152bdb6840a171c8f98cb2f17a1d05a --- /dev/null +++ b/plugins/ExampleLogTables/tests/System/expected/test_ExampleLogTables_men__VisitsSummary.get_month.xml @@ -0,0 +1,14 @@ + + + 2 + 2 + 56 + 88 + 0 + 28 + 1835 + 4 + 50% + 1.6 + 33 + \ No newline at end of file