diff options
author | diosmosis <benaka@piwik.pro> | 2015-03-12 12:26:38 +0300 |
---|---|---|
committer | diosmosis <benaka@piwik.pro> | 2015-03-12 12:26:38 +0300 |
commit | 54bd3f0f18b3ba9edde0823d7f06e0769e5abad7 (patch) | |
tree | 4cc4c26ff9558a8cbc0ccf6afb2cd7498a1a1996 /plugins | |
parent | 5197b1cd4ddd21e7c2ab4cfe2906dff92ac35734 (diff) | |
parent | 484935ce69c19e94d4b2a95aca33e75b0802abbe (diff) |
Merge branch 'master' into 7181_isolated_archive_purging
Conflicts:
plugins/SitesManager/SitesManager.php
Diffstat (limited to 'plugins')
58 files changed, 1022 insertions, 280 deletions
diff --git a/plugins/API/API.php b/plugins/API/API.php index 2f5f21f4e9..06f04447e2 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -196,7 +196,7 @@ class API extends \Piwik\Plugin\API 'segment' => 'visitIp', 'acceptedValues' => '13.54.122.1. </code>Select IP ranges with notation: <code>visitIp>13.54.122.0;visitIp<13.54.122.255', 'sqlSegment' => 'log_visit.location_ip', - 'sqlFilterValue' => array('Piwik\IP', 'P2N'), + 'sqlFilterValue' => array('Piwik\Network\IPUtils', 'stringToBinaryIP'), 'permission' => $isAuthenticatedWithViewAccess, ); diff --git a/plugins/Actions/API.php b/plugins/Actions/API.php index 91f1d70c45..891400e122 100644 --- a/plugins/Actions/API.php +++ b/plugins/Actions/API.php @@ -9,7 +9,6 @@ namespace Piwik\Plugins\Actions; use Exception; -use Piwik\API\Request; use Piwik\Archive; use Piwik\Common; use Piwik\DataTable; @@ -84,14 +83,17 @@ class API extends \Piwik\Plugin\API * @param bool $expanded * @param bool|int $idSubtable * @param bool|int $depth + * @param bool|int $flat * * @return DataTable|DataTable\Map */ public function getPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false, - $depth = false) + $depth = false, $flat = false) { - $dataTable = $this->getDataTableFromArchive('Actions_actions_url', $idSite, $period, $date, $segment, $expanded, $idSubtable, $depth); - $this->filterActionsDataTable($dataTable, $expanded); + $dataTable = Archive::createDataTableFromArchive('Actions_actions_url', $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable, $depth); + + $this->filterActionsDataTable($dataTable); + return $dataTable; } @@ -136,7 +138,7 @@ class API extends \Piwik\Plugin\API { // Keep only pages which are following site search $dataTable->filter('ColumnCallbackDeleteRow', array( - 'nb_hits_following_search', + PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, function ($value) { return $value <= 0; } @@ -167,17 +169,19 @@ class API extends \Piwik\Plugin\API public function getPageUrl($pageUrl, $idSite, $period, $date, $segment = false) { - $callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); + $callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null); $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Action::TYPE_PAGE_URL); $this->addPageProcessedMetrics($dataTable); $this->filterActionsDataTable($dataTable); return $dataTable; } - public function getPageTitles($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + public function getPageTitles($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false, $flat = false) { - $dataTable = $this->getDataTableFromArchive('Actions_actions', $idSite, $period, $date, $segment, $expanded, $idSubtable); - $this->filterActionsDataTable($dataTable, $expanded); + $dataTable = Archive::createDataTableFromArchive('Actions_actions', $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable); + + $this->filterActionsDataTable($dataTable); + return $dataTable; } @@ -207,38 +211,38 @@ class API extends \Piwik\Plugin\API public function getPageTitle($pageName, $idSite, $period, $date, $segment = false) { - $callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); + $callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null); $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Action::TYPE_PAGE_TITLE); $this->addPageProcessedMetrics($dataTable); $this->filterActionsDataTable($dataTable); return $dataTable; } - public function getDownloads($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + public function getDownloads($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false, $flat = false) { - $dataTable = $this->getDataTableFromArchive('Actions_downloads', $idSite, $period, $date, $segment, $expanded, $idSubtable); + $dataTable = Archive::createDataTableFromArchive('Actions_downloads', $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable); $this->filterActionsDataTable($dataTable, $expanded); return $dataTable; } public function getDownload($downloadUrl, $idSite, $period, $date, $segment = false) { - $callBackParameters = array('Actions_downloads', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); + $callBackParameters = array('Actions_downloads', $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null); $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $downloadUrl, Action::TYPE_DOWNLOAD); $this->filterActionsDataTable($dataTable); return $dataTable; } - public function getOutlinks($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + public function getOutlinks($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false, $flat = false) { - $dataTable = $this->getDataTableFromArchive('Actions_outlink', $idSite, $period, $date, $segment, $expanded, $idSubtable); + $dataTable = Archive::createDataTableFromArchive('Actions_outlink', $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable); $this->filterActionsDataTable($dataTable, $expanded); return $dataTable; } public function getOutlink($outlinkUrl, $idSite, $period, $date, $segment = false) { - $callBackParameters = array('Actions_outlink', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); + $callBackParameters = array('Actions_outlink', $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null); $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $outlinkUrl, Action::TYPE_OUTLINK); $this->filterActionsDataTable($dataTable); return $dataTable; @@ -249,6 +253,7 @@ class API extends \Piwik\Plugin\API $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment); $dataTable->deleteColumn(PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT); $this->filterActionsDataTable($dataTable); + $dataTable->filter('ReplaceColumnNames'); $this->addPagesPerSearchColumn($dataTable); return $dataTable; } @@ -266,7 +271,7 @@ class API extends \Piwik\Plugin\API protected function getSiteSearchKeywordsRaw($idSite, $period, $date, $segment) { - $dataTable = $this->getDataTableFromArchive('Actions_sitesearch', $idSite, $period, $date, $segment, $expanded = false); + $dataTable = Archive::createDataTableFromArchive('Actions_sitesearch', $idSite, $period, $date, $segment, $expanded = false); return $dataTable; } @@ -284,6 +289,7 @@ class API extends \Piwik\Plugin\API $dataTable->deleteRow(DataTable::ID_SUMMARY_ROW); $dataTable->deleteColumn(PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT); $this->filterActionsDataTable($dataTable); + $dataTable->filter('ReplaceColumnNames'); $this->addPagesPerSearchColumn($dataTable); return $dataTable; } @@ -332,6 +338,7 @@ class API extends \Piwik\Plugin\API } } $this->filterActionsDataTable($dataTable); + $dataTable->filter('ReplaceColumnNames'); $this->addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_actions'); return $dataTable; } @@ -361,7 +368,7 @@ class API extends \Piwik\Plugin\API if ($table === false) { // fetch the data table - $table = call_user_func_array(array($this, 'getDataTableFromArchive'), $callBackParameters); + $table = call_user_func_array('\Piwik\Archive::createDataTableFromArchive', $callBackParameters); if ($table instanceof DataTable\Map) { // search an array of tables, e.g. when using date=last30 @@ -416,7 +423,7 @@ class API extends \Piwik\Plugin\API // end of tree search reached if (count($searchTree) == 0) { - $result = new DataTable(); + $result = $table->getEmptyClone(); $result->addRow($row); $result->setAllTableMetadata($table->getAllTableMetadata()); return $result; @@ -424,7 +431,7 @@ class API extends \Piwik\Plugin\API // match found on this level and more levels remaining: go deeper $idSubTable = $row->getIdSubDataTable(); - $callBackParameters[6] = $idSubTable; + $callBackParameters[7] = $idSubTable; /** * @var \Piwik\Period $period @@ -434,7 +441,7 @@ class API extends \Piwik\Plugin\API $callBackParameters[3] = $period->getDateStart() . ',' . $period->getDateEnd(); } - $table = call_user_func_array(array($this, 'getDataTableFromArchive'), $callBackParameters); + $table = call_user_func_array('\Piwik\Archive::createDataTableFromArchive', $callBackParameters); return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree); } @@ -445,28 +452,15 @@ class API extends \Piwik\Plugin\API * Common filters for all Actions API * * @param DataTable|DataTable\Simple|DataTable\Map $dataTable - * @param bool $expanded */ - protected function filterActionsDataTable($dataTable, $expanded = false) + private function filterActionsDataTable($dataTable) { // Must be applied before Sort in this case, since the DataTable can contain both int and strings indexes // (in the transition period between pre 1.2 and post 1.2 datatable structure) - $dataTable->filter('ReplaceColumnNames'); - $dataTable->filter('Sort', array('nb_visits', 'desc', $naturalSort = false, $expanded)); - $dataTable->filter(function (DataTable $dataTable) { - foreach ($dataTable->getRows() as $row) { - $url = $row->getMetadata('url'); - if ($url) { - $row->setMetadata('segmentValue', urldecode($url)); - } - } - }); - $dataTable->filter('GroupBy', array('label', function ($label) { - return urldecode($label); - })); + $dataTable->filter('Piwik\Plugins\Actions\DataTable\Filter\Actions'); - $dataTable->queueFilter('ReplaceSummaryRowLabel'); + return $dataTable; } /** @@ -477,7 +471,7 @@ class API extends \Piwik\Plugin\API private function filterNonEntryActions($dataTable) { $dataTable->filter('ColumnCallbackDeleteRow', - array('entry_nb_visits', + array(PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS, function ($visits) { return !strlen($visits); } @@ -493,18 +487,13 @@ class API extends \Piwik\Plugin\API private function filterNonExitActions($dataTable) { $dataTable->filter('ColumnCallbackDeleteRow', - array('exit_nb_visits', + array(PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS, function ($visits) { return !strlen($visits); }) ); } - protected function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded = false, $idSubtable = null, $depth = null) - { - return Archive::getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable, $depth); - } - private function addPageProcessedMetrics(DataTable\DataTableInterface $dataTable) { $dataTable->filter(function (DataTable $table) { diff --git a/plugins/Actions/DataTable/Filter/Actions.php b/plugins/Actions/DataTable/Filter/Actions.php new file mode 100644 index 0000000000..71abe4190b --- /dev/null +++ b/plugins/Actions/DataTable/Filter/Actions.php @@ -0,0 +1,53 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\Actions\DataTable\Filter; + +use Piwik\DataTable\BaseFilter; +use Piwik\DataTable\Row; +use Piwik\DataTable; + +class Actions extends BaseFilter +{ + /** + * Constructor. + * + * @param DataTable $table The table to eventually filter. + */ + public function __construct($table) + { + parent::__construct($table); + } + + /** + * @param DataTable $table + */ + public function filter($table) + { + $table->filter(function (DataTable $dataTable) { + foreach ($dataTable->getRows() as $row) { + $url = $row->getMetadata('url'); + if ($url) { + $row->setMetadata('segmentValue', urldecode($url)); + } + } + }); + + // TODO can we remove this one again? + $table->queueFilter('GroupBy', array('label', function ($label) { + return urldecode($label); + })); + + foreach ($table->getRows() as $row) { + $subtable = $row->getSubtable(); + if ($subtable) { + $this->filter($subtable); + } + } + } +}
\ No newline at end of file diff --git a/plugins/Actions/Reports/Base.php b/plugins/Actions/Reports/Base.php index 68bca741a3..45c9c0af9c 100644 --- a/plugins/Actions/Reports/Base.php +++ b/plugins/Actions/Reports/Base.php @@ -9,6 +9,7 @@ namespace Piwik\Plugins\Actions\Reports; use Piwik\Common; +use Piwik\Metrics; use Piwik\Metrics\Formatter; use Piwik\Piwik; use Piwik\Plugin\ViewDataTable; @@ -22,6 +23,7 @@ abstract class Base extends \Piwik\Plugin\Report { $this->category = 'General_Actions'; $this->processedMetrics = false; + $this->recursiveLabelSeparator = '/'; } protected function addBaseDisplayProperties(ViewDataTable $view) @@ -90,7 +92,11 @@ abstract class Base extends \Piwik\Plugin\Report protected function addExcludeLowPopDisplayProperties(ViewDataTable $view) { if (Common::getRequestVar('enable_filter_excludelowpop', '0', 'string') != '0') { - $view->requestConfig->filter_excludelowpop = 'nb_hits'; + if (Common::getRequestVar('flat', 0, 'int') === 1) { + $view->requestConfig->filter_excludelowpop = 'nb_hits'; + } else { + $view->requestConfig->filter_excludelowpop = Metrics::INDEX_PAGE_NB_HITS; + } $view->requestConfig->filter_excludelowpop_value = function () { // computing minimum value to exclude (2 percent of the total number of actions) $visitsInfo = \Piwik\Plugins\VisitsSummary\Controller::getVisitsSummary()->getFirstRow(); diff --git a/plugins/CoreAdminHome/Controller.php b/plugins/CoreAdminHome/Controller.php index 732639874a..eacbb60406 100644 --- a/plugins/CoreAdminHome/Controller.php +++ b/plugins/CoreAdminHome/Controller.php @@ -321,7 +321,10 @@ class Controller extends ControllerAdmin public function optOut() { $trackVisits = !IgnoreCookie::isIgnoreCookieFound(); - + + $dntChecker = new DoNotTrackHeaderChecker(); + $dntFound = $dntChecker->isDoNotTrackFound(); + $nonce = Common::getRequestVar('nonce', false); $language = Common::getRequestVar('language', ''); if ($nonce !== false && Nonce::verifyNonce('Piwik_OptOut', $nonce)) { @@ -330,6 +333,8 @@ class Controller extends ControllerAdmin $trackVisits = !$trackVisits; } + + $lang = APILanguagesManager::getInstance()->isLanguageAvailable($language) ? $language : LanguagesManager::getLanguageCodeForCurrentUser(); @@ -339,6 +344,7 @@ class Controller extends ControllerAdmin // parameter is required) $view = new View("@CoreAdminHome/optOut"); $view->setXFrameOptions('allow'); + $view->dntFound = $dntFound; $view->trackVisits = $trackVisits; $view->nonce = Nonce::getNonce('Piwik_OptOut', 3600); $view->language = $lang; diff --git a/plugins/CoreAdminHome/Model/DuplicateActionRemover.php b/plugins/CoreAdminHome/Model/DuplicateActionRemover.php index 21a5613616..bd3f143b09 100644 --- a/plugins/CoreAdminHome/Model/DuplicateActionRemover.php +++ b/plugins/CoreAdminHome/Model/DuplicateActionRemover.php @@ -56,7 +56,7 @@ class DuplicateActionRemover * @param TableMetadata $tableMetadataAccess * @param LoggerInterface $logger */ - public function __construct($tableMetadataAccess = null, $logger = null) + public function __construct(TableMetadata $tableMetadataAccess = null, LoggerInterface $logger = null) { $this->tableMetadataAccess = $tableMetadataAccess ?: new TableMetadata(); $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface'); diff --git a/plugins/CoreAdminHome/lang/en.json b/plugins/CoreAdminHome/lang/en.json index 42c0ae3960..9579bdd518 100644 --- a/plugins/CoreAdminHome/lang/en.json +++ b/plugins/CoreAdminHome/lang/en.json @@ -59,6 +59,7 @@ "MenuDevelopment": "Development", "OptOutComplete": "Opt-out complete; your visits to this website will not be recorded by the Web Analytics tool.", "OptOutCompleteBis": "Note that if you clear your cookies, delete the opt-out cookie, or if you change computers or Web browsers, you will need to perform the opt-out procedure again.", + "OptOutDntFound": "You are not being tracked since your browser is reporting that you do not want to. This is a setting of your browser so you won't be able to opt-in until you disable the 'Do Not Track' feature.", "OptOutExplanation": "Piwik is dedicated to providing privacy on the Internet. To provide your visitors with the choice of opting-out of Piwik Web Analytics, you can add the following HTML code on one of your website page, for example in a Privacy Policy page.", "OptOutExplanationBis": "This code will display an Iframe containing a link for your visitors to opt-out of Piwik by setting an opt-out cookie in their browsers. %s Click here%s to view the content that will be displayed by the iFrame.", "OptOutForYourVisitors": "Piwik opt-out for your visitors", diff --git a/plugins/CoreAdminHome/templates/optOut.twig b/plugins/CoreAdminHome/templates/optOut.twig index e9af0948c1..5315747a48 100644 --- a/plugins/CoreAdminHome/templates/optOut.twig +++ b/plugins/CoreAdminHome/templates/optOut.twig @@ -4,31 +4,34 @@ <meta charset="utf-8"> </head> <body> -{% if not trackVisits %} - {{ 'CoreAdminHome_OptOutComplete'|translate }} - <br/> - {{ 'CoreAdminHome_OptOutCompleteBis'|translate }} +{% if dntFound %} + {{ 'CoreAdminHome_OptOutDntFound'|translate }} {% else %} - {{ 'CoreAdminHome_YouMayOptOut'|translate }} - <br/> - {{ 'CoreAdminHome_YouMayOptOutBis'|translate }} -{% endif %} -<br/><br/> - -<form method="post" action="?module=CoreAdminHome&action=optOut{% if language %}&language={{ language }}{% endif %}"> - <input type="hidden" name="nonce" value="{{ nonce }}" /> - <input type="hidden" name="fuzz" value="{{ "now"|date }}" /> - <input onclick="this.form.submit()" type="checkbox" id="trackVisits" name="trackVisits" {% if trackVisits %}checked="checked"{% endif %} /> - <label for="trackVisits"><strong> + {% if not trackVisits %} + {{ 'CoreAdminHome_OptOutComplete'|translate }} + <br/> + {{ 'CoreAdminHome_OptOutCompleteBis'|translate }} + {% else %} + {{ 'CoreAdminHome_YouMayOptOut'|translate }} + <br/> + {{ 'CoreAdminHome_YouMayOptOutBis'|translate }} + {% endif %} + <br/><br/> + <form method="post" action="?module=CoreAdminHome&action=optOut{% if language %}&language={{ language }}{% endif %}"> + <input type="hidden" name="nonce" value="{{ nonce }}" /> + <input type="hidden" name="fuzz" value="{{ "now"|date }}" /> + <input onclick="this.form.submit()" type="checkbox" id="trackVisits" name="trackVisits" {% if trackVisits %}checked="checked"{% endif %} /> + <label for="trackVisits"><strong> {% if trackVisits %} {{ 'CoreAdminHome_YouAreOptedIn'|translate }} {{ 'CoreAdminHome_ClickHereToOptOut'|translate }} {% else %} {{ 'CoreAdminHome_YouAreOptedOut'|translate }} {{ 'CoreAdminHome_ClickHereToOptIn'|translate }} {% endif %} - </strong></label> - <noscript> - <button type="submit">{{ 'General_Save'|translate }}</button> - </noscript> -</form> + </strong></label> + <noscript> + <button type="submit">{{ 'General_Save'|translate }}</button> + </noscript> + </form> +{% endif %} </body> </html> diff --git a/plugins/CoreHome/Columns/Metrics/CallableProcessedMetric.php b/plugins/CoreHome/Columns/Metrics/CallableProcessedMetric.php new file mode 100644 index 0000000000..646c48e862 --- /dev/null +++ b/plugins/CoreHome/Columns/Metrics/CallableProcessedMetric.php @@ -0,0 +1,47 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Plugins\CoreHome\Columns\Metrics; + +use Piwik\DataTable\Row; +use Piwik\Plugin\ProcessedMetric; + +class CallableProcessedMetric extends ProcessedMetric +{ + private $name; + private $callback; + private $dependentMetrics; + + public function __construct($name, $callback, $dependentMetrics = array()) + { + $this->name = $name; + $this->callback = $callback; + $this->dependentMetrics = $dependentMetrics; + } + + public function getName() + { + return $this->name; + } + + public function compute(Row $row) + { + if ($this->callback) { + return call_user_func($this->callback, $row); + } + } + + public function getTranslatedName() + { + return ''; + } + + public function getDependentMetrics() + { + return $this->dependentMetrics; + } +}
\ No newline at end of file diff --git a/plugins/CoreHome/Controller.php b/plugins/CoreHome/Controller.php index 7fdfe1d814..38474e5bb7 100644 --- a/plugins/CoreHome/Controller.php +++ b/plugins/CoreHome/Controller.php @@ -49,17 +49,11 @@ class Controller extends \Piwik\Plugin\Controller return 'redirectToCoreHomeIndex'; } - public function renderReportMenu($reportModule = null, $reportAction = null) + public function renderReportMenu(Report $report) { Piwik::checkUserHasSomeViewAccess(); $this->checkSitePermission(); - $report = Report::factory($reportModule, $reportAction); - - if (empty($report)) { - throw new Exception($this->translator->translate('General_ExceptionReportNotFound')); - } - $report->checkIsEnabled(); $menuTitle = $report->getMenuTitle(); @@ -69,38 +63,26 @@ class Controller extends \Piwik\Plugin\Controller } $menuTitle = $this->translator->translate($menuTitle); - $content = $this->renderReportWidget($reportModule, $reportAction); + $content = $this->renderReportWidget($report); return View::singleReport($menuTitle, $content); } - public function renderReportWidget($reportModule = null, $reportAction = null) + public function renderReportWidget(Report $report) { Piwik::checkUserHasSomeViewAccess(); $this->checkSitePermission(); - $report = Report::factory($reportModule, $reportAction); - - if (empty($report)) { - throw new Exception($this->translator->translate('General_ExceptionReportNotFound')); - } - $report->checkIsEnabled(); return $report->render(); } - public function renderWidget($widgetModule = null, $widgetAction = null) + public function renderWidget(PluginWidgets $widget, $method) { Piwik::checkUserHasSomeViewAccess(); - $widget = PluginWidgets::factory($widgetModule, $widgetAction); - - if (!empty($widget)) { - return $widget->$widgetAction(); - } - - throw new Exception($this->translator->translate('General_ExceptionWidgetNotFound')); + return $widget->$method(); } function redirectToCoreHomeIndex() diff --git a/plugins/CoreHome/DataTableRowAction/RowEvolution.php b/plugins/CoreHome/DataTableRowAction/RowEvolution.php index e1bc8d5d4f..85fa98bc3e 100644 --- a/plugins/CoreHome/DataTableRowAction/RowEvolution.php +++ b/plugins/CoreHome/DataTableRowAction/RowEvolution.php @@ -198,6 +198,7 @@ class RowEvolution $view->config->columns_to_display = array_keys($metrics ? : $this->graphMetrics); } + $view->requestConfig->request_parameters_to_modify['label'] = ''; $view->config->show_goals = false; $view->config->show_search = false; $view->config->show_all_views_icons = false; diff --git a/plugins/CoreHome/Menu.php b/plugins/CoreHome/Menu.php index b2c239885e..f63dc76eec 100644 --- a/plugins/CoreHome/Menu.php +++ b/plugins/CoreHome/Menu.php @@ -38,7 +38,7 @@ class Menu extends \Piwik\Plugin\Menu $module = $this->getLoginModule(); if (Piwik::isUserIsAnonymous()) { - $menu->addItem('Login_LogIn', null, array('module' => $module), 999); + $menu->addItem('Login_LogIn', null, array('module' => $module, 'action' => false), 999); } else { $menu->addItem('General_Logout', null, array('module' => $module, 'action' => 'logout', 'idSite' => null), 999); } diff --git a/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.less b/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.less index a2155872e4..da8b79a1fc 100644 --- a/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.less +++ b/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.less @@ -15,7 +15,6 @@ right: -15px; color: @theme-color-link; display: inline; - content: " \25BC"; font-size: 1px; height: 0px; width: 0px; diff --git a/plugins/CorePluginsAdmin/MarketplaceApiClient.php b/plugins/CorePluginsAdmin/MarketplaceApiClient.php index 6472da3dea..37020ef8cd 100644 --- a/plugins/CorePluginsAdmin/MarketplaceApiClient.php +++ b/plugins/CorePluginsAdmin/MarketplaceApiClient.php @@ -18,7 +18,7 @@ use Piwik\Version; class MarketplaceApiClient { const CACHE_TIMEOUT_IN_SECONDS = 1200; - const HTTP_REQUEST_TIMEOUT = 3; + const HTTP_REQUEST_TIMEOUT = 10; private $domain = 'http://plugins.piwik.org'; diff --git a/plugins/CoreUpdater/Controller.php b/plugins/CoreUpdater/Controller.php index 90b0d13aa5..c8f3e927b5 100644 --- a/plugins/CoreUpdater/Controller.php +++ b/plugins/CoreUpdater/Controller.php @@ -37,6 +37,8 @@ use Piwik\View; class Controller extends \Piwik\Plugin\Controller { const PATH_TO_EXTRACT_LATEST_VERSION = '/latest/'; + const LATEST_VERSION_URL = '://builds.piwik.org/piwik.zip'; + const LATEST_BETA_VERSION_URL = '://builds.piwik.org/piwik-%s.zip'; private $coreError = false; private $warningMessages = array(); @@ -48,9 +50,18 @@ class Controller extends \Piwik\Plugin\Controller protected static function getLatestZipUrl($newVersion) { if (@Config::getInstance()->Debug['allow_upgrades_to_beta']) { - return 'http://builds.piwik.org/piwik-' . $newVersion . '.zip'; + $url = sprintf(self::LATEST_BETA_VERSION_URL, $newVersion); + } else { + $url = self::LATEST_VERSION_URL; + } + + if (self::isUpdatingOverHttps()) { + $url = 'https' . $url; + } else { + $url = 'http' . $url; } - return Config::getInstance()->General['latest_version_url']; + + return $url; } public function newVersionAvailable() @@ -426,4 +437,11 @@ class Controller extends \Piwik\Plugin\Controller return PluginManager::getInstance()->getIncompatiblePlugins($piwikVersion); } + public static function isUpdatingOverHttps() + { + $openSslEnabled = extension_loaded('openssl'); + $usingMethodSupportingHttps = (Http::getTransportMethod() !== 'socket'); + + return $openSslEnabled && $usingMethodSupportingHttps; + } } diff --git a/plugins/CoreVisualizations/CoreVisualizations.php b/plugins/CoreVisualizations/CoreVisualizations.php index 54aa127c91..da8fbe9f94 100644 --- a/plugins/CoreVisualizations/CoreVisualizations.php +++ b/plugins/CoreVisualizations/CoreVisualizations.php @@ -9,6 +9,7 @@ namespace Piwik\Plugins\CoreVisualizations; +use Piwik\Common; use Piwik\ViewDataTable\Manager as ViewDataTableManager; require_once PIWIK_INCLUDE_PATH . '/plugins/CoreVisualizations/JqplotDataGenerator.php'; @@ -28,7 +29,8 @@ class CoreVisualizations extends \Piwik\Plugin 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', 'AssetManager.getJavaScriptFiles' => 'getJsFiles', 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', - 'UsersManager.deleteUser' => 'deleteUser' + 'UsersManager.deleteUser' => 'deleteUser', + 'ViewDataTable.addViewDataTable' => 'addViewDataTable' ); } @@ -37,6 +39,23 @@ class CoreVisualizations extends \Piwik\Plugin ViewDataTableManager::clearUserViewDataTableParameters($userLogin); } + public function addViewDataTable(&$viewDataTable) + { + // Both are the same HtmlTable, just the Pivot one has some extra logic in case Pivot is used. + // We don't want to use the same HtmlTable twice in the UI. Therefore we always need to remove one. + if (Common::getRequestVar('pivotBy', '')) { + $tableToRemove = 'Visualizations\HtmlTable'; + } else { + $tableToRemove = 'HtmlTable\PivotBy'; + } + + foreach ($viewDataTable as $index => $table) { + if (Common::stringEndsWith($table, $tableToRemove)) { + unset($viewDataTable[$index]); + } + } + } + public function getStylesheetFiles(&$stylesheets) { $stylesheets[] = "plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less"; diff --git a/plugins/CoreVisualizations/Visualizations/HtmlTable.php b/plugins/CoreVisualizations/Visualizations/HtmlTable.php index dcd2374df9..66cb4c26e9 100644 --- a/plugins/CoreVisualizations/Visualizations/HtmlTable.php +++ b/plugins/CoreVisualizations/Visualizations/HtmlTable.php @@ -70,10 +70,6 @@ class HtmlTable extends Visualization $dataTable = $request->process(); $this->assignTemplateVar('siteSummary', $dataTable); } - - if ($this->requestConfig->pivotBy) { - $this->config->columns_to_display = $this->dataTable->getColumns(); - } } } diff --git a/plugins/CoreVisualizations/Visualizations/HtmlTable/PivotBy.php b/plugins/CoreVisualizations/Visualizations/HtmlTable/PivotBy.php new file mode 100644 index 0000000000..1703988599 --- /dev/null +++ b/plugins/CoreVisualizations/Visualizations/HtmlTable/PivotBy.php @@ -0,0 +1,36 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable; + +use Piwik\DataTable; +use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable; +use Piwik\View; + +/** + * DataTable Visualization that derives from HtmlTable and sets show_extra_columns to true. + */ +class PivotBy extends HtmlTable +{ + public function beforeGenericFiltersAreAppliedToLoadedDataTable() + { + $this->config->columns_to_display = $this->dataTable->getColumns(); + + $this->dataTable->applyQueuedFilters(); + + parent::beforeGenericFiltersAreAppliedToLoadedDataTable(); + } + + public function beforeRender() + { + parent::beforeRender(); + + $this->config->columns_to_display = $this->dataTable->getColumns(); + } +} diff --git a/plugins/CoreVisualizations/Visualizations/HtmlTable/RequestConfig.php b/plugins/CoreVisualizations/Visualizations/HtmlTable/RequestConfig.php index c507d1857e..2f674a5872 100644 --- a/plugins/CoreVisualizations/Visualizations/HtmlTable/RequestConfig.php +++ b/plugins/CoreVisualizations/Visualizations/HtmlTable/RequestConfig.php @@ -11,6 +11,7 @@ namespace Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable; use Piwik\Common; use Piwik\Config as PiwikConfig; +use Piwik\Metrics; use Piwik\ViewDataTable\RequestConfig as VisualizationRequestConfig; /** @@ -33,7 +34,12 @@ class RequestConfig extends VisualizationRequestConfig $this->filter_limit = PiwikConfig::getInstance()->General['datatable_default_limit']; if (Common::getRequestVar('enable_filter_excludelowpop', false) == '1') { - $this->filter_excludelowpop = 'nb_visits'; + if (Common::getRequestVar('flat', 0, 'int') === 1) { + $this->filter_excludelowpop = 'nb_visits'; + } else { + $this->filter_excludelowpop = Metrics::INDEX_NB_VISITS; + } + $this->filter_excludelowpop_value = false; } diff --git a/plugins/CustomAlerts b/plugins/CustomAlerts -Subproject 40db402aeacae2d911331ec94df5d86df6f8713 +Subproject 628d2086db2742a29f39173c8be2fd348a64c6a diff --git a/plugins/CustomVariables/API.php b/plugins/CustomVariables/API.php index 727ef7d42c..1a6fc8c794 100644 --- a/plugins/CustomVariables/API.php +++ b/plugins/CustomVariables/API.php @@ -12,7 +12,6 @@ use Piwik\Archive; use Piwik\DataTable; use Piwik\Date; use Piwik\Metrics; -use Piwik\Piwik; use Piwik\Plugins\Actions\Actions\ActionSiteSearch; /** @@ -32,12 +31,17 @@ class API extends \Piwik\Plugin\API * * @return DataTable|DataTable\Map */ - protected function getDataTable($idSite, $period, $date, $segment, $expanded, $idSubtable) + protected function getDataTable($idSite, $period, $date, $segment, $expanded, $flat, $idSubtable) { - $dataTable = Archive::getDataTableFromArchive(Archiver::CUSTOM_VARIABLE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded, $idSubtable); + $dataTable = Archive::createDataTableFromArchive(Archiver::CUSTOM_VARIABLE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable); $dataTable->filter('Sort', array(Metrics::INDEX_NB_ACTIONS, 'desc', $naturalSort = false, $expanded)); - $dataTable->queueFilter('ReplaceColumnNames'); $dataTable->queueFilter('ColumnDelete', 'nb_uniq_visitors'); + + if ($flat) { + $dataTable->filterSubtables('Sort', array(Metrics::INDEX_NB_ACTIONS, 'desc', $naturalSort = false, $expanded)); + $dataTable->queueFilterSubtables('ColumnDelete', 'nb_uniq_visitors'); + } + return $dataTable; } @@ -48,12 +52,13 @@ class API extends \Piwik\Plugin\API * @param string|bool $segment * @param bool $expanded * @param bool $_leavePiwikCoreVariables + * @param bool $flat * * @return DataTable|DataTable\Map */ - public function getCustomVariables($idSite, $period, $date, $segment = false, $expanded = false, $_leavePiwikCoreVariables = false) + public function getCustomVariables($idSite, $period, $date, $segment = false, $expanded = false, $_leavePiwikCoreVariables = false, $flat = false) { - $dataTable = $this->getDataTable($idSite, $period, $date, $segment, $expanded, $idSubtable = null); + $dataTable = $this->getDataTable($idSite, $period, $date, $segment, $expanded, $flat, $idSubtable = null); if ($dataTable instanceof DataTable && !$_leavePiwikCoreVariables @@ -66,6 +71,11 @@ class API extends \Piwik\Plugin\API } } } + + if ($flat) { + $dataTable->filterSubtables('Piwik\Plugins\CustomVariables\DataTable\Filter\CustomVariablesValuesFromNameId'); + } + return $dataTable; } @@ -90,7 +100,7 @@ class API extends \Piwik\Plugin\API */ public function getCustomVariablesValuesFromNameId($idSite, $period, $date, $idSubtable, $segment = false, $_leavePriceViewedColumn = false) { - $dataTable = $this->getDataTable($idSite, $period, $date, $segment, $expanded = false, $idSubtable); + $dataTable = $this->getDataTable($idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable); if (!$_leavePriceViewedColumn) { $dataTable->deleteColumn('price_viewed'); @@ -98,11 +108,8 @@ class API extends \Piwik\Plugin\API // Hack Ecommerce product price tracking to display correctly $dataTable->renameColumn('price_viewed', 'price'); } - $dataTable->queueFilter('ColumnCallbackReplace', array('label', function ($label) { - return $label == \Piwik\Plugins\CustomVariables\Archiver::LABEL_CUSTOM_VALUE_NOT_DEFINED - ? Piwik::translate('General_NotDefined', Piwik::translate('CustomVariables_ColumnCustomVariableValue')) - : $label; - })); + $dataTable->filter('Piwik\Plugins\CustomVariables\DataTable\Filter\CustomVariablesValuesFromNameId'); + return $dataTable; } } diff --git a/plugins/CustomVariables/DataTable/Filter/CustomVariablesValuesFromNameId.php b/plugins/CustomVariables/DataTable/Filter/CustomVariablesValuesFromNameId.php new file mode 100644 index 0000000000..4bab2f290e --- /dev/null +++ b/plugins/CustomVariables/DataTable/Filter/CustomVariablesValuesFromNameId.php @@ -0,0 +1,42 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\CustomVariables\DataTable\Filter; + +use Piwik\DataTable\BaseFilter; +use Piwik\DataTable\Row; +use Piwik\DataTable; +use Piwik\Piwik; + +class CustomVariablesValuesFromNameId extends BaseFilter +{ + + /** + * Constructor. + * + * @param DataTable $table The table to eventually filter. + */ + public function __construct($table) + { + parent::__construct($table); + } + + /** + * @param DataTable $table + */ + public function filter($table) + { + $notDefinedLabel = Piwik::translate('General_NotDefined', Piwik::translate('CustomVariables_ColumnCustomVariableValue')); + + $table->queueFilter('ColumnCallbackReplace', array('label', function ($label) use ($notDefinedLabel) { + return $label == \Piwik\Plugins\CustomVariables\Archiver::LABEL_CUSTOM_VALUE_NOT_DEFINED + ? $notDefinedLabel + : $label; + })); + } +}
\ No newline at end of file diff --git a/plugins/Events/API.php b/plugins/Events/API.php index bb6584b777..7950cd2637 100644 --- a/plugins/Events/API.php +++ b/plugins/Events/API.php @@ -148,14 +148,17 @@ class API extends \Piwik\Plugin\API } } - protected function getDataTable($name, $idSite, $period, $date, $segment, $expanded = false, $idSubtable = null, $secondaryDimension = false) + protected function getDataTable($name, $idSite, $period, $date, $segment, $expanded = false, $idSubtable = null, $secondaryDimension = false, $flat = false) { Piwik::checkUserHasViewAccess($idSite); $this->checkSecondaryDimension($name, $secondaryDimension); $recordName = $this->getRecordNameForAction($name, $secondaryDimension); - $dataTable = Archive::getDataTableFromArchive($recordName, $idSite, $period, $date, $segment, $expanded, $idSubtable); - if (empty($idSubtable)) { + $dataTable = Archive::createDataTableFromArchive($recordName, $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable); + + if ($flat) { + $dataTable->filterSubtables('Piwik\Plugins\Events\DataTable\Filter\ReplaceEventNameNotSet'); + } else { $dataTable->filter('AddSegmentValue', array(function ($label) { if ($label === Archiver::EVENT_NAME_NOT_SET) { return false; @@ -165,23 +168,24 @@ class API extends \Piwik\Plugin\API })); } - $this->filterDataTable($dataTable); + $dataTable->filter('Piwik\Plugins\Events\DataTable\Filter\ReplaceEventNameNotSet'); + return $dataTable; } - public function getCategory($idSite, $period, $date, $segment = false, $expanded = false, $secondaryDimension = false) + public function getCategory($idSite, $period, $date, $segment = false, $expanded = false, $secondaryDimension = false, $flat = false) { - return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, $idSubtable = false, $secondaryDimension); + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, $idSubtable = false, $secondaryDimension, $flat); } - public function getAction($idSite, $period, $date, $segment = false, $expanded = false, $secondaryDimension = false) + public function getAction($idSite, $period, $date, $segment = false, $expanded = false, $secondaryDimension = false, $flat = false) { - return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, $idSubtable = false, $secondaryDimension); + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, $idSubtable = false, $secondaryDimension, $flat); } - public function getName($idSite, $period, $date, $segment = false, $expanded = false, $secondaryDimension = false) + public function getName($idSite, $period, $date, $segment = false, $expanded = false, $secondaryDimension = false, $flat = false) { - return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, $idSubtable = false, $secondaryDimension); + return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded, $idSubtable = false, $secondaryDimension, $flat); } public function getActionFromCategoryId($idSite, $period, $date, $idSubtable, $segment = false) @@ -213,20 +217,4 @@ class API extends \Piwik\Plugin\API { return $this->getDataTable(__FUNCTION__, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); } - - /** - * @param DataTable $dataTable - */ - protected function filterDataTable($dataTable) - { - $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS)); - $dataTable->queueFilter('ReplaceColumnNames'); - $dataTable->queueFilter('ReplaceSummaryRowLabel'); - $dataTable->filter(function (DataTable $table) { - $row = $table->getRowFromLabel(Archiver::EVENT_NAME_NOT_SET); - if ($row) { - $row->setColumn('label', Piwik::translate('General_NotDefined', Piwik::translate('Events_EventName'))); - } - }); - } }
\ No newline at end of file diff --git a/plugins/Events/DataTable/Filter/ReplaceEventNameNotSet.php b/plugins/Events/DataTable/Filter/ReplaceEventNameNotSet.php new file mode 100644 index 0000000000..a07695b5f8 --- /dev/null +++ b/plugins/Events/DataTable/Filter/ReplaceEventNameNotSet.php @@ -0,0 +1,39 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\Events\DataTable\Filter; + +use Piwik\DataTable\BaseFilter; +use Piwik\DataTable\Row; +use Piwik\DataTable; +use Piwik\Piwik; +use Piwik\Plugins\Events\Archiver; + +class ReplaceEventNameNotSet extends BaseFilter +{ + /** + * Constructor. + * + * @param DataTable $table The table to eventually filter. + */ + public function __construct($table) + { + parent::__construct($table); + } + + /** + * @param DataTable $table + */ + public function filter($table) + { + $row = $table->getRowFromLabel(Archiver::EVENT_NAME_NOT_SET); + if ($row) { + $row->setColumn('label', Piwik::translate('General_NotDefined', Piwik::translate('Events_EventName'))); + } + } +}
\ No newline at end of file diff --git a/plugins/ExampleUI/Controller.php b/plugins/ExampleUI/Controller.php index 9bfd517aa0..210093c8b5 100644 --- a/plugins/ExampleUI/Controller.php +++ b/plugins/ExampleUI/Controller.php @@ -90,6 +90,7 @@ class Controller extends \Piwik\Plugin\Controller $view = $this->getLastUnitGraphAcrossPlugins($this->pluginName, __FUNCTION__, $columns, $selectableColumns = array('server1', 'server2'), 'My documentation', 'ExampleUI.getTemperaturesEvolution'); $view->requestConfig->filter_sort_column = 'label'; + $view->requestConfig->filter_sort_order = 'asc'; if (empty($view->config->columns_to_display) && !empty($defaultColumns)) { $view->config->columns_to_display = $defaultColumns; diff --git a/plugins/Goals/Columns/Metrics/GoalSpecificProcessedMetric.php b/plugins/Goals/Columns/Metrics/GoalSpecificProcessedMetric.php index 1ae1c41149..d32290bfdc 100644 --- a/plugins/Goals/Columns/Metrics/GoalSpecificProcessedMetric.php +++ b/plugins/Goals/Columns/Metrics/GoalSpecificProcessedMetric.php @@ -9,9 +9,11 @@ namespace Piwik\Plugins\Goals\Columns\Metrics; use Piwik\Common; use Piwik\DataTable\Row; +use Piwik\Metrics; use Piwik\Piwik; use Piwik\Plugin\ProcessedMetric; use Piwik\Plugins\Goals\API as GoalsAPI; +use Piwik\Tracker\GoalManager; /** * Base class for processed metrics that are calculated using metrics that are @@ -60,6 +62,16 @@ abstract class GoalSpecificProcessedMetric extends ProcessedMetric $alternateKey = 'idgoal=' . $this->idGoal; if (isset($allGoalMetrics[$alternateKey])) { return $allGoalMetrics[$alternateKey]; + } elseif ($this->idGoal === Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER) { + $alternateKey = GoalManager::IDGOAL_ORDER; + if (isset($allGoalMetrics[$alternateKey])) { + return $allGoalMetrics[$alternateKey]; + } + } elseif ($this->idGoal === Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) { + $alternateKey = GoalManager::IDGOAL_CART; + if (isset($allGoalMetrics[$alternateKey])) { + return $allGoalMetrics[$alternateKey]; + } } else { return array(); } diff --git a/plugins/Installation/SystemCheck.php b/plugins/Installation/SystemCheck.php index de55506884..93ed1ac75f 100644 --- a/plugins/Installation/SystemCheck.php +++ b/plugins/Installation/SystemCheck.php @@ -19,6 +19,7 @@ use Piwik\Filechecks; use Piwik\Filesystem; use Piwik\Http; use Piwik\Piwik; +use Piwik\Plugins\CoreUpdater; use Piwik\Plugins\UserCountry\LocationProvider; use Piwik\SettingsServer; use Piwik\Url; @@ -102,6 +103,9 @@ class SystemCheck $infos['tracker_status'] = Common::getRequestVar('trackerStatus', 0, 'int'); $infos['is_nfs'] = Filesystem::checkIfFileSystemIsNFS(); + + $infos['https_update'] = CoreUpdater\Controller::isUpdatingOverHttps(); + $infos = self::enrichSystemChecks($infos); return $infos; diff --git a/plugins/Installation/lang/en.json b/plugins/Installation/lang/en.json index 9711d997c3..2d3afbf8c9 100644 --- a/plugins/Installation/lang/en.json +++ b/plugins/Installation/lang/en.json @@ -115,6 +115,8 @@ "SystemCheckCronArchiveProcess": "Archive Cron", "SystemCheckCronArchiveProcessCLI": "Managing processes via CLI", "SystemCheckPhpSetting": "To prevent some critical issue, you must set the following in your php.ini file: %s", + "SystemCheckUpdateHttps": "Update over HTTPS", + "SystemCheckUpdateHttpsNotSupported": "Piwik cannot use HTTPS to update, it will fall back to the insecure HTTP update. Check that CURL or allow_url_fopen is supported and that the openssl PHP extension is installed: http://piwik.org/faq/troubleshooting/faq_177/.", "NotSupported": "not supported", "Tables": "Creating the Tables", "TablesCreatedSuccess": "Tables created with success!", diff --git a/plugins/Installation/templates/_systemCheckSection.twig b/plugins/Installation/templates/_systemCheckSection.twig index f797d18969..b56db9b672 100755 --- a/plugins/Installation/templates/_systemCheckSection.twig +++ b/plugins/Installation/templates/_systemCheckSection.twig @@ -356,6 +356,17 @@ </tr> {% endif %} + <tr> + <td class="label">{{ 'Installation_SystemCheckUpdateHttps'|translate }}</td> + <td> + {% if infos.https_update %} + {{ ok }} + {% else %} + {{ warning }} {{ 'Installation_SystemCheckUpdateHttpsNotSupported'|translate }} + {% endif %} + </td> + </tr> + </table> {% include "@Installation/_integrityDetails.twig" %} diff --git a/plugins/Live/API.php b/plugins/Live/API.php index ed11c6c277..68d7819aa6 100644 --- a/plugins/Live/API.php +++ b/plugins/Live/API.php @@ -60,17 +60,60 @@ class API extends \Piwik\Plugin\API * @param int $idSite Id Site * @param int $lastMinutes Number of minutes to look back at * @param bool|string $segment + * @param array $showColumns The columns to show / not to request. Eg 'visits', 'actions', ... + * @param array $hideColumns The columns to hide / not to request. Eg 'visits', 'actions', ... * @return array( visits => N, actions => M, visitsConverted => P ) */ - public function getCounters($idSite, $lastMinutes, $segment = false) + public function getCounters($idSite, $lastMinutes, $segment = false, $showColumns = array(), $hideColumns = array()) { Piwik::checkUserHasViewAccess($idSite); $model = new Model(); - return $model->queryCounters($idSite, $lastMinutes, $segment); + + $counters = array(); + + $hasVisits = true; + if ($this->shouldColumnBePresentInResponse('visits', $showColumns, $hideColumns)) { + $counters['visits'] = $model->getNumVisits($idSite, $lastMinutes, $segment); + $hasVisits = !empty($counters['visits']); + } + + if ($this->shouldColumnBePresentInResponse('actions', $showColumns, $hideColumns)) { + if ($hasVisits) { + $counters['actions'] = $model->getNumActions($idSite, $lastMinutes, $segment); + } else { + $counters['actions'] = 0; + } + } + + if ($this->shouldColumnBePresentInResponse('visitors', $showColumns, $hideColumns)) { + if ($hasVisits) { + $counters['visitors'] = $model->getNumVisitors($idSite, $lastMinutes, $segment); + } else { + $counters['visitors'] = 0; + } + } + + if ($this->shouldColumnBePresentInResponse('visitsConverted', $showColumns, $hideColumns)) { + if ($hasVisits) { + $counters['visitsConverted'] = $model->getNumVisitsConverted($idSite, $lastMinutes, $segment); + } else { + $counters['visitsConverted'] = 0; + } + } + + return array($counters); + } + + private function shouldColumnBePresentInResponse($column, $showColumns, $hideColumns) + { + $show = (empty($showColumns) || in_array($column, $showColumns)); + $hide = in_array($column, $hideColumns); + + return $show && !$hide; } /** - * The same functionnality can be obtained using segment=visitorId==$visitorId with getLastVisitsDetails + * The same functionality can be obtained using segment=visitorId==$visitorId with getLastVisitsDetails * * @deprecated * @ignore diff --git a/plugins/Live/Controller.php b/plugins/Live/Controller.php index 421a0afb62..a2d3e44026 100644 --- a/plugins/Live/Controller.php +++ b/plugins/Live/Controller.php @@ -88,9 +88,9 @@ class Controller extends \Piwik\Plugin\Controller private function setCounters($view) { $segment = Request::getRawSegmentFromRequest(); - $last30min = API::getInstance()->getCounters($this->idSite, $lastMinutes = 30, $segment); + $last30min = API::getInstance()->getCounters($this->idSite, $lastMinutes = 30, $segment, array('visits', 'actions')); $last30min = $last30min[0]; - $today = API::getInstance()->getCounters($this->idSite, $lastMinutes = 24 * 60, $segment); + $today = API::getInstance()->getCounters($this->idSite, $lastMinutes = 24 * 60, $segment, array('visits', 'actions')); $today = $today[0]; $view->visitorsCountHalfHour = $last30min['visits']; $view->visitorsCountToday = $today['visits']; diff --git a/plugins/Live/Model.php b/plugins/Live/Model.php index 19a44ba1d6..0d1b0318fe 100644 --- a/plugins/Live/Model.php +++ b/plugins/Live/Model.php @@ -201,58 +201,101 @@ class Model * @param $idSite * @param $lastMinutes * @param $segment - * @return array + * @return int * @throws Exception */ - public function queryCounters($idSite, $lastMinutes, $segment) + public function getNumActions($idSite, $lastMinutes, $segment) { - $lastMinutes = (int)$lastMinutes; + return $this->getLastMinutesCounterForQuery( + $idSite, + $lastMinutes, + $segment, + 'COUNT(*)', + 'log_link_visit_action', + 'log_link_visit_action.server_time >= ?' + ); + } - $counters = array( - 'visits' => 0, - 'actions' => 0, - 'visitors' => 0, - 'visitsConverted' => 0, + /** + * @param $idSite + * @param $lastMinutes + * @param $segment + * @return int + * @throws Exception + */ + public function getNumVisitsConverted($idSite, $lastMinutes, $segment) + { + return $this->getLastMinutesCounterForQuery( + $idSite, + $lastMinutes, + $segment, + 'COUNT(*)', + 'log_conversion', + 'log_conversion.server_time >= ?' ); + } + + /** + * @param $idSite + * @param $lastMinutes + * @param $segment + * @return int + * @throws Exception + */ + public function getNumVisits($idSite, $lastMinutes, $segment) + { + return $this->getLastMinutesCounterForQuery( + $idSite, + $lastMinutes, + $segment, + 'COUNT(log_visit.visit_last_action_time)', + 'log_visit', + 'log_visit.visit_last_action_time >= ?' + ); + } + + /** + * @param $idSite + * @param $lastMinutes + * @param $segment + * @return int + * @throws Exception + */ + public function getNumVisitors($idSite, $lastMinutes, $segment) + { + return $this->getLastMinutesCounterForQuery( + $idSite, + $lastMinutes, + $segment, + 'COUNT(DISTINCT log_visit.idvisitor)', + 'log_visit', + 'log_visit.visit_last_action_time >= ?' + ); + } + + private function getLastMinutesCounterForQuery($idSite, $lastMinutes, $segment, $select, $from, $where) + { + $lastMinutes = (int)$lastMinutes; if (empty($lastMinutes)) { - return array($counters); + return 0; } - list($whereIdSites, $idSites) = $this->getIdSitesWhereClause($idSite); + list($whereIdSites, $idSites) = $this->getIdSitesWhereClause($idSite, $from); - $select = "count(*) as visits, COUNT(DISTINCT log_visit.idvisitor) as visitors"; - $where = $whereIdSites . "AND log_visit.visit_last_action_time >= ?"; - $bind = $idSites; + $bind = $idSites; $bind[] = Date::factory(time() - $lastMinutes * 60)->toString('Y-m-d H:i:s'); - $segment = new Segment($segment, $idSite); - $query = $segment->getSelectQuery($select, 'log_visit', $where, $bind); - - $data = Db::fetchAll($query['sql'], $query['bind']); - - $counters['visits'] = $data[0]['visits']; - $counters['visitors'] = $data[0]['visitors']; + $where = $whereIdSites . "AND " . $where; - $select = "count(*)"; - $from = 'log_link_visit_action'; - list($whereIdSites) = $this->getIdSitesWhereClause($idSite, $from); - $where = $whereIdSites . "AND log_link_visit_action.server_time >= ?"; - $query = $segment->getSelectQuery($select, $from, $where, $bind); - $counters['actions'] = Db::fetchOne($query['sql'], $query['bind']); + $segment = new Segment($segment, $idSite); + $query = $segment->getSelectQuery($select, $from, $where, $bind); - $select = "count(*)"; - $from = 'log_conversion'; - list($whereIdSites) = $this->getIdSitesWhereClause($idSite, $from); - $where = $whereIdSites . "AND log_conversion.server_time >= ?"; - $query = $segment->getSelectQuery($select, $from, $where, $bind); - $counters['visitsConverted'] = Db::fetchOne($query['sql'], $query['bind']); + $numVisitors = Db::fetchOne($query['sql'], $query['bind']); - return array($counters); + return $numVisitors; } - - /** * @param $idSite * @param string $table diff --git a/plugins/Live/Reports/GetSimpleLastVisitCount.php b/plugins/Live/Reports/GetSimpleLastVisitCount.php index 34ba773fa9..e2a17135c5 100644 --- a/plugins/Live/Reports/GetSimpleLastVisitCount.php +++ b/plugins/Live/Reports/GetSimpleLastVisitCount.php @@ -30,7 +30,8 @@ class GetSimpleLastVisitCount extends Base { $lastMinutes = Config::getInstance()->General[Controller::SIMPLE_VISIT_COUNT_WIDGET_LAST_MINUTES_CONFIG_KEY]; - $lastNData = Request::processRequest('Live.getCounters', array('lastMinutes' => $lastMinutes)); + $params = array('lastMinutes' => $lastMinutes, 'showColumns' => array('visits', 'visitors', 'actions')); + $lastNData = Request::processRequest('Live.getCounters', $params); $formatter = new Formatter(); diff --git a/plugins/Live/tests/System/APITest.php b/plugins/Live/tests/System/APITest.php index 2c85088a19..4b65a5ae4e 100644 --- a/plugins/Live/tests/System/APITest.php +++ b/plugins/Live/tests/System/APITest.php @@ -70,6 +70,38 @@ class APITest extends SystemTestCase $this->assertEquals($this->buildCounter(0, 0, 0, 0), $counters); } + public function test_GetCounters_ShouldHideAllColumnsIfRequested() + { + $exampleCounter = $this->buildCounter(0, 0, 0, 0); + $counters = $this->api->getCounters($this->idSite, 5, false, array(), array_keys($exampleCounter[0])); + $this->assertEquals(array(array()), $counters); + } + + public function test_GetCounters_ShouldHideSomeColumnsIfRequested() + { + $counters = $this->api->getCounters($this->idSite, 20, false, array(), array('visitsConverted', 'visitors')); + $this->assertEquals(array(array('visits' => 24, 'actions' => 60)), $counters); + } + + public function test_GetCounters_ShouldShowAllColumnsIfRequested() + { + $counter = $this->buildCounter(24, 60, 20, 40); + $counters = $this->api->getCounters($this->idSite, 20, false, array_keys($counter[0])); + $this->assertEquals($counter, $counters); + } + + public function test_GetCounters_ShouldShowSomeColumnsIfRequested() + { + $counters = $this->api->getCounters($this->idSite, 20, false, array('visits', 'actions')); + $this->assertEquals(array(array('visits' => 24, 'actions' => 60)), $counters); + } + + public function test_GetCounters_ShouldHideColumnIfGivenInShowAndHide() + { + $counters = $this->api->getCounters($this->idSite, 20, false, array('visits', 'actions'), array('actions')); + $this->assertEquals(array(array('visits' => 24)), $counters); + } + private function trackSomeVisits() { $nowTimestamp = time(); diff --git a/plugins/Login/lang/en.json b/plugins/Login/lang/en.json index d43d85c212..3164a4bbcd 100644 --- a/plugins/Login/lang/en.json +++ b/plugins/Login/lang/en.json @@ -16,7 +16,7 @@ "PasswordChanged": "Your password has been changed.", "PasswordRepeat": "Password (repeat)", "PasswordsDoNotMatch": "Passwords do not match.", - "PluginDescription": "Provides authentication via username and password as well as password reset functionnality. Authentication method can be changed by using another Login plugin such as LoginLdap available on the Marketplace.", + "PluginDescription": "Provides authentication via username and password as well as password reset functionality. Authentication method can be changed by using another Login plugin such as LoginLdap available on the Marketplace.", "RememberMe": "Remember Me", "ResetPasswordInstructions": "Enter a new password for your account." } diff --git a/plugins/Provider/Columns/Provider.php b/plugins/Provider/Columns/Provider.php index 24ab6cecd2..b7ca57e9c9 100644 --- a/plugins/Provider/Columns/Provider.php +++ b/plugins/Provider/Columns/Provider.php @@ -9,7 +9,7 @@ namespace Piwik\Plugins\Provider\Columns; use Piwik\Common; -use Piwik\IP; +use Piwik\Network\IP; use Piwik\Network\IPUtils; use Piwik\Piwik; use Piwik\Plugin\Dimension\VisitDimension; @@ -87,12 +87,17 @@ class Provider extends VisitDimension /** * Returns the hostname given the IP address string * - * @param string $ip IP Address + * @param string $ipStr IP Address * @return string hostname (or human-readable IP address) */ - private function getHost($ip) + private function getHost($ipStr) { - return trim(strtolower(@IP::getHostByAddr($ip))); + $ip = IP::fromStringIP($ipStr); + + $host = $ip->getHostname(); + $host = ($host === null ? $ipStr : $host); + + return trim(strtolower($host)); } public function getName() diff --git a/plugins/QueuedTracking b/plugins/QueuedTracking -Subproject ff1228265cc695fbec8b2997f442ed0bb90f289 +Subproject bc39d75e4c9df58c865ae44f6eae23676e7baf9 diff --git a/plugins/Referrers/API.php b/plugins/Referrers/API.php index 4ac28978e3..2cacbb2833 100644 --- a/plugins/Referrers/API.php +++ b/plugins/Referrers/API.php @@ -13,6 +13,7 @@ use Piwik\API\ResponseBuilder; use Piwik\Archive; use Piwik\Common; use Piwik\DataTable\Row; +use Piwik\DataTable\Manager as DataTableManager; use Piwik\DataTable; use Piwik\Date; use Piwik\Metrics; @@ -43,7 +44,6 @@ class API extends \Piwik\Plugin\API protected function getDataTable($name, $idSite, $period, $date, $segment, $expanded = false, $idSubtable = null) { $dataTable = Archive::getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable); - $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded)); $dataTable->queueFilter('ReplaceColumnNames'); return $dataTable; } @@ -137,19 +137,19 @@ class API extends \Piwik\Plugin\API return $dataTable; } - public function getKeywords($idSite, $period, $date, $segment = false, $expanded = false) + public function getKeywords($idSite, $period, $date, $segment = false, $expanded = false, $flat = false) { - $dataTable = $this->getDataTable(Archiver::KEYWORDS_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); - $dataTable->filter('AddSegmentValue'); - $dataTable->queueFilter('PrependSegment', array('referrerType==search;')); + $dataTable = Archive::createDataTableFromArchive(Archiver::KEYWORDS_RECORD_NAME, $idSite, $period, $date, $segment, $expanded, $flat); - $dataTable = $this->handleKeywordNotDefined($dataTable); - return $dataTable; - } + if ($flat) { + $dataTable->filterSubtables('Piwik\Plugins\Referrers\DataTable\Filter\SearchEnginesFromKeywordId', array($dataTable)); + } else { + $dataTable->filter('AddSegmentValue'); + $dataTable->queueFilter('PrependSegment', array('referrerType==search;')); + } + + $dataTable->queueFilter('Piwik\Plugins\Referrers\DataTable\Filter\KeywordNotDefined'); - protected function handleKeywordNotDefined($dataTable) - { - $dataTable->queueFilter('ColumnCallbackReplace', array('label', __NAMESPACE__ . '\API::getCleanKeyword')); return $dataTable; } @@ -225,27 +225,28 @@ class API extends \Piwik\Plugin\API public function getSearchEnginesFromKeywordId($idSite, $period, $date, $idSubtable, $segment = false) { $dataTable = $this->getDataTable(Archiver::KEYWORDS_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); - $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getSearchEngineUrlFromName')); - $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', __NAMESPACE__ . '\getSearchEngineLogoFromUrl')); - // get the keyword and create the URL to the search result page $keywords = $this->getKeywords($idSite, $period, $date, $segment); - $subTable = $keywords->getRowFromIdSubDataTable($idSubtable); - if ($subTable) { - $keyword = $subTable->getColumn('label'); - $dataTable->queueFilter('MetadataCallbackReplace', array('url', __NAMESPACE__ . '\getSearchEngineUrlFromUrlAndKeyword', array($keyword))); - } + $dataTable->filter('Piwik\Plugins\Referrers\DataTable\Filter\SearchEnginesFromKeywordId', array($keywords, $idSubtable)); + return $dataTable; } - public function getSearchEngines($idSite, $period, $date, $segment = false, $expanded = false) + public function getSearchEngines($idSite, $period, $date, $segment = false, $expanded = false, $flat = false) { - $dataTable = $this->getDataTable(Archiver::SEARCH_ENGINES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); + $dataTable = Archive::createDataTableFromArchive(Archiver::SEARCH_ENGINES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded, $flat); + + if ($flat) { + $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getSearchEngineUrlFromName')); + $dataTable->filter('MetadataCallbackAddMetadata', array('url', 'logo', __NAMESPACE__ . '\getSearchEngineLogoFromUrl')); + $dataTable->filterSubtables('Piwik\Plugins\Referrers\DataTable\Filter\KeywordsFromSearchEngineId', array($dataTable)); + } else { + $dataTable->filter('AddSegmentByLabel', array('referrerName')); + $dataTable->queueFilter('PrependSegment', array('referrerType==search;')); + $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getSearchEngineUrlFromName')); + $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', __NAMESPACE__ . '\getSearchEngineLogoFromUrl')); + } - $dataTable->filter('AddSegmentByLabel', array('referrerName')); - $dataTable->queueFilter('PrependSegment', array('referrerType==search;')); - $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getSearchEngineUrlFromName')); - $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', __NAMESPACE__ . '\getSearchEngineLogoFromUrl')); return $dataTable; } @@ -257,26 +258,8 @@ class API extends \Piwik\Plugin\API $searchEngines = $this->getSearchEngines($idSite, $period, $date, $segment); $searchEngines->applyQueuedFilters(); - if ($searchEngines instanceof DataTable\Map) { - $dataTables = $searchEngines->getDataTables(); - - // find first datatable containing data - foreach ($dataTables as $subTable) { + $dataTable->filter('Piwik\Plugins\Referrers\DataTable\Filter\KeywordsFromSearchEngineId', array($searchEngines, $idSubtable)); - $subTableRow = $subTable->getRowFromIdSubDataTable($idSubtable); - if (!empty($subTableRow)) { - break; - } - } - } else { - $subTableRow = $searchEngines->getRowFromIdSubDataTable($idSubtable); - } - - if (!empty($subTableRow)) { - $searchEngineUrl = $subTableRow->getMetadata('url'); - $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getSearchEngineUrlFromKeywordAndUrl', array($searchEngineUrl))); - } - $dataTable = $this->handleKeywordNotDefined($dataTable); return $dataTable; } @@ -296,22 +279,24 @@ class API extends \Piwik\Plugin\API return $dataTable; } - public function getWebsites($idSite, $period, $date, $segment = false, $expanded = false) + public function getWebsites($idSite, $period, $date, $segment = false, $expanded = false, $flat = false) { - $dataTable = $this->getDataTable(Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); - $dataTable->filter('AddSegmentByLabel', array('referrerName')); + $dataTable = Archive::createDataTableFromArchive(Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable = null); + + if ($flat) { + $dataTable->filterSubtables('Piwik\Plugins\Referrers\DataTable\Filter\UrlsFromWebsiteId'); + } else { + $dataTable->filter('AddSegmentByLabel', array('referrerName')); + } + return $dataTable; } public function getUrlsFromWebsiteId($idSite, $period, $date, $idSubtable, $segment = false) { $dataTable = $this->getDataTable(Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); - // the htmlspecialchars_decode call is for BC for before 1.1 - // as the Referrer URL was previously encoded in the log tables, but is now recorded raw - $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', function ($label) { - return htmlspecialchars_decode($label); - })); - $dataTable->queueFilter('ColumnCallbackReplace', array('label', __NAMESPACE__ . '\getPathFromUrl')); + $dataTable->filter('Piwik\Plugins\Referrers\DataTable\Filter\UrlsFromWebsiteId'); + return $dataTable; } @@ -324,11 +309,13 @@ class API extends \Piwik\Plugin\API * @param string $date * @param string|bool $segment * @param bool $expanded + * @param bool $flat * @return DataTable */ - public function getSocials($idSite, $period, $date, $segment = false, $expanded = false) + public function getSocials($idSite, $period, $date, $segment = false, $expanded = false, $flat = false) { - $dataTable = $this->getDataTable(Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); + $dataTable = Archive::createDataTableFromArchive(Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded, false); + $dataTable->filter('ColumnCallbackDeleteRow', array('label', function ($url) { return !isSocialUrl($url); })); $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getSocialMainUrl')); $dataTable->filter('GroupBy', array('label', __NAMESPACE__ . '\getSocialNetworkFromDomain')); @@ -336,6 +323,10 @@ class API extends \Piwik\Plugin\API $this->setSocialIdSubtables($dataTable); $this->removeSubtableMetadata($dataTable); + if ($flat) { + $this->buildExpandedTableForFlattenGetSocials($idSite, $period, $date, $segment, $expanded, $dataTable); + } + $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', __NAMESPACE__ . '\getSocialsLogoFromUrl')); return $dataTable; @@ -385,13 +376,7 @@ class API extends \Piwik\Plugin\API // merge the datatable's subtables which contain the individual URLs $dataTable = $dataTable->mergeSubtables(); - // make url labels clickable - $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url')); - - // prettify the DataTable - $dataTable->filter('ColumnCallbackReplace', array('label', __NAMESPACE__ . '\removeUrlProtocol')); - $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded)); - $dataTable->queueFilter('ReplaceColumnNames'); + $dataTable->filter('Piwik\Plugins\Referrers\DataTable\Filter\UrlsForSocial', array($expanded)); return $dataTable; } @@ -540,4 +525,45 @@ class API extends \Piwik\Plugin\API } } + /** + * @param int $idSite + * @param string $period + * @param string $date + * @param string|false $segment + * @param bool $expanded + * @param DataTable $dataTable + */ + private function buildExpandedTableForFlattenGetSocials($idSite, $period, $date, $segment, $expanded, $dataTable) + { + $urlsTable = Archive::createDataTableFromArchive(Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded, $flat = true); + $urlsTable->filter('ColumnCallbackDeleteRow', array('label', function ($url) { + return !isSocialUrl($url); + })); + $urlsTable = $urlsTable->mergeSubtables(); + + foreach ($dataTable->getRows() as $row) { + $row->removeSubtable(); + + $social = $row->getColumn('label'); + $newTable = $urlsTable->getEmptyClone(); + + $rows = $urlsTable->getRows(); + foreach ($rows as $id => $urlsTableRow) { + $url = $urlsTableRow->getColumn('label'); + if (isSocialUrl($url, $social)) { + $newTable->addRow($urlsTableRow); + $urlsTable->deleteRow($id); + } + } + + if ($newTable->getRowsCount()) { + $newTable->filter('Piwik\Plugins\Referrers\DataTable\Filter\UrlsForSocial', array($expanded)); + $row->setSubtable($newTable); + } + } + + Common::destroy($urlsTable); + $urlsTable = null; + } + } diff --git a/plugins/Referrers/DataTable/Filter/KeywordNotDefined.php b/plugins/Referrers/DataTable/Filter/KeywordNotDefined.php new file mode 100644 index 0000000000..dbee6f865b --- /dev/null +++ b/plugins/Referrers/DataTable/Filter/KeywordNotDefined.php @@ -0,0 +1,25 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\Referrers\DataTable\Filter; + +use Piwik\DataTable\Row; +use Piwik\DataTable; + +class KeywordNotDefined extends DataTable\Filter\ColumnCallbackReplace +{ + /** + * Constructor. + * + * @param DataTable $table The table to eventually filter. + */ + public function __construct($table) + { + parent::__construct($table, 'label', 'Piwik\Plugins\Referrers\API::getCleanKeyword'); + } +}
\ No newline at end of file diff --git a/plugins/Referrers/DataTable/Filter/KeywordsFromSearchEngineId.php b/plugins/Referrers/DataTable/Filter/KeywordsFromSearchEngineId.php new file mode 100644 index 0000000000..1688713a86 --- /dev/null +++ b/plugins/Referrers/DataTable/Filter/KeywordsFromSearchEngineId.php @@ -0,0 +1,61 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\Referrers\DataTable\Filter; + +use Piwik\DataTable\BaseFilter; +use Piwik\DataTable\Row; +use Piwik\DataTable; + +class KeywordsFromSearchEngineId extends BaseFilter +{ + /** + * @var DataTable + */ + private $firstLevelSearchEnginesTable; + + /** + * @var int + */ + private $idSubtable; + + /** + * Constructor. + * + * @param DataTable $table The table to eventually filter. + */ + public function __construct($table, $firstLevelSearchEnginesTable, $idSubtable = null) + { + parent::__construct($table); + + $this->firstLevelSearchEnginesTable = $firstLevelSearchEnginesTable; + $this->idSubtable = $idSubtable; + } + + /** + * @param DataTable $table + */ + public function filter($table) + { + $idSubtable = $this->idSubtable ? : $table->getId(); + $subTableRow = $this->firstLevelSearchEnginesTable->getRowFromIdSubDataTable($idSubtable); + + if (!empty($subTableRow)) { + $searchEngineUrl = $subTableRow->getMetadata('url'); + $table->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik\Plugins\Referrers\getSearchEngineUrlFromKeywordAndUrl', array($searchEngineUrl))); + $table->queueFilter(function (DataTable $table) { + $row = $table->getRowFromId(DataTable::ID_SUMMARY_ROW); + if ($row) { + $row->deleteMetadata('url'); + } + }); + } + + $table->queueFilter('Piwik\Plugins\Referrers\DataTable\Filter\KeywordNotDefined'); + } +}
\ No newline at end of file diff --git a/plugins/Referrers/DataTable/Filter/SearchEnginesFromKeywordId.php b/plugins/Referrers/DataTable/Filter/SearchEnginesFromKeywordId.php new file mode 100644 index 0000000000..feab8636af --- /dev/null +++ b/plugins/Referrers/DataTable/Filter/SearchEnginesFromKeywordId.php @@ -0,0 +1,63 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\Referrers\DataTable\Filter; + +use Piwik\DataTable\BaseFilter; +use Piwik\DataTable\Row; +use Piwik\DataTable; + +class SearchEnginesFromKeywordId extends BaseFilter +{ + /** + * @var DataTable + */ + private $firstLevelKeywordTable; + + /** + * @var int + */ + private $idSubtable; + + /** + * Constructor. + * + * @param DataTable $table The table to eventually filter. + */ + public function __construct($table, $firstLevelKeywordTable, $idSubtable = null) + { + parent::__construct($table); + + $this->firstLevelKeywordTable = $firstLevelKeywordTable; + $this->idSubtable = $idSubtable; + } + + /** + * @param DataTable $table + */ + public function filter($table) + { + $idSubtable = $this->idSubtable ? : $table->getId(); + + $table->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik\Plugins\Referrers\getSearchEngineUrlFromName')); + $table->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl')); + + // get the keyword and create the URL to the search result page + $rootRow = $this->firstLevelKeywordTable->getRowFromIdSubDataTable($idSubtable); + if ($rootRow) { + $keyword = $rootRow->getColumn('label'); + $table->queueFilter('MetadataCallbackReplace', array('url', 'Piwik\Plugins\Referrers\getSearchEngineUrlFromUrlAndKeyword', array($keyword))); + $table->queueFilter(function (DataTable $table) { + $row = $table->getRowFromId(DataTable::ID_SUMMARY_ROW); + if ($row) { + $row->deleteMetadata('url'); + } + }); + } + } +}
\ No newline at end of file diff --git a/plugins/Referrers/DataTable/Filter/UrlsForSocial.php b/plugins/Referrers/DataTable/Filter/UrlsForSocial.php new file mode 100644 index 0000000000..ad86093060 --- /dev/null +++ b/plugins/Referrers/DataTable/Filter/UrlsForSocial.php @@ -0,0 +1,49 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\Referrers\DataTable\Filter; + +use Piwik\DataTable\BaseFilter; +use Piwik\DataTable\Row; +use Piwik\DataTable; +use Piwik\Metrics; + +class UrlsForSocial extends BaseFilter +{ + /** + * @var bool + */ + private $sortRecursive; + + /** + * Constructor. + * + * @param DataTable $table The table to eventually filter. + * @param bool $sortRecursive Whether to sort recursive or not + */ + public function __construct($table, $sortRecursive) + { + parent::__construct($table); + + $this->sortRecursive = $sortRecursive; + } + + /** + * @param DataTable $table + */ + public function filter($table) + { + // make url labels clickable + $table->filter('ColumnCallbackAddMetadata', array('label', 'url')); + + // prettify the DataTable + $table->filter('ColumnCallbackReplace', array('label', 'Piwik\Plugins\Referrers\removeUrlProtocol')); + $table->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc', $naturalSort = false, $this->sortRecursive)); + $table->queueFilter('ReplaceColumnNames'); + } +}
\ No newline at end of file diff --git a/plugins/Referrers/DataTable/Filter/UrlsFromWebsiteId.php b/plugins/Referrers/DataTable/Filter/UrlsFromWebsiteId.php new file mode 100644 index 0000000000..c35d60b463 --- /dev/null +++ b/plugins/Referrers/DataTable/Filter/UrlsFromWebsiteId.php @@ -0,0 +1,46 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\Referrers\DataTable\Filter; + +use Piwik\DataTable\BaseFilter; +use Piwik\DataTable\Row; +use Piwik\DataTable; + +class UrlsFromWebsiteId extends BaseFilter +{ + /** + * Constructor. + * + * @param DataTable $table The table to eventually filter. + */ + public function __construct($table) + { + parent::__construct($table); + } + + /** + * @param DataTable $table + */ + public function filter($table) + { + // the htmlspecialchars_decode call is for BC for before 1.1 + // as the Referrer URL was previously encoded in the log tables, but is now recorded raw + $table->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', function ($label) { + return htmlspecialchars_decode($label); + })); + $table->queueFilter('ColumnCallbackReplace', array('label', 'Piwik\Plugins\Referrers\getPathFromUrl')); + + foreach ($table->getRows() as $row) { + $subtable = $row->getSubtable(); + if ($subtable) { + $this->filter($subtable); + } + } + } +}
\ No newline at end of file diff --git a/plugins/Referrers/Reports/GetWebsites.php b/plugins/Referrers/Reports/GetWebsites.php index e394eb43d9..18f9336a63 100644 --- a/plugins/Referrers/Reports/GetWebsites.php +++ b/plugins/Referrers/Reports/GetWebsites.php @@ -21,6 +21,7 @@ class GetWebsites extends Base $this->dimension = new Website(); $this->name = Piwik::translate('CorePluginsAdmin_Websites'); $this->documentation = Piwik::translate('Referrers_WebsitesReportDocumentation', '<br />'); + $this->recursiveLabelSeparator = '/'; $this->actionToLoadSubTables = 'getUrlsFromWebsiteId'; $this->hasGoalMetrics = true; $this->order = 5; diff --git a/plugins/SecurityInfo b/plugins/SecurityInfo -Subproject a7e2d59ac96e39e1141808fd944e863dc9588e8 +Subproject f037ffc0d6402d084ff1b6525b4c010f56e4276 diff --git a/plugins/SitesManager/Controller.php b/plugins/SitesManager/Controller.php index e7299ab03e..bf810bf5bf 100644 --- a/plugins/SitesManager/Controller.php +++ b/plugins/SitesManager/Controller.php @@ -15,6 +15,7 @@ use Piwik\Piwik; use Piwik\SettingsPiwik; use Piwik\Site; use Piwik\Tracker\TrackerCodeGenerator; +use Piwik\Url; use Piwik\View; /** @@ -118,4 +119,20 @@ class Controller extends \Piwik\Plugin\ControllerAdmin header('Content-Disposition: attachment; filename="' . $filename . '"'); return file_get_contents($path . $filename); } + + public function siteWithoutData() + { + $javascriptGenerator = new TrackerCodeGenerator(); + $piwikUrl = Url::getCurrentUrlWithoutFileName(); + + return $this->renderTemplate('siteWithoutData', array( + 'siteName' => $this->site->getName(), + 'trackingHelp' => $this->renderTemplate('_displayJavascriptCode', array( + 'displaySiteName' => $this->site->getName(), + 'jsTag' => $javascriptGenerator->generate($this->idSite, $piwikUrl), + 'idSite' => $this->idSite, + 'piwikUrl' => $piwikUrl, + )), + )); + } } diff --git a/plugins/SitesManager/SitesManager.php b/plugins/SitesManager/SitesManager.php index ca47a50540..6427dec875 100644 --- a/plugins/SitesManager/SitesManager.php +++ b/plugins/SitesManager/SitesManager.php @@ -9,6 +9,7 @@ namespace Piwik\Plugins\SitesManager; use Piwik\Archive\ArchiveInvalidator; use Piwik\Tracker\Cache; +use Piwik\Tracker\Model as TrackerModel; /** * @@ -29,10 +30,29 @@ class SitesManager extends \Piwik\Plugin 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', 'Tracker.Cache.getSiteAttributes' => 'recordWebsiteDataInCache', 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', - 'SitesManager.deleteSite.end' => 'onSiteDeleted' + 'SitesManager.deleteSite.end' => 'onSiteDeleted', + 'Request.dispatch' => 'redirectDashboardToWelcomePage', ); } + public function redirectDashboardToWelcomePage(&$module, &$action) + { + if ($module !== 'CoreHome' || $action !== 'index') { + return; + } + + $siteId = Common::getRequestVar('idSite', false, 'int'); + if (!$siteId) { + return; + } + + $trackerModel = new TrackerModel(); + if ($trackerModel->isSiteEmpty($siteId)) { + $module = 'SitesManager'; + $action = 'siteWithoutData'; + } + } + public function onSiteDeleted($idSite) { // we do not delete logs here on purpose (you can run these queries on the log_ tables to delete all data) diff --git a/plugins/SitesManager/lang/en.json b/plugins/SitesManager/lang/en.json index 072921ef48..7ee253b445 100644 --- a/plugins/SitesManager/lang/en.json +++ b/plugins/SitesManager/lang/en.json @@ -64,6 +64,9 @@ "ShowTrackingTag": "View Tracking code", "Sites": "Websites", "SiteSearchUse": "You can use Piwik to track and report what visitors are searching in your website's internal search engine.", + "SiteWithoutDataTitle": "No data has been recorded yet", + "SiteWithoutDataDescription": "No analytics data has been tracked for this website yet.", + "SiteWithoutDataSetupTracking": "Please set up the %1$sJavaScript tracking code%2$s on your website and refresh the page.", "SuperUserAccessCan": "A user with Super User access can also %s specify global settings%s for new websites.", "Timezone": "Time zone", "TrackingSiteSearch": "Tracking Internal Site Search", diff --git a/plugins/SitesManager/stylesheets/SitesManager.less b/plugins/SitesManager/stylesheets/SitesManager.less index de6c74f990..d847d0e8a7 100644 --- a/plugins/SitesManager/stylesheets/SitesManager.less +++ b/plugins/SitesManager/stylesheets/SitesManager.less @@ -35,4 +35,21 @@ td.editable-site-field:hover { .link_but:hover > span { text-decoration: underline; -}
\ No newline at end of file +} + +.site-without-data { + padding: 15px; + h2 { + border-bottom: 1px solid #ccc; + margin: 25px 0; + padding: 0 0 5px 0; + font-size: 24px; + } + h3 { + margin: 15px 0; + font-size: 18px; + line-height: 24px; + font-weight: normal; + color: black; + } +} diff --git a/plugins/SitesManager/templates/siteWithoutData.twig b/plugins/SitesManager/templates/siteWithoutData.twig new file mode 100644 index 0000000000..20b6326644 --- /dev/null +++ b/plugins/SitesManager/templates/siteWithoutData.twig @@ -0,0 +1,25 @@ +{% extends "dashboard.twig" %} + +{% block notification %}{% endblock %} + +{% block content %} + + {% include "@CoreHome/_siteSelectHeader.twig" %} + + <div class="site-without-data"> + + <h2>{{ 'SitesManager_SiteWithoutDataTitle'|translate }}</h2> + + <p> + {{ 'SitesManager_SiteWithoutDataDescription'|translate }} + {{ 'SitesManager_SiteWithoutDataSetupTracking'|translate('<a href="' ~ linkTo({ + 'module': 'CoreAdminHome', + 'action': 'trackingCodeGenerator', + }) ~ '">', '</a>')|raw }} + </p> + + {{ trackingHelp|raw }} + + </div> + +{% endblock %} diff --git a/plugins/TasksTimetable b/plugins/TasksTimetable -Subproject 4af9daf3388f59159cf0ff9eb60b8bc38461149 +Subproject a0f38fec7cf905d34059ec1b6a207156adf7579 diff --git a/plugins/TestRunner/Aws/config.ini.php b/plugins/TestRunner/Aws/config.ini.php index a6111e5685..65c4498b89 100644 --- a/plugins/TestRunner/Aws/config.ini.php +++ b/plugins/TestRunner/Aws/config.ini.php @@ -12,6 +12,7 @@ charset = "utf8" request_uri = "/" [database_tests] +username = "root" password = "secure" tables_prefix = "" diff --git a/plugins/TestRunner/Commands/TestsRunOnAws.php b/plugins/TestRunner/Commands/TestsRunOnAws.php index 8ff81fa0a5..65bce267e0 100644 --- a/plugins/TestRunner/Commands/TestsRunOnAws.php +++ b/plugins/TestRunner/Commands/TestsRunOnAws.php @@ -35,9 +35,10 @@ class TestsRunOnAws extends ConsoleCommand { $this->setName('tests:run-aws'); $this->addArgument('testsuite', InputArgument::OPTIONAL, 'Allowed values: ' . implode(', ', $this->allowedTestSuites)); + $this->addArgument('arguments', InputArgument::IS_ARRAY, 'Any additional argument will be passed to the test command.'); $this->addOption('launch-only', null, InputOption::VALUE_NONE, 'Only launches an instance and outputs the connection parameters. Useful if you want to connect via SSH.'); $this->addOption('update-only', null, InputOption::VALUE_NONE, 'Launches an instance, outputs the connection parameters and prepares the instance for a test run but does not actually run the tests. It will also checkout the specified version.'); - $this->addOption('one-instance-per-testsuite', null, InputOption::VALUE_NONE, 'Launches an instance, outputs the connection parameters and prepares the instance for a test run but does not actually run the tests. It will also checkout the specified version.'); + $this->addOption('one-instance-per-testsuite', null, InputOption::VALUE_NONE, 'Launches one instance for system tests and one for ui tests.'); $this->addOption('checkout', null, InputOption::VALUE_REQUIRED, 'Git hash, tag or branch to checkout. Defaults to current hash', $this->getCurrentGitHash()); $this->addOption('patch-file', null, InputOption::VALUE_REQUIRED, 'Apply the given patch file after performing a checkout'); $this->setDescription('Run a specific testsuite on AWS'); @@ -57,15 +58,15 @@ If you want to apply a patch on top of the checked out version you can apply the <comment>./console tests:run-aws --patch-file=test.patch ui</comment> This will checkout the same revision as you are currently on and then apply the patch. To generate a diff use for instance the command <comment>git diff > test.patch</comment>. This feature is still beta and there might be problems with pictures and/or binaries etc. + +You can also pass any argument to the command and they will be forwarded to the test command, for example to run a specific UI test: <comment>./console tests:run-aws ui Dashboard</comment>. '); } - /** - * Execute command like: ./console core:clear-caches - */ protected function execute(InputInterface $input, OutputInterface $output) { $testSuite = $this->getTestSuite($input); + $arguments = $input->getArgument('arguments'); $patchFile = $this->getPatchFile($input); $launchOnly = $input->getOption('launch-only'); $updateOnly = $input->getOption('update-only'); @@ -102,7 +103,7 @@ This feature is still beta and there might be problems with pictures and/or bina return 0; } - $testRunner->runTests($host, $testSuite); + $testRunner->runTests($host, $testSuite, $arguments); $message = $this->buildFinishedMessage($testSuite, $host); $output->writeln("\n$message\n"); diff --git a/plugins/TestRunner/Runner/Remote.php b/plugins/TestRunner/Runner/Remote.php index c0a5c5d8e1..d85c4a268a 100644 --- a/plugins/TestRunner/Runner/Remote.php +++ b/plugins/TestRunner/Runner/Remote.php @@ -55,11 +55,11 @@ class Remote } } - public function runTests($host, $testSuite) + public function runTests($host, $testSuite, array $arguments) { $this->prepareTestRun($host); $this->printVersionInfo(); - $this->doRunTests($testSuite); + $this->doRunTests($testSuite, $arguments); } private function prepareTestRun($host) @@ -74,17 +74,19 @@ class Remote $this->ssh->exec('phantomjs --version'); } - private function doRunTests($testSuite) + private function doRunTests($testSuite, array $arguments) { + $arguments = implode(' ', $arguments); + $this->ssh->exec("ps -ef | grep \"php console tests:run\" | grep -v grep | awk '{print $2}' | xargs kill -9"); if ('all' === $testSuite) { - $this->ssh->exec('php console tests:run --options="--colors"'); + $this->ssh->exec('php console tests:run --options="--colors" ' . $arguments); } elseif ('ui' === $testSuite) { - $this->ssh->exec('php console tests:run-ui --persist-fixture-data --assume-artifacts'); + $this->ssh->exec('php console tests:run-ui --persist-fixture-data --assume-artifacts ' . $arguments); } else { - $this->ssh->exec('php console tests:run --options="--colors" --testsuite="unit"'); - $this->ssh->exec('php console tests:run --options="--colors" --testsuite="' . $testSuite . '"'); + $this->ssh->exec('php console tests:run --options="--colors" --testsuite="unit" ' . $arguments); + $this->ssh->exec('php console tests:run --options="--colors" --testsuite="' . $testSuite . '" ' . $arguments); } if ('system' === $testSuite) { diff --git a/plugins/TestRunner/templates/travis.yml.twig b/plugins/TestRunner/templates/travis.yml.twig index 1971674108..b70a9bf345 100644 --- a/plugins/TestRunner/templates/travis.yml.twig +++ b/plugins/TestRunner/templates/travis.yml.twig @@ -14,7 +14,7 @@ language: php {% if phpVersions|default is empty %} php: - 5.6 - - 5.3 + - 5.3.3 # - hhvm {% else %} php: @@ -102,10 +102,6 @@ install: - rm -rf plugins/$PLUGIN_NAME - mv ../$PLUGIN_NAME plugins - # copy .coveralls.yml if none exists - - if [ ! -f ../coveralls.yml ]; - then cp .coveralls.yml ../coveralls.yml || true; - fi {% endif %} # make sure travis test scripts are always latest (so in older releases/branches, the latest scripts will still be used) @@ -125,11 +121,7 @@ before_script: {% endif %} - ./tests/travis/install_mysql_5.6.sh - - if ([ -z "$TEST_SUITE" ] || [ -n "$PLUGIN_NAME" ]); - then composer require satooshi/php-coveralls dev-master; - else - phpenv config-rm xdebug.ini; - fi + - phpenv config-rm xdebug.ini; # add always_populate_raw_post_data=-1 to php.ini - echo "always_populate_raw_post_data=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini @@ -191,10 +183,6 @@ after_script: # change directory back to root travis dir - cd $PIWIK_ROOT_DIR - - if ([ -z "$TEST_SUITE" ] || [ -n "$PLUGIN_NAME" ]); - then php vendor/bin/coveralls -v; - fi - # output contents of files w/ debugging info to screen - cat /var/log/nginx/error.log - cat $PIWIK_ROOT_DIR/tmp/php-fpm.log diff --git a/plugins/TreemapVisualization b/plugins/TreemapVisualization -Subproject 5dbb90f027b6d214aebfa4f222975d32e99d783 +Subproject 217f8298d5db252ceadd023ca409de1c66a051d diff --git a/plugins/UserCountry/Columns/Country.php b/plugins/UserCountry/Columns/Country.php index db8026ba6b..458e92b068 100644 --- a/plugins/UserCountry/Columns/Country.php +++ b/plugins/UserCountry/Columns/Country.php @@ -12,7 +12,7 @@ use Piwik\Common; use Piwik\Config; use Piwik\Container\StaticContainer; use Piwik\Intl\Data\Provider\RegionDataProvider; -use Piwik\IP; +use Piwik\Network\IP; use Piwik\Piwik; use Piwik\Plugin\Manager; use Piwik\Plugins\Provider\Provider as ProviderProvider; @@ -107,12 +107,17 @@ class Country extends Base /** * Returns the hostname given the IP address string * - * @param string $ip IP Address + * @param string $ipStr IP Address * @return string hostname (or human-readable IP address) */ - private function getHost($ip) + private function getHost($ipStr) { - return trim(strtolower(@IP::getHostByAddr($ip))); + $ip = IP::fromStringIP($ipStr); + + $host = $ip->getHostname(); + $host = ($host === null ? $ipStr : $host); + + return trim(strtolower($host)); } /** diff --git a/plugins/VisitorGenerator b/plugins/VisitorGenerator -Subproject 6a9cf2957920aa19b84ab10b348edbd80c58835 +Subproject 34d796330e256e947ff976f57e350c775dc061d |