diff options
author | Matthieu Napoli <matthieu@mnapoli.fr> | 2014-11-27 07:22:02 +0300 |
---|---|---|
committer | Matthieu Napoli <matthieu@mnapoli.fr> | 2014-11-27 07:22:02 +0300 |
commit | e00732e1ea29900860671a5b022d4450377776f2 (patch) | |
tree | 788c22f279ed6e337e36b5a0f99ff40e3f6b20d5 /core | |
parent | 56752448068fb162602502dd4041ffa7d1b60d3c (diff) | |
parent | abf6f8857e3c944446ff4137f1f5be78b8b0bbdc (diff) |
Merge branch 'master' into tmp-path
Conflicts:
plugins/Installation/SystemCheck.php
Diffstat (limited to 'core')
41 files changed, 1574 insertions, 810 deletions
diff --git a/core/API/DataTableGenericFilter.php b/core/API/DataTableGenericFilter.php index 681c1a12f8..46274b0c31 100644 --- a/core/API/DataTableGenericFilter.php +++ b/core/API/DataTableGenericFilter.php @@ -12,6 +12,8 @@ use Exception; use Piwik\Common; use Piwik\DataTable\Filter\AddColumnsProcessedMetricsGoal; use Piwik\DataTable; +use Piwik\Plugin\ProcessedMetric; +use Piwik\Plugin\Report; class DataTableGenericFilter { @@ -82,15 +84,6 @@ class DataTableGenericFilter 'filter_excludelowpop' => array('string'), 'filter_excludelowpop_value' => array('float', '0'), )), - array('AddColumnsProcessedMetrics', - array( - 'filter_add_columns_when_show_all_columns' => array('integer') - )), - array('AddColumnsProcessedMetricsGoal', - array( - 'filter_update_columns_when_show_all_goals' => array('integer'), - 'idGoal' => array('string', AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW), - )), array('Sort', array( 'filter_sort_column' => array('string'), @@ -105,7 +98,7 @@ class DataTableGenericFilter 'filter_offset' => array('integer', '0'), 'filter_limit' => array('integer'), 'keep_summary_row' => array('integer', '0'), - )), + )) ); } @@ -164,6 +157,45 @@ class DataTableGenericFilter $filterApplied = true; } } + return $filterApplied; } -} + + public function areProcessedMetricsNeededFor($metrics) + { + $columnQueryParameters = array( + 'filter_column', + 'filter_column_recursive', + 'filter_excludelowpop', + 'filter_sort_column' + ); + + foreach ($columnQueryParameters as $queryParamName) { + $queryParamValue = Common::getRequestVar($queryParamName, false, $type = null, $this->request); + if (!empty($queryParamValue) + && $this->containsProcessedMetric($metrics, $queryParamValue) + ) { + return true; + } + } + + return false; + } + + /** + * @param ProcessedMetric[] $metrics + * @param string $name + * @return bool + */ + private function containsProcessedMetric($metrics, $name) + { + foreach ($metrics as $metric) { + if ($metric instanceof ProcessedMetric + && $metric->getName() == $name + ) { + return true; + } + } + return false; + } +}
\ No newline at end of file diff --git a/core/API/DataTableManipulator.php b/core/API/DataTableManipulator.php index 6d744e50df..a1acc6e5b5 100644 --- a/core/API/DataTableManipulator.php +++ b/core/API/DataTableManipulator.php @@ -172,6 +172,8 @@ abstract class DataTableManipulator $request = $this->manipulateSubtableRequest($request); $request['serialize'] = 0; $request['expanded'] = 0; + $request['format'] = 'original'; + $request['format_metrics'] = 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 @@ -181,14 +183,7 @@ abstract class DataTableManipulator $dataTable = Proxy::getInstance()->call($class, $method, $request); $response = new ResponseBuilder($format = 'original', $request); $response->disableSendHeader(); - $dataTable = $response->getResponse($dataTable); - - if (Common::getRequestVar('disable_queued_filters', 0, 'int', $request) == 0) { - if (method_exists($dataTable, 'applyQueuedFilters')) { - $dataTable->applyQueuedFilters(); - } - } - + $dataTable = $response->getResponse($dataTable, $apiModule, $method); return $dataTable; } } diff --git a/core/API/DataTableManipulator/ReportTotalsCalculator.php b/core/API/DataTableManipulator/ReportTotalsCalculator.php index 1cbfeeb362..b6b82effad 100644 --- a/core/API/DataTableManipulator/ReportTotalsCalculator.php +++ b/core/API/DataTableManipulator/ReportTotalsCalculator.php @@ -13,7 +13,7 @@ use Piwik\DataTable; use Piwik\DataTable\Row; use Piwik\Metrics; use Piwik\Period; -use Piwik\Plugins\API\API; +use Piwik\Plugin\Report; /** * This class is responsible for setting the metadata property 'totals' on each dataTable if the report @@ -23,12 +23,6 @@ use Piwik\Plugins\API\API; class ReportTotalsCalculator extends DataTableManipulator { /** - * Cached report metadata array. - * @var array - */ - private static $reportMetadata = array(); - - /** * @param DataTable $table * @return \Piwik\DataTable|\Piwik\DataTable\Map */ @@ -61,7 +55,7 @@ class ReportTotalsCalculator extends DataTableManipulator { $report = $this->findCurrentReport(); - if (!empty($report) && empty($report['dimension'])) { + if (!empty($report) && !$report->getDimension() && !$this->isReportAllMetricsReport($report)) { // we currently do not calculate the total value for reports having no dimension return $dataTable; } @@ -73,12 +67,13 @@ class ReportTotalsCalculator extends DataTableManipulator $metricsToCalculate = Metrics::getMetricIdsToProcessReportTotal(); foreach ($metricsToCalculate as $metricId) { - if (!$this->hasDataTableMetric($firstLevelTable, $metricId)) { + $realMetricName = $this->hasDataTableMetric($firstLevelTable, $metricId); + if (empty($realMetricName)) { continue; } foreach ($firstLevelTable->getRows() as $row) { - $totalValues = $this->sumColumnValueToTotal($row, $metricId, $totalValues); + $totalValues = $this->sumColumnValueToTotal($row, $metricId, $realMetricName, $totalValues); } } @@ -95,34 +90,20 @@ class ReportTotalsCalculator extends DataTableManipulator return false; } - if (false === $this->getColumn($firstRow, $metricId)) { - return false; - } - - return true; - } - - /** - * Returns column from a given row. - * Will work with 2 types of datatable - * - raw datatables coming from the archive DB, which columns are int indexed - * - datatables processed resulting of API calls, which columns have human readable english names - * - * @param Row|array $row - * @param int $columnIdRaw see consts in Metrics:: - * @return mixed Value of column, false if not found - */ - private function getColumn($row, $columnIdRaw) - { - $columnIdReadable = Metrics::getReadableColumnName($columnIdRaw); + $readableColumnName = Metrics::getReadableColumnName($metricId); + $columnAlternatives = array( + $metricId, + $readableColumnName, + // TODO: this and below is a hack to get report totals to work correctly w/ MultiSites.getAll. can be corrected + // when all metrics are described by Metadata classes & internal naming quirks are handled by core system. + 'Goal_' . $readableColumnName, + 'Actions_' . $readableColumnName + ); - if ($row instanceof Row) { - $raw = $row->getColumn($columnIdRaw); - if ($raw !== false) { - return $raw; + foreach ($columnAlternatives as $column) { + if ($firstRow->getColumn($column) !== false) { + return $column; } - - return $row->getColumn($columnIdReadable); } return false; @@ -141,8 +122,8 @@ class ReportTotalsCalculator extends DataTableManipulator $module = $this->apiModule; $action = $this->apiMethod; } else { - $module = $firstLevelReport['module']; - $action = $firstLevelReport['action']; + $module = $firstLevelReport->getModule(); + $action = $firstLevelReport->getAction(); } $request = $this->request; @@ -170,9 +151,9 @@ class ReportTotalsCalculator extends DataTableManipulator return $table; } - private function sumColumnValueToTotal(Row $row, $metricId, $totalValues) + private function sumColumnValueToTotal(Row $row, $metricId, $realMetricId, $totalValues) { - $value = $this->getColumn($row, $metricId); + $value = $row->getColumn($realMetricId); if (false === $value) { @@ -217,38 +198,26 @@ class ReportTotalsCalculator extends DataTableManipulator return $request; } - private function getReportMetadata() - { - if (!empty(static::$reportMetadata)) { - return static::$reportMetadata; - } - - static::$reportMetadata = API::getInstance()->getReportMetadata(); - - return static::$reportMetadata; - } - private function findCurrentReport() { - foreach ($this->getReportMetadata() as $report) { - if ($this->apiMethod == $report['action'] - && $this->apiModule == $report['module']) { - - return $report; - } - } + return Report::factory($this->apiModule, $this->apiMethod); } private function findFirstLevelReport() { - foreach ($this->getReportMetadata() as $report) { - if (!empty($report['actionToLoadSubTables']) - && $this->apiMethod == $report['actionToLoadSubTables'] - && $this->apiModule == $report['module'] + foreach (Report::getAllReports() as $report) { + $actionToLoadSubtables = $report->getActionToLoadSubTables(); + if ($actionToLoadSubtables == $this->apiMethod + && $this->apiModule == $report->getModule() ) { - return $report; } } + return null; + } + + private function isReportAllMetricsReport(Report $report) + { + return $report->getModule() == 'API' && $report->getAction() == 'get'; } } diff --git a/core/API/DataTablePostProcessor.php b/core/API/DataTablePostProcessor.php new file mode 100644 index 0000000000..72facbf41c --- /dev/null +++ b/core/API/DataTablePostProcessor.php @@ -0,0 +1,391 @@ +<?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\API; + +use Exception; +use Piwik\API\DataTableManipulator\Flattener; +use Piwik\API\DataTableManipulator\LabelFilter; +use Piwik\API\DataTableManipulator\ReportTotalsCalculator; +use Piwik\Common; +use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; +use Piwik\DataTable\Filter\PivotByDimension; +use Piwik\Metrics\Formatter; +use Piwik\Plugin\ProcessedMetric; +use Piwik\Plugin\Report; + +/** + * Processes DataTables that should be served through Piwik's APIs. This processing handles + * special query parameters and computes processed metrics. It does not included rendering to + * output formats (eg, 'xml'). + */ +class DataTablePostProcessor +{ + const PROCESSED_METRICS_COMPUTED_FLAG = 'processed_metrics_computed'; + + /** + * @var null|Report + */ + private $report; + + /** + * @var string[] + */ + private $request; + + /** + * @var string + */ + private $apiModule; + + /** + * @var string + */ + private $apiMethod; + + /** + * @var Inconsistencies + */ + private $apiInconsistencies; + + /** + * @var Formatter + */ + private $formatter; + + /** + * Constructor. + */ + public function __construct($apiModule, $apiMethod, $request) + { + $this->apiModule = $apiModule; + $this->apiMethod = $apiMethod; + $this->request = $request; + + $this->report = Report::factory($apiModule, $apiMethod); + $this->apiInconsistencies = new Inconsistencies(); + $this->formatter = new Formatter(); + } + + /** + * Apply post-processing logic to a DataTable of a report for an API request. + * + * @param DataTableInterface $dataTable The data table to process. + * @return DataTableInterface A new data table. + */ + public function process(DataTableInterface $dataTable) + { + // TODO: when calculating metrics before hand, only calculate for needed metrics, not all. NOTE: + // this is non-trivial since it will require, eg, to make sure processed metrics aren't added + // after pivotBy is handled. + $dataTable = $this->applyPivotByFilter($dataTable); + $dataTable = $this->applyFlattener($dataTable); + $dataTable = $this->applyTotalsCalculator($dataTable); + + $dataTable = $this->applyGenericFilters($dataTable); + + $this->applyComputeProcessedMetrics($dataTable); + + // we automatically safe decode all datatable labels (against xss) + $dataTable->queueFilter('SafeDecodeLabel'); + + $dataTable = $this->applyQueuedFilters($dataTable); + $dataTable = $this->applyRequestedColumnDeletion($dataTable); + $dataTable = $this->applyLabelFilter($dataTable); + + $dataTable = $this->applyMetricsFormatting($dataTable, null); + + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTableInterface + */ + public function applyPivotByFilter(DataTableInterface $dataTable) + { + $pivotBy = Common::getRequestVar('pivotBy', false, 'string', $this->request); + if (!empty($pivotBy)) { + $this->applyComputeProcessedMetrics($dataTable); + + $reportId = $this->apiModule . '.' . $this->apiMethod; + $pivotByColumn = Common::getRequestVar('pivotByColumn', false, 'string', $this->request); + $pivotByColumnLimit = Common::getRequestVar('pivotByColumnLimit', false, 'int', $this->request); + + $dataTable->filter('PivotByDimension', array($reportId, $pivotBy, $pivotByColumn, $pivotByColumnLimit, + PivotByDimension::isSegmentFetchingEnabledInConfig())); + } + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTable|DataTableInterface|DataTable\Map + */ + public function applyFlattener($dataTable) + { + if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') { + $flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request); + if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') { + $flattener->includeAggregateRows(); + } + $dataTable = $flattener->flatten($dataTable); + } + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTableInterface + */ + public function applyTotalsCalculator($dataTable) + { + if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) { + $reportTotalsCalculator = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request); + $dataTable = $reportTotalsCalculator->calculate($dataTable); + } + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTableInterface + */ + public function applyGenericFilters($dataTable) + { + // if the flag disable_generic_filters is defined we skip the generic filters + if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) { + $this->applyProcessedMetricsGenericFilters($dataTable); + + $genericFilter = new DataTableGenericFilter($this->request); + + $self = $this; + $report = $this->report; + $dataTable->filter(function (DataTable $table) use ($genericFilter, $report, $self) { + $processedMetrics = Report::getProcessedMetricsForTable($table, $report); + if ($genericFilter->areProcessedMetricsNeededFor($processedMetrics)) { + $self->computeProcessedMetrics($table); + } + }); + + $label = self::getLabelFromRequest($this->request); + if (!empty($label)) { + $genericFilter->disableFilters(array('Limit', 'Truncate')); + } + + $genericFilter->filter($dataTable); + } + + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTableInterface + */ + public function applyProcessedMetricsGenericFilters($dataTable) + { + $addNormalProcessedMetrics = null; + try { + $addNormalProcessedMetrics = Common::getRequestVar( + 'filter_add_columns_when_show_all_columns', null, 'integer', $this->request); + } catch (Exception $ex) { + // ignore + } + + if ($addNormalProcessedMetrics !== null) { + $dataTable->filter('AddColumnsProcessedMetrics', array($addNormalProcessedMetrics)); + } + + $addGoalProcessedMetrics = null; + try { + $addGoalProcessedMetrics = Common::getRequestVar( + 'filter_update_columns_when_show_all_goals', null, 'integer', $this->request); + } catch (Exception $ex) { + // ignore + } + + if ($addGoalProcessedMetrics !== null) { + $idGoal = Common::getRequestVar( + 'idGoal', DataTable\Filter\AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW, 'string', $this->request); + + $dataTable->filter('AddColumnsProcessedMetricsGoal', array($ignore = true, $idGoal)); + } + + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTableInterface + */ + public function applyQueuedFilters($dataTable) + { + // if the flag disable_queued_filters is defined we skip the filters that were queued + if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) { + $dataTable->applyQueuedFilters(); + } + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTableInterface + */ + public function applyRequestedColumnDeletion($dataTable) + { + // use the ColumnDelete filter if hideColumns/showColumns is provided (must be done + // after queued filters are run so processed metrics can be removed, too) + $hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request); + $showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request); + if (!empty($hideColumns) + || !empty($showColumns) + ) { + $dataTable->filter('ColumnDelete', array($hideColumns, $showColumns)); + } else { + $this->removeTemporaryMetrics($dataTable); + } + + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + */ + public function removeTemporaryMetrics(DataTableInterface $dataTable) + { + $allColumns = !empty($this->report) ? $this->report->getAllMetrics() : array(); + + $report = $this->report; + $dataTable->filter(function (DataTable $table) use ($report, $allColumns) { + $processedMetrics = Report::getProcessedMetricsForTable($table, $report); + + $allTemporaryMetrics = array(); + foreach ($processedMetrics as $metric) { + $allTemporaryMetrics = array_merge($allTemporaryMetrics, $metric->getTemporaryMetrics()); + } + + if (!empty($allTemporaryMetrics)) { + $table->filter('ColumnDelete', array($allTemporaryMetrics)); + } + }); + } + + /** + * @param DataTableInterface $dataTable + * @return DataTableInterface + */ + public function applyLabelFilter($dataTable) + { + $label = self::getLabelFromRequest($this->request); + + // apply label filter: only return rows matching the label parameter (more than one if more than one label) + if (!empty($label)) { + $addLabelIndex = Common::getRequestVar('labelFilterAddLabelIndex', 0, 'int', $this->request) == 1; + + $filter = new LabelFilter($this->apiModule, $this->apiMethod, $this->request); + $dataTable = $filter->filter($label, $dataTable, $addLabelIndex); + } + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTableInterface + */ + public function applyMetricsFormatting($dataTable) + { + $formatMetrics = Common::getRequestVar('format_metrics', 0, 'string', $this->request); + if ($formatMetrics == '0') { + return $dataTable; + } + + // in Piwik 2.X & below, metrics are not formatted in API responses except for percents. + // this code implements this inconsistency + $onlyFormatPercents = $formatMetrics === 'bc'; + + $metricsToFormat = null; + if ($onlyFormatPercents) { + $metricsToFormat = $this->apiInconsistencies->getPercentMetricsToFormat(); + } + + $dataTable->filter(array($this->formatter, 'formatMetrics'), array($this->report, $metricsToFormat)); + return $dataTable; + } + + /** + * Returns the value for the label query parameter which can be either a string + * (ie, label=...) or array (ie, label[]=...). + * + * @param array $request + * @return array + */ + public static function getLabelFromRequest($request) + { + $label = Common::getRequestVar('label', array(), 'array', $request); + if (empty($label)) { + $label = Common::getRequestVar('label', '', 'string', $request); + if (!empty($label)) { + $label = array($label); + } + } + + $label = self::unsanitizeLabelParameter($label); + return $label; + } + + public static function unsanitizeLabelParameter($label) + { + // this is needed because Proxy uses Common::getRequestVar which in turn + // uses Common::sanitizeInputValue. This causes the > that separates recursive labels + // to become > and we need to undo that here. + $label = Common::unsanitizeInputValues($label); + return $label; + } + + public function computeProcessedMetrics(DataTable $dataTable) + { + if ($dataTable->getMetadata(self::PROCESSED_METRICS_COMPUTED_FLAG)) { + return; + } + + /** @var ProcessedMetric[] $processedMetrics */ + $processedMetrics = Report::getProcessedMetricsForTable($dataTable, $this->report); + if (empty($processedMetrics)) { + return; + } + + $dataTable->setMetadata(self::PROCESSED_METRICS_COMPUTED_FLAG, true); + + foreach ($processedMetrics as $name => $processedMetric) { + if (!$processedMetric->beforeCompute($this->report, $dataTable)) { + continue; + } + + foreach ($dataTable->getRows() as $row) { + if ($row->getColumn($name) === false) { // only compute the metric if it has not been computed already + $computedValue = $processedMetric->compute($row); + if ($computedValue !== false) { + $row->addColumn($name, $computedValue); + } + + $subtable = $row->getSubtable(); + if (!empty($subtable)) { + $this->computeProcessedMetrics($subtable); + } + } + } + } + } + + public function applyComputeProcessedMetrics(DataTableInterface $dataTable) + { + $dataTable->filter(array($this, 'computeProcessedMetrics')); + } +}
\ No newline at end of file diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php index 6a140ac8fb..3c88f62027 100644 --- a/core/API/DocumentationGenerator.php +++ b/core/API/DocumentationGenerator.php @@ -132,13 +132,13 @@ class DocumentationGenerator $lastNUrls = ''; if (preg_match('/(&period)|(&date)/', $exampleUrl)) { $exampleUrlRss = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet); - $lastNUrls = ", RSS of the last <a target=_blank href='$exampleUrlRss&format=rss$token_auth&translateColumnNames=1'>10 days</a>"; + $lastNUrls = ", RSS of the last <a target='_blank' href='$exampleUrlRss&format=rss$token_auth&translateColumnNames=1'>10 days</a>"; } $exampleUrl = $prefixUrls . $exampleUrl; $str .= " [ Example in - <a target=_blank href='$exampleUrl&format=xml$token_auth'>XML</a>, - <a target=_blank href='$exampleUrl&format=JSON$token_auth'>Json</a>, - <a target=_blank href='$exampleUrl&format=Tsv$token_auth&translateColumnNames=1'>Tsv (Excel)</a> + <a target='_blank' href='$exampleUrl&format=xml$token_auth'>XML</a>, + <a target='_blank' href='$exampleUrl&format=JSON$token_auth'>Json</a>, + <a target='_blank' href='$exampleUrl&format=Tsv$token_auth&translateColumnNames=1'>Tsv (Excel)</a> $lastNUrls ]"; } else { @@ -350,13 +350,13 @@ class DocumentationGenerator $lastNUrls = ''; if (preg_match('/(&period)|(&date)/', $exampleUrl)) { $exampleUrlRss = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet); - $lastNUrls = ", RSS of the last <a target=_blank href='$exampleUrlRss&format=rss$token_auth&translateColumnNames=1'>10 days</a>"; + $lastNUrls = ", RSS of the last <a target='_blank' href='$exampleUrlRss&format=rss$token_auth&translateColumnNames=1'>10 days</a>"; } $exampleUrl = $prefixUrls . $exampleUrl; $str .= " [ Example in - <a target=_blank href='$exampleUrl&format=xml$token_auth'>XML</a>, - <a target=_blank href='$exampleUrl&format=JSON$token_auth'>Json</a>, - <a target=_blank href='$exampleUrl&format=Tsv$token_auth&translateColumnNames=1'>Tsv (Excel)</a> + <a target='_blank' href='$exampleUrl&format=xml$token_auth'>XML</a>, + <a target='_blank' href='$exampleUrl&format=JSON$token_auth'>Json</a>, + <a target='_blank' href='$exampleUrl&format=Tsv$token_auth&translateColumnNames=1'>Tsv (Excel)</a> $lastNUrls ]"; } else { diff --git a/core/API/Inconsistencies.php b/core/API/Inconsistencies.php new file mode 100644 index 0000000000..5d0c20d418 --- /dev/null +++ b/core/API/Inconsistencies.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\API; + +/** + * Contains logic to replicate inconsistencies in Piwik's API. This class exists + * to provide a way to clean up existing Piwik code and behavior without breaking + * backwards compatibility immediately. + * + * Code that handles the case when the 'format_metrics' query parameter value is + * 'bc' should be removed as well. This code is in API\Request and DataTablePostProcessor. + * + * Should be removed before releasing Piwik 3.0. + */ +class Inconsistencies +{ + /** + * In Piwik 2.X and below, the "raw" API would format percent values but no others. + * This method returns the list of percent metrics that were returned from the API + * formatted so we can maintain BC. + * + * Used by DataTablePostProcessor. + */ + public function getPercentMetricsToFormat() + { + return array( + 'bounce_rate', + 'conversion_rate', + 'interaction_rate', + 'exit_rate', + 'bounce_rate_returning', + 'nb_visits_percentage', + '/.*_evolution/', + '/goal_.*_conversion_rate/' + ); + } +}
\ No newline at end of file diff --git a/core/API/Request.php b/core/API/Request.php index 64e8e054fa..979f1299d0 100644 --- a/core/API/Request.php +++ b/core/API/Request.php @@ -79,15 +79,23 @@ class Request * * @param string|array $request The base request string or array, eg, * `'module=UserSettings&action=getWidescreen'`. + * @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded + * from this. Defaults to `$_GET + $_POST`. * @return array */ - public static function getRequestArrayFromString($request) + public static function getRequestArrayFromString($request, $defaultRequest = null) { - $defaultRequest = $_GET + $_POST; + if ($defaultRequest === null) { + $defaultRequest = $_GET + $_POST; - $requestRaw = self::getRequestParametersGET(); - if (!empty($requestRaw['segment'])) { - $defaultRequest['segment'] = $requestRaw['segment']; + $requestRaw = self::getRequestParametersGET(); + if (!empty($requestRaw['segment'])) { + $defaultRequest['segment'] = $requestRaw['segment']; + } + + if (empty($defaultRequest['format_metrics'])) { + $defaultRequest['format_metrics'] = 'bc'; + } } $requestArray = $defaultRequest; @@ -120,10 +128,12 @@ class Request * eg, `'method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml'` * If a request is not provided, then we use the values in the `$_GET` and `$_POST` * superglobals. + * @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded + * from this. Defaults to `$_GET + $_POST`. */ - public function __construct($request = null) + public function __construct($request = null, $defaultRequest = null) { - $this->request = self::getRequestArrayFromString($request); + $this->request = self::getRequestArrayFromString($request, $defaultRequest); $this->sanitizeRequest(); } @@ -289,9 +299,13 @@ class Request * @param string $method The API method to call, ie, `'Actions.getPageTitles'`. * @param array $paramOverride The parameter name-value pairs to use instead of what's * in `$_GET` & `$_POST`. + * @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded + * from this. Defaults to `$_GET + $_POST`. + * + * To avoid using any parameters from $_GET or $_POST, set this to an empty `array()`. * @return mixed The result of the API request. See {@link process()}. */ - public static function processRequest($method, $paramOverride = array()) + public static function processRequest($method, $paramOverride = array(), $defaultRequest = null) { $params = array(); $params['format'] = 'original'; @@ -300,7 +314,7 @@ class Request $params = $paramOverride + $params; // process request - $request = new Request($params); + $request = new Request($params, $defaultRequest); return $request->process(); } diff --git a/core/API/ResponseBuilder.php b/core/API/ResponseBuilder.php index 5b00c01a13..4ebe50305d 100644 --- a/core/API/ResponseBuilder.php +++ b/core/API/ResponseBuilder.php @@ -9,15 +9,13 @@ namespace Piwik\API; use Exception; -use Piwik\API\DataTableManipulator\Flattener; -use Piwik\API\DataTableManipulator\LabelFilter; -use Piwik\API\DataTableManipulator\ReportTotalsCalculator; use Piwik\Common; use Piwik\DataTable; -use Piwik\DataTable\Filter\PivotByDimension; use Piwik\DataTable\Renderer; use Piwik\DataTable\DataTableInterface; use Piwik\DataTable\Filter\ColumnDelete; +use Piwik\Plugin\Report; +use Piwik\Plugins\API\Renderer\Original; /** */ @@ -166,66 +164,8 @@ class ResponseBuilder private function handleDataTable(DataTableInterface $datatable) { - $label = $this->getLabelFromRequest($this->request); - - // handle pivot by dimension filter - $pivotBy = Common::getRequestVar('pivotBy', false, 'string', $this->request); - if (!empty($pivotBy)) { - $reportId = $this->apiModule . '.' . $this->apiMethod; - $pivotByColumn = Common::getRequestVar('pivotByColumn', false, 'string', $this->request); - $pivotByColumnLimit = Common::getRequestVar('pivotByColumnLimit', false, 'int', $this->request); - - $datatable->filter('PivotByDimension', array($reportId, $pivotBy, $pivotByColumn, $pivotByColumnLimit, - PivotByDimension::isSegmentFetchingEnabledInConfig())); - } - - // if requested, flatten nested tables - if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') { - $flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request); - if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') { - $flattener->includeAggregateRows(); - } - $datatable = $flattener->flatten($datatable); - } - - if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) { - $genericFilter = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request); - $datatable = $genericFilter->calculate($datatable); - } - - // if the flag disable_generic_filters is defined we skip the generic filters - if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) { - $genericFilter = new DataTableGenericFilter($this->request); - if (!empty($label)) { - $genericFilter->disableFilters(array('Limit', 'Truncate')); - } - - $genericFilter->filter($datatable); - } - - // we automatically safe decode all datatable labels (against xss) - $datatable->queueFilter('SafeDecodeLabel'); - - // if the flag disable_queued_filters is defined we skip the filters that were queued - if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) { - $datatable->applyQueuedFilters(); - } - - // use the ColumnDelete filter if hideColumns/showColumns is provided (must be done - // after queued filters are run so processed metrics can be removed, too) - $hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request); - $showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request); - if ($hideColumns !== '' || $showColumns !== '') { - $datatable->filter('ColumnDelete', array($hideColumns, $showColumns)); - } - - // apply label filter: only return rows matching the label parameter (more than one if more than one label) - if (!empty($label)) { - $addLabelIndex = Common::getRequestVar('labelFilterAddLabelIndex', 0, 'int', $this->request) == 1; - - $filter = new LabelFilter($this->apiModule, $this->apiMethod, $this->request); - $datatable = $filter->filter($label, $datatable, $addLabelIndex); - } + $postProcessor = new DataTablePostProcessor($this->apiModule, $this->apiMethod, $this->request); + $datatable = $postProcessor->process($datatable); return $this->apiRenderer->renderDataTable($datatable); } @@ -260,36 +200,6 @@ class ResponseBuilder return $this->apiRenderer->renderArray($array); } - /** - * Returns the value for the label query parameter which can be either a string - * (ie, label=...) or array (ie, label[]=...). - * - * @param array $request - * @return array - */ - public static function getLabelFromRequest($request) - { - $label = Common::getRequestVar('label', array(), 'array', $request); - if (empty($label)) { - $label = Common::getRequestVar('label', '', 'string', $request); - if (!empty($label)) { - $label = array($label); - } - } - - $label = self::unsanitizeLabelParameter($label); - return $label; - } - - public static function unsanitizeLabelParameter($label) - { - // this is needed because Proxy uses Common::getRequestVar which in turn - // uses Common::sanitizeInputValue. This causes the > that separates recursive labels - // to become > and we need to undo that here. - $label = Common::unsanitizeInputValues($label); - return $label; - } - private function sendHeaderIfEnabled() { if ($this->sendHeader) { diff --git a/core/Archive.php b/core/Archive.php index 7101850c1d..b407465a45 100644 --- a/core/Archive.php +++ b/core/Archive.php @@ -744,7 +744,10 @@ class Archive */ private function getArchiveGroupOfPlugin($plugin) { - if ($this->getPeriodLabel() != 'range') { + $periods = $this->params->getPeriods(); + $periodLabel = reset($periods)->getLabel(); + + if (Rules::shouldProcessReportsAllPlugins($this->params->getIdSites(), $this->params->getSegment(), $periodLabel)) { return self::ARCHIVE_ALL_PLUGINS_FLAG; } diff --git a/core/Archive/DataTableFactory.php b/core/Archive/DataTableFactory.php index 71eaa7a8c1..59af8a4e0b 100644 --- a/core/Archive/DataTableFactory.php +++ b/core/Archive/DataTableFactory.php @@ -10,6 +10,7 @@ namespace Piwik\Archive; use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; use Piwik\DataTable\Row; use Piwik\Site; @@ -96,6 +97,23 @@ class DataTableFactory } /** + * Returns the ID of the site a table is related to based on the 'site' metadata entry, + * or null if there is none. + * + * @param DataTable $table + * @return int|null + */ + public static function getSiteIdFromMetadata(DataTable $table) + { + $site = $table->getMetadata('site'); + if (empty($site)) { + return null; + } else { + return $site->getId(); + } + } + + /** * Tells the factory instance to expand the DataTables that are created by * creating subtables and setting the subtable IDs of rows w/ subtables correctly. * @@ -345,10 +363,10 @@ class DataTableFactory * Converts site IDs and period string ranges into Site instances and * Period instances in DataTable metadata. */ - private function transformMetadata($table) + private function transformMetadata(DataTableInterface $table) { $periods = $this->periods; - $table->filter(function ($table) use ($periods) { + $table->filter(function (DataTable $table) use ($periods) { $table->setMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX, new Site($table->getMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX))); $table->setMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX, $periods[$table->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)]); }); @@ -373,7 +391,7 @@ class DataTableFactory * @param $keyMetadata * @param $result */ - private function setTableMetadata($keyMetadata, $result) + private function setTableMetadata($keyMetadata, DataTableInterface $result) { if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX])) { $keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX] = reset($this->sitesId); @@ -385,7 +403,7 @@ class DataTableFactory } // Note: $result can be a DataTable\Map - $result->filter(function ($table) use ($keyMetadata) { + $result->filter(function (DataTable $table) use ($keyMetadata) { foreach ($keyMetadata as $name => $value) { $table->setMetadata($name, $value); } @@ -423,5 +441,4 @@ class DataTableFactory $result = $table; return $result; } -} - +}
\ No newline at end of file diff --git a/core/CronArchive.php b/core/CronArchive.php index 151298fc6a..eb4e45e383 100644 --- a/core/CronArchive.php +++ b/core/CronArchive.php @@ -12,6 +12,7 @@ use Exception; use Piwik\ArchiveProcessor\Rules; use Piwik\CronArchive\FixedSiteIds; use Piwik\CronArchive\SharedSiteIds; +use Piwik\Metrics\Formatter; use Piwik\Period\Factory as PeriodFactory; use Piwik\DataAccess\InvalidatedReports; use Piwik\Plugins\SitesManager\API as APISitesManager; @@ -187,6 +188,8 @@ class CronArchive private $processed = 0; private $archivedPeriodsArchivesWebsite = 0; + private $formatter; + /** * Returns the option name of the option that stores the time core:archive was last executed. * @@ -209,6 +212,8 @@ class CronArchive */ public function __construct($piwikUrl = false) { + $this->formatter = new Formatter(); + $this->initLog(); $this->initPiwikHost($piwikUrl); $this->initCore(); @@ -489,7 +494,7 @@ class CronArchive if ($skipDayArchive) { $this->log("Skipped website id $idSite, already done " - . \Piwik\MetricsFormatter::getPrettyTimeFromSeconds($elapsedSinceLastArchiving, true, $isHtml = false) + . $this->formatter->getPrettyTimeFromSeconds($elapsedSinceLastArchiving, true) . " ago, " . $timerWebsite->__toString()); $this->skippedDayArchivesWebsites++; $this->skipped++; @@ -503,7 +508,7 @@ class CronArchive if (!$shouldArchivePeriods) { $this->log("Skipped website id $idSite periods processing, already done " - . \Piwik\MetricsFormatter::getPrettyTimeFromSeconds($elapsedSinceLastArchiving, true, $isHtml = false) + . $this->formatter->getPrettyTimeFromSeconds($elapsedSinceLastArchiving, true) . " ago, " . $timerWebsite->__toString()); $this->skippedDayArchivesWebsites++; $this->skipped++; @@ -860,6 +865,8 @@ class CronArchive $config->log = $log; + Log::unsetInstance(); + // Make sure we log at least INFO (if logger is set to DEBUG then keep it) $logLevel = Log::getInstance()->getLogLevel(); if ($logLevel < Log::INFO) { @@ -1048,7 +1055,7 @@ class CronArchive { $sitesIdWithVisits = APISitesManager::getInstance()->getSitesIdWithVisits(time() - $this->shouldArchiveOnlySitesWithTrafficSince); $websiteIds = !empty($sitesIdWithVisits) ? ", IDs: " . implode(", ", $sitesIdWithVisits) : ""; - $prettySeconds = \Piwik\MetricsFormatter::getPrettyTimeFromSeconds( $this->shouldArchiveOnlySitesWithTrafficSince, true, false); + $prettySeconds = $this->formatter->getPrettyTimeFromSeconds( $this->shouldArchiveOnlySitesWithTrafficSince, true); $this->log("- Will process " . count($sitesIdWithVisits) . " websites with new visits since " . $prettySeconds . " " @@ -1156,7 +1163,8 @@ class CronArchive // Try and not request older data we know is already archived if ($this->lastSuccessRunTimestamp !== false) { $dateLast = time() - $this->lastSuccessRunTimestamp; - $this->log("- Archiving was last executed without error " . \Piwik\MetricsFormatter::getPrettyTimeFromSeconds($dateLast, true, $isHtml = false) . " ago"); + $this->log("- Archiving was last executed without error " + . $this->formatter->getPrettyTimeFromSeconds($dateLast, true) . " ago"); } } @@ -1213,6 +1221,10 @@ class CronArchive $today = end($stats); + if (empty($today['nb_visits'])) { + return 0; + } + return $today['nb_visits']; } diff --git a/core/DataTable.php b/core/DataTable.php index 4d308f7d21..61e8abb1eb 100644 --- a/core/DataTable.php +++ b/core/DataTable.php @@ -200,6 +200,13 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess const LABEL_SUMMARY_ROW = -1; /** + * Name for metadata that contains extra {@link Piwik\Plugin\ProcessedMetric}s for a DataTable. + * These metrics will be added in addition to the ones specified in the table's associated + * {@link Piwik\Plugin\Report} class. + */ + const EXTRA_PROCESSED_METRICS_METADATA_NAME = 'extra_processed_metrics'; + + /** * Maximum nesting level. */ private static $maximumDepthLevelAllowed = self::MAX_DEPTH_DEFAULT; @@ -1672,4 +1679,4 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess { $this->deleteRow($offset); } -} +}
\ No newline at end of file diff --git a/core/DataTable/Filter/AddColumnsProcessedMetrics.php b/core/DataTable/Filter/AddColumnsProcessedMetrics.php index f3d8191b1f..a3a9bd9776 100644 --- a/core/DataTable/Filter/AddColumnsProcessedMetrics.php +++ b/core/DataTable/Filter/AddColumnsProcessedMetrics.php @@ -11,7 +11,11 @@ namespace Piwik\DataTable\Filter; use Piwik\DataTable\BaseFilter; use Piwik\DataTable\Row; use Piwik\DataTable; -use Piwik\Metrics; +use Piwik\Plugin\Metric; +use Piwik\Plugins\CoreHome\Columns\Metrics\ActionsPerVisit; +use Piwik\Plugins\CoreHome\Columns\Metrics\AverageTimeOnSite; +use Piwik\Plugins\CoreHome\Columns\Metrics\BounceRate; +use Piwik\Plugins\CoreHome\Columns\Metrics\ConversionRate; /** * Adds processed metrics columns to a {@link DataTable} using metrics that already exist. @@ -65,34 +69,21 @@ class AddColumnsProcessedMetrics extends BaseFilter $this->deleteRowsWithNoVisit($table); } - $metrics = new Metrics\Processed(); + $extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME); - foreach ($table->getRows() as $row) { - $this->tryToAddColumn($row, 'conversion_rate', array($metrics, 'getConversionRate')); - $this->tryToAddColumn($row, 'nb_actions_per_visit', array($metrics, 'getActionsPerVisit')); - $this->tryToAddColumn($row, 'avg_time_on_site', array($metrics, 'getAvgTimeOnSite')); - $this->tryToAddColumn($row, 'bounce_rate', array($metrics, 'getBounceRate')); + $extraProcessedMetrics[] = new ConversionRate(); + $extraProcessedMetrics[] = new ActionsPerVisit(); + $extraProcessedMetrics[] = new AverageTimeOnSite(); + $extraProcessedMetrics[] = new BounceRate(); - $this->filterSubTable($row); - } - } - - private function tryToAddColumn(Row $row, $column, $callable) - { - try { - $row->addColumn($column, $callable); - } catch (\Exception $e) { - - } + $table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics); } private function deleteRowsWithNoVisit(DataTable $table) { - $metrics = new Metrics\Processed(); - foreach ($table->getRows() as $key => $row) { - $nbVisits = $metrics->getColumn($row, Metrics::INDEX_NB_VISITS); - $nbActions = $metrics->getColumn($row, Metrics::INDEX_NB_ACTIONS); + $nbVisits = Metric::getMetric($row, 'nb_visits'); + $nbActions = Metric::getMetric($row, 'nb_actions'); if ($nbVisits == 0 && $nbActions == 0 @@ -102,4 +93,4 @@ class AddColumnsProcessedMetrics extends BaseFilter } } } -} +}
\ No newline at end of file diff --git a/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php b/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php index 963ac9acbd..d63d9b9bc5 100644 --- a/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php +++ b/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php @@ -8,10 +8,18 @@ */ namespace Piwik\DataTable\Filter; +use Piwik\Archive\DataTableFactory; use Piwik\DataTable; use Piwik\DataTable\Row; -use Piwik\Metrics; use Piwik\Piwik; +use Piwik\Plugin\Metric; +use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\AverageOrderRevenue; +use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\ConversionRate; +use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\Conversions; +use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\ItemsCount; +use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\Revenue; +use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\RevenuePerVisit as GoalSpecificRevenuePerVisit; +use Piwik\Plugins\Goals\Columns\Metrics\RevenuePerVisit; /** * Adds goal related metrics to a {@link DataTable} using metrics that already exist. @@ -66,8 +74,6 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics */ const GOALS_FULL_TABLE = 0; - private $expectedColumns = array(); - /** * Constructor. * @@ -78,19 +84,14 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics * If self::GOALS_OVERVIEW, only the main goal metrics will be added. * If an int > 0, then will process only metrics for this specific Goal. */ - public function __construct($table, $enable = true, $processOnlyIdGoal) + public function __construct($table, $enable = true, $processOnlyIdGoal, $goalsToProcess = null) { $this->processOnlyIdGoal = $processOnlyIdGoal; $this->isEcommerce = $this->processOnlyIdGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER || $this->processOnlyIdGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART; parent::__construct($table); // Ensure that all rows with no visit but conversions will be displayed $this->deleteRowsWithNoVisit = false; - } - - private function addColumn(Row $row, $columnName, $callback) - { - $this->expectedColumns[$columnName] = true; - $row->addColumn($columnName, $callback); + $this->goalsToProcess = $goalsToProcess; } /** @@ -104,40 +105,27 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics // Add standard processed metrics parent::filter($table); - $this->expectedColumns = array(); - - $metrics = new Metrics\ProcessedGoals(); - - foreach ($table->getRows() as $row) { - $goals = $metrics->getColumn($row, Metrics::INDEX_GOALS); - - if (!$goals) { - continue; - } - - $this->addColumn($row, 'revenue_per_visit', function (Row $row) use ($metrics) { - return $metrics->getRevenuePerVisit($row); - }); + $goals = $this->getGoalsInTable($table); + if (!empty($this->goalsToProcess)) { + $goals = array_unique(array_merge($goals, $this->goalsToProcess)); + sort($goals); + } - if ($this->processOnlyIdGoal == self::GOALS_MINIMAL_REPORT) { - continue; - } + $idSite = DataTableFactory::getSiteIdFromMetadata($table); - foreach ($goals as $goalId => $goalMetrics) { - $goalId = str_replace("idgoal=", "", $goalId); + $extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME); + $extraProcessedMetrics[] = new RevenuePerVisit(); + if ($this->processOnlyIdGoal != self::GOALS_MINIMAL_REPORT) { + foreach ($goals as $idGoal) { if (($this->processOnlyIdGoal > self::GOALS_FULL_TABLE || $this->isEcommerce) - && $this->processOnlyIdGoal != $goalId + && $this->processOnlyIdGoal != $idGoal ) { continue; } - $columnPrefix = 'goal_' . $goalId; - - $this->addColumn($row, $columnPrefix . '_conversion_rate', function (Row $row) use ($metrics, $goalMetrics) { - return $metrics->getConversionRate($row, $goalMetrics); - }); + $extraProcessedMetrics[] = new ConversionRate($idSite, $idGoal); // PerGoal\ConversionRate // When the table is displayed by clicking on the flag icon, we only display the columns // Visits, Conversions, Per goal conversion rate, Revenue @@ -145,51 +133,34 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics continue; } - // Goal Conversions - $this->addColumn($row, $columnPrefix . '_nb_conversions', function () use ($metrics, $goalMetrics) { - return $metrics->getNbConversions($goalMetrics); - }); - - // Goal Revenue per visit - $this->addColumn($row, $columnPrefix . '_revenue_per_visit', function (Row $row) use ($metrics, $goalMetrics) { - return $metrics->getRevenuePerVisitForGoal($row, $goalMetrics); - }); - - // Total revenue - $this->addColumn($row, $columnPrefix . '_revenue', function () use ($metrics, $goalMetrics) { - return $metrics->getRevenue($goalMetrics); - }); + $extraProcessedMetrics[] = new Conversions($idSite, $idGoal); // PerGoal\Conversions or GoalSpecific\ + $extraProcessedMetrics[] = new GoalSpecificRevenuePerVisit($idSite, $idGoal); // PerGoal\Revenue + $extraProcessedMetrics[] = new Revenue($idSite, $idGoal); // PerGoal\Revenue if ($this->isEcommerce) { - - // AOV Average Order Value - $this->addColumn($row, $columnPrefix . '_avg_order_revenue', function () use ($metrics, $goalMetrics) { - return $metrics->getAvgOrderRevenue($goalMetrics); - }); - - // Items qty - $this->addColumn($row, $columnPrefix . '_items', function () use ($metrics, $goalMetrics) { - return $metrics->getItems($goalMetrics); - }); - + $extraProcessedMetrics[] = new AverageOrderRevenue($idSite, $idGoal); + $extraProcessedMetrics[] = new ItemsCount($idSite, $idGoal); } } } - $expectedColumns = array_keys($this->expectedColumns); - $rows = $table->getRows(); - foreach ($rows as $row) { - foreach ($expectedColumns as $name) { - if (!$row->hasColumn($name)) { - if (strpos($name, 'conversion_rate') !== false) { - $row->addColumn($name, function () { - return '0%'; - }); - } else { - $row->addColumn($name, 0); - } - } + $table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics); + } + + private function getGoalsInTable(DataTable $table) + { + $result = array(); + foreach ($table->getRows() as $row) { + $goals = Metric::getMetric($row, 'goals'); + if (!$goals) { + continue; + } + + foreach ($goals as $goalId => $goalMetrics) { + $goalId = str_replace("idgoal=", "", $goalId); + $result[] = $goalId; } } + return array_unique($result); } -} +}
\ No newline at end of file diff --git a/core/DataTable/Filter/CalculateEvolutionFilter.php b/core/DataTable/Filter/CalculateEvolutionFilter.php index 5fa55b329e..a8bd04d08d 100755 --- a/core/DataTable/Filter/CalculateEvolutionFilter.php +++ b/core/DataTable/Filter/CalculateEvolutionFilter.php @@ -27,6 +27,7 @@ use Piwik\Site; * ((currentValue - pastValue) / pastValue) * 100 * * @api + * @deprecated since v2.10.0 */ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage { diff --git a/core/DataTable/Filter/ColumnCallbackReplace.php b/core/DataTable/Filter/ColumnCallbackReplace.php index ca53405d7e..3e167b5593 100644 --- a/core/DataTable/Filter/ColumnCallbackReplace.php +++ b/core/DataTable/Filter/ColumnCallbackReplace.php @@ -29,6 +29,7 @@ use Piwik\DataTable\Row; * // label, url and truncate_length are columns in $dataTable * $dataTable->filter('ColumnCallbackReplace', array('label', 'url'), $truncateString, null, array('truncate_length')); * + * @api */ class ColumnCallbackReplace extends BaseFilter { diff --git a/core/DataTable/Filter/ExcludeLowPopulation.php b/core/DataTable/Filter/ExcludeLowPopulation.php index ee135b59d6..cf54c5c7eb 100644 --- a/core/DataTable/Filter/ExcludeLowPopulation.php +++ b/core/DataTable/Filter/ExcludeLowPopulation.php @@ -10,6 +10,7 @@ namespace Piwik\DataTable\Filter; use Piwik\DataTable; use Piwik\DataTable\BaseFilter; +use Piwik\Plugin\Metric; /** * Deletes all rows for which a specific column has a value that is lower than @@ -59,6 +60,7 @@ class ExcludeLowPopulation extends BaseFilter public function __construct($table, $columnToFilter, $minimumValue, $minimumPercentageThreshold = false) { parent::__construct($table); + $this->columnToFilter = $columnToFilter; if ($minimumValue == 0) { diff --git a/core/DataTable/Filter/PivotByDimension.php b/core/DataTable/Filter/PivotByDimension.php index ce3ac98ec5..61e68423e8 100644 --- a/core/DataTable/Filter/PivotByDimension.php +++ b/core/DataTable/Filter/PivotByDimension.php @@ -161,7 +161,7 @@ class PivotByDimension extends BaseFilter $this->pivotByColumnLimit = $pivotByColumnLimit ?: self::getDefaultColumnLimit(); $this->isFetchingBySegmentEnabled = $isFetchingBySegmentEnabled; - $namesToId = Metrics::getMappingFromIdToName(); + $namesToId = Metrics::getMappingFromNameToId(); $this->metricIndexValue = isset($namesToId[$this->pivotColumn]) ? $namesToId[$this->pivotColumn] : null; $this->setPivotByDimension($pivotByDimension); diff --git a/core/DataTable/Filter/Sort.php b/core/DataTable/Filter/Sort.php index 9df2250288..6ffe33bced 100644 --- a/core/DataTable/Filter/Sort.php +++ b/core/DataTable/Filter/Sort.php @@ -192,7 +192,7 @@ class Sort extends BaseFilter return $this->columnToSort; } - $columnIdToName = Metrics::getMappingFromIdToName(); + $columnIdToName = Metrics::getMappingFromNameToId(); // sorting by "nb_visits" but the index is Metrics::INDEX_NB_VISITS in the table if (isset($columnIdToName[$this->columnToSort])) { $column = $columnIdToName[$this->columnToSort]; diff --git a/core/Error.php b/core/Error.php index c56e3301a7..d8fa0706fc 100644 --- a/core/Error.php +++ b/core/Error.php @@ -148,7 +148,7 @@ class Error $htmlString = ''; $htmlString .= "\n<div style='word-wrap: break-word; border: 3px solid red; padding:4px; width:70%; background-color:#FFFF96;'> <strong>There is an error. Please report the message (Piwik " . (class_exists('Piwik\Version') ? Version::VERSION : '') . ") - and full backtrace in the <a href='?module=Proxy&action=redirect&url=http://forum.piwik.org' target='_blank'>Piwik forums</a> (please do a Search first as it might have been reported already!).<br /><br/> + and full backtrace in the <a href='?module=Proxy&action=redirect&url=http://forum.piwik.org' rel='noreferrer' target='_blank'>Piwik forums</a> (please do a Search first as it might have been reported already!).<br /><br/> "; $htmlString .= Error::getErrNoString($message->errno); $htmlString .= ":</strong> <em>{$message->errstr}</em> in <strong>{$message->errfile}</strong>"; diff --git a/core/Metrics.php b/core/Metrics.php index 0f8baf5a10..009fa36151 100644 --- a/core/Metrics.php +++ b/core/Metrics.php @@ -10,6 +10,7 @@ namespace Piwik; use Piwik\Cache\LanguageAwareStaticCache; use Piwik\Cache\PluginAwareStaticCache; +use Piwik\Metrics\Formatter; require_once PIWIK_INCLUDE_PATH . "/core/Piwik.php"; @@ -183,11 +184,22 @@ class Metrics return $names; } - // TODO: this method is named wrong - public static function getMappingFromIdToName() + public static function getMappingFromNameToId() { - $idToName = array_flip(self::$mappingFromIdToName); - return $idToName; + static $nameToId = null; + if ($nameToId === null) { + $nameToId = array_flip(self::$mappingFromIdToName); + } + return $nameToId; + } + + public static function getMappingFromNameToIdGoal() + { + static $nameToId = null; + if ($nameToId === null) { + $nameToId = array_flip(self::$mappingFromIdToNameGoal); + } + return $nameToId; } /** @@ -223,7 +235,7 @@ class Metrics { $nameToUnit = array( '_rate' => '%', - 'revenue' => MetricsFormatter::getCurrencySymbol($idSite), + 'revenue' => Formatter::getCurrencySymbol($idSite), '_time_' => 's' ); diff --git a/core/Metrics/Base.php b/core/Metrics/Base.php deleted file mode 100644 index 838d7c2ee6..0000000000 --- a/core/Metrics/Base.php +++ /dev/null @@ -1,61 +0,0 @@ -<?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\Metrics; - -use Piwik\Metrics; -use Piwik\DataTable\Row; - -class Base -{ - protected $invalidDivision = 0; - protected $roundPrecision = 2; - - protected function getNumVisits(Row $row) - { - return (int) $this->getColumn($row, Metrics::INDEX_NB_VISITS); - } - - /** - * Returns column from a given row. - * Will work with 2 types of datatable - * - raw datatables coming from the archive DB, which columns are int indexed - * - datatables processed resulting of API calls, which columns have human readable english names - * - * @param Row|array $row - * @param int $columnIdRaw see consts in Archive:: - * @param bool|array $mappingIdToName - * @return mixed Value of column, false if not found - */ - public function getColumn($row, $columnIdRaw, $mappingIdToName = false) - { - if (empty($mappingIdToName)) { - $mappingIdToName = Metrics::$mappingFromIdToName; - } - - $columnIdReadable = $mappingIdToName[$columnIdRaw]; - - if ($row instanceof Row) { - $raw = $row->getColumn($columnIdRaw); - if ($raw !== false) { - return $raw; - } - return $row->getColumn($columnIdReadable); - } - - if (isset($row[$columnIdRaw])) { - return $row[$columnIdRaw]; - } - - if (isset($row[$columnIdReadable])) { - return $row[$columnIdReadable]; - } - - return false; - } -}
\ No newline at end of file diff --git a/core/Metrics/Formatter.php b/core/Metrics/Formatter.php new file mode 100644 index 0000000000..0e5bbe719e --- /dev/null +++ b/core/Metrics/Formatter.php @@ -0,0 +1,305 @@ +<?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\Metrics; + +use Piwik\Common; +use Piwik\DataTable; +use Piwik\Piwik; +use Piwik\Plugin\Metric; +use Piwik\Plugin\ProcessedMetric; +use Piwik\Plugin\Report; +use Piwik\Site; +use Piwik\Tracker\GoalManager; + +/** + * Contains methods to format metric values. Passed to the {@link \Piwik\Plugin\Metric::format()} + * method when formatting Metrics. + * + * @api + */ +class Formatter +{ + const PROCESSED_METRICS_FORMATTED_FLAG = 'processed_metrics_formatted'; + + private $decimalPoint = null; + private $thousandsSeparator = null; + + /** + * Returns a prettified string representation of a number. The result will have + * thousands separators and a decimal point specific to the current locale, eg, + * `'1,000,000.05'` or `'1.000.000,05'`. + * + * @param number $value + * @return string + */ + public function getPrettyNumber($value, $precision = 0) + { + if ($this->decimalPoint === null) { + $locale = localeconv(); + + $this->decimalPoint = $locale['decimal_point']; + $this->thousandsSeparator = $locale['thousands_sep']; + } + + return number_format($value, $precision, $this->decimalPoint, $this->thousandsSeparator); + } + + /** + * Returns a prettified time value (in seconds). + * + * @param int $numberOfSeconds The number of seconds. + * @param bool $displayTimeAsSentence If set to true, will output `"5min 17s"`, if false `"00:05:17"`. + * @param bool $round Whether to round to the nearest second or not. + * @return string + */ + public function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = false, $round = false) + { + $numberOfSeconds = $round ? (int)$numberOfSeconds : (float)$numberOfSeconds; + + $isNegative = false; + if ($numberOfSeconds < 0) { + $numberOfSeconds = -1 * $numberOfSeconds; + $isNegative = true; + } + + // Display 01:45:17 time format + if ($displayTimeAsSentence === false) { + $hours = floor($numberOfSeconds / 3600); + $minutes = floor(($reminder = ($numberOfSeconds - $hours * 3600)) / 60); + $seconds = floor($reminder - $minutes * 60); + $time = sprintf("%02s", $hours) . ':' . sprintf("%02s", $minutes) . ':' . sprintf("%02s", $seconds); + $centiSeconds = ($numberOfSeconds * 100) % 100; + if ($centiSeconds) { + $time .= '.' . sprintf("%02s", $centiSeconds); + } + if ($isNegative) { + $time = '-' . $time; + } + return $time; + } + $secondsInYear = 86400 * 365.25; + + $years = floor($numberOfSeconds / $secondsInYear); + $minusYears = $numberOfSeconds - $years * $secondsInYear; + $days = floor($minusYears / 86400); + + $minusDays = $numberOfSeconds - $days * 86400; + $hours = floor($minusDays / 3600); + + $minusDaysAndHours = $minusDays - $hours * 3600; + $minutes = floor($minusDaysAndHours / 60); + + $seconds = $minusDaysAndHours - $minutes * 60; + $precision = ($seconds > 0 && $seconds < 0.01 ? 3 : 2); + $seconds = round($seconds, $precision); + + if ($years > 0) { + $return = sprintf(Piwik::translate('General_YearsDays'), $years, $days); + } elseif ($days > 0) { + $return = sprintf(Piwik::translate('General_DaysHours'), $days, $hours); + } elseif ($hours > 0) { + $return = sprintf(Piwik::translate('General_HoursMinutes'), $hours, $minutes); + } elseif ($minutes > 0) { + $return = sprintf(Piwik::translate('General_MinutesSeconds'), $minutes, $seconds); + } else { + $return = sprintf(Piwik::translate('General_Seconds'), $seconds); + } + + if ($isNegative) { + $return = '-' . $return; + } + + return $return; + } + + /** + * Returns a prettified memory size value. + * + * @param number $size The size in bytes. + * @param string $unit The specific unit to use, if any. If null, the unit is determined by $size. + * @param int $precision The precision to use when rounding. + * @return string eg, `'128 M'` or `'256 K'`. + */ + public function getPrettySizeFromBytes($size, $unit = null, $precision = 1) + { + if ($size == 0) { + return '0 M'; + } + + $units = array('B', 'K', 'M', 'G', 'T'); + + $currentUnit = null; + foreach ($units as $idx => $currentUnit) { + if ($size >= 1024 && $unit != $currentUnit && $idx != count($units) - 1) { + $size = $size / 1024; + } else { + break; + } + } + + return round($size, $precision) . " " . $currentUnit; + } + + /** + * Returns a pretty formated monetary value using the currency associated with a site. + * + * @param int|string $value The monetary value to format. + * @param int $idSite The ID of the site whose currency will be used. + * @return string + */ + public function getPrettyMoney($value, $idSite) + { + $space = ' '; + + $currencySymbol = self::getCurrencySymbol($idSite); + + $currencyBefore = $currencySymbol . $space; + $currencyAfter = ''; + + // (maybe more currencies prefer this notation?) + $currencySymbolToAppend = array('€', 'kr', 'zł'); + + // manually put the currency symbol after the amount + if (in_array($currencySymbol, $currencySymbolToAppend)) { + $currencyAfter = $space . $currencySymbol; + $currencyBefore = ''; + } + + // if the input is a number (it could be a string or INPUT form), + // and if this number is not an int, we round to precision 2 + if (is_numeric($value)) { + if ($value == round($value)) { + // 0.0 => 0 + $value = round($value); + } else { + $precision = GoalManager::REVENUE_PRECISION; + $value = sprintf("%01." . $precision . "f", $value); + } + } + + $prettyMoney = $currencyBefore . $value . $currencyAfter; + return $prettyMoney; + } + + /** + * Returns a percent string from a quotient value. Forces the use of a '.' + * decimal place. + * + * @param float $value + * @return string + */ + public function getPrettyPercentFromQuotient($value) + { + $result = ($value * 100) . '%'; + return Common::forceDotAsSeparatorForDecimalPoint($result); + } + + /** + * Returns the currency symbol for a site. + * + * @param int $idSite The ID of the site to return the currency symbol for. + * @return string eg, `'$'`. + */ + public static function getCurrencySymbol($idSite) + { + $symbols = self::getCurrencyList(); + $currency = Site::getCurrencyFor($idSite); + + if (isset($symbols[$currency])) { + return $symbols[$currency][0]; + } + + return ''; + } + + /** + * Returns the list of all known currency symbols. + * + * @return array An array mapping currency codes to their respective currency symbols + * and a description, eg, `array('USD' => array('$', 'US dollar'))`. + */ + public static function getCurrencyList() + { + static $currenciesList = null; + + if (is_null($currenciesList)) { + require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Currencies.php'; + $currenciesList = $GLOBALS['Piwik_CurrencyList']; + } + + return $currenciesList; + } + + /** + * Formats all metrics, including processed metrics, for a DataTable. Metrics to format + * are found through report metadata and DataTable metadata. + * + * @param DataTable $dataTable The table to format metrics for. + * @param Report|null $report The report the table belongs to. + * @param string[]|null $metricsToFormat Whitelist of names of metrics to format. + */ + public function formatMetrics(DataTable $dataTable, Report $report = null, $metricsToFormat = null) + { + $metrics = $this->getMetricsToFormat($dataTable, $report); + if (empty($metrics) + || $dataTable->getMetadata(self::PROCESSED_METRICS_FORMATTED_FLAG) + ) { + return; + } + + $dataTable->setMetadata(self::PROCESSED_METRICS_FORMATTED_FLAG, true); + + if ($metricsToFormat !== null) { + $metricMatchRegex = $this->makeRegexToMatchMetrics($metricsToFormat); + $metrics = array_filter($metrics, function (ProcessedMetric $metric) use ($metricMatchRegex) { + return preg_match($metricMatchRegex, $metric->getName()); + }); + } + + foreach ($metrics as $name => $metric) { + if (!$metric->beforeFormat($report, $dataTable)) { + continue; + } + + foreach ($dataTable->getRows() as $row) { + $columnValue = $row->getColumn($name); + if ($columnValue !== false) { + $row->setColumn($name, $metric->format($columnValue, $this)); + } + + $subtable = $row->getSubtable(); + if (!empty($subtable)) { + $this->formatMetrics($subtable, $report, $metricsToFormat); + } + } + } + } + + private function makeRegexToMatchMetrics($metricsToFormat) + { + $metricsRegexParts = array(); + foreach ($metricsToFormat as $metricFilter) { + if ($metricFilter[0] == '/') { + $metricsRegexParts[] = '(?:' . substr($metricFilter, 1, strlen($metricFilter) - 2) . ')'; + } else { + $metricsRegexParts[] = preg_quote($metricFilter); + } + } + return '/^' . implode('|', $metricsRegexParts) . '$/'; + } + + /** + * @param DataTable $dataTable + * @param Report $report + * @return Metric[] + */ + private function getMetricsToFormat(DataTable $dataTable, Report $report = null) + { + return Report::getMetricsForTable($dataTable, $report, $baseType = 'Piwik\\Plugin\\Metric'); + } +}
\ No newline at end of file diff --git a/core/Metrics/Formatter/Html.php b/core/Metrics/Formatter/Html.php new file mode 100644 index 0000000000..c4f9e05664 --- /dev/null +++ b/core/Metrics/Formatter/Html.php @@ -0,0 +1,43 @@ +<?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\Metrics\Formatter; + +use Piwik\Metrics\Formatter; + +/** + * Metrics formatter that formats for HTML output. Uses non-breaking spaces in formatted values + * so text will stay unbroken in HTML views. + */ +class Html extends Formatter +{ + public function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = true, $round = false) + { + $result = parent::getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence, $round); + $result = $this->replaceSpaceWithNonBreakingSpace($result); + return $result; + } + + public function getPrettySizeFromBytes($size, $unit = null, $precision = 1) + { + $result = parent::getPrettySizeFromBytes($size, $unit, $precision); + $result = $this->replaceSpaceWithNonBreakingSpace($result); + return $result; + } + + public function getPrettyMoney($value, $idSite) + { + $result = parent::getPrettyMoney($value, $idSite); + $result = $this->replaceSpaceWithNonBreakingSpace($result); + return $result; + } + + private function replaceSpaceWithNonBreakingSpace($value) + { + return str_replace(' ', ' ', $value); + } +}
\ No newline at end of file diff --git a/core/Metrics/Processed.php b/core/Metrics/Processed.php deleted file mode 100644 index 0e6b7c969b..0000000000 --- a/core/Metrics/Processed.php +++ /dev/null @@ -1,68 +0,0 @@ -<?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\Metrics; - -use Piwik\Metrics; -use Piwik\DataTable\Row; -use Piwik\DataTable; - -class Processed extends Base -{ - - public function getConversionRate(Row $row) - { - $nbVisits = $this->getNumVisits($row); - - $nbVisitsConverted = (int) $this->getColumn($row, Metrics::INDEX_NB_VISITS_CONVERTED); - if ($nbVisitsConverted > 0) { - $conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $this->roundPrecision); - - return $conversionRate . '%'; - } - } - - public function getActionsPerVisit(Row $row) - { - $nbVisits = $this->getNumVisits($row); - $nbActions = $this->getColumn($row, Metrics::INDEX_NB_ACTIONS); - - if ($nbVisits == 0) { - return $this->invalidDivision; - } - - return round($nbActions / $nbVisits, $this->roundPrecision); - } - - public function getAvgTimeOnSite(Row $row) - { - $nbVisits = $this->getNumVisits($row); - - if ($nbVisits == 0) { - return $this->invalidDivision; - } - - $visitLength = $this->getColumn($row, Metrics::INDEX_SUM_VISIT_LENGTH); - - return round($visitLength / $nbVisits, $rounding = 0); - } - - public function getBounceRate(Row $row) - { - $nbVisits = $this->getNumVisits($row); - - if ($nbVisits == 0) { - return $this->invalidDivision; - } - - $bounceRate = round(100 * $this->getColumn($row, Metrics::INDEX_BOUNCE_COUNT) / $nbVisits, $this->roundPrecision); - - return $bounceRate . "%"; - } - -}
\ No newline at end of file diff --git a/core/Metrics/ProcessedGoals.php b/core/Metrics/ProcessedGoals.php deleted file mode 100644 index 6fb62231f3..0000000000 --- a/core/Metrics/ProcessedGoals.php +++ /dev/null @@ -1,123 +0,0 @@ -<?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\Metrics; - -use Piwik\Metrics; -use Piwik\DataTable\Row; -use Piwik\DataTable; -use Piwik\Piwik; -use Piwik\Tracker\GoalManager; - -class ProcessedGoals extends Base -{ - - public function getRevenuePerVisit(Row $row) - { - $goals = $this->getColumn($row, Metrics::INDEX_GOALS); - - $revenue = 0; - foreach ($goals as $goalId => $goalMetrics) { - if ($goalId == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) { - continue; - } - if ($goalId >= GoalManager::IDGOAL_ORDER - || $goalId == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER - ) { - $revenue += (int) $this->getColumn($goalMetrics, Metrics::INDEX_GOAL_REVENUE, Metrics::$mappingFromIdToNameGoal); - } - } - - if ($revenue == 0) { - $revenue = (int) $this->getColumn($row, Metrics::INDEX_REVENUE); - } - - $nbVisits = $this->getNumVisits($row); - $conversions = (int) $this->getColumn($row, Metrics::INDEX_NB_CONVERSIONS); - - // If no visit for this metric, but some conversions, we still want to display some kind of "revenue per visit" - // even though it will actually be in this edge case "Revenue per conversion" - $revenuePerVisit = $this->invalidDivision; - - if ($nbVisits > 0 - || $conversions > 0 - ) { - $revenuePerVisit = round($revenue / ($nbVisits == 0 ? $conversions : $nbVisits), GoalManager::REVENUE_PRECISION); - } - - return $revenuePerVisit; - } - - public function getConversionRate(Row $row, $goalMetrics) - { - $nbVisits = $this->getNumVisits($row); - - if ($nbVisits == 0) { - $value = $this->invalidDivision; - } else { - $conversions = $this->getNbConversions($goalMetrics); - $value = round(100 * $conversions / $nbVisits, GoalManager::REVENUE_PRECISION); - } - - if (empty($value)) { - return '0%'; - } - - return $value . "%"; - } - - public function getNbConversions($goalMetrics) - { - return (int) $this->getColumn($goalMetrics, - Metrics::INDEX_GOAL_NB_CONVERSIONS, - Metrics::$mappingFromIdToNameGoal); - } - - public function getRevenue($goalMetrics) - { - return (float) $this->getColumn($goalMetrics, - Metrics::INDEX_GOAL_REVENUE, - Metrics::$mappingFromIdToNameGoal); - } - - public function getRevenuePerVisitForGoal(Row $row, $goalMetrics) - { - $nbVisits = $this->getNumVisits($row); - - $div = $nbVisits; - if ($nbVisits == 0) { - $div = $this->getNbConversions($goalMetrics); - } - - $goalRevenue = $this->getRevenue($goalMetrics); - - return round($goalRevenue / $div, GoalManager::REVENUE_PRECISION); - } - - public function getAvgOrderRevenue($goalMetrics) - { - $goalRevenue = $this->getRevenue($goalMetrics); - $conversions = $this->getNbConversions($goalMetrics); - - return $goalRevenue / $conversions; - } - - public function getItems($goalMetrics) - { - $items = $this->getColumn($goalMetrics, - Metrics::INDEX_GOAL_ECOMMERCE_ITEMS, - Metrics::$mappingFromIdToNameGoal); - - if (empty($items)) { - return 0; - } - - return $items; - } - -}
\ No newline at end of file diff --git a/core/MetricsFormatter.php b/core/MetricsFormatter.php index 8554663525..daa8710fc6 100644 --- a/core/MetricsFormatter.php +++ b/core/MetricsFormatter.php @@ -4,251 +4,69 @@ * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * */ namespace Piwik; -use Piwik\Tracker\GoalManager; +use Piwik\Metrics\Formatter; +use Piwik\Plugins\API\ProcessedReport; /** * Contains helper function that format numerical values in different ways. * - * @api + * @deprecated */ class MetricsFormatter { - /** - * Returns a prettified string representation of a number. The result will have - * thousands separators and a decimal point specific to the current locale, eg, - * `'1,000,000.05'` or `'1.000.000,05'`. - * - * @param number $value - * @return string - */ - public static function getPrettyNumber($value) - { - static $decimalPoint = null; - static $thousandsSeparator = null; - - if ($decimalPoint === null) { - $locale = localeconv(); - - $decimalPoint = $locale['decimal_point']; - $thousandsSeparator = $locale['thousands_sep']; - } - - return number_format($value, 0, $decimalPoint, $thousandsSeparator); - } + private static $formatter = null; + private static $htmlFormatter = null; - /** - * Returns a prettified time value (in seconds). - * - * @param int $numberOfSeconds The number of seconds. - * @param bool $displayTimeAsSentence If set to true, will output `"5min 17s"`, if false `"00:05:17"`. - * @param bool $isHtml If true, replaces all spaces with `' '`. - * @param bool $round Whether to round to the nearest second or not. - * @return string - */ - public static function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = true, $isHtml = true, $round = false) + public static function getFormatter($isHtml = false) { - $numberOfSeconds = $round ? (int)$numberOfSeconds : (float)$numberOfSeconds; - - $isNegative = false; - if ($numberOfSeconds < 0) { - $numberOfSeconds = -1 * $numberOfSeconds; - $isNegative = true; - } - - // Display 01:45:17 time format - if ($displayTimeAsSentence === false) { - $hours = floor($numberOfSeconds / 3600); - $minutes = floor(($reminder = ($numberOfSeconds - $hours * 3600)) / 60); - $seconds = floor($reminder - $minutes * 60); - $time = sprintf("%02s", $hours) . ':' . sprintf("%02s", $minutes) . ':' . sprintf("%02s", $seconds); - $centiSeconds = ($numberOfSeconds * 100) % 100; - if ($centiSeconds) { - $time .= '.' . sprintf("%02s", $centiSeconds); - } - if ($isNegative) { - $time = '-' . $time; + if ($isHtml) { + if (self::$formatter === null) { + self::$formatter = new Formatter(); } - return $time; - } - $secondsInYear = 86400 * 365.25; - - $years = floor($numberOfSeconds / $secondsInYear); - $minusYears = $numberOfSeconds - $years * $secondsInYear; - $days = floor($minusYears / 86400); - - $minusDays = $numberOfSeconds - $days * 86400; - $hours = floor($minusDays / 3600); - - $minusDaysAndHours = $minusDays - $hours * 3600; - $minutes = floor($minusDaysAndHours / 60); - - $seconds = $minusDaysAndHours - $minutes * 60; - $precision = ($seconds > 0 && $seconds < 0.01 ? 3 : 2); - $seconds = round($seconds, $precision); - - if ($years > 0) { - $return = sprintf(Piwik::translate('General_YearsDays'), $years, $days); - } elseif ($days > 0) { - $return = sprintf(Piwik::translate('General_DaysHours'), $days, $hours); - } elseif ($hours > 0) { - $return = sprintf(Piwik::translate('General_HoursMinutes'), $hours, $minutes); - } elseif ($minutes > 0) { - $return = sprintf(Piwik::translate('General_MinutesSeconds'), $minutes, $seconds); + return self::$formatter; } else { - $return = sprintf(Piwik::translate('General_Seconds'), $seconds); - } - - if ($isNegative) { - $return = '-' . $return; + if (self::$htmlFormatter === null) { + self::$htmlFormatter = new Formatter\Html(); + } + return self::$htmlFormatter; } + } - if ($isHtml) { - return str_replace(' ', ' ', $return); - } + public static function getPrettyNumber($value) + { + return self::getFormatter()->getPrettyNumber($value); + } - return $return; + public static function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = true, $isHtml = true, $round = false) + { + return self::getFormatter($isHtml)->getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence, $round); } - /** - * Returns a prettified memory size value. - * - * @param number $size The size in bytes. - * @param string $unit The specific unit to use, if any. If null, the unit is determined by $size. - * @param int $precision The precision to use when rounding. - * @return string eg, `'128 M'` or `'256 K'`. - */ public static function getPrettySizeFromBytes($size, $unit = null, $precision = 1) { - if ($size == 0) { - return '0 M'; - } - - $units = array('B', 'K', 'M', 'G', 'T'); - foreach ($units as $currentUnit) { - if ($size >= 1024 && $unit != $currentUnit) { - $size = $size / 1024; - } else { - break; - } - } - - return round($size, $precision) . " " . $currentUnit; + return self::getFormatter()->getPrettySizeFromBytes($size, $unit, $precision); } - /** - * Returns a pretty formated monetary value using the currency associated with a site. - * - * @param int|string $value The monetary value to format. - * @param int $idSite The ID of the site whose currency will be used. - * @param bool $isHtml If true, replaces all spaces with `' '`. - * @return string - */ public static function getPrettyMoney($value, $idSite, $isHtml = true) { - $currencyBefore = MetricsFormatter::getCurrencySymbol($idSite); - - $space = ' '; - if ($isHtml) { - $space = ' '; - } - - $currencyAfter = ''; - // (maybe more currencies prefer this notation?) - $currencySymbolToAppend = array('€', 'kr', 'zł'); - - // manually put the currency symbol after the amount - if (in_array($currencyBefore, $currencySymbolToAppend)) { - $currencyAfter = $space . $currencyBefore; - $currencyBefore = ''; - } - - // if the input is a number (it could be a string or INPUT form), - // and if this number is not an int, we round to precision 2 - if (is_numeric($value)) { - if ($value == round($value)) { - // 0.0 => 0 - $value = round($value); - } else { - $precision = GoalManager::REVENUE_PRECISION; - $value = sprintf("%01." . $precision . "f", $value); - } - } - - $prettyMoney = $currencyBefore . $space . $value . $currencyAfter; - return $prettyMoney; + return self::getFormatter($isHtml)->getPrettyMoney($value, $idSite); } - /** - * Prettifies a metric value based on the column name. - * - * @param int $idSite The ID of the site the metric is for (used if the column value is an amount of money). - * @param string $columnName The metric name. - * @param mixed $value The metric value. - * @param bool $isHtml If true, replaces all spaces with `' '`. - * @return string - */ public static function getPrettyValue($idSite, $columnName, $value, $isHtml) { - // Display time in human readable - if (strpos($columnName, 'time') !== false) { - // Little hack: Display 15s rather than 00:00:15, only for "(avg|min|max)_generation_time" - $timeAsSentence = (substr($columnName, -16) == '_time_generation'); - return self::getPrettyTimeFromSeconds($value, $timeAsSentence); - } - - // Add revenue symbol to revenues - if (strpos($columnName, 'revenue') !== false && strpos($columnName, 'evolution') === false) { - return self::getPrettyMoney($value, $idSite, $isHtml); - } - - // Add % symbol to rates - if (strpos($columnName, '_rate') !== false) { - if (strpos($value, "%") === false) { - return $value . "%"; - } - } - - return $value; + return ProcessedReport::getPrettyValue(self::getFormatter($isHtml), $idSite, $columnName, $value); } - /** - * Returns the currency symbol for a site. - * - * @param int $idSite The ID of the site to return the currency symbol for. - * @return string eg, `'$'`. - */ public static function getCurrencySymbol($idSite) { - $symbols = MetricsFormatter::getCurrencyList(); - $site = new Site($idSite); - $currency = $site->getCurrency(); - - if (isset($symbols[$currency])) { - return $symbols[$currency][0]; - } - - return ''; + return Formatter::getCurrencySymbol($idSite); } - /** - * Returns the list of all known currency symbols. - * - * @return array An array mapping currency codes to their respective currency symbols - * and a description, eg, `array('USD' => array('$', 'US dollar'))`. - */ public static function getCurrencyList() { - static $currenciesList = null; - - if (is_null($currenciesList)) { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Currencies.php'; - $currenciesList = $GLOBALS['Piwik_CurrencyList']; - } - - return $currenciesList; + return Formatter::getCurrencyList(); } } diff --git a/core/Piwik.php b/core/Piwik.php index 80ed01cd8c..bc5244dd52 100644 --- a/core/Piwik.php +++ b/core/Piwik.php @@ -112,10 +112,23 @@ class Piwik */ public static function getPercentageSafe($dividend, $divisor, $precision = 0) { + return self::getQuotientSafe(100 * $dividend, $divisor, $precision); + } + + /** + * Safely compute a ratio. Returns 0 if divisor is 0 (to avoid division by 0 error). + * + * @param number $dividend + * @param number $divisor + * @param int $precision + * @return number + */ + public static function getQuotientSafe($dividend, $divisor, $precision = 0) + { if ($divisor == 0) { return 0; } - return round(100 * $dividend / $divisor, $precision); + return round($dividend / $divisor, $precision); } /** diff --git a/core/Plugin/AggregatedMetric.php b/core/Plugin/AggregatedMetric.php new file mode 100644 index 0000000000..7df1535727 --- /dev/null +++ b/core/Plugin/AggregatedMetric.php @@ -0,0 +1,22 @@ +<?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\Plugin; + +use Piwik\DataTable\Row; + +/** + * Base type for metric metadata classes that describe aggregated metrics. These metrics are + * computed in the backend data store and are aggregated in PHP when Piwik archives period reports. + * + * Note: This class is a placeholder. It will be filled out at a later date. Right now, only + * processed metrics can be defined this way. + */ +abstract class AggregatedMetric extends Metric +{ + // stub, to be filled out later +}
\ No newline at end of file diff --git a/core/Plugin/ComponentFactory.php b/core/Plugin/ComponentFactory.php index 9cb9cd1c1b..68415e9397 100644 --- a/core/Plugin/ComponentFactory.php +++ b/core/Plugin/ComponentFactory.php @@ -74,7 +74,7 @@ class ComponentFactory * @param callback $predicate * @return mixed The component that satisfies $predicate or null if not found. */ - public static function getComponentif ($componentTypeClass, $pluginName, $predicate) + public static function getComponentIf($componentTypeClass, $pluginName, $predicate) { $pluginManager = PluginManager::getInstance(); diff --git a/core/Plugin/ControllerAdmin.php b/core/Plugin/ControllerAdmin.php index 746395e88e..008cb4d2f4 100644 --- a/core/Plugin/ControllerAdmin.php +++ b/core/Plugin/ControllerAdmin.php @@ -131,7 +131,7 @@ abstract class ControllerAdmin extends Controller $message = sprintf("You are using the PHP accelerator & optimizer eAccelerator which is known to be not compatible with Piwik. We have disabled eAccelerator, which might affect the performance of Piwik. Read the %srelated ticket%s for more information and how to fix this problem.", - '<a target="_blank" href="https://github.com/piwik/piwik/issues/4439">', '</a>'); + '<a rel="noreferrer" target="_blank" href="https://github.com/piwik/piwik/issues/4439">', '</a>'); $notification = new Notification($message); $notification->context = Notification::CONTEXT_WARNING; diff --git a/core/Plugin/Metric.php b/core/Plugin/Metric.php new file mode 100644 index 0000000000..e475eb837f --- /dev/null +++ b/core/Plugin/Metric.php @@ -0,0 +1,179 @@ +<?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\Plugin; + +use Piwik\DataTable; +use Piwik\DataTable\Row; +use Piwik\Metrics; +use Piwik\Metrics\Formatter; + +/** + * Base type of metric metadata classes. + * + * A metric metadata class is a class that describes how a metric is described, computed and + * formatted. + * + * There are two types of metrics: aggregated and processed. An aggregated metric is computed + * in the backend datastore and aggregated in PHP when archiving period reports. + * + * Currently, only processed metrics can be defined as metric metadata classes. Support for + * aggregated metrics will be added at a later date. + * + * See {@link Piwik\Plugin\ProcessedMetric} and {@link Piwik\Plugin|AggregatedMetric}. + * + * @api + */ +abstract class Metric +{ + /** + * The sub-namespace name in a plugin where Metric components are stored. + */ + const COMPONENT_SUBNAMESPACE = 'Metrics'; + + /** + * Returns the column name of this metric, eg, `"nb_visits"` or `"avg_time_on_site"`. + * + * This string is what appears in API output. + * + * @return string + */ + abstract public function getName(); + + /** + * Returns the human readable translated name of this metric, eg, `"Visits"` or `"Avg. time on site"`. + * + * This string is what appears in the UI. + * + * @return string + */ + abstract public function getTranslatedName(); + + /** + * Returns a string describing what the metric represents. The result will be included in report metadata + * API output, including processed reports. + * + * Implementing this method is optional. + * + * @return string + */ + public function getDocumentation() + { + return ""; + } + + /** + * Returns a formatted metric value. This value is what appears in API output. From within Piwik, + * (core & plugins) the computed value is used. Only when outputting to the API does a metric + * get formatted. + * + * By default, just returns the value. + * + * @param mixed $value The metric value. + * @param Formatter $formatter The formatter to use when formatting a value. + * @return mixed $value + */ + public function format($value, Formatter $formatter) + { + return $value; + } + + /** + * Executed before formatting all metrics for a report. Implementers can return `false` + * to skip formatting this metric and can use this method to access information needed for + * formatting (for example, the site ID). + * + * @param Report $report + * @param DataTable $table + * @return bool Return `true` to format the metric for the table, `false` to skip formatting. + */ + public function beforeFormat($report, DataTable $table) + { + return true; + } + + /** + * Helper method that will access a metric in a {@link Piwik\DataTable\Row} or array either by + * its name or by its special numerical index value. + * + * @param Row|array $row + * @param string $columnName + * @param int[]|null $mappingNameToId A custom mapping of metric names to special index values. By + * default {@link Metrics::getMappingFromNameToId()} is used. + * @return mixed The metric value or false if none exists. + */ + public static function getMetric($row, $columnName, $mappingNameToId = null) + { + if (empty($mappingNameToId)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + if ($row instanceof Row) { + $value = $row->getColumn($columnName); + if ($value === false + && isset($mappingNameToId[$columnName]) + ) { + $value = $row->getColumn($mappingNameToId[$columnName]); + } + } else { + $value = @$row[$columnName]; + if ($value === null + && isset($mappingNameToId[$columnName]) + ) { + $columnName = $mappingNameToId[$columnName]; + $value = @$row[$columnName]; + } + return $value; + } + + return $value; + } + + /** + * Helper method that will determine the actual column name for a metric in a + * {@link Piwik\DataTable} and return every column value for this name. + * + * @param DataTable $table + * @param string $columnName + * @param int[]|null $mappingNameToId A custom mapping of metric names to special index values. By + * default {@link Metrics::getMappingFromNameToId()} is used. + * @return array + */ + public static function getMetricValues(DataTable $table, $columnName, $mappingNameToId = null) + { + if (empty($mappingIdToName)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + $columnName = self::getActualMetricColumn($table, $columnName, $mappingNameToId); + return $table->getColumn($columnName); + } + + /** + * Helper method that determines the actual column for a metric in a {@link Piwik\DataTable}. + * + * @param DataTable $table + * @param string $columnName + * @param int[]|null $mappingNameToId A custom mapping of metric names to special index values. By + * default {@link Metrics::getMappingFromNameToId()} is used. + * @return string + */ + public static function getActualMetricColumn(DataTable $table, $columnName, $mappingNameToId = null) + { + if (empty($mappingIdToName)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + $firstRow = $table->getFirstRow(); + if (!empty($firstRow) + && $firstRow->getColumn($columnName) === false + ) { + $columnName = $mappingNameToId[$columnName]; + } + return $columnName; + } +}
\ No newline at end of file diff --git a/core/Plugin/ProcessedMetric.php b/core/Plugin/ProcessedMetric.php new file mode 100644 index 0000000000..20201d0887 --- /dev/null +++ b/core/Plugin/ProcessedMetric.php @@ -0,0 +1,70 @@ +<?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\Plugin; + +use Piwik\DataTable; +use Piwik\DataTable\Row; + +/** + * Base type for processed metrics. A processed metric is a metric that is computed using + * one or more other metrics. + * + * @api + */ +abstract class ProcessedMetric extends Metric +{ + /** + * The sub-namespace name in a plugin where ProcessedMetrics are stored. + */ + const COMPONENT_SUBNAMESPACE = 'Columns\\Metrics'; + + /** + * Computes the metric using the values in a {@link Piwik\DataTable\Row}. + * + * The computed value should be numerical and not formatted in any way. For example, for + * a percent value, `0.14` should be returned instead of `"14%"`. + * + * @return mixed + */ + abstract public function compute(Row $row); + + /** + * Returns the array of metrics that are necessary for computing this metric. The elements + * of the array are metric names. + * + * @return string[] + */ + abstract public function getDependentMetrics(); + + /** + * Returns the array of metrics that are necessary for computing this metric, but should not + * be displayed to the user unless explicitly requested. These metrics are intermediate + * metrics that are not really valuable to the user. On a request, if showColumns or hideColumns + * is not used, they will be removed automatically. + * + * @return string[] + */ + public function getTemporaryMetrics() + { + return array(); + } + + /** + * Executed before computing all processed metrics for a report. Implementers can return `false` + * to skip computing this metric. + * + * @param Report $report + * @param DataTable $table + * @return bool Return `true` to compute the metric for the table, `false` to skip computing + * this metric. + */ + public function beforeCompute($report, DataTable $table) + { + return true; + } +}
\ No newline at end of file diff --git a/core/Plugin/Report.php b/core/Plugin/Report.php index c30e731d46..1824b6774f 100644 --- a/core/Plugin/Report.php +++ b/core/Plugin/Report.php @@ -119,7 +119,7 @@ class Report * platform default processed metrics, see {@link Metrics::getDefaultProcessedMetrics()}. Set it to boolean `false` * if your report does not support any processed metrics at all. Otherwise an array of metric names. * Eg `array('avg_time_on_site', 'nb_actions_per_visit', ...)` - * @var array|false + * @var array * @api */ protected $processedMetrics = array('nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate', 'conversion_rate'); @@ -196,6 +196,7 @@ class Report 'General_Visitors', 'DevicesDetection_DevicesDetection', 'UserSettings_VisitorSettings', + 'API' ); /** @@ -359,6 +360,44 @@ class Report } /** + * Returns the list of metrics required at minimum for a report factoring in the columns requested by + * the report requester. + * + * This will return all the metrics requested (or all the metrics in the report if nothing is requested) + * **plus** the metrics required to calculate the requested processed metrics. + * + * This method should be used in **Plugin.get** API methods. + * + * @param string[]|null $allMetrics The list of all available unprocessed metrics. Defaults to this report's + * metrics. + * @param string[]|null $restrictToColumns The requested columns. + * @return string[] + */ + public function getMetricsRequiredForReport($allMetrics = null, $restrictToColumns = null) + { + if (empty($allMetrics)) { + $allMetrics = $this->metrics; + } + + if (empty($restrictToColumns)) { + $restrictToColumns = array_merge($allMetrics, array_keys($this->getProcessedMetrics())); + } + + $processedMetricsById = $this->getProcessedMetricsById(); + $metricsSet = array_flip($allMetrics); + + $metrics = array(); + foreach ($restrictToColumns as $column) { + if (isset($processedMetricsById[$column])) { + $metrics = array_merge($metrics, $processedMetricsById[$column]->getDependentMetrics()); + } else if (isset($metricsSet[$column])) { + $metrics[] = $column; + } + } + return array_unique($metrics); + } + + /** * Returns an array of supported processed metrics and their corresponding translations. Eg * `array('nb_visits' => 'Visits')`. By default the given {@link $processedMetrics} are used and their * corresponding translations are looked up automatically. If a metric is not translated, you should add the @@ -378,6 +417,18 @@ class Report } /** + * Returns the array of all metrics displayed by this report. + * + * @return array + * @api + */ + public function getAllMetrics() + { + $processedMetrics = $this->getProcessedMetrics() ?: array(); + return array_keys(array_merge($this->getMetrics(), $processedMetrics)); + } + + /** * Returns an array of metric documentations and their corresponding translations. Eg * `array('nb_visits' => 'If a visitor comes to your website for the first time or if he visits a page more than 30 minutes after...')`. * By default the given {@link $metrics} are used and their corresponding translations are looked up automatically. @@ -399,6 +450,23 @@ class Report } } + $processedMetrics = $this->processedMetrics ?: array(); + foreach ($processedMetrics as $processedMetric) { + if (!($processedMetric instanceof ProcessedMetric)) { + continue; + } + + $name = $processedMetric->getName(); + $metricDocs = $processedMetric->getDocumentation(); + if (empty($metricDocs)) { + $metricDocs = @$translations[$name]; + } + + if (!empty($metricDocs)) { + $documentation[$processedMetric->getName()] = $metricDocs; + } + } + return $documentation; } @@ -666,7 +734,7 @@ class Report */ public static function getAllReports() { - $reports = PluginManager::getInstance()->findMultipleComponents('Reports', '\\Piwik\\Plugin\\Report'); + $reports = self::getAllReportClasses(); $cache = new LanguageAwareStaticCache('Reports' . implode('', $reports)); if (!$cache->has()) { @@ -685,6 +753,17 @@ class Report } /** + * Returns class names of all Report metadata classes. + * + * @return string[] + * @api + */ + public static function getAllReportClasses() + { + return PluginManager::getInstance()->findMultipleComponents('Reports', '\\Piwik\\Plugin\\Report'); + } + + /** * API metadata are sorted by category/name, * with a little tweak to replicate the standard Piwik category ordering * @@ -705,11 +784,15 @@ class Report $metrics = array(); foreach ($metricsToTranslate as $metric) { - if (!empty($translations[$metric])) { - $metrics[$metric] = $translations[$metric]; + if ($metric instanceof Metric) { + $metricName = $metric->getName(); + $translation = $metric->getTranslatedName(); } else { - $metrics[$metric] = $metric; + $metricName = $metric; + $translation = @$translations[$metric]; } + + $metrics[$metricName] = $translation ?: $metricName; } return $metrics; @@ -738,10 +821,76 @@ class Report */ public static function getForDimension(Dimension $dimension) { - return ComponentFactory::getComponentif (__CLASS__, $dimension->getModule(), function (Report $report) use ($dimension) { + return ComponentFactory::getComponentIf(__CLASS__, $dimension->getModule(), function (Report $report) use ($dimension) { return !$report->isSubtableReport() && $report->getDimension() && $report->getDimension()->getId() == $dimension->getId(); }); } -} + + /** + * Returns an array mapping the ProcessedMetrics served by this report by their string names. + * + * @return ProcessedMetric[] + */ + public function getProcessedMetricsById() + { + $processedMetrics = $this->processedMetrics ?: array(); + + $result = array(); + foreach ($processedMetrics as $processedMetric) { + if ($processedMetric instanceof ProcessedMetric) { // instanceof check for backwards compatibility + $result[$processedMetric->getName()] = $processedMetric; + } + } + return $result; + } + + /** + * Returns the Metrics that are displayed by a DataTable of a certain Report type. + * + * Includes ProcessedMetrics and Metrics. + * + * @param DataTable $dataTable + * @param Report|null $report + * @param string $baseType The base type each metric class needs to be of. + * @return Metric[] + * @api + */ + public static function getMetricsForTable(DataTable $dataTable, Report $report = null, $baseType = 'Piwik\\Plugin\\Metric') + { + $metrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME) ?: array(); + + if (!empty($report)) { + $metrics = array_merge($metrics, $report->getProcessedMetricsById()); + } + + $result = array(); + + /** @var Metric $metric */ + foreach ($metrics as $metric) { + if (!($metric instanceof $baseType)) { + continue; + } + + $result[$metric->getName()] = $metric; + } + + return $result; + } + + /** + * Returns the ProcessedMetrics that should be computed and formatted for a DataTable of a + * certain report. The ProcessedMetrics returned are those specified by the Report metadata + * as well as the DataTable metadata. + * + * @param DataTable $dataTable + * @param Report|null $report + * @return ProcessedMetric[] + * @api + */ + public static function getProcessedMetricsForTable(DataTable $dataTable, Report $report = null) + { + return self::getMetricsForTable($dataTable, $report, 'Piwik\\Plugin\\ProcessedMetric'); + } +}
\ No newline at end of file diff --git a/core/Plugin/Visualization.php b/core/Plugin/Visualization.php index 938bcb7a4d..7e6aec2eaa 100644 --- a/core/Plugin/Visualization.php +++ b/core/Plugin/Visualization.php @@ -9,11 +9,12 @@ namespace Piwik\Plugin; +use Piwik\API\DataTablePostProcessor; use Piwik\Common; use Piwik\DataTable; use Piwik\Date; use Piwik\Log; -use Piwik\MetricsFormatter; +use Piwik\Metrics\Formatter\Html as HtmlFormatter; use Piwik\NoAccessException; use Piwik\Option; use Piwik\Period; @@ -143,6 +144,12 @@ class Visualization extends ViewDataTable private $templateVars = array(); private $reportLastUpdatedMessage = null; private $metadata = null; + protected $metricsFormatter = null; + + /** + * @var Report + */ + protected $report; final public function __construct($controllerAction, $apiMethodToRequestDataTable, $params = array()) { @@ -152,7 +159,11 @@ class Visualization extends ViewDataTable throw new \Exception('You have not defined a constant named TEMPLATE_FILE in your visualization class.'); } + $this->metricsFormatter = new HtmlFormatter(); + parent::__construct($controllerAction, $apiMethodToRequestDataTable, $params); + + $this->report = Report::factory($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest()); } protected function buildView() @@ -160,20 +171,19 @@ class Visualization extends ViewDataTable $this->overrideSomeConfigPropertiesIfNeeded(); try { - $this->beforeLoadDataTable(); - $this->loadDataTableFromAPI(array('disable_generic_filters' => 1)); + $this->loadDataTableFromAPI(array('disable_generic_filters' => 1, 'format_metrics' => 0)); $this->postDataTableLoadedFromAPI(); $requestPropertiesAfterLoadDataTable = $this->requestConfig->getProperties(); $this->applyFilters(); + $this->addVisualizationInfoFromMetricMetadata(); $this->afterAllFiltersAreApplied(); $this->beforeRender(); $this->logMessageIfRequestPropertiesHaveChanged($requestPropertiesAfterLoadDataTable); - } catch (NoAccessException $e) { throw $e; } catch (\Exception $e) { @@ -306,6 +316,27 @@ class Visualization extends ViewDataTable } } + private function addVisualizationInfoFromMetricMetadata() + { + $dataTable = $this->dataTable instanceof DataTable\Map ? $this->dataTable->getFirstRow() : $this->dataTable; + + $metrics = Report::getMetricsForTable($dataTable, $this->report); + + // TODO: instead of iterating & calling translate everywhere, maybe we can get all translated names in one place. + // may be difficult, though, since translated metrics are specific to the report. + foreach ($metrics as $metric) { + $name = $metric->getName(); + + if (empty($this->config->translations[$name])) { + $this->config->translations[$name] = $metric->getTranslatedName(); + } + + if (empty($this->config->metrics_documentation[$name])) { + $this->config->metrics_documentation[$name] = $metric->getDocumentation(); + } + } + } + private function applyFilters() { list($priorityFilters, $otherFilters) = $this->config->getFiltersToRun(); @@ -322,10 +353,14 @@ class Visualization extends ViewDataTable $this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors, $this->dataTable->getColumns()); } + $postProcessor = $this->makeDataTablePostProcessor(); // must be created after requestConfig is final + if (!$this->requestConfig->areGenericFiltersDisabled()) { - $this->applyGenericFilters(); + $this->dataTable = $postProcessor->applyGenericFilters($this->dataTable); } + $postProcessor->applyComputeProcessedMetrics($this->dataTable); + $this->afterGenericFiltersAreAppliedToLoadedDataTable(); // queue other filters so they can be applied later if queued filters are disabled @@ -338,6 +373,12 @@ class Visualization extends ViewDataTable if (!$this->requestConfig->areQueuedFiltersDisabled()) { $this->dataTable->applyQueuedFilters(); } + + $formatter = $this->metricsFormatter; + $report = $this->report; + $this->dataTable->filter(function (DataTable $table) use ($formatter, $report) { + $formatter->formatMetrics($table, $report); + }); } private function removeEmptyColumnsFromDisplay() @@ -372,9 +413,8 @@ class Visualization extends ViewDataTable $today = mktime(0, 0, 0); if ($date->getTimestamp() > $today) { - $elapsedSeconds = time() - $date->getTimestamp(); - $timeAgo = MetricsFormatter::getPrettyTimeFromSeconds($elapsedSeconds); + $timeAgo = $this->metricsFormatter->getPrettyTimeFromSeconds($elapsedSeconds); return Piwik::translate('CoreHome_ReportGeneratedXAgo', $timeAgo); } @@ -567,10 +607,7 @@ class Visualization extends ViewDataTable // eg $this->config->showFooterColumns = true; } - /** - * Second, generic filters (Sort, Limit, Replace Column Names, etc.) - */ - private function applyGenericFilters() + private function makeDataTablePostProcessor() { $requestArray = $this->request->getRequestArray(); $request = \Piwik\API\Request::getRequestArrayFromString($requestArray); @@ -580,8 +617,7 @@ class Visualization extends ViewDataTable $request['filter_sort_order'] = ''; } - $genericFilter = new \Piwik\API\DataTableGenericFilter($request); - $genericFilter->filter($this->dataTable); + return new DataTablePostProcessor($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest(), $request); } private function logMessageIfRequestPropertiesHaveChanged(array $requestPropertiesBefore) diff --git a/core/Session.php b/core/Session.php index d55d399bbe..a002243f70 100644 --- a/core/Session.php +++ b/core/Session.php @@ -121,7 +121,7 @@ class Session extends Zend_Session $enableDbSessions = ''; if (DbHelper::isInstalled()) { $enableDbSessions = "<br/>If you still experience issues after trying these changes, - we recommend that you <a href='http://piwik.org/faq/how-to-install/#faq_133' target='_blank'>enable database session storage</a>."; + we recommend that you <a href='http://piwik.org/faq/how-to-install/#faq_133' rel='noreferrer' target='_blank'>enable database session storage</a>."; } $pathToSessions = Filechecks::getErrorMessageMissingPermissions(Filesystem::getPathToPiwikRoot() . '/tmp/sessions/'); diff --git a/core/Timer.php b/core/Timer.php index 82addaa1cd..c7401677bf 100644 --- a/core/Timer.php +++ b/core/Timer.php @@ -7,6 +7,7 @@ * */ namespace Piwik; +use Piwik\Metrics\Formatter; /** * @@ -15,12 +16,15 @@ class Timer { private $timerStart; private $memoryStart; + private $formatter; /** * @return \Piwik\Timer */ public function __construct() { + $this->formatter = new Formatter(); + $this->init(); } @@ -56,7 +60,7 @@ class Timer */ public function getMemoryLeak() { - return "Memory delta: " . MetricsFormatter::getPrettySizeFromBytes($this->getMemoryUsage() - $this->memoryStart); + return "Memory delta: " . $this->formatter->getPrettySizeFromBytes($this->getMemoryUsage() - $this->memoryStart); } /** diff --git a/core/Twig.php b/core/Twig.php index 1ce277e235..1127d47a60 100755 --- a/core/Twig.php +++ b/core/Twig.php @@ -11,6 +11,7 @@ namespace Piwik; use Exception; use Piwik\Container\StaticContainer; use Piwik\DataTable\Filter\SafeDecodeLabel; +use Piwik\Metrics\Formatter; use Piwik\Translate; use Piwik\View\RenderTokenParser; use Piwik\Visualization\Sparkline; @@ -35,6 +36,8 @@ class Twig */ private $twig; + private $formatter; + public function __construct() { $loader = $this->getDefaultThemeLoader(); @@ -45,6 +48,8 @@ class Twig $theme = $manager->getThemeEnabled(); $loaders = array(); + $this->formatter = new Formatter(); + //create loader for custom theme to overwrite twig templates if ($theme && $theme->getPluginName() != \Piwik\Plugin\Manager::DEFAULT_THEME) { $customLoader = $this->getCustomThemeLoader($theme); @@ -272,21 +277,23 @@ class Twig protected function addFilter_money() { - $moneyFilter = new Twig_SimpleFilter('money', function ($amount) { + $formatter = $this->formatter; + $moneyFilter = new Twig_SimpleFilter('money', function ($amount) use ($formatter) { if (func_num_args() != 2) { throw new Exception('the money modifier expects one parameter: the idSite.'); } $idSite = func_get_args(); $idSite = $idSite[1]; - return MetricsFormatter::getPrettyMoney($amount, $idSite); + return $formatter->getPrettyMoney($amount, $idSite); }); $this->twig->addFilter($moneyFilter); } protected function addFilter_sumTime() { - $sumtimeFilter = new Twig_SimpleFilter('sumtime', function ($numberOfSeconds) { - return MetricsFormatter::getPrettyTimeFromSeconds($numberOfSeconds); + $formatter = $this->formatter; + $sumtimeFilter = new Twig_SimpleFilter('sumtime', function ($numberOfSeconds) use ($formatter) { + return $formatter->getPrettyTimeFromSeconds($numberOfSeconds, true); }); $this->twig->addFilter($sumtimeFilter); } diff --git a/core/Updates/0.6-rc1.php b/core/Updates/0.6-rc1.php index 9f293dda97..9a946251ad 100644 --- a/core/Updates/0.6-rc1.php +++ b/core/Updates/0.6-rc1.php @@ -42,8 +42,8 @@ class Updates_0_6_rc1 extends Updates { // first we disable the plugins and keep an array of warnings messages $pluginsToDisableMessage = array( - 'SearchEnginePosition' => "SearchEnginePosition plugin was disabled, because it is not compatible with the new Piwik 0.6. \n You can download the latest version of the plugin, compatible with Piwik 0.6.\n<a target='_blank' href='?module=Proxy&action=redirect&url=https://github.com/piwik/piwik/issues/502'>Click here.</a>", - 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 0.6. \nYou can download the latest version of the plugin, compatible with Piwik 0.6.\n<a target='_blank' href='?module=Proxy&action=redirect&url=https://github.com/piwik/piwik/issues/45'>Click here.</a>" + 'SearchEnginePosition' => "SearchEnginePosition plugin was disabled, because it is not compatible with the new Piwik 0.6. \n You can download the latest version of the plugin, compatible with Piwik 0.6.\n<a rel='noreferrer' target='_blank' href='?module=Proxy&action=redirect&url=https://github.com/piwik/piwik/issues/502'>Click here.</a>", + 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 0.6. \nYou can download the latest version of the plugin, compatible with Piwik 0.6.\n<a rel='noreferrer' target='_blank' href='?module=Proxy&action=redirect&url=https://github.com/piwik/piwik/issues/45'>Click here.</a>" ); $disabledPlugins = array(); foreach ($pluginsToDisableMessage as $pluginToDisable => $warningMessage) { diff --git a/core/Updates/1.2-rc1.php b/core/Updates/1.2-rc1.php index e268ad8570..658dffeb27 100644 --- a/core/Updates/1.2-rc1.php +++ b/core/Updates/1.2-rc1.php @@ -129,7 +129,7 @@ class Updates_1_2_rc1 extends Updates { // first we disable the plugins and keep an array of warnings messages $pluginsToDisableMessage = array( - 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 1.2. \nYou can download the latest version of the plugin, compatible with Piwik 1.2.\n<a target='_blank' href='?module=Proxy&action=redirect&url=https://github.com/piwik/piwik/issues/45'>Click here.</a>", + 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 1.2. \nYou can download the latest version of the plugin, compatible with Piwik 1.2.\n<a rel='noreferrer' target='_blank' href='?module=Proxy&action=redirect&url=https://github.com/piwik/piwik/issues/45'>Click here.</a>", 'EntryPage' => "EntryPage plugin is not compatible with this version of Piwik, it was disabled.", ); $disabledPlugins = array(); diff --git a/core/testMinimumPhpVersion.php b/core/testMinimumPhpVersion.php index a24780077d..eec5a50d1b 100644 --- a/core/testMinimumPhpVersion.php +++ b/core/testMinimumPhpVersion.php @@ -52,11 +52,11 @@ if ($minimumPhpInvalid) { $composerInstall = "Download and run <a href=\"https://getcomposer.org/Composer-Setup.exe\"><b>Composer-Setup.exe</b></a>, it will install the latest Composer version and set up your PATH so that you can just call composer from any directory in your command line. " . " <br>Then run this command in a terminal in the piwik directory: <br> $ php composer.phar update "; } - $piwik_errorMessage .= "<p>It appears the <a href='https://getcomposer.org/' target='_blank'>composer</a> tool is not yet installed. You can install Composer in a few easy steps:\n\n". + $piwik_errorMessage .= "<p>It appears the <a href='https://getcomposer.org/' rel='noreferrer' target='_blank'>composer</a> tool is not yet installed. You can install Composer in a few easy steps:\n\n". "<br/>" . $composerInstall. " This will initialize composer for Piwik and download libraries we use in vendor/* directory.". "\n\n<br/><br/>Then reload this page to access your analytics reports." . - "\n\n<br/><br/>For more information check out this FAQ: <a href='http://piwik.org/faq/how-to-install/faq_18271/' target='_blank'>How do I use Piwik from the Git repository?</a>." . + "\n\n<br/><br/>For more information check out this FAQ: <a href='http://piwik.org/faq/how-to-install/faq_18271/' rel='noreferrer' target='_blank'>How do I use Piwik from the Git repository?</a>." . "\n\n<br/><br/>Note: if for some reasons you cannot install composer, instead install the latest Piwik release from ". "<a href='http://builds.piwik.org/piwik.zip'>builds.piwik.org</a>.</p>"; } @@ -122,11 +122,11 @@ if (!function_exists('Piwik_GetErrorMessagePage')) { if ($optionalLinks) { $optionalLinks = '<ul> - <li><a target="_blank" href="http://piwik.org">Piwik.org homepage</a></li> - <li><a target="_blank" href="http://piwik.org/faq/">Piwik Frequently Asked Questions</a></li> - <li><a target="_blank" href="http://piwik.org/docs/">Piwik Documentation</a></li> - <li><a target="_blank" href="http://forum.piwik.org/">Piwik Forums</a></li> - <li><a target="_blank" href="http://demo.piwik.org">Piwik Online Demo</a></li> + <li><a rel="noreferrer" target="_blank" href="http://piwik.org">Piwik.org homepage</a></li> + <li><a rel="noreferrer" target="_blank" href="http://piwik.org/faq/">Piwik Frequently Asked Questions</a></li> + <li><a rel="noreferrer" target="_blank" href="http://piwik.org/docs/">Piwik Documentation</a></li> + <li><a rel="noreferrer" target="_blank" href="http://forum.piwik.org/">Piwik Forums</a></li> + <li><a rel="noreferrer" target="_blank" href="http://demo.piwik.org">Piwik Online Demo</a></li> </ul>'; } if ($optionalLinkBack) { |