diff --git a/.travis.yml b/.travis.yml index c54d7a2b8..c1ef5b549 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,17 @@ dist: trusty language: php +install: + - cd .. + - ls + - mv wp-matomo matomo + - cd matomo + notifications: email: on_success: never on_failure: change -branches: - only: - - master - cache: directories: - $HOME/.composer/cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a695df25..23bc38a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.2.0 +- Update to Matomo 3.12.0 +- Improve session start success check + 0.1.9 - Better session error logging diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index fb5e4d3b2..b3ebf56cb 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -4,6 +4,13 @@ This is the Developer Changelog for Matomo platform developers. All changes in o The Product Changelog at **[matomo.org/changelog](https://matomo.org/changelog)** lets you see more details about any Matomo release, such as the list of new guides and FAQs, security fixes, and links to all closed issues. +## Matomo 3.12.0 + +### New API +* Added new event `Visualization.beforeRender`, triggered after immediately before rendering a visualization. +* Added new event `Http.sendHttpRequest` and `Http.sendHttpRequest.end` so plugins can listen to external HTTP requests, monitor them, or resolve the request themselves. +* Added new event `CliMulti.supportsAsync` so plugins can force or disable the usage of archiving through the CLI + ## Matomo 3.10.0 ### Breaking Changes diff --git a/app/LEGALNOTICE b/app/LEGALNOTICE index 295ff6bca..15ddc77bd 100644 --- a/app/LEGALNOTICE +++ b/app/LEGALNOTICE @@ -14,7 +14,7 @@ SOFTWARE LICENSE The free software license of Matomo is GNU General Public License v3 or later. A copy of GNU GPL v3 should have been included in this - software package in misc/gpl-3.0.txt. + software package in LICENSE. TRADEMARK @@ -36,7 +36,7 @@ TRADEMARK CREDITS The software consists of contributions made by many individuals. - Major contributors are listed in https://matomo.org/the-piwik-team/. + Major contributors are listed in https://matomo.org/team/. For detailed contribution history, refer to the source, tickets, patches, and Git revision history, available at @@ -217,6 +217,10 @@ THIRD-PARTY COMPONENTS AND LIBRARIES Link: http://raphaeljs.com/ License: MIT (Expat) + Name: iFrame Resizer + Link: https://github.com/davidjbradshaw/iframe-resizer + License: MIT + Name: lessphp Link: http://leafo.net/lessphp License: GPL3, MIT (Expat) @@ -275,7 +279,7 @@ THIRD-PARTY CONTENT License: GPL By: Alessandro Rei - http://www.kde-look.org/usermanager/search.php?username=mentalrey - Name: Material icons ("icon-info2", "icon-outline", "icon-settings", "icon-form", "icon-play", "icon-pause", "icon-replay", "icon-skip-next", "icon-skip-forward", "icon-stop", "icon-fast-forward", "icon-fast-rewind", "icon-bug", "icon-upload", "icon-segmented-visits-log") in plugins/Morpheus/fonts + Name: Material icons ("icon-info2", "icon-outline", "icon-settings", "icon-form", "icon-play", "icon-pause", "icon-replay", "icon-skip-next", "icon-skip-forward", "icon-stop", "icon-fast-forward", "icon-fast-rewind", "icon-bug", "icon-upload", "icon-segmented-visits-log") in plugins/Morpheus/fonts, and plugins/Morpheus/images/compare.svg Link: https://design.google.com/icons/ License: Apache License Version 2.0 diff --git a/app/README.md b/app/README.md index 600710685..a73e3f95c 100644 --- a/app/README.md +++ b/app/README.md @@ -1,8 +1,8 @@ # Matomo (formerly Piwik) - matomo.org -[![Latest Stable Version](https://poser.pugx.org/matomo-org/matomo/v/stable)](https://matomo.org/download/) -[![Latest Unstable Version](https://poser.pugx.org/matomo-org/matomo/v/unstable)](https://packagist.org/packages/piwik/piwik) -[![License](https://poser.pugx.org/matomo-org/matomo/license)](https://matomo.org/free-software/) +[![Latest Stable Version](https://poser.pugx.org/piwik/piwik/v/stable)](https://matomo.org/download/) +[![Latest Unstable Version](https://poser.pugx.org/piwik/piwik/v/unstable)](https://packagist.org/packages/piwik/piwik) +[![License](https://poser.pugx.org/piwik/piwik/license)](https://matomo.org/free-software/) ## Code Status @@ -28,7 +28,7 @@ Or in short: ## License -Matomo is released under the GPL v3 (or later) license, see [misc/gpl-3.0.txt](misc/gpl-3.0.txt) +Matomo is released under the GPL v3 (or later) license, see [LICENSE](LICENSE) ## Requirements @@ -107,7 +107,7 @@ What makes Matomo unique from the competition: * Modern, easy to use User Interface: you can fully customize your dashboard, drag and drop widgets and more. * Matomo features are built inside plugins: you can add new features and remove the ones you don’t need. - You can build your own web analytics plugins or hire a consultant to have your custom feature built-in Matomo + You can build your own web analytics plugins or hire a consultant to have your custom feature built-in Matomo. * A vibrant international Open community of more than 200,000 active users (tracking even more websites!) @@ -117,7 +117,3 @@ What makes Matomo unique from the competition: Documentation and more info on https://matomo.org We are together creating the best open analytics platform in the world! - -## Known issues - -* Untested is DB connection when configuring MYSQL SOCKETS OR PIPES as DB host, eg `define( 'DB_HOST', '127.0.0.1:/var/run/mysqld/mysqld.sock' );` \ No newline at end of file diff --git a/matomo_bootstrap.php b/app/bootstrap.php similarity index 97% rename from matomo_bootstrap.php rename to app/bootstrap.php index 5e88482b6..900c90fda 100644 --- a/matomo_bootstrap.php +++ b/app/bootstrap.php @@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { // prevent from loading twice - require_once( dirname( __FILE__ ) . '/../../../wp-load.php' ); + require_once( dirname( __FILE__ ) . '/../../../../wp-load.php' ); } if ( ! defined( 'ABSPATH' ) ) { diff --git a/app/config/global.ini.php b/app/config/global.ini.php old mode 100644 new mode 100755 index 01671b5c1..439b1ab03 --- a/app/config/global.ini.php +++ b/app/config/global.ini.php @@ -48,7 +48,6 @@ ; If configured, the following queries will be executed on the reader instead of the writer. ; * archiving queries that hit a log table ; * live queries that hit a log table -; * fetching of archives when viewing a report ; You only want to enable a reader if you can ensure there is minimal replication lag / delay on the reader. ; Otherwise you might get corrupt data in the reports. [database_reader] @@ -209,6 +208,12 @@ enabled_periods_UI = "day,week,month,year,range" enabled_periods_API = "day,week,month,year,range" +; whether to enable segment archiving cache +; Note: if you use any plugins, this need to be compliant with Matomo and +; * depending on the segment you create you may need a newer MySQL version (eg 5.7 or newer) +; * use a reader database for archiving in case you have configured a database reader +enable_segments_cache = 1 + ; whether to enable subquery cache for Custom Segment archiving queries enable_segments_subquery_cache = 0 ; Any segment subquery that matches more than segments_subquery_cache_limit IDs will not be cached, @@ -357,6 +362,11 @@ ; or make sure the date ranges users' want to see will be processed somehow. archiving_range_force_on_browser_request = 1 +; By default Matomo will automatically archive all date ranges any user has chosen in his account settings. +; This is limited to the available options last7, previous7, last30 and previous30. +; If you need any other period, or want to ensure one of those is always archived, you can define them here +archiving_custom_ranges[] = + ; By default Matomo runs OPTIMIZE TABLE SQL queries to free spaces after deleting some data. ; If your Matomo tracks millions of pages, the OPTIMIZE TABLE queries might run for hours (seen in "SHOW FULL PROCESSLIST \g") ; so you can disable these special queries here: @@ -538,6 +548,12 @@ ; this limit can be adjusted by changing this value live_visitor_profile_max_visits_to_aggregate = 100 +; If configured, will abort a MySQL query after the configured amount of seconds and show an error in the UI to for +; example lower the date range or tweak the segment (if one is applied). Set it to -1 if the query time should not be +; limited. Note: This feature requires a recent MySQL version (5.7 or newer). Some MySQL forks like MariaDB might not +; support this feature which uses the MAX_EXECUTION_TIME hint. +live_query_max_execution_time = -1 + ; In "All Websites" dashboard, when looking at today's reports (or a date range including today), ; the page will automatically refresh every 5 minutes. Set to 0 to disable automatic refresh multisites_refresh_after_seconds = 300 @@ -637,6 +653,9 @@ ; With this option, you can disable the framed mode of the Overlay plugin. Use it if your website contains a framebuster. overlay_disable_framed_mode = 0 +; Controls whether the user is able to upload a custom logo for their Matomo install +enable_custom_logo = 1 + ; By default we check whether the Custom logo is writable or not, before we display the Custom logo file uploader enable_custom_logo_check = 1 @@ -658,6 +677,12 @@ ; - links to Uninstall themes will be disabled (but user can still enable/disable themes) enable_plugins_admin = 1 +; By setting this option to 0 the users management will be disabled +enable_users_admin = 1 + +; By setting this option to 0 the websites management will be disabled +enable_sites_admin = 1 + ; By setting this option to 1, it will be possible for Super Users to upload Matomo plugin ZIP archives directly in Matomo Administration. ; Enabling this opens a remote code execution vulnerability where ; an attacker who gained Super User access could execute custom PHP code in a Matomo plugin. @@ -711,6 +736,12 @@ ; The number of days to wait before sending the JavaScript tracking code email reminder. num_days_before_tracking_code_reminder = 5 +; The maximum number of segments that can be compared simultaneously. +data_comparison_segment_limit = 5 + +; The maximum number of periods that can be compared simultaneously. +data_comparison_period_limit = 5 + ; The path to a custom cacert.pem file Matomo should use. ; By default Matomo uses a file extracted from the Firefox browser and provided here: https://curl.haxx.se/docs/caextract.html. ; The file contains root CAs and is used to determine if the chain of a SSL certificate is valid and it is safe to connect. @@ -722,7 +753,6 @@ ; Default is 1. enable_tracking_failures_notification = 1 - [Tracker] ; Matomo uses "Privacy by default" model. When one of your users visit multiple of your websites tracked in this Matomo, @@ -951,6 +981,7 @@ Plugins[] = RssWidget Plugins[] = Feedback Plugins[] = Monolog + Plugins[] = Login Plugins[] = TwoFactorAuth Plugins[] = UsersManager diff --git a/app/config/global.php b/app/config/global.php index fdcbd5577..17d7b9e01 100644 --- a/app/config/global.php +++ b/app/config/global.php @@ -9,6 +9,7 @@ return array( 'path.root' => PIWIK_DOCUMENT_ROOT, + 'path.misc.user' => 'misc/user/', 'path.tmp' => function (ContainerInterface $c) { @@ -117,6 +118,8 @@ 'misc/package/WebAppGallery/*.xml', 'misc/package/WebAppGallery/install.sql', 'plugins/ImageGraph/fonts/unifont.ttf', + 'plugins/*/config/tracker.php', + 'plugins/*/config/config.php', 'vendor/autoload.php', 'vendor/composer/autoload_real.php', 'vendor/szymach/c-pchart/app/*', diff --git a/app/console b/app/console old mode 100644 new mode 100755 index 122c46e6e..44768da38 --- a/app/console +++ b/app/console @@ -4,13 +4,9 @@ if (!defined('PIWIK_DOCUMENT_ROOT')) { define('PIWIK_DOCUMENT_ROOT', dirname(__FILE__) == '/' ? '' : dirname(__FILE__)); } -if (file_exists(PIWIK_DOCUMENT_ROOT . '/../matomo_bootstrap.php')) { - require_once PIWIK_DOCUMENT_ROOT . '/../matomo_bootstrap.php'; -} - if (file_exists(PIWIK_DOCUMENT_ROOT . '/bootstrap.php')) { require_once PIWIK_DOCUMENT_ROOT . '/bootstrap.php'; -} +} if (!defined('PIWIK_INCLUDE_PATH')) { define('PIWIK_INCLUDE_PATH', PIWIK_DOCUMENT_ROOT); diff --git a/app/core/API/ApiRenderer.php b/app/core/API/ApiRenderer.php index 6072bcd19..e14acf7a7 100644 --- a/app/core/API/ApiRenderer.php +++ b/app/core/API/ApiRenderer.php @@ -14,6 +14,7 @@ use Piwik\DataTable; use Piwik\Piwik; use Piwik\Plugin; +use Piwik\SettingsServer; /** * API renderer @@ -22,6 +23,8 @@ abstract class ApiRenderer { protected $request; + protected $hideIdSubDataTable; + final public function __construct($request) { $this->request = $request; @@ -30,6 +33,12 @@ final public function __construct($request) protected function init() { + $this->hideIdSubDataTable = Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request); + } + + protected function shouldSendBacktrace() + { + return Common::isPhpCliMode() && SettingsServer::isArchivePhpTriggered(); } abstract public function sendHeader(); @@ -101,7 +110,7 @@ protected function buildDataTableRenderer($dataTable) $renderer->setTable($dataTable); $renderer->setIdSite($idSite); $renderer->setRenderSubTables(Common::getRequestVar('expanded', false, 'int', $this->request)); - $renderer->setHideIdSubDatableFromResponse(Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request)); + $renderer->setHideIdSubDatableFromResponse($this->hideIdSubDataTable); return $renderer; } diff --git a/app/core/API/DataTableManipulator.php b/app/core/API/DataTableManipulator.php index ee24ec78a..4faf3ad77 100644 --- a/app/core/API/DataTableManipulator.php +++ b/app/core/API/DataTableManipulator.php @@ -192,6 +192,7 @@ protected function callApiAndReturnDataTable($apiModule, $method, $request) $request['expanded'] = 0; $request['format'] = 'original'; $request['format_metrics'] = 0; + $request['compare'] = 0; // don't want to run recursive filters on the subtables as they are loaded, // otherwise the result will be empty in places (or everywhere). instead we diff --git a/app/core/API/DataTableManipulator/Flattener.php b/app/core/API/DataTableManipulator/Flattener.php index 5e8b68a56..2db6017ec 100644 --- a/app/core/API/DataTableManipulator/Flattener.php +++ b/app/core/API/DataTableManipulator/Flattener.php @@ -102,8 +102,7 @@ protected function flattenDataTableInto($dataTable, $newDataTable, $level, $dime * @param string $dimensionName * @param bool $parentLogo */ - private function flattenRow - (Row $row, $rowId, DataTable $dataTable, $level, $dimensionName, + private function flattenRow(Row $row, $rowId, DataTable $dataTable, $level, $dimensionName, $labelPrefix = '', $parentLogo = false) { $dimensions = $dataTable->getMetadata('dimensions'); @@ -202,6 +201,12 @@ private function flattenRow if ($origLabel !== false) { foreach ($subTable->getRows() as $subRow) { foreach ($row->getMetadata() as $name => $value) { + // do not set 'segment' parameter if there is a segmentValue on the row, since that will prevent the segmentValue + // from being used in DataTablePostProcessor + if ($name == 'segment' && $subRow->getMetadata('segmentValue') !== false) { + continue; + } + if ($subRow->getMetadata($name) === false) { $subRow->setMetadata($name, $value); } diff --git a/app/core/API/DataTableManipulator/LabelFilter.php b/app/core/API/DataTableManipulator/LabelFilter.php index 6a7d23b52..369d006f5 100644 --- a/app/core/API/DataTableManipulator/LabelFilter.php +++ b/app/core/API/DataTableManipulator/LabelFilter.php @@ -28,6 +28,8 @@ class LabelFilter extends DataTableManipulator private $labels; private $addLabelIndex; + private $isComparing; + private $labelSeries; const FLAG_IS_ROW_EVOLUTION = 'label_index'; /** @@ -50,9 +52,18 @@ public function filter($labels, $dataTable, $addLabelIndex = false) $labels = array($labels); } - $this->labels = $labels; + $this->labels = array_values($labels); $this->addLabelIndex = (bool)$addLabelIndex; - return $this->manipulate($dataTable); + $this->isComparing = $this->isComparing(); + + $labelSeries = Common::getRequestVar('labelSeries', '', 'string', $this->request); + $labelSeries = explode(',', $labelSeries); + $labelSeries = array_filter($labelSeries, 'strlen'); + $this->labelSeries = $labelSeries; + + $result = $this->manipulate($dataTable); + + return $result; } /** @@ -175,9 +186,27 @@ protected function manipulateDataTable($dataTable) $row = $this->doFilterRecursiveDescend($labelVariation, $dataTable); if ($row) { + if ($this->isComparing + && isset($this->labelSeries[$labelIndex]) + ) { + $comparisons = $row->getComparisons(); + if (!empty($comparisons)) { + $labelSeriesIndex = $this->labelSeries[$labelIndex]; + + $originalLabel = $row->getColumn('label'); + + $row = $comparisons->getRowFromId($labelSeriesIndex); + + // add label and make sure it is the first column + $columns = array_merge(['label' => $originalLabel . ' ' . $row->getMetadata('compareSeriesPretty')], $row->getColumns()); + $row->setColumns($columns); + } + } + if ($this->addLabelIndex) { $row->setMetadata(self::FLAG_IS_ROW_EVOLUTION, $labelIndex); } + $result->addRow($row); break; } @@ -185,4 +214,9 @@ protected function manipulateDataTable($dataTable) } return $result; } + + private function isComparing() + { + return Common::getRequestVar('compare', 0, 'int', $this->request) == 1; + } } diff --git a/app/core/API/DataTableManipulator/ReportTotalsCalculator.php b/app/core/API/DataTableManipulator/ReportTotalsCalculator.php index 45ffec9ee..a6d4fcc53 100644 --- a/app/core/API/DataTableManipulator/ReportTotalsCalculator.php +++ b/app/core/API/DataTableManipulator/ReportTotalsCalculator.php @@ -148,8 +148,10 @@ protected function manipulateDataTable($dataTable) $dataTable->setMetadata('totals', $totals); if (1 === Common::getRequestVar('keep_totals_row', 0, 'integer', $this->request)) { + $totalLabel = Common::getRequestVar('keep_totals_row_label', Piwik::translate('General_Totals'), 'string', $this->request); + $row->deleteMetadata(false); - $row->setColumn('label', Piwik::translate('General_Totals')); + $row->setColumn('label', $totalLabel); $dataTable->setTotalsRow($row); } } diff --git a/app/core/API/DataTablePostProcessor.php b/app/core/API/DataTablePostProcessor.php index cd49c8bd9..66e87828e 100644 --- a/app/core/API/DataTablePostProcessor.php +++ b/app/core/API/DataTablePostProcessor.php @@ -21,6 +21,7 @@ use Piwik\Plugin\ProcessedMetric; use Piwik\Plugin\Report; use Piwik\Plugin\ReportsProvider; +use Piwik\Plugins\API\Filter\DataComparisonFilter; /** * Processes DataTables that should be served through Piwik's APIs. This processing handles @@ -119,6 +120,7 @@ public function process(DataTableInterface $dataTable) $dataTable = $this->applyGenericFilters($dataTable); $this->applyComputeProcessedMetrics($dataTable); + $dataTable = $this->applyComparison($dataTable); if ($this->callbackAfterGenericFilters) { call_user_func($this->callbackAfterGenericFilters, $dataTable); @@ -126,6 +128,7 @@ public function process(DataTableInterface $dataTable) // we automatically safe decode all datatable labels (against xss) $dataTable->queueFilter('SafeDecodeLabel'); + $dataTable = $this->convertSegmentValueToSegment($dataTable); $dataTable = $this->applyQueuedFilters($dataTable); $dataTable = $this->applyRequestedColumnDeletion($dataTable); @@ -473,5 +476,27 @@ public function applyComputeProcessedMetrics(DataTableInterface $dataTable) { $dataTable->filter(array($this, 'computeProcessedMetrics')); } + + public function applyComparison(DataTableInterface $dataTable) + { + $compare = Common::getRequestVar('compare', '0', 'int', $this->request); + if ($compare != 1) { + return $dataTable; + } + + $filter = new DataComparisonFilter($this->request, $this->report); + $filter->compare($dataTable); + + $dataTable->filter(function (DataTable $table) { + foreach ($table->getRows() as $row) { + $comparisons = $row->getComparisons(); + if (!empty($comparisons)) { + $this->computeProcessedMetrics($comparisons); + } + } + }); + + return $dataTable; + } } diff --git a/app/core/API/DocumentationGenerator.php b/app/core/API/DocumentationGenerator.php index f398eb62f..be18398b3 100644 --- a/app/core/API/DocumentationGenerator.php +++ b/app/core/API/DocumentationGenerator.php @@ -262,6 +262,7 @@ public function getExampleUrl($class, $methodName, $parametersToSet = array()) $aParameters['language'] = false; $aParameters['translateColumnNames'] = false; $aParameters['label'] = false; + $aParameters['labelSeries'] = false; $aParameters['flat'] = false; $aParameters['include_aggregate_rows'] = false; $aParameters['filter_offset'] = false; @@ -285,6 +286,11 @@ public function getExampleUrl($class, $methodName, $parametersToSet = array()) $aParameters['expanded'] = false; $aParameters['idDimenson'] = false; $aParameters['format_metrics'] = false; + $aParameters['compare'] = false; + $aParameters['compareDates'] = false; + $aParameters['comparePeriods'] = false; + $aParameters['compareSegments'] = false; + $aParameters['comparisonIdSubtables'] = false; $entityNames = StaticContainer::get('entities.idNames'); foreach ($entityNames as $entityName) { diff --git a/app/core/API/Request.php b/app/core/API/Request.php index 7b81cbaed..cf323f448 100644 --- a/app/core/API/Request.php +++ b/app/core/API/Request.php @@ -500,6 +500,7 @@ public static function processRequest($method, $paramOverride = array(), $defaul $params['serialize'] = '0'; $params['module'] = 'API'; $params['method'] = $method; + $params['compare'] = '0'; $params = $paramOverride + $params; // process request @@ -556,6 +557,10 @@ public static function getCurrentUrlWithoutGenericFilters($params) } } + $params['compareDates'] = null; + $params['comparePeriods'] = null; + $params['compareSegments'] = null; + return Url::getCurrentQueryStringWithParametersModified($params); } diff --git a/app/core/Access.php b/app/core/Access.php index 1c410d66d..7a253831c 100644 --- a/app/core/Access.php +++ b/app/core/Access.php @@ -12,6 +12,7 @@ use Piwik\Access\CapabilitiesProvider; use Piwik\Access\RolesProvider; use Piwik\Container\StaticContainer; +use Piwik\Exception\InvalidRequestParameterException; use Piwik\Plugins\SitesManager\API as SitesManagerApi; /** @@ -412,7 +413,7 @@ public function getSitesIdWithWriteAccess() public function checkUserHasSuperUserAccess() { if (!$this->hasSuperUserAccess()) { - throw new NoAccessException(Piwik::translate('General_ExceptionPrivilege', array("'superuser'"))); + $this->throwNoAccessException(Piwik::translate('General_ExceptionPrivilege', array("'superuser'"))); } } @@ -456,7 +457,7 @@ public function isUserHasSomeAdminAccess() public function checkUserHasSomeWriteAccess() { if (!$this->isUserHasSomeWriteAccess()) { - throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('write'))); + $this->throwNoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('write'))); } } @@ -468,7 +469,7 @@ public function checkUserHasSomeWriteAccess() public function checkUserHasSomeAdminAccess() { if (!$this->isUserHasSomeAdminAccess()) { - throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin'))); + $this->throwNoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin'))); } } @@ -486,7 +487,7 @@ public function checkUserHasSomeViewAccess() $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); if (count($idSitesAccessible) == 0) { - throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('view'))); + $this->throwNoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('view'))); } } @@ -508,7 +509,7 @@ public function checkUserHasAdminAccess($idSites) foreach ($idSites as $idsite) { if (!in_array($idsite, $idSitesAccessible)) { - throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite))); + $this->throwNoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite))); } } } @@ -531,7 +532,7 @@ public function checkUserHasViewAccess($idSites) foreach ($idSites as $idsite) { if (!in_array($idsite, $idSitesAccessible)) { - throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite))); + $this->throwNoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite))); } } } @@ -554,11 +555,21 @@ public function checkUserHasWriteAccess($idSites) foreach ($idSites as $idsite) { if (!in_array($idsite, $idSitesAccessible)) { - throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'write'", $idsite))); + $this->throwNoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'write'", $idsite))); } } } + public function checkUserIsNotAnonymous() + { + if ($this->hasSuperUserAccess()) { + return; + } + if (Piwik::isUserIsAnonymous()) { + $this->throwNoAccessException(Piwik::translate('General_YouMustBeLoggedIn')); + } + } + private function getSitesIdWithCapability($capability) { if (!empty($this->idsitesByAccess[$capability])) { @@ -578,7 +589,7 @@ public function checkUserHasCapability($idSites, $capability) foreach ($idSites as $idsite) { if (!in_array($idsite, $idSitesAccessible)) { - throw new NoAccessException(Piwik::translate('ExceptionCapabilityAccessWebsite', array("'" . $capability ."'", $idsite))); + $this->throwNoAccessException(Piwik::translate('ExceptionCapabilityAccessWebsite', array("'" . $capability ."'", $idsite))); } } @@ -600,7 +611,7 @@ protected function getIdSites($idSites) $idSites = Site::getIdSitesFromIdSitesString($idSites); if (empty($idSites)) { - throw new NoAccessException("The parameter 'idSite=' is missing from the request."); + $this->throwNoAccessException("The parameter 'idSite=' is missing from the request."); } return $idSites; @@ -688,6 +699,28 @@ public function getCapabilitiesForSite($idSite) } return $result; } + + /** + * Throw a NoAccessException with the given message, or a more generic 'You need to log in' message if the + * user is not currently logged in (e.g. if session has expired). + * @param $message + * @throws NoAccessException + */ + private function throwNoAccessException($message) + { + if (Piwik::isUserIsAnonymous()) { + $message = Piwik::translate('General_YouMustBeLoggedIn'); + } + // Try to detect whether user was previously logged in so that we can display a different message + $referrer = Url::getReferrer(); + if ($referrer && Url::isValidHost(Url::getHostFromUrl($referrer)) && + strpos($_SERVER['HTTP_REFERER'], SettingsPiwik::getPiwikUrl()) === 0 + ) { + $message = Piwik::translate('General_YourSessionHasExpired'); + } + + throw new NoAccessException($message); + } } /** @@ -695,6 +728,6 @@ public function getCapabilitiesForSite($idSite) * * @api */ -class NoAccessException extends \Exception +class NoAccessException extends InvalidRequestParameterException { } diff --git a/app/core/Application/Kernel/EnvironmentValidator.php b/app/core/Application/Kernel/EnvironmentValidator.php index 6f324e2d0..3a526a8c5 100644 --- a/app/core/Application/Kernel/EnvironmentValidator.php +++ b/app/core/Application/Kernel/EnvironmentValidator.php @@ -10,6 +10,7 @@ use Piwik\Common; use Piwik\Config; +use Piwik\Exception\InvalidRequestParameterException; use Piwik\Exception\NotYetInstalledException; use Piwik\Filechecks; use Piwik\Piwik; diff --git a/app/core/Application/Kernel/PluginList.php b/app/core/Application/Kernel/PluginList.php index dbab86e3b..e3275c2c8 100644 --- a/app/core/Application/Kernel/PluginList.php +++ b/app/core/Application/Kernel/PluginList.php @@ -33,6 +33,7 @@ class PluginList */ private $corePluginsDisabledByDefault = array( 'DBStats', + 'ExamplePlugin', 'ExampleCommand', 'ExampleSettingsPlugin', 'ExampleUI', @@ -41,6 +42,7 @@ class PluginList 'ExampleTracker', 'ExampleLogTables', 'ExampleReport', + 'ExampleAPI', 'MobileAppMeasurable', 'Provider', 'TagManager' diff --git a/app/core/Archive.php b/app/core/Archive.php index 8d4b46e96..3bf6d5b18 100644 --- a/app/core/Archive.php +++ b/app/core/Archive.php @@ -453,31 +453,6 @@ public static function createDataTableFromArchive($recordName, $idSite, $period, return $dataTable; } - private function canUseDbReader() - { - if (Common::isPhpCliMode() ) { - // we are likely archiving or we are in CronArchive class etc. where it is important to detect if a - // specific archive already exist or not to possibly prevent triggering an unneeded archive request... - // also we only want to read archives from the reader for requests from the web - return false; - } - - if (SettingsServer::isArchivePhpTriggered()) { - // when archiving is triggered, we want to make sure to read archives from master to ensure most recent - // archives are read etc - return false; - } - - if (Rules::isArchivingDisabledFor($this->params->getIdSites(), $this->params->getSegment(), $this->getPeriodLabel())) { - // in this case we know we won't be creating any archives and we will only want to read archives in order - // to present the data in Matomo. We want to use the reader in this case - return true; - } - - // archiving could be triggered during this request, better not use the reader - return false; - } - private function getSiteIdsThatAreRequestedInThisArchiveButWereNotInvalidatedYet() { if (is_null(self::$cache)) { @@ -569,7 +544,7 @@ protected function get($archiveNames, $archiveDataType, $idSubtable = null) } $result = new Archive\DataCollection( - $dataNames, $archiveDataType, $this->params->getIdSites(), $this->params->getPeriods(), $defaultRow = null); + $dataNames, $archiveDataType, $this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $defaultRow = null); $archiveIds = $this->getArchiveIds($archiveNames); if (empty($archiveIds)) { @@ -581,7 +556,7 @@ protected function get($archiveNames, $archiveDataType, $idSubtable = null) return $result; } - $archiveData = ArchiveSelector::getArchiveData($archiveIds, $archiveNames, $archiveDataType, $idSubtable, $this->canUseDbReader()); + $archiveData = ArchiveSelector::getArchiveData($archiveIds, $archiveNames, $archiveDataType, $idSubtable); $isNumeric = $archiveDataType == 'numeric'; @@ -674,6 +649,15 @@ private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins) foreach ($this->params->getIdSites() as $idSite) { $site = new Site($idSite); + if ($period->getLabel() === 'day' + && !$this->params->getSegment()->isEmpty() + && Common::getRequestVar('skipArchiveSegmentToday', 0, 'int') + && $period->getDateStart()->toString() == Date::factory('now', $site->getTimezone())->toString()) { + + Log::debug("Skipping archive %s for %s as segment today is disabled", $period->getLabel(), $period->getPrettyString()); + continue; + } + // if the END of the period is BEFORE the website creation date // we already know there are no stats for this period // we add one day to make sure we don't miss the day of the website creation @@ -705,7 +689,7 @@ private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins) private function cacheArchiveIdsWithoutLaunching($plugins) { $idarchivesByReport = ArchiveSelector::getArchiveIds( - $this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins, $this->canUseDbReader()); + $this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins); // initialize archive ID cache for each report foreach ($plugins as $plugin) { diff --git a/app/core/Archive/ArchiveInvalidator.php b/app/core/Archive/ArchiveInvalidator.php index 0dab3baa2..4f8bf5e97 100644 --- a/app/core/Archive/ArchiveInvalidator.php +++ b/app/core/Archive/ArchiveInvalidator.php @@ -205,6 +205,50 @@ public function markArchivesAsInvalidated(array $idSites, array $dates, $period, return $invalidationInfo; } + /** + * @param $idSites int[] + * @param $dates Date[] + * @param $period string + * @param $segment Segment + * @param bool $cascadeDown + * @return InvalidationResult + * @throws \Exception + */ + public function markArchivesOverlappingRangeAsInvalidated(array $idSites, array $dates, Segment $segment = null) + { + $invalidationInfo = new InvalidationResult(); + + $ranges = array(); + foreach ($dates as $dateRange) { + $ranges[] = $dateRange[0] . ',' . $dateRange[1]; + } + $periodsByType = array(Period\Range::PERIOD_ID => $ranges); + + $invalidatedMonths = array(); + $archiveNumericTables = ArchiveTableCreator::getTablesArchivesInstalled($type = ArchiveTableCreator::NUMERIC_TABLE); + foreach ($archiveNumericTables as $table) { + $tableDate = ArchiveTableCreator::getDateFromTableName($table); + + $result = $this->model->updateArchiveAsInvalidated($table, $idSites, $periodsByType, $segment); + $rowsAffected = $result->rowCount(); + if ($rowsAffected > 0) { + $invalidatedMonths[] = $tableDate; + } + } + + foreach ($idSites as $idSite) { + foreach ($dates as $dateRange) { + $this->forgetRememberedArchivedReportsToInvalidate($idSite, $dateRange[0]); + $invalidationInfo->processedDates[] = $dateRange[0]; + } + } + + $archivesToPurge = new ArchivesToPurgeDistributedList(); + $archivesToPurge->add($invalidatedMonths); + + return $invalidationInfo; + } + /** * @param string[][][] $periodDates * @return string[][][] @@ -230,11 +274,13 @@ private function getPeriodsToInvalidate($dates, $periodType, $cascadeDown) { $periodsToInvalidate = array(); - foreach ($dates as $date) { - if ($periodType == 'range') { - $date = $date . ',' . $date; - } + if ($periodType == 'range') { + $rangeString = $dates[0] . ',' . $dates[1]; + $periodsToInvalidate[] = Period\Factory::build('range', $rangeString); + return $periodsToInvalidate; + } + foreach ($dates as $date) { $period = Period\Factory::build($periodType, $date); $periodsToInvalidate[] = $period; @@ -242,9 +288,7 @@ private function getPeriodsToInvalidate($dates, $periodType, $cascadeDown) $periodsToInvalidate = array_merge($periodsToInvalidate, $period->getAllOverlappingChildPeriods()); } - if ($periodType != 'year' - && $periodType != 'range' - ) { + if ($periodType != 'year') { $periodsToInvalidate[] = Period\Factory::build('year', $date); } } @@ -264,7 +308,11 @@ private function getPeriodDatesByYearMonthAndPeriodType($periods) $periodType = $period->getId(); $yearMonth = ArchiveTableCreator::getTableMonthFromDate($date); - $result[$yearMonth][$periodType][] = $date->toString(); + $dateString = $date->toString(); + if ($periodType == Period\Range::PERIOD_ID) { + $dateString = $period->getRangeString(); + } + $result[$yearMonth][$periodType][] = $dateString; } return $result; } diff --git a/app/core/Archive/ArchivePurger.php b/app/core/Archive/ArchivePurger.php index cfb04d223..cd330ea37 100644 --- a/app/core/Archive/ArchivePurger.php +++ b/app/core/Archive/ArchivePurger.php @@ -158,21 +158,23 @@ public function purgeOutdatedArchives(Date $dateStart) public function purgeDeletedSiteArchives(Date $dateStart) { - $idArchivesToDelete = $this->getDeletedSiteArchiveIds($dateStart); + $archiveTable = ArchiveTableCreator::getNumericTable($dateStart); + $idArchivesToDelete = $this->model->getArchiveIdsForDeletedSites($archiveTable); return $this->purge($idArchivesToDelete, $dateStart, 'deleted sites'); } /** * @param Date $dateStart - * @param array $segmentHashesByIdSite List of valid segment hashes, indexed by site ID + * @param array $deletedSegments List of segments whose archives should be purged * @return int */ - public function purgeDeletedSegmentArchives(Date $dateStart, array $segmentHashesByIdSite) + public function purgeDeletedSegmentArchives(Date $dateStart, array $deletedSegments) { - $idArchivesToDelete = $this->getDeletedSegmentArchiveIds($dateStart, $segmentHashesByIdSite); - - return $this->purge($idArchivesToDelete, $dateStart, 'deleted segments'); + if (count($deletedSegments)) { + $idArchivesToDelete = $this->getDeletedSegmentArchiveIds($dateStart, $deletedSegments); + return $this->purge($idArchivesToDelete, $dateStart, 'deleted segments'); + } } /** @@ -210,20 +212,11 @@ protected function purge(array $idArchivesToDelete, Date $dateStart, $reason) return $deletedRowCount; } - protected function getDeletedSiteArchiveIds(Date $date) - { - $archiveTable = ArchiveTableCreator::getNumericTable($date); - return $this->model->getArchiveIdsForDeletedSites( - $archiveTable, - $this->getOldestTemporaryArchiveToKeepThreshold() - ); - } - - protected function getDeletedSegmentArchiveIds(Date $date, array $segmentHashesByIdSite) + protected function getDeletedSegmentArchiveIds(Date $date, array $deletedSegments) { $archiveTable = ArchiveTableCreator::getNumericTable($date); - return $this->model->getArchiveIdsForDeletedSegments( - $archiveTable, $segmentHashesByIdSite, $this->getOldestTemporaryArchiveToKeepThreshold() + return $this->model->getArchiveIdsForSegments( + $archiveTable, $deletedSegments, $this->getOldestTemporaryArchiveToKeepThreshold() ); } diff --git a/app/core/Archive/DataCollection.php b/app/core/Archive/DataCollection.php index c172464af..8f7f61b01 100644 --- a/app/core/Archive/DataCollection.php +++ b/app/core/Archive/DataCollection.php @@ -105,7 +105,7 @@ class DataCollection * @param \Piwik\Period[] $periods @see $this->periods * @param array $defaultRow @see $this->defaultRow */ - public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultRow = null) + public function __construct($dataNames, $dataType, $sitesId, $periods, $segment, $defaultRow = null) { $this->dataNames = $dataNames; $this->dataType = $dataType; @@ -119,6 +119,8 @@ public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultR foreach ($periods as $period) { $this->periods[$period->getRangeString()] = $period; } + + $this->segment = $segment; $this->defaultRow = $defaultRow; } @@ -221,7 +223,7 @@ public function getIndexedArray($resultIndices) public function getDataTable($resultIndices) { $dataTableFactory = new DataTableFactory( - $this->dataNames, $this->dataType, $this->sitesId, $this->periods, $this->defaultRow); + $this->dataNames, $this->dataType, $this->sitesId, $this->periods, $this->segment, $this->defaultRow); $index = $this->getIndexedArray($resultIndices); @@ -238,7 +240,7 @@ public function getDataTable($resultIndices) public function getMergedDataTable($resultIndices) { $dataTableFactory = new DataTableFactory( - $this->dataNames, $this->dataType, $this->sitesId, $this->periods, $this->defaultRow); + $this->dataNames, $this->dataType, $this->sitesId, $this->periods, $this->segment, $this->defaultRow); $index = $this->getIndexedArray($resultIndices); @@ -278,7 +280,7 @@ public function getExpandedDataTable($resultIndices, $idSubTable = null, $depth } $dataTableFactory = new DataTableFactory( - $this->dataNames, 'blob', $this->sitesId, $this->periods, $this->defaultRow); + $this->dataNames, 'blob', $this->sitesId, $this->periods, $this->segment, $this->defaultRow); $dataTableFactory->expandDataTable($depth, $addMetadataSubTableId); $dataTableFactory->useSubtable($idSubTable); diff --git a/app/core/Archive/DataTableFactory.php b/app/core/Archive/DataTableFactory.php index 58bbfba56..fdc69a5f5 100644 --- a/app/core/Archive/DataTableFactory.php +++ b/app/core/Archive/DataTableFactory.php @@ -9,9 +9,15 @@ namespace Piwik\Archive; +use Piwik\API\Request; +use Piwik\Cache; +use Piwik\Cache\Transient; +use Piwik\CacheId; use Piwik\DataTable; use Piwik\DataTable\Row; use Piwik\Period\Week; +use Piwik\Piwik; +use Piwik\Segment; use Piwik\Site; /** @@ -22,6 +28,9 @@ */ class DataTableFactory { + const TABLE_METADATA_SEGMENT_INDEX = 'segment'; + const TABLE_METADATA_SEGMENT_PRETTY_INDEX = 'segmentPretty'; + /** * @see DataCollection::$dataNames. */ @@ -67,6 +76,11 @@ class DataTableFactory */ private $periods; + /** + * @var Segment + */ + private $segment; + /** * The ID of the subtable to create a DataTable for. Only relevant for blob data. * @@ -85,7 +99,7 @@ class DataTableFactory /** * Constructor. */ - public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultRow) + public function __construct($dataNames, $dataType, $sitesId, $periods, Segment $segment, $defaultRow) { $this->dataNames = $dataNames; $this->dataType = $dataType; @@ -93,6 +107,7 @@ public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultR //here index period by string only $this->periods = $periods; + $this->segment = $segment; $this->defaultRow = $defaultRow; } @@ -239,7 +254,10 @@ public function makeMerged($index, $resultIndices) private function makeFromBlobRow($blobRow, $keyMetadata) { if ($blobRow === false) { - return new DataTable(); + $table = new DataTable(); + $table->setAllTableMetadata($keyMetadata); + $this->setPrettySegmentMetadata($table); + return $table; } if (count($this->dataNames) === 1) { @@ -271,7 +289,8 @@ private function makeDataTableFromSingleBlob($blobRow, $keyMetadata) } // set table metadata - $table->setAllTableMetadata(array_merge(DataCollection::getDataRowMetadata($blobRow), $keyMetadata)); + $table->setAllTableMetadata(array_merge($table->getAllTableMetadata(), DataCollection::getDataRowMetadata($blobRow), $keyMetadata)); + $this->setPrettySegmentMetadata($table); if ($this->expandDataTable) { $table->enableRecursiveFilters(); @@ -297,7 +316,8 @@ private function makeIndexedByRecordNameDataTable($blobRow, $keyMetadata) foreach ($blobRow as $name => $blob) { $newTable = DataTable::fromSerializedArray($blob); - $newTable->setAllTableMetadata($tableMetadata); + $newTable->setAllTableMetadata(array_merge($newTable->getAllTableMetadata(), $tableMetadata)); + $this->setPrettySegmentMetadata($newTable); $table->addTable($newTable, $name); } @@ -397,8 +417,13 @@ private function setSubtables($dataTable, $blobRow, $treeLevel = 0) } $blobName = $dataName . "_" . $sid; - if (isset($blobRow[$blobName])) { + if (!empty($blobRow[$blobName])) { $subtable = DataTable::fromSerializedArray($blobRow[$blobName]); + $subtable->setMetadata(self::TABLE_METADATA_PERIOD_INDEX, $dataTable->getMetadata(self::TABLE_METADATA_PERIOD_INDEX)); + $subtable->setMetadata(self::TABLE_METADATA_SITE_INDEX, $dataTable->getMetadata(self::TABLE_METADATA_SITE_INDEX)); + $subtable->setMetadata(self::TABLE_METADATA_SEGMENT_INDEX, $dataTable->getMetadata(self::TABLE_METADATA_SEGMENT_INDEX)); + $subtable->setMetadata(self::TABLE_METADATA_SEGMENT_PRETTY_INDEX, $dataTable->getMetadata(self::TABLE_METADATA_SEGMENT_PRETTY_INDEX)); + $this->setSubtables($subtable, $blobRow, $treeLevel + 1); // we edit the subtable ID so that it matches the newly table created in memory @@ -419,6 +444,8 @@ private function getDefaultMetadata() return array( DataTableFactory::TABLE_METADATA_SITE_INDEX => new Site(reset($this->sitesId)), DataTableFactory::TABLE_METADATA_PERIOD_INDEX => reset($this->periods), + DataTableFactory::TABLE_METADATA_SEGMENT_INDEX => $this->segment->getString(), + DataTableFactory::TABLE_METADATA_SEGMENT_PRETTY_INDEX => $this->segment->getString(), ); } @@ -452,7 +479,8 @@ private function makeFromMetricsArray($data, $keyMetadata) $table = new DataTable\Simple(); if (!empty($data)) { - $table->setAllTableMetadata(array_merge(DataCollection::getDataRowMetadata($data), $keyMetadata)); + $table->setAllTableMetadata(array_merge($table->getAllTableMetadata(), DataCollection::getDataRowMetadata($data), $keyMetadata)); + $this->setPrettySegmentMetadata($table); DataCollection::removeMetadataFromDataRow($data); @@ -470,7 +498,8 @@ private function makeFromMetricsArray($data, $keyMetadata) $table->addRow(new Row(array(Row::COLUMNS => array($name => 0)))); } - $table->setAllTableMetadata($keyMetadata); + $table->setAllTableMetadata(array_merge($table->getAllTableMetadata(), $keyMetadata)); + $this->setPrettySegmentMetadata($table); } $result = $table; @@ -496,7 +525,8 @@ private function makeMergedTableWithPeriodAndSiteIndex($index, $resultIndices, $ $table = new DataTable(); } - $table->setAllTableMetadata($metadata); + $table->setAllTableMetadata(array_merge($table->getAllTableMetadata(), $metadata)); + $this->setPrettySegmentMetadata($table); $map->addTable($table, $this->prettifyIndexLabel(self::TABLE_METADATA_PERIOD_INDEX, $range)); $tables[$range] = $table; @@ -532,6 +562,7 @@ private function makeMergedWithSiteIndex($index, $useSimpleDataTable, $isNumeric } $table->setAllTableMetadata(array(DataTableFactory::TABLE_METADATA_PERIOD_INDEX => reset($this->periods))); + $this->setPrettySegmentMetadata($table); foreach ($index as $idsite => $row) { if (!empty($row)) { @@ -549,4 +580,15 @@ private function makeMergedWithSiteIndex($index, $useSimpleDataTable, $isNumeric return $table; } + + private function setPrettySegmentMetadata(DataTable $table) + { + $site = $table->getMetadata(self::TABLE_METADATA_SITE_INDEX); + $idSite = $site ? $site->getId() : false; + + $segmentPretty = $this->segment->getStoredSegmentName($idSite); + + $table->setMetadata('segment', $this->segment->getString()); + $table->setMetadata('segmentPretty', $segmentPretty); + } } diff --git a/app/core/ArchiveProcessor/Loader.php b/app/core/ArchiveProcessor/Loader.php index cf153c8a5..f08e63003 100644 --- a/app/core/ArchiveProcessor/Loader.php +++ b/app/core/ArchiveProcessor/Loader.php @@ -24,13 +24,6 @@ */ class Loader { - /** - * Is the current archive temporary. ie. - * - today - * - current week / month / year - */ - protected $temporaryArchive; - /** * Idarchive in the DB for the requested archive * @@ -105,7 +98,7 @@ protected function prepareCoreMetricsArchive($visits, $visitsConverted) $this->params->setRequestedPlugin('VisitsSummary'); - $pluginsArchiver = new PluginsArchiver($this->params, $this->isArchiveTemporary()); + $pluginsArchiver = new PluginsArchiver($this->params); $metrics = $pluginsArchiver->callAggregateCoreMetrics(); $pluginsArchiver->finalizeArchive(); @@ -120,7 +113,7 @@ protected function prepareCoreMetricsArchive($visits, $visitsConverted) protected function prepareAllPluginsArchive($visits, $visitsConverted) { - $pluginsArchiver = new PluginsArchiver($this->params, $this->isArchiveTemporary()); + $pluginsArchiver = new PluginsArchiver($this->params); if ($this->mustProcessVisitCount($visits) || $this->doesRequestedPluginIncludeVisitsSummary() @@ -171,14 +164,11 @@ protected function loadExistingArchiveIdFromDb() { $noArchiveFound = array(false, false, false); - // see isArchiveTemporary() - $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed(); - if ($this->isArchivingForcedToTrigger()) { return $noArchiveFound; } - $idAndVisits = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC); + $idAndVisits = ArchiveSelector::getArchiveIdAndVisits($this->params); if (!$idAndVisits) { return $noArchiveFound; @@ -187,31 +177,6 @@ protected function loadExistingArchiveIdFromDb() return $idAndVisits; } - /** - * Returns the minimum archive processed datetime to look at. Only public for tests. - * - * @return int|bool Datetime timestamp, or false if must look at any archive available - */ - protected function getMinTimeArchiveProcessed() - { - $endDateTimestamp = self::determineIfArchivePermanent($this->params->getDateEnd()); - $isArchiveTemporary = ($endDateTimestamp === false); - $this->temporaryArchive = $isArchiveTemporary; - - if ($endDateTimestamp) { - // Permanent archive - return $endDateTimestamp; - } - - $dateStart = $this->params->getDateStart(); - $period = $this->params->getPeriod(); - $segment = $this->params->getSegment(); - $site = $this->params->getSite(); - - // Temporary archive - return Rules::getMinTimeProcessedForTemporaryArchive($dateStart, $period, $segment, $site); - } - protected static function determineIfArchivePermanent(Date $dateEnd) { $now = time(); @@ -226,15 +191,6 @@ protected static function determineIfArchivePermanent(Date $dateEnd) return false; } - protected function isArchiveTemporary() - { - if (is_null($this->temporaryArchive)) { - throw new \Exception("getMinTimeArchiveProcessed() should be called prior to isArchiveTemporary()"); - } - - return $this->temporaryArchive; - } - private function shouldArchiveForSiteEvenWhenNoVisits() { $idSitesToArchive = $this->getIdSitesToArchiveWhenNoVisits(); diff --git a/app/core/ArchiveProcessor/Parameters.php b/app/core/ArchiveProcessor/Parameters.php index a099b8b0f..e76578792 100644 --- a/app/core/ArchiveProcessor/Parameters.php +++ b/app/core/ArchiveProcessor/Parameters.php @@ -221,12 +221,9 @@ public function isSingleSite() return count($this->getIdSites()) == 1; } - public function logStatusDebug($isTemporary) + public function logStatusDebug() { $temporary = 'definitive archive'; - if ($isTemporary) { - $temporary = 'temporary archive'; - } Log::debug( "%s archive, idSite = %d (%s), segment '%s', report = '%s', UTC datetime [%s -> %s]", $this->getPeriod()->getLabel(), diff --git a/app/core/ArchiveProcessor/PluginsArchiver.php b/app/core/ArchiveProcessor/PluginsArchiver.php index 001c29567..a5f828ee6 100644 --- a/app/core/ArchiveProcessor/PluginsArchiver.php +++ b/app/core/ArchiveProcessor/PluginsArchiver.php @@ -61,14 +61,14 @@ class PluginsArchiver */ private $shouldAggregateFromRawData; - public function __construct(Parameters $params, $isTemporaryArchive, ArchiveWriter $archiveWriter = null) + public function __construct(Parameters $params, ArchiveWriter $archiveWriter = null) { $this->params = $params; - $this->isTemporaryArchive = $isTemporaryArchive; - $this->archiveWriter = $archiveWriter ?: new ArchiveWriter($this->params, $this->isTemporaryArchive); + $this->archiveWriter = $archiveWriter ?: new ArchiveWriter($this->params); $this->archiveWriter->initNewArchive(); $this->logAggregator = new LogAggregator($params); + $this->logAggregator->allowUsageSegmentCache(); $this->archiveProcessor = new ArchiveProcessor($this->params, $this->archiveWriter, $this->logAggregator); @@ -99,6 +99,7 @@ public function __construct(Parameters $params, $isTemporaryArchive, ArchiveWrit */ public function callAggregateCoreMetrics() { + $this->logAggregator->cleanup(); $this->logAggregator->setQueryOriginHint('Core'); if ($this->shouldAggregateFromRawData) { @@ -193,13 +194,17 @@ public function callAggregateAllPlugins($visits, $visitsConverted, $forceArchivi Manager::getInstance()->deleteAll($latestUsedTableId); unset($archiver); } + + $this->logAggregator->cleanup(); } public function finalizeArchive() { - $this->params->logStatusDebug($this->archiveWriter->isArchiveTemporary); + $this->params->logStatusDebug(); $this->archiveWriter->finalizeArchive(); - return $this->archiveWriter->getIdArchive(); + $idArchive = $this->archiveWriter->getIdArchive(); + + return $idArchive; } /** @@ -318,9 +323,9 @@ private function makeNewArchiverObject($archiverClass, $pluginName) * @param \Piwik\Plugin\Archiver &$archiver The newly created plugin archiver instance. * @param string $pluginName The name of plugin of which archiver instance was created. * @param array $this->params Array containing archive parameters (Site, Period, Date and Segment) - * @param bool $this->isTemporaryArchive Flag indicating whether the archive being processed is temporary (ie. the period isn't finished yet) or final (the period is already finished and in the past). + * @param bool false This parameter is deprecated and will be removed. */ - Piwik::postEvent('Archiving.makeNewArchiverObject', array($archiver, $pluginName, $this->params, $this->isTemporaryArchive)); + Piwik::postEvent('Archiving.makeNewArchiverObject', array($archiver, $pluginName, $this->params, false)); return $archiver; } diff --git a/app/core/ArchiveProcessor/Rules.php b/app/core/ArchiveProcessor/Rules.php index ebd054fe6..90d8d46a2 100644 --- a/app/core/ArchiveProcessor/Rules.php +++ b/app/core/ArchiveProcessor/Rules.php @@ -57,7 +57,7 @@ public static function getDoneStringFlagFor(array $idSites, $segment, $periodLab public static function shouldProcessReportsAllPlugins(array $idSites, Segment $segment, $periodLabel) { - if ($segment->isEmpty() && $periodLabel != 'range') { + if ($segment->isEmpty() && ($periodLabel != 'range' || SettingsServer::isArchivePhpTriggered())) { return true; } diff --git a/app/core/AssetManager.php b/app/core/AssetManager.php index bf9fc6fd2..3581822a4 100644 --- a/app/core/AssetManager.php +++ b/app/core/AssetManager.php @@ -75,6 +75,29 @@ public function __construct() } } + /** + * @inheritDoc + * @return AssetManager + */ + public static function getInstance() + { + $assetManager = parent::getInstance(); + + /** + * Triggered when creating an instance of the asset manager. Lets you overwite the + * asset manager behavior. + * + * @param AssetManager &$assetManager + * + * @ignore + * This event is not a public event since we don't want to make the asset manager itself public + * API + */ + Piwik::postEvent('AssetManager.makeNewAssetManagerObject', array(&$assetManager)); + + return $assetManager; + } + /** * @param UIAssetCacheBuster $cacheBuster */ @@ -188,19 +211,6 @@ public function getMergedNonCoreJavaScript() return $this->getMergedJavascript($this->getNonCoreJScriptFetcher(), $this->getMergedNonCoreJSAsset()); } - /** - * @inheritDoc - * @return AssetManager - */ - public static function getInstance() - { - $assetManager = parent::getInstance(); - - Piwik::postEvent('AssetManager.makeNewAssetManagerObject', array(&$assetManager)); - - return $assetManager; - } - /** * @param boolean $core * @return string[] @@ -302,7 +312,7 @@ private function getMergedJavascript($assetFetcher, $mergedAsset) * * @return string */ - private function getIndividualCoreAndNonCoreJsIncludes() + protected function getIndividualCoreAndNonCoreJsIncludes() { return $this->getIndividualJsIncludesFromAssetFetcher($this->getCoreJScriptFetcher()) . diff --git a/app/core/AssetManager/UIAsset/OnDiskUIAsset.php b/app/core/AssetManager/UIAsset/OnDiskUIAsset.php index 6c3d21460..e6ff9135c 100644 --- a/app/core/AssetManager/UIAsset/OnDiskUIAsset.php +++ b/app/core/AssetManager/UIAsset/OnDiskUIAsset.php @@ -55,7 +55,7 @@ public function getAbsoluteLocation() public function getRelativeLocation() { - if (!empty($this->relativeRootDir)) { + if (isset($this->relativeRootDir)) { return $this->relativeRootDir . $this->relativeLocation; } return $this->relativeLocation; diff --git a/app/core/CliMulti.php b/app/core/CliMulti.php index 3a9c5ea1f..863bea1b3 100644 --- a/app/core/CliMulti.php +++ b/app/core/CliMulti.php @@ -239,21 +239,23 @@ private function generateCommandId($command) */ public function supportsAsync() { - $supportsAsync = Process::isSupported() && !Common::isPhpCgiType() && $this->findPhpBinary(); - /** - * Triggered to allow plugins to force the usage of async cli multi execution or to disable it. - * - * **Example** - * - * public function supportsAsync(&$supportsAsync) - * { - * $supportsAsync = false; // do not allow async climulti execution - * } - * - * @param bool &$supportsAsync Whether async is supported or not. - */ - Piwik::postEvent('CliMulti.supportsAsync', array(&$supportsAsync)); - return $supportsAsync; + $supportsAsync = Process::isSupported() && !Common::isPhpCgiType() && $this->findPhpBinary(); + + /** + * Triggered to allow plugins to force the usage of async cli multi execution or to disable it. + * + * **Example** + * + * public function supportsAsync(&$supportsAsync) + * { + * $supportsAsync = false; // do not allow async climulti execution + * } + * + * @param bool &$supportsAsync Whether async is supported or not. + */ + Piwik::postEvent('CliMulti.supportsAsync', array(&$supportsAsync)); + + return $supportsAsync; } private function findPhpBinary() diff --git a/app/core/Common.php b/app/core/Common.php index 6a6fad5df..15f709ce7 100644 --- a/app/core/Common.php +++ b/app/core/Common.php @@ -1259,34 +1259,20 @@ public static function destroy(&$var) } /** - * @todo This method is weird, it's debugging statements but seem to only work for the tracker, maybe it - * should be moved elsewhere + * @deprecated Use the logger directly instead. */ public static function printDebug($info = '') { - if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) { - if (!headers_sent()) { - // prevent XSS in tracker debug output - Common::sendHeader('Content-type: text/plain'); - } - - if (is_object($info)) { - $info = var_export($info, true); - } - - $logger = StaticContainer::get('Psr\Log\LoggerInterface'); + if (is_object($info)) { + $info = var_export($info, true); + } - if (is_array($info) || is_object($info)) { - $info = Common::sanitizeInputValues($info); - $out = var_export($info, true); - foreach (explode("\n", $out) as $line) { - $logger->debug($line); - } - } else { - foreach (explode("\n", $info) as $line) { - $logger->debug($line); - } - } + $logger = StaticContainer::get('Psr\Log\LoggerInterface'); + if (is_array($info) || is_object($info)) { + $out = var_export($info, true); + $logger->debug($out); + } else { + $logger->debug($info); } } diff --git a/app/core/Concurrency/Lock.php b/app/core/Concurrency/Lock.php new file mode 100644 index 000000000..38818fd15 --- /dev/null +++ b/app/core/Concurrency/Lock.php @@ -0,0 +1,99 @@ +backend = $backend; + $this->lockKeyStart = $lockKeyStart; + $this->lockKey = $this->lockKeyStart; + } + + public function getNumberOfAcquiredLocks() + { + return count($this->getAllAcquiredLockKeys()); + } + + public function getAllAcquiredLockKeys() + { + return $this->backend->getKeysMatchingPattern($this->lockKeyStart . '*'); + } + + public function acquireLock($id, $ttlInSeconds = 60) + { + $this->lockKey = $this->lockKeyStart . $id; + + $lockValue = substr(Common::generateUniqId(), 0, 12); + $locked = $this->backend->setIfNotExists($this->lockKey, $lockValue, $ttlInSeconds); + + if ($locked) { + $this->lockValue = $lockValue; + } + + return $locked; + } + + public function isLocked() + { + if (!$this->lockValue) { + return false; + } + + return $this->lockValue === $this->backend->get($this->lockKey); + } + + public function unlock() + { + if ($this->lockValue) { + $this->backend->deleteIfKeyHasValue($this->lockKey, $this->lockValue); + $this->lockValue = null; + } + } + + public function expireLock($ttlInSeconds) + { + if ($ttlInSeconds > 0 && $this->lockValue) { + $success = $this->backend->expireIfKeyHasValue($this->lockKey, $this->lockValue, $ttlInSeconds); + if (!$success) { + $value = $this->backend->get($this->lockKey); + $message = sprintf('Failed to expire key %s (%s / %s).', $this->lockKey, $this->lockValue, (string) $value); + + if ($value === false) { + Common::printDebug($message . ' It seems like the key already expired as it no longer exists.'); + } elseif (!empty($value) && $value == $this->lockValue) { + Common::printDebug($message . ' We still have the lock but for some reason it did not expire.'); + } elseif (!empty($value)) { + Common::printDebug($message . ' This lock has been acquired by another process/server.'); + } else { + Common::printDebug($message . ' Failed to expire key.'); + } + + return false; + } + + return true; + } + + return false; + } +} diff --git a/app/core/Concurrency/LockBackend.php b/app/core/Concurrency/LockBackend.php new file mode 100644 index 000000000..7f67cbafa --- /dev/null +++ b/app/core/Concurrency/LockBackend.php @@ -0,0 +1,58 @@ +getQueryPartExpiryTime()); + $pattern = str_replace('*', '%', $pattern); + $keys = Db::fetchAll($sql, array($pattern)); + $raw = array_column($keys, 'key'); + return $raw; + } + + public function setIfNotExists($key, $value, $ttlInSeconds) + { + if (empty($ttlInSeconds)) { + $ttlInSeconds = 999999999; + } + + // FYI: We used to have an INSERT INTO ... ON DUPLICATE UPDATE ... However, this can be problematic in concurrency issues + // because the ON DUPLICATE UPDATE may work successfully for 2 jobs at the same time but only one of them got the lock then. + // This would be perfectly fine if we did something like `return $this->get($key) === $value` to 100% detect which process + // got the lock as we do now. However, maybe the expireTime gets overwritten with a wrong value or so. That's why we + // rather try to get the lock with the insert only because only one job can succeed with this. If below flow with the + // delete becomes to slow, we may be able to use the INSERT INTO ... ON DUPLICATE UPDATE again. + + if ($this->get($key)) { + return false; // a value is set, won't be possible to insert + } + + $tablePrefixed = self::getTableName(); + + // remove any existing but expired lock + // todo: we could combine get() and keyExists() in one query! + if ($this->keyExists($key)) { + // most of the time an expired key should not exist... we don't want to lock the row unncessarily therefore we check first + // if value exists... + $sql = sprintf('DELETE FROM %s WHERE `key` = ? and not (%s)', $tablePrefixed, $this->getQueryPartExpiryTime()); + Db::query($sql, array($key)); + } + + $query = sprintf('INSERT INTO %s (`key`, `value`, `expiry_time`) + VALUES (?,?,(UNIX_TIMESTAMP() + ?))', + $tablePrefixed); + // we make sure to update the row if the key is expired and consider it as "deleted" + + try { + Db::query($query, array($key, $value, (int) $ttlInSeconds)); + } catch (\Exception $e) { + if ($e->getCode() == 23000 + || strpos($e->getMessage(), 'Duplicate entry') !== false + || strpos($e->getMessage(), ' 1062 ') !== false) { + return false; + } + throw $e; + } + + // we make sure we got the lock + return $this->get($key) === $value; + } + + public function get($key) + { + $sql = sprintf('SELECT SQL_NO_CACHE `value` FROM %s WHERE `key` = ? AND %s LIMIT 1', self::getTableName(), $this->getQueryPartExpiryTime()); + return Db::fetchOne($sql, array($key)); + } + + public function deleteIfKeyHasValue($key, $value) + { + if (empty($value)) { + return false; + } + + $sql = sprintf('DELETE FROM %s WHERE `key` = ? and `value` = ?', self::getTableName()); + return $this->queryDidMakeChange($sql, array($key, $value)); + } + + public function expireIfKeyHasValue($key, $value, $ttlInSeconds) + { + if (empty($value)) { + return false; + } + + // we need to use unix_timestamp in mysql and not time() in php since the local time might be different on each server + // better to rely on one central DB server time only + $sql = sprintf('UPDATE %s SET expiry_time = (UNIX_TIMESTAMP() + ?) WHERE `key` = ? and `value` = ?', self::getTableName()); + $success = $this->queryDidMakeChange($sql, array((int) $ttlInSeconds, $key, $value)); + + if (!$success) { + // the above update did not work because the same time was already set and we just tried to set the same ttl + // again too fast within one second + return $value === $this->get($key); + } + + return true; + } + + public function keyExists($key) + { + $sql = sprintf('SELECT SQL_NO_CACHE 1 FROM %s WHERE `key` = ? LIMIT 1', self::getTableName()); + $value = Db::fetchOne($sql, array($key)); + return !empty($value); + } + + private function queryDidMakeChange($sql, $bind = array()) + { + $query = Db::query($sql, $bind); + if (is_object($query) && method_exists($query, 'rowCount')) { + // anything else but mysqli in tracker mode + return (bool) $query->rowCount(); + } else { + // mysqli in tracker mode + return (bool) Db::get()->rowCount($query); + } + } + + private static function getTableName() + { + return Common::prefixTable(self::TABLE_NAME); + } + + private function getQueryPartExpiryTime() + { + return 'UNIX_TIMESTAMP() <= expiry_time'; + } +} \ No newline at end of file diff --git a/app/core/Config.php b/app/core/Config.php index c55250035..f434a51fd 100644 --- a/app/core/Config.php +++ b/app/core/Config.php @@ -11,6 +11,8 @@ use Exception; use Piwik\Application\Kernel\GlobalSettingsProvider; +use Piwik\Config\Cache; +use Piwik\Config\IniFileChain; use Piwik\Container\StaticContainer; use Piwik\Exception\MissingFilePermissionException; use Piwik\ProfessionalServices\Advertising; @@ -139,7 +141,7 @@ public static function getLocalConfigPath() if (!empty($GLOBALS['CONFIG_INI_PATH_RESOLVER']) && is_callable($GLOBALS['CONFIG_INI_PATH_RESOLVER'])) { return call_user_func($GLOBALS['CONFIG_INI_PATH_RESOLVER']); } - + $path = self::getByDomainConfigPath(); if ($path) { return $path; @@ -332,7 +334,7 @@ public function existsLocalConfig() public function deleteLocalConfig() { $configLocal = $this->getLocalPath(); - + if(file_exists($configLocal)){ @unlink($configLocal); } @@ -368,7 +370,7 @@ public function getFromCommonConfig($name) { return $this->settings->getIniFileChain()->getFrom($this->getCommonPath(), $name); } - + /** * @api */ @@ -407,16 +409,9 @@ public function dumpConfig() /** * Write user configuration file * - * @param array $configLocal - * @param array $configGlobal - * @param array $configCommon - * @param array $configCache - * @param string $pathLocal - * @param bool $clear - * * @throws \Exception if config file not writable */ - protected function writeConfig($clear = true) + protected function writeConfig() { $output = $this->dumpConfig(); if ($output !== null @@ -435,6 +430,8 @@ protected function writeConfig($clear = true) throw $this->getConfigNotWritableException(); } + $this->settings->getIniFileChain()->deleteConfigCache(); + /** * Triggered when a INI config file is changed on disk. * @@ -442,10 +439,6 @@ protected function writeConfig($clear = true) */ Piwik::postEvent('Core.configFileChanged', [$localPath]); } - - if ($clear) { - //$this->reload(); - } } /** diff --git a/app/core/Config/Cache.php b/app/core/Config/Cache.php new file mode 100644 index 000000000..aa0bf80e9 --- /dev/null +++ b/app/core/Config/Cache.php @@ -0,0 +1,89 @@ +host = $this->getHost(); + + // because the config is not yet loaded we cannot identify the instanceId... + // need to use the hostname + $dir = $this->makeCacheDir($this->host); + + parent::__construct($dir); + } + + private function makeCacheDir($host) + { + return PIWIK_INCLUDE_PATH . '/tmp/' . $host . '/cache/tracker'; + } + + public function isValidHost($mergedConfigSettings) + { + if (!isset($mergedConfigSettings['General']['trusted_hosts']) || !is_array($mergedConfigSettings['General']['trusted_hosts'])) { + return false; + } + // note: we do not support "enable_trusted_host_check" to keep things secure + return in_array($this->host, $mergedConfigSettings['General']['trusted_hosts'], true); + } + + private function getHost() + { + $host = Url::getHost($checkIfTrusted = false); + $host = Url::getHostSanitized($host); // Remove any port number to get actual hostname + $host = Common::sanitizeInputValue($host); + + if (empty($host) + || strpos($host, '..') !== false + || strpos($host, '\\') !== false + || strpos($host, '/') !== false) { + throw new \Exception('Unsupported host'); + } + + $this->host = $host; + + return $host; + } + + public function doDelete($id) + { + // when the config changes, we need to invalidate the config caches for all configured hosts as well, not only + // the currently trusted host + $hosts = Url::getTrustedHosts(); + $initialDir = $this->directory; + + foreach ($hosts as $host) + { + $dir = $this->makeCacheDir($host); + if (@is_dir($dir)) { + $this->directory = $dir; + $success = parent::doDelete($id); + if ($success) { + Piwik::postEvent('Core.configFileDeleted', array($this->getFilename($id))); + } + } + } + + $this->directory = $initialDir; + } + +} diff --git a/app/core/Config/IniFileChain.php b/app/core/Config/IniFileChain.php index eb773b582..a3ac25d74 100644 --- a/app/core/Config/IniFileChain.php +++ b/app/core/Config/IniFileChain.php @@ -35,6 +35,7 @@ */ class IniFileChain { + const CONFIG_CACHE_KEY = 'config.ini'; /** * Maps INI file names with their parsed contents. The order of the files signifies the order * in the chain. Files with lower index are overwritten/merged with files w/ a higher index. @@ -209,6 +210,19 @@ public function reload($defaultSettingsFiles = array(), $userSettingsFile = null $this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile); } + if (!empty($userSettingsFile) && !empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])) { + $cache = new Cache(); + $values = $cache->doFetch(self::CONFIG_CACHE_KEY); + + if (!empty($values) + && isset($values['mergedSettings']) + && isset($values['settingsChain'])) { + $this->mergedSettings = $values['mergedSettings']; + $this->settingsChain = $values['settingsChain']; + return; + } + } + $reader = new IniReader(); foreach ($this->settingsChain as $file => $ignore) { if (is_readable($file)) { @@ -228,9 +242,31 @@ public function reload($defaultSettingsFiles = array(), $userSettingsFile = null // on PHP 7+ as they would be always equal $this->mergedSettings = $this->copy($merged); - if (!empty($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS']) && !empty($this->mergedSettings)) { - $this->mergedSettings = call_user_func($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS'], $this->mergedSettings); - } + if (!empty($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS']) && !empty($this->mergedSettings)) { + $this->mergedSettings = call_user_func($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS'], $this->mergedSettings); + } + + if (!empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE']) + && !empty($userSettingsFile) + && !empty($this->mergedSettings) + && !empty($this->settingsChain)) { + + $ttlOneHour = 3600; + $cache = new Cache(); + if ($cache->isValidHost($this->mergedSettings)) { + // we make sure to save the config only if the host is valid... + $data = array('mergedSettings' => $this->mergedSettings, 'settingsChain' => $this->settingsChain); + $cache->doSave(self::CONFIG_CACHE_KEY, $data, $ttlOneHour); + } + } + } + + public function deleteConfigCache() + { + if (!empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])) { + $cache = new Cache(); + $cache->doDelete(IniFileChain::CONFIG_CACHE_KEY); + } } private function copy($merged) diff --git a/app/core/Console.php b/app/core/Console.php index a25d65b8d..9a2c45d6a 100644 --- a/app/core/Console.php +++ b/app/core/Console.php @@ -8,12 +8,15 @@ */ namespace Piwik; +use Exception; use Piwik\Application\Environment; use Piwik\Config\ConfigNotFoundException; use Piwik\Container\StaticContainer; +use Piwik\Exception\AuthenticationFailedException; use Piwik\Plugin\Manager as PluginManager; use Piwik\Plugins\Monolog\Handler\FailureLogMessageDetector; use Piwik\Version; +use Psr\Log\LoggerInterface; use Symfony\Bridge\Monolog\Handler\ConsoleHandler; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; @@ -79,6 +82,8 @@ public function doRun(InputInterface $input, OutputInterface $output) Log::warning($e->getMessage()); } + $this->initAuth(); + $commands = $this->getAvailableCommands(); foreach ($commands as $command) { @@ -260,4 +265,18 @@ private function getCommandsFromPluginsMarkedInConfig() } return $commands; } + + private function initAuth() + { + Piwik::postEvent('Request.initAuthenticationObject'); + try { + StaticContainer::get('Piwik\Auth'); + } catch (Exception $e) { + $message = "Authentication object cannot be found in the container. Maybe the Login plugin is not activated? + You can activate the plugin by adding: + Plugins[] = Login + under the [Plugins] section in your config/config.ini.php"; + StaticContainer::get(LoggerInterface::class)->warning($message); + } + } } diff --git a/app/core/Container/ContainerFactory.php b/app/core/Container/ContainerFactory.php index 3c08c930b..1ae6f6a2d 100644 --- a/app/core/Container/ContainerFactory.php +++ b/app/core/Container/ContainerFactory.php @@ -87,12 +87,10 @@ public function create() $this->addEnvironmentConfig($builder, $environment); } - $configPhpPath = PIWIK_USER_PATH . '/config/config.php'; - // User config - if (file_exists($configPhpPath) + if (file_exists(PIWIK_USER_PATH . '/config/config.php') && !in_array('test', $this->environments, true)) { - $builder->addDefinitions($configPhpPath); + $builder->addDefinitions(PIWIK_USER_PATH . '/config/config.php'); } if (!empty($this->definitions)) { @@ -114,7 +112,7 @@ private function addEnvironmentConfig(ContainerBuilder $builder, $environment) return; } - $file = sprintf('%s/config/environment/%s.php', PIWIK_DOCUMENT_ROOT, $environment); + $file = sprintf('%s/config/environment/%s.php', PIWIK_USER_PATH, $environment); if (file_exists($file)) { $builder->addDefinitions($file); diff --git a/app/core/Cookie.php b/app/core/Cookie.php index ab6c81745..25c17c4e8 100644 --- a/app/core/Cookie.php +++ b/app/core/Cookie.php @@ -204,7 +204,7 @@ private function extractSignedContent($content) $signature = substr($content, -40); if (substr($content, -43, 3) == self::VALUE_SEPARATOR . '_=' && - $signature == sha1(substr($content, 0, -40) . SettingsPiwik::getSalt()) + $signature === sha1(substr($content, 0, -40) . SettingsPiwik::getSalt()) ) { // strip trailing: VALUE_SEPARATOR '_=' signature" return substr($content, 0, -43); diff --git a/app/core/CronArchive.php b/app/core/CronArchive.php index 3f3d645d9..2e3d6fdca 100644 --- a/app/core/CronArchive.php +++ b/app/core/CronArchive.php @@ -222,6 +222,13 @@ class CronArchive */ public $disableSegmentsArchiving = false; + /** + * If enabled, segments will be only archived for yesterday, but not today. If the segment was created recently, + * then it will still be archived for today and the setting will be ignored for this segment. + * @var bool + */ + public $skipSegmentsToday = false; + private $websitesWithVisitsSinceLastRun = 0; private $skippedPeriodsArchivesWebsite = 0; private $skippedPeriodsNoDataInPeriod = 0; @@ -288,6 +295,7 @@ public function __construct($processNewSegmentsFrom = null, LoggerInterface $log $this->formatter = new Formatter(); $processNewSegmentsFrom = $processNewSegmentsFrom ?: StaticContainer::get('ini.General.process_new_segments_from'); + $this->segmentArchivingRequestUrlProvider = new SegmentArchivingRequestUrlProvider($processNewSegmentsFrom); $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator'); @@ -359,6 +367,10 @@ public function init() $this->logger->info('Will ignore websites and help finish a previous started queue instead. IDs: ' . implode(', ', $this->websites->getInitialSiteIds())); } + if ($this->skipSegmentsToday) { + $this->logger->info('Will skip segments archiving for today unless they were created recently'); + } + $this->logForcedSegmentInfo(); /** @@ -1685,9 +1697,17 @@ private function getCustomDateRangeToPreProcess($idSite) if (is_null($cache)) { $cache = $this->loadCustomDateRangeToPreProcess(); } + if (empty($cache[$idSite])) { - return array(); + $cache[$idSite] = array(); } + + $customRanges = array_filter(Config::getInstance()->General['archiving_custom_ranges']); + + if (!empty($customRanges)) { + $cache[$idSite] = array_merge($cache[$idSite], $customRanges); + } + $dates = array_unique($cache[$idSite]); return $dates; } @@ -1760,6 +1780,22 @@ private function makeRequestUrl($url) return $url; } + protected function wasSegmentChangedRecently($definition, $allSegments) + { + foreach ($allSegments as $segment) { + if ($segment['definition'] === $definition) { + $twentyFourHoursAgo = Date::now()->subHour(24); + $segmentDate = $segment['ts_created']; + if (!empty($segment['ts_last_edit'])) { + $segmentDate = $segment['ts_last_edit']; + } + return Date::factory($segmentDate)->isLater($twentyFourHoursAgo); + } + } + + return false; + } + /** * @param $idSite * @param $period @@ -1785,12 +1821,29 @@ private function getUrlsWithSegment($idSite, $period, $date) $segmentCount = count($segments); $processedSegmentCount = 0; + $allSegmentsFullInfo = array(); + if ($this->skipSegmentsToday) { + // small performance tweak... only needed when skip segments today + $segmentEditorModel = StaticContainer::get('Piwik\Plugins\SegmentEditor\Model'); + $allSegmentsFullInfo = $segmentEditorModel->getSegmentsToAutoArchive($idSite); + } + foreach ($segments as $segment) { + $shouldSkipToday = $this->skipSegmentsToday && !$this->wasSegmentChangedRecently($segment, $allSegmentsFullInfo); + + if ($this->skipSegmentsToday && !$shouldSkipToday) { + $this->logger->info(sprintf('Segment "%s" was created or changed recently and will therefore archive today', $segment)); + } + $dateParamForSegment = $this->segmentArchivingRequestUrlProvider->getUrlParameterDateString($idSite, $period, $date, $segment); $urlWithSegment = $this->getVisitsRequestUrl($idSite, $period, $dateParamForSegment, $segment); $urlWithSegment = $this->makeRequestUrl($urlWithSegment); + if ($shouldSkipToday) { + $urlWithSegment .= '&skipArchiveSegmentToday=1'; + } + if ($this->isAlreadyArchivingSegment($urlWithSegment, $idSite, $period, $segment)) { continue; } diff --git a/app/core/DataAccess/ArchiveSelector.php b/app/core/DataAccess/ArchiveSelector.php index 1c306f7a0..5e73f1cb8 100644 --- a/app/core/DataAccess/ArchiveSelector.php +++ b/app/core/DataAccess/ArchiveSelector.php @@ -45,7 +45,13 @@ private static function getModel() return new Model(); } - public static function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params, $minDatetimeArchiveProcessedUTC) + /** + * @param ArchiveProcessor\Parameters $params + * @param bool $minDatetimeArchiveProcessedUTC deprecated. will be removed in Matomo 4. + * @return array|bool + * @throws Exception + */ + public static function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params, $minDatetimeArchiveProcessedUTC = false) { $idSite = $params->getSite()->getId(); $period = $params->getPeriod()->getId(); @@ -55,11 +61,6 @@ public static function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params $numericTable = ArchiveTableCreator::getNumericTable($dateStart); - $minDatetimeIsoArchiveProcessedUTC = null; - if ($minDatetimeArchiveProcessedUTC) { - $minDatetimeIsoArchiveProcessedUTC = Date::factory($minDatetimeArchiveProcessedUTC)->getDatetime(); - } - $requestedPlugin = $params->getRequestedPlugin(); $segment = $params->getSegment(); $plugins = array("VisitsSummary", $requestedPlugin); @@ -67,7 +68,7 @@ public static function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params $doneFlags = Rules::getDoneFlags($plugins, $segment); $doneFlagValues = Rules::getSelectableDoneFlagValues(); - $results = self::getModel()->getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $doneFlagValues); + $results = self::getModel()->getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $doneFlags, $doneFlagValues); if (empty($results)) { return false; @@ -138,7 +139,6 @@ protected static function getMostRecentIdArchiveFromResults(Segment $segment, $r * @param array $periods * @param Segment $segment * @param array $plugins List of plugin names for which data is being requested. - * @param bool $canUseReaderDb if enabled, will try to read archive from reader * @return array Archive IDs are grouped by archive name and period range, ie, * array( * 'VisitsSummary.done' => array( @@ -147,7 +147,7 @@ protected static function getMostRecentIdArchiveFromResults(Segment $segment, $r * ) * @throws */ - public static function getArchiveIds($siteIds, $periods, $segment, $plugins, $canUseReaderDb) + public static function getArchiveIds($siteIds, $periods, $segment, $plugins) { if (empty($siteIds)) { throw new \Exception("Website IDs could not be read from the request, ie. idSite="); @@ -174,11 +174,7 @@ public static function getArchiveIds($siteIds, $periods, $segment, $plugins, $ca $monthToPeriods[$table][] = $period; } - if ($canUseReaderDb) { - $db = Db::getReader(); - } else { - $db = Db::get(); - } + $db = Db::get(); // for every month within the archive query, select from numeric table $result = array(); @@ -235,19 +231,14 @@ public static function getArchiveIds($siteIds, $periods, $segment, $plugins, $ca * @param string $archiveDataType The archive data type (either, 'blob' or 'numeric'). * @param int|null|string $idSubtable null if the root blob should be loaded, an integer if a subtable should be * loaded and 'all' if all subtables should be loaded. - * @param bool $canUseReaderDb if enabled, will try to read archive from reader DB * @return array *@throws Exception */ - public static function getArchiveData($archiveIds, $recordNames, $archiveDataType, $idSubtable, $canUseReaderDb) + public static function getArchiveData($archiveIds, $recordNames, $archiveDataType, $idSubtable) { $chunk = new Chunk(); - if ($canUseReaderDb) { - $db = Db::getReader(); - } else { - $db = Db::get(); - } + $db = Db::get(); // create the SQL to select archive data $loadAllSubtables = $idSubtable == Archive::ID_SUBTABLE_LOAD_ALL_SUBTABLES; diff --git a/app/core/DataAccess/ArchiveWriter.php b/app/core/DataAccess/ArchiveWriter.php index 5aa7cee1b..7a5bc464f 100644 --- a/app/core/DataAccess/ArchiveWriter.php +++ b/app/core/DataAccess/ArchiveWriter.php @@ -9,13 +9,11 @@ namespace Piwik\DataAccess; use Exception; -use Piwik\Archive; use Piwik\Archive\Chunk; use Piwik\ArchiveProcessor\Rules; use Piwik\ArchiveProcessor; use Piwik\Db; use Piwik\Db\BatchInsert; -use Piwik\Period; /** * This class is used to create a new Archive. @@ -37,11 +35,15 @@ class ArchiveWriter * @var int */ const DONE_ERROR = 2; + /** * Flag indicates the archive is over a period that is not finished, eg. the current day, current week, etc. * Archives flagged will be regularly purged from the DB. * + * This flag is deprecated, new archives should not be written as temporary. + * * @var int + * @deprecated */ const DONE_OK_TEMPORARY = 3; @@ -61,7 +63,20 @@ class ArchiveWriter 'name', 'value'); - public function __construct(ArchiveProcessor\Parameters $params, $isArchiveTemporary) + private $recordsToWriteSpool = array( + 'numeric' => array(), + 'blob' => array() + ); + + const MAX_SPOOL_SIZE = 50; + + /** + * ArchiveWriter constructor. + * @param ArchiveProcessor\Parameters $params + * @param bool $isArchiveTemporary Deprecated. Has no effect. + * @throws Exception + */ + public function __construct(ArchiveProcessor\Parameters $params, $isArchiveTemporary = false) { $this->idArchive = false; $this->idSite = $params->getSite()->getId(); @@ -70,7 +85,6 @@ public function __construct(ArchiveProcessor\Parameters $params, $isArchiveTempo $idSites = array($this->idSite); $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin()); - $this->isArchiveTemporary = $isArchiveTemporary; $this->dateStart = $this->period->getDateStart(); } @@ -86,11 +100,10 @@ public function __construct(ArchiveProcessor\Parameters $params, $isArchiveTempo public function insertBlobRecord($name, $values) { if (is_array($values)) { - $clean = array(); if (isset($values[0])) { // we always store the root table in a single blob for fast access - $clean[] = array($name, $this->compress($values[0])); + $this->insertRecord($name, $this->compress($values[0])); unset($values[0]); } @@ -99,16 +112,13 @@ public function insertBlobRecord($name, $values) $chunk = new Chunk(); $chunks = $chunk->moveArchiveBlobsIntoChunks($name, $values); foreach ($chunks as $index => $subtables) { - $clean[] = array($index, $this->compress(serialize($subtables))); + $this->insertRecord($index, $this->compress(serialize($subtables))); } } - - $this->insertBulkRecords($clean); - return; + } else { + $values = $this->compress($values); + $this->insertRecord($name, $values); } - - $values = $this->compress($values); - $this->insertRecord($name, $values); } public function getIdArchive() @@ -128,12 +138,12 @@ public function initNewArchive() public function finalizeArchive() { + $this->flushSpools(); + $numericTable = $this->getTableNumeric(); $idArchive = $this->getIdArchive(); - $this->getModel()->deletePreviousArchiveStatus($numericTable, $idArchive, $this->doneFlag); - - $this->logArchiveStatusAsFinal(); + $this->getModel()->updateArchiveStatus($numericTable, $idArchive, $this->doneFlag, self::DONE_OK); } protected function compress($data) @@ -163,29 +173,9 @@ protected function logArchiveStatusAsIncomplete() $this->insertRecord($this->doneFlag, self::DONE_ERROR); } - protected function logArchiveStatusAsFinal() + private function batchInsertSpool($valueType) { - $status = self::DONE_OK; - - if ($this->isArchiveTemporary) { - $status = self::DONE_OK_TEMPORARY; - } - - $this->insertRecord($this->doneFlag, $status); - } - - protected function insertBulkRecords($records) - { - // Using standard plain INSERT if there is only one record to insert - if ($DEBUG_DO_NOT_USE_BULK_INSERT = false - || count($records) == 1 - ) { - foreach ($records as $record) { - $this->insertRecord($record[0], $record[1]); - } - - return true; - } + $records = $this->recordsToWriteSpool[$valueType]; $bindSql = $this->getInsertRecordBind(); $values = array(); @@ -212,7 +202,12 @@ protected function insertBulkRecords($records) $tableName = $this->getTableNameToInsert($valueSeen); $fields = $this->getInsertFields(); - BatchInsert::tableInsertBatch($tableName, $fields, $values, $throwException = false, $charset = 'latin1'); + // For numeric records it's faster to do the insert directly; for blobs the data infile is better + if ($valueType == 'numeric') { + BatchInsert::tableInsertBatchSql($tableName, $fields, $values); + } else { + BatchInsert::tableInsertBatch($tableName, $fields, $values, $throwException = false, $charset = 'latin1'); + } return true; } @@ -231,15 +226,42 @@ public function insertRecord($name, $value) return false; } - $tableName = $this->getTableNameToInsert($value); - $fields = $this->getInsertFields(); - $record = $this->getInsertRecordBind(); + $valueType = $this->isRecordNumeric($value) ? 'numeric' : 'blob'; + $this->recordsToWriteSpool[$valueType][] = array( + 0 => $name, + 1 => $value + ); - $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value); + if (count($this->recordsToWriteSpool[$valueType]) >= self::MAX_SPOOL_SIZE) { + $this->flushSpool($valueType); + } return true; } + public function flushSpools() + { + $this->flushSpool('numeric'); + $this->flushSpool('blob'); + } + + private function flushSpool($valueType) + { + $numRecords = count($this->recordsToWriteSpool[$valueType]); + + if ($numRecords > 1) { + $this->batchInsertSpool($valueType); + } elseif ($numRecords == 1) { + list($name, $value) = $this->recordsToWriteSpool[$valueType][0]; + $tableName = $this->getTableNameToInsert($value); + $fields = $this->getInsertFields(); + $record = $this->getInsertRecordBind(); + + $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value); + } + $this->recordsToWriteSpool[$valueType] = array(); + } + protected function getInsertRecordBind() { return array($this->getIdArchive(), @@ -252,7 +274,7 @@ protected function getInsertRecordBind() protected function getTableNameToInsert($value) { - if (is_numeric($value)) { + if ($this->isRecordNumeric($value)) { return $this->getTableNumeric(); } @@ -273,4 +295,9 @@ protected function isRecordZero($value) { return ($value === '0' || $value === false || $value === 0 || $value === 0.0); } + + private function isRecordNumeric($value) + { + return is_numeric($value); + } } diff --git a/app/core/DataAccess/LogAggregator.php b/app/core/DataAccess/LogAggregator.php index ada5e405a..397d5a659 100644 --- a/app/core/DataAccess/LogAggregator.php +++ b/app/core/DataAccess/LogAggregator.php @@ -10,12 +10,18 @@ use Piwik\ArchiveProcessor\Parameters; use Piwik\Common; +use Piwik\Config; use Piwik\Container\StaticContainer; use Piwik\DataArray; use Piwik\Date; use Piwik\Db; +use Piwik\DbHelper; use Piwik\Metrics; use Piwik\Period; +use Piwik\Piwik; +use Piwik\Plugin\LogTablesProvider; +use Piwik\Segment; +use Piwik\Segment\SegmentExpression; use Piwik\Tracker\GoalManager; use Psr\Log\LoggerInterface; @@ -127,6 +133,8 @@ class LogAggregator const FIELDS_SEPARATOR = ", \n\t\t\t"; + const LOG_TABLE_SEGMENT_TEMPORARY_PREFIX = 'logtmpsegment'; + /** @var \Piwik\Date */ protected $dateStart; @@ -149,6 +157,15 @@ class LogAggregator */ private $logger; + /** + * @var bool + */ + private $isRootArchiveRequest; + + /** + * @var bool + */ + private $allowUsageSegmentCache = false; /** * Constructor. @@ -161,6 +178,7 @@ public function __construct(Parameters $params, LoggerInterface $logger = null) $this->dateEnd = $params->getDateTimeEnd(); $this->segment = $params->getSegment(); $this->sites = $params->getIdSites(); + $this->isRootArchiveRequest = $params->isRootArchiveRequest(); $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface'); } @@ -174,10 +192,173 @@ public function setQueryOriginHint($nameOfOrigiin) $this->queryOriginHint = $nameOfOrigiin; } + private function getSegmentTmpTableName() + { + $bind = $this->getGeneralQueryBindParams(); + return self::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX . md5(json_encode($bind) . $this->segment->getString()); + } + + public function cleanup() + { + if (!$this->segment->isEmpty() && $this->isSegmentCacheEnabled()) { + $segmentTable = $this->getSegmentTmpTableName(); + $segmentTable = Common::prefixTable($segmentTable); + + if ($this->doesSegmentTableExist($segmentTable)) { + // safety in case an older MySQL version is used that does not drop table at the end of the connection + // automatically. also helps us release disk space/memory earlier when multiple segments are archived + $this->getDb()->query('DROP TEMPORARY TABLE IF EXISTS ' . $segmentTable); + } + + $logTablesProvider = $this->getLogTableProvider(); + if ($logTablesProvider->getLogTable($segmentTable)) { + $logTablesProvider->setTempTable(null); // no longer available + } + } + } + + private function doesSegmentTableExist($segmentTablePrefixed) + { + try { + // using DROP TABLE IF EXISTS would not work on a DB reader if the table doesn't exist... + $this->getDb()->fetchOne('SELECT 1 FROM ' . $segmentTablePrefixed . ' LIMIT 1'); + $tableExists = true; + } catch (\Exception $e) { + $tableExists = false; + } + + return $tableExists; + } + + private function isSegmentCacheEnabled() + { + if (!$this->allowUsageSegmentCache) { + return false; + } + + $config = Config::getInstance(); + $general = $config->General; + return !empty($general['enable_segments_cache']); + } + + public function allowUsageSegmentCache() + { + $this->allowUsageSegmentCache = true; + } + + private function getLogTableProvider() + { + return StaticContainer::get(LogTablesProvider::class); + } + + private function createTemporaryTable($unprefixedSegmentTableName, $segmentSelectSql, $segmentSelectBind) + { + $table = Common::prefixTable($unprefixedSegmentTableName); + + if ($this->doesSegmentTableExist($table)) { + return; // no need to create the table, it was already created... better to have a select vs unneeded create table + } + + $engine = ''; + if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) { + $engine = 'ENGINE=MEMORY'; + } + $createTableSql = 'CREATE TEMPORARY TABLE ' . $table . ' (idvisit BIGINT(10) UNSIGNED NOT NULL) ' . $engine; + // we do not insert the data right away using create temporary table ... select ... + // to avoid metadata lock see eg https://www.percona.com/blog/2018/01/10/why-avoid-create-table-as-select-statement/ + + $readerDb = Db::getReader(); + try { + $readerDb->query($createTableSql); + } catch (\Exception $e) { + if ($readerDb->isErrNo($e, \Piwik\Updater\Migration\Db::ERROR_CODE_TABLE_EXISTS)) { + return; + } + throw $e; + } + + $transactionLevel = new Db\TransactionLevel($readerDb); + $canSetTransactionLevel = $transactionLevel->canLikelySetTransactionLevel(); + + if ($canSetTransactionLevel) { + // i know this could be shortened to one if or one line but I want to make sure this line where we + // set uncomitted is easily noticable in the code as it could be missed quite easily otherwise + // we set uncommitted so we don't make the INSERT INTO... SELECT... locking ... we do not want to lock + // eg the visits table + if (!$transactionLevel->setUncommitted()) { + $canSetTransactionLevel = false; + } + } + + if (!$canSetTransactionLevel) { + // transaction level doesn't work... we're instead executing the select individually and then insert the data + // this uses more memory but at least is not locking + $all = $readerDb->fetchAll($segmentSelectSql, $segmentSelectBind); + if (!empty($all)) { + // we're not using batchinsert since this would not support the reader DB. + $readerDb->query('INSERT INTO ' . $table . ' VALUES ('.implode('),(', array_column($all, 'idvisit')).')'); + } + return; + } + + $insertIntoStatement = 'INSERT INTO ' . $table . ' (idvisit) ' . $segmentSelectSql; + $readerDb->query($insertIntoStatement, $segmentSelectBind); + + $transactionLevel->restorePreviousStatus(); + } + public function generateQuery($select, $from, $where, $groupBy, $orderBy, $limit = 0, $offset = 0) { + $segment = $this->segment; $bind = $this->getGeneralQueryBindParams(); - $query = $this->segment->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy, $limit, $offset); + + if (!$this->segment->isEmpty() && $this->isSegmentCacheEnabled()) { + // here we create the TMP table and apply the segment including the datetime and the requested idsite + // at the end we generated query will no longer need to apply the datetime/idsite and segment + $segment = new Segment('', $this->sites); + + $segmentTable = $this->getSegmentTmpTableName(); + + $segmentWhere = $this->getWhereStatement('log_visit', 'visit_last_action_time'); + $segmentBind = $this->getGeneralQueryBindParams(); + + $logQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder'); + $forceGroupByBackup = $logQueryBuilder->getForcedInnerGroupBySubselect(); + $logQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT); + $segmentSql = $this->segment->getSelectQuery('distinct log_visit.idvisit as idvisit', 'log_visit', $segmentWhere, $segmentBind, 'log_visit.idvisit ASC'); + $logQueryBuilder->forceInnerGroupBySubselect($forceGroupByBackup); + + $this->createTemporaryTable($segmentTable, $segmentSql['sql'], $segmentSql['bind']); + + if (!is_array($from)) { + $from = array($segmentTable, $from); + } else { + array_unshift($from, $segmentTable); + } + + $logTablesProvider = $this->getLogTableProvider(); + $logTablesProvider->setTempTable(new LogTableTemporary($segmentTable)); + + foreach ($logTablesProvider->getAllLogTables() as $logTable) { + if ($logTable->getDateTimeColumn()) { + $whereTest = $this->getWhereStatement($logTable->getName(), $logTable->getDateTimeColumn()); + if (strpos($where, $whereTest) === 0) { + // we don't need to apply the where statement again as it would have been applied already + // in the temporary table... instead it should join the tables through the idvisit index + $where = ltrim(str_replace($whereTest, '', $where)); + if (stripos($where, 'and ') === 0) { + $where = substr($where, strlen('and ')); + } + $bind = array(); + break; + } + } + + } + + } + + $query = $segment->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy, $limit, $offset); $select = 'SELECT'; if ($this->queryOriginHint && is_array($query) && 0 === strpos(trim($query['sql']), $select)) { @@ -185,7 +366,7 @@ public function generateQuery($select, $from, $where, $groupBy, $orderBy, $limit $query['sql'] = 'SELECT /* ' . $this->queryOriginHint . ' */' . substr($query['sql'], strlen($select)); } - // Log on DEBUG level all SQL archiving queries + // Log on DEBUG level all SQL archiving queries $this->logger->debug($query['sql']); return $query; @@ -687,13 +868,21 @@ public function queryEcommerceItems($dimension) * * If a string is used for this parameter, the table alias is not * suffixed (since there is only one column). + * @param string $secondaryOrderBy A secondary order by clause for the ranking query * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of * {@link Piwik\RankingQuery::execute()}. Read [this](#queryEcommerceItems-result-set) * to see what aggregate data is calculated by the query. * @api */ - public function queryActionsByDimension($dimensions, $where = '', $additionalSelects = array(), $metrics = false, $rankingQuery = null, $joinLogActionOnColumn = false) - { + public function queryActionsByDimension( + $dimensions, + $where = '', + $additionalSelects = array(), + $metrics = false, + $rankingQuery = null, + $joinLogActionOnColumn = false, + $secondaryOrderBy = null + ) { $tableName = self::LOG_ACTIONS_TABLE; $availableMetrics = $this->getActionsMetricFields(); @@ -701,7 +890,6 @@ public function queryActionsByDimension($dimensions, $where = '', $additionalSel $from = array($tableName); $where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where); $groupBy = $this->getGroupByStatement($dimensions, $tableName); - $orderBy = false; if ($joinLogActionOnColumn !== false) { $multiJoin = is_array($joinLogActionOnColumn); @@ -727,8 +915,12 @@ public function queryActionsByDimension($dimensions, $where = '', $additionalSel } } + $orderBy = false; if ($rankingQuery) { $orderBy = '`' . Metrics::INDEX_NB_ACTIONS . '` DESC'; + if ($secondaryOrderBy) { + $orderBy .= ', ' . $secondaryOrderBy; + } } $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); diff --git a/app/core/DataAccess/LogQueryBuilder.php b/app/core/DataAccess/LogQueryBuilder.php index e2b130de4..9e6e57653 100644 --- a/app/core/DataAccess/LogQueryBuilder.php +++ b/app/core/DataAccess/LogQueryBuilder.php @@ -17,6 +17,8 @@ class LogQueryBuilder { + const FORCE_INNER_GROUP_BY_NO_SUBSELECT = '__##nosubquery##__'; + /** * @var LogTablesProvider */ @@ -42,6 +44,11 @@ public function forceInnerGroupBySubselect($innerGroupBy) $this->forcedInnerGroupBy = $innerGroupBy; } + public function getForcedInnerGroupBySubselect() + { + return $this->forcedInnerGroupBy; + } + public function getSelectQueryString(SegmentExpression $segmentExpression, $select, $from, $where, $bind, $groupBy, $orderBy, $limitAndOffset) { @@ -71,7 +78,11 @@ public function getSelectQueryString(SegmentExpression $segmentExpression, $sele && strpos($from, 'log_link_visit_action') !== false); if (!empty($this->forcedInnerGroupBy)) { - $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables, $this->forcedInnerGroupBy); + if ($this->forcedInnerGroupBy === self::FORCE_INNER_GROUP_BY_NO_SUBSELECT) { + $sql = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset); + } else { + $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables, $this->forcedInnerGroupBy); + } } elseif ($useSpecialConversionGroupBy) { $innerGroupBy = "CONCAT(log_conversion.idvisit, '_' , log_conversion.idgoal, '_', log_conversion.buster)"; $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables, $innerGroupBy); @@ -89,7 +100,7 @@ public function getSelectQueryString(SegmentExpression $segmentExpression, $sele private function getKnownTables() { $names = array(); - foreach ($this->logTableProvider->getAllLogTables() as $logTable) { + foreach ($this->logTableProvider->getAllLogTablesWithTemporary() as $logTable) { $names[] = $logTable->getName(); } return $names; diff --git a/app/core/DataAccess/LogQueryBuilder/JoinGenerator.php b/app/core/DataAccess/LogQueryBuilder/JoinGenerator.php index 5cc5d48f6..08f6ebdc2 100644 --- a/app/core/DataAccess/LogQueryBuilder/JoinGenerator.php +++ b/app/core/DataAccess/LogQueryBuilder/JoinGenerator.php @@ -11,6 +11,7 @@ use Exception; use Piwik\Common; +use Piwik\DataAccess\LogAggregator; use Piwik\Tracker\LogTable; class JoinGenerator @@ -190,8 +191,20 @@ public function generate() continue; } + $joinName = 'LEFT JOIN'; + if ($i > 0 + && $this->tables[$i - 1] + && is_string($this->tables[$i - 1]) + && strpos($this->tables[$i - 1], LogAggregator::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX) === 0) { + $joinName = 'INNER JOIN'; + // when we archive a segment there will be eg `logtmpsegment$HASH` as first table. + // then we join log_conversion for example... if we didn't use INNER JOIN we would as a result + // get rows for visits even when they didn't have a conversion. Instead we only want to find rows + // that have an entry in both tables when doing eg + // logtmpsegment57cd546b7203d68a41027547c4abe1a2.idvisit = log_conversion.idvisit + } // the join sql the default way - $this->joinString .= " LEFT JOIN $tableSql ON " . $join; + $this->joinString .= " $joinName $tableSql ON " . $join; } $availableLogTables[$table] = $logTable; diff --git a/app/core/DataAccess/LogQueryBuilder/JoinTables.php b/app/core/DataAccess/LogQueryBuilder/JoinTables.php index a5b1f34d8..efce94d3e 100644 --- a/app/core/DataAccess/LogQueryBuilder/JoinTables.php +++ b/app/core/DataAccess/LogQueryBuilder/JoinTables.php @@ -10,6 +10,7 @@ namespace Piwik\DataAccess\LogQueryBuilder; use Exception; +use Piwik\DataAccess\LogAggregator; use Piwik\Plugin\LogTablesProvider; class JoinTables extends \ArrayObject @@ -148,10 +149,15 @@ public function sort() // the first entry is always the FROM table $firstTable = array_shift($tables); + $sorted = [$firstTable]; + + if (strpos($firstTable, LogAggregator::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX) === 0) { + // the first table might be a temporary segment table in which case we need to keep the next one as well + $sorted[] = array_shift($tables); + } $dependencies = $this->parseDependencies($tables); - $sorted = [$firstTable]; $this->visitTableListDfs($tables, $dependencies, function ($tableInfo) use (&$sorted) { $sorted[] = $tableInfo; }); diff --git a/app/core/DataAccess/LogTableTemporary.php b/app/core/DataAccess/LogTableTemporary.php new file mode 100644 index 000000000..8be027c11 --- /dev/null +++ b/app/core/DataAccess/LogTableTemporary.php @@ -0,0 +1,47 @@ +tableName = $name; + } + + public function setName($name) + { + $this->tableName = $name; + } + + public function getName() + { + return $this->tableName; + } + + public function getIdColumn() + { + return 'idvist'; + } + + public function getColumnToJoinOnIdVisit() + { + return 'idvisit'; + } + public function getPrimaryKey() + { + return array('idvisit'); + } +} \ No newline at end of file diff --git a/app/core/DataAccess/Model.php b/app/core/DataAccess/Model.php index 8ab8f5cff..6b0d5e264 100644 --- a/app/core/DataAccess/Model.php +++ b/app/core/DataAccess/Model.php @@ -115,10 +115,21 @@ public function updateArchiveAsInvalidated($archiveTable, $idSites, $datesByPeri foreach ($datesByPeriodType as $periodType => $dates) { $dateConditions = array(); - foreach ($dates as $date) { - $dateConditions[] = "(date1 <= ? AND ? <= date2)"; - $bind[] = $date; - $bind[] = $date; + if ($periodType == Period\Range::PERIOD_ID) { + foreach ($dates as $date) { + // Ranges in the DB match if their date2 is after the start of the search range and date1 is before the end + // e.g. search range is 2019-01-01 to 2019-01-31 + // date2 >= startdate -> Ranges with date2 < 2019-01-01 (ended before 1 January) and are excluded + // date1 <= endate -> Ranges with date1 > 2019-01-31 (started after 31 January) and are excluded + $dateConditions[] = "(date2 >= ? AND date1 <= ?)"; + $bind = array_merge($bind, explode(',', $date)); + } + } else { + foreach ($dates as $date) { + $dateConditions[] = "(date1 <= ? AND ? <= date2)"; + $bind[] = $date; + $bind[] = $date; + } } $dateConditionsSql = implode(" OR ", $dateConditions); @@ -149,7 +160,6 @@ public function updateArchiveAsInvalidated($archiveTable, $idSites, $datesByPeri return Db::query($sql, $bind); } - public function getTemporaryArchivesOlderThan($archiveTable, $purgeArchivesOlderThan) { $query = "SELECT idarchive FROM " . $archiveTable . " @@ -186,13 +196,15 @@ public function deleteArchivesWithPeriod($numericTable, $blobTable, $period, $da public function deleteArchiveIds($numericTable, $blobTable, $idsToDelete) { $idsToDelete = array_values($idsToDelete); - $query = "DELETE FROM %s WHERE idarchive IN (" . Common::getSqlStringFieldsArray($idsToDelete) . ")"; - $queryObj = Db::query(sprintf($query, $numericTable), $idsToDelete); + $idsToDelete = array_map('intval', $idsToDelete); + $query = "DELETE FROM %s WHERE idarchive IN (" . implode(',', $idsToDelete) . ")"; + + $queryObj = Db::query(sprintf($query, $numericTable), array()); $deletedRows = $queryObj->rowCount(); try { - $queryObj = Db::query(sprintf($query, $blobTable), $idsToDelete); + $queryObj = Db::query(sprintf($query, $blobTable), array()); $deletedRows += $queryObj->rowCount(); } catch (Exception $e) { // Individual blob tables could be missing @@ -205,7 +217,7 @@ public function deleteArchiveIds($numericTable, $blobTable, $idsToDelete) return $deletedRows; } - public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $doneFlagValues) + public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $doneFlags, $doneFlagValues) { $bindSQL = array($idSite, $dateStartIso, @@ -213,12 +225,6 @@ public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStar $period, ); - $timeStampWhere = ''; - if ($minDatetimeIsoArchiveProcessedUTC) { - $timeStampWhere = " AND ts_archived >= ? "; - $bindSQL[] = $minDatetimeIsoArchiveProcessedUTC; - } - $sqlWhereArchiveName = self::getNameCondition($doneFlags, $doneFlagValues); $sqlQuery = "SELECT idarchive, value, name, date1 as startDate FROM $numericTable @@ -229,7 +235,6 @@ public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStar AND ( ($sqlWhereArchiveName) OR name = '" . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . "' OR name = '" . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "') - $timeStampWhere ORDER BY idarchive DESC"; $results = Db::fetchAll($sqlQuery, $bindSQL); @@ -279,25 +284,11 @@ public function allocateNewArchiveId($numericTable) return $idarchive; } - public function deletePreviousArchiveStatus($numericTable, $archiveId, $doneFlag) + public function updateArchiveStatus($numericTable, $archiveId, $doneFlag, $value) { - $tableWithoutLeadingPrefix = $numericTable; - $lenNumericTableWithoutPrefix = strlen('archive_numeric_MM_YYYY'); - - if (strlen($numericTable) >= $lenNumericTableWithoutPrefix) { - $tableWithoutLeadingPrefix = substr($numericTable, strlen($numericTable) - $lenNumericTableWithoutPrefix); - // we need to make sure lock name is less than 64 characters see https://github.com/piwik/piwik/issues/9131 - } - $dbLockName = "rmPrevArchiveStatus.$tableWithoutLeadingPrefix.$archiveId"; - - // without advisory lock here, the DELETE would acquire Exclusive Lock - $this->acquireArchiveTableLock($dbLockName); - - Db::query("DELETE FROM $numericTable WHERE idarchive = ? AND (name = '" . $doneFlag . "')", - array($archiveId) + Db::query("UPDATE $numericTable SET `value` = ? WHERE idarchive = ? and `name` = ?", + array($value, $archiveId, $doneFlag) ); - - $this->releaseArchiveTableLock($dbLockName); } public function insertRecord($tableName, $fields, $record, $name, $value) @@ -340,14 +331,31 @@ public function getSitesWithInvalidatedArchive($numericTable) * @param string $oldestToKeep Datetime string * @return array of IDs */ - public function getArchiveIdsForDeletedSites($archiveTableName, $oldestToKeep) + public function getArchiveIdsForDeletedSites($archiveTableName) { - $sql = "SELECT DISTINCT idarchive FROM " . $archiveTableName . " a " - . " LEFT JOIN " . Common::prefixTable('site') . " s USING (idsite)" - . " WHERE s.idsite IS NULL" - . " AND ts_archived < ?"; + $sql = "SELECT DISTINCT idsite FROM " . $archiveTableName; + $rows = Db::getReader()->fetchAll($sql, array()); - $rows = Db::fetchAll($sql, array($oldestToKeep)); + if (empty($rows)) { + return array(); // nothing to delete + } + + $idSitesUsed = array_column($rows, 'idsite'); + + $model = new \Piwik\Plugins\SitesManager\Model(); + $idSitesExisting = $model->getSitesId(); + + $deletedSites = array_diff($idSitesUsed, $idSitesExisting); + + if (empty($deletedSites)) { + return array(); + } + $deletedSites = array_values($deletedSites); + $deletedSites = array_map('intval', $deletedSites); + + $sql = "SELECT DISTINCT idarchive FROM " . $archiveTableName . " WHERE idsite IN (".implode(',',$deletedSites).")"; + + $rows = Db::getReader()->fetchAll($sql, array()); return array_column($rows, 'idarchive'); } @@ -356,53 +364,54 @@ public function getArchiveIdsForDeletedSites($archiveTableName, $oldestToKeep) * Get a list of IDs of archives with segments that no longer exist in the DB. Excludes temporary archives that * may still be in use, as specified by the $oldestToKeep passed in. * @param string $archiveTableName - * @param array $segmentHashesById Whitelist of existing segments, indexed by site ID + * @param array $segments List of segments to match against * @param string $oldestToKeep Datetime string * @return array With keys idarchive, name, idsite */ - public function getArchiveIdsForDeletedSegments($archiveTableName, array $segmentHashesById, $oldestToKeep) + public function getArchiveIdsForSegments($archiveTableName, array $segments, $oldestToKeep) { - if (empty($segmentHashesById)) { - return array(); - } - $validSegmentClauses = []; - - foreach ($segmentHashesById as $idSite => $segments) { - // segments are md5 hashes and such not a problem re sql injection. for performance etc we don't want to use - // bound parameters for the query - foreach ($segments as $segment) { - if (!preg_match('/^[a-z0-9A-Z]+$/', $segment)) { - throw new Exception($segment . ' expected to be an md5 hash'); - } + $segmentClauses = []; + foreach ($segments as $segment) { + if (!empty($segment['definition'])) { + $segmentClauses[] = $this->getDeletedSegmentWhereClause($segment); } + } - // Special case as idsite=0 means the segments are not site-specific - if ($idSite === 0) { - foreach ($segments as $segmentHash) { - $validSegmentClauses[] = '(name LIKE "done' . $segmentHash . '%")'; - } - continue; - } - - $idSite = (int)$idSite; - - // Vanilla case - segments that are valid for a single site only - $sql = '(idsite = ' . $idSite . ' AND ('; - $sql .= 'name LIKE "done' . implode('%" OR name LIKE "done', $segments) . '%"'; - $sql .= '))'; - $validSegmentClauses[] = $sql; + if (empty($segmentClauses)) { + return array(); } - $isValidSegmentSql = implode(' OR ', $validSegmentClauses); + $segmentClauses = implode(' OR ', $segmentClauses); $sql = 'SELECT idarchive FROM ' . $archiveTableName - . ' WHERE name LIKE "done%" AND name != "done"' - . ' AND ts_archived < ?' - . ' AND NOT (' . $isValidSegmentSql . ')'; + . ' WHERE ts_archived < ?' + . ' AND (' . $segmentClauses . ')'; $rows = Db::fetchAll($sql, array($oldestToKeep)); - return array_map(function($row) { return $row['idarchive']; }, $rows); + return array_column($rows, 'idarchive'); + } + + private function getDeletedSegmentWhereClause(array $segment) + { + $idSite = (int)$segment['enable_only_idsite']; + $segmentHash = Segment::getSegmentHash($segment['definition']); + // Valid segment hashes are md5 strings - just confirm that it is so it's safe for SQL injection + if (!ctype_xdigit($segmentHash)) { + throw new Exception($segment . ' expected to be an md5 hash'); + } + + $nameClause = 'name LIKE "done' . $segmentHash . '%"'; + $idSiteClause = ''; + if ($idSite > 0) { + $idSiteClause = ' AND idsite = ' . $idSite; + } elseif (! empty($segment['idsites_to_preserve'])) { + // A segment for all sites was deleted, but there are segments for a single site with the same definition + $idSitesToPreserve = array_map('intval', $segment['idsites_to_preserve']); + $idSiteClause = ' AND idsite NOT IN (' . implode(',', $idSitesToPreserve) . ')'; + } + + return "($nameClause $idSiteClause)"; } /** @@ -417,15 +426,4 @@ private static function getNameCondition($doneFlags, $possibleValues) return "((name IN ($allDoneFlags)) AND (value IN (" . implode(',', $possibleValues) . ")))"; } - protected function acquireArchiveTableLock($dbLockName) - { - if (Db::getDbLock($dbLockName, $maxRetries = 30) === false) { - throw new Exception("Cannot get named lock $dbLockName."); - } - } - - protected function releaseArchiveTableLock($dbLockName) - { - Db::releaseDbLock($dbLockName); - } } diff --git a/app/core/DataAccess/RawLogDao.php b/app/core/DataAccess/RawLogDao.php index 5fd94f122..a3d7f0ab5 100644 --- a/app/core/DataAccess/RawLogDao.php +++ b/app/core/DataAccess/RawLogDao.php @@ -103,18 +103,41 @@ public function countVisitsWithDatesLimit($from, $to) * ``` * @param int $iterationStep The number of rows to query at a time. * @param callable $callback The callback that processes each chunk of rows. + * @param string $willDelete Set to true if you will make sure to delete all rows that were fetched. If you are in + * doubt and not sure if to set true or false, use "false". Setting it to true will + * enable an internal performance improvement but it can result in an endless loop if not + * used properly. */ - public function forAllLogs($logTable, $fields, $conditions, $iterationStep, $callback) + public function forAllLogs($logTable, $fields, $conditions, $iterationStep, $callback, $willDelete) { - $idField = $this->getIdFieldForLogTable($logTable); + $lastId = 0; + + if ($willDelete) { + // we don't want to look at eg idvisit so the query will be mostly index covered as the + // "where idvisit > 0 ... ORDER BY idvisit ASC" will be gone... meaning we don't need to look at a huge range + // of visits... + $idField = null; + $bindFunction = function ($bind, $lastId) { + return $bind; + }; + } else { + // when we are not deleting, we need to ensure to iterate over each visitor step by step... meaning we + // need to remember which visit we have already looked at and which one not. Therefore we need to apply + // "where idvisit > $lastId" in the query and "order by idvisit ASC" + $idField = $this->getIdFieldForLogTable($logTable); + $bindFunction = function ($bind, $lastId) { + return array_merge(array($lastId), $bind); + }; + } + list($query, $bind) = $this->createLogIterationQuery($logTable, $idField, $fields, $conditions, $iterationStep); - $lastId = 0; do { - $rows = Db::fetchAll($query, array_merge(array($lastId), $bind)); + $rows = Db::fetchAll($query, call_user_func($bindFunction, $bind, $lastId)); if (!empty($rows)) { - $lastId = $rows[count($rows) - 1][$idField]; - + if ($idField) { + $lastId = $rows[count($rows) - 1][$idField]; + } $callback($rows); } } while (count($rows) == $iterationStep); @@ -250,23 +273,34 @@ private function createLogIterationQuery($logTable, $idField, $fields, $conditio { $bind = array(); - $sql = "SELECT " . implode(', ', $fields) . " FROM `" . Common::prefixTable($logTable) . "` WHERE $idField > ?"; + $sql = "SELECT " . implode(', ', $fields) . " FROM `" . Common::prefixTable($logTable) . "` WHERE "; + + $parts = array(); + + if ($idField) { + $parts[] = "$idField > ?"; + } foreach ($conditions as $condition) { list($column, $operator, $value) = $condition; if (is_array($value)) { - $sql .= " AND $column IN (" . Common::getSqlStringFieldsArray($value) . ")"; + $parts[] = "$column IN (" . Common::getSqlStringFieldsArray($value) . ")"; $bind = array_merge($bind, $value); } else { - $sql .= " AND $column $operator ?"; + $parts[]= "$column $operator ?"; $bind[] = $value; } } + $sql .= implode(' AND ', $parts); + + if ($idField) { + $sql .= " ORDER BY $idField ASC"; + } - $sql .= " ORDER BY $idField ASC LIMIT " . (int)$iterationStep; + $sql .= " LIMIT " . (int)$iterationStep; return array($sql, $bind); } diff --git a/app/core/DataTable.php b/app/core/DataTable.php index 727cc5dd0..9065afcef 100644 --- a/app/core/DataTable.php +++ b/app/core/DataTable.php @@ -200,9 +200,17 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess /** The ID of the Summary Row. */ const ID_SUMMARY_ROW = -1; + /** + * The ID of the special metadata row. This row only exists in the serialized row data and stores the datatable metadata. + * + * This allows us to save datatable metadata in archive data. + */ + const ID_ARCHIVED_METADATA_ROW = -3; + /** The original label of the Summary Row. */ const LABEL_SUMMARY_ROW = -1; const LABEL_TOTALS_ROW = -2; + const LABEL_ARCHIVED_METADATA_ROW = '__datatable_metadata__'; /** * Name for metadata that contains extra {@link Piwik\Plugin\ProcessedMetric}s for a DataTable. @@ -664,6 +672,12 @@ public function getRowFromLabel($label) ) { return $this->summaryRow; } + if (empty($rowId) + && !empty($this->totalsRow) + && $label == $this->totalsRow->getColumn('label') + ) { + return $this->totalsRow; + } if ($rowId instanceof Row) { return $rowId; } @@ -1291,6 +1305,15 @@ public function getSerialized($maximumRowsInDataTable = null, $subtableId = 0; throw new Exception("Maximum recursion level of " . self::$maximumDepthLevelAllowed . " reached. Maybe you have set a DataTable\Row with an associated DataTable belonging already to one of its parent tables?"); } + + // gather metadata before filters are called, so their metadata is not stored in serialized form + $metadata = $this->getAllTableMetadata(); + foreach ($metadata as $key => $value) { + if (!is_scalar($value) && !is_string($value)) { + unset($metadata[$key]); + } + } + if (!is_null($maximumRowsInDataTable)) { $this->filter('Truncate', array($maximumRowsInDataTable - 1, @@ -1341,6 +1364,16 @@ public function getSerialized($maximumRowsInDataTable = null, $rows[self::ID_SUMMARY_ROW] = $this->summaryRow->export(); } + if (!empty($metadata)) { + $metadataRow = new Row(); + $metadataRow->setColumns($metadata); + + // set the label so the row will be indexed correctly internally + $metadataRow->setColumn('label', self::LABEL_ARCHIVED_METADATA_ROW); + + $rows[self::ID_ARCHIVED_METADATA_ROW] = $metadataRow->export(); + } + $aSerializedDataTable[$forcedId] = serialize($rows); unset($rows); @@ -1402,6 +1435,13 @@ public function addRowsFromSerializedArray($serialized) unset($rows[self::ID_SUMMARY_ROW]); } + if (array_key_exists(self::ID_ARCHIVED_METADATA_ROW, $rows)) { + $metadata = $rows[self::ID_ARCHIVED_METADATA_ROW][Row::COLUMNS]; + unset($metadata['label']); + $this->setAllTableMetadata($metadata); + unset($rows[self::ID_ARCHIVED_METADATA_ROW]); + } + foreach ($rows as $id => $row) { if (isset($row->c)) { $this->addRow(new Row($row->c)); // Pre Piwik 2.13 @@ -1573,7 +1613,12 @@ public static function makeFromIndexedArray($array, $subtablePerLabel = null) if (isset($subtablePerLabel[$label])) { $cleanRow[Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label]; } - $table->addRow(new Row($cleanRow)); + + if ($label === RankingQuery::LABEL_SUMMARY_ROW) { + $table->addSummaryRow(new Row($cleanRow)); + } else { + $table->addRow(new Row($cleanRow)); + } } return $table; } diff --git a/app/core/DataTable/Filter/ReplaceColumnNames.php b/app/core/DataTable/Filter/ReplaceColumnNames.php index 334057304..f996f48e2 100644 --- a/app/core/DataTable/Filter/ReplaceColumnNames.php +++ b/app/core/DataTable/Filter/ReplaceColumnNames.php @@ -56,7 +56,7 @@ class ReplaceColumnNames extends BaseFilter public function __construct($table, $mappingToApply = null) { parent::__construct($table); - $this->mappingToApply = Metrics::$mappingFromIdToName; + $this->mappingToApply = Metrics::getMappingFromIdToName(); if (!is_null($mappingToApply)) { $this->mappingToApply = $mappingToApply; } @@ -81,7 +81,14 @@ public function filter($table) */ protected function filterTable($table) { - foreach ($table->getRows() as $row) { + $rows = $table->getRows(); + + $totalRow = $table->getTotalsRow(); + if ($totalRow) { + $rows[] = $totalRow; + } + + foreach ($rows as $row) { $newColumns = $this->getRenamedColumns($row->getColumns()); $row->setColumns($newColumns); $this->filterSubTable($row); diff --git a/app/core/DataTable/Filter/SafeDecodeLabel.php b/app/core/DataTable/Filter/SafeDecodeLabel.php index af851ab4b..1a2f26bac 100644 --- a/app/core/DataTable/Filter/SafeDecodeLabel.php +++ b/app/core/DataTable/Filter/SafeDecodeLabel.php @@ -17,6 +17,8 @@ */ class SafeDecodeLabel extends BaseFilter { + const APPLIED_METADATA_NAME = 'SafeDecodeLabelApplied'; + private $columnToDecode; /** @@ -59,6 +61,10 @@ public static function decodeLabelSafe($value) */ public function filter($table) { + if ($table->getMetadata(self::APPLIED_METADATA_NAME)) { + return; + } + foreach ($table->getRows() as $row) { $value = $row->getColumn($this->columnToDecode); if ($value !== false) { diff --git a/app/core/DataTable/Renderer/Console.php b/app/core/DataTable/Renderer/Console.php index c16bcfd6d..57b21f534 100644 --- a/app/core/DataTable/Renderer/Console.php +++ b/app/core/DataTable/Renderer/Console.php @@ -148,6 +148,8 @@ protected function renderTable($table, $prefix = "") foreach ($metadataIn as $name => $value) { if (is_object($value) && !method_exists( $value, '__toString' )) { $value = 'Object [' . get_class($value) . ']'; + } elseif (is_array($value)) { + $value = 'Array ' . json_encode($value); } $output .= $prefix . $prefix . "$name => $value"; } diff --git a/app/core/DataTable/Renderer/Csv.php b/app/core/DataTable/Renderer/Csv.php index dc0723a53..ada62e67c 100644 --- a/app/core/DataTable/Renderer/Csv.php +++ b/app/core/DataTable/Renderer/Csv.php @@ -423,7 +423,9 @@ private function makeArrayFromDataTable($table, &$allColumns) $name = 'metadata_' . $name; } - if (is_array($value)) { + if (is_array($value) + || is_object($value) + ) { if (!in_array($name, $this->unsupportedColumns)) { $this->unsupportedColumns[] = $name; } diff --git a/app/core/DataTable/Renderer/Html.php b/app/core/DataTable/Renderer/Html.php index b42076cad..424c5ba6a 100644 --- a/app/core/DataTable/Renderer/Html.php +++ b/app/core/DataTable/Renderer/Html.php @@ -10,6 +10,7 @@ use Exception; use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; use Piwik\DataTable\Renderer; /** @@ -51,7 +52,7 @@ public function render() /** * Computes the output for the given data table * - * @param DataTable $table + * @param DataTableInterface $table * @return string */ protected function renderTable($table) @@ -119,6 +120,8 @@ protected function buildTableStructure($table, $columnToAdd = null, $valueToAdd $value = "'$value'"; } else if (is_array($value)) { $value = var_export($value, true); + } else if ($value instanceof DataTable\DataTableInterface) { + $value = $this->renderTable($value); } $metadata[] = "'$name' => $value"; } diff --git a/app/core/DataTable/Renderer/Json.php b/app/core/DataTable/Renderer/Json.php index 81a03f3e4..28d57eb6f 100644 --- a/app/core/DataTable/Renderer/Json.php +++ b/app/core/DataTable/Renderer/Json.php @@ -62,6 +62,9 @@ protected function renderTable($table) $array = array('value' => $array); } + // convert datatable column/metadata values + $this->convertDataTableColumnMetadataValues($array); + // decode all entities $callback = function (&$value, $key) { if (is_string($value)) { @@ -117,4 +120,18 @@ private function convertDataTableToArray($table) return $array; } + + private function convertDataTableColumnMetadataValues(&$table) + { + if (empty($table)) { + return; + } + + array_walk_recursive($table, function (&$value, $key) { + if ($value instanceof DataTable) { + $value = $this->convertDataTableToArray($value); + $this->convertDataTableColumnMetadataValues($value); + } + }); + } } diff --git a/app/core/DataTable/Renderer/Php.php b/app/core/DataTable/Renderer/Php.php index 65229d676..5bcb86195 100644 --- a/app/core/DataTable/Renderer/Php.php +++ b/app/core/DataTable/Renderer/Php.php @@ -122,9 +122,14 @@ public function flatRender($dataTable = null) } elseif ($dataTable instanceof Simple) { $flatArray = $this->renderSimpleTable($dataTable); + reset($flatArray); + $firstKey = key($flatArray); + // if we return only one numeric value then we print out the result in a simple tag // keep it simple! - if (count($flatArray) == 1) { + if (count($flatArray) == 1 + && $firstKey !== DataTable\Row::COMPARISONS_METADATA_NAME + ) { $flatArray = current($flatArray); } } // A normal DataTable needs to be handled specifically @@ -206,6 +211,10 @@ protected function renderTable($table) $newRow['issummaryrow'] = true; } + if (isset($newRow['metadata'][DataTable\Row::COMPARISONS_METADATA_NAME])) { + $newRow['metadata'][DataTable\Row::COMPARISONS_METADATA_NAME] = $row->getComparisons(); + } + $subTable = $row->getSubtable(); if ($this->isRenderSubtables() && $subTable @@ -245,6 +254,12 @@ protected function renderSimpleTable($table) foreach ($row->getColumns() as $columnName => $columnValue) { $array[$columnName] = $columnValue; } + + $comparisons = $row->getComparisons(); + if (!empty($comparisons)) { + $array[DataTable\Row::COMPARISONS_METADATA_NAME] = $comparisons; + } + return $array; } } diff --git a/app/core/DataTable/Renderer/Rss.php b/app/core/DataTable/Renderer/Rss.php index 2b001725d..f90dbc255 100644 --- a/app/core/DataTable/Renderer/Rss.php +++ b/app/core/DataTable/Renderer/Rss.php @@ -147,7 +147,9 @@ protected function renderDataTable($table) foreach ($row->getColumns() as $column => $value) { // for example, goals data is array: not supported in export RSS // in the future we shall reuse ViewDataTable for html exports in RSS anyway - if (is_array($value)) { + if (is_array($value) + || is_object($value) + ) { continue; } $allColumns[$column] = true; diff --git a/app/core/DataTable/Renderer/Xml.php b/app/core/DataTable/Renderer/Xml.php index b13eae78c..939b3a897 100644 --- a/app/core/DataTable/Renderer/Xml.php +++ b/app/core/DataTable/Renderer/Xml.php @@ -233,9 +233,16 @@ protected function renderDataTableMap($table, $array, $prefixLines = "") foreach ($array as $valueAttribute => $value) { if (empty($value)) { $xml .= $prefixLines . "\t\n"; - } elseif ($value instanceof Map) { - $out = $this->renderTable($value, true); + } elseif ($value instanceof DataTable\DataTableInterface) { //TODO somehow this code is not tested, cover this case + $out = $this->renderTable($value, true); + $xml .= "\t\n$out\n"; + } else if (is_array($value)) { + if (!is_array(reset($value))) { + $out = $this->renderDataTableSimple($value); + } else { + $out = $this->renderDataTable($value); + } $xml .= "\t\n$out\n"; } else { $xml .= $prefixLines . "\t" . self::formatValueXml($value) . "\n"; @@ -265,7 +272,11 @@ protected function renderDataTableMap($table, $array, $prefixLines = "") $xml .= $prefixLines . "\t\n"; } else { if (is_array($dataTableSimple)) { - $dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t"; + if (!is_array(reset($dataTableSimple))) { + $dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t"; + } else { + $dataTableSimple = "\n" . $this->renderDataTable($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t"; + } } $xml .= $prefixLines . "\t" . $dataTableSimple . "\n"; } @@ -369,7 +380,15 @@ protected function renderDataTable($array, $prefixLine = "") $out .= "\n"; foreach ($row as $name => $value) { // handle the recursive dataTable case by XML outputting the recursive table - if (is_array($value)) { + if ($value instanceof DataTable) { + $value = $this->getArrayFromDataTable($value); + if ($value instanceof Simple) { + $value = "\n" . $this->renderDataTableSimple($value, $prefixLine . "\t\t"); + } else { + $value = "\n" . $this->renderDataTable($value, $prefixLine . "\t\t"); + } + $value .= $prefixLine . "\t\t"; + } else if (is_array($value)) { if (is_array(reset($value))) { $value = "\n" . $this->renderDataTable($value, $prefixLine . "\t\t"); } else { @@ -414,8 +433,16 @@ protected function renderDataTableSimple($array, $prefixLine = "") foreach ($array as $keyName => $value) { $xmlValue = self::formatValueXml($value); list($tagStart, $tagEnd) = $this->getTagStartAndEndFor($keyName, $columnsHaveInvalidChars); - if (strlen($xmlValue) == 0) { + if (is_string($xmlValue) && strlen($xmlValue) == 0) { $out .= $prefixLine . "\t<$tagStart />\n"; + } else if ($value instanceof DataTable || is_array($value)) { + $arrayValue = $this->getArrayFromDataTable($value); + if (!is_array(reset($arrayValue))) { + $xmlTable = $this->renderDataTableSimple($arrayValue, $prefixLine . "\t"); + } else { + $xmlTable = $this->renderDataTable($arrayValue, $prefixLine . "\t"); + } + $out .= $prefixLine . "\t<$tagStart>\n" . $xmlTable . $prefixLine . "\t\n"; } else { $out .= $prefixLine . "\t<$tagStart>" . $xmlValue . "\n"; } diff --git a/app/core/DataTable/Row.php b/app/core/DataTable/Row.php index 4c3ecc623..f881f1cbf 100644 --- a/app/core/DataTable/Row.php +++ b/app/core/DataTable/Row.php @@ -23,6 +23,8 @@ */ class Row extends \ArrayObject { + const COMPARISONS_METADATA_NAME = 'comparisons'; + /** * List of columns that cannot be summed. An associative array for speed. * @@ -87,9 +89,11 @@ public function __construct($row = array()) */ public function export() { + $metadataToPersist = $this->metadata; + unset($metadataToPersist[self::COMPARISONS_METADATA_NAME]); return array( self::COLUMNS => $this->getArrayCopy(), - self::METADATA => $this->metadata, + self::METADATA => $metadataToPersist, self::DATATABLE_ASSOCIATED => $this->subtableId, ); } @@ -590,6 +594,30 @@ public function isSummaryRow() return $this->getColumn('label') === DataTable::LABEL_SUMMARY_ROW; } + /** + * Returns the associated comparisons DataTable, if any. + * + * @return DataTable|null + */ + public function getComparisons() + { + $dataTableId = $this->getMetadata(self::COMPARISONS_METADATA_NAME); + if (empty($dataTableId)) { + return null; + } + return Manager::getInstance()->getTable($dataTableId); + } + + /** + * Associates the supplied table with this row as the comparisons table. + * + * @param DataTable $table + */ + public function setComparisons(DataTable $table) + { + $this->setMetadata(self::COMPARISONS_METADATA_NAME, $table->getId()); + } + /** * Helper function: sums 2 values * diff --git a/app/core/Db.php b/app/core/Db.php index 9af8ec2c6..af2563869 100644 --- a/app/core/Db.php +++ b/app/core/Db.php @@ -180,6 +180,7 @@ public static function createReaderDatabaseObject($dbConfig = null) $dbConfig['schema'] = $masterDbConfig['schema']; $dbConfig['type'] = $masterDbConfig['type']; $dbConfig['tables_prefix'] = $masterDbConfig['tables_prefix']; + $dbConfig['charset'] = $masterDbConfig['charset']; $db = @Adapter::factory($dbConfig['adapter'], $dbConfig); diff --git a/app/core/Db/BatchInsert.php b/app/core/Db/BatchInsert.php index b2693d7e9..f2b146e4e 100644 --- a/app/core/Db/BatchInsert.php +++ b/app/core/Db/BatchInsert.php @@ -15,6 +15,7 @@ use Piwik\Db; use Piwik\Log; use Piwik\SettingsServer; +use Piwik\SettingsPiwik; class BatchInsert { @@ -41,6 +42,30 @@ public static function tableInsertBatchIterate($tableName, $fields, $values, $ig } } + /** + * Performs a batch insert into a specific table by sending all data in one SQL statement. + * + * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name + * @param array $fields array of unquoted field names + * @param array $values array of data to be inserted + * @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows + */ + public static function tableInsertBatchSql($tableName, $fields, $values, $ignoreWhenDuplicate = true) + { + $insertLines = array(); + $bind = array(); + foreach ($values as $row) { + $insertLines[] = "(" . Common::getSqlStringFieldsArray($row) . ")"; + $bind = array_merge($bind, $row); + } + + $fieldList = '(' . implode(',', $fields) . ')'; + $insertLines = implode(',', $insertLines); + $ignore = $ignoreWhenDuplicate ? 'IGNORE' : ''; + $query = "INSERT $ignore INTO $tableName $fieldList VALUES $insertLines"; + Db::query($query, $bind); + } + /** * Performs a batch insert into a specific table using either LOAD DATA INFILE or plain INSERTs, * as a fallback. On MySQL, LOAD DATA INFILE is 20x faster than a series of plain INSERTs. @@ -62,7 +87,12 @@ public static function tableInsertBatch($tableName, $fields, $values, $throwExce && Db::get()->hasBulkLoader()) { $path = self::getBestPathForLoadData(); - $filePath = $path . $tableName . '-' . Common::generateUniqId() . '.csv'; + $instanceId = SettingsPiwik::getPiwikInstanceId(); + if (empty($instanceId)) { + $instanceId = ''; + } + $filePath = $path . $tableName . '-' . $instanceId . Common::generateUniqId() . '.csv'; + try { $fileSpec = array( diff --git a/app/core/Db/Schema/Mysql.php b/app/core/Db/Schema/Mysql.php index afa5c9778..10c049e14 100644 --- a/app/core/Db/Schema/Mysql.php +++ b/app/core/Db/Schema/Mysql.php @@ -154,7 +154,7 @@ public function getTablesCreateSql() 'log_action' => "CREATE TABLE {$prefixTables}log_action ( idaction INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - name TEXT, + name VARCHAR(4096), hash INTEGER(10) UNSIGNED NOT NULL, type TINYINT UNSIGNED NULL, url_prefix TINYINT(2) NULL, @@ -209,7 +209,7 @@ public function getTablesCreateSql() buster int unsigned NOT NULL, idorder varchar(100) default NULL, items SMALLINT UNSIGNED DEFAULT NULL, - url text NOT NULL, + url VARCHAR(4096) NOT NULL, PRIMARY KEY (idvisit, idgoal, buster), UNIQUE KEY unique_idsite_idorder (idsite, idorder), INDEX index_idsite_datetime ( idsite, server_time ) @@ -310,6 +310,13 @@ public function getTablesCreateSql() PRIMARY KEY(`idsite`, `idfailure`) ) ENGINE=$engine DEFAULT CHARSET=utf8 ", + 'locks' => "CREATE TABLE `{$prefixTables}locks` ( + `key` VARCHAR(70) NOT NULL, + `value` VARCHAR(255) NULL DEFAULT NULL, + `expiry_time` BIGINT UNSIGNED DEFAULT 9999999999, + PRIMARY KEY (`key`) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", ); return $tables; diff --git a/app/core/Db/TransactionLevel.php b/app/core/Db/TransactionLevel.php new file mode 100644 index 000000000..c19699084 --- /dev/null +++ b/app/core/Db/TransactionLevel.php @@ -0,0 +1,83 @@ +db = $db; + } + + public function canLikelySetTransactionLevel() + { + $dbSettings = new Db\Settings(); + + return strtolower($dbSettings->getEngine()) === 'innodb'; + } + + public function setUncommitted() + { + try { + $backup = $this->db->fetchOne('SELECT @@TX_ISOLATION'); + } catch (\Exception $e) { + try { + $backup = $this->db->fetchOne('SELECT @@transaction_isolation'); + } catch (\Exception $e) { + return false; + } + } + + try { + $this->db->query('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED'); + $this->statusBackup = $backup; + + Option::set(self::TEST_OPTION_NAME, '1'); // try setting something w/ the new transaction isolation level + } catch (\Exception $e) { + // catch eg 1665 Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED + $this->restorePreviousStatus(); + return false; + } + + return true; + } + + public function restorePreviousStatus() + { + if ($this->statusBackup) { + $value = strtoupper($this->statusBackup); + $this->statusBackup = null; + + $value = str_replace('-', ' ', $value); + if (in_array($value, array('REPEATABLE READ', 'READ COMMITTED', 'SERIALIZABLE'))) { + $this->db->query('SET SESSION TRANSACTION ISOLATION LEVEL '.$value); + } elseif ($value !== 'READ UNCOMMITTED') { + $this->db->query('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ'); + } + } + + } + +} diff --git a/app/core/DeviceDetectorCache.php b/app/core/DeviceDetector/DeviceDetectorCache.php similarity index 96% rename from app/core/DeviceDetectorCache.php rename to app/core/DeviceDetector/DeviceDetectorCache.php index fe4815136..ad2d8573e 100644 --- a/app/core/DeviceDetectorCache.php +++ b/app/core/DeviceDetector/DeviceDetectorCache.php @@ -6,7 +6,7 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * */ -namespace Piwik; +namespace Piwik\DeviceDetector; use Piwik\Cache as PiwikCache; @@ -27,7 +27,7 @@ class DeviceDetectorCache implements \DeviceDetector\Cache\Cache public function __construct($ttl = 300) { $this->ttl = (int) $ttl; - $this->cache = PiwikCache::getEagerCache(); + $this->cache = PiwikCache::getLazyCache(); } /** diff --git a/app/core/DeviceDetector/DeviceDetectorFactory.php b/app/core/DeviceDetector/DeviceDetectorFactory.php new file mode 100644 index 000000000..ce8b98bbd --- /dev/null +++ b/app/core/DeviceDetector/DeviceDetectorFactory.php @@ -0,0 +1,64 @@ +getDeviceDetectionInfo($userAgent); + + self::$deviceDetectorInstances[$userAgent] = $deviceDetector; + + return $deviceDetector; + } + + public static function getNormalizedUserAgent($userAgent) + { + return Common::mb_substr(trim($userAgent), 0, 500); + } + + /** + * Creates a new DeviceDetector for the user agent. Called by makeInstance() when no matching instance + * was found in the cache. + * @param $userAgent + * @return DeviceDetector + */ + protected function getDeviceDetectionInfo($userAgent) + { + $deviceDetector = new DeviceDetector($userAgent); + $deviceDetector->discardBotInformation(); + $deviceDetector->setCache(new DeviceDetectorCache(86400)); + $deviceDetector->parse(); + return $deviceDetector; + } + + public static function clearInstancesCache() + { + self::$deviceDetectorInstances = array(); + } +} \ No newline at end of file diff --git a/app/core/DeviceDetectorFactory.php b/app/core/DeviceDetectorFactory.php index 2217dbe7c..1d392cf95 100644 --- a/app/core/DeviceDetectorFactory.php +++ b/app/core/DeviceDetectorFactory.php @@ -9,32 +9,19 @@ namespace Piwik; use DeviceDetector\DeviceDetector; -use Piwik\Common; +use Piwik\Container\StaticContainer; class DeviceDetectorFactory { - protected static $deviceDetectorInstances = array(); - /** - * Returns a Singleton instance of DeviceDetector for the given user agent + * Returns a Singleton instance of DeviceDetector for the given user agent. * @param string $userAgent * @return DeviceDetector + * @deprecated Should get a factory via StaticContainer and call makeInstance() on it instead */ public static function getInstance($userAgent) { - $userAgent = Common::mb_substr($userAgent, 0, 500); - - if (array_key_exists($userAgent, self::$deviceDetectorInstances)) { - return self::$deviceDetectorInstances[$userAgent]; - } - - $deviceDetector = new DeviceDetector($userAgent); - $deviceDetector->discardBotInformation(); - $deviceDetector->setCache(new DeviceDetectorCache(86400)); - $deviceDetector->parse(); - - self::$deviceDetectorInstances[$userAgent] = $deviceDetector; - - return $deviceDetector; + $factory = StaticContainer::get(\Piwik\DeviceDetector\DeviceDetectorFactory::class); + return $factory->makeInstance($userAgent); } -} +} \ No newline at end of file diff --git a/app/core/ErrorHandler.php b/app/core/ErrorHandler.php index 272187d82..92a47c4f2 100644 --- a/app/core/ErrorHandler.php +++ b/app/core/ErrorHandler.php @@ -161,8 +161,12 @@ public static function errorHandler($errno, $errstr, $errfile, $errline) case E_DEPRECATED: case E_USER_DEPRECATED: default: + $context = array('trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 15)); try { - StaticContainer::get(LoggerInterface::class)->warning(self::createLogMessage($errno, $errstr, $errfile, $errline)); + StaticContainer::get(LoggerInterface::class)->warning( + self::createLogMessage($errno, $errstr, $errfile, $errline), + $context + ); } catch (\Exception $ex) { // ignore (it's possible for this to happen if the StaticContainer hasn't been created yet) } diff --git a/app/core/Exception/NoWebsiteFoundException.php b/app/core/Exception/NoWebsiteFoundException.php index 6d4c77e8f..3d38be164 100644 --- a/app/core/Exception/NoWebsiteFoundException.php +++ b/app/core/Exception/NoWebsiteFoundException.php @@ -8,6 +8,6 @@ */ namespace Piwik\Exception; -class NoWebsiteFoundException extends Exception +class NoWebsiteFoundException extends InvalidRequestParameterException { } diff --git a/app/core/Exception/NotYetInstalledException.php b/app/core/Exception/NotYetInstalledException.php index 388f001c2..95e31d63b 100644 --- a/app/core/Exception/NotYetInstalledException.php +++ b/app/core/Exception/NotYetInstalledException.php @@ -8,6 +8,6 @@ */ namespace Piwik\Exception; -class NotYetInstalledException extends Exception +class NotYetInstalledException extends InvalidRequestParameterException { } diff --git a/app/core/Http.php b/app/core/Http.php index bb4a44d3d..f7dc89f9d 100644 --- a/app/core/Http.php +++ b/app/core/Http.php @@ -136,7 +136,7 @@ public static function ensureDestinationDirectoryExists($destinationPath) * @param array $additionalHeaders List of additional headers to set for the request * * @return string|array true (or string/array) on success; false on HTTP response error code (1xx or 4xx) - * @throws Exception + *@throws Exception */ public static function sendHttpRequestBy( $method = 'socket', @@ -244,19 +244,19 @@ public static function sendHttpRequestBy( * described below * @ignore */ - Piwik::postEvent('Http.sendHttpRequest.end', array($aUrl, $httpEventParams, &$response, &$status, &$headers)); - - if ($destinationPath && file_exists($destinationPath)) { - return true; - } - if ($getExtendedInfo) { - return array( - 'status' => $status, - 'headers' => $headers, - 'data' => $response - ); + Piwik::postEvent('Http.sendHttpRequest.end', array($aUrl, $httpEventParams, &$response, &$status, &$headers)); + + if ($destinationPath && file_exists($destinationPath)) { + return true; + } + if ($getExtendedInfo) { + return array( + 'status' => $status, + 'headers' => $headers, + 'data' => $response + ); } else { - return trim($response); + return trim($response); } } diff --git a/app/core/LogDeleter.php b/app/core/LogDeleter.php index 5a4fa15b0..e97416e44 100644 --- a/app/core/LogDeleter.php +++ b/app/core/LogDeleter.php @@ -10,6 +10,7 @@ use Piwik\DataAccess\RawLogDao; use Piwik\Plugin\LogTablesProvider; +use Piwik\Plugins\SitesManager\Model; /** * Service that deletes log entries. Methods in this class cascade, so deleting visits will delete visit actions, @@ -69,7 +70,7 @@ public function deleteVisits($visitIds) * @param callable $afterChunkDeleted Callback executed after every chunk of visits are deleted. * @return int The number of visits deleted. */ - public function deleteVisitsFor($startDatetime, $endDatetime, $idSite = null, $iterationStep = 1000, $afterChunkDeleted = null) + public function deleteVisitsFor($startDatetime, $endDatetime, $idSite = null, $iterationStep = 2000, $afterChunkDeleted = null) { $fields = array('idvisit'); $conditions = array(); @@ -84,6 +85,12 @@ public function deleteVisitsFor($startDatetime, $endDatetime, $idSite = null, $i if (!empty($idSite)) { $conditions[] = array('idsite', '=', $idSite); + } elseif (!empty($startDatetime) || !empty($endDatetime)) { + // make sure to use index! + $sitesModel = new Model(); + $allIdSites = $sitesModel->getSitesId(); + $allIdSites = array_map('intval', $allIdSites); + $conditions[] = array('idsite', '', $allIdSites); } $logsDeleted = 0; @@ -95,8 +102,8 @@ public function deleteVisitsFor($startDatetime, $endDatetime, $idSite = null, $i if (!empty($afterChunkDeleted)) { $afterChunkDeleted($logsDeleted); } - }); + }, $willDelete = true); return $logsDeleted; } -} \ No newline at end of file +} diff --git a/app/core/Mail.php b/app/core/Mail.php index 64295c526..ddc229944 100644 --- a/app/core/Mail.php +++ b/app/core/Mail.php @@ -147,11 +147,7 @@ public function setSubject($subject) return parent::setSubject($subject); } - /** - * @param string $email - * @return string - */ - protected function parseDomainPlaceholderAsPiwikHostName($email) + public function getMailHost() { $hostname = Config::getInstance()->mail['defaultHostnameIfEmpty']; $piwikHost = Url::getCurrentHost($hostname); @@ -162,7 +158,16 @@ protected function parseDomainPlaceholderAsPiwikHostName($email) if ($this->isHostDefinedAndNotLocal($url)) { $piwikHost = $url['host']; } + return $piwikHost; + } + /** + * @param string $email + * @return string + */ + protected function parseDomainPlaceholderAsPiwikHostName($email) + { + $piwikHost = $this->getMailHost(); return str_replace('{DOMAIN}', $piwikHost, $email); } diff --git a/app/core/Metrics.php b/app/core/Metrics.php index 9ee9a16ce..144e0ec29 100644 --- a/app/core/Metrics.php +++ b/app/core/Metrics.php @@ -9,6 +9,8 @@ namespace Piwik; use Piwik\Cache as PiwikCache; +use Piwik\Columns\MetricsList; +use Piwik\Container\StaticContainer; require_once PIWIK_INCLUDE_PATH . "/core/Piwik.php"; @@ -175,6 +177,36 @@ class Metrics Metrics::INDEX_NB_VISITS_CONVERTED, ); + public static function getMappingFromIdToName() + { + $cache = StaticContainer::get(PiwikCache\Transient::class); + $cacheKey = CacheId::siteAware(CacheId::pluginAware('Metrics.mappingFromIdToName')); + + $value = $cache->fetch($cacheKey); + if (empty($value)) { + $value = self::$mappingFromIdToName; + + /** + * Use this event if your plugin uses custom metric integer IDs to associate those IDs with the + * actual metric names (eg, 2 => nb_visits). This allows matomo to automate the replacing + * of IDs => metric names for your new metrics. + * + * **Example** + * + * public function addMetricIdToNameMapping(&$mapping) + * { + * $mapping[Archiver::INDEX_MY_NEW_METRIC] = $mapping['MyPlugin_myNewMetric']; + * } + * + * @ignore + */ + Piwik::postEvent('Metrics.addMetricIdToNameMapping', [&$value]); + + $cache->save($cacheKey, $value); + } + return $value; + } + public static function getVisitsMetricNames() { $names = array(); diff --git a/app/core/Metrics/Formatter.php b/app/core/Metrics/Formatter.php index 8852e2610..25e80ef81 100644 --- a/app/core/Metrics/Formatter.php +++ b/app/core/Metrics/Formatter.php @@ -207,7 +207,11 @@ public function formatMetrics(DataTable $dataTable, Report $report = null, $metr foreach ($dataTable->getRows() as $row) { $subtable = $row->getSubtable(); if (!empty($subtable)) { - $this->formatMetrics($subtable, $report, $metricsToFormat); + $this->formatMetrics($subtable, $report, $metricsToFormat, $formatAll); + } + $comparisons = $row->getComparisons(); + if (!empty($comparisons)) { + $this->formatMetrics($comparisons, $report, $metricsToFormat, $formatAll); } } diff --git a/app/core/Piwik.php b/app/core/Piwik.php index e252b09de..1768b8219 100644 --- a/app/core/Piwik.php +++ b/app/core/Piwik.php @@ -332,12 +332,7 @@ public static function isUserIsAnonymous() */ public static function checkUserIsNotAnonymous() { - if (Access::getInstance()->hasSuperUserAccess()) { - return; - } - if (self::isUserIsAnonymous()) { - throw new NoAccessException(Piwik::translate('General_YouMustBeLoggedIn')); - } + Access::getInstance()->checkUserIsNotAnonymous(); } /** @@ -606,7 +601,7 @@ public static function getAction() * @param array|string $columns * @return array */ - public static function getArrayFromApiParameter($columns) + public static function getArrayFromApiParameter($columns, $unique = true) { if (empty($columns)) { return array(); @@ -615,7 +610,9 @@ public static function getArrayFromApiParameter($columns) return $columns; } $array = explode(',', $columns); - $array = array_unique($array); + if ($unique) { + $array = array_unique($array); + } return $array; } diff --git a/app/core/Plugin.php b/app/core/Plugin.php index ebd80595d..a3187bf70 100644 --- a/app/core/Plugin.php +++ b/app/core/Plugin.php @@ -12,8 +12,9 @@ use Piwik\Plugin\Dependency; use Piwik\Plugin\Manager; use Piwik\Plugin\MetadataLoader; -if (!class_exists('\Piwik\Plugin')) { +if (!class_exists('Piwik\Plugin')) { + /** * Base class of all Plugin Descriptor classes. * @@ -595,3 +596,4 @@ private function makeDependency($piwikVersion) } } } + diff --git a/app/core/Plugin/Controller.php b/app/core/Plugin/Controller.php index 2fa31f341..176a040dc 100644 --- a/app/core/Plugin/Controller.php +++ b/app/core/Plugin/Controller.php @@ -314,6 +314,10 @@ protected function renderTemplateAs($template, array $variables = array(), $view $view->$key = $value; } + if (isset($view->siteName)) { + $view->siteNameDecoded = Common::unsanitizeInputValue($view->siteName); + } + return $view->render(); } @@ -616,7 +620,6 @@ protected function setGeneralVariablesViewAs($view, $viewType) $this->setPeriodVariablesView($view); $view->siteName = $this->site->getName(); - $view->siteNameDecoded = Common::unsanitizeInputValue($view->siteName); $view->siteMainUrl = $this->site->getMainUrl(); $siteTimezone = $this->site->getTimezone(); @@ -1026,8 +1029,8 @@ public static function getPrettyDate($date, $period) protected function checkSitePermission() { - if (!empty($this->idSite) && empty($this->site)) { - throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $this->idSite))); + if (!empty($this->idSite)) { + Access::getInstance()->checkUserHasViewAccess($this->idSite); } elseif (empty($this->site) || empty($this->idSite)) { throw new Exception("The requested website idSite is not found in the request, or is invalid. Please check that you are logged in Matomo and have permission to access the specified website."); @@ -1065,3 +1068,4 @@ private function checkViewType($viewType) } } } + diff --git a/app/core/Plugin/ControllerAdmin.php b/app/core/Plugin/ControllerAdmin.php index 90189d555..82b5e2e4c 100644 --- a/app/core/Plugin/ControllerAdmin.php +++ b/app/core/Plugin/ControllerAdmin.php @@ -128,7 +128,6 @@ private static function notifyAnyInvalidPlugin() protected function setBasicVariablesViewAs($view, $viewType = 'admin') { $this->setBasicVariablesNoneAdminView($view); - if ($viewType == 'admin') { self::setBasicVariablesAdminView($view); } diff --git a/app/core/Plugin/LogTablesProvider.php b/app/core/Plugin/LogTablesProvider.php index 97381bb77..8cd5884f2 100644 --- a/app/core/Plugin/LogTablesProvider.php +++ b/app/core/Plugin/LogTablesProvider.php @@ -9,6 +9,7 @@ namespace Piwik\Plugin; use Piwik\Container\StaticContainer; +use Piwik\DataAccess\LogTableTemporary; use Piwik\Piwik; use Piwik\Tracker\LogTable; @@ -24,6 +25,11 @@ class LogTablesProvider { */ private $tablesCache; + /** + * @var LogTableTemporary + */ + private $tempTable; + public function __construct(Manager $pluginManager) { $this->pluginManager = $pluginManager; @@ -37,6 +43,9 @@ public function __construct(Manager $pluginManager) */ public function getLogTable($tableNameWithoutPrefix) { + if ($this->tempTable && $this->tempTable->getName() === $tableNameWithoutPrefix) { + return $this->tempTable; + } foreach ($this->getAllLogTables() as $table) { if ($table->getName() === $tableNameWithoutPrefix) { return $table; @@ -44,6 +53,32 @@ public function getLogTable($tableNameWithoutPrefix) } } + /** + * @param LogTableTemporary|null $table + */ + public function setTempTable($table) + { + $this->tempTable = $table; + } + + public function clearCache() + { + $this->tablesCache = null; + } + + /** + * Needed for log query builder + * @return LogTable[] + */ + public function getAllLogTablesWithTemporary() + { + $tables = $this->getAllLogTables(); + if ($this->tempTable) { + $tables[] = $this->tempTable; + } + return $tables; + } + /** * Get all log table instances defined by any activated and loaded plugin. The returned tables are not sorted in * any order. diff --git a/app/core/Plugin/Manager.php b/app/core/Plugin/Manager.php index 5cddf2fc7..fd5b9d56b 100644 --- a/app/core/Plugin/Manager.php +++ b/app/core/Plugin/Manager.php @@ -35,8 +35,6 @@ use Piwik\Translation\Translator; use Piwik\Updater; -require_once PIWIK_INCLUDE_PATH . '/core/EventDispatcher.php'; - /** * The singleton that manages plugin loading/unloading and installation/uninstallation. */ @@ -369,17 +367,7 @@ public static function initPluginDirectories() $pluginDirs = self::getPluginsDirectories(); if (count($pluginDirs) > 1) { - spl_autoload_register(function ($className) use ($pluginDirs) { - if (strpos($className, 'Piwik\Plugins\\') === 0) { - $withoutPrefix = str_replace('Piwik\Plugins\\', '', $className); - $path = str_replace('\\', DIRECTORY_SEPARATOR, $withoutPrefix) . '.php'; - foreach ($pluginDirs as $pluginsDirectory) { - if (file_exists($pluginsDirectory . $path)) { - require_once $pluginsDirectory . $path; - } - } - } - }); + self::registerPluginDirAutoload($pluginDirs); } } @@ -395,6 +383,26 @@ public static function initPluginDirectories() } } + /** + * Registers a new autoloader to support the loading of Matomo plugin classes when the plugins are installed + * outside the Matomo plugins folder. + * @param array $pluginDirs + */ + public static function registerPluginDirAutoload($pluginDirs) + { + spl_autoload_register(function ($className) use ($pluginDirs) { + if (strpos($className, 'Piwik\Plugins\\') === 0) { + $withoutPrefix = str_replace('Piwik\Plugins\\', '', $className); + $path = str_replace('\\', DIRECTORY_SEPARATOR, $withoutPrefix) . '.php'; + foreach ($pluginDirs as $pluginsDirectory) { + if (file_exists($pluginsDirectory . $path)) { + require_once $pluginsDirectory . $path; + } + } + } + }); + } + public static function getAlternativeWebRootDirectories() { $dirs = array(); @@ -1647,3 +1655,4 @@ public function loadPluginTranslations() } } } + diff --git a/app/core/Plugin/ViewDataTable.php b/app/core/Plugin/ViewDataTable.php index 64248a327..2cb2837b6 100644 --- a/app/core/Plugin/ViewDataTable.php +++ b/app/core/Plugin/ViewDataTable.php @@ -9,10 +9,14 @@ namespace Piwik\Plugin; use Piwik\API\Request; +use Piwik\API\Request as ApiRequest; use Piwik\Common; use Piwik\DataTable; use Piwik\Period; use Piwik\Piwik; +use Piwik\Plugins\API\Filter\DataComparisonFilter; +use Piwik\Plugins\CoreVisualizations\Visualizations\Sparkline; +use Piwik\View; use Piwik\View\ViewInterface; use Piwik\ViewDataTable\Config as VizConfig; use Piwik\ViewDataTable\Manager as ViewDataTableManager; @@ -169,6 +173,8 @@ abstract class ViewDataTable implements ViewInterface */ protected $request; + private $isComparing = null; + /** * Constructor. Initializes display and request properties to their default values. * Posts the {@hook ViewDataTable.configure} event which plugins can use to configure the @@ -356,7 +362,12 @@ protected function loadDataTableFromAPI() return $this->dataTable; } - $this->dataTable = $this->request->loadDataTableFromAPI(); + $extraParams = []; + if ($this->isComparing()) { + $extraParams['compare'] = '1'; + } + + $this->dataTable = $this->request->loadDataTableFromAPI($extraParams); return $this->dataTable; } @@ -590,4 +601,41 @@ public function getNonOverridableParams($overrideParams) return $paramsCannotBeOverridden; } + /** + * Returns true if both this current visualization supports comparison, and if comparison query parameters + * are present in the URL. + * + * @return bool + */ + public function isComparing() + { + if (!$this->supportsComparison() + || $this->config->disable_comparison + ) { + return false; + } + + $request = $this->request->getRequestArray(); + $request = ApiRequest::getRequestArrayFromString($request); + + $result = DataComparisonFilter::isCompareParamsPresent($request); + return $result; + } + + /** + * Implementations should override this method if they support a special comparison view. By + * default, it is assumed visualizations do not support comparison. + * + * @return bool + */ + public function supportsComparison() + { + return false; + } + + public function getRequestArray() + { + $requestArray = $this->request->getRequestArray(); + return ApiRequest::getRequestArrayFromString($requestArray); + } } diff --git a/app/core/Plugin/Visualization.php b/app/core/Plugin/Visualization.php index b130f3c28..1695bd04c 100644 --- a/app/core/Plugin/Visualization.php +++ b/app/core/Plugin/Visualization.php @@ -12,6 +12,7 @@ use Piwik\API\DataTablePostProcessor; use Piwik\API\Proxy; use Piwik\API\Request; +use Piwik\API\Request as ApiRequest; use Piwik\API\ResponseBuilder; use Piwik\Common; use Piwik\Container\StaticContainer; @@ -25,11 +26,12 @@ use Piwik\Period; use Piwik\Piwik; use Piwik\Plugins\API\API as ApiApi; +use Piwik\Plugins\API\Filter\DataComparisonFilter; use Piwik\Plugins\PrivacyManager\PrivacyManager; +use Piwik\SettingsPiwik; use Piwik\View; use Piwik\ViewDataTable\Manager as ViewDataTableManager; use Piwik\Plugin\Manager as PluginManager; -use Piwik\API\Request as ApiRequest; use Psr\Log\LoggerInterface; /** @@ -190,7 +192,9 @@ public function render() $this->applyFilters(); $this->addVisualizationInfoFromMetricMetadata(); $this->afterAllFiltersAreApplied(); + $this->beforeRender(); + $this->fireBeforeRenderHook(); $this->logMessageIfRequestPropertiesHaveChanged($requestPropertiesAfterLoadDataTable); } catch (NoAccessException $e) { @@ -245,6 +249,17 @@ public function render() $view->footerIcons = $this->config->footer_icons; $view->isWidget = Common::getRequestVar('widget', 0, 'int'); $view->notifications = []; + $view->isComparing = $this->isComparing(); + + if (!$this->supportsComparison() + && DataComparisonFilter::isCompareParamsPresent() + && empty($view->dataTableHasNoData) + ) { + if (empty($view->properties['show_footer_message'])) { + $view->properties['show_footer_message'] = ''; + } + $view->properties['show_footer_message'] .= '
' . Piwik::translate('General_VisualizationDoesNotSupportComparison'); + } if (empty($this->dataTable) || !$this->hasAnyData($this->dataTable)) { /** @@ -403,6 +418,19 @@ private function postDataTableLoadedFromAPI() $hasNbVisits = in_array('nb_visits', $columns); $hasNbUniqVisitors = in_array('nb_uniq_visitors', $columns); + // if any comparison period doesn't support unique visitors, we can't display it for the main table + if ($this->isComparing()) { + $request = $this->getRequestArray(); + if (!empty($request['comparePeriods'])) { + foreach ($request['comparePeriods'] as $comparePeriod) { + if (!SettingsPiwik::isUniqueVisitorsEnabled($comparePeriod)) { + $hasNbUniqVisitors = false; + break; + } + } + } + } + // default columns_to_display to label, nb_uniq_visitors/nb_visits if those columns exist in the // dataset. otherwise, default to all columns in dataset. if (empty($this->config->columns_to_display)) { @@ -490,7 +518,6 @@ private function applyFilters() foreach ($self->config->getPresentationFilters() as $filter) { $dataTable->queueFilter($filter[0], $filter[1]); } - }); $this->dataTable = $postProcessor->process($this->dataTable); @@ -666,6 +693,10 @@ protected function getClientSideParametersToSet() $javascriptVariablesToSet['segment'] = $rawSegment; } + if (isset($javascriptVariablesToSet['compareSegments'])) { + $javascriptVariablesToSet['compareSegments'] = Common::unsanitizeInputValues($javascriptVariablesToSet['compareSegments']); + } + return $javascriptVariablesToSet; } @@ -723,6 +754,17 @@ public function beforeRender() // eg $this->config->showFooterColumns = true; } + private function fireBeforeRenderHook() + { + /** + * Posted immediately before rendering the view. Plugins can use this event to perform last minute + * configuration of the view based on it's data or the report being viewed. + * + * @param Visualization $view The instance to configure. + */ + Piwik::postEvent('Visualization.beforeRender', [$this]); + } + private function makeDataTablePostProcessor() { $request = $this->buildApiRequestArray(); @@ -788,8 +830,7 @@ private function makeSureArrayContainsOnlyStrings($array) */ public function buildApiRequestArray() { - $requestArray = $this->request->getRequestArray(); - $request = ApiRequest::getRequestArrayFromString($requestArray); + $request = $this->getRequestArray(); if (false === $this->config->enable_sort) { $request['filter_sort_column'] = ''; @@ -804,6 +845,10 @@ public function buildApiRequestArray() unset($request['disable_queued_filters']); } + if ($this->isComparing()) { + $request['compare'] = '1'; + } + return $request; } } diff --git a/app/core/ProfessionalServices/Advertising.php b/app/core/ProfessionalServices/Advertising.php index 56ea72c5c..b59d8b868 100644 --- a/app/core/ProfessionalServices/Advertising.php +++ b/app/core/ProfessionalServices/Advertising.php @@ -55,7 +55,7 @@ public function areAdsForProfessionalServicesEnabled() */ public function getPromoUrlForProfessionalServices($campaignMedium, $campaignContent = '') { - $url = 'https://matomo.org/support/?'; + $url = 'https://matomo.org/support-plans/?'; $campaign = $this->getCampaignParametersForPromoUrl( $name = self::CAMPAIGN_NAME_PROFESSIONAL_SERVICES, @@ -102,7 +102,7 @@ public function addPromoCampaignParametersToUrl($url, $campaignName, $campaignMe */ private function getCampaignParametersForPromoUrl($campaignName, $campaignMedium, $campaignContent = '') { - $campaignName = sprintf('pk_campaign=%s&pk_medium=%s&pk_source=Piwik_App', $campaignName, $campaignMedium); + $campaignName = sprintf('pk_campaign=%s&pk_medium=%s&pk_source=Matomo_App', $campaignName, $campaignMedium); if (!empty($campaignContent)) { $campaignName .= '&pk_content=' . $campaignContent; diff --git a/app/core/Profiler.php b/app/core/Profiler.php index 49bba4188..1e019733b 100644 --- a/app/core/Profiler.php +++ b/app/core/Profiler.php @@ -210,7 +210,7 @@ public static function setupProfilerXHProf($mainRun = false, $setupDuringTrackin } $hasXhprof = function_exists('xhprof_enable'); - $hasTidewaysXhprof = function_exists('tideways_xhprof_enable'); + $hasTidewaysXhprof = function_exists('tideways_xhprof_enable') || function_exists('tideways_enable'); if (!$hasXhprof && !$hasTidewaysXhprof) { $xhProfPath = PIWIK_INCLUDE_PATH . '/vendor/facebook/xhprof/extension/modules/xhprof.so'; @@ -248,6 +248,8 @@ function xhprof_error($out) if (function_exists('xhprof_enable')) { xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY); + } elseif (function_exists('tideways_enable')) { + tideways_enable(TIDEWAYS_FLAGS_MEMORY | TIDEWAYS_FLAGS_CPU); } elseif (function_exists('tideways_xhprof_enable')) { tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_CPU); } @@ -257,8 +259,12 @@ function xhprof_error($out) $xhprofData = xhprof_disable(); $xhprofRuns = new XHProfRuns_Default(); $runId = $xhprofRuns->save_run($xhprofData, $profilerNamespace); - } elseif (function_exists('tideways_xhprof_disable')) { - $xhprofData = tideways_xhprof_disable(); + } elseif (function_exists('tideways_xhprof_disable') || function_exists('tideways_disable')) { + if (function_exists('tideways_xhprof_disable')) { + $xhprofData = tideways_xhprof_disable(); + } else { + $xhprofData = tideways_disable(); + } $runId = uniqid(); file_put_contents( $outputDir . DIRECTORY_SEPARATOR . $runId . '.' . $profilerNamespace . '.xhprof', diff --git a/app/core/RankingQuery.php b/app/core/RankingQuery.php index fd4f69b16..c78400039 100644 --- a/app/core/RankingQuery.php +++ b/app/core/RankingQuery.php @@ -41,6 +41,9 @@ */ class RankingQuery { + // a special label used to mark the 'Others' row in a ranking query result set. this is mapped to the + // datatable summary row during archiving. + const LABEL_SUMMARY_ROW = '__mtm_ranking_query_others__'; /** * Contains the labels of the inner query. @@ -84,7 +87,7 @@ class RankingQuery * The value to use in the label of the 'Others' row. * @var string */ - private $othersLabelValue = 'Others'; + private $othersLabelValue = self::LABEL_SUMMARY_ROW; /** * Constructor. @@ -223,7 +226,7 @@ public function partitionResultIntoMultipleGroups($partitionColumn, $possibleVal public function execute($innerQuery, $bind = array()) { $query = $this->generateRankingQuery($innerQuery); - $data = Db::fetchAll($query, $bind); + $data = Db::getReader()->fetchAll($query, $bind); if ($this->columnToMarkExcludedRows !== false) { // split the result into the regular result and the rows with special treatment diff --git a/app/core/Segment.php b/app/core/Segment.php index 765eeb344..c10eee2cf 100644 --- a/app/core/Segment.php +++ b/app/core/Segment.php @@ -13,7 +13,7 @@ use Piwik\ArchiveProcessor\Rules; use Piwik\Container\StaticContainer; use Piwik\DataAccess\LogQueryBuilder; -use Piwik\Plugins\API\API; +use Piwik\Plugins\SegmentEditor\SegmentEditor; use Piwik\Segment\SegmentExpression; /** @@ -75,6 +75,11 @@ class Segment */ private $segmentQueryBuilder; + /** + * @var bool + */ + private $isSegmentEncoded; + /** * Truncate the Segments to 8k */ @@ -99,12 +104,31 @@ public function __construct($segmentCondition, $idSites) throw new Exception("The Super User has disabled the Segmentation feature."); } - // First try with url decoded value. If that fails, try with raw value. - // If that also fails, it will throw the exception + // The segment expression can be urlencoded. Unfortunately, both the encoded and decoded versions + // can usually be parsed successfully. To pick the right one, we try both and pick the one w/ more + // successfully parsed subexpressions. + $subexpressionsDecoded = 0; try { $this->initializeSegment(urldecode($segmentCondition), $idSites); + $subexpressionsDecoded = $this->segmentExpression->getSubExpressionCount(); } catch (Exception $e) { + // ignore + } + + $subexpressionsRaw = 0; + try { $this->initializeSegment($segmentCondition, $idSites); + $subexpressionsRaw = $this->segmentExpression->getSubExpressionCount(); + } catch (Exception $e) { + // ignore + } + + if ($subexpressionsRaw > $subexpressionsDecoded) { + $this->initializeSegment($segmentCondition, $idSites); + $this->isSegmentEncoded = false; + } else { + $this->initializeSegment(urldecode($segmentCondition), $idSites); + $this->isSegmentEncoded = true; } } @@ -331,11 +355,13 @@ public static function getSegmentHash($definition) * @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`. * @param int $limit Limit number of result to $limit * @param int $offset Specified the offset of the first row to return + * @param bool $forceGroupBy Force the group by and not using a subquery. Note: This may make the query slower see https://github.com/matomo-org/matomo/issues/9200#issuecomment-183641293 + * A $groupBy value needs to be set for this to work. * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value. * Use only when you're not aggregating or it will sample the data. * @return string The entire select query. */ - public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0, $offset = 0) + public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0, $offset = 0, $forceGroupBy = false) { $segmentExpression = $this->segmentExpression; @@ -344,8 +370,23 @@ public function getSelectQuery($select, $from, $where = false, $bind = array(), $limitAndOffset = (int) $offset . ', ' . (int) $limit; } - return $this->segmentQueryBuilder->getSelectQueryString($segmentExpression, $select, $from, $where, $bind, - $groupBy, $orderBy, $limitAndOffset); + try { + if ($forceGroupBy && $groupBy) { + $this->segmentQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT); + } + $result = $this->segmentQueryBuilder->getSelectQueryString($segmentExpression, $select, $from, $where, $bind, + $groupBy, $orderBy, $limitAndOffset); + } catch (Exception $e) { + if ($forceGroupBy && $groupBy) { + $this->segmentQueryBuilder->forceInnerGroupBySubselect(''); + } + throw $e; + } + + if ($forceGroupBy && $groupBy) { + $this->segmentQueryBuilder->forceInnerGroupBySubselect(''); + } + return $result; } /** @@ -408,4 +449,30 @@ private static function containsCondition($segment, $operator, $segmentCondition || $segment === urlencode($segmentCondition) || $segment === urldecode($segmentCondition); } + + public function getStoredSegmentName($idSite) + { + $segment = $this->getString(); + if (empty($segment)) { + return Piwik::translate('SegmentEditor_DefaultAllVisits'); + } + + $availableSegments = SegmentEditor::getAllSegmentsForSite($idSite); + + $foundStoredSegment = null; + foreach ($availableSegments as $storedSegment) { + if ($storedSegment['definition'] == $segment + || $storedSegment['definition'] == urldecode($segment) + || $storedSegment['definition'] == urlencode($segment) + ) { + $foundStoredSegment = $storedSegment; + } + } + + if (isset($foundStoredSegment)) { + return $foundStoredSegment['name']; + } + + return $this->isSegmentEncoded ? urldecode($segment) : $segment; + } } diff --git a/app/core/Segment/SegmentExpression.php b/app/core/Segment/SegmentExpression.php index f9386fa44..4a85fc0ab 100644 --- a/app/core/Segment/SegmentExpression.php +++ b/app/core/Segment/SegmentExpression.php @@ -74,6 +74,15 @@ public function isEmpty() protected $tree = array(); protected $parsedSubExpressions = array(); + public function getSubExpressionCount() + { + $cleaned = array_filter($this->parsedSubExpressions, function ($part) { + $isExpressionColumnPresent = !empty($part[1][0]); + return $isExpressionColumnPresent; + }); + return count($cleaned); + } + /** * Given the array of parsed filters containing, for each filter, * the boolean operator (AND/OR) and the operand, diff --git a/app/core/Session.php b/app/core/Session.php index 1dbca1580..024c5246d 100644 --- a/app/core/Session.php +++ b/app/core/Session.php @@ -54,9 +54,8 @@ public static function start($options = false) ) { return; } - self::$sessionStarted = true; - + if (defined('PIWIK_SESSION_NAME')) { self::$sessionName = PIWIK_SESSION_NAME; } @@ -149,7 +148,7 @@ public static function start($options = false) } else { $pathToSessions = Filechecks::getErrorMessageMissingPermissions(self::getSessionsDirectory()); } - + $message = sprintf("Error: %s %s\n
Debug: the original error was \n%s
", Piwik::translate('General_ExceptionUnableToStartSession'), $pathToSessions, @@ -175,10 +174,11 @@ public static function getSessionsDirectory() public static function close() { - if (self::isSessionStarted()) { - // only write/close session if the session was actually started by us - parent::writeClose(); - } + if (self::isSessionStarted()) { + // only write/close session if the session was actually started by us + // otherwise we will set the session values to base64 encoded and whoever the session started might not expect the values in that way + parent::writeClose(); + } } public static function isSessionStarted() diff --git a/app/core/Session/SessionFingerprint.php b/app/core/Session/SessionFingerprint.php index 4955247d0..df8a519e5 100644 --- a/app/core/Session/SessionFingerprint.php +++ b/app/core/Session/SessionFingerprint.php @@ -36,6 +36,9 @@ */ class SessionFingerprint { + // used in case the global.ini.php becomes corrupt or doesn't update properly + const DEFAULT_IDLE_TIMEOUT = 3600; + const USER_NAME_SESSION_VAR_NAME = 'user.name'; const SESSION_INFO_SESSION_VAR_NAME = 'session.info'; const SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED = 'twofactorauth.verified'; @@ -138,8 +141,17 @@ private function getExpirationTimeFromNow($time = null) { $time = $time ?: Date::now()->getTimestampUTC(); - $nonRememberedSessionExpireTime = Config::getInstance()->General['login_session_not_remembered_idle_timeout']; - $sessionCookieLifetime = Config::getInstance()->General['login_cookie_expire']; + $general = Config::getInstance()->General; + + if (!isset($general['login_session_not_remembered_idle_timeout']) + || (int) $general['login_session_not_remembered_idle_timeout'] <= 0 + ) { + $nonRememberedSessionExpireTime = self::DEFAULT_IDLE_TIMEOUT; + } else { + $nonRememberedSessionExpireTime = (int) $general['login_session_not_remembered_idle_timeout']; + } + + $sessionCookieLifetime = $general['login_cookie_expire']; if ($this->isRemembered()) { $expireDuration = $sessionCookieLifetime; diff --git a/app/core/Tracker.php b/app/core/Tracker.php index cca32fae6..3e0cefc4c 100644 --- a/app/core/Tracker.php +++ b/app/core/Tracker.php @@ -9,6 +9,7 @@ namespace Piwik; use Exception; +use Piwik\Container\StaticContainer; use Piwik\Plugins\BulkTracking\Tracker\Requests; use Piwik\Plugins\PrivacyManager\Config as PrivacyManagerConfig; use Piwik\Tracker\Db as TrackerDb; @@ -19,6 +20,7 @@ use Piwik\Tracker\TrackerConfig; use Piwik\Tracker\Visit; use Piwik\Plugin\Manager as PluginManager; +use Psr\Log\LoggerInterface; /** * Class used by the logging script piwik.php called by the javascript tag. @@ -43,6 +45,16 @@ class Tracker private $countOfLoggedRequests = 0; protected $isInstalled = null; + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct() + { + $this->logger = StaticContainer::get(LoggerInterface::class); + } + public function isDebugModeEnabled() { return array_key_exists('PIWIK_TRACKER_DEBUG', $GLOBALS) && $GLOBALS['PIWIK_TRACKER_DEBUG'] === true; @@ -53,7 +65,7 @@ public function shouldRecordStatistics() $record = TrackerConfig::getConfigValue('record_statistics') != 0; if (!$record) { - Common::printDebug('Tracking is disabled in the config.ini.php via record_statistics=0'); + $this->logger->debug('Tracking is disabled in the config.ini.php via record_statistics=0'); } return $record && $this->isInstalled(); @@ -74,8 +86,9 @@ private function init() ErrorHandler::registerErrorHandler(); ExceptionHandler::setUp(); - Common::printDebug("Debug enabled - Input parameters: "); - Common::printDebug(var_export($_GET + $_POST, true)); + $this->logger->debug("Debug enabled - Input parameters: {params}", [ + 'params' => var_export($_GET + $_POST, true), + ]); } } @@ -129,9 +142,11 @@ public function track(Handler $handler, RequestSet $requestSet) public function trackRequest(Request $request) { if ($request->isEmptyRequest()) { - Common::printDebug("The request is empty"); + $this->logger->debug('The request is empty'); } else { - Common::printDebug("Current datetime: " . date("Y-m-d H:i:s", $request->getCurrentTimestamp())); + $this->logger->debug('Current datetime: {date}', [ + 'date' => date("Y-m-d H:i:s", $request->getCurrentTimestamp()), + ]); $visit = Visit\Factory::make(); $visit->setRequest($request); @@ -306,15 +321,20 @@ protected function loadTrackerPlugins() try { $pluginManager = PluginManager::getInstance(); $pluginsTracker = $pluginManager->loadTrackerPlugins(); - Common::printDebug("Loading plugins: { " . implode(", ", $pluginsTracker) . " }"); + + $this->logger->debug("Loading plugins: { {plugins} }", [ + 'plugins' => implode(", ", $pluginsTracker), + ]); } catch (Exception $e) { - Common::printDebug("ERROR: " . $e->getMessage()); + $this->logger->error('Error loading tracker plugins: {exception}', [ + 'exception' => $e, + ]); } } private function handleFatalErrors() { - register_shutdown_function(function () { + register_shutdown_function(function () { // TODO: add a log here $lastError = error_get_last(); if (!empty($lastError) && $lastError['type'] == E_ERROR) { Common::sendResponseCode(500); diff --git a/app/core/Tracker/Action.php b/app/core/Tracker/Action.php index 5a7a3d9b0..d0caddcdc 100644 --- a/app/core/Tracker/Action.php +++ b/app/core/Tracker/Action.php @@ -11,10 +11,12 @@ use Exception; use Piwik\Common; +use Piwik\Container\StaticContainer; use Piwik\Piwik; use Piwik\Plugin\Dimension\ActionDimension; use Piwik\Plugin\Manager; use Piwik\Tracker; +use Psr\Log\LoggerInterface; /** * An action @@ -76,6 +78,11 @@ abstract class Action */ private $rawActionUrl; + /** + * @var mixed|LoggerInterface + */ + private $logger; + /** * Makes the correct Action object based on the request. * @@ -148,6 +155,7 @@ public function __construct($type, Request $request) { $this->actionType = $type; $this->request = $request; + $this->logger = StaticContainer::get(LoggerInterface::class); } /** @@ -202,8 +210,12 @@ protected function setActionUrl($url) $this->actionUrl = PageUrl::getUrlIfLookValid($url2); if ($url != $this->rawActionUrl) { - Common::printDebug(' Before was "' . $this->rawActionUrl . '"'); - Common::printDebug(' After is "' . $url2 . '"'); + $this->logger->debug(' Before was "{rawActionUrl}"', [ + 'rawActionUrl' => $this->rawActionUrl, + ]); + $this->logger->debug(' After is "{url2}"', [ + 'url2' => $url2, + ]); } } @@ -332,7 +344,7 @@ public function loadIdsFromLogActionTable() $actionId = $dimension->getActionId(); $actions[$field] = array($value, $actionId); - Common::printDebug("$field = $value"); + $this->logger->debug("$field = $value"); } } @@ -403,10 +415,11 @@ public function record(Visitor $visitor, $idReferrerActionUrl, $idReferrerAction $visitAction['idlink_va'] = $this->idLinkVisitAction; - Common::printDebug("Inserted new action:"); $visitActionDebug = $visitAction; $visitActionDebug['idvisitor'] = bin2hex($visitActionDebug['idvisitor']); - Common::printDebug($visitActionDebug); + $this->logger->debug("Inserted new action: {action}", [ + 'action' => var_export($visitActionDebug, true), + ]); } public function writeDebugInfo() @@ -415,9 +428,11 @@ public function writeDebugInfo() $name = $this->getActionName(); $url = $this->getActionUrl(); - Common::printDebug("Action is a $type, - Action name = " . $name . ", - Action URL = " . $url); + $this->logger->debug('Action is a {type}, Action name = {name}, Action URL = {url}', [ + 'type' => $type, + 'name' => $name, + 'url' => $url, + ]); return true; } diff --git a/app/core/Tracker/Cache.php b/app/core/Tracker/Cache.php index cd6061446..f20223d23 100644 --- a/app/core/Tracker/Cache.php +++ b/app/core/Tracker/Cache.php @@ -13,9 +13,11 @@ use Piwik\Cache as PiwikCache; use Piwik\Common; use Piwik\Config; +use Piwik\Container\StaticContainer; use Piwik\Option; use Piwik\Piwik; use Piwik\Tracker; +use Psr\Log\LoggerInterface; /** * Simple cache mechanism used in Tracker to avoid requesting settings from mysql on every request @@ -116,7 +118,9 @@ public static function updateCacheWebsiteAttributes($idSite) * @param int $idSite The site ID to get attributes for. */ Piwik::postEvent('Tracker.Cache.getSiteAttributes', array(&$content, $idSite)); - Common::printDebug("Website $idSite tracker cache was re-created."); + + $logger = StaticContainer::get(LoggerInterface::class); + $logger->debug("Website $idSite tracker cache was re-created."); }); // if nothing is returned from the plugins, we don't save the content @@ -191,7 +195,9 @@ public static function updateGeneralCache() */ Piwik::postEvent('Tracker.setTrackerCacheGeneral', array(&$cacheContent)); self::setCacheGeneral($cacheContent); - Common::printDebug("General tracker cache was re-created."); + + $logger = StaticContainer::get(LoggerInterface::class); + $logger->debug("General tracker cache was re-created."); Tracker::restoreTrackerPlugins(); diff --git a/app/core/Tracker/Db.php b/app/core/Tracker/Db.php index c07b26ebb..c3d1eeb98 100644 --- a/app/core/Tracker/Db.php +++ b/app/core/Tracker/Db.php @@ -261,13 +261,13 @@ public static function factory($configDb) */ Piwik::postEvent('Tracker.getDatabaseConfig', array(&$configDb)); - $className = 'Piwik\Tracker\Db\\' . str_replace(' ', '\\', ucwords(str_replace(array('_', '\\'), ' ', strtolower($configDb['adapter'])))); + $className = 'Piwik\Tracker\Db\\' . str_replace(' ', '\\', ucwords(str_replace(array('_', '\\'), ' ', strtolower($configDb['adapter'])))); - if (!class_exists($className)) { - throw new Exception('Unsupported database adapter ' . $configDb['adapter']); - } + if (!class_exists($className)) { + throw new Exception('Unsupported database adapter ' . $configDb['adapter']); + } - return new $className($configDb); + return new $className($configDb); } public static function connectPiwikTrackerDb() diff --git a/app/core/Tracker/Handler.php b/app/core/Tracker/Handler.php index cb4707a01..f4c9fa933 100644 --- a/app/core/Tracker/Handler.php +++ b/app/core/Tracker/Handler.php @@ -10,11 +10,13 @@ namespace Piwik\Tracker; use Piwik\Common; +use Piwik\Container\StaticContainer; use Piwik\Exception\InvalidRequestParameterException; use Piwik\Exception\UnexpectedWebsiteFoundException; use Piwik\Tracker; use Exception; use Piwik\Url; +use Psr\Log\LoggerInterface; class Handler { @@ -28,9 +30,15 @@ class Handler */ private $tasksRunner; + /** + * @var LoggerInterface + */ + private $logger; + public function __construct() { $this->setResponse(new Response()); + $this->logger = StaticContainer::get(LoggerInterface::class); } public function setResponse($response) @@ -81,8 +89,6 @@ public function setScheduledTasksRunner(ScheduledTasksRunner $runner) public function onException(Tracker $tracker, RequestSet $requestSet, Exception $e) { - Common::printDebug("Exception: " . $e->getMessage()); - $statusCode = 500; if ($e instanceof UnexpectedWebsiteFoundException) { $statusCode = 400; @@ -90,6 +96,17 @@ public function onException(Tracker $tracker, RequestSet $requestSet, Exception $statusCode = 400; } + // if an internal server error, log as a real error, otherwise it's just malformed input + if ($statusCode == 500) { + $this->logger->error('Exception: {exception}', [ + 'exception' => $e, + ]); + } else { + $this->logger->debug('Exception: {exception}', [ + 'exception' => $e, + ]); + } + $this->response->outputException($tracker, $e, $statusCode); $this->redirectIfNeeded($requestSet); } diff --git a/app/core/Tracker/LogTable.php b/app/core/Tracker/LogTable.php index b077069f9..164abb33b 100644 --- a/app/core/Tracker/LogTable.php +++ b/app/core/Tracker/LogTable.php @@ -79,6 +79,17 @@ public function shouldJoinWithSubSelect() { return false; } + + /** + * Defines a column that stores the date/time at which time an entry was written or updated. Setting this + * can help improve the performance of some archive queries. For example the log_link_visit_action table would define + * server_time while log_visit would define visit_last_action_time + * @return string + */ + public function getDateTimeColumn() + { + return ''; + } /** * Returns the name of a log table that allows to join on a visit. Eg if there is a table "action", and it is not diff --git a/app/core/Tracker/Model.php b/app/core/Tracker/Model.php index 7564ab6e0..f7a8406a1 100644 --- a/app/core/Tracker/Model.php +++ b/app/core/Tracker/Model.php @@ -10,7 +10,9 @@ use Exception; use Piwik\Common; +use Piwik\Container\StaticContainer; use Piwik\Tracker; +use Psr\Log\LoggerInterface; class Model { @@ -76,7 +78,9 @@ public function updateConversion($idVisit, $idGoal, $newConversion) try { $this->getDb()->query($sql, $sqlBind); } catch (Exception $e) { - Common::printDebug("There was an error while updating the Conversion: " . $e->getMessage()); + StaticContainer::get(LoggerInterface::class)->error("There was an error while updating the Conversion: {exception}", [ + 'exception' => $e, + ]); return false; } @@ -313,7 +317,7 @@ public function updateVisit($idSite, $idVisit, $valuesToUpdate) { list($updateParts, $sqlBind) = $this->fieldsToQuery($valuesToUpdate); - $parts = implode($updateParts, ', '); + $parts = implode(', ',$updateParts); $table = Common::prefixTable('log_visit'); $sqlQuery = "UPDATE $table SET $parts WHERE idsite = ? AND idvisit = ?"; diff --git a/app/core/Tracker/Request.php b/app/core/Tracker/Request.php index e1c59f5e0..fce9b8f0c 100644 --- a/app/core/Tracker/Request.php +++ b/app/core/Tracker/Request.php @@ -502,6 +502,18 @@ protected function getCustomTimestamp() } } + $cache = Tracker\Cache::getCacheGeneral(); + if (!empty($cache['delete_logs_enable']) && !empty($cache['delete_logs_older_than'])) { + $scheduleInterval = $cache['delete_logs_schedule_lowest_interval']; + $maxLogAge = $cache['delete_logs_older_than']; + $logEntryCutoff = time() - (($maxLogAge + $scheduleInterval) * 60*60*24); + if ($cdt < $logEntryCutoff) { + $message = "Custom timestamp is older than the configured 'deleted old raw data' value of $maxLogAge days"; + Common::printDebug($message); + throw new InvalidRequestParameterException($message); + } + } + return $cdt; } diff --git a/app/core/Tracker/Settings.php b/app/core/Tracker/Settings.php index 06657d097..6db7b998d 100644 --- a/app/core/Tracker/Settings.php +++ b/app/core/Tracker/Settings.php @@ -9,8 +9,9 @@ namespace Piwik\Tracker; use Piwik\Config; +use Piwik\Container\StaticContainer; use Piwik\Tracker; -use Piwik\DeviceDetectorFactory; +use Piwik\DeviceDetector\DeviceDetectorFactory; use Piwik\SettingsPiwik; class Settings // TODO: merge w/ visitor recognizer or make it it's own service. the class name is required for BC. @@ -37,7 +38,7 @@ public function getConfigId(Request $request, $ipAddress) $userAgent = $request->getUserAgent(); - $deviceDetector = DeviceDetectorFactory::getInstance($userAgent); + $deviceDetector = StaticContainer::get(DeviceDetectorFactory::class)->makeInstance($userAgent); $aBrowserInfo = $deviceDetector->getClient(); if ($aBrowserInfo['type'] != 'browser') { diff --git a/app/core/Tracker/TrackerCodeGenerator.php b/app/core/Tracker/TrackerCodeGenerator.php index c1081cc06..ba5ce0a7d 100644 --- a/app/core/Tracker/TrackerCodeGenerator.php +++ b/app/core/Tracker/TrackerCodeGenerator.php @@ -247,7 +247,7 @@ private function getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasU foreach ($websiteUrls as $site_url) { $referrerParsed = parse_url($site_url); - if (!isset($firstHost)) { + if (!isset($firstHost) && isset($referrerParsed['host'])) { $firstHost = $referrerParsed['host']; } @@ -267,4 +267,17 @@ private function getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasU } return $options; } + + /** + * When including the JS tracking code in a mailto link, we need to strip the surrounding HTML tags off. This + * ensures consistent behaviour between mail clients that render the mailto body as plain text (as in the + * spec), and those which try to render it as HTML and therefore hide the tags. + * @param string $jsTrackingCode JS tracking code as returned from the generate() function. + * @return string + */ + public static function stripTags($jsTrackingCode) + { + // Strip off open and close +{% endif %} \ No newline at end of file diff --git a/app/plugins/Login/Controller.php b/app/plugins/Login/Controller.php index a1c81f680..b5ce90908 100644 --- a/app/plugins/Login/Controller.php +++ b/app/plugins/Login/Controller.php @@ -143,6 +143,10 @@ function login($messageNoAccess = null, $infoMessage = false) $messageNoAccess = $this->getMessageExceptionNoAccess(); } } + + if ($messageNoAccess) { + http_response_code(403); + } $view = new View('@Login/login'); $view->AccessErrorString = $messageNoAccess; diff --git a/app/plugins/Login/config/tracker.php b/app/plugins/Login/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Login/config/tracker.php +++ b/app/plugins/Login/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Login/lang/de.json b/app/plugins/Login/lang/de.json index a5d472dca..087c74731 100644 --- a/app/plugins/Login/lang/de.json +++ b/app/plugins/Login/lang/de.json @@ -36,6 +36,7 @@ "PasswordChanged": "Das Passwort wurde geändert.", "PasswordRepeat": "Passwort (wiederholen)", "PasswordsDoNotMatch": "Die Passwörter stimmen nicht überein.", + "PasswordResetAlreadySent": "Sie haben in der letzten Zeit zu viele Anfragen für Passwortzurücksetzungen erstellt. Eine neue Anfrage kann frühestens in einer Stunde erfolgen. Falls Sie Probleme beim Zurücksetzen Ihres Passworts haben, kontaktieren Sie bitte Ihren Administrator.", "WrongPasswordEntered": "Bitte geben Sie das korrekte Passwort ein.", "ConfirmPasswordToContinue": "Bitte bestätigen Sie Ihr Passwort um fortzufahren", "PluginDescription": "Unterstützt Authentifizierung via Benutzername und Passwort sowie Password Reset Funktionalität. Die Authentifizierungsmethode kann durch ein anderes Login Plugin wie LoginLdap (verfügbar im Marketplace) geändert werden.", diff --git a/app/plugins/Login/lang/el.json b/app/plugins/Login/lang/el.json index 370c35e42..7eb7441ee 100644 --- a/app/plugins/Login/lang/el.json +++ b/app/plugins/Login/lang/el.json @@ -36,7 +36,7 @@ "PasswordChanged": "Ο κωδικός σας έχει αλλάξει.", "PasswordRepeat": "Κωδικός (επανάληψη)", "PasswordsDoNotMatch": "Οι κωδικοί δεν ταιριάζουν.", - "PasswordResetAlreadySent": "Έχετε ζητήσει πολλές επαναφορές συνθηματικού πρόσφατα. Μια νέα αίτηση μπορεί να γίνει σε μία ώρα. Αν έχετε προβλήματα με την επαναφορά του συνθηματικού σας, επικοινωνήστε με τον διαχειριστή σας για βοήθεια.", + "PasswordResetAlreadySent": "Έχετε ζητήσει πολλές επαναφορές συνθηματικού πρόσφατα. Νέα αίτηση επαναφοράς συνθηματικού μπορεί να γίνει μετά από μία ώρα. Αν έχετε προβλήματα με την επαναφορά του συνθηματικού σας, επικοινωνήστε με τον διαχειριστή σας για βοήθεια.", "WrongPasswordEntered": "Παρακαλώ εισάγετε το σωστό συνθηματικό σας.", "ConfirmPasswordToContinue": "Επιβεβαιώστε το συνθηματικό σας για να συνεχίσετε", "PluginDescription": "Παρέχει αυθεντικοποίηση μέσω ονόματος χρήστη και συνθηματικού όπως επίσης και λειτουργικότητα επαναφοράς συνθηματικού. Η μέθοδος αυθεντικοποίησης μπορεί να αλλαχθεί χρησιμοποιώντας άλλο πρόσθετο, όπως το LoginLdap που υπάρχει διαθέσιμο στην Αγορά.", diff --git a/app/plugins/Login/lang/es-ar.json b/app/plugins/Login/lang/es-ar.json index 3b7eb5aff..565af2f95 100644 --- a/app/plugins/Login/lang/es-ar.json +++ b/app/plugins/Login/lang/es-ar.json @@ -1,20 +1,44 @@ { "Login": { + "BruteForceLog": "Registro de fuerza bruta", "ConfirmationLinkSent": "Se envió un enlace de confirmación a tu cuenta de correo. Revisá tu email y visitá ese enlace para autorizar tu solicitud de cambio de contraseña.", + "ContactAdmin": "Posible razón: tu servidor puede tener deshabilitado la función mail().
Por favor, contactá a tu administrador de Matomo.", "ExceptionInvalidSuperUserAccessAuthenticationMethod": "Un usuario con acceso a súperusuario no puede ser autenticado usando el mecanismo \"%s\".", "ExceptionPasswordMD5HashExpected": "Se espera que el parámetro contraseña sea un chequeo de integridad MD5 de la contraseña.", + "InvalidNonceOrHeadersOrReferrer": "Falló la seguridad del formulario. Por favor, recargá el formulario y comprobá que tus cookies estén habilitadas. Si usás un proxy, tenés que %1$s configurar Matomo para aceptar la cabecera \"Proxy\"%2$s que precede la cabecera \"Host\". También revisá que tu cabecera \"Referer\" se envíe correctamente.", + "InvalidNonceSSLMisconfigured": "También podés %1$s forzar Matomo a usar una conexión segura%2$s: en tu archivo de configuración %3$s establecé %4$s debajo de la sección %5$s", "InvalidOrExpiredToken": "El indicio no es válido o venció.", "InvalidUsernameEmail": "Nombre de usuario y\/o dirección de correo electrónico no válidos.", "LogIn": "Iniciar sesión", + "LoginOrEmail": "Usuario o correo electrónico", + "HelpIpRange": "Introducí una dirección IP o un rango IP por línea. Podés usar notación CIDR, por ejemplo %1$s o podés usar comodines, por ejemplo: %2$s o %3$s.", + "SettingBruteForceEnable": "Habilitar detección de fuerza bruta", + "SettingBruteForceEnableHelp": "La detección de fuerza bruta es una importante función de seguridad usada para proteger tus datos de accesos no autorizados. En lugar de permitirle a cualquier usuario probar miles o millones de combinaciones posibles en un muy corto período de tiempo, sólo se le permitirá una cantidad específica de inicios de sesión fallidos dentro de un corto período de tiempo. Si se dan muchos intentos fallidos de inicio de sesión en ese margen de tiempo, el usuario no podrá iniciar sesión pasado un tiempo. Por favor, tené en cuenta que si se bloquea una dirección IP, cada usuario que use esa misma dirección IP será también impedido de iniciar sesión.", + "SettingBruteForceWhitelistIp": "Nunca bloquear estas direcciones IP para iniciar sesión", + "SettingBruteForceBlacklistIp": "Siempre bloquear estas direcciones IP para iniciar sesión", + "SettingBruteForceMaxFailedLogins": "Número de intentos de inicio de sesión permitidos dentro del margen de tiempo", + "SettingBruteForceMaxFailedLoginsHelp": "Si se registan más cantidades de inicios fallidos de sesión que estos, dentro del margen de tiempo configurado abajo, bloquear la dirección IP.", + "SettingBruteForceTimeRange": "Número de intentos de inicio de sesión dentro del margen de tiempo en minutos", + "SettingBruteForceTimeRangeHelp": "Ingresá los minútos en números", + "LoginNotAllowedBecauseBlocked": "Actualmente no se te permite iniciar sesión debido a demasiados intentos fallidos. Por favor, intentá más tarde.", + "CurrentlyBlockedIPs": "Direcciones IP bloqueadas actualmente", + "IPsAlwaysBlocked": "Estas direcciones IP están siempre bloqueadas", + "UnblockAllIPs": "Desbloquear todas las direcciones IP bloqueadas actualmente", + "CurrentlyBlockedIPsUnblockInfo": "Podés desbloquear direcciones IP que están actualmente bloqueadas, así se puede iniciar sesión de nuevo, en caso de una falsa alarma y ante la necesidad de ingresar nuevamente.", + "CurrentlyBlockedIPsUnblockConfirm": "¿Estás seguro que querés desbloquear todas las direcciones IP bloqueadas actualmente?", "LoginPasswordNotCorrect": "Combinación no válida de usuario y contraseña.", "LostYourPassword": "¿Te olvidaste la contraseña?", "ChangeYourPassword": "Cambiá tu contraseña", + "MailPasswordChangeBody2": "Hola, %1$s.\n\nSe recibió una solicitud de restablecimiento de contraseña desde %2$s. Para confirmar este cambio de contraseña e iniciar con tus nuevas credenciales, por favor, copiá y pegá el siguiente enlace en tu navegador web:\n\n%3$s\n\nNota: este enlace vencerá en 24 horas.\n\n¡Gracias por usar Matomo!", "MailTopicPasswordChange": "Confirmar el cambio de contraseña", "NewPassword": "Nueva contraseña", "NewPasswordRepeat": "Nueva contraseña (repetirla)", "PasswordChanged": "Se cambió tu contraseña.", "PasswordRepeat": "Contraseña (repetirla)", "PasswordsDoNotMatch": "Las contraseñas no coinciden.", + "PasswordResetAlreadySent": "Solicitaste demasiadas solicitudes de restablecimientos de contraseña recientemente. Una nueva solicitud se podrá hacer dentro de una hora. Si tenés problemas para restablecer tu contraseña, por favor, solicitale ayuda a tu administrador.", + "WrongPasswordEntered": "Por favor, ingresá tu contraseña correcta.", + "ConfirmPasswordToContinue": "Confirmá tu contraseña para seguir", "PluginDescription": "Ofrece autenticación vía nombre de usuario y contraseña, así como función de restablecimiento de contraseña. El método de autenticación se puede cambiar al usar otro plugin de inicio de sesión, como LoginLdap, disponible en el mercado.", "RememberMe": "Recordarme" } diff --git a/app/plugins/Login/lang/es.json b/app/plugins/Login/lang/es.json index 718ebcd10..f1204c983 100644 --- a/app/plugins/Login/lang/es.json +++ b/app/plugins/Login/lang/es.json @@ -36,7 +36,7 @@ "PasswordChanged": "Su contraseña ha sido modificada.", "PasswordRepeat": "Contraseña (repetir)", "PasswordsDoNotMatch": "Las contraseñas no coinciden.", - "PasswordResetAlreadySent": "Ha solicitado demasiados restablecimientos de contraseña en un breve período. Podrá realizar una nueva petición en una hora. Si tiene problemas para restablecer su contraseña, comuníquese con su administrador para obtener ayuda.", + "PasswordResetAlreadySent": "Ha solicitado demasiados restablecimientos de contraseña recientemente. Podrá realizar una nueva petición en una hora. Si tiene problemas para restablecer su contraseña, comuníquese con su administrador para obtener ayuda.", "WrongPasswordEntered": "Por favor ingrese correctamente su contraseña.", "ConfirmPasswordToContinue": "Confirme su contraseña a continuación", "PluginDescription": "Proporciona autentificación vía un nombre de usuario y contraseña así como también la funcionalidad de reestablecimiento de contraseña. El método de autentificación puede ser modificado usando otro complemento de inicio de sesión, tal como LoginLdap que se encuentra disponible en el Mercado.", diff --git a/app/plugins/Login/lang/fr.json b/app/plugins/Login/lang/fr.json index 31b0b6ca6..6b8e2b381 100644 --- a/app/plugins/Login/lang/fr.json +++ b/app/plugins/Login/lang/fr.json @@ -36,6 +36,7 @@ "PasswordChanged": "Votre mot de passe a été modifié", "PasswordRepeat": "Mot de passe (répétez)", "PasswordsDoNotMatch": "Les mots de passe ne correspondent pas.", + "PasswordResetAlreadySent": "Vous avez demandé trop de changements de mot de passe récemment. Une nouvelle demande pourra être faite dans une heure. Si vous rencontrez des problèmes pour changer votre mot de passe, merci de contacter votre administrateur pour obtenir de l'aide.", "WrongPasswordEntered": "Veuillez entrer votre mot de passe correctement.", "ConfirmPasswordToContinue": "Confirmez votre mot de passe pour continuer", "PluginDescription": "Fournit une authentification via nom d'utilisateur et mot de passe ainsi qu'une fonctionnalité de réinitialisation. La méthode d'authentification peut être changée en utilisant un autre composant d'identification comme LoginLdap disponible depuis le Marché.", diff --git a/app/plugins/Login/lang/it.json b/app/plugins/Login/lang/it.json index 2eda582f6..19551b9f6 100644 --- a/app/plugins/Login/lang/it.json +++ b/app/plugins/Login/lang/it.json @@ -36,6 +36,7 @@ "PasswordChanged": "La tua password è stata cambiata.", "PasswordRepeat": "Password (ripeti)", "PasswordsDoNotMatch": "La password non corrisponde.", + "PasswordResetAlreadySent": "Di recente hai richiesto troppi ripristini di password. Una nuova richiesta può essere effettuata tra un'ora. In caso di problemi con la reimpostazione della password, contatta l'amministratore per avere assistenza.", "WrongPasswordEntered": "Si prega di inserire la tua password corretta.", "ConfirmPasswordToContinue": "Conferma la tua password per continuare", "PluginDescription": "Fornisce l'autenticazione tramite user name e password, e anche la funzione di reset della password. Il metodo di autenticazione può essere cambiato utilizzando un altro plugin di accesso, come LoginLdap disponibile nel Marketplace.", diff --git a/app/plugins/Login/lang/ja.json b/app/plugins/Login/lang/ja.json index a8be360e1..e3da4d2ae 100644 --- a/app/plugins/Login/lang/ja.json +++ b/app/plugins/Login/lang/ja.json @@ -36,6 +36,7 @@ "PasswordChanged": "パスワードが変更されました。", "PasswordRepeat": "パスワード(再入力)", "PasswordsDoNotMatch": "パスワードが一致しません。", + "PasswordResetAlreadySent": "最近リクエストしたパスワードのリセットが多すぎます。 新しいリクエストは1時間で作成できます。 パスワードのリセットに問題がある場合は、管理者に連絡してください。", "WrongPasswordEntered": "正しいパスワードを入力してください。", "ConfirmPasswordToContinue": "続行するにはパスワードを確認してください", "PluginDescription": "パスワードリセットの機能と同様にユーザ名とパスワードで認証を提供します。 Marketplace で利用可能な LoginLdap など別の Login プラグインを使用することによって、認証方法を変えることができます。", diff --git a/app/plugins/Login/lang/pt-br.json b/app/plugins/Login/lang/pt-br.json index d0115cb25..1d009e4e6 100644 --- a/app/plugins/Login/lang/pt-br.json +++ b/app/plugins/Login/lang/pt-br.json @@ -1,23 +1,45 @@ { "Login": { + "BruteForceLog": "Log de força bruta", "ConfirmationLinkSent": "Um link de confirmação foi enviado para a sua caixa de entrada. Verifique seu e-mail e acesse o link para autorizar seu pedido de alteração de senha.", + "ContactAdmin": "Possível motivo: seu host pode ter desabilitado a função mail().
Por favor entre em contato com o seu administrador do Matomo.", "ExceptionInvalidSuperUserAccessAuthenticationMethod": "Um usuário com acesso de Super Usuário não pode ser autenticado usando o mecanismo '%s'.", "ExceptionPasswordMD5HashExpected": "É esperado que o parâmetro da senha seja um hash MD5 da senha.", - "InvalidNonceOrHeadersOrReferrer": "O formulário de segurança falhou. Por favor, recarregue o formulário e verifique se seus cookies estão habilitados. Se você usa um servidor proxy, você deve configurar %1$s Matomo para aceitar proxy header%2$s que encaminhe para o Host header. Além disso, verifique se o header do Referenciador esta sendo enviada corretamente.", - "InvalidNonceSSLMisconfigured": "Além disso, você pode %1$s forçar o Matomo a usar uma conexão segura%2$s: em seu arquivo de configuração %3$s configure %4$s abaixo da secção %5$s", - "InvalidOrExpiredToken": "Token inválido ou expirado", - "InvalidUsernameEmail": "Nome de usuário ou e-mail inválido", + "InvalidNonceOrHeadersOrReferrer": "O formulário de segurança falhou. Por favor recarregue o formulário e verifique se seus cookies estão habilitados. Se você usa um servidor proxy, você deve %1$s configurar o Matomo para aceitar o cabeçalho do proxy%2$s que encaminha o cabeçalho do host. Além disso, verifique se o cabeçalho do Referenciador está sendo enviado corretamente.", + "InvalidNonceSSLMisconfigured": "Além disso, você pode %1$s forçar o Matomo a usar uma conexão segura%2$s: em seu arquivo de configuração %3$s configure %4$s abaixo da seção %5$s", + "InvalidOrExpiredToken": "O token é inválido ou expirou.", + "InvalidUsernameEmail": "Nome de usuário ou e-mail inválido.", "LogIn": "Entrar", - "LoginPasswordNotCorrect": "Combinação errada de Nome de Usuário e senha.", - "LostYourPassword": "Esqueceu a sua senha?", + "LoginOrEmail": "Nome de usuário ou Email", + "HelpIpRange": "Digite um endereço IP ou um intervalo de IPs por linha. Você pode usar notação CIDR ex. %1$s ou você pode usar caracteres curinga, ex. %2$s ou %3$s", + "SettingBruteForceEnable": "Habilitar detecção de força bruta", + "SettingBruteForceEnableHelp": "A detecção de força bruta é um importante recurso de segurança usado para proteger seus dados de acessos não autorizados. Ao invés de permitir a qualquer usuário tentar milhares ou milhões de combinações de senhas em pouco tempo, ela irá permitir apenas uma quantidade específica de logins com falha em um curto período de tempo. Se muitas falhas de login ocorrerem nesse período, o usuário não será capaz de fazer login por um certo tempo. Por favor note que se um IP estiver bloqueado, todos os usuários que usam aquele IP estarão bloqueados de fazer login também.", + "SettingBruteForceWhitelistIp": "Nunca bloquear estes IPs de fazer login", + "SettingBruteForceBlacklistIp": "Sempre bloquear estes IPs de fazer login", + "SettingBruteForceMaxFailedLogins": "Número de tentativas de login no período de tempo", + "SettingBruteForceMaxFailedLoginsHelp": "Se mais que esse número de logins com falha forem registrados dentro do período de tempo configurado abaixo, bloqueie o IP.", + "SettingBruteForceTimeRange": "Contar tentativas de login dentro deste período de tempo em minutos", + "SettingBruteForceTimeRangeHelp": "Digite um número em minutos.", + "LoginNotAllowedBecauseBlocked": "Você não tem permissão de fazer login atualmente porque você realizou muitas tentativas, tente novamente mais tarde.", + "CurrentlyBlockedIPs": "IPs atualmente bloqueados", + "IPsAlwaysBlocked": "Estes IPs estão sempre bloqueados", + "UnblockAllIPs": "Desbloquear todos os IPs atualmente bloqueados", + "CurrentlyBlockedIPsUnblockInfo": "Você pode desbloquear IPs que estão atualmente bloqueados para que eles possam voltar a fazer login caso eles tenham sido reportados erroneamente e precisem ser capazes de voltar a fazer login.", + "CurrentlyBlockedIPsUnblockConfirm": "Você tem certeza que deseja desbloquear todos os IPs atualmente bloqueados?", + "LoginPasswordNotCorrect": "Combinação errada de nome de usuário e senha.", + "LostYourPassword": "Esqueceu sua senha?", "ChangeYourPassword": "Trocar sua senha", - "MailTopicPasswordChange": "Confirme Alteração de Senha", + "MailPasswordChangeBody2": "Olá, %1$s,\n\nUm pedido de redefinição de senha foi recebido de %2$s. Para confirmar esta alteração de senha para que você possa fazer login com suas novas credenciais, por favor copie e cole o seguinte link no seu navegador:\n\n%3$s\n\nObs: este link irá expirar em 24 horas.\n\nE obrigado por usar o Matomo!", + "MailTopicPasswordChange": "Confirmar alteração de senha", "NewPassword": "Nova senha", "NewPasswordRepeat": "Nova senha (repetir)", "PasswordChanged": "Sua senha foi alterada.", "PasswordRepeat": "Senha (repetir)", "PasswordsDoNotMatch": "Senhas não conferem.", - "PluginDescription": "Fornece autenticação através do nome de usuário e senha, bem como a funcionalidade de redefinição de senha. O método de autenticação pode ser alterado utilizando outro plug-in, como, por exemplo, o LoginLdap disponível no Mercado.", - "RememberMe": "Lembrar-me" + "PasswordResetAlreadySent": "Você solicitou muitas redefinições de senha recentemente. Uma nova solicitação pode ser feita em uma hora. Se você tiver problemas ao redefinir sua senha, por favor entre em contato com o seu administrador para obter ajuda.", + "WrongPasswordEntered": "Por favor digite sua senha correta.", + "ConfirmPasswordToContinue": "Confirme sua senha para continuar", + "PluginDescription": "Fornece autenticação através do nome de usuário e senha, bem como a funcionalidade de redefinição de senha. O método de autenticação pode ser alterado utilizando outro plugin de login, como o LoginLdap disponível no Mercado.", + "RememberMe": "Lembrar de mim" } } \ No newline at end of file diff --git a/app/plugins/Login/lang/pt.json b/app/plugins/Login/lang/pt.json index bb7a73ca9..575203c08 100644 --- a/app/plugins/Login/lang/pt.json +++ b/app/plugins/Login/lang/pt.json @@ -24,6 +24,7 @@ "CurrentlyBlockedIPs": "IPs atualmente bloqueados", "IPsAlwaysBlocked": "Estes IPs são sempre bloqueados", "UnblockAllIPs": "Desbloquear todos os endereços de IP atualmente bloqueados", + "CurrentlyBlockedIPsUnblockInfo": "Pode desbloquear IPs que estejam atualmente bloqueados, para que possam iniciar sessão novamente caso tenham sido erroneamente selecionados e necessitem de iniciar sessão novamente.", "CurrentlyBlockedIPsUnblockConfirm": "Tem a certeza que pretende desbloquear todos os IPs atualmente bloqueados?", "LoginPasswordNotCorrect": "Combinação de nome de utilizador e palavra-passe incorreta.", "LostYourPassword": "Perdeu a sua palavra-passe?", @@ -35,6 +36,7 @@ "PasswordChanged": "A sua palavra-passe foi alterada.", "PasswordRepeat": "Palavra-passe (repetir)", "PasswordsDoNotMatch": "As palavras-passe não coincidem.", + "PasswordResetAlreadySent": "Procedeu a demasiados pedidos de alteração de palavra-passe recentemente. Pode realizar um novo pedido dentro de uma hora. Caso se depare com algum problema no processo de alteração de palavra-passe, por favor entre em contacto com o administrador para apoio.", "WrongPasswordEntered": "Por favor, introduza a sua palavra-passe correta.", "ConfirmPasswordToContinue": "Confirme a sua palavra-passe para prosseguir", "PluginDescription": "Fornece autenticação via nome de utilizador e palavra-passe bem como a funcionalidade de reposição de palavra-passe. O método de autenticação pode ser alterado utilizando outra extensão de autenticação, como o LoginLdap disponível na loja.", diff --git a/app/plugins/Login/lang/ru.json b/app/plugins/Login/lang/ru.json index ef81e7614..b5408f6c2 100644 --- a/app/plugins/Login/lang/ru.json +++ b/app/plugins/Login/lang/ru.json @@ -22,6 +22,7 @@ "CurrentlyBlockedIPs": "Заблокированные IP", "IPsAlwaysBlocked": "Эти IP всегда заблокированы", "UnblockAllIPs": "Разблокировать все заблокированные IP", + "CurrentlyBlockedIPsUnblockInfo": "Вы можете разблокировать IP-адреса, которые в настоящее время заблокированы, чтобы они могли войти снова, если они были ложно помечены и должны иметь возможность войти снова.", "CurrentlyBlockedIPsUnblockConfirm": "Вы уверены, что хотите разблокировать все заблокированные сейчас IP?", "LoginPasswordNotCorrect": "Логин или пароль неверны", "LostYourPassword": "Потеряли пароль?", @@ -32,6 +33,7 @@ "PasswordChanged": "Ваш пароль был изменен.", "PasswordRepeat": "Пароль еще раз", "PasswordsDoNotMatch": "Пароли не совпадают.", + "PasswordResetAlreadySent": "Вы недавно запросили слишком много сбросов пароля. Новый запрос может быть сделан через один час. Если у вас возникли проблемы со сбросом пароля, обратитесь за помощью к администратору.", "WrongPasswordEntered": "Пожалуйста, введите ваш правильный пароль.", "ConfirmPasswordToContinue": "Подтвердите пароль для продолжения", "PluginDescription": "Предоставляет авторизацию через имя пользователя и пароль, а также функцию сброса пароля. Способ авторизации может быть изменён если использовать другой Login плагин, такой как LoginLdap, доступный через Marketplace.", diff --git a/app/plugins/Login/lang/sq.json b/app/plugins/Login/lang/sq.json index 3ee239eff..96d3038db 100644 --- a/app/plugins/Login/lang/sq.json +++ b/app/plugins/Login/lang/sq.json @@ -36,6 +36,7 @@ "PasswordChanged": "Fjalëkalimi juaj u ndryshua.", "PasswordRepeat": "Fjalëkalim (sërish)", "PasswordsDoNotMatch": "Fjalëkalimet nuk përputhen.", + "PasswordResetAlreadySent": "Tani së fundi keni kërkuar shumë ricaktime fjalëkalimi. Një kërkesë e re mund të bëhet pas një ore. Nëse keni probleme me ricaktimin e fjalëkalimit tuaj, ju lutemi, për ndihmë, lidhuni me përgjegjësin tuaj.", "WrongPasswordEntered": "Ju lutemi, jepni fjalëkalimin tuaj të saktë.", "ConfirmPasswordToContinue": "Që të vazhdohet, ripohoni fjalëkalimin tuaj", "PluginDescription": "Ofron mirëfilltësim përmes emri përdoruesi dhe fjalëkalimi, si dhe funksionin e ricaktimit të fjalëkalimeve. Metoda e mirëfilltësimit mund të ndryshohet duke përdorur një tjetër shtojcë Hyrjesh, të tillë si LoginLdap, të cilën e gjeni te Marketplace-i.", diff --git a/app/plugins/Login/stylesheets/login.less b/app/plugins/Login/stylesheets/login.less index 54cf9acdc..c9ed140a8 100644 --- a/app/plugins/Login/stylesheets/login.less +++ b/app/plugins/Login/stylesheets/login.less @@ -3,14 +3,14 @@ #loginPage { #logo { - padding-top: 5px; + padding-top: 6px; img.default-piwik-logo { width: 171px; } img { - max-height: 30px; + max-height: 32px; } } diff --git a/app/plugins/Marketplace/Categories/BrowseSubcategory.php b/app/plugins/Marketplace/Categories/BrowseSubcategory.php new file mode 100644 index 000000000..3a49ba31d --- /dev/null +++ b/app/plugins/Marketplace/Categories/BrowseSubcategory.php @@ -0,0 +1,19 @@ +isAutoUpdatePossible = SettingsPiwik::isAutoUpdatePossible(); $view->isAutoUpdateEnabled = SettingsPiwik::isAutoUpdateEnabled(); $view->isPluginUploadEnabled = CorePluginsAdmin::isPluginUploadEnabled(); + $view->inReportingMenu = (bool) Common::getRequestVar('embed', 0, 'int'); return $view->render(); } diff --git a/app/plugins/Marketplace/Marketplace.php b/app/plugins/Marketplace/Marketplace.php index 28f0b156b..825f75860 100644 --- a/app/plugins/Marketplace/Marketplace.php +++ b/app/plugins/Marketplace/Marketplace.php @@ -77,8 +77,7 @@ public function getClientSideTranslationKeys(&$translationKeys) public function filterWidgets($list) { if (!SettingsPiwik::isInternetEnabled()) { - $list->remove(GetPremiumFeatures::getCategory(), GetPremiumFeatures::getName()); - $list->remove(GetNewPlugins::getCategory(), GetNewPlugins::getName()); + $list->remove('Marketplace_Marketplace'); } } diff --git a/app/plugins/Marketplace/Widgets/GetNewPlugins.php b/app/plugins/Marketplace/Widgets/GetNewPlugins.php index 2b1c7270d..9de478eae 100644 --- a/app/plugins/Marketplace/Widgets/GetNewPlugins.php +++ b/app/plugins/Marketplace/Widgets/GetNewPlugins.php @@ -9,6 +9,7 @@ namespace Piwik\Plugins\Marketplace\Widgets; use Piwik\Common; +use Piwik\Piwik; use Piwik\Plugins\Marketplace\Api\Client; use Piwik\Plugins\Marketplace\Input\PurchaseType; use Piwik\Plugins\Marketplace\Input\Sort; @@ -27,25 +28,18 @@ public function __construct(Client $marketplaceApiClient) $this->marketplaceApiClient = $marketplaceApiClient; } - public static function getCategory() - { - return 'About Matomo'; - } - - public static function getName() - { - return 'Latest Marketplace Updates'; - } - public static function configure(WidgetConfig $config) { - $config->setCategoryId(self::getCategory()); - $config->setName(self::getName()); + $config->setCategoryId('Marketplace_Marketplace'); + $config->setName('Marketplace_LatestMarketplaceUpdates'); $config->setOrder(19); + $config->setIsEnabled(!Piwik::isUserIsAnonymous()); } public function render() { + Piwik::checkUserIsNotAnonymous(); + $isAdminPage = Common::getRequestVar('isAdminPage', 0, 'int'); if (!empty($isAdminPage)) { diff --git a/app/plugins/Marketplace/Widgets/GetPremiumFeatures.php b/app/plugins/Marketplace/Widgets/GetPremiumFeatures.php index fa0909f7f..32ba683cc 100644 --- a/app/plugins/Marketplace/Widgets/GetPremiumFeatures.php +++ b/app/plugins/Marketplace/Widgets/GetPremiumFeatures.php @@ -29,25 +29,18 @@ public function __construct(Client $marketplaceApiClient) $this->marketplaceApiClient = $marketplaceApiClient; } - public static function getCategory() - { - return 'About Matomo'; - } - - public static function getName() - { - return Piwik::translate('Marketplace_PaidPlugins'); - } - public static function configure(WidgetConfig $config) { - $config->setCategoryId(self::getCategory()); - $config->setName(self::getName()); + $config->setCategoryId('Marketplace_Marketplace'); + $config->setSubcategoryId('Marketplace_PaidPlugins'); + $config->setName('Marketplace_PaidPlugins'); $config->setOrder(20); + $config->setIsEnabled(!Piwik::isUserIsAnonymous()); } public function render() { + Piwik::checkUserIsNotAnonymous(); $template = 'getPremiumFeatures'; $plugins = $this->marketplaceApiClient->searchForPlugins('', '', Sort::METHOD_LAST_UPDATED, PurchaseType::TYPE_PAID); diff --git a/app/plugins/Marketplace/Widgets/Marketplace.php b/app/plugins/Marketplace/Widgets/Marketplace.php new file mode 100644 index 000000000..2b09c2d5b --- /dev/null +++ b/app/plugins/Marketplace/Widgets/Marketplace.php @@ -0,0 +1,35 @@ +setCategoryId('Marketplace_Marketplace'); + $config->setSubcategoryId('Marketplace_Browse'); + $config->setName(Piwik::translate('Marketplace_Marketplace')); + $config->setModule('Marketplace'); + $config->setAction('overview'); + $config->setParameters(array('embed' => '1')); + $config->setIsNotWidgetizable(); + $config->setOrder(19); + $config->setIsEnabled(!Piwik::isUserIsAnonymous()); + } + + +} \ No newline at end of file diff --git a/app/plugins/Marketplace/config/tracker.php b/app/plugins/Marketplace/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Marketplace/config/tracker.php +++ b/app/plugins/Marketplace/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Marketplace/lang/da.json b/app/plugins/Marketplace/lang/da.json index 398c8c1c3..5678e617c 100644 --- a/app/plugins/Marketplace/lang/da.json +++ b/app/plugins/Marketplace/lang/da.json @@ -7,6 +7,7 @@ "AllowedUploadFormats": "Du kan via denne side overføre en programudvidelse eller tema i .zip-format.", "Authors": "Forfattere", "BackToMarketplace": "Tilbage til markedspladsen", + "BrowseMarketplace": "Browse Markedsplads", "ByXDevelopers": "af %s udviklere", "CannotInstall": "Kan ikke installere", "CannotUpdate": "Kan ikke opdatere", diff --git a/app/plugins/Marketplace/lang/de.json b/app/plugins/Marketplace/lang/de.json index b50e8e1b4..d7479fbf5 100644 --- a/app/plugins/Marketplace/lang/de.json +++ b/app/plugins/Marketplace/lang/de.json @@ -7,6 +7,8 @@ "AddToCart": "Zum Warenkorb hinzufügen", "AllowedUploadFormats": "Sie können über diese Seite ein Plugin oder Theme im .zip-Format hochladen.", "Authors": "Autoren", + "Browse": "Durchsuchen", + "LatestMarketplaceUpdates": "Neueste Marketplace Updates", "BackToMarketplace": "Zurück zum Marketplace", "BrowseMarketplace": "Marketplace durchsuchen", "ByXDevelopers": "von %s Entwicklern", @@ -17,6 +19,7 @@ "ConfirmRemoveLicense": "Siend Sie sicher, dass Sie diesen Lizenzschlüssel entfernen wollen? Sie werden für keines Ihrer gekauften Plugins mehr Updates erhalten.", "Developer": "Entwickler", "DevelopersLearnHowToDevelopPlugins": "Für Entwickler: Lernen Sie wie Sie Matomo erweitern und personalisieren können, in dem Sie %1$sPlugins oder Themes entwickeln%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "Sie können den Marketplace aus dem Berichtsmenü entfernen, indem Sie das %1$sWhite Label%2$s Plugin installieren.", "Marketplace": "Marketplace", "PaidPlugins": "Premium Funktionen", "FeaturedPlugin": "Top-Plugin", @@ -42,8 +45,8 @@ "UpgradeSubscription": "Abonnement erneuern", "ViewSubscriptionsSummary": "%1$sIhre Plugin Abonnements anzeigen.%2$s", "ViewSubscriptions": "Abonnements anzeigen", - "ExceptionLinceseKeyIsExpired": "Der Lizenzschlüssel ist abgelaufen.", - "ExceptionLinceseKeyIsNotValid": "Der Lizenzschlüssel ist nicht gültig.", + "ExceptionLinceseKeyIsExpired": "Dieser Lizenzschlüssel ist abgelaufen.", + "ExceptionLinceseKeyIsNotValid": "Dieser Lizenzschlüssel ist ungültig.", "LicenseKeyIsValidShort": "Lizenzschlüssel ist gültig!", "RemoveLicenseKey": "Lizenzschlüssel entfernen", "InstallAllPurchasedPlugins": "Installiere alle erworbenen Plugins auf einmal", diff --git a/app/plugins/Marketplace/lang/el.json b/app/plugins/Marketplace/lang/el.json index 1daeb18d4..9c37fb77c 100644 --- a/app/plugins/Marketplace/lang/el.json +++ b/app/plugins/Marketplace/lang/el.json @@ -7,6 +7,8 @@ "AddToCart": "Προσθήκη στο καλάθι", "AllowedUploadFormats": "Μπορείτε να ανεβάσετε ένα πρόσθετο ή θέμα σε μορφή .zip από αυτή τη σελίδα.", "Authors": "Συγγραφείς", + "Browse": "Περιήγηση", + "LatestMarketplaceUpdates": "Τελευταίες Ενημερώσεις στην Αγορά", "BackToMarketplace": "Πίσω στην Αγορά", "BrowseMarketplace": "Περιήγηση στην Αγορά", "ByXDevelopers": "από %s προγραμματιστές", @@ -17,6 +19,7 @@ "ConfirmRemoveLicense": "Είστε σίγουροι ότι επιθυμείτε να αφαιρέσετε την άδειά σας; Δε θα λαμβάνετε πλέον ενημερώσεις για οποιοδήποτε από τα αγορασμένα πρόσθετα.", "Developer": "Προγραμματιστής", "DevelopersLearnHowToDevelopPlugins": "Προγραμματιστές: Μάθετε πώς να επεκτείνετε και να προσαρμόζετε το Matomo %1$sδημιουργώντας πρόσθετα ή θέματα%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "Μπορείτε να αφαιρέσετε την Αγορά από το μενού αναφορών με εγκατάσταση του πρόσθετου %1$sWhite Label%2$s.", "Marketplace": "Αγορά", "PaidPlugins": "Χαρακτηριστικά επί πληρωμή", "FeaturedPlugin": "Προβαλλόμενο πρόσθετο", diff --git a/app/plugins/Marketplace/lang/en.json b/app/plugins/Marketplace/lang/en.json index 53a38401a..a84b87924 100644 --- a/app/plugins/Marketplace/lang/en.json +++ b/app/plugins/Marketplace/lang/en.json @@ -7,6 +7,8 @@ "AddToCart": "Add to cart", "AllowedUploadFormats": "You may upload a plugin or theme in .zip format via this page.", "Authors": "Authors", + "Browse": "Browse", + "LatestMarketplaceUpdates": "Latest Marketplace Updates", "BackToMarketplace": "Back to Marketplace", "BrowseMarketplace": "Browse Marketplace", "ByXDevelopers": "by %s developers", @@ -17,6 +19,7 @@ "ConfirmRemoveLicense": "Are you sure you want to remove your license key? You will no longer receive any updates for any of your purchased plugins.", "Developer": "Developer", "DevelopersLearnHowToDevelopPlugins": "Developers: Learn how you can extend and customize Matomo by %1$sdeveloping plugins or themes%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "You can remove the Marketplace from the reporting menu by installing the %1$sWhite Label%2$s plugin.", "Marketplace": "Marketplace", "PaidPlugins": "Premium Features", "FeaturedPlugin": "Featured plugin", diff --git a/app/plugins/Marketplace/lang/fa.json b/app/plugins/Marketplace/lang/fa.json index 0df0e993f..8b0dc2fa8 100644 --- a/app/plugins/Marketplace/lang/fa.json +++ b/app/plugins/Marketplace/lang/fa.json @@ -7,6 +7,7 @@ "AllowedUploadFormats": "شما می توانید یک پلاگین یا زیمنه (تم) را در قالب فایل زیپ در این صفحه آپلود نمایید.", "Authors": "نویسنده ها", "BackToMarketplace": "بازگشت به بازار", + "BrowseMarketplace": "باز کردن بازار", "ByXDevelopers": "توسط %s برنامه نویس", "Developer": "توسعه دهنده ها", "Marketplace": "بازار", diff --git a/app/plugins/Marketplace/lang/fi.json b/app/plugins/Marketplace/lang/fi.json index 0e397042d..1cfeeabe2 100644 --- a/app/plugins/Marketplace/lang/fi.json +++ b/app/plugins/Marketplace/lang/fi.json @@ -7,6 +7,8 @@ "AddToCart": "Lisää koriin", "AllowedUploadFormats": "Tällä sivulla voit lisätä liitännäisen tai teeman .zip-formaatissa.", "Authors": "Tekijät", + "Browse": "Selaa", + "LatestMarketplaceUpdates": "Viimeisimmät kaupan päivitykset", "BackToMarketplace": "Takaisin kauppaan", "BrowseMarketplace": "Selaa kauppaa", "ByXDevelopers": "%s kehittäjältä", diff --git a/app/plugins/Marketplace/lang/fr.json b/app/plugins/Marketplace/lang/fr.json index eccd25636..a9f6dbc3d 100644 --- a/app/plugins/Marketplace/lang/fr.json +++ b/app/plugins/Marketplace/lang/fr.json @@ -7,6 +7,8 @@ "AddToCart": "Ajoutier au panier", "AllowedUploadFormats": "Vous pouvez téléverser un composant additionnel ou un thème au format zip via cette page.", "Authors": "Auteurs", + "Browse": "Parcourir", + "LatestMarketplaceUpdates": "Dernières mises à jour du Marketplace.", "BackToMarketplace": "Retour au Marché", "BrowseMarketplace": "Parcourir le marché", "ByXDevelopers": "par %s développeurs", @@ -17,6 +19,7 @@ "ConfirmRemoveLicense": "Êtes-vous sûr(e) de vouloir supprimer votre clé de licence ? Vous ne recevrez plus aucune mise à jour des composants achetés.", "Developer": "Développeur", "DevelopersLearnHowToDevelopPlugins": "Développeurs : Apprenez comment vous pouvez étendre et personaliser Matomo en %1$sdévelopant des composants ou themes%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "Vous pouvez supprimer le Marketplace depuis le menu de rapports en installant le plug-in %1$sWhite Label%2$s.", "Marketplace": "Marché", "PaidPlugins": "Fonctionnalités Premium", "FeaturedPlugin": "Composant mis en avant", diff --git a/app/plugins/Marketplace/lang/it.json b/app/plugins/Marketplace/lang/it.json index d7ac751dc..3a6da4f5c 100644 --- a/app/plugins/Marketplace/lang/it.json +++ b/app/plugins/Marketplace/lang/it.json @@ -7,6 +7,8 @@ "AddToCart": "Aggiungi al carrello", "AllowedUploadFormats": "Tramite questa pagina puoi caricare un plugin o un tema nel formato .zip.", "Authors": "Autori", + "Browse": "Naviga", + "LatestMarketplaceUpdates": "Ultimi Aggiornamenti nel Marketplace", "BackToMarketplace": "Torna al Marketplace", "BrowseMarketplace": "Guarda nel Marketplace", "ByXDevelopers": "da %s sviluppatori", @@ -17,6 +19,7 @@ "ConfirmRemoveLicense": "Sei sicuro di voler rimuovere la tua chiave di licenza? Non potrai più ricevere gli aggiornamenti per nessuno dei plugin acquistati.", "Developer": "Sviluppatore", "DevelopersLearnHowToDevelopPlugins": "Per gli sviluppatori: Imparate come ampliare e personalizzare Matomo %1$ssviluppando plugin e temi%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "Puoi rimuovere il Marketplace dal menu dei report installando il plug-in %1$sWhite Label%2$s.", "Marketplace": "Marketplace", "PaidPlugins": "Funzionalità Premium", "FeaturedPlugin": "Plugin in evidenza", diff --git a/app/plugins/Marketplace/lang/ja.json b/app/plugins/Marketplace/lang/ja.json index bfff25b56..3e52505f5 100644 --- a/app/plugins/Marketplace/lang/ja.json +++ b/app/plugins/Marketplace/lang/ja.json @@ -7,6 +7,8 @@ "AddToCart": "カートに追加", "AllowedUploadFormats": "このページから ZIP 形式のプラグインやテーマをアップロードをすることができます", "Authors": "著者", + "Browse": "ブラウズ", + "LatestMarketplaceUpdates": "マーケットプレイスの最新情報", "BackToMarketplace": "マーケットプレイスへ戻る", "BrowseMarketplace": "ブラウズ・マーケットプレイス", "ByXDevelopers": "%s の開発者", diff --git a/app/plugins/Marketplace/lang/pt-br.json b/app/plugins/Marketplace/lang/pt-br.json index 39db5ce18..d0e208087 100644 --- a/app/plugins/Marketplace/lang/pt-br.json +++ b/app/plugins/Marketplace/lang/pt-br.json @@ -5,41 +5,49 @@ "ActionActivateTheme": "Ativar tema", "ActionInstall": "Instalar", "AddToCart": "Adicionar ao carrinho", - "AllowedUploadFormats": "Você pode carregar um plugin ou tema em formato zip através desta página.", + "AllowedUploadFormats": "Você pode carregar um plugin ou tema em formato .zip através desta página.", "Authors": "Autores", + "Browse": "Navegar", + "LatestMarketplaceUpdates": "Atualizações mais recentes do Marketplace", "BackToMarketplace": "Voltar ao Marketplace", - "BrowseMarketplace": "Navegue no Marketplace", + "BrowseMarketplace": "Navegar no Marketplace", "ByXDevelopers": "por %s desenvolvedores", - "CannotInstall": "Não pode instalar", - "CannotUpdate": "Não pode atualizar", - "ClickToCompletePurchase": "Clicar para completar a compra", - "CurrentNumPiwikUsers": "Seu Matomo atualmente possui %1$s usuários registrados", - "ConfirmRemoveLicense": "Tem certeza que deseja remover sua chave de licença? Você não receberá mais atualizações para nenhum de seus plugins comprados.", + "CannotInstall": "Não foi possível instalar", + "CannotUpdate": "Não foi possível atualizar", + "ClickToCompletePurchase": "Clique para completar a compra.", + "CurrentNumPiwikUsers": "Seu Matomo atualmente possui %1$s usuários registrados.", + "ConfirmRemoveLicense": "Você tem certeza que deseja remover sua chave de licença? Você não receberá mais atualizações para nenhum de seus plugins comprados.", "Developer": "Desenvolvedor", "DevelopersLearnHowToDevelopPlugins": "Desenvolvedores: Saibam como estender e personalizar o Matomo %1$sdesenvolvendo plugins ou temas%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "Você pode remover o Marketplace do menu de relatórios instalando o plugin %1$sWhite Label%2$s.", "Marketplace": "Marketplace", - "PaidPlugins": "Características Premium", + "PaidPlugins": "Funcionalidades Premium", "FeaturedPlugin": "Plugin destaque", "InstallingNewPluginViaMarketplaceOrUpload": "Você pode instalar automaticamente %1$sdo Marketplace ou %2$s carregar um %3$s %4$s no formato .zip.", "InstallingPlugin": "Instalando %s", "InstallPurchasedPlugins": "Instalar plugins comprados", "LastCommitTime": "(último commit %s)", - "LastUpdated": "Última Atualização", + "LastUpdated": "Última atualização", "License": "Licença", "LicenseKey": "Chave de licença", - "LicenseKeyActivatedSuccess": "Chave de licença ativada com sucesso", - "LicenseKeyDeletedSuccess": "Chave de licença removida com sucesso", + "LicenseKeyActivatedSuccess": "Chave de licença ativada com sucesso!", + "LicenseKeyDeletedSuccess": "Chave de licença excluída com sucesso.", "Exceeded": "Excedido", "LicenseMissing": "Licença ausente", + "LicenseMissingDeactivatedDescription": "Os seguintes plugins foram desativados porque você está usando-os sem uma licença: %1$s. %2$sPara resolver isto, atualize sua chave de licença, ou %3$s obtenha agora uma inscrição%4$s ou desative o plugin.", + "PluginLicenseMissingDescription": "Você não tem permissão de baixar este plugin porque não há uma licença para este plugin. Pare resolver isto, atualize sua chave de licença, obtenha uma inscrição ou desinstale o plugin.", "LicenseExceeded": "Licença excedida", + "LicenseExceededDescription": "As licenças para os seguintes plugins não estão mais válidas pois o número de usuários autorizados para a licença foi excedido: %1$s. %2$sVocê não poderá baixar atualizações para estes plugins. Para resolver isto, exclua alguns usuários, ou %3$satualize agora sua inscrição%4$s.", + "PluginLicenseExceededDescription": "Você não tem permissão para baixar este plugin. A licença para este plugin não está mais válida pois o número de usuários autorizados para a licença foi excedido. Para resolver isto, exclua alguns usuários, ou atualize agora sua inscrição.", "LicenseExpired": "Licença expirada", - "LicenseRenewsNextPaymentDate": "Renova no próxima vencimento", - "UpgradeSubscription": "Assinatura de atualização", - "ViewSubscriptionsSummary": "%1$sVeja suas assinaturas de plugins.%2$s", - "ViewSubscriptions": "Visualizar inscrições", - "ExceptionLinceseKeyIsExpired": "A chave de licença está expirada", - "ExceptionLinceseKeyIsNotValid": "A chave de licença não é válida", - "LicenseKeyIsValidShort": "Chave de licença é válida", + "LicenseExpiredDescription": "As licenças para os seguintes plugins estão expiradas: %1$s. %2$sVocê não receberá mais atualizações para estes plugins. Para resolver isto, %3$srenove agora sua inscrição%4$s, ou desative o plugin se você não o utiliza mais.", + "LicenseRenewsNextPaymentDate": "Renova na próxima data de pagamento", + "UpgradeSubscription": "Atualizar inscrição", + "ViewSubscriptionsSummary": "%1$sVeja suas inscrições de plugins.%2$s", + "ViewSubscriptions": "Ver inscrições", + "ExceptionLinceseKeyIsExpired": "Esta chave de licença está expirada.", + "ExceptionLinceseKeyIsNotValid": "Esta chave de licença não é válida.", + "LicenseKeyIsValidShort": "A chave de licença é válida!", "RemoveLicenseKey": "Remover chave de licença", "InstallAllPurchasedPlugins": "Instalar todos os plugins comprados de uma só vez", "InstallAllPurchasedPluginsAction": "Instalar e ativar %d plugins comprados", @@ -51,9 +59,18 @@ "NotAllowedToBrowseMarketplaceThemes": "Você pode navegar na lista de temas que podem ser instalados para personalizar a aparência da plataforma Matomo. Por favor, contate seu administrador para solicitar a instalação de qualquer um destes para você.", "NoPluginsFound": "Nenhum plugin encontrado", "NoThemesFound": "Nenhum tema encontrado", - "NoSubscriptionsFound": "Nenhuma inscrição foi encontrada", + "NoSubscriptionsFound": "Nenhuma inscrição encontrada", "NumDownloadsLatestVersion": "Última versão: %s Downloads", "OverviewPluginSubscriptions": "Visão geral de suas inscrições de plugins", + "OverviewPluginSubscriptionsMissingLicense": "Você não possui um conjunto de chaves de licença. Se você comprou uma inscrição de plugin, vá ao %1$sMarketplace%2$s e digite sua chave de licença.", + "OverviewPluginSubscriptionsAllDetails": "Para ver todos os detalhes, ou para alterar uma inscrição, faça login em sua conta.", + "OverviewPluginSubscriptionsMissingInfo": "Pode ser possível que uma inscrição esteja faltando, por exemplo se um pagamento ainda não foi completado. Neste caso tente novamente em algumas horas, ou entre em contato com o time Matomo.", + "NoValidSubscriptionNoUpdates": "Quando uma inscrição estiver expirada você não receberá mais nenhuma atualização para este plugin.", + "PluginSubscriptionsList": "Esta é uma lista de inscrições associadas à sua chave de licença.", + "PaidPluginsNoLicenseKeyIntro": "Se você comprou um %1$splugin premium pago%2$s, por favor insira abaixo a chave de licença recebida.", + "PaidPluginsWithLicenseKeyIntro": "Uma chave de licença válida foi configurada. Por questões de segurança nós não exibimos aqui a chave de licença. Se você perdeu sua chave de licença, por favor entre em contato com o time Matomo.", + "PaidPluginsNoLicenseKeyIntroNoSuperUserAccess": "Caso você tenha comprado um %1$splugin premium pago%2$s no Marketplace, por favor solicite a um usuário com acesso de Super Usuário que adicione a chave de licença.", + "PluginDescription": "Estenda e expanda a funcionalidade do Matomo através do Marketplace baixando plugins e temas.", "PluginKeywords": "Palavras-Chave", "PluginUpdateAvailable": "Você está usando a versão %1$s e uma nova versão %2$s está disponível.", "PluginVersionInfo": "%1$s de %2$s", @@ -64,6 +81,7 @@ "ShownPriceIsExclTax": "O preço mostrado é excl. tax.", "Screenshots": "Screenshots", "SortByNewest": "Mais recente", + "SortByAlpha": "Por ordem alfabética", "SortByLastUpdated": "Última atualização", "SortByPopular": "Popular", "StepDownloadingPluginFromMarketplace": "Transferindo plugin do Marketplace", @@ -80,15 +98,19 @@ "SubscriptionStartDate": "Data de início", "SubscriptionEndDate": "Data final", "SubscriptionNextPaymentDate": "Próximo vencimento", - "SubscriptionInvalid": "Esta assinatura é inválida ou expirou", - "SubscriptionExpiresSoon": "Esta assinatura expira em breve", + "SubscriptionInvalid": "Esta inscrição é inválida ou expirou", + "SubscriptionExpiresSoon": "Esta inscrição expira em breve", "Support": "Suporte", "TeaserExtendPiwikByUpload": "Estenda o Matomo carregando um arquivo ZIP", + "LicenseExceededPossibleCause": "A licença está excedida. Possivelmente há mais usuários nesta instalação do Matomo do que a inscrição permite.", "Updated": "Atualizado(a)", "UpdatingPlugin": "Atualizando %1$s", "UploadZipFile": "Carregar arquivo ZIP", + "PluginUploadDisabled": "O carregamento de plugins está desabilitado no arquivo de configuração. Para habilitar esta funcionalidade por favor atualize sua configuração ou entre em contato com o seu administrador.", "LicenseKeyExpiresSoon": "Sua chave de licença expira em breve, entre em contato %1$s.", "LicenseKeyIsExpired": "Sua chave de licença está expirada, entre em contato %1$s.", + "MultiServerEnvironmentWarning": "Você não pode instalar ou atualizar o plugin diretamente pois você está usando o Matomo em múltiplos servidores. O plugin seria instalado apenas em um servidor. Ao invés disso, baixe o plugin e implante-o manualmente em todos os servidores.", + "AutoUpdateDisabledWarning": "Você não pode instalar ou atualizar o plugin diretamente pois as atualizações automáticas estão desabilitadas na configuração. Para habilitar atualizações automáticas defina %1$s em %2$s.", "ViewRepositoryChangelog": "Ver as mudanças" } } \ No newline at end of file diff --git a/app/plugins/Marketplace/lang/pt.json b/app/plugins/Marketplace/lang/pt.json index 661bbb16a..9a558ab96 100644 --- a/app/plugins/Marketplace/lang/pt.json +++ b/app/plugins/Marketplace/lang/pt.json @@ -7,6 +7,8 @@ "AddToCart": "Adicionar ao carrinho", "AllowedUploadFormats": "Pode enviar uma extensão ou um tema no formato .zip através desta página.", "Authors": "Autores", + "Browse": "Explorar", + "LatestMarketplaceUpdates": "Últimas atualizações na Loja", "BackToMarketplace": "Voltar à loja", "BrowseMarketplace": "Explorar a loja", "ByXDevelopers": "por %s programadores", @@ -17,6 +19,7 @@ "ConfirmRemoveLicense": "Tem a certeza que pretende remover a sua chave da licença? Não irá receber mais quaisquer atualizações para as extensões adquiridas.", "Developer": "Programador", "DevelopersLearnHowToDevelopPlugins": "Programadores: aprendam a estender e a personalizar o Matomo %1$sdesenvolvendo extensões ou temas%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "Pode remover a Loja do menu de relatórios instalando a extensão %1$sWhite Label%2$s.", "Marketplace": "Loja", "PaidPlugins": "Funcionalidades exclusivas", "FeaturedPlugin": "Extensão destacada", @@ -65,6 +68,7 @@ "NoValidSubscriptionNoUpdates": "Depois de uma subscrição ter expirado, não irá mais receber quaisquer atualizações para esta extensão.", "PluginSubscriptionsList": "Esta é uma lista de subscrições associadas à sua chave da licença.", "PaidPluginsNoLicenseKeyIntro": "Se adquiriu uma %1$sextensão exclusiva paga%2$s, por favor insira a chave da licença recebida abaixo.", + "PaidPluginsWithLicenseKeyIntro": "Foi definida uma chave de licença válida. Por razões de segurança, não podemos mostrar a chave de licença aqui. No caso de ter perdido a sua chave de licença, por favor, entre em contacto com a equipa do Matomo.", "PaidPluginsNoLicenseKeyIntroNoSuperUserAccess": "No caso de ter adquirido uma %1$sextensão exclusiva paga%2$s na loja, por favor peça a um utilizador com acesso de super-utilizador para adicionar a chave da licença.", "PluginDescription": "Estenda ou expanda a funcionalidade do Matomo através da loja, transferindo extensões e temas.", "PluginKeywords": "Palavras-chave", @@ -105,6 +109,7 @@ "PluginUploadDisabled": "O envio de extensões está desativo no ficheiro de configuração. Para ativar esta funcionalidade, por favor, atualize a sua configuração ou contacte o seu administrador", "LicenseKeyExpiresSoon": "A sua chave de licença expira em breve, por favor, contacte %1$s.", "LicenseKeyIsExpired": "A sua chave de licença expirou, por favor, contacte %1$s.", + "MultiServerEnvironmentWarning": "Não é possível instalar ou atualizar a extensão diretamente, dado que está a utilizar o Matomo em múltiplos servidores. A extensão seria somente instalada num só servidor. Em vez disso, transfira a extensão e proceda à instalação da mesma manualmente em todos os seus servidores.", "AutoUpdateDisabledWarning": "Não pode instalar ou atualizar a extensão diretamente dados que as atualizações automáticas estão desativadas na configuração. Para ativar as atualizações automáticas, defina %1$s em %2$s.", "ViewRepositoryChangelog": "Ver as alterações" } diff --git a/app/plugins/Marketplace/lang/ru.json b/app/plugins/Marketplace/lang/ru.json index eb866cda1..c9049e2f9 100644 --- a/app/plugins/Marketplace/lang/ru.json +++ b/app/plugins/Marketplace/lang/ru.json @@ -7,6 +7,8 @@ "AddToCart": "Добавить в корзину", "AllowedUploadFormats": "Вы можете загрузить плагин или тему в формате .zip на этой странице.", "Authors": "Авторы", + "Browse": "Просмотреть", + "LatestMarketplaceUpdates": "Последние обновления Marketplace", "BackToMarketplace": "Вернуться к Marketplace", "BrowseMarketplace": "Обзор Marketplace", "ByXDevelopers": "разработчиками %s", @@ -60,12 +62,24 @@ "NumDownloadsLatestVersion": "Последняя версия скачена: %s раз", "OverviewPluginSubscriptions": "Обзор подписок ваших плагинов", "OverviewPluginSubscriptionsMissingLicense": "У вас не установлен лицензионный ключ. Если вы приобрели подписку на плагин, перейдите в %1$sMarketplace%2$s и введите свой лицензионный ключ.", + "OverviewPluginSubscriptionsAllDetails": "Чтобы увидеть все детали или изменить подписку, войдите в свою учетную запись.", + "OverviewPluginSubscriptionsMissingInfo": "Возможно, подписка отсутствует, например, если платеж еще не завершен. В таком случае повторите попытку через несколько часов или свяжитесь с командой Matomo.", + "NoValidSubscriptionNoUpdates": "После истечения срока подписки вы больше не будете получать обновления для этого плагина.", + "PluginSubscriptionsList": "Это список подписок, связанных с вашим лицензионным ключом.", + "PaidPluginsNoLicenseKeyIntro": "Если вы приобрели %1$s платный подключаемый модуль %2$s премиум-класса, вставьте полученный лицензионный ключ ниже.", + "PaidPluginsWithLicenseKeyIntro": "Действительный лицензионный ключ был установлен. В целях безопасности мы не показываем здесь лицензионный ключ. Если вы потеряли свой лицензионный ключ, пожалуйста, свяжитесь с командой Matomo.", "PluginKeywords": "Ключевые слова", "PluginUpdateAvailable": "Вы используете версию %1$s последняя доступная %2$s", "PluginVersionInfo": "%1$s – %2$s", "PluginWebsite": "Сайт плагина", + "PriceExclTax": "%1$s%2$s без налога", + "PriceFromPerPeriod": "С %1$s\/%2$s\/", + "Reviews": "Отзывы", + "ShownPriceIsExclTax": "Указанная цена не включает налог.", "Screenshots": "Скриншоты", "SortByNewest": "Новейшие", + "SortByAlpha": "В алфавитном порядке", + "SortByLastUpdated": "Последнее обновление", "SortByPopular": "Популярные", "StepDownloadingPluginFromMarketplace": "Скачать плагин из Marketplace", "StepDownloadingThemeFromMarketplace": "Скачать тему из Marketplace", @@ -78,11 +92,20 @@ "StepReplaceExistingTheme": "Замена существующей темы", "StepThemeSuccessfullyUpdated": "Вы успешно обновили тему %1$s %2$s.", "SubscriptionType": "Тип", + "SubscriptionStartDate": "Дата начала", + "SubscriptionEndDate": "Дата окончания", + "SubscriptionNextPaymentDate": "Дата следующего платежа", + "SubscriptionInvalid": "Эта подписка недействительна или просрочена", + "SubscriptionExpiresSoon": "Срок действия этой подписки скоро истекает", "Support": "Поддержка", "TeaserExtendPiwikByUpload": "Улучшение Matomo закачкой ZIP файла", + "LicenseExceededPossibleCause": "Лицензия превышена. Возможно, в этой установке Matomo больше пользователей, чем разрешает подписка.", "Updated": "Обновлено", "UpdatingPlugin": "Обновление %1$s", "UploadZipFile": "Закачать ZIP файл", + "PluginUploadDisabled": "Загрузка плагина отключена в конфигурационном файле. Чтобы включить эту функцию, обновите конфигурацию или обратитесь к администратору.", + "LicenseKeyExpiresSoon": "Срок действия вашего лицензионного ключа скоро истекает, пожалуйста, свяжитесь с %1$s.", + "LicenseKeyIsExpired": "Срок действия вашего лицензионного ключа истек, пожалуйста, свяжитесь с %1$s.", "ViewRepositoryChangelog": "Посмотреть изменения" } } \ No newline at end of file diff --git a/app/plugins/Marketplace/lang/sq.json b/app/plugins/Marketplace/lang/sq.json index 8b47b0625..f4f77cf2c 100644 --- a/app/plugins/Marketplace/lang/sq.json +++ b/app/plugins/Marketplace/lang/sq.json @@ -7,6 +7,8 @@ "AddToCart": "Shtoje në shportë", "AllowedUploadFormats": "Një shtojcë apo një temë mund ta ngarkoni në formatin .zip përmes kësaj faqeje.", "Authors": "Autorë", + "Browse": "Shfletoni", + "LatestMarketplaceUpdates": "Përditësimet Më të Reja Nga Marketplace-i", "BackToMarketplace": "Mbrapsht te Marketplace", "BrowseMarketplace": "Shfletoni në Marketplace", "ByXDevelopers": "nga zhvilluesit %s", @@ -17,6 +19,7 @@ "ConfirmRemoveLicense": "Jeni i sigurt se doni të hiqe kyçi juaj i licencës? Nuk do të merrni më përditësime për cilëndo nga shtojcat që keni blerë.", "Developer": "Zhvillues", "DevelopersLearnHowToDevelopPlugins": "Zhvillues: Mësoni se si ta thelloni dhe përshtatni Matomo-n përmes %1$shartimit të shtojcave ose temave%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "Marketplace-in mund ta hiqni nga menuja e raporteve duke instaluar shtojcën %1$sWhite Label%2$s.", "Marketplace": "Marketplace", "PaidPlugins": "Veçori Me Pagesë", "FeaturedPlugin": "Shtojcë e zgjedhur", diff --git a/app/plugins/Marketplace/lang/sv.json b/app/plugins/Marketplace/lang/sv.json index 4baebfe11..b29c81ad7 100644 --- a/app/plugins/Marketplace/lang/sv.json +++ b/app/plugins/Marketplace/lang/sv.json @@ -77,6 +77,7 @@ "ShownPriceIsExclTax": "Priserna är ex. moms.", "Screenshots": "Skärmdumpar", "SortByNewest": "Nyast", + "SortByAlpha": "Alfabetiskt", "SortByLastUpdated": "Senast uppdaterad", "SortByPopular": "Populära", "StepDownloadingPluginFromMarketplace": "Ladda ner plugin från Butiken.", diff --git a/app/plugins/Marketplace/lang/tr.json b/app/plugins/Marketplace/lang/tr.json index af30cf924..b631ddad5 100644 --- a/app/plugins/Marketplace/lang/tr.json +++ b/app/plugins/Marketplace/lang/tr.json @@ -7,6 +7,8 @@ "AddToCart": "Sepete ekle", "AllowedUploadFormats": "Buradan .zip biçiminde bir uygulama eki ya da tema dosyası yükleyebilirsiniz.", "Authors": "Geliştiriciler", + "Browse": "Gözat", + "LatestMarketplaceUpdates": "Son Mağaza Güncellemeleri", "BackToMarketplace": "Mağazaya geri dön", "BrowseMarketplace": "Mağazaya Gözat", "ByXDevelopers": "%s geliştirici tarafından", @@ -17,6 +19,7 @@ "ConfirmRemoveLicense": "Lisans anahtarınızı kaldırmak istediğinizden emin misiniz? Anahtarı kaldırdığınızda satın aldığınız uygulama ekleri için güncellemeleri alamazsınız.", "Developer": "Geliştirici", "DevelopersLearnHowToDevelopPlugins": "Geliştiriciler: Matomo uygulamasını geliştirmek ve gereksinimlerinize göre özelleştirmek için %1$suygulama eki ya da tema geliştirmeyi öğrenin%2$s.", + "NoticeRemoveMarketplaceFromReportingMenu": "%1$sWhite Label%2$s uygulama ekini kurarak rapor menüsünden Mağaza seçeneğini kaldırabilirsiniz.", "Marketplace": "Mağaza", "PaidPlugins": "Premium Özellikler", "FeaturedPlugin": "Öne çıkarılmış uygulama eki", diff --git a/app/plugins/Marketplace/templates/overview.twig b/app/plugins/Marketplace/templates/overview.twig index 55091e9e9..1eccd5d40 100644 --- a/app/plugins/Marketplace/templates/overview.twig +++ b/app/plugins/Marketplace/templates/overview.twig @@ -1,4 +1,4 @@ -{% extends "admin.twig" %} +{% extends inReportingMenu ? "empty.twig" : "admin.twig" %} {% import '@CorePluginsAdmin/macros.twig' as pluginsMacro %} {% set title %}{{ 'Marketplace_Marketplace'|translate }}{% endset %} @@ -10,6 +10,7 @@

{{ title|e('html_attr') }}

+

{% if not isSuperUser %} {% if showThemes %} @@ -24,6 +25,9 @@ {{ 'CorePluginsAdmin_PluginsExtendPiwik'|translate }} {{ 'Marketplace_InstallingNewPluginViaMarketplaceOrUpload'|translate(('General_Plugins'|translate), '', ('General_Plugin'|translate), '')|raw }} {% endif %} + {% if isSuperUser and inReportingMenu %} + {{ 'Marketplace_NoticeRemoveMarketplaceFromReportingMenu'|translate('', '')|raw }} + {% endif %}

{% include '@Marketplace/licenseform.twig' %} diff --git a/app/plugins/MobileAppMeasurable/config/config.php b/app/plugins/MobileAppMeasurable/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/MobileAppMeasurable/config/config.php +++ b/app/plugins/MobileAppMeasurable/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/MobileAppMeasurable/config/tracker.php b/app/plugins/MobileAppMeasurable/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/MobileAppMeasurable/config/tracker.php +++ b/app/plugins/MobileAppMeasurable/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/MobileAppMeasurable/lang/de.json b/app/plugins/MobileAppMeasurable/lang/de.json index 58507abd7..f4ee887ac 100644 --- a/app/plugins/MobileAppMeasurable/lang/de.json +++ b/app/plugins/MobileAppMeasurable/lang/de.json @@ -2,6 +2,6 @@ "MobileAppMeasurable": { "MobileApp": "Mobile App", "MobileApps": "Mobile Apps", - "MobileAppDescription": "Eine native mobile App für iOS, Android oder ein anderes mobiles Betriebssystem." + "MobileAppDescription": "Eine native Mobile App für iOS, Android oder ein anderes mobiles Betriebssystem." } } \ No newline at end of file diff --git a/app/plugins/MobileAppMeasurable/lang/pt-br.json b/app/plugins/MobileAppMeasurable/lang/pt-br.json index 606ad5206..7ea0b9aca 100644 --- a/app/plugins/MobileAppMeasurable/lang/pt-br.json +++ b/app/plugins/MobileAppMeasurable/lang/pt-br.json @@ -1,7 +1,7 @@ { "MobileAppMeasurable": { - "MobileApp": "Aplicativo Móvel", - "MobileApps": "Aplicativos Móveis", + "MobileApp": "Aplicativo móvel", + "MobileApps": "Aplicativos móveis", "MobileAppDescription": "Um aplicativo móvel nativo para iOS, Android ou qualquer outro sistema operacional móvel." } } \ No newline at end of file diff --git a/app/plugins/MobileAppMeasurable/lang/zh-cn.json b/app/plugins/MobileAppMeasurable/lang/zh-cn.json new file mode 100644 index 000000000..7dae2df53 --- /dev/null +++ b/app/plugins/MobileAppMeasurable/lang/zh-cn.json @@ -0,0 +1,7 @@ +{ + "MobileAppMeasurable": { + "MobileApp": "移动应用程序", + "MobileApps": "移动应用", + "MobileAppDescription": "iOS、Android或任何其他移动操作系统的本地移动应用程序。" + } +} \ No newline at end of file diff --git a/app/plugins/MobileMessaging/config/config.php b/app/plugins/MobileMessaging/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/MobileMessaging/config/config.php +++ b/app/plugins/MobileMessaging/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/MobileMessaging/config/tracker.php b/app/plugins/MobileMessaging/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/MobileMessaging/config/tracker.php +++ b/app/plugins/MobileMessaging/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/MobileMessaging/lang/pt-br.json b/app/plugins/MobileMessaging/lang/pt-br.json index 9965fc9e2..673bc369e 100644 --- a/app/plugins/MobileMessaging/lang/pt-br.json +++ b/app/plugins/MobileMessaging/lang/pt-br.json @@ -1,48 +1,48 @@ { "MobileMessaging": { - "Exception_UnknownProvider": "Provedor '%1$s' desconhecido. Tente um dos seguintes em vez: %2$s.", - "MobileReport_AdditionalPhoneNumbers": "Você pode adicionar mais números de telefone, acessando", - "MobileReport_MobileMessagingSettingsLink": "Página de configurações de Mensagens móveis", + "Exception_UnknownProvider": "Nome do provedor '%1$s' desconhecido. No lugar, tente um dos seguintes: %2$s.", + "MobileReport_AdditionalPhoneNumbers": "Você pode adicionar mais números de telefone acessando", + "MobileReport_MobileMessagingSettingsLink": "a página de configurações de mensagens móveis", "MobileReport_NoPhoneNumbers": "Por favor ative pelo menos um número de telefone acessando", - "MultiSites_Must_Be_Activated": "Para gerar textos SMS de estatísticas do seu site, por favor, ative o plugin multisites no Matomo.", + "MultiSites_Must_Be_Activated": "Para gerar textos SMS de estatísticas do seu site, por favor ative o plugin MultiSites no Matomo.", "PhoneNumbers": "Números de telefone", - "PluginDescription": "Criar e baixar relatórios SMS personalizados e tê-los enviado para o seu celular diária, semanal ou mensalmente.", - "Settings_APIKey": "API Key", + "PluginDescription": "Crie e baixe relatórios SMS personalizados, e tenha-os enviados para o seu celular diária, semanal ou mensalmente.", + "Settings_APIKey": "Chave da API", "Settings_CountryCode": "Código do país", "Settings_SelectCountry": "Selecionar país", - "Settings_CredentialNotProvided": "Antes que você possa criar e gerenciar números de telefone, ligue Matomo na sua Conta SMS acima.", - "Settings_CredentialNotProvidedByAdmin": "Antes que você possa criar e gerenciar números de telefone, por favor, solicite ao seu administrador para conectar o Matomo a uma conta SMS.", + "Settings_CredentialNotProvided": "Antes que você possa criar e gerenciar números de telefone, por favor conecte o Matomo à sua conta SMS acima.", + "Settings_CredentialNotProvidedByAdmin": "Antes que você possa criar e gerenciar números de telefone, por favor solicite ao seu administrador para conectar o Matomo a uma conta SMS.", "Settings_CredentialProvided": "Sua conta API SMS %s está configurada corretamente!", - "Settings_CredentialInvalid": "Sua conta SMS API %1$s foi configurada, mas um erro ocorreu ao tentar receber os créditos disponíveis.", + "Settings_CredentialInvalid": "Sua conta API SMS %1$s está configurada, mas um erro ocorreu ao tentar receber os créditos disponíveis.", "Settings_DeleteAccountConfirm": "Tem certeza de que deseja apagar esta conta SMS?", "Settings_DelegatedSmsProviderOnlyAppliesToYou": "O provedor de SMS configurado será usado somente por você e não pelos outros usuários.", - "Settings_DelegatedPhoneNumbersOnlyUsedByYou": "Os números de telefone configurados serão visualizados e usados somente por você e não pelos outros usuários.", + "Settings_DelegatedPhoneNumbersOnlyUsedByYou": "Os números de telefone configurados somente podem ser visualizados e usados por você e não pelos outros usuários.", "Settings_EnterActivationCode": "Informe o código de ativação", - "Settings_InvalidActivationCode": "O Código informado não é válido, por favor tente novamente.", - "Settings_LetUsersManageAPICredential": "Permite aos usuários gerenciar seus próprios provedores SMS API", + "Settings_InvalidActivationCode": "O código informado não é válido, por favor tente novamente.", + "Settings_LetUsersManageAPICredential": "Permite aos usuários gerenciarem seu próprio provedor SMS", "Settings_LetUsersManageAPICredential_No_Help": "Todos os usuários são capazes de receber relatórios de SMS e utilizarão créditos da sua conta.", - "Settings_LetUsersManageAPICredential_Yes_Help": "Cada usuário será capaz de configurar sua própria conta SMS API e não utilizar o seu crédito.", + "Settings_LetUsersManageAPICredential_Yes_Help": "Cada usuário será capaz de configurar sua própria conta API SMS e não utilizará o seu crédito.", "Settings_ManagePhoneNumbers": "Gerenciar números de telefone", - "Settings_PhoneActivated": "Telefone validado! Agora você pode receber SMS com suas estatísticas.", + "Settings_PhoneActivated": "Número de telefone validado! Agora você pode receber SMS com suas estatísticas.", "Settings_PhoneNumber": "Número de telefone", "Settings_PhoneNumbers_Add": "Adicionar novo número de telefone", - "Settings_PhoneNumbers_CountryCode_Help": "Se você não souber o código de país do seu telefone, procure aqui.", - "Settings_PhoneNumbers_Help": "Antes de receber relatórios SMS (mensagens de texto) em um telefone o número do telefone deve ser informado abaixo.", + "Settings_PhoneNumbers_CountryCode_Help": "Se você não souber o código de país do seu telefone, procure seu país aqui.", + "Settings_PhoneNumbers_Help": "Antes de receber relatórios SMS (mensagens de texto) em um telefone, o número do telefone deve ser informado abaixo.", "Settings_PhoneNumbers_HelpAdd": "Quando você clicar em \"Adicionar\", um SMS contendo um código será enviado para o telefone. O usuário que recebe o código deve então fazer o login no Matomo, clicar em Configurações e, em seguida, clicar em Mensagens Móveis. Depois de inserir o código, o usuário poderá receber relatórios de texto em seu telefone.", - "Settings_PleaseSignUp": "Para criar relatórios de SMS e receber mensagens curtas de texto com as suas estatísticas de website em seu telefone móvel, inscreva-se com a API SMS e insira as informações abaixo.", + "Settings_PleaseSignUp": "Para criar relatórios de SMS e receber mensagens curtas de texto com as estatísticas do seu site em seu telefone móvel, inscreva-se com a API SMS e insira suas informações abaixo.", "Settings_SMSAPIAccount": "Gerenciar conta de API SMS", "Settings_SMSProvider": "Provedor SMS", "Settings_SuperAdmin": "Configurações de super usuário", "Settings_SuspiciousPhoneNumber": "Se você não receber a mensagem de texto, você pode experimentar sem o zero inicial. Ex.: %s", - "Settings_UpdateOrDeleteAccount": "Você também pode %1$satualizar%2$s ou %3$sapagar%4$s esta conta.", + "Settings_UpdateOrDeleteAccount": "Você também pode %1$satualizar%2$s ou %3$sexcluir%4$s esta conta.", "Settings_ValidatePhoneNumber": "Validar", - "Settings_VerificationCodeJustSent": "Acabamos de enviar um SMS para este número com um código: por favor digite o código acima e clique em \"Validar\".", - "SettingsMenu": "Mensagem móvel", + "Settings_VerificationCodeJustSent": "Acabamos de enviar um SMS para este número com um código: por favor digite acima o código e clique em \"Validar\".", + "SettingsMenu": "Mensagens móveis", "SMS_Content_Too_Long": "[muito longo]", "Available_Credits": "Créditos disponíveis: %1$s", - "TopLinkTooltip": "Obter relatórios analíticos da Web em seu e-mail ou telefone móvel!", + "TopLinkTooltip": "Receba relatórios de web analytics em seu email ou telefone móvel.", "TopMenu": "Relatórios de e-mail e SMS", "UserKey": "Chave do usuário", - "VerificationText": "O código é %1$s. Para validar o seu número de telefone e receber relatórios Matomo de SMS, copie este código no formulário disponível no Matomo em > %2$s > %3$s." + "VerificationText": "O código é %1$s. Para validar o seu número de telefone e receber relatórios Matomo de SMS, por favor copie este código no formulário disponível em Matomo > %2$s > %3$s." } } \ No newline at end of file diff --git a/app/plugins/MobileMessaging/lang/pt.json b/app/plugins/MobileMessaging/lang/pt.json index de9033f45..18a1c65a5 100644 --- a/app/plugins/MobileMessaging/lang/pt.json +++ b/app/plugins/MobileMessaging/lang/pt.json @@ -21,12 +21,14 @@ "Settings_InvalidActivationCode": "O código introduzido não é válido, por favor, tente novamente.", "Settings_LetUsersManageAPICredential": "Permitir que os utilizadores façam a gestão do seu próprio fornecedor de SMS", "Settings_LetUsersManageAPICredential_No_Help": "Todos os utilizadores poderão receber relatórios por SMS e irão utilizar os créditos da sua conta.", + "Settings_LetUsersManageAPICredential_Yes_Help": "Cada utilizador poderá configurar a sua própria conta de API SMS e não utilizarão o seu crédito.", "Settings_ManagePhoneNumbers": "Gerir números de telefone", "Settings_PhoneActivated": "Número de telefone validado! Já pode receber SMS com as suas estatísticas.", "Settings_PhoneNumber": "Número de telefone", "Settings_PhoneNumbers_Add": "Adicionar um novo número de telefone", "Settings_PhoneNumbers_CountryCode_Help": "Se não sabe o código telefónico do país, procure pelo seu país aqui.", "Settings_PhoneNumbers_Help": "Antes de receber relatórios por SMS (mensagens de texto) num telefone, o número de telefone deve ser introduzido abaixo.", + "Settings_PhoneNumbers_HelpAdd": "Ao clicar em \"Adicionar\", uma mensagem SMS contendo um código será enviada para o telemóvel. O utilizador que a receba deverá então autenticar-se no Matomo, clicar em \"Definições\" e depois em \"Mensagens móveis\". Após a introdução do código, o utilizador poderá receber relatórios de texto no respetivo telemóvel.", "Settings_PleaseSignUp": "Para criar relatórios SMS e receber pequenas mensagens de texto com as estatísticas dos seus sites no seu telemóvel, por favor, subscreva à API SMS e introduza a sua informação em baixo.", "Settings_SMSAPIAccount": "Gerir conta da API SMS", "Settings_SMSProvider": "Fornecedor de SMS", @@ -34,6 +36,7 @@ "Settings_SuspiciousPhoneNumber": "Se não recebe a mensagem de texto, pode tentar sem o zero no início, por exemplo %s", "Settings_UpdateOrDeleteAccount": "Pode também %1$satualizar%2$s ou %3$seliminar%4$s esta conta.", "Settings_ValidatePhoneNumber": "Validar", + "Settings_VerificationCodeJustSent": "Acabámos de enviar uma mensagem SMS para este número contendo um código: por favor introduza o código em cima e clique em \"Validar\".", "SettingsMenu": "Mensagens móveis", "SMS_Content_Too_Long": "[demasiado comprido]", "Available_Credits": "Créditos disponíveis: %1$s", diff --git a/app/plugins/Monolog/Formatter/LineMessageFormatter.php b/app/plugins/Monolog/Formatter/LineMessageFormatter.php index 306838d34..5a64002c9 100644 --- a/app/plugins/Monolog/Formatter/LineMessageFormatter.php +++ b/app/plugins/Monolog/Formatter/LineMessageFormatter.php @@ -61,16 +61,36 @@ public function format(array $record) private function formatMessage($class, $message, $date, $record) { + $trace = isset($record['context']['trace']) ? self::formatTrace($record['context']['trace']) : ''; $message = str_replace( - array('%tag%', '%message%', '%datetime%', '%level%'), - array($class, $message, $date, $record['level_name']), + array('%tag%', '%message%', '%datetime%', '%level%', '%trace%'), + array($class, $message, $date, $record['level_name'], $trace), $this->logMessageFormat ); - $message .= "\n"; + $message = trim($message) . "\n"; return $message; } + private static function formatTrace(array $trace, $numLevels = 10) + { + $strTrace = ''; + for ($i = 0; $i < $numLevels; $i++) { + if (!isset($trace[$i])) { + continue; + } + + $level = $trace[$i]; + if (isset($level['file'], $level['line'])) { + $levelTrace = '#' . $i . (str_replace(PIWIK_DOCUMENT_ROOT, '', $level['file'])) . '(' . $level['line'] . ')'; + } else { + $levelTrace = '[internal function]: ' . $level['class'] . $level['type'] . $level['function'] . '()'; + } + $strTrace .= $levelTrace . ","; + } + return trim($strTrace, ","); + } + public function formatBatch(array $records) { foreach ($records as $key => $record) { diff --git a/app/plugins/Monolog/Handler/WebNotificationHandler.php b/app/plugins/Monolog/Handler/WebNotificationHandler.php index a71b0ff28..c56e2d54e 100644 --- a/app/plugins/Monolog/Handler/WebNotificationHandler.php +++ b/app/plugins/Monolog/Handler/WebNotificationHandler.php @@ -47,6 +47,7 @@ protected function write(array $record) } $message = $record['level_name'] . ': ' . htmlentities($record['message'], ENT_COMPAT | ENT_HTML401, 'UTF-8'); + $message .= $this->getLiteDebuggingInfo(); $notification = new Notification($message); $notification->context = $context; @@ -58,4 +59,30 @@ protected function write(array $record) // Silently ignore the error. } } + + private function getLiteDebuggingInfo() + { + $info = [ + 'Module' => Common::getRequestVar('module', false), + 'Action' => Common::getRequestVar('action', false), + 'Method' => Common::getRequestVar('method', false), + 'Trigger' => Common::getRequestVar('trigger', false), + 'In CLI mode' => Common::isPhpCliMode() ? 'true' : 'false', + ]; + + $parts = []; + foreach ($info as $title => $value) { + if (empty($value)) { + continue; + } + + $parts[] = "$title: $value"; + } + + if (empty($parts)) { + return ""; + } + + return "\n(" . implode(', ', $parts) . ")"; + } } diff --git a/app/plugins/Monolog/Processor/ExceptionToTextProcessor.php b/app/plugins/Monolog/Processor/ExceptionToTextProcessor.php index 98958fa82..ab828dc82 100644 --- a/app/plugins/Monolog/Processor/ExceptionToTextProcessor.php +++ b/app/plugins/Monolog/Processor/ExceptionToTextProcessor.php @@ -9,6 +9,7 @@ namespace Piwik\Plugins\Monolog\Processor; use Piwik\ErrorHandler; +use Piwik\Exception\InvalidRequestParameterException; use Piwik\Log; /** @@ -25,6 +26,10 @@ public function __invoke(array $record) /** @var \Exception $exception */ $exception = $record['context']['exception']; + if ($exception instanceof InvalidRequestParameterException) { + return $record; + } + $exceptionStr = sprintf( "%s(%d): %s\n%s", $exception instanceof \Exception ? $exception->getFile() : $exception['file'], diff --git a/app/plugins/Monolog/config/config.php b/app/plugins/Monolog/config/config.php index ff60f7c5b..b81fb089c 100644 --- a/app/plugins/Monolog/config/config.php +++ b/app/plugins/Monolog/config/config.php @@ -40,7 +40,10 @@ $writers = []; foreach ($writerNames as $writerName) { - if ($writerName === 'screen' && \Piwik\Common::isPhpCliMode()) { + if ($writerName === 'screen' + && \Piwik\Common::isPhpCliMode() + && !defined('PIWIK_TEST_MODE') + ) { continue; // screen writer is only valid for web requests } @@ -94,16 +97,13 @@ ->constructor(DI\get('log.file.filename'), DI\get('log.level.file')) ->method('setFormatter', DI\get('log.lineMessageFormatter.file')), - 'log.lineMessageFormatter.file' => DI\object('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter') - ->constructorParameter('allowInlineLineBreaks', false), - 'Piwik\Plugins\Monolog\Handler\DatabaseHandler' => DI\object() ->constructor(DI\get('log.level.database')) - ->method('setFormatter', DI\get('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter')), + ->method('setFormatter', DI\get('log.lineMessageFormatter')), 'Piwik\Plugins\Monolog\Handler\WebNotificationHandler' => DI\object() ->constructor(DI\get('log.level.screen')) - ->method('setFormatter', DI\get('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter')), + ->method('setFormatter', DI\get('log.lineMessageFormatter')), 'log.level' => DI\factory(function (ContainerInterface $c) { if ($c->has('ini.log.log_level')) { @@ -112,6 +112,7 @@ return Log::getMonologLevel(constant('Piwik\Log::'.strtoupper($level))); } } + return Logger::WARNING; }), @@ -171,16 +172,29 @@ return $logPath; }), - 'Piwik\Plugins\Monolog\Formatter\LineMessageFormatter' => DI\object() - ->constructor(DI\get('log.format')), + 'Piwik\Plugins\Monolog\Formatter\LineMessageFormatter' => DI\object('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter') + ->constructor(DI\get('log.short.format')), + 'log.lineMessageFormatter' => DI\object('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter') + ->constructor(DI\get('log.short.format')), + + 'log.lineMessageFormatter.file' => DI\object('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter') + ->constructor(DI\get('log.trace.format')) + ->constructorParameter('allowInlineLineBreaks', false), - 'log.format' => DI\factory(function (ContainerInterface $c) { + 'log.short.format' => DI\factory(function (ContainerInterface $c) { if ($c->has('ini.log.string_message_format')) { return $c->get('ini.log.string_message_format'); } return '%level% %tag%[%datetime%] %message%'; }), + 'log.trace.format' => DI\factory(function (ContainerInterface $c) { + if ($c->has('ini.log.string_message_format_trace')) { + return $c->get('ini.log.string_message_format_trace'); + } + return '%level% %tag%[%datetime%] %message% %trace%'; + }), + 'archiving.performance.handlers' => function (ContainerInterface $c) { $logFile = trim($c->get('ini.Debug.archive_profiling_log')); if (empty($logFile)) { diff --git a/app/plugins/Monolog/config/tracker.php b/app/plugins/Monolog/config/tracker.php index 620101ff7..bb7820f9f 100644 --- a/app/plugins/Monolog/config/tracker.php +++ b/app/plugins/Monolog/config/tracker.php @@ -10,17 +10,13 @@ function isTrackerDebugEnabled(ContainerInterface $c) return array( - 'Psr\Log\LoggerInterface' => \DI\decorate(function ($previous, ContainerInterface $c) { - if (isTrackerDebugEnabled($c)) { - return $previous; - } else { - return new \Psr\Log\NullLogger(); - } - }), - - 'log.handler.classes' => DI\decorate(function ($previous) { - if (isset($previous['screen'])) { + 'log.handler.classes' => DI\decorate(function ($previous, ContainerInterface $c) { + if (isset($previous['screen']) + && isTrackerDebugEnabled($c) + ) { $previous['screen'] = 'Piwik\Plugins\Monolog\Handler\EchoHandler'; + } else { + unset($previous['screen']); } return $previous; diff --git a/app/plugins/Morpheus/config/config.php b/app/plugins/Morpheus/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/Morpheus/config/config.php +++ b/app/plugins/Morpheus/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Morpheus/config/tracker.php b/app/plugins/Morpheus/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Morpheus/config/tracker.php +++ b/app/plugins/Morpheus/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Morpheus/icons/dist/brand/AGM.png b/app/plugins/Morpheus/icons/dist/brand/AGM.png new file mode 100644 index 000000000..c11fb65eb Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/AGM.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/AMGOO.png b/app/plugins/Morpheus/icons/dist/brand/AMGOO.png new file mode 100644 index 000000000..59c2a5a65 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/AMGOO.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Advan.png b/app/plugins/Morpheus/icons/dist/brand/Advan.png new file mode 100644 index 000000000..f9d8247fe Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Advan.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/AllCall.png b/app/plugins/Morpheus/icons/dist/brand/AllCall.png new file mode 100644 index 000000000..bd8f8380a Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/AllCall.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Beeline.png b/app/plugins/Morpheus/icons/dist/brand/Beeline.png new file mode 100644 index 000000000..dc280e7e5 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Beeline.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Digi.png b/app/plugins/Morpheus/icons/dist/brand/Digi.png new file mode 100644 index 000000000..48c5b0ddc Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Digi.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Digicel.png b/app/plugins/Morpheus/icons/dist/brand/Digicel.png new file mode 100644 index 000000000..7f37a96d6 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Digicel.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/EE.png b/app/plugins/Morpheus/icons/dist/brand/EE.png new file mode 100644 index 000000000..a406825ab Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/EE.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Echo_Mobiles.png b/app/plugins/Morpheus/icons/dist/brand/Echo_Mobiles.png new file mode 100644 index 000000000..a665f0286 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Echo_Mobiles.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Eks_Mobility.png b/app/plugins/Morpheus/icons/dist/brand/Eks_Mobility.png new file mode 100644 index 000000000..8b47a4bdf Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Eks_Mobility.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Energizer.png b/app/plugins/Morpheus/icons/dist/brand/Energizer.png new file mode 100644 index 000000000..2b933c639 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Energizer.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Ergo.png b/app/plugins/Morpheus/icons/dist/brand/Ergo.png new file mode 100644 index 000000000..3467b1afb Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Ergo.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Evercoss.png b/app/plugins/Morpheus/icons/dist/brand/Evercoss.png new file mode 100644 index 000000000..9d1231715 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Evercoss.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/EvroMedia.png b/app/plugins/Morpheus/icons/dist/brand/EvroMedia.png new file mode 100644 index 000000000..4c3babdc8 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/EvroMedia.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Extrem.png b/app/plugins/Morpheus/icons/dist/brand/Extrem.png new file mode 100644 index 000000000..26eddafa3 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Extrem.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/FiGO.png b/app/plugins/Morpheus/icons/dist/brand/FiGO.png new file mode 100644 index 000000000..6c27924d6 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/FiGO.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Fondi.png b/app/plugins/Morpheus/icons/dist/brand/Fondi.png new file mode 100644 index 000000000..0e29fd10c Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Fondi.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Ginzzu.png b/app/plugins/Morpheus/icons/dist/brand/Ginzzu.png new file mode 100644 index 000000000..4fa045a6f Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Ginzzu.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Hafury.png b/app/plugins/Morpheus/icons/dist/brand/Hafury.png new file mode 100644 index 000000000..4031f25d8 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Hafury.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Hoozo.png b/app/plugins/Morpheus/icons/dist/brand/Hoozo.png new file mode 100644 index 000000000..85f95b143 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Hoozo.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/IMO_Mobile.png b/app/plugins/Morpheus/icons/dist/brand/IMO_Mobile.png new file mode 100644 index 000000000..fe42395ae Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/IMO_Mobile.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/InFocus.png b/app/plugins/Morpheus/icons/dist/brand/InFocus.png new file mode 100644 index 000000000..2047a7ef2 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/InFocus.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/InnJoo.png b/app/plugins/Morpheus/icons/dist/brand/InnJoo.png new file mode 100644 index 000000000..ba3bcf475 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/InnJoo.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Inoi.png b/app/plugins/Morpheus/icons/dist/brand/Inoi.png new file mode 100644 index 000000000..6ea4a28c0 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Inoi.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Just5.png b/app/plugins/Morpheus/icons/dist/brand/Just5.png new file mode 100644 index 000000000..d0f91c9e4 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Just5.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Kalley.png b/app/plugins/Morpheus/icons/dist/brand/Kalley.png new file mode 100644 index 000000000..ceb2b519c Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Kalley.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Kempler_&_Strauss.png b/app/plugins/Morpheus/icons/dist/brand/Kempler_Strauss.png similarity index 100% rename from app/plugins/Morpheus/icons/dist/brand/Kempler_&_Strauss.png rename to app/plugins/Morpheus/icons/dist/brand/Kempler_Strauss.png diff --git a/app/plugins/Morpheus/icons/dist/brand/Keneksi.png b/app/plugins/Morpheus/icons/dist/brand/Keneksi.png new file mode 100644 index 000000000..5cdda3f56 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Keneksi.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Kocaso.png b/app/plugins/Morpheus/icons/dist/brand/Kocaso.png new file mode 100644 index 000000000..8140aa1f2 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Kocaso.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Kodak.png b/app/plugins/Morpheus/icons/dist/brand/Kodak.png new file mode 100644 index 000000000..b0044614d Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Kodak.png differ diff --git "a/app/plugins/Morpheus/icons/dist/brand/Kr\303\274ger_Matz.png" "b/app/plugins/Morpheus/icons/dist/brand/Kr\303\274ger_Matz.png" new file mode 100644 index 000000000..de95ac8b0 Binary files /dev/null and "b/app/plugins/Morpheus/icons/dist/brand/Kr\303\274ger_Matz.png" differ diff --git a/app/plugins/Morpheus/icons/dist/brand/LAIQ.png b/app/plugins/Morpheus/icons/dist/brand/LAIQ.png new file mode 100644 index 000000000..7f09f1bf0 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/LAIQ.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Land_Rover.png b/app/plugins/Morpheus/icons/dist/brand/Land_Rover.png new file mode 100644 index 000000000..d27f96ce3 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Land_Rover.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Leagoo.png b/app/plugins/Morpheus/icons/dist/brand/Leagoo.png new file mode 100644 index 000000000..631315b2a Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Leagoo.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/M4tel.png b/app/plugins/Morpheus/icons/dist/brand/M4tel.png new file mode 100644 index 000000000..fdd847b9b Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/M4tel.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Maxwest.png b/app/plugins/Morpheus/icons/dist/brand/Maxwest.png new file mode 100644 index 000000000..f264f2991 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Maxwest.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Mobiola.png b/app/plugins/Morpheus/icons/dist/brand/Mobiola.png new file mode 100644 index 000000000..01b72d714 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Mobiola.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Movic.png b/app/plugins/Morpheus/icons/dist/brand/Movic.png new file mode 100644 index 000000000..6d5cd814f Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Movic.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/MyWigo.png b/app/plugins/Morpheus/icons/dist/brand/MyWigo.png new file mode 100644 index 000000000..afc75b79b Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/MyWigo.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/NOA.png b/app/plugins/Morpheus/icons/dist/brand/NOA.png new file mode 100644 index 000000000..6b5ada701 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/NOA.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/NUU_Mobile.png b/app/plugins/Morpheus/icons/dist/brand/NUU_Mobile.png new file mode 100644 index 000000000..2dd4d6850 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/NUU_Mobile.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/NYX_Mobile.png b/app/plugins/Morpheus/icons/dist/brand/NYX_Mobile.png new file mode 100644 index 000000000..81e0f752f Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/NYX_Mobile.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/PCD.png b/app/plugins/Morpheus/icons/dist/brand/PCD.png new file mode 100644 index 000000000..074ec6935 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/PCD.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/PCD_Argentina.png b/app/plugins/Morpheus/icons/dist/brand/PCD_Argentina.png new file mode 100644 index 000000000..2f6eec9a7 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/PCD_Argentina.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Panacom.png b/app/plugins/Morpheus/icons/dist/brand/Panacom.png new file mode 100644 index 000000000..d00f282ab Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Panacom.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Plum.png b/app/plugins/Morpheus/icons/dist/brand/Plum.png new file mode 100644 index 000000000..2d9f4b0ff Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Plum.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Quantum.png b/app/plugins/Morpheus/icons/dist/brand/Quantum.png new file mode 100644 index 000000000..577ba7328 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Quantum.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/RIM.png b/app/plugins/Morpheus/icons/dist/brand/RIM.png index ac3311345..8006dc3f3 100644 Binary files a/app/plugins/Morpheus/icons/dist/brand/RIM.png and b/app/plugins/Morpheus/icons/dist/brand/RIM.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/RT_Project.png b/app/plugins/Morpheus/icons/dist/brand/RT_Project.png new file mode 100644 index 000000000..d54424627 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/RT_Project.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Riviera.png b/app/plugins/Morpheus/icons/dist/brand/Riviera.png new file mode 100644 index 000000000..13c2b709b Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Riviera.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Rokit.png b/app/plugins/Morpheus/icons/dist/brand/Rokit.png new file mode 100644 index 000000000..c603f2616 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Rokit.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Rombica.png b/app/plugins/Morpheus/icons/dist/brand/Rombica.png new file mode 100644 index 000000000..5af3f90b1 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Rombica.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Safaricom.png b/app/plugins/Morpheus/icons/dist/brand/Safaricom.png new file mode 100644 index 000000000..a3c1b9b43 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Safaricom.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Selfix.png b/app/plugins/Morpheus/icons/dist/brand/Selfix.png new file mode 100644 index 000000000..d71182d69 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Selfix.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Senwa.png b/app/plugins/Morpheus/icons/dist/brand/Senwa.png new file mode 100644 index 000000000..252cb42bf Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Senwa.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Silent_Circle.png b/app/plugins/Morpheus/icons/dist/brand/Silent_Circle.png new file mode 100644 index 000000000..22edd1b5c Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Silent_Circle.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Simbans.png b/app/plugins/Morpheus/icons/dist/brand/Simbans.png new file mode 100644 index 000000000..9f4200bd1 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Simbans.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Sonim.png b/app/plugins/Morpheus/icons/dist/brand/Sonim.png new file mode 100644 index 000000000..3f2684fa8 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Sonim.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Timovi.png b/app/plugins/Morpheus/icons/dist/brand/Timovi.png new file mode 100644 index 000000000..711c67755 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Timovi.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/True.png b/app/plugins/Morpheus/icons/dist/brand/True.png new file mode 100644 index 000000000..455d29b17 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/True.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/U_S_Cellular.png b/app/plugins/Morpheus/icons/dist/brand/U_S_Cellular.png new file mode 100644 index 000000000..e0a5a74d2 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/U_S_Cellular.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Verizon.png b/app/plugins/Morpheus/icons/dist/brand/Verizon.png new file mode 100644 index 000000000..4e96b990b Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Verizon.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Vorago.png b/app/plugins/Morpheus/icons/dist/brand/Vorago.png new file mode 100644 index 000000000..abc87c872 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Vorago.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/X-TIGI.png b/app/plugins/Morpheus/icons/dist/brand/X-TIGI.png new file mode 100644 index 000000000..139fbeaaf Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/X-TIGI.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Yandex.png b/app/plugins/Morpheus/icons/dist/brand/Yandex.png new file mode 100644 index 000000000..8b59dbd43 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Yandex.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/Yezz.png b/app/plugins/Morpheus/icons/dist/brand/Yezz.png new file mode 100644 index 000000000..839272d76 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/Yezz.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/iHunt.png b/app/plugins/Morpheus/icons/dist/brand/iHunt.png new file mode 100644 index 000000000..120d8e566 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/iHunt.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/iPro.png b/app/plugins/Morpheus/icons/dist/brand/iPro.png new file mode 100644 index 000000000..4e5f09f95 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/iPro.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/iRola.png b/app/plugins/Morpheus/icons/dist/brand/iRola.png new file mode 100644 index 000000000..38791b1d4 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/iRola.png differ diff --git a/app/plugins/Morpheus/icons/dist/brand/iView.png b/app/plugins/Morpheus/icons/dist/brand/iView.png new file mode 100644 index 000000000..cdcce7830 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/brand/iView.png differ diff --git "a/app/plugins/Morpheus/icons/dist/brand/\303\266wn.png" "b/app/plugins/Morpheus/icons/dist/brand/\303\266wn.png" new file mode 100644 index 000000000..463f15b17 Binary files /dev/null and "b/app/plugins/Morpheus/icons/dist/brand/\303\266wn.png" differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/2B.png b/app/plugins/Morpheus/icons/dist/browsers/2B.png new file mode 100644 index 000000000..e3d8dcb35 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/2B.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/AD.png b/app/plugins/Morpheus/icons/dist/browsers/AD.png new file mode 100644 index 000000000..5334a0d7a Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/AD.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/BA.png b/app/plugins/Morpheus/icons/dist/browsers/BA.png new file mode 100644 index 000000000..0b31c1d63 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/BA.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/CE.png b/app/plugins/Morpheus/icons/dist/browsers/CE.png new file mode 100644 index 000000000..dd5b0c973 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/CE.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/CV.png b/app/plugins/Morpheus/icons/dist/browsers/CV.png new file mode 100644 index 000000000..fbf7b62d0 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/CV.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/DD.png b/app/plugins/Morpheus/icons/dist/browsers/DD.png new file mode 100644 index 000000000..9e64e4223 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/DD.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/EC.png b/app/plugins/Morpheus/icons/dist/browsers/EC.png new file mode 100644 index 000000000..1875baeeb Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/EC.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/EP.png b/app/plugins/Morpheus/icons/dist/browsers/EP.png index 93dd95af6..dfdbecd04 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/EP.png and b/app/plugins/Morpheus/icons/dist/browsers/EP.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/F1.png b/app/plugins/Morpheus/icons/dist/browsers/F1.png new file mode 100644 index 000000000..11fd80083 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/F1.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/FF.png b/app/plugins/Morpheus/icons/dist/browsers/FF.png index 397fd9399..11fd80083 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/FF.png and b/app/plugins/Morpheus/icons/dist/browsers/FF.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/FM.png b/app/plugins/Morpheus/icons/dist/browsers/FM.png index 397fd9399..11fd80083 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/FM.png and b/app/plugins/Morpheus/icons/dist/browsers/FM.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/FR.png b/app/plugins/Morpheus/icons/dist/browsers/FR.png new file mode 100644 index 000000000..c8f3391b5 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/FR.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/HC.png b/app/plugins/Morpheus/icons/dist/browsers/HC.png index d7dd68592..a9e49cc76 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/HC.png and b/app/plugins/Morpheus/icons/dist/browsers/HC.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/I3.png b/app/plugins/Morpheus/icons/dist/browsers/I3.png new file mode 100644 index 000000000..e1726d12e Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/I3.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/I4.png b/app/plugins/Morpheus/icons/dist/browsers/I4.png new file mode 100644 index 000000000..f34bd3cf2 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/I4.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/KW.png b/app/plugins/Morpheus/icons/dist/browsers/KW.png new file mode 100644 index 000000000..e03a18da6 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/KW.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/KY.png b/app/plugins/Morpheus/icons/dist/browsers/KY.png index 741356b0c..649ca81af 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/KY.png and b/app/plugins/Morpheus/icons/dist/browsers/KY.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/MO.png b/app/plugins/Morpheus/icons/dist/browsers/MO.png new file mode 100644 index 000000000..c13075370 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/MO.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/MS.png b/app/plugins/Morpheus/icons/dist/browsers/MS.png index b541966a6..2e8d191c0 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/MS.png and b/app/plugins/Morpheus/icons/dist/browsers/MS.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/MT.png b/app/plugins/Morpheus/icons/dist/browsers/MT.png new file mode 100644 index 000000000..889c6f674 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/MT.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/O1.png b/app/plugins/Morpheus/icons/dist/browsers/O1.png new file mode 100644 index 000000000..2cd3c5927 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/O1.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/OG.png b/app/plugins/Morpheus/icons/dist/browsers/OG.png new file mode 100644 index 000000000..ed5363abd Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/OG.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/OH.png b/app/plugins/Morpheus/icons/dist/browsers/OH.png new file mode 100644 index 000000000..15fe636cb Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/OH.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/Q1.png b/app/plugins/Morpheus/icons/dist/browsers/Q1.png new file mode 100644 index 000000000..533f5b222 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/Q1.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/QM.png b/app/plugins/Morpheus/icons/dist/browsers/QM.png new file mode 100644 index 000000000..d49a146aa Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/QM.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/QT.png b/app/plugins/Morpheus/icons/dist/browsers/QT.png index af34b3c77..beee6838c 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/QT.png and b/app/plugins/Morpheus/icons/dist/browsers/QT.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/QW.png b/app/plugins/Morpheus/icons/dist/browsers/QW.png new file mode 100644 index 000000000..51d68b8da Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/QW.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/SB.png b/app/plugins/Morpheus/icons/dist/browsers/SB.png index 2968b3ce0..1e4df1c1d 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/SB.png and b/app/plugins/Morpheus/icons/dist/browsers/SB.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/SO.png b/app/plugins/Morpheus/icons/dist/browsers/SO.png new file mode 100644 index 000000000..6688d1d49 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/SO.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/ST.png b/app/plugins/Morpheus/icons/dist/browsers/ST.png index c54614138..78622138f 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/ST.png and b/app/plugins/Morpheus/icons/dist/browsers/ST.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/SZ.png b/app/plugins/Morpheus/icons/dist/browsers/SZ.png new file mode 100644 index 000000000..fcd304dda Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/SZ.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/TB.png b/app/plugins/Morpheus/icons/dist/browsers/TB.png new file mode 100644 index 000000000..aa695173c Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/TB.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/TF.png b/app/plugins/Morpheus/icons/dist/browsers/TF.png new file mode 100644 index 000000000..b8e1281fb Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/TF.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/UM.png b/app/plugins/Morpheus/icons/dist/browsers/UM.png new file mode 100644 index 000000000..870a650ed Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/UM.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/WF.png b/app/plugins/Morpheus/icons/dist/browsers/WF.png index c3cb0243c..b083f5c9e 100644 Binary files a/app/plugins/Morpheus/icons/dist/browsers/WF.png and b/app/plugins/Morpheus/icons/dist/browsers/WF.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/WH.png b/app/plugins/Morpheus/icons/dist/browsers/WH.png new file mode 100644 index 000000000..7e510b229 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/WH.png differ diff --git a/app/plugins/Morpheus/icons/dist/browsers/WP.png b/app/plugins/Morpheus/icons/dist/browsers/WP.png new file mode 100644 index 000000000..96400a75e Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/browsers/WP.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/ai.png b/app/plugins/Morpheus/icons/dist/flags/ai.png index 75737a511..5b2c2392f 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/ai.png and b/app/plugins/Morpheus/icons/dist/flags/ai.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/aq.png b/app/plugins/Morpheus/icons/dist/flags/aq.png index 130cff584..5ad7fcdb6 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/aq.png and b/app/plugins/Morpheus/icons/dist/flags/aq.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/at.png b/app/plugins/Morpheus/icons/dist/flags/at.png index e15fdc507..f544e41eb 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/at.png and b/app/plugins/Morpheus/icons/dist/flags/at.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/ca.png b/app/plugins/Morpheus/icons/dist/flags/ca.png index bc8769036..f06557799 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/ca.png and b/app/plugins/Morpheus/icons/dist/flags/ca.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/gb.png b/app/plugins/Morpheus/icons/dist/flags/gb.png index 88b128d3c..a05572c35 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/gb.png and b/app/plugins/Morpheus/icons/dist/flags/gb.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/gf.png b/app/plugins/Morpheus/icons/dist/flags/gf.png index bda5b92f6..537b14fc5 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/gf.png and b/app/plugins/Morpheus/icons/dist/flags/gf.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/gr.png b/app/plugins/Morpheus/icons/dist/flags/gr.png index 94fd21129..20f6b7a10 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/gr.png and b/app/plugins/Morpheus/icons/dist/flags/gr.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/gu.png b/app/plugins/Morpheus/icons/dist/flags/gu.png index 21822af69..e119db8b1 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/gu.png and b/app/plugins/Morpheus/icons/dist/flags/gu.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/nc.png b/app/plugins/Morpheus/icons/dist/flags/nc.png index 537b14fc5..44bd5f3f6 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/nc.png and b/app/plugins/Morpheus/icons/dist/flags/nc.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/nl.png b/app/plugins/Morpheus/icons/dist/flags/nl.png index 97ebad3f0..370d624ec 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/nl.png and b/app/plugins/Morpheus/icons/dist/flags/nl.png differ diff --git a/app/plugins/Morpheus/icons/dist/flags/si.png b/app/plugins/Morpheus/icons/dist/flags/si.png index 99574c437..430c59ec8 100644 Binary files a/app/plugins/Morpheus/icons/dist/flags/si.png and b/app/plugins/Morpheus/icons/dist/flags/si.png differ diff --git a/app/plugins/Morpheus/icons/dist/os/FOS.png b/app/plugins/Morpheus/icons/dist/os/FOS.png index 397fd9399..11fd80083 100644 Binary files a/app/plugins/Morpheus/icons/dist/os/FOS.png and b/app/plugins/Morpheus/icons/dist/os/FOS.png differ diff --git a/app/plugins/Morpheus/icons/dist/socials/peepeth.com.png b/app/plugins/Morpheus/icons/dist/socials/peepeth.com.png new file mode 100644 index 000000000..4ecf919e7 Binary files /dev/null and b/app/plugins/Morpheus/icons/dist/socials/peepeth.com.png differ diff --git a/app/plugins/Morpheus/images/compare.svg b/app/plugins/Morpheus/images/compare.svg new file mode 100644 index 000000000..efefc4367 --- /dev/null +++ b/app/plugins/Morpheus/images/compare.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/plugins/Morpheus/images/logo-email.png b/app/plugins/Morpheus/images/logo-email.png old mode 100644 new mode 100755 index 8d4581f27..2a80c3cc2 Binary files a/app/plugins/Morpheus/images/logo-email.png and b/app/plugins/Morpheus/images/logo-email.png differ diff --git a/app/plugins/Morpheus/images/logo-header.png b/app/plugins/Morpheus/images/logo-header.png old mode 100644 new mode 100755 index 234d96a75..2a80c3cc2 Binary files a/app/plugins/Morpheus/images/logo-header.png and b/app/plugins/Morpheus/images/logo-header.png differ diff --git a/app/plugins/Morpheus/images/logo.png b/app/plugins/Morpheus/images/logo.png old mode 100644 new mode 100755 index 24e59ee09..0ce182d9e Binary files a/app/plugins/Morpheus/images/logo.png and b/app/plugins/Morpheus/images/logo.png differ diff --git a/app/plugins/Morpheus/images/logo.svg b/app/plugins/Morpheus/images/logo.svg index 809c79a91..2efc5caec 100644 --- a/app/plugins/Morpheus/images/logo.svg +++ b/app/plugins/Morpheus/images/logo.svg @@ -1,6 +1,174 @@ - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/plugins/Morpheus/stylesheets/base/colors.less b/app/plugins/Morpheus/stylesheets/base/colors.less index 8c82888f0..9a8150c33 100644 --- a/app/plugins/Morpheus/stylesheets/base/colors.less +++ b/app/plugins/Morpheus/stylesheets/base/colors.less @@ -31,14 +31,46 @@ @color-blue-brandSocialVeryLight: #00aced; @color-orange-brand: #ff9600; -@graph-colors-data-series1: #3450A3; -@graph-colors-data-series2: #43a047; -@graph-colors-data-series3: #ff7f00; -@graph-colors-data-series4: #d4291f; -@graph-colors-data-series5: #6a3d9a; -@graph-colors-data-series6: #b15928; -@graph-colors-data-series7: #fdbf6f; -@graph-colors-data-series8: #cab2d6; +// see https://material.io/design/color/#tools-for-picking-colors for source for colors +@graph-colors-data-series0: #0277BD; // light blue 50 series (800) +@graph-series0-shade1-color: #40C4FF; // (A200) +@graph-series0-shade2-color: #00B0FF; // (A400) +@graph-series0-shade3-color: #0091EA; // (A700) + +@graph-colors-data-series1: #FF8F00; // amber 50 series (800) +@graph-series1-shade1-color: #FFD740; // (A200) +@graph-series1-shade2-color: #FFC400; // (A400) +@graph-series1-shade3-color: #FFAB00; // (A700) + +@graph-colors-data-series2: #AD1457; // pink 50 series (800) +@graph-series2-shade1-color: #FF4081; // (A200) +@graph-series2-shade2-color: #F50057; // (A400) +@graph-series2-shade3-color: #C51162; // (A700) + +@graph-colors-data-series3: #6A1B9A; // purple 50 series (800) +@graph-series3-shade1-color: #E040FB; // (A200) +@graph-series3-shade2-color: #D500F9; // (A400) +@graph-series3-shade3-color: #AA00FF; // (A700) + +@graph-colors-data-series4: #558B2F; // light green 50 series (800) +@graph-series4-shade1-color: #B2FF59; // (A200) +@graph-series4-shade2-color: #76FF03; // (A400) +@graph-series4-shade3-color: #64DD17; // (A700) + +@graph-colors-data-series5: #00838F; // cyan 50 series (800) +@graph-series5-shade1-color: #18FFFF; // (A200) +@graph-series5-shade2-color: #00E5FF; // (A400) +@graph-series5-shade3-color: #00B8D4; // (A700) + +@graph-colors-data-series6: #283593; // indigo 50 series (800) +@graph-series6-shade1-color: #536DFE; // (A200) +@graph-series6-shade2-color: #3D5AFE; // (A400) +@graph-series6-shade3-color: #304FFE; // (A700) + +@graph-colors-data-series7: #D84315; // deep orange 50 series (800) +@graph-series7-shade1-color: #FF6E40; // (A200) +@graph-series7-shade2-color: #FF3D00; // (A400) +@graph-series7-shade3-color: #DD2C00; // (A700) @default-box-shade: 0 2px 3px 0 rgba(0,0,0,0.16), 0 0px 3px 0 rgba(0,0,0,0.12); diff --git a/app/plugins/Morpheus/stylesheets/main.less b/app/plugins/Morpheus/stylesheets/main.less index 950e06de4..659a5af4a 100644 --- a/app/plugins/Morpheus/stylesheets/main.less +++ b/app/plugins/Morpheus/stylesheets/main.less @@ -382,7 +382,7 @@ table.dataTable { } } - tr { + tr:not(.subDataTableContainer) { td { border-bottom: 1px solid @color-silver-l95 !important; border-color: @color-silver-l95 !important; @@ -405,6 +405,12 @@ table.dataTable { border-left: 0; } + &:hover { + td:not(.cellSubDataTable):not(.parentComparisonRow) { + background-color: @color-silver-l95; + } + } + &.label + td.column { // first column after label should have a bit less padding padding-left: 10px; @@ -481,23 +487,23 @@ table.dataTable { div.dataTableVizHtmlTable:not(.dataTableActions), div.dataTableVizAllColumns, div.dataTableVizGoals { - tr.subDataTable > td:first-child::before { + tr.subDataTable > td > span.label::before { display: inline-block; float: left; top: 0; width: 12px; height: 12px; - margin-left: -1px; + margin-left: -2px; margin-top: 3px; margin-right: 8px; content: ''; } - tr.subDataTable:not(.expanded) > td:first-child::before { + tr.subDataTable:not(.expanded) > td > span.label::before { background-image: url(plugins/Morpheus/images/plus.png); } - tr.subDataTable.expanded > td:first-child::before { + tr.subDataTable.expanded > td > span.label::before { background-image: url(plugins/Morpheus/images/minus.png); } } @@ -520,10 +526,25 @@ div.dataTableVizHtmlTable:not(.dataTableActions), } } + .UserCountryMap-btn-zoom { padding-left: 0; } +h6.sparkline-title { + margin-left: 2px; + text-transform: uppercase; + font-size: .8em; + font-weight: bold; + color: #999; + margin-bottom: 4px; + + max-width: 95px; + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; +} + div.sparkline { display: -ms-flexbox; -ms-box-orient: horizontal; @@ -532,10 +553,11 @@ div.sparkline { display: -moz-flex; display: -ms-flex; display: flex; - -webkit-justify-content: space-around; - -moz-justify-content: space-around; - -ms-justify-content: space-around; - justify-content: space-around; + -webkit-justify-content: flex-start; + -moz-justify-content: flex-start; + -ms-justify-content: flex-start; + justify-content: flex-start; + align-items: center; border-bottom: 0; margin-bottom: 10px; @@ -547,8 +569,20 @@ div.sparkline { border-bottom: 1px dashed #c3c3c3; } } + + .metric-group-title { + display: block; + font-size: .7em; + text-transform: uppercase; + color: #999; + } + + .sparkline-metrics { + margin-bottom: 4px; + } } + div.sparkline img { -webkit-flex-shrink: 0; -moz-flex-shrink: 0; @@ -561,7 +595,6 @@ div.sparkline script + div { margin: 1px 0 0 1px; } - .widgetpreview-base li.widgetpreview-choosen { background: @color-silver-l95; position: relative; diff --git a/app/plugins/Morpheus/stylesheets/simple_structure.css b/app/plugins/Morpheus/stylesheets/simple_structure.css index 3a37b7c8e..f43676d55 100644 --- a/app/plugins/Morpheus/stylesheets/simple_structure.css +++ b/app/plugins/Morpheus/stylesheets/simple_structure.css @@ -12,7 +12,7 @@ body#simple { #simple .logo { color: #888; text-align: center; - font-size: 12px; + font-size: 6px; background-color: #3450a3 !important; padding: 15px 0; } diff --git a/app/plugins/Morpheus/stylesheets/ui/_charts.less b/app/plugins/Morpheus/stylesheets/ui/_charts.less index f85008d8d..917fe9bd1 100644 --- a/app/plugins/Morpheus/stylesheets/ui/_charts.less +++ b/app/plugins/Morpheus/stylesheets/ui/_charts.less @@ -1,3 +1,132 @@ +// comparison series colors & shades +.comparison-series-color[data-name=series0] { // series0 + color: @graph-colors-data-series0; +} + +.comparison-series-color[data-name=series0-shade1] { + color: @graph-series0-shade1-color; +} + +.comparison-series-color[data-name=series0-shade2] { + color: @graph-series0-shade2-color; +} + +.comparison-series-color[data-name=series0-shade3] { + color: @graph-series0-shade3-color; +} + +.comparison-series-color[data-name=series1] { // series1 + color: @graph-colors-data-series1; +} + +.comparison-series-color[data-name=series1-shade1] { + color: @graph-series1-shade1-color; +} + +.comparison-series-color[data-name=series1-shade2] { + color: @graph-series1-shade2-color; +} + +.comparison-series-color[data-name=series1-shade3] { + color: @graph-series1-shade3-color; +} + +.comparison-series-color[data-name=series2] { // series2 + color: @graph-colors-data-series2; +} + +.comparison-series-color[data-name=series2-shade1] { + color: @graph-series2-shade1-color; +} + +.comparison-series-color[data-name=series2-shade2] { + color: @graph-series2-shade2-color; +} + +.comparison-series-color[data-name=series2-shade3] { + color: @graph-series2-shade3-color; +} + +.comparison-series-color[data-name=series3] { // series3 + color: @graph-colors-data-series3; +} + +.comparison-series-color[data-name=series3-shade1] { + color: @graph-series3-shade1-color; +} + +.comparison-series-color[data-name=series3-shade2] { + color: @graph-series3-shade2-color; +} + +.comparison-series-color[data-name=series3-shade3] { + color: @graph-series3-shade3-color; +} + +.comparison-series-color[data-name=series4] { // series4 + color: @graph-colors-data-series4; +} + +.comparison-series-color[data-name=series4-shade1] { + color: @graph-series4-shade1-color; +} + +.comparison-series-color[data-name=series4-shade2] { + color: @graph-series4-shade2-color; +} + +.comparison-series-color[data-name=series4-shade3] { + color: @graph-series4-shade3-color; +} + +.comparison-series-color[data-name=series5] { // series5 + color: @graph-colors-data-series5; +} + +.comparison-series-color[data-name=series5-shade1] { + color: @graph-series5-shade1-color; +} + +.comparison-series-color[data-name=series5-shade2] { + color: @graph-series5-shade2-color; +} + +.comparison-series-color[data-name=series5-shade3] { + color: @graph-series5-shade3-color; +} + +.comparison-series-color[data-name=series6] { // series6 + color: @graph-colors-data-series6; +} + +.comparison-series-color[data-name=series6-shade1] { + color: @graph-series6-shade1-color; +} + +.comparison-series-color[data-name=series6-shade2] { + color: @graph-series6-shade2-color; +} + +.comparison-series-color[data-name=series6-shade3] { + color: @graph-series6-shade3-color; +} + +.comparison-series-color[data-name=series7] { // series7 + color: @graph-colors-data-series7; +} + +.comparison-series-color[data-name=series7-shade1] { + color: @graph-series7-shade1-color; +} + +.comparison-series-color[data-name=series7-shade2] { + color: @graph-series7-shade2-color; +} + +.comparison-series-color[data-name=series7-shade3] { + color: @graph-series7-shade3-color; +} + // bar graph colors .bar-graph-colors[data-name=grid-background] { color: @theme-color-background-contrast; @@ -12,35 +141,35 @@ } .bar-graph-colors[data-name=series1] { - color: @graph-colors-data-series1; + color: @graph-colors-data-series0; } .bar-graph-colors[data-name=series2] { - color: @graph-colors-data-series2; + color: @graph-colors-data-series1; } .bar-graph-colors[data-name=series3] { - color: @graph-colors-data-series3; + color: @graph-colors-data-series2; } .bar-graph-colors[data-name=series4] { - color: @graph-colors-data-series4; + color: @graph-colors-data-series3; } .bar-graph-colors[data-name=series5] { - color: @graph-colors-data-series5; + color: @graph-colors-data-series4; } -.bar-graph-colors[data-name=series6] { - color: @graph-colors-data-series6; +.bar-graph-colors[data-name=series5] { + color: @graph-colors-data-series5; } .bar-graph-colors[data-name=series7] { - color: @graph-colors-data-series7; + color: @graph-colors-data-series6; } .bar-graph-colors[data-name=series8] { - color: @graph-colors-data-series8; + color: @graph-colors-data-series7; } .bar-graph-colors[data-name=ticks] { @@ -61,35 +190,35 @@ } .pie-graph-colors[data-name=series1] { - color: @graph-colors-data-series1; + color: @graph-colors-data-series0; } .pie-graph-colors[data-name=series2] { - color: @graph-colors-data-series2; + color: @graph-colors-data-series1; } .pie-graph-colors[data-name=series3] { - color: @graph-colors-data-series3; + color: @graph-colors-data-series2; } .pie-graph-colors[data-name=series4] { - color: @graph-colors-data-series4; + color: @graph-colors-data-series3; } .pie-graph-colors[data-name=series5] { - color: @graph-colors-data-series5; + color: @graph-colors-data-series4; } .pie-graph-colors[data-name=series6] { - color: @graph-colors-data-series6; + color: @graph-colors-data-series5; } .pie-graph-colors[data-name=series7] { - color: @graph-colors-data-series7; + color: @graph-colors-data-series6; } .pie-graph-colors[data-name=series8] { - color: @graph-colors-data-series8; + color: @graph-colors-data-series7; } .pie-graph-colors[data-name=ticks] { @@ -105,35 +234,35 @@ // evolution graph colors .evolution-graph-colors[data-name=series1] { - color: @graph-colors-data-series1; + color: @graph-colors-data-series0; } .evolution-graph-colors[data-name=series2] { - color: @graph-colors-data-series2; + color: @graph-colors-data-series1; } .evolution-graph-colors[data-name=series3] { - color: @graph-colors-data-series3; + color: @graph-colors-data-series2; } .evolution-graph-colors[data-name=series4] { - color: @graph-colors-data-series4; + color: @graph-colors-data-series3; } .evolution-graph-colors[data-name=series5] { - color: @graph-colors-data-series5; + color: @graph-colors-data-series4; } .evolution-graph-colors[data-name=series6] { - color: @graph-colors-data-series6; + color: @graph-colors-data-series5; } .evolution-graph-colors[data-name=series7] { - color: @graph-colors-data-series7; + color: @graph-colors-data-series6; } .evolution-graph-colors[data-name=series8] { - color: @graph-colors-data-series8; + color: @graph-colors-data-series7; } .evolution-graph-colors[data-name=ticks] { diff --git a/app/plugins/Morpheus/stylesheets/uibase/_header.less b/app/plugins/Morpheus/stylesheets/uibase/_header.less index e31cf081d..f9d8d2d19 100644 --- a/app/plugins/Morpheus/stylesheets/uibase/_header.less +++ b/app/plugins/Morpheus/stylesheets/uibase/_header.less @@ -2,13 +2,13 @@ #root { #logo { padding-left: 16px; - padding-top: 4px; + padding-top: 6px; img { - max-height: 28px; + max-height: 32px; &.default-piwik-logo { - width: 144px; + width: 150px; } } } diff --git a/app/plugins/Morpheus/stylesheets/uibase/_periodSelect.less b/app/plugins/Morpheus/stylesheets/uibase/_periodSelect.less index 023f27844..f14fc64fd 100644 --- a/app/plugins/Morpheus/stylesheets/uibase/_periodSelect.less +++ b/app/plugins/Morpheus/stylesheets/uibase/_periodSelect.less @@ -56,10 +56,6 @@ padding: 0 0 4px 0; } -#periodMore { - overflow: hidden; -} - #periodString .period-date, #periodString .period-range { padding: 0 16px 0 0; @@ -77,3 +73,16 @@ #periodString label.selected-period-label { text-decoration: underline; } + +#periodString .compareCheckbox { + transform: scale(.8); + margin-left: -29px; + + .form-group { + margin: 15px 0 0; + } + + label { + padding-left: 25px; + } +} diff --git a/app/plugins/Morpheus/templates/_jsGlobalVariables.twig b/app/plugins/Morpheus/templates/_jsGlobalVariables.twig index d49738c8f..7f127d5b2 100644 --- a/app/plugins/Morpheus/templates/_jsGlobalVariables.twig +++ b/app/plugins/Morpheus/templates/_jsGlobalVariables.twig @@ -22,7 +22,12 @@ {% if idSite is defined %}piwik.idSite = "{{ idSite }}";{% endif %} - {% if siteName is defined %}piwik.siteName = "{{ siteName|e('js') }}";{% endif %} + {% if siteName is defined %} + // NOTE: siteName is currently considered deprecated, use piwik.currentSiteName instead, which will not contain HTML entities + piwik.siteName = "{{ siteName|e('js') }}"; + {% if siteNameDecoded is defined %} // just to be safe + piwik.currentSiteName = {{ siteNameDecoded|json_encode|raw }};{% endif %} + {% endif %} {% if siteMainUrl is defined %}piwik.siteMainUrl = "{{ siteMainUrl|e('js') }}";{% endif %} @@ -54,3 +59,4 @@ piwik.shouldPropagateTokenAuth = {{ shouldPropagateTokenAuth|json_encode|raw }}; {{ postEvent("Template.jsGlobalVariables") }} + diff --git a/app/plugins/Morpheus/templates/ajaxMacros.twig b/app/plugins/Morpheus/templates/ajaxMacros.twig index 4546e4a0a..da40e5bf9 100644 --- a/app/plugins/Morpheus/templates/ajaxMacros.twig +++ b/app/plugins/Morpheus/templates/ajaxMacros.twig @@ -31,7 +31,7 @@ {%- if areAdsForProfessionalServicesEnabled %} – - {% set supportUrl = 'https://matomo.org/support/?pk_campaign=Help&pk_medium=AjaxError&pk_content=' ~ currentModule ~ '&pk_source=Piwik_App' %} + {% set supportUrl = 'https://matomo.org/support-plans/?pk_campaign=Help&pk_medium=AjaxError&pk_content=' ~ currentModule ~ '&pk_source=Matomo_App' %} {{ 'Feedback_ProfessionalHelp'|translate }} {%- endif %}.
diff --git a/app/plugins/Morpheus/templates/dashboard.twig b/app/plugins/Morpheus/templates/dashboard.twig index 36b357d3a..1ce02012f 100644 --- a/app/plugins/Morpheus/templates/dashboard.twig +++ b/app/plugins/Morpheus/templates/dashboard.twig @@ -51,6 +51,8 @@
+ + {% block content %} {% endblock %} diff --git a/app/plugins/MultiSites/MultiSites.php b/app/plugins/MultiSites/MultiSites.php index 4b7f614e6..c4fbd355f 100644 --- a/app/plugins/MultiSites/MultiSites.php +++ b/app/plugins/MultiSites/MultiSites.php @@ -21,10 +21,16 @@ public function registerEvents() 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', 'AssetManager.getJavaScriptFiles' => 'getJsFiles', 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', - 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations' + 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', ); } + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = 'MultiSites.index'; + } + public function addMetricTranslations(&$translations) { $appendix = " " . Piwik::translate('MultiSites_Evolution'); diff --git a/app/plugins/MultiSites/angularjs/dashboard/dashboard.directive.html b/app/plugins/MultiSites/angularjs/dashboard/dashboard.directive.html index 2be78996d..5a8eac3aa 100644 --- a/app/plugins/MultiSites/angularjs/dashboard/dashboard.directive.html +++ b/app/plugins/MultiSites/angularjs/dashboard/dashboard.directive.html @@ -67,7 +67,7 @@ – {{ 'Feedback_CommunityHelp'|translate }} - {{ 'Feedback_ProfessionalHelp'|translate }}. + {{ 'Feedback_ProfessionalHelp'|translate }}. diff --git a/app/plugins/MultiSites/config/config.php b/app/plugins/MultiSites/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/MultiSites/config/config.php +++ b/app/plugins/MultiSites/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/MultiSites/config/tracker.php b/app/plugins/MultiSites/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/MultiSites/config/tracker.php +++ b/app/plugins/MultiSites/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/MultiSites/lang/pt-br.json b/app/plugins/MultiSites/lang/pt-br.json index a38450106..71a861df0 100644 --- a/app/plugins/MultiSites/lang/pt-br.json +++ b/app/plugins/MultiSites/lang/pt-br.json @@ -2,7 +2,7 @@ "MultiSites": { "Evolution": "Evolução", "LoadingWebsites": "Carregando sites", - "PluginDescription": "Veja e compare todos os seus sites e aplicativos neste útil painel 'Todos Websites'.", - "TopLinkTooltip": "Comparar estatísticas de todos os websites." + "PluginDescription": "Veja e compare todos os seus sites e aplicativos neste útil painel 'Todos os sites'.", + "TopLinkTooltip": "Comparar estatísticas de Web Analytics de todos os seus sites." } } \ No newline at end of file diff --git a/app/plugins/Overlay/Controller.php b/app/plugins/Overlay/Controller.php index 670819652..98ff9937d 100644 --- a/app/plugins/Overlay/Controller.php +++ b/app/plugins/Overlay/Controller.php @@ -177,6 +177,7 @@ public function startOverlaySession() $view->mainUrl = $site['main_url']; $this->outputCORSHeaders(); + $view->setUseStrictReferrerPolicy(false); Common::sendHeader('Content-Type: text/html; charset=UTF-8'); return $view->render(); diff --git a/app/plugins/Overlay/config/config.php b/app/plugins/Overlay/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/Overlay/config/config.php +++ b/app/plugins/Overlay/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Overlay/config/tracker.php b/app/plugins/Overlay/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Overlay/config/tracker.php +++ b/app/plugins/Overlay/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Overlay/lang/cs.json b/app/plugins/Overlay/lang/cs.json index 9f6308fe4..20f5c1e2b 100644 --- a/app/plugins/Overlay/lang/cs.json +++ b/app/plugins/Overlay/lang/cs.json @@ -14,6 +14,7 @@ "OpenFullScreen": "Přejít na celou obrazovku (bez postranní lišty)", "Overlay": "Překryv stránky", "PluginDescription": "Podívejte se na analytická data jako na překryv na svých stránkách. Zjistěte, kolik uživatelů kliklo na jaký odkaz. Poznámka: vyžaduje povolený zásuvný modul přechodů.", + "RedirectUrlError": "Pokoušíte se otevřít překryvnou stránku pro URL \"%1$s\". %2$s Odkaz neodpovídá žádnému z domén v nastavení Matomo.", "RedirectUrlErrorAdmin": "Doménu můžete jako další URL přidat v %1$snastavení%2$s.", "RedirectUrlErrorUser": "Požádejte svého administrátora o přidání stránky jako další URL:." } diff --git a/app/plugins/Overlay/lang/da.json b/app/plugins/Overlay/lang/da.json index 31971b96c..3dc68db6c 100644 --- a/app/plugins/Overlay/lang/da.json +++ b/app/plugins/Overlay/lang/da.json @@ -14,6 +14,7 @@ "OpenFullScreen": "Vis fuldskærm (ingen sidebjælke)", "Overlay": "Side overlejring", "PluginDescription": "Se dine analytiske data som et lag over din aktuelle webside. Se hvor mange gange brugere har klikket på de enkelte links. Note: Kræver at Transitions programtilføjelsen er aktiveret.", + "RedirectUrlError": "Du forsøger at åbne sideoverlejring for URL \"%1$s\". %2$s Ingen af ​​domæner fra Matomos indstillinger matcher linket.", "RedirectUrlErrorAdmin": "Du kan tilføje domænet som en yderligere URL %1$si indstillingerne%2$s.", "RedirectUrlErrorUser": "Spørg administratoren om at tilføje domænet som en yderligere URL." } diff --git a/app/plugins/Overlay/lang/es-ar.json b/app/plugins/Overlay/lang/es-ar.json index f8d56b85c..feff16580 100644 --- a/app/plugins/Overlay/lang/es-ar.json +++ b/app/plugins/Overlay/lang/es-ar.json @@ -1,17 +1,21 @@ { "Overlay": { - "Clicks": "%s clicks", - "ClicksFromXLinks": "%1$s clics desde uno de %2$s links", + "Clicks": "%s clics", + "ClicksFromXLinks": "%1$s clics desde uno de %2$s enlaces", "Domain": "Dominio", - "ErrorNotLoading": "La sesión de superposición de página no puede ser aun iniciada.", - "ErrorNotLoadingLink": "Clic aquí para obtener más ayuda acerca de soluciones de problemas.", + "ErrorNotLoading": "Aún no se pudo lanzar la sesión de superposición de página.", + "ErrorNotLoadingDetails": "Quizá la página cargada en la derecha no tiene el código de seguimiento de Matomo. De ser así, intentá lanzando la superposición para una página diferente desde el informe de páginas.", + "ErrorNotLoadingDetailsSSL": "Debido a que estás usando Matomo sobre HTTPS, lo más probable es que tu sitio web no soporte SSL. Intentá usar Matomo sobre HTTP.", + "ErrorNotLoadingLink": "Hacé clic acá para obtener más ayuda acerca de soluciones de problema.", "Link": "Enlace", "Location": "Ubicación", "NoData": "No hay información para esta página en el período seleccionado.", "OneClick": "1 clic", - "OpenFullScreen": "Ir a pantalla completa", + "OpenFullScreen": "Ir a pantalla completa (sin barra lateral)", "Overlay": "Vísta general de página", - "RedirectUrlErrorAdmin": "Puede agregar el dominio como una dirección de internet %1$sen la configuración%2$s.", - "RedirectUrlErrorUser": "Pregunte a su administrador para agregar el dominio como una dirección de internet adicional." + "PluginDescription": "Mirá tus datos analíticos sobre una superposición en tu sitio web real. Mirá cuántas veces tus usuarios hicieron clic en cada enlace. Nota: requiere que el plugin Transitions esté habilitado.", + "RedirectUrlError": "Estás intentando abrir la superposición de página para la dirección web \"%1$s\". %2$s Ninguno de los dominios en la configuración de Matomo coincide con el enlace.", + "RedirectUrlErrorAdmin": "Podés agregar el dominio como una dirección web %1$sen la configuración%2$s.", + "RedirectUrlErrorUser": "Pedile a tu administrador que agregue el dominio como una dirección web adicional." } } \ No newline at end of file diff --git a/app/plugins/Overlay/lang/it.json b/app/plugins/Overlay/lang/it.json index b1b2012c4..6834fc24a 100644 --- a/app/plugins/Overlay/lang/it.json +++ b/app/plugins/Overlay/lang/it.json @@ -4,7 +4,7 @@ "ClicksFromXLinks": "%1$s clicks per uno di %2$s links", "Domain": "Dominio", "ErrorNotLoading": "La sessione di Overlay Pagina non può ancora essere lanciata.", - "ErrorNotLoadingDetails": "Forse la pagina caricata sulla destra non ha il codice di tracking di Matomo. In questo caso prova ad avviare Overlay per una pagina diversa dal report pagine.", + "ErrorNotLoadingDetails": "Forse la pagina caricata sulla destra non ha il codice di tracking di Matomo. In questo caso prova a lanciare Overlay per una pagina diversa dal report pagine.", "ErrorNotLoadingDetailsSSL": "Dal momento che si sta utilizzando Matomo su HTTPS, la causa più probabile è che il vostro sito web non supporti SSL. Prova a utilizzare Matomo su HTTP.", "ErrorNotLoadingLink": "Ottieni altri suggerimenti sui problemi", "Link": "Link", diff --git a/app/plugins/Overlay/lang/zh-cn.json b/app/plugins/Overlay/lang/zh-cn.json index 04c9c0d48..26fc5f447 100644 --- a/app/plugins/Overlay/lang/zh-cn.json +++ b/app/plugins/Overlay/lang/zh-cn.json @@ -14,6 +14,7 @@ "OpenFullScreen": "全屏(无边框)", "Overlay": "页面叠加", "PluginDescription": "请参阅您的分析数据为您的实际网站的叠加。查看有多少次你的用户点击每一个环节上。注:需要在转换插件启用。", + "RedirectUrlError": "您正在尝试打开URL“ %1$s”的页面覆盖。 %2$s Matomo设置中的所有域都不匹配该链接。", "RedirectUrlErrorAdmin": "您可以在 %1$s管理设置%2$s 中以附加网址添加域名。", "RedirectUrlErrorUser": "请管理员以附加网址来添加域名。" } diff --git a/app/plugins/PrivacyManager/PrivacyManager.php b/app/plugins/PrivacyManager/PrivacyManager.php index d073dd0cd..99deb31fc 100644 --- a/app/plugins/PrivacyManager/PrivacyManager.php +++ b/app/plugins/PrivacyManager/PrivacyManager.php @@ -203,6 +203,11 @@ public function setTrackerCacheGeneral(&$cacheContent) $config = new Config(); $cacheContent = $config->setTrackerCacheGeneral($cacheContent); $cacheContent[self::OPTION_USERID_SALT] = self::getUserIdSalt(); + + $purgeSettings = PrivacyManager::getPurgeDataSettings(); + $cacheContent['delete_logs_enable'] = $purgeSettings['delete_logs_enable']; + $cacheContent['delete_logs_schedule_lowest_interval'] = $purgeSettings['delete_logs_schedule_lowest_interval']; + $cacheContent['delete_logs_older_than'] = $purgeSettings['delete_logs_older_than']; } public function getJsFiles(&$jsFiles) diff --git a/app/plugins/PrivacyManager/config/config.php b/app/plugins/PrivacyManager/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/PrivacyManager/config/config.php +++ b/app/plugins/PrivacyManager/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/PrivacyManager/config/tracker.php b/app/plugins/PrivacyManager/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/PrivacyManager/config/tracker.php +++ b/app/plugins/PrivacyManager/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/PrivacyManager/lang/de.json b/app/plugins/PrivacyManager/lang/de.json index f02bcc586..c52fc82b2 100644 --- a/app/plugins/PrivacyManager/lang/de.json +++ b/app/plugins/PrivacyManager/lang/de.json @@ -3,7 +3,7 @@ "AnonymizeData": "Daten anonymisieren", "AnonymizeIpDescription": "Wählen Sie \"Ja\", wenn Matomo keine vollständigen IP-Adressen speichern soll.", "AnonymizeIpInlineHelp": "Um den Datenschutzbestimmungen Ihres Landes gerecht zu werden, können Sie mit diesem Plugin die letzten Bytes der IP-Adresse Ihrer Besucher anonymisieren.", - "AnonymizeIpExtendedHelp": "Wenn Benutzer Ihre Website besuchen, wird Matomo nicht die komplette IP-Adresse (so wie %1$s) benützen, sondern sie stattdessen zuerst anonymisieren (zu %2$s). Die Anonymisierung der IP-Adresse ist in einigen Ländern eine gesetzliche Pflicht, zum Beispiel in Deutschland vorgegeben durch das Datenschutzrecht.", + "AnonymizeIpExtendedHelp": "Wenn Benutzer Ihre Webseite besuchen, wird Matomo nicht die komplette IP-Adresse (so wie %1$s) benützen, sondern sie stattdessen zuerst anonymisieren (zu %2$s). Die Anonymisierung der IP-Adresse ist in einigen Ländern eine gesetzliche Pflicht, zum Beispiel in Deutschland vorgegeben durch das Datenschutzrecht.", "AnonymizeIpMaskLengtDescription": "Wählen Sie aus, wieviele Bytes der Besucher-IP maskiert werden sollen.", "AnonymizeIpMaskLength": "%1$s byte(s) - z.B. %2$s", "AskingForConsent": "Um Erlaubnis bitten", @@ -30,7 +30,7 @@ "DeleteReportsOlderThan": "Lösche Berichte, die älter sind als", "DeleteSchedulingSettings": "Löschung alter Daten planen", "DeleteDataSettings": "Alte Besucher-Logs und Berichte löschen", - "DoNotTrack_Description": "Do-not-Track ist eine Technologie und ein Richtlinienvorschlag, der es Benutzern ermöglichen soll, selbständig zu entscheiden, ob ihr Verhalten von Websites, Werbenetzwerken und Sozialen Netzwerken verfolgt wird.", + "DoNotTrack_Description": "Do-not-Track ist eine Technologie und ein Richtlinienvorschlag, der es Benutzern ermöglichen soll, selbständig zu entscheiden, ob ihr Verhalten von Webseiten, Werbenetzwerken und Sozialen Netzwerken verfolgt wird.", "DoNotTrack_Disable": "Do-not-Track Unterstützung deaktivieren", "DoNotTrack_Disabled": "Matomo verfolgt derzeit das Verhalten aller Besucher, auch derjenigen, die explizit in ihrem Browser \"Ich möchte nicht getrackt werden\" eingestellt haben.", "DoNotTrack_DisabledMoreInfo": "Wir empfehlen, die DoNotTrack Unterstützung zu aktivieren, damit die Privatsphäre Ihrer Besucher respektiert wird.", diff --git a/app/plugins/PrivacyManager/lang/es-ar.json b/app/plugins/PrivacyManager/lang/es-ar.json index 990340147..cf7b12329 100644 --- a/app/plugins/PrivacyManager/lang/es-ar.json +++ b/app/plugins/PrivacyManager/lang/es-ar.json @@ -1,44 +1,90 @@ { "PrivacyManager": { - "AnonymizeIpInlineHelp": "Ocultar la dirección IP del visitante para cumplir las leyes\/directrices de privacidad de su localidad.", - "AnonymizeIpMaskLengtDescription": "Seleccione cuantos bytes de las direcciones IP de los visitantes deben ser ocultados.", - "AnonymizeIpMaskLength": "%1$s byte(s) - ej. %2$s", - "ClickHereSettings": "Haga clic aquí para acceder a la configuración de %s", - "CurrentDBSize": "Tamaño actual de la base de datso", - "DBPurged": "DB purgado.", - "DeleteDataInterval": "Borrar antigua información cada", - "DeleteLogDescription2": "Cuando habilite la eliminación automática de registros, debe comprobar que todos los reportes diarios previos han sido procesados, de modo que ningún dato es perdido.", + "AnonymizeData": "Anonimizar datos", + "AnonymizeIpDescription": "Seleccioná \"Sí\" si querés que Matomo no rastre direcciones IP totalmente calificadas.", + "AnonymizeIpInlineHelp": "Anonimizá la última parte de las direcciones IP de tus visitantes para cumplir con las guías\/leyes de privacidad local.", + "AnonymizeIpExtendedHelp": "Cuando los usuarios visiten tu sitio web, Matomo no usará la dirección IP completa (como %1$s) sino que Matomo la anonimizará primero (a %2$s). La anonimización de dirección IP es uno de los requerimientos establecidos por leyes sobre la privacidad en algunos países como Alemania.", + "AnonymizeIpMaskLengtDescription": "Seleccioná cuántos bytes de las direcciones IP de los visitantes deben ser ocultados.", + "AnonymizeIpMaskLength": "%1$s byte\/s - ej. %2$s", + "AskingForConsent": "Pedir consentimiento", + "ClickHereSettings": "Hacé clic acá para acceder a la configuración de %s.", + "CurrentDBSize": "Tamaño actual de la base de datos", + "DBPurged": "Base da datos purgada.", + "DeleteBothConfirm": "Estás a punto de habilitar tanto la eliminación de datos en crudo como la eliminación de datos de informes. Esto quitará permanentemente tu habilidad de ver datos analíticos antiguos. ¿Estás seguro que querés hacer esto?", + "DeleteDataDescription": "Podés configurar a Matomo para que regularmente elimine datos crudos antiguos y\/o informes agregados, para mantener chica tu base de datos o para cumplir con regulaciones de privacidad como el GDPR (Reglamento General de Protección de Datos).", + "DeleteDataInterval": "Eliminar datos antiguos cada", + "DeleteOldVisitorLogs": "Eliminar registros antiguos de visitantes", + "DeleteOldRawData": "Eliminar datos crudos antiguos regularmente", + "DeleteOldAggregatedReports": "Eliminar datos antiguos de informes agregados", + "DeleteLogDescription2": "Cuando habilitás la eliminación automática de registros, tenés que asegurarte que todos los informes diarios previos fueron procesados, de modo que ningún dato se pierde.", + "DeleteRawDataInfo": "Los datos en crudo contienen todos los detalles sobre cada visita individual y cada acción que tus visitantes tomaron. Cuando elimines los datos en crudo, la información eliminada no estará más disponible en el registro de visitas. Además, si luego querés crear un segmento, los informes segmentados no estarán disponibles para el marco de tiempo que fue eliminado, ya que todos los informes agregados son generados desde estos datos en crudo.", + "DeleteLogsConfirm": "Estás a punto de habilitar la eliminación de datos en crudo. Si se quitan los datos en crudo antiguos y los informes no fueron generados aún, no vas a poder ver los datos analíticos históricos. ¿Estás seguro que querés hacer esto?", "DeleteLogsOlderThan": "Eliminar registros más antiguos que", - "DeleteMaxRows": "Número máximo de filas que serán eliminadas por cada ejecución", + "DeleteMaxRows": "Número máximo de filas para eliminar en una ejecución:", "DeleteMaxRowsNoLimit": "sin límites", - "DeleteReportsConfirm": "Está disponiendo el borrado de datos de informes. Si un informe antiguo es borrado, tendrá que reprocesarlos nuevamente para visualizarlos. Está seguro que quiere hacer esto?", - "DeleteReportsOlderThan": "Borrar informes mayores que", - "DeleteDataSettings": "Borrar los registros y reportes de los visitantes antiguos", - "DoNotTrack_Description": "Do Not Track es una tecnología y una propuesta política que permite a los usuarios decidir no ser rastreados por sitios de internet que no visitan, tales como servicios analíticos, redes de publicidad y plataformas sociales.", - "DoNotTrack_Disable": "Deshabilitado, no da soporte al seguimiento", - "DoNotTrack_Enable": "Habilitar soporte a Do Not Track", - "DoNotTrack_Enabled": "Actualmente está respetando la privacidad de sus visitantes, ¡Bravo!", - "DoNotTrack_SupportDNTPreference": "Soporte a la preferencia Do Not Track", - "EstimatedDBSizeAfterPurge": "Tamaño aproximado de la base de datos después de la purga", - "EstimatedSpaceSaved": "Espacio estimado guardado", - "GeolocationAnonymizeIpNote": "Nota: Geolocalización tendrá aproximadamente los mismos resultados con 1 byte anónimo. Con 2 bytes o más, Geolocalización será imprecisa.", + "DeleteReportsConfirm": "Estás a punto de habilitar la eliminación de datos de informes. Si un informe antiguo es borrado, tendrás que reprocesarlos nuevamente para visualizarlos. ¿Estás seguro que querés seguir?", + "DeleteAggregateReportsDetailedInfo": "Cuando habilités esta configuración, todos los informes agregados serán eliminados. Los informes agregados son generados desde los datos en crudo y representan datos agregados de varias visitas individuales. Por ejemplo: el informe \"País\" enlista números agregados para saber cuántas visitas tuviste desde cada país.", + "KeepBasicMetricsReportsDetailedInfo": "Cuando habilités esta configuración, algunos indicadores numéricos clave no serán eliminados.", + "DeleteReportsInfo2": "Si eliminás informes viejos, podrían ser reprocesados de nuevo desde tus datos en crudo cuando los solicités.", + "DeleteReportsInfo3": "Si también habilitaste \"%s\", entonces los datos de informes que eliminés se perderán permanentemente.", + "DeleteReportsOlderThan": "Eliminar informes más antiguos que", + "DeleteSchedulingSettings": "Programar eliminación de datos viejos", + "DeleteDataSettings": "Eliminar los registros e informes de los visitantes antiguos", + "DoNotTrack_Description": "Do Not Track es una tecnología y una propuesta política que permite a los usuarios decidir no ser rastreados por sitios web que visitan, tales como servicios analíticos, redes de publicidad y plataformas sociales.", + "DoNotTrack_Disable": "Deshabilitar el soporte de Do Not Track", + "DoNotTrack_Disabled": "Matomo actualmente está rastreando todos los visitantes, incluso cuando especificaron \"No quiero que me persigan\" en sus navegadores web.", + "DoNotTrack_DisabledMoreInfo": "Recomendamos habilitar el soporte para \"DoNotTrack\", para así respetar la privacidad de tus visitantes", + "DoNotTrack_Enable": "Habilitar el soporte de Do Not Track", + "DoNotTrack_Enabled": "Actualmente estás respetando la privacidad de tus visitantes, ¡Bravo!", + "DoNotTrack_EnabledMoreInfo": "Cuando los usuarios establecen en sus navegadores web la opción \"No quiero que me persigan\" (esto es, cuando la función \"DoNotTrack\" está habilitada), Matomo no rastreará estas visitas.", + "DoNotTrack_SupportDNTPreference": "Soporte de Do Not Track", + "EstimatedDBSizeAfterPurge": "Tamaño estimado de la base de datos después de la purga", + "EstimatedSpaceSaved": "Espacio ahorrado estimado", + "GeolocationAnonymizeIpNote": "Nota: la geolocalización tendrá aproximadamente los mismos resultados con 1 byte anónimo. Con 2 bytes o más, la geolocalización será imprecisa.", + "GDPR": "GDPR (Reglamento General de Protección de Datos)", + "GdprManager": "Administrador GDPR", + "GdprOverview": "Vista general de GDPR", + "GdprTools": "Herramientas de GDPR", "GetPurgeEstimate": "Obtener estimación de purga", "KeepBasicMetrics": "Mantener métricas básicas (visitantes, vistas de páginas, tasa de rebote, conversiones de meta, conversiones de comercio electrónico, etc.)", - "KeepReportSegments": "Para mantener la información superior, también mantenga informes segmentados", + "KeepDataFor": "Conservar todos los datos de", + "KeepReportSegments": "Para mantener la información superior, también mantené informes segmentados", "LastDelete": "La última eliminación fue en", - "LeastDaysInput": "Por favor, especifique un número de días más grande que %s.", - "LeastMonthsInput": "Por favor, especifique un número de meses mayor que %s.", + "LeastDaysInput": "Por favor, especificá un número de días mayor que %s.", + "LeastMonthsInput": "Por favor, especificá un número de meses mayor que %s.", "MenuPrivacySettings": "Privacidad", "NextDelete": "La próxima eliminación programada es en", - "PurgeNow": "Purgar DB ahora", - "PurgeNowConfirm": "Está a punto de borrar información permanentemente de su base de datos. Está seguro que quiere continuar?", - "PurgingData": "Purgando información...", + "PluginDescription": "Aumentá la privacidad de todos tus usuarios y hacé que tu instancia de Matomo sea compatible con las normas de privacidad de tu legislación local.", + "PurgeNow": "Purgar base de datos ahora", + "PurgeNowConfirm": "Estás a punto de eliminar datos permanentemente de tu base de datos. ¿Estás seguro que querés continuar?", + "PurgingData": "Purgando datos…", + "RecommendedForPrivacy": "Recomendado para la privacidad", "ReportsDataSavedEstimate": "Tamaño de la base de datos", - "SaveSettingsBeforePurge": "Ha cambiado la configuración de borrado de información. Por favor, guárdelas antes de iniciar la purga.", - "SeeAlsoOurOfficialGuidePrivacy": "See also our official guide: %1$sWeb Analytics Privacy%2$s", - "TeaserHeadline": "Configuración de Privacidad", - "UseAnonymizedIpForVisitEnrichment": "Also use the Anonymized IP addresses when enriching visits.", - "UseAnonymizeIp": "Hacer anónimas las direcciones IP de los visitantes", - "UseDeleteReports": "Regularmente borre los reportes antiguos de la base de datos" + "SaveSettingsBeforePurge": "Cambiaste la configuración de eliminación de datos. Por favor, guardalas antes de iniciar la purga.", + "SeeAlsoOurOfficialGuidePrivacy": "También lee nuestra guía oficial: %1$sPrivacidad de los análisis web%2$s", + "TeaserHeader": "En esta página, podés personalizar Matomo para que sea compatible con las legislaciones existentes, al: %1$sanonimizar la dirección IP del visitante%2$s, %3$sautomáticamente eliminar registros antiguos de visitas de la base de datos%4$s y %5$sanonimizar los datos crudos de usuarios previamente rastreados%6$s.", + "TeaserHeadline": "Configuración de privacidad", + "UseAnonymizedIpForVisitEnrichment": "También usá las direcciones IP anonimizadas al enriquecer visitas.", + "UseAnonymizedIpForVisitEnrichmentNote": "Plugins como Geo Location vía dirección IP y proveedor mejoran los metadatos del visitante. Predeterminadamente, estos plugins usan las direcciones IP anonimizadas. Si seleccionás \"No\", entonces se usará la dirección IP completa, sin anonimizar, resultando en menos privacidad, pero con datos más precisos.", + "PseudonymizeUserIdNote": "Cuando habilités esta opción, la identificación del usuario será reemplazada por un pseudónimo para evitar almacenamiento directo y visualización personalmente identificable como una dirección de correo electrónico. En términos técnicos: dado tu identificación de usuario, Matomo procesará el pseudónimo de identificación de usuario usando una función hash salteada.", + "PseudonymizeUserIdNote2": "Nota: reemplazar con un pseudónimo no es lo mismo que la anonimización. En términos del Reglamento General de Protección de Datos: el pseudónimo de identificación de usuario todavía cuenta como datos personales. La identificación original de usuario todavía podría ser identificable si hay cierta información adicional disponible (la cual sólo Matomo y tu procesador de datos tienen acceso).", + "AnonymizeOrderIdNote": "Debido a que una identificación de orden puede ser consultado con otro sistema —normalmente, un local de comercio electrónico—, la identificación de orden puede contar como información personal bajo el GDPR. Cuando habilités esta opción, una identificación de orden se anonimizará automáticamente así no se rastrea ninguna información personal.", + "UseAnonymizeIp": "Anonimizar las direcciones IP de los visitantes", + "UseAnonymizeTrackingData": "Anonimizar datos de rastreo", + "UseAnonymizeUserId": "Anonimizar identificación del usuario", + "PseudonymizeUserId": "Reemplazar identificación del usuario con un pseudónimo", + "UseAnonymizeOrderId": "Anonimizar identificación de orden", + "UseDeleteLog": "Eliminar los datos crudos antiguos de la base de datos regularmente", + "UseDeleteReports": "Eliminar los informes antiguos de la base de datos regularmente", + "UsersOptOut": "Excepción de los usuarios", + "PrivacyPolicyUrl": "Dirección web de la política de privacidad", + "PrivacyPolicyUrlDescription": "Un enlace a tu página de política de privacidad.", + "TermsAndConditionUrl": "Dirección web de los términos y condiciones.", + "TermsAndConditionUrlDescription": "Un enlace a tu página de los términos y condiciones del servicio.", + "PrivacyPolicyUrlDescriptionSuffix": "Si establecés esto, se mostrará en la parte inferior de la página de inicio de sesión y en las páginas en las que el usuario \"%1$s\" pueda acceder.", + "ShowInEmbeddedWidgets": "Mostrar en widgets insertados", + "ShowInEmbeddedWidgetsDescription": "Si está seleccionado, se mostrará un enlace a tu política de privacidad y términos y condiciones en la parte inferior de los widgets insertados.", + "PrivacyPolicy": "Política de privacidad", + "TermsAndConditions": "Términos y condiciones" } } \ No newline at end of file diff --git a/app/plugins/PrivacyManager/lang/fr.json b/app/plugins/PrivacyManager/lang/fr.json index ecee270c2..fabaa59e3 100644 --- a/app/plugins/PrivacyManager/lang/fr.json +++ b/app/plugins/PrivacyManager/lang/fr.json @@ -70,7 +70,7 @@ "PseudonymizeUserIdNote2": "Note : remplacer par un pseudonyme n'est pas identique à une anonymisation des données. Dans les termes énoncés par RGPD : un pseudonyme identifiant l'utilisateur compte toujours comme une données personnelle. L'identifiant utilisateur originel pourrait toujours être identifié si certaines informations complémentaires étaient disponibles (auxquelles uniquement Matomo et votre traitement de données ont accès).", "AnonymizeOrderIdNote": "Parce qu'un identifiant de commande peut être référencé au travers de plusieurs systèmes, typiquement un magasin de commerce électronique, l'identifiant de commande pourrait compter comme donnée personnelle pour RGPD. En sélectionnant cette option, un identifiant de commande va être automatiquement anonymisé de sorte qu'aucune information personnelle ne sera enregistrée.", "UseAnonymizeIp": "Rendre anonymes les adresses IP des visiteurs", - "UseAnonymizeTrackingData": "Anonymiser les données de suivit", + "UseAnonymizeTrackingData": "Anonymiser les données de suivi", "UseAnonymizeUserId": "Anonymiser l'identifiant utilisateur", "PseudonymizeUserId": "Remplacer l'identifiant utilisateur par un pseudonyme", "UseAnonymizeOrderId": "Anonymiser l'identifiant de la commande", diff --git a/app/plugins/PrivacyManager/lang/pt-br.json b/app/plugins/PrivacyManager/lang/pt-br.json index 418458b49..1b96386ad 100644 --- a/app/plugins/PrivacyManager/lang/pt-br.json +++ b/app/plugins/PrivacyManager/lang/pt-br.json @@ -1,51 +1,90 @@ { "PrivacyManager": { - "AnonymizeIpDescription": "Selecione \"Sim\" se você quer que o Matomo não rastreie os endereços de IP totalmente qualificados.", - "AnonymizeIpInlineHelp": "Anonimize o último byte do endereço IP dos visitantes para obedecer suas leis\/guias locais de privacidade.", - "AnonymizeIpMaskLengtDescription": "Selecione quantos bytes de IPs dos visitantes deve ser mascarado.", + "AnonymizeData": "Anonimizar dados", + "AnonymizeIpDescription": "Selecione \"Sim\" se você quer que o Matomo não rastreie endereços IP totalmente qualificados.", + "AnonymizeIpInlineHelp": "Anonimizar o(s) último(s) byte(s) do endereço IP dos visitantes para obedecer suas leis\/guias locais de privacidade.", + "AnonymizeIpExtendedHelp": "Quando usuários visitam o seu site, o Matomo não usará o endereço IP completo (como %1$s) e no lugar o Matomo irá primeiro anonimizá-lo (para %2$s). A anonimização de endereço IP é um dos requisitos definidos pelas leis de privacidade em alguns países como Alemanha.", + "AnonymizeIpMaskLengtDescription": "Selecione quantos bytes dos IPs dos visitantes devem ser mascarados.", "AnonymizeIpMaskLength": "%1$s byte(s) - e.g. %2$s", + "AskingForConsent": "Pedir consentimento", "ClickHereSettings": "Clique aqui para acessar as configurações do %s.", - "CurrentDBSize": "Tamanho atual do banco de dados", - "DBPurged": "DB purgado.", - "DeleteDataInterval": "Excluir dados antigoa a cada", - "DeleteOldVisitorLogs": "Apagar logs de antigos visitantes", - "DeleteLogDescription2": "Quando você habilitar a exclusão automática de log, você deve garantir que todos os relatórios anteriores diários tenham sido processados​​, de modo que nenhum dado seja perdido.", - "DeleteLogsOlderThan": "Excluir logs mais velhos que", - "DeleteMaxRows": "O número máximo de linhas a serem excluídos em uma execução:", + "CurrentDBSize": "Tamanho atual da base de dados", + "DBPurged": "BD purgada.", + "DeleteBothConfirm": "Você está prestes a habilitar tanto a exclusão de dados brutos como a exclusão de dados de relatórios. Isto irá remover permanentemente sua capacidade de visualizar dados antigos de análises. Você tem certeza de que deseja fazer isto?", + "DeleteDataDescription": "Você pode configurar o Matomo para excluir regularmente dados brutos antigos e\/ou relatórios agregados para manter sua base de dados pequena ou para atender regulamentações de privacidade como a GDPR.", + "DeleteDataInterval": "Excluir dados antigos a cada", + "DeleteOldVisitorLogs": "Excluir logs antigos de visitantes", + "DeleteOldRawData": "Excluir regularmente dados brutos antigos", + "DeleteOldAggregatedReports": "Excluir dados de relatórios agregados antigos", + "DeleteLogDescription2": "Quando você habilita a exclusão automática de logs, você deve garantir que todos os relatórios diários anteriores tenham sido processados​​, para que nenhum dado seja perdido.", + "DeleteRawDataInfo": "Os dados brutos contêm todos os detalhes sobre cada visita individual e cada ação tomada por seus visitantes. Quando você exclui dados brutos, as informações excluídas não estarão mais disponíveis no log de visitantes. Além disso, se depois você decidir criar um segmento, os relatórios segmentados não estarão disponíveis para o período que foi excluído já que todos os relatórios agregados são gerados a partir de dados brutos.", + "DeleteLogsConfirm": "Você está prestes a habilitar a exclusão de dados brutos. Se dados brutos antigos forem excluídos e relatórios ainda não tiverem sido criados, você não conseguirá ver dados de análises históricas passadas. Você tem certeza que deseja fazer isto?", + "DeleteLogsOlderThan": "Excluir logs mais antigos que", + "DeleteMaxRows": "Número máximo de linhas a serem excluídas em uma execução:", "DeleteMaxRowsNoLimit": "nenhum limite", "DeleteReportsConfirm": "Você está prestes a permitir a exclusão dos dados do relatório. Se os relatórios antigos são removidos, você vai ter que re-processar-los em ordem de visualização. Tem certeza de que quer fazer isto?", - "DeleteReportsOlderThan": "Apagar relatórios com mais de", - "DeleteSchedulingSettings": "Agendar eliminação de dados antigos", + "DeleteAggregateReportsDetailedInfo": "Quando você habilitar esta configuração, todos os relatórios agregados serão excluídos. Relatórios agregados são gerados a partir dos dados brutos e representam dados agregados de diversas visitas individuais. Por exemplo, o relatório \"País\" lista números agregados para ver quantas visitas você teve de cada país.", + "KeepBasicMetricsReportsDetailedInfo": "Quando você habilitar esta configuração, alguns indicadores-chave de desempenho numéricos não serão excluídos.", + "DeleteReportsInfo2": "Se você excluir relatórios antigos, eles podem ser reprocessados novamente a partir dos dados brutos quando você solicitá-los.", + "DeleteReportsInfo3": "Se você também tiver habilitado \"%s\", então os dados de relatórios que você excluir serão permanentemente perdidos.", + "DeleteReportsOlderThan": "Excluir relatórios mais antigos que", + "DeleteSchedulingSettings": "Agendar exclusão de dados antigos", "DeleteDataSettings": "Excluir logs de visitantes e relatórios antigos", - "DoNotTrack_Description": "Do Not Track é uma tecnologia e proposta de política que permite aos usuários optar por não ser rastreado por sites que não visitam, incluindo serviços de análise, redes de publicidade e plataformas sociais.", - "DoNotTrack_Disable": "Desabilitar o suporte Do Not Track", - "DoNotTrack_Enable": "Ativar suporte para 'Não rastrear'", - "DoNotTrack_Enabled": "Você está respeitando a sua privacidade usuários, Bravo!", - "DoNotTrack_SupportDNTPreference": "Preferencias do suporte Do Not Track", - "EstimatedDBSizeAfterPurge": "Tamanho do banco estimado após purge", + "DoNotTrack_Description": "Não Rastrear é uma tecnologia e proposta de política que permite aos usuários optarem por não serem rastreados por sites que visitam, incluindo serviços de análise, redes de publicidade e plataformas sociais.", + "DoNotTrack_Disable": "Desabilitar o suporte a Não Rastrear", + "DoNotTrack_Disabled": "O Matomo atualmente está rastreando todos os visitantes, mesmo quando eles especificam \"Eu não quero ser rastreado\" em seus navegadores web.", + "DoNotTrack_DisabledMoreInfo": "Nós recomendamos habilitar o suporte a Não Rastrear para respeitar a privacidade dos seus visitantes.", + "DoNotTrack_Enable": "Habilitar suporte a Não Rastrear", + "DoNotTrack_Enabled": "Você atualmente está respeitando a privacidade dos seus usuários, parabéns!", + "DoNotTrack_EnabledMoreInfo": "Quando os usuários definirem \"Eu não quero ser rastreado\" em seus navegadores web (Não Rastrear está habilitado) então o Matomo não irá rastrear estas visitas.", + "DoNotTrack_SupportDNTPreference": "Preferências do suporte a Não Rastrear", + "EstimatedDBSizeAfterPurge": "Tamanho estimado da base de dados após expurgação", "EstimatedSpaceSaved": "Espaço estimado salvo", - "GeolocationAnonymizeIpNote": "Nota: Geolocation terá aproximadamente os mesmos resultados com um byte anónimos. Com dois bytes ou mais, Geolocation será impreciso.", + "GeolocationAnonymizeIpNote": "Obs: A geolocalização terá aproximadamente os mesmos resultados com 1 byte anonimizado. Com 2 bytes ou mais, a geolocalização será imprecisa.", "GDPR": "GDPR", - "GdprManager": "Gerenciar GDPR", - "GetPurgeEstimate": "Obter estimativa de purge", - "KeepBasicMetrics": "Mantenha métricas básicas (visitas, exibições de página, taxa de rejeição, conversões de meta, conversões de comércio eletrônico, etc)", - "KeepDataFor": "Manter todos os dados para", - "KeepReportSegments": "Para dados mantidos acima, também manter relatórios segmentados", + "GdprManager": "Gerenciador GDPR", + "GdprOverview": "Visão geral da GDPR", + "GdprTools": "Ferramentas GDPR", + "GetPurgeEstimate": "Obter estimativa de expurgação", + "KeepBasicMetrics": "Manter métricas básicas (visitas, exibições de página, taxa de rejeição, conversões de meta, conversões de comércio eletrônico, etc.)", + "KeepDataFor": "Manter todos os dados por", + "KeepReportSegments": "Para os dados mantidos acima, também manter relatórios segmentados", "LastDelete": "Última deleção foi em", "LeastDaysInput": "Por favor, especifique um número de dias maior do que %s.", "LeastMonthsInput": "Por favor, especifique um número de meses maior que %s.", "MenuPrivacySettings": "Privacidade", "NextDelete": "Próxima exclusão agendada em", + "PluginDescription": "Aumente a privacidade para seus usuários e deixe a sua instância Matomo em conformidade de privacidade com a sua legislação local.", "PurgeNow": "Purge DB Agora", "PurgeNowConfirm": "Você está prestes a excluir permanentemente os dados de seu banco de dados. Tem certeza de que quer continuar?", "PurgingData": "Purgando dados...", "RecommendedForPrivacy": "Recomendado para privacidade", - "ReportsDataSavedEstimate": "Tamanho do banco de dados", + "ReportsDataSavedEstimate": "Tamanho da base de dados", "SaveSettingsBeforePurge": "Você mudou as configurações de exclusão de dados. Guarde-os antes de iniciar um expurgo.", - "SeeAlsoOurOfficialGuidePrivacy": "Veja também nosso guia oficial: %1$sWeb Analytics Privacy%2$s", + "SeeAlsoOurOfficialGuidePrivacy": "Veja também nosso guia oficial: %1$sPrivacidade de Web Analytics%2$s", + "TeaserHeader": "Nesta página você pode personalizar o Matomo para deixá-lo em conformidade com legislações existentes: %1$s anonimizando o IP do visitante%2$s,%3$s removendo automaticamente da base de dados logs antigos de visitantes%4$s, e %5$s anonimizando dados brutos de usuários anteriormente rastreados%6$s.", "TeaserHeadline": "Configurações de privacidade", "UseAnonymizedIpForVisitEnrichment": "Utilize também o endereço de IP Anonimizado ao enriquecer visitas.", + "UseAnonymizedIpForVisitEnrichmentNote": "Plugins como Geolocalização via IP e Provedor melhoram os metadados de visitantes. Por padrão, estes plugins usam os endereços IP anonimizados. Se você selecionar 'Não', então o endereço IP completo não-anonimizado será usado nesse caso, resultando em menos privacidade mas melhor precisão dos dados.", + "PseudonymizeUserIdNote": "Quando você habilita esta opção, o ID de usuário será substituído por um pseudônimo para evitar armazenar e exibir diretamente informações de identificação pessoal como um endereço de e-mail. Em termos técnicos: dado o seu ID de usuário, o Matomo irá processar o pseudônimo do ID de usuário usando uma função de hash com sal.", + "PseudonymizeUserIdNote2": "Obs: substituir por um pseudônimo não é o mesmo que anonimização. Em termos da GDPR: o pseudônimo do ID de usuário ainda conta como dado pessoal. O ID de usuário original ainda pode ser identificado se certas informações adicionais estiverem disponíveis (às quais apenas o Matomo e o seu processador de dados têm acesso).", + "AnonymizeOrderIdNote": "Como um ID de pedido pode ser cruzado com outro sistema, tipicamente uma loja de comércio eletrônico, o ID de pedido pode contar como informação pessoal sob a GDPR. Quando você habilita esta opção, um ID de pedido será automaticamente anonimizado para que nenhuma informação pessoal seja rastreada.", "UseAnonymizeIp": "Anonimizar endereços IP dos visitantes", - "UseDeleteReports": "Exclua regularmente relatórios antigos do banco de dados" + "UseAnonymizeTrackingData": "Anonimizar dados de rastreamento", + "UseAnonymizeUserId": "Anonimizar ID de usuário", + "PseudonymizeUserId": "Substituir ID de usuário por um pseudônimo", + "UseAnonymizeOrderId": "Anonimizar ID do pedido", + "UseDeleteLog": "Excluir regularmente dados brutos antigos da base de dados", + "UseDeleteReports": "Excluir regularmente relatórios antigos da base de dados", + "UsersOptOut": "Usuários não participarem", + "PrivacyPolicyUrl": "URL da Política de Privacidade", + "PrivacyPolicyUrlDescription": "Um link para a sua página de Política de Privacidade.", + "TermsAndConditionUrl": "URL dos Termos & Condições", + "TermsAndConditionUrlDescription": "Um link para a sua página de Termos & Condições.", + "PrivacyPolicyUrlDescriptionSuffix": "Se você definir isto, será exibido no rodapé da página de login e nas páginas que o usuário '%1$s' pode acessar.", + "ShowInEmbeddedWidgets": "Mostrar em widgets embutidos", + "ShowInEmbeddedWidgetsDescription": "Se marcado, um link para sua Política de Privacidade e para seus Termos & Condições serão exibidos no rodapé dos widgets embutidos.", + "PrivacyPolicy": "Política de Privacidade", + "TermsAndConditions": "Termos & Condições" } } \ No newline at end of file diff --git a/app/plugins/PrivacyManager/lang/pt.json b/app/plugins/PrivacyManager/lang/pt.json index f84be32c8..e26ac8a77 100644 --- a/app/plugins/PrivacyManager/lang/pt.json +++ b/app/plugins/PrivacyManager/lang/pt.json @@ -3,6 +3,7 @@ "AnonymizeData": "Anonimizar dados", "AnonymizeIpDescription": "Selecione \"Sim\" se pretende que o Matomo não acompanhe os endereços de IP totalmente qualificados.", "AnonymizeIpInlineHelp": "Torna o(s) último(s) byte(s) dos endereços IP dos visitantes anónimo(s) para obedecer às suas leis\/regulamentos de privacidade locais.", + "AnonymizeIpExtendedHelp": "Quando os utilizadores visitarem o seu site, ao invés de utilizar o endereço de IP completo (por exemplo %1$s), o Matomo irá primeiro anonimizar o endereço de IP (para %2$s). A anonimização de IP é um dos requisitos definidos nas leis de privacidade em determinados países, como a Alemanha.", "AnonymizeIpMaskLengtDescription": "Selecione quantos bytes dos IPs dos visitantes devem ser mascarados.", "AnonymizeIpMaskLength": "%1$s byte(s) - por exemplo %2$s", "AskingForConsent": "Solicitando consentimento", @@ -32,6 +33,7 @@ "DoNotTrack_Description": "Do Not Track é uma proposta de tecnologia e de política que permite que os utilizadores optem por não serem acompanhados pelos sites que visitam, incluindo serviços de analytics, redes de publicidade e plataformas sociais.", "DoNotTrack_Disable": "Desativar o suporte ao Do Not Track", "DoNotTrack_Disabled": "O Matomo está atualmente a acompanhar todos os visitantes, mesmo quando estes especificaram \"Eu não pretendo ser acompanhado\" nos seus navegadores.", + "DoNotTrack_DisabledMoreInfo": "Recomendamos que ative o suporte ao DoNotTrack de modo a respeitar a privacidade dos seus visitantes", "DoNotTrack_Enable": "Ativar o suporte ao Do Not Track", "DoNotTrack_Enabled": "Atualmente está a respeitar a privacidade dos seus utilizadores. Excelente!", "DoNotTrack_EnabledMoreInfo": "Quando os utilizadores têm o seu navegador configurado para \"Eu não quero ser acompanhado\" (o DoNotTrack está ativo), então o Matomo não irá acompanhar estas visitas.", @@ -60,10 +62,13 @@ "ReportsDataSavedEstimate": "Tamanho da base de dados", "SaveSettingsBeforePurge": "Alterou as definições de eliminação de dados. Por favor, guarde estas definições antes de iniciar uma limpeza.", "SeeAlsoOurOfficialGuidePrivacy": "Consulte também o nosso guia oficial: %1$sPrivacidade em Web Analytics%2$s", + "TeaserHeader": "Nesta página, pode configurar o Matomo de forma a que este esteja em conformidade com a legislação vigente, através: %1$sda anonimização do IP do visitante %2$s, %3$sremoção automática de registos antigos dos visitantes da base de dados %4$s, e %5$s da anonimização dos dados em bruto previamente acompanhados dos utilizadores %6$s.", "TeaserHeadline": "Definições de privacidade", "UseAnonymizedIpForVisitEnrichment": "Também utilizamos os endereços de IP anonimizados ao enriquecer as visitas.", + "UseAnonymizedIpForVisitEnrichmentNote": "As extensões como a Geolocalização via IP e Fornecedor enriquecem os meta-dados dos visitantes. Por defeito, estas extensões utilizam os endereços de IP anonimizados. Ao selecionar \"Não\", o endereço de IP completo e não-anonimizado passará a ser utilizado, resultando assim numa menor privacidade mas numa melhor fiabilidade dos dados.", "PseudonymizeUserIdNote": "Quando ativa esta opção, o ID de utilizador será substituído por um pseudónimo para evitar o armazenamento e a apresentação direta de informação pessoal identificável, tal como o endereço de e-mail. Em termos técnicos: com base no seu ID de utilizador, o Matomo irá processar o pseudónimo do ID de utilizador utilizando uma função hash com salt.", "PseudonymizeUserIdNote2": "Nota: a substituição com um pseudónimo não é o mesmo que anonimização. Nos termos do RGPD: o pseudónimo do ID de utilizador ainda é considerado dado pessoal. O ID de utilizador ainda pode ser identificado se determinadas informações adicionais estiverem disponíveis (a que apenas o Matomo e o seu processador de dados têm acesso).", + "AnonymizeOrderIdNote": "Dado que um ID de um pedido pode ser cruzado com outro sistema, habitualmente uma loja de comércio eletrónico, o ID de um pedido pode ser considerado como informação pessoal segundo o RGPD. Ao ativar esta opção, o ID de um pedido será automaticamente anonimizado, impedindo assim o registo de qualquer informação pessoal.", "UseAnonymizeIp": "Anonimizar os endereços IP dos visitantes", "UseAnonymizeTrackingData": "Anonimizar as informações de acompanhamento", "UseAnonymizeUserId": "Anonimizar ID de utilizador", diff --git a/app/plugins/PrivacyManager/lang/ru.json b/app/plugins/PrivacyManager/lang/ru.json index 321c7893f..4e85d1e6d 100644 --- a/app/plugins/PrivacyManager/lang/ru.json +++ b/app/plugins/PrivacyManager/lang/ru.json @@ -1,6 +1,7 @@ { "PrivacyManager": { "AnonymizeData": "Анонимизировать данные", + "AnonymizeIpDescription": "Выберите «Да», если вы хотите, чтобы Matomo не отслеживал полностью определенные IP-адреса.", "AnonymizeIpInlineHelp": "Скрыть последний байт IP-адресов ваших посетителей согласно вашим принципам конфиденциальности или законодательству.", "AnonymizeIpMaskLengtDescription": "Выберите, как много байтов IP-адреса посетителей должно быть скрыто.", "AnonymizeIpMaskLength": "%1$s байт(ов), например, %2$s", @@ -8,6 +9,9 @@ "CurrentDBSize": "Текущий размер базы данных", "DBPurged": "База данных очищена.", "DeleteDataInterval": "Удалять старые данные каждые", + "DeleteOldVisitorLogs": "Удалить старые журналы посетителей", + "DeleteOldRawData": "Регулярно удаляйте старые необработанные данные", + "DeleteOldAggregatedReports": "Удалить старые агрегированные данные отчета", "DeleteLogDescription2": "Если вы используете автоматическое удаление логов, убедитесь, что все предыдущие отчеты за день были обработаны, чтобы не потерять ни каких данных.", "DeleteLogsOlderThan": "Удалить логи, старше чем", "DeleteMaxRows": "Максимальное число строк для удаления за один раз:", @@ -23,6 +27,7 @@ "EstimatedDBSizeAfterPurge": "Ожидаемый размер базы данных после чистки", "EstimatedSpaceSaved": "Ожидаемый сохраненный размер space saved", "GeolocationAnonymizeIpNote": "Подсказка: Геолоакция будет иметь практически те же результаты с 1 скрытым байтом IP адреса. Если скрыто два или более байтов, геолокация будет определять местонахождене пользователя неточно.", + "GDPR": "GDPR", "GetPurgeEstimate": "Оценить очистку базы по времени", "KeepBasicMetrics": "Сохранить основные показатели (посещения, просмотры страниц, процент отскоков, конверсию целей, конверсию эл. заказов и др.)", "KeepDataFor": "Сохранить данные для", @@ -44,6 +49,9 @@ "UseAnonymizeTrackingData": "Анонимизировать данные отслеживания", "PseudonymizeUserId": "Заменить ID пользователя всевдонимом", "UseAnonymizeOrderId": "Анонимизировать ID заказа", - "UseDeleteReports": "Всегда удалять старые отчеты из баз данных" + "UseDeleteReports": "Всегда удалять старые отчеты из баз данных", + "ShowInEmbeddedWidgets": "Показать во встроенных виджетах", + "PrivacyPolicy": "Политика конфиденциальности", + "TermsAndConditions": "Условия использования" } } \ No newline at end of file diff --git a/app/plugins/ProfessionalServices/config/config.php b/app/plugins/ProfessionalServices/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/ProfessionalServices/config/config.php +++ b/app/plugins/ProfessionalServices/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/ProfessionalServices/config/tracker.php b/app/plugins/ProfessionalServices/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/ProfessionalServices/config/tracker.php +++ b/app/plugins/ProfessionalServices/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Provider/config/config.php b/app/plugins/Provider/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/Provider/config/config.php +++ b/app/plugins/Provider/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Provider/config/tracker.php b/app/plugins/Provider/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Provider/config/tracker.php +++ b/app/plugins/Provider/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Provider/lang/es-ar.json b/app/plugins/Provider/lang/es-ar.json index 05caa3f39..99b3c2dcf 100644 --- a/app/plugins/Provider/lang/es-ar.json +++ b/app/plugins/Provider/lang/es-ar.json @@ -2,6 +2,7 @@ "Provider": { "ColumnProvider": "Proveedor", "PluginDescription": "Informa el proveedor de Internet de los visitantes.", + "ProviderReportDocumentation": "Este informe muestra qué proveedores de Internet (ISPs) usan tus visitantes para acceder al sitio web. Podés hacer clic en el nombre de un proveedor para más detalles. %s Si Matomo no puede determinar el proveedor de un visitante, se mostrará la IP.", "WidgetProviders": "Proveedores", "ProviderReportFooter": "Un proveedor desconocido significa que la dirección IP no pudo ser resuelta." } diff --git a/app/plugins/Provider/lang/zh-cn.json b/app/plugins/Provider/lang/zh-cn.json index 87f207c3e..864aca4ab 100644 --- a/app/plugins/Provider/lang/zh-cn.json +++ b/app/plugins/Provider/lang/zh-cn.json @@ -1,7 +1,9 @@ { "Provider": { "ColumnProvider": "网络服务商", + "PluginDescription": "报告访问者的Internet服务提供商。", "ProviderReportDocumentation": "本报表显示访客的网络服务商。点击服务商名字查看详细资料。%s 如果 Matomo 无法判断访客的网络服务商,就列出IP地址。", - "WidgetProviders": "网络服务商" + "WidgetProviders": "网络服务商", + "ProviderReportFooter": "未知的提供程序意味着无法查找IP地址。" } } \ No newline at end of file diff --git a/app/plugins/Proxy/config/config.php b/app/plugins/Proxy/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/Proxy/config/config.php +++ b/app/plugins/Proxy/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Proxy/config/tracker.php b/app/plugins/Proxy/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Proxy/config/tracker.php +++ b/app/plugins/Proxy/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Referrers/API.php b/app/plugins/Referrers/API.php index dadd58945..dd5d223c8 100644 --- a/app/plugins/Referrers/API.php +++ b/app/plugins/Referrers/API.php @@ -14,9 +14,13 @@ use Piwik\Archive; use Piwik\Common; use Piwik\DataTable; +use Piwik\DataTable\Filter\ColumnCallbackAddColumnPercentage; use Piwik\Date; +use Piwik\Metrics; use Piwik\Piwik; +use Piwik\Plugin\ReportsProvider; use Piwik\Plugins\Referrers\DataTable\Filter\GroupDifferentSocialWritings; +use Piwik\Plugins\Referrers\Reports\Get; use Piwik\Site; /** @@ -30,6 +34,50 @@ */ class API extends \Piwik\Plugin\API { + public function get($idSite, $period, $date, $segment = false, $columns = false) + { + Piwik::checkUserHasViewAccess($idSite); + + $dataTableReferrersType = $this->getReferrerType($idSite, $period, $date, $segment); + $dataTable = $this->createReferrerTypeTable($dataTableReferrersType); + + $archive = Archive::build($idSite, $period, $date, $segment); + + $numericArchives = $archive->getDataTableFromNumeric([ + Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME, + Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME, + Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME, + Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME, + Archiver::METRIC_DISTINCT_URLS_RECORD_NAME, + Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME, + ]); + $this->mergeNumericArchives($dataTable, $numericArchives); + + $totalVisits = array_sum($dataTableReferrersType->getColumn(Metrics::INDEX_NB_VISITS)); + + $percentColumns = [ + 'Referrers_visitorsFromDirectEntry', + 'Referrers_visitorsFromSearchEngines', + 'Referrers_visitorsFromCampaigns', + 'Referrers_visitorsFromSocialNetworks', + 'Referrers_visitorsFromWebsites', + ]; + foreach ($percentColumns as $column) { + $dataTable->filter(ColumnCallbackAddColumnPercentage::class, [ + $column . '_percent', + $column, + $totalVisits + ]); + } + + if (!empty($requestedColumns)) { + $requestedColumns = Piwik::getArrayFromApiParameter($columns); + $dataTable->filter(DataTable\Filter\ColumnDelete::class, [[], $requestedColumns]); + } + + return $dataTable; + } + /** * @param string $name * @param int $idSite @@ -352,7 +400,9 @@ public function getUrlsFromWebsiteId($idSite, $period, $date, $idSubtable, $segm Piwik::checkUserHasViewAccess($idSite); $dataTable = $this->getDataTable(Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); $dataTable->filter('Piwik\Plugins\Referrers\DataTable\Filter\UrlsFromWebsiteId'); - $dataTable->filter('AddSegmentByLabel', array('referrerUrl')); + $dataTable->filter('MetadataCallbackAddMetadata', array('url', 'segment', function($url) { + return 'referrerUrl==' . urlencode($url); + })); return $dataTable; } @@ -669,4 +719,72 @@ private function buildExpandedTableForFlattenGetSocials($idSite, $period, $date, $urlsTable = null; } + private function createReferrerTypeTable(DataTable\DataTableInterface $table) + { + if ($table instanceof DataTable) { + $nameToColumnId = array( + 'Referrers_visitorsFromSearchEngines' => Common::REFERRER_TYPE_SEARCH_ENGINE, + 'Referrers_visitorsFromSocialNetworks' => Common::REFERRER_TYPE_SOCIAL_NETWORK, + 'Referrers_visitorsFromDirectEntry' => Common::REFERRER_TYPE_DIRECT_ENTRY, + 'Referrers_visitorsFromWebsites' => Common::REFERRER_TYPE_WEBSITE, + 'Referrers_visitorsFromCampaigns' => Common::REFERRER_TYPE_CAMPAIGN, + ); + + $newRow = array(); + foreach ($nameToColumnId as $nameVar => $columnId) { + $value = 0; + $row = $table->getRowFromLabel($columnId); + if ($row !== false) { + $value = $row->getColumn(Metrics::INDEX_NB_VISITS); + } + $newRow[$nameVar] = $value; + } + + $result = new DataTable\Simple(); + $result->addRowFromSimpleArray($newRow); + return $result; + } else if ($table instanceof DataTable\Map) { + $result = new DataTable\Map(); + $result->setKeyName($table->getKeyName()); + foreach ($table->getDataTables() as $label => $childTable) { + $referrerTypeTable = $this->createReferrerTypeTable($childTable); + $result->addTable($referrerTypeTable, $label); + } + } else { + throw new \Exception("Unexpected DataTable type: " . get_class($table)); // sanity check + } + return $result; + } + + private function mergeNumericArchives(DataTable\DataTableInterface $table, DataTable\DataTableInterface $numericArchives = null) + { + if ($table instanceof DataTable) { + /** @var DataTable $numericArchives */ + if (empty($numericArchives)) { + return; + } + + $table->setAllTableMetadata($numericArchives->getAllTableMetadata()); + + if ($table->getRows() == 0) { + $table->addRow(new DataTable\Row()); + } + + if ($numericArchives->getRowsCount() == 0) { + return; + } + + $row = $table->getFirstRow(); + foreach ($numericArchives->getFirstRow() as $name => $value) { + $row->setColumn($name, $value); + } + } else if ($table instanceof DataTable\Map) { + foreach ($table->getDataTables() as $label => $childTable) { + $numericArchiveChildTable = $numericArchives->getTable($label); + $this->mergeNumericArchives($childTable, $numericArchiveChildTable); + } + } else { + throw new \Exception("Unexpected DataTable type: " . get_class($table)); // sanity check + } + } } diff --git a/app/plugins/Referrers/Controller.php b/app/plugins/Referrers/Controller.php index bc2d25fb7..66184b881 100644 --- a/app/plugins/Referrers/Controller.php +++ b/app/plugins/Referrers/Controller.php @@ -12,6 +12,7 @@ use Piwik\Common; use Piwik\DataTable\Filter\CalculateEvolutionFilter; use Piwik\DataTable\Map; +use Piwik\FrontController; use Piwik\Metrics; use Piwik\NumberFormatter; use Piwik\Period\Range; @@ -41,213 +42,12 @@ public function __construct(Translator $translator) public function getSparklines() { - $metrics = $this->getReferrersVisitorsByType(); - $distinctMetrics = $this->getDistinctReferrersMetrics(); + $_GET['forceView'] = '1'; + $_GET['viewDataTable'] = Sparklines::ID; - $numberFormatter = NumberFormatter::getInstance(); - - $totalVisits = array_sum($metrics); - foreach ($metrics as $name => $value) { - - // calculate percent of total, if there were any visits - if ($value != 0 && $totalVisits != 0) { - $percentName = $name . 'Percent'; - $metrics[$percentName] = round(($value / $totalVisits) * 100, 0); - } - } - - // calculate evolution for visit metrics & distinct metrics - list($lastPeriodDate, $ignore) = Range::getLastDate(); - if ($lastPeriodDate !== false) { - $date = Common::getRequestVar('date'); - $period = Common::getRequestVar('period'); - - $prettyDate = self::getPrettyDate($date, $period); - $prettyLastPeriodDate = self::getPrettyDate($lastPeriodDate, $period); - - // visit metrics - $previousValues = $this->getReferrersVisitorsByType($lastPeriodDate); - $metrics = $this->addEvolutionPropertiesToView($prettyDate, $metrics, $prettyLastPeriodDate, $previousValues); - - // distinct metrics - $previousValues = $this->getDistinctReferrersMetrics($lastPeriodDate); - $distinctMetrics = $this->addEvolutionPropertiesToView($prettyDate, $distinctMetrics, $prettyLastPeriodDate, $previousValues); - } - - /** @var Sparklines $view */ - $view = ViewDataTable\Factory::build(Sparklines::ID, $api = '', $controller = '', $force = true, $loadUserParams = false); - - // DIRECT ENTRY - $metrics['visitorsFromDirectEntry'] = $numberFormatter->formatNumber($metrics['visitorsFromDirectEntry']); - $values = array($metrics['visitorsFromDirectEntry']); - $descriptions = array(Piwik::translate('Referrers_TypeDirectEntries')); - - if (!empty($metrics['visitorsFromDirectEntryPercent'])) { - $metrics['visitorsFromDirectEntryPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromDirectEntryPercent'], $precision = 1); - $values[] = $metrics['visitorsFromDirectEntryPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - - $directEntryParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_DIRECT_ENTRY); - - $view->config->addSparkline($directEntryParams, $values, $descriptions, @$metrics['visitorsFromDirectEntryEvolution']); - - - // WEBSITES - $metrics['visitorsFromWebsites'] = $numberFormatter->formatNumber($metrics['visitorsFromWebsites']); - $values = array($metrics['visitorsFromWebsites']); - $descriptions = array(Piwik::translate('Referrers_TypeWebsites')); - - if (!empty($metrics['visitorsFromWebsitesPercent'])) { - $metrics['visitorsFromWebsitesPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromWebsitesPercent'], $precision = 1); - $values[] = $metrics['visitorsFromWebsitesPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - - $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_WEBSITE); - - $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromWebsitesEvolution']); - - - // SEARCH ENGINES - $metrics['visitorsFromSearchEngines'] = $numberFormatter->formatNumber($metrics['visitorsFromSearchEngines']); - $values = array($metrics['visitorsFromSearchEngines']); - $descriptions = array(Piwik::translate('Referrers_TypeSearchEngines')); - - if (!empty($metrics['visitorsFromSearchEnginesPercent'])) { - $metrics['visitorsFromSearchEnginesPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromSearchEnginesPercent'], $precision = 1); - $values[] = $metrics['visitorsFromSearchEnginesPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_SEARCH_ENGINE); - - $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromSearchEnginesEvolution']); - - // SOCIAL NETWORKS - $metrics['visitorsFromSocialNetworks'] = $numberFormatter->formatNumber($metrics['visitorsFromSocialNetworks']); - $values = array($metrics['visitorsFromSocialNetworks']); - $descriptions = array(Piwik::translate('Referrers_TypeSocialNetworks')); - - if (!empty($metrics['visitorsFromSocialNetworksPercent'])) { - $metrics['visitorsFromSocialNetworksPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromSocialNetworksPercent'], $precision = 1); - $values[] = $metrics['visitorsFromSocialNetworksPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - $socialNetworkParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_SOCIAL_NETWORK); - - $view->config->addSparkline($socialNetworkParams, $values, $descriptions, @$metrics['visitorsFromSocialNetworksEvolution']); - - - // CAMPAIGNS - $metrics['visitorsFromCampaigns'] = $numberFormatter->formatNumber($metrics['visitorsFromCampaigns']); - $values = array($metrics['visitorsFromCampaigns']); - $descriptions = array(Piwik::translate('Referrers_TypeCampaigns')); - - if (!empty($metrics['visitorsFromCampaignsPercent'])) { - $metrics['visitorsFromCampaignsPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromCampaignsPercent'], $precision = 1); - $values[] = $metrics['visitorsFromCampaignsPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - - $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_CAMPAIGN); - - $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromCampaignsEvolution']); - - - // DISTINCT SEARCH ENGINES - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctSearchEnginesGraph'); - $value = $distinctMetrics['numberDistinctSearchEngines']; - $value = $numberFormatter->formatNumber($value); - $description = Piwik::translate('Referrers_DistinctSearchEngines'); - - $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctSearchEnginesEvolution']); - - - // DISTINCT SOCIAL NETWORKS - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctSocialNetworksGraph'); - $value = $distinctMetrics['numberDistinctSocialNetworks']; - $value = $numberFormatter->formatNumber($value); - $description = Piwik::translate('Referrers_DistinctSocialNetworks'); - - $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctSocialNetworksEvolution']); - - - // DISTINCT WEBSITES - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctWebsitesGraph'); - - $distinctMetrics['numberDistinctWebsites'] = $numberFormatter->formatNumber($distinctMetrics['numberDistinctWebsites']); - $distinctMetrics['numberDistinctWebsitesUrls'] = $numberFormatter->formatNumber($distinctMetrics['numberDistinctWebsitesUrls']); - - $values = array($distinctMetrics['numberDistinctWebsites'], $distinctMetrics['numberDistinctWebsitesUrls']); - $descriptions = array(Piwik::translate('Referrers_DistinctWebsites'), Piwik::translate('Referrers_UsingNDistinctUrls')); - - $view->config->addSparkline($sparklineParams, $values, $descriptions, @$distinctMetrics['numberDistinctWebsitesEvolution']); - - - // DISTINCT KEYWORDS - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctKeywordsGraph'); - $value = $distinctMetrics['numberDistinctKeywords']; - $value = $numberFormatter->formatNumber($value); - $description = Piwik::translate('Referrers_DistinctKeywords'); - - $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctKeywordsEvolution']); - - - // DISTINCT CAMPAIGNS - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctCampaignsGraph'); - $value = $distinctMetrics['numberDistinctCampaigns']; - $value = $numberFormatter->formatNumber($value); - $description = Piwik::translate('Referrers_DistinctCampaigns'); - - $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctCampaignsEvolution']); - - return $view->render(); - } - - private function getDistinctSparklineUrlParams($action) - { - return array('module' => $this->pluginName, 'action' => $action); + return FrontController::getInstance()->fetchDispatch('Referrers', 'get'); } - protected function getReferrersVisitorsByType($date = false) - { - if ($date === false) { - $date = Common::getRequestVar('date', false); - } - - // we disable the queued filters because here we want to get the visits coming from search engines - // if the filters were applied we would have to look up for a label looking like "Search Engines" - // which is not good when we have translations - $dataTableReferrersType = Request::processRequest( - "Referrers.getReferrerType", array('disable_queued_filters' => '1', 'date' => $date)); - - $nameToColumnId = array( - 'visitorsFromSearchEngines' => Common::REFERRER_TYPE_SEARCH_ENGINE, - 'visitorsFromSocialNetworks' => Common::REFERRER_TYPE_SOCIAL_NETWORK, - 'visitorsFromDirectEntry' => Common::REFERRER_TYPE_DIRECT_ENTRY, - 'visitorsFromWebsites' => Common::REFERRER_TYPE_WEBSITE, - 'visitorsFromCampaigns' => Common::REFERRER_TYPE_CAMPAIGN, - ); - $return = array(); - foreach ($nameToColumnId as $nameVar => $columnId) { - $value = 0; - $row = $dataTableReferrersType->getRowFromLabel($columnId); - if ($row !== false) { - $value = $row->getColumn(Metrics::INDEX_NB_VISITS); - } - $return[$nameVar] = $value; - } - return $return; - } - - protected $referrerTypeToLabel = array( - Common::REFERRER_TYPE_DIRECT_ENTRY => 'Referrers_DirectEntry', - Common::REFERRER_TYPE_SEARCH_ENGINE => 'Referrers_SearchEngines', - Common::REFERRER_TYPE_SOCIAL_NETWORK => 'Referrers_Socials', - Common::REFERRER_TYPE_WEBSITE => 'Referrers_Websites', - Common::REFERRER_TYPE_CAMPAIGN => 'Referrers_Campaigns', - ); - public function getEvolutionGraph($typeReferrer = false, array $columns = array(), array $defaultColumns = array()) { $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Referrers.getReferrerType'); @@ -369,84 +169,4 @@ public static function getTranslatedReferrerTypeLabel($typeReferrer) return Piwik::translate($label); } - /** - * Returns the URL for the sparkline of visits with a specific referrer type. - * - * @param int $referrerType The referrer type. Referrer types are defined in Common class. - * @return string The URL that can be used to get a sparkline image. - */ - private function getReferrerSparklineParams($referrerType) - { - $totalRow = $this->translator->translate('General_Total'); - - return array( - 'columns' => array('nb_visits'), - 'rows' => array(self::getTranslatedReferrerTypeLabel($referrerType), $totalRow), - 'typeReferrer' => $referrerType, - 'module' => $this->pluginName, - 'action' => 'getReferrerType' - ); - } - - /** - * Returns an array containing the number of distinct referrers for each - * referrer type. - * - * @param bool|string $date The date to use when getting metrics. If false, the - * date query param is used. - * @return array The metrics. - */ - private function getDistinctReferrersMetrics($date = false) - { - $propertyToAccessorMapping = array( - 'numberDistinctSearchEngines' => 'getNumberOfDistinctSearchEngines', - 'numberDistinctSocialNetworks' => 'getNumberOfDistinctSocialNetworks', - 'numberDistinctKeywords' => 'getNumberOfDistinctKeywords', - 'numberDistinctWebsites' => 'getNumberOfDistinctWebsites', - 'numberDistinctWebsitesUrls' => 'getNumberOfDistinctWebsitesUrls', - 'numberDistinctCampaigns' => 'getNumberOfDistinctCampaigns', - ); - - $result = array(); - foreach ($propertyToAccessorMapping as $property => $method) { - $result[$property] = $this->getNumericValue('Referrers.' . $method, $date); - } - return $result; - } - - /** - * Utility method that calculates evolution values for a set of current & past values - * and sets properties on a View w/ HTML that displays the evolution percents. - * - * @param string $date The date of the current values. - * @param array $currentValues Array mapping view property names w/ present values. - * @param string $lastPeriodDate The date of the period in the past. - * @param array $previousValues Array mapping view property names w/ past values. Keys - * in this array should be the same as keys in $currentValues. - * @return array Added current values - */ - private function addEvolutionPropertiesToView($date, $currentValues, $lastPeriodDate, $previousValues) - { - foreach ($previousValues as $name => $pastValue) { - $currentValue = $currentValues[$name]; - $evolutionName = $name . 'Evolution'; - - $currentValueFormatted = NumberFormatter::getInstance()->format($currentValue); - $pastValueFormatted = NumberFormatter::getInstance()->format($pastValue); - - $currentValues[$evolutionName] = array( - 'currentValue' => $currentValue, - 'pastValue' => $pastValue, - 'tooltip' => Piwik::translate('General_EvolutionSummaryGeneric', array( - Piwik::translate('General_NVisits', $currentValueFormatted), - $date, - Piwik::translate('General_NVisits', $pastValueFormatted), - $lastPeriodDate, - CalculateEvolutionFilter::calculate($currentValue, $pastValue, $precision = 1) - )) - ); - } - - return $currentValues; - } } diff --git a/app/plugins/Referrers/Referrers.php b/app/plugins/Referrers/Referrers.php index 479323eeb..5558d0891 100644 --- a/app/plugins/Referrers/Referrers.php +++ b/app/plugins/Referrers/Referrers.php @@ -10,6 +10,7 @@ use Piwik\Common; use Piwik\Piwik; +use Piwik\Plugins\Referrers\Reports\Get; use Piwik\Plugins\SitesManager\SiteUrls; /** @@ -33,9 +34,40 @@ public function registerEvents() 'Tracker.setTrackerCacheGeneral' => 'setTrackerCacheGeneral', 'AssetManager.getJavaScriptFiles' => 'getJsFiles', 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', + 'Metrics.getDefaultMetricTranslations' => 'getDefaultMetricTranslations', ); } + public function getDefaultMetricTranslations(&$translations) + { + $translations['Referrers_visitorsFromSearchEngines'] = Piwik::translate('Referrers_VisitorsFromSearchEngines'); + $translations['Referrers_visitorsFromSearchEngines_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromSearchEngines']); + + $translations['Referrers_visitorsFromSocialNetworks'] = Piwik::translate('Referrers_VisitorsFromSocialNetworks'); + $translations['Referrers_visitorsFromSocialNetworks_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromSocialNetworks']); + + $translations['Referrers_visitorsFromDirectEntry'] = Piwik::translate('Referrers_VisitorsFromDirectEntry'); + $translations['Referrers_visitorsFromDirectEntry_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromDirectEntry']); + + $translations['Referrers_visitorsFromWebsites'] = Piwik::translate('Referrers_VisitorsFromWebsites'); + $translations['Referrers_visitorsFromWebsites_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromWebsites']); + + $translations['Referrers_visitorsFromCampaigns'] = Piwik::translate('Referrers_VisitorsFromCampaigns'); + $translations['Referrers_visitorsFromCampaigns_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromCampaigns']); + + $translations[Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctSearchEngines')); + $translations[Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctSocialNetworks')); + $translations[Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctWebsites')); + $translations[Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctKeywords')); + $translations[Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctCampaigns')); + } + + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = 'Referrers_Referrers.Referrers_URLCampaignBuilder'; + } + public function getStylesheetFiles(&$stylesheets) { $stylesheets[] = 'plugins/Referrers/angularjs/campaign-builder/campaign-builder.directive.less'; diff --git a/app/plugins/Referrers/Reports/Get.php b/app/plugins/Referrers/Reports/Get.php new file mode 100644 index 000000000..07c429cc6 --- /dev/null +++ b/app/plugins/Referrers/Reports/Get.php @@ -0,0 +1,200 @@ +name = Piwik::translate('Referrers_ReferrersOverview'); + $this->documentation = ''; + $this->processedMetrics = [ + // none + ]; + $this->metrics = [ + 'Referrers_visitorsFromSearchEngines', + 'Referrers_visitorsFromSearchEngines_percent', + 'Referrers_visitorsFromSocialNetworks', + 'Referrers_visitorsFromSocialNetworks_percent', + 'Referrers_visitorsFromDirectEntry', + 'Referrers_visitorsFromDirectEntry_percent', + 'Referrers_visitorsFromWebsites', + 'Referrers_visitorsFromWebsites_percent', + 'Referrers_visitorsFromCampaigns', + 'Referrers_visitorsFromCampaigns_percent', + Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME, + Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME, + Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME, + Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME, + Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME, + ]; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + // empty + } + + public function configureView(ViewDataTable $view) + { + if ($view->isViewDataTableId(Sparklines::ID) + && $view instanceof Sparklines + ) { + $this->addSparklineColumns($view); + $view->config->addTranslations($this->getSparklineTranslations()); + + // add evolution values + list($lastPeriodDate, $ignore) = Range::getLastDate(); + if ($lastPeriodDate !== false) { + $date = Common::getRequestVar('date'); + + /** @var DataTable $previousData */ + $previousData = Request::processRequest('Referrers.get', ['date' => $lastPeriodDate]); + $previousDataRow = $previousData->getFirstRow(); + + $view->config->compute_evolution = function ($columns) use ($date, $lastPeriodDate, $previousDataRow) { + $value = reset($columns); + $columnName = key($columns); + + if (!in_array($columnName, $this->metrics)) { + return; + } + + $pastValue = $previousDataRow->getColumn($columnName); + + $currentValueFormatted = NumberFormatter::getInstance()->format($value); + $pastValueFormatted = NumberFormatter::getInstance()->format($pastValue); + + return [ + 'currentValue' => $value, + 'pastValue' => $pastValue, + 'tooltip' => Piwik::translate('General_EvolutionSummaryGeneric', array( + Piwik::translate('General_NVisits', $currentValueFormatted), + $date, + Piwik::translate('General_NVisits', $pastValueFormatted), + $lastPeriodDate, + CalculateEvolutionFilter::calculate($value, $pastValue, $precision = 1) + )), + ]; + }; + } + } + } + + /** + * Returns the pretty date representation + * + * @param $date string + * @param $period string + * @return string Pretty date + */ + public static function getPrettyDate($date, $period) + { + return self::getCalendarPrettyDate(Factory::build($period, Date::factory($date))); + } + + /** + * Returns a prettified date string for use in period selector widget. + * + * @param Period $period The period to return a pretty string for. + * @return string + * @api + */ + public static function getCalendarPrettyDate($period) + { + if ($period instanceof Month) { + // show month name when period is for a month + + return $period->getLocalizedLongString(); + } else { + return $period->getPrettyString(); + } + } + + private function addSparklineColumns(Sparklines $view) + { + $directEntry = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_DIRECT_ENTRY); + $directEntry = urlencode($directEntry); + + $website = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_WEBSITE); + $website = urlencode($website); + + $searchEngine = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_SEARCH_ENGINE); + $searchEngine = urlencode($searchEngine); + + $campaigns = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_CAMPAIGN); + $campaigns = urlencode($campaigns); + + $socialNetworks = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_SOCIAL_NETWORK); + $socialNetworks = urlencode($socialNetworks); + + $total = Piwik::translate('General_Total'); + + $view->config->addSparklineMetric(['Referrers_visitorsFromDirectEntry', 'Referrers_visitorsFromDirectEntry_percent'], 10, ['rows' => $directEntry . ',' . $total]); + $view->config->addSparklineMetric(['Referrers_visitorsFromWebsites', 'Referrers_visitorsFromWebsites_percent'], 20, ['rows' => $website . ',' . $total]); + $view->config->addSparklineMetric(['Referrers_visitorsFromSearchEngines', 'Referrers_visitorsFromSearchEngines_percent'], 30, ['rows' => $searchEngine . ',' . $total]); + $view->config->addSparklineMetric(['Referrers_visitorsFromSocialNetworks', 'Referrers_visitorsFromSocialNetworks_percent'], 40, ['rows' => $socialNetworks . ',' . $total]); + $view->config->addSparklineMetric(['Referrers_visitorsFromCampaigns', 'Referrers_visitorsFromCampaigns_percent'], 50, ['rows' => $campaigns . ',' . $total]); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME], 50); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME], 60); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME], 70); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME], 80); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME], 90); + } + + private function getSparklineTranslations() + { + $translations = [ + 'Referrers_visitorsFromDirectEntry' => Piwik::translate('Referrers_TypeDirectEntries'), + 'Referrers_visitorsFromWebsites' => Piwik::translate('Referrers_TypeWebsites'), + 'Referrers_visitorsFromSearchEngines' => Piwik::translate('Referrers_TypeSearchEngines'), + 'Referrers_visitorsFromSocialNetworks' => Piwik::translate('Referrers_TypeSocialNetworks'), + 'Referrers_visitorsFromCampaigns' => Piwik::translate('Referrers_TypeCampaigns'), + ]; + + foreach ($translations as $name => $label) { + $translations[$name . '_percent'] = Piwik::translate('Referrers_XPercentOfVisits'); + } + + $translations = array_merge($translations, [ + Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME => Piwik::translate('Referrers_DistinctSearchEngines'), + Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME => Piwik::translate('Referrers_DistinctSocialNetworks'), + Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME => Piwik::translate('Referrers_DistinctWebsites'), + Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME => Piwik::translate('Referrers_DistinctKeywords'), + Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME => Piwik::translate('Referrers_DistinctCampaigns'), + ]); + + return $translations; + } +} \ No newline at end of file diff --git a/app/plugins/Referrers/Tasks.php b/app/plugins/Referrers/Tasks.php index 8f1264d2c..1d895e99f 100644 --- a/app/plugins/Referrers/Tasks.php +++ b/app/plugins/Referrers/Tasks.php @@ -17,6 +17,41 @@ class Tasks extends \Piwik\Plugin\Tasks { public function schedule() { + if(SettingsPiwik::isInternetEnabled() === true){ + $this->weekly('updateSearchEngines'); + $this->weekly('updateSocials'); + } } + /** + * Update the search engine definitions + * + * @see https://github.com/matomo-org/searchengine-and-social-list + */ + public function updateSearchEngines() + { + $url = 'https://raw.githubusercontent.com/matomo-org/searchengine-and-social-list/master/SearchEngines.yml'; + $list = Http::sendHttpRequest($url, 30); + $searchEngines = SearchEngine::getInstance()->loadYmlData($list); + if (count($searchEngines) < 200) { + return; + } + Option::set(SearchEngine::OPTION_STORAGE_NAME, base64_encode(serialize($searchEngines))); + } + + /** + * Update the social definitions + * + * @see https://github.com/matomo-org/searchengine-and-social-list + */ + public function updateSocials() + { + $url = 'https://raw.githubusercontent.com/matomo-org/searchengine-and-social-list/master/Socials.yml'; + $list = Http::sendHttpRequest($url, 30); + $socials = Social::getInstance()->loadYmlData($list); + if (count($socials) < 50) { + return; + } + Option::set(Social::OPTION_STORAGE_NAME, base64_encode(serialize($socials))); + } } diff --git a/app/plugins/Referrers/config/config.php b/app/plugins/Referrers/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/Referrers/config/config.php +++ b/app/plugins/Referrers/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Referrers/config/tracker.php b/app/plugins/Referrers/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Referrers/config/tracker.php +++ b/app/plugins/Referrers/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Referrers/lang/cs.json b/app/plugins/Referrers/lang/cs.json index 548c0b6da..3b1a4b966 100644 --- a/app/plugins/Referrers/lang/cs.json +++ b/app/plugins/Referrers/lang/cs.json @@ -43,6 +43,7 @@ "WidgetExternalWebsites": "Odkazující weby", "WidgetSocials": "Seznam sociálních sítí", "WidgetTopKeywordsForPages": "Nejčastější klíčová slova pro URL stránky", - "XPercentOfVisits": "%s návštěv" + "XPercentOfVisits": "%s návštěv", + "Acquisition": "Akvizice" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/da.json b/app/plugins/Referrers/lang/da.json index 806420e99..9a58fc618 100644 --- a/app/plugins/Referrers/lang/da.json +++ b/app/plugins/Referrers/lang/da.json @@ -33,6 +33,7 @@ "SocialsReportDocumentation": "Rapporten viser, hvilke sociale netværk der førte besøgende til hjemmesiden. Ved at klikke på en række i tabellen, kan du se, fra hvilke sociale netværkssider besøgende kom til hjemmesiden.", "SubmenuSearchEngines": "Søgemaskine og søgeord", "SubmenuWebsitesOnly": "Hjemmesider", + "Type": "Kanaltype", "TypeCampaigns": "%s fra kampagner", "TypeDirectEntries": "%s direkte besøg", "TypeSearchEngines": "%s fra søgemaskine", diff --git a/app/plugins/Referrers/lang/de.json b/app/plugins/Referrers/lang/de.json index 89c2466a2..cad50642b 100644 --- a/app/plugins/Referrers/lang/de.json +++ b/app/plugins/Referrers/lang/de.json @@ -26,6 +26,7 @@ "DistinctSearchEngines": "verschiedene Suchmaschinen", "DistinctSocialNetworks": "ausgeprägt soziale Netzwerke", "DistinctWebsites": "verschiedene Websites", + "DistinctWebsiteUrls": "verschiedene Webseiten URLs", "EvolutionDocumentation": "Dies ist eine Übersicht der Verweise, die Besucher auf Ihre Website geführt haben.", "EvolutionDocumentationMoreInfo": "Für mehr Informationen über die verschiedenen Kanaltypen, lesen Sie die Dokumentation der %s Tabelle.", "Keywords": "Suchbegriffe", @@ -68,6 +69,12 @@ "WidgetSocials": "Liste Sozialer Netzwerke", "WidgetTopKeywordsForPages": "Top-Suchbegriffe für Seiten-URL", "XPercentOfVisits": "%s aller Besuche", - "Acquisition": "Akquisition" + "Acquisition": "Akquisition", + "VisitorsFromSearchEngines": "Besucher von Suchmaschinen", + "PercentOfX": "Prozent von %s", + "VisitorsFromSocialNetworks": "Besucher von sozialen Netzwerken", + "VisitorsFromDirectEntry": "Besucher von Direktzugriffen", + "VisitorsFromWebsites": "Besucher von Webseiten", + "VisitorsFromCampaigns": "Besucher von Kampagnen" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/el.json b/app/plugins/Referrers/lang/el.json index a1c8f3641..dc5ee37b4 100644 --- a/app/plugins/Referrers/lang/el.json +++ b/app/plugins/Referrers/lang/el.json @@ -26,6 +26,7 @@ "DistinctSearchEngines": "διαχωρισμός μηχανών αναζήτησης", "DistinctSocialNetworks": "διακριτά κοινωνικά δίκτυα", "DistinctWebsites": "διαχωρισμός σελίδων", + "DistinctWebsiteUrls": "ξεχωριστές διευθύνσεις URL", "EvolutionDocumentation": "Αυτή είναι μια επισκόπηση των αναφορέων που έστειλαν επισκέπτες στην ιστοσελίδα.", "EvolutionDocumentationMoreInfo": "Για περισσότερες πληροφορίες σχετικά με τους διαφορετικούς τύπους καναλιών, δείτε την τεκμηρίωση για τον πίνακα %s.", "Keywords": "Λέξεις κλειδιά", @@ -68,6 +69,12 @@ "WidgetSocials": "Λίστα κοινωνικών δικτύων", "WidgetTopKeywordsForPages": "Δημοφιλέστερες Λέξεις-Κλειδιά για τη Διεύθυνση URL της σελίδας", "XPercentOfVisits": "%s επισκέψεων", - "Acquisition": "Εκμάθηση" + "Acquisition": "Εκμάθηση", + "VisitorsFromSearchEngines": "Επισκέπτες από Μηχανές Αναζήτησης", + "PercentOfX": "Τοις εκατό του %s", + "VisitorsFromSocialNetworks": "Επισκέπτες από Κοινωνικά Δίκτυα", + "VisitorsFromDirectEntry": "Επισκέπτες από Απευθείας Είσοδο", + "VisitorsFromWebsites": "Επισκέπτες από Ιστοσελίδες", + "VisitorsFromCampaigns": "Επισκέπτες από Καμπάνιες" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/en.json b/app/plugins/Referrers/lang/en.json index 53019f50e..0c4d88992 100644 --- a/app/plugins/Referrers/lang/en.json +++ b/app/plugins/Referrers/lang/en.json @@ -26,6 +26,7 @@ "DistinctSearchEngines": "distinct search engines", "DistinctSocialNetworks": "distinct social networks", "DistinctWebsites": "distinct websites", + "DistinctWebsiteUrls": "distinct website URLs", "EvolutionDocumentation": "This is an overview of the referrers that led visitors to your website.", "EvolutionDocumentationMoreInfo": "For more information about the different channel types, see the documentation of the %s table.", "Keywords": "Keywords", @@ -68,6 +69,12 @@ "WidgetSocials": "List of social networks", "WidgetTopKeywordsForPages": "Top Keywords for Page URL", "XPercentOfVisits": "%s of visits", - "Acquisition": "Acquisition" + "Acquisition": "Acquisition", + "VisitorsFromSearchEngines": "Visitors from Search Engines", + "PercentOfX": "Percent of %s", + "VisitorsFromSocialNetworks": "Visitors from Social Networks", + "VisitorsFromDirectEntry": "Visitors from Direct Entry", + "VisitorsFromWebsites": "Visitors from Websites", + "VisitorsFromCampaigns": "Visitors from Campaigns" } } diff --git a/app/plugins/Referrers/lang/es-ar.json b/app/plugins/Referrers/lang/es-ar.json index 69b3898b7..43399d38a 100644 --- a/app/plugins/Referrers/lang/es-ar.json +++ b/app/plugins/Referrers/lang/es-ar.json @@ -1,46 +1,80 @@ { "Referrers": { - "AllReferrersReportDocumentation": "This report shows all your Referrers in one unified report, listing all Websites, Search keywords and Campaigns used by your visitors to find your website.", + "AllReferrersReportDocumentation": "Este informe muestra todos tus orígenes de visitantes en un informe unificado, listando todos los sitios web, palabras claves en la búsqueda y campañas usadas por tus visitantes para encontrar tu sitio web.", "Campaigns": "Campañas", - "CampaignsDocumentation": "Visitantes que visitan su sitio de internet como resultado de una campaña. %1$s Vea el %2$s informe para mayores detalles.", - "CampaignsReportDocumentation": "Este reporte muestra qué campaña llevó visitantes a su sitio de internet. %1$s Para una mayor información acerca del rastreo de las campañas, lea la documentación %2$scampañas en matomo.org%3$s", + "CampaignsDocumentation": "Visitantes que visitan tu sitio web como resultado de una campaña. %1$s Leé el %2$s informe para más detalles.", + "CampaignsReportDocumentation": "Este informee muestra qué campaña llevó visitantes a tu sitio web. %1$s Para una mayor información acerca del rastreo de las campañas, leé la %2$sdocumentación de campañas en matomo.org%3$s", "ColumnCampaign": "Campaña", - "ColumnSearchEngine": "Motores de búsqueda", + "CampaignPageUrlHelp": "La dirección web de la página para la cual va dirigida esta campaña, por ejemplo: \"http:\/\/ejemplo.org\/oferta_diciembre_2019.html\".", + "CampaignNameHelp": "Elegí un nombre que describa para qué fue creada la campaña y que la diferencie de otras. Por ejemplo: \"ofertasverano-porcorreoelectronico\" o \"ofertasverano-publicidadpaga\".", + "CampaignKeywordHelp": "Si tenés varias campañas con el mismo nombre, podés diferenciarlas especificando una palabra clave o subcategoría.", + "CampaignSource": "Fuente de campaña", + "CampaignSourceHelp": "Usado para rastrear la fuente de la campaña, como \"boletin\" para tu márketing por correo electrónico, \"afiliado\", o el nombre del sitio web que muestra tus publicidades.", + "CampaignContent": "Contenido de la campaña", + "CampaignContentHelp": "Este parámetro es a menudo usando cuando estás probando varias publicidades e incluís el nombre de cada publicidad para saber cuál fue el más efectivo a la hora de generar tráfico.", + "CampaignMedium": "Medio de campaña", + "CampaignMediumHelp": "Usado para describir la actividad de márketing. Por ejemplo: \"PPC\" para una campaña en la que se paga por clic, o \"PAG\" para publicidad paga, o \"analisis\" para rastrear el análisis de un producto en un sitio afiliado.", + "ColumnSearchEngine": "Motor de búsqueda", "ColumnSocial": "Red social", "ColumnWebsite": "Sitio web", "ColumnWebsitePage": "Página web", "DirectEntry": "Entrada directa", + "DirectEntryDocumentation": "Un visitante ingresó la dirección web en su navegador y comenzó a navegar tu sitio web. Es decir, se ingresó la dirección manualmente o desde un marcador.", + "Distinct": "Distinguir orígenes de visita por tipo de canal", "DistinctCampaigns": "campañas distintas", "DistinctKeywords": "palabras clave distintas", "DistinctSearchEngines": "motores de búsqueda distintos", + "DistinctSocialNetworks": "distintas redes sociales", "DistinctWebsites": "páginas web distintas", - "EvolutionDocumentation": "Esta es una viste general de las páginas web externas que condujeron visitantes a su web.", + "DistinctWebsiteUrls": "distintas direcciones web", + "EvolutionDocumentation": "Esta es una vista general de las páginas web externas que condujeron visitantes a tu sitio web.", + "EvolutionDocumentationMoreInfo": "Para más información sobre los diferentes tipos de canales, leé la documentación de la tabla %s.", "Keywords": "Palabras clave", - "KeywordsReportDocumentation": "Este informe revela que palabras claves estuvieron buscando antes de ingresar en su sitio de internet. %s Cliqueando en la tabla, puede observar la distribución de los motores de búsqueda en la búsqueda de dichas palabras claves.", - "Referrer": "Referrer", - "ReferrerName": "Nombre de la referencia", - "Referrers": "Referencias", - "ReferrersOverview": "Referencias Visión", + "KeywordsReportDocumentation": "Este informe muestra qué palabras claves los usuarios estuvieron buscando antes de ingresar en tu sitio web. %s Haciendo clic en la tabla, podés ver la distribución de los motores de búsqueda en la búsqueda de dichas palabras claves.", + "KeywordsReportDocumentationNote": "Nota: este informe enlista las principales palabras claves como no definidas, porque la mayoría de los motores de búsqueda no envía la palabra clave exacta usada.", + "PluginDescription": "Informa los datos de los orígenes de visita: motores de búsqueda, palabras claves, sitios web, campañas, medios sociales, visitas directas.", + "Referrer": "Origen de visita", + "ReferrerName": "Nombre del origen de la visita", + "ReferrerNames": "Nombres de los orígenes de visita", + "Referrers": "Orígenes de visita", + "ReferrersOverview": "Vista general de los orígenes de las visitas", + "ReferrerTypes": "Tipos de canal", + "ReferrerURLs": "Direcciones web de los orígenes de visita", "SearchEngines": "Motores de búsqueda", - "SearchEnginesDocumentation": "Un visitante fue remitido a su sitio de internet por un motor de búsqueda. %1$s Vea el %2$s reporte para mayores detalles.", - "SearchEnginesReportDocumentation": "Este reporte muestra que motores de búsqueda enviaron users a su web. %s Haciendo clic en una fila en la tabla, usted puede ver que usuarios estaban buscando en un motor de búsqueda específico.", + "SearchEnginesDocumentation": "Un visitante fue enviado a tu sitio web por un motor de búsqueda. %1$s Leé el %2$s informe para más detalles.", + "SearchEnginesReportDocumentation": "Este informe muestra qué motores de búsqueda enviaron usuarios a tu sitio web. %s Haciendo clic en una fila en la tabla, podés ver qué estaban buscando los usuarios en un motor de búsqueda específico.", "Socials": "Redes sociales", - "SocialsReportDocumentation": "Este informe muestra que red social redirige visitantes a su sitio de internet.
Simplemente cliqueando en una fila de la tabla, puede observar desde que páginas de determinada red social provienen sus visitantes.", + "SocialsReportDocumentation": "Este informe muestra qué red social envió visitantes a tu sitio web.
Haciendo clic en una fila de la tabla, podés ver desde qué páginas de determinada red social provienen tus visitantes.", "SubmenuSearchEngines": "Motores de búsqueda y palabras clave", "SubmenuWebsitesOnly": "Sitios web", + "Type": "Tipo de canal", "TypeCampaigns": "%s desde campañas", "TypeDirectEntries": "%s entradas directas", + "TypeReportDocumentation": "Esta tabla contiene información sobre la distribución de los tipos de canales.", "TypeSearchEngines": "%s desde motores de búsqueda", + "TypeSocialNetworks": "%s desde redes sociales", "TypeWebsites": "%s desde sitios web", - "UsingNDistinctUrls": "(usando %s urls distintas)", - "ViewAllReferrers": "Ver todas las Referencias", - "ViewReferrersBy": "View Referrers by %s", - "Websites": "Sitios", - "WebsitesDocumentation": "El visitante siguió un enlace en otro sitio de internet que lo dirigió a su sitio. %1$s Vea el %2$s informe para mayores detalles.", - "WebsitesReportDocumentation": "En esta tabla, puede observar qué sitio de internet envió visitantes a su sitio. %s Cliqueando en una fila de la tabla, puede ver que dirección de internet enlaza a su sitio", - "WidgetExternalWebsites": "Lista de páginas web externas", + "UsingNDistinctUrls": "(usando %s direcciones web distintas)", + "GenerateUrl": "Generar dirección web", + "URLCampaignBuilder": "Constructor de dirección web de campaña", + "URLCampaignBuilderIntro": "La %1$sherramienta constructora de dirección web%2$s te permite generar direcciones web lista para usar en el rastreo de campañas en Matomo. Leé la documentación sobre %3$srastreo de campañas%4$s para más información.", + "URLCampaignBuilderResult": "Generá una dirección web que podés pegar en tus campañas, boletines informativos por correo electrónicos, publicidad en Facebook o tweets:", + "ViewAllReferrers": "Ver todos los orígenes de visita", + "ViewReferrersBy": "Ver orígenes de visita por %s", + "Websites": "Sitios web", + "WebsitesDocumentation": "El visitante siguió un enlace en otro sitio web que lo llevó a tu sitio. %1$s Leé el %2$s informe para más detalles.", + "WebsitesReportDocumentation": "En esta tabla, podés ver qué sitio web envió visitantes a tu sitio. %s Haciendo clic en una fila de la tabla, podés ver qué dirección web enlaza a tu sitio.", + "WidgetExternalWebsites": "Sitios web de origen", + "WidgetGetAll": "Todos los canales", "WidgetSocials": "Lista de redes sociales", - "WidgetTopKeywordsForPages": "Principales Palabras clave para la Página URL", - "XPercentOfVisits": "%s of visits" + "WidgetTopKeywordsForPages": "Principales palabras clave para la dirección web de la página", + "XPercentOfVisits": "%s de visitas", + "Acquisition": "Adquisición", + "VisitorsFromSearchEngines": "Visitantes desde motores de búsqueda", + "PercentOfX": "Porcentaje de %s", + "VisitorsFromSocialNetworks": "Visitantes desde redes sociales", + "VisitorsFromDirectEntry": "Visitantes directos", + "VisitorsFromWebsites": "Visitantes desde sitios web", + "VisitorsFromCampaigns": "Visitantes desde campañas" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/fi.json b/app/plugins/Referrers/lang/fi.json index 693f2729d..b36eb271a 100644 --- a/app/plugins/Referrers/lang/fi.json +++ b/app/plugins/Referrers/lang/fi.json @@ -63,6 +63,11 @@ "WidgetSocials": "Sosiaalisten verkostojen lista", "WidgetTopKeywordsForPages": "Käytetyimmät avainsanat sivun URL:lle", "XPercentOfVisits": "%s käynneistä", - "Acquisition": "Saapumiset" + "Acquisition": "Saapumiset", + "VisitorsFromSearchEngines": "Kävijöitä hakukoneista", + "VisitorsFromSocialNetworks": "Kävijöitä sosiaalisista verkostoista", + "VisitorsFromDirectEntry": "Kävijöitä suoraan", + "VisitorsFromWebsites": "Kävijöitä verkkosivustoilta", + "VisitorsFromCampaigns": "Kävijöitä kamppanjoista" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/fr.json b/app/plugins/Referrers/lang/fr.json index 297c662e0..1db64f14e 100644 --- a/app/plugins/Referrers/lang/fr.json +++ b/app/plugins/Referrers/lang/fr.json @@ -26,6 +26,7 @@ "DistinctSearchEngines": "Moteurs de recherche distincts", "DistinctSocialNetworks": "réseaux sociaux uniques", "DistinctWebsites": "sites web distincts", + "DistinctWebsiteUrls": "URL distinctes de sites Internet", "EvolutionDocumentation": "Ceci est un aperçu des référents qui ont conduit des visiteurs sur votre site web.", "EvolutionDocumentationMoreInfo": "Pour plus d'information à propos des différents types de canaux, consultez la documentation de la table %s.", "Keywords": "Mots-clés", @@ -56,7 +57,7 @@ "UsingNDistinctUrls": "(utilisant %s urls distinctes)", "GenerateUrl": "Générer l'URL", "URLCampaignBuilder": "Bâtisseur d'URL de campagne", - "URLCampaignBuilderIntro": "L'outil %1$s bâtisseur d'URL%2$s vous permet de générer des URLs prêtes à être utilisées pour suivre des campagnes Matomo. Reportez-vous à la documentation à propos %3$sdu suivit de Campagne%4$s pour plus d'information.", + "URLCampaignBuilderIntro": "L'outil %1$s bâtisseur d'URL%2$s vous permet de générer des URLs prêtes à être utilisées pour suivre des campagnes Matomo. Reportez-vous à la documentation à propos %3$sdu suivi de Campagne%4$s pour plus d'information.", "URLCampaignBuilderResult": "URL générée que vous pouvez copier\/coller dans vos Campagnes, Info courriel, Publicité Facebook ou Tweets:", "ViewAllReferrers": "Afficher tous les référents", "ViewReferrersBy": "Afficher les référents par %s", @@ -68,6 +69,12 @@ "WidgetSocials": "Liste des réseaux sociaux", "WidgetTopKeywordsForPages": "Meilleurs mots-clés pour l'URL", "XPercentOfVisits": "%s des visites", - "Acquisition": "Acquisition" + "Acquisition": "Acquisition", + "VisitorsFromSearchEngines": "Visiteurs de moteurs de recherche", + "PercentOfX": "Pourcentage de %s", + "VisitorsFromSocialNetworks": "Visiteurs de réseaux sociaux", + "VisitorsFromDirectEntry": "Visiteurs entrés directement", + "VisitorsFromWebsites": "Visiteurs de sites web", + "VisitorsFromCampaigns": "Visiteurs de campagnes" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/pt-br.json b/app/plugins/Referrers/lang/pt-br.json index 5039768a2..3d0dc4483 100644 --- a/app/plugins/Referrers/lang/pt-br.json +++ b/app/plugins/Referrers/lang/pt-br.json @@ -2,46 +2,79 @@ "Referrers": { "AllReferrersReportDocumentation": "Este relatório mostra todos os seus Referenciadores em um relatório unificado, listando todos os Websites, Palavras-Chave de Pesquisa e Campanhas utilizadas ​​por seus visitantes para encontrar seu website.", "Campaigns": "Campanhas", - "CampaignsDocumentation": "Os visitantes que vieram para o seu site como o resultado de uma campanha. %1$s Ver relatório %2$s para mais detalhes.", + "CampaignsDocumentation": "Visitantes que vieram para o seu site como resultado de uma campanha. %1$s Veja o relatório %2$s para mais detalhes.", "CampaignsReportDocumentation": "Este relatório mostra quais as campanhas lideradas visitantes para o seu site. %1$s para mais informações sobre as campanhas de monitoramento, leia a documentação %2$s campanhas em matomo.org %3$s", - "ColumnCampaign": "campanha", - "ColumnSearchEngine": "Motor de Busca", + "ColumnCampaign": "Campanha", + "CampaignPageUrlHelp": "A URL da página para a qual essa campanha direciona, por exemplo 'http:\/\/exemplo.org.br\/oferta.html'.", + "CampaignNameHelp": "Escolha um nome que descreva para que a campanha será criada e que distingua essa campanha de outras campanhas suas. Por exemplo 'Email-OfertasVerao' ou 'AdsPagos-OfertasVerao'.", + "CampaignKeywordHelp": "Se você tem diversas campanhas com o mesmo nome, você pode distinguir essas campanhas especificando uma palavra-chave ou uma subcategoria.", + "CampaignSource": "Origem da campanha", + "CampaignSourceHelp": "Usada para rastrear a origem da campanha, como 'boletim informativo' para seu marketing de e-mail, 'subsidiária', ou o nome do site exibindo seus anúncios.", + "CampaignContent": "Conteúdo da campanha", + "CampaignContentHelp": "Este parâmetro é usado frequentemente quando você está testando diversos anúncios, e inclui o nome de cada anúncio para ver qual foi mais efetivo para direcionar tráfego.", + "CampaignMedium": "Ambiente da campanha", + "CampaignMediumHelp": "Usado para descrever a atividade de marketing, por exemplo 'PPC' para uma campanha pay-per-click, ou 'SEM' para anúncios de busca pagos, ou 'avaliação' para rastrear uma avaliação de produto em um site associado.", + "ColumnSearchEngine": "Motor de busca", "ColumnSocial": "Rede social", - "ColumnWebsite": "Website", - "ColumnWebsitePage": "Página da Web", - "DirectEntry": "Entrada Direta", - "DistinctCampaigns": "Campanhas distintas", - "DistinctKeywords": "Palavras-chave distintas", - "DistinctSearchEngines": "Motores de busca distintos", - "DistinctWebsites": "Websites distintos", + "ColumnWebsite": "Site", + "ColumnWebsitePage": "Página do site", + "DirectEntry": "Entrada direta", + "DirectEntryDocumentation": "Um visitante digitou a URL em seu navegador e começou a navegar em seu site - ele entraram no site diretamente.", + "Distinct": "Referenciadores distintos por tipo de canal", + "DistinctCampaigns": "campanhas distintas", + "DistinctKeywords": "palavras-chave distintas", + "DistinctSearchEngines": "motores de busca distintos", + "DistinctSocialNetworks": "redes sociais distintas", + "DistinctWebsites": "sites distintos", + "DistinctWebsiteUrls": "URLs distintas de sites", "EvolutionDocumentation": "Esta é uma visão geral dos referenciadores que levaram visitantes ao seu site.", + "EvolutionDocumentationMoreInfo": "Para mais informações sobre os diferentes tipos de canal, veja a documentação da tabela %s.", "Keywords": "Palavras-chave", "KeywordsReportDocumentation": "Este relatório mostra quais palavras-chave os usuários estavam procurando antes de eles serem encaminhados para o seu site. %s Ao clicar em uma linha na tabela, você pode ver a distribuição dos motores de busca, que foram consultados para a palavra-chave.", + "KeywordsReportDocumentationNote": "Obs: Este relatório lista a maioria das palavras-chave como não definidas pois a maioria dos motores de busca não enviam a palavra-chave exata usada no motor de busca.", "PluginDescription": "Relatórios de Dados de Referenciadores: Motores de Busca, Palavras-Chave, Websites, Campanhas, Mídias Sociais e Entrada Direta.", "Referrer": "Referenciador", "ReferrerName": "Nome do referenciador", + "ReferrerNames": "Nomes dos referenciadores", "Referrers": "Referenciadores", - "ReferrersOverview": "Visão Geral dos Referenciadores", - "SearchEngines": "Motores de Busca", + "ReferrersOverview": "Visão geral dos referenciadores", + "ReferrerTypes": "Tipos de canal", + "ReferrerURLs": "URLs de referenciadores", + "SearchEngines": "Motores de busca", "SearchEnginesDocumentation": "Um visitante foi encaminhado para o seu site por um motor de busca. %1$s Ver o relatório %2$s para mais detalhes.", "SearchEnginesReportDocumentation": "Este relatório mostra quais motores de busca encaminhou usuários ao seu site. %s Ao clicar em uma linha na tabela, você pode ver o que os usuários estavam procurando utilizando um motor de busca específico.", - "Socials": "Redes Sociais", + "Socials": "Redes sociais", "SocialsReportDocumentation": "Este relatório mostra quais as redes sociais levaram visitantes ao seu site.
Ao clicar em uma linha na tabela, você pode ver de quais páginas os visitantes de redes sociais vieram para o seu site.", "SubmenuSearchEngines": "Motores de Busca & Palavras-Chave", - "SubmenuWebsitesOnly": "Websites", + "SubmenuWebsitesOnly": "Sites", + "Type": "Tipo de canal", "TypeCampaigns": "%s de campanhas", "TypeDirectEntries": "%s entradas diretas", - "TypeSearchEngines": "%s de motores de buscas", - "TypeWebsites": "%s de websites", - "UsingNDistinctUrls": "(usando urls %s distintas)", + "TypeReportDocumentation": "Esta tabela contém informações sobre a distribuição dos tipos de canal.", + "TypeSearchEngines": "%s de motores de busca", + "TypeSocialNetworks": "%s de redes sociais", + "TypeWebsites": "%s de sites", + "UsingNDistinctUrls": "(usando %s urls distintas)", + "GenerateUrl": "Gerar URL", + "URLCampaignBuilder": "Construtor de URL de campanha", + "URLCampaignBuilderIntro": "A %1$sferramenta Construtor de URL%2$s permite gerar URLs prontas para uso no rastreamento de campanhas no Matomo. Veja a documentação sobre %3$sRastreamento de Campanha%4$s para mais informações.", + "URLCampaignBuilderResult": "URL gerada que você pode copiar e colar em suas Campanhas, Boletim informativo por e-mail, Anúncios no Facebook ou tweets:", "ViewAllReferrers": "Ver todos os Referenciadores", "ViewReferrersBy": "Ver Referenciadores por %s", - "Websites": "Websites", + "Websites": "Sites", "WebsitesDocumentation": "O visitante seguiu um link em outro site que o levou para o seu site. %1$s Ver relatório %2$s para mais detalhes.", - "WebsitesReportDocumentation": "Nesta tabela, você pode ver quais website encaminharam visitantes para o seu site. %s Ao clicar em uma linha na tabela, você pode ver quais os links de URLs que seu site estava.", - "WidgetExternalWebsites": "Websites Referenciadores", + "WebsitesReportDocumentation": "Nesta tabela, você pode ver quais sites encaminharam visitantes para o seu site. %s Ao clicar em uma linha na tabela, você pode ver quais os links de URLs que seu site estava.", + "WidgetExternalWebsites": "Sites referenciadores", + "WidgetGetAll": "Todos os canais", "WidgetSocials": "Lista de redes sociais", "WidgetTopKeywordsForPages": "Principais Palavras-chave para a URL da página", - "XPercentOfVisits": "%s de visitas" + "XPercentOfVisits": "%s de visitas", + "Acquisition": "Aquisição", + "VisitorsFromSearchEngines": "Visitantes de motores de busca", + "PercentOfX": "Percentual de %s", + "VisitorsFromSocialNetworks": "Visitantes de redes sociais", + "VisitorsFromDirectEntry": "Visitantes de entrada direta", + "VisitorsFromWebsites": "Visitantes de sites", + "VisitorsFromCampaigns": "Visitantes de campanhas" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/pt.json b/app/plugins/Referrers/lang/pt.json index 312b28b41..3ad2dd416 100644 --- a/app/plugins/Referrers/lang/pt.json +++ b/app/plugins/Referrers/lang/pt.json @@ -26,6 +26,7 @@ "DistinctSearchEngines": "motores de pesquisa distintos", "DistinctSocialNetworks": "redes sociais distintas", "DistinctWebsites": "sites distintos", + "DistinctWebsiteUrls": "endereços distintos do site", "EvolutionDocumentation": "Isto é uma visão global dos referenciadores que trouxeram visitantes ao seu site.", "EvolutionDocumentationMoreInfo": "Para mais informações sobre os diferentes tipos de canais, consulte a documentação da tabela %s.", "Keywords": "Palavras-chave", @@ -68,6 +69,12 @@ "WidgetSocials": "Lista de redes sociais", "WidgetTopKeywordsForPages": "Principais palavras-chave por endereço de página", "XPercentOfVisits": "%s de visitas", - "Acquisition": "Aquisição" + "Acquisition": "Aquisição", + "VisitorsFromSearchEngines": "Visitantes de motores de pesquisa", + "PercentOfX": "Percentagem de %s", + "VisitorsFromSocialNetworks": "Visitantes de redes sociais", + "VisitorsFromDirectEntry": "Visitantes de entrada direta", + "VisitorsFromWebsites": "Visitantes de sites", + "VisitorsFromCampaigns": "Visitantes de campanhas" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/sq.json b/app/plugins/Referrers/lang/sq.json index 5f7c3f533..efc0c5464 100644 --- a/app/plugins/Referrers/lang/sq.json +++ b/app/plugins/Referrers/lang/sq.json @@ -26,6 +26,7 @@ "DistinctSearchEngines": "motorë kërkimesh të dallueshëm", "DistinctSocialNetworks": "rrjete shoqërorë të dalluar", "DistinctWebsites": "sajte të dallueshëm", + "DistinctWebsiteUrls": "URL-ra sajtesh të dallueshëm", "EvolutionDocumentation": "Kjo është një përmbledhje e sjellësve që prunë vizitorë te sajti juaj.", "EvolutionDocumentationMoreInfo": "Për më tepër të dhëna rreth llojeve të ndryshme të kanaleve, shihni dokumentimin për tabelën %s.", "Keywords": "Fjalëkyçe", @@ -68,6 +69,12 @@ "WidgetSocials": "Listë rrjetesh shoqërore", "WidgetTopKeywordsForPages": "Fjalëkyçe Kryesuese për URL Faqesh", "XPercentOfVisits": "%s e vizitave", - "Acquisition": "Blerje" + "Acquisition": "Blerje", + "VisitorsFromSearchEngines": "Vizitorë prej Motorë Kërkimesh", + "PercentOfX": "Përqindje e %s", + "VisitorsFromSocialNetworks": "Vizitorë prej Rrjetesh Shoqërore", + "VisitorsFromDirectEntry": "Vizitorë prej Hyrjesh të Drejtpërdrejta", + "VisitorsFromWebsites": "Vizitorë prej Sajtesh", + "VisitorsFromCampaigns": "Vizitorë prej Fushatash" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/sv.json b/app/plugins/Referrers/lang/sv.json index d56825cb0..be8b605c7 100644 --- a/app/plugins/Referrers/lang/sv.json +++ b/app/plugins/Referrers/lang/sv.json @@ -5,6 +5,8 @@ "CampaignsDocumentation": "Besökare som kom till din webbplats som resulterades av en kampanj. %1$s Se rapporten %2$s för mer information.", "CampaignsReportDocumentation": "Rapporten visar vilka kampanjer som har lett besökare till din webbplats. %1$s För mer information om hur du spårar kampanjer, läs %2$skampanjernas dokumentation på matomo.org%3$s", "ColumnCampaign": "Kampanj", + "CampaignSource": "Kampanjkälla", + "CampaignContent": "Kampanjinnehåll", "ColumnSearchEngine": "Sökmotor", "ColumnSocial": "Sociala nätverk", "ColumnWebsite": "Webbplats", @@ -40,6 +42,7 @@ "TypeSocialNetworks": "%s från sociala nätverk", "TypeWebsites": "%s från webbplatser", "UsingNDistinctUrls": "(använder %s distinkta urler)", + "GenerateUrl": "Skapa URL", "URLCampaignBuilderResult": "Genererad URL som du kan kopiera och klistra in i dina kampanjer, nyhetsbrev, Facebook annonser eller tweets:", "ViewAllReferrers": "Visa alla hänvisningar", "ViewReferrersBy": "Visa hänvisningar efter %s", @@ -50,6 +53,9 @@ "WidgetSocials": "Lista över sociala nätverk", "WidgetTopKeywordsForPages": "Toppnyckelord för sid-URL", "XPercentOfVisits": "%s av besökare", - "Acquisition": "Förvärv" + "Acquisition": "Förvärv", + "VisitorsFromSocialNetworks": "Besökare från sociala nätverk", + "VisitorsFromWebsites": "Besökare från webbplatser", + "VisitorsFromCampaigns": "Besökare från kampanjer" } } \ No newline at end of file diff --git a/app/plugins/Referrers/lang/tr.json b/app/plugins/Referrers/lang/tr.json index bb9a08461..e5148623e 100644 --- a/app/plugins/Referrers/lang/tr.json +++ b/app/plugins/Referrers/lang/tr.json @@ -26,6 +26,7 @@ "DistinctSearchEngines": "ayrı arama motoru", "DistinctSocialNetworks": "ayrı sosyal ağlar", "DistinctWebsites": "ayrı web siteleri", + "DistinctWebsiteUrls": "ayrı web sitesi adresleri", "EvolutionDocumentation": "Web sitenize ziyaretçi yönlendirenlerin özeti.", "EvolutionDocumentationMoreInfo": "Farklı kanal türleri hakkında ayrıntılı bilgi almak için %s tablosunun belgelerine bakın.", "Keywords": "Anahtar Sözcükler", @@ -68,6 +69,12 @@ "WidgetSocials": "Tüm sosyal ağlar", "WidgetTopKeywordsForPages": "Sayfa Adresi için Çok Kullanılan Anahtar Sözcükler", "XPercentOfVisits": "ziyaretlerde %s", - "Acquisition": "Veri Toplama" + "Acquisition": "Veri Toplama", + "VisitorsFromSearchEngines": "Arama Motorlarından Gelen Ziyaretçiler", + "PercentOfX": "%s yüzdesi", + "VisitorsFromSocialNetworks": "Sosyal Ağlardan Gelen Ziyaretçiler", + "VisitorsFromDirectEntry": "Doğrudan Giriş Yapan Ziyaretçiler", + "VisitorsFromWebsites": "Web Sitelerinden Gelen Ziyaretçiler", + "VisitorsFromCampaigns": "Kampanyalardan Gelen Ziyaretçiler" } } \ No newline at end of file diff --git a/app/plugins/Resolution/config/config.php b/app/plugins/Resolution/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/Resolution/config/config.php +++ b/app/plugins/Resolution/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Resolution/config/tracker.php b/app/plugins/Resolution/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Resolution/config/tracker.php +++ b/app/plugins/Resolution/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Resolution/lang/zh-cn.json b/app/plugins/Resolution/lang/zh-cn.json index ad8ae4895..efc25325f 100644 --- a/app/plugins/Resolution/lang/zh-cn.json +++ b/app/plugins/Resolution/lang/zh-cn.json @@ -3,6 +3,7 @@ "ColumnConfiguration": "客户端配置", "ColumnResolution": "分辨率", "Configurations": "客户端配置", + "PluginDescription": "报告访客的屏幕分辨率。", "Resolutions": "分辨率", "WidgetGlobalVisitors": "访客设置", "WidgetGlobalVisitorsDocumentation": "本报表显示您的访客最常用的系统配置。系统配置是操作系统、浏览器类型及显示器分辨率的组合。", diff --git a/app/plugins/RssWidget/RssRenderer.php b/app/plugins/RssWidget/RssRenderer.php index ca00fb773..ed3dcd13c 100644 --- a/app/plugins/RssWidget/RssRenderer.php +++ b/app/plugins/RssWidget/RssRenderer.php @@ -55,8 +55,12 @@ public function get() if (!$output) { try { - $content = Http::fetchRemoteFile($this->url); - $rss = simplexml_load_string($content); + $content = Http::fetchRemoteFile($this->url, null, 0, 15); + + $rss = @simplexml_load_string($content); + if ($rss === false) { + throw new \Exception("Failed to parse XML."); + } } catch (\Exception $e) { throw new \Exception("Error while importing feed: {$e->getMessage()}\n"); } diff --git a/app/plugins/RssWidget/Widgets/RssChangelog.php b/app/plugins/RssWidget/Widgets/RssChangelog.php index 3a12f22ce..f93ce51ec 100644 --- a/app/plugins/RssWidget/Widgets/RssChangelog.php +++ b/app/plugins/RssWidget/Widgets/RssChangelog.php @@ -41,7 +41,7 @@ private function getFeed($URL) { public function render() { try { - return $this->getFeed('https://matomo.org/changelog/feed'); + return $this->getFeed('https://matomo.org/changelog/feed/'); } catch (\Exception $e) { return $this->error($e); } diff --git a/app/plugins/RssWidget/config/config.php b/app/plugins/RssWidget/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/RssWidget/config/config.php +++ b/app/plugins/RssWidget/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/RssWidget/config/tracker.php b/app/plugins/RssWidget/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/RssWidget/config/tracker.php +++ b/app/plugins/RssWidget/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/SEO/API.php b/app/plugins/SEO/API.php index 3d6ad91b0..59f07af93 100644 --- a/app/plugins/SEO/API.php +++ b/app/plugins/SEO/API.php @@ -43,7 +43,17 @@ public function getRank($url) $domain = Url::getHostFromUrl($url); $metrics = $metricProvider->getMetrics($domain); - return $this->toDataTable($metrics); + $dataTable = $this->toDataTable($metrics); + $dataTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, [ + 'id' => 'skip', + 'rank' => 'skip', + 'logo' => 'skip', + 'logo_link' => 'skip', + 'logo_tooltip' => 'skip', + 'rank_suffix' => 'skip', + ]); + $dataTable->disableFilter('Limit'); + return $dataTable; } /** diff --git a/app/plugins/SEO/SEO.php b/app/plugins/SEO/SEO.php index 9fbdb51de..e0f7416c4 100644 --- a/app/plugins/SEO/SEO.php +++ b/app/plugins/SEO/SEO.php @@ -18,10 +18,16 @@ class SEO extends \Piwik\Plugin public function registerEvents() { return [ - 'Widget.filterWidgets' => 'filterWidgets' + 'Widget.filterWidgets' => 'filterWidgets', + 'AssetManager.getJavaScriptFiles' => 'getJsFiles', ]; } + public function getJsFiles(&$jsFiles) + { + $jsFiles[] = "plugins/SEO/javascripts/rank.js"; + } + /** * @param WidgetsList $list */ diff --git a/app/plugins/SEO/config/config.php b/app/plugins/SEO/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/SEO/config/config.php +++ b/app/plugins/SEO/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/SEO/config/tracker.php b/app/plugins/SEO/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/SEO/config/tracker.php +++ b/app/plugins/SEO/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/SEO/templates/getRank.twig b/app/plugins/SEO/templates/getRank.twig index 9ecd2131e..4c504d0ab 100644 --- a/app/plugins/SEO/templates/getRank.twig +++ b/app/plugins/SEO/templates/getRank.twig @@ -1,43 +1,17 @@
-
+
- +
- - {% import "ajaxMacros.twig" as ajax %} {{ ajax.LoadingDiv('ajaxLoadingSEO') }} @@ -74,5 +48,5 @@ {% endif %}
-
-
+ + \ No newline at end of file diff --git a/app/plugins/ScheduledReports/SubscriptionModel.php b/app/plugins/ScheduledReports/SubscriptionModel.php index b548b80c8..fcdfa0c10 100644 --- a/app/plugins/ScheduledReports/SubscriptionModel.php +++ b/app/plugins/ScheduledReports/SubscriptionModel.php @@ -14,6 +14,7 @@ use Piwik\Db; use Piwik\DbHelper; use Piwik\Piwik; +use Piwik\Plugins\ScheduledReports\API as APIScheduledReports; class SubscriptionModel { @@ -80,20 +81,12 @@ public function unsubscribe($token) } if ($emailFound) { - Access::doAsSuperUser(function() use ($report) { - Request::processRequest('ScheduledReports.updateReport', [ - 'idReport' => $report['idreport'], - 'idSite' => $report['idsite'], - 'description' => $report['description'], - 'period' => $report['period'], - 'hour' => $report['hour'], - 'reportType' => $report['type'], - 'reportFormat' => $report['format'], - 'reports' => $report['reports'], - 'parameters' => $report['parameters'], - 'idSegment' => $report['idsegment'], - ]); - }); + $reportModel = new Model(); + $reportModel->updateReport($report['idreport'], array( + 'parameters' => json_encode($report['parameters']) + )); + // Reset the cache manually since we didn't call the API method which would do it for us + APIScheduledReports::$cache = array(); Piwik::postEvent('Report.unsubscribe', [$report['idreport'], $email]); diff --git a/app/plugins/ScheduledReports/config/tracker.php b/app/plugins/ScheduledReports/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/ScheduledReports/config/tracker.php +++ b/app/plugins/ScheduledReports/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/ScheduledReports/lang/da.json b/app/plugins/ScheduledReports/lang/da.json index eb75df973..02b39ef27 100644 --- a/app/plugins/ScheduledReports/lang/da.json +++ b/app/plugins/ScheduledReports/lang/da.json @@ -1,5 +1,6 @@ { "ScheduledReports": { + "AggregateReportsFormat": "Vis valgmuligheder", "AggregateReportsFormat_GraphsOnly": "Vis kun diagrammer (ingen tabeller)", "AggregateReportsFormat_TablesAndGraphs": "Vis tabeller og diagrammer for alle rapporter", "AggregateReportsFormat_TablesOnly": "(standard) Vis tabeller (diagrammer kun for nøglemålinger)", @@ -22,10 +23,13 @@ "NoRecipients": "Denne rapport har ingen modtagere", "Pagination": "Side %1$s af %2$s", "PiwikReports": "Matomo rapporter", + "PleaseFindAttachedFile": "Se venligst %1$s rapport for %2$s i den vedhæftede fil.", "SentFromX": "Sendt fra %s.", - "PleaseFindBelow": "Find rapporten nedenfor %1$s for %2$s.", + "PleaseFindBelow": "Se venligst %1$s rapport for %2$s i den vedhæftede fil.", + "PluginDescription": "Opret tilpassede rapporter og planlæg den udsendt pr. e-mail dagligt, ugentligt eller månedsvis til en eller flere modtagere. Adskillige rapport-formater understøttes (html, pdf, csv, billeder).", "ReportFormat": "Rapportformat", "ReportHour": "Send rapport kl. %s", + "ReportHourWithUTC": "Klokken %s UTC", "ReportIncludeNWebsites": "Rapporten indeholder de vigtigste nøgletal for alle hjemmesider, der har mindst ét ​​besøg (fra %s hjemmesider der pt er tilgængelige).", "ReportSent": "Rapport afsent", "ReportsIncluded": "Statikker inkluderet", @@ -47,6 +51,13 @@ "ReportUnsubscribe": "Afmeld en rapport", "UnsubscribeReportConfirmation": "Er du sikker på, at du vil afmelde rapporten %1$s?", "SuccessfullyUnsubscribed": "Du er blevet afmeldt rapporten %1$s.", - "UnsubscribeFooter": "Klik her for at afmelde denne rapport: %1$s" + "UnsubscribeFooter": "Klik her for at afmelde denne rapport: %1$s", + "NoTokenProvided": "Der blev ikke leveret nogen token til URLen", + "NoSubscriptionFound": "Ingen tilmelding fundet. Måske var rapporten allerede afmeldt eller fjernet.", + "EvolutionGraphsShowForEachInPeriod": "Graf der viser udviklingen for %1$s hver dag %2$s de sidste %3$s", + "EvolutionGraphsShowForPreviousN": "Graf der viser udviklingen over den sidste N %s", + "ReportPeriod": "Rapport-periode", + "ReportPeriodHelp": "Perioden over data som er dækket i denne rapport. Som standard er det samme periode som i e-mail planen, så hvis rapporten er sendt ugentligt, vil den indeholde information for den sidste uge.", + "ReportPeriodHelp2": "Du kan imidlertid ændre dette, hvis du ønsker at se anden information og fortsat vil modtage den planlagte e-mail udsendelse. Hvis for eksempel e-mailen er planlagt udsendt ugentlig, og rapport-perioden er 'dag', vil du modtage information om den sidste dag, hver uge." } } \ No newline at end of file diff --git a/app/plugins/ScheduledReports/lang/es-ar.json b/app/plugins/ScheduledReports/lang/es-ar.json index c94c89263..00e4e4ec1 100644 --- a/app/plugins/ScheduledReports/lang/es-ar.json +++ b/app/plugins/ScheduledReports/lang/es-ar.json @@ -1,42 +1,63 @@ { "ScheduledReports": { - "AggregateReportsFormat_GraphsOnly": "Mostrar sólo gráficas (no reportes de tablas)", - "AggregateReportsFormat_TablesAndGraphs": "Mostrar Reporte de tablas y Gráficas para todos los reportes", - "AggregateReportsFormat_TablesOnly": "(por defecto) Mostrar Reporte de tablas (Gráficas sólo para mediciones clave)", - "AlsoSendReportToTheseEmails": "También envíe el reporte a estos correos (un correo por linea):", - "AreYouSureDeleteReport": "¿Está seguro de que desea eliminar este reporte y su envío programado?", - "CancelAndReturnToReports": "Cancelar y %1$svolver a la lista de reportes%2$s", - "CreateAndScheduleReport": "Crear y Programar un reporte", - "CreateReport": "Crear Reporte", - "CustomVisitorSegment": "Segmento de Visitante Personalizado:", - "DescriptionOnFirstPage": "La descripción del reporte será mostrada en la primera página del reporte.", - "DisplayFormat_TablesOnly": "Mostrar sólo Tablas (no gráficas)", - "EmailHello": "Hola,", + "AggregateReportsFormat": "Configuración de visualización", + "AggregateReportsFormat_GraphsOnly": "Mostrar sólo gráficos (sin tablas de informes)", + "AggregateReportsFormat_TablesAndGraphs": "Mostrar tablas de informes y gráficos para todos los informes", + "AggregateReportsFormat_TablesOnly": "(Predeterminado) Mostrar tablas de informes (gráficos sólo para mediciones clave)", + "AlsoSendReportToTheseEmails": "También enviar el informe a estas direcciones de correos electrónicos (una dirección por linea):", + "AreYouSureDeleteReport": "¿Estás seguro que querés eliminar este informe y su programación?", + "CancelAndReturnToReports": "Cancelar y %1$svolver a la lista de informes%2$s", + "CreateAndScheduleReport": "Crear y programar un informe", + "CreateReport": "Crear informe", + "CustomVisitorSegment": "Segmento de visitante personalizado:", + "DescriptionOnFirstPage": "La descripción del informe será mostrada en la primera página del mismo.", + "DisplayFormat_TablesOnly": "Mostrar sólo tablas (no gráficos)", + "EmailHello": "Hola:", "EmailReports": "Informes por correo electrónico", - "EmailSchedule": "Programe el envío del Email", - "EvolutionGraph": "Mostrar Gráficas Históricas para los %s valores más altos", - "FrontPage": "Página Incial", - "MonthlyScheduleHelp": "Envío mensual: los reportes serán enviados el primer día de cada mes.", - "MustBeLoggedIn": "Usted debe haber iniciado sesión para crear y programar reportes personalizados.", - "NoRecipients": "Este reporte no tiene destinatarios", + "EmailSchedule": "Programar el envío del correo electrónico", + "EvolutionGraph": "Mostrar gráficos históricos para los %s valores más altos", + "FrontPage": "Página principal", + "PersonalEmailReports": "Informes a correo electrónico personal", + "MonthlyScheduleHelp": "Envío mensual: los informes serán enviados el primer día de cada mes.", + "MustBeLoggedIn": "Tenés que haber iniciado sesión para crear y programar informes personalizados.", + "NoRecipients": "Este informe no tiene destinatarios", "Pagination": "Página %1$s de %2$s", - "PleaseFindBelow": "Por favor encuentre debajo su reporte %1$s para %2$s.", - "ReportFormat": "Formato del Reporte", - "ReportIncludeNWebsites": "El reporte incluirá las principales métricas para todos los sitios de internet con al menos una visita (de todo %s los sitios de internet disponibles).", - "ReportSent": "Reporte enviado", + "PiwikReports": "Informes de Matomo", + "PleaseFindAttachedFile": "Por favor, entontrá tu informe %1$s de %2$s en el archivo adjunto.", + "SentFromX": "Enviado desde %s.", + "PleaseFindBelow": "Por favor, encontrá abajo tu %1$s informe para %2$s.", + "PluginDescription": "Creá informes personalizados y programalos para que se envíen por correo electrónico diariamente, semanalmente o mensualmente, a una persona o a varias. Se soportan varios formatos de informe (HTML, PDF, CDV, imágenes).", + "ReportFormat": "Formato del informe", + "ReportHour": "Enviar informe a las %s", + "ReportHourWithUTC": "%s UTC", + "ReportIncludeNWebsites": "El informe incluirá las principales métricas para todos los sitios web con al menos una visita (de todos los %s sitios web disponibles).", + "ReportSent": "Informe enviado", "ReportsIncluded": "Estadísticas incluidas", - "ReportType": "Enviar reporte vía", - "ReportUpdated": "Report updated", - "Segment_Deletion_Error": "This segment cannot be deleted or made invisible to other users because it is used to generate email report(s) %s. Please retry after removing this segment from this report(s).", - "Segment_Help": "You can select an existing custom segment to apply to data in this email report. You may create and edit custom segments in your dashboard %1$s(click here to open)%2$s, then click on the \"%3$s\" box, then \"%4$s\".", - "SegmentAppliedToReports": "El segmento '%s' está siendo aplicado a los informes.", - "SendReportNow": "Enviar Reporte ahora", - "SendReportTo": "Enviar reporte a", + "ReportType": "Enviar informe vía", + "ReportUpdated": "Informe actualizado", + "Segment_Deletion_Error": "Este segmento no se puede eliminar o hacer invisible para otros usuarios porque es usado para generar informes por correo electrónico %s. Por favor, intentá de nuevo después de quitar este segmento de el\/los informe\/s.", + "Segment_Help": "Podés seleccionar un segmento personalizado existente para aplicar a datos en este informe de correo electrónico. Podés crear y editar segmentos personalizados en tu panel. %1$s(hacé clic para abrir)%2$s, luego hacé clic en la caja \"%3$s\", luego \"%4$s\".", + "SegmentAppliedToReports": "El segmento \"%s\" está siendo aplicado a los informes.", + "SendReportNow": "Enviar informe ahora", + "SendReportTo": "Enviar informe a", "SentToMe": "Enviarme una copia", - "TableOfContent": "Lista de reportes", - "ThereIsNoReportToManage": "No hay reportes que administrar para el sitio web %s", + "TableOfContent": "Lista de informes", + "ThereIsNoReportToManage": "No hay informess que administrar para el sitio web %s", + "TopLinkTooltip": "Creá informes de correo electrónico para que se envíen las estadísticas de Matomo a tu dirección de correo o a la de tus clientes, ¡automáticamente!", "TopOfReport": "Volver arriba", - "UpdateReport": "Actualizar Reporte", - "WeeklyScheduleHelp": "Envío semanal: los reportes serán enviados el primer Lunes de cada semana." + "UpdateReport": "Actualizar informe", + "WeeklyScheduleHelp": "Envío semanal: los informes serán enviados el primer lunes de cada semana.", + "Unsubscribe": "Desuscribirse", + "ReportUnsubscribe": "Desuscribirse de un informe", + "UnsubscribeReportConfirmation": "¿Estás seguro que querés desuscribirte del informe \"%1$s\"?", + "SuccessfullyUnsubscribed": "Te desuscribiste exitosamente del informe \"%1$s\".", + "UnsubscribeFooter": "Para desuscribirte de este informe, por favor, seguí este enlace: %1$s", + "NoTokenProvided": "No hay código en la dirección web", + "NoSubscriptionFound": "No se encontró ninguna suscripción. Quizá el informe ya fue desuscripto o quitado.", + "EvolutionGraphsShowForEachInPeriod": "Los gráficos evolutivos muestran la evolución de %1$s cada día %2$s en los últimos %3$s", + "EvolutionGraphsShowForPreviousN": "Los gráficos evolutivos muestran la evolución sobre los anteriores \"N\" %s", + "ReportPeriod": "Período del informe", + "ReportPeriodHelp": "El período de datos cubierto por este informe. De forma predeterminada, esto es lo mismo a la programación de correo electrónico, por lo tanto el informe se envía semanalmente, conteniendo información de la última semana.", + "ReportPeriodHelp2": "Podés cambiar esto si querés ver diferente información y aún así retener la programación del correo electrónico. Por ejemplo: si la programación del correo electrónico es semanal, y el período del informe es diaria, vas a obtener información del último día, todas las semanas." } } \ No newline at end of file diff --git a/app/plugins/ScheduledReports/lang/pt-br.json b/app/plugins/ScheduledReports/lang/pt-br.json index eeb80f561..e913e1306 100644 --- a/app/plugins/ScheduledReports/lang/pt-br.json +++ b/app/plugins/ScheduledReports/lang/pt-br.json @@ -23,11 +23,13 @@ "NoRecipients": "Este relatório não tem destinatários", "Pagination": "Página %1$s de %2$s", "PiwikReports": "Relatórios Matomo", + "PleaseFindAttachedFile": "Favor verificar seu relatório %1$s de %2$s no arquivo anexado.", "SentFromX": "Enviado de %s.", "PleaseFindBelow": "Veja abaixo o seu relatório %1$s para %2$s.", "PluginDescription": "Crie relatórios personalizados e programe-os para serem enviado por dia, semana ou mês para uma ou várias pessoas. Vários formatos de relatório são suportados (imagens html, pdf, csv).", "ReportFormat": "Formato do relatório", "ReportHour": "Enviar relatório no horário: %s", + "ReportHourWithUTC": "%s horas UTC", "ReportIncludeNWebsites": "O relatório incluirá as principais métricas para todos os sites que têm pelo menos uma visita (a partir de sites %s atualmente disponível).", "ReportSent": "Relatório enviado", "ReportsIncluded": "Estatísticas incluídas", @@ -44,6 +46,18 @@ "TopLinkTooltip": "Criar relatórios de e-mail com estatísticas para enviar para seu e-mail ou o e-mail de clientes automaticamente!", "TopOfReport": "Voltar ao topo", "UpdateReport": "Atualizar relatório", - "WeeklyScheduleHelp": "Programação semanal: relatório será enviado na segunda-feira de cada semana." + "WeeklyScheduleHelp": "Programação semanal: relatório será enviado na segunda-feira de cada semana.", + "Unsubscribe": "Cancelar inscrição", + "ReportUnsubscribe": "Cancelar a inscrição de um relatório", + "UnsubscribeReportConfirmation": "Tem certeza que deseja cancelar a inscrição ao relatório %1$s?", + "SuccessfullyUnsubscribed": "Sua inscrição ao relatório %1$s foi cancelada com sucesso.", + "UnsubscribeFooter": "Para cancelar a inscrição a este relatório favor ir em: %1$s", + "NoTokenProvided": "Nenhum token foi fornecido na URL", + "NoSubscriptionFound": "Nenhuma inscrição encontrada. Talvez o relatório tenha sido removido ou a inscrição a ele já tenha sido cancelada.", + "EvolutionGraphsShowForEachInPeriod": "Gráficos de evolução mostram a evolução %1$spor dia%2$s nos últimos %3$s", + "EvolutionGraphsShowForPreviousN": "Gráficos de evolução mostram a evolução nos últimos N %s", + "ReportPeriod": "Período do relatório", + "ReportPeriodHelp": "O período dos dados cobertos neste relatório. Por padrão este é o mesmo do agendamento do email, então se o relatório é enviado semanalmente, ele irá conter informação sobre a última semana.", + "ReportPeriodHelp2": "Você pode alterar isto, entretanto, se você quiser ver informações diferentes e ainda manter o agendamento do email. Por exemplo, se o agendamento do email for semanal, e o período do relatório for 'dia', você receberá informações do sobre o último dia, toda semana." } } \ No newline at end of file diff --git a/app/plugins/ScheduledReports/lang/pt.json b/app/plugins/ScheduledReports/lang/pt.json index a0f741804..f563affe8 100644 --- a/app/plugins/ScheduledReports/lang/pt.json +++ b/app/plugins/ScheduledReports/lang/pt.json @@ -55,6 +55,9 @@ "NoTokenProvided": "Não foi fornecido nenhum código neste endereço", "NoSubscriptionFound": "Não foi encontrada nenhuma subscrição. Talvez a subscrição neste relatório já tenha sido anulada ou removida.", "EvolutionGraphsShowForEachInPeriod": "Os gráficos de evolução mostram a evolução para %1$scada dia%2$s nós últimos %3$s", - "EvolutionGraphsShowForPreviousN": "Os gráficos de evolução mostram a evolução durante os últimos N %s" + "EvolutionGraphsShowForPreviousN": "Os gráficos de evolução mostram a evolução durante os últimos N %s", + "ReportPeriod": "Período do relatório", + "ReportPeriodHelp": "O período de dados coberto por este relatório. Por predefinição, isto é o mesmo que o agendamento do e-mail, pelo que se o relatório é enviado semanalmente, irá conter informação relativa à última semana.", + "ReportPeriodHelp2": "No entanto, pode alterar isto, se quiser ver informação diferente e ainda assim manter o agendamento do e-mail. Por exemplo, se o agendamento do e-mail é semanal, e o relatório do período é diário, irá obter informação para o último dia, cada semana." } } \ No newline at end of file diff --git a/app/plugins/ScheduledReports/lang/zh-cn.json b/app/plugins/ScheduledReports/lang/zh-cn.json index 6a9f0baa2..773a610da 100644 --- a/app/plugins/ScheduledReports/lang/zh-cn.json +++ b/app/plugins/ScheduledReports/lang/zh-cn.json @@ -22,7 +22,7 @@ "MustBeLoggedIn": "登录后才能创建和自定义报表。", "NoRecipients": "这个报表没有收件人", "Pagination": "第 %1$s 页,共 %2$s 页", - "PiwikReports": "Matomo 报表", + "PiwikReports": "Matomo仓库", "PleaseFindAttachedFile": "请在附件中查找%2$s的报告%1$s。", "SentFromX": "来自%s发送。", "PleaseFindBelow": "下面是您的 %2$s 的 %1$s 报表。", @@ -46,6 +46,18 @@ "TopLinkTooltip": "创建报表邮件让 Matomo 自动将统计资料发到您或者客户的邮箱中!", "TopOfReport": "回到顶部", "UpdateReport": "更新报表", - "WeeklyScheduleHelp": "每周计划: 报表将会在每周的星期一寄出。" + "WeeklyScheduleHelp": "每周计划: 报表将会在每周的星期一寄出。", + "Unsubscribe": "退订", + "ReportUnsubscribe": "退订报告", + "UnsubscribeReportConfirmation": "您确定要退订报告%1$s吗?", + "SuccessfullyUnsubscribed": "您已成功取消订阅报告%1$s。", + "UnsubscribeFooter": "要退订该报告,请点击以下链接:%1$s", + "NoTokenProvided": "URL中未提供令牌", + "NoSubscriptionFound": "找不到订阅。 该报告可能已被取消订阅或删除。", + "EvolutionGraphsShowForEachInPeriod": "演化图显示了最近%3$s天中每个%1$s天%2$s的演化", + "EvolutionGraphsShowForPreviousN": "演化图显示了先前N %s的演化", + "ReportPeriod": "报告期间", + "ReportPeriodHelp": "本报告涵盖的数据周期。 默认情况下,这与电子邮件时间表相同,因此,如果报告是每周发送一次,它将包含有关上周的信息。", + "ReportPeriodHelp2": "但是,如果您想查看其他信息并仍然保留电子邮件时间表,则可以更改此设置。 例如,如果电子邮件计划是每周一次,而报告期限是“天”,则您将获得每周最后一天的信息。" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/Model.php b/app/plugins/SegmentEditor/Model.php index da30fe535..c505f7a4f 100644 --- a/app/plugins/SegmentEditor/Model.php +++ b/app/plugins/SegmentEditor/Model.php @@ -9,6 +9,7 @@ namespace Piwik\Plugins\SegmentEditor; use Piwik\Common; +use Piwik\Date; use Piwik\Db; use Piwik\DbHelper; @@ -121,17 +122,103 @@ public function getAllSegmentsForAllUsers($idSite = false) public function getSegmentByDefinition($definition) { - $sql = $this->buildQuerySortedByName("definition = ?"); + $sql = $this->buildQuerySortedByName("definition = ? AND deleted = 0"); $bind = [$definition]; $segment = $this->getDb()->fetchRow($sql, $bind); return $segment; } + /** + * Gets a list of segments that have been deleted in the last week and therefore may have orphaned archives. + * @param Date $date Segments deleted on or after this date will be returned. + * @return array of segments. The segments are only populated with the fields needed for archive invalidation + * (e.g. definition, enable_only_idsite). + * @throws \Exception + */ + public function getSegmentsDeletedSince(Date $date) + { + $dateStr = $date->getDatetime(); + $sql = "SELECT DISTINCT definition, enable_only_idsite FROM " . Common::prefixTable('segment') + . " WHERE deleted = 1 AND ts_last_edit >= ?"; + $deletedSegments = Db::fetchAll($sql, array($dateStr)); + + if (empty($deletedSegments)) { + return array(); + } + + $existingSegments = $this->getExistingSegmentsLike($deletedSegments); + + foreach ($deletedSegments as $i => $deleted) { + $deletedSegments[$i]['idsites_to_preserve'] = array(); + foreach ($existingSegments as $existing) { + if ($existing['definition'] != $deleted['definition'] && + $existing['definition'] != urlencode($deleted['definition']) && + $existing['definition'] != urldecode($deleted['definition']) + ) { + continue; + } + + if ( + $existing['enable_only_idsite'] == $deleted['enable_only_idsite'] + || $existing['enable_only_idsite'] == 0 + ) { + // There is an identical segment (for either the specific site or for all sites) that is active + // The archives for this segment will therefore still be needed + unset($deletedSegments[$i]); + break; + } elseif ($deleted['enable_only_idsite'] == 0) { + // It is an all-sites segment that got deleted, but there is a single-site segment that is active + // Need to make sure we don't erase the segment's archives for that particular site + $deletedSegments[$i]['idsites_to_preserve'][] = $existing['enable_only_idsite']; + } + } + } + + return $deletedSegments; + } + + private function getExistingSegmentsLike(array $segments) + { + if (empty($segments)) { + return array(); + } + + $whereClauses = array(); + $bind = array(); + $definitionWhereClauseTemplate = '(definition = ? OR definition = ? OR definition = ?)'; + foreach ($segments as $segment) { + // Sometimes they are stored encoded and sometimes they aren't + $bind[] = $segment['definition']; + $bind[] = urlencode($segment['definition']); + $bind[] = urldecode($segment['definition']); + + if ($segment['enable_only_idsite'] == 0) { + // They deleted an all-sites segment, but there is a single-site segment with same definition? + // Need to handle this carefully so that the archives for the single-site segment are preserved + $whereClauses[] = "$definitionWhereClauseTemplate"; + } else { + $whereClauses[] = "($definitionWhereClauseTemplate AND (enable_only_idsite = ? OR enable_only_idsite = 0))"; + $bind[] = $segment['enable_only_idsite']; + } + } + $whereClauses = implode(' OR ', $whereClauses); + + // Check for any non-deleted segments with the same definition + $sql = "SELECT DISTINCT definition, enable_only_idsite FROM " . Common::prefixTable('segment') + . " WHERE deleted = 0 AND (" . $whereClauses . ")"; + return Db::fetchAll($sql, $bind); + } + public function deleteSegment($idSegment) { + $fieldsToSet = array( + 'deleted' => 1, + 'ts_last_edit' => Date::factory('now')->toString('Y-m-d H:i:s') + ); + $db = $this->getDb(); - $db->delete($this->getTable(), 'idsegment = ' . (int) $idSegment); + $db->update($this->getTable(), $fieldsToSet, 'idsegment = ' . (int) $idSegment); } public function updateSegment($idSegment, $segment) diff --git a/app/plugins/SegmentEditor/SegmentEditor.php b/app/plugins/SegmentEditor/SegmentEditor.php index c00e980c8..b60861427 100644 --- a/app/plugins/SegmentEditor/SegmentEditor.php +++ b/app/plugins/SegmentEditor/SegmentEditor.php @@ -11,6 +11,8 @@ use Piwik\API\Request; use Piwik\ArchiveProcessor\PluginsArchiver; use Piwik\ArchiveProcessor\Rules; +use Piwik\Cache; +use Piwik\CacheId; use Piwik\Common; use Piwik\Config; use Piwik\Container\StaticContainer; @@ -267,6 +269,7 @@ public function getClientSideTranslationKeys(&$translationKeys) $translationKeys[] = 'SegmentEditor_OperatorAND'; $translationKeys[] = 'SegmentEditor_OperatorOR'; $translationKeys[] = 'SegmentEditor_AddANDorORCondition'; + $translationKeys[] = 'SegmentEditor_DefaultAllVisits'; $translationKeys[] = 'General_OperationEquals'; $translationKeys[] = 'General_OperationNotEquals'; $translationKeys[] = 'General_OperationAtMost'; @@ -279,5 +282,26 @@ public function getClientSideTranslationKeys(&$translationKeys) $translationKeys[] = 'General_OperationDoesNotContain'; $translationKeys[] = 'General_OperationStartsWith'; $translationKeys[] = 'General_OperationEndsWith'; + $translationKeys[] = 'General_Unknown'; + $translationKeys[] = 'SegmentEditor_ThisSegmentIsCompared'; + $translationKeys[] = 'SegmentEditor_ThisSegmentIsSelectedAndCannotBeCompared'; + $translationKeys[] = 'SegmentEditor_CompareThisSegment'; + $translationKeys[] = 'Live_VisitsLog'; + } + + public static function getAllSegmentsForSite($idSite) + { + $cache = Cache::getTransientCache(); + $cacheKey = CacheId::siteAware('SegmentEditor_getAll', [$idSite]); + + $segments = $cache->fetch($cacheKey); + if (!is_array($segments)) { + $segments = Request::processRequest('SegmentEditor.getAll', ['idSite' => $idSite], $default = []); + usort($segments, function ($lhs, $rhs) { + return strcmp($lhs['name'], $rhs['name']); + }); + $cache->save($cacheKey, $segments); + } + return $segments; } } diff --git a/app/plugins/SegmentEditor/config/tracker.php b/app/plugins/SegmentEditor/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/SegmentEditor/config/tracker.php +++ b/app/plugins/SegmentEditor/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/ar.json b/app/plugins/SegmentEditor/lang/ar.json new file mode 100644 index 000000000..946d3e502 --- /dev/null +++ b/app/plugins/SegmentEditor/lang/ar.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "اختبار" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/be.json b/app/plugins/SegmentEditor/lang/be.json new file mode 100644 index 000000000..1909cb810 --- /dev/null +++ b/app/plugins/SegmentEditor/lang/be.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "Тэст" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/bg.json b/app/plugins/SegmentEditor/lang/bg.json index faf7f322f..44c1fc8d3 100644 --- a/app/plugins/SegmentEditor/lang/bg.json +++ b/app/plugins/SegmentEditor/lang/bg.json @@ -18,6 +18,7 @@ "ThisSegmentIsVisibleTo": "Този сегмент е видим за:", "VisibleToAllUsers": "всички потребители", "VisibleToMe": "аз", - "YouMustBeLoggedInToCreateSegments": "Трябва да сте вписани, за да създавате и редактирате персонализираните посетителски сегменти." + "YouMustBeLoggedInToCreateSegments": "Трябва да сте вписани, за да създавате и редактирате персонализираните посетителски сегменти.", + "Test": "Тест" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/ca.json b/app/plugins/SegmentEditor/lang/ca.json new file mode 100644 index 000000000..26470300a --- /dev/null +++ b/app/plugins/SegmentEditor/lang/ca.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "Prova" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/cs.json b/app/plugins/SegmentEditor/lang/cs.json index 1123ee450..80163290f 100644 --- a/app/plugins/SegmentEditor/lang/cs.json +++ b/app/plugins/SegmentEditor/lang/cs.json @@ -34,6 +34,7 @@ "SegmentXIsAUnionOf": "%s je propojení těchto segmentů:", "CustomSegment": "Vlastní segment", "SegmentOperatorIsNullOrEmpty": "je nulový nebo prázdný", - "SegmentOperatorIsNotNullNorEmpty": "není nulový nebo prázdný" + "SegmentOperatorIsNotNullNorEmpty": "není nulový nebo prázdný", + "Test": "Vyzkoušet" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/da.json b/app/plugins/SegmentEditor/lang/da.json index b3be3846a..9442b9b81 100644 --- a/app/plugins/SegmentEditor/lang/da.json +++ b/app/plugins/SegmentEditor/lang/da.json @@ -23,6 +23,7 @@ "VisibleToSuperUser": "Synlig for dig, fordi du har superbrugeradgang", "YouMustBeLoggedInToCreateSegments": "Du skal være logget ind for at oprette og anvende brugerdefinerede besøgssegmenter.", "YouDontHaveAccessToCreateSegments": "Du har ikke det nødvendige adgangsniveau til at oprette og redigere segmenter.", - "AddingSegmentForAllWebsitesDisabled": "Tilføjelse af segmenter for alle websteder er blevet deaktiveret." + "AddingSegmentForAllWebsitesDisabled": "Tilføjelse af segmenter for alle websteder er blevet deaktiveret.", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/de.json b/app/plugins/SegmentEditor/lang/de.json index b0749c3a2..c9f746a8d 100644 --- a/app/plugins/SegmentEditor/lang/de.json +++ b/app/plugins/SegmentEditor/lang/de.json @@ -52,6 +52,10 @@ "CustomUnprocessedSegmentApiError5": "Bitte beachten Sie dass Sie testen können ob Ihr Segment funktioniert ohne dass Sie auf die Verarbeitung warten müssen, in dem Sie die Live.getLastVisitsDetails API verwenden.", "CustomUnprocessedSegmentApiError6": "Wenn sie diese API Methode verwenden, werden Sie sehen welche Benutzer und Aktionen vom Ihrem &segment= Parameter betroffen sind.", "CustomUnprocessedSegmentNoData": "Um Daten für dieses Segment zu sehen, müssen Sie das Segment manuell im Segment Editor erstellen, dann ein paar Stunden auf die Verarbeitung warten.", - "AddThisToMatomo": "Dieses Segment zu Matomo hinzufügen" + "AddThisToMatomo": "Dieses Segment zu Matomo hinzufügen", + "ThisSegmentIsCompared": "Dieses Segment wird gerade verglichen.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Dieses Segment ist aktuell ausgewählt und kann deshalb nicht für einen Vergleich ausgewählt werden.", + "CompareThisSegment": "Vergleiche dieses Segment mit ausgewähltem Segment und Zeitraum.", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/el.json b/app/plugins/SegmentEditor/lang/el.json index 172409cb9..888dc19ae 100644 --- a/app/plugins/SegmentEditor/lang/el.json +++ b/app/plugins/SegmentEditor/lang/el.json @@ -52,6 +52,10 @@ "CustomUnprocessedSegmentApiError5": "Πρέπει να σημειωθεί ότι μπορείτε να δοκιμάσετε αν θα δουλεύει το τμήμα σας χωρίς να περιμένετε να γίνει επεξεργασία του με χρήση της μεθόδου Live.getLastVisitsDetails του API.", "CustomUnprocessedSegmentApiError6": "Όταν χρησιμοποιείτε αυτή τη μέθοδο του API, θα δείτε ποιοι χρήστες και ενέργειες βρέθηκαν από την παράμετρο &segment=.", "CustomUnprocessedSegmentNoData": "Για να δείτε δεδομένα για το τμήμα, θα πρέπει πρώτα να δημιουργήσετε ένα τμήμα χειροκίνητα στον Επεξεργαστή Τμημάτων και να περιμένετε λίγες ώρες για να ολοκληρωθεί η προ-επεξεργασία.", - "AddThisToMatomo": "Προσθήκη του τμήματος στο Matomo" + "AddThisToMatomo": "Προσθήκη του τμήματος στο Matomo", + "ThisSegmentIsCompared": "Αυτή τη στιγμή γίνεται σύγκριση του τμήματος.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Το τμήμα αυτό είναι αυτή τη στιγμή επιλεγμένο και δεν μπορεί να επιλεγεί για σύγκριση.", + "CompareThisSegment": "Σύγκριση του τμήματος με το επιλεγμένο τμήμα και περίοδο.", + "Test": "Δοκιμή" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/en.json b/app/plugins/SegmentEditor/lang/en.json index 19446db9a..5713dfbd7 100644 --- a/app/plugins/SegmentEditor/lang/en.json +++ b/app/plugins/SegmentEditor/lang/en.json @@ -52,6 +52,10 @@ "CustomUnprocessedSegmentApiError5": "Please note that you can test whether your segment will work without having to wait for it to be processed by using the Live.getLastVisitsDetails API.", "CustomUnprocessedSegmentApiError6": "When using this API method, you will see which users and actions were matched by your &segment= parameter.", "CustomUnprocessedSegmentNoData": "To see data for this segment, you must create this segment manually in the Segment Editor, then wait a couple hours for preprocessing to complete.", - "AddThisToMatomo": "Add this segment to Matomo" + "AddThisToMatomo": "Add this segment to Matomo", + "ThisSegmentIsCompared": "This segment is currently compared.", + "ThisSegmentIsSelectedAndCannotBeCompared": "This segment is currently selected so cannot be selected to compare.", + "CompareThisSegment": "Compare this segment with the selected segment and period.", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/es-ar.json b/app/plugins/SegmentEditor/lang/es-ar.json index f4d647baf..e7d86d4d4 100644 --- a/app/plugins/SegmentEditor/lang/es-ar.json +++ b/app/plugins/SegmentEditor/lang/es-ar.json @@ -1,5 +1,6 @@ { "SegmentEditor": { + "PluginDescription": "Un segmento es un conjunto de criterios usados para seleccionar sólo una parte del conjunto entero de visitas. Al usar segmentos, podés inyectar un contexto arbitrario a tus informes.", "AddANDorORCondition": "Agregar condición %s", "AddNewSegment": "Agregar nuevo segmento", "AreYouSureDeleteSegment": "¿Estás seguro que querés eliminar este segmento?", @@ -21,6 +22,7 @@ "SegmentDisplayedThisWebsiteOnly": "sólo este sitio web", "SegmentIsDisplayedForWebsite": "y procesado para", "SegmentNotApplied": "Segmento \"%s'\" no aplicado", + "SegmentNotAppliedMessage": "Estás solicitando datos para el segmento personalizado \"%s\". Esta configuración de Matomo actualmente evita procesamiento en tiempo real de informes por razones de rendimiento.", "SelectSegmentOfVisits": "Seleccionar un segmento de visitas:", "ThisSegmentIsVisibleTo": "Este segmento es visible para:", "VisibleToAllUsers": "todos los usuarios", @@ -34,6 +36,26 @@ "SegmentXIsAUnionOf": "%s es una unión de estos fragmentos:", "CustomSegment": "El segmento personalizado", "SegmentOperatorIsNullOrEmpty": "es nulo o vacío", - "SegmentOperatorIsNotNullNorEmpty": "no es nulo ni vacío" + "SegmentOperatorIsNotNullNorEmpty": "no es nulo ni vacío", + "UnprocessedSegmentNoData1": "Estos informes no tienen datos, porque el segmento que solicitaste %1$s todavía no fue procesado por el sistema.", + "UnprocessedSegmentNoData2": "Los datos para este segmento deberían estar disponibles en algunas horas, cuando el procesamiento se complete (si no lo hace, puede haber algún problema).", + "UnprocessedSegmentInVisitorLog1": "%1$sMientras tanto, podés usar el registro de visitas%2$s para probar si tu segmento coincidirá con tus usuarios correctamente al aplicarlo allí.", + "UnprocessedSegmentInVisitorLog2": "Al aplicarlo, podés ver inmediatamente qué visitas y acciones coincidieron con tu segmento.", + "UnprocessedSegmentInVisitorLog3": "Esto puede ayudarte a confirmar si tu segmento coincide con los usuarios y acciones que esperás.", + "UnprocessedSegmentApiError1": "El segmento \"%1$s\" está establecido a \"%2$s\" pero Matomo no está configurado actualmente para procesar informes segmentados en las solicitudes de API.", + "UnprocessedSegmentApiError2": "Para ver datos de este informe en el futuro, vas a necesitar editar tu segmento y elegir la opción etiquetada como \"%s\".", + "UnprocessedSegmentApiError3": "Luego de unas horas, los datos de tu segmento deberían estar disponibles a través de la API (si no lo está, puede haber algún problema).", + "CustomUnprocessedSegmentApiError1": "El segmento que solicitaste no fue creado en el editor de segmento y por lo tanto los datos del informe no fueron preprocesados.", + "CustomUnprocessedSegmentApiError2": "Para ver datos de este segmento, tenés que ir a Matomo y crear este segmento manualmente en el Editor de segmento.", + "CustomUnprocessedSegmentApiError3": "(Alternativamente, podés crear un nuevo segmento de forma programada usando el método de la API \"SegmentEditor.add\".)", + "CustomUnprocessedSegmentApiError4": "Una vez creado el segmento en el editor (o vía API), este mensaje de error desaparecerá y a las pocas horas vas a ver tus datos de informe segmentado, luego de que los datos de segmento hayan sido preprocesados (si no lo son, puede haber un problema).", + "CustomUnprocessedSegmentApiError5": "Por favor, tené en cuenta que podés probar si tu segmento funcionará sin tener que esperar a que sea procesado, usando la API \"Live.getLastVisitsDetails\".", + "CustomUnprocessedSegmentApiError6": "Al usar este método de API, vas a ver cuáles usuarios y acciones coincidieron con tu parámetro \"&segment=\".", + "CustomUnprocessedSegmentNoData": "Para ver datos para este segmento, tenér que crear este segmento manualmente en el Editor de segmento, luego esperar unas horas hasta que el proceso se complete.", + "AddThisToMatomo": "Agregar este segmento a Matomo", + "ThisSegmentIsCompared": "Actualmente se está comparando este segmento.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Actualmente se está comparando este segmento, por lo que no es necesario seleccionarlo para compararlo.", + "CompareThisSegment": "Comparar este segmento con el segmento y período seleccionados.", + "Test": "Prueba" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/es.json b/app/plugins/SegmentEditor/lang/es.json index c482e8458..d10312d34 100644 --- a/app/plugins/SegmentEditor/lang/es.json +++ b/app/plugins/SegmentEditor/lang/es.json @@ -52,6 +52,7 @@ "CustomUnprocessedSegmentApiError5": "Tenga en cuenta que puede comprobar si su segmento funcionará sin tener que esperar a que se procese, simplemente utilizando la API Live.getLastVisitsDetails.", "CustomUnprocessedSegmentApiError6": "Cuando utilice este método API, verá qué usuarios y acciones coincidieron con su parámetro &segment=.", "CustomUnprocessedSegmentNoData": "Para ver los datos de este segmento, debe crear este segmento manualmente en el Editor de segmentos y luego esperar un par de horas para que se complete el preprocesamiento.", - "AddThisToMatomo": "Agregar este segmento a Matomo" + "AddThisToMatomo": "Agregar este segmento a Matomo", + "Test": "Prueba" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/et.json b/app/plugins/SegmentEditor/lang/et.json index cbde4ae07..ee65b5bde 100644 --- a/app/plugins/SegmentEditor/lang/et.json +++ b/app/plugins/SegmentEditor/lang/et.json @@ -14,6 +14,7 @@ "SegmentDisplayedThisWebsiteOnly": "ainult see veebileht", "ThisSegmentIsVisibleTo": "Antud segment on nähtav:", "VisibleToAllUsers": "kõik kasutajad", - "VisibleToMe": "mina" + "VisibleToMe": "mina", + "Test": "Testi" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/eu.json b/app/plugins/SegmentEditor/lang/eu.json new file mode 100644 index 000000000..366673fa3 --- /dev/null +++ b/app/plugins/SegmentEditor/lang/eu.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "Probatu" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/fa.json b/app/plugins/SegmentEditor/lang/fa.json index 08c8a77a9..6c90d5242 100644 --- a/app/plugins/SegmentEditor/lang/fa.json +++ b/app/plugins/SegmentEditor/lang/fa.json @@ -17,6 +17,7 @@ "ThisSegmentIsVisibleTo": "این بخش قابل رؤیت است:", "VisibleToAllUsers": "تمام کاربران", "VisibleToMe": "من", - "YouMustBeLoggedInToCreateSegments": "شما باید برای ایجاد و ویرایش بخش های بازدید کننده سفارشی، وارد سیستم شوید." + "YouMustBeLoggedInToCreateSegments": "شما باید برای ایجاد و ویرایش بخش های بازدید کننده سفارشی، وارد سیستم شوید.", + "Test": "آزمایش" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/fi.json b/app/plugins/SegmentEditor/lang/fi.json index 7b9b41176..0488c71ab 100644 --- a/app/plugins/SegmentEditor/lang/fi.json +++ b/app/plugins/SegmentEditor/lang/fi.json @@ -30,6 +30,7 @@ "CustomSegment": "Kustomoidut segmentit", "SegmentOperatorIsNullOrEmpty": "on tyhjä", "SegmentOperatorIsNotNullNorEmpty": "ei ole tyhjä", - "AddThisToMatomo": "Lisää tämä segmentti Matomoon" + "AddThisToMatomo": "Lisää tämä segmentti Matomoon", + "Test": "Testi" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/fr.json b/app/plugins/SegmentEditor/lang/fr.json index ec5bab46f..845f2afee 100644 --- a/app/plugins/SegmentEditor/lang/fr.json +++ b/app/plugins/SegmentEditor/lang/fr.json @@ -52,6 +52,10 @@ "CustomUnprocessedSegmentApiError5": "Veuillez noter que vous pouvez tester le fonctionnement de votre segment sans avoir à attendre qu'il soit analysé en utilisant l'API Live.GetLastVisitsDetails.", "CustomUnprocessedSegmentApiError6": "Lorsque vous utilisez la méthode de l'API, vous verrez quels utilisateurs et actions ont été identifiées par votre paramètres &segment=.", "CustomUnprocessedSegmentNoData": "Afin de voir des données pour ce segment, vous devez le créer manuellement dans l'éditeur de segment, puis attendre quelques heures afin que la pré-analyse se termine.", - "AddThisToMatomo": "Ajouter ce segment à Matomo" + "AddThisToMatomo": "Ajouter ce segment à Matomo", + "ThisSegmentIsCompared": "Ce segment est actuellement comparé.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Ce segment est actuellement sélectionné et ne peut pas être sélectionné pour être comparé.", + "CompareThisSegment": "Comparer ce segment avec le segment et la période sélectionnés.", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/he.json b/app/plugins/SegmentEditor/lang/he.json index 0ff47c462..10ab76c68 100644 --- a/app/plugins/SegmentEditor/lang/he.json +++ b/app/plugins/SegmentEditor/lang/he.json @@ -1,6 +1,7 @@ { "SegmentEditor": { "DefaultAllVisits": "כל הביקורים", - "VisibleToAllUsers": "כל המשתמשים" + "VisibleToAllUsers": "כל המשתמשים", + "Test": "בדיקה" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/hi.json b/app/plugins/SegmentEditor/lang/hi.json index 2941b8a12..474c80835 100644 --- a/app/plugins/SegmentEditor/lang/hi.json +++ b/app/plugins/SegmentEditor/lang/hi.json @@ -19,6 +19,7 @@ "ThisSegmentIsVisibleTo": "इस खंड के लिए दिख रहा है:", "VisibleToAllUsers": "सभी उपयोगकर्ताओं", "VisibleToMe": "मुझे", - "YouMustBeLoggedInToCreateSegments": "आपको कस्टम आगंतुक खंड बनाने और संपादित करने के लिए लॉग इन करना होगा." + "YouMustBeLoggedInToCreateSegments": "आपको कस्टम आगंतुक खंड बनाने और संपादित करने के लिए लॉग इन करना होगा.", + "Test": "परीक्षण" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/hu.json b/app/plugins/SegmentEditor/lang/hu.json new file mode 100644 index 000000000..76a62c270 --- /dev/null +++ b/app/plugins/SegmentEditor/lang/hu.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "Teszt" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/id.json b/app/plugins/SegmentEditor/lang/id.json index 149ec43bb..5dd93f954 100644 --- a/app/plugins/SegmentEditor/lang/id.json +++ b/app/plugins/SegmentEditor/lang/id.json @@ -21,6 +21,7 @@ "VisibleToMe": "saya", "SharedWithYou": "Berbagi dengan Anda", "YouMustBeLoggedInToCreateSegments": "Anda harus masuk-log untuk membuat dan menyunting pecahan pengunjung kustom.", - "SegmentOperatorIsNotNullNorEmpty": "tidak boleh null atau kosong" + "SegmentOperatorIsNotNullNorEmpty": "tidak boleh null atau kosong", + "Test": "Percobaan" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/it.json b/app/plugins/SegmentEditor/lang/it.json index 1cca38486..78de221de 100644 --- a/app/plugins/SegmentEditor/lang/it.json +++ b/app/plugins/SegmentEditor/lang/it.json @@ -52,6 +52,7 @@ "CustomUnprocessedSegmentApiError5": "Tieni presente che puoi verificare se il tuo segmento funzionerà, senza dover attendere che venga elaborato, utilizzando l'API Live.getLastVisitsDetails.", "CustomUnprocessedSegmentApiError6": "Quando utilizzi questo metodo API, vedrai quali utenti e azioni hanno avuto corrispondenza dal tuo parametro &segment.", "CustomUnprocessedSegmentNoData": "Per visualizzare i dati di questo segmento, è necessario creare manualmente questo segmento nell'Editor dei Segmenti, quindi attendere un paio d'ore per il completamento della pre-elaborazione.", - "AddThisToMatomo": "Aggiungi a Matomo questo segmento" + "AddThisToMatomo": "Aggiungi a Matomo questo segmento", + "Test": "Testa" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/ja.json b/app/plugins/SegmentEditor/lang/ja.json index 246b7e469..05ae21a37 100644 --- a/app/plugins/SegmentEditor/lang/ja.json +++ b/app/plugins/SegmentEditor/lang/ja.json @@ -52,6 +52,7 @@ "CustomUnprocessedSegmentApiError5": "あなたのセグメントが Live.getLastVisitsDetails API を使用して処理されるのを待たずに動作するかどうかテストできます。", "CustomUnprocessedSegmentApiError6": "この API メソッドを使用すると、&segment= parameter で一致したユーザーとアクションが表示されます。", "CustomUnprocessedSegmentNoData": "このセグメントのデータを表示するには、セグメントエディタでこのセグメントを手動で作成し、前処理が完了するまで数時間待つ必要があります。", - "AddThisToMatomo": "このセグメントを Matomo に追加する" + "AddThisToMatomo": "このセグメントを Matomo に追加する", + "Test": "テスト" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/ka.json b/app/plugins/SegmentEditor/lang/ka.json new file mode 100644 index 000000000..1dcf5321a --- /dev/null +++ b/app/plugins/SegmentEditor/lang/ka.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "ტესტი" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/ko.json b/app/plugins/SegmentEditor/lang/ko.json new file mode 100644 index 000000000..4661e1ee8 --- /dev/null +++ b/app/plugins/SegmentEditor/lang/ko.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "테스트" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/lt.json b/app/plugins/SegmentEditor/lang/lt.json index bd373755e..e4436eeea 100644 --- a/app/plugins/SegmentEditor/lang/lt.json +++ b/app/plugins/SegmentEditor/lang/lt.json @@ -1,5 +1,6 @@ { "SegmentEditor": { - "OperatorAND": "IR" + "OperatorAND": "IR", + "Test": "Testas" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/lv.json b/app/plugins/SegmentEditor/lang/lv.json new file mode 100644 index 000000000..e3aec0d25 --- /dev/null +++ b/app/plugins/SegmentEditor/lang/lv.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "Testēt" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/nb.json b/app/plugins/SegmentEditor/lang/nb.json index 51e6751a1..f851f839f 100644 --- a/app/plugins/SegmentEditor/lang/nb.json +++ b/app/plugins/SegmentEditor/lang/nb.json @@ -11,6 +11,7 @@ "ThisSegmentIsVisibleTo": "Dette segmentet er synlig for:", "VisibleToAllUsers": "alle brukere", "VisibleToMe": "meg", - "SharedWithYou": "Delt med deg" + "SharedWithYou": "Delt med deg", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/nl.json b/app/plugins/SegmentEditor/lang/nl.json index 5995aa774..28f38a9d0 100644 --- a/app/plugins/SegmentEditor/lang/nl.json +++ b/app/plugins/SegmentEditor/lang/nl.json @@ -1,5 +1,6 @@ { "SegmentEditor": { + "PluginDescription": "Een segment is een set criteria die wordt gebruikt om slechts een deel van de hele set bezoeken te selecteren. Met behulp van segmenten kan je willekeurige context terug in uw rapporten injecteren.", "AddANDorORCondition": "%s-voorwaarde toevoegen", "AddNewSegment": "Nieuw segment toevoegen", "AreYouSureDeleteSegment": "Weet u zeker dat u dit segment wilt verwijderen?", @@ -35,6 +36,14 @@ "CustomSegment": "Aangepast segment", "SegmentOperatorIsNullOrEmpty": "is null of leeg", "SegmentOperatorIsNotNullNorEmpty": "is niet null of leeg", - "UnprocessedSegmentNoData1": "Deze rapporten hebben geen data omdat het opgevraagde segment %1$s nog niet is verwerkt door het systeem." + "UnprocessedSegmentNoData1": "Deze rapporten hebben geen data omdat het opgevraagde segment %1$s nog niet is verwerkt door het systeem.", + "UnprocessedSegmentNoData2": "Gegevens voor dit segment zouden binnen enkele uren beschikbaar moeten zijn wanneer de verwerking is voltooid. (Als dit niet het geval is, is er mogelijk een probleem.)", + "UnprocessedSegmentInVisitorLog2": "Wanneer toegepast, kan je direct zien welke bezoeken en acties zijn gekoppeld aan uw segment.", + "UnprocessedSegmentInVisitorLog3": "Dit kan helpen te bevestigen dat het segment overeenkomt met de gebruikers en acties die ervan verwacht worden.", + "CustomUnprocessedSegmentApiError6": "Wanneer deze API-methode wordt gebruikt, zie je welke gebruikers en acties zijn gekoppeld aan uw & segment = parameter.", + "AddThisToMatomo": "Voeg dit segment toe aan Matomo", + "ThisSegmentIsCompared": "Dit segment wordt momenteel vergeleken.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Dit segment is momenteel geselecteerd en kan niet geselecteerd worden om te vergelijken.", + "Test": "Testen" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/nn.json b/app/plugins/SegmentEditor/lang/nn.json new file mode 100644 index 000000000..9d76db713 --- /dev/null +++ b/app/plugins/SegmentEditor/lang/nn.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "Test" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/pl.json b/app/plugins/SegmentEditor/lang/pl.json index 555ba43d3..58e19ac76 100644 --- a/app/plugins/SegmentEditor/lang/pl.json +++ b/app/plugins/SegmentEditor/lang/pl.json @@ -34,6 +34,7 @@ "SegmentXIsAUnionOf": "%s jest złączeniem tych grup:", "CustomSegment": "Grupa użytkownika", "SegmentOperatorIsNullOrEmpty": "nie istnieje lub puste", - "SegmentOperatorIsNotNullNorEmpty": "istnieje lub nie jest puste" + "SegmentOperatorIsNotNullNorEmpty": "istnieje lub nie jest puste", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/pt-br.json b/app/plugins/SegmentEditor/lang/pt-br.json index 43bf67a62..1ebfbf5e1 100644 --- a/app/plugins/SegmentEditor/lang/pt-br.json +++ b/app/plugins/SegmentEditor/lang/pt-br.json @@ -1,10 +1,11 @@ { "SegmentEditor": { + "PluginDescription": "Um segmento é um conjunto de critérios usados para selecionar apenas uma parte de todo o conjunto de visitas. Ao usar segmentos você pode inserir contexto de volta aos seus relatórios.", "AddANDorORCondition": "Adicionar a condição %s", "AddNewSegment": "Adicionar novo segmento", - "AreYouSureDeleteSegment": "Confirma a exclusão deste segmento?", - "AutoArchivePreProcessed": "Relatórios segmentados são pré-processados ​​(Para agilizar, é necessário usar o cron em: archive.php)", - "AutoArchiveRealTime": "Relatórios segmentados são processados em tempo real", + "AreYouSureDeleteSegment": "Você tem certeza de que deseja excluir este segmento?", + "AutoArchivePreProcessed": "relatórios segmentados são pré-processados ​​(mais rápido, requer usar cron)", + "AutoArchiveRealTime": "relatórios segmentados são processados em tempo real", "ChangingSegmentDefinitionConfirmationNotProcessedOnRequest": "Você está prestes a mudar a definição do segmento. Os relatórios analíticos para este novo segmento não estarão disponíveis até que os relatórios sejam reprocessados. Pode levar algumas horas para que os dados de relatórios mostrem para esse segmento. Continuar mesmo assim?", "ChangingSegmentDefinitionConfirmationProcessedOnRequest": "Você está prestes a mudar a definição do segmento. Os relatórios analíticos para este novo segmento serão reprocessados sob demanda na próxima vez que solicitá-los. Seus relatórios pode demorar alguns minutos para aparecer. Continuar mesmo assim?", "ChooseASegment": "Escolha um segmento", @@ -13,27 +14,48 @@ "DefaultAllVisits": "Todas as visitas", "DragDropCondition": "Condição Drag & Drop", "HideMessageInFuture": "Ocultar esta mensagem no futuro", - "LoadingSegmentedDataMayTakeSomeTime": "O processamento segmentado de dados de visitantes pode demorar alguns mitutos...", - "OperatorAND": "e", - "OperatorOR": "ou", + "LoadingSegmentedDataMayTakeSomeTime": "O processamento segmentado de dados de visitantes pode demorar alguns minutos...", + "OperatorAND": "E", + "OperatorOR": "OU", "SaveAndApply": "Salvar e Aplicar", - "SegmentDisplayedAllWebsites": "Todos os Sites", - "SegmentDisplayedThisWebsiteOnly": "Somente neste website", + "SegmentDisplayedAllWebsites": "todos os sites", + "SegmentDisplayedThisWebsiteOnly": "somente neste website", "SegmentIsDisplayedForWebsite": "e processado ​​para", "SegmentNotApplied": "Segmento '%s' não aplicado", + "SegmentNotAppliedMessage": "Você está solicitando dados para o Segmento Customizado '%s', esta configuração do Matomo atualmente não permite o processamento de relatórios em tempo real por questão de performance.", "SelectSegmentOfVisits": "Selecione um segmento de visitas:", "ThisSegmentIsVisibleTo": "Este segmento é visível para:", - "VisibleToAllUsers": "Todos os Usuários", - "VisibleToMe": "mim", - "YouMayChangeSetting": "Alternativamente, você pode alterar a configuração no arquivo de configuração (%1$s), ou editar este Segmento e escolher %2$s.", + "VisibleToAllUsers": "todos os usuários", + "VisibleToMe": "eu", + "YouMayChangeSetting": "Alternativamente, você pode alterar a configuração no arquivo de configuração (%1$s), ou editar este Segmento e escolher '%2$s'.", "VisibleToSuperUser": "Visível pra você porque você tem acesso Super Usuário", "SharedWithYou": "Compartilhado com você", "YouMustBeLoggedInToCreateSegments": "Você precisa estar logado para criar e editar segmentos personalizados de visitantes.", "YouDontHaveAccessToCreateSegments": "Você não tem o nível de acesso necessário para criar e editar segmentos.", "AddingSegmentForAllWebsitesDisabled": "Adicionar segmentos para todos os sites foi desativado.", "SegmentXIsAUnionOf": "%s é uma união destes segmentos:", - "CustomSegment": "Segmento Personalizado", + "CustomSegment": "Segmento personalizado", "SegmentOperatorIsNullOrEmpty": "está nulo ou vazio", - "SegmentOperatorIsNotNullNorEmpty": "não está nulo nem vazio" + "SegmentOperatorIsNotNullNorEmpty": "não está nulo nem vazio", + "UnprocessedSegmentNoData1": "Estes relatórios não possuem dados pois o Segmento que você solicitou %1$s ainda não foi processado pelo sistema.", + "UnprocessedSegmentNoData2": "Dados para este Segmento devem estar disponíveis em algumas horas quando o processamento terminar. (Se não ficarem disponíveis, pode haver um problema.)", + "UnprocessedSegmentInVisitorLog1": "%1$sEnquanto isso você pode usar o Log de Visitantes%2$s para testar se o seu segmento irá coincidir corretamente com seus usuários ao aplicá-lo lá.", + "UnprocessedSegmentInVisitorLog2": "Quando aplicado, você pode ver imediatamente quais visitas e ações coincidiram com o seu segmento.", + "UnprocessedSegmentInVisitorLog3": "Isto pode te ajudar a confirmar que seu Segmento coincide com os usuários e ações que você espera.", + "UnprocessedSegmentApiError1": "O Segmento '%1$s' está definido como '%2$s' mas o Matomo atualmente não está configurado para processar relatórios segmentados em solicitações de API.", + "UnprocessedSegmentApiError2": "Para ver dados deste relatório no futuro, você precisará editar o seu segmento e escolher a opção '%s'.", + "UnprocessedSegmentApiError3": "Então depois de algumas horas os dados do seu segmento devem estar disponíveis através da API. (Se não estiverem, deve ter ocorrido um problema.)", + "CustomUnprocessedSegmentApiError1": "O segmento que você solicitou ainda não foi criado no Editor de Segmento e portanto os dados do relatório não foram processados.", + "CustomUnprocessedSegmentApiError2": "Para ver dados para este segmento você deve ir ao Matomo e criar manualmente este segmento no Editor de Segmento.", + "CustomUnprocessedSegmentApiError3": "(Como alternativa, você pode criar um novo segmento programaticamente usando o método SegmentEditor.add da API).", + "CustomUnprocessedSegmentApiError4": "Uma vez criado o segmento no editor (ou via API), esta mensagem de erro irá desaparecer e dentro de algumas horas você verá os dados segmentados do seu relatório, após os dados segmentados terem sido pré-processados. (Se não acontecer, deve ter ocorrido um problema.)", + "CustomUnprocessedSegmentApiError5": "Por favor, note que você pode testar se o seu segmento irá funcionar, sem ter que esperar que ele seja processado, usando o método Live.getLastVisitsDetails da API.", + "CustomUnprocessedSegmentApiError6": "Ao usar este método da API, você verá quais usuários e ações coincidiram com o seu parâmetro &segment=.", + "CustomUnprocessedSegmentNoData": "Para ver dados para este segmento, você deve criar este segmento manualmente no Editor de Segmento, então esperar algumas horas para que o pré-processamento finalize.", + "AddThisToMatomo": "Adicionar este segmento ao Matomo", + "ThisSegmentIsCompared": "Este segmento está atualmente comparado.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Este segmento está atualmente selecionado e então não pode ser escolhido para comparação.", + "CompareThisSegment": "Comparar este segmento com o segmento e período selecionados.", + "Test": "Teste" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/pt.json b/app/plugins/SegmentEditor/lang/pt.json index edc29b2af..089961c6c 100644 --- a/app/plugins/SegmentEditor/lang/pt.json +++ b/app/plugins/SegmentEditor/lang/pt.json @@ -1,5 +1,6 @@ { "SegmentEditor": { + "PluginDescription": "Um segmento é um conjunto de critérios utilizados para selecionar apenas uma parte do conjunto completo de visitas. Ao utilizar segmentos, pode injetar contextos arbitrários nos seus relatórios.", "AddANDorORCondition": "Adicionar condição %s", "AddNewSegment": "Adicionar novo segmento", "AreYouSureDeleteSegment": "Tem a certeza de que pretende eliminar este segmento?", @@ -50,6 +51,11 @@ "CustomUnprocessedSegmentApiError4": "Depois de criar o segmento no editor (ou via a API), esta mensagem de erro irá desaparecer e, no espaço de algumas horas, irá ver o relatório de dados segmentados depois do segmento de dados ser pré-processado. (Se isto não acontecer, pode existir um problema.)", "CustomUnprocessedSegmentApiError5": "Por favor, note que, pode testar se o seu segmento vai funcionar sem ter de aguardar que o mesmo seja processado, utilizando a API Live.getLastVisitsDetails.", "CustomUnprocessedSegmentApiError6": "Quando utiliza este método da API, irá ver que utilizadores e ações coincidiram com o seu parâmetro &segment=.", - "CustomUnprocessedSegmentNoData": "Para ver dados para este segmento, deve criar este segmento manualmente no editor de segmentos, depois aguardar algumas horas até o pré-processamento terminar." + "CustomUnprocessedSegmentNoData": "Para ver dados para este segmento, deve criar este segmento manualmente no editor de segmentos, depois aguardar algumas horas até o pré-processamento terminar.", + "AddThisToMatomo": "Adicionar este segmento ao Matomo", + "ThisSegmentIsCompared": "Este segmentos está atualmente a ser comparado.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Este segmento está atualmente selecionado pelo que não pode ser selecionado para a comparação.", + "CompareThisSegment": "Comparar este segmento com o segmento e período selecionado.", + "Test": "Teste" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/ro.json b/app/plugins/SegmentEditor/lang/ro.json index a948c7d0c..ba7943d1d 100644 --- a/app/plugins/SegmentEditor/lang/ro.json +++ b/app/plugins/SegmentEditor/lang/ro.json @@ -21,6 +21,7 @@ "VisibleToMe": "mie", "YouMayChangeSetting": "Alternativ, puteți schimba setările în fișierul de configurare (%1$s), sau pentru a edita acest segment și alegeți '%2$s'.", "YouMustBeLoggedInToCreateSegments": "Trebuie să fii logat pentru a crea și edita segmente de vizitatori personalizate.", - "YouDontHaveAccessToCreateSegments": "Nu ai nivelul de acces necesar pentru a crea și edita segmente." + "YouDontHaveAccessToCreateSegments": "Nu ai nivelul de acces necesar pentru a crea și edita segmente.", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/ru.json b/app/plugins/SegmentEditor/lang/ru.json index 584c06682..77991bfbd 100644 --- a/app/plugins/SegmentEditor/lang/ru.json +++ b/app/plugins/SegmentEditor/lang/ru.json @@ -15,6 +15,7 @@ "ThisSegmentIsVisibleTo": "Этот сегмент видим для:", "VisibleToAllUsers": "все пользователи", "VisibleToMe": "меня", - "AddingSegmentForAllWebsitesDisabled": "Добавление сегментов для всех веб-сайтов было отключено." + "AddingSegmentForAllWebsitesDisabled": "Добавление сегментов для всех веб-сайтов было отключено.", + "Test": "Тест" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/sk.json b/app/plugins/SegmentEditor/lang/sk.json index fd149516e..24a8780c1 100644 --- a/app/plugins/SegmentEditor/lang/sk.json +++ b/app/plugins/SegmentEditor/lang/sk.json @@ -1,6 +1,7 @@ { "SegmentEditor": { "AddNewSegment": "Pridať nový segment", - "DefaultAllVisits": "Všetky návštevy" + "DefaultAllVisits": "Všetky návštevy", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/sl.json b/app/plugins/SegmentEditor/lang/sl.json index 0be062058..8c5969b98 100644 --- a/app/plugins/SegmentEditor/lang/sl.json +++ b/app/plugins/SegmentEditor/lang/sl.json @@ -1,5 +1,6 @@ { "SegmentEditor": { - "DefaultAllVisits": "Vsi obiski" + "DefaultAllVisits": "Vsi obiski", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/sq.json b/app/plugins/SegmentEditor/lang/sq.json index 4e5f6cd5c..099fefc3c 100644 --- a/app/plugins/SegmentEditor/lang/sq.json +++ b/app/plugins/SegmentEditor/lang/sq.json @@ -52,6 +52,10 @@ "CustomUnprocessedSegmentApiError5": "Ju lutemi, mbani parasysh që mund të testoni nëse segmenti juaj do të funksionojë apo jo, pa u dashur të pritni që ai të përpunohet, duke përdorur API-n Live.getLastVisitsDetails.", "CustomUnprocessedSegmentApiError6": "Kur përdoret kjo metodë API, do të shihni se cilët përdorues dhe veprime patën përputhje me &segment= parameter tuaj.", "CustomUnprocessedSegmentNoData": "Që të shihni të dhëna për këtë segment, duhet ta krijoni këtë segment dorazi te Përpunues Segmentesh, dhe mandej të pritni dy-tre orë që të plotësohet parapërpunimi.", - "AddThisToMatomo": "Shtoje këtë segment te Matomo" + "AddThisToMatomo": "Shtoje këtë segment te Matomo", + "ThisSegmentIsCompared": "Ky segment po krahasohet tani.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Ky segment është aktualisht i përzgjedhur, ndaj s’mund të përzgjidhet për krahasim.", + "CompareThisSegment": "Krahasojeni këtë segment me segmentin dhe periudhën e përzgjedhur.", + "Test": "Provë" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/sr.json b/app/plugins/SegmentEditor/lang/sr.json index bb85a1b65..dcec54142 100644 --- a/app/plugins/SegmentEditor/lang/sr.json +++ b/app/plugins/SegmentEditor/lang/sr.json @@ -34,6 +34,7 @@ "SegmentXIsAUnionOf": "%s je unija ovih segmenata:", "CustomSegment": "Korisnički definisan segment", "SegmentOperatorIsNullOrEmpty": "je prazan ili nije definisan", - "SegmentOperatorIsNotNullNorEmpty": "niti je prazan niti je nedefinisan" + "SegmentOperatorIsNotNullNorEmpty": "niti je prazan niti je nedefinisan", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/sv.json b/app/plugins/SegmentEditor/lang/sv.json index 4c5da0ca8..5376bc41f 100644 --- a/app/plugins/SegmentEditor/lang/sv.json +++ b/app/plugins/SegmentEditor/lang/sv.json @@ -50,6 +50,7 @@ "CustomUnprocessedSegmentApiError4": "När du skapat segmentet i redigeraren (eller via API), kommer detta felmeddelande att försvinna och inom några timmar ser du din segmenterade rapportdata efter segmentdata har bearbetats. (Om det inte gör det kan ett fel ha uppstått.)", "CustomUnprocessedSegmentApiError5": "Observera att du kan testa om ditt segment kommer fungera utan att behöva vänta på att det ska behandlas med hjälp av Live.getLastVisitsDetails API.", "CustomUnprocessedSegmentApiError6": "När du använder den här API-metoden ser du vilka användare och åtgärder som matchades av din &segment=parameter.", - "CustomUnprocessedSegmentNoData": "Om du vill se data för det här segmentet måste du skapa det här segmentet manuellt i Segmentredigeraren och därefter vänta ett par timmar innan bearbetningen har slutförts." + "CustomUnprocessedSegmentNoData": "Om du vill se data för det här segmentet måste du skapa det här segmentet manuellt i Segmentredigeraren och därefter vänta ett par timmar innan bearbetningen har slutförts.", + "Test": "Test" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/th.json b/app/plugins/SegmentEditor/lang/th.json new file mode 100644 index 000000000..bb2442e25 --- /dev/null +++ b/app/plugins/SegmentEditor/lang/th.json @@ -0,0 +1,5 @@ +{ + "SegmentEditor": { + "Test": "คำสั่ง" + } +} \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/tl.json b/app/plugins/SegmentEditor/lang/tl.json index 7933c0fe3..728d4fc5c 100644 --- a/app/plugins/SegmentEditor/lang/tl.json +++ b/app/plugins/SegmentEditor/lang/tl.json @@ -21,6 +21,7 @@ "YouMayChangeSetting": "Maaari mo ring baguhin ang settings sa config file (%1$s) o i-edit ang mga Segment at piliin ang '%2$s'.", "YouMustBeLoggedInToCreateSegments": "Kailangan mong mag log-in upang gumawa at mag-edit ng custom visitor segments.", "YouDontHaveAccessToCreateSegments": "Wala kang mga kinakailangang access level upang lumikha at mag edit ng mga segment.", - "AddingSegmentForAllWebsitesDisabled": "Ang pagdagdag ng bahagi para sa lahat ng website ay hindi na pinagana." + "AddingSegmentForAllWebsitesDisabled": "Ang pagdagdag ng bahagi para sa lahat ng website ay hindi na pinagana.", + "Test": "Pagsusulit" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/tr.json b/app/plugins/SegmentEditor/lang/tr.json index 30ee40c19..7c66b2072 100644 --- a/app/plugins/SegmentEditor/lang/tr.json +++ b/app/plugins/SegmentEditor/lang/tr.json @@ -52,6 +52,10 @@ "CustomUnprocessedSegmentApiError5": "Live.getLastVisitsDetails API yöntemini kullanarak verilerin ön hazırlığının tamamlanmasını beklemeden dilimin çalışıp çalışmadığını sınayabileceğinizi unutmayın.", "CustomUnprocessedSegmentApiError6": "Bu API yöntemini kullanarak, &segment= parametre ölçütünüze uyan kullanıcı ve işlemleri görebilirsiniz.", "CustomUnprocessedSegmentNoData": "Bu dilimin verilerini görüntülemek için, Dilim Düzenleyici üzerinden bu dilimi el ile ekledikten sonra verilerin hazırlanması için bir kaç saat beklemelisiniz.", - "AddThisToMatomo": "Bu parçayı Matomo üzerine ekle" + "AddThisToMatomo": "Bu parçayı Matomo üzerine ekle", + "ThisSegmentIsCompared": "Bu dilim şu anda karşılaştırılıyor.", + "ThisSegmentIsSelectedAndCannotBeCompared": "Bu dilim şu anda seçilmiş olduğundan karşılaştırmak için seçilemez.", + "CompareThisSegment": "Bu dilimi seçilmiş dilim ve aralık ile karşılaştır.", + "Test": "Sına" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/uk.json b/app/plugins/SegmentEditor/lang/uk.json index 39763abd2..b01a5cea8 100644 --- a/app/plugins/SegmentEditor/lang/uk.json +++ b/app/plugins/SegmentEditor/lang/uk.json @@ -34,6 +34,7 @@ "SegmentXIsAUnionOf": "%s є об'єднанням цих сегментів:", "CustomSegment": "Користувальницький Сегмент", "SegmentOperatorIsNullOrEmpty": "є нульовим або порожнім", - "SegmentOperatorIsNotNullNorEmpty": "не нульовий або порожній" + "SegmentOperatorIsNotNullNorEmpty": "не нульовий або порожній", + "Test": "Тест" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/vi.json b/app/plugins/SegmentEditor/lang/vi.json index d94eb8f35..33d46dc23 100644 --- a/app/plugins/SegmentEditor/lang/vi.json +++ b/app/plugins/SegmentEditor/lang/vi.json @@ -17,6 +17,7 @@ "ThisSegmentIsVisibleTo": "Phân đoạn này có thể nhìn thấy:", "VisibleToAllUsers": "Tất cả người dùng", "VisibleToMe": "Tôi", - "YouMustBeLoggedInToCreateSegments": "Bạn phải đăng nhập để tạo và chỉnh sửa các phân đoạn khách hàng." + "YouMustBeLoggedInToCreateSegments": "Bạn phải đăng nhập để tạo và chỉnh sửa các phân đoạn khách hàng.", + "Test": "Thử nghiệm" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/zh-cn.json b/app/plugins/SegmentEditor/lang/zh-cn.json index bcba0173e..28aad1515 100644 --- a/app/plugins/SegmentEditor/lang/zh-cn.json +++ b/app/plugins/SegmentEditor/lang/zh-cn.json @@ -18,6 +18,7 @@ "ThisSegmentIsVisibleTo": "这个分段对其可见:", "VisibleToAllUsers": "所有用户", "VisibleToMe": "我", - "YouMustBeLoggedInToCreateSegments": "您必须登录后创建和修改自定义访客分段" + "YouMustBeLoggedInToCreateSegments": "您必须登录后创建和修改自定义访客分段", + "Test": "测试" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/lang/zh-tw.json b/app/plugins/SegmentEditor/lang/zh-tw.json index 238e74fc7..83ae81561 100644 --- a/app/plugins/SegmentEditor/lang/zh-tw.json +++ b/app/plugins/SegmentEditor/lang/zh-tw.json @@ -34,6 +34,7 @@ "SegmentXIsAUnionOf": "%s 是這些區隔的聯合:", "CustomSegment": "自訂區隔", "SegmentOperatorIsNullOrEmpty": "是無效或空值", - "SegmentOperatorIsNotNullNorEmpty": "不是無效或空值" + "SegmentOperatorIsNotNullNorEmpty": "不是無效或空值", + "Test": "測試" } } \ No newline at end of file diff --git a/app/plugins/SegmentEditor/stylesheets/segmentation.less b/app/plugins/SegmentEditor/stylesheets/segmentation.less index 8f3d973f9..a27e24fb0 100644 --- a/app/plugins/SegmentEditor/stylesheets/segmentation.less +++ b/app/plugins/SegmentEditor/stylesheets/segmentation.less @@ -206,9 +206,16 @@ div.scrollable { cursor: pointer; } -.segmentationContainer .submenu ul li:hover { +.segmentationContainer .submenu ul li:hover, +.segmentationContainer .submenu ul li:focus, +.segmentationContainer .submenu ul li:focus-within { color: #255792; background: @color-silver-l95; + outline: none; + + > * { + outline: none; + } } .segmentationContainer ul.submenu { @@ -217,19 +224,46 @@ div.scrollable { margin-bottom: 5px; } -.segmentationContainer ul.submenu > li span.editSegment { - display: block; - float: right; - text-align: center; - margin-right: 4px; - font-weight: normal; - background: url(plugins/SegmentEditor/images/edit_segment.png) no-repeat; - width: 16px; - height: 16px; - .opacity(0.5); +.segmentationContainer ul.submenu > li { + span.editSegment, span.compareSegment { + display: block; + float: right; + text-align: center; + margin-right: 4px; + font-weight: normal; + width: 16px; + height: 16px; + .opacity(0.5); + + &:hover { + .opacity(1); + } + } + + span.editSegment { + background: url(plugins/SegmentEditor/images/edit_segment.png) no-repeat; + } + + span.compareSegment { + background: url(plugins/Morpheus/images/compare.svg) no-repeat; + background-size: cover; + + &.allVisitsCompareSegment { + margin-right: 24px; + } + } + + li.segmentSelected, li.comparedSegment { + span.compareSegment { + pointer-events: none; + opacity: 0.2; + } + } +} - &:hover { - .opacity(1); +html.comparisonsDisabled .segmentationContainer ul.submenu { + span.compareSegment { + display: none; } } @@ -384,7 +418,11 @@ a.metric_category { margin: 8px; } -.segmentSelected, .segmentSelected:hover, .segmentEditorPanel .segmentationContainer .submenu li .segmentSelected { +.segmentSelected, +.segmentSelected:hover, +.segmentEditorPanel .segmentationContainer .submenu li .segmentSelected, +.segmentEditorPanel .segmentationContainer .submenu li:focus, +.segmentEditorPanel .segmentationContainer .submenu li:focus-within { font-weight: bold; } diff --git a/app/plugins/SegmentEditor/templates/_segmentSelector.twig b/app/plugins/SegmentEditor/templates/_segmentSelector.twig index befd058c1..f4cbfc837 100644 --- a/app/plugins/SegmentEditor/templates/_segmentSelector.twig +++ b/app/plugins/SegmentEditor/templates/_segmentSelector.twig @@ -70,6 +70,7 @@
{{ 'General_Delete'|translate }} {{ 'General_Close'|translate }} + {{ 'SegmentEditor_Test'|translate }} @@ -99,10 +100,10 @@
-

{{ 'SegmentEditor_SegmentNotApplied'|translate(nameOfCurrentSegment)|raw }}

+

{{ 'SegmentEditor_SegmentNotApplied'|translate(nameOfCurrentSegment)|rawSafeDecoded|raw }}

{% set segmentSetting %}{{ 'SegmentEditor_AutoArchivePreProcessed'|translate }}{% endset %}

- {{ 'SegmentEditor_SegmentNotAppliedMessage'|translate(nameOfCurrentSegment)|raw }} + {{ 'SegmentEditor_SegmentNotAppliedMessage'|translate(nameOfCurrentSegment)|rawSafeDecoded|raw }}
{{ 'SegmentEditor_DataAvailableAtLaterDate'|translate }} {% if isSuperUser %} diff --git a/app/plugins/SitesManager/API.php b/app/plugins/SitesManager/API.php index b8da10cf3..909d694f2 100644 --- a/app/plugins/SitesManager/API.php +++ b/app/plugins/SitesManager/API.php @@ -615,6 +615,7 @@ public function addSite($siteName, $excludeUnknownUrls = null) { Piwik::checkUserHasSuperUserAccess(); + SitesManager::dieIfSitesAdminIsDisabled(); $this->checkName($siteName); @@ -795,6 +796,7 @@ private function postUpdateWebsite($idSite) public function deleteSite($idSite) { Piwik::checkUserHasSuperUserAccess(); + SitesManager::dieIfSitesAdminIsDisabled(); $idSites = $this->getSitesId(); if (!in_array($idSite, $idSites)) { @@ -1255,6 +1257,7 @@ public function updateSite($idSite, $excludeUnknownUrls = null) { Piwik::checkUserHasAdminAccess($idSite); + SitesManager::dieIfSitesAdminIsDisabled(); $idSites = $this->getSitesId(); diff --git a/app/plugins/SitesManager/Controller.php b/app/plugins/SitesManager/Controller.php index f30a53eb0..6d651e932 100644 --- a/app/plugins/SitesManager/Controller.php +++ b/app/plugins/SitesManager/Controller.php @@ -14,6 +14,7 @@ use Piwik\Common; use Piwik\Exception\UnexpectedWebsiteFoundException; use Piwik\Piwik; +use Piwik\Plugin\Manager; use Piwik\Session; use Piwik\Settings\Measurable\MeasurableSettings; use Piwik\SettingsPiwik; @@ -33,6 +34,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin public function index() { Piwik::checkUserHasSomeAdminAccess(); + SitesManager::dieIfSitesAdminIsDisabled(); return $this->renderTemplate('index'); } @@ -138,11 +140,46 @@ public function siteWithoutData() Piwik::checkUserHasViewAccess($this->idSite); } + $jsTag = Request::processRequest('SitesManager.getJavascriptTag', array('idSite' => $this->idSite, 'piwikUrl' => $piwikUrl)); + + // Strip off open and close {% endif %} -

\ No newline at end of file + diff --git a/app/plugins/Transitions/API.php b/app/plugins/Transitions/API.php index 4c57d2722..88bca868e 100644 --- a/app/plugins/Transitions/API.php +++ b/app/plugins/Transitions/API.php @@ -271,6 +271,7 @@ protected function queryFollowingActions($idaction, $actionType, LogAggregator $ $types[Action::TYPE_DOWNLOAD] = 'downloads'; $rankingQuery = new RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + $rankingQuery->setOthersLabel('Others'); $rankingQuery->addLabelColumn(array('name', 'url_prefix')); $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($types)); @@ -282,7 +283,15 @@ protected function queryFollowingActions($idaction, $actionType, LogAggregator $ } $metrics = array(Metrics::INDEX_NB_ACTIONS); - $data = $logAggregator->queryActionsByDimension(array($dimension), $where, $selects, $metrics, $rankingQuery, $joinLogActionColumn); + $data = $logAggregator->queryActionsByDimension( + array($dimension), + $where, + $selects, + $metrics, + $rankingQuery, + $joinLogActionColumn, + $secondaryOrderBy = "`name`" + ); $dataTables = $this->makeDataTablesFollowingActions($types, $data); @@ -301,6 +310,7 @@ protected function queryFollowingActions($idaction, $actionType, LogAggregator $ protected function queryExternalReferrers($idaction, $actionType, $logAggregator, $limitBeforeGrouping = false) { $rankingQuery = new RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + $rankingQuery->setOthersLabel('Others'); // we generate a single column that contains the interesting data for each referrer. // the reason we cannot group by referer_* becomes clear when we look at search engine keywords. @@ -379,6 +389,7 @@ protected function queryInternalReferrers($idaction, $actionType, $logAggregator $keyIsSiteSearchAction = 2; $rankingQuery = new RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + $rankingQuery->setOthersLabel('Others'); $rankingQuery->addLabelColumn(array('name', 'url_prefix')); $rankingQuery->setColumnToMarkExcludedRows('is_self'); $rankingQuery->partitionResultIntoMultipleGroups('action_partition', array($keyIsOther, $keyIsPageUrlAction, $keyIsSiteSearchAction)); @@ -415,7 +426,15 @@ protected function queryInternalReferrers($idaction, $actionType, $logAggregator $joinLogActionOn = $dimension; } $metrics = array(Metrics::INDEX_NB_ACTIONS); - $data = $logAggregator->queryActionsByDimension(array($dimension), $where, $selects, $metrics, $rankingQuery, $joinLogActionOn); + $data = $logAggregator->queryActionsByDimension( + array($dimension), + $where, + $selects, + $metrics, + $rankingQuery, + $joinLogActionOn, + $secondaryOrderBy = "`name`" + ); $loops = 0; $nbPageviews = 0; diff --git a/app/plugins/Transitions/Transitions.php b/app/plugins/Transitions/Transitions.php index 8649bf110..7d6248fdc 100644 --- a/app/plugins/Transitions/Transitions.php +++ b/app/plugins/Transitions/Transitions.php @@ -21,10 +21,16 @@ public function registerEvents() return array( 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', 'AssetManager.getJavaScriptFiles' => 'getJsFiles', - 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys' + 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', ); } + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = "General_Actions.Transitions_Transitions"; + } + public function getStylesheetFiles(&$stylesheets) { $stylesheets[] = 'plugins/Transitions/stylesheets/transitions.less'; diff --git a/app/plugins/Transitions/config/config.php b/app/plugins/Transitions/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/Transitions/config/config.php +++ b/app/plugins/Transitions/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Transitions/config/tracker.php b/app/plugins/Transitions/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/Transitions/config/tracker.php +++ b/app/plugins/Transitions/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/Transitions/lang/es-ar.json b/app/plugins/Transitions/lang/es-ar.json index 245689b23..e6569e02c 100644 --- a/app/plugins/Transitions/lang/es-ar.json +++ b/app/plugins/Transitions/lang/es-ar.json @@ -1,27 +1,38 @@ { "Transitions": { "BouncesInline": "%s rebotes", - "DirectEntries": "Ingresos directos", - "ErrorBack": "Regrese a la acción anterior", + "DirectEntries": "Entradas directas", + "Transitions": "Transiciones", + "ErrorBack": "Volver a la acción anterior", "ExitsInline": "%s salidas", + "NumPageviews": "%s vistas", + "NumDownloads": "%s descargas", + "NumOutlinks": "%s enlaces externos", + "TopX": "Principales %s etiquetas", + "FeatureDescription": "Las transiciones te dan un informe que muestra las cosas que tus visitantes hicieron directamente antes y después de ver cierta página. Esta página explicará cómo acceder, entender y usar el poderoso informe de Transiciones.", + "AvailableInOtherReports": "¿Lo sabías? Las transiciones también están disponibles como una acción de fila en los siguientes informes:", + "AvailableInOtherReports2": "Simplemente pasá el mouse sobre una fila de cualquiera de estos informes y hacé clic en el ícono de transición %s para ejecutarlo.", "FromCampaigns": "Desde campañas", "FromPreviousPages": "Desde páginas internas", "FromPreviousPagesInline": "%s desde páginas internas", "FromPreviousSiteSearches": "Desde búsquedas internas", "FromPreviousSiteSearchesInline": "%s desde búsquedas internas", "FromSearchEngines": "Desde motores de búsqueda", - "FromWebsites": "Desde sitios de internet", + "FromSocialNetworks": "Desde redes sociales", + "FromWebsites": "Desde sitios web", "IncomingTraffic": "Tráfico entrante", "LoopsInline": "%s recargas de página", - "NoDataForAction": "No hay información para %s", - "NoDataForActionDetails": "O la acción no tuvo vistas de páginas durante el período %s o es inválida.", + "NoDataForAction": "No hay datos para %s", + "NoDataForActionDetails": "O la acción no tuvo vistas de páginas durante el período %s o no es válida.", "OutgoingTraffic": "Tráfico saliente", + "PluginDescription": "Acciones siguientes y anteriores del informe para cada dirección web de página en un nuevo informe de Transiciones, disponible en lis informes de Acciones a través de un nuevo ícono.", "ShareOfAllPageviews": "Esta página tuvo %1$s vistas de página (%2$s de todas las vistas de páginas)", "ToFollowingPages": "A páginas internas", "ToFollowingPagesInline": "%s a páginas internas", "ToFollowingSiteSearches": "Búsquedas internas", "ToFollowingSiteSearchesInline": "%s búsquedas internas", "XOfAllPageviews": "%s de todas las vistas de esta página", - "XOutOfYVisits": "%1$s (de %2$s)" + "XOutOfYVisits": "%1$s (de %2$s)", + "PageURLTransitions": "Dirección web de la página de transiciones" } } \ No newline at end of file diff --git a/app/plugins/Transitions/lang/pt-br.json b/app/plugins/Transitions/lang/pt-br.json index a2b952ade..098406100 100644 --- a/app/plugins/Transitions/lang/pt-br.json +++ b/app/plugins/Transitions/lang/pt-br.json @@ -2,30 +2,37 @@ "Transitions": { "BouncesInline": "%s rejeições", "DirectEntries": "Entradas diretas", + "Transitions": "Transições", "ErrorBack": "Voltar para a ação anterior", "ExitsInline": "%s saídas", "NumPageviews": "%s exibições de página", "NumDownloads": "%s baixados", "NumOutlinks": "%s links externos", + "TopX": "%s principais rótulos", + "FeatureDescription": "Transições mostra um relatório de o que seus visitantes fizeram imediatamente antes e depois de visualizarem uma página específica. Esta página explicará como acessar, entender e usar o poderoso relatório Transições.", + "AvailableInOtherReports": "Você sabia? Transições também está disponível como uma ação de linha nos seguintes relatórios:", + "AvailableInOtherReports2": "Passe o mouse sobre qualquer um desses relatórios e clique no ícone de transição %s para abri-lo.", "FromCampaigns": "De campanhas", - "FromPreviousPages": "De Páginas internas", + "FromPreviousPages": "De páginas internas", "FromPreviousPagesInline": "%s de páginas internas", - "FromPreviousSiteSearches": "de pesquisas internas", + "FromPreviousSiteSearches": "De pesquisa interna", "FromPreviousSiteSearchesInline": "%s de pesquisas internas", "FromSearchEngines": "Dos motores de busca", - "FromWebsites": "de Websites", + "FromSocialNetworks": "Das redes sociais", + "FromWebsites": "De sites web", "IncomingTraffic": "Tráfego de entrada", "LoopsInline": "%s atualizações de página", "NoDataForAction": "Não há dados para %s", "NoDataForActionDetails": "Ou a ação não tinha visualizações de página durante o período de %s ou é inválido.", - "OutgoingTraffic": "O tráfego de saída", + "OutgoingTraffic": "Tráfego de saída", "PluginDescription": "Informa ações anteriores e seguintes para cada URL da página em um novo relatório de Transições, disponível nos relatórios de Ações através de um novo ícone.", - "ShareOfAllPageviews": "Esta página teve %1$s exibições (%2$s de todos as exibições)", + "ShareOfAllPageviews": "Esta página teve %1$s exibições (%2$s de todas as exibições)", "ToFollowingPages": "Para páginas internas", "ToFollowingPagesInline": "%s para páginas internas", "ToFollowingSiteSearches": "Pesquisas internas", "ToFollowingSiteSearchesInline": "%s pesquisas internas", "XOfAllPageviews": "%s de todas as visualizações desta página", - "XOutOfYVisits": "%1$s (fora de %2$s)" + "XOutOfYVisits": "%1$s (de %2$s)", + "PageURLTransitions": "Transições de URL da página" } } \ No newline at end of file diff --git a/app/plugins/Transitions/lang/sv.json b/app/plugins/Transitions/lang/sv.json index c8f1ea2c2..c88bab2f9 100644 --- a/app/plugins/Transitions/lang/sv.json +++ b/app/plugins/Transitions/lang/sv.json @@ -2,11 +2,14 @@ "Transitions": { "BouncesInline": "%s avvisningar", "DirectEntries": "Direktträffar", + "Transitions": "Övergångar", "ErrorBack": "Gå tillbaka till föregående åtgärd", "ExitsInline": "%s utgångar", "NumPageviews": "%s sidvisningar", "NumDownloads": "%s nerladdningar", "NumOutlinks": "%s utlänkar", + "TopX": "%s främsta etiketterna", + "AvailableInOtherReports": "Visste du att övergångar även finns som en radåtgärd i följande rapporter?", "FromCampaigns": "Från kampanjer", "FromPreviousPages": "Från interna sidor", "FromPreviousPagesInline": "%s från interna sidor", diff --git a/app/plugins/Transitions/lang/zh-cn.json b/app/plugins/Transitions/lang/zh-cn.json index d2cf543a0..694198754 100644 --- a/app/plugins/Transitions/lang/zh-cn.json +++ b/app/plugins/Transitions/lang/zh-cn.json @@ -2,17 +2,23 @@ "Transitions": { "BouncesInline": "%s 次跳出", "DirectEntries": "直接输入", + "Transitions": "转场", "ErrorBack": "返回上次的活动", "ExitsInline": "%s 次退出", "NumPageviews": "%s PV", "NumDownloads": "%s 下载", "NumOutlinks": "%s 外链", + "TopX": "前%s个标签", + "FeatureDescription": "转换提供一个报告,显示访问者在查看某个页面之前和之后直接执行的操作。本页将解释如何访问、理解和使用功能强大的转换报表。", + "AvailableInOtherReports": "你知道吗?在以下报表中,转换也可用作行操作:", + "AvailableInOtherReports2": "只需将这些报表中的一行悬停,然后单击转换图标%s即可启动它。", "FromCampaigns": "来自广告", "FromPreviousPages": "来自内部页面", "FromPreviousPagesInline": "%s 次来自站内页面", "FromPreviousSiteSearches": "来自站内搜索", "FromPreviousSiteSearchesInline": "%s 次来自站内搜索", "FromSearchEngines": "来自搜索引擎", + "FromSocialNetworks": "来自社交网络", "FromWebsites": "来自网站", "IncomingTraffic": "入口流量", "LoopsInline": "%s 次刷新页面", @@ -26,6 +32,7 @@ "ToFollowingSiteSearches": "站内搜索", "ToFollowingSiteSearchesInline": "%s 次站内搜索", "XOfAllPageviews": "%s 的本页浏览量", - "XOutOfYVisits": "%1$s (共 %2$s)" + "XOutOfYVisits": "%1$s (共 %2$s)", + "PageURLTransitions": "页面URL转换" } } \ No newline at end of file diff --git a/app/plugins/TwoFactorAuth/Validator.php b/app/plugins/TwoFactorAuth/Validator.php index 9840635b5..77b4bb271 100644 --- a/app/plugins/TwoFactorAuth/Validator.php +++ b/app/plugins/TwoFactorAuth/Validator.php @@ -9,6 +9,7 @@ namespace Piwik\Plugins\TwoFactorAuth; use Piwik\Common; +use Piwik\Exception\NotYetInstalledException; use Piwik\Piwik; use Piwik\Session\SessionFingerprint; use Exception; @@ -45,7 +46,7 @@ public function checkCanUseTwoFa() Piwik::checkUserIsNotAnonymous(); if (!SettingsPiwik::isPiwikInstalled()) { - throw new \Exception('Matomo is not set up yet'); + throw new NotYetInstalledException('Matomo is not set up yet'); } } diff --git a/app/plugins/TwoFactorAuth/config/config.php b/app/plugins/TwoFactorAuth/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/TwoFactorAuth/config/config.php +++ b/app/plugins/TwoFactorAuth/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/TwoFactorAuth/config/tracker.php b/app/plugins/TwoFactorAuth/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/TwoFactorAuth/config/tracker.php +++ b/app/plugins/TwoFactorAuth/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/TwoFactorAuth/lang/da.json b/app/plugins/TwoFactorAuth/lang/da.json index 50fe66421..772aeda78 100644 --- a/app/plugins/TwoFactorAuth/lang/da.json +++ b/app/plugins/TwoFactorAuth/lang/da.json @@ -1,6 +1,7 @@ { "TwoFactorAuth": { "TwoFactorAuthentication": "Tofaktorgodkendelse", + "TwoFAShort": "2FA", "TwoFactorAuthenticationIntro": "%1$sTofaktorgodkendelse%2$s øger sikkerheden for din konto ved at tilføje et ekstra niveau af godkendelse, når du logger ind. Hver gang du logger ind, vil du ikke kun blive spurgt om dit brugernavn og din adgangskode men tillige en godkendelseskode, der skifter løbende og er dannet eksempelvis på din mobiltelefon. Det betyder, at selvom nogen kender din brugernavn og din adgangskode, kan de stadig ikke logge ind, medmindre de også har adgang til din mobiltelefon.", "TwoFactorAuthenticationIsEnabled": "Tofaktorgodkendelse er nu aktiveret.", "TwoFactorAuthenticationIsDisabled": "Tofaktorgodkendelse er pt. deaktiveret.", diff --git a/app/plugins/TwoFactorAuth/lang/es-ar.json b/app/plugins/TwoFactorAuth/lang/es-ar.json new file mode 100644 index 000000000..a153a42b2 --- /dev/null +++ b/app/plugins/TwoFactorAuth/lang/es-ar.json @@ -0,0 +1,49 @@ +{ + "TwoFactorAuth": { + "TwoFactorAuthentication": "Autenticación de 2 factores", + "TwoFAShort": "2FA", + "TwoFactorAuthenticationIntro": "La %1$sautenticación de 2 factores%2$s aumenta la seguridad de tu cuenta al agregar una capa adicional de verificación cuando iniciás sesión. Cada vez que intentés iniciar sesión, no sólo se te pedirá tus credenciales habituales (tu dirección de correo electrónico y tu contraseña), sino un código adicional de autenticación, el cual cambia periódicamente y que es generado, por ejemplo, en tu dispositivo móvil. Esto significa que incluso si alguien sabe tu nombre de usuario y contraseña, esa persona no podrá iniciar sesión, a menos que tenga acceso a tu dispositivo móvil, siguiendo el ejemplo.", + "TwoFactorAuthenticationIsEnabled": "La autenticación de 2 factores actualmente está habilitada.", + "TwoFactorAuthenticationIsDisabled": "La autenticación de 2 factores actualmente está deshabilitada.", + "TwoFactorAuthenticationRequired": "Es obligatorio que la autenticación de 2 factores esté habilitada para todos; no podés deshabilitarla.", + "ConfigureDifferentDevice": "Configurar un dispositivo diferente", + "SetUpTwoFactorAuthentication": "Configurar la autenticación de 2 factores (2FA)", + "RequiredToSetUpTwoFactorAuthentication": "Es obligatorio que configurés la autenticación de 2 factores antes de iniciar sesión", + "AuthenticationCode": "Código de autenticación", + "Verify": "Verificar", + "StepX": "Paso %s", + "MissingAuthCodeAPI": "Por favor, especificá el código de autenticación de 2 factores.", + "InvalidAuthCode": "El código de autenticación de 2 factores no es correcto.", + "RequiredAuthCodeNotConfiguredAPI": "Es obligatorio que configurés la autenticación de 2 factores. Por favor, iniciá sesión.", + "VerifyIdentifyExplanation": "Abrí la aplicación de la autenticación de 2 factores en tu dispositivo para ver tu código de autenticación y verificar tu identidad.", + "DontHaveYourMobileDevice": "¿No tenés tu dispositivo móvil?", + "EnterRecoveryCodeInstead": "Ingresá uno de tus códigos de recuperación", + "AskSuperUserResetAuthenticationCode": "Pedile a un súperusuario que restablezca tu código de autenticación", + "SetupIntroFollowSteps": "Por favor, seguí estos pasos para configurar la autenticación de 2 factores:", + "SetupFinishedTitle": "¡Felicitaciones! Ahora tu cuenta es más segura.", + "SetupFinishedSubtitle": "Configuraste tu autenticación de 2 factores exitosamente. La próxima vez que intentés iniciar sesión, vas a tener que ingresar el código de autenticación. Asegurate de tener tu dispositivo móvil al alcance o de tener los códigos de recuperación.", + "WarningChangingConfiguredDevice": "Estás a punto de cambiar el dispositivo configurado con la autenticación de 2 factores. Esto invalidará cualquier otro dispositivo configurado.", + "ShowRecoveryCodes": "Mostrar códigos de recuperación", + "ConfirmSetup": "Confirmar configuración", + "NotPossibleToLogIn": "No se pudo iniciar sesión en Matomo Analytics", + "LostAuthenticationDevice": "Hola, %1$s. Tengo la autenticación de 2 factores habilitada y perdí mi dispositivo de autenticación. Por favor, ¿podrías restablecer la autenticación de 2 factores para mi nombre de usuario \"%5$s\"? Podés encontrar las instrucciones acá: %6$s. %2$sLa dirección web de Matomo es: %3$s.%4$sGracias.", + "WrongAuthCodeTryAgain": "Se ingresó un código de autenticación incorrecto. Por favor, intentá de nuevo.", + "DisableTwoFA": "Deshabilitar la autenticación de 2 factores", + "EnableTwoFA": "Habilitar la autenticación de 2 factores", + "ConfirmDisableTwoFA": "¿Estás seguro que querés deshabilitar la autenticación de 2 factores en tu cuenta? Teniéndola habilitada aumenta la seguridad en la misma.", + "VerifyAuthCodeIntro": "Por favor, ingresá abajo el código de 6 dígitos que aparece en tu aplicación de autenticación para confirmar que configuraste tu dispositivo exitosamente.", + "VerifyAuthCodeHelp": "Por favor, ingresá el código de 6 dígitos que se generó en tu dispositivo móvil luego de escanear el código de barra.", + "Your2FaAuthSecret": "Tu autenticación de 2 factores secreto", + "SetupAuthenticatorOnDevice": "Configurar la aplicación de autenticación en tu dispositivo", + "SetupAuthenticatorOnDeviceStep1": "Instalá una aplicación de autenticación, por ejemplo:", + "SetupAuthenticatorOnDeviceStep2": "A continuación, abrí la aplicación y escaneá el código de barras con la aplicación de autenticación de 2 factores en tu dispositivo móvil. Si no podés escanear el código de barras, %1$singresá este código%2$s.", + "SetupBackupRecoveryCodes": "Por favor, resguardá tus códigos de recuperación usando uno de los métodos anteriores antes de continuar con la autenticación de 2 factores.", + "RecoveryCodes": "Códigos de recuperación", + "RecoveryCodesExplanation": "Podés usar los códigos de recuperación para acceder a tu cuenta cuando no podés usar los códigos de la autenticación de 2 factores, como por ejemplo, cuando no tenés al alcance tu dispositivo móvil.", + "RecoveryCodesSecurity": "Por favor, tratá tus códigos de recuperación ¡con el mismo nivel de seguridad que lo harías con tu contraseña!", + "RecoveryCodesAllUsed": "Ya se usaron todos los códigos de recuperación. Es altamente recomendable que regenerés tus códigos.", + "RecoveryCodesRegenerated": "Se regeneraron los códigos de recuperación. Asegurate de descargar o imprimir los nuevos códigos generados.", + "GenerateNewRecoveryCodes": "Generar nuevos códigos de recuperación", + "GenerateNewRecoveryCodesInfo": "Cuando generás nuevos códigos de recuperación, los anteriores dejan de funcionar. Asegurate de descargar o imprimir los nuevos códigos generados." + } +} \ No newline at end of file diff --git a/app/plugins/TwoFactorAuth/lang/pt-br.json b/app/plugins/TwoFactorAuth/lang/pt-br.json new file mode 100644 index 000000000..c6385443e --- /dev/null +++ b/app/plugins/TwoFactorAuth/lang/pt-br.json @@ -0,0 +1,49 @@ +{ + "TwoFactorAuth": { + "TwoFactorAuthentication": "Autenticação de dois fatores", + "TwoFAShort": "A2F", + "TwoFactorAuthenticationIntro": "A %1$sautenticação de dois fatores%2$s aumenta a segurança da sua conta ao adicionar uma camada a mais de verificação quando você faz login. Cada vez que você fizer login você não apenas terá que informar seu login e senha, mas tembém um token adicional de autenticação que muda periodicamente e é gerado por exemplo no seu dispositivo móvel. Isto significa que mesmo que alguém saiba seu usuário e senha, ele não conseguirá fazer login a não ser que ele tenha acesso ao seu dispositivo móvel, por exemplo.", + "TwoFactorAuthenticationIsEnabled": "A autenticação de dois fatores está atualmente habilitada.", + "TwoFactorAuthenticationIsDisabled": "A autenticação de dois fatores está atualmente desabilitada.", + "TwoFactorAuthenticationRequired": "A autenticação de dois fatores está configurada como necessária para todos, você não pode desabilitá-la.", + "ConfigureDifferentDevice": "Configurar um outro dispositivo", + "SetUpTwoFactorAuthentication": "Configurar a autenticação de dois fatores (A2F)", + "RequiredToSetUpTwoFactorAuthentication": "Você deve configurar a autenticação de dois fatores antes de poder fazer login", + "AuthenticationCode": "Código de autenticação", + "Verify": "Verificar", + "StepX": "Passo %s", + "MissingAuthCodeAPI": "Favor especificar o código da autenticação de dois fatores.", + "InvalidAuthCode": "O código da autenticação de dois fatores não está correto.", + "RequiredAuthCodeNotConfiguredAPI": "Você deve configurar a autenticação de dois fatores. Favor fazer login em sua conta.", + "VerifyIdentifyExplanation": "Abra o aplicativo de autenticação de dois fatores no seu dispositivo para ver seu código de autenticação e verificar sua identidade.", + "DontHaveYourMobileDevice": "Não está com seu dispositivo móvel?", + "EnterRecoveryCodeInstead": "Digite um dos seus códigos de recuperação", + "AskSuperUserResetAuthenticationCode": "Peça a um super usuário para redefinir seu código de autenticação", + "SetupIntroFollowSteps": "Por favor siga estes passos para configurar a autenticação de dois fatores:", + "SetupFinishedTitle": "Parabéns! Sua conta está mais segura agora.", + "SetupFinishedSubtitle": "Você configurou com sucesso a autenticação de dois fatores. Na próxima vez que fizer login, você precisará digitar também o código de autenticação. Assegure-se de ter com você seu dispositivo móvel ou seus códigos de backup.", + "WarningChangingConfiguredDevice": "Você está prestes a mudar o dispositivo configurado com autenticação de dois fatores. Isto irá invalidar qualquer dispositivo configurado anteriormente.", + "ShowRecoveryCodes": "Mostrar códigos de recuperação", + "ConfirmSetup": "Confirmar configuração", + "NotPossibleToLogIn": "Não foi possível fazer login no Matomo Analytics", + "LostAuthenticationDevice": "Olá, %1$s Eu estou com a autenticação de fois fatores habilitada e perdi meu dispositivo de autenticação. Você poderia por favor redefinir a autenticação de dois fatores para o meu usuário %5$s? Você pode ver as instruções para isto aqui: %6$s.%2$s A URL do Matomo é %3$s.%4$sObrigado", + "WrongAuthCodeTryAgain": "Código de autenticação digitado está incorreto. Por favor tente novamente.", + "DisableTwoFA": "Desabilitar autenticação de dois fatores", + "EnableTwoFA": "Habilitar autenticação de dois fatores", + "ConfirmDisableTwoFA": "Tem certeza de que deseja desabilitar a autenticação de dois fatores para sua conta? Ter habilitada a autenticação de dois fatores aumenta a segurança da sua conta.", + "VerifyAuthCodeIntro": "Por favor digite abaixo o código de seis dígitos do seu aplicativo autenticador para confirmar que a configuração foi feita com sucesso no seu dispositivo.", + "VerifyAuthCodeHelp": "Por favor digite o código de seis dígitos que foi gerado no seu dispositivo móvel após escanear o código de barras.", + "Your2FaAuthSecret": "Seu segredo da autenticação de dois fatores", + "SetupAuthenticatorOnDevice": "Configurar o autenticador no seu dispositivo", + "SetupAuthenticatorOnDeviceStep1": "Instale um aplicativo autenticador, por exemplo:", + "SetupAuthenticatorOnDeviceStep2": "Depois, abra o aplicativo e escaneie o código de barras abaixo com o aplicativo de autenticação de dois fatores no seu telefone. Se você não conseguir escanear o código de barras, %1$sdigite este código%2$s no lugar.", + "SetupBackupRecoveryCodes": "Por favor faça backup dos seus códigos de recuperação usando um dos métodos acima antes de continuar a configuração da autenticação de dois fatores.", + "RecoveryCodes": "Códigos de recuperação", + "RecoveryCodesExplanation": "Você pode usar códigos de recuperação para acessar sua conta quando você não puder receber códigos de autenticação de dois fatores, por exemplo quando você não estiver com seu dispositivo móvel.", + "RecoveryCodesSecurity": "Por favor trate os seus códigos de recuperação com o mesmo nível de segurança que você trata a sua senha!", + "RecoveryCodesAllUsed": "Todos os códigos de segurança foram usados, é altamente recomendado que você gere novamente os seus códigos de recuperação.", + "RecoveryCodesRegenerated": "Os códigos de recuperação foram gerados novamente. Não esqueça de baixar ou imprimir os novos códigos gerados.", + "GenerateNewRecoveryCodes": "Gerar novos códigos de recuperação", + "GenerateNewRecoveryCodesInfo": "Quando você gera novos códigos de recuperação, os antigos códigos não funcionarão mais. Não esqueça de baixar ou imprimir os seus novos códigos." + } +} \ No newline at end of file diff --git a/app/plugins/TwoFactorAuth/lang/ru.json b/app/plugins/TwoFactorAuth/lang/ru.json new file mode 100644 index 000000000..3ee46ca11 --- /dev/null +++ b/app/plugins/TwoFactorAuth/lang/ru.json @@ -0,0 +1,23 @@ +{ + "TwoFactorAuth": { + "TwoFactorAuthentication": "Двухфакторная аутентификация", + "TwoFAShort": "2FA", + "TwoFactorAuthenticationIsEnabled": "Двухфакторная аутентификация в настоящее время включена.", + "TwoFactorAuthenticationIsDisabled": "Двухфакторная аутентификация в настоящее время отключена.", + "TwoFactorAuthenticationRequired": "Двухфакторная аутентификация должна быть включена для всех, ее нельзя отключить.", + "ConfigureDifferentDevice": "Настроить другое устройство", + "SetUpTwoFactorAuthentication": "Настройка двухфакторной аутентификации (2FA)", + "RequiredToSetUpTwoFactorAuthentication": "Вы должны настроить двухфакторную аутентификацию, прежде чем вы сможете войти", + "AuthenticationCode": "Код аутентификации", + "Verify": "Подтвердить", + "MissingAuthCodeAPI": "Пожалуйста, укажите код двухфакторной аутентификации.", + "InvalidAuthCode": "Код двухфакторной аутентификации неверен.", + "RequiredAuthCodeNotConfiguredAPI": "Вам необходимо настроить двухфакторную аутентификацию. Пожалуйста, войдите в свой аккаунт.", + "VerifyIdentifyExplanation": "Откройте приложение двухфакторной аутентификации на своем устройстве, чтобы просмотреть код аутентификации и подтвердить свою личность.", + "DontHaveYourMobileDevice": "У вас нет мобильного устройства?", + "EnterRecoveryCodeInstead": "Введите один из ваших кодов восстановления", + "AskSuperUserResetAuthenticationCode": "Попросите супер пользователя сбросить ваш код аутентификации", + "SetupIntroFollowSteps": "Пожалуйста, выполните следующие действия для настройки двухфакторной аутентификации:", + "SetupFinishedTitle": "Поздравляем! Ваша учетная запись стала более безопасной." + } +} \ No newline at end of file diff --git a/app/plugins/UserCountry/Archiver.php b/app/plugins/UserCountry/Archiver.php index 1fb9ad68b..16eca3bd2 100644 --- a/app/plugins/UserCountry/Archiver.php +++ b/app/plugins/UserCountry/Archiver.php @@ -107,12 +107,13 @@ private function makeRegionCityLabelsUnique(&$row) $row[$column] = str_replace(self::LOCATION_SEPARATOR, '', $row[$column]); } - if (!empty($row[self::REGION_FIELD])) { - $row[self::REGION_FIELD] = $row[self::REGION_FIELD] . self::LOCATION_SEPARATOR . $row[self::COUNTRY_FIELD]; + // set city first, as containing region might be manipulated afterwards if not empty + if (!empty($row[self::CITY_FIELD])) { + $row[self::CITY_FIELD] = $row[self::CITY_FIELD] . self::LOCATION_SEPARATOR . $row[self::REGION_FIELD] . self::LOCATION_SEPARATOR . $row[self::COUNTRY_FIELD]; } - if (!empty($row[self::CITY_FIELD])) { - $row[self::CITY_FIELD] = $row[self::CITY_FIELD] . self::LOCATION_SEPARATOR . $row[self::REGION_FIELD]; + if (!empty($row[self::REGION_FIELD])) { + $row[self::REGION_FIELD] = $row[self::REGION_FIELD] . self::LOCATION_SEPARATOR . $row[self::COUNTRY_FIELD]; } } diff --git a/app/plugins/UserCountry/Columns/Base.php b/app/plugins/UserCountry/Columns/Base.php index 1b737bd05..1ee6ea9d1 100644 --- a/app/plugins/UserCountry/Columns/Base.php +++ b/app/plugins/UserCountry/Columns/Base.php @@ -25,6 +25,11 @@ abstract class Base extends VisitDimension private $visitorGeolocator; protected function getUrlOverrideValueIfAllowed($urlParamToOverride, Request $request) + { + return self::getValueFromUrlParamsIfAllowed($urlParamToOverride, $request); + } + + public static function getValueFromUrlParamsIfAllowed($urlParamToOverride, Request $request) { $value = Common::getRequestVar($urlParamToOverride, false, 'string', $request->getParams()); diff --git a/app/plugins/UserCountry/VisitorGeolocator.php b/app/plugins/UserCountry/VisitorGeolocator.php index 3aa52bd3f..5f992af39 100644 --- a/app/plugins/UserCountry/VisitorGeolocator.php +++ b/app/plugins/UserCountry/VisitorGeolocator.php @@ -265,7 +265,7 @@ public function reattributeVisitLogs($from, $to, $idSite = null, $iterationStep $onLogProcessed($row, $updatedValues); } } - }); + }, $willDelete = false); } /** diff --git a/app/plugins/UserCountry/config/tracker.php b/app/plugins/UserCountry/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/UserCountry/config/tracker.php +++ b/app/plugins/UserCountry/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UserCountry/lang/da.json b/app/plugins/UserCountry/lang/da.json index a8f8bee6d..3cf9d8ce2 100644 --- a/app/plugins/UserCountry/lang/da.json +++ b/app/plugins/UserCountry/lang/da.json @@ -13,10 +13,12 @@ "Continent": "Kontinent", "Continents": "Kontinenter", "Country": "Land", + "CountryCode": "Landekode", "country_a1": "Anonym proxy", "country_a2": "Satellit udbyder", "country_cat": "Catalansk-talende samfund", "country_o1": "Andet land", + "VisitLocation": "Brugerlokation", "CurrentLocationIntro": "I følge denne tjeneste er din aktuelle lokation", "DefaultLocationProviderDesc1": "Standardlokationstjenesten gætter en besøgendes land baseret på det sprog de bruger.", "DefaultLocationProviderDesc2": "Dette er ikke særlig nøjagtigt, så %1$svi anbefaler at du installerer og bruger %2$sGeoIP%3$s.%4$s", @@ -30,10 +32,12 @@ "GeoIPCannotFindMbstringExtension": "Kan ikke finde %1$s funktionen. Sørg for at %2$s udvidelsen er installeret og indlæst.", "GeoIPDatabases": "GeoIP databaser", "GeoIPDocumentationSuffix": "For at se data for rapporten, skal du opsætte GeoIP i Geolokaliseringsfanen. Den kommercielle %1$sMaxmind%2$s GeoIP-database er mere præcise end den gratis er. For at se hvor nøjagtig den er, skal du klikke %3$sher%4$s.", + "GeoIPNoDatabaseFound": "Denne GeoIP implementering kunne ikke finde nogen database.", "GeoIPImplHasAccessTo": "GeoIP implementering har adgang til følgende typer af databaser", "GeoIPIncorrectDatabaseFormat": "Din GeoIP-database lader ikke til at have det korrekte format. Måske er den beskadiget. Vær sikker på du bruger den binære udgave og prøv evt. at erstatte den med en anden kopi.", "GeoIpLocationProviderDesc_Pecl1": "Lokationstjenesten bruger en GeoIP-database og et PECL-modul til præcist og effektivt at bestemme lokationen på ​​dine besøgende.", "GeoIpLocationProviderDesc_Pecl2": "Der er ingen begrænsninger med denne tjeneste, så det er den vi anbefaler du bruger.", + "GeoIpLocationProviderDesc_Php1": "Denne lokationsudbyder er simpel at installere, da den ikke kræver nogen serverkonfiguration (ideel for delt hosting!). Den bruger GeoIP database og MasxMinds PHP API til præcist at fastsætte dine besøgendes lokation.", "GeoIpLocationProviderDesc_Php2": "Hvis hjemmesiden får en masse trafik, kan du opleve, at denne lokationstjeneste er for langsom. I så fald skal du installere %1$sPECL-udvidelsen%2$s eller et %3$sservermodul%4$s.", "GeoIpLocationProviderDesc_ServerBased1": "Lokasationstjenesten bruger det GeoIP-modul, der er installeret i din HTTP-server. Tjenesten er hurtig og præcis, men %1$skan kun bruges med normal browser tracking.%2$s", "GeoIpLocationProviderDesc_ServerBased2": "Hvis du skal importere logfiler eller gøre noget andet, der kræver indstilling af IP-adresser, skal du bruge %1$sPECL GeoIP implementering (anbefales)%2$s eller %3$sPHP GeoIP implementering%4$s.", @@ -67,11 +71,13 @@ "ISPDatabase": "Internetudbyder (ISP) database", "IWantToDownloadFreeGeoIP": "Jeg vil hente den gratis GeoIP database...", "Latitude": "Breddegrad", + "Latitudes": "Breddegrader", "Location": "Sted", "LocationDatabase": "Lokationsdatabase", "LocationDatabaseHint": "En lokationsdatabase er enten en land-, region- eller by-database.", "LocationProvider": "Lokationstjeneste", "Longitude": "Længdegrad", + "Longitudes": "Længdegrader", "NoDataForGeoIPReport1": "Der er ingen data for rapporten, fordi der enten ikke er lokaliseringsdata til rådighed, eller besøgendes IP-adresser ikke kan geografisk placeres.", "NoDataForGeoIPReport2": "For at aktivere nøjagtig geolokalisering skal indstillingerne ændres %1$sher%2$s og bruge en %3$sbyniveaudatabase%4$s.", "Organization": "Organisation", diff --git a/app/plugins/UserCountry/lang/es-ar.json b/app/plugins/UserCountry/lang/es-ar.json index dcdc310fe..2c5d8bd0d 100644 --- a/app/plugins/UserCountry/lang/es-ar.json +++ b/app/plugins/UserCountry/lang/es-ar.json @@ -1,83 +1,103 @@ { "UserCountry": { - "AssumingNonApache": "No se puede encontrar la función apache_get_modules, asumiendo que se posee un servidor de internet no-Apache.", - "CannotFindGeoIPDatabaseInArchive": "No se encuentra el archivo %1$s en el archivo tar %2$s!", - "CannotFindGeoIPServerVar": "La variable %s no está especificada. Su servidor puede no estar configurado correctamente.", - "CannotFindPeclGeoIPDb": "No se encuentra en la base de datos el país, región o ciudad en el módulo GeoIP PECL. Asegúrese que su base de datos GeoIP esté localizada en %1$s y sea nombrada %2$s o %3$s, de no ser así, el módulo PECL no lo reconocerá.", + "AssumingNonApache": "No se puede encontrar la función \"apache_get_modules\", asumiendo que se posee un servidor de internet no-Apache.", + "CannotFindGeoIPDatabaseInArchive": "¡No se encuentra el archivo %1$s en el archivo tar %2$s!", + "CannotFindGeoIPServerVar": "La variable %s no está especificada. Tu servidor puede no estar configurado correctamente.", + "CannotFindPeclGeoIPDb": "No se pudo encontrar una base de datos de país, región o ciudad en el módulo GeoIP PECL. Asegurate de que tu base de datos GeoIP esté ubicada en %1$s y que se llame %2$s o %3$s; de lo contrario, el módulo PECL no la reconocerá.", "CannotListContent": "No se pudo enumerar el contenido de %1$s: %2$s", "CannotLocalizeLocalIP": "La dirección IP %s es una dirección local y no puede ser geolocalizada.", - "CannotUnzipDatFile": "No se puede descomprimir el archivo dat en %1$s: %2$s", + "CannotSetupGeoIPAutoUpdating": "Parece que estás guardando tus bases de datos de GeoIP por fuera de Matomo (nos damos cuenta porque no hay base de datos en el subdirectorio \"misc\", pero tu GeoIP está funcionando). Matomo no puede actualizar automáticamente tus bases de datos GeoIP si están ubicadas fuera del directorio \"misc\".", + "CannotUnzipDatFile": "No se pudo descomprimir el archivo dat en %1$s: %2$s", "City": "Ciudad", "CityAndCountry": "%1$s, %2$s", "Continent": "Continente", + "Continents": "Continentes", "Country": "País", - "country_a1": "Proxy Anonimo", - "country_a2": "Proveedor Satelital", + "CountryCode": "Código de país", + "country_a1": "Proxy anónimo", + "country_a2": "Proveedor satelital", "country_cat": "Comunidades de habla catalana", - "country_o1": "Otro País", - "CurrentLocationIntro": "De acuerdo a este proveedor, su actual ubicación es", - "DefaultLocationProviderDesc1": "El proveedor de ubicación por defecto asume que el país del visitante es por el lenguaje que él utiliza.", - "DefaultLocationProviderDesc2": "No es muy preciso, por lo tanto %1$srecomendamos instalar y utilizar %2$sGeoIP%3$s.%4$s", + "country_o1": "Otro país", + "VisitLocation": "Ubicación de la visita", + "CurrentLocationIntro": "De acuerdo a este proveedor, tu ubicación actual es", + "DefaultLocationProviderDesc1": "El proveedor de ubicación predeterminado asume que el país del visitante es por el lenguaje que él utiliza.", + "DefaultLocationProviderDesc2": "Esto no es muy preciso, por lo tanto %1$srecomendamos instalar y utilizar %2$sGeoIP%3$s.%4$s", + "DefaultLocationProviderExplanation": "Estás usando el proveedor de ubicación predeterminado, lo que significa que Matomo intentará adivinar la ubicación del visitante basándose en el idioma que usa. %1$sLeé esto%2$s para aprender cómo configurar una geolocalización más precisa.", "DistinctCountries": "%s países distintos", "DownloadingDb": "Descargando %s", "DownloadNewDatabasesEvery": "Actualizar bases de datos cada", - "FromDifferentCities": "different cities", - "GeoIPCannotFindMbstringExtension": "No encuentro la función %1$s. Por favor, asegúrese que la extensión %2$s esté instalada y cargada.", + "FatalErrorDuringDownload": "Ocurrió un error fatal al descargar este archivo. Puede ser que haya algo malo con tu conexión de Internet, con la base de datos de GeoIP que descargaste o con Matomo. Intentá descargarla e instalarla manualmente.", + "FoundApacheModules": "Matomo encontró los siguientes módulos Apache", + "FromDifferentCities": "ciudades diferentes", + "GeoIPCannotFindMbstringExtension": "No se encuentra la función %1$s. Por favor, asegurate que la extensión %2$s esté instalada y cargada.", "GeoIPDatabases": "Bases de datos GeoIP", - "GeoIPDocumentationSuffix": "Para observar la información de este informe, debe configurar GeoIP en la sección Geolocalización en la lengüeta del administrador. Las bases de datos GeoIP comerciales %1$sMaxmind%2$s son más fiables que las gratuitas. Para ver cuan seguras son, clic %3$saquí%4$s.", + "GeoIPDocumentationSuffix": "Para ver la información de este informe, tenés que configurar GeoIP en la sección \"Geolocalización\" en la pestaña del administrador. Las bases de datos GeoIP comerciales %1$sMaxmind%2$s son más fiables que las gratuitas. Para ver cuán precisas son, hacé clic %3$sacá%4$s.", + "GeoIPNoDatabaseFound": "Esta implementación GeoIP no pudo encontrar ninguna base de datos.", "GeoIPImplHasAccessTo": "Esta implementación GeoIP tiene acceso a los siguientes tipos de bases de datos", - "GeoIPIncorrectDatabaseFormat": "Your GeoIP database does not seem to have the correct format. It may be corrupt. Make sure you are using the binary version and try replacing it with another copy.", - "GeoIpLocationProviderDesc_Pecl1": "Este proveedor de ubicación utiliza una base de datos GeoIP y un módulo PECL para precisamente y eficientemente determinar la ubicación de sus visitantes.", + "GeoIPIncorrectDatabaseFormat": "Tu base de datos GeoIP no parece tener el formato correcto. Podría estar corrompida. Asegurate de estar usando la versión binaria y tratá de reemplazarla con otra copia.", + "GeoIpLocationProviderDesc_Pecl1": "Este proveedor de ubicación utiliza una base de datos GeoIP y un módulo PECL para precisamente y eficientemente determinar la ubicación de tus visitantes.", "GeoIpLocationProviderDesc_Pecl2": "No existen limitaciones con este proveedor, es el que recomendamos utilizar.", - "GeoIpLocationProviderDesc_Php2": "Si su sitio de internet posee un montón de tráfico, puede que su proveedor es demasiado lento. En este caso, debe instalar la extensión%2$s %1$sPECL o el módulo%4$s %3$sservidor.", - "GeoIpLocationProviderDesc_ServerBased1": "Este proveedor de ubicación utiliza el módulo GeoIP que se ha instalado en su servidor HTTP. Este proveedor es fiable y rápido, pero %1$solo puede ser utilizado con el rastreo de navegadores de internet normales.%2$s", - "GeoIpLocationProviderDesc_ServerBased2": "Si tiene que importar archivos de registro o cualquier otra acción que requiera configurar direcciones IP, utilice la implementación GeoIP %1$sPECL (recomendada)%2$s o la %3$simplementación GeoIP PHP%4$s.", - "GeoIpLocationProviderDesc_ServerBasedAnonWarn": "Nota: La anonimización de las direcciónes IP no tiene efecto en las ubicaciones informadas por este proveedor. Antes de utilizarlo con anonimización de IP, asegúrese que no viola leyes de privacidad que puede usted estar sujeto.", - "GeoIpLocationProviderNotRecomnended": "Geolocation works, but you are not using one of the recommended providers.", + "GeoIpLocationProviderDesc_Php1": "El proveedor de ubicación es el más sencillo de instalar ya que no requiere de configuración del servidor (¡es ideal para el hospedaje compartido!). Usa una base de datos GeoIP y la API PHP de MaxMind para determinar con precisión la ubicación de tus visitantes.", + "GeoIpLocationProviderDesc_Php2": "Si tu sitio web tiene mucho tráfico, entonces podrías encontrar que este proveedor de ubicación es demasiado lento. En este caso, deberías instalar la %1$sextensión PECL%2$s o un %3$smódulo de servidor%4$s.", + "GeoIpLocationProviderDesc_ServerBased1": "Este proveedor de ubicación utiliza el módulo GeoIP que se ha instalado en su servidor HTTP. Este proveedor es fiable y rápido, pero %1$ssólo puede ser usado con el rastreo común de los navegadores web.%2$s", + "GeoIpLocationProviderDesc_ServerBased2": "Si tenés que importar archivos de registro o cualquier otra acción que requiera configurar direcciones IP, usá la %1$simplementación GeoIP PECL (recomendada)%2$s o la %3$simplementación GeoIP PHP%4$s.", + "GeoIpLocationProviderDesc_ServerBasedAnonWarn": "Nota: La anonimización de las direcciónes IP no tiene efecto en las ubicaciones informadas por este proveedor. Antes de utilizarlo con anonimización de IP, asegurate de que no viola leyes de privacidad a las que podés estar sujeto.", + "GeoIpLocationProviderNotRecomnended": "La geolocalización funciona, pero no estás usando uno de nuestros proveedores recomendados.", + "GeoIPNoServerVars": "Matomo no puede encontrar ninguna variable GeoIP de %s.", "GeoIPPeclCustomDirNotSet": "La opción %s PHP ini no se ha establecido.", - "GeoIPUpdaterInstructions": "Ingrese abajo los enlaces de descarga de sus base de datos. Si ha comprado bases de datos desde %3$sMaxMind%4$s, puede encontrar dichos enlaces %1$saquí%2$s. Por favor, contáctese con %3$sMaxMind%4$s si tiene problemas para acceder a ellos.", - "GeoLiteCityLink": "Si está utilizando la base de datos GEoLite City, utilice este enlace: %1$s%2$s%3$s.", + "GeoIPServerVarsFound": "Matomo detecta las siguientes variables GeoIP de %s", + "GeoIPUpdaterInstructions": "Ingresá abajo los enlaces de descarga de tus base de datos. Si compraste bases de datos a %3$sMaxMind%4$s, podés encontrar dichos enlaces %1$sacá%2$s. Por favor, contactá con %3$sMaxMind%4$s si tenés problemas para acceder a ellos.", + "GeoIPUpdaterIntro": "Matomo actualmente está administrando actualizaciones para las siguientes bases de datos GeoIP", + "GeoLiteCityLink": "Si estás usando la base de datos GEoLite City, usá este enlace: %1$s%2$s%3$s.", "Geolocation": "Geolocalización", - "getCityDocumentation": "Este informe muestra las ciudades desde las cuales sus visitantes accedieron a su sitio de internet.", - "getContinentDocumentation": "Este informe muestra en que continente están sus visitantes cuando acceden a su sitio de internet.", - "getCountryDocumentation": "Este informe muestra desde que país accedieron a su sitio de internet sus visitantes.", - "getRegionDocumentation": "Este informe muestra en que región están sus visitantes en el momento que accedían a su sitio de internet.", - "HowToInstallApacheModule": "Cómo instalar el módulo GeoIP en Apache?", - "HowToInstallGeoIPDatabases": "Cómo obtengo las bases de datos GeoIP?", - "HowToInstallGeoIpPecl": "Cómo instalar la extensión GeoIP PECL?", - "HowToInstallNginxModule": "Cómo instalo el módulo GeoIP en Nginx?", + "GeolocationPageDesc": "En esta página podés cambiar cómo Matomo determina las ubicaciones de los visitantes.", + "getCityDocumentation": "Este informe muestra las ciudades desde las cuales tus visitantes accedieron a tu sitio web.", + "getContinentDocumentation": "Este informe muestra en qué continente estaban tus visitantes cuando visitaron tu sitio web.", + "getCountryDocumentation": "Este informe muestra en qué país estaban tus visitantes cuando visitaron tu sitio web.", + "getRegionDocumentation": "Este informe muestra en qué región estaban tus visitantes cuando visitaron tu sitio web.", + "HowToInstallApacheModule": "¿Cómo instalo el módulo GeoIP en Apache?", + "HowToInstallGeoIPDatabases": "¿Cómo obtengo las bases de datos GeoIP?", + "HowToInstallGeoIpPecl": "¿Cómo instalo la extensión GeoIP PECL?", + "HowToInstallNginxModule": "¿Cómo instalo el módulo GeoIP en Nginx?", "HowToSetupGeoIP": "Cómo configurar una geolocalización precisa con GeoIP", - "HowToSetupGeoIP_Step1": "%1$sDescargue%2$s la base de datos GeoLite City desde %3$sMaxMind%4$s.", - "HowToSetupGeoIP_Step3": "Volver a cargar esta pantalla. El proveedor %1$sGeoIP (PHP)%2$s ahora será %3$sinstalado%4$s. Selecciónelo.", - "HowToSetupGeoIPIntro": "Parece ser que la configuración de la Geolocalización no es confiable. Esta es una función útil y sin ella no verá una información precisa y completa de la ubicación de sus visitantes. Aquí un rápido vistazo de como utilizarla:", + "HowToSetupGeoIP_Step1": "%1$sDescargá%2$s la base de datos GeoLite City desde %3$sMaxMind%4$s.", + "HowToSetupGeoIP_Step2": "Extraé este archivo y copia el resultado, %1$s en el subdirectorio %2$smisc%3$s de Matomo (podés hacerlo por FTP o SSH).", + "HowToSetupGeoIP_Step3": "Refrescar esta pantalla. El proveedor %1$sGeoIP (PHP)%2$s ahora será %3$sinstalado%4$s. Seleccionalo.", + "HowToSetupGeoIP_Step4": "¡Listo! Acabás de configurar Matomo para que use GeoIP, lo que significa que vas a poder ver las regiones y ciudades de tus visitantes con información muy precisa por país.", + "HowToSetupGeoIPIntro": "Parece ser que la configuración de la geolocalización no es confiable. Esta es una función útil y sin ella no verás una información precisa y completa de la ubicación de tus visitantes. Aquí un rápido vistazo de cómo utilizarla:", "HttpServerModule": "Módulo servidor HTTP", - "InvalidGeoIPUpdatePeriod": "Período inválido para el actualizador GeoIP: %1$s. Los valores correctos son %2$s.", - "IPurchasedGeoIPDBs": "Adquirí más %1$sbase de datos de MaxMind%2$s y quiero configurar las actualizaciones automáticas.", - "ISPDatabase": "Base de dato ISP", - "IWantToDownloadFreeGeoIP": "Quiero descargar la base de datos gratuita GeoIP...", + "InvalidGeoIPUpdatePeriod": "Período no válido para el actualizador GeoIP: %1$s. Los valores correctos son %2$s.", + "IPurchasedGeoIPDBs": "Compré más %1$sbase de datos precisas de MaxMind%2$s y quiero configurar las actualizaciones automáticas.", + "ISPDatabase": "Base de datos de ISP", + "IWantToDownloadFreeGeoIP": "Quiero descargar la base de datos gratuita GeoIP…", "Latitude": "Latitud", + "Latitudes": "Latitudes", "Location": "Ubicación", "LocationDatabase": "Base de datos de ubicaciones", - "LocationDatabaseHint": "Una base de datos de ubicación es una base de datos ya sea un país, región o ciudad.", + "LocationDatabaseHint": "Una base de datos de ubicación es una base de datos, ya sea de país, región o ciudad.", "LocationProvider": "Proveedor de ubicación", "Longitude": "Longitud", - "NoDataForGeoIPReport1": "No hay datos para este informe debido a que no existe información o las direcciones IP de los visitantes no puede ser geolocalizada.", - "NoDataForGeoIPReport2": "Para habilitar una geolocalización precisa, cambie la opción %1$saquí%2$s y utilice una %3$sbase de datos a nivel ciudad%4$s.", + "Longitudes": "Longitudes", + "NoDataForGeoIPReport1": "No hay datos para este informe debido a que no existe información o las direcciones IP de los visitantes no pueden ser geolocalizadas.", + "NoDataForGeoIPReport2": "Para habilitar una geolocalización precisa, cambiá la configuración %1$sacá%2$s y usá una %3$sbase de datos a nivel ciudad%4$s.", "Organization": "Organización", "OrgDatabase": "Base de datos de la organización", - "PeclGeoIPNoDBDir": "El módulo PECL está buscando base de datos en %1$s, pero esta carpeta no existe. Por favor, créela y agreguéle una base de datos GeoIP a la misma. Alternativamente, puede disponer %2$s una determinada carpeta en su archivo php.ini", - "PeclGeoLiteError": "Su base de datos GeoIP en %1$s está nombrada %2$s. Desafortunadamente, el módulo PECL no la reconocerá con dicho nombre. Por favor, renómbrela a %3$s.", + "PeclGeoIPNoDBDir": "El módulo PECL está buscando base de datos en %1$s, pero esta carpeta no existe. Por favor, creala y agregale una base de datos GeoIP a la misma. Alternativamente, podés establecer %2$s a una determinada carpeta en tu archivo \"php.ini\".", + "PeclGeoLiteError": "Tu base de datos GeoIP en %1$s tiene como nombre \"%2$s\". Desafortunadamente, el módulo PECL no la reconocerá con dicho nombre. Por favor, renombrala a \"%3$s\".", + "PiwikNotManagingGeoIPDBs": "Matomo actualmente no está administrando ninguna base de datos GeoIP.", + "PluginDescription": "Informa la ubicación de tus visitantes: país, región, ciudad y coordenadas geográficas (latitud y longitud).", "Region": "Región", - "SetupAutomaticUpdatesOfGeoIP": "Configurar actualizaciones automáticas de sus base de datos GeoIP", - "SubmenuLocations": "Localizaciones", - "ThisUrlIsNotAValidGeoIPDB": "El archivo descargado no es una base de dato GeoIP válida. Por favor, verifique la dirección de internet o descargue el archivo manualmente.", - "ToGeolocateOldVisits": "Para obtener información de ubicación de sus antiguos visitantes, utilice esta información descripta %1$saquí%2$s.", - "UnsupportedArchiveType": "Se encontró un tipo de archivo %1$s no deseable.", - "UpdaterHasNotBeenRun": "El actualizador nunca se ha ejecutado.", - "UpdaterIsNotScheduledToRun": "It is not scheduled to run in the future.", - "UpdaterScheduledForNextRun": "It is scheduled to run during the next archive.php cron execution.", - "UpdaterWasLastRun": "El actualizador se ejecuto por última vez en %s.", - "UpdaterWillRunNext": "It is next scheduled to run on %s.", + "SetupAutomaticUpdatesOfGeoIP": "Configurar actualizaciones automáticas de tus base de datos GeoIP", + "SubmenuLocations": "Ubicaciones", + "TestIPLocatorFailed": "Matomo intentó revisar la ubicación de una dirección IP conocida (%1$s), pero el servidor respondió %2$s. Si el proveedor estuviese configurado correctamente, debería responder %3$s.", + "ThisUrlIsNotAValidGeoIPDB": "El archivo descargado no es una base de dato GeoIP válida. Por favor, verificá la dirección web o descargá el archivo manualmente.", + "ToGeolocateOldVisits": "Para obtener información de ubicación de tus antiguas visitas, usá el script descripto %1$sacá%2$s.", + "UnsupportedArchiveType": "Se encontró un tipo de archivo %1$s no soportado.", + "UpdaterHasNotBeenRun": "Nunca se ejecutó el actualizador.", + "UpdaterIsNotScheduledToRun": "No está programado para ejecutarse en el futuro.", + "UpdaterScheduledForNextRun": "Está programado para ejecutarse durante la próxima ejecución de comando cron \"core:archive\".", + "UpdaterWasLastRun": "El actualizador se ejecutó por última vez: %s.", + "UpdaterWillRunNext": "Está programado para ejecutarse: %s.", "WidgetLocation": "Ubicación del visitante" } } \ No newline at end of file diff --git a/app/plugins/UserCountry/lang/fi.json b/app/plugins/UserCountry/lang/fi.json index 850dbbeb4..15336a44e 100644 --- a/app/plugins/UserCountry/lang/fi.json +++ b/app/plugins/UserCountry/lang/fi.json @@ -34,9 +34,10 @@ "GeoIPDocumentationSuffix": "Nähdäksesi tietoja tässä raportissa, sinun täytää asettaa GeoIP geopaikannuksen admin-välilehdessä. Kaupalliset %1$sMaxmind%2$s GeoIP tietokannat ovat tarkempia kuin ilmaiset. Klikkaa %3$stästä%4$s nähdäksesi miten tarkkoja ne ovat.", "GeoIPNoDatabaseFound": "GeoIP-toteutus ei löytänyt yhtäkään tietokantaa.", "GeoIPImplHasAccessTo": "Tällä GeoIP-sovelluksella on pääsy seuraavanlaisiin tietokantoihin", - "GeoIPIncorrectDatabaseFormat": "GeoIP tietokantasi ei vaikuta olevan oikeassa formaatissa. Se saattaa olla vioittunut. Varmista, että käytät binääriversiota ja yritä korvata se toisella kopiolla.", + "GeoIPIncorrectDatabaseFormat": "GeoIP-tietokantasi ei vaikuta olevan oikeassa formaatissa. Se saattaa olla vioittunut. Varmista, että käytät binääriversiota ja yritä korvata se toisella kopiolla.", "GeoIpLocationProviderDesc_Pecl1": "Tämä sijainnintarjoaja käyttää GeoIP-tietokantaa ja PECL-moduulia kävijöiden paikallistamiseen.", "GeoIpLocationProviderDesc_Pecl2": "Suosittelemme tätä palvelua, sillä tällä ei ole rajoituksia.", + "GeoIpLocationProviderDesc_Php1": "Tämä sijainnintarjoaja on helpoin asennettava, koska se ei vaadi palvelinmäärityksiä, joten se on luonteva valinta jaettuun hosting-ympäristöön. Se käyttää GeoIP-tietokantaa ja MaxMindin PHP-rajapintaa vierailijoiden tarkan sijainnin selvittämiseksi.", "GeoIpLocationProviderDesc_Php2": "Jos verkkosivullasi on paljon liikennettä, tämä palvelu saattaa olla mielestäsi liian hidas. Asenna siinä tapauksessa %1$sPECL-laajennus%2$s tai %3$spalvelinmoduuli%4$s.", "GeoIpLocationProviderDesc_ServerBased1": "Tämä toteutus käyttää GeoIP-moduulia HTTP-palvelimesta. Tämä toteutus on nopea ja tarkka, mutta %1$stoimii vain normaalin selainseurannan kanssa.%2$s", "GeoIpLocationProviderDesc_ServerBased2": "Jos olet ladannut lokitiedostoja, käytä %1$sPECL:n GeoIP-toteutusta (suositeltu)%2$s tai %3$sPHP:n GeoIP-toteutusta%4$s.", diff --git a/app/plugins/UserCountry/lang/pt-br.json b/app/plugins/UserCountry/lang/pt-br.json index 6a8fa1f31..9c7d78514 100644 --- a/app/plugins/UserCountry/lang/pt-br.json +++ b/app/plugins/UserCountry/lang/pt-br.json @@ -11,28 +11,33 @@ "City": "Cidade", "CityAndCountry": "%1$s, %2$s", "Continent": "Continente", + "Continents": "Continentes", "Country": "País", + "CountryCode": "Código do país", "country_a1": "Proxy anônimo", "country_a2": "Provedor de satélite", "country_cat": "Comunidades Catalães", "country_o1": "Outro país", + "VisitLocation": "Localização da visita", "CurrentLocationIntro": "De acordo com este provedor, sua localização é", "DefaultLocationProviderDesc1": "Detecta a localização padrão de um visitante baseando-se no idioma que ele usa", "DefaultLocationProviderDesc2": "Isso não é muito preciso, então %1$s nós recomendamos a instalação e utilização de %2$s GeoIP %3$s.%4$s", "DefaultLocationProviderExplanation": "Você está usando o provedor de localização padrão, que significa que o Matomo irá supor a localização do visitante, baseado no idioma que eles usam. %1$sLeia isso%2$s para aprender como configurar uma geolocalização mais precisa.", "DistinctCountries": "%s países distintos", "DownloadingDb": "Baixando %s", - "DownloadNewDatabasesEvery": "Atualiza o banco de dados a cada", + "DownloadNewDatabasesEvery": "Atualizar o banco de dados a cada", "FatalErrorDuringDownload": "Um erro fatal ocorreu durante o download deste arquivo. Pode haver algo de errado com sua conexão de internet, com o banco de dados GeoIP que você baixou ou Matomo. Tente fazer o download e instalá-lo manualmente.", "FoundApacheModules": "Matomo encontrou os seguintes módulos do Apache", "FromDifferentCities": "cidades diferentes", "GeoIPCannotFindMbstringExtension": "Não é possível encontrar a função %1$s. Por favor, certifique-se que a extensão %2$s está instalada e carregada.", - "GeoIPDatabases": "GeoIP Databases", + "GeoIPDatabases": "Bases de dados GeoIP", "GeoIPDocumentationSuffix": "Para ver os dados para este relatório, você deve configurar GeoIP na guia de administração de Geolocalização. Os %1$s banco de dados GeoIP Maxmind comerciais %2$s são mais precisos que os livres. Para ver como são precisos, clique %3$saqui%4$s.", + "GeoIPNoDatabaseFound": "Esta implementação GeoIP não encontrou nenhuma base de dados.", "GeoIPImplHasAccessTo": "Esta implementação GeoIP tem acesso aos seguintes tipos de bancos de dados", "GeoIPIncorrectDatabaseFormat": "Seu banco de dados GeoIP não parece ter o formato correto. Ele pode estar corrompido. Certifique-se de que você está usando a versão binária e tente substituí-lo com outra cópia.", "GeoIpLocationProviderDesc_Pecl1": "Esse provedor de localização utiliza um banco de dados GeoIP e um módulo PECL para de forma precisa e eficaz determinar a localização seus visitantes.", "GeoIpLocationProviderDesc_Pecl2": "Não existem limitações com este fornecedor, por isso é o que nós recomendamos usar.", + "GeoIpLocationProviderDesc_Php1": "Este provedor de localização é o mais simples de ser instalado pois não requer configuração de servidor (ideal para hospedagem compartilhada!). Ele usa uma base de dados GeoIP e a API PHP da MaxMind para determinar com precisão a localização dos seus visitantes.", "GeoIpLocationProviderDesc_Php2": "Se o seu site recebe uma grande quantidade de tráfego, você pode achar que o provedor de localização está muito lento. Neste caso, você deve instalar a extensão %1$s PECL %2$s ou o %3$s módulo do servidor %4$s.", "GeoIpLocationProviderDesc_ServerBased1": "Este provedor de localização utiliza o módulo GeoIP que foi instalado em seu servidor HTTP. Este provedor é rápido e preciso, mas %1$s só pode ser usado com acompanhamento browser normal. %2$s", "GeoIpLocationProviderDesc_ServerBased2": "Se você tiver que importar arquivos de log ou fazer outra coisa que requer o estabelecimento de endereços IP, use a %1$s implementação PECL GeoIP (recomendado) %2$s ou %3$s PHP implementação GeoIP %4$s.", @@ -63,27 +68,29 @@ "HttpServerModule": "Módulo do servidor HTTP", "InvalidGeoIPUpdatePeriod": "Período inválido para o atualizador GeoIP: %1$s. Os valores válidos são %2$s.", "IPurchasedGeoIPDBs": "Eu comprei %1$s bases de dados mais precisas de MaxMind %2$s e quero configurar as atualizações automáticas.", - "ISPDatabase": "Banco de dados ISP", - "IWantToDownloadFreeGeoIP": "Quero baixar o banco de dados GeoIP livre...", + "ISPDatabase": "Base de dados de provedores de Internet", + "IWantToDownloadFreeGeoIP": "Quero baixar a base de dados GeoIP gratuita...", "Latitude": "Latitude", + "Latitudes": "Latitudes", "Location": "Localização", - "LocationDatabase": "Banco de dados de localização", - "LocationDatabaseHint": "Um banco de dados de locais, um banco de dados de país, região ou cidade.", + "LocationDatabase": "Base de dados de localização", + "LocationDatabaseHint": "Uma base de dados de localização é uma base de dados de país, região ou cidade.", "LocationProvider": "Provedor de localização", "Longitude": "Longitude", + "Longitudes": "Longitudes", "NoDataForGeoIPReport1": "Não há dados para este relatório porque não há nenhum dado de localização disponível ou o endereço IP do visitante não pode ser geolocalizado.", - "NoDataForGeoIPReport2": "Para ativar ageolocalização mais precisa, mudar a configuração %1$s aqui%2$s e usar um %3$s banco de dados no nível de cidade %4$s.", + "NoDataForGeoIPReport2": "Para ativar a geolocalização mais precisa, mude a configuração %1$s aqui%2$s e use uma %3$s base de dados no nível de cidade %4$s.", "Organization": "Organização", - "OrgDatabase": "Banco de dados da organização", - "PeclGeoIPNoDBDir": "O módulo PECL está à procura de bancos de dados em %1$s mas este diretório não existe. Por favor, crie-o e adicione as bases de dados GeoIP a ele. Como alternativa, você pode definir %2$s para o diretório correto em seu arquivo php.ini.", - "PeclGeoLiteError": "Seu banco de dados GeoIP em %1$s é nomeado %2$s. Infelizmente, o módulo PECL não irá reconhecê-lo com este nome. Por favor, mude o nome para %3$s.", + "OrgDatabase": "Base de dados da organização", + "PeclGeoIPNoDBDir": "O módulo PECL está à procura de bases de dados em %1$s mas este diretório não existe. Por favor, crie-o e adicione as bases de dados GeoIP a ele. Como alternativa, você pode definir %2$s para o diretório correto em seu arquivo php.ini.", + "PeclGeoLiteError": "Sua base de dados GeoIP em %1$s tem o nome %2$s. Infelizmente, o módulo PECL não irá reconhecê-la com este nome. Por favor, mude o nome para %3$s.", "PiwikNotManagingGeoIPDBs": "Matomo atualmente não gerencia nenhuma base de dados GeoIP.", "PluginDescription": "Informa a localização de seus visitantes: país, região, cidade e coordenadas geográficas (latitude \/ longitude).", "Region": "Região", - "SetupAutomaticUpdatesOfGeoIP": "Configurar as atualizações automáticas de bancos de dados GeoIP", + "SetupAutomaticUpdatesOfGeoIP": "Configurar atualizações automáticas das bases de dados GeoIP", "SubmenuLocations": "Locais", "TestIPLocatorFailed": "Matomo tentou verificar a localização de um endereço IP conhecido (%1$s), mas o servidor retornou %2$s. Se este provedor estivsse configurado corretamente, ele voltaria %3$s.", - "ThisUrlIsNotAValidGeoIPDB": "O arquivo baixado não é um banco de dados GeoIP válido. Por favor, verifique novamente a URL ou baixar o arquivo manualmente.", + "ThisUrlIsNotAValidGeoIPDB": "O arquivo baixado não é uma base de dados GeoIP válida. Por favor verifique novamente a URL ou baixe o arquivo manualmente.", "ToGeolocateOldVisits": "Para obter os dados de localização para as suas visitas antigas, use o script descrito %1$s aqui %2$s.", "UnsupportedArchiveType": "Encontrado tipo de arquivo não suportado %1$s.", "UpdaterHasNotBeenRun": "O atualizador nunca foi executado.", diff --git a/app/plugins/UserCountry/lang/pt.json b/app/plugins/UserCountry/lang/pt.json index 777aee0a4..99e8d4f93 100644 --- a/app/plugins/UserCountry/lang/pt.json +++ b/app/plugins/UserCountry/lang/pt.json @@ -13,6 +13,7 @@ "Continent": "Continente", "Continents": "Continentes", "Country": "País", + "CountryCode": "Código do país", "country_a1": "Proxy anónimo", "country_a2": "Fornecedor por satélite", "country_cat": "Comunidades língua Catalã", @@ -36,6 +37,7 @@ "GeoIPIncorrectDatabaseFormat": "A sua base de dados GeoIP não parece ter o formato correto. Pode estar corrompida. Confirme que está a utilizar a versão binária e tente substituí-la por outra cópia.", "GeoIpLocationProviderDesc_Pecl1": "Este fornecedor de localização utiliza uma base de dados GeoIP e um módulo PECL para determinar com precisão e eficiência a localização dos seus visitantes.", "GeoIpLocationProviderDesc_Pecl2": "Não existem limitações com este fornecedor, pelo que recomendamos a utilização do mesmo.", + "GeoIpLocationProviderDesc_Php1": "Este fornecedor de localização é o mais simples de instalar, dado que não exige configurações no servidor (é ideal para alojamentos partilhados!). Utiliza uma base de dados GeoIP e a API PHP do MaxMind para determinar com precisão a localização dos seus visitantes.", "GeoIpLocationProviderDesc_Php2": "Se o seu site tiver muito tráfego, pode descobrir que este fornecedor de localização é demasiado lento. Neste caso, deve instalar a %1$sextensão PECL%2$s ou um %3$smódulo de servidor%4$s.", "GeoIpLocationProviderDesc_ServerBased1": "Este fornecedor de localização utiliza o módulo GeoIP que foi instalado no seu servidor HTTP. Este fornecedor é rápido e preciso, mas %1$ssó pode ser utilizado para acompanhamento normal de navegadores.%2$s", "GeoIpLocationProviderDesc_ServerBased2": "Se tiver de importar ficheiros de registo ou fazer algo mais que necessite a definição de endereços de IP, utilize a %1$simplementação PECL do GeoIP (recomendado)%2$s ou a %3$simplementação PHP dp GeoIP%4$s.", diff --git a/app/plugins/UserCountry/lang/sv.json b/app/plugins/UserCountry/lang/sv.json index fb557e859..da7362d25 100644 --- a/app/plugins/UserCountry/lang/sv.json +++ b/app/plugins/UserCountry/lang/sv.json @@ -13,6 +13,7 @@ "Continent": "Kontinent", "Continents": "Kontinenter", "Country": "Land", + "CountryCode": "Landskod", "country_a1": "Anonym proxy", "country_a2": "Satellitleverantör", "country_cat": "Katalanska språkgemenskaperna", diff --git a/app/plugins/UserCountry/lang/zh-cn.json b/app/plugins/UserCountry/lang/zh-cn.json index 4b932acd1..06fa0424b 100644 --- a/app/plugins/UserCountry/lang/zh-cn.json +++ b/app/plugins/UserCountry/lang/zh-cn.json @@ -13,10 +13,12 @@ "Continent": "大洲", "Continents": "大洲", "Country": "国家", + "CountryCode": "国家代码", "country_a1": "匿名代理", "country_a2": "卫星供应商", "country_cat": "加泰罗尼亚语社区", "country_o1": "其它国家", + "VisitLocation": "参观地点", "CurrentLocationIntro": "根据提供商信息,您现在的位置是", "DefaultLocationProviderDesc1": "默认的地理位置查询以访客的浏览器猜测国家。", "DefaultLocationProviderDesc2": "这不是很准确,所以 %1$s我们建议安装使用 %2$sGeoIP%3$s.%4$s", @@ -30,10 +32,12 @@ "GeoIPCannotFindMbstringExtension": "没有找到 %1$s 功能。请确认 %2$s 扩展已安装和加载。", "GeoIPDatabases": "GeoIP 数据库", "GeoIPDocumentationSuffix": "要看到本报表的数据,您需要在管理页面的地理位置菜单下设置 GeoIP. 商业版 %1$sMaxmind%2$s GeoIP 数据库比免费的更精确。要查看详细内容,请点%3$s这里%4$s.", + "GeoIPNoDatabaseFound": "此GeoIP实现无法找到任何数据库。", "GeoIPImplHasAccessTo": "这个 GeoIP 方案可以读取以下类型的数据库", "GeoIPIncorrectDatabaseFormat": "你的GeoIP数据库似乎不是正确的格式。它可能已损坏。确保您所使用的是二进制版本。", "GeoIpLocationProviderDesc_Pecl1": "这个地理位置服务商使用 GeoIP 数据库和 PECL 模块来精确、有效地定位访客的地址。", "GeoIpLocationProviderDesc_Pecl2": "这个服务商没有限制,我们推荐使用。", + "GeoIpLocationProviderDesc_Php1": "该位置提供程序是最简单的安装,因为它不需要服务器配置(共享主机的理想选择!)。 它使用GeoIP数据库和MaxMind的PHP API来准确确定访问者的位置。", "GeoIpLocationProviderDesc_Php2": "如果您的网站流量很大,这个服务速度会很慢。如果这样,您最好安装 %1$sPECL 扩展%2$s 或者 %3$s服务器模块%4$s。", "GeoIpLocationProviderDesc_ServerBased1": "本地理位置服务商使用已经安装在 HTTP 服务器上的 GeoIP 模块。本服务商速度快也更精确,但是 %1$s只能使用一般的浏览器跟踪。%2$s", "GeoIpLocationProviderDesc_ServerBased2": "如果您需要导入日志文件,或者需要设置 IP 地址的操作,使用 %1$sPECL GeoIP 方案 (推荐)%2$s 或者 %3$sPHP GeoIP 方案%4$s。", diff --git a/app/plugins/UserCountryMap/Controller.php b/app/plugins/UserCountryMap/Controller.php index 21b862fad..854362f73 100644 --- a/app/plugins/UserCountryMap/Controller.php +++ b/app/plugins/UserCountryMap/Controller.php @@ -16,6 +16,7 @@ use Piwik\Piwik; use Piwik\Plugins\Goals\API as APIGoals; use Piwik\Plugins\VisitsSummary\API as VisitsSummaryAPI; +use Piwik\SettingsPiwik; use Piwik\Site; use Piwik\Translation\Translator; use Piwik\View; @@ -286,10 +287,7 @@ private function getMetrics($idSite, $period, $date, $token_auth) $metrics = array(); if (!empty($metaData[0]['metrics']) && is_array($metaData[0]['metrics'])) { foreach ($metaData[0]['metrics'] as $id => $val) { - // todo: should use SettingsPiwik::isUniqueVisitorsEnabled ? - if (Common::getRequestVar('period') == 'day' || $id != 'nb_uniq_visitors') { - $metrics[] = array($id, $val); - } + $metrics[] = array($id, $val); } } if (!empty($metaData[0]['processedMetrics']) && is_array($metaData[0]['processedMetrics'])) { diff --git a/app/plugins/UserCountryMap/UserCountryMap.php b/app/plugins/UserCountryMap/UserCountryMap.php index b2b47e2a7..392df7291 100644 --- a/app/plugins/UserCountryMap/UserCountryMap.php +++ b/app/plugins/UserCountryMap/UserCountryMap.php @@ -31,11 +31,17 @@ public function registerEvents() $hooks = array( 'AssetManager.getJavaScriptFiles' => 'getJsFiles', 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', - 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys' + 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', ); return $hooks; } + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = 'General_Visitors.UserCountryMap_RealTimeMap'; + } + public function getJsFiles(&$jsFiles) { $jsFiles[] = "libs/bower_components/visibilityjs/lib/visibility.core.js"; diff --git a/app/plugins/UserCountryMap/config/config.php b/app/plugins/UserCountryMap/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/UserCountryMap/config/config.php +++ b/app/plugins/UserCountryMap/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UserCountryMap/config/tracker.php b/app/plugins/UserCountryMap/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/UserCountryMap/config/tracker.php +++ b/app/plugins/UserCountryMap/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UserCountryMap/lang/es-ar.json b/app/plugins/UserCountryMap/lang/es-ar.json index b2fa73130..5a3c17ab9 100644 --- a/app/plugins/UserCountryMap/lang/es-ar.json +++ b/app/plugins/UserCountryMap/lang/es-ar.json @@ -1,5 +1,6 @@ { "UserCountryMap": { + "PluginDescription": "Este plugin ofrece los widgets Visitor Map y Real-time Map. Nota: requiere que el plugin UserCountry esté habilitado.", "AndNOthers": "y %s otros", "Cities": "Ciudades", "Countries": "Países", @@ -10,13 +11,17 @@ "MinutesAgo": "Hace %s minutos", "None": "Ninguno", "NoVisit": "Ninguna visita", - "RealTimeMap": "Mapa en Tiempo Real", + "RealTimeMap": "Mapa en tiempo real", "Regions": "Regiones", "Searches": "%s búsquedas", "SecondsAgo": "Hace %s segundos", - "ShowingVisits": "Geo-ubicadas últimas visitas", - "Unlocated": "%s<\/b> %p de las visitas desde %c no pudieron ser geo ubicadas.", - "VisitorMap": "Mapa de Visitantes", - "WorldWide": "Mundial" + "ShowingVisits": "Visitas geolocalizadas de los últimos", + "Unlocated": "%s<\/b> %p de las visitas desde %c no pudieron ser geolocalizadas.", + "VisitorMap": "Mapa de visitantes", + "WorldWide": "Mundial", + "WithUnknownRegion": "%s con región desconocida", + "WithUnknownCity": "%s con ciudad desconocida", + "NoVisitsInfo": "No hay visitas mostradas actualmente, porque ninguna visita en este período tiene información correcta de geolocalización (latitud y longitud).", + "NoVisitsInfo2": "Para resolver este problema, asegurate que estás usando un proveedor de geolocalización GeoIP con base de datos de ciudades GeoIP. Si esto no resuelve tu problema, entonces es posible (pero poco probable) que tus visitantes tengan direcciones IP que no puedan ser geolocalizadas." } } \ No newline at end of file diff --git a/app/plugins/UserCountryMap/lang/zh-cn.json b/app/plugins/UserCountryMap/lang/zh-cn.json index ff39a730f..03cbf8b5e 100644 --- a/app/plugins/UserCountryMap/lang/zh-cn.json +++ b/app/plugins/UserCountryMap/lang/zh-cn.json @@ -20,6 +20,8 @@ "VisitorMap": "访客地图", "WorldWide": "全世界", "WithUnknownRegion": "%s未知区域", - "WithUnknownCity": "%s未知城市" + "WithUnknownCity": "%s未知城市", + "NoVisitsInfo": "当前没有显示访问,因为此期间没有访问具有正确的地理位置信息(纬度和经度)。", + "NoVisitsInfo2": "要解决此问题,请确保将GeoIP地理位置提供程序与GeoIP城市数据库配合使用。 如果这不能解决您的问题,则您的访问很可能(尽管不太可能)具有无法进行地理位置定位的IP地址。" } } \ No newline at end of file diff --git a/app/plugins/UserId/Archiver.php b/app/plugins/UserId/Archiver.php index ae7fdcee8..07e6be51d 100644 --- a/app/plugins/UserId/Archiver.php +++ b/app/plugins/UserId/Archiver.php @@ -83,7 +83,6 @@ protected function aggregateUsers() $rankingQuery = false; if ($rankingQueryLimit > 0) { $rankingQuery = new RankingQuery($rankingQueryLimit); - $rankingQuery->setOthersLabel(DataTable::LABEL_SUMMARY_ROW); $rankingQuery->addLabelColumn($userIdFieldName); $rankingQuery->addLabelColumn($visitorIdFieldName); } @@ -119,21 +118,6 @@ protected function insertDayReports() { /** @var DataTable $dataTable */ $dataTable = $this->arrays->asDataTable(); - - // deal w/ ranking query summary row - $rankingQuerySummaryRow = $dataTable->getRowFromLabel(DataTable::LABEL_SUMMARY_ROW); - if ($rankingQuerySummaryRow) { - $rankingQuerySummaryRowId = $dataTable->getRowIdFromLabel(DataTable::LABEL_SUMMARY_ROW); - $dataTable->deleteRow($rankingQuerySummaryRowId); - - $actualSummaryRow = $dataTable->getRowFromId(DataTable::ID_SUMMARY_ROW); - if ($actualSummaryRow) { - $actualSummaryRow->sumRow($rankingQuerySummaryRow); - } else { - $dataTable->addSummaryRow($rankingQuerySummaryRow); - } - } - $this->setVisitorIds($dataTable); $report = $dataTable->getSerialized($this->maximumRowsInDataTableLevelZero, null, PiwikMetrics::INDEX_NB_VISITS); $this->getProcessor()->insertBlobRecord(self::USERID_ARCHIVE_RECORD, $report); diff --git a/app/plugins/UserId/config/config.php b/app/plugins/UserId/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/UserId/config/config.php +++ b/app/plugins/UserId/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UserId/config/tracker.php b/app/plugins/UserId/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/UserId/config/tracker.php +++ b/app/plugins/UserId/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UserId/lang/id.json b/app/plugins/UserId/lang/id.json index 28bd2a775..2ec10bf80 100644 --- a/app/plugins/UserId/lang/id.json +++ b/app/plugins/UserId/lang/id.json @@ -1,6 +1,7 @@ { "UserId": { "UserId": "IdPengguna", + "UserReportTitle": "ID pengguna", "PluginDescription": "Tampilkan laporan pengguna" } } \ No newline at end of file diff --git a/app/plugins/UserId/lang/pt-br.json b/app/plugins/UserId/lang/pt-br.json index ab2513c7a..a4429390f 100644 --- a/app/plugins/UserId/lang/pt-br.json +++ b/app/plugins/UserId/lang/pt-br.json @@ -1,7 +1,7 @@ { "UserId": { "UserId": "UserId", - "UserReportTitle": "Usuário IDs", + "UserReportTitle": "IDs de usuário", "PluginDescription": "Mostra relatórios de usuários" } } \ No newline at end of file diff --git a/app/plugins/UserId/lang/vi.json b/app/plugins/UserId/lang/vi.json index 20bd27443..b4e3df8d9 100644 --- a/app/plugins/UserId/lang/vi.json +++ b/app/plugins/UserId/lang/vi.json @@ -1,6 +1,7 @@ { "UserId": { "UserId": "ID người dùng", + "UserReportTitle": "ID người dùng", "PluginDescription": "Hiển thị báo cáo người dùng" } } \ No newline at end of file diff --git a/app/plugins/UserLanguage/config/config.php b/app/plugins/UserLanguage/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/UserLanguage/config/config.php +++ b/app/plugins/UserLanguage/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UserLanguage/config/tracker.php b/app/plugins/UserLanguage/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/UserLanguage/config/tracker.php +++ b/app/plugins/UserLanguage/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UserLanguage/lang/de.json b/app/plugins/UserLanguage/lang/de.json index b730c1cab..1d6e1bfd8 100644 --- a/app/plugins/UserLanguage/lang/de.json +++ b/app/plugins/UserLanguage/lang/de.json @@ -2,6 +2,6 @@ "UserLanguage": { "BrowserLanguage": "Browsersprache", "LanguageCode": "Sprach-Code", - "PluginDescription": "Bericht darüber, welche Sprache die Besucher im Browser eingestellt haben." + "PluginDescription": "Stellt Berichte zur Verfügung, die die Sprachen anzeigen, die von den Browsern Ihrer Besucher verwendet werden." } } \ No newline at end of file diff --git a/app/plugins/UserLanguage/lang/pt-br.json b/app/plugins/UserLanguage/lang/pt-br.json index e7c6a36d3..5c9763e11 100644 --- a/app/plugins/UserLanguage/lang/pt-br.json +++ b/app/plugins/UserLanguage/lang/pt-br.json @@ -1,7 +1,7 @@ { "UserLanguage": { "BrowserLanguage": "Idioma do navegador", - "LanguageCode": "Código do Idioma", + "LanguageCode": "Código do idioma", "PluginDescription": "Informa o idioma usado pelos navegadores dos visitantes." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/API.php b/app/plugins/UsersManager/API.php index b25a1d6f7..d7f6bc33e 100644 --- a/app/plugins/UsersManager/API.php +++ b/app/plugins/UsersManager/API.php @@ -323,19 +323,27 @@ public function getUsersPlusRole($idSite, $limit = null, $offset = 0, $filter_se $loginsToLimit = $this->model->getUsersWithAccessToSites($adminIdSites); } - list($users, $totalResults) = $this->model->getUsersWithRole($idSite, $limit, $offset, $filter_search, $filter_access, $loginsToLimit); - - foreach ($users as &$user) { - $user['superuser_access'] = $user['superuser_access'] == 1; - if ($user['superuser_access']) { - $user['role'] = 'superuser'; - $user['capabilities'] = []; - } else { - list($user['role'], $user['capabilities']) = $this->getRoleAndCapabilitiesFromAccess($user['access']); - $user['role'] = empty($user['role']) ? 'noaccess' : reset($user['role']); - } + if ($loginsToLimit !== null && empty($loginsToLimit)) { + // if the current user is not the superuser, and getUsersWithAccessToSites() returned an empty result, + // access is managed by another plugin, and the current user cannot manage any user with UsersManager + Common::sendHeader('X-Matomo-Total-Results: 0'); + return []; - unset($user['access']); + } else { + list($users, $totalResults) = $this->model->getUsersWithRole($idSite, $limit, $offset, $filter_search, $filter_access, $loginsToLimit); + + foreach ($users as &$user) { + $user['superuser_access'] = $user['superuser_access'] == 1; + if ($user['superuser_access']) { + $user['role'] = 'superuser'; + $user['capabilities'] = []; + } else { + list($user['role'], $user['capabilities']) = $this->getRoleAndCapabilitiesFromAccess($user['access']); + $user['role'] = empty($user['role']) ? 'noaccess' : reset($user['role']); + } + + unset($user['access']); + } } } @@ -651,6 +659,7 @@ private function getCleanAlias($alias, $userLogin) public function addUser($userLogin, $password, $email, $alias = false, $_isPasswordHashed = false, $initialIdSite = null) { Piwik::checkUserHasSomeAdminAccess(); + UsersManager::dieIfUsersAdminIsDisabled(); if (!Piwik::hasUserSuperUserAccess()) { if (empty($initialIdSite)) { @@ -709,6 +718,7 @@ public function setSuperUserAccess($userLogin, $hasSuperUserAccess, $passwordCon { Piwik::checkUserHasSuperUserAccess(); $this->checkUserIsNotAnonymous($userLogin); + UsersManager::dieIfUsersAdminIsDisabled(); $requirePasswordConfirmation = self::$SET_SUPERUSER_ACCESS_REQUIRE_PASSWORD_CONFIRMATION; self::$SET_SUPERUSER_ACCESS_REQUIRE_PASSWORD_CONFIRMATION = true; @@ -874,6 +884,7 @@ public function updateUser($userLogin, $password = false, $email = false, $alias $isEmailNotificationOnInConfig = Config::getInstance()->General['enable_update_users_email']; Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin); + UsersManager::dieIfUsersAdminIsDisabled(); $this->checkUserIsNotAnonymous($userLogin); $this->checkUserExists($userLogin); @@ -912,7 +923,9 @@ public function updateUser($userLogin, $password = false, $email = false, $alias $email = $userInfo['email']; } - if ($email != $userInfo['email']) { + $hasEmailChanged = Common::mb_strtolower($email) !== Common::mb_strtolower($userInfo['email']); + + if ($hasEmailChanged) { $this->checkEmail($email); $changeShouldRequirePasswordConfirmation = true; } @@ -927,7 +940,7 @@ public function updateUser($userLogin, $password = false, $email = false, $alias Cache::deleteTrackerCache(); - if ($email != $userInfo['email'] && $isEmailNotificationOnInConfig) { + if ($hasEmailChanged && $isEmailNotificationOnInConfig) { $this->sendEmailChangedEmail($userInfo, $email); } @@ -957,6 +970,7 @@ public function updateUser($userLogin, $password = false, $email = false, $alias public function deleteUser($userLogin) { Piwik::checkUserHasSuperUserAccess(); + UsersManager::dieIfUsersAdminIsDisabled(); $this->checkUserIsNotAnonymous($userLogin); $this->checkUserExist($userLogin); @@ -969,6 +983,7 @@ public function deleteUser($userLogin) } $this->model->deleteUserOnly($userLogin); + $this->model->deleteUserOptions($userLogin); $this->model->deleteUserAccess($userLogin); Cache::deleteTrackerCache(); @@ -1048,6 +1063,8 @@ public function getUserLoginFromUserEmail($userEmail) */ public function setUserAccess($userLogin, $access, $idSites) { + UsersManager::dieIfUsersAdminIsDisabled(); + if ($access != 'noaccess') { $this->checkAccessType($access); } @@ -1362,6 +1379,18 @@ public function getTokenAuth($userLogin, $md5Password) return $user['token_auth']; } + public function newsletterSignup() + { + Piwik::checkUserIsNotAnonymous(); + + $userLogin = Piwik::getCurrentUserLogin(); + $email = Piwik::getCurrentUserEmail(); + + $success = NewsletterSignup::signupForNewsletter($userLogin, $email, true); + $result = $success ? array('success' => true) : array('error' => true); + return $result; + } + private function isUserHasAdminAccessTo($idSite) { try { diff --git a/app/plugins/UsersManager/Controller.php b/app/plugins/UsersManager/Controller.php index 9b0c9b475..3d6732746 100644 --- a/app/plugins/UsersManager/Controller.php +++ b/app/plugins/UsersManager/Controller.php @@ -13,7 +13,9 @@ use Piwik\API\ResponseBuilder; use Piwik\Common; use Piwik\Container\StaticContainer; +use Piwik\Option; use Piwik\Piwik; +use Piwik\Plugin; use Piwik\Plugin\ControllerAdmin; use Piwik\Plugins\LanguagesManager\API as APILanguagesManager; use Piwik\Plugins\LanguagesManager\LanguagesManager; @@ -52,6 +54,7 @@ public function index() { Piwik::checkUserIsNotAnonymous(); Piwik::checkUserHasSomeAdminAccess(); + UsersManager::dieIfUsersAdminIsDisabled(); $view = new View('@UsersManager/index'); @@ -182,6 +185,11 @@ public function userSettings() $view->userEmail = $user['email']; $view->userTokenAuth = Piwik::getCurrentUserTokenAuth(); $view->ignoreSalt = $this->getIgnoreCookieSalt(); + $view->isUsersAdminEnabled = UsersManager::isUsersAdminEnabled(); + + $newsletterSignupOptionKey = NewsletterSignup::NEWSLETTER_SIGNUP_OPTION . $userLogin; + $view->showNewsletterSignup = Option::get($newsletterSignupOptionKey) === false + && SettingsPiwik::isInternetEnabled(); $userPreferences = new UserPreferences(); $defaultReport = $userPreferences->getDefaultReport(); @@ -205,11 +213,14 @@ public function userSettings() $view->defaultReportSiteName = Site::getNameFor($defaultReport); } - $view->defaultReportOptions = array( - array('key' => 'MultiSites', 'value' => Piwik::translate('General_AllWebsitesDashboard')), - array('key' => $reportOptionsValue, 'value' => Piwik::translate('General_DashboardForASpecificWebsite')), - ); + $defaultReportOptions = array(); + if (Plugin\Manager::getInstance()->isPluginActivated('MultiSites')) { + $defaultReportOptions[] = array('key' => 'MultiSites', 'value' => Piwik::translate('General_AllWebsitesDashboard')); + } + + $defaultReportOptions[] = array('key' => $reportOptionsValue, 'value' => Piwik::translate('General_DashboardForASpecificWebsite')); + $view->defaultReportOptions = $defaultReportOptions; $view->defaultDate = $this->getDefaultDateForUser($userLogin); $view->availableDefaultDates = $this->getDefaultDates(); @@ -378,7 +389,9 @@ public function recordUserSettings() Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin); - $this->processPasswordChange($userLogin); + if (UsersManager::isUsersAdminEnabled()) { + $this->processPasswordChange($userLogin); + } LanguagesManager::setLanguageForSession($language); diff --git a/app/plugins/UsersManager/Menu.php b/app/plugins/UsersManager/Menu.php index 77fc8ced0..7945480d4 100644 --- a/app/plugins/UsersManager/Menu.php +++ b/app/plugins/UsersManager/Menu.php @@ -15,7 +15,7 @@ class Menu extends \Piwik\Plugin\Menu { public function configureAdminMenu(MenuAdmin $menu) { - if (Piwik::isUserHasSomeAdminAccess()) { + if (Piwik::isUserHasSomeAdminAccess() && UsersManager::isUsersAdminEnabled()) { $menu->addSystemItem('UsersManager_MenuUsers', $this->urlForAction('index'), $order = 15); } diff --git a/app/plugins/UsersManager/Model.php b/app/plugins/UsersManager/Model.php index 22d7413cc..839fdbd57 100644 --- a/app/plugins/UsersManager/Model.php +++ b/app/plugins/UsersManager/Model.php @@ -12,6 +12,7 @@ use Piwik\Common; use Piwik\Date; use Piwik\Db; +use Piwik\Option; use Piwik\Piwik; use Piwik\Plugins\SitesManager\SitesManager; use Piwik\Plugins\UsersManager\Sql\SiteAccessFilter; @@ -392,6 +393,11 @@ public function deleteUserOnly($userLogin) Piwik::postEvent('UsersManager.deleteUser', array($userLogin)); } + public function deleteUserOptions($userLogin) + { + Option::deleteLike('UsersManager.%.' . $userLogin); + } + /** * @param string $userLogin */ diff --git a/app/plugins/UsersManager/NewsletterSignup.php b/app/plugins/UsersManager/NewsletterSignup.php new file mode 100644 index 000000000..af489f556 --- /dev/null +++ b/app/plugins/UsersManager/NewsletterSignup.php @@ -0,0 +1,53 @@ +General['api_service_url']; + $url .= '/1.0/subscribeNewsletter/'; + + $params = array( + 'email' => $email, + 'piwikorg' => (int)$matomoOrg, + 'piwikpro' => (int)$professionalServices, + 'url' => Url::getCurrentUrlWithoutQueryString(), + 'language' => StaticContainer::get('Piwik\Translation\Translator')->getCurrentLanguage(), + ); + + $url .= '?' . Http::buildQuery($params); + try { + Http::sendHttpRequest($url, $timeout = 2); + $optionKey = self::NEWSLETTER_SIGNUP_OPTION . $userLogin; + Option::set($optionKey, 1); + return true; + } catch (Exception $e) { + return false; + } + } +} \ No newline at end of file diff --git a/app/plugins/UsersManager/UsersManager.php b/app/plugins/UsersManager/UsersManager.php index a844aee25..0340acda2 100644 --- a/app/plugins/UsersManager/UsersManager.php +++ b/app/plugins/UsersManager/UsersManager.php @@ -14,9 +14,11 @@ use Piwik\API\Request; use Piwik\Auth\Password; use Piwik\Common; +use Piwik\Config; use Piwik\Option; use Piwik\Piwik; use Piwik\Plugins\CoreHome\SystemSummary; +use Piwik\Plugins\CorePluginsAdmin\CorePluginsAdmin; use Piwik\SettingsPiwik; /** @@ -45,8 +47,25 @@ public function registerEvents() ); } + public static function isUsersAdminEnabled() + { + return (bool) Config::getInstance()->General['enable_users_admin']; + } + + public static function dieIfUsersAdminIsDisabled() + { + Piwik::checkUserIsNotAnonymous(); + if (!self::isUsersAdminEnabled()) { + throw new \Exception('Creating, updating, and deleting users has been disabled.'); + } + } + public function addSystemSummaryItems(&$systemSummary) { + if (!self::isUsersAdminEnabled()) { + return; + } + $userLogins = Request::processRequest('UsersManager.getUsersLogin', array('filter_limit' => '-1')); $numUsers = count($userLogins); @@ -239,6 +258,7 @@ public function getClientSideTranslationKeys(&$translationKeys) $translationKeys[] = "General_Save"; $translationKeys[] = "General_Done"; $translationKeys[] = "General_Pagination"; + $translationKeys[] = "General_PleaseTryAgain"; $translationKeys[] = "UsersManager_DeleteConfirm"; $translationKeys[] = "UsersManager_ConfirmGrantSuperUserAccess"; $translationKeys[] = "UsersManager_ConfirmProhibitOtherUsersSuperUserAccess"; @@ -314,6 +334,7 @@ public function getClientSideTranslationKeys(&$translationKeys) $translationKeys[] = 'General_Warning'; $translationKeys[] = 'General_Add'; $translationKeys[] = 'General_Note'; + $translationKeys[] = 'General_Yes'; $translationKeys[] = 'UsersManager_FilterByWebsite'; $translationKeys[] = 'UsersManager_GiveAccessToAll'; $translationKeys[] = 'UsersManager_OrManageIndividually'; @@ -324,5 +345,7 @@ public function getClientSideTranslationKeys(&$translationKeys) $translationKeys[] = 'UsersManager_AreYouSureAddCapability'; $translationKeys[] = 'UsersManager_AreYouSureRemoveCapability'; $translationKeys[] = 'UsersManager_IncludedInUsersRole'; + $translationKeys[] = 'UsersManager_NewsletterSignupFailureMessage'; + $translationKeys[] = 'UsersManager_NewsletterSignupSuccessMessage'; } } diff --git a/app/plugins/UsersManager/config/config.php b/app/plugins/UsersManager/config/config.php index 4932533ad..d266508bc 100644 --- a/app/plugins/UsersManager/config/config.php +++ b/app/plugins/UsersManager/config/config.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UsersManager/config/tracker.php b/app/plugins/UsersManager/config/tracker.php index febb40801..ca2affec3 100644 --- a/app/plugins/UsersManager/config/tracker.php +++ b/app/plugins/UsersManager/config/tracker.php @@ -1,3 +1,2 @@ \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/da.json b/app/plugins/UsersManager/lang/da.json index b9e43213a..11ce93022 100644 --- a/app/plugins/UsersManager/lang/da.json +++ b/app/plugins/UsersManager/lang/da.json @@ -1,10 +1,16 @@ { "UsersManager": { + "2FA": "2FA", "UsesTwoFactorAuthentication": "Benytter tofaktorgodkendelse", "TwoFactorAuthentication": "Tofaktorgodkendelse", "ResetTwoFactorAuthentication": "Nulstil tofaktorgodkendelse", + "ResetTwoFactorAuthenticationInfo": "Hvis en bruger ikke længere kan logge ind pga. mistede genskabelseskoder eller mistet tofaktorgodkendelsesenhed, kan du nulstille tofaktorgodkendelse for brugeren, så de kan logge ind igen.", "AddUser": "Opret ny bruger", + "AddExistingUser": "Tilføj en eksisterende bruger", + "AddNewUser": "Tilføj ny bruger", "EditUser": "Rediger bruger", + "CreateUser": "Opret bruger", + "SaveBasicInfo": "Gem basisoplysninger", "Alias": "Alias", "AllWebsites": "Alle hjemmesider", "AnonymousUser": "Anonym bruger", diff --git a/app/plugins/UsersManager/lang/de.json b/app/plugins/UsersManager/lang/de.json index 169280cee..89355de9a 100644 --- a/app/plugins/UsersManager/lang/de.json +++ b/app/plugins/UsersManager/lang/de.json @@ -161,6 +161,10 @@ "EmailChangedEmail2": "Diese Änderung wurde von folgendem Gerät veranlasst: %1$s (IP-Adresse = %2$s).", "IfThisWasYouIgnoreIfNot": "Falls Sie dies waren, können Sie diese E-Mail ignonieren. Falls Sie dies nicht waren, loggen Sie sich bitte ein, korrigieren Sie Ihre E-Mail-Adresse, ändern Sie Ihr Passwort und kontaktieren Sie Ihren Matomo-Administrator.", "PasswordChangeNotificationSubject": "Das Passwort Ihres Matomo Benutzerkontos wurde soeben geändert", - "PasswordChangedEmail": "Ihr Passwort wurde soeben geändert. Diese Änderung wurde von diesem Gerät veranlasst: %1$s (IP-Adresse = %2$s)." + "PasswordChangedEmail": "Ihr Passwort wurde soeben geändert. Diese Änderung wurde von diesem Gerät veranlasst: %1$s (IP-Adresse = %2$s).", + "NewsletterSignupTitle": "Newsletter-Anmeldung", + "NewsletterSignupMessage": "Abonnieren Sie unseren Newsletter, um regelmäßig Informationen über Matomo zu erhalten. Deabonnieren ist jederzeit möglich. Dieser Dienst verwendet MadMimi. Erfahren Sie mehr in unserer %1$sDatenschutzerklärung%2$s.", + "NewsletterSignupFailureMessage": "Hoppla, etwas ist schief gelaufen. Wir konnten Sie nicht für unseren Newsletter registrieren.", + "NewsletterSignupSuccessMessage": "Super, Sie wurden erfolgreich registriert! Wir werden in Kürze von uns hören lassen." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/el.json b/app/plugins/UsersManager/lang/el.json index 060b73783..dd0a1bc8e 100644 --- a/app/plugins/UsersManager/lang/el.json +++ b/app/plugins/UsersManager/lang/el.json @@ -161,6 +161,10 @@ "EmailChangedEmail2": "Η αλλαγή έγινε από την παρακάτω συσκευή: %1$s (Διεύθυνση IP = %2$s).", "IfThisWasYouIgnoreIfNot": "Αν ήσασταν εσείς, αγνοήστε το παρόν μήνυμα. Αν δεν ήσασταν εσείς, παρακαλούμε κάνετε είσοδο, διορθώστε την διεύθυνση email σας, αλλάξτε το συνθηματικό σας και επικοινωνήστε με το διαχειριστή σας του Matomo.", "PasswordChangeNotificationSubject": "Μόλις άλλαξε το συνθηματικό του λογαριασμού σας στο Matomo", - "PasswordChangedEmail": "Το συνθηματικό σας μόλις άλλαξε. Η αλλαγή έγινε από την παρακάτω συσκευή: %1$s (διεύθυνση IP = %2$s)." + "PasswordChangedEmail": "Το συνθηματικό σας μόλις άλλαξε. Η αλλαγή έγινε από την παρακάτω συσκευή: %1$s (διεύθυνση IP = %2$s).", + "NewsletterSignupTitle": "Εγγραφή στα Μηνύματα με Νέα", + "NewsletterSignupMessage": "Εγγραφείτε στα μηνύματα με τα νέα για να λαμβάνετε συχνές πληροφορίες σχετικά με το Matomo. Μπορείτε να διαγραφείτε από αυτό οποιαδήποτε στιγμή. Η υπηρεσία χρησιμοποιεί το MadMimi. Μάθετε περισσότερα για αυτό στη %1$sΣελίδα Πολιτικής Ιδιωτικότητας%2$s.", + "NewsletterSignupFailureMessage": "Ουπς, κάτι λάθος συνέβη. Δεν ήταν δυνατή η εγγραφή σας στα μηνύματα με τα νέα.", + "NewsletterSignupSuccessMessage": "Τέλεια, έχετε κάνει εγγραφή! Θα έρθουμε σε επικοινωνία σύντομα." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/en.json b/app/plugins/UsersManager/lang/en.json index 866b52567..c8f7e4de4 100644 --- a/app/plugins/UsersManager/lang/en.json +++ b/app/plugins/UsersManager/lang/en.json @@ -161,6 +161,10 @@ "EmailChangedEmail2": "This change was initiated from the following device: %1$s (IP address = %2$s).", "IfThisWasYouIgnoreIfNot": "If this was you, feel free to ignore this email. If this was not you, please login, correct your email address, change your password and contact your Matomo administrator.", "PasswordChangeNotificationSubject": "Your Matomo account's password has just been changed", - "PasswordChangedEmail": "Your password has just been changed. The change was initiated from the following device: %1$s (IP address = %2$s)." + "PasswordChangedEmail": "Your password has just been changed. The change was initiated from the following device: %1$s (IP address = %2$s).", + "NewsletterSignupTitle": "Newsletter Signup", + "NewsletterSignupMessage": "Subscribe to our newsletter to receive regular information about Matomo. You can unsubscribe from it any time. This service uses MadMimi. Learn more about it on our %1$sPrivacy Policy page%2$s.", + "NewsletterSignupFailureMessage": "Whoops, something went wrong. We weren't able to sign you up for the newsletter.", + "NewsletterSignupSuccessMessage": "Super, you're all signed up! We'll be in touch soon." } } diff --git a/app/plugins/UsersManager/lang/es-ar.json b/app/plugins/UsersManager/lang/es-ar.json index a033639e3..274b490c4 100644 --- a/app/plugins/UsersManager/lang/es-ar.json +++ b/app/plugins/UsersManager/lang/es-ar.json @@ -1,47 +1,170 @@ { "UsersManager": { - "AddUser": "Añadir un nuevo usuario", + "2FA": "2FA", + "UsesTwoFactorAuthentication": "Usa autenticación de 2 factores", + "TwoFactorAuthentication": "Autenticación de 2 factores", + "ResetTwoFactorAuthentication": "Restablecer autenticación de 2 factores", + "ResetTwoFactorAuthenticationInfo": "Si el usuario no puede iniciar sesión porque perdió los códigos de recuperación o un dispositivo de autenticación, podés restablecer la autenticación de 2 factores para el usuario, así puede iniciar sesión de nuevo.", + "AddUser": "Agregar un nuevo usuario", + "AddExistingUser": "Agregar un usuario existente", + "AddNewUser": "Agregar nuevo usuario", + "EditUser": "Editar usuario", + "CreateUser": "Crear usuario", + "SaveBasicInfo": "Guardar información básica", "Alias": "Alias", "AllWebsites": "Todos los sitios web", + "AnonymousAccessConfirmation": "Estás a punto de garantizar al usuario anónimo el acceso \"de vista\" a este sitio web. Esto significa que tus informes de análisis y la información de tus visitantes será visible públicamente por todos, incluso sin iniciar sesión. ¿Estás seguro que querés continuar?", + "AnonymousUser": "Usuario anónimo", "AnonymousUserHasViewAccess": "Nota: el usuario %1$s tiene acceso para %2$s para este sitio web.", "AnonymousUserHasViewAccess2": "Tus informes de análisis y la información de tus visitantes son vistos públicamente.", "ApplyToAllWebsites": "Aplicar a todos los sitios web", - "ConfirmGrantSuperUserAccess": "Do you really want to grant '%s' Super User access? Warning: the user will have access to all websites and will be able to perform administrative tasks.", - "DeleteConfirm": "¿Está seguro que desea eliminar al usuario %s?", - "Email": "Email", - "EmailYourAdministrator": "%1$sEnviar un correo electrónico a su administrador acerca de este problema%2$s.", - "ExceptionDeleteDoesNotExist": "El usuario '%s' no existe, por lo tanto, no puede ser borrado.", - "ExceptionDeleteOnlyUserWithSuperUserAccess": "Deleting user '%s' is not possible.", - "ExceptionEmailExists": "Ya existe un usuario con el email '%s'.", - "ExceptionInvalidEmail": "El email no tiene un formato válido.", - "ExceptionPasswordMD5HashExpected": "UsersManager.getTokenAuth está a la espera de una contraseña con algoritmo hash MD5 (32 caracteres de longitud). Por favor llame a la función md5() en la contraseña antes de llamar a este método.", - "ExceptionRemoveSuperUserAccessOnlySuperUser": "Removing the Super User access from user '%s' is not possible.", - "ExceptionUserDoesNotExist": "El usuario '%s' no existe.", - "ExceptionYouMustGrantSuperUserAccessFirst": "There has to be at least one user with Super User access. Please grant Super User access to another user first.", - "ExcludeVisitsViaCookie": "Excluir sus visitas usando una cookie", - "ForAnonymousUsersReportDateToLoadByDefault": "Para los usuarios anónimos, fecha del reporte a cargar por defecto", - "InjectedHostCannotChangePwd": "Está actualmente visitando con un medio desconocido (%1$s). No puede cambiar su contraseña hasta que el problema esté soluionado.", - "ManageAccess": "Administrar el Acceso", - "MenuAnonymousUserSettings": "Configuración de usuario Anónimo", + "ChangeAllConfirm": "¿Estás seguro que querés darle a \"%s! acceso a todos los sitios web?", + "ClickHereToDeleteTheCookie": "Hacé clic acá para eliminar la cookie y dejar que Matomo rastree tus visitas", + "ClickHereToSetTheCookieOnDomain": "Hacé clic acá para establecer una cookie que excluya tus visitas en sitios web rastreados por Matomo en %s", + "ConfirmGrantSuperUserAccess": "¿Estás seguro que querés garantizarle a \"%s\" acceso de súperusuario? Advertencia: el usuario tendrá acceso a todos los sitios web y podrá ejecutar tareas administrativas.", + "ConfirmProhibitMySuperUserAccess": "%s, ¿estás seguro que querés quitar tu propio acceso de súperusuario? Vas a perder todos tus permisos y accesos a todos los sitios web y cerrarás tu sesión de Matomo.", + "ConfirmProhibitOtherUsersSuperUserAccess": "¿Estás seguro que querés quitar el acceso de súperusuario de \"%s\"? El usuario va a perder todos sus permisos y accesos a todos los sitios web. Si es necesario, luego asegurate de darle acceso a todos los sitios web necesarios.", + "DeleteConfirm": "¿Estás seguro que querés eliminar al usuario %s?", + "Email": "Correo electrónico", + "EmailYourAdministrator": "%1$sEnviale un correo electrónico a tu administrador sobre este problema%2$s.", + "EnterUsernameOrEmail": "Ingresá un nombre de usuario o dirección de correo electrónico", + "ExceptionAccessValues": "El acceso de parámetro debe tener uno de los siguientes valores: [%1$s], \"%2$s\" dado.", + "ExceptionNoRoleSet": "No hay rol establecido pero uno de estos necesita ser establecido: %s", + "ExceptionMultipleRoleSet": "Sólo un rol puede ser establecido pero se establecieron varios. Usá sólo uno de; %s", + "ExceptionAnonymousNoCapabilities": "No podés garantizar ninguna capacidad al usuario \"anónimo\".", + "ExceptionAnonymousAccessNotPossible": "Sólo podés establecer acceso %1$s o acceso %2$s al usuario \"anónimo\".", + "ExceptionDeleteDoesNotExist": "El usuario \"%s\" no existe, por lo tanto, no puede ser eliminado.", + "ExceptionDeleteOnlyUserWithSuperUserAccess": "No es posible eliminar al usuario \"%s\".", + "ExceptionEditAnonymous": "El usuario anónimo no puede ser borrado o editado. Matomo lo utiliza para definir que usuario que no se ha conectado todavía. Por ejemplo, podés hacer públicas tus estadísticas mediante el acceso \"ver\" al usuario \"anónimo\".", + "ExceptionEmailExists": "Ya existe un usuario con el correo electrónico \"%s\".", + "ExceptionInvalidEmail": "El correo electrónico no tiene un formato válido.", + "ExceptionInvalidLoginFormat": "El nombre de usuario debe tener una longitud de entre %1$s y %2$s caracteres y sólo contener letras sin signos diacríticos, números, o los caracteres subguión (\"_\"), guión (\"-\"), punto (\".\"), arroba (\"@\") o más (\"+\").", + "ExceptionInvalidPassword": "La longitud de la contraseña debe ser mayor de %1$s caracteres.", + "ExceptionInvalidPasswordTooLong": "La longitud de la contraseña debe ser menor de %1$s caracteres.", + "ExceptionLoginExists": "El nombre de usuario \"%s\" ya existe.", + "ExceptionPasswordMD5HashExpected": "\"UsersManager.getTokenAuth\" está a la espera de una contraseña con algoritmo hash MD5 (32 caracteres de longitud). Por favor, invocá la función md5() en la contraseña antes de invocar este método.", + "ExceptionRemoveSuperUserAccessOnlySuperUser": "Quitar el acceso de súperusuario a \"%s\" no es posible.", + "ExceptionSuperUserAccess": "El usuario tiene acceso de súperusuario y ya tiene permiso para acceder y modificar todos los sitios web en Matomo. Podés quitarle el acceso de súperusuario a este usuario e intentar de nuevo.", + "ExceptionUserHasSuperUserAccess": "El usuario \"%s\" tiene acceso de súperusuario y ya tiene permiso para acceder y modificar todos los sitios web en Matomo. Podés quitarle el acceso de súperusuario a este usuario e intentar de nuevo.", + "ExceptionUserDoesNotExist": "El usuario \"%s\" no existe.", + "ExceptionYouMustGrantSuperUserAccessFirst": "Tiene que haber, al menos, un usuario con acceso de súperusuario. Por favor, primero garantizale el acceso de súperusuario a otro usuario.", + "ExceptionUserHasViewAccessAlready": "Este usuario ya tiene acceso a este sitio web.", + "ExceptionNoValueForUsernameOrEmail": "Por favor, ingresá un nombre de usuario o direccion de correo electrónico.", + "ExcludeVisitsViaCookie": "Excluir tus visitas usando una cookie", + "ForAnonymousUsersReportDateToLoadByDefault": "Para los usuarios anónimos, fecha del informe a cargar predeterminadamente", + "GiveUserAccess": "Dar a \"%1$s\" acceso %2$s para %3$s.", + "GiveViewAccess": "Dar acceso de vista para %1$s", + "GiveViewAccessTitle": "Dar a un usuario existente acceso para ver informes de %s", + "GiveViewAccessInstructions": "Para dar a un usuario existente acceso para %s, ingresá el nombre de usuario o dirección de correo electrónico de dicho usuario existente", + "IfYouWouldLikeToChangeThePasswordTypeANewOne": "Si querés cambiar la contraseña, ingresá una nueva. De lo contrario, dejá este campo en blanco.", + "YourCurrentPassword": "Tu contraseña actual", + "CurrentPasswordNotCorrect": "La contraseña actual que ingresaste no es correcta.", + "ConfirmWithPassword": "Por favor, ingresá tu contraseña para confirmar este cambio.", + "InjectedHostCannotChangePwd": "Actualmente estás visitando con un servidor desconocido (%1$s). No podés cambiar tu contraseña hasta que el problema sea solucionado.", + "LastSeen": "Última vez visto", + "MainDescription": "Decidí qué usuarios tienen acceso a tus sitios web. También podés darles acceso a todos los sitios web de una sola vez, eligiendo \"Aplicar a todos los sitios web\" en el selector de sitios web.", + "ManageAccess": "Administrar el acceso", + "MenuAnonymousUserSettings": "Configuración de usuario anónimo", "MenuUsers": "Usuarios", "MenuUserSettings": "Configuración de usuario", - "NoteNoAnonymousUserAccessSettingsWontBeUsed2": "Nota: No puede modificar la configuración en esta sección, debido a que no posee ningún sitio de internet que pueda ser contactado por un usuario anónimo.", - "NoUsersExist": "There are no users yet.", + "MenuPersonal": "Personal", + "PersonalSettings": "Configuración personal", + "NoteNoAnonymousUserAccessSettingsWontBeUsed2": "Nota: no podés modificar la configuración en esta sección, porque no tenés ningún sitio web que pueda ser accedido por un usuario anónimo.", + "NoUsersExist": "Todavía no hay usuarios.", + "PluginDescription": "La administración de usuarios te permite agregar nuevos usuarios, editar usuarios existentes y darles acceso para ver o administrar sitios web.", "PrivAdmin": "Admin", + "PrivAdminDescription": "Los usuarios con este rol pueden administrar un sitio web y dale acceso a otros usuarios al sitio web. También pueden hacer todo lo que el rol de %s puede hacer.", + "PrivWrite": "Escribir", + "PrivWriteDescription": "Los usuarios con este rol pueden ver todo el contenido, además de crear, administrar y eliminar entidades como Metas, Embudos, Mapas de calor, Registros de sesión y Formularios de este sitio web.", "PrivNone": "Sin acceso", "PrivView": "Ver", - "ReportDateToLoadByDefault": "Fecha del reporte a cargar por defecto", - "ReportToLoadByDefault": "Reporte a cargar por defecto", - "SuperUserAccessManagement": "Manage Super User access", - "SuperUserAccessManagementMainDescription": "Super users have the highest permissions. They can perform all administrative tasks such as adding new websites to monitor, adding users, changing user permissions, activating and deactivating plugins and even installing new plugins from the Marketplace.", - "TheLoginScreen": "Pantalla de ingreso", + "PrivViewDescription": "Un usuario con este rol puede ver todos los informes.", + "RemoveUserAccess": "Quitar acceso a \"%1$s\" de %2$s.", + "ReportDateToLoadByDefault": "Fecha del informe a cargar predeterminadamente", + "ReportToLoadByDefault": "Informe a cargar predeterminadamente", + "SuperUserAccessManagement": "Administrar acceso de súperusuario", + "SuperUserAccessManagementGrantMore": "Acá podés garantizar acceso de súperusuario a otros usuarios de Matomo. Por favor, usá esta función con cuidado.", + "SuperUserAccessManagementMainDescription": "Los súperusuarios tienen los permisos más altos. Ellos pueden ejecutar todas las tareas administrativas como agregar nuevos sitios web para monitorear, agregar usuarios, cambiar permisos de usuarios, activar y desactivar plugins e incluso instalar nuevos plugins desde el Mercado.", + "TheLoginScreen": "Pantalla de inicio de sesión", "ThereAreCurrentlyNRegisteredUsers": "Actualmente hay %s usuarios registrados.", - "TypeYourPasswordAgain": "Ingrese la contraseña de nuevo", + "TokenAuth": "Clave de API de autenticación", + "TokenRegenerateConfirmSelf": "Cambiar la clave de API de autenticación invalidará tu propia clave. Si la clave actual está en uso, vas a necesitar actualizar todos los clientes API con la nueva clave generada. ¿Estás seguro que querés cambiar tu clave de autenticación?", + "TokenRegenerateTitle": "Regenerar", + "TypeYourPasswordAgain": "Ingresá tu nueva contraseña de nuevo.", "User": "Usuario", - "UsersManagement": "Administración de Usuarios", - "UsersManagementMainDescription": "Cree nuevos usuarios o actualice los actuales. Luego puede configurar sus permisos.", - "YourUsernameCannotBeChanged": "Su nombre de usuario no puede cambiarse.", + "UserHasPermission": "%1$s actualmente tiene acceso %2$s para %3$s.", + "UserHasNoPermission": "%1$s actualmente tiene %2$s para %3$s", + "UsersManagement": "Administración de usuarios", + "UsersManagementMainDescription": "Creá nuevos usuarios o actualizá los actuales. Luego podés configurar sus permisos arriba.", + "WhenUsersAreNotLoggedInAndVisitPiwikTheyShouldAccess": "Cuando los usuarios que no hayan iniciado sesión visitan Matomo, deberían ver inicialmente", + "YourUsernameCannotBeChanged": "No se puede cambiar tu nombre de usuario.", + "YourVisitsAreIgnoredOnDomain": "%1$sTus visitas son ignoradas por Matomo en %2$s%3$s (se encontró en tu navegador web la cookie para ignorar a Matomo).", + "YourVisitsAreNotIgnored": "%1$sTus visitas no son ignoradas por Matomo%2$s (no se encontró en tu navegador web la cookie para ignorar a Matomo).", + "AddUserNoInitialAccessError": "Los nuevos usuarios tienen que darle acceso a tu sitio web. Por favor, establecé el parámetro \"initialIdSite\".", + "AtLeastView": "Vista como mínimo", + "ManageUsers": "Administrar usuarios", + "ManageUsersDesc": "Creá nuevos usuarios o actualizá a los existentes. Luego podés establecerles permisos, también desde acá.", + "NoAccessWarning": "A este usuario no se le garantizó acceso a un sitio web. Cuando inicie sesión, verá un mensaje de error. Para prevenir eso, concedele acceso a un sitio web, abajo.", + "BulkActions": "Acciones masivas", + "SetPermission": "Establecer permiso", + "RemovePermissions": "Quitar permisos", + "RolesHelp": "Los roles determinan lo que un usuario puede hacer en Matomo con respecto a un sitio web específico. Aprendé más sobre los roles de %1$svista%2$s y de %3$sadministración%4$s.", + "Role": "Rol", + "TheDisplayedWebsitesAreSelected": "Están seleccionados los %1$s sitios web mostrados.", + "ClickToSelectAll": "Hacé clic para seleccionar todos los %1$s.", + "AllWebsitesAreSelected": "Están seleccionados todos los %1$s sitios web.", + "ClickToSelectDisplayedWebsites": "Hacé clic para seleccionar los %1$s sitios web mostrados.", + "DeletePermConfirmSingle": "¿Estás seguro que querés quitarle a %1$s el acceso a %2$s?", + "DeletePermConfirmMultiple": "¿Estás seguro que querés quitarle a %1$s el acceso a los %2$s sitios web seleccionados?", + "ChangePermToSiteConfirmSingle": "¿Estás seguro que querés cambiar el rol de %1$s, de %2$s a %3$s?", + "ChangePermToSiteConfirmMultiple": "¿Estás seguro que querés cambiar el rol de %1$s a los %2$s sitios web seleccionados, a %3$s?", + "BasicInformation": "Información básica", + "Permissions": "Permisos", + "SuperUserAccess": "Acceso de súperusuario", + "FirstSiteInlineHelp": "Se requiere dar a un nuevo usuario un rol de vista para un sitio web sobre la creación. Si no se le concede acceso, el usuario verá un mensaje de error al iniciar sesión. Podés darle más permisos luego de que el usuario esté creado en la pestaña \"Permisos\" que aparecerá en la izquierda.", + "SuperUsersPermissionsNotice": "Los súperusuarios tienen acceso de administrador a todos los sitios web, por lo que no hay necesidad de administrar sus permisos por cada sitio web.", + "SuperUserIntro1": "Los súperusuarios tiene los permisos más altos. Ellos pueden ejecutar todas las tareas administrativas tales como agregar nuevos sitios web para monitorear, agregar usuarios, cambiar los permisos de los usuarios, activar y desactivar plugins e incluso instalar nuevos plugins desde el mercado. Podés garantizarle acceso de súperusuario a otros usuarios de Divezone acá.", + "SuperUserIntro2": "Por favor, usá esta función con ciudado.", + "HasSuperUserAccess": "Tiene acceso de súperusuario", + "AreYouSure": "¿Estás seguro?", + "RemoveSuperuserAccessConfirm": "Quitar acceso de súperusuario dejará a este usuario sin permisos (tendrás que agregarlos luego). Ingresá tu contraseña para continuar.", + "AddSuperuserAccessConfirm": "Dar acceso de súperusuario a un usuario estándar permitirá que tenga control total sobre Matomo y debería hacerse con moderación. Ingresá tu contraseña para continuar.", + "DeleteUsers": "Eliminar usuarios", + "UserSearch": "Búsqueda de usuarios", + "FilterByAccess": "Filtrar por acceso", + "FilterByWebsite": "Filtrar por sitio web", "ShowAll": "Mostrar todo", - "Username": "Nombre de usuario" + "Username": "Nombre de usuario", + "RoleFor": "Rol de", + "TheDisplayedUsersAreSelected": "Están seleccionados los %1$s usuarios mostrados.", + "AllUsersAreSelected": "Están seleccionados todos los %1$s usuarios.", + "ClickToSelectDisplayedUsers": "Hacé clic para seleccionar los %1$s usuarios mostrados.", + "RemoveAllAccessToThisSite": "Quitar todos los accesos a este sitio web", + "DeleteUserConfirmSingle": "¿Estás seguro que querés eliminar %1$s?", + "DeleteUserConfirmMultiple": "¿Estás seguro que querés eliminar los %1$s usuarios seleccionados?", + "DeleteUserPermConfirmSingle": "¿Estás seguro que querés cambiar el rol de %1$s, de %2$s a %3$s?", + "DeleteUserPermConfirmMultiple": "¿Estás seguro que querés cambiar el rol de los %1$s usuarios seleccionados, de %2$s a %3$s?", + "AreYouSureChangeDetails": "¿Estás seguro que querés cambiar la información de usuario de %s?", + "AnonymousUserRoleChangeWarning": "Dar al usuario %1$s el rol de %2$s hará que los datos de este sitio web sean publicos y que estén disponibles para todos, incluso sin iniciar sesión en Matomo.", + "GiveAccessToAll": "Darle a este usuario acceso a todos los sitios web", + "OrManageIndividually": "O administrá el acceso de este usuario a cada sitio web individualmente", + "ChangePermToAllSitesConfirm": "¿Estás seguro que querés darle al %1$susuario%2$s acceso a cada sitio web en los que actualmente tenés acceso de administrador?", + "ChangePermToAllSitesConfirm2": "Nota: esto sólo afectará los sitios web existentes actualmente. Los nuevos sitios web que vayás a crear no serán accesibles automáticamente para este usuario.", + "CapabilitiesHelp": "Las capacidades son habilidades individuales que se le pueden garantizar a los usuarios. Los roles pueden, predeterminadamente, garantizar ciertas capacidades. Por ejemplo, el rol de administrador le permitirá automáticamente a los usuarios editar etiquetas en el Administrador de etiquetas. Para usuarios con menos poder, sin embargo, podés darle capacidades explícitamente.", + "Capabilities": "Capacidades", + "AreYouSureAddCapability": "¿Estás seguro que querés darle a %1$s la capacidad %2$s para %3$s?", + "AreYouSureRemoveCapability": "¿Estás seguro que querés quitarle a %2$s la capacidad %1$s para %3$s?", + "IncludedInUsersRole": "Incluida en el rol de este usuario.", + "Capability": "Capacidad", + "EmailChangeNotificationSubject": "Se acaba de cambiar la dirección de correo electrónico de tu cuenta de Matomo", + "EmailChangedEmail1": "La dirección de correo electrónico asociada a tu cuenta fue cambiada a %1$s", + "EmailChangedEmail2": "Este cambio se produjo desde el siguiente dispositivo: %1$s (dirección IP: %2$s).", + "IfThisWasYouIgnoreIfNot": "Si fuiste vos, simplemente ignorá este mensaje. Si no fuiste vos, por favor, iniciá sesión, cambiá tu dirección de correo electrónico y contraseña, y contactá a tu administrador de Matomo.", + "PasswordChangeNotificationSubject": "Se acaba de cambiar la contraseña de tu cuenta de Matomo", + "PasswordChangedEmail": "Tu contraseña se cambió. El cambio se produjo desde el siguiente dispositivo: %1$s (dirección IP: %2$s).", + "NewsletterSignupTitle": "Registro en el boletín informativo", + "NewsletterSignupMessage": "Suscribite a nuestro boletín informativo para recibir regularmente información sobre Matomo. Podés desuscribirte cuando quieras. Este servicio usa MadMini. Conocé más al respecto en nuestra %1$spágina de política de privacidad%2$s.", + "NewsletterSignupFailureMessage": "Epa, algo salió mal. No pudimos suscribirte al boletín informativo.", + "NewsletterSignupSuccessMessage": "¡Joya, ya estás registrado! Nos estaremos contactando pronto." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/fi.json b/app/plugins/UsersManager/lang/fi.json index 1167d4d74..4abe81905 100644 --- a/app/plugins/UsersManager/lang/fi.json +++ b/app/plugins/UsersManager/lang/fi.json @@ -126,6 +126,7 @@ "EmailChangedEmail2": "Tämä muutos toteutettiin seuraavasta laitteesta: %1$s (IP-osoite = %2$s).", "IfThisWasYouIgnoreIfNot": "Jos se olit sinä, tämä viesti ei aiheuta toimenpiteitä. Jos se et ollut sinä, kirjaudu Matomoon, vaihda sähköpostiosoitteesi ja salasanasi, ja ole lopuksi yhteydessä käyttämäsi Matomon ylläpitoon.", "PasswordChangeNotificationSubject": "Matomo-tilisi salasana on juuri vaihdettu", - "PasswordChangedEmail": "Salasanasi on vaihdettu. Vaihto toteutettiin seuraavasta laitteesta: %1$s (IP-osoite = %2$s)." + "PasswordChangedEmail": "Salasanasi on vaihdettu. Vaihto toteutettiin seuraavasta laitteesta: %1$s (IP-osoite = %2$s).", + "NewsletterSignupTitle": "Uutiskirjeen tilaus" } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/fr.json b/app/plugins/UsersManager/lang/fr.json index ff01bff58..2ef88a11a 100644 --- a/app/plugins/UsersManager/lang/fr.json +++ b/app/plugins/UsersManager/lang/fr.json @@ -161,6 +161,10 @@ "EmailChangedEmail2": "Ce changement a été initié depuis le périphérique suivant : %1$s (Adresse IP = %2$s).", "IfThisWasYouIgnoreIfNot": "Si c'était vous, vous pouvez ignorer ce courriel. Si ce n'était pas vous, veuillez vous identifier, corrigez votre adresse de courriel, modifiez votre mot de passe et contactez votre administrateur Matomo.", "PasswordChangeNotificationSubject": "Le mot de passe de votre compte Matomo vient d'être modifié", - "PasswordChangedEmail": "Votre mot de passe vient d'être modifié. Le changement a été initié depuis le périphérique suivant : %1$s (Adresse IP = %2$s)." + "PasswordChangedEmail": "Votre mot de passe vient d'être modifié. Le changement a été initié depuis le périphérique suivant : %1$s (Adresse IP = %2$s).", + "NewsletterSignupTitle": "Enregistrement à l'infolettre", + "NewsletterSignupMessage": "Abonnez vous à notre infolettre afin de recevoir des informations régulières à propos de Matomo. Vous pouvez vous désinscrire à tout moment. Ce service utilise MadMimi. Apprenez en plus sur notre page de %1$spolitique de respect de la vie privée%2$s.", + "NewsletterSignupFailureMessage": "Oops, il y a eu un problème. Nous ne somme pas parvenus à vous enregistrer à l'infolettre.", + "NewsletterSignupSuccessMessage": "Super, vous êtes enregistré(e) ! Nous aurez bientôt de nos nouvelles." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/it.json b/app/plugins/UsersManager/lang/it.json index 20adc4278..03e1bf197 100644 --- a/app/plugins/UsersManager/lang/it.json +++ b/app/plugins/UsersManager/lang/it.json @@ -161,6 +161,10 @@ "EmailChangedEmail2": "Questo cambiamento ha avuto origine dal seguente dispositivo: %1$s (Indirizzo IP = %2$s).", "IfThisWasYouIgnoreIfNot": "Se sei stato tu, ignora questa email. Altrimenti, accedi, correggi il tuo indirizzo email, cambia la password e contatta il tuo amministratore di Matomo.", "PasswordChangeNotificationSubject": "La password del tuo account Matomo è stata cambiata adesso", - "PasswordChangedEmail": "La tua password è stata cambiata adesso. Il cambiamento ha avuto origine dal seguente dispositivo: %1$s (Indirizzo IP =%2$s)." + "PasswordChangedEmail": "La tua password è stata cambiata adesso. Il cambiamento ha avuto origine dal seguente dispositivo: %1$s (Indirizzo IP =%2$s).", + "NewsletterSignupTitle": "Iscrizione alla Newsletter", + "NewsletterSignupMessage": "Iscriviti alla nostra newsletter per ricevere regolarmente informazioni su Matomo. Puoi cancellare l'iscrizione in ogni momento. Qeusto servizio utilizza MadMimi. Leggi di più su di esso nella nostra pagina %1$sPolitiche sulla Privacy%2$s.", + "NewsletterSignupFailureMessage": "Oooops, qualcosa è andata storta. Non abbiamo potuto iscriverti alla newsletter.", + "NewsletterSignupSuccessMessage": "Super, sei iscritto! Ci sentiremo presto." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/ja.json b/app/plugins/UsersManager/lang/ja.json index dd4229d91..5040ea756 100644 --- a/app/plugins/UsersManager/lang/ja.json +++ b/app/plugins/UsersManager/lang/ja.json @@ -161,6 +161,10 @@ "EmailChangedEmail2": "この変更は次の端末から開始されました:%1$s( IP アドレス= %2$s)。", "IfThisWasYouIgnoreIfNot": "これがあなたなら、この E メールを無視してください。 そうでない場合は、ログインして電子メールアドレスを修正し、パスワードを変更して Matomo 管理者に連絡してください。", "PasswordChangeNotificationSubject": "あなたの Matomo アカウントのパスワードは変更されたばかりです", - "PasswordChangedEmail": "あなたのパスワードは変更されたばかりです。 変更は次のデバイスから開始されました:%1$s( IP アドレス= %2$s)。" + "PasswordChangedEmail": "あなたのパスワードは変更されたばかりです。 変更は次のデバイスから開始されました:%1$s( IP アドレス= %2$s)。", + "NewsletterSignupTitle": "ニュースレターに登録", + "NewsletterSignupMessage": "Matomoの情報を受信するためにニュースレターに登録してください。ニュースレターからいつでも登録を解除することができます。このサービスはMadMimiを使用しています。詳細は%1$sプライバシーポリシーの%2$sページを確認してください。", + "NewsletterSignupFailureMessage": "登録に失敗しました。", + "NewsletterSignupSuccessMessage": "登録が完了しました。" } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/pl.json b/app/plugins/UsersManager/lang/pl.json index 13b6096d5..a1faabb87 100644 --- a/app/plugins/UsersManager/lang/pl.json +++ b/app/plugins/UsersManager/lang/pl.json @@ -1,8 +1,16 @@ { "UsersManager": { "2FA": "2SA", - "TwoFactorAuthentication": "Autentykacja 2 stopniowa", + "UsesTwoFactorAuthentication": "Wykorzystuje 2 składnikowe uwierzytelnianie", + "TwoFactorAuthentication": "Uwierzytelnianie 2 składnikowe", + "ResetTwoFactorAuthentication": "Resetuj uwierzytelnienie 2 składnikowe", + "ResetTwoFactorAuthenticationInfo": "Jeśli użytkownik nie może zalogować się po utracie kodów dostępu lub utracie tokena sprzętowego, możesz zresetować uwierzytelnianie 2 składnikowe dla niego, aby umożliwić ponowne logowanie.", "AddUser": "Dodaj nowego użytkownika", + "AddExistingUser": "Dodaj istniejącego użytkownika", + "AddNewUser": "Dodaj nowego użytkownika", + "EditUser": "Edytuj użytkownika", + "CreateUser": "Utwórz użytkownika", + "SaveBasicInfo": "Zapisz Podstawowe Info", "Alias": "Alias", "AllWebsites": "Wszystkie strony", "AnonymousAccessConfirmation": "Zamierzasz przyznać anonimowemu użytkownikowi dostęp 'podgląd' do tego serwisu. To oznacza, że raporty analityczne i informacje o odwiedzających będą publicznie dostępne dla każdego nawet bez loginu. Czy na pewno chcesz kontynuować?", @@ -76,6 +84,7 @@ "YourVisitsAreIgnoredOnDomain": "%1$sTwoje odwiedziny są ignorowane przez Matomo na %2$s %3$s (Matomo zignoruje ciasteczka cookie które znajdzie w twojej przeglądarce).", "YourVisitsAreNotIgnored": "%1$sTwoje odwiedziny nie będą ignorowane przez Matomo%2$s (Matomo nie odnalazł właściwego dla wykluczenia ciasteczka cookie w twojej przeglądarce).", "ShowAll": "Pokaż wszystko", - "Username": "Nazwa użytkownika" + "Username": "Nazwa użytkownika", + "EmailChangeNotificationSubject": "Adres email powiązany z Twoim kontem Matomo został zmieniony" } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/pt-br.json b/app/plugins/UsersManager/lang/pt-br.json index 6d978edab..9b029ffa5 100644 --- a/app/plugins/UsersManager/lang/pt-br.json +++ b/app/plugins/UsersManager/lang/pt-br.json @@ -1,88 +1,147 @@ { "UsersManager": { + "2FA": "A2F", + "UsesTwoFactorAuthentication": "Usar autenticação de dois fatores", + "TwoFactorAuthentication": "Autenticação de dois fatores", + "ResetTwoFactorAuthentication": "Redefinir autenticação de dois fatores", + "ResetTwoFactorAuthenticationInfo": "Se o usuário não puder mais fazer login devido à perda dos códigos de recuperação ou à perda de um dispositivo de autenticação, você pode redefinir a autenticação de dois fatores para o usuário para que ele possa fazer login novamente.", "AddUser": "Adicionar novo usuário", "AddExistingUser": "Adicionar um usuário existente", "AddNewUser": "Adicionar novo usuário", "EditUser": "Editar usuário", "CreateUser": "Criar usuário", - "SaveBasicInfo": "Salvar Informação Básica", + "SaveBasicInfo": "Salvar informação básica", "Alias": "Apelido", - "AllWebsites": "Todos os websites", - "AnonymousAccessConfirmation": "Você está prestes a conceder para o usuário anônimo o acesso de 'visualização' para este website. Isso significa que seus relatórios e as informações de seus visitantes serão publicamente visíveis por qualquer pessoa, mesmo sem login. Vocês tem certeza que deseja continuar?", + "AllWebsites": "Todos os sites", + "AnonymousAccessConfirmation": "Você está prestes a conceder para o usuário anônimo o acesso de 'visualização' para este site. Isso significa que seus relatórios de análise e as informações de seus visitantes serão publicamente visíveis por qualquer pessoa, mesmo sem login. Você tem certeza de que deseja continuar?", "AnonymousUser": "Usuário anônimo", - "AnonymousUserHasViewAccess": "Nota: o usuário %1$s tem acesso %2$s para este site.", + "AnonymousUserHasViewAccess": "Obs: o usuário %1$s tem acesso %2$s para este site.", "AnonymousUserHasViewAccess2": "Seus relatórios de análise e suas informações de visitantes são visíveis publicamente.", - "ApplyToAllWebsites": "Aplicar a todos os websites", - "ChangeAllConfirm": "Você tem certeza que quer dar acesso a '%s' para todos os websites?", - "ClickHereToDeleteTheCookie": "Clique aqui para deletar o cookie e deixar o Matomo rastrear suas visitas", - "ClickHereToSetTheCookieOnDomain": "Clique aqui para setar um cookie que excluirá suas visitas em websites rastrados pelo Matomo em %s", + "ApplyToAllWebsites": "Aplicar a todos os sites", + "ChangeAllConfirm": "Você tem certeza que quer dar acesso a '%s' para todos os sites?", + "ClickHereToDeleteTheCookie": "Clique aqui para excluir o cookie e deixar o Matomo rastrear suas visitas", + "ClickHereToSetTheCookieOnDomain": "Clique aqui para definir um cookie que excluirá suas visitas em sites rastreados pelo Matomo em %s", "ConfirmGrantSuperUserAccess": "Você realmente deseja conceder a '%s' o acesso de Super Usuário? Aviso: o usuário terá acesso a todos os sites e será capaz de executar tarefas administrativas.", "ConfirmProhibitMySuperUserAccess": "%s, você realmente deseja remover o seu próprio acesso de Super Usuário? Você perderá todas as permissões e acessos a todos os sites e será desconectado do Matomo.", "ConfirmProhibitOtherUsersSuperUserAccess": "Você realmente deseja remover o acesso de Super Usuário de '%s'? O usuário perderá todas as permissões e acesso a todos os sites. Certifique-se de dar acesso a sites necessários mais tarde, se for preciso.", - "DeleteConfirm": "Tem certeza que deseja apagar o usuário %s?", + "DeleteConfirm": "Tem certeza que deseja excluir o usuário %s?", "Email": "E-mail", - "EmailYourAdministrator": "%1$s E-mail a seu administrador sobre este problema %2$s.", + "EmailYourAdministrator": "%1$sEnviar e-mail a seu administrador sobre este problema%2$s.", "EnterUsernameOrEmail": "Digite um nome de usuário ou endereço de e-mail", + "ExceptionAccessValues": "O parâmetro de acesso deve conter um dos seguintes valores: [ %1$s ], '%2$s' fornecido.", + "ExceptionNoRoleSet": "Nenhum perfil está definido, mas um desses precisa ser definido: %s", + "ExceptionMultipleRoleSet": "Apenas um perfil pode ser definido, mas vários foram definidos. Use somente um desses: %s", "ExceptionAnonymousNoCapabilities": "Você não pode conceder nenhuma capacidade para o usuário 'anônimo'.", - "ExceptionDeleteDoesNotExist": "O usuário '%s' não existe assim não é possível excluí-lo.", - "ExceptionDeleteOnlyUserWithSuperUserAccess": "Não é possível excluir o usuário '%s'", + "ExceptionAnonymousAccessNotPossible": "Você pode definir somente acesso %1$s ou %2$s acesso para o usuário 'anônimo(a)'.", + "ExceptionDeleteDoesNotExist": "O usuário '%s' não existe portanto não é possível excluí-lo.", + "ExceptionDeleteOnlyUserWithSuperUserAccess": "Não é possível excluir o usuário '%s'.", "ExceptionEditAnonymous": "O usuário anonymous não pode ser editado ou apagado. Ele é usado pelo sistema para definir um usuário que ainda não não entrou. Por examplo, você pode tornar as estatísticas públicas concedendo acesso 'view' para o usuário 'anonymous'.", "ExceptionEmailExists": "Usuário com o e-mail '%s' já existe.", "ExceptionInvalidEmail": "O e-mail não tem um formato válido.", "ExceptionInvalidLoginFormat": "O nome de usuário deve ter entre %1$s e %2$s caracteres e conter apenas letras, números ou os caracteres '_' or '-' or '.' or '@' or '+'", "ExceptionInvalidPassword": "O comprimento da senha deve ser maior que %1$s caracteres.", + "ExceptionInvalidPasswordTooLong": "O tamanho da senha deve ser menor que %1$s caracteres.", "ExceptionLoginExists": "O nome de usuário '%s' já existe.", - "ExceptionPasswordMD5HashExpected": "UsersManager.getTokenAuth está esperando por uma senha MD5-hashed (32 caracteres). Por favor, chame a função md5() na senha antes de chamar este método.", - "ExceptionRemoveSuperUserAccessOnlySuperUser": "Não é possível remover o acesso de Super Usuário do usuário '%s'", - "ExceptionSuperUserAccess": "Este usuário tem acesso Super Usuário e já tem permissão para acessar e modificar todos os websites no Matomo. Você pode remover o acesso de Super Usuário deste usuário e tentar novamente.", + "ExceptionPasswordMD5HashExpected": "UsersManager.getTokenAuth está esperando por um hash MD5 da senha (32 caracteres). Por favor, chame a função md5() na senha antes de chamar este método.", + "ExceptionRemoveSuperUserAccessOnlySuperUser": "Não é possível remover o acesso de Super Usuário do usuário '%s'.", + "ExceptionSuperUserAccess": "Este usuário tem acesso Super Usuário e já tem permissão para acessar e modificar todos os sites no Matomo. Você pode remover o acesso de Super Usuário deste usuário e tentar novamente.", + "ExceptionUserHasSuperUserAccess": "O usuário '%s' tem acesso Super Usuário e já tem permissão para acessar e modificar todos os sites no Matomo. Você pode remover o acesso Super Usuário deste usuário e tentar novamente.", "ExceptionUserDoesNotExist": "Usuário '%s' não existe.", "ExceptionYouMustGrantSuperUserAccessFirst": "É preciso haver pelo menos um usuário com acesso de Super Usuário. Por favor, conceda acesso de Super Usuário para outro usuário primeiro.", - "ExceptionUserHasViewAccessAlready": "Esse usuário já tem acesso a este website.", + "ExceptionUserHasViewAccessAlready": "Esse usuário já tem acesso a este site.", "ExceptionNoValueForUsernameOrEmail": "Por favor, digite um nome de usuário ou endereço de e-mail.", - "ExcludeVisitsViaCookie": "Excluir suas visitas usando cookie", - "ForAnonymousUsersReportDateToLoadByDefault": "Para usuários anônimos, reportar dara para carregar por padrão", + "ExcludeVisitsViaCookie": "Excluir suas visitas usando um cookie", + "ForAnonymousUsersReportDateToLoadByDefault": "Para usuários anônimos, reportar data a ser carregada por padrão", "GiveUserAccess": "Permitir '%1$s' %2$s acesso para %3$s.", "GiveViewAccess": "Permitir acesso de visualização para %1$s", "GiveViewAccessTitle": "Dar a usuário existente acesso para visualizar relatórios de %s", "GiveViewAccessInstructions": "Para dar acesso a um usuário existente para %s digite o nome de usuário ou endereço de e-mail de um usuário existente", + "IfYouWouldLikeToChangeThePasswordTypeANewOne": "Se você deseja alterar sua senha, digite uma nova. Caso contrário, deixe em branco.", + "YourCurrentPassword": "Sua senha atual", + "CurrentPasswordNotCorrect": "A senha atual informada não está correta.", + "ConfirmWithPassword": "Por favor digite sua senha para confirmar esta alteração.", "InjectedHostCannotChangePwd": "Você está visitando com um host desconhecido (%1$s). Você não pode mudar sua senha até que este problema seja resolvido.", - "LastSeen": "Último visto", - "MainDescription": "Decida quais usuários têm acesso a seus sites. Você também pode dar acesso a todos os sites de uma só vez, escolhendo \"Aplicar a todos os websites\" no seletor de website.", + "LastSeen": "Visto por último", + "MainDescription": "Decida quais usuários têm acesso a seus sites. Você também pode dar acesso a todos os sites de uma só vez, escolhendo \"Aplicar a todos os sites\" no seletor de site.", "ManageAccess": "Gerenciar acesso", "MenuAnonymousUserSettings": "Configurações de usuário anônimo", "MenuUsers": "Usuários", "MenuUserSettings": "Configurações de usuário", "MenuPersonal": "Pessoal", "PersonalSettings": "Configurações pessoais", - "NoteNoAnonymousUserAccessSettingsWontBeUsed2": "Nota: Você não pode alterar as configurações nesta seção, porque você não tem nenhum site que pode ser acessado pelo usuário anônimo.", - "NoUsersExist": "Não há usúarios ainda.", - "PluginDescription": "O Gerenciamento de Usuários permite que você adicione novos usuários, edite usuários existentes e dá-lhes acesso para visualizar ou administrar websites.", + "NoteNoAnonymousUserAccessSettingsWontBeUsed2": "Obs: Você não pode alterar as configurações nesta seção, porque você não tem nenhum site que pode ser acessado pelo usuário anônimo.", + "NoUsersExist": "Não há usuários ainda.", + "PluginDescription": "O Gerenciamento de Usuários permite que você adicione novos usuários, edite usuários existentes e dê a eles acesso para visualizar ou administrar sites.", "PrivAdmin": "Administrador", - "PrivAdminDescription": "Usuários com esse papel podem gerenciar um website a dar acesso a esse website para outros usuários. Eles também podem fazer qualquer coisa que o papel %s pode fazer.", + "PrivAdminDescription": "Usuários com este perfil podem gerenciar um site e dar acesso a esse site para outros usuários. Eles também podem fazer qualquer coisa que o perfil %s pode fazer.", + "PrivWrite": "Escrever", + "PrivWriteDescription": "Usuários com este perfil podem visualizar todo o conteúdo, além de criar, gerenciar e excluir entidades como Metas, Funis, Mapas de Calor, Gravações de Sessão e Formulários para este site.", "PrivNone": "Sem acesso", "PrivView": "Visualização", - "PrivViewDescription": "Um usuário com esse papel pode visualizar todos os relatórios.", + "PrivViewDescription": "Um usuário com este perfil pode visualizar todos os relatórios.", "RemoveUserAccess": "Remover acesso para '%1$s' para %2$s.", "ReportDateToLoadByDefault": "Relate data para carregar por padrão", "ReportToLoadByDefault": "Relate para carregar por padrão", "SuperUserAccessManagement": "Gerenciar o acesso do Super Usuário", "SuperUserAccessManagementGrantMore": "Você pode conceder o acesso de Super Usuário a outros usuários do Matomo aqui. Utilize este recurso com cuidado.", - "SuperUserAccessManagementMainDescription": "Super usuários têm as maiores permissões. Eles podem executar todas as tarefas administrativas, tais como adição de novos websites para monitorar, adicionar usuários, alterar permissões de usuários, ativar e desativar plugins e até mesmo instalar novos plugins no Mercado.", - "TheLoginScreen": "Tela de login", - "ThereAreCurrentlyNRegisteredUsers": "Existem actualmente %s usuários registrados.", - "TokenAuth": "Token de autenticação API", + "SuperUserAccessManagementMainDescription": "Super usuários têm as maiores permissões. Eles podem executar todas as tarefas administrativas, tais como adição de novos sites para monitorar, adicionar usuários, alterar permissões de usuários, ativar e desativar plugins e até mesmo instalar novos plugins do Mercado.", + "TheLoginScreen": "A tela de login", + "ThereAreCurrentlyNRegisteredUsers": "Existem atualmente %s usuários registrados.", + "TokenAuth": "Token de autenticação da API", + "TokenRegenerateConfirmSelf": "Alterar o token de autenticação da API irá invalidar o seu próprio token. Se o token atual estiver em uso, você precisa atualizar todos os clientes da API com o novo token gerado. Você quer mesmo alterar o seu token de autenticação?", "TokenRegenerateTitle": "Regenerar", "TypeYourPasswordAgain": "Digite sua nova senha novamente.", - "User": "Usurário", - "UserHasPermission": "%1$s tem atualmente %2$s acesso para %3$s.", + "User": "Usuário", + "UserHasPermission": "%1$s tem atualmente acesso %2$s para %3$s.", "UserHasNoPermission": "%1$s tem atualmente %2$s para %3$s", "UsersManagement": "Gerenciamento de usuários", "UsersManagementMainDescription": "Crie novos usuários ou atualize os usuários existentes. Você poderá então ajustar as permissões deles acima.", + "WhenUsersAreNotLoggedInAndVisitPiwikTheyShouldAccess": "Quando os usuários não estão conectados e visitam o Matomo, eles devem ver inicialmente.", "YourUsernameCannotBeChanged": "Seu nome de usuário não pode ser alterado.", "YourVisitsAreIgnoredOnDomain": "%1$s Suas visitas são ignoradas pelo Matomo em %2$s %3$s (o Matomo ignora o cookie encontrado em seu navegador).", - "YourVisitsAreNotIgnored": "%1$sSuas visitas não são ignoradas pelo Matomo%2$s (o Matomo ignora o cookie não encotrado em seu navegador)", + "YourVisitsAreNotIgnored": "%1$sSuas visitas não são ignoradas pelo Matomo%2$s (o cookie para ignorar o Matomo não foi encontrado em seu navegador).", + "AddUserNoInitialAccessError": "Novos usuários devem ter acesso a um site, por favor defina o parâmetro 'initialIdSite'.", + "AtLeastView": "Pelo menos visualizar", "ManageUsers": "Gerenciar Usuários", + "ManageUsersDesc": "Crie novos usuários ou atualize os usuários existentes. Então você pode definir as permissões deles aqui também.", + "NoAccessWarning": "Este usuário não tem acesso a um site. Quando ele fizer login, ele verá uma mensagem de erro. Para evitar isto, conceda acesso a um site abaixo.", + "BulkActions": "Ações em massa", + "SetPermission": "Definir permissão", + "RemovePermissions": "Remover permissões", + "RolesHelp": "Perfis determinam o que um usuário pode fazer no Matomo em relação a um site específico. Saiba mais sobre os perfis %1$sVisualizar%2$s e %3$sAdministrador%4$s.", + "Role": "Perfil", + "TheDisplayedWebsitesAreSelected": "Os %1$s sites exibidos estão selecionados.", + "ClickToSelectAll": "Clique para selecionar tudo %1$s.", + "AllWebsitesAreSelected": "Todos os %1$s sites estão selecionados.", + "ClickToSelectDisplayedWebsites": "Clique para selecionar os %1$s sites exibidos.", + "DeletePermConfirmSingle": "Você tem certeza de que deseja remover o acesso de %1$s ao %2$s?", + "DeletePermConfirmMultiple": "Você tem certeza de que deseja remover o acesso de %1$s aos %2$s sites selecionados?", + "ChangePermToSiteConfirmSingle": "Você tem certeza de que deseja alterar o perfil de %1$s em %2$s para %3$s?", + "ChangePermToSiteConfirmMultiple": "Você tem certeza de que deseja alterar o perfil de %1$s nos %2$s sites selecionados para %3$s?", + "BasicInformation": "Informação básica", + "Permissions": "Permissões", + "SuperUserAccess": "Acesso super usuário", + "FirstSiteInlineHelp": "É necessário dar a um novo usuário, em sua criação, um perfil de visualização para um site. Se nenhum acesso for dado, o usuário verá um erro ao fazer login. Você pode conceder mais permissões após a criação do usuário na aba 'Permissões' que irá aparecer na esquerda.", + "SuperUsersPermissionsNotice": "Super usuários têm acesso de administrador em todos os sites, então não há necessidade de gerenciar suas permissões por site.", + "SuperUserIntro1": "Super usuários têm as maiores permissões. Eles podem executar todas as tarefas administrativas, tais como adição de novos sites para monitorar, adicionar usuários, alterar permissões de usuários, ativar e desativar plugins e até mesmo instalar novos plugins do Mercado. Você pode conceder acesso de Super Usuário para outros usuários do Divezone aqui.", + "SuperUserIntro2": "Por favor, use essa funcionalidade com cuidado.", + "HasSuperUserAccess": "Possui Acesso Super Usuário", + "AreYouSure": "Você tem certeza?", + "DeleteUsers": "Excluir usuários", + "UserSearch": "Procurar usuário", + "FilterByAccess": "Filtrar por acesso", + "FilterByWebsite": "Filtrar por site", "ShowAll": "Exibir tudo", - "Username": "Nome de Usuário" + "Username": "Nome de Usuário", + "RoleFor": "Perfil para", + "RemoveAllAccessToThisSite": "Remover todo acesso a este site", + "DeleteUserPermConfirmSingle": "Você tem certeza de que deseja alterar o perfil de %1$s para %2$s em %3$s?", + "DeleteUserPermConfirmMultiple": "Você tem certeza de que deseja alterar os perfis dos %1$s usuários selecionados para %2$s em %3$s?", + "AnonymousUserRoleChangeWarning": "Conceder ao usuário %1$s o perfil %2$s irá tornar os dados deste site públicos e disponíveis a todos, mesmo quem não tem um login Matomo.", + "GiveAccessToAll": "Dar a este usuário acesso a todos os sites", + "OrManageIndividually": "Ou gerencie o acesso deste usuário em cada site individualmente.", + "ChangePermToAllSitesConfirm": "Você tem certeza de que deseja dar ao usuário %1$s acesso %2$s a todo site que você atualmente tem acesso de administrador?", + "ChangePermToAllSitesConfirm2": "Obs: isto irá afetar apenas os sites existentes atualmente. Novos sites que você criar não estarão acessíveis automaticamente para este usuário.", + "IncludedInUsersRole": "Incluído no perfil deste usuário." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/pt.json b/app/plugins/UsersManager/lang/pt.json index 419190675..b73016a05 100644 --- a/app/plugins/UsersManager/lang/pt.json +++ b/app/plugins/UsersManager/lang/pt.json @@ -4,6 +4,7 @@ "UsesTwoFactorAuthentication": "Utiliza a autenticação de dois fatores", "TwoFactorAuthentication": "Autenticação de dois fatores", "ResetTwoFactorAuthentication": "Repor a autenticação de dois fatores", + "ResetTwoFactorAuthenticationInfo": "Se o utilizador não conseguir iniciar sessão devido à perda de códigos de recuperação ou perda do dispositivo de autenticação, pode repor a a autenticação de dois fatores do utilizador, para que este possa iniciar sessão novamente.", "AddUser": "Adicionar um novo utilizador", "AddExistingUser": "Adicionar um utilizador existente", "AddNewUser": "Adicionar um novo utilizador", @@ -55,6 +56,7 @@ "GiveViewAccess": "Dê acesso de visualização a %1$s", "GiveViewAccessTitle": "Dê a um utilizador existente acesso para visualizar relatórios para %s", "GiveViewAccessInstructions": "Para dar acesso de visualização a um utilizador existente para %s introduza o nome de utilizador ou endereço de e-mail de um utilizador existente", + "IfYouWouldLikeToChangeThePasswordTypeANewOne": "Se deseja alterar a sua palavra-passe, introduza uma nova. Caso contrário, deixe este campo em branco.", "YourCurrentPassword": "A sua palavra-passe atual", "CurrentPasswordNotCorrect": "A palavra-passe atual que introduziu não está correta.", "ConfirmWithPassword": "Por favor, introduza a sua palavra-passe para confirmar esta alteração.", @@ -94,6 +96,7 @@ "UserHasNoPermission": "%1$s atualmente tem %2$s para %3$s", "UsersManagement": "Gestão de utilizadores", "UsersManagementMainDescription": "Criar novos utilizadores ou atualizar utilizadores existentes. Depois, pode definir as respetivas permissões acima.", + "WhenUsersAreNotLoggedInAndVisitPiwikTheyShouldAccess": "Quando os utilizadores não têm sessão iniciada e visitam o Matomo, devem inicialmente ver", "YourUsernameCannotBeChanged": "O seu nome de utilizador não pode ser alterado.", "YourVisitsAreIgnoredOnDomain": "%1$sAs suas visitas são ignoradas pelo Matomo em %2$s %3$s (foi encontrado no seu navegador o cookie do Matomo para ignorar).", "YourVisitsAreNotIgnored": "%1$sAs suas visitas não são ignoradas pelo Matomo%2$s (não foi encontrado no seu navegador o cookie do Matomo para ignorar).", @@ -124,6 +127,8 @@ "SuperUserIntro2": "Por favor, utilize esta funcionalidade com cuidado.", "HasSuperUserAccess": "Tem acesso de super-utilizador", "AreYouSure": "Tem a certeza?", + "RemoveSuperuserAccessConfirm": "Remover o acesso de super-utilizador deixará o utilizador sem permissões (terá, posteriormente, de as adicionar). Introduza a sua palavra-passe para continuar.", + "AddSuperuserAccessConfirm": "Atribuir a um utilizador o acesso de super-utilizador confere-lhe um controlo absoluto do Matomo, algo que deve ser feito com moderação. Introduza a sua palavra-passe para continuar.", "DeleteUsers": "Eliminar utilizadores", "UserSearch": "Pesquisa de utilizadores", "FilterByAccess": "Filtrar por acesso", @@ -150,6 +155,16 @@ "AreYouSureAddCapability": "Tem a certeza que pretende atribuir o privilégio %2$s a %1$s para %3$s?", "AreYouSureRemoveCapability": "Tem a certeza que pretende remover o privilégio %2$s a %1$s para %3$s?", "IncludedInUsersRole": "Incluído no papel do utilizador", - "Capability": "Privilégio" + "Capability": "Privilégio", + "EmailChangeNotificationSubject": "O endereço de e-mail da sua conta do Matomo foi alterado", + "EmailChangedEmail1": "O endereço de e-mail associado à sua conta foi alterado para %1$s", + "EmailChangedEmail2": "Esta alteração foi iniciada a partir do seguinte dispositivo: %1$s (endereço de IP = %2$s).", + "IfThisWasYouIgnoreIfNot": "Caso tenha sido iniciada por si, ignore este e-mail. Caso contrário, por favor, inicie sessão, corrija o seu endereço de e-mail, altere a sua palavra-passe e entre em contacto com o seu administrador do Matomo.", + "PasswordChangeNotificationSubject": "A palavra-passe da sua conta do Matomo foi alterada", + "PasswordChangedEmail": "A sua palavra-passe foi alterada. Esta alteração foi iniciada a partir do seguinte dispositivo: %1$s (endereço de IP = %2$s).", + "NewsletterSignupTitle": "Subscrição da Newsletter", + "NewsletterSignupMessage": "Subscreva à nossa newsletter para receber regularmente informações sobre o Matomo. Pode cancelar a sua subscrição a qualquer momento. Este serviço utiliza MadMimi. Para mais informações, consulte a nossa %1$spágina da Política de Privacidade %2$s.", + "NewsletterSignupFailureMessage": "Parece que algo correu mal. Não conseguimos concluir o seu processo de subscrição à newsletter.", + "NewsletterSignupSuccessMessage": "Fantástico, está subscrito. Entraremos em contacto em breve." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/ru.json b/app/plugins/UsersManager/lang/ru.json index 989f8ee36..1e1d8b70a 100644 --- a/app/plugins/UsersManager/lang/ru.json +++ b/app/plugins/UsersManager/lang/ru.json @@ -1,6 +1,16 @@ { "UsersManager": { + "2FA": "2FA", + "UsesTwoFactorAuthentication": "Использует двухфакторную аутентификацию", + "TwoFactorAuthentication": "Двухфакторная аутентификация", + "ResetTwoFactorAuthentication": "Сбросить двухфакторную аутентификацию", + "ResetTwoFactorAuthenticationInfo": "Если пользователь больше не может войти в систему из-за потерянных кодов восстановления или из-за потерянного устройства аутентификации, вы можете сбросить двухфакторную аутентификацию для пользователя, чтобы он мог снова войти в систему.", "AddUser": "Добавить нового пользователя", + "AddExistingUser": "Добавить существующего пользователя", + "AddNewUser": "Добавить нового пользователя", + "EditUser": "Редактировать пользователя", + "CreateUser": "Создать пользователя", + "SaveBasicInfo": "Сохранить основную информацию", "Alias": "Псевдоним", "AllWebsites": "Все сайты", "AnonymousUser": "Анонимный пользователь", @@ -14,20 +24,27 @@ "DeleteConfirm": "Вы уверены, что хотите удалить пользователя %s?", "Email": "Email", "EmailYourAdministrator": "%1$sНапишите вашему администратору об этой проблеме%2$s.", + "EnterUsernameOrEmail": "Введите имя пользователя или адрес электронной почты", + "ExceptionAnonymousNoCapabilities": "Вы не можете предоставлять какие-либо возможности «анонимному» пользователю.", "ExceptionDeleteDoesNotExist": "Пользователя '%s' не существует, поэтому он не может быть удален.", "ExceptionDeleteOnlyUserWithSuperUserAccess": "Невозможно удалить пользователя '%s'", "ExceptionEditAnonymous": "Анонимный пользователь не может быть удален. Он необходим системе Веб-аналитики для идентификации пользователей, которые не вошли в систему. Допустим, вы можете сделать статистику публичной, предоставляя право 'Просмотр' анонимному пользователю.", "ExceptionEmailExists": "Пользователь с Email '%s' уже существует.", "ExceptionInvalidEmail": "Email неправильного формата", + "ExceptionInvalidPassword": "Длина пароля должна быть больше %1$s символов.", + "ExceptionInvalidPasswordTooLong": "Длина пароля должна быть меньше %1$s символов.", "ExceptionLoginExists": "Имя пользователя '%s' уже существует.", "ExceptionPasswordMD5HashExpected": "UsersManager.getTokenAuth ожидает MD5-хэшированный пароль (строка в 32 символа). Пожалуйста, вызовите функцию md5() к паролю перед вызовом этого метода.", "ExceptionRemoveSuperUserAccessOnlySuperUser": "Удаление прав суперпользователя у пользователя «%s» не представляется возможным.", "ExceptionSuperUserAccess": "Этот пользователь уже имеет статус суперпользователя и разрешение на изменение всех веб-сайты в Matomo. Вы можете удалить права суперпользователя у этого пользователя, и попробовать снова.", "ExceptionUserDoesNotExist": "Пользователь '%s' не существует.", "ExceptionYouMustGrantSuperUserAccessFirst": "Должен быть по крайней мере один суперпользователь. Пожалуйста, сделайте сначала другого суперпользователя.", + "ExceptionUserHasViewAccessAlready": "Этот пользователь уже имеет доступ к этому сайту.", "ExceptionNoValueForUsernameOrEmail": "Пожалуйста, введите имя пользователя или E-mail адрес.", "ExcludeVisitsViaCookie": "Cookie исключения из статистики", "ForAnonymousUsersReportDateToLoadByDefault": "Отчет для анонимных пользователей отображается за", + "GiveViewAccess": "Предоставить доступ для просмотра %1$s", + "YourCurrentPassword": "Ваш текущий пароль", "InjectedHostCannotChangePwd": "В данный момент вы находитесь в Matomo с неизвестного хоста (%1$s). Вы не можете изменить пароль, пока эта проблема не будет решена.", "LastSeen": "Последнее посещение", "ManageAccess": "Управление правами доступа", @@ -55,7 +72,14 @@ "YourUsernameCannotBeChanged": "Имя вашего пользователя не может быть изменено.", "YourVisitsAreIgnoredOnDomain": "%1$sВаші відвідування ігноруються системою Matomo в %2$s %3$s (Matomo знашов cookie у вашому браузері з вказівкою ігнорувати).", "YourVisitsAreNotIgnored": "%1$sВаші відвідування відслідковуються системою Matomo %2$s (Matomo не знашов cookie у вашому браузері).", + "BulkActions": "Массовые действия", + "Role": "Роль", + "DeleteUsers": "Удалить пользователей", "ShowAll": "Показать все", - "Username": "Имя пользователя" + "Username": "Имя пользователя", + "EmailChangeNotificationSubject": "Адрес электронной почты вашей учетной записи Matomo был только что изменен", + "EmailChangedEmail1": "Адрес электронной почты, связанный с вашей учетной записью, был изменен на %1$s", + "PasswordChangeNotificationSubject": "Пароль вашей учетной записи Matomo был только что изменен", + "NewsletterSignupFailureMessage": "Упс, что-то пошло не так. Мы не смогли подписать Вас на рассылку." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/sq.json b/app/plugins/UsersManager/lang/sq.json index bc19a4b68..83ddc0170 100644 --- a/app/plugins/UsersManager/lang/sq.json +++ b/app/plugins/UsersManager/lang/sq.json @@ -161,6 +161,10 @@ "EmailChangedEmail2": "Ky ndryshim zuri fill prej pajisjes vijuese: %1$s (Adresë IP = %2$s).", "IfThisWasYouIgnoreIfNot": "Nëse qetë ju, mos ngurroni ta shpërfillni këtë email. Nëse s’qetë ju, ju lutemi, bëni hyrjen në llogarinë tuaj, ndreqni adresën tuaj email, ndryshoni fjalëkalimin tuaj dhe lidhuni me përgjegjësin e instancës tuaj Matomo.", "PasswordChangeNotificationSubject": "Fjalëkalimi i llogarisë tuaj Matomo sapo u ndryshua", - "PasswordChangedEmail": "Fjalëkalimi juaj sapo u ndryshua. Ndryshimi zuri fill prej pajisjes vijuese: %1$s (Adresë IP = %2$s)." + "PasswordChangedEmail": "Fjalëkalimi juaj sapo u ndryshua. Ndryshimi zuri fill prej pajisjes vijuese: %1$s (Adresë IP = %2$s).", + "NewsletterSignupTitle": "Regjistrim Për Buletin", + "NewsletterSignupMessage": "Pajtohuni te buletini ynë për të marrë informacione periodike rreth Matomo-s. Mund të shpajtoheni kur të doni. Ky shërbim përdor MadMimi. Mësoni më tepër rreth tij te %1$sfaqja e Rregullave tona të Privatësisë%2$s.", + "NewsletterSignupFailureMessage": "Hëm, diç shkoi ters. S’qemë në gjendje t’ju regjistronim për buletinin tonë.", + "NewsletterSignupSuccessMessage": "Super, u regjistruat! Do të lidhemi së shpejti." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/lang/tr.json b/app/plugins/UsersManager/lang/tr.json index 6611d952c..270b7c5dc 100644 --- a/app/plugins/UsersManager/lang/tr.json +++ b/app/plugins/UsersManager/lang/tr.json @@ -161,6 +161,10 @@ "EmailChangedEmail2": "Bu değişiklik şu aygıt üzerinden yapıldı: %1$s (IP adresi: %2$s).", "IfThisWasYouIgnoreIfNot": "Bu değişikliği siz yaptıysanız bu bildirimi yok sayabilirsiniz. Değişikliği siz yapmadıysanız lütfen oturum açarak e-posta adresinizi düzeltin, parolanızı değiştirin ve Matomo yöneticiniz ile görüşün.", "PasswordChangeNotificationSubject": "Matomo hesabınızın parolası değiştirildi", - "PasswordChangedEmail": "Parolanız değiştirildi. Bu değişiklik şu aygıt üzerinden yapıldı: %1$s (IP adresi: %2$s)." + "PasswordChangedEmail": "Parolanız değiştirildi. Bu değişiklik şu aygıt üzerinden yapıldı: %1$s (IP adresi: %2$s).", + "NewsletterSignupTitle": "Bülten Aboneliği", + "NewsletterSignupMessage": "Matomo hakkında düzenli bilgi almak için haber bültenimize abone olabilirsiniz. Bülten aboneliğinden istediğiniz zaman çıkabilirsiniz. Bu hizmet için MadMimi kullanılır. Ayrıntılı bilgi almak için %1$sKişisel Gizlilik%2$s bölümüne bakabilirsiniz.", + "NewsletterSignupFailureMessage": "Maalesef bir şeyler ters gitti. Bülten aboneliğiniz yapılamadı.", + "NewsletterSignupSuccessMessage": "Harika, abone oldunuz! Yakında görüşürüz." } } \ No newline at end of file diff --git a/app/plugins/UsersManager/templates/userSettings.twig b/app/plugins/UsersManager/templates/userSettings.twig index 72ac592a2..22c9a58cc 100644 --- a/app/plugins/UsersManager/templates/userSettings.twig +++ b/app/plugins/UsersManager/templates/userSettings.twig @@ -20,6 +20,7 @@ inline-help="{{ 'UsersManager_YourUsernameCannotBeChanged'|translate|e('html_attr') }}"> + {% if isUsersAdminEnabled %}
+ {% endif %} - {% if isValidHost is defined and isValidHost %} + {% if isValidHost is defined and isValidHost and isUsersAdminEnabled %}
+{% if showNewsletterSignup %} +
+
+ +
', '')|e('html_attr') }}" + > +
+ +
+
+
+
+{% endif %} +

diff --git a/app/plugins/VisitFrequency/Controller.php b/app/plugins/VisitFrequency/Controller.php
index 48860fe42..479a91da5 100644
--- a/app/plugins/VisitFrequency/Controller.php
+++ b/app/plugins/VisitFrequency/Controller.php
@@ -12,6 +12,7 @@
 use Piwik\FrontController;
 use Piwik\Piwik;
 use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines;
+use Piwik\SettingsPiwik;
 use Piwik\Translation\Translator;
 use Piwik\View;
 
@@ -69,7 +70,7 @@ public function getEvolutionGraph()
 
         $period = Common::getRequestVar('period', false);
 
-        if ($period == 'day') {
+        if (SettingsPiwik::isUniqueVisitorsEnabled($period)) {
             // add number of unique (returning) visitors for period=day
             $selectableColumns = array_merge(
                 array($selectableColumns[0]),
diff --git a/app/plugins/VisitFrequency/config/config.php b/app/plugins/VisitFrequency/config/config.php
index 4932533ad..d266508bc 100644
--- a/app/plugins/VisitFrequency/config/config.php
+++ b/app/plugins/VisitFrequency/config/config.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/VisitFrequency/config/tracker.php b/app/plugins/VisitFrequency/config/tracker.php
index febb40801..ca2affec3 100644
--- a/app/plugins/VisitFrequency/config/tracker.php
+++ b/app/plugins/VisitFrequency/config/tracker.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/VisitFrequency/lang/zh-cn.json b/app/plugins/VisitFrequency/lang/zh-cn.json
index 488efc6da..b0969237b 100644
--- a/app/plugins/VisitFrequency/lang/zh-cn.json
+++ b/app/plugins/VisitFrequency/lang/zh-cn.json
@@ -11,8 +11,14 @@
         "ColumnSumVisitLengthReturning": "老访客总的停留时间 (秒)",
         "ColumnUniqueReturningVisitors": "独立老访客数",
         "ColumnReturningUsers": "老访客",
+        "PluginDescription": "报告有关您的首次访问者和回访者的指标。",
+        "ReturnActions": "回访行动",
+        "ReturnAverageVisitDuration": "回访者的平均参观时间",
+        "ReturnAvgActions": "每次回访的操作",
+        "ReturnBounceRate": "回访次数反弹(访问一页后离开网站)",
         "ReturningVisitDocumentation": "老访客 (相对于新访客) 指至少访问过这个网站一次的访客。",
         "ReturningVisitsDocumentation": "这是老访客的概览。",
+        "ReturnVisits": "回访",
         "SubmenuFrequency": "频率",
         "WidgetGraphReturning": "老访客趋势",
         "WidgetOverview": "频率概览"
diff --git a/app/plugins/VisitTime/config/config.php b/app/plugins/VisitTime/config/config.php
index 4932533ad..d266508bc 100644
--- a/app/plugins/VisitTime/config/config.php
+++ b/app/plugins/VisitTime/config/config.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/VisitTime/config/tracker.php b/app/plugins/VisitTime/config/tracker.php
index febb40801..ca2affec3 100644
--- a/app/plugins/VisitTime/config/tracker.php
+++ b/app/plugins/VisitTime/config/tracker.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/VisitTime/lang/es-ar.json b/app/plugins/VisitTime/lang/es-ar.json
index c6a93692b..066943efd 100644
--- a/app/plugins/VisitTime/lang/es-ar.json
+++ b/app/plugins/VisitTime/lang/es-ar.json
@@ -1,17 +1,35 @@
 {
     "VisitTime": {
         "ColumnLocalTime": "Hora local",
+        "ColumnLocalHour": "Hora local: hora (inicio de la visita)",
+        "ColumnLocalMinute": "Hora local: minuto (inicio de la visita)",
         "ColumnServerTime": "Hora del servidor",
+        "ColumnServerHour": "Hora del servidor: hora",
+        "ColumnVisitEndServerHour": "Hora del servidor: hour (fin de la visita)",
+        "ColumnVisitEndServerMinute": "Hora del servidor: minuto (fin de la visita)",
+        "ColumnVisitStartServerHour": "Hora del servidor: hora (inicio de la visita)",
+        "ColumnVisitStartServerMinute": "Hora del servidor: minuto (inicio de la visita)",
+        "ColumnVisitEndServerDate": "Hora del servidor: fecha (fin de la visita)",
+        "ColumnVisitEndServerDayOfMonth": "Hora del servidor: día del mes (fin de la visita)",
+        "ColumnVisitEndServerDayOfWeek": "Hora del servidor: día de la semana (fin de la visita)",
+        "ColumnVisitEndServerDayOfYear": "Hora del servidor: día del año (fin de la visita)",
+        "ColumnVisitEndServerQuarter": "Hora del servidor: trimestre (fin de la visita)",
+        "ColumnVisitEndServerSecond": "Hora del servidor: segundo (fin de la visita)",
+        "ColumnVisitEndServerWeekOfYear": "Hora del servidor: semana del año (fin de la visita)",
+        "ColumnVisitEndServerMonth": "Hora del servidor: mes (fin de la visita)",
+        "ColumnVisitEndServerYear": "Hora del servidor: año (fin de la visita)",
+        "ColumnServerMinute": "Hora del servidor: minuto",
         "DayOfWeek": "Día de la semana",
         "LocalTime": "Visitas por hora local",
         "NHour": "%sh",
+        "PluginDescription": "Informa la hora local y la hora del servidor cuando tus visitantes visualizaron el sitio web o la aplicación.",
         "ServerTime": "Visitas por hora del servidor",
-        "SubmenuTimes": "Tiempos",
+        "SubmenuTimes": "Veces",
         "VisitsByDayOfWeek": "Visitas por día de la semana",
-        "WidgetByDayOfWeekDocumentation": "Este gráfico muestra el número de visitas que su sitio de internet recibió en cada día de la semana.",
+        "WidgetByDayOfWeekDocumentation": "Este gráfico muestra el número de visitas que tu sitio web recibió en cada día de la semana.",
         "WidgetLocalTime": "Visitas por hora local",
-        "WidgetLocalTimeDocumentation": "Este gráfico muestra que horario fue %1$sen las zonas horarias de los visitantes %2$s durante sus visitas.",
+        "WidgetLocalTimeDocumentation": "Este gráfico muestra qué hora era %1$sen las zonas horarias de los visitantes %2$s durante sus visitas.",
         "WidgetServerTime": "Visitas por hora del servidor",
-        "WidgetServerTimeDocumentation": "Este gráfico muestra cual fue el horario en la %1$s zona horaria del servidor %2$s durante las visitas."
+        "WidgetServerTimeDocumentation": "Este gráfico muestra qué hora era en la %1$s zona horaria del servidor %2$s durante las visitas."
     }
 }
\ No newline at end of file
diff --git a/app/plugins/VisitTime/lang/zh-cn.json b/app/plugins/VisitTime/lang/zh-cn.json
index cb14c8315..db6beef3d 100644
--- a/app/plugins/VisitTime/lang/zh-cn.json
+++ b/app/plugins/VisitTime/lang/zh-cn.json
@@ -1,7 +1,24 @@
 {
     "VisitTime": {
         "ColumnLocalTime": "客户端时间",
+        "ColumnLocalHour": "当地时间-小时(访问开始)",
+        "ColumnLocalMinute": "当地时间-分钟(访问开始)",
         "ColumnServerTime": "服务器时间",
+        "ColumnServerHour": "服务器时间-小时",
+        "ColumnVisitEndServerHour": "服务器时间-小时(访问结束)",
+        "ColumnVisitEndServerMinute": "服务器时间-分钟(访问结束)",
+        "ColumnVisitStartServerHour": "服务器时间-小时(访问开始)",
+        "ColumnVisitStartServerMinute": "服务器时间-分钟(访问开始)",
+        "ColumnVisitEndServerDate": "服务器时间-日期(访问结束)",
+        "ColumnVisitEndServerDayOfMonth": "服务器时间-月中的一天(访问结束)",
+        "ColumnVisitEndServerDayOfWeek": "服务器时间-星期几(访问结束)",
+        "ColumnVisitEndServerDayOfYear": "服务器时间-一年中的一天(访问结束)",
+        "ColumnVisitEndServerQuarter": "服务器时间-季度(访问结束)",
+        "ColumnVisitEndServerSecond": "服务器时间-秒(访问结束)",
+        "ColumnVisitEndServerWeekOfYear": "服务器时间-一年中的第几周(访问结束)",
+        "ColumnVisitEndServerMonth": "服务器时间-月(访问结束)",
+        "ColumnVisitEndServerYear": "服务器时间-年(访问结束)",
+        "ColumnServerMinute": "服务器时间-分钟",
         "DayOfWeek": "星期几的",
         "LocalTime": "日报表",
         "NHour": "%s 点",
diff --git a/app/plugins/VisitorInterest/Archiver.php b/app/plugins/VisitorInterest/Archiver.php
index 09a77e2e4..2aa6d6908 100644
--- a/app/plugins/VisitorInterest/Archiver.php
+++ b/app/plugins/VisitorInterest/Archiver.php
@@ -21,7 +21,7 @@ class Archiver extends \Piwik\Plugin\Archiver
     const VISITS_COUNT_RECORD_NAME = 'VisitorInterest_visitsByVisitCount';
     const DAYS_SINCE_LAST_RECORD_NAME = 'VisitorInterest_daysSinceLastVisit';
 
-    protected static $timeGap = array(
+    public static $timeGap = array(
         array(0, 10, 's'),
         array(11, 30, 's'),
         array(31, 60, 's'),
@@ -33,7 +33,7 @@ class Archiver extends \Piwik\Plugin\Archiver
         array(15, 30),
         array(30)
     );
-    protected static $pageGap = array(
+    public static $pageGap = array(
         array(1, 1),
         array(2, 2),
         array(3, 3),
diff --git a/app/plugins/VisitorInterest/config/config.php b/app/plugins/VisitorInterest/config/config.php
index 4932533ad..d266508bc 100644
--- a/app/plugins/VisitorInterest/config/config.php
+++ b/app/plugins/VisitorInterest/config/config.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/VisitorInterest/config/tracker.php b/app/plugins/VisitorInterest/config/tracker.php
index febb40801..ca2affec3 100644
--- a/app/plugins/VisitorInterest/config/tracker.php
+++ b/app/plugins/VisitorInterest/config/tracker.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/VisitorInterest/lang/es-ar.json b/app/plugins/VisitorInterest/lang/es-ar.json
index f9227a66d..7fe7bc841 100644
--- a/app/plugins/VisitorInterest/lang/es-ar.json
+++ b/app/plugins/VisitorInterest/lang/es-ar.json
@@ -1,23 +1,24 @@
 {
     "VisitorInterest": {
         "BetweenXYMinutes": "%1$s-%2$s min",
-        "BetweenXYSeconds": "%1$s-%2$ss",
+        "BetweenXYSeconds": "%1$s-%2$sseg",
         "ColumnPagesPerVisit": "Páginas por visita",
-        "ColumnVisitDuration": "Duración de las visitas",
+        "ColumnVisitDuration": "Duración de la visita",
         "Engagement": "Compromiso",
         "NPages": "%s páginas",
         "OnePage": "1 página",
+        "PluginDescription": "Informes acerca de los intereses de los visitantes: número de páginas visitadas, tiempo navegando el sitio, cantidad de días desde la última visita, y más.",
         "VisitNum": "Número de visita",
         "VisitsByDaysSinceLast": "Visitas por días desde la última visita",
         "visitsByVisitCount": "Visitas por número de visita",
         "VisitsPerDuration": "Visitas por duración de visita",
         "VisitsPerNbOfPages": "Visitas por número de páginas",
         "WidgetLengths": "Duración de las visitas",
-        "WidgetLengthsDocumentation": "En este informe, puede ver cuantas visitas tuvieron una cierta duración total. Inicialmente, el mismo es mostraedo como una nube de etiquetas, las duraciones más comunes en una fuente más notoria.",
+        "WidgetLengthsDocumentation": "En este informe, podés ver cuántas visitas tuvieron una cierta duración total. Inicialmente, el informe es mostrado como una nube de etiquetas, las duraciones más comunes son mostradas en una tipografía más grande.",
         "WidgetPages": "Páginas por visita",
-        "WidgetPagesDocumentation": "En este informe, puede observar cuantos visitantes están involucrados con un cierto número de vistas de páginas. Inicialmente, el informe se muestra como una nube de etiquetas, los números de páginas más usuales en una fuente mayor.",
+        "WidgetPagesDocumentation": "En este informe, podés ver cuántos visitantes están involucrados con un cierto número de vistas de páginas. Inicialmente, el informe se muestra como una nube de etiquetas, los números de páginas más comunes son mostrados en una tipografía más grande.",
         "WidgetVisitsByDaysSinceLast": "Visitas por días desde la última visita",
-        "WidgetVisitsByDaysSinceLastDocumentation": "En este informe, puede observar cuantas visitas cuya última visita fue un cierto número de días atrás.",
-        "WidgetVisitsByNumDocumentation": "En este informe, puede ver el número de visitas que fueron la N visita, por ejemplo: visitantes que visitaron su sitio de internet al menos N veces"
+        "WidgetVisitsByDaysSinceLastDocumentation": "En este informe, podés ver cuántas visitas cuya última visita fue un cierto número de días atrás.",
+        "WidgetVisitsByNumDocumentation": "En este informe, podés ver el número de visitas que fueron la \"N\" visita, por ejemplo: visitantes que visitaron tu sitio web al menos \"N\" veces."
     }
 }
\ No newline at end of file
diff --git a/app/plugins/VisitsSummary/Controller.php b/app/plugins/VisitsSummary/Controller.php
index 91928bd91..51b808767 100644
--- a/app/plugins/VisitsSummary/Controller.php
+++ b/app/plugins/VisitsSummary/Controller.php
@@ -15,6 +15,7 @@
 use Piwik\FrontController;
 use Piwik\Piwik;
 use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines;
+use Piwik\SettingsPiwik;
 use Piwik\Site;
 use Piwik\Translation\Translator;
 use Piwik\View;
@@ -103,6 +104,12 @@ public function getEvolutionGraph()
             'avg_time_generation'
         );
 
+        $currentPeriod = Common::getRequestVar('period', false);
+
+        if (!SettingsPiwik::isUniqueVisitorsEnabled($currentPeriod)) {
+            $selectableColumns = array_diff($selectableColumns, ['nb_uniq_visitors', 'nb_users']);
+        }
+
         $displaySiteSearch = Site::isSiteSearchEnabledFor($this->idSite);
 
         if ($displaySiteSearch) {
diff --git a/app/plugins/VisitsSummary/config/config.php b/app/plugins/VisitsSummary/config/config.php
index 4932533ad..d266508bc 100644
--- a/app/plugins/VisitsSummary/config/config.php
+++ b/app/plugins/VisitsSummary/config/config.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/VisitsSummary/config/tracker.php b/app/plugins/VisitsSummary/config/tracker.php
index febb40801..ca2affec3 100644
--- a/app/plugins/VisitsSummary/config/tracker.php
+++ b/app/plugins/VisitsSummary/config/tracker.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/VisitsSummary/lang/ru.json b/app/plugins/VisitsSummary/lang/ru.json
index 56c10e9f2..3860bf693 100644
--- a/app/plugins/VisitsSummary/lang/ru.json
+++ b/app/plugins/VisitsSummary/lang/ru.json
@@ -15,6 +15,8 @@
         "NbUniqueOutlinksDescription": "уникальные внешние переходы",
         "NbUniquePageviewsDescription": "уникальные показы страниц",
         "NbUniqueVisitors": "уникальные посетители",
+        "NbUsersDescription": "пользователи",
+        "NbVisitsDescription": "посещения",
         "NbVisitsBounced": "посетителей \"отскочило\" (ушло после одной станицы)",
         "VisitsSummary": "Посещения",
         "VisitsSummaryDocumentation": "Это обзор динамики посещений.",
diff --git a/app/plugins/WebsiteMeasurable/config/config.php b/app/plugins/WebsiteMeasurable/config/config.php
index 4932533ad..d266508bc 100644
--- a/app/plugins/WebsiteMeasurable/config/config.php
+++ b/app/plugins/WebsiteMeasurable/config/config.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/WebsiteMeasurable/config/tracker.php b/app/plugins/WebsiteMeasurable/config/tracker.php
index febb40801..ca2affec3 100644
--- a/app/plugins/WebsiteMeasurable/config/tracker.php
+++ b/app/plugins/WebsiteMeasurable/config/tracker.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/WebsiteMeasurable/lang/pt-br.json b/app/plugins/WebsiteMeasurable/lang/pt-br.json
index 46ea12aa3..162f3ab23 100644
--- a/app/plugins/WebsiteMeasurable/lang/pt-br.json
+++ b/app/plugins/WebsiteMeasurable/lang/pt-br.json
@@ -1,7 +1,7 @@
 {
     "WebsiteMeasurable": {
-        "Website": "Website",
-        "Websites": "Websites",
-        "WebsiteDescription": "Um site é composto de páginas normalmente funcionando a partir de um único domínio."
+        "Website": "Site",
+        "Websites": "Sites",
+        "WebsiteDescription": "Um site é composto de páginas web normalmente servidas a partir de um único domínio web."
     }
 }
\ No newline at end of file
diff --git a/app/plugins/Widgetize/Controller.php b/app/plugins/Widgetize/Controller.php
index ceb40c176..8481f0a60 100644
--- a/app/plugins/Widgetize/Controller.php
+++ b/app/plugins/Widgetize/Controller.php
@@ -36,6 +36,18 @@ public function iframe()
             throw new \Exception("Widgetizing API requests is not supported for security reasons. Please change query parameter 'moduleToWidgetize'.");
         }
 
+        if ($controllerName == 'Widgetize') {
+            throw new \Exception("Please set 'moduleToWidgetize' to a valid value.");
+        }
+
+        if ($controllerName == 'CoreHome' && $actionName == 'index') {
+            $message = 'CoreHome cannot be widgetized. '  . 
+                'You can enable it to be embedded directly into an iframe (passing module=CoreHme instead of module=Widgetize) ' .
+                'instead by enabling the \'enable_framed_pages\' setting in your config. ' .
+                'See https://matomo.org/faq/how-to/faq_193/ for more info.';
+            throw new \Exception($message);
+        }
+
         $shouldEmbedEmpty = false;
 
         /**
diff --git a/app/plugins/Widgetize/config/config.php b/app/plugins/Widgetize/config/config.php
index 4932533ad..d266508bc 100644
--- a/app/plugins/Widgetize/config/config.php
+++ b/app/plugins/Widgetize/config/config.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/Widgetize/config/tracker.php b/app/plugins/Widgetize/config/tracker.php
index febb40801..ca2affec3 100644
--- a/app/plugins/Widgetize/config/tracker.php
+++ b/app/plugins/Widgetize/config/tracker.php
@@ -1,3 +1,2 @@
 
\ No newline at end of file
diff --git a/app/plugins/Widgetize/lang/es-ar.json b/app/plugins/Widgetize/lang/es-ar.json
index cdc9727f7..efb994372 100644
--- a/app/plugins/Widgetize/lang/es-ar.json
+++ b/app/plugins/Widgetize/lang/es-ar.json
@@ -1,5 +1,7 @@
 {
     "Widgetize": {
-        "OpenInNewWindow": "Abrir en una nueva ventana"
+        "OpenInNewWindow": "Abrir en una nueva ventana",
+        "PluginDescription": "Muestra cualquier informe de Matomo en tu sitio web o aplicación con una simple etiqueta HTML insertada.",
+        "TopLinkTooltip": "Exportar los informes de Matomo como widgets e insertar el panel en tu aplicación como un iFrame."
     }
 }
\ No newline at end of file
diff --git a/app/plugins/Widgetize/lang/pl.json b/app/plugins/Widgetize/lang/pl.json
index 04eaa4b31..36275d22b 100644
--- a/app/plugins/Widgetize/lang/pl.json
+++ b/app/plugins/Widgetize/lang/pl.json
@@ -1,7 +1,7 @@
 {
     "Widgetize": {
         "OpenInNewWindow": "Otwórz w nowym oknie",
-        "PluginDescription": "Wyświetl dowolny raport Matomo na swojej stronie lub w aplikacji przy pomocy prostego Zagnieżdżonego HTML'a.",
+        "PluginDescription": "Wyświetl dowolny raport Matomo na swojej stronie lub w aplikacji przy pomocy zagnieżdżonego HTML'a.",
         "TopLinkTooltip": "Eksportuj raporty Matomo jako widżety i zagnieżdżaj Pulpit w swoich aplikacjach jako iframe."
     }
 }
\ No newline at end of file
diff --git a/app/plugins/Widgetize/lang/pt-br.json b/app/plugins/Widgetize/lang/pt-br.json
index 297156054..f06837e77 100644
--- a/app/plugins/Widgetize/lang/pt-br.json
+++ b/app/plugins/Widgetize/lang/pt-br.json
@@ -1,7 +1,7 @@
 {
     "Widgetize": {
         "OpenInNewWindow": "Abrir em uma nova janela",
-        "PluginDescription": "Exibe qualquer relatório Matomo no seu site ou aplicativo com uma simples tag HTML Embed.",
-        "TopLinkTooltip": "Exportar relatórios Matomo como widgets do Dashboard e incorporar em seu aplicativo como um iframe."
+        "PluginDescription": "Exiba qualquer relatório Matomo no seu site ou aplicativo com uma simples tag HTML Embed.",
+        "TopLinkTooltip": "Exporte relatórios Matomo como widgets e incorpore o Painel em seu aplicativo como um iframe."
     }
 }
\ No newline at end of file
diff --git a/app/plugins/Widgetize/stylesheets/widgetize.less b/app/plugins/Widgetize/stylesheets/widgetize.less
index d4bc1d43a..447c7dc7f 100644
--- a/app/plugins/Widgetize/stylesheets/widgetize.less
+++ b/app/plugins/Widgetize/stylesheets/widgetize.less
@@ -47,4 +47,10 @@ body.widgetized {
   #pageFooter {
     margin-bottom: 0;
   }
+
+  table.dataTable {
+    table-layout: fixed;
+    width: auto;
+    min-width: 100%;
+  }
 }
\ No newline at end of file
diff --git a/app/plugins/Widgetize/templates/iframe.twig b/app/plugins/Widgetize/templates/iframe.twig
index 9a8c5ec16..e4ac11f1a 100644
--- a/app/plugins/Widgetize/templates/iframe.twig
+++ b/app/plugins/Widgetize/templates/iframe.twig
@@ -5,7 +5,6 @@
         
         
         {% include "_jsGlobalVariables.twig" %}
-
         {% include "_jsCssIncludes.twig" %}
     
     
diff --git a/app/vendor/autoload.php b/app/vendor/autoload.php
index 1ae10a8a0..4981eed82 100644
--- a/app/vendor/autoload.php
+++ b/app/vendor/autoload.php
@@ -4,4 +4,4 @@
 
 require_once __DIR__ . '/composer/autoload_real.php';
 
-return ComposerAutoloaderInit20965b5e193daae3814e448b2561c788::getLoader();
+return ComposerAutoloaderInita8ac692f43bf07ab29c010fd95958205::getLoader();
diff --git a/app/vendor/composer/autoload_classmap.php b/app/vendor/composer/autoload_classmap.php
index d8d3192bb..9c568285a 100644
--- a/app/vendor/composer/autoload_classmap.php
+++ b/app/vendor/composer/autoload_classmap.php
@@ -501,7 +501,11 @@
     'Piwik\\Common' => $baseDir . '/core/Common.php',
     'Piwik\\Composer\\ScriptHandler' => $baseDir . '/core/Composer/ScriptHandler.php',
     'Piwik\\Concurrency\\DistributedList' => $baseDir . '/core/Concurrency/DistributedList.php',
+    'Piwik\\Concurrency\\Lock' => $baseDir . '/core/Concurrency/Lock.php',
+    'Piwik\\Concurrency\\LockBackend' => $baseDir . '/core/Concurrency/LockBackend.php',
+    'Piwik\\Concurrency\\LockBackend\\MySqlLockBackend' => $baseDir . '/core/Concurrency/LockBackend/MySqlLockBackend.php',
     'Piwik\\Config' => $baseDir . '/core/Config.php',
+    'Piwik\\Config\\Cache' => $baseDir . '/core/Config/Cache.php',
     'Piwik\\Config\\ConfigNotFoundException' => $baseDir . '/core/Config/ConfigNotFoundException.php',
     'Piwik\\Config\\IniFileChain' => $baseDir . '/core/Config/IniFileChain.php',
     'Piwik\\Console' => $baseDir . '/core/Console.php',
@@ -527,6 +531,7 @@
     'Piwik\\DataAccess\\LogQueryBuilder' => $baseDir . '/core/DataAccess/LogQueryBuilder.php',
     'Piwik\\DataAccess\\LogQueryBuilder\\JoinGenerator' => $baseDir . '/core/DataAccess/LogQueryBuilder/JoinGenerator.php',
     'Piwik\\DataAccess\\LogQueryBuilder\\JoinTables' => $baseDir . '/core/DataAccess/LogQueryBuilder/JoinTables.php',
+    'Piwik\\DataAccess\\LogTableTemporary' => $baseDir . '/core/DataAccess/LogTableTemporary.php',
     'Piwik\\DataAccess\\Model' => $baseDir . '/core/DataAccess/Model.php',
     'Piwik\\DataAccess\\RawLogDao' => $baseDir . '/core/DataAccess/RawLogDao.php',
     'Piwik\\DataAccess\\TableMetadata' => $baseDir . '/core/DataAccess/TableMetadata.php',
@@ -599,14 +604,16 @@
     'Piwik\\Db\\SchemaInterface' => $baseDir . '/core/Db/SchemaInterface.php',
     'Piwik\\Db\\Schema\\Mysql' => $baseDir . '/core/Db/Schema/Mysql.php',
     'Piwik\\Db\\Settings' => $baseDir . '/core/Db/Settings.php',
+    'Piwik\\Db\\TransactionLevel' => $baseDir . '/core/Db/TransactionLevel.php',
     'Piwik\\Decompress\\DecompressInterface' => $vendorDir . '/piwik/decompress/src/DecompressInterface.php',
     'Piwik\\Decompress\\Gzip' => $vendorDir . '/piwik/decompress/src/Gzip.php',
     'Piwik\\Decompress\\PclZip' => $vendorDir . '/piwik/decompress/src/PclZip.php',
     'Piwik\\Decompress\\Tar' => $vendorDir . '/piwik/decompress/src/Tar.php',
     'Piwik\\Decompress\\ZipArchive' => $vendorDir . '/piwik/decompress/src/ZipArchive.php',
     'Piwik\\Development' => $baseDir . '/core/Development.php',
-    'Piwik\\DeviceDetectorCache' => $baseDir . '/core/DeviceDetectorCache.php',
     'Piwik\\DeviceDetectorFactory' => $baseDir . '/core/DeviceDetectorFactory.php',
+    'Piwik\\DeviceDetector\\DeviceDetectorCache' => $baseDir . '/core/DeviceDetector/DeviceDetectorCache.php',
+    'Piwik\\DeviceDetector\\DeviceDetectorFactory' => $baseDir . '/core/DeviceDetector/DeviceDetectorFactory.php',
     'Piwik\\Email\\ContentGenerator' => $baseDir . '/core/Email/ContentGenerator.php',
     'Piwik\\ErrorHandler' => $baseDir . '/core/ErrorHandler.php',
     'Piwik\\EventDispatcher' => $baseDir . '/core/EventDispatcher.php',
@@ -718,6 +725,8 @@
     'Piwik\\Plugins\\API\\API' => $baseDir . '/plugins/API/API.php',
     'Piwik\\Plugins\\API\\Controller' => $baseDir . '/plugins/API/Controller.php',
     'Piwik\\Plugins\\API\\DataTable\\MergeDataTables' => $baseDir . '/plugins/API/DataTable/MergeDataTables.php',
+    'Piwik\\Plugins\\API\\Filter\\DataComparisonFilter' => $baseDir . '/plugins/API/Filter/DataComparisonFilter.php',
+    'Piwik\\Plugins\\API\\Filter\\DataComparisonFilter\\ComparisonRowGenerator' => $baseDir . '/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php',
     'Piwik\\Plugins\\API\\Glossary' => $baseDir . '/plugins/API/Glossary.php',
     'Piwik\\Plugins\\API\\Menu' => $baseDir . '/plugins/API/Menu.php',
     'Piwik\\Plugins\\API\\Plugin' => $baseDir . '/plugins/API/API.php',
@@ -738,6 +747,7 @@
     'Piwik\\Plugins\\API\\WidgetMetadata' => $baseDir . '/plugins/API/WidgetMetadata.php',
     'Piwik\\Plugins\\API\\test\\Unit\\CsvRendererTest' => $baseDir . '/plugins/API/tests/Unit/CsvRendererTest.php',
     'Piwik\\Plugins\\API\\tests\\Integration\\APITest' => $baseDir . '/plugins/API/tests/Integration/APITest.php',
+    'Piwik\\Plugins\\API\\tests\\Integration\\Filter\\DataComparisonFilter\\ComparisonRowGeneratorTest' => $baseDir . '/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php',
     'Piwik\\Plugins\\API\\tests\\Integration\\RowEvolutionTest' => $baseDir . '/plugins/API/tests/Integration/RowEvolutionTest.php',
     'Piwik\\Plugins\\API\\tests\\Integration\\RssRendererTest' => $baseDir . '/plugins/API/tests/Integration/RssRendererTest.php',
     'Piwik\\Plugins\\API\\tests\\System\\AutoSuggestAPITest' => $baseDir . '/plugins/API/tests/System/AutoSuggestAPITest.php',
@@ -812,6 +822,7 @@
     'Piwik\\Plugins\\Actions\\Segment' => $baseDir . '/plugins/Actions/Segment.php',
     'Piwik\\Plugins\\Actions\\Tracker\\ActionsRequestProcessor' => $baseDir . '/plugins/Actions/Tracker/ActionsRequestProcessor.php',
     'Piwik\\Plugins\\Actions\\VisitorDetails' => $baseDir . '/plugins/Actions/VisitorDetails.php',
+    'Piwik\\Plugins\\Actions\\tests\\System\\ApiTest' => $baseDir . '/plugins/Actions/tests/System/ApiTest.php',
     'Piwik\\Plugins\\Actions\\tests\\Unit\\ActionSiteSearchTest' => $baseDir . '/plugins/Actions/tests/Integration/ActionSiteSearchTest.php',
     'Piwik\\Plugins\\Actions\\tests\\Unit\\ArchiverTests' => $baseDir . '/plugins/Actions/tests/Unit/ArchiverTest.php',
     'Piwik\\Plugins\\Annotations\\API' => $baseDir . '/plugins/Annotations/API.php',
@@ -986,6 +997,7 @@
     'Piwik\\Plugins\\CoreHome\\Widgets\\GetDonateForm' => $baseDir . '/plugins/CoreHome/Widgets/GetDonateForm.php',
     'Piwik\\Plugins\\CoreHome\\Widgets\\GetPromoVideo' => $baseDir . '/plugins/CoreHome/Widgets/GetPromoVideo.php',
     'Piwik\\Plugins\\CoreHome\\Widgets\\GetSystemSummary' => $baseDir . '/plugins/CoreHome/Widgets/GetSystemSummary.php',
+    'Piwik\\Plugins\\CoreHome\\Widgets\\QuickLinks' => $baseDir . '/plugins/CoreHome/Widgets/QuickLinks.php',
     'Piwik\\Plugins\\CoreHome\\tests\\Integration\\Column\\UserIdTest' => $baseDir . '/plugins/CoreHome/tests/Integration/Column/UserIdTest.php',
     'Piwik\\Plugins\\CoreHome\\tests\\Integration\\CoreHomeTest' => $baseDir . '/plugins/CoreHome/tests/Integration/CoreHomeTest.php',
     'Piwik\\Plugins\\CoreHome\\tests\\Integration\\CustomLoginWhitelist' => $baseDir . '/plugins/CoreHome/tests/Integration/LoginWhitelistTest.php',
@@ -1346,6 +1358,7 @@
     'Piwik\\Plugins\\Feedback\\tests\\Unit\\FeedbackTest' => $baseDir . '/plugins/Feedback/tests/Integration/FeedbackTest.php',
     'Piwik\\Plugins\\GeoIp2\\Columns\\Region' => $baseDir . '/plugins/GeoIp2/Columns/Region.php',
     'Piwik\\Plugins\\GeoIp2\\Commands\\ConvertRegionCodesToIso' => $baseDir . '/plugins/GeoIp2/Commands/ConvertRegionCodesToIso.php',
+    'Piwik\\Plugins\\GeoIp2\\Commands\\UpdateRegionCodes' => $baseDir . '/plugins/GeoIp2/Commands/UpdateRegionCodes.php',
     'Piwik\\Plugins\\GeoIp2\\GeoIP2AutoUpdater' => $baseDir . '/plugins/GeoIp2/GeoIP2AutoUpdater.php',
     'Piwik\\Plugins\\GeoIp2\\GeoIp2' => $baseDir . '/plugins/GeoIp2/GeoIp2.php',
     'Piwik\\Plugins\\GeoIp2\\LocationProvider\\GeoIp2' => $baseDir . '/plugins/GeoIp2/LocationProvider/GeoIp2.php',
@@ -1508,6 +1521,7 @@
     'Piwik\\Plugins\\Live\\Categories\\RealTimeVisitorsSubcategory' => $baseDir . '/plugins/Live/Categories/RealTimeVisitorsSubcategory.php',
     'Piwik\\Plugins\\Live\\Categories\\VisitorLogSubcategory' => $baseDir . '/plugins/Live/Categories/VisitorLogSubcategory.php',
     'Piwik\\Plugins\\Live\\Controller' => $baseDir . '/plugins/Live/Controller.php',
+    'Piwik\\Plugins\\Live\\Exception\\MaxExecutionTimeExceededException' => $baseDir . '/plugins/Live/Exception/MaxExecutionTimeExceededException.php',
     'Piwik\\Plugins\\Live\\Live' => $baseDir . '/plugins/Live/Live.php',
     'Piwik\\Plugins\\Live\\Model' => $baseDir . '/plugins/Live/Model.php',
     'Piwik\\Plugins\\Live\\ProfileSummaryProvider' => $baseDir . '/plugins/Live/ProfileSummaryProvider.php',
@@ -1563,6 +1577,9 @@
     'Piwik\\Plugins\\Marketplace\\Api\\Exception' => $baseDir . '/plugins/Marketplace/Api/Exception.php',
     'Piwik\\Plugins\\Marketplace\\Api\\Service' => $baseDir . '/plugins/Marketplace/Api/Service.php',
     'Piwik\\Plugins\\Marketplace\\Api\\Service\\Exception' => $baseDir . '/plugins/Marketplace/Api/Service/Exception.php',
+    'Piwik\\Plugins\\Marketplace\\Categories\\BrowseSubcategory' => $baseDir . '/plugins/Marketplace/Categories/BrowseSubcategory.php',
+    'Piwik\\Plugins\\Marketplace\\Categories\\MarketplaceCategory' => $baseDir . '/plugins/Marketplace/Categories/MarketplaceCategory.php',
+    'Piwik\\Plugins\\Marketplace\\Categories\\PremiumFeaturesSubcategory' => $baseDir . '/plugins/Marketplace/Categories/PremiumFeaturesSubcategory.php',
     'Piwik\\Plugins\\Marketplace\\Consumer' => $baseDir . '/plugins/Marketplace/Consumer.php',
     'Piwik\\Plugins\\Marketplace\\Controller' => $baseDir . '/plugins/Marketplace/Controller.php',
     'Piwik\\Plugins\\Marketplace\\Environment' => $baseDir . '/plugins/Marketplace/Environment.php',
@@ -1579,6 +1596,7 @@
     'Piwik\\Plugins\\Marketplace\\UpdateCommunication' => $baseDir . '/plugins/Marketplace/UpdateCommunication.php',
     'Piwik\\Plugins\\Marketplace\\Widgets\\GetNewPlugins' => $baseDir . '/plugins/Marketplace/Widgets/GetNewPlugins.php',
     'Piwik\\Plugins\\Marketplace\\Widgets\\GetPremiumFeatures' => $baseDir . '/plugins/Marketplace/Widgets/GetPremiumFeatures.php',
+    'Piwik\\Plugins\\Marketplace\\Widgets\\Marketplace' => $baseDir . '/plugins/Marketplace/Widgets/Marketplace.php',
     'Piwik\\Plugins\\Marketplace\\tests\\Fixtures\\SimpleFixtureTrackFewVisits' => $baseDir . '/plugins/Marketplace/tests/Fixtures/SimpleFixtureTrackFewVisits.php',
     'Piwik\\Plugins\\Marketplace\\tests\\Framework\\Mock\\Client' => $baseDir . '/plugins/Marketplace/tests/Framework/Mock/Client.php',
     'Piwik\\Plugins\\Marketplace\\tests\\Framework\\Mock\\Consumer' => $baseDir . '/plugins/Marketplace/tests/Framework/Mock/Consumer.php',
@@ -1742,6 +1760,7 @@
     'Piwik\\Plugins\\Referrers\\DataTable\\Filter\\UrlsFromWebsiteId' => $baseDir . '/plugins/Referrers/DataTable/Filter/UrlsFromWebsiteId.php',
     'Piwik\\Plugins\\Referrers\\Referrers' => $baseDir . '/plugins/Referrers/Referrers.php',
     'Piwik\\Plugins\\Referrers\\Reports\\Base' => $baseDir . '/plugins/Referrers/Reports/Base.php',
+    'Piwik\\Plugins\\Referrers\\Reports\\Get' => $baseDir . '/plugins/Referrers/Reports/Get.php',
     'Piwik\\Plugins\\Referrers\\Reports\\GetAll' => $baseDir . '/plugins/Referrers/Reports/GetAll.php',
     'Piwik\\Plugins\\Referrers\\Reports\\GetCampaigns' => $baseDir . '/plugins/Referrers/Reports/GetCampaigns.php',
     'Piwik\\Plugins\\Referrers\\Reports\\GetKeywords' => $baseDir . '/plugins/Referrers/Reports/GetKeywords.php',
@@ -1767,6 +1786,7 @@
     'Piwik\\Plugins\\Referrers\\tests\\SearchEngineTest' => $baseDir . '/plugins/Referrers/tests/Unit/SearchEngineTest.php',
     'Piwik\\Plugins\\Referrers\\tests\\SocialTest' => $baseDir . '/plugins/Referrers/tests/Unit/SocialTest.php',
     'Piwik\\Plugins\\Referrers\\tests\\System\\ApiTest' => $baseDir . '/plugins/Referrers/tests/System/ApiTest.php',
+    'Piwik\\Plugins\\Referrers\\tests\\Unit\\DataTable\\Filter\\UrlsFromWebsiteIdTest' => $baseDir . '/plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php',
     'Piwik\\Plugins\\Resolution\\API' => $baseDir . '/plugins/Resolution/API.php',
     'Piwik\\Plugins\\Resolution\\Archiver' => $baseDir . '/plugins/Resolution/Archiver.php',
     'Piwik\\Plugins\\Resolution\\Columns\\Configuration' => $baseDir . '/plugins/Resolution/Columns/Configuration.php',
@@ -1821,6 +1841,7 @@
     'Piwik\\Plugins\\SegmentEditor\\Services\\StoredSegmentService' => $baseDir . '/plugins/SegmentEditor/Services/StoredSegmentService.php',
     'Piwik\\Plugins\\SegmentEditor\\UnprocessedSegmentException' => $baseDir . '/plugins/SegmentEditor/UnprocessedSegmentException.php',
     'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\ApiTest' => $baseDir . '/plugins/SegmentEditor/tests/Integration/ApiTest.php',
+    'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\ModelTest' => $baseDir . '/plugins/SegmentEditor/tests/Integration/ModelTest.php',
     'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\SegmentEditorTest' => $baseDir . '/plugins/SegmentEditor/tests/Integration/SegmentEditorTest.php',
     'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\SegmentFormatterTest' => $baseDir . '/plugins/SegmentEditor/tests/Integration/SegmentFormatterTest.php',
     'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\SegmentListTest' => $baseDir . '/plugins/SegmentEditor/tests/Integration/SegmentListTest.php',
@@ -2245,6 +2266,7 @@
     'Piwik\\Plugins\\UsersManager\\LastSeenTimeLogger' => $baseDir . '/plugins/UsersManager/LastSeenTimeLogger.php',
     'Piwik\\Plugins\\UsersManager\\Menu' => $baseDir . '/plugins/UsersManager/Menu.php',
     'Piwik\\Plugins\\UsersManager\\Model' => $baseDir . '/plugins/UsersManager/Model.php',
+    'Piwik\\Plugins\\UsersManager\\NewsletterSignup' => $baseDir . '/plugins/UsersManager/NewsletterSignup.php',
     'Piwik\\Plugins\\UsersManager\\Sql\\SiteAccessFilter' => $baseDir . '/plugins/UsersManager/Sql/SiteAccessFilter.php',
     'Piwik\\Plugins\\UsersManager\\Sql\\UserTableFilter' => $baseDir . '/plugins/UsersManager/Sql/UserTableFilter.php',
     'Piwik\\Plugins\\UsersManager\\Tasks' => $baseDir . '/plugins/UsersManager/Tasks.php',
@@ -2567,6 +2589,8 @@
     'Piwik\\Updates\\Updates_3_10_0_b2' => $baseDir . '/core/Updates/3.10.0-b2.php',
     'Piwik\\Updates\\Updates_3_10_0_rc5' => $baseDir . '/core/Updates/3.10.0-rc5.php',
     'Piwik\\Updates\\Updates_3_11_0_b1' => $baseDir . '/core/Updates/3.11.0-b1.php',
+    'Piwik\\Updates\\Updates_3_12_0_b1' => $baseDir . '/core/Updates/3.12.0-b1.php',
+    'Piwik\\Updates\\Updates_3_12_0_b7' => $baseDir . '/core/Updates/3.12.0-b7.php',
     'Piwik\\Updates\\Updates_3_5_0_b2' => $baseDir . '/core/Updates/3.5.0-b2.php',
     'Piwik\\Updates\\Updates_3_5_0_b4' => $baseDir . '/core/Updates/3.5.0-b4.php',
     'Piwik\\Updates\\Updates_3_5_0_rc2' => $baseDir . '/core/Updates/3.5.0-rc2.php',
@@ -2579,6 +2603,7 @@
     'Piwik\\Updates\\Updates_3_7_0_b1' => $baseDir . '/core/Updates/3.7.0-b1.php',
     'Piwik\\Updates\\Updates_3_8_0_b3' => $baseDir . '/core/Updates/3.8.0-b3.php',
     'Piwik\\Updates\\Updates_3_8_0_b4' => $baseDir . '/core/Updates/3.8.0-b4.php',
+    'Piwik\\Updates\\Updates_4_0_0_b1' => $baseDir . '/core/Updates/4.0.0-b1.php',
     'Piwik\\Url' => $baseDir . '/core/Url.php',
     'Piwik\\UrlHelper' => $baseDir . '/core/UrlHelper.php',
     'Piwik\\Validators\\BaseValidator' => $baseDir . '/core/Validators/BaseValidator.php',
@@ -2835,6 +2860,7 @@
     'Twig\\Node\\EmbedNode' => $vendorDir . '/twig/twig/src/Node/EmbedNode.php',
     'Twig\\Node\\Expression\\AbstractExpression' => $vendorDir . '/twig/twig/src/Node/Expression/AbstractExpression.php',
     'Twig\\Node\\Expression\\ArrayExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ArrayExpression.php',
+    'Twig\\Node\\Expression\\ArrowFunctionExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ArrowFunctionExpression.php',
     'Twig\\Node\\Expression\\AssignNameExpression' => $vendorDir . '/twig/twig/src/Node/Expression/AssignNameExpression.php',
     'Twig\\Node\\Expression\\Binary\\AbstractBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/AbstractBinary.php',
     'Twig\\Node\\Expression\\Binary\\AddBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/AddBinary.php',
@@ -2870,6 +2896,7 @@
     'Twig\\Node\\Expression\\Filter\\DefaultFilter' => $vendorDir . '/twig/twig/src/Node/Expression/Filter/DefaultFilter.php',
     'Twig\\Node\\Expression\\FunctionExpression' => $vendorDir . '/twig/twig/src/Node/Expression/FunctionExpression.php',
     'Twig\\Node\\Expression\\GetAttrExpression' => $vendorDir . '/twig/twig/src/Node/Expression/GetAttrExpression.php',
+    'Twig\\Node\\Expression\\InlinePrint' => $vendorDir . '/twig/twig/src/Node/Expression/InlinePrint.php',
     'Twig\\Node\\Expression\\MethodCallExpression' => $vendorDir . '/twig/twig/src/Node/Expression/MethodCallExpression.php',
     'Twig\\Node\\Expression\\NameExpression' => $vendorDir . '/twig/twig/src/Node/Expression/NameExpression.php',
     'Twig\\Node\\Expression\\NullCoalesceExpression' => $vendorDir . '/twig/twig/src/Node/Expression/NullCoalesceExpression.php',
@@ -2933,6 +2960,7 @@
     'Twig\\Test\\NodeTestCase' => $vendorDir . '/twig/twig/src/Test/NodeTestCase.php',
     'Twig\\Token' => $vendorDir . '/twig/twig/src/Token.php',
     'Twig\\TokenParser\\AbstractTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/AbstractTokenParser.php',
+    'Twig\\TokenParser\\ApplyTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/ApplyTokenParser.php',
     'Twig\\TokenParser\\AutoEscapeTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/AutoEscapeTokenParser.php',
     'Twig\\TokenParser\\BlockTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/BlockTokenParser.php',
     'Twig\\TokenParser\\DeprecatedTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/DeprecatedTokenParser.php',
@@ -3160,18 +3188,6 @@
     'Twig_Util_DeprecationCollector' => $vendorDir . '/twig/twig/lib/Twig/Util/DeprecationCollector.php',
     'Twig_Util_TemplateDirIterator' => $vendorDir . '/twig/twig/lib/Twig/Util/TemplateDirIterator.php',
     'Zend_Config' => $baseDir . '/libs/Zend/Config.php',
-    'Zend_Config_Exception' => $baseDir . '/libs/Zend/Config/Exception.php',
-    'Zend_Config_Ini' => $baseDir . '/libs/Zend/Config/Ini.php',
-    'Zend_Config_Json' => $baseDir . '/libs/Zend/Config/Json.php',
-    'Zend_Config_Writer' => $baseDir . '/libs/Zend/Config/Writer.php',
-    'Zend_Config_Writer_Array' => $baseDir . '/libs/Zend/Config/Writer/Array.php',
-    'Zend_Config_Writer_FileAbstract' => $baseDir . '/libs/Zend/Config/Writer/FileAbstract.php',
-    'Zend_Config_Writer_Ini' => $baseDir . '/libs/Zend/Config/Writer/Ini.php',
-    'Zend_Config_Writer_Json' => $baseDir . '/libs/Zend/Config/Writer/Json.php',
-    'Zend_Config_Writer_Xml' => $baseDir . '/libs/Zend/Config/Writer/Xml.php',
-    'Zend_Config_Writer_Yaml' => $baseDir . '/libs/Zend/Config/Writer/Yaml.php',
-    'Zend_Config_Xml' => $baseDir . '/libs/Zend/Config/Xml.php',
-    'Zend_Config_Yaml' => $baseDir . '/libs/Zend/Config/Yaml.php',
     'Zend_Db' => $baseDir . '/libs/Zend/Db.php',
     'Zend_Db_Adapter_Abstract' => $baseDir . '/libs/Zend/Db/Adapter/Abstract.php',
     'Zend_Db_Adapter_Db2' => $baseDir . '/libs/Zend/Db/Adapter/Db2.php',
diff --git a/app/vendor/composer/autoload_real.php b/app/vendor/composer/autoload_real.php
index daa448092..c40687869 100644
--- a/app/vendor/composer/autoload_real.php
+++ b/app/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@
 
 // autoload_real.php @generated by Composer
 
-class ComposerAutoloaderInit20965b5e193daae3814e448b2561c788
+class ComposerAutoloaderInita8ac692f43bf07ab29c010fd95958205
 {
     private static $loader;
 
@@ -19,9 +19,9 @@ public static function getLoader()
             return self::$loader;
         }
 
-        spl_autoload_register(array('ComposerAutoloaderInit20965b5e193daae3814e448b2561c788', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInita8ac692f43bf07ab29c010fd95958205', 'loadClassLoader'), true, false);
         self::$loader = $loader = new \Composer\Autoload\ClassLoader();
-        spl_autoload_unregister(array('ComposerAutoloaderInit20965b5e193daae3814e448b2561c788', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInita8ac692f43bf07ab29c010fd95958205', 'loadClassLoader'));
 
         $includePaths = require __DIR__ . '/include_paths.php';
         $includePaths[] = get_include_path();
@@ -31,7 +31,7 @@ public static function getLoader()
         if ($useStaticLoader) {
             require_once __DIR__ . '/autoload_static.php';
 
-            call_user_func(\Composer\Autoload\ComposerStaticInit20965b5e193daae3814e448b2561c788::getInitializer($loader));
+            call_user_func(\Composer\Autoload\ComposerStaticInita8ac692f43bf07ab29c010fd95958205::getInitializer($loader));
         } else {
             $map = require __DIR__ . '/autoload_namespaces.php';
             foreach ($map as $namespace => $path) {
@@ -52,19 +52,19 @@ public static function getLoader()
         $loader->register(false);
 
         if ($useStaticLoader) {
-            $includeFiles = Composer\Autoload\ComposerStaticInit20965b5e193daae3814e448b2561c788::$files;
+            $includeFiles = Composer\Autoload\ComposerStaticInita8ac692f43bf07ab29c010fd95958205::$files;
         } else {
             $includeFiles = require __DIR__ . '/autoload_files.php';
         }
         foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequire20965b5e193daae3814e448b2561c788($fileIdentifier, $file);
+            composerRequirea8ac692f43bf07ab29c010fd95958205($fileIdentifier, $file);
         }
 
         return $loader;
     }
 }
 
-function composerRequire20965b5e193daae3814e448b2561c788($fileIdentifier, $file)
+function composerRequirea8ac692f43bf07ab29c010fd95958205($fileIdentifier, $file)
 {
     if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
         require $file;
diff --git a/app/vendor/composer/autoload_static.php b/app/vendor/composer/autoload_static.php
index 76c0eb6d9..7e42403e1 100644
--- a/app/vendor/composer/autoload_static.php
+++ b/app/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@
 
 namespace Composer\Autoload;
 
-class ComposerStaticInit20965b5e193daae3814e448b2561c788
+class ComposerStaticInita8ac692f43bf07ab29c010fd95958205
 {
     public static $files = array (
         '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php',
@@ -743,7 +743,11 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Common' => __DIR__ . '/../..' . '/core/Common.php',
         'Piwik\\Composer\\ScriptHandler' => __DIR__ . '/../..' . '/core/Composer/ScriptHandler.php',
         'Piwik\\Concurrency\\DistributedList' => __DIR__ . '/../..' . '/core/Concurrency/DistributedList.php',
+        'Piwik\\Concurrency\\Lock' => __DIR__ . '/../..' . '/core/Concurrency/Lock.php',
+        'Piwik\\Concurrency\\LockBackend' => __DIR__ . '/../..' . '/core/Concurrency/LockBackend.php',
+        'Piwik\\Concurrency\\LockBackend\\MySqlLockBackend' => __DIR__ . '/../..' . '/core/Concurrency/LockBackend/MySqlLockBackend.php',
         'Piwik\\Config' => __DIR__ . '/../..' . '/core/Config.php',
+        'Piwik\\Config\\Cache' => __DIR__ . '/../..' . '/core/Config/Cache.php',
         'Piwik\\Config\\ConfigNotFoundException' => __DIR__ . '/../..' . '/core/Config/ConfigNotFoundException.php',
         'Piwik\\Config\\IniFileChain' => __DIR__ . '/../..' . '/core/Config/IniFileChain.php',
         'Piwik\\Console' => __DIR__ . '/../..' . '/core/Console.php',
@@ -769,6 +773,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\DataAccess\\LogQueryBuilder' => __DIR__ . '/../..' . '/core/DataAccess/LogQueryBuilder.php',
         'Piwik\\DataAccess\\LogQueryBuilder\\JoinGenerator' => __DIR__ . '/../..' . '/core/DataAccess/LogQueryBuilder/JoinGenerator.php',
         'Piwik\\DataAccess\\LogQueryBuilder\\JoinTables' => __DIR__ . '/../..' . '/core/DataAccess/LogQueryBuilder/JoinTables.php',
+        'Piwik\\DataAccess\\LogTableTemporary' => __DIR__ . '/../..' . '/core/DataAccess/LogTableTemporary.php',
         'Piwik\\DataAccess\\Model' => __DIR__ . '/../..' . '/core/DataAccess/Model.php',
         'Piwik\\DataAccess\\RawLogDao' => __DIR__ . '/../..' . '/core/DataAccess/RawLogDao.php',
         'Piwik\\DataAccess\\TableMetadata' => __DIR__ . '/../..' . '/core/DataAccess/TableMetadata.php',
@@ -841,14 +846,16 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Db\\SchemaInterface' => __DIR__ . '/../..' . '/core/Db/SchemaInterface.php',
         'Piwik\\Db\\Schema\\Mysql' => __DIR__ . '/../..' . '/core/Db/Schema/Mysql.php',
         'Piwik\\Db\\Settings' => __DIR__ . '/../..' . '/core/Db/Settings.php',
+        'Piwik\\Db\\TransactionLevel' => __DIR__ . '/../..' . '/core/Db/TransactionLevel.php',
         'Piwik\\Decompress\\DecompressInterface' => __DIR__ . '/..' . '/piwik/decompress/src/DecompressInterface.php',
         'Piwik\\Decompress\\Gzip' => __DIR__ . '/..' . '/piwik/decompress/src/Gzip.php',
         'Piwik\\Decompress\\PclZip' => __DIR__ . '/..' . '/piwik/decompress/src/PclZip.php',
         'Piwik\\Decompress\\Tar' => __DIR__ . '/..' . '/piwik/decompress/src/Tar.php',
         'Piwik\\Decompress\\ZipArchive' => __DIR__ . '/..' . '/piwik/decompress/src/ZipArchive.php',
         'Piwik\\Development' => __DIR__ . '/../..' . '/core/Development.php',
-        'Piwik\\DeviceDetectorCache' => __DIR__ . '/../..' . '/core/DeviceDetectorCache.php',
         'Piwik\\DeviceDetectorFactory' => __DIR__ . '/../..' . '/core/DeviceDetectorFactory.php',
+        'Piwik\\DeviceDetector\\DeviceDetectorCache' => __DIR__ . '/../..' . '/core/DeviceDetector/DeviceDetectorCache.php',
+        'Piwik\\DeviceDetector\\DeviceDetectorFactory' => __DIR__ . '/../..' . '/core/DeviceDetector/DeviceDetectorFactory.php',
         'Piwik\\Email\\ContentGenerator' => __DIR__ . '/../..' . '/core/Email/ContentGenerator.php',
         'Piwik\\ErrorHandler' => __DIR__ . '/../..' . '/core/ErrorHandler.php',
         'Piwik\\EventDispatcher' => __DIR__ . '/../..' . '/core/EventDispatcher.php',
@@ -960,6 +967,8 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\API\\API' => __DIR__ . '/../..' . '/plugins/API/API.php',
         'Piwik\\Plugins\\API\\Controller' => __DIR__ . '/../..' . '/plugins/API/Controller.php',
         'Piwik\\Plugins\\API\\DataTable\\MergeDataTables' => __DIR__ . '/../..' . '/plugins/API/DataTable/MergeDataTables.php',
+        'Piwik\\Plugins\\API\\Filter\\DataComparisonFilter' => __DIR__ . '/../..' . '/plugins/API/Filter/DataComparisonFilter.php',
+        'Piwik\\Plugins\\API\\Filter\\DataComparisonFilter\\ComparisonRowGenerator' => __DIR__ . '/../..' . '/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php',
         'Piwik\\Plugins\\API\\Glossary' => __DIR__ . '/../..' . '/plugins/API/Glossary.php',
         'Piwik\\Plugins\\API\\Menu' => __DIR__ . '/../..' . '/plugins/API/Menu.php',
         'Piwik\\Plugins\\API\\Plugin' => __DIR__ . '/../..' . '/plugins/API/API.php',
@@ -980,6 +989,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\API\\WidgetMetadata' => __DIR__ . '/../..' . '/plugins/API/WidgetMetadata.php',
         'Piwik\\Plugins\\API\\test\\Unit\\CsvRendererTest' => __DIR__ . '/../..' . '/plugins/API/tests/Unit/CsvRendererTest.php',
         'Piwik\\Plugins\\API\\tests\\Integration\\APITest' => __DIR__ . '/../..' . '/plugins/API/tests/Integration/APITest.php',
+        'Piwik\\Plugins\\API\\tests\\Integration\\Filter\\DataComparisonFilter\\ComparisonRowGeneratorTest' => __DIR__ . '/../..' . '/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php',
         'Piwik\\Plugins\\API\\tests\\Integration\\RowEvolutionTest' => __DIR__ . '/../..' . '/plugins/API/tests/Integration/RowEvolutionTest.php',
         'Piwik\\Plugins\\API\\tests\\Integration\\RssRendererTest' => __DIR__ . '/../..' . '/plugins/API/tests/Integration/RssRendererTest.php',
         'Piwik\\Plugins\\API\\tests\\System\\AutoSuggestAPITest' => __DIR__ . '/../..' . '/plugins/API/tests/System/AutoSuggestAPITest.php',
@@ -1054,6 +1064,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\Actions\\Segment' => __DIR__ . '/../..' . '/plugins/Actions/Segment.php',
         'Piwik\\Plugins\\Actions\\Tracker\\ActionsRequestProcessor' => __DIR__ . '/../..' . '/plugins/Actions/Tracker/ActionsRequestProcessor.php',
         'Piwik\\Plugins\\Actions\\VisitorDetails' => __DIR__ . '/../..' . '/plugins/Actions/VisitorDetails.php',
+        'Piwik\\Plugins\\Actions\\tests\\System\\ApiTest' => __DIR__ . '/../..' . '/plugins/Actions/tests/System/ApiTest.php',
         'Piwik\\Plugins\\Actions\\tests\\Unit\\ActionSiteSearchTest' => __DIR__ . '/../..' . '/plugins/Actions/tests/Integration/ActionSiteSearchTest.php',
         'Piwik\\Plugins\\Actions\\tests\\Unit\\ArchiverTests' => __DIR__ . '/../..' . '/plugins/Actions/tests/Unit/ArchiverTest.php',
         'Piwik\\Plugins\\Annotations\\API' => __DIR__ . '/../..' . '/plugins/Annotations/API.php',
@@ -1228,6 +1239,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\CoreHome\\Widgets\\GetDonateForm' => __DIR__ . '/../..' . '/plugins/CoreHome/Widgets/GetDonateForm.php',
         'Piwik\\Plugins\\CoreHome\\Widgets\\GetPromoVideo' => __DIR__ . '/../..' . '/plugins/CoreHome/Widgets/GetPromoVideo.php',
         'Piwik\\Plugins\\CoreHome\\Widgets\\GetSystemSummary' => __DIR__ . '/../..' . '/plugins/CoreHome/Widgets/GetSystemSummary.php',
+        'Piwik\\Plugins\\CoreHome\\Widgets\\QuickLinks' => __DIR__ . '/../..' . '/plugins/CoreHome/Widgets/QuickLinks.php',
         'Piwik\\Plugins\\CoreHome\\tests\\Integration\\Column\\UserIdTest' => __DIR__ . '/../..' . '/plugins/CoreHome/tests/Integration/Column/UserIdTest.php',
         'Piwik\\Plugins\\CoreHome\\tests\\Integration\\CoreHomeTest' => __DIR__ . '/../..' . '/plugins/CoreHome/tests/Integration/CoreHomeTest.php',
         'Piwik\\Plugins\\CoreHome\\tests\\Integration\\CustomLoginWhitelist' => __DIR__ . '/../..' . '/plugins/CoreHome/tests/Integration/LoginWhitelistTest.php',
@@ -1588,6 +1600,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\Feedback\\tests\\Unit\\FeedbackTest' => __DIR__ . '/../..' . '/plugins/Feedback/tests/Integration/FeedbackTest.php',
         'Piwik\\Plugins\\GeoIp2\\Columns\\Region' => __DIR__ . '/../..' . '/plugins/GeoIp2/Columns/Region.php',
         'Piwik\\Plugins\\GeoIp2\\Commands\\ConvertRegionCodesToIso' => __DIR__ . '/../..' . '/plugins/GeoIp2/Commands/ConvertRegionCodesToIso.php',
+        'Piwik\\Plugins\\GeoIp2\\Commands\\UpdateRegionCodes' => __DIR__ . '/../..' . '/plugins/GeoIp2/Commands/UpdateRegionCodes.php',
         'Piwik\\Plugins\\GeoIp2\\GeoIP2AutoUpdater' => __DIR__ . '/../..' . '/plugins/GeoIp2/GeoIP2AutoUpdater.php',
         'Piwik\\Plugins\\GeoIp2\\GeoIp2' => __DIR__ . '/../..' . '/plugins/GeoIp2/GeoIp2.php',
         'Piwik\\Plugins\\GeoIp2\\LocationProvider\\GeoIp2' => __DIR__ . '/../..' . '/plugins/GeoIp2/LocationProvider/GeoIp2.php',
@@ -1750,6 +1763,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\Live\\Categories\\RealTimeVisitorsSubcategory' => __DIR__ . '/../..' . '/plugins/Live/Categories/RealTimeVisitorsSubcategory.php',
         'Piwik\\Plugins\\Live\\Categories\\VisitorLogSubcategory' => __DIR__ . '/../..' . '/plugins/Live/Categories/VisitorLogSubcategory.php',
         'Piwik\\Plugins\\Live\\Controller' => __DIR__ . '/../..' . '/plugins/Live/Controller.php',
+        'Piwik\\Plugins\\Live\\Exception\\MaxExecutionTimeExceededException' => __DIR__ . '/../..' . '/plugins/Live/Exception/MaxExecutionTimeExceededException.php',
         'Piwik\\Plugins\\Live\\Live' => __DIR__ . '/../..' . '/plugins/Live/Live.php',
         'Piwik\\Plugins\\Live\\Model' => __DIR__ . '/../..' . '/plugins/Live/Model.php',
         'Piwik\\Plugins\\Live\\ProfileSummaryProvider' => __DIR__ . '/../..' . '/plugins/Live/ProfileSummaryProvider.php',
@@ -1805,6 +1819,9 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\Marketplace\\Api\\Exception' => __DIR__ . '/../..' . '/plugins/Marketplace/Api/Exception.php',
         'Piwik\\Plugins\\Marketplace\\Api\\Service' => __DIR__ . '/../..' . '/plugins/Marketplace/Api/Service.php',
         'Piwik\\Plugins\\Marketplace\\Api\\Service\\Exception' => __DIR__ . '/../..' . '/plugins/Marketplace/Api/Service/Exception.php',
+        'Piwik\\Plugins\\Marketplace\\Categories\\BrowseSubcategory' => __DIR__ . '/../..' . '/plugins/Marketplace/Categories/BrowseSubcategory.php',
+        'Piwik\\Plugins\\Marketplace\\Categories\\MarketplaceCategory' => __DIR__ . '/../..' . '/plugins/Marketplace/Categories/MarketplaceCategory.php',
+        'Piwik\\Plugins\\Marketplace\\Categories\\PremiumFeaturesSubcategory' => __DIR__ . '/../..' . '/plugins/Marketplace/Categories/PremiumFeaturesSubcategory.php',
         'Piwik\\Plugins\\Marketplace\\Consumer' => __DIR__ . '/../..' . '/plugins/Marketplace/Consumer.php',
         'Piwik\\Plugins\\Marketplace\\Controller' => __DIR__ . '/../..' . '/plugins/Marketplace/Controller.php',
         'Piwik\\Plugins\\Marketplace\\Environment' => __DIR__ . '/../..' . '/plugins/Marketplace/Environment.php',
@@ -1821,6 +1838,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\Marketplace\\UpdateCommunication' => __DIR__ . '/../..' . '/plugins/Marketplace/UpdateCommunication.php',
         'Piwik\\Plugins\\Marketplace\\Widgets\\GetNewPlugins' => __DIR__ . '/../..' . '/plugins/Marketplace/Widgets/GetNewPlugins.php',
         'Piwik\\Plugins\\Marketplace\\Widgets\\GetPremiumFeatures' => __DIR__ . '/../..' . '/plugins/Marketplace/Widgets/GetPremiumFeatures.php',
+        'Piwik\\Plugins\\Marketplace\\Widgets\\Marketplace' => __DIR__ . '/../..' . '/plugins/Marketplace/Widgets/Marketplace.php',
         'Piwik\\Plugins\\Marketplace\\tests\\Fixtures\\SimpleFixtureTrackFewVisits' => __DIR__ . '/../..' . '/plugins/Marketplace/tests/Fixtures/SimpleFixtureTrackFewVisits.php',
         'Piwik\\Plugins\\Marketplace\\tests\\Framework\\Mock\\Client' => __DIR__ . '/../..' . '/plugins/Marketplace/tests/Framework/Mock/Client.php',
         'Piwik\\Plugins\\Marketplace\\tests\\Framework\\Mock\\Consumer' => __DIR__ . '/../..' . '/plugins/Marketplace/tests/Framework/Mock/Consumer.php',
@@ -1984,6 +2002,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\Referrers\\DataTable\\Filter\\UrlsFromWebsiteId' => __DIR__ . '/../..' . '/plugins/Referrers/DataTable/Filter/UrlsFromWebsiteId.php',
         'Piwik\\Plugins\\Referrers\\Referrers' => __DIR__ . '/../..' . '/plugins/Referrers/Referrers.php',
         'Piwik\\Plugins\\Referrers\\Reports\\Base' => __DIR__ . '/../..' . '/plugins/Referrers/Reports/Base.php',
+        'Piwik\\Plugins\\Referrers\\Reports\\Get' => __DIR__ . '/../..' . '/plugins/Referrers/Reports/Get.php',
         'Piwik\\Plugins\\Referrers\\Reports\\GetAll' => __DIR__ . '/../..' . '/plugins/Referrers/Reports/GetAll.php',
         'Piwik\\Plugins\\Referrers\\Reports\\GetCampaigns' => __DIR__ . '/../..' . '/plugins/Referrers/Reports/GetCampaigns.php',
         'Piwik\\Plugins\\Referrers\\Reports\\GetKeywords' => __DIR__ . '/../..' . '/plugins/Referrers/Reports/GetKeywords.php',
@@ -2009,6 +2028,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\Referrers\\tests\\SearchEngineTest' => __DIR__ . '/../..' . '/plugins/Referrers/tests/Unit/SearchEngineTest.php',
         'Piwik\\Plugins\\Referrers\\tests\\SocialTest' => __DIR__ . '/../..' . '/plugins/Referrers/tests/Unit/SocialTest.php',
         'Piwik\\Plugins\\Referrers\\tests\\System\\ApiTest' => __DIR__ . '/../..' . '/plugins/Referrers/tests/System/ApiTest.php',
+        'Piwik\\Plugins\\Referrers\\tests\\Unit\\DataTable\\Filter\\UrlsFromWebsiteIdTest' => __DIR__ . '/../..' . '/plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php',
         'Piwik\\Plugins\\Resolution\\API' => __DIR__ . '/../..' . '/plugins/Resolution/API.php',
         'Piwik\\Plugins\\Resolution\\Archiver' => __DIR__ . '/../..' . '/plugins/Resolution/Archiver.php',
         'Piwik\\Plugins\\Resolution\\Columns\\Configuration' => __DIR__ . '/../..' . '/plugins/Resolution/Columns/Configuration.php',
@@ -2063,6 +2083,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\SegmentEditor\\Services\\StoredSegmentService' => __DIR__ . '/../..' . '/plugins/SegmentEditor/Services/StoredSegmentService.php',
         'Piwik\\Plugins\\SegmentEditor\\UnprocessedSegmentException' => __DIR__ . '/../..' . '/plugins/SegmentEditor/UnprocessedSegmentException.php',
         'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\ApiTest' => __DIR__ . '/../..' . '/plugins/SegmentEditor/tests/Integration/ApiTest.php',
+        'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\ModelTest' => __DIR__ . '/../..' . '/plugins/SegmentEditor/tests/Integration/ModelTest.php',
         'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\SegmentEditorTest' => __DIR__ . '/../..' . '/plugins/SegmentEditor/tests/Integration/SegmentEditorTest.php',
         'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\SegmentFormatterTest' => __DIR__ . '/../..' . '/plugins/SegmentEditor/tests/Integration/SegmentFormatterTest.php',
         'Piwik\\Plugins\\SegmentEditor\\tests\\Integration\\SegmentListTest' => __DIR__ . '/../..' . '/plugins/SegmentEditor/tests/Integration/SegmentListTest.php',
@@ -2487,6 +2508,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Plugins\\UsersManager\\LastSeenTimeLogger' => __DIR__ . '/../..' . '/plugins/UsersManager/LastSeenTimeLogger.php',
         'Piwik\\Plugins\\UsersManager\\Menu' => __DIR__ . '/../..' . '/plugins/UsersManager/Menu.php',
         'Piwik\\Plugins\\UsersManager\\Model' => __DIR__ . '/../..' . '/plugins/UsersManager/Model.php',
+        'Piwik\\Plugins\\UsersManager\\NewsletterSignup' => __DIR__ . '/../..' . '/plugins/UsersManager/NewsletterSignup.php',
         'Piwik\\Plugins\\UsersManager\\Sql\\SiteAccessFilter' => __DIR__ . '/../..' . '/plugins/UsersManager/Sql/SiteAccessFilter.php',
         'Piwik\\Plugins\\UsersManager\\Sql\\UserTableFilter' => __DIR__ . '/../..' . '/plugins/UsersManager/Sql/UserTableFilter.php',
         'Piwik\\Plugins\\UsersManager\\Tasks' => __DIR__ . '/../..' . '/plugins/UsersManager/Tasks.php',
@@ -2809,6 +2831,8 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Updates\\Updates_3_10_0_b2' => __DIR__ . '/../..' . '/core/Updates/3.10.0-b2.php',
         'Piwik\\Updates\\Updates_3_10_0_rc5' => __DIR__ . '/../..' . '/core/Updates/3.10.0-rc5.php',
         'Piwik\\Updates\\Updates_3_11_0_b1' => __DIR__ . '/../..' . '/core/Updates/3.11.0-b1.php',
+        'Piwik\\Updates\\Updates_3_12_0_b1' => __DIR__ . '/../..' . '/core/Updates/3.12.0-b1.php',
+        'Piwik\\Updates\\Updates_3_12_0_b7' => __DIR__ . '/../..' . '/core/Updates/3.12.0-b7.php',
         'Piwik\\Updates\\Updates_3_5_0_b2' => __DIR__ . '/../..' . '/core/Updates/3.5.0-b2.php',
         'Piwik\\Updates\\Updates_3_5_0_b4' => __DIR__ . '/../..' . '/core/Updates/3.5.0-b4.php',
         'Piwik\\Updates\\Updates_3_5_0_rc2' => __DIR__ . '/../..' . '/core/Updates/3.5.0-rc2.php',
@@ -2821,6 +2845,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Piwik\\Updates\\Updates_3_7_0_b1' => __DIR__ . '/../..' . '/core/Updates/3.7.0-b1.php',
         'Piwik\\Updates\\Updates_3_8_0_b3' => __DIR__ . '/../..' . '/core/Updates/3.8.0-b3.php',
         'Piwik\\Updates\\Updates_3_8_0_b4' => __DIR__ . '/../..' . '/core/Updates/3.8.0-b4.php',
+        'Piwik\\Updates\\Updates_4_0_0_b1' => __DIR__ . '/../..' . '/core/Updates/4.0.0-b1.php',
         'Piwik\\Url' => __DIR__ . '/../..' . '/core/Url.php',
         'Piwik\\UrlHelper' => __DIR__ . '/../..' . '/core/UrlHelper.php',
         'Piwik\\Validators\\BaseValidator' => __DIR__ . '/../..' . '/core/Validators/BaseValidator.php',
@@ -3077,6 +3102,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Twig\\Node\\EmbedNode' => __DIR__ . '/..' . '/twig/twig/src/Node/EmbedNode.php',
         'Twig\\Node\\Expression\\AbstractExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/AbstractExpression.php',
         'Twig\\Node\\Expression\\ArrayExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ArrayExpression.php',
+        'Twig\\Node\\Expression\\ArrowFunctionExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ArrowFunctionExpression.php',
         'Twig\\Node\\Expression\\AssignNameExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/AssignNameExpression.php',
         'Twig\\Node\\Expression\\Binary\\AbstractBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/AbstractBinary.php',
         'Twig\\Node\\Expression\\Binary\\AddBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/AddBinary.php',
@@ -3112,6 +3138,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Twig\\Node\\Expression\\Filter\\DefaultFilter' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Filter/DefaultFilter.php',
         'Twig\\Node\\Expression\\FunctionExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FunctionExpression.php',
         'Twig\\Node\\Expression\\GetAttrExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/GetAttrExpression.php',
+        'Twig\\Node\\Expression\\InlinePrint' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/InlinePrint.php',
         'Twig\\Node\\Expression\\MethodCallExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/MethodCallExpression.php',
         'Twig\\Node\\Expression\\NameExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/NameExpression.php',
         'Twig\\Node\\Expression\\NullCoalesceExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/NullCoalesceExpression.php',
@@ -3175,6 +3202,7 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Twig\\Test\\NodeTestCase' => __DIR__ . '/..' . '/twig/twig/src/Test/NodeTestCase.php',
         'Twig\\Token' => __DIR__ . '/..' . '/twig/twig/src/Token.php',
         'Twig\\TokenParser\\AbstractTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/AbstractTokenParser.php',
+        'Twig\\TokenParser\\ApplyTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/ApplyTokenParser.php',
         'Twig\\TokenParser\\AutoEscapeTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/AutoEscapeTokenParser.php',
         'Twig\\TokenParser\\BlockTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/BlockTokenParser.php',
         'Twig\\TokenParser\\DeprecatedTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/DeprecatedTokenParser.php',
@@ -3402,18 +3430,6 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
         'Twig_Util_DeprecationCollector' => __DIR__ . '/..' . '/twig/twig/lib/Twig/Util/DeprecationCollector.php',
         'Twig_Util_TemplateDirIterator' => __DIR__ . '/..' . '/twig/twig/lib/Twig/Util/TemplateDirIterator.php',
         'Zend_Config' => __DIR__ . '/../..' . '/libs/Zend/Config.php',
-        'Zend_Config_Exception' => __DIR__ . '/../..' . '/libs/Zend/Config/Exception.php',
-        'Zend_Config_Ini' => __DIR__ . '/../..' . '/libs/Zend/Config/Ini.php',
-        'Zend_Config_Json' => __DIR__ . '/../..' . '/libs/Zend/Config/Json.php',
-        'Zend_Config_Writer' => __DIR__ . '/../..' . '/libs/Zend/Config/Writer.php',
-        'Zend_Config_Writer_Array' => __DIR__ . '/../..' . '/libs/Zend/Config/Writer/Array.php',
-        'Zend_Config_Writer_FileAbstract' => __DIR__ . '/../..' . '/libs/Zend/Config/Writer/FileAbstract.php',
-        'Zend_Config_Writer_Ini' => __DIR__ . '/../..' . '/libs/Zend/Config/Writer/Ini.php',
-        'Zend_Config_Writer_Json' => __DIR__ . '/../..' . '/libs/Zend/Config/Writer/Json.php',
-        'Zend_Config_Writer_Xml' => __DIR__ . '/../..' . '/libs/Zend/Config/Writer/Xml.php',
-        'Zend_Config_Writer_Yaml' => __DIR__ . '/../..' . '/libs/Zend/Config/Writer/Yaml.php',
-        'Zend_Config_Xml' => __DIR__ . '/../..' . '/libs/Zend/Config/Xml.php',
-        'Zend_Config_Yaml' => __DIR__ . '/../..' . '/libs/Zend/Config/Yaml.php',
         'Zend_Db' => __DIR__ . '/../..' . '/libs/Zend/Db.php',
         'Zend_Db_Adapter_Abstract' => __DIR__ . '/../..' . '/libs/Zend/Db/Adapter/Abstract.php',
         'Zend_Db_Adapter_Db2' => __DIR__ . '/../..' . '/libs/Zend/Db/Adapter/Db2.php',
@@ -3575,11 +3591,11 @@ class ComposerStaticInit20965b5e193daae3814e448b2561c788
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit20965b5e193daae3814e448b2561c788::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit20965b5e193daae3814e448b2561c788::$prefixDirsPsr4;
-            $loader->prefixesPsr0 = ComposerStaticInit20965b5e193daae3814e448b2561c788::$prefixesPsr0;
-            $loader->fallbackDirsPsr0 = ComposerStaticInit20965b5e193daae3814e448b2561c788::$fallbackDirsPsr0;
-            $loader->classMap = ComposerStaticInit20965b5e193daae3814e448b2561c788::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInita8ac692f43bf07ab29c010fd95958205::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInita8ac692f43bf07ab29c010fd95958205::$prefixDirsPsr4;
+            $loader->prefixesPsr0 = ComposerStaticInita8ac692f43bf07ab29c010fd95958205::$prefixesPsr0;
+            $loader->fallbackDirsPsr0 = ComposerStaticInita8ac692f43bf07ab29c010fd95958205::$fallbackDirsPsr0;
+            $loader->classMap = ComposerStaticInita8ac692f43bf07ab29c010fd95958205::$classMap;
 
         }, null, ClassLoader::class);
     }
diff --git a/app/vendor/composer/ca-bundle/composer.json b/app/vendor/composer/ca-bundle/composer.json
deleted file mode 100644
index ca2a0d34d..000000000
--- a/app/vendor/composer/ca-bundle/composer.json
+++ /dev/null
@@ -1,54 +0,0 @@
-{
-    "name": "composer/ca-bundle",
-    "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
-    "type": "library",
-    "license": "MIT",
-    "keywords": [
-        "cabundle",
-        "cacert",
-        "certificate",
-        "ssl",
-        "tls"
-    ],
-    "authors": [
-        {
-            "name": "Jordi Boggiano",
-            "email": "j.boggiano@seld.be",
-            "homepage": "http://seld.be"
-        }
-    ],
-    "support": {
-        "irc": "irc://irc.freenode.org/composer",
-        "issues": "https://github.com/composer/ca-bundle/issues"
-    },
-    "require": {
-        "ext-openssl": "*",
-        "ext-pcre": "*",
-        "php": "^5.3.2 || ^7.0"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
-        "psr/log": "^1.0",
-        "symfony/process": "^2.5 || ^3.0 || ^4.0"
-    },
-    "autoload": {
-        "psr-4": {
-            "Composer\\CaBundle\\": "src"
-        }
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "Composer\\CaBundle\\": "tests"
-        }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.x-dev"
-        }
-    },
-    "config": {
-        "platform": {
-            "php": "5.3.9"
-        }
-    }
-}
diff --git a/app/vendor/composer/installed.json b/app/vendor/composer/installed.json
index 68e162e59..9d2b6db92 100644
--- a/app/vendor/composer/installed.json
+++ b/app/vendor/composer/installed.json
@@ -156,18 +156,12 @@
     },
     {
         "name": "davaxi/sparkline",
-        "version": "1.1.2",
-        "version_normalized": "1.1.2.0",
+        "version": "dev-multiple-series",
+        "version_normalized": "dev-multiple-series",
         "source": {
             "type": "git",
-            "url": "https://github.com/davaxi/Sparkline.git",
-            "reference": "d563481f0960bac1acb4e24743ab884f4ce251e2"
-        },
-        "dist": {
-            "type": "zip",
-            "url": "https://api.github.com/repos/davaxi/Sparkline/zipball/d563481f0960bac1acb4e24743ab884f4ce251e2",
-            "reference": "d563481f0960bac1acb4e24743ab884f4ce251e2",
-            "shasum": ""
+            "url": "https://github.com/matomo-org/Sparkline.git",
+            "reference": "d5840d8bd753eab9cadf7589af76b51dfd92e098"
         },
         "require": {
             "ext-gd": "*",
@@ -186,15 +180,14 @@
             "squizlabs/php_codesniffer": "^3.1",
             "wearejust/grumphp-extra-tasks": "^2.1"
         },
-        "time": "2017-12-15T15:45:18+00:00",
+        "time": "2019-08-26T06:43:23+00:00",
         "type": "library",
-        "installation-source": "dist",
+        "installation-source": "source",
         "autoload": {
             "psr-4": {
                 "Davaxi\\": "src/"
             }
         },
-        "notification-url": "https://packagist.org/downloads/",
         "license": [
             "MIT"
         ],
@@ -211,7 +204,11 @@
             "php",
             "picture",
             "sparkline"
-        ]
+        ],
+        "support": {
+            "issues": "https://github.com/davaxi/Sparkline/issues",
+            "source": "https://github.com/davaxi/Sparkline/releases"
+        }
     },
     {
         "name": "doctrine/cache",
@@ -807,18 +804,18 @@
         "authors": [
             {
                 "name": "Greg Beaver",
-                "email": "cellog@php.net",
-                "role": "Helper"
+                "role": "Helper",
+                "email": "cellog@php.net"
             },
             {
                 "name": "Andrei Zmievski",
-                "email": "andrei@php.net",
-                "role": "Lead"
+                "role": "Lead",
+                "email": "andrei@php.net"
             },
             {
                 "name": "Stig Bakken",
-                "email": "stig@php.net",
-                "role": "Developer"
+                "role": "Developer",
+                "email": "stig@php.net"
             }
         ],
         "description": "More info available on: http://pear.php.net/package/Console_Getopt"
@@ -863,8 +860,8 @@
         "authors": [
             {
                 "name": "Christian Weiske",
-                "email": "cweiske@php.net",
-                "role": "Lead"
+                "role": "Lead",
+                "email": "cweiske@php.net"
             }
         ],
         "description": "Minimal set of PEAR core files to be used as composer dependency"
@@ -1663,7 +1660,7 @@
             },
             {
                 "name": "Gert de Pagter",
-                "email": "backendtea@gmail.com"
+                "email": "BackEndTea@gmail.com"
             }
         ],
         "description": "Symfony polyfill for ctype functions",
@@ -1807,33 +1804,33 @@
     },
     {
         "name": "twig/twig",
-        "version": "v1.38.4",
-        "version_normalized": "1.38.4.0",
+        "version": "v1.42.3",
+        "version_normalized": "1.42.3.0",
         "source": {
             "type": "git",
             "url": "https://github.com/twigphp/Twig.git",
-            "reference": "7732e9e7017d751313811bd118de61302e9c8b35"
+            "reference": "201baee843e0ffe8b0b956f336dd42b2a92fae4e"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/twigphp/Twig/zipball/7732e9e7017d751313811bd118de61302e9c8b35",
-            "reference": "7732e9e7017d751313811bd118de61302e9c8b35",
+            "url": "https://api.github.com/repos/twigphp/Twig/zipball/201baee843e0ffe8b0b956f336dd42b2a92fae4e",
+            "reference": "201baee843e0ffe8b0b956f336dd42b2a92fae4e",
             "shasum": ""
         },
         "require": {
-            "php": ">=5.4.0",
+            "php": ">=5.5.0",
             "symfony/polyfill-ctype": "^1.8"
         },
         "require-dev": {
             "psr/container": "^1.0",
-            "symfony/debug": "^2.7",
-            "symfony/phpunit-bridge": "^3.4.19|^4.1.8"
+            "symfony/debug": "^3.4|^4.2",
+            "symfony/phpunit-bridge": "^4.4@dev|^5.0"
         },
-        "time": "2019-03-23T14:27:19+00:00",
+        "time": "2019-08-24T12:51:03+00:00",
         "type": "library",
         "extra": {
             "branch-alias": {
-                "dev-master": "1.38-dev"
+                "dev-master": "1.42-dev"
             }
         },
         "installation-source": "dist",
@@ -1856,15 +1853,15 @@
                 "homepage": "http://fabien.potencier.org",
                 "role": "Lead Developer"
             },
-            {
-                "name": "Armin Ronacher",
-                "email": "armin.ronacher@active-4.com",
-                "role": "Project Founder"
-            },
             {
                 "name": "Twig Team",
                 "homepage": "https://twig.symfony.com/contributors",
                 "role": "Contributors"
+            },
+            {
+                "name": "Armin Ronacher",
+                "email": "armin.ronacher@active-4.com",
+                "role": "Project Founder"
             }
         ],
         "description": "Twig, the flexible, fast, and secure template language for PHP",
diff --git a/app/vendor/composer/semver/composer.json b/app/vendor/composer/semver/composer.json
deleted file mode 100644
index b0400cd7b..000000000
--- a/app/vendor/composer/semver/composer.json
+++ /dev/null
@@ -1,58 +0,0 @@
-{
-    "name": "composer/semver",
-    "description": "Semver library that offers utilities, version constraint parsing and validation.",
-    "type": "library",
-    "license": "MIT",
-    "keywords": [
-        "semver",
-        "semantic",
-        "versioning",
-        "validation"
-    ],
-    "authors": [
-        {
-            "name": "Nils Adermann",
-            "email": "naderman@naderman.de",
-            "homepage": "http://www.naderman.de"
-        },
-        {
-            "name": "Jordi Boggiano",
-            "email": "j.boggiano@seld.be",
-            "homepage": "http://seld.be"
-        },
-        {
-            "name": "Rob Bast",
-            "email": "rob.bast@gmail.com",
-            "homepage": "http://robbast.nl"
-        }
-    ],
-    "support": {
-        "irc": "irc://irc.freenode.org/composer",
-        "issues": "https://github.com/composer/semver/issues"
-    },
-    "require": {
-        "php": "^5.3.2 || ^7.0"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "^4.5 || ^5.0.5",
-        "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
-    },
-    "autoload": {
-        "psr-4": {
-            "Composer\\Semver\\": "src"
-        }
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "Composer\\Semver\\": "tests"
-        }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.x-dev"
-        }
-    },
-    "scripts": {
-        "test": "phpunit"
-    }
-}
diff --git a/app/vendor/container-interop/container-interop/composer.json b/app/vendor/container-interop/container-interop/composer.json
deleted file mode 100644
index 855f76672..000000000
--- a/app/vendor/container-interop/container-interop/composer.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-    "name": "container-interop/container-interop",
-    "type": "library",
-    "description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
-    "homepage": "https://github.com/container-interop/container-interop",
-    "license": "MIT",
-    "autoload": {
-        "psr-4": {
-            "Interop\\Container\\": "src/Interop/Container/"
-        }
-    },
-    "require": {
-        "psr/container": "^1.0"
-    }
-}
diff --git a/app/vendor/davaxi/sparkline/README.md b/app/vendor/davaxi/sparkline/README.md
index 57ce74fc5..ced173a50 100644
--- a/app/vendor/davaxi/sparkline/README.md
+++ b/app/vendor/davaxi/sparkline/README.md
@@ -27,7 +27,7 @@ There are two options for obtaining the files for the client library.
 
 You can install the library by adding it as a dependency to your composer.json.
 
-```
+```json
   "require": {
     "davaxi/sparkline": "^1.1"
   }
@@ -37,7 +37,7 @@ You can install the library by adding it as a dependency to your composer.json.
 
 The library is available on [GitHub](https://github.com/davaxi/Sparkline). You can clone it into a local repository with the git clone command.
 
-```
+```sh
 git clone https://github.com/davaxi/Sparkline.git
 ```
 
@@ -45,17 +45,17 @@ git clone https://github.com/davaxi/Sparkline.git
 
 After obtaining the files, ensure they are available to your code. If you're using Composer, this is handled for you automatically. If not, you will need to add the `autoload.php` file inside the client library.
 
-```
+```php
 require '/path/to/sparkline/folder/autoload.php';
 ```
 
 ## Usage
 
-Exemple: 
+Example: 
 
 ![Sparkline](https://raw.githubusercontent.com/davaxi/Sparkline/master/tests/data/testGenerate2-mockup.png)
 
-```
+```php
 display();
 
 ## Documentation
 
-```
+```php
 $sparkline = new Davaxi\Sparkline();
 
 // Change format (Default value 80x20)
diff --git a/app/vendor/davaxi/sparkline/composer.json b/app/vendor/davaxi/sparkline/composer.json
deleted file mode 100644
index dbca75168..000000000
--- a/app/vendor/davaxi/sparkline/composer.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
-    "name": "davaxi/sparkline",
-    "description": "PHP Class (using GD) to generate sparklines",
-    "version": "1.1.2",
-    "type": "library",
-    "keywords": [
-        "sparkline",
-        "php",
-        "picture"
-    ],
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "David Patiashvili",
-            "email": "stratagem.david@gmail.com",
-            "homepage": "https://www.patiashvili.fr/",
-            "role": "Developer"
-        }
-    ],
-    "support": {
-        "issues": "https://github.com/davaxi/Sparkline/issues",
-        "source": "https://github.com/davaxi/Sparkline/releases"
-    },
-    "minimum-stability": "stable",
-    "require": {
-        "php": ">=5.3.0",
-        "ext-gd": "*"
-    },
-    "require-dev": {
-        "php": ">=5.3.0",
-        "codeclimate/php-test-reporter": "dev-master",
-        "ext-gd": "*",
-        "phpro/grumphp": "^0.12.0",
-        "jakub-onderka/php-parallel-lint": "^0.9.2",
-        "sensiolabs/security-checker": "^4.1",
-        "povils/phpmnd": "^1.1",
-        "sebastian/phpcpd": "^3.0",
-        "squizlabs/php_codesniffer": "^3.1",
-        "friendsofphp/php-cs-fixer": "^2.8",
-        "wearejust/grumphp-extra-tasks": "^2.1"
-    },
-    "autoload": {
-        "psr-4": {
-            "Davaxi\\": "src/"
-        }
-    }
-}
diff --git a/app/vendor/davaxi/sparkline/phpunit.xml b/app/vendor/davaxi/sparkline/phpunit.xml
deleted file mode 100644
index 4920a3579..000000000
--- a/app/vendor/davaxi/sparkline/phpunit.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-    
-        
-            ./tests/
-        
-    
-
\ No newline at end of file
diff --git a/app/vendor/davaxi/sparkline/src/Sparkline.php b/app/vendor/davaxi/sparkline/src/Sparkline.php
index 90fc2bf3f..485fe46f3 100644
--- a/app/vendor/davaxi/sparkline/src/Sparkline.php
+++ b/app/vendor/davaxi/sparkline/src/Sparkline.php
@@ -110,23 +110,34 @@ public function generate()
         list($width, $height) = $this->getNormalizedSize();
 
         $count = $this->getCount();
-        list($polygon, $line) = $this->getChartElements($this->data);
 
         $picture = new Picture($width, $height);
         $picture->applyBackground($this->backgroundColor);
         $picture->applyThickness($this->lineThickness * $this->ratioComputing);
-        $picture->applyPolygon($polygon, $this->fillColor, $count);
-        $picture->applyLine($line, $this->lineColor);
-
-        foreach ($this->points as $point) {
-            $isFirst = $point['index'] === 0;
-            $lineIndex = $isFirst ? 0 : $point['index'] - 1;
-            $picture->applyDot(
-                $line[$lineIndex][$isFirst ? 0 : 2],
-                $line[$lineIndex][$isFirst ? 1 : 3],
-                $point['radius'] * $this->ratioComputing,
-                $point['color']
-            );
+
+        $stepCount = $this->getMaxNumberOfDataPointsAcrossSerieses();
+
+        foreach ($this->data as $seriesIndex => $series) {
+            list($polygon, $line) = $this->getChartElements($series, $stepCount);
+            $picture->applyPolygon($polygon, $this->fillColor, $count);
+
+            $lineColor = isset($this->lineColor[$seriesIndex]) ? $this->lineColor[$seriesIndex] : $this->lineColor[0];
+            $picture->applyLine($line, $lineColor);
+
+            foreach ($this->points as $point) {
+                if ($point['series'] != $seriesIndex) {
+                    continue;
+                }
+
+                $isFirst = $point['index'] === 0;
+                $lineIndex = $isFirst ? 0 : $point['index'] - 1;
+                $picture->applyDot(
+                    $line[$lineIndex][$isFirst ? 0 : 2],
+                    $line[$lineIndex][$isFirst ? 1 : 3],
+                    $point['radius'] * $this->ratioComputing,
+                    $point['color']
+                );
+            }
         }
 
         $this->file = $picture->generate($this->width, $this->height);
diff --git a/app/vendor/davaxi/sparkline/src/Sparkline/DataTrait.php b/app/vendor/davaxi/sparkline/src/Sparkline/DataTrait.php
index e944c3644..ecafdf6c4 100644
--- a/app/vendor/davaxi/sparkline/src/Sparkline/DataTrait.php
+++ b/app/vendor/davaxi/sparkline/src/Sparkline/DataTrait.php
@@ -22,7 +22,9 @@ trait DataTrait
     /**
      * @var array
      */
-    protected $data = [0, 0];
+    protected $data = [
+        [0, 0],
+    ];
 
     /**
      * @param $base
@@ -42,32 +44,53 @@ public function setOriginValue($originValue)
         $this->originValue = $originValue;
     }
 
+    /**
+     * @param array $data,...
+     */
+    public function setData()
+    {
+        $allSeries = func_get_args();
+
+        $this->data = [];
+        foreach ($allSeries as $data) {
+            $this->addSeries($data);
+        }
+    }
+
     /**
      * @param array $data
      */
-    public function setData(array $data)
+    public function addSeries($data)
     {
         $data = array_values($data);
         $count = count($data);
         if (!$count) {
-            $this->data = [0, 0];
+            $this->data[] = [0, 0];
 
             return;
         }
         if ($count < static::MIN_DATA_LENGTH) {
-            $this->data = array_fill(0, 2, $data[0]);
+            $this->data[] = array_fill(0, 2, $data[0]);
 
             return;
         }
-        $this->data = $data;
+        $this->data[] = $data;
+    }
+
+    /**
+     * @return int
+     */
+    public function getSeriesCount()
+    {
+        return count($this->data);
     }
 
     /**
      * @return array
      */
-    public function getNormalizedData()
+    public function getNormalizedData($seriesIndex = 0)
     {
-        $data = $this->data;
+        $data = $this->data[$seriesIndex];
         foreach ($data as $i => $value) {
             $data[$i] = max(0, $value - $this->originValue);
         }
@@ -78,26 +101,26 @@ public function getNormalizedData()
     /**
      * @return array
      */
-    public function getData()
+    public function getData($seriesIndex = 0)
     {
-        return $this->data;
+        return $this->data[$seriesIndex];
     }
 
     /**
      * @return int
      */
-    public function getCount()
+    public function getCount($seriesIndex = 0)
     {
-        return count($this->data);
+        return count($this->data[$seriesIndex]);
     }
 
     /**
      * @return array
      */
-    protected function getMaxValueWithIndex()
+    protected function getMaxValueWithIndex($seriesIndex = 0)
     {
-        $max = max($this->data);
-        $maxKeys = array_keys($this->data, $max);
+        $max = max($this->data[$seriesIndex]);
+        $maxKeys = array_keys($this->data[$seriesIndex], $max);
         $maxIndex = end($maxKeys);
         if ($this->base) {
             $max = $this->base;
@@ -109,22 +132,42 @@ protected function getMaxValueWithIndex()
     /**
      * @return float
      */
-    protected function getMaxValue()
+    protected function getMaxValue($seriesIndex = 0)
+    {
+        if ($this->base) {
+            return $this->base;
+        }
+
+        return max($this->data[$seriesIndex]);
+    }
+
+    /**
+     * TODO: this could be cached somehow
+     * @return float
+     */
+    protected function getMaxValueAcrossSeries()
     {
         if ($this->base) {
             return $this->base;
         }
 
-        return max($this->data);
+        $maxes = array_map('max', $this->data);
+        return max($maxes);
+    }
+
+    protected function getMaxNumberOfDataPointsAcrossSerieses()
+    {
+        $counts = array_map('count', $this->data);
+        return max($counts);
     }
 
     /**
      * @return array
      */
-    protected function getMinValueWithIndex()
+    protected function getMinValueWithIndex($seriesIndex = 0)
     {
-        $min = min($this->data);
-        $minKey = array_keys($this->data, $min);
+        $min = min($this->data[$seriesIndex]);
+        $minKey = array_keys($this->data[$seriesIndex], $min);
         $minIndex = end($minKey);
 
         return [$minIndex, $min];
@@ -133,10 +176,10 @@ protected function getMinValueWithIndex()
     /**
      * @return array
      */
-    protected function getExtremeValues()
+    protected function getExtremeValues($seriesIndex = 0)
     {
-        list($minIndex, $min) = $this->getMinValueWithIndex();
-        list($maxIndex, $max) = $this->getMaxValueWithIndex();
+        list($minIndex, $min) = $this->getMinValueWithIndex($seriesIndex);
+        list($maxIndex, $max) = $this->getMaxValueWithIndex($seriesIndex);
 
         return [$minIndex, $min, $maxIndex, $max];
     }
diff --git a/app/vendor/davaxi/sparkline/src/Sparkline/FormatTrait.php b/app/vendor/davaxi/sparkline/src/Sparkline/FormatTrait.php
index 72b6c4b83..997318799 100644
--- a/app/vendor/davaxi/sparkline/src/Sparkline/FormatTrait.php
+++ b/app/vendor/davaxi/sparkline/src/Sparkline/FormatTrait.php
@@ -181,7 +181,7 @@ protected function getStepWidth($count)
      */
     protected function getDataForChartElements(array $data, $height)
     {
-        $max = $this->getMaxValue();
+        $max = $this->getMaxValueAcrossSeries();
         $minHeight = 1 * $this->ratioComputing;
         $maxHeight = $height - $minHeight;
         foreach ($data as $i => $value) {
@@ -200,12 +200,11 @@ protected function getDataForChartElements(array $data, $height)
 
     /**
      * @param array $data
-     *
+     * @param int $count count of steps in sparkline image (does not have to == count($data))
      * @return array
      */
-    protected function getChartElements(array $data)
+    protected function getChartElements(array $data, $count)
     {
-        $count = count($data);
         $step = $this->getStepWidth($count);
         $height = $this->getInnerNormalizedHeight();
         $normalizedPadding = $this->getNormalizedPadding();
@@ -223,7 +222,7 @@ protected function getChartElements(array $data)
         // First element
         $polygon[] = $pictureX1;
         $polygon[] = $pictureY1;
-        for ($i = 1; $i < $count; ++$i) {
+        for ($i = 1; $i < count($data); ++$i) {
             $pictureX2 = $pictureX1 + $step;
             $pictureY2 = $normalizedPadding['top'] + $height - $data[$i];
 
diff --git a/app/vendor/davaxi/sparkline/src/Sparkline/PointTrait.php b/app/vendor/davaxi/sparkline/src/Sparkline/PointTrait.php
index de5077d9b..5fb3d1ade 100644
--- a/app/vendor/davaxi/sparkline/src/Sparkline/PointTrait.php
+++ b/app/vendor/davaxi/sparkline/src/Sparkline/PointTrait.php
@@ -17,17 +17,18 @@ trait PointTrait
      * @param $dotRadius
      * @param $colorHex
      */
-    public function addPoint($index, $dotRadius, $colorHex)
+    public function addPoint($index, $dotRadius, $colorHex, $seriesIndex = 0)
     {
-        $mapping = $this->getPointIndexMapping();
+        $mapping = $this->getPointIndexMapping($seriesIndex);
         if (array_key_exists($index, $mapping)) {
             $index = $mapping[$index];
             if ($index < 0) {
                 return;
             }
         }
-        $this->checkPointIndex($index);
+        $this->checkPointIndex($index, $seriesIndex);
         $this->points[] = [
+            'series' => $seriesIndex,
             'index' => $index,
             'radius' => $dotRadius,
             'color' => $this->colorHexToRGB($colorHex),
@@ -37,10 +38,10 @@ public function addPoint($index, $dotRadius, $colorHex)
     /**
      * @return array
      */
-    protected function getPointIndexMapping()
+    protected function getPointIndexMapping($seriesIndex = 0)
     {
-        $count = $this->getCount();
-        list($minIndex, $min, $maxIndex, $max) = $this->getExtremeValues();
+        $count = $this->getCount($seriesIndex);
+        list($minIndex, $min, $maxIndex, $max) = $this->getExtremeValues($seriesIndex);
 
         $mapping = [];
         $mapping['first'] = $count > 1 ? 0 : -1;
@@ -54,9 +55,9 @@ protected function getPointIndexMapping()
     /**
      * @param $index
      */
-    protected function checkPointIndex($index)
+    protected function checkPointIndex($index, $seriesIndex)
     {
-        $count = $this->getCount();
+        $count = $this->getCount($seriesIndex);
         if (!is_numeric($index)) {
             throw new \InvalidArgumentException('Invalid index : ' . $index);
         }
diff --git a/app/vendor/davaxi/sparkline/src/Sparkline/StyleTrait.php b/app/vendor/davaxi/sparkline/src/Sparkline/StyleTrait.php
index 5feccbcb1..f7818a8cc 100644
--- a/app/vendor/davaxi/sparkline/src/Sparkline/StyleTrait.php
+++ b/app/vendor/davaxi/sparkline/src/Sparkline/StyleTrait.php
@@ -17,7 +17,9 @@ trait StyleTrait
      * @var array (rgb)
      *            Default: #1388db
      */
-    protected $lineColor = [19, 136, 219];
+    protected $lineColor = [
+        [19, 136, 219],
+    ];
 
     /**
      * @var array (rgb)
@@ -61,10 +63,10 @@ public function setBackgroundColorRGB($red, $green, $blue)
     /**
      * @param string $color (hexadecimal)
      */
-    public function setLineColorHex($color)
+    public function setLineColorHex($color, $seriesIndex = 0)
     {
         list($red, $green, $blue) = $this->colorHexToRGB($color);
-        $this->setLineColorRGB($red, $green, $blue);
+        $this->setLineColorRGB($red, $green, $blue, $seriesIndex);
     }
 
     /**
@@ -72,9 +74,9 @@ public function setLineColorHex($color)
      * @param int $green
      * @param int $blue
      */
-    public function setLineColorRGB($red, $green, $blue)
+    public function setLineColorRGB($red, $green, $blue, $seriesIndex = 0)
     {
-        $this->lineColor = [$red, $green, $blue];
+        $this->lineColor[$seriesIndex] = [$red, $green, $blue];
     }
 
     /**
diff --git a/app/vendor/doctrine/cache/composer.json b/app/vendor/doctrine/cache/composer.json
deleted file mode 100644
index 7ef1727a1..000000000
--- a/app/vendor/doctrine/cache/composer.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
-    "name": "doctrine/cache",
-    "type": "library",
-    "description": "Caching library offering an object-oriented API for many cache backends",
-    "keywords": ["cache", "caching"],
-    "homepage": "http://www.doctrine-project.org",
-    "license": "MIT",
-    "authors": [
-        {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
-        {"name": "Roman Borschel", "email": "roman@code-factory.org"},
-        {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
-        {"name": "Jonathan Wage", "email": "jonwage@gmail.com"},
-        {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"}
-    ],
-    "require": {
-        "php": "~5.5|~7.0"
-    },
-    "require-dev": {
-        "phpunit/phpunit":         "~4.8|~5.0",
-        "satooshi/php-coveralls":  "~0.6",
-        "predis/predis":           "~1.0"
-    },
-    "conflict": {
-        "doctrine/common": ">2.2,<2.4"
-    },
-    "autoload": {
-        "psr-4": { "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" }
-    },
-    "autoload-dev": {
-        "psr-4": { "Doctrine\\Tests\\": "tests/Doctrine/Tests" }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.6.x-dev"
-        }
-    }
-}
diff --git a/app/vendor/doctrine/cache/phpunit.xml.dist b/app/vendor/doctrine/cache/phpunit.xml.dist
deleted file mode 100644
index 40cc24dee..000000000
--- a/app/vendor/doctrine/cache/phpunit.xml.dist
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-    
-        
-    
-
-    
-        
-            ./tests/Doctrine/
-        
-    
-
-    
-        
-            ./lib/Doctrine/
-        
-    
-
diff --git a/app/vendor/geoip2/geoip2/composer.json b/app/vendor/geoip2/geoip2/composer.json
deleted file mode 100644
index 53ae0b8fc..000000000
--- a/app/vendor/geoip2/geoip2/composer.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
-    "name": "geoip2/geoip2",
-    "description": "MaxMind GeoIP2 PHP API",
-    "keywords": ["geoip", "geoip2", "geolocation", "ip", "maxmind"],
-    "homepage": "https://github.com/maxmind/GeoIP2-php",
-    "type": "library",
-    "license": "Apache-2.0",
-    "authors": [
-        {
-            "name": "Gregory J. Oschwald",
-            "email": "goschwald@maxmind.com",
-            "homepage": "http://www.maxmind.com/"
-        }
-    ],
-    "require": {
-        "maxmind-db/reader": "~1.0",
-        "maxmind/web-service-common": "~0.5",
-        "php": ">=5.4"
-    },
-    "require-dev": {
-        "friendsofphp/php-cs-fixer": "2.*",
-        "phpunit/phpunit": "4.*",
-        "squizlabs/php_codesniffer": "3.*"
-    },
-    "autoload": {
-        "psr-4": {
-            "GeoIp2\\": "src"
-        }
-    }
-}
diff --git a/app/vendor/leafo/lessphp/composer.json b/app/vendor/leafo/lessphp/composer.json
deleted file mode 100644
index 0f06ba090..000000000
--- a/app/vendor/leafo/lessphp/composer.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-    "name": "leafo/lessphp",
-    "type": "library",
-    "description": "lessphp is a compiler for LESS written in PHP.",
-    "homepage": "http://leafo.net/lessphp/",
-    "license": [
-      "MIT",
-      "GPL-3.0"
-    ],
-    "authors": [
-        {
-            "name": "Leaf Corcoran",
-            "email": "leafot@gmail.com",
-            "homepage": "http://leafo.net"
-        }
-    ],
-    "autoload": {
-        "classmap": ["lessc.inc.php"]
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "0.4.x-dev"
-        }
-    }
-}
diff --git a/app/vendor/leafo/lessphp/lessify b/app/vendor/leafo/lessphp/lessify
old mode 100644
new mode 100755
diff --git a/app/vendor/leafo/lessphp/package.sh b/app/vendor/leafo/lessphp/package.sh
old mode 100644
new mode 100755
diff --git a/app/vendor/leafo/lessphp/plessc b/app/vendor/leafo/lessphp/plessc
old mode 100644
new mode 100755
diff --git a/app/vendor/matomo-org/jshrink/composer.json b/app/vendor/matomo-org/jshrink/composer.json
deleted file mode 100644
index 9483d5e0e..000000000
--- a/app/vendor/matomo-org/jshrink/composer.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-  "name": "tedivm/jshrink",
-  "description": "Javascript Minifier built in PHP",
-  "keywords": ["minifier","javascript"],
-  "homepage": "http://github.com/tedious/JShrink",
-  "type": "library",
-  "license": "BSD-3-Clause",
-  "authors": [
-    {
-      "name": "Robert Hafner",
-      "email": "tedivm@tedivm.com"
-    }
-  ],
-  "require": {
-    "php": "^5.6|^7.0"
-  },
-  "require-dev": {
-      "phpunit/phpunit": "^6",
-      "friendsofphp/php-cs-fixer": "^2.8",
-      "php-coveralls/php-coveralls": "^1.1.0"
-  },
-  "autoload": {
-    "psr-0": {"JShrink": "src/"}
-  }
-}
diff --git a/app/vendor/matomo-org/jshrink/phpunit.xml.dist b/app/vendor/matomo-org/jshrink/phpunit.xml.dist
deleted file mode 100644
index 72c6470ed..000000000
--- a/app/vendor/matomo-org/jshrink/phpunit.xml.dist
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-    
-        
-            ./tests/JShrink/
-        
-    
-    
-        
-            requests
-            development
-        
-    
-    
-        
-            ./src/JShrink/
-        
-    
-    
-        
-    
-
diff --git a/app/vendor/matomo/referrer-spam-blacklist/composer.json b/app/vendor/matomo/referrer-spam-blacklist/composer.json
deleted file mode 100644
index 51a5bf932..000000000
--- a/app/vendor/matomo/referrer-spam-blacklist/composer.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-    "name": "matomo/referrer-spam-blacklist",
-    "description": "Community-contributed list of referrer spammers",
-    "license": "CC0-1.0",
-    "replace": {
-        "piwik/referrer-spam-blacklist":"*"
-    }
-}
diff --git a/app/vendor/matomo/searchengine-and-social-list/composer.json b/app/vendor/matomo/searchengine-and-social-list/composer.json
deleted file mode 100644
index b81ecc362..000000000
--- a/app/vendor/matomo/searchengine-and-social-list/composer.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "name": "matomo/searchengine-and-social-list",
-  "description": "Search engine and social network definitions used by Matomo (formerly Piwik)",
-  "license": "CC0-1.0",
-  "replace": {
-    "piwik/searchengine-and-social-list":"*"
-  }
-}
diff --git a/app/vendor/matomo/searchengine-and-social-list/package.json b/app/vendor/matomo/searchengine-and-social-list/package.json
deleted file mode 100644
index 1bab14aab..000000000
--- a/app/vendor/matomo/searchengine-and-social-list/package.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-  "name": "searchengine-and-social-list",
-  "version": "1.4.1",
-  "description": "Search engine and social network definitions used by Matomo (formerly Piwik)",
-  "keywords": [
-    "searchengine",
-    "social network"
-  ],
-  "main": "SearchEngines.yml",
-  "repository": {
-    "type": "git",
-    "url": "git://github.com/matomo-org/searchengine-and-social-list.git"
-  },
-  "homepage": "https://github.com/matomo-org/searchengine-and-social-list#readme",
-  "bugs": {
-    "url": "https://github.com/matomo-org/searchengine-and-social-list/issues"
-  },
-  "author": "Matomo Team",
-  "license": "CC0-1.0"
-}
diff --git a/app/vendor/maxmind-db/reader/composer.json b/app/vendor/maxmind-db/reader/composer.json
deleted file mode 100644
index dce4428e7..000000000
--- a/app/vendor/maxmind-db/reader/composer.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-    "name": "maxmind-db/reader",
-    "description": "MaxMind DB Reader API",
-    "keywords": ["database", "geoip", "geoip2", "geolocation", "maxmind"],
-    "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php",
-    "type": "library",
-    "license": "Apache-2.0",
-    "authors": [
-        {
-            "name": "Gregory J. Oschwald",
-            "email": "goschwald@maxmind.com",
-            "homepage": "http://www.maxmind.com/"
-        }
-    ],
-    "require": {
-        "php": ">=5.4"
-    },
-    "suggest": {
-        "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
-        "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
-        "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups"
-    },
-    "require-dev": {
-        "friendsofphp/php-cs-fixer": "2.*",
-        "phpunit/phpunit": "4.* || 5.*",
-        "satooshi/php-coveralls": "1.0.*",
-        "squizlabs/php_codesniffer": "3.*"
-    },
-    "autoload": {
-        "psr-4": {
-            "MaxMind\\Db\\": "src/MaxMind/Db"
-        }
-    }
-}
diff --git a/app/vendor/maxmind/web-service-common/composer.json b/app/vendor/maxmind/web-service-common/composer.json
deleted file mode 100644
index 1d224ae82..000000000
--- a/app/vendor/maxmind/web-service-common/composer.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
-  "name": "maxmind/web-service-common",
-  "description": "Internal MaxMind Web Service API",
-  "minimum-stability": "stable",
-  "homepage": "https://github.com/maxmind/web-service-common-php",
-  "type": "library",
-  "license": "Apache-2.0",
-  "authors": [
-    {
-      "name": "Gregory Oschwald",
-      "email": "goschwald@maxmind.com"
-    }
-  ],
-  "require": {
-    "php": ">=5.4",
-    "composer/ca-bundle": "^1.0.3",
-    "ext-curl": "*",
-    "ext-json": "*"
-  },
-  "require-dev": {
-    "friendsofphp/php-cs-fixer": "2.*",
-    "phpunit/phpunit": "4.*",
-    "squizlabs/php_codesniffer": "3.*"
-  },
-  "autoload": {
-    "psr-4": {
-      "MaxMind\\Exception\\": "src/Exception",
-      "MaxMind\\WebService\\": "src/WebService"
-    }
-  }
-}
diff --git a/app/vendor/monolog/monolog/composer.json b/app/vendor/monolog/monolog/composer.json
deleted file mode 100644
index 3b0c88055..000000000
--- a/app/vendor/monolog/monolog/composer.json
+++ /dev/null
@@ -1,66 +0,0 @@
-{
-    "name": "monolog/monolog",
-    "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
-    "keywords": ["log", "logging", "psr-3"],
-    "homepage": "http://github.com/Seldaek/monolog",
-    "type": "library",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "Jordi Boggiano",
-            "email": "j.boggiano@seld.be",
-            "homepage": "http://seld.be"
-        }
-    ],
-    "require": {
-        "php": ">=5.3.0",
-        "psr/log": "~1.0"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "~4.5",
-        "graylog2/gelf-php": "~1.0",
-        "sentry/sentry": "^0.13",
-        "ruflin/elastica": ">=0.90 <3.0",
-        "doctrine/couchdb": "~1.0@dev",
-        "aws/aws-sdk-php": "^2.4.9 || ^3.0",
-        "php-amqplib/php-amqplib": "~2.4",
-        "swiftmailer/swiftmailer": "^5.3|^6.0",
-        "php-console/php-console": "^3.1.3",
-        "phpunit/phpunit-mock-objects": "2.3.0",
-        "jakub-onderka/php-parallel-lint": "0.9"
-    },
-    "_": "phpunit/phpunit-mock-objects required in 2.3.0 due to https://github.com/sebastianbergmann/phpunit-mock-objects/issues/223 - needs hhvm 3.8+ on travis",
-    "suggest": {
-        "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
-        "sentry/sentry": "Allow sending log messages to a Sentry server",
-        "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
-        "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
-        "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
-        "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
-        "ext-mongo": "Allow sending log messages to a MongoDB server",
-        "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
-        "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
-        "rollbar/rollbar": "Allow sending log messages to Rollbar",
-        "php-console/php-console": "Allow sending log messages to Google Chrome"
-    },
-    "autoload": {
-        "psr-4": {"Monolog\\": "src/Monolog"}
-    },
-    "autoload-dev": {
-        "psr-4": {"Monolog\\": "tests/Monolog"}
-    },
-    "provide": {
-        "psr/log-implementation": "1.0.0"
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "2.0.x-dev"
-        }
-    },
-    "scripts": {
-        "test": [
-            "parallel-lint . --exclude vendor",
-            "phpunit"
-        ]
-    }
-}
diff --git a/app/vendor/monolog/monolog/phpunit.xml.dist b/app/vendor/monolog/monolog/phpunit.xml.dist
deleted file mode 100644
index 20d82b631..000000000
--- a/app/vendor/monolog/monolog/phpunit.xml.dist
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-    
-        
-            tests/Monolog/
-        
-    
-
-    
-        
-            src/Monolog/
-        
-    
-
-    
-        
-    
-
diff --git a/app/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php b/app/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php
old mode 100644
new mode 100755
diff --git a/app/vendor/mustangostang/spyc/composer.json b/app/vendor/mustangostang/spyc/composer.json
deleted file mode 100644
index 5d2a50716..000000000
--- a/app/vendor/mustangostang/spyc/composer.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-    "name": "mustangostang/spyc",
-    "description": "A simple YAML loader/dumper class for PHP",
-    "type": "library",
-    "keywords": [
-        "spyc",
-        "yaml",
-        "yml"
-    ],
-    "homepage": "https://github.com/mustangostang/spyc/",
-    "authors" : [{
-        "name": "mustangostang",
-        "email": "vlad.andersen@gmail.com"
-    }],
-    "license": "MIT License",
-    "require": {
-        "php": ">=5.3.1"
-    },
-    "autoload": {
-        "files": [ "Spyc.php" ]
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "0.5.x-dev"
-        }
-    }
-}
diff --git a/app/vendor/pear/archive_tar/composer.json b/app/vendor/pear/archive_tar/composer.json
deleted file mode 100644
index e464d9d7b..000000000
--- a/app/vendor/pear/archive_tar/composer.json
+++ /dev/null
@@ -1,54 +0,0 @@
-{
-    "name": "pear/archive_tar",
-    "description": "Tar file management class with compression support (gzip, bzip2, lzma2)",
-    "type": "library",
-    "keywords": [
-        "archive",
-        "tar"
-    ],
-    "homepage": "https://github.com/pear/Archive_Tar",
-    "license": "BSD-3-Clause",
-    "authors": [
-        {
-            "name": "Vincent Blavet",
-            "email": "vincent@phpconcept.net"
-        },
-        {
-            "name": "Greg Beaver",
-            "email": "greg@chiaraquartet.net"
-        },
-        {
-            "name": "Michiel Rook",
-            "email": "mrook@php.net"
-        }
-    ],
-    "require": {
-        "php": ">=5.2.0",
-        "pear/pear-core-minimal": "^1.10.0alpha2"
-    },
-    "suggest": {
-        "ext-zlib": "Gzip compression support.",
-        "ext-bz2": "Bz2 compression support.",
-        "ext-xz": "Lzma2 compression support."
-    },
-    "autoload": {
-        "psr-0": {
-            "Archive_Tar": ""
-        }
-    },
-    "include-path": [
-        "./"
-    ],
-    "support": {
-        "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Archive_Tar",
-        "source": "https://github.com/pear/Archive_Tar"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "*"
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.4.x-dev"
-        }
-    }
-}
diff --git a/app/vendor/pear/archive_tar/scripts/phptar.in b/app/vendor/pear/archive_tar/scripts/phptar.in
old mode 100644
new mode 100755
diff --git a/app/vendor/pear/archive_tar/sync-php4 b/app/vendor/pear/archive_tar/sync-php4
old mode 100644
new mode 100755
diff --git a/app/vendor/pear/console_getopt/composer.json b/app/vendor/pear/console_getopt/composer.json
deleted file mode 100644
index 4dc7e7cca..000000000
--- a/app/vendor/pear/console_getopt/composer.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-    "authors": [
-        {
-            "email": "andrei@php.net",
-            "name": "Andrei Zmievski",
-            "role": "Lead"
-        },
-        {
-            "email": "stig@php.net",
-            "name": "Stig Bakken",
-            "role": "Developer"
-        },
-        {
-            "email": "cellog@php.net",
-            "name": "Greg Beaver",
-            "role": "Helper"
-        }
-    ],
-    "autoload": {
-        "psr-0": {
-            "Console": "./"
-        }
-    },
-    "description": "More info available on: http://pear.php.net/package/Console_Getopt",
-    "include-path": [
-        "./"
-    ],
-    "license": "BSD-2-Clause",
-    "name": "pear/console_getopt",
-    "support": {
-        "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Console_Getopt",
-        "source": "https://github.com/pear/Console_Getopt"
-    },
-    "type": "library"
-}
diff --git a/app/vendor/pear/pear-core-minimal/composer.json b/app/vendor/pear/pear-core-minimal/composer.json
deleted file mode 100644
index d805f56ae..000000000
--- a/app/vendor/pear/pear-core-minimal/composer.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
-    "name": "pear/pear-core-minimal",
-    "description": "Minimal set of PEAR core files to be used as composer dependency",
-    "license": "BSD-3-Clause",
-    "authors": [
-        {
-            "email": "cweiske@php.net",
-            "name": "Christian Weiske",
-            "role": "Lead"
-        }
-    ],
-    "autoload": {
-        "psr-0": {
-            "": "src/"
-        }
-    },
-    "include-path": [
-        "src/"
-    ],
-    "support": {
-        "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR",
-        "source": "https://github.com/pear/pear-core-minimal"
-    },
-    "type": "library",
-    "require": {
-        "pear/console_getopt": "~1.4",
-        "pear/pear_exception": "~1.0"
-    },
-    "replace": {
-        "rsky/pear-core-min": "self.version"
-    }
-}
diff --git a/app/vendor/pear/pear_exception/composer.json b/app/vendor/pear/pear_exception/composer.json
deleted file mode 100644
index ce33ed1c8..000000000
--- a/app/vendor/pear/pear_exception/composer.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-    "name": "pear/pear_exception",
-    "description": "The PEAR Exception base class.",
-    "type": "class",
-    "keywords": [
-        "exception"
-    ],
-    "homepage": "https://github.com/pear/PEAR_Exception",
-    "license": "BSD-2-Clause",
-    "authors": [
-        {
-            "name": "Helgi Thormar",
-            "email": "dufuz@php.net"
-        },
-        {
-            "name": "Greg Beaver",
-            "email": "cellog@php.net"
-        }
-    ],
-    "require": {
-        "php": ">=4.4.0"
-    },
-    "autoload": {
-        "psr-0": {
-            "PEAR": ""
-        }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.0.x-dev"
-        }
-    },
-    "include-path": [
-        "."
-    ],
-    "support": {
-        "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception",
-        "source": "https://github.com/pear/PEAR_Exception"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "*"
-    }
-}
diff --git a/app/vendor/php-di/invoker/composer.json b/app/vendor/php-di/invoker/composer.json
deleted file mode 100644
index 3f0c04169..000000000
--- a/app/vendor/php-di/invoker/composer.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-    "name": "php-di/invoker",
-    "description": "Generic and extensible callable invoker",
-    "keywords": ["invoker", "dependency-injection", "dependency", "injection", "callable", "invoke"],
-    "homepage": "https://github.com/PHP-DI/Invoker",
-    "license": "MIT",
-    "type": "library",
-    "autoload": {
-        "psr-4": {
-            "Invoker\\": "src/"
-        }
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "Invoker\\Test\\": "tests/"
-        }
-    },
-    "require": {
-        "container-interop/container-interop": "~1.1"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "~4.5",
-        "athletic/athletic": "~0.1.8"
-    }
-}
diff --git a/app/vendor/php-di/php-di/composer.json b/app/vendor/php-di/php-di/composer.json
deleted file mode 100644
index 0f22f9c4d..000000000
--- a/app/vendor/php-di/php-di/composer.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
-    "name": "php-di/php-di",
-    "type": "library",
-    "description": "The dependency injection container for humans",
-    "keywords": ["di", "dependency injection", "container"],
-    "homepage": "http://php-di.org/",
-    "license": "MIT",
-    "autoload": {
-        "psr-4": {
-            "DI\\": "src/DI/"
-        },
-        "files": [
-            "src/DI/functions.php"
-        ]
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "DI\\Test\\IntegrationTest\\": "tests/IntegrationTest/",
-            "DI\\Test\\UnitTest\\": "tests/UnitTest/"
-        }
-    },
-    "scripts": {
-        "test": "phpunit"
-    },
-    "require": {
-        "php": ">=5.5.0",
-        "container-interop/container-interop": "~1.2",
-        "psr/container": "~1.0",
-        "php-di/invoker": "^1.3.2",
-        "php-di/phpdoc-reader": "^2.0.1"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "~4.5",
-        "mnapoli/phpunit-easymock": "~0.2.0",
-        "doctrine/cache": "~1.4",
-        "doctrine/annotations": "~1.2",
-        "phpbench/phpbench": "@dev",
-        "ocramius/proxy-manager": "~1.0|~2.0"
-    },
-    "replace": {
-        "mnapoli/php-di": "*"
-    },
-    "provide": {
-        "container-interop/container-interop-implementation": "^1.0",
-        "psr/container-implementation": "^1.0"
-    },
-    "suggest": {
-        "doctrine/cache": "Install it if you want to use the cache (version ~1.4)",
-        "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)",
-        "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~1.0 or ~2.0)"
-    }
-}
diff --git a/app/vendor/php-di/php-di/phpunit.xml.dist b/app/vendor/php-di/php-di/phpunit.xml.dist
deleted file mode 100644
index 81c7f7386..000000000
--- a/app/vendor/php-di/php-di/phpunit.xml.dist
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-    
-        
-            ./tests/UnitTest/
-        
-        
-            ./tests/IntegrationTest/
-        
-    
-
-    
-        
-            src
-        
-    
-
-
diff --git a/app/vendor/php-di/phpdoc-reader/composer.json b/app/vendor/php-di/phpdoc-reader/composer.json
deleted file mode 100644
index 67713071c..000000000
--- a/app/vendor/php-di/phpdoc-reader/composer.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-    "name": "php-di/phpdoc-reader",
-    "type": "library",
-    "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)",
-    "keywords": ["phpdoc", "reflection"],
-    "license": "MIT",
-    "autoload": {
-        "psr-4": {
-            "PhpDocReader\\": "src/PhpDocReader"
-        }
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "UnitTest\\PhpDocReader\\": "tests/"
-        }
-    },
-    "require": {
-        "php": ">=5.4.0"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "~4.6"
-    }
-}
diff --git a/app/vendor/piwik/cache/composer.json b/app/vendor/piwik/cache/composer.json
deleted file mode 100644
index a3b1d6c38..000000000
--- a/app/vendor/piwik/cache/composer.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
-    "name": "piwik/cache",
-    "type": "library",
-    "license": "LGPL-3.0",
-    "description": "PHP caching library based on Doctrine cache",
-    "keywords": ["cache","array","file","redis"],
-    "authors": [
-        {
-            "name": "The Piwik Team",
-            "email": "hello@piwik.org",
-            "homepage": "http://piwik.org/the-piwik-team/"
-        }
-    ],
-    "autoload": {
-        "psr-4": {
-            "Piwik\\Cache\\": "src/"
-        }
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "Tests\\Piwik\\Cache\\": "tests/"
-        }
-    },
-    "require": {
-        "php": ">=5.5.9",
-        "doctrine/cache": "~1.4"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "~4.0"
-    },
-    "config":{
-        "platform": {
-            "php": "5.5.9"
-        }
-    }
-}
diff --git a/app/vendor/piwik/cache/composer.lock b/app/vendor/piwik/cache/composer.lock
deleted file mode 100644
index e558ace69..000000000
--- a/app/vendor/piwik/cache/composer.lock
+++ /dev/null
@@ -1,1199 +0,0 @@
-{
-    "_readme": [
-        "This file locks the dependencies of your project to a known state",
-        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
-        "This file is @generated automatically"
-    ],
-    "hash": "dd87ddbff7af729d560ebcbe752d5c04",
-    "content-hash": "ac8cdd23aeb659cc5679ee3f40dac33d",
-    "packages": [
-        {
-            "name": "doctrine/cache",
-            "version": "v1.6.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/cache.git",
-                "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/cache/zipball/f8af318d14bdb0eff0336795b428b547bd39ccb6",
-                "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6",
-                "shasum": ""
-            },
-            "require": {
-                "php": "~5.5|~7.0"
-            },
-            "conflict": {
-                "doctrine/common": ">2.2,<2.4"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.8|~5.0",
-                "predis/predis": "~1.0",
-                "satooshi/php-coveralls": "~0.6"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.6.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Roman Borschel",
-                    "email": "roman@code-factory.org"
-                },
-                {
-                    "name": "Benjamin Eberlei",
-                    "email": "kontakt@beberlei.de"
-                },
-                {
-                    "name": "Guilherme Blanco",
-                    "email": "guilhermeblanco@gmail.com"
-                },
-                {
-                    "name": "Jonathan Wage",
-                    "email": "jonwage@gmail.com"
-                },
-                {
-                    "name": "Johannes Schmitt",
-                    "email": "schmittjoh@gmail.com"
-                }
-            ],
-            "description": "Caching library offering an object-oriented API for many cache backends",
-            "homepage": "http://www.doctrine-project.org",
-            "keywords": [
-                "cache",
-                "caching"
-            ],
-            "time": "2015-12-31 16:37:02"
-        }
-    ],
-    "packages-dev": [
-        {
-            "name": "doctrine/instantiator",
-            "version": "1.0.5",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3,<8.0-DEV"
-            },
-            "require-dev": {
-                "athletic/athletic": "~0.1.8",
-                "ext-pdo": "*",
-                "ext-phar": "*",
-                "phpunit/phpunit": "~4.0",
-                "squizlabs/php_codesniffer": "~2.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Marco Pivetta",
-                    "email": "ocramius@gmail.com",
-                    "homepage": "http://ocramius.github.com/"
-                }
-            ],
-            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
-            "homepage": "https://github.com/doctrine/instantiator",
-            "keywords": [
-                "constructor",
-                "instantiate"
-            ],
-            "time": "2015-06-14 21:17:01"
-        },
-        {
-            "name": "phpdocumentor/reflection-common",
-            "version": "1.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
-                "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
-                "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.5"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^4.6"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src"
-                    ]
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Jaap van Otterdijk",
-                    "email": "opensource@ijaap.nl"
-                }
-            ],
-            "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
-            "homepage": "http://www.phpdoc.org",
-            "keywords": [
-                "FQSEN",
-                "phpDocumentor",
-                "phpdoc",
-                "reflection",
-                "static analysis"
-            ],
-            "time": "2015-12-27 11:43:31"
-        },
-        {
-            "name": "phpdocumentor/reflection-docblock",
-            "version": "3.1.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "9270140b940ff02e58ec577c237274e92cd40cdd"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9270140b940ff02e58ec577c237274e92cd40cdd",
-                "reference": "9270140b940ff02e58ec577c237274e92cd40cdd",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.5",
-                "phpdocumentor/reflection-common": "^1.0@dev",
-                "phpdocumentor/type-resolver": "^0.2.0",
-                "webmozart/assert": "^1.0"
-            },
-            "require-dev": {
-                "mockery/mockery": "^0.9.4",
-                "phpunit/phpunit": "^4.4"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src/"
-                    ]
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Mike van Riel",
-                    "email": "me@mikevanriel.com"
-                }
-            ],
-            "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2016-06-10 09:48:41"
-        },
-        {
-            "name": "phpdocumentor/type-resolver",
-            "version": "0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443",
-                "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.5",
-                "phpdocumentor/reflection-common": "^1.0"
-            },
-            "require-dev": {
-                "mockery/mockery": "^0.9.4",
-                "phpunit/phpunit": "^5.2||^4.8.24"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src/"
-                    ]
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Mike van Riel",
-                    "email": "me@mikevanriel.com"
-                }
-            ],
-            "time": "2016-06-10 07:14:17"
-        },
-        {
-            "name": "phpspec/prophecy",
-            "version": "v1.6.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "58a8137754bc24b25740d4281399a4a3596058e0"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0",
-                "reference": "58a8137754bc24b25740d4281399a4a3596058e0",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/instantiator": "^1.0.2",
-                "php": "^5.3|^7.0",
-                "phpdocumentor/reflection-docblock": "^2.0|^3.0.2",
-                "sebastian/comparator": "^1.1",
-                "sebastian/recursion-context": "^1.0"
-            },
-            "require-dev": {
-                "phpspec/phpspec": "^2.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.6.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-0": {
-                    "Prophecy\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Konstantin Kudryashov",
-                    "email": "ever.zet@gmail.com",
-                    "homepage": "http://everzet.com"
-                },
-                {
-                    "name": "Marcello Duarte",
-                    "email": "marcello.duarte@gmail.com"
-                }
-            ],
-            "description": "Highly opinionated mocking framework for PHP 5.3+",
-            "homepage": "https://github.com/phpspec/prophecy",
-            "keywords": [
-                "Double",
-                "Dummy",
-                "fake",
-                "mock",
-                "spy",
-                "stub"
-            ],
-            "time": "2016-06-07 08:13:47"
-        },
-        {
-            "name": "phpunit/php-code-coverage",
-            "version": "2.2.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979",
-                "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3",
-                "phpunit/php-file-iterator": "~1.3",
-                "phpunit/php-text-template": "~1.2",
-                "phpunit/php-token-stream": "~1.3",
-                "sebastian/environment": "^1.3.2",
-                "sebastian/version": "~1.0"
-            },
-            "require-dev": {
-                "ext-xdebug": ">=2.1.4",
-                "phpunit/phpunit": "~4"
-            },
-            "suggest": {
-                "ext-dom": "*",
-                "ext-xdebug": ">=2.2.1",
-                "ext-xmlwriter": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.2.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
-            "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
-            "keywords": [
-                "coverage",
-                "testing",
-                "xunit"
-            ],
-            "time": "2015-10-06 15:47:00"
-        },
-        {
-            "name": "phpunit/php-file-iterator",
-            "version": "1.4.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
-                "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "FilterIterator implementation that filters files based on a list of suffixes.",
-            "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
-            "keywords": [
-                "filesystem",
-                "iterator"
-            ],
-            "time": "2015-06-21 13:08:43"
-        },
-        {
-            "name": "phpunit/php-text-template",
-            "version": "1.2.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-text-template.git",
-                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
-                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Simple template engine.",
-            "homepage": "https://github.com/sebastianbergmann/php-text-template/",
-            "keywords": [
-                "template"
-            ],
-            "time": "2015-06-21 13:50:34"
-        },
-        {
-            "name": "phpunit/php-timer",
-            "version": "1.0.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260",
-                "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4|~5"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Utility class for timing",
-            "homepage": "https://github.com/sebastianbergmann/php-timer/",
-            "keywords": [
-                "timer"
-            ],
-            "time": "2016-05-12 18:03:57"
-        },
-        {
-            "name": "phpunit/php-token-stream",
-            "version": "1.4.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
-                "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
-                "shasum": ""
-            },
-            "require": {
-                "ext-tokenizer": "*",
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.2"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Wrapper around PHP's tokenizer extension.",
-            "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
-            "keywords": [
-                "tokenizer"
-            ],
-            "time": "2015-09-15 10:49:45"
-        },
-        {
-            "name": "phpunit/phpunit",
-            "version": "4.8.27",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "c062dddcb68e44b563f66ee319ddae2b5a322a90"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c062dddcb68e44b563f66ee319ddae2b5a322a90",
-                "reference": "c062dddcb68e44b563f66ee319ddae2b5a322a90",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "ext-json": "*",
-                "ext-pcre": "*",
-                "ext-reflection": "*",
-                "ext-spl": "*",
-                "php": ">=5.3.3",
-                "phpspec/prophecy": "^1.3.1",
-                "phpunit/php-code-coverage": "~2.1",
-                "phpunit/php-file-iterator": "~1.4",
-                "phpunit/php-text-template": "~1.2",
-                "phpunit/php-timer": "^1.0.6",
-                "phpunit/phpunit-mock-objects": "~2.3",
-                "sebastian/comparator": "~1.1",
-                "sebastian/diff": "~1.2",
-                "sebastian/environment": "~1.3",
-                "sebastian/exporter": "~1.2",
-                "sebastian/global-state": "~1.0",
-                "sebastian/version": "~1.0",
-                "symfony/yaml": "~2.1|~3.0"
-            },
-            "suggest": {
-                "phpunit/php-invoker": "~1.1"
-            },
-            "bin": [
-                "phpunit"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.8.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "The PHP Unit Testing framework.",
-            "homepage": "https://phpunit.de/",
-            "keywords": [
-                "phpunit",
-                "testing",
-                "xunit"
-            ],
-            "time": "2016-07-21 06:48:14"
-        },
-        {
-            "name": "phpunit/phpunit-mock-objects",
-            "version": "2.3.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983",
-                "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/instantiator": "^1.0.2",
-                "php": ">=5.3.3",
-                "phpunit/php-text-template": "~1.2",
-                "sebastian/exporter": "~1.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.4"
-            },
-            "suggest": {
-                "ext-soap": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.3.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Mock Object library for PHPUnit",
-            "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
-            "keywords": [
-                "mock",
-                "xunit"
-            ],
-            "time": "2015-10-02 06:51:40"
-        },
-        {
-            "name": "sebastian/comparator",
-            "version": "1.2.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "937efb279bd37a375bcadf584dec0726f84dbf22"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22",
-                "reference": "937efb279bd37a375bcadf584dec0726f84dbf22",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3",
-                "sebastian/diff": "~1.2",
-                "sebastian/exporter": "~1.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.2.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Volker Dusch",
-                    "email": "github@wallbash.com"
-                },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@2bepublished.at"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Provides the functionality to compare PHP values for equality",
-            "homepage": "http://www.github.com/sebastianbergmann/comparator",
-            "keywords": [
-                "comparator",
-                "compare",
-                "equality"
-            ],
-            "time": "2015-07-26 15:48:44"
-        },
-        {
-            "name": "sebastian/diff",
-            "version": "1.4.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e",
-                "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.8"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Kore Nordmann",
-                    "email": "mail@kore-nordmann.de"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Diff implementation",
-            "homepage": "https://github.com/sebastianbergmann/diff",
-            "keywords": [
-                "diff"
-            ],
-            "time": "2015-12-08 07:14:41"
-        },
-        {
-            "name": "sebastian/environment",
-            "version": "1.3.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea",
-                "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.3.3 || ^7.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^4.8 || ^5.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.3.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Provides functionality to handle HHVM/PHP environments",
-            "homepage": "http://www.github.com/sebastianbergmann/environment",
-            "keywords": [
-                "Xdebug",
-                "environment",
-                "hhvm"
-            ],
-            "time": "2016-08-18 05:49:44"
-        },
-        {
-            "name": "sebastian/exporter",
-            "version": "1.2.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4",
-                "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3",
-                "sebastian/recursion-context": "~1.0"
-            },
-            "require-dev": {
-                "ext-mbstring": "*",
-                "phpunit/phpunit": "~4.4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.3.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Volker Dusch",
-                    "email": "github@wallbash.com"
-                },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@2bepublished.at"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Adam Harvey",
-                    "email": "aharvey@php.net"
-                }
-            ],
-            "description": "Provides the functionality to export PHP variables for visualization",
-            "homepage": "http://www.github.com/sebastianbergmann/exporter",
-            "keywords": [
-                "export",
-                "exporter"
-            ],
-            "time": "2016-06-17 09:04:28"
-        },
-        {
-            "name": "sebastian/global-state",
-            "version": "1.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
-                "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.2"
-            },
-            "suggest": {
-                "ext-uopz": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Snapshotting of global state",
-            "homepage": "http://www.github.com/sebastianbergmann/global-state",
-            "keywords": [
-                "global state"
-            ],
-            "time": "2015-10-12 03:26:01"
-        },
-        {
-            "name": "sebastian/recursion-context",
-            "version": "1.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "913401df809e99e4f47b27cdd781f4a258d58791"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791",
-                "reference": "913401df809e99e4f47b27cdd781f4a258d58791",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Adam Harvey",
-                    "email": "aharvey@php.net"
-                }
-            ],
-            "description": "Provides functionality to recursively process PHP variables",
-            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
-            "time": "2015-11-11 19:50:13"
-        },
-        {
-            "name": "sebastian/version",
-            "version": "1.0.6",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/version.git",
-                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
-                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
-                "shasum": ""
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Library that helps with managing the version number of Git-hosted PHP projects",
-            "homepage": "https://github.com/sebastianbergmann/version",
-            "time": "2015-06-21 13:59:46"
-        },
-        {
-            "name": "symfony/yaml",
-            "version": "v3.1.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/yaml.git",
-                "reference": "f291ed25eb1435bddbe8a96caaef16469c2a092d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/f291ed25eb1435bddbe8a96caaef16469c2a092d",
-                "reference": "f291ed25eb1435bddbe8a96caaef16469c2a092d",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.5.9"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.1-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Yaml\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony Yaml Component",
-            "homepage": "https://symfony.com",
-            "time": "2016-09-02 02:12:52"
-        },
-        {
-            "name": "webmozart/assert",
-            "version": "1.1.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/webmozart/assert.git",
-                "reference": "bb2d123231c095735130cc8f6d31385a44c7b308"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308",
-                "reference": "bb2d123231c095735130cc8f6d31385a44c7b308",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.3.3|^7.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^4.6",
-                "sebastian/version": "^1.0.1"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.2-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Webmozart\\Assert\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@gmail.com"
-                }
-            ],
-            "description": "Assertions to validate method input/output with nice error messages.",
-            "keywords": [
-                "assert",
-                "check",
-                "validate"
-            ],
-            "time": "2016-08-09 15:02:57"
-        }
-    ],
-    "aliases": [],
-    "minimum-stability": "stable",
-    "stability-flags": [],
-    "prefer-stable": false,
-    "prefer-lowest": false,
-    "platform": {
-        "php": ">=5.5.9"
-    },
-    "platform-dev": [],
-    "platform-overrides": {
-        "php": "5.5.9"
-    }
-}
diff --git a/app/vendor/piwik/cache/phpunit.xml b/app/vendor/piwik/cache/phpunit.xml
deleted file mode 100644
index cda26b984..000000000
--- a/app/vendor/piwik/cache/phpunit.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-    
-        
-            ./tests/
-        
-    
-
-
\ No newline at end of file
diff --git a/app/vendor/piwik/decompress/composer.json b/app/vendor/piwik/decompress/composer.json
deleted file mode 100644
index 10cfa7bb2..000000000
--- a/app/vendor/piwik/decompress/composer.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-    "name": "piwik/decompress",
-    "type": "library",
-    "license": "LGPL-3.0",
-    "autoload": {
-        "psr-4": {
-            "Piwik\\Decompress\\": "src/"
-        },
-        "classmap": ["libs/PclZip"]
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "Tests\\Piwik\\Decompress\\": "tests/"
-        }
-    },
-    "require": {
-        "php": ">=5.3.2",
-        "pear/archive_tar": "~1.3,>=1.3.15"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "4.4"
-    }
-}
diff --git a/app/vendor/piwik/decompress/composer.lock b/app/vendor/piwik/decompress/composer.lock
deleted file mode 100644
index c708a01ff..000000000
--- a/app/vendor/piwik/decompress/composer.lock
+++ /dev/null
@@ -1,1077 +0,0 @@
-{
-    "_readme": [
-        "This file locks the dependencies of your project to a known state",
-        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
-        "This file is @generated automatically"
-    ],
-    "hash": "5b6d0d5a291b49ad770e2c21eec0828b",
-    "content-hash": "794ee0553135983edb75892c591fd8f8",
-    "packages": [
-        {
-            "name": "pear/archive_tar",
-            "version": "1.4.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/pear/Archive_Tar.git",
-                "reference": "bdd47347df76dbaa89227c5e1afd6f6809985b4c"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/bdd47347df76dbaa89227c5e1afd6f6809985b4c",
-                "reference": "bdd47347df76dbaa89227c5e1afd6f6809985b4c",
-                "shasum": ""
-            },
-            "require": {
-                "pear/pear-core-minimal": "^1.10.0alpha2",
-                "php": ">=5.2.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "*"
-            },
-            "suggest": {
-                "ext-bz2": "bz2 compression support.",
-                "ext-xz": "lzma2 compression support.",
-                "ext-zlib": "Gzip compression support."
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-0": {
-                    "Archive_Tar": ""
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                "./"
-            ],
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Vincent Blavet",
-                    "email": "vincent@phpconcept.net"
-                },
-                {
-                    "name": "Greg Beaver",
-                    "email": "greg@chiaraquartet.net"
-                },
-                {
-                    "name": "Michiel Rook",
-                    "email": "mrook@php.net"
-                }
-            ],
-            "description": "Tar file management class",
-            "homepage": "https://github.com/pear/Archive_Tar",
-            "keywords": [
-                "archive",
-                "tar"
-            ],
-            "time": "2016-02-25 10:30:39"
-        },
-        {
-            "name": "pear/console_getopt",
-            "version": "v1.4.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/pear/Console_Getopt.git",
-                "reference": "82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/pear/Console_Getopt/zipball/82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f",
-                "reference": "82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f",
-                "shasum": ""
-            },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "Console": "./"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                "./"
-            ],
-            "license": [
-                "BSD-2-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Greg Beaver",
-                    "email": "cellog@php.net",
-                    "role": "Helper"
-                },
-                {
-                    "name": "Andrei Zmievski",
-                    "email": "andrei@php.net",
-                    "role": "Lead"
-                },
-                {
-                    "name": "Stig Bakken",
-                    "email": "stig@php.net",
-                    "role": "Developer"
-                }
-            ],
-            "description": "More info available on: http://pear.php.net/package/Console_Getopt",
-            "time": "2015-07-20 20:28:12"
-        },
-        {
-            "name": "pear/pear-core-minimal",
-            "version": "v1.10.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/pear/pear-core-minimal.git",
-                "reference": "cae0f1ce0cb5bddb611b0a652d322905a65a5896"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/cae0f1ce0cb5bddb611b0a652d322905a65a5896",
-                "reference": "cae0f1ce0cb5bddb611b0a652d322905a65a5896",
-                "shasum": ""
-            },
-            "require": {
-                "pear/console_getopt": "~1.3",
-                "pear/pear_exception": "~1.0"
-            },
-            "replace": {
-                "rsky/pear-core-min": "self.version"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                "src/"
-            ],
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Christian Weiske",
-                    "email": "cweiske@php.net",
-                    "role": "Lead"
-                }
-            ],
-            "description": "Minimal set of PEAR core files to be used as composer dependency",
-            "time": "2015-10-17 11:41:19"
-        },
-        {
-            "name": "pear/pear_exception",
-            "version": "v1.0.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/pear/PEAR_Exception.git",
-                "reference": "8c18719fdae000b690e3912be401c76e406dd13b"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/8c18719fdae000b690e3912be401c76e406dd13b",
-                "reference": "8c18719fdae000b690e3912be401c76e406dd13b",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=4.4.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "*"
-            },
-            "type": "class",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-0": {
-                    "PEAR": ""
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                "."
-            ],
-            "license": [
-                "BSD-2-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Helgi Thormar",
-                    "email": "dufuz@php.net"
-                },
-                {
-                    "name": "Greg Beaver",
-                    "email": "cellog@php.net"
-                }
-            ],
-            "description": "The PEAR Exception base class.",
-            "homepage": "https://github.com/pear/PEAR_Exception",
-            "keywords": [
-                "exception"
-            ],
-            "time": "2015-02-10 20:07:52"
-        }
-    ],
-    "packages-dev": [
-        {
-            "name": "doctrine/instantiator",
-            "version": "1.0.5",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3,<8.0-DEV"
-            },
-            "require-dev": {
-                "athletic/athletic": "~0.1.8",
-                "ext-pdo": "*",
-                "ext-phar": "*",
-                "phpunit/phpunit": "~4.0",
-                "squizlabs/php_codesniffer": "~2.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Marco Pivetta",
-                    "email": "ocramius@gmail.com",
-                    "homepage": "http://ocramius.github.com/"
-                }
-            ],
-            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
-            "homepage": "https://github.com/doctrine/instantiator",
-            "keywords": [
-                "constructor",
-                "instantiate"
-            ],
-            "time": "2015-06-14 21:17:01"
-        },
-        {
-            "name": "phpunit/php-code-coverage",
-            "version": "2.2.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979",
-                "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3",
-                "phpunit/php-file-iterator": "~1.3",
-                "phpunit/php-text-template": "~1.2",
-                "phpunit/php-token-stream": "~1.3",
-                "sebastian/environment": "^1.3.2",
-                "sebastian/version": "~1.0"
-            },
-            "require-dev": {
-                "ext-xdebug": ">=2.1.4",
-                "phpunit/phpunit": "~4"
-            },
-            "suggest": {
-                "ext-dom": "*",
-                "ext-xdebug": ">=2.2.1",
-                "ext-xmlwriter": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.2.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
-            "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
-            "keywords": [
-                "coverage",
-                "testing",
-                "xunit"
-            ],
-            "time": "2015-10-06 15:47:00"
-        },
-        {
-            "name": "phpunit/php-file-iterator",
-            "version": "1.3.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb",
-                "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "File/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                ""
-            ],
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "FilterIterator implementation that filters files based on a list of suffixes.",
-            "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
-            "keywords": [
-                "filesystem",
-                "iterator"
-            ],
-            "time": "2013-10-10 15:34:57"
-        },
-        {
-            "name": "phpunit/php-text-template",
-            "version": "1.2.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-text-template.git",
-                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
-                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Simple template engine.",
-            "homepage": "https://github.com/sebastianbergmann/php-text-template/",
-            "keywords": [
-                "template"
-            ],
-            "time": "2015-06-21 13:50:34"
-        },
-        {
-            "name": "phpunit/php-timer",
-            "version": "1.0.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260",
-                "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4|~5"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Utility class for timing",
-            "homepage": "https://github.com/sebastianbergmann/php-timer/",
-            "keywords": [
-                "timer"
-            ],
-            "time": "2016-05-12 18:03:57"
-        },
-        {
-            "name": "phpunit/php-token-stream",
-            "version": "1.4.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
-                "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
-                "shasum": ""
-            },
-            "require": {
-                "ext-tokenizer": "*",
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.2"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Wrapper around PHP's tokenizer extension.",
-            "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
-            "keywords": [
-                "tokenizer"
-            ],
-            "time": "2015-09-15 10:49:45"
-        },
-        {
-            "name": "phpunit/phpunit",
-            "version": "4.4.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "bbe7bcb83b6ec1a9eaabbe1b70d4795027c53ee0"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bbe7bcb83b6ec1a9eaabbe1b70d4795027c53ee0",
-                "reference": "bbe7bcb83b6ec1a9eaabbe1b70d4795027c53ee0",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "ext-json": "*",
-                "ext-pcre": "*",
-                "ext-reflection": "*",
-                "ext-spl": "*",
-                "php": ">=5.3.3",
-                "phpunit/php-code-coverage": "~2.0",
-                "phpunit/php-file-iterator": "~1.3.2",
-                "phpunit/php-text-template": "~1.2",
-                "phpunit/php-timer": "~1.0.2",
-                "phpunit/phpunit-mock-objects": "~2.3",
-                "sebastian/comparator": "~1.0",
-                "sebastian/diff": "~1.1",
-                "sebastian/environment": "~1.1",
-                "sebastian/exporter": "~1.0",
-                "sebastian/global-state": "~1.0",
-                "sebastian/version": "~1.0",
-                "symfony/yaml": "~2.0"
-            },
-            "suggest": {
-                "phpunit/php-invoker": "~1.1"
-            },
-            "bin": [
-                "phpunit"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "The PHP Unit Testing framework.",
-            "homepage": "https://phpunit.de/",
-            "keywords": [
-                "phpunit",
-                "testing",
-                "xunit"
-            ],
-            "time": "2014-12-05 06:49:03"
-        },
-        {
-            "name": "phpunit/phpunit-mock-objects",
-            "version": "2.3.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983",
-                "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/instantiator": "^1.0.2",
-                "php": ">=5.3.3",
-                "phpunit/php-text-template": "~1.2",
-                "sebastian/exporter": "~1.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.4"
-            },
-            "suggest": {
-                "ext-soap": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.3.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Mock Object library for PHPUnit",
-            "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
-            "keywords": [
-                "mock",
-                "xunit"
-            ],
-            "time": "2015-10-02 06:51:40"
-        },
-        {
-            "name": "sebastian/comparator",
-            "version": "1.2.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "937efb279bd37a375bcadf584dec0726f84dbf22"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22",
-                "reference": "937efb279bd37a375bcadf584dec0726f84dbf22",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3",
-                "sebastian/diff": "~1.2",
-                "sebastian/exporter": "~1.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.2.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Volker Dusch",
-                    "email": "github@wallbash.com"
-                },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@2bepublished.at"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Provides the functionality to compare PHP values for equality",
-            "homepage": "http://www.github.com/sebastianbergmann/comparator",
-            "keywords": [
-                "comparator",
-                "compare",
-                "equality"
-            ],
-            "time": "2015-07-26 15:48:44"
-        },
-        {
-            "name": "sebastian/diff",
-            "version": "1.4.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e",
-                "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.8"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Kore Nordmann",
-                    "email": "mail@kore-nordmann.de"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Diff implementation",
-            "homepage": "https://github.com/sebastianbergmann/diff",
-            "keywords": [
-                "diff"
-            ],
-            "time": "2015-12-08 07:14:41"
-        },
-        {
-            "name": "sebastian/environment",
-            "version": "1.3.7",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716",
-                "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.3.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Provides functionality to handle HHVM/PHP environments",
-            "homepage": "http://www.github.com/sebastianbergmann/environment",
-            "keywords": [
-                "Xdebug",
-                "environment",
-                "hhvm"
-            ],
-            "time": "2016-05-17 03:18:57"
-        },
-        {
-            "name": "sebastian/exporter",
-            "version": "1.2.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4",
-                "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3",
-                "sebastian/recursion-context": "~1.0"
-            },
-            "require-dev": {
-                "ext-mbstring": "*",
-                "phpunit/phpunit": "~4.4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.3.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Volker Dusch",
-                    "email": "github@wallbash.com"
-                },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@2bepublished.at"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Adam Harvey",
-                    "email": "aharvey@php.net"
-                }
-            ],
-            "description": "Provides the functionality to export PHP variables for visualization",
-            "homepage": "http://www.github.com/sebastianbergmann/exporter",
-            "keywords": [
-                "export",
-                "exporter"
-            ],
-            "time": "2016-06-17 09:04:28"
-        },
-        {
-            "name": "sebastian/global-state",
-            "version": "1.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
-                "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.2"
-            },
-            "suggest": {
-                "ext-uopz": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Snapshotting of global state",
-            "homepage": "http://www.github.com/sebastianbergmann/global-state",
-            "keywords": [
-                "global state"
-            ],
-            "time": "2015-10-12 03:26:01"
-        },
-        {
-            "name": "sebastian/recursion-context",
-            "version": "1.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "913401df809e99e4f47b27cdd781f4a258d58791"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791",
-                "reference": "913401df809e99e4f47b27cdd781f4a258d58791",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Adam Harvey",
-                    "email": "aharvey@php.net"
-                }
-            ],
-            "description": "Provides functionality to recursively process PHP variables",
-            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
-            "time": "2015-11-11 19:50:13"
-        },
-        {
-            "name": "sebastian/version",
-            "version": "1.0.6",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/version.git",
-                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
-                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
-                "shasum": ""
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Library that helps with managing the version number of Git-hosted PHP projects",
-            "homepage": "https://github.com/sebastianbergmann/version",
-            "time": "2015-06-21 13:59:46"
-        },
-        {
-            "name": "symfony/yaml",
-            "version": "v2.8.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/yaml.git",
-                "reference": "dba4bb5846798cd12f32e2d8f3f35d77045773c8"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/dba4bb5846798cd12f32e2d8f3f35d77045773c8",
-                "reference": "dba4bb5846798cd12f32e2d8f3f35d77045773c8",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.9"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.8-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Yaml\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony Yaml Component",
-            "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:29:29"
-        }
-    ],
-    "aliases": [],
-    "minimum-stability": "stable",
-    "stability-flags": [],
-    "prefer-stable": false,
-    "prefer-lowest": false,
-    "platform": {
-        "php": ">=5.3.2"
-    },
-    "platform-dev": []
-}
diff --git a/app/vendor/piwik/decompress/libs/PclZip/gnu-lgpl.txt b/app/vendor/piwik/decompress/libs/PclZip/gnu-lgpl.txt
new file mode 100644
index 000000000..b1e3f5a26
--- /dev/null
+++ b/app/vendor/piwik/decompress/libs/PclZip/gnu-lgpl.txt
@@ -0,0 +1,504 @@
+		  GNU LESSER GENERAL PUBLIC LICENSE
+		       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+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 this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+		  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser 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 Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+			    NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "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
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY 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
+LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey 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 library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library 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
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  , 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/app/vendor/piwik/decompress/libs/PclZip/pclzip.lib.php b/app/vendor/piwik/decompress/libs/PclZip/pclzip.lib.php
new file mode 100644
index 000000000..0e4e2ffdd
--- /dev/null
+++ b/app/vendor/piwik/decompress/libs/PclZip/pclzip.lib.php
@@ -0,0 +1,5414 @@
+zipname             = $p_zipname;
+    $this->zip_fd              = 0;
+    $this->magic_quotes_status = -1;
+
+    // ----- Return
+    return;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function :
+  //   create($p_filelist, $p_add_dir="", $p_remove_dir="")
+  //   create($p_filelist, $p_option, $p_option_value, ...)
+  // Description :
+  //   This method supports two different synopsis. The first one is historical.
+  //   This method creates a Zip Archive. The Zip file is created in the
+  //   filesystem. The files and directories indicated in $p_filelist
+  //   are added in the archive. See the parameters description for the
+  //   supported format of $p_filelist.
+  //   When a directory is in the list, the directory and its content is added
+  //   in the archive.
+  //   In this synopsis, the function takes an optional variable list of
+  //   options. See bellow the supported options.
+  // Parameters :
+  //   $p_filelist : An array containing file or directory names, or
+  //                 a string containing one filename or one directory name, or
+  //                 a string containing a list of filenames and/or directory
+  //                 names separated by spaces.
+  //   $p_add_dir : A path to add before the real path of the archived file,
+  //                in order to have it memorized in the archive.
+  //   $p_remove_dir : A path to remove from the real path of the file to archive,
+  //                   in order to have a shorter path memorized in the archive.
+  //                   When $p_add_dir and $p_remove_dir are set, $p_remove_dir
+  //                   is removed first, before $p_add_dir is added.
+  // Options :
+  //   PCLZIP_OPT_ADD_PATH :
+  //   PCLZIP_OPT_REMOVE_PATH :
+  //   PCLZIP_OPT_REMOVE_ALL_PATH :
+  //   PCLZIP_OPT_COMMENT :
+  //   PCLZIP_CB_PRE_ADD :
+  //   PCLZIP_CB_POST_ADD :
+  // Return Values :
+  //   0 on failure,
+  //   The list of the added files, with a status of the add action.
+  //   (see PclZip::listContent() for list entry format)
+  // --------------------------------------------------------------------------------
+  public function create($p_filelist)
+  {
+    $v_result = 1;
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Set default values
+    $v_options                            = array();
+    $v_options[PCLZIP_OPT_NO_COMPRESSION] = false;
+
+    // ----- Look for variable options arguments
+    $v_size = func_num_args();
+
+    // ----- Look for arguments
+    if ($v_size > 1) {
+      // ----- Get the arguments
+      $v_arg_list = func_get_args();
+
+      // ----- Remove from the options list the first argument
+      array_shift($v_arg_list);
+      $v_size--;
+
+      // ----- Look for first arg
+      if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+        // ----- Parse the options
+        $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+            PCLZIP_OPT_REMOVE_PATH => 'optional',
+            PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+            PCLZIP_OPT_ADD_PATH => 'optional',
+            PCLZIP_CB_PRE_ADD => 'optional',
+            PCLZIP_CB_POST_ADD => 'optional',
+            PCLZIP_OPT_NO_COMPRESSION => 'optional',
+            PCLZIP_OPT_COMMENT => 'optional',
+            PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+            PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+            PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+          //, PCLZIP_OPT_CRYPT => 'optional'
+        ));
+        if ($v_result != 1) {
+          return 0;
+        }
+
+        // ----- Look for 2 args
+        // Here we need to support the first historic synopsis of the
+        // method.
+      } else {
+
+        // ----- Get the first argument
+        $v_options[PCLZIP_OPT_ADD_PATH] = $v_arg_list[0];
+
+        // ----- Look for the optional second argument
+        if ($v_size == 2) {
+          $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1];
+        } elseif ($v_size > 2) {
+          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+          return 0;
+        }
+      }
+    }
+
+    // ----- Look for default option values
+    $this->privOptionDefaultThreshold($v_options);
+
+    // ----- Init
+    $v_string_list    = array();
+    $v_att_list       = array();
+    $v_filedescr_list = array();
+    $p_result_list    = array();
+
+    // ----- Look if the $p_filelist is really an array
+    if (is_array($p_filelist)) {
+
+      // ----- Look if the first element is also an array
+      //       This will mean that this is a file description entry
+      if (isset($p_filelist[0]) && is_array($p_filelist[0])) {
+        $v_att_list = $p_filelist;
+
+        // ----- The list is a list of string names
+      } else {
+        $v_string_list = $p_filelist;
+      }
+
+      // ----- Look if the $p_filelist is a string
+    } elseif (is_string($p_filelist)) {
+      // ----- Create a list from the string
+      $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist);
+
+      // ----- Invalid variable type for $p_filelist
+    } else {
+      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_filelist");
+
+      return 0;
+    }
+
+    // ----- Reformat the string list
+    if (sizeof($v_string_list) != 0) {
+      foreach ($v_string_list as $v_string) {
+        if ($v_string != '') {
+          $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string;
+        } else {
+        }
+      }
+    }
+
+    // ----- For each file in the list check the attributes
+    $v_supported_attributes = array(
+        PCLZIP_ATT_FILE_NAME => 'mandatory',
+        PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional',
+        PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional',
+        PCLZIP_ATT_FILE_MTIME => 'optional',
+        PCLZIP_ATT_FILE_CONTENT => 'optional',
+        PCLZIP_ATT_FILE_COMMENT => 'optional'
+    );
+    foreach ($v_att_list as $v_entry) {
+      $v_result = $this->privFileDescrParseAtt($v_entry, $v_filedescr_list[], $v_options, $v_supported_attributes);
+      if ($v_result != 1) {
+        return 0;
+      }
+    }
+
+    // ----- Expand the filelist (expand directories)
+    $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options);
+    if ($v_result != 1) {
+      return 0;
+    }
+
+    // ----- Call the create fct
+    $v_result = $this->privCreate($v_filedescr_list, $p_result_list, $v_options);
+    if ($v_result != 1) {
+      return 0;
+    }
+
+    // ----- Return
+    return $p_result_list;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function :
+  //   add($p_filelist, $p_add_dir="", $p_remove_dir="")
+  //   add($p_filelist, $p_option, $p_option_value, ...)
+  // Description :
+  //   This method supports two synopsis. The first one is historical.
+  //   This methods add the list of files in an existing archive.
+  //   If a file with the same name already exists, it is added at the end of the
+  //   archive, the first one is still present.
+  //   If the archive does not exist, it is created.
+  // Parameters :
+  //   $p_filelist : An array containing file or directory names, or
+  //                 a string containing one filename or one directory name, or
+  //                 a string containing a list of filenames and/or directory
+  //                 names separated by spaces.
+  //   $p_add_dir : A path to add before the real path of the archived file,
+  //                in order to have it memorized in the archive.
+  //   $p_remove_dir : A path to remove from the real path of the file to archive,
+  //                   in order to have a shorter path memorized in the archive.
+  //                   When $p_add_dir and $p_remove_dir are set, $p_remove_dir
+  //                   is removed first, before $p_add_dir is added.
+  // Options :
+  //   PCLZIP_OPT_ADD_PATH :
+  //   PCLZIP_OPT_REMOVE_PATH :
+  //   PCLZIP_OPT_REMOVE_ALL_PATH :
+  //   PCLZIP_OPT_COMMENT :
+  //   PCLZIP_OPT_ADD_COMMENT :
+  //   PCLZIP_OPT_PREPEND_COMMENT :
+  //   PCLZIP_CB_PRE_ADD :
+  //   PCLZIP_CB_POST_ADD :
+  // Return Values :
+  //   0 on failure,
+  //   The list of the added files, with a status of the add action.
+  //   (see PclZip::listContent() for list entry format)
+  // --------------------------------------------------------------------------------
+  public function add($p_filelist)
+  {
+    $v_result = 1;
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Set default values
+    $v_options                            = array();
+    $v_options[PCLZIP_OPT_NO_COMPRESSION] = false;
+
+    // ----- Look for variable options arguments
+    $v_size = func_num_args();
+
+    // ----- Look for arguments
+    if ($v_size > 1) {
+      // ----- Get the arguments
+      $v_arg_list = func_get_args();
+
+      // ----- Remove form the options list the first argument
+      array_shift($v_arg_list);
+      $v_size--;
+
+      // ----- Look for first arg
+      if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+        // ----- Parse the options
+        $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+            PCLZIP_OPT_REMOVE_PATH => 'optional',
+            PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+            PCLZIP_OPT_ADD_PATH => 'optional',
+            PCLZIP_CB_PRE_ADD => 'optional',
+            PCLZIP_CB_POST_ADD => 'optional',
+            PCLZIP_OPT_NO_COMPRESSION => 'optional',
+            PCLZIP_OPT_COMMENT => 'optional',
+            PCLZIP_OPT_ADD_COMMENT => 'optional',
+            PCLZIP_OPT_PREPEND_COMMENT => 'optional',
+            PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+            PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+            PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+          //, PCLZIP_OPT_CRYPT => 'optional'
+        ));
+        if ($v_result != 1) {
+          return 0;
+        }
+
+        // ----- Look for 2 args
+        // Here we need to support the first historic synopsis of the
+        // method.
+      } else {
+
+        // ----- Get the first argument
+        $v_options[PCLZIP_OPT_ADD_PATH] = $v_add_path = $v_arg_list[0];
+
+        // ----- Look for the optional second argument
+        if ($v_size == 2) {
+          $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1];
+        } elseif ($v_size > 2) {
+          // ----- Error log
+          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+          // ----- Return
+          return 0;
+        }
+      }
+    }
+
+    // ----- Look for default option values
+    $this->privOptionDefaultThreshold($v_options);
+
+    // ----- Init
+    $v_string_list    = array();
+    $v_att_list       = array();
+    $v_filedescr_list = array();
+    $p_result_list    = array();
+
+    // ----- Look if the $p_filelist is really an array
+    if (is_array($p_filelist)) {
+
+      // ----- Look if the first element is also an array
+      //       This will mean that this is a file description entry
+      if (isset($p_filelist[0]) && is_array($p_filelist[0])) {
+        $v_att_list = $p_filelist;
+
+        // ----- The list is a list of string names
+      } else {
+        $v_string_list = $p_filelist;
+      }
+
+      // ----- Look if the $p_filelist is a string
+    } elseif (is_string($p_filelist)) {
+      // ----- Create a list from the string
+      $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist);
+
+      // ----- Invalid variable type for $p_filelist
+    } else {
+      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type '" . gettype($p_filelist) . "' for p_filelist");
+
+      return 0;
+    }
+
+    // ----- Reformat the string list
+    if (sizeof($v_string_list) != 0) {
+      foreach ($v_string_list as $v_string) {
+        $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string;
+      }
+    }
+
+    // ----- For each file in the list check the attributes
+    $v_supported_attributes = array(
+        PCLZIP_ATT_FILE_NAME => 'mandatory',
+        PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional',
+        PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional',
+        PCLZIP_ATT_FILE_MTIME => 'optional',
+        PCLZIP_ATT_FILE_CONTENT => 'optional',
+        PCLZIP_ATT_FILE_COMMENT => 'optional'
+    );
+    foreach ($v_att_list as $v_entry) {
+      $v_result = $this->privFileDescrParseAtt($v_entry, $v_filedescr_list[], $v_options, $v_supported_attributes);
+      if ($v_result != 1) {
+        return 0;
+      }
+    }
+
+    // ----- Expand the filelist (expand directories)
+    $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options);
+    if ($v_result != 1) {
+      return 0;
+    }
+
+    // ----- Call the create fct
+    $v_result = $this->privAdd($v_filedescr_list, $p_result_list, $v_options);
+    if ($v_result != 1) {
+      return 0;
+    }
+
+    // ----- Return
+    return $p_result_list;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : listContent()
+  // Description :
+  //   This public method, gives the list of the files and directories, with their
+  //   properties.
+  //   The properties of each entries in the list are (used also in other functions) :
+  //     filename : Name of the file. For a create or add action it is the filename
+  //                given by the user. For an extract function it is the filename
+  //                of the extracted file.
+  //     stored_filename : Name of the file / directory stored in the archive.
+  //     size : Size of the stored file.
+  //     compressed_size : Size of the file's data compressed in the archive
+  //                       (without the headers overhead)
+  //     mtime : Last known modification date of the file (UNIX timestamp)
+  //     comment : Comment associated with the file
+  //     folder : true | false
+  //     index : index of the file in the archive
+  //     status : status of the action (depending of the action) :
+  //              Values are :
+  //                ok : OK !
+  //                filtered : the file / dir is not extracted (filtered by user)
+  //                already_a_directory : the file can not be extracted because a
+  //                                      directory with the same name already exists
+  //                write_protected : the file can not be extracted because a file
+  //                                  with the same name already exists and is
+  //                                  write protected
+  //                newer_exist : the file was not extracted because a newer file exists
+  //                path_creation_fail : the file is not extracted because the folder
+  //                                     does not exist and can not be created
+  //                write_error : the file was not extracted because there was a
+  //                              error while writing the file
+  //                read_error : the file was not extracted because there was a error
+  //                             while reading the file
+  //                invalid_header : the file was not extracted because of an archive
+  //                                 format error (bad file header)
+  //   Note that each time a method can continue operating when there
+  //   is an action error on a file, the error is only logged in the file status.
+  // Return Values :
+  //   0 on an unrecoverable failure,
+  //   The list of the files in the archive.
+  // --------------------------------------------------------------------------------
+  public function listContent()
+  {
+    $v_result = 1;
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Check archive
+    if (!$this->privCheckFormat()) {
+      return (0);
+    }
+
+    // ----- Call the extracting fct
+    $p_list = array();
+    if (($v_result = $this->privList($p_list)) != 1) {
+      unset($p_list);
+
+      return (0);
+    }
+
+    // ----- Return
+    return $p_list;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function :
+  //   extract($p_path="./", $p_remove_path="")
+  //   extract([$p_option, $p_option_value, ...])
+  // Description :
+  //   This method supports two synopsis. The first one is historical.
+  //   This method extract all the files / directories from the archive to the
+  //   folder indicated in $p_path.
+  //   If you want to ignore the 'root' part of path of the memorized files
+  //   you can indicate this in the optional $p_remove_path parameter.
+  //   By default, if a newer file with the same name already exists, the
+  //   file is not extracted.
+  //
+  //   If both PCLZIP_OPT_PATH and PCLZIP_OPT_ADD_PATH aoptions
+  //   are used, the path indicated in PCLZIP_OPT_ADD_PATH is append
+  //   at the end of the path value of PCLZIP_OPT_PATH.
+  // Parameters :
+  //   $p_path : Path where the files and directories are to be extracted
+  //   $p_remove_path : First part ('root' part) of the memorized path
+  //                    (if any similar) to remove while extracting.
+  // Options :
+  //   PCLZIP_OPT_PATH :
+  //   PCLZIP_OPT_ADD_PATH :
+  //   PCLZIP_OPT_REMOVE_PATH :
+  //   PCLZIP_OPT_REMOVE_ALL_PATH :
+  //   PCLZIP_CB_PRE_EXTRACT :
+  //   PCLZIP_CB_POST_EXTRACT :
+  // Return Values :
+  //   0 or a negative value on failure,
+  //   The list of the extracted files, with a status of the action.
+  //   (see PclZip::listContent() for list entry format)
+  // --------------------------------------------------------------------------------
+  public function extract()
+  {
+    $v_result = 1;
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Check archive
+    if (!$this->privCheckFormat()) {
+      return (0);
+    }
+
+    // ----- Set default values
+    $v_options         = array();
+    //    $v_path = "./";
+    $v_path            = '';
+    $v_remove_path     = "";
+    $v_remove_all_path = false;
+
+    // ----- Look for variable options arguments
+    $v_size = func_num_args();
+
+    // ----- Default values for option
+    $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+
+    // ----- Look for arguments
+    if ($v_size > 0) {
+      // ----- Get the arguments
+      $v_arg_list = func_get_args();
+
+      // ----- Look for first arg
+      if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+        // ----- Parse the options
+        $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+            PCLZIP_OPT_PATH => 'optional',
+            PCLZIP_OPT_REMOVE_PATH => 'optional',
+            PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+            PCLZIP_OPT_ADD_PATH => 'optional',
+            PCLZIP_CB_PRE_EXTRACT => 'optional',
+            PCLZIP_CB_POST_EXTRACT => 'optional',
+            PCLZIP_OPT_SET_CHMOD => 'optional',
+            PCLZIP_OPT_BY_NAME => 'optional',
+            PCLZIP_OPT_BY_EREG => 'optional',
+            PCLZIP_OPT_BY_PREG => 'optional',
+            PCLZIP_OPT_BY_INDEX => 'optional',
+            PCLZIP_OPT_EXTRACT_AS_STRING => 'optional',
+            PCLZIP_OPT_EXTRACT_IN_OUTPUT => 'optional',
+            PCLZIP_OPT_REPLACE_NEWER => 'optional',
+            PCLZIP_OPT_STOP_ON_ERROR => 'optional',
+            PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional',
+            PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+            PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+            PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+        ));
+        if ($v_result != 1) {
+          return 0;
+        }
+
+        // ----- Set the arguments
+        if (isset($v_options[PCLZIP_OPT_PATH])) {
+          $v_path = $v_options[PCLZIP_OPT_PATH];
+        }
+        if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) {
+          $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH];
+        }
+        if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+          $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+        }
+        if (isset($v_options[PCLZIP_OPT_ADD_PATH])) {
+          // ----- Check for '/' in last path char
+          if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) {
+            $v_path .= '/';
+          }
+          $v_path .= $v_options[PCLZIP_OPT_ADD_PATH];
+        }
+
+        // ----- Look for 2 args
+        // Here we need to support the first historic synopsis of the
+        // method.
+      } else {
+
+        // ----- Get the first argument
+        $v_path = $v_arg_list[0];
+
+        // ----- Look for the optional second argument
+        if ($v_size == 2) {
+          $v_remove_path = $v_arg_list[1];
+        } elseif ($v_size > 2) {
+          // ----- Error log
+          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+          // ----- Return
+          return 0;
+        }
+      }
+    }
+
+    // ----- Look for default option values
+    $this->privOptionDefaultThreshold($v_options);
+
+    // ----- Trace
+
+    // ----- Call the extracting fct
+    $p_list   = array();
+    $v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, $v_remove_all_path, $v_options);
+    if ($v_result < 1) {
+      unset($p_list);
+
+      return (0);
+    }
+
+    // ----- Return
+    return $p_list;
+  }
+  // --------------------------------------------------------------------------------
+
+
+  // --------------------------------------------------------------------------------
+  // Function :
+  //   extractByIndex($p_index, $p_path="./", $p_remove_path="")
+  //   extractByIndex($p_index, [$p_option, $p_option_value, ...])
+  // Description :
+  //   This method supports two synopsis. The first one is historical.
+  //   This method is doing a partial extract of the archive.
+  //   The extracted files or folders are identified by their index in the
+  //   archive (from 0 to n).
+  //   Note that if the index identify a folder, only the folder entry is
+  //   extracted, not all the files included in the archive.
+  // Parameters :
+  //   $p_index : A single index (integer) or a string of indexes of files to
+  //              extract. The form of the string is "0,4-6,8-12" with only numbers
+  //              and '-' for range or ',' to separate ranges. No spaces or ';'
+  //              are allowed.
+  //   $p_path : Path where the files and directories are to be extracted
+  //   $p_remove_path : First part ('root' part) of the memorized path
+  //                    (if any similar) to remove while extracting.
+  // Options :
+  //   PCLZIP_OPT_PATH :
+  //   PCLZIP_OPT_ADD_PATH :
+  //   PCLZIP_OPT_REMOVE_PATH :
+  //   PCLZIP_OPT_REMOVE_ALL_PATH :
+  //   PCLZIP_OPT_EXTRACT_AS_STRING : The files are extracted as strings and
+  //     not as files.
+  //     The resulting content is in a new field 'content' in the file
+  //     structure.
+  //     This option must be used alone (any other options are ignored).
+  //   PCLZIP_CB_PRE_EXTRACT :
+  //   PCLZIP_CB_POST_EXTRACT :
+  // Return Values :
+  //   0 on failure,
+  //   The list of the extracted files, with a status of the action.
+  //   (see PclZip::listContent() for list entry format)
+  // --------------------------------------------------------------------------------
+  //function extractByIndex($p_index, options...)
+  public function extractByIndex($p_index)
+  {
+    $v_result = 1;
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Check archive
+    if (!$this->privCheckFormat()) {
+      return (0);
+    }
+
+    // ----- Set default values
+    $v_options         = array();
+    //    $v_path = "./";
+    $v_path            = '';
+    $v_remove_path     = "";
+    $v_remove_all_path = false;
+
+    // ----- Look for variable options arguments
+    $v_size = func_num_args();
+
+    // ----- Default values for option
+    $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+
+    // ----- Look for arguments
+    if ($v_size > 1) {
+      // ----- Get the arguments
+      $v_arg_list = func_get_args();
+
+      // ----- Remove form the options list the first argument
+      array_shift($v_arg_list);
+      $v_size--;
+
+      // ----- Look for first arg
+      if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+        // ----- Parse the options
+        $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+            PCLZIP_OPT_PATH => 'optional',
+            PCLZIP_OPT_REMOVE_PATH => 'optional',
+            PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+            PCLZIP_OPT_EXTRACT_AS_STRING => 'optional',
+            PCLZIP_OPT_ADD_PATH => 'optional',
+            PCLZIP_CB_PRE_EXTRACT => 'optional',
+            PCLZIP_CB_POST_EXTRACT => 'optional',
+            PCLZIP_OPT_SET_CHMOD => 'optional',
+            PCLZIP_OPT_REPLACE_NEWER => 'optional',
+            PCLZIP_OPT_STOP_ON_ERROR => 'optional',
+            PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional',
+            PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+            PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+            PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+        ));
+        if ($v_result != 1) {
+          return 0;
+        }
+
+        // ----- Set the arguments
+        if (isset($v_options[PCLZIP_OPT_PATH])) {
+          $v_path = $v_options[PCLZIP_OPT_PATH];
+        }
+        if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) {
+          $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH];
+        }
+        if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+          $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+        }
+        if (isset($v_options[PCLZIP_OPT_ADD_PATH])) {
+          // ----- Check for '/' in last path char
+          if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) {
+            $v_path .= '/';
+          }
+          $v_path .= $v_options[PCLZIP_OPT_ADD_PATH];
+        }
+        if (!isset($v_options[PCLZIP_OPT_EXTRACT_AS_STRING])) {
+          $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+        } else {
+        }
+
+        // ----- Look for 2 args
+        // Here we need to support the first historic synopsis of the
+        // method.
+      } else {
+
+        // ----- Get the first argument
+        $v_path = $v_arg_list[0];
+
+        // ----- Look for the optional second argument
+        if ($v_size == 2) {
+          $v_remove_path = $v_arg_list[1];
+        } elseif ($v_size > 2) {
+          // ----- Error log
+          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+          // ----- Return
+          return 0;
+        }
+      }
+    }
+
+    // ----- Trace
+
+    // ----- Trick
+    // Here I want to reuse extractByRule(), so I need to parse the $p_index
+    // with privParseOptions()
+    $v_arg_trick     = array(
+        PCLZIP_OPT_BY_INDEX,
+        $p_index
+    );
+    $v_options_trick = array();
+    $v_result        = $this->privParseOptions($v_arg_trick, sizeof($v_arg_trick), $v_options_trick, array(
+        PCLZIP_OPT_BY_INDEX => 'optional'
+    ));
+    if ($v_result != 1) {
+      return 0;
+    }
+    $v_options[PCLZIP_OPT_BY_INDEX] = $v_options_trick[PCLZIP_OPT_BY_INDEX];
+
+    // ----- Look for default option values
+    $this->privOptionDefaultThreshold($v_options);
+
+    // ----- Call the extracting fct
+    if (($v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, $v_remove_all_path, $v_options)) < 1) {
+      return (0);
+    }
+
+    // ----- Return
+    return $p_list;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function :
+  //   delete([$p_option, $p_option_value, ...])
+  // Description :
+  //   This method removes files from the archive.
+  //   If no parameters are given, then all the archive is emptied.
+  // Parameters :
+  //   None or optional arguments.
+  // Options :
+  //   PCLZIP_OPT_BY_INDEX :
+  //   PCLZIP_OPT_BY_NAME :
+  //   PCLZIP_OPT_BY_EREG :
+  //   PCLZIP_OPT_BY_PREG :
+  // Return Values :
+  //   0 on failure,
+  //   The list of the files which are still present in the archive.
+  //   (see PclZip::listContent() for list entry format)
+  // --------------------------------------------------------------------------------
+  public function delete()
+  {
+    $v_result = 1;
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Check archive
+    if (!$this->privCheckFormat()) {
+      return (0);
+    }
+
+    // ----- Set default values
+    $v_options = array();
+
+    // ----- Look for variable options arguments
+    $v_size = func_num_args();
+
+    // ----- Look for arguments
+    if ($v_size > 0) {
+      // ----- Get the arguments
+      $v_arg_list = func_get_args();
+
+      // ----- Parse the options
+      $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+          PCLZIP_OPT_BY_NAME => 'optional',
+          PCLZIP_OPT_BY_EREG => 'optional',
+          PCLZIP_OPT_BY_PREG => 'optional',
+          PCLZIP_OPT_BY_INDEX => 'optional'
+      ));
+      if ($v_result != 1) {
+        return 0;
+      }
+    }
+
+    // ----- Magic quotes trick
+    $this->privDisableMagicQuotes();
+
+    // ----- Call the delete fct
+    $v_list = array();
+    if (($v_result = $this->privDeleteByRule($v_list, $v_options)) != 1) {
+      $this->privSwapBackMagicQuotes();
+      unset($v_list);
+
+      return (0);
+    }
+
+    // ----- Magic quotes trick
+    $this->privSwapBackMagicQuotes();
+
+    // ----- Return
+    return $v_list;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : deleteByIndex()
+  // Description :
+  //   ***** Deprecated *****
+  //   delete(PCLZIP_OPT_BY_INDEX, $p_index) should be prefered.
+  // --------------------------------------------------------------------------------
+  public function deleteByIndex($p_index)
+  {
+
+    $p_list = $this->delete(PCLZIP_OPT_BY_INDEX, $p_index);
+
+    // ----- Return
+    return $p_list;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : properties()
+  // Description :
+  //   This method gives the properties of the archive.
+  //   The properties are :
+  //     nb : Number of files in the archive
+  //     comment : Comment associated with the archive file
+  //     status : not_exist, ok
+  // Parameters :
+  //   None
+  // Return Values :
+  //   0 on failure,
+  //   An array with the archive properties.
+  // --------------------------------------------------------------------------------
+  public function properties()
+  {
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Magic quotes trick
+    $this->privDisableMagicQuotes();
+
+    // ----- Check archive
+    if (!$this->privCheckFormat()) {
+      $this->privSwapBackMagicQuotes();
+
+      return (0);
+    }
+
+    // ----- Default properties
+    $v_prop            = array();
+    $v_prop['comment'] = '';
+    $v_prop['nb']      = 0;
+    $v_prop['status']  = 'not_exist';
+
+    // ----- Look if file exists
+    if (@is_file($this->zipname)) {
+      // ----- Open the zip file
+      if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0) {
+        $this->privSwapBackMagicQuotes();
+
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in binary read mode');
+
+        // ----- Return
+        return 0;
+      }
+
+      // ----- Read the central directory informations
+      $v_central_dir = array();
+      if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+        $this->privSwapBackMagicQuotes();
+
+        return 0;
+      }
+
+      // ----- Close the zip file
+      $this->privCloseFd();
+
+      // ----- Set the user attributes
+      $v_prop['comment'] = $v_central_dir['comment'];
+      $v_prop['nb']      = $v_central_dir['entries'];
+      $v_prop['status']  = 'ok';
+    }
+
+    // ----- Magic quotes trick
+    $this->privSwapBackMagicQuotes();
+
+    // ----- Return
+    return $v_prop;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : duplicate()
+  // Description :
+  //   This method creates an archive by copying the content of an other one. If
+  //   the archive already exist, it is replaced by the new one without any warning.
+  // Parameters :
+  //   $p_archive : The filename of a valid archive, or
+  //                a valid PclZip object.
+  // Return Values :
+  //   1 on success.
+  //   0 or a negative value on error (error code).
+  // --------------------------------------------------------------------------------
+  public function duplicate($p_archive)
+  {
+    $v_result = 1;
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Look if the $p_archive is a PclZip object
+    if ((is_object($p_archive)) && (get_class($p_archive) == 'pclzip')) {
+
+      // ----- Duplicate the archive
+      $v_result = $this->privDuplicate($p_archive->zipname);
+
+      // ----- Look if the $p_archive is a string (so a filename)
+    } elseif (is_string($p_archive)) {
+
+      // ----- Check that $p_archive is a valid zip file
+      // TBC : Should also check the archive format
+      if (!is_file($p_archive)) {
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "No file with filename '" . $p_archive . "'");
+        $v_result = PCLZIP_ERR_MISSING_FILE;
+      } else {
+        // ----- Duplicate the archive
+        $v_result = $this->privDuplicate($p_archive);
+      }
+
+      // ----- Invalid variable
+    } else {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add");
+      $v_result = PCLZIP_ERR_INVALID_PARAMETER;
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : merge()
+  // Description :
+  //   This method merge the $p_archive_to_add archive at the end of the current
+  //   one ($this).
+  //   If the archive ($this) does not exist, the merge becomes a duplicate.
+  //   If the $p_archive_to_add archive does not exist, the merge is a success.
+  // Parameters :
+  //   $p_archive_to_add : It can be directly the filename of a valid zip archive,
+  //                       or a PclZip object archive.
+  // Return Values :
+  //   1 on success,
+  //   0 or negative values on error (see below).
+  // --------------------------------------------------------------------------------
+  public function merge($p_archive_to_add)
+  {
+    $v_result = 1;
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Check archive
+    if (!$this->privCheckFormat()) {
+      return (0);
+    }
+
+    // ----- Look if the $p_archive_to_add is a PclZip object
+    if ((is_object($p_archive_to_add)) && (get_class($p_archive_to_add) == 'pclzip')) {
+
+      // ----- Merge the archive
+      $v_result = $this->privMerge($p_archive_to_add);
+
+      // ----- Look if the $p_archive_to_add is a string (so a filename)
+    } elseif (is_string($p_archive_to_add)) {
+
+      // ----- Create a temporary archive
+      $v_object_archive = new PclZip($p_archive_to_add);
+
+      // ----- Merge the archive
+      $v_result = $this->privMerge($v_object_archive);
+
+      // ----- Invalid variable
+    } else {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add");
+      $v_result = PCLZIP_ERR_INVALID_PARAMETER;
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : errorCode()
+  // Description :
+  // Parameters :
+  // --------------------------------------------------------------------------------
+  public function errorCode()
+  {
+    if (PCLZIP_ERROR_EXTERNAL == 1) {
+      return (PclErrorCode());
+    } else {
+      return ($this->error_code);
+    }
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : errorName()
+  // Description :
+  // Parameters :
+  // --------------------------------------------------------------------------------
+  public function errorName($p_with_code = false)
+  {
+    $v_name = array(
+        PCLZIP_ERR_NO_ERROR => 'PCLZIP_ERR_NO_ERROR',
+        PCLZIP_ERR_WRITE_OPEN_FAIL => 'PCLZIP_ERR_WRITE_OPEN_FAIL',
+        PCLZIP_ERR_READ_OPEN_FAIL => 'PCLZIP_ERR_READ_OPEN_FAIL',
+        PCLZIP_ERR_INVALID_PARAMETER => 'PCLZIP_ERR_INVALID_PARAMETER',
+        PCLZIP_ERR_MISSING_FILE => 'PCLZIP_ERR_MISSING_FILE',
+        PCLZIP_ERR_FILENAME_TOO_LONG => 'PCLZIP_ERR_FILENAME_TOO_LONG',
+        PCLZIP_ERR_INVALID_ZIP => 'PCLZIP_ERR_INVALID_ZIP',
+        PCLZIP_ERR_BAD_EXTRACTED_FILE => 'PCLZIP_ERR_BAD_EXTRACTED_FILE',
+        PCLZIP_ERR_DIR_CREATE_FAIL => 'PCLZIP_ERR_DIR_CREATE_FAIL',
+        PCLZIP_ERR_BAD_EXTENSION => 'PCLZIP_ERR_BAD_EXTENSION',
+        PCLZIP_ERR_BAD_FORMAT => 'PCLZIP_ERR_BAD_FORMAT',
+        PCLZIP_ERR_DELETE_FILE_FAIL => 'PCLZIP_ERR_DELETE_FILE_FAIL',
+        PCLZIP_ERR_RENAME_FILE_FAIL => 'PCLZIP_ERR_RENAME_FILE_FAIL',
+        PCLZIP_ERR_BAD_CHECKSUM => 'PCLZIP_ERR_BAD_CHECKSUM',
+        PCLZIP_ERR_INVALID_ARCHIVE_ZIP => 'PCLZIP_ERR_INVALID_ARCHIVE_ZIP',
+        PCLZIP_ERR_MISSING_OPTION_VALUE => 'PCLZIP_ERR_MISSING_OPTION_VALUE',
+        PCLZIP_ERR_INVALID_OPTION_VALUE => 'PCLZIP_ERR_INVALID_OPTION_VALUE',
+        PCLZIP_ERR_UNSUPPORTED_COMPRESSION => 'PCLZIP_ERR_UNSUPPORTED_COMPRESSION',
+        PCLZIP_ERR_UNSUPPORTED_ENCRYPTION => 'PCLZIP_ERR_UNSUPPORTED_ENCRYPTION',
+        PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE => 'PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE',
+        PCLZIP_ERR_DIRECTORY_RESTRICTION => 'PCLZIP_ERR_DIRECTORY_RESTRICTION'
+    );
+
+    if (isset($v_name[$this->error_code])) {
+      $v_value = $v_name[$this->error_code];
+    } else {
+      $v_value = 'NoName';
+    }
+
+    if ($p_with_code) {
+      return ($v_value . ' (' . $this->error_code . ')');
+    } else {
+      return ($v_value);
+    }
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : errorInfo()
+  // Description :
+  // Parameters :
+  // --------------------------------------------------------------------------------
+  public function errorInfo($p_full = false)
+  {
+    if (PCLZIP_ERROR_EXTERNAL == 1) {
+      return (PclErrorString());
+    } else {
+      if ($p_full) {
+        return ($this->errorName(true) . " : " . $this->error_string);
+      } else {
+        return ($this->error_string . " [code " . $this->error_code . "]");
+      }
+    }
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // ***** UNDER THIS LINE ARE DEFINED PRIVATE INTERNAL FUNCTIONS *****
+  // *****                                                        *****
+  // *****       THESES FUNCTIONS MUST NOT BE USED DIRECTLY       *****
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privCheckFormat()
+  // Description :
+  //   This method check that the archive exists and is a valid zip archive.
+  //   Several level of check exists. (futur)
+  // Parameters :
+  //   $p_level : Level of check. Default 0.
+  //              0 : Check the first bytes (magic codes) (default value))
+  //              1 : 0 + Check the central directory (futur)
+  //              2 : 1 + Check each file header (futur)
+  // Return Values :
+  //   true on success,
+  //   false on error, the error code is set.
+  // --------------------------------------------------------------------------------
+  public function privCheckFormat($p_level = 0)
+  {
+    $v_result = true;
+
+    // ----- Reset the file system cache
+    clearstatcache();
+
+    // ----- Reset the error handler
+    $this->privErrorReset();
+
+    // ----- Look if the file exits
+    if (!is_file($this->zipname)) {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "Missing archive file '" . $this->zipname . "'");
+
+      return (false);
+    }
+
+    // ----- Check that the file is readeable
+    if (!is_readable($this->zipname)) {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to read archive '" . $this->zipname . "'");
+
+      return (false);
+    }
+
+    // ----- Check the magic code
+    // TBC
+
+    // ----- Check the central header
+    // TBC
+
+    // ----- Check each file header
+    // TBC
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privParseOptions()
+  // Description :
+  //   This internal methods reads the variable list of arguments ($p_options_list,
+  //   $p_size) and generate an array with the options and values ($v_result_list).
+  //   $v_requested_options contains the options that can be present and those that
+  //   must be present.
+  //   $v_requested_options is an array, with the option value as key, and 'optional',
+  //   or 'mandatory' as value.
+  // Parameters :
+  //   See above.
+  // Return Values :
+  //   1 on success.
+  //   0 on failure.
+  // --------------------------------------------------------------------------------
+  public function privParseOptions(&$p_options_list, $p_size, &$v_result_list, $v_requested_options = false)
+  {
+    $v_result = 1;
+
+    // ----- Read the options
+    $i = 0;
+    while ($i < $p_size) {
+
+      // ----- Check if the option is supported
+      if (!isset($v_requested_options[$p_options_list[$i]])) {
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid optional parameter '" . $p_options_list[$i] . "' for this method");
+
+        // ----- Return
+        return PclZip::errorCode();
+      }
+
+      // ----- Look for next option
+      switch ($p_options_list[$i]) {
+        // ----- Look for options that request a path value
+        case PCLZIP_OPT_PATH:
+        case PCLZIP_OPT_REMOVE_PATH:
+        case PCLZIP_OPT_ADD_PATH:
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value
+          $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i + 1], false);
+          $i++;
+          break;
+
+        case PCLZIP_OPT_TEMP_FILE_THRESHOLD:
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            return PclZip::errorCode();
+          }
+
+          // ----- Check for incompatible options
+          if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'");
+
+            return PclZip::errorCode();
+          }
+
+          // ----- Check the value
+          $v_value = $p_options_list[$i + 1];
+          if ((!is_integer($v_value)) || ($v_value < 0)) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Integer expected for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value (and convert it in bytes)
+          $v_result_list[$p_options_list[$i]] = $v_value * 1048576;
+          $i++;
+          break;
+
+        case PCLZIP_OPT_TEMP_FILE_ON:
+          // ----- Check for incompatible options
+          if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'");
+
+            return PclZip::errorCode();
+          }
+
+          $v_result_list[$p_options_list[$i]] = true;
+          break;
+
+        case PCLZIP_OPT_TEMP_FILE_OFF:
+          // ----- Check for incompatible options
+          if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_ON])) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_ON'");
+
+            return PclZip::errorCode();
+          }
+          // ----- Check for incompatible options
+          if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_THRESHOLD'");
+
+            return PclZip::errorCode();
+          }
+
+          $v_result_list[$p_options_list[$i]] = true;
+          break;
+
+        case PCLZIP_OPT_EXTRACT_DIR_RESTRICTION:
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value
+          if (is_string($p_options_list[$i + 1]) && ($p_options_list[$i + 1] != '')) {
+            $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i + 1], false);
+            $i++;
+          } else {
+          }
+          break;
+
+        // ----- Look for options that request an array of string for value
+        case PCLZIP_OPT_BY_NAME:
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value
+          if (is_string($p_options_list[$i + 1])) {
+            $v_result_list[$p_options_list[$i]][0] = $p_options_list[$i + 1];
+          } elseif (is_array($p_options_list[$i + 1])) {
+            $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+          } else {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+          $i++;
+          break;
+
+        // ----- Look for options that request an EREG or PREG expression
+        case PCLZIP_OPT_BY_EREG:
+          $p_options_list[$i] = PCLZIP_OPT_BY_PREG;
+        // ereg() is deprecated starting with PHP 5.3. Move PCLZIP_OPT_BY_EREG
+        // to PCLZIP_OPT_BY_PREG
+        case PCLZIP_OPT_BY_PREG:
+          //case PCLZIP_OPT_CRYPT :
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value
+          if (is_string($p_options_list[$i + 1])) {
+            $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+          } else {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+          $i++;
+          break;
+
+        // ----- Look for options that takes a string
+        case PCLZIP_OPT_COMMENT:
+        case PCLZIP_OPT_ADD_COMMENT:
+        case PCLZIP_OPT_PREPEND_COMMENT:
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value
+          if (is_string($p_options_list[$i + 1])) {
+            $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+          } else {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+          $i++;
+          break;
+
+        // ----- Look for options that request an array of index
+        case PCLZIP_OPT_BY_INDEX:
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value
+          $v_work_list = array();
+          if (is_string($p_options_list[$i + 1])) {
+
+            // ----- Remove spaces
+            $p_options_list[$i + 1] = strtr($p_options_list[$i + 1], ' ', '');
+
+            // ----- Parse items
+            $v_work_list = explode(",", $p_options_list[$i + 1]);
+          } elseif (is_integer($p_options_list[$i + 1])) {
+            $v_work_list[0] = $p_options_list[$i + 1] . '-' . $p_options_list[$i + 1];
+          } elseif (is_array($p_options_list[$i + 1])) {
+            $v_work_list = $p_options_list[$i + 1];
+          } else {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Value must be integer, string or array for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Reduce the index list
+          // each index item in the list must be a couple with a start and
+          // an end value : [0,3], [5-5], [8-10], ...
+          // ----- Check the format of each item
+          $v_sort_flag  = false;
+          $v_sort_value = 0;
+          for ($j = 0; $j < sizeof($v_work_list); $j++) {
+            // ----- Explode the item
+            $v_item_list      = explode("-", $v_work_list[$j]);
+            $v_size_item_list = sizeof($v_item_list);
+
+            // ----- TBC : Here we might check that each item is a
+            // real integer ...
+
+            // ----- Look for single value
+            if ($v_size_item_list == 1) {
+              // ----- Set the option value
+              $v_result_list[$p_options_list[$i]][$j]['start'] = $v_item_list[0];
+              $v_result_list[$p_options_list[$i]][$j]['end']   = $v_item_list[0];
+            } elseif ($v_size_item_list == 2) {
+              // ----- Set the option value
+              $v_result_list[$p_options_list[$i]][$j]['start'] = $v_item_list[0];
+              $v_result_list[$p_options_list[$i]][$j]['end']   = $v_item_list[1];
+            } else {
+              // ----- Error log
+              PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Too many values in index range for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+              // ----- Return
+              return PclZip::errorCode();
+            }
+
+            // ----- Look for list sort
+            if ($v_result_list[$p_options_list[$i]][$j]['start'] < $v_sort_value) {
+              $v_sort_flag = true;
+
+              // ----- TBC : An automatic sort should be writen ...
+              // ----- Error log
+              PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Invalid order of index range for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+              // ----- Return
+              return PclZip::errorCode();
+            }
+            $v_sort_value = $v_result_list[$p_options_list[$i]][$j]['start'];
+          }
+
+          // ----- Sort the items
+          if ($v_sort_flag) {
+            // TBC : To Be Completed
+          }
+
+          // ----- Next option
+          $i++;
+          break;
+
+        // ----- Look for options that request no value
+        case PCLZIP_OPT_REMOVE_ALL_PATH:
+        case PCLZIP_OPT_EXTRACT_AS_STRING:
+        case PCLZIP_OPT_NO_COMPRESSION:
+        case PCLZIP_OPT_EXTRACT_IN_OUTPUT:
+        case PCLZIP_OPT_REPLACE_NEWER:
+        case PCLZIP_OPT_STOP_ON_ERROR:
+          $v_result_list[$p_options_list[$i]] = true;
+          break;
+
+        // ----- Look for options that request an octal value
+        case PCLZIP_OPT_SET_CHMOD:
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value
+          $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+          $i++;
+          break;
+
+        // ----- Look for options that request a call-back
+        case PCLZIP_CB_PRE_EXTRACT:
+        case PCLZIP_CB_POST_EXTRACT:
+        case PCLZIP_CB_PRE_ADD:
+        case PCLZIP_CB_POST_ADD:
+          /* for futur use
+                    case PCLZIP_CB_PRE_DELETE :
+                    case PCLZIP_CB_POST_DELETE :
+                    case PCLZIP_CB_PRE_LIST :
+                    case PCLZIP_CB_POST_LIST :
+                    */
+          // ----- Check the number of parameters
+          if (($i + 1) >= $p_size) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Get the value
+          $v_function_name = $p_options_list[$i + 1];
+
+          // ----- Check that the value is a valid existing function
+          if ((is_string($v_function_name) && !function_exists($v_function_name)) && !is_callable($v_function_name)) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Function '" . $v_function_name . "()' is not an existing function for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+
+          // ----- Set the attribute
+          $v_result_list[$p_options_list[$i]] = $v_function_name;
+          $i++;
+          break;
+
+        default:
+          // ----- Error log
+          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Unknown parameter '" . $p_options_list[$i] . "'");
+
+          // ----- Return
+          return PclZip::errorCode();
+      }
+
+      // ----- Next options
+      $i++;
+    }
+
+    // ----- Look for mandatory options
+    if ($v_requested_options !== false) {
+      for ($key = reset($v_requested_options); $key = key($v_requested_options); $key = next($v_requested_options)) {
+        // ----- Look for mandatory option
+        if ($v_requested_options[$key] == 'mandatory') {
+          // ----- Look if present
+          if (!isset($v_result_list[$key])) {
+            // ----- Error log
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter " . PclZipUtilOptionText($key) . "(" . $key . ")");
+
+            // ----- Return
+            return PclZip::errorCode();
+          }
+        }
+      }
+    }
+
+    // ----- Look for default values
+    if (!isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) {
+
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privOptionDefaultThreshold()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privOptionDefaultThreshold(&$p_options)
+  {
+    $v_result = 1;
+
+    if (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) || isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) {
+      return $v_result;
+    }
+
+    // ----- Get 'memory_limit' configuration value
+    $v_memory_limit = trim(ini_get('memory_limit'));
+    $last           = strtolower(substr($v_memory_limit, -1));
+    $v_memory_limit = intval($v_memory_limit);
+
+    if ($last == 'g') {
+      //$v_memory_limit = $v_memory_limit*1024*1024*1024;
+      $v_memory_limit = $v_memory_limit * 1073741824;
+    }
+    if ($last == 'm') {
+      //$v_memory_limit = $v_memory_limit*1024*1024;
+      $v_memory_limit = $v_memory_limit * 1048576;
+    }
+    if ($last == 'k') {
+      $v_memory_limit = $v_memory_limit * 1024;
+    }
+
+    $p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] = floor($v_memory_limit * PCLZIP_TEMPORARY_FILE_RATIO);
+
+    // ----- Sanity check : No threshold if value lower than 1M
+    if ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] < 1048576) {
+      unset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]);
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privFileDescrParseAtt()
+  // Description :
+  // Parameters :
+  // Return Values :
+  //   1 on success.
+  //   0 on failure.
+  // --------------------------------------------------------------------------------
+  public function privFileDescrParseAtt(&$p_file_list, &$p_filedescr, $v_options, $v_requested_options = false)
+  {
+    $v_result = 1;
+
+    // ----- For each file in the list check the attributes
+    foreach ($p_file_list as $v_key => $v_value) {
+
+      // ----- Check if the option is supported
+      if (!isset($v_requested_options[$v_key])) {
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file attribute '" . $v_key . "' for this file");
+
+        // ----- Return
+        return PclZip::errorCode();
+      }
+
+      // ----- Look for attribute
+      switch ($v_key) {
+        case PCLZIP_ATT_FILE_NAME:
+          if (!is_string($v_value)) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+            return PclZip::errorCode();
+          }
+
+          $p_filedescr['filename'] = PclZipUtilPathReduction($v_value);
+
+          if ($p_filedescr['filename'] == '') {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+            return PclZip::errorCode();
+          }
+
+          break;
+
+        case PCLZIP_ATT_FILE_NEW_SHORT_NAME:
+          if (!is_string($v_value)) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+            return PclZip::errorCode();
+          }
+
+          $p_filedescr['new_short_name'] = PclZipUtilPathReduction($v_value);
+
+          if ($p_filedescr['new_short_name'] == '') {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty short filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+            return PclZip::errorCode();
+          }
+          break;
+
+        case PCLZIP_ATT_FILE_NEW_FULL_NAME:
+          if (!is_string($v_value)) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+            return PclZip::errorCode();
+          }
+
+          $p_filedescr['new_full_name'] = PclZipUtilPathReduction($v_value);
+
+          if ($p_filedescr['new_full_name'] == '') {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty full filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+            return PclZip::errorCode();
+          }
+          break;
+
+        // ----- Look for options that takes a string
+        case PCLZIP_ATT_FILE_COMMENT:
+          if (!is_string($v_value)) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+            return PclZip::errorCode();
+          }
+
+          $p_filedescr['comment'] = $v_value;
+          break;
+
+        case PCLZIP_ATT_FILE_MTIME:
+          if (!is_integer($v_value)) {
+            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". Integer expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+            return PclZip::errorCode();
+          }
+
+          $p_filedescr['mtime'] = $v_value;
+          break;
+
+        case PCLZIP_ATT_FILE_CONTENT:
+          $p_filedescr['content'] = $v_value;
+          break;
+
+        default:
+          // ----- Error log
+          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Unknown parameter '" . $v_key . "'");
+
+          // ----- Return
+          return PclZip::errorCode();
+      }
+
+      // ----- Look for mandatory options
+      if ($v_requested_options !== false) {
+        for ($key = reset($v_requested_options); $key = key($v_requested_options); $key = next($v_requested_options)) {
+          // ----- Look for mandatory option
+          if ($v_requested_options[$key] == 'mandatory') {
+            // ----- Look if present
+            if (!isset($p_file_list[$key])) {
+              PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter " . PclZipUtilOptionText($key) . "(" . $key . ")");
+
+              return PclZip::errorCode();
+            }
+          }
+        }
+      }
+
+      // end foreach
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privFileDescrExpand()
+  // Description :
+  //   This method look for each item of the list to see if its a file, a folder
+  //   or a string to be added as file. For any other type of files (link, other)
+  //   just ignore the item.
+  //   Then prepare the information that will be stored for that file.
+  //   When its a folder, expand the folder with all the files that are in that
+  //   folder (recursively).
+  // Parameters :
+  // Return Values :
+  //   1 on success.
+  //   0 on failure.
+  // --------------------------------------------------------------------------------
+  public function privFileDescrExpand(&$p_filedescr_list, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Create a result list
+    $v_result_list = array();
+
+    // ----- Look each entry
+    for ($i = 0; $i < sizeof($p_filedescr_list); $i++) {
+
+      // ----- Get filedescr
+      $v_descr = $p_filedescr_list[$i];
+
+      // ----- Reduce the filename
+      $v_descr['filename'] = PclZipUtilTranslateWinPath($v_descr['filename'], false);
+      $v_descr['filename'] = PclZipUtilPathReduction($v_descr['filename']);
+
+      // ----- Look for real file or folder
+      if (file_exists($v_descr['filename'])) {
+        if (@is_file($v_descr['filename'])) {
+          $v_descr['type'] = 'file';
+        } elseif (@is_dir($v_descr['filename'])) {
+          $v_descr['type'] = 'folder';
+        } elseif (@is_link($v_descr['filename'])) {
+          // skip
+          continue;
+        } else {
+          // skip
+          continue;
+        }
+
+        // ----- Look for string added as file
+      } elseif (isset($v_descr['content'])) {
+        $v_descr['type'] = 'virtual_file';
+
+        // ----- Missing file
+      } else {
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "File '" . $v_descr['filename'] . "' does not exist");
+
+        // ----- Return
+        return PclZip::errorCode();
+      }
+
+      // ----- Calculate the stored filename
+      $this->privCalculateStoredFilename($v_descr, $p_options);
+
+      // ----- Add the descriptor in result list
+      $v_result_list[sizeof($v_result_list)] = $v_descr;
+
+      // ----- Look for folder
+      if ($v_descr['type'] == 'folder') {
+        // ----- List of items in folder
+        $v_dirlist_descr = array();
+        $v_dirlist_nb    = 0;
+        if ($v_folder_handler = @opendir($v_descr['filename'])) {
+          while (($v_item_handler = @readdir($v_folder_handler)) !== false) {
+
+            // ----- Skip '.' and '..'
+            if (($v_item_handler == '.') || ($v_item_handler == '..')) {
+              continue;
+            }
+
+            // ----- Compose the full filename
+            $v_dirlist_descr[$v_dirlist_nb]['filename'] = $v_descr['filename'] . '/' . $v_item_handler;
+
+            // ----- Look for different stored filename
+            // Because the name of the folder was changed, the name of the
+            // files/sub-folders also change
+            if (($v_descr['stored_filename'] != $v_descr['filename']) && (!isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))) {
+              if ($v_descr['stored_filename'] != '') {
+                $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_descr['stored_filename'] . '/' . $v_item_handler;
+              } else {
+                $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_item_handler;
+              }
+            }
+
+            $v_dirlist_nb++;
+          }
+
+          @closedir($v_folder_handler);
+        } else {
+          // TBC : unable to open folder in read mode
+        }
+
+        // ----- Expand each element of the list
+        if ($v_dirlist_nb != 0) {
+          // ----- Expand
+          if (($v_result = $this->privFileDescrExpand($v_dirlist_descr, $p_options)) != 1) {
+            return $v_result;
+          }
+
+          // ----- Concat the resulting list
+          $v_result_list = array_merge($v_result_list, $v_dirlist_descr);
+        } else {
+        }
+
+        // ----- Free local array
+        unset($v_dirlist_descr);
+      }
+    }
+
+    // ----- Get the result list
+    $p_filedescr_list = $v_result_list;
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privCreate()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privCreate($p_filedescr_list, &$p_result_list, &$p_options)
+  {
+    $v_result      = 1;
+    $v_list_detail = array();
+
+    // ----- Magic quotes trick
+    $this->privDisableMagicQuotes();
+
+    // ----- Open the file in write mode
+    if (($v_result = $this->privOpenFd('wb')) != 1) {
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Add the list of files
+    $v_result = $this->privAddList($p_filedescr_list, $p_result_list, $p_options);
+
+    // ----- Close
+    $this->privCloseFd();
+
+    // ----- Magic quotes trick
+    $this->privSwapBackMagicQuotes();
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privAdd()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privAdd($p_filedescr_list, &$p_result_list, &$p_options)
+  {
+    $v_result      = 1;
+    $v_list_detail = array();
+
+    // ----- Look if the archive exists or is empty
+    if ((!is_file($this->zipname)) || (filesize($this->zipname) == 0)) {
+
+      // ----- Do a create
+      $v_result = $this->privCreate($p_filedescr_list, $p_result_list, $p_options);
+
+      // ----- Return
+      return $v_result;
+    }
+    // ----- Magic quotes trick
+    $this->privDisableMagicQuotes();
+
+    // ----- Open the zip file
+    if (($v_result = $this->privOpenFd('rb')) != 1) {
+      // ----- Magic quotes trick
+      $this->privSwapBackMagicQuotes();
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Read the central directory informations
+    $v_central_dir = array();
+    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+      $this->privCloseFd();
+      $this->privSwapBackMagicQuotes();
+
+      return $v_result;
+    }
+
+    // ----- Go to beginning of File
+    @rewind($this->zip_fd);
+
+    // ----- Creates a temporay file
+    $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.tmp';
+
+    // ----- Open the temporary file in write mode
+    if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0) {
+      $this->privCloseFd();
+      $this->privSwapBackMagicQuotes();
+
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_zip_temp_name . '\' in binary write mode');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Copy the files from the archive to the temporary file
+    // TBC : Here I should better append the file and go back to erase the central dir
+    $v_size = $v_central_dir['offset'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = fread($this->zip_fd, $v_read_size);
+      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Swap the file descriptor
+    // Here is a trick : I swap the temporary fd with the zip fd, in order to use
+    // the following methods on the temporary fil and not the real archive
+    $v_swap        = $this->zip_fd;
+    $this->zip_fd  = $v_zip_temp_fd;
+    $v_zip_temp_fd = $v_swap;
+
+    // ----- Add the files
+    $v_header_list = array();
+    if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1) {
+      fclose($v_zip_temp_fd);
+      $this->privCloseFd();
+      @unlink($v_zip_temp_name);
+      $this->privSwapBackMagicQuotes();
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Store the offset of the central dir
+    $v_offset = @ftell($this->zip_fd);
+
+    // ----- Copy the block of file headers from the old archive
+    $v_size = $v_central_dir['size'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @fread($v_zip_temp_fd, $v_read_size);
+      @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Create the Central Dir files header
+    for ($i = 0, $v_count = 0; $i < sizeof($v_header_list); $i++) {
+      // ----- Create the file header
+      if ($v_header_list[$i]['status'] == 'ok') {
+        if (($v_result = $this->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+          fclose($v_zip_temp_fd);
+          $this->privCloseFd();
+          @unlink($v_zip_temp_name);
+          $this->privSwapBackMagicQuotes();
+
+          // ----- Return
+          return $v_result;
+        }
+        $v_count++;
+      }
+
+      // ----- Transform the header to a 'usable' info
+      $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+    }
+
+    // ----- Zip file comment
+    $v_comment = $v_central_dir['comment'];
+    if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+      $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+    }
+    if (isset($p_options[PCLZIP_OPT_ADD_COMMENT])) {
+      $v_comment = $v_comment . $p_options[PCLZIP_OPT_ADD_COMMENT];
+    }
+    if (isset($p_options[PCLZIP_OPT_PREPEND_COMMENT])) {
+      $v_comment = $p_options[PCLZIP_OPT_PREPEND_COMMENT] . $v_comment;
+    }
+
+    // ----- Calculate the size of the central header
+    $v_size = @ftell($this->zip_fd) - $v_offset;
+
+    // ----- Create the central dir footer
+    if (($v_result = $this->privWriteCentralHeader($v_count + $v_central_dir['entries'], $v_size, $v_offset, $v_comment)) != 1) {
+      // ----- Reset the file list
+      unset($v_header_list);
+      $this->privSwapBackMagicQuotes();
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Swap back the file descriptor
+    $v_swap        = $this->zip_fd;
+    $this->zip_fd  = $v_zip_temp_fd;
+    $v_zip_temp_fd = $v_swap;
+
+    // ----- Close
+    $this->privCloseFd();
+
+    // ----- Close the temporary file
+    @fclose($v_zip_temp_fd);
+
+    // ----- Magic quotes trick
+    $this->privSwapBackMagicQuotes();
+
+    // ----- Delete the zip file
+    // TBC : I should test the result ...
+    @unlink($this->zipname);
+
+    // ----- Rename the temporary file
+    // TBC : I should test the result ...
+    //@rename($v_zip_temp_name, $this->zipname);
+    PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privOpenFd()
+  // Description :
+  // Parameters :
+  // --------------------------------------------------------------------------------
+  public function privOpenFd($p_mode)
+  {
+    $v_result = 1;
+
+    // ----- Look if already open
+    if ($this->zip_fd != 0) {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Zip file \'' . $this->zipname . '\' already open');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Open the zip file
+    if (($this->zip_fd = @fopen($this->zipname, $p_mode)) == 0) {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in ' . $p_mode . ' mode');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privCloseFd()
+  // Description :
+  // Parameters :
+  // --------------------------------------------------------------------------------
+  public function privCloseFd()
+  {
+    $v_result = 1;
+
+    if ($this->zip_fd != 0) {
+      @fclose($this->zip_fd);
+    }
+    $this->zip_fd = 0;
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privAddList()
+  // Description :
+  //   $p_add_dir and $p_remove_dir will give the ability to memorize a path which is
+  //   different from the real path of the file. This is usefull if you want to have PclTar
+  //   running in any directory, and memorize relative path from an other directory.
+  // Parameters :
+  //   $p_list : An array containing the file or directory names to add in the tar
+  //   $p_result_list : list of added files with their properties (specially the status field)
+  //   $p_add_dir : Path to add in the filename path archived
+  //   $p_remove_dir : Path to remove in the filename path archived
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  //  function privAddList($p_list, &$p_result_list, $p_add_dir, $p_remove_dir, $p_remove_all_dir, &$p_options)
+  public function privAddList($p_filedescr_list, &$p_result_list, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Add the files
+    $v_header_list = array();
+    if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1) {
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Store the offset of the central dir
+    $v_offset = @ftell($this->zip_fd);
+
+    // ----- Create the Central Dir files header
+    for ($i = 0, $v_count = 0; $i < sizeof($v_header_list); $i++) {
+      // ----- Create the file header
+      if ($v_header_list[$i]['status'] == 'ok') {
+        if (($v_result = $this->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+          // ----- Return
+          return $v_result;
+        }
+        $v_count++;
+      }
+
+      // ----- Transform the header to a 'usable' info
+      $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+    }
+
+    // ----- Zip file comment
+    $v_comment = '';
+    if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+      $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+    }
+
+    // ----- Calculate the size of the central header
+    $v_size = @ftell($this->zip_fd) - $v_offset;
+
+    // ----- Create the central dir footer
+    if (($v_result = $this->privWriteCentralHeader($v_count, $v_size, $v_offset, $v_comment)) != 1) {
+      // ----- Reset the file list
+      unset($v_header_list);
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privAddFileList()
+  // Description :
+  // Parameters :
+  //   $p_filedescr_list : An array containing the file description
+  //                      or directory names to add in the zip
+  //   $p_result_list : list of added files with their properties (specially the status field)
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privAddFileList($p_filedescr_list, &$p_result_list, &$p_options)
+  {
+    $v_result = 1;
+    $v_header = array();
+
+    // ----- Recuperate the current number of elt in list
+    $v_nb = sizeof($p_result_list);
+
+    // ----- Loop on the files
+    for ($j = 0; ($j < sizeof($p_filedescr_list)) && ($v_result == 1); $j++) {
+      // ----- Format the filename
+      $p_filedescr_list[$j]['filename'] = PclZipUtilTranslateWinPath($p_filedescr_list[$j]['filename'], false);
+
+      // ----- Skip empty file names
+      // TBC : Can this be possible ? not checked in DescrParseAtt ?
+      if ($p_filedescr_list[$j]['filename'] == "") {
+        continue;
+      }
+
+      // ----- Check the filename
+      if (($p_filedescr_list[$j]['type'] != 'virtual_file') && (!file_exists($p_filedescr_list[$j]['filename']))) {
+        PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "File '" . $p_filedescr_list[$j]['filename'] . "' does not exist");
+
+        return PclZip::errorCode();
+      }
+
+      // ----- Look if it is a file or a dir with no all path remove option
+      // or a dir with all its path removed
+      //      if (   (is_file($p_filedescr_list[$j]['filename']))
+      //          || (   is_dir($p_filedescr_list[$j]['filename'])
+      if (($p_filedescr_list[$j]['type'] == 'file') || ($p_filedescr_list[$j]['type'] == 'virtual_file') || (($p_filedescr_list[$j]['type'] == 'folder') && (!isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH]) || !$p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))) {
+
+        // ----- Add the file
+        $v_result = $this->privAddFile($p_filedescr_list[$j], $v_header, $p_options);
+        if ($v_result != 1) {
+          return $v_result;
+        }
+
+        // ----- Store the file infos
+        $p_result_list[$v_nb++] = $v_header;
+      }
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privAddFile()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privAddFile($p_filedescr, &$p_header, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Working variable
+    $p_filename = $p_filedescr['filename'];
+
+    // TBC : Already done in the fileAtt check ... ?
+    if ($p_filename == "") {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file list parameter (invalid or empty list)");
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Look for a stored different filename
+    /* TBC : Removed
+        if (isset($p_filedescr['stored_filename'])) {
+        $v_stored_filename = $p_filedescr['stored_filename'];
+        } else {
+        $v_stored_filename = $p_filedescr['stored_filename'];
+        }
+        */
+
+    // ----- Set the file properties
+    clearstatcache();
+    $p_header['version']           = 20;
+    $p_header['version_extracted'] = 10;
+    $p_header['flag']              = 0;
+    $p_header['compression']       = 0;
+    $p_header['crc']               = 0;
+    $p_header['compressed_size']   = 0;
+    $p_header['filename_len']      = strlen($p_filename);
+    $p_header['extra_len']         = 0;
+    $p_header['disk']              = 0;
+    $p_header['internal']          = 0;
+    $p_header['offset']            = 0;
+    $p_header['filename']          = $p_filename;
+    // TBC : Removed    $p_header['stored_filename'] = $v_stored_filename;
+    $p_header['stored_filename']   = $p_filedescr['stored_filename'];
+    $p_header['extra']             = '';
+    $p_header['status']            = 'ok';
+    $p_header['index']             = -1;
+
+    // ----- Look for regular file
+    if ($p_filedescr['type'] == 'file') {
+      $p_header['external'] = 0x00000000;
+      $p_header['size']     = filesize($p_filename);
+
+      // ----- Look for regular folder
+    } elseif ($p_filedescr['type'] == 'folder') {
+      $p_header['external'] = 0x00000010;
+      $p_header['mtime']    = filemtime($p_filename);
+      $p_header['size']     = filesize($p_filename);
+
+      // ----- Look for virtual file
+    } elseif ($p_filedescr['type'] == 'virtual_file') {
+      $p_header['external'] = 0x00000000;
+      $p_header['size']     = strlen($p_filedescr['content']);
+    }
+
+    // ----- Look for filetime
+    if (isset($p_filedescr['mtime'])) {
+      $p_header['mtime'] = $p_filedescr['mtime'];
+    } elseif ($p_filedescr['type'] == 'virtual_file') {
+      $p_header['mtime'] = time();
+    } else {
+      $p_header['mtime'] = filemtime($p_filename);
+    }
+
+    // ------ Look for file comment
+    if (isset($p_filedescr['comment'])) {
+      $p_header['comment_len'] = strlen($p_filedescr['comment']);
+      $p_header['comment']     = $p_filedescr['comment'];
+    } else {
+      $p_header['comment_len'] = 0;
+      $p_header['comment']     = '';
+    }
+
+    // ----- Look for pre-add callback
+    if (isset($p_options[PCLZIP_CB_PRE_ADD])) {
+
+      // ----- Generate a local information
+      $v_local_header = array();
+      $this->privConvertHeader2FileInfo($p_header, $v_local_header);
+
+      // ----- Call the callback
+      // Here I do not use call_user_func() because I need to send a reference to the
+      // header.
+      //      eval('$v_result = '.$p_options[PCLZIP_CB_PRE_ADD].'(PCLZIP_CB_PRE_ADD, $v_local_header);');
+      $v_result = $p_options[PCLZIP_CB_PRE_ADD](PCLZIP_CB_PRE_ADD, $v_local_header);
+      if ($v_result == 0) {
+        // ----- Change the file status
+        $p_header['status'] = "skipped";
+        $v_result           = 1;
+      }
+
+      // ----- Update the informations
+      // Only some fields can be modified
+      if ($p_header['stored_filename'] != $v_local_header['stored_filename']) {
+        $p_header['stored_filename'] = PclZipUtilPathReduction($v_local_header['stored_filename']);
+      }
+    }
+
+    // ----- Look for empty stored filename
+    if ($p_header['stored_filename'] == "") {
+      $p_header['status'] = "filtered";
+    }
+
+    // ----- Check the path length
+    if (strlen($p_header['stored_filename']) > 0xFF) {
+      $p_header['status'] = 'filename_too_long';
+    }
+
+    // ----- Look if no error, or file not skipped
+    if ($p_header['status'] == 'ok') {
+
+      // ----- Look for a file
+      if ($p_filedescr['type'] == 'file') {
+        // ----- Look for using temporary file to zip
+        if ((!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON]) || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_header['size'])))) {
+          $v_result = $this->privAddFileUsingTempFile($p_filedescr, $p_header, $p_options);
+          if ($v_result < PCLZIP_ERR_NO_ERROR) {
+            return $v_result;
+          }
+
+          // ----- Use "in memory" zip algo
+        } else {
+
+          // ----- Open the source file
+          if (($v_file = @fopen($p_filename, "rb")) == 0) {
+            PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode");
+
+            return PclZip::errorCode();
+          }
+
+          // ----- Read the file content
+          $v_content = @fread($v_file, $p_header['size']);
+
+          // ----- Close the file
+          @fclose($v_file);
+
+          // ----- Calculate the CRC
+          $p_header['crc'] = @crc32($v_content);
+
+          // ----- Look for no compression
+          if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) {
+            // ----- Set header parameters
+            $p_header['compressed_size'] = $p_header['size'];
+            $p_header['compression']     = 0;
+
+            // ----- Look for normal compression
+          } else {
+            // ----- Compress the content
+            $v_content = @gzdeflate($v_content);
+
+            // ----- Set header parameters
+            $p_header['compressed_size'] = strlen($v_content);
+            $p_header['compression']     = 8;
+          }
+
+          // ----- Call the header generation
+          if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+            @fclose($v_file);
+
+            return $v_result;
+          }
+
+          // ----- Write the compressed (or not) content
+          @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']);
+
+        }
+
+        // ----- Look for a virtual file (a file from string)
+      } elseif ($p_filedescr['type'] == 'virtual_file') {
+
+        $v_content = $p_filedescr['content'];
+
+        // ----- Calculate the CRC
+        $p_header['crc'] = @crc32($v_content);
+
+        // ----- Look for no compression
+        if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) {
+          // ----- Set header parameters
+          $p_header['compressed_size'] = $p_header['size'];
+          $p_header['compression']     = 0;
+
+          // ----- Look for normal compression
+        } else {
+          // ----- Compress the content
+          $v_content = @gzdeflate($v_content);
+
+          // ----- Set header parameters
+          $p_header['compressed_size'] = strlen($v_content);
+          $p_header['compression']     = 8;
+        }
+
+        // ----- Call the header generation
+        if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+          @fclose($v_file);
+
+          return $v_result;
+        }
+
+        // ----- Write the compressed (or not) content
+        @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']);
+
+        // ----- Look for a directory
+      } elseif ($p_filedescr['type'] == 'folder') {
+        // ----- Look for directory last '/'
+        if (@substr($p_header['stored_filename'], -1) != '/') {
+          $p_header['stored_filename'] .= '/';
+        }
+
+        // ----- Set the file properties
+        $p_header['size']     = 0;
+        //$p_header['external'] = 0x41FF0010;   // Value for a folder : to be checked
+        $p_header['external'] = 0x00000010; // Value for a folder : to be checked
+
+        // ----- Call the header generation
+        if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+          return $v_result;
+        }
+      }
+    }
+
+    // ----- Look for post-add callback
+    if (isset($p_options[PCLZIP_CB_POST_ADD])) {
+
+      // ----- Generate a local information
+      $v_local_header = array();
+      $this->privConvertHeader2FileInfo($p_header, $v_local_header);
+
+      // ----- Call the callback
+      // Here I do not use call_user_func() because I need to send a reference to the
+      // header.
+      //      eval('$v_result = '.$p_options[PCLZIP_CB_POST_ADD].'(PCLZIP_CB_POST_ADD, $v_local_header);');
+      $v_result = $p_options[PCLZIP_CB_POST_ADD](PCLZIP_CB_POST_ADD, $v_local_header);
+      if ($v_result == 0) {
+        // ----- Ignored
+        $v_result = 1;
+      }
+
+      // ----- Update the informations
+      // Nothing can be modified
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privAddFileUsingTempFile()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privAddFileUsingTempFile($p_filedescr, &$p_header, &$p_options)
+  {
+    $v_result = PCLZIP_ERR_NO_ERROR;
+
+    // ----- Working variable
+    $p_filename = $p_filedescr['filename'];
+
+    // ----- Open the source file
+    if (($v_file = @fopen($p_filename, "rb")) == 0) {
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode");
+
+      return PclZip::errorCode();
+    }
+
+    // ----- Creates a compressed temporary file
+    $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.gz';
+    if (($v_file_compressed = @gzopen($v_gzip_temp_name, "wb")) == 0) {
+      fclose($v_file);
+      PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary write mode');
+
+      return PclZip::errorCode();
+    }
+
+    // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+    $v_size = filesize($p_filename);
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @fread($v_file, $v_read_size);
+      //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+      @gzputs($v_file_compressed, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Close the file
+    @fclose($v_file);
+    @gzclose($v_file_compressed);
+
+    // ----- Check the minimum file size
+    if (filesize($v_gzip_temp_name) < 18) {
+      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'gzip temporary file \'' . $v_gzip_temp_name . '\' has invalid filesize - should be minimum 18 bytes');
+
+      return PclZip::errorCode();
+    }
+
+    // ----- Extract the compressed attributes
+    if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) {
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+      return PclZip::errorCode();
+    }
+
+    // ----- Read the gzip file header
+    $v_binary_data = @fread($v_file_compressed, 10);
+    $v_data_header = unpack('a1id1/a1id2/a1cm/a1flag/Vmtime/a1xfl/a1os', $v_binary_data);
+
+    // ----- Check some parameters
+    $v_data_header['os'] = bin2hex($v_data_header['os']);
+
+    // ----- Read the gzip file footer
+    @fseek($v_file_compressed, filesize($v_gzip_temp_name) - 8);
+    $v_binary_data = @fread($v_file_compressed, 8);
+    $v_data_footer = unpack('Vcrc/Vcompressed_size', $v_binary_data);
+
+    // ----- Set the attributes
+    $p_header['compression']     = ord($v_data_header['cm']);
+    //$p_header['mtime'] = $v_data_header['mtime'];
+    $p_header['crc']             = $v_data_footer['crc'];
+    $p_header['compressed_size'] = filesize($v_gzip_temp_name) - 18;
+
+    // ----- Close the file
+    @fclose($v_file_compressed);
+
+    // ----- Call the header generation
+    if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+      return $v_result;
+    }
+
+    // ----- Add the compressed data
+    if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) {
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+      return PclZip::errorCode();
+    }
+
+    // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+    fseek($v_file_compressed, 10);
+    $v_size = $p_header['compressed_size'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @fread($v_file_compressed, $v_read_size);
+      //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+      @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Close the file
+    @fclose($v_file_compressed);
+
+    // ----- Unlink the temporary file
+    @unlink($v_gzip_temp_name);
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privCalculateStoredFilename()
+  // Description :
+  //   Based on file descriptor properties and global options, this method
+  //   calculate the filename that will be stored in the archive.
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privCalculateStoredFilename(&$p_filedescr, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Working variables
+    $p_filename = $p_filedescr['filename'];
+    if (isset($p_options[PCLZIP_OPT_ADD_PATH])) {
+      $p_add_dir = $p_options[PCLZIP_OPT_ADD_PATH];
+    } else {
+      $p_add_dir = '';
+    }
+    if (isset($p_options[PCLZIP_OPT_REMOVE_PATH])) {
+      $p_remove_dir = $p_options[PCLZIP_OPT_REMOVE_PATH];
+    } else {
+      $p_remove_dir = '';
+    }
+    if (isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+      $p_remove_all_dir = $p_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+    } else {
+      $p_remove_all_dir = 0;
+    }
+
+    // ----- Look for full name change
+    if (isset($p_filedescr['new_full_name'])) {
+      // ----- Remove drive letter if any
+      $v_stored_filename = PclZipUtilTranslateWinPath($p_filedescr['new_full_name']);
+
+      // ----- Look for path and/or short name change
+    } else {
+
+      // ----- Look for short name change
+      // Its when we cahnge just the filename but not the path
+      if (isset($p_filedescr['new_short_name'])) {
+        $v_path_info = pathinfo($p_filename);
+        $v_dir       = '';
+        if ($v_path_info['dirname'] != '') {
+          $v_dir = $v_path_info['dirname'] . '/';
+        }
+        $v_stored_filename = $v_dir . $p_filedescr['new_short_name'];
+      } else {
+        // ----- Calculate the stored filename
+        $v_stored_filename = $p_filename;
+      }
+
+      // ----- Look for all path to remove
+      if ($p_remove_all_dir) {
+        $v_stored_filename = basename($p_filename);
+
+        // ----- Look for partial path remove
+      } elseif ($p_remove_dir != "") {
+        if (substr($p_remove_dir, -1) != '/') {
+          $p_remove_dir .= "/";
+        }
+
+        if ((substr($p_filename, 0, 2) == "./") || (substr($p_remove_dir, 0, 2) == "./")) {
+
+          if ((substr($p_filename, 0, 2) == "./") && (substr($p_remove_dir, 0, 2) != "./")) {
+            $p_remove_dir = "./" . $p_remove_dir;
+          }
+          if ((substr($p_filename, 0, 2) != "./") && (substr($p_remove_dir, 0, 2) == "./")) {
+            $p_remove_dir = substr($p_remove_dir, 2);
+          }
+        }
+
+        $v_compare = PclZipUtilPathInclusion($p_remove_dir, $v_stored_filename);
+        if ($v_compare > 0) {
+          if ($v_compare == 2) {
+            $v_stored_filename = "";
+          } else {
+            $v_stored_filename = substr($v_stored_filename, strlen($p_remove_dir));
+          }
+        }
+      }
+
+      // ----- Remove drive letter if any
+      $v_stored_filename = PclZipUtilTranslateWinPath($v_stored_filename);
+
+      // ----- Look for path to add
+      if ($p_add_dir != "") {
+        if (substr($p_add_dir, -1) == "/") {
+          $v_stored_filename = $p_add_dir . $v_stored_filename;
+        } else {
+          $v_stored_filename = $p_add_dir . "/" . $v_stored_filename;
+        }
+      }
+    }
+
+    // ----- Filename (reduce the path of stored name)
+    $v_stored_filename              = PclZipUtilPathReduction($v_stored_filename);
+    $p_filedescr['stored_filename'] = $v_stored_filename;
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privWriteFileHeader()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privWriteFileHeader(&$p_header)
+  {
+    $v_result = 1;
+
+    // ----- Store the offset position of the file
+    $p_header['offset'] = ftell($this->zip_fd);
+
+    // ----- Transform UNIX mtime to DOS format mdate/mtime
+    $v_date  = getdate($p_header['mtime']);
+    $v_mtime = ($v_date['hours'] << 11) + ($v_date['minutes'] << 5) + $v_date['seconds'] / 2;
+    $v_mdate = (($v_date['year'] - 1980) << 9) + ($v_date['mon'] << 5) + $v_date['mday'];
+
+    // ----- Packed data
+    $v_binary_data = pack("VvvvvvVVVvv", 0x04034b50, $p_header['version_extracted'], $p_header['flag'], $p_header['compression'], $v_mtime, $v_mdate, $p_header['crc'], $p_header['compressed_size'], $p_header['size'], strlen($p_header['stored_filename']), $p_header['extra_len']);
+
+    // ----- Write the first 148 bytes of the header in the archive
+    fputs($this->zip_fd, $v_binary_data, 30);
+
+    // ----- Write the variable fields
+    if (strlen($p_header['stored_filename']) != 0) {
+      fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename']));
+    }
+    if ($p_header['extra_len'] != 0) {
+      fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']);
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privWriteCentralFileHeader()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privWriteCentralFileHeader(&$p_header)
+  {
+    $v_result = 1;
+
+    // TBC
+    //for (reset($p_header); $key = key($p_header); next($p_header)) {
+    //}
+
+    // ----- Transform UNIX mtime to DOS format mdate/mtime
+    $v_date  = getdate($p_header['mtime']);
+    $v_mtime = ($v_date['hours'] << 11) + ($v_date['minutes'] << 5) + $v_date['seconds'] / 2;
+    $v_mdate = (($v_date['year'] - 1980) << 9) + ($v_date['mon'] << 5) + $v_date['mday'];
+
+    // ----- Packed data
+    $v_binary_data = pack("VvvvvvvVVVvvvvvVV", 0x02014b50, $p_header['version'], $p_header['version_extracted'], $p_header['flag'], $p_header['compression'], $v_mtime, $v_mdate, $p_header['crc'], $p_header['compressed_size'], $p_header['size'], strlen($p_header['stored_filename']), $p_header['extra_len'], $p_header['comment_len'], $p_header['disk'], $p_header['internal'], $p_header['external'], $p_header['offset']);
+
+    // ----- Write the 42 bytes of the header in the zip file
+    fputs($this->zip_fd, $v_binary_data, 46);
+
+    // ----- Write the variable fields
+    if (strlen($p_header['stored_filename']) != 0) {
+      fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename']));
+    }
+    if ($p_header['extra_len'] != 0) {
+      fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']);
+    }
+    if ($p_header['comment_len'] != 0) {
+      fputs($this->zip_fd, $p_header['comment'], $p_header['comment_len']);
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privWriteCentralHeader()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privWriteCentralHeader($p_nb_entries, $p_size, $p_offset, $p_comment)
+  {
+    $v_result = 1;
+
+    // ----- Packed data
+    $v_binary_data = pack("VvvvvVVv", 0x06054b50, 0, 0, $p_nb_entries, $p_nb_entries, $p_size, $p_offset, strlen($p_comment));
+
+    // ----- Write the 22 bytes of the header in the zip file
+    fputs($this->zip_fd, $v_binary_data, 22);
+
+    // ----- Write the variable fields
+    if (strlen($p_comment) != 0) {
+      fputs($this->zip_fd, $p_comment, strlen($p_comment));
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privList()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privList(&$p_list)
+  {
+    $v_result = 1;
+
+    // ----- Magic quotes trick
+    $this->privDisableMagicQuotes();
+
+    // ----- Open the zip file
+    if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0) {
+      // ----- Magic quotes trick
+      $this->privSwapBackMagicQuotes();
+
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in binary read mode');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Read the central directory informations
+    $v_central_dir = array();
+    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+      $this->privSwapBackMagicQuotes();
+
+      return $v_result;
+    }
+
+    // ----- Go to beginning of Central Dir
+    @rewind($this->zip_fd);
+    if (@fseek($this->zip_fd, $v_central_dir['offset'])) {
+      $this->privSwapBackMagicQuotes();
+
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Read each entry
+    for ($i = 0; $i < $v_central_dir['entries']; $i++) {
+      // ----- Read the file header
+      if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1) {
+        $this->privSwapBackMagicQuotes();
+
+        return $v_result;
+      }
+      $v_header['index'] = $i;
+
+      // ----- Get the only interesting attributes
+      $this->privConvertHeader2FileInfo($v_header, $p_list[$i]);
+      unset($v_header);
+    }
+
+    // ----- Close the zip file
+    $this->privCloseFd();
+
+    // ----- Magic quotes trick
+    $this->privSwapBackMagicQuotes();
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privConvertHeader2FileInfo()
+  // Description :
+  //   This function takes the file informations from the central directory
+  //   entries and extract the interesting parameters that will be given back.
+  //   The resulting file infos are set in the array $p_info
+  //     $p_info['filename'] : Filename with full path. Given by user (add),
+  //                           extracted in the filesystem (extract).
+  //     $p_info['stored_filename'] : Stored filename in the archive.
+  //     $p_info['size'] = Size of the file.
+  //     $p_info['compressed_size'] = Compressed size of the file.
+  //     $p_info['mtime'] = Last modification date of the file.
+  //     $p_info['comment'] = Comment associated with the file.
+  //     $p_info['folder'] = true/false : indicates if the entry is a folder or not.
+  //     $p_info['status'] = status of the action on the file.
+  //     $p_info['crc'] = CRC of the file content.
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privConvertHeader2FileInfo($p_header, &$p_info)
+  {
+    $v_result = 1;
+
+    // ----- Get the interesting attributes
+    $v_temp_path               = PclZipUtilPathReduction($p_header['filename']);
+    $p_info['filename']        = $v_temp_path;
+    $v_temp_path               = PclZipUtilPathReduction($p_header['stored_filename']);
+    $p_info['stored_filename'] = $v_temp_path;
+    $p_info['size']            = $p_header['size'];
+    $p_info['compressed_size'] = $p_header['compressed_size'];
+    $p_info['mtime']           = $p_header['mtime'];
+    $p_info['comment']         = $p_header['comment'];
+    $p_info['folder']          = (($p_header['external'] & 0x00000010) == 0x00000010);
+    $p_info['index']           = $p_header['index'];
+    $p_info['status']          = $p_header['status'];
+    $p_info['crc']             = $p_header['crc'];
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privExtractByRule()
+  // Description :
+  //   Extract a file or directory depending of rules (by index, by name, ...)
+  // Parameters :
+  //   $p_file_list : An array where will be placed the properties of each
+  //                  extracted file
+  //   $p_path : Path to add while writing the extracted files
+  //   $p_remove_path : Path to remove (from the file memorized path) while writing the
+  //                    extracted files. If the path does not match the file path,
+  //                    the file is extracted with its memorized path.
+  //                    $p_remove_path does not apply to 'list' mode.
+  //                    $p_path and $p_remove_path are commulative.
+  // Return Values :
+  //   1 on success,0 or less on error (see error code list)
+  // --------------------------------------------------------------------------------
+  public function privExtractByRule(&$p_file_list, $p_path, $p_remove_path, $p_remove_all_path, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Magic quotes trick
+    $this->privDisableMagicQuotes();
+
+    // ----- Check the path
+    if (($p_path == "") || ((substr($p_path, 0, 1) != "/") && (substr($p_path, 0, 3) != "../") && (substr($p_path, 1, 2) != ":/"))) {
+      $p_path = "./" . $p_path;
+    }
+
+    // ----- Reduce the path last (and duplicated) '/'
+    if (($p_path != "./") && ($p_path != "/")) {
+      // ----- Look for the path end '/'
+      while (substr($p_path, -1) == "/") {
+        $p_path = substr($p_path, 0, strlen($p_path) - 1);
+      }
+    }
+
+    // ----- Look for path to remove format (should end by /)
+    if (($p_remove_path != "") && (substr($p_remove_path, -1) != '/')) {
+      $p_remove_path .= '/';
+    }
+    $p_remove_path_size = strlen($p_remove_path);
+
+    // ----- Open the zip file
+    if (($v_result = $this->privOpenFd('rb')) != 1) {
+      $this->privSwapBackMagicQuotes();
+
+      return $v_result;
+    }
+
+    // ----- Read the central directory informations
+    $v_central_dir = array();
+    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+      // ----- Close the zip file
+      $this->privCloseFd();
+      $this->privSwapBackMagicQuotes();
+
+      return $v_result;
+    }
+
+    // ----- Start at beginning of Central Dir
+    $v_pos_entry = $v_central_dir['offset'];
+
+    // ----- Read each entry
+    $j_start = 0;
+    for ($i = 0, $v_nb_extracted = 0; $i < $v_central_dir['entries']; $i++) {
+
+      // ----- Read next Central dir entry
+      @rewind($this->zip_fd);
+      if (@fseek($this->zip_fd, $v_pos_entry)) {
+        // ----- Close the zip file
+        $this->privCloseFd();
+        $this->privSwapBackMagicQuotes();
+
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+        // ----- Return
+        return PclZip::errorCode();
+      }
+
+      // ----- Read the file header
+      $v_header = array();
+      if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1) {
+        // ----- Close the zip file
+        $this->privCloseFd();
+        $this->privSwapBackMagicQuotes();
+
+        return $v_result;
+      }
+
+      // ----- Store the index
+      $v_header['index'] = $i;
+
+      // ----- Store the file position
+      $v_pos_entry = ftell($this->zip_fd);
+
+      // ----- Look for the specific extract rules
+      $v_extract = false;
+
+      // ----- Look for extract by name rule
+      if ((isset($p_options[PCLZIP_OPT_BY_NAME])) && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) {
+
+        // ----- Look if the filename is in the list
+        for ($j = 0; ($j < sizeof($p_options[PCLZIP_OPT_BY_NAME])) && (!$v_extract); $j++) {
+
+          // ----- Look for a directory
+          if (substr($p_options[PCLZIP_OPT_BY_NAME][$j], -1) == "/") {
+
+            // ----- Look if the directory is in the filename path
+            if ((strlen($v_header['stored_filename']) > strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) && (substr($v_header['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+              $v_extract = true;
+            }
+
+            // ----- Look for a filename
+          } elseif ($v_header['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) {
+            $v_extract = true;
+          }
+        }
+        // ----- Look for extract by ereg rule
+        // ereg() is deprecated with PHP 5.3
+        /*
+            elseif (   (isset($p_options[PCLZIP_OPT_BY_EREG]))
+            && ($p_options[PCLZIP_OPT_BY_EREG] != "")) {
+
+            if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header['stored_filename'])) {
+            $v_extract = true;
+            }
+            }
+            */
+
+        // ----- Look for extract by preg rule
+      } elseif ((isset($p_options[PCLZIP_OPT_BY_PREG])) && ($p_options[PCLZIP_OPT_BY_PREG] != "")) {
+
+        if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header['stored_filename'])) {
+          $v_extract = true;
+        }
+
+        // ----- Look for extract by index rule
+      } elseif ((isset($p_options[PCLZIP_OPT_BY_INDEX])) && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) {
+
+        // ----- Look if the index is in the list
+        for ($j = $j_start; ($j < sizeof($p_options[PCLZIP_OPT_BY_INDEX])) && (!$v_extract); $j++) {
+
+          if (($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i <= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) {
+            $v_extract = true;
+          }
+          if ($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) {
+            $j_start = $j + 1;
+          }
+
+          if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start'] > $i) {
+            break;
+          }
+        }
+
+        // ----- Look for no rule, which means extract all the archive
+      } else {
+        $v_extract = true;
+      }
+
+      // ----- Check compression method
+      if (($v_extract) && (($v_header['compression'] != 8) && ($v_header['compression'] != 0))) {
+        $v_header['status'] = 'unsupported_compression';
+
+        // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+        if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+          $this->privSwapBackMagicQuotes();
+
+          PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_COMPRESSION, "Filename '" . $v_header['stored_filename'] . "' is " . "compressed by an unsupported compression " . "method (" . $v_header['compression'] . ") ");
+
+          return PclZip::errorCode();
+        }
+      }
+
+      // ----- Check encrypted files
+      if (($v_extract) && (($v_header['flag'] & 1) == 1)) {
+        $v_header['status'] = 'unsupported_encryption';
+
+        // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+        if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+          $this->privSwapBackMagicQuotes();
+
+          PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, "Unsupported encryption for " . " filename '" . $v_header['stored_filename'] . "'");
+
+          return PclZip::errorCode();
+        }
+      }
+
+      // ----- Look for real extraction
+      if (($v_extract) && ($v_header['status'] != 'ok')) {
+        $v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++]);
+        if ($v_result != 1) {
+          $this->privCloseFd();
+          $this->privSwapBackMagicQuotes();
+
+          return $v_result;
+        }
+
+        $v_extract = false;
+      }
+
+      // ----- Look for real extraction
+      if ($v_extract) {
+
+        // ----- Go to the file position
+        @rewind($this->zip_fd);
+        if (@fseek($this->zip_fd, $v_header['offset'])) {
+          // ----- Close the zip file
+          $this->privCloseFd();
+
+          $this->privSwapBackMagicQuotes();
+
+          // ----- Error log
+          PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+          // ----- Return
+          return PclZip::errorCode();
+        }
+
+        // ----- Look for extraction as string
+        if ($p_options[PCLZIP_OPT_EXTRACT_AS_STRING]) {
+
+          $v_string = '';
+
+          // ----- Extracting the file
+          $v_result1 = $this->privExtractFileAsString($v_header, $v_string, $p_options);
+          if ($v_result1 < 1) {
+            $this->privCloseFd();
+            $this->privSwapBackMagicQuotes();
+
+            return $v_result1;
+          }
+
+          // ----- Get the only interesting attributes
+          if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted])) != 1) {
+            // ----- Close the zip file
+            $this->privCloseFd();
+            $this->privSwapBackMagicQuotes();
+
+            return $v_result;
+          }
+
+          // ----- Set the file content
+          $p_file_list[$v_nb_extracted]['content'] = $v_string;
+
+          // ----- Next extracted file
+          $v_nb_extracted++;
+
+          // ----- Look for user callback abort
+          if ($v_result1 == 2) {
+            break;
+          }
+
+          // ----- Look for extraction in standard output
+        } elseif ((isset($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) && ($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) {
+          // ----- Extracting the file in standard output
+          $v_result1 = $this->privExtractFileInOutput($v_header, $p_options);
+          if ($v_result1 < 1) {
+            $this->privCloseFd();
+            $this->privSwapBackMagicQuotes();
+
+            return $v_result1;
+          }
+
+          // ----- Get the only interesting attributes
+          if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) {
+            $this->privCloseFd();
+            $this->privSwapBackMagicQuotes();
+
+            return $v_result;
+          }
+
+          // ----- Look for user callback abort
+          if ($v_result1 == 2) {
+            break;
+          }
+
+          // ----- Look for normal extraction
+        } else {
+          // ----- Extracting the file
+          $v_result1 = $this->privExtractFile($v_header, $p_path, $p_remove_path, $p_remove_all_path, $p_options);
+          if ($v_result1 < 1) {
+            $this->privCloseFd();
+            $this->privSwapBackMagicQuotes();
+
+            return $v_result1;
+          }
+
+          // ----- Get the only interesting attributes
+          if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) {
+            // ----- Close the zip file
+            $this->privCloseFd();
+            $this->privSwapBackMagicQuotes();
+
+            return $v_result;
+          }
+
+          // ----- Look for user callback abort
+          if ($v_result1 == 2) {
+            break;
+          }
+        }
+      }
+    }
+
+    // ----- Close the zip file
+    $this->privCloseFd();
+    $this->privSwapBackMagicQuotes();
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privExtractFile()
+  // Description :
+  // Parameters :
+  // Return Values :
+  //
+  // 1 : ... ?
+  // PCLZIP_ERR_USER_ABORTED(2) : User ask for extraction stop in callback
+  // --------------------------------------------------------------------------------
+  public function privExtractFile(&$p_entry, $p_path, $p_remove_path, $p_remove_all_path, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Read the file header
+    if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Check that the file header is coherent with $p_entry info
+    if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+      // TBC
+    }
+
+    // ----- Look for all path to remove
+    if ($p_remove_all_path == true) {
+      // ----- Look for folder entry that not need to be extracted
+      if (($p_entry['external'] & 0x00000010) == 0x00000010) {
+
+        $p_entry['status'] = "filtered";
+
+        return $v_result;
+      }
+
+      // ----- Get the basename of the path
+      $p_entry['filename'] = basename($p_entry['filename']);
+
+      // ----- Look for path to remove
+    } elseif ($p_remove_path != "") {
+      if (PclZipUtilPathInclusion($p_remove_path, $p_entry['filename']) == 2) {
+
+        // ----- Change the file status
+        $p_entry['status'] = "filtered";
+
+        // ----- Return
+        return $v_result;
+      }
+
+      $p_remove_path_size = strlen($p_remove_path);
+      if (substr($p_entry['filename'], 0, $p_remove_path_size) == $p_remove_path) {
+
+        // ----- Remove the path
+        $p_entry['filename'] = substr($p_entry['filename'], $p_remove_path_size);
+
+      }
+    }
+
+    // ----- Add the path
+    if ($p_path != '') {
+      $p_entry['filename'] = $p_path . "/" . $p_entry['filename'];
+    }
+
+    // ----- Check a base_dir_restriction
+    if (isset($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION])) {
+      $v_inclusion = PclZipUtilPathInclusion($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION], $p_entry['filename']);
+      if ($v_inclusion == 0) {
+
+        PclZip::privErrorLog(PCLZIP_ERR_DIRECTORY_RESTRICTION, "Filename '" . $p_entry['filename'] . "' is " . "outside PCLZIP_OPT_EXTRACT_DIR_RESTRICTION");
+
+        return PclZip::errorCode();
+      }
+    }
+
+    // ----- Look for pre-extract callback
+    if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+      // ----- Generate a local information
+      $v_local_header = array();
+      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+      // ----- Call the callback
+      // Here I do not use call_user_func() because I need to send a reference to the
+      // header.
+      //      eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+      $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+      if ($v_result == 0) {
+        // ----- Change the file status
+        $p_entry['status'] = "skipped";
+        $v_result          = 1;
+      }
+
+      // ----- Look for abort result
+      if ($v_result == 2) {
+        // ----- This status is internal and will be changed in 'skipped'
+        $p_entry['status'] = "aborted";
+        $v_result          = PCLZIP_ERR_USER_ABORTED;
+      }
+
+      // ----- Update the informations
+      // Only some fields can be modified
+      $p_entry['filename'] = $v_local_header['filename'];
+    }
+
+    // ----- Look if extraction should be done
+    if ($p_entry['status'] == 'ok') {
+
+      // ----- Look for specific actions while the file exist
+      if (file_exists($p_entry['filename'])) {
+
+        // ----- Look if file is a directory
+        if (is_dir($p_entry['filename'])) {
+
+          // ----- Change the file status
+          $p_entry['status'] = "already_a_directory";
+
+          // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+          // For historical reason first PclZip implementation does not stop
+          // when this kind of error occurs.
+          if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+            PclZip::privErrorLog(PCLZIP_ERR_ALREADY_A_DIRECTORY, "Filename '" . $p_entry['filename'] . "' is " . "already used by an existing directory");
+
+            return PclZip::errorCode();
+          }
+
+          // ----- Look if file is write protected
+        } elseif (!is_writeable($p_entry['filename'])) {
+
+          // ----- Change the file status
+          $p_entry['status'] = "write_protected";
+
+          // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+          // For historical reason first PclZip implementation does not stop
+          // when this kind of error occurs.
+          if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+            PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, "Filename '" . $p_entry['filename'] . "' exists " . "and is write protected");
+
+            return PclZip::errorCode();
+          }
+
+          // ----- Look if the extracted file is older
+        } elseif (filemtime($p_entry['filename']) > $p_entry['mtime']) {
+          // ----- Change the file status
+          if ((isset($p_options[PCLZIP_OPT_REPLACE_NEWER])) && ($p_options[PCLZIP_OPT_REPLACE_NEWER] === true)) {
+          } else {
+            $p_entry['status'] = "newer_exist";
+
+            // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+            // For historical reason first PclZip implementation does not stop
+            // when this kind of error occurs.
+            if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+              PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, "Newer version of '" . $p_entry['filename'] . "' exists " . "and option PCLZIP_OPT_REPLACE_NEWER is not selected");
+
+              return PclZip::errorCode();
+            }
+          }
+        } else {
+        }
+
+        // ----- Check the directory availability and create it if necessary
+      } else {
+        if ((($p_entry['external'] & 0x00000010) == 0x00000010) || (substr($p_entry['filename'], -1) == '/')) {
+          $v_dir_to_check = $p_entry['filename'];
+        } elseif (!strstr($p_entry['filename'], "/")) {
+          $v_dir_to_check = "";
+        } else {
+          $v_dir_to_check = dirname($p_entry['filename']);
+        }
+
+        if (($v_result = $this->privDirCheck($v_dir_to_check, (($p_entry['external'] & 0x00000010) == 0x00000010))) != 1) {
+
+          // ----- Change the file status
+          $p_entry['status'] = "path_creation_fail";
+
+          // ----- Return
+          //return $v_result;
+          $v_result = 1;
+        }
+      }
+    }
+
+    // ----- Look if extraction should be done
+    if ($p_entry['status'] == 'ok') {
+
+      // ----- Do the extraction (if not a folder)
+      if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+        // ----- Look for not compressed file
+        if ($p_entry['compression'] == 0) {
+
+          // ----- Opening destination file
+          if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+
+            // ----- Change the file status
+            $p_entry['status'] = "write_error";
+
+            // ----- Return
+            return $v_result;
+          }
+
+          // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+          $v_size = $p_entry['compressed_size'];
+          while ($v_size != 0) {
+            $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+            $v_buffer    = @fread($this->zip_fd, $v_read_size);
+            /* Try to speed up the code
+                        $v_binary_data = pack('a'.$v_read_size, $v_buffer);
+                        @fwrite($v_dest_file, $v_binary_data, $v_read_size);
+                        */
+            @fwrite($v_dest_file, $v_buffer, $v_read_size);
+            $v_size -= $v_read_size;
+          }
+
+          // ----- Closing the destination file
+          fclose($v_dest_file);
+
+          // ----- Change the file mtime
+          @touch($p_entry['filename'], $p_entry['mtime']);
+
+        } else {
+          // ----- TBC
+          // Need to be finished
+          if (($p_entry['flag'] & 1) == 1) {
+            PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, 'File \'' . $p_entry['filename'] . '\' is encrypted. Encrypted files are not supported.');
+
+            return PclZip::errorCode();
+          }
+
+          // ----- Look for using temporary file to unzip
+          if ((!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON]) || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_entry['size'])))) {
+            $v_result = $this->privExtractFileUsingTempFile($p_entry, $p_options);
+            if ($v_result < PCLZIP_ERR_NO_ERROR) {
+              return $v_result;
+            }
+
+            // ----- Look for extract in memory
+          } else {
+
+            // ----- Read the compressed file in a buffer (one shot)
+            $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+            // ----- Decompress the file
+            $v_file_content = @gzinflate($v_buffer);
+            unset($v_buffer);
+            if ($v_file_content === false) {
+
+              // ----- Change the file status
+              // TBC
+              $p_entry['status'] = "error";
+
+              return $v_result;
+            }
+
+            // ----- Opening destination file
+            if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+
+              // ----- Change the file status
+              $p_entry['status'] = "write_error";
+
+              return $v_result;
+            }
+
+            // ----- Write the uncompressed data
+            @fwrite($v_dest_file, $v_file_content, $p_entry['size']);
+            unset($v_file_content);
+
+            // ----- Closing the destination file
+            @fclose($v_dest_file);
+
+          }
+
+          // ----- Change the file mtime
+          @touch($p_entry['filename'], $p_entry['mtime']);
+        }
+
+        // ----- Look for chmod option
+        if (isset($p_options[PCLZIP_OPT_SET_CHMOD])) {
+
+          // ----- Change the mode of the file
+          @chmod($p_entry['filename'], $p_options[PCLZIP_OPT_SET_CHMOD]);
+        }
+
+      }
+    }
+
+    // ----- Change abort status
+    if ($p_entry['status'] == "aborted") {
+      $p_entry['status'] = "skipped";
+
+      // ----- Look for post-extract callback
+    } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+      // ----- Generate a local information
+      $v_local_header = array();
+      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+      // ----- Call the callback
+      // Here I do not use call_user_func() because I need to send a reference to the
+      // header.
+      //      eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+      $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+      // ----- Look for abort result
+      if ($v_result == 2) {
+        $v_result = PCLZIP_ERR_USER_ABORTED;
+      }
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privExtractFileUsingTempFile()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privExtractFileUsingTempFile(&$p_entry, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Creates a temporary file
+    $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.gz';
+    if (($v_dest_file = @fopen($v_gzip_temp_name, "wb")) == 0) {
+      fclose($v_file);
+      PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary write mode');
+
+      return PclZip::errorCode();
+    }
+
+    // ----- Write gz file format header
+    $v_binary_data = pack('va1a1Va1a1', 0x8b1f, Chr($p_entry['compression']), Chr(0x00), time(), Chr(0x00), Chr(3));
+    @fwrite($v_dest_file, $v_binary_data, 10);
+
+    // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+    $v_size = $p_entry['compressed_size'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @fread($this->zip_fd, $v_read_size);
+      //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+      @fwrite($v_dest_file, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Write gz file format footer
+    $v_binary_data = pack('VV', $p_entry['crc'], $p_entry['size']);
+    @fwrite($v_dest_file, $v_binary_data, 8);
+
+    // ----- Close the temporary file
+    @fclose($v_dest_file);
+
+    // ----- Opening destination file
+    if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+      $p_entry['status'] = "write_error";
+
+      return $v_result;
+    }
+
+    // ----- Open the temporary gz file
+    if (($v_src_file = @gzopen($v_gzip_temp_name, 'rb')) == 0) {
+      @fclose($v_dest_file);
+      $p_entry['status'] = "read_error";
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+      return PclZip::errorCode();
+    }
+
+    // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+    $v_size = $p_entry['size'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @gzread($v_src_file, $v_read_size);
+      //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+      @fwrite($v_dest_file, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+    @fclose($v_dest_file);
+    @gzclose($v_src_file);
+
+    // ----- Delete the temporary file
+    @unlink($v_gzip_temp_name);
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privExtractFileInOutput()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privExtractFileInOutput(&$p_entry, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Read the file header
+    if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+      return $v_result;
+    }
+
+    // ----- Check that the file header is coherent with $p_entry info
+    if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+      // TBC
+    }
+
+    // ----- Look for pre-extract callback
+    if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+      // ----- Generate a local information
+      $v_local_header = array();
+      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+      // ----- Call the callback
+      // Here I do not use call_user_func() because I need to send a reference to the
+      // header.
+      //      eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+      $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+      if ($v_result == 0) {
+        // ----- Change the file status
+        $p_entry['status'] = "skipped";
+        $v_result          = 1;
+      }
+
+      // ----- Look for abort result
+      if ($v_result == 2) {
+        // ----- This status is internal and will be changed in 'skipped'
+        $p_entry['status'] = "aborted";
+        $v_result          = PCLZIP_ERR_USER_ABORTED;
+      }
+
+      // ----- Update the informations
+      // Only some fields can be modified
+      $p_entry['filename'] = $v_local_header['filename'];
+    }
+
+    // ----- Trace
+
+    // ----- Look if extraction should be done
+    if ($p_entry['status'] == 'ok') {
+
+      // ----- Do the extraction (if not a folder)
+      if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+        // ----- Look for not compressed file
+        if ($p_entry['compressed_size'] == $p_entry['size']) {
+
+          // ----- Read the file in a buffer (one shot)
+          $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+          // ----- Send the file to the output
+          echo $v_buffer;
+          unset($v_buffer);
+        } else {
+
+          // ----- Read the compressed file in a buffer (one shot)
+          $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+          // ----- Decompress the file
+          $v_file_content = gzinflate($v_buffer);
+          unset($v_buffer);
+
+          // ----- Send the file to the output
+          echo $v_file_content;
+          unset($v_file_content);
+        }
+      }
+    }
+
+    // ----- Change abort status
+    if ($p_entry['status'] == "aborted") {
+      $p_entry['status'] = "skipped";
+
+      // ----- Look for post-extract callback
+    } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+      // ----- Generate a local information
+      $v_local_header = array();
+      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+      // ----- Call the callback
+      // Here I do not use call_user_func() because I need to send a reference to the
+      // header.
+      //      eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+      $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+      // ----- Look for abort result
+      if ($v_result == 2) {
+        $v_result = PCLZIP_ERR_USER_ABORTED;
+      }
+    }
+
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privExtractFileAsString()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privExtractFileAsString(&$p_entry, &$p_string, &$p_options)
+  {
+    $v_result = 1;
+
+    // ----- Read the file header
+    $v_header = array();
+    if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Check that the file header is coherent with $p_entry info
+    if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+      // TBC
+    }
+
+    // ----- Look for pre-extract callback
+    if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+      // ----- Generate a local information
+      $v_local_header = array();
+      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+      // ----- Call the callback
+      // Here I do not use call_user_func() because I need to send a reference to the
+      // header.
+      //      eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+      $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+      if ($v_result == 0) {
+        // ----- Change the file status
+        $p_entry['status'] = "skipped";
+        $v_result          = 1;
+      }
+
+      // ----- Look for abort result
+      if ($v_result == 2) {
+        // ----- This status is internal and will be changed in 'skipped'
+        $p_entry['status'] = "aborted";
+        $v_result          = PCLZIP_ERR_USER_ABORTED;
+      }
+
+      // ----- Update the informations
+      // Only some fields can be modified
+      $p_entry['filename'] = $v_local_header['filename'];
+    }
+
+    // ----- Look if extraction should be done
+    if ($p_entry['status'] == 'ok') {
+
+      // ----- Do the extraction (if not a folder)
+      if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+        // ----- Look for not compressed file
+        //      if ($p_entry['compressed_size'] == $p_entry['size'])
+        if ($p_entry['compression'] == 0) {
+
+          // ----- Reading the file
+          $p_string = @fread($this->zip_fd, $p_entry['compressed_size']);
+        } else {
+
+          // ----- Reading the file
+          $v_data = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+          // ----- Decompress the file
+          if (($p_string = @gzinflate($v_data)) === false) {
+            // TBC
+          }
+        }
+
+        // ----- Trace
+      } else {
+        // TBC : error : can not extract a folder in a string
+      }
+
+    }
+
+    // ----- Change abort status
+    if ($p_entry['status'] == "aborted") {
+      $p_entry['status'] = "skipped";
+
+      // ----- Look for post-extract callback
+    } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+      // ----- Generate a local information
+      $v_local_header = array();
+      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+      // ----- Swap the content to header
+      $v_local_header['content'] = $p_string;
+      $p_string                  = '';
+
+      // ----- Call the callback
+      // Here I do not use call_user_func() because I need to send a reference to the
+      // header.
+      //      eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+      $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+      // ----- Swap back the content to header
+      $p_string = $v_local_header['content'];
+      unset($v_local_header['content']);
+
+      // ----- Look for abort result
+      if ($v_result == 2) {
+        $v_result = PCLZIP_ERR_USER_ABORTED;
+      }
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privReadFileHeader()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privReadFileHeader(&$p_header)
+  {
+    $v_result = 1;
+
+    // ----- Read the 4 bytes signature
+    $v_binary_data = @fread($this->zip_fd, 4);
+    $v_data        = unpack('Vid', $v_binary_data);
+
+    // ----- Check signature
+    if ($v_data['id'] != 0x04034b50) {
+
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Read the first 42 bytes of the header
+    $v_binary_data = fread($this->zip_fd, 26);
+
+    // ----- Look for invalid block size
+    if (strlen($v_binary_data) != 26) {
+      $p_header['filename'] = "";
+      $p_header['status']   = "invalid_header";
+
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : " . strlen($v_binary_data));
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Extract the values
+    $v_data = unpack('vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $v_binary_data);
+
+    // ----- Get filename
+    $p_header['filename'] = fread($this->zip_fd, $v_data['filename_len']);
+
+    // ----- Get extra_fields
+    if ($v_data['extra_len'] != 0) {
+      $p_header['extra'] = fread($this->zip_fd, $v_data['extra_len']);
+    } else {
+      $p_header['extra'] = '';
+    }
+
+    // ----- Extract properties
+    $p_header['version_extracted'] = $v_data['version'];
+    $p_header['compression']       = $v_data['compression'];
+    $p_header['size']              = $v_data['size'];
+    $p_header['compressed_size']   = $v_data['compressed_size'];
+    $p_header['crc']               = $v_data['crc'];
+    $p_header['flag']              = $v_data['flag'];
+    $p_header['filename_len']      = $v_data['filename_len'];
+
+    // ----- Recuperate date in UNIX format
+    $p_header['mdate'] = $v_data['mdate'];
+    $p_header['mtime'] = $v_data['mtime'];
+    if ($p_header['mdate'] && $p_header['mtime']) {
+      // ----- Extract time
+      $v_hour    = ($p_header['mtime'] & 0xF800) >> 11;
+      $v_minute  = ($p_header['mtime'] & 0x07E0) >> 5;
+      $v_seconde = ($p_header['mtime'] & 0x001F) * 2;
+
+      // ----- Extract date
+      $v_year  = (($p_header['mdate'] & 0xFE00) >> 9) + 1980;
+      $v_month = ($p_header['mdate'] & 0x01E0) >> 5;
+      $v_day   = $p_header['mdate'] & 0x001F;
+
+      // ----- Get UNIX date format
+      $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);
+
+    } else {
+      $p_header['mtime'] = time();
+    }
+
+    // TBC
+    //for (reset($v_data); $key = key($v_data); next($v_data)) {
+    //}
+
+    // ----- Set the stored filename
+    $p_header['stored_filename'] = $p_header['filename'];
+
+    // ----- Set the status field
+    $p_header['status'] = "ok";
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privReadCentralFileHeader()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privReadCentralFileHeader(&$p_header)
+  {
+    $v_result = 1;
+
+    // ----- Read the 4 bytes signature
+    $v_binary_data = @fread($this->zip_fd, 4);
+    $v_data        = unpack('Vid', $v_binary_data);
+
+    // ----- Check signature
+    if ($v_data['id'] != 0x02014b50) {
+
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Read the first 42 bytes of the header
+    $v_binary_data = fread($this->zip_fd, 42);
+
+    // ----- Look for invalid block size
+    if (strlen($v_binary_data) != 42) {
+      $p_header['filename'] = "";
+      $p_header['status']   = "invalid_header";
+
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : " . strlen($v_binary_data));
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Extract the values
+    $p_header = unpack('vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $v_binary_data);
+
+    // ----- Get filename
+    if ($p_header['filename_len'] != 0) {
+      $p_header['filename'] = fread($this->zip_fd, $p_header['filename_len']);
+    } else {
+      $p_header['filename'] = '';
+    }
+
+    // ----- Get extra
+    if ($p_header['extra_len'] != 0) {
+      $p_header['extra'] = fread($this->zip_fd, $p_header['extra_len']);
+    } else {
+      $p_header['extra'] = '';
+    }
+
+    // ----- Get comment
+    if ($p_header['comment_len'] != 0) {
+      $p_header['comment'] = fread($this->zip_fd, $p_header['comment_len']);
+    } else {
+      $p_header['comment'] = '';
+    }
+
+    // ----- Extract properties
+
+    // ----- Recuperate date in UNIX format
+    //if ($p_header['mdate'] && $p_header['mtime'])
+    // TBC : bug : this was ignoring time with 0/0/0
+    if (1) {
+      // ----- Extract time
+      $v_hour    = ($p_header['mtime'] & 0xF800) >> 11;
+      $v_minute  = ($p_header['mtime'] & 0x07E0) >> 5;
+      $v_seconde = ($p_header['mtime'] & 0x001F) * 2;
+
+      // ----- Extract date
+      $v_year  = (($p_header['mdate'] & 0xFE00) >> 9) + 1980;
+      $v_month = ($p_header['mdate'] & 0x01E0) >> 5;
+      $v_day   = $p_header['mdate'] & 0x001F;
+
+      // ----- Get UNIX date format
+      $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);
+
+    } else {
+      $p_header['mtime'] = time();
+    }
+
+    // ----- Set the stored filename
+    $p_header['stored_filename'] = $p_header['filename'];
+
+    // ----- Set default status to ok
+    $p_header['status'] = 'ok';
+
+    // ----- Look if it is a directory
+    if (substr($p_header['filename'], -1) == '/') {
+      //$p_header['external'] = 0x41FF0010;
+      $p_header['external'] = 0x00000010;
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privCheckFileHeaders()
+  // Description :
+  // Parameters :
+  // Return Values :
+  //   1 on success,
+  //   0 on error;
+  // --------------------------------------------------------------------------------
+  public function privCheckFileHeaders(&$p_local_header, &$p_central_header)
+  {
+    $v_result = 1;
+
+    // ----- Check the static values
+    // TBC
+    if ($p_local_header['filename'] != $p_central_header['filename']) {
+    }
+    if ($p_local_header['version_extracted'] != $p_central_header['version_extracted']) {
+    }
+    if ($p_local_header['flag'] != $p_central_header['flag']) {
+    }
+    if ($p_local_header['compression'] != $p_central_header['compression']) {
+    }
+    if ($p_local_header['mtime'] != $p_central_header['mtime']) {
+    }
+    if ($p_local_header['filename_len'] != $p_central_header['filename_len']) {
+    }
+
+    // ----- Look for flag bit 3
+    if (($p_local_header['flag'] & 8) == 8) {
+      $p_local_header['size']            = $p_central_header['size'];
+      $p_local_header['compressed_size'] = $p_central_header['compressed_size'];
+      $p_local_header['crc']             = $p_central_header['crc'];
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privReadEndCentralDir()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privReadEndCentralDir(&$p_central_dir)
+  {
+    $v_result = 1;
+
+    // ----- Go to the end of the zip file
+    $v_size = filesize($this->zipname);
+    @fseek($this->zip_fd, $v_size);
+    if (@ftell($this->zip_fd) != $v_size) {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to go to the end of the archive \'' . $this->zipname . '\'');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- First try : look if this is an archive with no commentaries (most of the time)
+    // in this case the end of central dir is at 22 bytes of the file end
+    $v_found = 0;
+    if ($v_size > 26) {
+      @fseek($this->zip_fd, $v_size - 22);
+      if (($v_pos = @ftell($this->zip_fd)) != ($v_size - 22)) {
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \'' . $this->zipname . '\'');
+
+        // ----- Return
+        return PclZip::errorCode();
+      }
+
+      // ----- Read for bytes
+      $v_binary_data = @fread($this->zip_fd, 4);
+      $v_data        = @unpack('Vid', $v_binary_data);
+
+      // ----- Check signature
+      if ($v_data['id'] == 0x06054b50) {
+        $v_found = 1;
+      }
+
+      $v_pos = ftell($this->zip_fd);
+    }
+
+    // ----- Go back to the maximum possible size of the Central Dir End Record
+    if (!$v_found) {
+      $v_maximum_size = 65557; // 0xFFFF + 22;
+      if ($v_maximum_size > $v_size) {
+        $v_maximum_size = $v_size;
+      }
+      @fseek($this->zip_fd, $v_size - $v_maximum_size);
+      if (@ftell($this->zip_fd) != ($v_size - $v_maximum_size)) {
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \'' . $this->zipname . '\'');
+
+        // ----- Return
+        return PclZip::errorCode();
+      }
+
+      // ----- Read byte per byte in order to find the signature
+      $v_pos   = ftell($this->zip_fd);
+      $v_bytes = 0x00000000;
+      while ($v_pos < $v_size) {
+        // ----- Read a byte
+        $v_byte = @fread($this->zip_fd, 1);
+
+        // -----  Add the byte
+        //$v_bytes = ($v_bytes << 8) | Ord($v_byte);
+        // Note we mask the old value down such that once shifted we can never end up with more than a 32bit number
+        // Otherwise on systems where we have 64bit integers the check below for the magic number will fail.
+        $v_bytes = (($v_bytes & 0xFFFFFF) << 8) | Ord($v_byte);
+
+        // ----- Compare the bytes
+        if ($v_bytes == 0x504b0506) {
+          $v_pos++;
+          break;
+        }
+
+        $v_pos++;
+      }
+
+      // ----- Look if not found end of central dir
+      if ($v_pos == $v_size) {
+
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Unable to find End of Central Dir Record signature");
+
+        // ----- Return
+        return PclZip::errorCode();
+      }
+    }
+
+    // ----- Read the first 18 bytes of the header
+    $v_binary_data = fread($this->zip_fd, 18);
+
+    // ----- Look for invalid block size
+    if (strlen($v_binary_data) != 18) {
+
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid End of Central Dir Record size : " . strlen($v_binary_data));
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Extract the values
+    $v_data = unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', $v_binary_data);
+
+    // ----- Check the global size
+    if (($v_pos + $v_data['comment_size'] + 18) != $v_size) {
+
+      // ----- Removed in release 2.2 see readme file
+      // The check of the file size is a little too strict.
+      // Some bugs where found when a zip is encrypted/decrypted with 'crypt'.
+      // While decrypted, zip has training 0 bytes
+      if (0) {
+        // ----- Error log
+        PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'The central dir is not at the end of the archive.' . ' Some trailing bytes exists after the archive.');
+
+        // ----- Return
+        return PclZip::errorCode();
+      }
+    }
+
+    // ----- Get comment
+    if ($v_data['comment_size'] != 0) {
+      $p_central_dir['comment'] = fread($this->zip_fd, $v_data['comment_size']);
+    } else {
+      $p_central_dir['comment'] = '';
+    }
+
+    $p_central_dir['entries']      = $v_data['entries'];
+    $p_central_dir['disk_entries'] = $v_data['disk_entries'];
+    $p_central_dir['offset']       = $v_data['offset'];
+    $p_central_dir['size']         = $v_data['size'];
+    $p_central_dir['disk']         = $v_data['disk'];
+    $p_central_dir['disk_start']   = $v_data['disk_start'];
+
+    // TBC
+    //for (reset($p_central_dir); $key = key($p_central_dir); next($p_central_dir)) {
+    //}
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privDeleteByRule()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privDeleteByRule(&$p_result_list, &$p_options)
+  {
+    $v_result      = 1;
+    $v_list_detail = array();
+
+    // ----- Open the zip file
+    if (($v_result = $this->privOpenFd('rb')) != 1) {
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Read the central directory informations
+    $v_central_dir = array();
+    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+      $this->privCloseFd();
+
+      return $v_result;
+    }
+
+    // ----- Go to beginning of File
+    @rewind($this->zip_fd);
+
+    // ----- Scan all the files
+    // ----- Start at beginning of Central Dir
+    $v_pos_entry = $v_central_dir['offset'];
+    @rewind($this->zip_fd);
+    if (@fseek($this->zip_fd, $v_pos_entry)) {
+      // ----- Close the zip file
+      $this->privCloseFd();
+
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Read each entry
+    $v_header_list = array();
+    $j_start       = 0;
+    for ($i = 0, $v_nb_extracted = 0; $i < $v_central_dir['entries']; $i++) {
+
+      // ----- Read the file header
+      $v_header_list[$v_nb_extracted] = array();
+      if (($v_result = $this->privReadCentralFileHeader($v_header_list[$v_nb_extracted])) != 1) {
+        // ----- Close the zip file
+        $this->privCloseFd();
+
+        return $v_result;
+      }
+
+      // ----- Store the index
+      $v_header_list[$v_nb_extracted]['index'] = $i;
+
+      // ----- Look for the specific extract rules
+      $v_found = false;
+
+      // ----- Look for extract by name rule
+      if ((isset($p_options[PCLZIP_OPT_BY_NAME])) && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) {
+
+        // ----- Look if the filename is in the list
+        for ($j = 0; ($j < sizeof($p_options[PCLZIP_OPT_BY_NAME])) && (!$v_found); $j++) {
+
+          // ----- Look for a directory
+          if (substr($p_options[PCLZIP_OPT_BY_NAME][$j], -1) == "/") {
+
+            // ----- Look if the directory is in the filename path
+            if ((strlen($v_header_list[$v_nb_extracted]['stored_filename']) > strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) && (substr($v_header_list[$v_nb_extracted]['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+              $v_found = true;
+            } elseif ((($v_header_list[$v_nb_extracted]['external'] & 0x00000010) == 0x00000010) /* Indicates a folder */ && ($v_header_list[$v_nb_extracted]['stored_filename'] . '/' == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+              $v_found = true;
+            }
+
+            // ----- Look for a filename
+          } elseif ($v_header_list[$v_nb_extracted]['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) {
+            $v_found = true;
+          }
+        }
+
+        // ----- Look for extract by ereg rule
+        // ereg() is deprecated with PHP 5.3
+        /*
+            elseif (   (isset($p_options[PCLZIP_OPT_BY_EREG]))
+            && ($p_options[PCLZIP_OPT_BY_EREG] != "")) {
+
+            if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header_list[$v_nb_extracted]['stored_filename'])) {
+            $v_found = true;
+            }
+            }
+            */
+
+        // ----- Look for extract by preg rule
+      } elseif ((isset($p_options[PCLZIP_OPT_BY_PREG])) && ($p_options[PCLZIP_OPT_BY_PREG] != "")) {
+
+        if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header_list[$v_nb_extracted]['stored_filename'])) {
+          $v_found = true;
+        }
+
+        // ----- Look for extract by index rule
+      } elseif ((isset($p_options[PCLZIP_OPT_BY_INDEX])) && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) {
+
+        // ----- Look if the index is in the list
+        for ($j = $j_start; ($j < sizeof($p_options[PCLZIP_OPT_BY_INDEX])) && (!$v_found); $j++) {
+
+          if (($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i <= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) {
+            $v_found = true;
+          }
+          if ($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) {
+            $j_start = $j + 1;
+          }
+
+          if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start'] > $i) {
+            break;
+          }
+        }
+      } else {
+        $v_found = true;
+      }
+
+      // ----- Look for deletion
+      if ($v_found) {
+        unset($v_header_list[$v_nb_extracted]);
+      } else {
+        $v_nb_extracted++;
+      }
+    }
+
+    // ----- Look if something need to be deleted
+    if ($v_nb_extracted > 0) {
+
+      // ----- Creates a temporay file
+      $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.tmp';
+
+      // ----- Creates a temporary zip archive
+      $v_temp_zip = new PclZip($v_zip_temp_name);
+
+      // ----- Open the temporary zip file in write mode
+      if (($v_result = $v_temp_zip->privOpenFd('wb')) != 1) {
+        $this->privCloseFd();
+
+        // ----- Return
+        return $v_result;
+      }
+
+      // ----- Look which file need to be kept
+      for ($i = 0; $i < sizeof($v_header_list); $i++) {
+
+        // ----- Calculate the position of the header
+        @rewind($this->zip_fd);
+        if (@fseek($this->zip_fd, $v_header_list[$i]['offset'])) {
+          // ----- Close the zip file
+          $this->privCloseFd();
+          $v_temp_zip->privCloseFd();
+          @unlink($v_zip_temp_name);
+
+          // ----- Error log
+          PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+          // ----- Return
+          return PclZip::errorCode();
+        }
+
+        // ----- Read the file header
+        $v_local_header = array();
+        if (($v_result = $this->privReadFileHeader($v_local_header)) != 1) {
+          // ----- Close the zip file
+          $this->privCloseFd();
+          $v_temp_zip->privCloseFd();
+          @unlink($v_zip_temp_name);
+
+          // ----- Return
+          return $v_result;
+        }
+
+        // ----- Check that local file header is same as central file header
+        if ($this->privCheckFileHeaders($v_local_header, $v_header_list[$i]) != 1) {
+          // TBC
+        }
+        unset($v_local_header);
+
+        // ----- Write the file header
+        if (($v_result = $v_temp_zip->privWriteFileHeader($v_header_list[$i])) != 1) {
+          // ----- Close the zip file
+          $this->privCloseFd();
+          $v_temp_zip->privCloseFd();
+          @unlink($v_zip_temp_name);
+
+          // ----- Return
+          return $v_result;
+        }
+
+        // ----- Read/write the data block
+        if (($v_result = PclZipUtilCopyBlock($this->zip_fd, $v_temp_zip->zip_fd, $v_header_list[$i]['compressed_size'])) != 1) {
+          // ----- Close the zip file
+          $this->privCloseFd();
+          $v_temp_zip->privCloseFd();
+          @unlink($v_zip_temp_name);
+
+          // ----- Return
+          return $v_result;
+        }
+      }
+
+      // ----- Store the offset of the central dir
+      $v_offset = @ftell($v_temp_zip->zip_fd);
+
+      // ----- Re-Create the Central Dir files header
+      for ($i = 0; $i < sizeof($v_header_list); $i++) {
+        // ----- Create the file header
+        if (($v_result = $v_temp_zip->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+          $v_temp_zip->privCloseFd();
+          $this->privCloseFd();
+          @unlink($v_zip_temp_name);
+
+          // ----- Return
+          return $v_result;
+        }
+
+        // ----- Transform the header to a 'usable' info
+        $v_temp_zip->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+      }
+
+      // ----- Zip file comment
+      $v_comment = '';
+      if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+        $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+      }
+
+      // ----- Calculate the size of the central header
+      $v_size = @ftell($v_temp_zip->zip_fd) - $v_offset;
+
+      // ----- Create the central dir footer
+      if (($v_result = $v_temp_zip->privWriteCentralHeader(sizeof($v_header_list), $v_size, $v_offset, $v_comment)) != 1) {
+        // ----- Reset the file list
+        unset($v_header_list);
+        $v_temp_zip->privCloseFd();
+        $this->privCloseFd();
+        @unlink($v_zip_temp_name);
+
+        // ----- Return
+        return $v_result;
+      }
+
+      // ----- Close
+      $v_temp_zip->privCloseFd();
+      $this->privCloseFd();
+
+      // ----- Delete the zip file
+      // TBC : I should test the result ...
+      @unlink($this->zipname);
+
+      // ----- Rename the temporary file
+      // TBC : I should test the result ...
+      //@rename($v_zip_temp_name, $this->zipname);
+      PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+      // ----- Destroy the temporary archive
+      unset($v_temp_zip);
+
+      // ----- Remove every files : reset the file
+    } elseif ($v_central_dir['entries'] != 0) {
+      $this->privCloseFd();
+
+      if (($v_result = $this->privOpenFd('wb')) != 1) {
+        return $v_result;
+      }
+
+      if (($v_result = $this->privWriteCentralHeader(0, 0, 0, '')) != 1) {
+        return $v_result;
+      }
+
+      $this->privCloseFd();
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privDirCheck()
+  // Description :
+  //   Check if a directory exists, if not it creates it and all the parents directory
+  //   which may be useful.
+  // Parameters :
+  //   $p_dir : Directory path to check.
+  // Return Values :
+  //    1 : OK
+  //   -1 : Unable to create directory
+  // --------------------------------------------------------------------------------
+  public function privDirCheck($p_dir, $p_is_dir = false)
+  {
+    $v_result = 1;
+
+    // ----- Remove the final '/'
+    if (($p_is_dir) && (substr($p_dir, -1) == '/')) {
+      $p_dir = substr($p_dir, 0, strlen($p_dir) - 1);
+    }
+
+    // ----- Check the directory availability
+    if ((is_dir($p_dir)) || ($p_dir == "")) {
+      return 1;
+    }
+
+    // ----- Extract parent directory
+    $p_parent_dir = dirname($p_dir);
+
+    // ----- Just a check
+    if ($p_parent_dir != $p_dir) {
+      // ----- Look for parent directory
+      if ($p_parent_dir != "") {
+        if (($v_result = $this->privDirCheck($p_parent_dir)) != 1) {
+          return $v_result;
+        }
+      }
+    }
+
+    // ----- Create the directory
+    if (!@mkdir($p_dir, 0777)) {
+      // ----- Error log
+      PclZip::privErrorLog(PCLZIP_ERR_DIR_CREATE_FAIL, "Unable to create directory '$p_dir'");
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privMerge()
+  // Description :
+  //   If $p_archive_to_add does not exist, the function exit with a success result.
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privMerge(&$p_archive_to_add)
+  {
+    $v_result = 1;
+
+    // ----- Look if the archive_to_add exists
+    if (!is_file($p_archive_to_add->zipname)) {
+
+      // ----- Nothing to merge, so merge is a success
+      $v_result = 1;
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Look if the archive exists
+    if (!is_file($this->zipname)) {
+
+      // ----- Do a duplicate
+      $v_result = $this->privDuplicate($p_archive_to_add->zipname);
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Open the zip file
+    if (($v_result = $this->privOpenFd('rb')) != 1) {
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Read the central directory informations
+    $v_central_dir = array();
+    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+      $this->privCloseFd();
+
+      return $v_result;
+    }
+
+    // ----- Go to beginning of File
+    @rewind($this->zip_fd);
+
+    // ----- Open the archive_to_add file
+    if (($v_result = $p_archive_to_add->privOpenFd('rb')) != 1) {
+      $this->privCloseFd();
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Read the central directory informations
+    $v_central_dir_to_add = array();
+    if (($v_result = $p_archive_to_add->privReadEndCentralDir($v_central_dir_to_add)) != 1) {
+      $this->privCloseFd();
+      $p_archive_to_add->privCloseFd();
+
+      return $v_result;
+    }
+
+    // ----- Go to beginning of File
+    @rewind($p_archive_to_add->zip_fd);
+
+    // ----- Creates a temporay file
+    $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.tmp';
+
+    // ----- Open the temporary file in write mode
+    if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0) {
+      $this->privCloseFd();
+      $p_archive_to_add->privCloseFd();
+
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_zip_temp_name . '\' in binary write mode');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Copy the files from the archive to the temporary file
+    // TBC : Here I should better append the file and go back to erase the central dir
+    $v_size = $v_central_dir['offset'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = fread($this->zip_fd, $v_read_size);
+      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Copy the files from the archive_to_add into the temporary file
+    $v_size = $v_central_dir_to_add['offset'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = fread($p_archive_to_add->zip_fd, $v_read_size);
+      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Store the offset of the central dir
+    $v_offset = @ftell($v_zip_temp_fd);
+
+    // ----- Copy the block of file headers from the old archive
+    $v_size = $v_central_dir['size'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @fread($this->zip_fd, $v_read_size);
+      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Copy the block of file headers from the archive_to_add
+    $v_size = $v_central_dir_to_add['size'];
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @fread($p_archive_to_add->zip_fd, $v_read_size);
+      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Merge the file comments
+    $v_comment = $v_central_dir['comment'] . ' ' . $v_central_dir_to_add['comment'];
+
+    // ----- Calculate the size of the (new) central header
+    $v_size = @ftell($v_zip_temp_fd) - $v_offset;
+
+    // ----- Swap the file descriptor
+    // Here is a trick : I swap the temporary fd with the zip fd, in order to use
+    // the following methods on the temporary fil and not the real archive fd
+    $v_swap        = $this->zip_fd;
+    $this->zip_fd  = $v_zip_temp_fd;
+    $v_zip_temp_fd = $v_swap;
+
+    // ----- Create the central dir footer
+    if (($v_result = $this->privWriteCentralHeader($v_central_dir['entries'] + $v_central_dir_to_add['entries'], $v_size, $v_offset, $v_comment)) != 1) {
+      $this->privCloseFd();
+      $p_archive_to_add->privCloseFd();
+      @fclose($v_zip_temp_fd);
+      $this->zip_fd = null;
+
+      // ----- Reset the file list
+      unset($v_header_list);
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Swap back the file descriptor
+    $v_swap        = $this->zip_fd;
+    $this->zip_fd  = $v_zip_temp_fd;
+    $v_zip_temp_fd = $v_swap;
+
+    // ----- Close
+    $this->privCloseFd();
+    $p_archive_to_add->privCloseFd();
+
+    // ----- Close the temporary file
+    @fclose($v_zip_temp_fd);
+
+    // ----- Delete the zip file
+    // TBC : I should test the result ...
+    @unlink($this->zipname);
+
+    // ----- Rename the temporary file
+    // TBC : I should test the result ...
+    //@rename($v_zip_temp_name, $this->zipname);
+    PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privDuplicate()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privDuplicate($p_archive_filename)
+  {
+    $v_result = 1;
+
+    // ----- Look if the $p_archive_filename exists
+    if (!is_file($p_archive_filename)) {
+
+      // ----- Nothing to duplicate, so duplicate is a success.
+      $v_result = 1;
+
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Open the zip file
+    if (($v_result = $this->privOpenFd('wb')) != 1) {
+      // ----- Return
+      return $v_result;
+    }
+
+    // ----- Open the temporary file in write mode
+    if (($v_zip_temp_fd = @fopen($p_archive_filename, 'rb')) == 0) {
+      $this->privCloseFd();
+
+      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive file \'' . $p_archive_filename . '\' in binary write mode');
+
+      // ----- Return
+      return PclZip::errorCode();
+    }
+
+    // ----- Copy the files from the archive to the temporary file
+    // TBC : Here I should better append the file and go back to erase the central dir
+    $v_size = filesize($p_archive_filename);
+    while ($v_size != 0) {
+      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = fread($v_zip_temp_fd, $v_read_size);
+      @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+      $v_size -= $v_read_size;
+    }
+
+    // ----- Close
+    $this->privCloseFd();
+
+    // ----- Close the temporary file
+    @fclose($v_zip_temp_fd);
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privErrorLog()
+  // Description :
+  // Parameters :
+  // --------------------------------------------------------------------------------
+  public function privErrorLog($p_error_code = 0, $p_error_string = '')
+  {
+    if (PCLZIP_ERROR_EXTERNAL == 1) {
+      PclError($p_error_code, $p_error_string);
+    } else {
+      $this->error_code   = $p_error_code;
+      $this->error_string = $p_error_string;
+    }
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privErrorReset()
+  // Description :
+  // Parameters :
+  // --------------------------------------------------------------------------------
+  public function privErrorReset()
+  {
+    if (PCLZIP_ERROR_EXTERNAL == 1) {
+      PclErrorReset();
+    } else {
+      $this->error_code   = 0;
+      $this->error_string = '';
+    }
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privDisableMagicQuotes()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privDisableMagicQuotes()
+  {
+    $v_result = 1;
+
+    // ----- Look if function exists
+    if ((!function_exists("get_magic_quotes_runtime")) || (!function_exists("set_magic_quotes_runtime"))) {
+      return $v_result;
+    }
+
+    // ----- Look if already done
+    if ($this->magic_quotes_status != -1) {
+      return $v_result;
+    }
+
+    // ----- Get and memorize the magic_quote value
+    $this->magic_quotes_status = @get_magic_quotes_runtime();
+
+    // ----- Disable magic_quotes
+    if ($this->magic_quotes_status == 1) {
+      @set_magic_quotes_runtime(0);
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------------
+  // Function : privSwapBackMagicQuotes()
+  // Description :
+  // Parameters :
+  // Return Values :
+  // --------------------------------------------------------------------------------
+  public function privSwapBackMagicQuotes()
+  {
+    $v_result = 1;
+
+    // ----- Look if function exists
+    if ((!function_exists("get_magic_quotes_runtime")) || (!function_exists("set_magic_quotes_runtime"))) {
+      return $v_result;
+    }
+
+    // ----- Look if something to do
+    if ($this->magic_quotes_status != -1) {
+      return $v_result;
+    }
+
+    // ----- Swap back magic_quotes
+    if ($this->magic_quotes_status == 1) {
+      @set_magic_quotes_runtime($this->magic_quotes_status);
+    }
+
+    // ----- Return
+    return $v_result;
+  }
+  // --------------------------------------------------------------------------------
+}
+
+// End of class
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilPathReduction()
+// Description :
+// Parameters :
+// Return Values :
+// --------------------------------------------------------------------------------
+function PclZipUtilPathReduction($p_dir)
+{
+  $v_result = "";
+
+  // ----- Look for not empty path
+  if ($p_dir != "") {
+    // ----- Explode path by directory names
+    $v_list = explode("/", $p_dir);
+
+    // ----- Study directories from last to first
+    $v_skip = 0;
+    for ($i = sizeof($v_list) - 1; $i >= 0; $i--) {
+      // ----- Look for current path
+      if ($v_list[$i] == ".") {
+        // ----- Ignore this directory
+        // Should be the first $i=0, but no check is done
+      } elseif ($v_list[$i] == "..") {
+        $v_skip++;
+      } elseif ($v_list[$i] == "") {
+        // ----- First '/' i.e. root slash
+        if ($i == 0) {
+          $v_result = "/" . $v_result;
+          if ($v_skip > 0) {
+            // ----- It is an invalid path, so the path is not modified
+            // TBC
+            $v_result = $p_dir;
+            $v_skip   = 0;
+          }
+
+          // ----- Last '/' i.e. indicates a directory
+        } elseif ($i == (sizeof($v_list) - 1)) {
+          $v_result = $v_list[$i];
+
+          // ----- Double '/' inside the path
+        } else {
+          // ----- Ignore only the double '//' in path,
+          // but not the first and last '/'
+        }
+      } else {
+        // ----- Look for item to skip
+        if ($v_skip > 0) {
+          $v_skip--;
+        } else {
+          $v_result = $v_list[$i] . ($i != (sizeof($v_list) - 1) ? "/" . $v_result : "");
+        }
+      }
+    }
+
+    // ----- Look for skip
+    if ($v_skip > 0) {
+      while ($v_skip > 0) {
+        $v_result = '../' . $v_result;
+        $v_skip--;
+      }
+    }
+  }
+
+  // ----- Return
+  return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilPathInclusion()
+// Description :
+//   This function indicates if the path $p_path is under the $p_dir tree. Or,
+//   said in an other way, if the file or sub-dir $p_path is inside the dir
+//   $p_dir.
+//   The function indicates also if the path is exactly the same as the dir.
+//   This function supports path with duplicated '/' like '//', but does not
+//   support '.' or '..' statements.
+// Parameters :
+// Return Values :
+//   0 if $p_path is not inside directory $p_dir
+//   1 if $p_path is inside directory $p_dir
+//   2 if $p_path is exactly the same as $p_dir
+// --------------------------------------------------------------------------------
+function PclZipUtilPathInclusion($p_dir, $p_path)
+{
+  $v_result = 1;
+
+  // ----- Look for path beginning by ./
+  if (($p_dir == '.') || ((strlen($p_dir) >= 2) && (substr($p_dir, 0, 2) == './'))) {
+    $p_dir = PclZipUtilTranslateWinPath(getcwd(), false) . '/' . substr($p_dir, 1);
+  }
+  if (($p_path == '.') || ((strlen($p_path) >= 2) && (substr($p_path, 0, 2) == './'))) {
+    $p_path = PclZipUtilTranslateWinPath(getcwd(), false) . '/' . substr($p_path, 1);
+  }
+
+  // ----- Explode dir and path by directory separator
+  $v_list_dir       = explode("/", $p_dir);
+  $v_list_dir_size  = sizeof($v_list_dir);
+  $v_list_path      = explode("/", $p_path);
+  $v_list_path_size = sizeof($v_list_path);
+
+  // ----- Study directories paths
+  $i = 0;
+  $j = 0;
+  while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result)) {
+
+    // ----- Look for empty dir (path reduction)
+    if ($v_list_dir[$i] == '') {
+      $i++;
+      continue;
+    }
+    if ($v_list_path[$j] == '') {
+      $j++;
+      continue;
+    }
+
+    // ----- Compare the items
+    if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ($v_list_path[$j] != '')) {
+      $v_result = 0;
+    }
+
+    // ----- Next items
+    $i++;
+    $j++;
+  }
+
+  // ----- Look if everything seems to be the same
+  if ($v_result) {
+    // ----- Skip all the empty items
+    while (($j < $v_list_path_size) && ($v_list_path[$j] == '')) {
+      $j++;
+    }
+    while (($i < $v_list_dir_size) && ($v_list_dir[$i] == '')) {
+      $i++;
+    }
+
+    if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size)) {
+      // ----- There are exactly the same
+      $v_result = 2;
+    } elseif ($i < $v_list_dir_size) {
+      // ----- The path is shorter than the dir
+      $v_result = 0;
+    }
+  }
+
+  // ----- Return
+  return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilCopyBlock()
+// Description :
+// Parameters :
+//   $p_mode : read/write compression mode
+//             0 : src & dest normal
+//             1 : src gzip, dest normal
+//             2 : src normal, dest gzip
+//             3 : src & dest gzip
+// Return Values :
+// --------------------------------------------------------------------------------
+function PclZipUtilCopyBlock($p_src, $p_dest, $p_size, $p_mode = 0)
+{
+  $v_result = 1;
+
+  if ($p_mode == 0) {
+    while ($p_size != 0) {
+      $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @fread($p_src, $v_read_size);
+      @fwrite($p_dest, $v_buffer, $v_read_size);
+      $p_size -= $v_read_size;
+    }
+  } elseif ($p_mode == 1) {
+    while ($p_size != 0) {
+      $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @gzread($p_src, $v_read_size);
+      @fwrite($p_dest, $v_buffer, $v_read_size);
+      $p_size -= $v_read_size;
+    }
+  } elseif ($p_mode == 2) {
+    while ($p_size != 0) {
+      $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @fread($p_src, $v_read_size);
+      @gzwrite($p_dest, $v_buffer, $v_read_size);
+      $p_size -= $v_read_size;
+    }
+  } elseif ($p_mode == 3) {
+    while ($p_size != 0) {
+      $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+      $v_buffer    = @gzread($p_src, $v_read_size);
+      @gzwrite($p_dest, $v_buffer, $v_read_size);
+      $p_size -= $v_read_size;
+    }
+  }
+
+  // ----- Return
+  return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilRename()
+// Description :
+//   This function tries to do a simple rename() function. If it fails, it
+//   tries to copy the $p_src file in a new $p_dest file and then unlink the
+//   first one.
+// Parameters :
+//   $p_src : Old filename
+//   $p_dest : New filename
+// Return Values :
+//   1 on success, 0 on failure.
+// --------------------------------------------------------------------------------
+function PclZipUtilRename($p_src, $p_dest)
+{
+  $v_result = 1;
+
+  // ----- Try to rename the files
+  if (!@rename($p_src, $p_dest)) {
+
+    // ----- Try to copy & unlink the src
+    if (!@copy($p_src, $p_dest)) {
+      $v_result = 0;
+    } elseif (!@unlink($p_src)) {
+      $v_result = 0;
+    }
+  }
+
+  // ----- Return
+  return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilOptionText()
+// Description :
+//   Translate option value in text. Mainly for debug purpose.
+// Parameters :
+//   $p_option : the option value.
+// Return Values :
+//   The option text value.
+// --------------------------------------------------------------------------------
+function PclZipUtilOptionText($p_option)
+{
+
+  $v_list = get_defined_constants();
+  for (reset($v_list); $v_key = key($v_list); next($v_list)) {
+    $v_prefix = substr($v_key, 0, 10);
+    if ((($v_prefix == 'PCLZIP_OPT') || ($v_prefix == 'PCLZIP_CB_') || ($v_prefix == 'PCLZIP_ATT')) && ($v_list[$v_key] == $p_option)) {
+      return $v_key;
+    }
+  }
+
+  $v_result = 'Unknown';
+
+  return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilTranslateWinPath()
+// Description :
+//   Translate windows path by replacing '\' by '/' and optionally removing
+//   drive letter.
+// Parameters :
+//   $p_path : path to translate.
+//   $p_remove_disk_letter : true | false
+// Return Values :
+//   The path translated.
+// --------------------------------------------------------------------------------
+function PclZipUtilTranslateWinPath($p_path, $p_remove_disk_letter = true)
+{
+  if (stristr(PHP_OS, 'windows')) {
+    // ----- Look for potential disk letter
+    if (($p_remove_disk_letter) && (($v_position = strpos($p_path, ':')) != false)) {
+      $p_path = substr($p_path, $v_position + 1);
+    }
+    // ----- Change potential windows directory separator
+    if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\')) {
+      $p_path = strtr($p_path, '\\', '/');
+    }
+  }
+
+  return $p_path;
+}
+// --------------------------------------------------------------------------------
diff --git a/app/vendor/piwik/decompress/libs/PclZip/readme.txt b/app/vendor/piwik/decompress/libs/PclZip/readme.txt
new file mode 100644
index 000000000..d1b11e258
--- /dev/null
+++ b/app/vendor/piwik/decompress/libs/PclZip/readme.txt
@@ -0,0 +1,421 @@
+// --------------------------------------------------------------------------------
+// PclZip 2.8.2 - readme.txt
+// --------------------------------------------------------------------------------
+// License GNU/LGPL - August 2009
+// Vincent Blavet - vincent@phpconcept.net
+// http://www.phpconcept.net
+// --------------------------------------------------------------------------------
+// $Id: readme.txt,v 1.60 2009/09/30 20:35:21 vblavet Exp $
+// --------------------------------------------------------------------------------
+
+
+
+0 - Sommaire
+============
+    1 - Introduction
+    2 - What's new
+    3 - Corrected bugs
+    4 - Known bugs or limitations
+    5 - License
+    6 - Warning
+    7 - Documentation
+    8 - Author
+    9 - Contribute
+
+1 - Introduction
+================
+
+  PclZip is a library that allow you to manage a Zip archive.
+
+  Full documentation about PclZip can be found here : http://www.phpconcept.net/pclzip
+
+2 - What's new
+==============
+
+  Version 2.8.2 :
+    - PCLZIP_CB_PRE_EXTRACT and PCLZIP_CB_POST_EXTRACT are now supported with 
+      extraction as a string (PCLZIP_OPT_EXTRACT_AS_STRING). The string
+      can also be modified in the post-extract call back.
+    **Bugs correction :
+    - PCLZIP_OPT_REMOVE_ALL_PATH was not working correctly    
+    - Remove use of eval() and do direct call to callback functions
+    - Correct support of 64bits systems (Thanks to WordPress team)
+
+  Version 2.8.1 :
+    - Move option PCLZIP_OPT_BY_EREG to PCLZIP_OPT_BY_PREG because ereg() is
+      deprecated in PHP 5.3. When using option PCLZIP_OPT_BY_EREG, PclZip will
+      automatically replace it by PCLZIP_OPT_BY_PREG.
+  
+  Version 2.8 :
+    - Improve extraction of zip archive for large files by using temporary files
+      This feature is working like the one defined in r2.7.
+      Options are renamed : PCLZIP_OPT_TEMP_FILE_ON, PCLZIP_OPT_TEMP_FILE_OFF,
+      PCLZIP_OPT_TEMP_FILE_THRESHOLD
+    - Add a ratio constant PCLZIP_TEMPORARY_FILE_RATIO to configure the auto
+      sense of temporary file use.
+    - Bug correction : Reduce filepath in returned file list to remove ennoying
+      './/' preambule in file path.
+
+  Version 2.7 :
+    - Improve creation of zip archive for large files :
+      PclZip will now autosense the configured memory and use temporary files
+      when large file is suspected.
+      This feature can also ne triggered by manual options in create() and add()
+      methods. 'PCLZIP_OPT_ADD_TEMP_FILE_ON' force the use of temporary files,
+      'PCLZIP_OPT_ADD_TEMP_FILE_OFF' disable the autosense technic, 
+      'PCLZIP_OPT_ADD_TEMP_FILE_THRESHOLD' allow for configuration of a size
+      threshold to use temporary files.
+      Using "temporary files" rather than "memory" might take more time, but
+      might give the ability to zip very large files :
+      Tested on my win laptop with a 88Mo file :
+        Zip "in-memory" : 18sec (max_execution_time=30, memory_limit=180Mo)
+        Zip "tmporary-files" : 23sec (max_execution_time=30, memory_limit=30Mo)
+    - Replace use of mktime() by time() to limit the E_STRICT error messages.
+    - Bug correction : When adding files with full windows path (drive letter)
+      PclZip is now working. Before, if the drive letter is not the default
+      path, PclZip was not able to add the file.
+
+  Version 2.6 :
+    - Code optimisation
+    - New attributes PCLZIP_ATT_FILE_COMMENT gives the ability to
+      add a comment for a specific file. (Don't really know if this is usefull)
+    - New attribute PCLZIP_ATT_FILE_CONTENT gives the ability to add a string 
+      as a file.
+    - New attribute PCLZIP_ATT_FILE_MTIME modify the timestamp associated with
+      a file.
+    - Correct a bug. Files archived with a timestamp with 0h0m0s were extracted
+      with current time
+    - Add CRC value in the informations returned back for each file after an
+      action.
+    - Add missing closedir() statement.
+    - When adding a folder, and removing the path of this folder, files were
+      incorrectly added with a '/' at the beginning. Which means files are 
+      related to root in unix systems. Corrected.
+    - Add conditional if before constant definition. This will allow users
+      to redefine constants without changing the file, and then improve
+      upgrade of pclzip code for new versions.
+  
+  Version 2.5 :
+    - Introduce the ability to add file/folder with individual properties (file descriptor).
+      This gives for example the ability to change the filename of a zipped file.
+      . Able to add files individually
+      . Able to change full name
+      . Able to change short name
+      . Compatible with global options
+    - New attributes : PCLZIP_ATT_FILE_NAME, PCLZIP_ATT_FILE_NEW_SHORT_NAME, PCLZIP_ATT_FILE_NEW_FULL_NAME
+    - New error code : PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE
+    - Add a security control feature. PclZip can extract any file in any folder
+      of a system. People may use this to upload a zip file and try to override
+      a system file. The PCLZIP_OPT_EXTRACT_DIR_RESTRICTION will give the
+      ability to forgive any directory transversal behavior.
+    - New PCLZIP_OPT_EXTRACT_DIR_RESTRICTION : check extraction path
+    - New error code : PCLZIP_ERR_DIRECTORY_RESTRICTION
+    - Modification in PclZipUtilPathInclusion() : dir and path beginning with ./ will be prepend
+      by current path (getcwd())
+  
+  Version 2.4 :
+    - Code improvment : try to speed up the code by removing unusefull call to pack()
+    - Correct bug in delete() : delete() should be called with no argument. This was not
+      the case in 2.3. This is corrected in 2.4.
+    - Correct a bug in path_inclusion function. When the path has several '../../', the
+      result was bad.
+    - Add a check for magic_quotes_runtime configuration. If enabled, PclZip will 
+      disable it while working and det it back to its original value.
+      This resolve a lots of bad formated archive errors.
+    - Bug correction : PclZip now correctly unzip file in some specific situation,
+      when compressed content has same size as uncompressed content.
+    - Bug correction : When selecting option 'PCLZIP_OPT_REMOVE_ALL_PATH', 
+      directories are not any more created.
+    - Code improvment : correct unclosed opendir(), better handling of . and .. in
+      loops.
+
+
+  Version 2.3 :
+    - Correct a bug with PHP5 : affecting the value 0xFE49FFE0 to a variable does not
+      give the same result in PHP4 and PHP5 ....
+
+  Version 2.2 :
+    - Try development of PCLZIP_OPT_CRYPT .....
+      However this becomes to a stop. To crypt/decrypt I need to multiply 2 long integers,
+      the result (greater than a long) is not supported by PHP. Even the use of bcmath
+      functions does not help. I did not find yet a solution ...;
+    - Add missing '/' at end of directory entries
+    - Check is a file is encrypted or not. Returns status 'unsupported_encryption' and/or
+      error code PCLZIP_ERR_UNSUPPORTED_ENCRYPTION.
+    - Corrected : Bad "version need to extract" field in local file header
+    - Add private method privCheckFileHeaders() in order to check local and central
+      file headers. PclZip is now supporting purpose bit flag bit 3. Purpose bit flag bit 3 gives
+      the ability to have a local file header without size, compressed size and crc filled.
+    - Add a generic status 'error' for file status
+    - Add control of compression type. PclZip only support deflate compression method.
+      Before v2.2, PclZip does not check the compression method used in an archive while
+      extracting. With v2.2 PclZip returns a new error status for a file using an unsupported
+      compression method. New status is "unsupported_compression". New error code is
+      PCLZIP_ERR_UNSUPPORTED_COMPRESSION.
+    - Add optional attribute PCLZIP_OPT_STOP_ON_ERROR. This will stop the extract of files
+      when errors like 'a folder with same name exists' or 'a newer file exists' or
+      'a write protected file' exists, rather than set a status for the concerning file
+      and resume the extract of the zip.
+    - Add optional attribute PCLZIP_OPT_REPLACE_NEWER. This will force, during an extract' the
+      replacement of the file, even if a  newer version of the file exists.
+      Note that today if a file with the same name already exists but is older it will be
+      replaced by the extracted one.
+    - Improve PclZipUtilOption()
+    - Support of zip archive with trailing bytes. Before 2.2, PclZip checks that the central
+      directory structure is the last data in the archive. Crypt encryption/decryption of
+      zip archive put trailing 0 bytes after decryption. PclZip is now supporting this.
+
+  Version 2.1 :
+    - Add the ability to abort the extraction by using a user callback function.
+      The user can now return the value '2' in its callback which indicates to stop the
+      extraction. For a pre call-back extract is stopped before the extration of the current
+      file. For a post call back, the extraction is stopped after.
+    - Add the ability to extract a file (or several files) directly in the standard output.
+      This is done by the new parameter PCLZIP_OPT_EXTRACT_IN_OUTPUT with method extract().
+    - Add support for parameters PCLZIP_OPT_COMMENT, PCLZIP_OPT_ADD_COMMENT,
+      PCLZIP_OPT_PREPEND_COMMENT. This will create, replace, add, or prepend comments
+      in the zip archive.
+    - When merging two archives, the comments are not any more lost, but merged, with a 
+      blank space separator.
+    - Corrected bug : Files are not deleted when all files are asked to be deleted.
+    - Corrected bug : Folders with name '0' made PclZip to abort the create or add feature.
+
+
+  Version 2.0 :
+    ***** Warning : Some new features may break the backward compatibility for your scripts.
+                    Please carefully read the readme file.
+    - Add the ability to delete by Index, name and regular expression. This feature is 
+      performed by the method delete(), which uses the optional parameters
+      PCLZIP_OPT_BY_INDEX, PCLZIP_OPT_BY_NAME, PCLZIP_OPT_BY_EREG or PCLZIP_OPT_BY_PREG.
+    - Add the ability to extract by regular expression. To extract by regexp you must use the method
+      extract(), with the option PCLZIP_OPT_BY_EREG or PCLZIP_OPT_BY_PREG 
+      (depending if you want to use ereg() or preg_match() syntax) followed by the 
+      regular expression pattern.
+    - Add the ability to extract by index, directly with the extract() method. This is a
+      code improvment of the extractByIndex() method.
+    - Add the ability to extract by name. To extract by name you must use the method
+      extract(), with the option PCLZIP_OPT_BY_NAME followed by the filename to
+      extract or an array of filenames to extract. To extract all a folder, use the folder
+      name rather than the filename with a '/' at the end.
+    - Add the ability to add files without compression. This is done with a new attribute
+      which is PCLZIP_OPT_NO_COMPRESSION.
+    - Add the attribute PCLZIP_OPT_EXTRACT_AS_STRING, which allow to extract a file directly
+      in a string without using any file (or temporary file).
+    - Add constant PCLZIP_SEPARATOR for static configuration of filename separators in a single string.
+      The default separator is now a comma (,) and not any more a blank space.
+      THIS BREAK THE BACKWARD COMPATIBILITY : Please check if this may have an impact with
+      your script.
+    - Improve algorythm performance by removing the use of temporary files when adding or 
+      extracting files in an archive.
+    - Add (correct) detection of empty filename zipping. This can occurs when the removed
+      path is the same
+      as a zipped dir. The dir is not zipped (['status'] = filtered), only its content.
+    - Add better support for windows paths (thanks for help from manus@manusfreedom.com).
+    - Corrected bug : When the archive file already exists with size=0, the add() method
+      fails. Corrected in 2.0.
+    - Remove the use of OS_WINDOWS constant. Use php_uname() function rather.
+    - Control the order of index ranges in extract by index feature.
+    - Change the internal management of folders (better handling of internal flag).
+
+
+  Version 1.3 :
+    - Removing the double include check. This is now done by include_once() and require_once()
+      PHP directives.
+    - Changing the error handling mecanism : Remove the use of an external error library.
+      The former PclError...() functions are replaced by internal equivalent methods.
+      By changing the environment variable PCLZIP_ERROR_EXTERNAL you can still use the former library.
+      Introducing the use of constants for error codes rather than integer values. This will help
+      in futur improvment.
+      Introduction of error handling functions like errorCode(), errorName() and errorInfo().
+    - Remove the deprecated use of calling function with arguments passed by reference.
+    - Add the calling of extract(), extractByIndex(), create() and add() functions
+      with variable options rather than fixed arguments.
+    - Add the ability to remove all the file path while extracting or adding,
+      without any need to specify the path to remove.
+      This is available for extract(), extractByIndex(), create() and add() functionS by using
+      the new variable options parameters :
+      - PCLZIP_OPT_REMOVE_ALL_PATH : by indicating this option while calling the fct.
+    - Ability to change the mode of a file after the extraction (chmod()).
+      This is available for extract() and extractByIndex() functionS by using
+      the new variable options parameters.
+      - PCLZIP_OPT_SET_CHMOD : by setting the value of this option.
+    - Ability to definition call-back options. These call-back will be called during the adding,
+      or the extracting of file (extract(), extractByIndex(), create() and add() functions) :
+      - PCLZIP_CB_PRE_EXTRACT : will be called before each extraction of a file. The user
+        can trigerred the change the filename of the extracted file. The user can triggered the
+        skip of the extraction. This is adding a 'skipped' status in the file list result value.
+      - PCLZIP_CB_POST_EXTRACT : will be called after each extraction of a file.
+        Nothing can be triggered from that point.
+      - PCLZIP_CB_PRE_ADD : will be called before each add of a file. The user
+        can trigerred the change the stored filename of the added file. The user can triggered the
+        skip of the add. This is adding a 'skipped' status in the file list result value.
+      - PCLZIP_CB_POST_ADD : will be called after each add of a file.
+        Nothing can be triggered from that point.
+    - Two status are added in the file list returned as function result : skipped & filename_too_long
+      'skipped' is used when a call-back function ask for skipping the file.
+      'filename_too_long' is used while adding a file with a too long filename to archive (the file is
+      not added)
+    - Adding the function PclZipUtilPathInclusion(), that check the inclusion of a path into
+      a directory.
+    - Add a check of the presence of the archive file before some actions (like list, ...)
+    - Add the initialisation of field "index" in header array. This means that by
+      default index will be -1 when not explicitly set by the methods.
+
+  Version 1.2 :
+    - Adding a duplicate function.
+    - Adding a merge function. The merge function is a "quick merge" function,
+      it just append the content of an archive at the end of the first one. There
+      is no check for duplicate files or more recent files.
+    - Improve the search of the central directory end.
+
+  Version 1.1.2 :
+
+    - Changing the license of PclZip. PclZip is now released under the GNU / LGPL license
+      (see License section).
+    - Adding the optional support of a static temporary directory. You will need to configure
+      the constant PCLZIP_TEMPORARY_DIR if you want to use this feature.
+    - Improving the rename() function. In some cases rename() does not work (different
+      Filesystems), so it will be replaced by a copy() + unlink() functions.
+
+  Version 1.1.1 :
+
+    - Maintenance release, no new feature.
+
+  Version 1.1 :
+
+    - New method Add() : adding files in the archive
+    - New method ExtractByIndex() : partial extract of the archive, files are identified by
+      their index in the archive
+    - New method DeleteByIndex() : delete some files/folder entries from the archive,
+      files are identified by their index in the archive.
+    - Adding a test of the zlib extension presence. If not present abort the script.
+
+  Version 1.0.1 :
+
+    - No new feature
+
+
+3 - Corrected bugs
+==================
+
+  Corrected in Version 2.0 :
+    - Corrected : During an extraction, if a call-back fucntion is used and try to skip
+                  a file, all the extraction process is stopped. 
+
+  Corrected in Version 1.3 :
+    - Corrected : Support of static synopsis for method extract() is broken.
+    - Corrected : invalid size of archive content field (0xFF) should be (0xFFFF).
+    - Corrected : When an extract is done with a remove_path parameter, the entry for
+      the directory with exactly the same path is not skipped/filtered.
+    - Corrected : extractByIndex() and deleteByIndex() were not managing index in the
+      right way. For example indexes '1,3-5,11' will only extract files 1 and 11. This
+      is due to a sort of the index resulting table that puts 11 before 3-5 (sort on
+      string and not interger). The sort is temporarilly removed, this means that
+      you must provide a sorted list of index ranges.
+
+  Corrected in Version 1.2 :
+
+    - Nothing.
+
+  Corrected in Version 1.1.2 :
+
+    - Corrected : Winzip is unable to delete or add new files in a PclZip created archives.
+
+  Corrected in Version 1.1.1 :
+
+    - Corrected : When archived file is not compressed (0% compression), the
+      extract method fails.
+
+  Corrected in Version 1.1 :
+
+    - Corrected : Adding a complete tree of folder may result in a bad archive
+      creation.
+
+  Corrected in Version 1.0.1 :
+
+    - Corrected : Error while compressing files greater than PCLZIP_READ_BLOCK_SIZE (default=1024).
+
+
+4 - Known bugs or limitations
+=============================
+
+  Please publish bugs reports in SourceForge :
+    http://sourceforge.net/tracker/?group_id=40254&atid=427564
+
+  In Version 2.x :
+    - PclZip does only support file uncompressed or compressed with deflate (compression method 8)
+    - PclZip does not support password protected zip archive
+    - Some concern were seen when changing mtime of a file while archiving. 
+      Seems to be linked to Daylight Saving Time (PclTest_changing_mtime).
+
+  In Version 1.2 :
+
+    - merge() methods does not check for duplicate files or last date of modifications.
+
+  In Version 1.1 :
+
+    - Limitation : Using 'extract' fields in the file header in the zip archive is not supported.
+    - WinZip is unable to delete a single file in a PclZip created archive. It is also unable to
+      add a file in a PclZip created archive. (Corrected in v.1.2)
+
+  In Version 1.0.1 :
+
+    - Adding a complete tree of folder may result in a bad archive
+      creation. (Corrected in V.1.1).
+    - Path given to methods must be in the unix format (/) and not the Windows format (\).
+      Workaround : Use only / directory separators.
+    - PclZip is using temporary files that are sometime the name of the file with a .tmp or .gz
+      added suffix. Files with these names may already exist and may be overwritten.
+      Workaround : none.
+    - PclZip does not check if the zlib extension is present. If it is absent, the zip
+      file is not created and the lib abort without warning.
+      Workaround : enable the zlib extension on the php install
+
+  In Version 1.0 :
+
+    - Error while compressing files greater than PCLZIP_READ_BLOCK_SIZE (default=1024).
+      (Corrected in v.1.0.1)
+    - Limitation : Multi-disk zip archive are not supported.
+
+
+5 - License
+===========
+
+  Since version 1.1.2, PclZip Library is released under GNU/LGPL license.
+  This library is free, so you can use it at no cost.
+
+  HOWEVER, if you release a script, an application, a library or any kind of
+  code using PclZip library (or a part of it), YOU MUST :
+  - Indicate in the documentation (or a readme file), that your work
+    uses PclZip Library, and make a reference to the author and the web site
+    http://www.phpconcept.net
+  - Gives the ability to the final user to update the PclZip libary.
+
+  I will also appreciate that you send me a mail (vincent@phpconcept.net), just to
+  be aware that someone is using PclZip.
+
+  For more information about GNU/LGPL license : http://www.gnu.org
+
+6 - Warning
+=================
+
+  This library and the associated files are non commercial, non professional work.
+  It should not have unexpected results. However if any damage is caused by this software
+  the author can not be responsible.
+  The use of this software is at the risk of the user.
+
+7 - Documentation
+=================
+  PclZip User Manuel is available in English on PhpConcept : http://www.phpconcept.net/pclzip/man/en/index.php
+  A Russian translation was done by Feskov Kuzma : http://php.russofile.ru/ru/authors/unsort/zip/
+
+8 - Author
+==========
+
+  This software was written by Vincent Blavet (vincent@phpconcept.net) on its leasure time.
+
+9 - Contribute
+==============
+  If you want to contribute to the development of PclZip, please contact vincent@phpconcept.net.
+  If you can help in financing PhpConcept hosting service, please go to
+  http://www.phpconcept.net/soutien.php
diff --git a/app/vendor/piwik/decompress/phpunit.xml b/app/vendor/piwik/decompress/phpunit.xml
deleted file mode 100644
index b9aa39406..000000000
--- a/app/vendor/piwik/decompress/phpunit.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-    
-        
-            ./tests/
-        
-    
-
-    
-        
-            src
-        
-    
-
-
-
diff --git a/app/vendor/piwik/decompress/src/Gzip.php b/app/vendor/piwik/decompress/src/Gzip.php
old mode 100644
new mode 100755
diff --git a/app/vendor/piwik/decompress/src/Tar.php b/app/vendor/piwik/decompress/src/Tar.php
old mode 100644
new mode 100755
diff --git a/app/vendor/piwik/device-detector/composer.json b/app/vendor/piwik/device-detector/composer.json
deleted file mode 100644
index 533e0f5c1..000000000
--- a/app/vendor/piwik/device-detector/composer.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
-    "name": "piwik/device-detector",
-    "type": "library",
-    "description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, media players, mobile apps, feed readers, libraries, etc), operating systems, devices, brands and models.",
-    "keywords": ["useragent","parser","devicedetection"],
-    "homepage": "https://matomo.org",
-    "license": "LGPL-3.0-or-later",
-    "authors": [
-        {
-            "name": "The Matomo Team",
-            "email": "hello@matomo.org",
-            "homepage": "https://matomo.org/team/"
-        }
-    ],
-    "support": {
-        "forum": "http://forum.matomo.org/",
-        "issues": "https://github.com/matomo-org/device-detector/issues",
-        "wiki": "https://dev.matomo.org/",
-        "source": "https://github.com/matomo-org/piwik"
-    },
-    "autoload": {
-        "psr-4": { "DeviceDetector\\": "" }
-    },
-    "require": {
-        "php": ">=5.5",
-        "mustangostang/spyc": "*"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "^4.8.36",
-        "fabpot/php-cs-fixer": "~1.7",
-        "psr/cache": "^1.0",
-        "psr/simple-cache": "^1.0",
-        "matthiasmullie/scrapbook": "@stable"
-    },
-    "suggest": {
-        "doctrine/cache": "Can directly be used for caching purpose",
-        "ext-yaml": "Necessary for using the Pecl YAML parser"
-    },
-    "archive": {
-        "exclude": ["/autoload.php"]
-    }
-}
diff --git a/app/vendor/piwik/ini/composer.json b/app/vendor/piwik/ini/composer.json
deleted file mode 100644
index 7d486b409..000000000
--- a/app/vendor/piwik/ini/composer.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
-    "name": "piwik/ini",
-    "type": "library",
-    "license": "LGPL-3.0",
-    "autoload": {
-        "psr-4": {
-            "Piwik\\Ini\\": "src/"
-        }
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "Piwik\\Tests\\Ini\\": "tests/"
-        }
-    },
-    "require": {
-        "php": ">=5.3.3"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "~4.0",
-        "athletic/athletic": "0.1.*"
-    }
-}
diff --git a/app/vendor/piwik/ini/phpunit.xml b/app/vendor/piwik/ini/phpunit.xml
deleted file mode 100644
index cda26b984..000000000
--- a/app/vendor/piwik/ini/phpunit.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-    
-        
-            ./tests/
-        
-    
-
-
\ No newline at end of file
diff --git a/app/vendor/piwik/network/composer.json b/app/vendor/piwik/network/composer.json
deleted file mode 100644
index 74fd79b11..000000000
--- a/app/vendor/piwik/network/composer.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-    "name": "piwik/network",
-    "type": "library",
-    "license": "LGPL-3.0",
-    "autoload": {
-        "psr-4": {
-            "Piwik\\Network\\": "src/"
-        }
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "Tests\\Piwik\\Network\\": "tests/"
-        }
-    },
-    "require": {
-        "php": ">=5.3.2"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "~4.0"
-    }
-}
diff --git a/app/vendor/piwik/network/phpunit.xml b/app/vendor/piwik/network/phpunit.xml
deleted file mode 100644
index 00124c92c..000000000
--- a/app/vendor/piwik/network/phpunit.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-    
-        
-            ./tests/
-        
-    
-
-
diff --git a/app/vendor/piwik/piwik-php-tracker/PiwikTracker.php b/app/vendor/piwik/piwik-php-tracker/PiwikTracker.php
index 1737f603e..72bdc2a5b 100644
--- a/app/vendor/piwik/piwik-php-tracker/PiwikTracker.php
+++ b/app/vendor/piwik/piwik-php-tracker/PiwikTracker.php
@@ -516,7 +516,7 @@ protected static function domainFixup($domain)
         if (strlen($domain) > 0) {
             $dl = strlen($domain) - 1;
             // remove trailing '.'
-            if ($domain{$dl} === '.') {
+	        if ($domain[$dl] === '.') {
                 $domain = substr($domain, 0, $dl);
             }
             // remove leading '*'
@@ -1674,14 +1674,13 @@ protected function getRequest($idSite)
         if (!empty($this->customParameters)) {
             $customFields = '&' . http_build_query($this->customParameters, '', '&');
         }
-        $baseUrl = $this->getBaseUrl();
-        $start = '?';
-        if (strpos($baseUrl, '?') !== false) {
-        	$start = '&';
-        }
-
-        $url = $baseUrl . $start .
-            '&idsite=' . $idSite .
+	    $baseUrl = $this->getBaseUrl();
+	    $start = '?';
+	    if (strpos($baseUrl, '?') !== false) {
+		    $start = '&';
+	    }
+	    $url = $baseUrl . $start .
+            'idsite=' . $idSite .
             '&rec=1' .
             '&apiv=' . self::VERSION .
             '&r=' . substr(strval(mt_rand()), 2, 6) .
diff --git a/app/vendor/piwik/piwik-php-tracker/composer.json b/app/vendor/piwik/piwik-php-tracker/composer.json
deleted file mode 100644
index 4b7babd14..000000000
--- a/app/vendor/piwik/piwik-php-tracker/composer.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
-    "name": "piwik/piwik-php-tracker",
-    "description": "PHP Client for Piwik Analytics Tracking API",
-    "keywords": ["piwik","tracker","analytics"],
-    "homepage": "http://piwik.org",
-    "license": "BSD-2-Clause",
-    "authors": [
-        {
-            "name": "The Piwik Team",
-            "email": "hello@piwik.org",
-            "homepage": "http://piwik.org/the-piwik-team/"
-        }
-    ],
-    "support": {
-        "forum": "http://forum.piwik.org/",
-        "issues": "https://github.com/piwik/piwik-php-tracker/issues",
-        "source": "https://github.com/piwik/piwik-php-tracker"
-    },
-    "autoload": {
-        "classmap": ["."]
-    }
-}
diff --git a/app/vendor/psr/container/composer.json b/app/vendor/psr/container/composer.json
deleted file mode 100644
index b8ee01265..000000000
--- a/app/vendor/psr/container/composer.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-    "name": "psr/container",
-    "type": "library",
-    "description": "Common Container Interface (PHP FIG PSR-11)",
-    "keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"],
-    "homepage": "https://github.com/php-fig/container",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "PHP-FIG",
-            "homepage": "http://www.php-fig.org/"
-        }
-    ],
-    "require": {
-        "php": ">=5.3.0"
-    },
-    "autoload": {
-        "psr-4": {
-            "Psr\\Container\\": "src/"
-        }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.0.x-dev"
-        }
-    }
-}
diff --git a/app/vendor/psr/log/composer.json b/app/vendor/psr/log/composer.json
deleted file mode 100644
index 87934d707..000000000
--- a/app/vendor/psr/log/composer.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-    "name": "psr/log",
-    "description": "Common interface for logging libraries",
-    "keywords": ["psr", "psr-3", "log"],
-    "homepage": "https://github.com/php-fig/log",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "PHP-FIG",
-            "homepage": "http://www.php-fig.org/"
-        }
-    ],
-    "require": {
-        "php": ">=5.3.0"
-    },
-    "autoload": {
-        "psr-4": {
-            "Psr\\Log\\": "Psr/Log/"
-        }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.0.x-dev"
-        }
-    }
-}
diff --git a/app/vendor/symfony/console/Symfony/Component/Console/composer.json b/app/vendor/symfony/console/Symfony/Component/Console/composer.json
deleted file mode 100644
index 2efbd2cbb..000000000
--- a/app/vendor/symfony/console/Symfony/Component/Console/composer.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
-    "name": "symfony/console",
-    "type": "library",
-    "description": "Symfony Console Component",
-    "keywords": [],
-    "homepage": "https://symfony.com",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "Fabien Potencier",
-            "email": "fabien@symfony.com"
-        },
-        {
-            "name": "Symfony Community",
-            "homepage": "https://symfony.com/contributors"
-        }
-    ],
-    "require": {
-        "php": ">=5.3.3"
-    },
-    "require-dev": {
-        "symfony/phpunit-bridge": "~2.7",
-        "symfony/event-dispatcher": "~2.1",
-        "symfony/process": "~2.1",
-        "psr/log": "~1.0"
-    },
-    "suggest": {
-        "symfony/event-dispatcher": "",
-        "symfony/process": "",
-        "psr/log": "For using the console logger"
-    },
-    "autoload": {
-        "psr-0": { "Symfony\\Component\\Console\\": "" }
-    },
-    "target-dir": "Symfony/Component/Console",
-    "minimum-stability": "dev",
-    "extra": {
-        "branch-alias": {
-            "dev-master": "2.6-dev"
-        }
-    }
-}
diff --git a/app/vendor/symfony/console/Symfony/Component/Console/phpunit.xml.dist b/app/vendor/symfony/console/Symfony/Component/Console/phpunit.xml.dist
deleted file mode 100644
index 729c433aa..000000000
--- a/app/vendor/symfony/console/Symfony/Component/Console/phpunit.xml.dist
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-    
-        
-    
-    
-        
-            ./Tests/
-        
-    
-
-    
-        
-            ./
-            
-                ./Resources
-                ./Tests
-                ./vendor
-            
-        
-    
-
diff --git a/app/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/composer.json b/app/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/composer.json
deleted file mode 100644
index 1b1bd39ac..000000000
--- a/app/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/composer.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-    "name": "symfony/event-dispatcher",
-    "type": "library",
-    "description": "Symfony EventDispatcher Component",
-    "keywords": [],
-    "homepage": "https://symfony.com",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "Fabien Potencier",
-            "email": "fabien@symfony.com"
-        },
-        {
-            "name": "Symfony Community",
-            "homepage": "https://symfony.com/contributors"
-        }
-    ],
-    "require": {
-        "php": ">=5.3.3"
-    },
-    "require-dev": {
-        "symfony/phpunit-bridge": "~2.7",
-        "symfony/dependency-injection": "~2.6",
-        "symfony/expression-language": "~2.6",
-        "symfony/config": "~2.0,>=2.0.5",
-        "symfony/stopwatch": "~2.3",
-        "psr/log": "~1.0"
-    },
-    "suggest": {
-        "symfony/dependency-injection": "",
-        "symfony/http-kernel": ""
-    },
-    "autoload": {
-        "psr-0": { "Symfony\\Component\\EventDispatcher\\": "" }
-    },
-    "target-dir": "Symfony/Component/EventDispatcher",
-    "minimum-stability": "dev",
-    "extra": {
-        "branch-alias": {
-            "dev-master": "2.6-dev"
-        }
-    }
-}
diff --git a/app/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/phpunit.xml.dist b/app/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/phpunit.xml.dist
deleted file mode 100644
index b14fde575..000000000
--- a/app/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/phpunit.xml.dist
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-    
-        
-    
-    
-        
-            ./Tests/
-        
-    
-
-    
-        
-            ./
-            
-                ./Resources
-                ./Tests
-                ./vendor
-            
-        
-    
-
diff --git a/app/vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/composer.json b/app/vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/composer.json
deleted file mode 100644
index d4457d8a3..000000000
--- a/app/vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/composer.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-    "name": "symfony/monolog-bridge",
-    "type": "symfony-bridge",
-    "description": "Symfony Monolog Bridge",
-    "keywords": [],
-    "homepage": "https://symfony.com",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "Fabien Potencier",
-            "email": "fabien@symfony.com"
-        },
-        {
-            "name": "Symfony Community",
-            "homepage": "https://symfony.com/contributors"
-        }
-    ],
-    "require": {
-        "php": ">=5.3.3",
-        "monolog/monolog": "~1.11"
-    },
-    "require-dev": {
-        "symfony/phpunit-bridge": "~2.7",
-        "symfony/http-kernel": "~2.4",
-        "symfony/console": "~2.4",
-        "symfony/event-dispatcher": "~2.2"
-    },
-    "suggest": {
-        "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.",
-        "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings. You need version ~2.3 of the console for it.",
-        "symfony/event-dispatcher": "Needed when using log messages in console commands"
-     },
-    "autoload": {
-        "psr-0": { "Symfony\\Bridge\\Monolog\\": "" }
-    },
-    "target-dir": "Symfony/Bridge/Monolog",
-    "minimum-stability": "dev",
-    "extra": {
-        "branch-alias": {
-            "dev-master": "2.6-dev"
-        }
-    }
-}
diff --git a/app/vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/phpunit.xml.dist b/app/vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/phpunit.xml.dist
deleted file mode 100644
index efd48709d..000000000
--- a/app/vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/phpunit.xml.dist
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-    
-        
-    
-    
-        
-            ./Tests/
-        
-    
-
-    
-        
-            ./
-            
-                ./Resources
-                ./Tests
-            
-        
-    
-
diff --git a/app/vendor/symfony/polyfill-ctype/composer.json b/app/vendor/symfony/polyfill-ctype/composer.json
deleted file mode 100644
index c24e20ca7..000000000
--- a/app/vendor/symfony/polyfill-ctype/composer.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-    "name": "symfony/polyfill-ctype",
-    "type": "library",
-    "description": "Symfony polyfill for ctype functions",
-    "keywords": ["polyfill", "compatibility", "portable", "ctype"],
-    "homepage": "https://symfony.com",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "Gert de Pagter",
-            "email": "BackEndTea@gmail.com"
-        },
-        {
-            "name": "Symfony Community",
-            "homepage": "https://symfony.com/contributors"
-        }
-    ],
-    "require": {
-        "php": ">=5.3.3"
-    },
-    "autoload": {
-        "psr-4": { "Symfony\\Polyfill\\Ctype\\": "" },
-        "files": [ "bootstrap.php" ]
-    },
-    "suggest": {
-        "ext-ctype": "For best performance"
-    },
-    "minimum-stability": "dev",
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.11-dev"
-        }
-    }
-}
diff --git a/app/vendor/szymach/c-pchart/composer.json b/app/vendor/szymach/c-pchart/composer.json
deleted file mode 100644
index 7d571ee3c..000000000
--- a/app/vendor/szymach/c-pchart/composer.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-    "name": "szymach/c-pchart",
-    "license": "GPL-3.0",
-    "type": "project",
-    "description": "Port of \"pChart\" library into PHP 5",
-    "keywords": ["pchart", "pChart", "statistics", "charts", "CpChart", "c-pChart"],
-    "homepage": "https://github.com/szymach/c-pchart",
-    "authors": [
-        {
-            "name": "Jean-Damien Pogolotti",
-            "homepage": "http://www.pchart.net",
-            "role": "Creator of the original pChart library"
-        },
-        {
-            "name": "Piotr Szymaszek",
-            "homepage": "https://github.com/szymach",
-            "role": "Developer of the CpChart wrapper package"
-        }
-    ],
-    "autoload": {
-        "psr-4": { "CpChart\\": "src/" },
-        "files": ["src/Resources/data/constants.php"]
-    },
-    "autoload-dev": {
-        "psr-4": { "Test\\CpChart\\": "tests/" }
-    },
-    "require": {
-        "php": ">=5.4",
-        "ext-gd": "*"
-    },
-    "require-dev" : {
-        "codeception/codeception": "^2.3",
-        "phpunit/phpunit": "^4.8|6.1",
-        "squizlabs/php_codesniffer": "^2.8|3.0"
-    },
-    "config": {
-        "bin-dir": "bin"
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "2.0.x-dev"
-        }
-    }
-}
diff --git a/app/vendor/szymach/c-pchart/coverage.sh b/app/vendor/szymach/c-pchart/coverage.sh
old mode 100644
new mode 100755
diff --git a/app/vendor/tecnickcom/tcpdf/composer.json b/app/vendor/tecnickcom/tcpdf/composer.json
deleted file mode 100644
index 1f19dfd86..000000000
--- a/app/vendor/tecnickcom/tcpdf/composer.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
-  "name": "tecnickcom/tcpdf",
-  "version": "6.2.26",
-  "homepage": "http://www.tcpdf.org/",
-  "type": "library",
-  "description": "TCPDF is a PHP class for generating PDF documents and barcodes.",
-  "keywords": [
-    "PDF",
-    "tcpdf",
-    "PDFD32000-2008",
-    "qrcode",
-    "datamatrix",
-    "pdf417",
-    "barcodes"
-  ],
-  "license": "LGPL-3.0",
-  "authors": [
-    {
-      "name": "Nicola Asuni",
-      "email": "info@tecnick.com",
-      "role": "lead"
-    }
-  ],
-  "require": {
-    "php": ">=5.3.0"
-  },
-  "autoload": {
-    "classmap": [
-      "config",
-      "include",
-      "tcpdf.php",
-      "tcpdf_parser.php",
-      "tcpdf_import.php",
-      "tcpdf_barcodes_1d.php",
-      "tcpdf_barcodes_2d.php",
-      "include/tcpdf_colors.php",
-      "include/tcpdf_filters.php",
-      "include/tcpdf_font_data.php",
-      "include/tcpdf_fonts.php",
-      "include/tcpdf_images.php",
-      "include/tcpdf_static.php",
-      "include/barcodes/datamatrix.php",
-      "include/barcodes/pdf417.php",
-      "include/barcodes/qrcode.php"
-    ]
-  }
-}
diff --git a/app/vendor/tecnickcom/tcpdf/tools/tcpdf_addfont.php b/app/vendor/tecnickcom/tcpdf/tools/tcpdf_addfont.php
old mode 100644
new mode 100755
diff --git a/app/vendor/twig/twig/.php_cs.dist b/app/vendor/twig/twig/.php_cs.dist
index 1b31c0a3d..b81882fbf 100644
--- a/app/vendor/twig/twig/.php_cs.dist
+++ b/app/vendor/twig/twig/.php_cs.dist
@@ -4,6 +4,8 @@ return PhpCsFixer\Config::create()
     ->setRules([
         '@Symfony' => true,
         '@Symfony:risky' => true,
+        '@PHPUnit75Migration:risky' => true,
+        'php_unit_dedicate_assert' => ['target' => '5.6'],
         'array_syntax' => ['syntax' => 'short'],
         'php_unit_fqcn_annotation' => true,
         'no_unreachable_default_argument_value' => false,
diff --git a/app/vendor/twig/twig/CHANGELOG b/app/vendor/twig/twig/CHANGELOG
index 86f3617cf..6ca3d45a1 100644
--- a/app/vendor/twig/twig/CHANGELOG
+++ b/app/vendor/twig/twig/CHANGELOG
@@ -1,3 +1,62 @@
+* 1.42.3 (2019-08-24)
+
+ * fixed the "split" filter when the delimiter is "0"
+ * fixed the "empty" test on Traversable instances
+ * fixed cache when opcache is installed but disabled
+ * fixed PHP 7.4 compatibility
+ * bumped the minimal PHP version to 5.5
+
+* 1.42.2 (2019-06-18)
+
+ * Display partial output (PHP buffer) when an error occurs in debug mode
+
+* 1.42.1 (2019-06-04)
+
+ * added support for "Twig\Markup" instances in the "in" test (again)
+ * allowed string operators as variables names in assignments
+
+* 1.42.0 (2019-05-31)
+
+ * fixed the "filter" filter when the argument is \Traversable but does not implement \Iterator (\SimpleXmlElement for instance)
+ * fixed a PHP fatal error when calling a macro imported in a block in a nested block
+ * fixed a PHP fatal error when calling a macro imported in the template in another macro
+ * fixed wrong error message on "import" and "from"
+
+* 1.41.0 (2019-05-14)
+
+ * fixed support for PHP 7.4
+ * added "filter", "map", and "reduce" filters (and support for arrow functions)
+ * fixed partial output leak when a PHP fatal error occurs
+ * optimized context access on PHP 7.4
+
+* 1.40.1 (2019-04-29)
+
+* fixed regression in NodeTraverser
+
+* 1.40.0 (2019-04-28)
+
+ * allowed Twig\NodeVisitor\NodeVisitorInterface::leaveNode() to return "null" instead of "false" (same meaning)
+ * added the "apply" tag as a replacement for the "filter" tag
+ * allowed Twig\Loader\FilesystemLoader::findTemplate() to return "null" instead of "false" (same meaning)
+ * added support for "Twig\Markup" instances in the "in" test
+ * fixed Lexer when using custom options containing the # char
+ * fixed "import" when macros are stored in a template string
+
+* 1.39.1 (2019-04-16)
+
+ * fixed EscaperNodeVisitor
+
+* 1.39.0 (2019-04-16)
+
+ * added Traversable support for the length filter
+ * fixed some wrong location in error messages
+ * made exception creation faster
+ * made escaping on ternary expressions (?: and ??) more fine-grained
+ * added the possibility to give a nice name to string templates (template_from_string function)
+ * fixed the "with" behavior to always include the globals (for consistency with the "include" and "embed" tags)
+ * fixed "include" with "ignore missing" when an error loading occurs in the included template
+ * added support for a new whitespace trimming option ({%~ ~%}, {{~ ~}}, {#~ ~#})
+
 * 1.38.4 (2019-03-23)
 
  * fixed CheckToStringNode implementation (broken when a function/filter is variadic)
diff --git a/app/vendor/twig/twig/README.rst b/app/vendor/twig/twig/README.rst
index f33ea336d..d896ff500 100644
--- a/app/vendor/twig/twig/README.rst
+++ b/app/vendor/twig/twig/README.rst
@@ -7,6 +7,15 @@ and documentation).
 Twig uses a syntax similar to the Django and Jinja template languages which
 inspired the Twig runtime environment.
 
+Sponsors
+--------
+
+.. raw:: html
+
+    
+        Blackfire.io
+    
+
 More Information
 ----------------
 
diff --git a/app/vendor/twig/twig/composer.json b/app/vendor/twig/twig/composer.json
deleted file mode 100644
index 7eae2f075..000000000
--- a/app/vendor/twig/twig/composer.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
-    "name": "twig/twig",
-    "type": "library",
-    "description": "Twig, the flexible, fast, and secure template language for PHP",
-    "keywords": ["templating"],
-    "homepage": "https://twig.symfony.com",
-    "license": "BSD-3-Clause",
-    "authors": [
-        {
-            "name": "Fabien Potencier",
-            "email": "fabien@symfony.com",
-            "homepage": "http://fabien.potencier.org",
-            "role": "Lead Developer"
-        },
-        {
-            "name": "Twig Team",
-            "homepage": "https://twig.symfony.com/contributors",
-            "role": "Contributors"
-        },
-        {
-            "name": "Armin Ronacher",
-            "email": "armin.ronacher@active-4.com",
-            "role": "Project Founder"
-        }
-    ],
-    "require": {
-        "php": ">=5.4.0",
-        "symfony/polyfill-ctype": "^1.8"
-    },
-    "require-dev": {
-        "symfony/phpunit-bridge": "^3.4.19|^4.1.8",
-        "symfony/debug": "^2.7",
-        "psr/container": "^1.0"
-    },
-    "autoload": {
-        "psr-0" : {
-            "Twig_" : "lib/"
-        },
-        "psr-4" : {
-            "Twig\\" : "src/"
-        }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.38-dev"
-        }
-    }
-}
diff --git a/app/vendor/twig/twig/drupal_test.sh b/app/vendor/twig/twig/drupal_test.sh
new file mode 100755
index 000000000..0374593c2
--- /dev/null
+++ b/app/vendor/twig/twig/drupal_test.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+set -x
+set -e
+
+REPO=`pwd`
+cd /tmp
+rm -rf drupal-twig-test
+composer create-project --no-interaction drupal-composer/drupal-project:8.x-dev drupal-twig-test
+cd drupal-twig-test
+(cd vendor/twig && rm -rf twig && ln -sf $REPO twig)
+echo '$config["system.logging"]["error_level"] = "verbose";' >> web/sites/default/settings.php
+php ./web/core/scripts/drupal install --no-interaction demo_umami > output
+perl -p -i -e 's/^([A-Za-z]+)\: (.+)$/export DRUPAL_\1=\2/' output
+source output
+
+wget https://get.symfony.com/cli/installer -O - | bash
+export PATH="$HOME/.symfony/bin:$PATH"
+symfony server:start -d --no-tls
+ENDPOINT=`symfony server:status -no-ansi | sed -E 's/^.+ http/http/'`
+
+curl -OLsS https://get.blackfire.io/blackfire-player.phar
+chmod +x blackfire-player.phar
+cat > drupal-tests.bkf <
-
-
-  
-    
-      ./test/Twig/
-    
-  
-
-  
-    
-  
-
-  
-    
-  
-
-  
-    
-      ./src/
-    
-  
-
diff --git a/app/vendor/twig/twig/src/Cache/FilesystemCache.php b/app/vendor/twig/twig/src/Cache/FilesystemCache.php
index 7e228799a..b7c1e438e 100644
--- a/app/vendor/twig/twig/src/Cache/FilesystemCache.php
+++ b/app/vendor/twig/twig/src/Cache/FilesystemCache.php
@@ -67,8 +67,8 @@ public function write($key, $content)
 
             if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) {
                 // Compile cached file into bytecode cache
-                if (\function_exists('opcache_invalidate')) {
-                    opcache_invalidate($key, true);
+                if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN)) {
+                    @opcache_invalidate($key, true);
                 } elseif (\function_exists('apc_compile_file')) {
                     apc_compile_file($key);
                 }
diff --git a/app/vendor/twig/twig/src/Environment.php b/app/vendor/twig/twig/src/Environment.php
index f99e4d37a..acd8233a7 100644
--- a/app/vendor/twig/twig/src/Environment.php
+++ b/app/vendor/twig/twig/src/Environment.php
@@ -41,11 +41,11 @@
  */
 class Environment
 {
-    const VERSION = '1.38.4';
-    const VERSION_ID = 13804;
-    const MAJOR_VERSION = 2;
-    const MINOR_VERSION = 38;
-    const RELEASE_VERSION = 4;
+    const VERSION = '1.42.3';
+    const VERSION_ID = 14203;
+    const MAJOR_VERSION = 1;
+    const MINOR_VERSION = 42;
+    const RELEASE_VERSION = 3;
     const EXTRA_VERSION = '';
 
     protected $charset;
@@ -83,7 +83,6 @@ class Environment
     private $runtimeLoaders = [];
     private $runtimes = [];
     private $optionsHash;
-    private $loading = [];
 
     /**
      * Constructor.
@@ -471,6 +470,7 @@ public function loadClass($cls, $name, $index = null)
                 $this->cache->load($key);
             }
 
+            $source = null;
             if (!class_exists($cls, false)) {
                 $loader = $this->getLoader();
                 if (!$loader instanceof SourceContextLoaderInterface) {
@@ -507,22 +507,7 @@ public function loadClass($cls, $name, $index = null)
             $this->initRuntime();
         }
 
-        if (isset($this->loading[$cls])) {
-            throw new RuntimeError(sprintf('Circular reference detected for Twig template "%s", path: %s.', $name, implode(' -> ', array_merge($this->loading, [$name]))));
-        }
-
-        $this->loading[$cls] = $name;
-
-        try {
-            $this->loadedTemplates[$cls] = new $cls($this);
-            unset($this->loading[$cls]);
-        } catch (\Exception $e) {
-            unset($this->loading[$cls]);
-
-            throw $e;
-        }
-
-        return $this->loadedTemplates[$cls];
+        return $this->loadedTemplates[$cls] = new $cls($this);
     }
 
     /**
@@ -531,15 +516,21 @@ public function loadClass($cls, $name, $index = null)
      * This method should not be used as a generic way to load templates.
      *
      * @param string $template The template name
+     * @param string $name     An optional name of the template to be used in error messages
      *
      * @return TemplateWrapper A template instance representing the given template name
      *
      * @throws LoaderError When the template cannot be found
      * @throws SyntaxError When an error occurred during compilation
      */
-    public function createTemplate($template)
+    public function createTemplate($template, $name = null)
     {
-        $name = sprintf('__string_template__%s', hash('sha256', $template, false));
+        $hash = hash('sha256', $template, false);
+        if (null !== $name) {
+            $name = sprintf('%s (string template %s)', $name, $hash);
+        } else {
+            $name = sprintf('__string_template__%s', $hash);
+        }
 
         $loader = new ChainLoader([
             new ArrayLoader([$name => $template]),
diff --git a/app/vendor/twig/twig/src/Error/Error.php b/app/vendor/twig/twig/src/Error/Error.php
index e43a36374..2aa70f153 100644
--- a/app/vendor/twig/twig/src/Error/Error.php
+++ b/app/vendor/twig/twig/src/Error/Error.php
@@ -49,21 +49,15 @@ class Error extends \Exception
     /**
      * Constructor.
      *
-     * Set both the line number and the name to false to
-     * disable automatic guessing of the original template name
-     * and line number.
-     *
      * Set the line number to -1 to enable its automatic guessing.
      * Set the name to null to enable its automatic guessing.
      *
-     * By default, automatic guessing is enabled.
-     *
      * @param string             $message  The error message
      * @param int                $lineno   The template line where the error occurred
      * @param Source|string|null $source   The source context where the error occurred
      * @param \Exception         $previous The previous exception
      */
-    public function __construct($message, $lineno = -1, $source = null, \Exception $previous = null, $autoGuess = true)
+    public function __construct($message, $lineno = -1, $source = null, \Exception $previous = null)
     {
         if (null === $source) {
             $name = null;
@@ -79,13 +73,7 @@ public function __construct($message, $lineno = -1, $source = null, \Exception $
 
         $this->lineno = $lineno;
         $this->filename = $name;
-
-        if ($autoGuess && (-1 === $lineno || null === $name || null === $this->sourcePath)) {
-            $this->guessTemplateInfo();
-        }
-
         $this->rawMessage = $message;
-
         $this->updateRepr();
     }
 
diff --git a/app/vendor/twig/twig/src/Error/LoaderError.php b/app/vendor/twig/twig/src/Error/LoaderError.php
index 5a1cd1ecc..dc5a9f1af 100644
--- a/app/vendor/twig/twig/src/Error/LoaderError.php
+++ b/app/vendor/twig/twig/src/Error/LoaderError.php
@@ -14,22 +14,10 @@
 /**
  * Exception thrown when an error occurs during template loading.
  *
- * Automatic template information guessing is always turned off as
- * if a template cannot be loaded, there is nothing to guess.
- * However, when a template is loaded from another one, then, we need
- * to find the current context and this is automatically done by
- * Twig\Template::displayWithErrorHandling().
- *
- * This strategy makes Twig\Environment::resolveTemplate() much faster.
- *
  * @author Fabien Potencier 
  */
 class LoaderError extends Error
 {
-    public function __construct($message, $lineno = -1, $source = null, \Exception $previous = null)
-    {
-        parent::__construct($message, $lineno, $source, $previous, false);
-    }
 }
 
 class_alias('Twig\Error\LoaderError', 'Twig_Error_Loader');
diff --git a/app/vendor/twig/twig/src/ExpressionParser.php b/app/vendor/twig/twig/src/ExpressionParser.php
index a786e6fb1..9066ade16 100644
--- a/app/vendor/twig/twig/src/ExpressionParser.php
+++ b/app/vendor/twig/twig/src/ExpressionParser.php
@@ -14,6 +14,7 @@
 
 use Twig\Error\SyntaxError;
 use Twig\Node\Expression\ArrayExpression;
+use Twig\Node\Expression\ArrowFunctionExpression;
 use Twig\Node\Expression\AssignNameExpression;
 use Twig\Node\Expression\Binary\ConcatBinary;
 use Twig\Node\Expression\BlockReferenceExpression;
@@ -68,8 +69,12 @@ public function __construct(Parser $parser, $env = null)
         }
     }
 
-    public function parseExpression($precedence = 0)
+    public function parseExpression($precedence = 0, $allowArrow = false)
     {
+        if ($allowArrow && $arrow = $this->parseArrow()) {
+            return $arrow;
+        }
+
         $expr = $this->getPrimary();
         $token = $this->parser->getCurrentToken();
         while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
@@ -98,6 +103,64 @@ public function parseExpression($precedence = 0)
         return $expr;
     }
 
+    /**
+     * @return ArrowFunctionExpression|null
+     */
+    private function parseArrow()
+    {
+        $stream = $this->parser->getStream();
+
+        // short array syntax (one argument, no parentheses)?
+        if ($stream->look(1)->test(Token::ARROW_TYPE)) {
+            $line = $stream->getCurrent()->getLine();
+            $token = $stream->expect(Token::NAME_TYPE);
+            $names = [new AssignNameExpression($token->getValue(), $token->getLine())];
+            $stream->expect(Token::ARROW_TYPE);
+
+            return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
+        }
+
+        // first, determine if we are parsing an arrow function by finding => (long form)
+        $i = 0;
+        if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) {
+            return null;
+        }
+        ++$i;
+        while (true) {
+            // variable name
+            ++$i;
+            if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) {
+                break;
+            }
+            ++$i;
+        }
+        if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) {
+            return null;
+        }
+        ++$i;
+        if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
+            return null;
+        }
+
+        // yes, let's parse it properly
+        $token = $stream->expect(Token::PUNCTUATION_TYPE, '(');
+        $line = $token->getLine();
+
+        $names = [];
+        while (true) {
+            $token = $stream->expect(Token::NAME_TYPE);
+            $names[] = new AssignNameExpression($token->getValue(), $token->getLine());
+
+            if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
+                break;
+            }
+        }
+        $stream->expect(Token::PUNCTUATION_TYPE, ')');
+        $stream->expect(Token::ARROW_TYPE);
+
+        return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
+    }
+
     protected function getPrimary()
     {
         $token = $this->parser->getCurrentToken();
@@ -499,7 +562,7 @@ public function parseFilterExpressionRaw($node, $tag = null)
             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
                 $arguments = new Node();
             } else {
-                $arguments = $this->parseArguments(true);
+                $arguments = $this->parseArguments(true, false, true);
             }
 
             $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
@@ -526,7 +589,7 @@ public function parseFilterExpressionRaw($node, $tag = null)
      *
      * @throws SyntaxError
      */
-    public function parseArguments($namedArguments = false, $definition = false)
+    public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
     {
         $args = [];
         $stream = $this->parser->getStream();
@@ -541,7 +604,7 @@ public function parseArguments($namedArguments = false, $definition = false)
                 $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
                 $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
             } else {
-                $value = $this->parseExpression();
+                $value = $this->parseExpression(0, $allowArrow);
             }
 
             $name = null;
@@ -558,7 +621,7 @@ public function parseArguments($namedArguments = false, $definition = false)
                         throw new SyntaxError(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $stream->getSourceContext());
                     }
                 } else {
-                    $value = $this->parseExpression();
+                    $value = $this->parseExpression(0, $allowArrow);
                 }
             }
 
@@ -586,7 +649,13 @@ public function parseAssignmentExpression()
         $stream = $this->parser->getStream();
         $targets = [];
         while (true) {
-            $token = $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
+            $token = $this->parser->getCurrentToken();
+            if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
+                // in this context, string operators are variable names
+                $this->parser->getStream()->next();
+            } else {
+                $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
+            }
             $value = $token->getValue();
             if (\in_array(strtolower($value), ['true', 'false', 'none', 'null'])) {
                 throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
@@ -627,7 +696,7 @@ private function parseTestExpression(\Twig_NodeInterface $node)
         $class = $this->getTestNodeClass($test);
         $arguments = null;
         if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
-            $arguments = $this->parser->getExpressionParser()->parseArguments(true);
+            $arguments = $this->parseArguments(true);
         }
 
         return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
diff --git a/app/vendor/twig/twig/src/Extension/CoreExtension.php b/app/vendor/twig/twig/src/Extension/CoreExtension.php
index c959e315b..5f3cc24a1 100644
--- a/app/vendor/twig/twig/src/Extension/CoreExtension.php
+++ b/app/vendor/twig/twig/src/Extension/CoreExtension.php
@@ -11,6 +11,7 @@
 
 namespace Twig\Extension {
 use Twig\ExpressionParser;
+use Twig\TokenParser\ApplyTokenParser;
 use Twig\TokenParser\BlockTokenParser;
 use Twig\TokenParser\DeprecatedTokenParser;
 use Twig\TokenParser\DoTokenParser;
@@ -139,6 +140,7 @@ public function getNumberFormat()
     public function getTokenParsers()
     {
         return [
+            new ApplyTokenParser(),
             new ForTokenParser(),
             new IfTokenParser(),
             new ExtendsTokenParser(),
@@ -192,6 +194,9 @@ public function getFilters()
             new TwigFilter('sort', 'twig_sort_filter'),
             new TwigFilter('merge', 'twig_array_merge'),
             new TwigFilter('batch', 'twig_array_batch'),
+            new TwigFilter('filter', 'twig_array_filter'),
+            new TwigFilter('map', 'twig_array_map'),
+            new TwigFilter('reduce', 'twig_array_reduce'),
 
             // string/array filters
             new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]),
@@ -514,7 +519,9 @@ function twig_replace_filter($str, $from, $to = null)
         @trigger_error('Using "replace" with character by character replacement is deprecated since version 1.22 and will be removed in Twig 2.0', E_USER_DEPRECATED);
 
         return strtr($str, $from, $to);
-    } elseif (!twig_test_iterable($from)) {
+    }
+
+    if (!twig_test_iterable($from)) {
         throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from)));
     }
 
@@ -668,7 +675,7 @@ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKe
         if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
             try {
                 return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys);
-            } catch (\OutOfBoundsException $exception) {
+            } catch (\OutOfBoundsException $e) {
                 return [];
             }
         }
@@ -739,9 +746,13 @@ function twig_last(Environment $env, $item)
  */
 function twig_join_filter($value, $glue = '', $and = null)
 {
+    if (!twig_test_iterable($value)) {
+        $value = (array) $value;
+    }
+
     $value = twig_to_array($value, false);
 
-    if (!\is_array($value) || 0 === \count($value)) {
+    if (0 === \count($value)) {
         return '';
     }
 
@@ -779,7 +790,7 @@ function twig_join_filter($value, $glue = '', $and = null)
  */
 function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
 {
-    if (!empty($delimiter)) {
+    if (\strlen($delimiter) > 0) {
         return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
     }
 
@@ -929,6 +940,13 @@ function twig_sort_filter($array)
  */
 function twig_in_filter($value, $compare)
 {
+    if ($value instanceof Markup) {
+        $value = (string) $value;
+    }
+    if ($compare instanceof Markup) {
+        $compare = (string) $compare;
+    }
+
     if (\is_array($compare)) {
         return \in_array($value, $compare, \is_object($value) || \is_resource($value));
     } elseif (\is_string($compare) && (\is_string($value) || \is_int($value) || \is_float($value))) {
@@ -1301,20 +1319,16 @@ function twig_length_filter(Environment $env, $thing)
             return mb_strlen($thing, $env->getCharset());
         }
 
-        if ($thing instanceof \SimpleXMLElement) {
+        if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) {
             return \count($thing);
         }
 
-        if (\is_object($thing) && method_exists($thing, '__toString') && !$thing instanceof \Countable) {
-            return mb_strlen((string) $thing, $env->getCharset());
-        }
-
-        if ($thing instanceof \Countable || \is_array($thing)) {
-            return \count($thing);
+        if ($thing instanceof \Traversable) {
+            return iterator_count($thing);
         }
 
-        if ($thing instanceof \IteratorAggregate) {
-            return iterator_count($thing);
+        if (\is_object($thing) && method_exists($thing, '__toString')) {
+            return mb_strlen((string) $thing, $env->getCharset());
         }
 
         return 1;
@@ -1468,15 +1482,11 @@ function twig_to_array($seq, $preserveKeys = true)
         return iterator_to_array($seq, $preserveKeys);
     }
 
-    if (!is_array($seq)) {
-        return (array) $seq;
-    }
-
-    if(!$preserveKeys) {
-        return array_values($seq);
+    if (!\is_array($seq)) {
+        return $seq;
     }
 
-    return $seq;
+    return $preserveKeys ? $seq : array_values($seq);
 }
 
 /**
@@ -1497,6 +1507,10 @@ function twig_test_empty($value)
         return 0 == \count($value);
     }
 
+    if ($value instanceof \Traversable) {
+        return !iterator_count($value);
+    }
+
     if (\is_object($value) && method_exists($value, '__toString')) {
         return '' === (string) $value;
     }
@@ -1548,9 +1562,9 @@ function twig_include(Environment $env, $context, $template, $variables = [], $w
         }
     }
 
-    $result = '';
+    $loaded = null;
     try {
-        $result = $env->resolveTemplate($template)->render($variables);
+        $loaded = $env->resolveTemplate($template);
     } catch (LoaderError $e) {
         if (!$ignoreMissing) {
             if ($isSandboxed && !$alreadySandboxed) {
@@ -1573,11 +1587,21 @@ function twig_include(Environment $env, $context, $template, $variables = [], $w
         throw $e;
     }
 
+    try {
+        $ret = $loaded ? $loaded->render($variables) : '';
+    } catch (\Exception $e) {
+        if ($isSandboxed && !$alreadySandboxed) {
+            $sandbox->disableSandbox();
+        }
+
+        throw $e;
+    }
+
     if ($isSandboxed && !$alreadySandboxed) {
         $sandbox->disableSandbox();
     }
 
-    return $result;
+    return $ret;
 }
 
 /**
@@ -1649,6 +1673,10 @@ function twig_constant_is_defined($constant, $object = null)
  */
 function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
 {
+    if (!twig_test_iterable($items)) {
+        throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items)));
+    }
+
     $size = ceil($size);
 
     $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys);
@@ -1656,7 +1684,7 @@ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
     if (null !== $fill && $result) {
         $last = \count($result) - 1;
         if ($fillCount = $size - \count($result[$last])) {
-            for ($i = 0; $i < $fillCount; $i++) {
+            for ($i = 0; $i < $fillCount; ++$i) {
                 $result[$last][] = $fill;
             }
         }
@@ -1664,4 +1692,37 @@ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
 
     return $result;
 }
+
+function twig_array_filter($array, $arrow)
+{
+    if (\is_array($array)) {
+        if (\PHP_VERSION_ID >= 50600) {
+            return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
+        }
+
+        return array_filter($array, $arrow);
+    }
+
+    // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
+    return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
+}
+
+function twig_array_map($array, $arrow)
+{
+    $r = [];
+    foreach ($array as $k => $v) {
+        $r[$k] = $arrow($v, $k);
+    }
+
+    return $r;
+}
+
+function twig_array_reduce($array, $arrow, $initial = null)
+{
+    if (!\is_array($array)) {
+        $array = iterator_to_array($array);
+    }
+
+    return array_reduce($array, $arrow, $initial);
+}
 }
diff --git a/app/vendor/twig/twig/src/Extension/StringLoaderExtension.php b/app/vendor/twig/twig/src/Extension/StringLoaderExtension.php
index 9d7e2aa53..93ac834ac 100644
--- a/app/vendor/twig/twig/src/Extension/StringLoaderExtension.php
+++ b/app/vendor/twig/twig/src/Extension/StringLoaderExtension.php
@@ -35,7 +35,7 @@ class_alias('Twig\Extension\StringLoaderExtension', 'Twig_Extension_StringLoader
 
 namespace {
 use Twig\Environment;
-use Twig\Template;
+use Twig\TemplateWrapper;
 
 /**
  * Loads a template from a string.
@@ -43,11 +43,12 @@ class_alias('Twig\Extension\StringLoaderExtension', 'Twig_Extension_StringLoader
  *     {{ include(template_from_string("Hello {{ name }}")) }}
  *
  * @param string $template A template as a string or object implementing __toString()
+ * @param string $name     An optional name of the template to be used in error messages
  *
- * @return Template
+ * @return TemplateWrapper
  */
-function twig_template_from_string(Environment $env, $template)
+function twig_template_from_string(Environment $env, $template, $name = null)
 {
-    return $env->createTemplate((string) $template);
+    return $env->createTemplate((string) $template, $name);
 }
 }
diff --git a/app/vendor/twig/twig/src/Lexer.php b/app/vendor/twig/twig/src/Lexer.php
index fcb5d5511..8cae3597f 100644
--- a/app/vendor/twig/twig/src/Lexer.php
+++ b/app/vendor/twig/twig/src/Lexer.php
@@ -62,20 +62,101 @@ public function __construct(Environment $env, array $options = [])
             'tag_block' => ['{%', '%}'],
             'tag_variable' => ['{{', '}}'],
             'whitespace_trim' => '-',
+            'whitespace_line_trim' => '~',
+            'whitespace_line_chars' => ' \t\0\x0B',
             'interpolation' => ['#{', '}'],
         ], $options);
 
+        // when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default
         $this->regexes = [
-            'lex_var' => '/\s*'.preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_variable'][1], '/').'/A',
-            'lex_block' => '/\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')\n?/A',
-            'lex_raw_data' => '/('.preg_quote($this->options['tag_block'][0].$this->options['whitespace_trim'], '/').'|'.preg_quote($this->options['tag_block'][0], '/').')\s*(?:end%s)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/s',
+            // }}
+            'lex_var' => '{
+                \s*
+                (?:'.
+                    preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '#').'\s*'. // -}}\s*
+                    '|'.
+                    preg_quote($this->options['whitespace_line_trim'].$this->options['tag_variable'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~}}[ \t\0\x0B]*
+                    '|'.
+                    preg_quote($this->options['tag_variable'][1], '#'). // }}
+                ')
+            }Ax',
+
+            // %}
+            'lex_block' => '{
+                \s*
+                (?:'.
+                    preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*\n?'. // -%}\s*\n?
+                    '|'.
+                    preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]*
+                    '|'.
+                    preg_quote($this->options['tag_block'][1], '#').'\n?'. // %}\n?
+                ')
+            }Ax',
+
+            // {% endverbatim %}
+            'lex_raw_data' => '{'.
+                preg_quote($this->options['tag_block'][0], '#'). // {%
+                '('.
+                    $this->options['whitespace_trim']. // -
+                    '|'.
+                    $this->options['whitespace_line_trim']. // ~
+                ')?\s*'.
+                '(?:end%s)'. // endraw or endverbatim
+                '\s*'.
+                '(?:'.
+                    preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%}
+                    '|'.
+                    preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]*
+                    '|'.
+                    preg_quote($this->options['tag_block'][1], '#'). // %}
+                ')
+            }sx',
+
             'operator' => $this->getOperatorRegex(),
-            'lex_comment' => '/(?:'.preg_quote($this->options['whitespace_trim'], '/').preg_quote($this->options['tag_comment'][1], '/').'\s*|'.preg_quote($this->options['tag_comment'][1], '/').')\n?/s',
-            'lex_block_raw' => '/\s*(raw|verbatim)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/As',
-            'lex_block_line' => '/\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '/').'/As',
-            'lex_tokens_start' => '/('.preg_quote($this->options['tag_variable'][0], '/').'|'.preg_quote($this->options['tag_block'][0], '/').'|'.preg_quote($this->options['tag_comment'][0], '/').')('.preg_quote($this->options['whitespace_trim'], '/').')?/s',
-            'interpolation_start' => '/'.preg_quote($this->options['interpolation'][0], '/').'\s*/A',
-            'interpolation_end' => '/\s*'.preg_quote($this->options['interpolation'][1], '/').'/A',
+
+            // #}
+            'lex_comment' => '{
+                (?:'.
+                    preg_quote($this->options['whitespace_trim']).preg_quote($this->options['tag_comment'][1], '#').'\s*\n?'. // -#}\s*\n?
+                    '|'.
+                    preg_quote($this->options['whitespace_line_trim'].$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~#}[ \t\0\x0B]*
+                    '|'.
+                    preg_quote($this->options['tag_comment'][1], '#').'\n?'. // #}\n?
+                ')
+            }sx',
+
+            // verbatim %}
+            'lex_block_raw' => '{
+                \s*
+                (raw|verbatim)
+                \s*
+                (?:'.
+                    preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%}\s*
+                    '|'.
+                    preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]*
+                    '|'.
+                    preg_quote($this->options['tag_block'][1], '#'). // %}
+                ')
+            }Asx',
+
+            'lex_block_line' => '{\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '#').'}As',
+
+            // {{ or {% or {#
+            'lex_tokens_start' => '{
+                ('.
+                    preg_quote($this->options['tag_variable'][0], '#'). // {{
+                    '|'.
+                    preg_quote($this->options['tag_block'][0], '#'). // {%
+                    '|'.
+                    preg_quote($this->options['tag_comment'][0], '#'). // {#
+                ')('.
+                    preg_quote($this->options['whitespace_trim'], '#'). // -
+                    '|'.
+                    preg_quote($this->options['whitespace_line_trim'], '#'). // ~
+                ')?
+            }sx',
+            'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A',
+            'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A',
         ];
     }
 
@@ -175,8 +256,17 @@ protected function lexData()
 
         // push the template text first
         $text = $textContent = substr($this->code, $this->cursor, $position[1] - $this->cursor);
+
+        // trim?
         if (isset($this->positions[2][$this->position][0])) {
-            $text = rtrim($text);
+            if ($this->options['whitespace_trim'] === $this->positions[2][$this->position][0]) {
+                // whitespace_trim detected ({%-, {{- or {#-)
+                $text = rtrim($text);
+            } elseif ($this->options['whitespace_line_trim'] === $this->positions[2][$this->position][0]) {
+                // whitespace_line_trim detected ({%~, {{~ or {#~)
+                // don't trim \r and \n
+                $text = rtrim($text, " \t\0\x0B");
+            }
         }
         $this->pushToken(Token::TEXT_TYPE, $text);
         $this->moveCursor($textContent.$position[0]);
@@ -188,11 +278,11 @@ protected function lexData()
 
             case $this->options['tag_block'][0]:
                 // raw data?
-                if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, null, $this->cursor)) {
+                if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) {
                     $this->moveCursor($match[0]);
                     $this->lexRawData($match[1]);
                 // {% line \d+ %}
-                } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, null, $this->cursor)) {
+                } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) {
                     $this->moveCursor($match[0]);
                     $this->lineno = (int) $match[1];
                 } else {
@@ -212,7 +302,7 @@ protected function lexData()
 
     protected function lexBlock()
     {
-        if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, null, $this->cursor)) {
+        if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
             $this->pushToken(Token::BLOCK_END_TYPE);
             $this->moveCursor($match[0]);
             $this->popState();
@@ -223,7 +313,7 @@ protected function lexBlock()
 
     protected function lexVar()
     {
-        if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, null, $this->cursor)) {
+        if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
             $this->pushToken(Token::VAR_END_TYPE);
             $this->moveCursor($match[0]);
             $this->popState();
@@ -235,7 +325,7 @@ protected function lexVar()
     protected function lexExpression()
     {
         // whitespace
-        if (preg_match('/\s+/A', $this->code, $match, null, $this->cursor)) {
+        if (preg_match('/\s+/A', $this->code, $match, 0, $this->cursor)) {
             $this->moveCursor($match[0]);
 
             if ($this->cursor >= $this->end) {
@@ -243,18 +333,23 @@ protected function lexExpression()
             }
         }
 
+        // arrow function
+        if ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) {
+            $this->pushToken(Token::ARROW_TYPE, '=>');
+            $this->moveCursor('=>');
+        }
         // operators
-        if (preg_match($this->regexes['operator'], $this->code, $match, null, $this->cursor)) {
+        elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
             $this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0]));
             $this->moveCursor($match[0]);
         }
         // names
-        elseif (preg_match(self::REGEX_NAME, $this->code, $match, null, $this->cursor)) {
+        elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) {
             $this->pushToken(Token::NAME_TYPE, $match[0]);
             $this->moveCursor($match[0]);
         }
         // numbers
-        elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, null, $this->cursor)) {
+        elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) {
             $number = (float) $match[0];  // floats
             if (ctype_digit($match[0]) && $number <= PHP_INT_MAX) {
                 $number = (int) $match[0]; // integers lower than the maximum
@@ -284,12 +379,12 @@ protected function lexExpression()
             ++$this->cursor;
         }
         // strings
-        elseif (preg_match(self::REGEX_STRING, $this->code, $match, null, $this->cursor)) {
+        elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
             $this->pushToken(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)));
             $this->moveCursor($match[0]);
         }
         // opening double quoted string
-        elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) {
+        elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
             $this->brackets[] = ['"', $this->lineno];
             $this->pushState(self::STATE_STRING);
             $this->moveCursor($match[0]);
@@ -313,8 +408,16 @@ protected function lexRawData($tag)
         $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
         $this->moveCursor($text.$match[0][0]);
 
-        if (false !== strpos($match[1][0], $this->options['whitespace_trim'])) {
-            $text = rtrim($text);
+        // trim?
+        if (isset($match[1][0])) {
+            if ($this->options['whitespace_trim'] === $match[1][0]) {
+                // whitespace_trim detected ({%-, {{- or {#-)
+                $text = rtrim($text);
+            } else {
+                // whitespace_line_trim detected ({%~, {{~ or {#~)
+                // don't trim \r and \n
+                $text = rtrim($text, " \t\0\x0B");
+            }
         }
 
         $this->pushToken(Token::TEXT_TYPE, $text);
@@ -331,15 +434,15 @@ protected function lexComment()
 
     protected function lexString()
     {
-        if (preg_match($this->regexes['interpolation_start'], $this->code, $match, null, $this->cursor)) {
+        if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) {
             $this->brackets[] = [$this->options['interpolation'][0], $this->lineno];
             $this->pushToken(Token::INTERPOLATION_START_TYPE);
             $this->moveCursor($match[0]);
             $this->pushState(self::STATE_INTERPOLATION);
-        } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, null, $this->cursor) && \strlen($match[0]) > 0) {
+        } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) {
             $this->pushToken(Token::STRING_TYPE, stripcslashes($match[0]));
             $this->moveCursor($match[0]);
-        } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) {
+        } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
             list($expect, $lineno) = array_pop($this->brackets);
             if ('"' != $this->code[$this->cursor]) {
                 throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
@@ -356,7 +459,7 @@ protected function lexString()
     protected function lexInterpolation()
     {
         $bracket = end($this->brackets);
-        if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, null, $this->cursor)) {
+        if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
             array_pop($this->brackets);
             $this->pushToken(Token::INTERPOLATION_END_TYPE);
             $this->moveCursor($match[0]);
diff --git a/app/vendor/twig/twig/src/Loader/FilesystemLoader.php b/app/vendor/twig/twig/src/Loader/FilesystemLoader.php
index c30b41a1b..19b43a295 100644
--- a/app/vendor/twig/twig/src/Loader/FilesystemLoader.php
+++ b/app/vendor/twig/twig/src/Loader/FilesystemLoader.php
@@ -140,19 +140,27 @@ public function getSource($name)
     {
         @trigger_error(sprintf('Calling "getSource" on "%s" is deprecated since 1.27. Use getSourceContext() instead.', \get_class($this)), E_USER_DEPRECATED);
 
-        return file_get_contents($this->findTemplate($name));
+        if (null === ($path = $this->findTemplate($name)) || false === $path) {
+            return '';
+        }
+
+        return file_get_contents($path);
     }
 
     public function getSourceContext($name)
     {
-        $path = $this->findTemplate($name);
+        if (null === ($path = $this->findTemplate($name)) || false === $path) {
+            return new Source('', $name, '');
+        }
 
         return new Source(file_get_contents($path), $name, $path);
     }
 
     public function getCacheKey($name)
     {
-        $path = $this->findTemplate($name);
+        if (null === ($path = $this->findTemplate($name)) || false === $path) {
+            return '';
+        }
         $len = \strlen($this->rootPath);
         if (0 === strncmp($this->rootPath, $path, $len)) {
             return substr($path, $len);
@@ -170,8 +178,8 @@ public function exists($name)
         }
 
         try {
-            return false !== $this->findTemplate($name, false);
-        } catch (LoaderError $exception) {
+            return null !== ($path = $this->findTemplate($name, false)) && false !== $path;
+        } catch (LoaderError $e) {
             @trigger_error(sprintf('In %s::findTemplate(), you must accept a second argument that when set to "false" returns "false" instead of throwing an exception. Not supporting this argument is deprecated since version 1.27.', \get_class($this)), E_USER_DEPRECATED);
 
             return false;
@@ -180,9 +188,21 @@ public function exists($name)
 
     public function isFresh($name, $time)
     {
-        return filemtime($this->findTemplate($name)) < $time;
+        // false support to be removed in 3.0
+        if (null === ($path = $this->findTemplate($name)) || false === $path) {
+            return false;
+        }
+
+        return filemtime($path) < $time;
     }
 
+    /**
+     * Checks if the template can be found.
+     *
+     * @param string $name The template name
+     *
+     * @return string|false|null The template name or false/null
+     */
     protected function findTemplate($name)
     {
         $throw = \func_num_args() > 1 ? func_get_arg(1) : true;
diff --git a/app/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php b/app/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php
new file mode 100644
index 000000000..36b77da86
--- /dev/null
+++ b/app/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php
@@ -0,0 +1,64 @@
+
+ */
+class ArrowFunctionExpression extends AbstractExpression
+{
+    public function __construct(AbstractExpression $expr, Node $names, $lineno, $tag = null)
+    {
+        parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno, $tag);
+    }
+
+    public function compile(Compiler $compiler)
+    {
+        $compiler
+            ->addDebugInfo($this)
+            ->raw('function (')
+        ;
+        foreach ($this->getNode('names') as $i => $name) {
+            if ($i) {
+                $compiler->raw(', ');
+            }
+
+            $compiler
+                ->raw('$__')
+                ->raw($name->getAttribute('name'))
+                ->raw('__')
+            ;
+        }
+        $compiler
+            ->raw(') use ($context) { ')
+        ;
+        foreach ($this->getNode('names') as $name) {
+            $compiler
+                ->raw('$context["')
+                ->raw($name->getAttribute('name'))
+                ->raw('"] = $__')
+                ->raw($name->getAttribute('name'))
+                ->raw('__; ')
+            ;
+        }
+        $compiler
+            ->raw('return ')
+            ->subcompile($this->getNode('expr'))
+            ->raw('; }')
+        ;
+    }
+}
diff --git a/app/vendor/twig/twig/src/Node/Expression/CallExpression.php b/app/vendor/twig/twig/src/Node/Expression/CallExpression.php
index 162393bdf..d202a7395 100644
--- a/app/vendor/twig/twig/src/Node/Expression/CallExpression.php
+++ b/app/vendor/twig/twig/src/Node/Expression/CallExpression.php
@@ -121,7 +121,7 @@ protected function getArguments($callable, $arguments)
                 $named = true;
                 $name = $this->normalizeName($name);
             } elseif ($named) {
-                throw new SyntaxError(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), null, null, false);
+                throw new SyntaxError(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
             }
 
             $parameters[$name] = $node;
@@ -153,14 +153,14 @@ protected function getArguments($callable, $arguments)
 
             if (\array_key_exists($name, $parameters)) {
                 if (\array_key_exists($pos, $parameters)) {
-                    throw new SyntaxError(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), null, null, false);
+                    throw new SyntaxError(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
                 }
 
                 if (\count($missingArguments)) {
                     throw new SyntaxError(sprintf(
                         'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
                         $name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
-                    ), $this->getTemplateLine(), null, null, false);
+                    ), $this->getTemplateLine(), $this->getSourceContext());
                 }
 
                 $arguments = array_merge($arguments, $optionalArguments);
@@ -182,7 +182,7 @@ protected function getArguments($callable, $arguments)
                     $missingArguments[] = $name;
                 }
             } else {
-                throw new SyntaxError(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), null, null, false);
+                throw new SyntaxError(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
             }
         }
 
@@ -212,10 +212,14 @@ protected function getArguments($callable, $arguments)
                 }
             }
 
-            throw new SyntaxError(sprintf(
-                'Unknown argument%s "%s" for %s "%s(%s)".',
-                \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names)
-            ), $unknownParameter ? $unknownParameter->getTemplateLine() : $this->getTemplateLine(), null, null, false);
+            throw new SyntaxError(
+                sprintf(
+                    'Unknown argument%s "%s" for %s "%s(%s)".',
+                    \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names)
+                ),
+                $unknownParameter ? $unknownParameter->getTemplateLine() : $this->getTemplateLine(),
+                $unknownParameter ? $unknownParameter->getSourceContext() : $this->getSourceContext()
+            );
         }
 
         return $arguments;
diff --git a/app/vendor/twig/twig/src/Node/Expression/InlinePrint.php b/app/vendor/twig/twig/src/Node/Expression/InlinePrint.php
new file mode 100644
index 000000000..469e73675
--- /dev/null
+++ b/app/vendor/twig/twig/src/Node/Expression/InlinePrint.php
@@ -0,0 +1,35 @@
+ $node], [], $lineno);
+    }
+
+    public function compile(Compiler $compiler)
+    {
+        $compiler
+            ->raw('print (')
+            ->subcompile($this->getNode('node'))
+            ->raw(')')
+        ;
+    }
+}
diff --git a/app/vendor/twig/twig/src/Node/Expression/NameExpression.php b/app/vendor/twig/twig/src/Node/Expression/NameExpression.php
index 516ba285e..d3f7d107f 100644
--- a/app/vendor/twig/twig/src/Node/Expression/NameExpression.php
+++ b/app/vendor/twig/twig/src/Node/Expression/NameExpression.php
@@ -36,13 +36,20 @@ public function compile(Compiler $compiler)
         if ($this->getAttribute('is_defined_test')) {
             if ($this->isSpecial()) {
                 $compiler->repr(true);
+            } elseif (\PHP_VERSION_ID >= 700400) {
+                $compiler
+                    ->raw('array_key_exists(')
+                    ->string($name)
+                    ->raw(', $context)')
+                ;
             } else {
                 $compiler
                     ->raw('(isset($context[')
                     ->string($name)
                     ->raw(']) || array_key_exists(')
                     ->string($name)
-                    ->raw(', $context))');
+                    ->raw(', $context))')
+                ;
             }
         } elseif ($this->isSpecial()) {
             $compiler->raw($this->specialVars[$name]);
diff --git a/app/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php b/app/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php
index 6197fb93c..2222e11cf 100644
--- a/app/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php
+++ b/app/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php
@@ -47,7 +47,7 @@ public function __construct(\Twig_NodeInterface $node, $name, \Twig_NodeInterfac
         } elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) {
             $node = new ConstantExpression(true, $node->getTemplateLine());
         } else {
-            throw new SyntaxError('The "defined" test only works with simple variables.', $this->getTemplateLine(), null, null, false);
+            throw new SyntaxError('The "defined" test only works with simple variables.', $lineno);
         }
 
         parent::__construct($node, $name, $arguments, $lineno);
diff --git a/app/vendor/twig/twig/src/Node/ImportNode.php b/app/vendor/twig/twig/src/Node/ImportNode.php
index 309bd1ac7..236db8900 100644
--- a/app/vendor/twig/twig/src/Node/ImportNode.php
+++ b/app/vendor/twig/twig/src/Node/ImportNode.php
@@ -46,7 +46,7 @@ public function compile(Compiler $compiler)
                 ->repr($this->getTemplateName())
                 ->raw(', ')
                 ->repr($this->getTemplateLine())
-                ->raw(')')
+                ->raw(')->unwrap()')
             ;
         }
 
diff --git a/app/vendor/twig/twig/src/Node/IncludeNode.php b/app/vendor/twig/twig/src/Node/IncludeNode.php
index 72b924771..544db81ea 100644
--- a/app/vendor/twig/twig/src/Node/IncludeNode.php
+++ b/app/vendor/twig/twig/src/Node/IncludeNode.php
@@ -37,43 +37,54 @@ public function compile(Compiler $compiler)
         $compiler->addDebugInfo($this);
 
         if ($this->getAttribute('ignore_missing')) {
+            $template = $compiler->getVarName();
+
             $compiler
+                ->write(sprintf("$%s = null;\n", $template))
                 ->write("try {\n")
                 ->indent()
+                ->write(sprintf('$%s = ', $template))
             ;
-        }
-
-        $this->addGetTemplate($compiler);
 
-        $compiler->raw('->display(');
+            $this->addGetTemplate($compiler);
 
-        $this->addTemplateArguments($compiler);
-
-        $compiler->raw(");\n");
-
-        if ($this->getAttribute('ignore_missing')) {
             $compiler
+                ->raw(";\n")
                 ->outdent()
                 ->write("} catch (LoaderError \$e) {\n")
                 ->indent()
                 ->write("// ignore missing template\n")
                 ->outdent()
-                ->write("}\n\n")
+                ->write("}\n")
+                ->write(sprintf("if ($%s) {\n", $template))
+                ->indent()
+                ->write(sprintf('$%s->display(', $template))
             ;
+            $this->addTemplateArguments($compiler);
+            $compiler
+                ->raw(");\n")
+                ->outdent()
+                ->write("}\n")
+            ;
+        } else {
+            $this->addGetTemplate($compiler);
+            $compiler->raw('->display(');
+            $this->addTemplateArguments($compiler);
+            $compiler->raw(");\n");
         }
     }
 
     protected function addGetTemplate(Compiler $compiler)
     {
         $compiler
-             ->write('$this->loadTemplate(')
-             ->subcompile($this->getNode('expr'))
-             ->raw(', ')
-             ->repr($this->getTemplateName())
-             ->raw(', ')
-             ->repr($this->getTemplateLine())
-             ->raw(')')
-         ;
+            ->write('$this->loadTemplate(')
+            ->subcompile($this->getNode('expr'))
+            ->raw(', ')
+            ->repr($this->getTemplateName())
+            ->raw(', ')
+            ->repr($this->getTemplateLine())
+            ->raw(')')
+        ;
     }
 
     protected function addTemplateArguments(Compiler $compiler)
diff --git a/app/vendor/twig/twig/src/Node/MacroNode.php b/app/vendor/twig/twig/src/Node/MacroNode.php
index 31f4baa7a..6eb67955f 100644
--- a/app/vendor/twig/twig/src/Node/MacroNode.php
+++ b/app/vendor/twig/twig/src/Node/MacroNode.php
@@ -27,7 +27,7 @@ public function __construct($name, \Twig_NodeInterface $body, \Twig_NodeInterfac
     {
         foreach ($arguments as $argumentName => $argument) {
             if (self::VARARGS_NAME === $argumentName) {
-                throw new SyntaxError(sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), null, null, false);
+                throw new SyntaxError(sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext());
             }
         }
 
@@ -104,7 +104,13 @@ public function compile(Compiler $compiler)
             ->outdent()
             ->write("]);\n\n")
             ->write("\$blocks = [];\n\n")
-            ->write("ob_start();\n")
+        ;
+        if ($compiler->getEnvironment()->isDebug()) {
+            $compiler->write("ob_start();\n");
+        } else {
+            $compiler->write("ob_start(function () { return ''; });\n");
+        }
+        $compiler
             ->write("try {\n")
             ->indent()
             ->subcompile($this->getNode('body'))
diff --git a/app/vendor/twig/twig/src/Node/ModuleNode.php b/app/vendor/twig/twig/src/Node/ModuleNode.php
index 2eb0d49dc..aab2aa33f 100644
--- a/app/vendor/twig/twig/src/Node/ModuleNode.php
+++ b/app/vendor/twig/twig/src/Node/ModuleNode.php
@@ -28,15 +28,13 @@
  */
 class ModuleNode extends Node
 {
-    private $source;
-
     public function __construct(\Twig_NodeInterface $body, AbstractExpression $parent = null, \Twig_NodeInterface $blocks, \Twig_NodeInterface $macros, \Twig_NodeInterface $traits, $embeddedTemplates, $name, $source = '')
     {
         if (!$name instanceof Source) {
             @trigger_error(sprintf('Passing a string as the $name argument of %s() is deprecated since version 1.27. Pass a \Twig\Source instance instead.', __METHOD__), E_USER_DEPRECATED);
-            $this->source = new Source($source, $name);
+            $source = new Source($source, $name);
         } else {
-            $this->source = $name;
+            $source = $name;
         }
 
         $nodes = [
@@ -57,15 +55,16 @@ public function __construct(\Twig_NodeInterface $body, AbstractExpression $paren
         // embedded templates are set as attributes so that they are only visited once by the visitors
         parent::__construct($nodes, [
             // source to be remove in 2.0
-            'source' => $this->source->getCode(),
+            'source' => $source->getCode(),
             // filename to be remove in 2.0 (use getTemplateName() instead)
-            'filename' => $this->source->getName(),
+            'filename' => $source->getName(),
             'index' => null,
             'embedded_templates' => $embeddedTemplates,
         ], 1);
 
         // populate the template name of all node children
-        $this->setTemplateName($this->source->getName());
+        $this->setTemplateName($source->getName());
+        $this->setSourceContext($source);
     }
 
     public function setIndex($index)
@@ -143,7 +142,7 @@ protected function compileGetParent(Compiler $compiler)
                 ->raw('$this->loadTemplate(')
                 ->subcompile($parent)
                 ->raw(', ')
-                ->repr($this->source->getName())
+                ->repr($this->getSourceContext()->getName())
                 ->raw(', ')
                 ->repr($parent->getTemplateLine())
                 ->raw(')')
@@ -178,8 +177,8 @@ protected function compileClassHeader(Compiler $compiler)
         }
         $compiler
             // if the template name contains */, add a blank to avoid a PHP parse error
-            ->write('/* '.str_replace('*/', '* /', $this->source->getName())." */\n")
-            ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->source->getName(), $this->getAttribute('index')))
+            ->write('/* '.str_replace('*/', '* /', $this->getSourceContext()->getName())." */\n")
+            ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getSourceContext()->getName(), $this->getAttribute('index')))
             ->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->getBaseTemplateClass()))
             ->write("{\n")
             ->indent()
@@ -198,17 +197,6 @@ protected function compileConstructor(Compiler $compiler)
         // parent
         if (!$this->hasNode('parent')) {
             $compiler->write("\$this->parent = false;\n\n");
-        } elseif (($parent = $this->getNode('parent')) && $parent instanceof ConstantExpression) {
-            $compiler
-                ->addDebugInfo($parent)
-                ->write('$this->parent = $this->loadTemplate(')
-                ->subcompile($parent)
-                ->raw(', ')
-                ->repr($this->source->getName())
-                ->raw(', ')
-                ->repr($parent->getTemplateLine())
-                ->raw(");\n")
-            ;
         }
 
         $countTraits = \count($this->getNode('traits'));
@@ -217,13 +205,16 @@ protected function compileConstructor(Compiler $compiler)
             foreach ($this->getNode('traits') as $i => $trait) {
                 $this->compileLoadTemplate($compiler, $trait->getNode('template'), sprintf('$_trait_%s', $i));
 
+                $node = $trait->getNode('template');
                 $compiler
-                    ->addDebugInfo($trait->getNode('template'))
+                    ->addDebugInfo($node)
                     ->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i))
                     ->indent()
                     ->write("throw new RuntimeError('Template \"'.")
                     ->subcompile($trait->getNode('template'))
-                    ->raw(".'\" cannot be used as a trait.');\n")
+                    ->raw(".'\" cannot be used as a trait.', ")
+                    ->repr($node->getTemplateLine())
+                    ->raw(", \$this->getSourceContext());\n")
                     ->outdent()
                     ->write("}\n")
                     ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i))
@@ -239,7 +230,9 @@ protected function compileConstructor(Compiler $compiler)
                         ->string($key)
                         ->raw(' is not defined in trait ')
                         ->subcompile($trait->getNode('template'))
-                        ->raw(".'));\n")
+                        ->raw(".'), ")
+                        ->repr($node->getTemplateLine())
+                        ->raw(", \$this->getSourceContext());\n")
                         ->outdent()
                         ->write("}\n\n")
 
@@ -331,8 +324,18 @@ protected function compileDisplay(Compiler $compiler)
 
         if ($this->hasNode('parent')) {
             $parent = $this->getNode('parent');
+
             $compiler->addDebugInfo($parent);
             if ($parent instanceof ConstantExpression) {
+                $compiler
+                    ->write('$this->parent = $this->loadTemplate(')
+                    ->subcompile($parent)
+                    ->raw(', ')
+                    ->repr($this->getSourceContext()->getName())
+                    ->raw(', ')
+                    ->repr($parent->getTemplateLine())
+                    ->raw(");\n")
+                ;
                 $compiler->write('$this->parent');
             } else {
                 $compiler->write('$this->getParent($context)');
@@ -367,7 +370,7 @@ protected function compileGetTemplateName(Compiler $compiler)
             ->write("public function getTemplateName()\n", "{\n")
             ->indent()
             ->write('return ')
-            ->repr($this->source->getName())
+            ->repr($this->getSourceContext()->getName())
             ->raw(";\n")
             ->outdent()
             ->write("}\n\n")
@@ -457,11 +460,11 @@ protected function compileGetSourceContext(Compiler $compiler)
             ->write("public function getSourceContext()\n", "{\n")
             ->indent()
             ->write('return new Source(')
-            ->string($compiler->getEnvironment()->isDebug() ? $this->source->getCode() : '')
+            ->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '')
             ->raw(', ')
-            ->string($this->source->getName())
+            ->string($this->getSourceContext()->getName())
             ->raw(', ')
-            ->string($this->source->getPath())
+            ->string($this->getSourceContext()->getPath())
             ->raw(");\n")
             ->outdent()
             ->write("}\n")
diff --git a/app/vendor/twig/twig/src/Node/Node.php b/app/vendor/twig/twig/src/Node/Node.php
index d60367ff2..c890feb72 100644
--- a/app/vendor/twig/twig/src/Node/Node.php
+++ b/app/vendor/twig/twig/src/Node/Node.php
@@ -13,6 +13,7 @@
 namespace Twig\Node;
 
 use Twig\Compiler;
+use Twig\Source;
 
 /**
  * Represents a node in the AST.
@@ -27,13 +28,9 @@ class Node implements \Twig_NodeInterface
     protected $tag;
 
     private $name;
+    private $sourceContext;
 
     /**
-     * Constructor.
-     *
-     * The nodes are automatically made available as properties ($this->node).
-     * The attributes are automatically made available as array items ($this['name']).
-     *
      * @param array  $nodes      An array of named nodes
      * @param array  $attributes An array of attributes (should not be nodes)
      * @param int    $lineno     The line number
@@ -43,7 +40,7 @@ public function __construct(array $nodes = [], array $attributes = [], $lineno =
     {
         foreach ($nodes as $name => $node) {
             if (!$node instanceof \Twig_NodeInterface) {
-                @trigger_error(sprintf('Using "%s" for the value of node "%s" of "%s" is deprecated since version 1.25 and will be removed in 2.0.', \is_object($node) ? \get_class($node) : null === $node ? 'null' : \gettype($node), $name, \get_class($this)), E_USER_DEPRECATED);
+                @trigger_error(sprintf('Using "%s" for the value of node "%s" of "%s" is deprecated since version 1.25 and will be removed in 2.0.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, \get_class($this)), E_USER_DEPRECATED);
             }
         }
         $this->nodes = $nodes;
@@ -199,7 +196,7 @@ public function getNode($name)
     public function setNode($name, $node = null)
     {
         if (!$node instanceof \Twig_NodeInterface) {
-            @trigger_error(sprintf('Using "%s" for the value of node "%s" of "%s" is deprecated since version 1.25 and will be removed in 2.0.', \is_object($node) ? \get_class($node) : null === $node ? 'null' : \gettype($node), $name, \get_class($this)), E_USER_DEPRECATED);
+            @trigger_error(sprintf('Using "%s" for the value of node "%s" of "%s" is deprecated since version 1.25 and will be removed in 2.0.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, \get_class($this)), E_USER_DEPRECATED);
         }
 
         $this->nodes[$name] = $node;
@@ -235,6 +232,21 @@ public function getTemplateName()
         return $this->name;
     }
 
+    public function setSourceContext(Source $source)
+    {
+        $this->sourceContext = $source;
+        foreach ($this->nodes as $node) {
+            if ($node instanceof Node) {
+                $node->setSourceContext($source);
+            }
+        }
+    }
+
+    public function getSourceContext()
+    {
+        return $this->sourceContext;
+    }
+
     /**
      * @deprecated since 1.27 (to be removed in 2.0)
      */
diff --git a/app/vendor/twig/twig/src/Node/SetNode.php b/app/vendor/twig/twig/src/Node/SetNode.php
index 2c10a3a92..656103b9f 100644
--- a/app/vendor/twig/twig/src/Node/SetNode.php
+++ b/app/vendor/twig/twig/src/Node/SetNode.php
@@ -57,8 +57,12 @@ public function compile(Compiler $compiler)
             $compiler->raw(')');
         } else {
             if ($this->getAttribute('capture')) {
+                if ($compiler->getEnvironment()->isDebug()) {
+                    $compiler->write("ob_start();\n");
+                } else {
+                    $compiler->write("ob_start(function () { return ''; });\n");
+                }
                 $compiler
-                    ->write("ob_start();\n")
                     ->subcompile($this->getNode('values'))
                 ;
             }
diff --git a/app/vendor/twig/twig/src/Node/SpacelessNode.php b/app/vendor/twig/twig/src/Node/SpacelessNode.php
index 9beebf32c..c8d32daf6 100644
--- a/app/vendor/twig/twig/src/Node/SpacelessNode.php
+++ b/app/vendor/twig/twig/src/Node/SpacelessNode.php
@@ -31,7 +31,13 @@ public function compile(Compiler $compiler)
     {
         $compiler
             ->addDebugInfo($this)
-            ->write("ob_start();\n")
+        ;
+        if ($compiler->getEnvironment()->isDebug()) {
+            $compiler->write("ob_start();\n");
+        } else {
+            $compiler->write("ob_start(function () { return ''; });\n");
+        }
+        $compiler
             ->subcompile($this->getNode('body'))
             ->write("echo trim(preg_replace('/>\s+<', ob_get_clean()));\n")
         ;
diff --git a/app/vendor/twig/twig/src/Node/WithNode.php b/app/vendor/twig/twig/src/Node/WithNode.php
index 665aa4b12..f5ae9246d 100644
--- a/app/vendor/twig/twig/src/Node/WithNode.php
+++ b/app/vendor/twig/twig/src/Node/WithNode.php
@@ -35,21 +35,20 @@ public function compile(Compiler $compiler)
         $compiler->addDebugInfo($this);
 
         if ($this->hasNode('variables')) {
+            $node = $this->getNode('variables');
             $varsName = $compiler->getVarName();
             $compiler
                 ->write(sprintf('$%s = ', $varsName))
-                ->subcompile($this->getNode('variables'))
+                ->subcompile($node)
                 ->raw(";\n")
-                ->write(sprintf("if (\$%s instanceof \\Traversable) {\n", $varsName))
+                ->write(sprintf("if (!twig_test_iterable(\$%s)) {\n", $varsName))
                 ->indent()
-                ->write(sprintf("\$%s = iterator_to_array(\$%s);\n", $varsName, $varsName))
-                ->outdent()
-                ->write("}\n")
-                ->write(sprintf("if (!is_array(\$%s)) {\n", $varsName))
-                ->indent()
-                ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.');\n")
+                ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ")
+                ->repr($node->getTemplateLine())
+                ->raw(", \$this->getSourceContext());\n")
                 ->outdent()
                 ->write("}\n")
+                ->write(sprintf("\$%s = twig_to_array(\$%s);\n", $varsName, $varsName))
             ;
 
             if ($this->getAttribute('only')) {
@@ -58,7 +57,7 @@ public function compile(Compiler $compiler)
                 $compiler->write("\$context['_parent'] = \$context;\n");
             }
 
-            $compiler->write(sprintf("\$context = array_merge(\$context, \$%s);\n", $varsName));
+            $compiler->write(sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName));
         } else {
             $compiler->write("\$context['_parent'] = \$context;\n");
         }
diff --git a/app/vendor/twig/twig/src/NodeTraverser.php b/app/vendor/twig/twig/src/NodeTraverser.php
index 8b0f85c52..bd25d3cc7 100644
--- a/app/vendor/twig/twig/src/NodeTraverser.php
+++ b/app/vendor/twig/twig/src/NodeTraverser.php
@@ -69,7 +69,11 @@ protected function traverseForVisitor(NodeVisitorInterface $visitor, \Twig_NodeI
         $node = $visitor->enterNode($node, $this->env);
 
         foreach ($node as $k => $n) {
-            if (false !== $m = $this->traverseForVisitor($visitor, $n)) {
+            if (null === $n) {
+                continue;
+            }
+
+            if (false !== ($m = $this->traverseForVisitor($visitor, $n)) && null !== $m) {
                 if ($m !== $n) {
                     $node->setNode($k, $m);
                 }
diff --git a/app/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php
index 184be8ecb..b66c3c6f1 100644
--- a/app/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php
+++ b/app/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php
@@ -17,6 +17,8 @@
 /**
  * Used to make node visitors compatible with Twig 1.x and 2.x.
  *
+ * To be removed in Twig 3.1.
+ *
  * @author Fabien Potencier 
  */
 abstract class AbstractNodeVisitor implements NodeVisitorInterface
@@ -49,7 +51,7 @@ abstract protected function doEnterNode(Node $node, Environment $env);
     /**
      * Called after child nodes are visited.
      *
-     * @return Node|false The modified node or false if the node must be removed
+     * @return Node|false|null The modified node or null if the node must be removed
      */
     abstract protected function doLeaveNode(Node $node, Environment $env);
 }
diff --git a/app/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php
index 959108f05..f6e16fa7d 100644
--- a/app/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php
+++ b/app/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php
@@ -15,8 +15,11 @@
 use Twig\Node\AutoEscapeNode;
 use Twig\Node\BlockNode;
 use Twig\Node\BlockReferenceNode;
+use Twig\Node\DoNode;
+use Twig\Node\Expression\ConditionalExpression;
 use Twig\Node\Expression\ConstantExpression;
 use Twig\Node\Expression\FilterExpression;
+use Twig\Node\Expression\InlinePrint;
 use Twig\Node\ImportNode;
 use Twig\Node\ModuleNode;
 use Twig\Node\Node;
@@ -69,8 +72,13 @@ protected function doLeaveNode(Node $node, Environment $env)
             $this->blocks = [];
         } elseif ($node instanceof FilterExpression) {
             return $this->preEscapeFilterNode($node, $env);
-        } elseif ($node instanceof PrintNode) {
-            return $this->escapePrintNode($node, $env, $this->needEscaping($env));
+        } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping($env)) {
+            $expression = $node->getNode('expr');
+            if ($expression instanceof ConditionalExpression && $this->shouldUnwrapConditional($expression, $env, $type)) {
+                return new DoNode($this->unwrapConditional($expression, $env, $type), $expression->getTemplateLine());
+            }
+
+            return $this->escapePrintNode($node, $env, $type);
         }
 
         if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) {
@@ -82,6 +90,44 @@ protected function doLeaveNode(Node $node, Environment $env)
         return $node;
     }
 
+    private function shouldUnwrapConditional(ConditionalExpression $expression, Environment $env, $type)
+    {
+        $expr2Safe = $this->isSafeFor($type, $expression->getNode('expr2'), $env);
+        $expr3Safe = $this->isSafeFor($type, $expression->getNode('expr3'), $env);
+
+        return $expr2Safe !== $expr3Safe;
+    }
+
+    private function unwrapConditional(ConditionalExpression $expression, Environment $env, $type)
+    {
+        // convert "echo a ? b : c" to "a ? echo b : echo c" recursively
+        $expr2 = $expression->getNode('expr2');
+        if ($expr2 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr2, $env, $type)) {
+            $expr2 = $this->unwrapConditional($expr2, $env, $type);
+        } else {
+            $expr2 = $this->escapeInlinePrintNode(new InlinePrint($expr2, $expr2->getTemplateLine()), $env, $type);
+        }
+        $expr3 = $expression->getNode('expr3');
+        if ($expr3 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr3, $env, $type)) {
+            $expr3 = $this->unwrapConditional($expr3, $env, $type);
+        } else {
+            $expr3 = $this->escapeInlinePrintNode(new InlinePrint($expr3, $expr3->getTemplateLine()), $env, $type);
+        }
+
+        return new ConditionalExpression($expression->getNode('expr1'), $expr2, $expr3, $expression->getTemplateLine());
+    }
+
+    private function escapeInlinePrintNode(InlinePrint $node, Environment $env, $type)
+    {
+        $expression = $node->getNode('node');
+
+        if ($this->isSafeFor($type, $expression, $env)) {
+            return $node;
+        }
+
+        return new InlinePrint($this->getEscaperFilter($type, $expression), $node->getTemplateLine());
+    }
+
     protected function escapePrintNode(PrintNode $node, Environment $env, $type)
     {
         if (false === $type) {
@@ -96,10 +142,7 @@ protected function escapePrintNode(PrintNode $node, Environment $env, $type)
 
         $class = \get_class($node);
 
-        return new $class(
-            $this->getEscaperFilter($type, $expression),
-            $node->getTemplateLine()
-        );
+        return new $class($this->getEscaperFilter($type, $expression), $node->getTemplateLine());
     }
 
     protected function preEscapeFilterNode(FilterExpression $filter, Environment $env)
diff --git a/app/vendor/twig/twig/src/NodeVisitor/NodeVisitorInterface.php b/app/vendor/twig/twig/src/NodeVisitor/NodeVisitorInterface.php
index 5e21c4f61..9b8730b48 100644
--- a/app/vendor/twig/twig/src/NodeVisitor/NodeVisitorInterface.php
+++ b/app/vendor/twig/twig/src/NodeVisitor/NodeVisitorInterface.php
@@ -30,7 +30,7 @@ public function enterNode(\Twig_NodeInterface $node, Environment $env);
     /**
      * Called after child nodes are visited.
      *
-     * @return \Twig_NodeInterface|false The modified node or false if the node must be removed
+     * @return \Twig_NodeInterface|false|null The modified node or null if the node must be removed
      */
     public function leaveNode(\Twig_NodeInterface $node, Environment $env);
 
diff --git a/app/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php
index c1d302a89..c9403398f 100644
--- a/app/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php
+++ b/app/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php
@@ -102,7 +102,7 @@ protected function doLeaveNode(Node $node, Environment $env)
         if ($node instanceof ModuleNode) {
             $this->inAModule = false;
 
-            $node->setNode('constructor_end', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('display_start')]));
+            $node->getNode('constructor_end')->setNode('_security_check', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('display_start')]));
         } elseif ($this->inAModule) {
             if ($node instanceof PrintNode || $node instanceof SetNode) {
                 $this->needsToStringWrap = false;
diff --git a/app/vendor/twig/twig/src/Parser.php b/app/vendor/twig/twig/src/Parser.php
index 444d4c5a1..0ea102cc8 100644
--- a/app/vendor/twig/twig/src/Parser.php
+++ b/app/vendor/twig/twig/src/Parser.php
@@ -251,7 +251,7 @@ public function getBlockStack()
 
     public function peekBlockStack()
     {
-        return $this->blockStack[\count($this->blockStack) - 1];
+        return isset($this->blockStack[\count($this->blockStack) - 1]) ? $this->blockStack[\count($this->blockStack) - 1] : null;
     }
 
     public function popBlockStack()
@@ -334,10 +334,18 @@ public function addImportedSymbol($type, $alias, $name = null, AbstractExpressio
 
     public function getImportedSymbol($type, $alias)
     {
-        foreach ($this->importedSymbols as $functions) {
-            if (isset($functions[$type][$alias])) {
-                return $functions[$type][$alias];
+        if (null !== $this->peekBlockStack()) {
+            foreach ($this->importedSymbols as $functions) {
+                if (isset($functions[$type][$alias])) {
+                    if (\count($this->blockStack) > 1) {
+                        return null;
+                    }
+
+                    return $functions[$type][$alias];
+                }
             }
+        } else {
+            return isset($this->importedSymbols[0][$type][$alias]) ? $this->importedSymbols[0][$type][$alias] : null;
         }
     }
 
diff --git a/app/vendor/twig/twig/src/Profiler/Profile.php b/app/vendor/twig/twig/src/Profiler/Profile.php
index f33963b31..d83da40af 100644
--- a/app/vendor/twig/twig/src/Profiler/Profile.php
+++ b/app/vendor/twig/twig/src/Profiler/Profile.php
@@ -86,7 +86,7 @@ public function addProfile(self $profile)
     /**
      * Returns the duration in microseconds.
      *
-     * @return int
+     * @return float
      */
     public function getDuration()
     {
@@ -160,12 +160,28 @@ public function getIterator()
 
     public function serialize()
     {
-        return serialize([$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles]);
+        return serialize($this->__serialize());
     }
 
     public function unserialize($data)
     {
-        list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = unserialize($data);
+        $this->__unserialize(unserialize($data));
+    }
+
+    /**
+     * @internal
+     */
+    public function __serialize()
+    {
+        return [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles];
+    }
+
+    /**
+     * @internal
+     */
+    public function __unserialize(array $data)
+    {
+        list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = $data;
     }
 }
 
diff --git a/app/vendor/twig/twig/src/Sandbox/SecurityPolicyInterface.php b/app/vendor/twig/twig/src/Sandbox/SecurityPolicyInterface.php
index d2d783d48..a31863f6c 100644
--- a/app/vendor/twig/twig/src/Sandbox/SecurityPolicyInterface.php
+++ b/app/vendor/twig/twig/src/Sandbox/SecurityPolicyInterface.php
@@ -12,7 +12,7 @@
 namespace Twig\Sandbox;
 
 /**
- * Interfaces that all security policy classes must implements.
+ * Interface that all security policy classes must implements.
  *
  * @author Fabien Potencier 
  */
diff --git a/app/vendor/twig/twig/src/Template.php b/app/vendor/twig/twig/src/Template.php
index 8889c024d..3f7447c12 100644
--- a/app/vendor/twig/twig/src/Template.php
+++ b/app/vendor/twig/twig/src/Template.php
@@ -227,7 +227,10 @@ public function displayBlock($name, array $context, array $blocks = [], $useBloc
 
                 throw $e;
             } catch (\Exception $e) {
-                throw new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
+                $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
+                $e->guess();
+
+                throw $e;
             }
         } elseif (false !== $parent = $this->getParent($context)) {
             $parent->displayBlock($name, $context, array_merge($this->blocks, $blocks), false);
@@ -250,7 +253,11 @@ public function displayBlock($name, array $context, array $blocks = [], $useBloc
      */
     public function renderParentBlock($name, array $context, array $blocks = [])
     {
-        ob_start();
+        if ($this->env->isDebug()) {
+            ob_start();
+        } else {
+            ob_start(function () { return ''; });
+        }
         $this->displayParentBlock($name, $context, $blocks);
 
         return ob_get_clean();
@@ -271,7 +278,11 @@ public function renderParentBlock($name, array $context, array $blocks = [])
      */
     public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true)
     {
-        ob_start();
+        if ($this->env->isDebug()) {
+            ob_start();
+        } else {
+            ob_start(function () { return ''; });
+        }
         $this->displayBlock($name, $context, $blocks, $useBlocks);
 
         return ob_get_clean();
@@ -340,6 +351,9 @@ public function getBlockNames(array $context = null, array $blocks = [])
         return array_unique($names);
     }
 
+    /**
+     * @return Template|TemplateWrapper
+     */
     protected function loadTemplate($template, $templateName = null, $line = null, $index = null)
     {
         try {
@@ -352,7 +366,7 @@ protected function loadTemplate($template, $templateName = null, $line = null, $
             }
 
             if ($template === $this->getTemplateName()) {
-                $class = get_class($this);
+                $class = \get_class($this);
                 if (false !== $pos = strrpos($class, '___', -1)) {
                     $class = substr($class, 0, $pos);
                 }
@@ -380,6 +394,16 @@ protected function loadTemplate($template, $templateName = null, $line = null, $
         }
     }
 
+    /**
+     * @internal
+     *
+     * @return Template
+     */
+    protected function unwrap()
+    {
+        return $this;
+    }
+
     /**
      * Returns all blocks.
      *
@@ -401,7 +425,11 @@ public function display(array $context, array $blocks = [])
     public function render(array $context)
     {
         $level = ob_get_level();
-        ob_start();
+        if ($this->env->isDebug()) {
+            ob_start();
+        } else {
+            ob_start(function () { return ''; });
+        }
         try {
             $this->display($context);
         } catch (\Exception $e) {
@@ -438,7 +466,10 @@ protected function displayWithErrorHandling(array $context, array $blocks = [])
 
             throw $e;
         } catch (\Exception $e) {
-            throw new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
+            $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
+            $e->guess();
+
+            throw $e;
         }
     }
 
@@ -506,7 +537,7 @@ protected function getAttribute($object, $item, array $arguments = [], $type = s
         if (self::METHOD_CALL !== $type) {
             $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
 
-            if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, $object)))
+            if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object)))
                 || ($object instanceof \ArrayAccess && isset($object[$arrayItem]))
             ) {
                 if ($isDefinedTest) {
@@ -573,7 +604,7 @@ protected function getAttribute($object, $item, array $arguments = [], $type = s
 
         // object property
         if (self::METHOD_CALL !== $type && !$object instanceof self) { // \Twig\Template does not have public properties, and we don't want to allow access to internal ones
-            if (isset($object->$item) || \array_key_exists((string) $item, $object)) {
+            if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) {
                 if ($isDefinedTest) {
                     return true;
                 }
diff --git a/app/vendor/twig/twig/src/TemplateWrapper.php b/app/vendor/twig/twig/src/TemplateWrapper.php
index 5ddee9229..e2654094e 100644
--- a/app/vendor/twig/twig/src/TemplateWrapper.php
+++ b/app/vendor/twig/twig/src/TemplateWrapper.php
@@ -96,7 +96,11 @@ public function renderBlock($name, $context = [])
     {
         $context = $this->env->mergeGlobals($context);
         $level = ob_get_level();
-        ob_start();
+        if ($this->env->isDebug()) {
+            ob_start();
+        } else {
+            ob_start(function () { return ''; });
+        }
         try {
             $this->template->displayBlock($name, $context);
         } catch (\Exception $e) {
@@ -138,10 +142,20 @@ public function getSourceContext()
     /**
      * @return string
      */
-    public function getTemplatename()
+    public function getTemplateName()
     {
         return $this->template->getTemplateName();
     }
+
+    /**
+     * @internal
+     *
+     * @return Template
+     */
+    public function unwrap()
+    {
+        return $this->template;
+    }
 }
 
 class_alias('Twig\TemplateWrapper', 'Twig_TemplateWrapper');
diff --git a/app/vendor/twig/twig/src/Test/IntegrationTestCase.php b/app/vendor/twig/twig/src/Test/IntegrationTestCase.php
index d1b633ab9..36b360758 100644
--- a/app/vendor/twig/twig/src/Test/IntegrationTestCase.php
+++ b/app/vendor/twig/twig/src/Test/IntegrationTestCase.php
@@ -194,7 +194,7 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e
                     $message = $e->getMessage();
                     $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $message)));
                     $last = substr($message, \strlen($message) - 1);
-                    $this->assertTrue('.' === $last || '?' === $last, $message, 'Exception message must end with a dot or a question mark.');
+                    $this->assertTrue('.' === $last || '?' === $last, 'Exception message must end with a dot or a question mark.');
 
                     return;
                 }
diff --git a/app/vendor/twig/twig/src/Test/NodeTestCase.php b/app/vendor/twig/twig/src/Test/NodeTestCase.php
index b35cd2185..f3358cb9a 100644
--- a/app/vendor/twig/twig/src/Test/NodeTestCase.php
+++ b/app/vendor/twig/twig/src/Test/NodeTestCase.php
@@ -56,7 +56,7 @@ protected function getVariableGetter($name, $line = false)
         $line = $line > 0 ? "// line {$line}\n" : '';
 
         if (\PHP_VERSION_ID >= 70000) {
-            return sprintf('%s($context["%s"] ?? null)', $line, $name, $name);
+            return sprintf('%s($context["%s"] ?? null)', $line, $name);
         }
 
         if (\PHP_VERSION_ID >= 50400) {
diff --git a/app/vendor/twig/twig/src/Token.php b/app/vendor/twig/twig/src/Token.php
index 933897299..a0bb11af1 100644
--- a/app/vendor/twig/twig/src/Token.php
+++ b/app/vendor/twig/twig/src/Token.php
@@ -38,6 +38,7 @@ class Token
     const PUNCTUATION_TYPE = 9;
     const INTERPOLATION_START_TYPE = 10;
     const INTERPOLATION_END_TYPE = 11;
+    const ARROW_TYPE = 12;
 
     /**
      * @param int    $type   The type of the token
@@ -157,6 +158,9 @@ public static function typeToString($type, $short = false)
             case self::INTERPOLATION_END_TYPE:
                 $name = 'INTERPOLATION_END_TYPE';
                 break;
+            case self::ARROW_TYPE:
+                $name = 'ARROW_TYPE';
+                break;
             default:
                 throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type));
         }
@@ -200,6 +204,8 @@ public static function typeToEnglish($type)
                 return 'begin of string interpolation';
             case self::INTERPOLATION_END_TYPE:
                 return 'end of string interpolation';
+            case self::ARROW_TYPE:
+                return 'arrow function';
             default:
                 throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type));
         }
diff --git a/app/vendor/twig/twig/src/TokenParser/AbstractTokenParser.php b/app/vendor/twig/twig/src/TokenParser/AbstractTokenParser.php
index fc8c11a23..2c2f90b7f 100644
--- a/app/vendor/twig/twig/src/TokenParser/AbstractTokenParser.php
+++ b/app/vendor/twig/twig/src/TokenParser/AbstractTokenParser.php
@@ -20,6 +20,9 @@
  */
 abstract class AbstractTokenParser implements TokenParserInterface
 {
+    /**
+     * @var Parser
+     */
     protected $parser;
 
     public function setParser(Parser $parser)
diff --git a/app/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php b/app/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php
new file mode 100644
index 000000000..879879a2b
--- /dev/null
+++ b/app/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php
@@ -0,0 +1,58 @@
+getLine();
+        $name = $this->parser->getVarName();
+
+        $ref = new TempNameExpression($name, $lineno);
+        $ref->setAttribute('always_defined', true);
+
+        $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref, $this->getTag());
+
+        $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
+        $body = $this->parser->subparse([$this, 'decideApplyEnd'], true);
+        $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
+
+        return new Node([
+            new SetNode(true, $ref, $body, $lineno, $this->getTag()),
+            new PrintNode($filter, $lineno, $this->getTag()),
+        ]);
+    }
+
+    public function decideApplyEnd(Token $token)
+    {
+        return $token->test('endapply');
+    }
+
+    public function getTag()
+    {
+        return 'apply';
+    }
+}
diff --git a/app/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php b/app/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php
index 74f129c56..e66789a7b 100644
--- a/app/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php
+++ b/app/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php
@@ -13,6 +13,7 @@
 namespace Twig\TokenParser;
 
 use Twig\Error\SyntaxError;
+use Twig\Node\Node;
 use Twig\Token;
 
 /**
@@ -28,8 +29,10 @@ public function parse(Token $token)
     {
         $stream = $this->parser->getStream();
 
-        if (!$this->parser->isMainScope()) {
-            throw new SyntaxError('Cannot extend from a block.', $token->getLine(), $stream->getSourceContext());
+        if ($this->parser->peekBlockStack()) {
+            throw new SyntaxError('Cannot use "extend" in a block.', $token->getLine(), $stream->getSourceContext());
+        } elseif (!$this->parser->isMainScope()) {
+            throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext());
         }
 
         if (null !== $this->parser->getParent()) {
@@ -38,6 +41,8 @@ public function parse(Token $token)
         $this->parser->setParent($this->parser->getExpressionParser()->parseExpression());
 
         $stream->expect(Token::BLOCK_END_TYPE);
+
+        return new Node();
     }
 
     public function getTag()
diff --git a/app/vendor/twig/twig/src/TokenParser/FromTokenParser.php b/app/vendor/twig/twig/src/TokenParser/FromTokenParser.php
index a47f197e8..4cce650d6 100644
--- a/app/vendor/twig/twig/src/TokenParser/FromTokenParser.php
+++ b/app/vendor/twig/twig/src/TokenParser/FromTokenParser.php
@@ -29,7 +29,7 @@ public function parse(Token $token)
     {
         $macro = $this->parser->getExpressionParser()->parseExpression();
         $stream = $this->parser->getStream();
-        $stream->expect('import');
+        $stream->expect(Token::NAME_TYPE, 'import');
 
         $targets = [];
         do {
@@ -49,14 +49,15 @@ public function parse(Token $token)
 
         $stream->expect(Token::BLOCK_END_TYPE);
 
-        $node = new ImportNode($macro, new AssignNameExpression($this->parser->getVarName(), $token->getLine()), $token->getLine(), $this->getTag());
+        $var = new AssignNameExpression($this->parser->getVarName(), $token->getLine());
+        $node = new ImportNode($macro, $var, $token->getLine(), $this->getTag());
 
         foreach ($targets as $name => $alias) {
             if ($this->parser->isReservedMacroName($name)) {
                 throw new SyntaxError(sprintf('"%s" cannot be an imported macro as it is a reserved keyword.', $name), $token->getLine(), $stream->getSourceContext());
             }
 
-            $this->parser->addImportedSymbol('function', $alias, 'get'.$name, $node->getNode('var'));
+            $this->parser->addImportedSymbol('function', $alias, 'get'.$name, $var);
         }
 
         return $node;
diff --git a/app/vendor/twig/twig/src/TokenParser/ImportTokenParser.php b/app/vendor/twig/twig/src/TokenParser/ImportTokenParser.php
index 317e0a5ea..88395b924 100644
--- a/app/vendor/twig/twig/src/TokenParser/ImportTokenParser.php
+++ b/app/vendor/twig/twig/src/TokenParser/ImportTokenParser.php
@@ -27,7 +27,7 @@ class ImportTokenParser extends AbstractTokenParser
     public function parse(Token $token)
     {
         $macro = $this->parser->getExpressionParser()->parseExpression();
-        $this->parser->getStream()->expect('as');
+        $this->parser->getStream()->expect(Token::NAME_TYPE, 'as');
         $var = new AssignNameExpression($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine());
         $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
 
diff --git a/app/vendor/twig/twig/src/TokenParser/MacroTokenParser.php b/app/vendor/twig/twig/src/TokenParser/MacroTokenParser.php
index 734ebc60f..a0d66e7be 100644
--- a/app/vendor/twig/twig/src/TokenParser/MacroTokenParser.php
+++ b/app/vendor/twig/twig/src/TokenParser/MacroTokenParser.php
@@ -14,6 +14,7 @@
 use Twig\Error\SyntaxError;
 use Twig\Node\BodyNode;
 use Twig\Node\MacroNode;
+use Twig\Node\Node;
 use Twig\Token;
 
 /**
@@ -49,6 +50,8 @@ public function parse(Token $token)
         $stream->expect(Token::BLOCK_END_TYPE);
 
         $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno, $this->getTag()));
+
+        return new Node();
     }
 
     public function decideBlockEnd(Token $token)
diff --git a/app/vendor/twig/twig/src/TokenStream.php b/app/vendor/twig/twig/src/TokenStream.php
index b5b089fd6..459781695 100644
--- a/app/vendor/twig/twig/src/TokenStream.php
+++ b/app/vendor/twig/twig/src/TokenStream.php
@@ -97,9 +97,10 @@ public function expect($type, $value = null, $message = null)
         $token = $this->tokens[$this->current];
         if (!$token->test($type, $value)) {
             $line = $token->getLine();
-            throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s).',
+            throw new SyntaxError(sprintf('%sUnexpected token "%s"%s ("%s" expected%s).',
                 $message ? $message.'. ' : '',
-                Token::typeToEnglish($token->getType()), $token->getValue(),
+                Token::typeToEnglish($token->getType()),
+                $token->getValue() ? sprintf(' of value "%s"', $token->getValue()) : '',
                 Token::typeToEnglish($type), $value ? sprintf(' with value "%s"', $value) : ''),
                 $line,
                 $this->source
diff --git a/assets/js/asset_manager_core_js.js b/assets/js/asset_manager_core_js.js
index 42b120d29..1532486fc 100644
--- a/assets/js/asset_manager_core_js.js
+++ b/assets/js/asset_manager_core_js.js
@@ -1,4 +1,4 @@
-/* Matomo Javascript - cb=fec13f34443f4f3df7f8844f4e0912d8*/
+/* Matomo Javascript - cb=dacad556ff7033a37b6357892bdc7f60*/
 
 /*! jQuery Browser - v0.1.0 - 3/23/2012
 * https://github.com/jquery/jquery-browser
@@ -740,7 +740,7 @@ return currentModule;};})(window);
 function _pk_translate(translationStringId,values){if(typeof(piwik_translations[translationStringId])!='undefined'){var translation=piwik_translations[translationStringId];if(typeof values!='undefined'&&values&&values.length){values.unshift(translation);return sprintf.apply(null,values);}
 return translation;}
 return"The string "+translationStringId+" was not loaded in javascript. Make sure it is added in the Translate.getClientSideTranslationKeys hook.";}
-var piwikHelper={getRelativePluginPath:function(pluginName){return'';},htmlDecode:function(value){var textArea=document.createElement('textarea');textArea.innerHTML=value;return textArea.value;},sendContentAsDownload:function(filename,content,mimeType){if(!mimeType){mimeType='text/plain';}
+var piwikHelper={htmlDecode:function(value){var textArea=document.createElement('textarea');textArea.innerHTML=value;return textArea.value;},sendContentAsDownload:function(filename,content,mimeType){if(!mimeType){mimeType='text/plain';}
 function downloadFile(content){var node=document.createElement('a');node.style.display='none';if('string'===typeof content){node.setAttribute('href','data:'+mimeType+';charset=utf-8,'+encodeURIComponent(content));}else{node.href=window.URL.createObjectURL(blob);}
 node.setAttribute('download',filename);document.body.appendChild(node);node.click();document.body.removeChild(node);}
 var node;if('function'===typeof Blob){try{var blob=new Blob([content],{type:mimeType});if(window.navigator.msSaveOrOpenBlob){window.navigator.msSaveBlob(blob,filename);return;}else{downloadFile(blob);return;}}catch(e){downloadFile(content);}}
@@ -786,10 +786,12 @@ eAngle-=0.000001;oldArc.call(this,x,y,r,sAngle,eAngle,clockwise);};jQuery.ui.dia
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 var globalAjaxQueue=[];globalAjaxQueue.active=0;globalAjaxQueue.clean=function(){for(var i=this.length;i--;){if(!this[i]||this[i].readyState==4){this.splice(i,1);}}};globalAjaxQueue.push=function(){this.active+=arguments.length;this.clean();return Array.prototype.push.apply(this,arguments);};globalAjaxQueue.abort=function(){for(var i=this.length;i--;){this[i]&&this[i].abort&&this[i].abort();}
-this.splice(0,this.length);this.active=0;};function ajaxHelper(){this.format='json';this.async=true;this.timeout=null;this.callback=function(){};this.useRegularCallbackInCaseOfError=false;this.errorCallback=this.defaultErrorCallback;this.withToken=false;this.completeCallback=function(){};this.getParams={};this.getUrl='?';this.postParams={};this.loadingElement=null;this.errorElement='#ajaxError';this.requestHandle=null;this.defaultParams=['idSite','period','date','segment'];this.addParams=function(params,type){if(typeof params=='string'){params=broadcast.getValuesFromUrl(params);}
-for(var key in params){if(type.toLowerCase()=='get'){this.getParams[key]=params[key];}else if(type.toLowerCase()=='post'){this.postParams[key]=params[key];}}};this.withTokenInUrl=function(){this.withToken=true;};this.setUrl=function(url){this.addParams(broadcast.getValuesFromUrl(url),'GET');};this.setBulkRequests=function(){var urls=[];for(var i=0;i!=arguments.length;++i){urls.push($.param(arguments[i]));}
+this.splice(0,this.length);this.active=0;};function ajaxHelper(){this.format='json';this.async=true;this.timeout=null;this.callback=function(){};this.useRegularCallbackInCaseOfError=false;this.errorCallback;this.withToken=false;this.completeCallback=function(){};this.getParams={};this.getUrl='?';this.postParams={};this.loadingElement=null;this.errorElement='#ajaxError';this.requestHandle=null;this.defaultParams=['idSite','period','date','segment'];this.addParams=function(params,type){if(typeof params=='string'){params=broadcast.getValuesFromUrl(params);}
+var arrayParams=['compareSegments','comparePeriods','compareDates'];for(var key in params){if(arrayParams.indexOf(key)!==-1&&!params[key]){continue;}
+if(type.toLowerCase()=='get'){this.getParams[key]=params[key];}else if(type.toLowerCase()=='post'){this.postParams[key]=params[key];}}};this.withTokenInUrl=function(){this.withToken=true;};this.setUrl=function(url){this.addParams(broadcast.getValuesFromUrl(url),'GET');};this.setBulkRequests=function(){var urls=[];for(var i=0;i!=arguments.length;++i){urls.push($.param(arguments[i]));}
 this.addParams({module:'API',method:'API.getBulkRequest',urls:urls,format:'json'},'post');};this.setTimeout=function(timeout){this.timeout=timeout;};this.setCallback=function(callback){this.callback=callback;};this.useCallbackInCaseOfError=function(){this.useRegularCallbackInCaseOfError=true;};this.redirectOnSuccess=function(params){this.setCallback(function(){piwikHelper.redirect(params);});};this.setErrorCallback=function(callback){this.errorCallback=callback;};this.setCompleteCallback=function(callback){this.completeCallback=callback;};this.defaultErrorCallback=function(deferred,status){if(status=='abort'){return;}
-$('#loadingError').show();setTimeout(function(){$('#loadingError').fadeOut('slow');},2000);};this.setFormat=function(format){this.format=format;};this.setLoadingElement=function(element){if(!element){element='#ajaxLoadingDiv';}
+var loadingError=$('#loadingError');if(Piwik_Popover.isOpen()&&deferred&&deferred.status===500){if(deferred&&deferred.status===500){$(document.body).html(piwikHelper.escape(deferred.responseText));}}else{loadingError.show();}}
+this.errorCallback=this.defaultErrorCallback;this.setFormat=function(format){this.format=format;};this.setLoadingElement=function(element){if(!element){element='#ajaxLoadingDiv';}
 this.loadingElement=element;};this.setErrorElement=function(element){if(!element){return;}
 this.errorElement=element;};this._useGETDefaultParameter=function(parameter){if(parameter&&this.defaultParams){var i;for(i=0;i=0){var endStr=url.indexOf("&",startStr);if(endStr==-1){endStr=url.length;}
-var value=url.substring(startStr+param.length+1,endStr);if(param!='segment'){value=value.replace(/[^_%~\*\+\-\<\>!@\$\.()=,;0-9a-zA-Z]/gi,'');}
-return value;}else{return'';}},getHash:function(){return broadcast.getHashFromUrl().replace(/^#/,'').split('#')[0];},_removeHashFromUrl:function(url){var searchString='';if(url){var urlParts=url.split('#');searchString=urlParts[0];}else{searchString=location.search;}
+hashStr=hashStr.split('#')[0];return broadcast.getParamValue(param,hashStr);},getParamValue:function(param,url){var lookFor=param+'=';var startStr=url.indexOf(lookFor);if(startStr>=0){return getSingleValue(startStr,url);}else{url=decodeURIComponent(url);lookFor=param+'[]=';startStr=url.indexOf(lookFor);if(startStr>=0){var result=[getSingleValue(startStr)];while((startStr=url.indexOf(lookFor,startStr+1))!==-1){result.push(getSingleValue(startStr));}
+return result;}else{return'';}}
+function getSingleValue(startPos){var endStr=url.indexOf("&",startPos);if(endStr===-1){endStr=url.length;}
+var value=url.substring(startPos+lookFor.length,endStr);if(param!='segment'){value=value.replace(/[^_%~\*\+\-\<\>!@\$\.()=,;0-9a-zA-Z]/gi,'');}
+return value;}},getHash:function(){return broadcast.getHashFromUrl().replace(/^#/,'').split('#')[0];},_removeHashFromUrl:function(url){var searchString='';if(url){var urlParts=url.split('#');searchString=urlParts[0];}else{searchString=window.location.search;}
 return searchString;}};
 /*!
  * Piwik - free/libre analytics platform
@@ -896,7 +899,7 @@ if(backLabel){var back=$(document.createElement('a')).addClass('Piwik_Popover_Er
 if(!isOpen){openPopover();}
 this.setContent(error);},onClose:function(callback){closeCallback=callback;},close:function(){if(isOpen){isProgrammaticClose=true;container.dialog('close');isProgrammaticClose=false;}},createPopupAndLoadUrl:function(url,loadingName,dialogClass,ajaxRequest){var ensureMinimumTop=function(){var popoverContainer=$('#Piwik_Popover').parent();if(popoverContainer.position().top<106){popoverContainer.css('top','15px');}};var box=Piwik_Popover.showLoading(loadingName,null,null,dialogClass);ensureMinimumTop();var callback=function(html){function setPopoverTitleIfOneFoundInContainer(){var title=$('h1,h2',container);if(title.length==1){Piwik_Popover.setTitle(title.text());$(title).hide();}}
 Piwik_Popover.setContent(html);setPopoverTitleIfOneFoundInContainer();ensureMinimumTop();};if('undefined'===typeof ajaxRequest){ajaxRequest=new ajaxHelper();}
-ajaxRequest.addParams(piwikHelper.getArrayFromQueryString(url),'get');ajaxRequest.setCallback(callback);ajaxRequest.setFormat('html');ajaxRequest.send();}};})();
+ajaxRequest.addParams(piwikHelper.getArrayFromQueryString(url),'get');ajaxRequest.setCallback(callback);ajaxRequest.setFormat('html');ajaxRequest.send();},isOpen:function(){return isOpen;}};})();
 /*!
  * Piwik - free/libre analytics platform
  *
@@ -919,19 +922,20 @@ draggingSlider=false;}});$('body').on('piwik:changePosition','.piwik-donate-slid
 (function($,require){var exports=require('piwik/UI'),UIControl=exports.UIControl;function DataTable(element){UIControl.call(this,element);this.init();}
 DataTable._footerIconHandlers={};DataTable.initNewDataTables=function(){$('div.dataTable').each(function(){if(!$(this).attr('id')){var tableType=$(this).attr('data-table-type')||'DataTable',klass=require('piwik/UI')[tableType]||require(tableType);if(klass&&$.isFunction(klass)){var table=new klass(this);}}});};DataTable.registerFooterIconHandler=function(id,handler){var handlers=DataTable._footerIconHandlers;if(handlers[id]){setTimeout(function(){throw new Exception("DataTable footer icon handler '"+id+"' is already being used.")},1);return;}
 handlers[id]=handler;};DataTable.getDataTableByReport=function(report){var result=undefined;$('div.dataTable').each(function(){if($(this).attr('data-report')==report){result=this;return false;}});return result;};$.extend(DataTable.prototype,UIControl.prototype,{_init:function(domElem){},_destroy:function(){UIControl.prototype._destroy.call(this);if(this.windowResizeTableAttached){$(window).off('resize',this._resizeDataTable);}
-if(this._bodyMouseUp){$('body').off('mouseup',this._bodyMouseUp);}},init:function(){var domElem=this.$element;this.workingDivId=this._createDivId();domElem.attr('id',this.workingDivId);this.maxNumRowsToHandleEvents=255;this.loadedSubDataTable={};this.isEmpty=$('.pk-emptyDataTable',domElem).length>0;this.bindEventsAndApplyStyle(domElem);this._init(domElem);this.initialized=true;},onClickSort:function(domElem){var self=this;var newColumnToSort=$(domElem).attr('id');if(self.param.filter_sort_column==newColumnToSort){if(this.param.filter_sort_order=='asc'){self.param.filter_sort_order='desc';}
+if(this._bodyMouseUp){$('body').off('mouseup',this._bodyMouseUp);}},init:function(){var domElem=this.$element;this.workingDivId=this._createDivId();domElem.attr('id',this.workingDivId);this.loadedSubDataTable={};this.isEmpty=$('.pk-emptyDataTable',domElem).length>0;this.bindEventsAndApplyStyle(domElem);this._init(domElem);this.initialized=true;},onClickSort:function(domElem){var self=this;var newColumnToSort=$(domElem).attr('id');if(self.param.filter_sort_column==newColumnToSort){if(this.param.filter_sort_order=='asc'){self.param.filter_sort_order='desc';}
 else{self.param.filter_sort_order='asc';}}
 self.param.filter_offset=0;self.param.filter_sort_column=newColumnToSort;if(!self.isDashboard()){self.notifyWidgetParametersChange(domElem,{filter_sort_column:newColumnToSort,filter_sort_order:self.param.filter_sort_order});}
 self.reloadAjaxDataTable();},setGraphedColumn:function(columnName){this.param.columns=columnName;},isWithinDialog:function(domElem){return!!$(domElem).parents('.ui-dialog').length;},isDashboard:function(){return!!$('#dashboardWidgetsArea').length;},getReportMetadata:function(){return JSON.parse(this.$element.attr('data-report-metadata')||'{}');},resetAllFilters:function(){var self=this;var FiltersToRestore={};var filters=['filter_column','filter_pattern','filter_column_recursive','filter_pattern_recursive','enable_filter_excludelowpop','filter_offset','filter_limit','filter_sort_column','filter_sort_order','disable_generic_filters','columns','flat','totals','include_aggregate_rows','totalRows','pivotBy','pivotByColumn'];for(var key=0;key=600){return;}
+ajaxRequest.addParams(params,'get');if(extraParams){ajaxRequest.addParams(extraParams,'post');}
+ajaxRequest.withTokenInUrl();ajaxRequest.setCallback(function(response){container.trigger('piwikDestroyPlot');container.off('piwikDestroyPlot');callbackSuccess(response);});ajaxRequest.setErrorCallback(function(deferred,status){if(status=='abort'||!deferred||deferred.status<400||deferred.status>=600){return;}
 $('#'+self.workingDivId+' .loadingPiwik').last().css('display','none');$('#loadingError').show();});ajaxRequest.setFormat('html');ajaxRequest.send();},dataTableLoaded:function(response,workingDivId,doScroll){var content=$(response);if($.trim($('.dataTableControls',content).html())===''){$('.dataTableControls',content).append(' ');}
 var idToReplace=workingDivId||$(content).attr('id');var dataTableSel=$('#'+idToReplace);table=$(content).parents('table.dataTable');if(dataTableSel.parents('.dataTable').is('table')){$(content).find('table.dataTable').addClass('subDataTable');$(content).find('.dataTableFeatures').addClass('subDataTable');dataTableSel.replaceWith(content);}
 else{dataTableSel.find('object').remove();dataTableSel.replaceWith(content);}
@@ -939,11 +943,12 @@ content.trigger('piwik:dataTableLoaded');if(doScroll||'undefined'===typeof doScr
 piwikHelper.compileAngularComponents(content);return content;},bindEventsAndApplyStyle:function(domElem){var self=this;self.cleanParams();self.preBindEventsAndApplyStyleHook(domElem);self.handleSort(domElem);self.handleLimit(domElem);self.handlePeriod(domElem);self.handleOffsetInformation(domElem);self.handleAnnotationsButton(domElem);self.handleEvolutionAnnotations(domElem);self.handleExportBox(domElem);self.applyCosmetics(domElem);self.handleSubDataTable(domElem);self.handleConfigurationBox(domElem);self.handleSearchBox(domElem);self.handleColumnDocumentation(domElem);self.handleRowActions(domElem);self.handleCellTooltips(domElem);self.handleRelatedReports(domElem);self.handleTriggeredEvents(domElem);self.handleColumnHighlighting(domElem);self.setFixWidthToMakeEllipsisWork(domElem);self.handleSummaryRow(domElem);self.postBindEventsAndApplyStyleHook(domElem);},preBindEventsAndApplyStyleHook:function(domElem){},postBindEventsAndApplyStyleHook:function(domElem){},isWidgetized:function(){return-1!==location.search.indexOf('module=Widgetize');},setFixWidthToMakeEllipsisWork:function(domElem){var self=this;function getTableWidth(domElem){var totalWidth=$(domElem).width();var totalWidthTable=$('table.dataTable',domElem).width();if(totalWidthTablerequiredTableWidth){domElem.css('max-width',requiredTableWidth+'px');}}
+var tableWidth=getTableWidth(domElem);if(tableWidth<=maxTableWidth){return;}
 if(self.isWidgetized()||self.isDashboard()){return;}
 if(dataTableInCard&&dataTableInCard.length){dataTableInCard.width(maxTableWidth);}else{$domElem.width(maxTableWidth);}
 if(parentDataTable&&parentDataTable.length){if(dataTableInCard.length){dataTableInCard.width(maxTableWidth);}else{parentDataTable.width(maxTableWidth);}}}
-function getLabelWidth(domElem,tableWidth,minLabelWidth,maxLabelWidth){var labelWidth=minLabelWidth;var columnsInFirstRow=$('tr:nth-child(1) td:not(.label)',domElem);var widthOfAllColumns=0;columnsInFirstRow.each(function(index,column){widthOfAllColumns+=$(column).outerWidth();});if(tableWidth-widthOfAllColumns>=minLabelWidth){labelWidth=tableWidth-widthOfAllColumns;}else if(widthOfAllColumns>=tableWidth){labelWidth=tableWidth*0.5;}
+function getLabelWidth(domElem,tableWidth,minLabelWidth,maxLabelWidth){var labelWidth=minLabelWidth;var columnsInFirstRow=$('tbody tr:not(.parentComparisonRow):not(.comparePeriod):eq(0) td:not(.label)',domElem);var widthOfAllColumns=0;columnsInFirstRow.each(function(index,column){widthOfAllColumns+=$(column).outerWidth();});if(tableWidth-widthOfAllColumns>=minLabelWidth){labelWidth=tableWidth-widthOfAllColumns;}else if(widthOfAllColumns>=tableWidth){labelWidth=tableWidth*0.5;}
 var innerWidth=0;var innerWrapper=domElem.find('.dataTableWrapper');if(innerWrapper&&innerWrapper.length){innerWidth=innerWrapper.width();}
 if(labelWidth>maxLabelWidth&&!self.isWidgetized()&&innerWidth!==domElem.width()&&!self.isDashboard()){labelWidth=maxLabelWidth;}
 return parseInt(labelWidth / $('tr:nth-child(1) td.label',domElem).length,10);}
@@ -1023,19 +1028,19 @@ if(!iconHighlighted&&!(self.param.viewDataTable=='table'||self.param.viewDataTab
 var $domElement=$(domElement);if($domElement.data('tooltip')=='enabled'){return;}
 $domElement.data('tooltip','enabled');if(!isTextEllipsized($domElement)){return;}
 var customToolTipText=$domElement.attr('title')||$domElement.text();if(customToolTipText){$domElement.attr('title',customToolTipText);}
-$domElement.tooltip({track:true,show:false,hide:false});},applyCosmetics:function(domElem){var self=this;$("th:first-child",domElem).addClass('label');$("td:first-child",domElem).addClass('label');var metadata=this.getReportMetadata();if(self.param.flat=="1"&&self.param.show_dimensions=="1"&&metadata.dimensions&&Object.keys(metadata.dimensions).length>1){for(var i=1;i tbody > tr',table);if(!maxWidth[nthChild]){maxWidth[nthChild]=0;rows.find("td:nth-child("+(nthChild)+").column .value").each(function(index,element){var width=$(element).width();if(width>maxWidth[nthChild]){maxWidth[nthChild]=width;}});rows.find("td:nth-child("+(nthChild)+").column .value").each(function(index,element){$(element).css({width:maxWidth[nthChild],display:'inline-block'});});}
-if(currentNthChild===nthChild){return;}
-currentNthChild=nthChild;rows.children("td:nth-child("+(nthChild)+")").addClass('highlight');self.repositionRowActions($this.parent('tr'));},function(event){var $this=$(this);var table=$this.closest('table');var $parentTr=$this.parent('tr');var tr=$parentTr.children();var nthChild=$parentTr.children().index($this);var targetTd=$(event.relatedTarget).closest('td');var nthChildTarget=targetTd.parent('tr').children().index(targetTd);if(nthChild==nthChildTarget){return;}
-currentNthChild=null;var rows=$('tr',table);rows.find("td:nth-child("+(nthChild+1)+")").removeClass('highlight');});},handleSubDataTable:function(domElem){var self=this;self.numberOfSubtables=$('tr.subDataTable',domElem).click(function(){var idSubTable=$(this).attr('id');var divIdToReplaceWithSubTable='subDataTable_'+idSubTable;if(typeof self.loadedSubDataTable[divIdToReplaceWithSubTable]=="undefined"){var numberOfColumns=$(this).children().length;$(this).after(''+''+'
'+''+_pk_translate('General_Loading')+''+'
'+''+'');var savedActionVariable=self.param.action;var filtersToRestore=self.resetAllFilters();self.param.enable_filter_excludelowpop=filtersToRestore.enable_filter_excludelowpop;self.param.idSubtable=idSubTable;self.param.action=self.props.subtable_controller_action;delete self.param.totalRows;self.reloadAjaxDataTable(false,function(response){self.dataTableLoaded(response,divIdToReplaceWithSubTable);});self.param.action=savedActionVariable;delete self.param.idSubtable;self.restoreAllFilters(filtersToRestore);self.loadedSubDataTable[divIdToReplaceWithSubTable]=true;$(this).next().toggle();$(this).find('div.dataTableRowActions').hide();} -$(this).next().toggle();$(this).toggleClass('expanded');self.repositionRowActions($(this));}).length;},handleColumnDocumentation:function(domElem){if(this.isDashboard()){return;} +$domElement.tooltip({track:true,show:false,hide:false});},applyCosmetics:function(domElem){},handleColumnHighlighting:function(domElem){var maxWidth={};var currentNthChild=null;var self=this;$('td',domElem).each(function(){var $this=$(this);if($this.hasClass('label')){return;} +var table=$this.closest('table');var nthChild=$this.parent('tr').children().index($(this))+1;var rows=$('> tbody > tr',table);if(!maxWidth[nthChild]){maxWidth[nthChild]=0;rows.find("td:nth-child("+(nthChild)+").column .value").add('> thead th:not(.label) .thDIV',table).each(function(index,element){var width=$(element).width();if(width>maxWidth[nthChild]){maxWidth[nthChild]=width;}});rows.find("td:nth-child("+(nthChild)+").column .value").each(function(index,element){$(element).closest('td').css({width:maxWidth[nthChild]});});}});$(domElem).on('mouseenter','td',function(e){e.stopPropagation();var $this=$(e.target);if($this.hasClass('label')){return;} +var table=$this.closest('table');var nthChild=$this.parent('tr').children().index($(e.target))+1;var rows=$('> tbody > tr',table);if(currentNthChild===nthChild){return;} +currentNthChild=nthChild;rows.children("td:nth-child("+(nthChild)+")").addClass('highlight');self.repositionRowActions($this.parent('tr'));});$(domElem).on('mouseleave','td',function(event){var $this=$(event.target);var table=$this.closest('table');var $parentTr=$this.parent('tr');var tr=$parentTr.children();var nthChild=$parentTr.children().index($this);var targetTd=$(event.relatedTarget).closest('td');var nthChildTarget=targetTd.parent('tr').children().index(targetTd);if(nthChild==nthChildTarget){return;} +currentNthChild=null;var rows=$('tr',table);rows.find("td:nth-child("+(nthChild+1)+")").removeClass('highlight');});},getComparisonIdSubtables:function($row){if($row.is('.parentComparisonRow')){var comparisonRows=$row.nextUntil('.parentComparisonRow').filter('.comparisonRow');var comparisonIdSubtables={};comparisonRows.each(function(){var comparisonSeriesIndex=+$(this).data('comparison-series');comparisonIdSubtables[comparisonSeriesIndex]=$(this).data('idsubtable');});return JSON.stringify(comparisonIdSubtables);} +return undefined;},handleSubDataTable:function(domElem){var self=this;self.numberOfSubtables=$('tr.subDataTable',domElem).click(function(){var idSubTable=$(this).attr('id');var divIdToReplaceWithSubTable='subDataTable_'+idSubTable;if(typeof self.loadedSubDataTable[divIdToReplaceWithSubTable]=="undefined"){var numberOfColumns=$(this).closest('table').find('thead tr').first().children().length;var $insertAfter=$(this).nextUntil(':not(.comparePeriod):not(.comparisonRow)').last();if(!$insertAfter.length){$insertAfter=$(this);} +var newRow=$insertAfter.after(''+''+'
'+''+_pk_translate('General_Loading')+''+'
'+''+'');piwikHelper.lazyScrollTo(newRow);var savedActionVariable=self.param.action;var filtersToRestore=self.resetAllFilters();self.param.enable_filter_excludelowpop=filtersToRestore.enable_filter_excludelowpop;self.param.idSubtable=idSubTable;self.param.action=self.props.subtable_controller_action;delete self.param.totalRows;var extraParams={};extraParams.comparisonIdSubtables=self.getComparisonIdSubtables($(this));self.reloadAjaxDataTable(false,function(response){self.dataTableLoaded(response,divIdToReplaceWithSubTable);},extraParams);self.param.action=savedActionVariable;delete self.param.idSubtable;self.restoreAllFilters(filtersToRestore);self.loadedSubDataTable[divIdToReplaceWithSubTable]=true;$(this).find('div.dataTableRowActions').hide();}else{var $toToggle=$(this).nextUntil('.subDataTableContainer').last();$toToggle=$toToggle.length?$toToggle:$(this);$toToggle.next().toggle();} +$(this).toggleClass('expanded');self.repositionRowActions($(this));}).length;},handleColumnDocumentation:function(domElem){if(this.isDashboard()){return;} $('th:has(.columnDocumentation)',domElem).each(function(){var th=$(this);var tooltip=th.find('.columnDocumentation');tooltip.next().hover(function(){var left=(-1*tooltip.outerWidth()/ 2)+th.width()/ 2;var top=-1*tooltip.outerHeight();var thPos=th.position();var thPosTop=0;if(thPos&&thPos.top){thPosTop=thPos.top;} top=top+thPosTop;if(!th.next().length){left=(-1*tooltip.outerWidth())+th.width()+ parseInt(th.css('padding-right'),10);} if(th.offset().top+top<0){top=thPosTop+th.outerHeight();} -tooltip.css({marginLeft:left,marginTop:top,top:0});tooltip.stop(true,true).fadeIn(250);},function(){$(this).prev().stop(true,true).fadeOut(400);});});},canHandleRowEvents:function(domElem){return domElem.find('table > tbody > tr').length<=this.maxNumRowsToHandleEvents;},handleRowActions:function(domElem){this.doHandleRowActions(domElem.find('table > tbody > tr'));},handleCellTooltips:function(domElem){domElem.find('span.cell-tooltip').tooltip({track:true,items:'span',content:function(){return $(this).parent().data('tooltip');},show:false,hide:false,tooltipClass:'small'});},handleRelatedReports:function(domElem){var self=this,hideShowRelatedReports=function(thisReport){$('span',$(thisReport).parent().parent()).each(function(){if(thisReport==this) +tooltip.css({marginLeft:left,marginTop:top,top:0});tooltip.stop(true,true).fadeIn(250);},function(){$(this).prev().stop(true,true).fadeOut(400);});});},handleRowActions:function(domElem){this.doHandleRowActions(domElem.find('table > tbody > tr'));},handleCellTooltips:function(domElem){domElem.find('span.cell-tooltip').tooltip({track:true,items:'span',content:function(){return $(this).parent().data('tooltip');},show:false,hide:false,tooltipClass:'small'});domElem.find('span.ratio').tooltip({track:true,content:function(){var title=$(this).attr('title');return piwikHelper.escape(title.replace(/\n/g,'
'));},show:{delay:700,duration:200},hide:false})},handleRelatedReports:function(domElem){var self=this,hideShowRelatedReports=function(thisReport){$('span',$(thisReport).parent().parent()).each(function(){if(thisReport==this) $(this).hide();else $(this).show();});},thisReport=$('.datatableRelatedReports span:hidden',domElem)[0];function replaceReportTitleAndHelp(domElem,relatedReportName){if(!domElem||!domElem.length){return;} var $title='';var $headline=domElem.prev('h2');if($headline.length){$title=$headline.find('.title:not(.ng-hide)');}else{var $widget=domElem.parents('.widget');if($widget.length){$title=$widget.find('.widgetName > span');}} @@ -1044,15 +1049,16 @@ scope.featureName=$.trim(relatedReportName);setTimeout(function(){scope.$apply() hideShowRelatedReports(thisReport);var relatedReports=$('.datatableRelatedReports span',domElem);if(!relatedReports.length){$('.datatableRelatedReports',domElem).hide();} relatedReports.each(function(){var clicked=this;$(this).unbind('click').click(function(e){var $this=$(this);var url=$this.attr('href');self.resetAllFilters();var newParams=broadcast.getValuesFromUrl(url);for(var key in newParams){self.param[key]=decodeURIComponent(newParams[key]);} delete self.param.pivotBy;delete self.param.pivotByColumn;var relatedReportName=$this.text();self.reloadAjaxDataTable(true,(function(relatedReportName){return function(newReport){var newDomElem=self.dataTableLoaded(newReport,self.workingDivId);hideShowRelatedReports(clicked);replaceReportTitleAndHelp(newDomElem,relatedReportName);}})(relatedReportName));});});},handleTriggeredEvents:function(domElem){var self=this;$(domElem).bind('reload',function(e,paramOverride){paramOverride=paramOverride||{};for(var name in paramOverride){self.param[name]=paramOverride[name];} -self.reloadAjaxDataTable(true);});},handleSummaryRow:function(domElem){var details=_pk_translate('General_LearnMore',[' (',')']);domElem.find('tr.summaryRow').each(function(){var labelSpan=$(this).find('.label .value').filter(function(index,elem){return $(elem).text()!='-';}).last();var defaultLabel=labelSpan.text();$(this).hover(function(){labelSpan.html(defaultLabel+details);},function(){labelSpan.text(defaultLabel);});});},doHandleRowActions:function(trs){if(!trs||trs.length>this.maxNumRowsToHandleEvents){return;} -var self=this;var merged=$.extend({},self.param,self.props);var availableActionsForReport=DataTable_RowActions_Registry.getAvailableActionsForReport(merged);if(availableActionsForReport.length==0){return;} +self.reloadAjaxDataTable(true);});},handleSummaryRow:function(domElem){var details=_pk_translate('General_LearnMore',[' (',')']);domElem.find('tr.summaryRow').each(function(){var labelSpan=$(this).find('.label .value').filter(function(index,elem){return $(elem).text()!='-';}).last();var defaultLabel=labelSpan.text();$(this).hover(function(){labelSpan.html(defaultLabel+details);},function(){labelSpan.text(defaultLabel);});});},doHandleRowActions:function(trs){if(!trs||!trs.length||!trs[0]){return;} +var parent=$(trs[0]).parents('table');var self=this;var merged=$.extend({},self.param,self.props);var availableActionsForReport=DataTable_RowActions_Registry.getAvailableActionsForReport(merged);if(availableActionsForReport.length==0){return;} var actionInstances={};for(var i=0;i=600||useTouchEvent){actionsDom.show();} -if(useTouchEvent){actionsDom.prop('rowActionsVisible',true);}});if(!useTouchEvent){tr.on('mouseleave',function(){if(actionsDom!==null){actionsDom.hide();}});}});},createRowActions:function(availableActionsForReport,tr,actionInstances){var container=$(document.createElement('div')).addClass('dataTableRowActions');for(var i=availableActionsForReport.length-1;i>=0;i--){var action=availableActionsForReport[i];if(!action.isAvailableOnRow(this.param,tr)){continue;} +var useTouchEvent=false;var listenEvent='mouseenter';var userAgent=String(navigator.userAgent).toLowerCase();if(userAgent.match(/(iPod|iPhone|iPad|Android|IEMobile|Windows Phone)/i)){useTouchEvent=true;listenEvent='click';} +parent.on(listenEvent,'tr:not(.subDataTableContainer)',function(){var tr=this;var $tr=$(tr);var td=$tr.find('td.label:last');for(var i=0;i=600||useTouchEvent){tr.actionsDom.show();} +if(useTouchEvent){tr.actionsDom.prop('rowActionsVisible',true);}});if(!useTouchEvent){parent.on('mouseleave','tr',function(){var tr=this;if(tr.actionsDom){tr.actionsDom.hide();}});}},createRowActions:function(availableActionsForReport,tr,actionInstances){var container=$(document.createElement('div')).addClass('dataTableRowActions');for(var i=availableActionsForReport.length-1;i>=0;i--){var action=availableActionsForReport[i];if(!action.isAvailableOnRow(this.param,tr)){continue;} var actionEl=$(document.createElement('a')).attr({href:'#'}).addClass('action'+action.name);if(action.dataTableIcon.indexOf('icon-')===0){actionEl.append($(document.createElement('span')).addClass(action.dataTableIcon+' rowActionIcon'));}else{actionEl.append($(document.createElement('img')).attr({src:action.dataTableIcon}));} container.append(actionEl);if(i==availableActionsForReport.length-1){actionEl.addClass('leftmost');} if(i==0){actionEl.addClass('rightmost');} @@ -1061,7 +1067,7 @@ actionInstances[action.name].trigger(tr,e);return false;}})(action,actionEl));if if(typeof action.dataTableIconTooltip!='undefined'){actionEl.tooltip({track:true,items:'a',content:'

'+action.dataTableIconTooltip[0]+'

'+action.dataTableIconTooltip[1],tooltipClass:'rowActionTooltip',show:false,hide:false});}} return container;},repositionRowActions:function(tr){if(!tr){return;} var td=tr.find('td.label:last');var actions=tr.find('div.dataTableRowActions');if(!actions){return;} -actions.height(tr.innerHeight()-6);actions.css('marginLeft',(td.width()+3-actions.outerWidth())+'px');},_findReportHeader:function(domElem){var h2=false;if(domElem.prev().is('h2')){h2=domElem.prev();} +actions.height(tr.innerHeight()-6);actions.css('marginLeft',(td.width()-3-actions.outerWidth())+'px');},_findReportHeader:function(domElem){var h2=false;if(domElem.prev().is('h2')){h2=domElem.prev();} else if(this.param.viewDataTable=='tableGoals'){h2=$('#titleGoalsByDimension');} else if($('h2',domElem)){h2=$('h2',domElem);} return h2;},_createDivId:function(){return'dataTable_'+this._controlId;}});var switchToHtmlTable=function(dataTable,viewDataTable){dataTable.param.viewDataTable=viewDataTable;delete dataTable.param.enable_filter_excludelowpop;delete dataTable.param.filter_sort_column;delete dataTable.param.filter_sort_order;delete dataTable.param.columns;delete dataTable.param.totals;dataTable.reloadAjaxDataTable();dataTable.notifyWidgetParametersChange(dataTable.$element,{viewDataTable:viewDataTable});};var switchToEcommerceView=function(dataTable,viewDataTable){if(viewDataTable=='ecommerceOrder'){dataTable.param.abandonedCarts='0';}else{dataTable.param.abandonedCarts='1';} @@ -1074,21 +1080,23 @@ return false;}};DataTable_RowActions_Registry.register({name:'RowEvolution',data if(dataTable===null&¶m){var report=param.split(':')[0];var div=$(require('piwik/UI').DataTable.getDataTableByReport(report));if(div.length&&div.data('uiControlObject')){dataTable=div.data('uiControlObject');if(typeof dataTable.rowEvolutionActionInstance!='undefined'){return dataTable.rowEvolutionActionInstance;}}} var instance=new DataTable_RowActions_RowEvolution(dataTable);if(dataTable!==null){dataTable.rowEvolutionActionInstance=instance;} return instance;},isAvailableOnReport:function(dataTableParams){return(typeof dataTableParams.disable_row_evolution=='undefined'||dataTableParams.disable_row_evolution=="0");},isAvailableOnRow:function(dataTableParams,tr){return!tr.hasClass('totalsRow');}});function DataTable_RowAction(dataTable){this.dataTable=dataTable;this.trEventName='piwikTriggerRowAction';this.actionName='RowAction';} -DataTable_RowAction.prototype.initTr=function(tr){var self=this;tr.bind(self.trEventName,function(e,params){self.trigger($(this),params.originalEvent,params.label);});};DataTable_RowAction.prototype.trigger=function(tr,e,subTableLabel){var label=this.getLabelFromTr(tr);if(subTableLabel){var separator=' > ';label+=separator+subTableLabel;} -var subtable=tr.closest('table');if(subtable.is('.subDataTable')){subtable.closest('tr').prev().trigger(this.trEventName,{label:label,originalEvent:e});return;} +DataTable_RowAction.prototype.initTr=function(tr){var self=this;tr.bind(self.trEventName,function(e,params){self.trigger($(this),params.originalEvent,params.label,params.originalRow);});};DataTable_RowAction.prototype.trigger=function(tr,e,subTableLabel,originalRow){var label=this.getLabelFromTr(tr);if(subTableLabel){var separator=' > ';label+=separator+subTableLabel;} +var subtable=tr.closest('table');if(subtable.is('.subDataTable')){subtable.closest('tr').prev().trigger(this.trEventName,{label:label,originalEvent:e,originalRow:tr});return;} if(subtable.closest('div.dataTableActions').length){var allClasses=tr.attr('class');var matches=allClasses.match(/level[0-9]+/);var level=parseInt(matches[0].substring(5,matches[0].length),10);if(level>0){var findLevel='level'+(level-1);var ptr=tr;while((ptr=ptr.prev()).length){if(!ptr.hasClass(findLevel)||ptr.hasClass('nodata')){continue;} -ptr.trigger(this.trEventName,{label:label,originalEvent:e});return;}}} -this.performAction(label,tr,e);};DataTable_RowAction.prototype.getLabelFromTr=function(tr){var rowMetadata=this.getRowMetadata(tr);if(rowMetadata.combinedLabel){return'@'+rowMetadata.combinedLabel;} +ptr.trigger(this.trEventName,{label:label,originalEvent:e,originalRow:tr});return;}}} +this.performAction(label,tr,e,originalRow);};DataTable_RowAction.prototype.getLabelFromTr=function(tr){if(tr.data('label')){return tr.data('label');} +var rowMetadata=this.getRowMetadata(tr);if(rowMetadata.combinedLabel){return'@'+rowMetadata.combinedLabel;} var label=tr.find('span.label');var value=label.data('originalText');if(!value){value=label.text();} value=value.trim();value=encodeURIComponent(value);if(!tr.hasClass('subDataTable')){value='@'+value;} -return value;};DataTable_RowAction.prototype.getRowMetadata=function(tr){return tr.data('row-metadata')||{};};DataTable_RowAction.prototype.openPopover=function(parameter){broadcast.propagateNewPopoverParameter('RowAction',this.actionName+':'+parameter);};broadcast.addPopoverHandler('RowAction',function(param){var paramParts=param.split(':');var rowActionName=paramParts[0];paramParts.shift();param=paramParts.join(':');var rowAction=DataTable_RowActions_Registry.getActionByName(rowActionName);if(rowAction){rowAction.createInstance(null,param).doOpenPopover(param);}});DataTable_RowAction.prototype.performAction=function(label,tr,e){};DataTable_RowAction.prototype.doOpenPopover=function(parameter){};function DataTable_RowActions_RowEvolution(dataTable){this.dataTable=dataTable;this.trEventName='piwikTriggerRowEvolution';this.multiEvolutionRows=[];} -DataTable_RowActions_RowEvolution.launch=function(apiMethod,label){var param='RowEvolution:'+apiMethod+':0:'+label;broadcast.propagateNewPopoverParameter('RowAction',param);};DataTable_RowActions_RowEvolution.prototype=new DataTable_RowAction;DataTable_RowActions_RowEvolution.prototype.performAction=function(label,tr,e){if(e.shiftKey){this.addMultiEvolutionRow(label);return;} -this.addMultiEvolutionRow(label);var extraParams={};if(this.multiEvolutionRows.length>1){extraParams.action='getMultiRowEvolutionPopover';label=this.multiEvolutionRows.join(',');} +return value;};DataTable_RowAction.prototype.getRowMetadata=function(tr){return tr.data('row-metadata')||{};};DataTable_RowAction.prototype.openPopover=function(parameter){broadcast.propagateNewPopoverParameter('RowAction',this.actionName+':'+parameter);};broadcast.addPopoverHandler('RowAction',function(param){var paramParts=param.split(':');var rowActionName=paramParts[0];paramParts.shift();param=paramParts.join(':');var rowAction=DataTable_RowActions_Registry.getActionByName(rowActionName);if(rowAction){rowAction.createInstance(null,param).doOpenPopover(param);}});DataTable_RowAction.prototype.performAction=function(label,tr,e){};DataTable_RowAction.prototype.doOpenPopover=function(parameter){};function DataTable_RowActions_RowEvolution(dataTable){this.dataTable=dataTable;this.trEventName='piwikTriggerRowEvolution';this.multiEvolutionRows=[];this.multiEvolutionRowsSeries=[];} +DataTable_RowActions_RowEvolution.launch=function(apiMethod,label){var param='RowEvolution:'+apiMethod+':0:'+label;broadcast.propagateNewPopoverParameter('RowAction',param);};DataTable_RowActions_RowEvolution.prototype=new DataTable_RowAction;DataTable_RowActions_RowEvolution.prototype.performAction=function(label,tr,e,originalRow){if(e.shiftKey){this.addMultiEvolutionRow(label,$(originalRow||tr).data('comparison-series'));return;} +this.addMultiEvolutionRow(label,$(originalRow||tr).data('comparison-series'));var extraParams=$.extend({},$(originalRow||tr).data('param-override'));if(typeof extraParams!=='object'){extraParams={};} +if(this.multiEvolutionRows.length>1){extraParams.action='getMultiRowEvolutionPopover';label=this.multiEvolutionRows.join(',');if(this.multiEvolutionRowsSeries.length>1){var piwikUrl=piwikHelper.getAngularDependency('piwikUrl');extraParams.compareDates=piwikUrl.getSearchParam('compareDates');extraParams.comparePeriods=piwikUrl.getSearchParam('comparePeriods');extraParams.compareSegments=piwikUrl.getSearchParam('compareSegments');extraParams.labelSeries=this.multiEvolutionRowsSeries.join(',');delete extraParams.period;delete extraParams.date;delete extraParams.segment;}} $.each(this.dataTable.param,function(index,value){if(index!=='idSite'&&index.indexOf('id')===0&&$.isNumeric(value)){extraParams[index]=value;}});if(this.dataTable.param.abandonedCarts!==undefined){extraParams['abandonedCarts']=this.dataTable.param.abandonedCarts;} if(this.dataTable.param.flat!==undefined){extraParams['flat']=this.dataTable.param.flat;} -var apiMethod=this.dataTable.param.module+'.'+this.dataTable.param.action;this.openPopover(apiMethod,extraParams,label);};DataTable_RowActions_RowEvolution.prototype.addMultiEvolutionRow=function(label){if($.inArray(label,this.multiEvolutionRows)==-1){this.multiEvolutionRows.push(label);}};DataTable_RowActions_RowEvolution.prototype.openPopover=function(apiMethod,extraParams,label){var urlParam=apiMethod+':'+encodeURIComponent(JSON.stringify(extraParams))+':'+label;DataTable_RowAction.prototype.openPopover.apply(this,[urlParam]);};DataTable_RowActions_RowEvolution.prototype.doOpenPopover=function(urlParam){var urlParamParts=urlParam.split(':');var apiMethod=urlParamParts.shift();var extraParamsString=urlParamParts.shift(),extraParams={};try{extraParams=JSON.parse(decodeURIComponent(extraParamsString));}catch(e){if(extraParamsString=='1'){extraParams.action='getMultiRowEvolutionPopover';}else if(extraParamsString!='0'){extraParams.action='getMultiRowEvolutionPopover';extraParams.column=extraParamsString;}} +var apiMethod=this.dataTable.param.module+'.'+this.dataTable.param.action;this.openPopover(apiMethod,extraParams,label);};DataTable_RowActions_RowEvolution.prototype.addMultiEvolutionRow=function(label,seriesIndex){if(typeof seriesIndex!=='undefined'){var self=this;var found=false;this.multiEvolutionRows.forEach(function(rowLabel,index){var rowSeriesIndex=self.multiEvolutionRowsSeries[index];if(label===rowLabel&&seriesIndex===rowSeriesIndex){found=true;return false;}});if(!found){this.multiEvolutionRows.push(label);this.multiEvolutionRowsSeries.push(seriesIndex);}}else if($.inArray(label,this.multiEvolutionRows)===-1){this.multiEvolutionRows.push(label);this.multiEvolutionRowsSeries=[];}};DataTable_RowActions_RowEvolution.prototype.openPopover=function(apiMethod,extraParams,label){var urlParam=apiMethod+':'+encodeURIComponent(JSON.stringify(extraParams))+':'+label;DataTable_RowAction.prototype.openPopover.apply(this,[urlParam]);};DataTable_RowActions_RowEvolution.prototype.doOpenPopover=function(urlParam){var urlParamParts=urlParam.split(':');var apiMethod=urlParamParts.shift();var extraParamsString=urlParamParts.shift(),extraParams={};try{extraParams=JSON.parse(decodeURIComponent(extraParamsString));}catch(e){if(extraParamsString=='1'){extraParams.action='getMultiRowEvolutionPopover';}else if(extraParamsString!='0'){extraParams.action='getMultiRowEvolutionPopover';extraParams.column=extraParamsString;}} var label=urlParamParts.join(':');this.showRowEvolution(apiMethod,label,extraParams);};DataTable_RowActions_RowEvolution.prototype.showRowEvolution=function(apiMethod,label,extraParams){var self=this;var box=Piwik_Popover.showLoading('Row Evolution');box.addClass('rowEvolutionPopover');var requestParams={apiMethod:apiMethod,label:label,disableLink:1};var callback=function(html){Piwik_Popover.setContent(html);var title=box.find('div.popover-title');if(title.length){Piwik_Popover.setTitle(title.html());title.remove();} -Piwik_Popover.onClose(function(){self.multiEvolutionRows=[];});if(self.dataTable!==null){box.find('.rowevolution-startmulti').click(function(){Piwik_Popover.onClose(false);broadcast.propagateNewPopoverParameter(false);return false;});}else{box.find('.compare-container, .rowevolution-startmulti').remove();} +Piwik_Popover.onClose(function(){self.multiEvolutionRows=[];self.multiEvolutionRowsSeries=[];});if(self.dataTable!==null){box.find('.rowevolution-startmulti').click(function(){Piwik_Popover.onClose(false);broadcast.propagateNewPopoverParameter(false);return false;});}else{box.find('.compare-container, .rowevolution-startmulti').remove();} box.find('select.multirowevoltion-metric').change(function(){var metric=$(this).val();Piwik_Popover.onClose(false);var extraParams={action:'getMultiRowEvolutionPopover',column:metric};self.openPopover(apiMethod,extraParams,label);return true;});};requestParams.module='CoreHome';requestParams.action='getRowEvolutionPopover';requestParams.colors=JSON.stringify(piwik.getSparklineColors());var idDimension;if(broadcast.getValueFromUrl('module')==='Widgetize'){idDimension=broadcast.getValueFromUrl('subcategory');}else{idDimension=broadcast.getValueFromHash('subcategory');} if(idDimension&&(''+idDimension).indexOf('customdimension')===0){idDimension=(''+idDimension).replace('customdimension','');idDimension=parseInt(idDimension,10);if(idDimension>0){requestParams.idDimension=idDimension;}} if(self.dataTable&&self.dataTable.jsViewDataTable==='tableGoals'){if(extraParams['idGoal']){delete(extraParams['idGoal']);}} @@ -1108,12 +1116,14 @@ $('#periodString .title').trigger('click').focus();});}(jQuery)); * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ -(function($){var sparklineColorNames=['backgroundColor','lineColor','minPointColor','maxPointColor','lastPointColor','fillColor'];var sparklineDisplayHeight=25;var sparklineDisplayWidth=100;piwik.getSparklineColors=function(){return piwik.ColorManager.getColors('sparkline-colors',sparklineColorNames);};piwik.initSparklines=function(){$('.sparkline > img').each(function(){var $self=$(this);if($self.attr('src')){return;} -var colors=JSON.stringify(piwik.getSparklineColors());var appendToSparklineUrl='&colors='+encodeURIComponent(colors);var token_auth=broadcast.getValueFromUrl('token_auth');if(token_auth.length&&piwik.shouldPropagateTokenAuth){appendToSparklineUrl+='&token_auth='+token_auth;} -$self.attr('width',sparklineDisplayWidth);$self.attr('height',sparklineDisplayHeight);$self.attr('src',$self.attr('data-src')+appendToSparklineUrl);});};window.initializeSparklines=function(){var sparklineUrlParamsToIgnore=['module','action','idSite','period','date','showtitle','viewDataTable','forceView','random'];$('.dataTableVizEvolution[data-report]').each(function(){var graph=$(this);var selectorsToFindParent=['.widget','[piwik-widget-container]','.reporting-page','body'];var index=0,selector,parent;for(index;indexe&&(e=c,h(W,"Set "+d+" to min value")),e>b&&(e=b,h(W,"Set "+d+" to max value")),V[d]=""+e}function k(){function a(){function a(){var a=0,d=!1;for(h(W,"Checking connection is from allowed list of origins: "+c);aP[w]["max"+a])throw new Error("Value for min"+a+" can not be greater than max"+a)}c("Height"),c("Width"),b("maxHeight"),b("minHeight"),b("maxWidth"),b("minWidth")}function e(){var a=c&&c.id||S.id+F++;return null!==document.getElementById(a)&&(a+=F++),a}function f(b){return R=b,""===b&&(a.id=b=e(),G=(c||{}).log,R=b,h(b,"Added missing iframe ID: "+b+" ("+a.src+")")),b}function g(){h(w,"IFrame scrolling "+(P[w].scrolling?"enabled":"disabled")+" for "+w),a.style.overflow=!1===P[w].scrolling?"hidden":"auto",a.scrolling=!1===P[w].scrolling?"no":"yes"}function i(){("number"==typeof P[w].bodyMargin||"0"===P[w].bodyMargin)&&(P[w].bodyMarginV1=P[w].bodyMargin,P[w].bodyMargin=""+P[w].bodyMargin+"px")}function k(){var b=P[w].firstRun,c=P[w].heightCalculationMethod in O;!b&&c&&r({iframe:a,height:0,width:0,type:"init"})}function l(){Function.prototype.bind&&(P[w].iframe.iFrameResizer={close:n.bind(null,P[w].iframe),resize:u.bind(null,"Window resize","resize",P[w].iframe),moveToAnchor:function(a){u("Move to anchor","inPageLink:"+a,P[w].iframe,w)},sendMessage:function(a){a=JSON.stringify(a),u("Send Message","message:"+a,P[w].iframe,w)}})}function m(c){function d(){u("iFrame.onload",c,a),k()}b(a,"load",d),u("init",c,a)}function o(a){if("object"!=typeof a)throw new TypeError("Options is not an object")}function p(a){for(var b in S)S.hasOwnProperty(b)&&(P[w][b]=a.hasOwnProperty(b)?a[b]:S[b])}function q(a){return""===a||"file://"===a?"*":a}function s(b){b=b||{},P[w]={firstRun:!0,iframe:a,remoteHost:a.src.split("/").slice(0,3).join("/")},o(b),p(b),P[w].targetOrigin=!0===P[w].checkOrigin?q(P[w].remoteHost):"*"}function t(){return w in P&&"iFrameResizer"in a}var w=f(a.id);t()?j(w,"Ignored iFrame, already setup."):(s(c),g(),d(),i(),m(v(w)),l())}function x(a,b){null===Q&&(Q=setTimeout(function(){Q=null,a()},b))}function y(){function b(){function a(a){function b(b){return"0px"===P[a].iframe.style[b]}function c(a){return null!==a.offsetParent}c(P[a].iframe)&&(b("height")||b("width"))&&u("Visibility change","resize",P[a].iframe,a)}for(var b in P)a(b)}function c(a){h("window","Mutation observed: "+a[0].target+" "+a[0].type),x(b,16)}function d(){var a=document.querySelector("body"),b={attributes:!0,attributeOldValue:!1,characterData:!0,characterDataOldValue:!1,childList:!0,subtree:!0},d=new e(c);d.observe(a,b)}var e=a.MutationObserver||a.WebKitMutationObserver;e&&d()}function z(a){function b(){B("Window "+a,"resize")}h("window","Trigger event: "+a),x(b,16)}function A(){function a(){B("Tab Visable","resize")}"hidden"!==document.visibilityState&&(h("document","Trigger event: Visiblity change"),x(a,16))}function B(a,b){function c(a){return"parent"===P[a].resizeFrom&&P[a].autoResize&&!P[a].firstRun}for(var d in P)c(d)&&u(a,b,document.getElementById(d),d)}function C(){b(a,"message",l),b(a,"resize",function(){z("resize")}),b(document,"visibilitychange",A),b(document,"-webkit-visibilitychange",A),b(a,"focusin",function(){z("focus")}),b(a,"focus",function(){z("focus")})}function D(){function a(a,c){function d(){if(!c.tagName)throw new TypeError("Object is not a valid DOM element");if("IFRAME"!==c.tagName.toUpperCase())throw new TypeError("Expected ', $result ); + $this->assertSame( '', $result ); } public function test_matomo_opt_out_size_percent_px_values() { diff --git a/tests/phpunit/wpmatomo/test-release.php b/tests/phpunit/wpmatomo/test-release.php new file mode 100644 index 000000000..c87681cd4 --- /dev/null +++ b/tests/phpunit/wpmatomo/test-release.php @@ -0,0 +1,35 @@ +assertFileExists(plugin_dir_path(MATOMO_ANALYTICS_FILE) . $file); + } + + public function get_needed_files() + { + return array( + array('app/bootstrap.php'), + array('app/plugins/CoreAdminHome/javascripts/optOut.js'), + array('app/plugins/Overlay/javascripts/Piwik_Overlay.js'), + array('app/plugins/TagManager/javascripts/previewmode.js'), + array('app/plugins/TagManager/javascripts/previewmodedetection.js'), + array('app/plugins/TagManager/javascripts/tagmanager.js'), + array('app/plugins/TagManager/javascripts/tagmanager.min.js'), + array('app/core/.htaccess'), + array('app/js/.htaccess'), + array('app/lang/.htaccess'), + array('app/plugins/.htaccess'), + array('app/libs/.htaccess'), + array('app/vendor/.htaccess'), + array('app/robots.txt'), + ); + } + +} diff --git a/tests/phpunit/wpmatomo/trackingcode/test-trackingcodegenerator.php b/tests/phpunit/wpmatomo/trackingcode/test-trackingcodegenerator.php index 93686726e..b4719dfcf 100644 --- a/tests/phpunit/wpmatomo/trackingcode/test-trackingcodegenerator.php +++ b/tests/phpunit/wpmatomo/trackingcode/test-trackingcodegenerator.php @@ -48,8 +48,8 @@ public function test_get_tracking_code_when_using_default_tracking_code() { 'track_mode' => TrackingSettings::TRACK_MODE_DEFAULT ) ); $this->assertSame( '', $this->get_tracking_code() ); +_paq.push([\'trackPageView\']);_paq.push([\'enableLinkTracking\']);_paq.push([\'setTrackerUrl\', "\/\/example.org\/wp-content\/plugins\/matomo\/app\/matomo.php"]);_paq.push([\'setSiteId\', \'21\']);var d=document, g=d.createElement(\'script\'), s=d.getElementsByTagName(\'script\')[0]; +g.type=\'text/javascript\'; g.async=true; g.defer=true; g.src="\/\/example.org\/wp-content\/plugins\/matomo\/app\/matomo.js"; s.parentNode.insertBefore(g,s);', $this->get_tracking_code() ); } public function test_get_tracking_code_when_using_default_tracking_code_using_rest_api_and_other_features() { @@ -71,9 +71,9 @@ public function test_get_tracking_code_when_using_default_tracking_code_using_re _paq.push([\'setLinkClasses\', "clickme|foo"]); _paq.push([\'disableCookies\']); _paq.push([\'enableCrossDomainLinking\']); - _paq.push(["setCookieDomain", "*.example.org"]); -_paq.push([\'trackAllContentImpressions\']);_paq.push([\'trackPageView\']);_paq.push([\'enableLinkTracking\']);_paq.push([\'setTrackerUrl\', "\/\/example.org\/index.php?rest_route=\/ma\/v1\/hit\/"]);_paq.push([\'setSiteId\', \'21\']);var d=document, g=d.createElement(\'script\'), s=d.getElementsByTagName(\'script\')[0]; - g.type=\'text/javascript\'; g.async=true; g.defer=true; g.src="\/\/example.org\/index.php?rest_route=\/ma\/v1\/hit\/"; s.parentNode.insertBefore(g,s);', $this->get_tracking_code() ); +_paq.push(["setCookieDomain", "*.example.org"]); +_paq.push([\'trackAllContentImpressions\']);_paq.push([\'trackPageView\']);_paq.push([\'enableLinkTracking\']);_paq.push([\'setTrackerUrl\', "\/\/example.org\/index.php?rest_route=\/matomo\/v1\/hit\/"]);_paq.push([\'setSiteId\', \'21\']);var d=document, g=d.createElement(\'script\'), s=d.getElementsByTagName(\'script\')[0]; +g.type=\'text/javascript\'; g.async=true; g.defer=true; g.src="\/\/example.org\/index.php?rest_route=\/matomo\/v1\/hit\/"; s.parentNode.insertBefore(g,s);', $this->get_tracking_code() ); } public function test_get_tracking_code_test_user_id() { @@ -100,13 +100,18 @@ public function test_get_tracking_code_when_using_tagmanager_mode() { 'track_mode' => TrackingSettings::TRACK_MODE_TAGMANAGER, 'tagmanger_container_ids' => array('abcdefgh' => 1, 'cfk3jjw' => 0) ) ); - $this->assertSame( ' + + if (is_multisite()) { + $this->assertSame( '', $this->get_tracking_code() ); + } else { + $this->assertSame( ' ', $this->get_tracking_code() ); + } } public function test_get_tracking_code_when_using_tagmanager_mode_and_no_containers() { @@ -114,7 +119,12 @@ public function test_get_tracking_code_when_using_tagmanager_mode_and_no_contain 'track_mode' => TrackingSettings::TRACK_MODE_TAGMANAGER, 'tagmanger_container_ids' => array() ) ); - $this->assertSame( '', $this->get_tracking_code() ); + if (is_multisite()) { + $this->assertSame( '', $this->get_tracking_code() ); + } else { + $this->assertSame( '', $this->get_tracking_code() ); + } + }