Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorMatthieu Napoli <matthieu@mnapoli.fr>2014-11-27 07:22:02 +0300
committerMatthieu Napoli <matthieu@mnapoli.fr>2014-11-27 07:22:02 +0300
commite00732e1ea29900860671a5b022d4450377776f2 (patch)
tree788c22f279ed6e337e36b5a0f99ff40e3f6b20d5 /core
parent56752448068fb162602502dd4041ffa7d1b60d3c (diff)
parentabf6f8857e3c944446ff4137f1f5be78b8b0bbdc (diff)
Merge branch 'master' into tmp-path
Conflicts: plugins/Installation/SystemCheck.php
Diffstat (limited to 'core')
-rw-r--r--core/API/DataTableGenericFilter.php54
-rw-r--r--core/API/DataTableManipulator.php11
-rw-r--r--core/API/DataTableManipulator/ReportTotalsCalculator.php95
-rw-r--r--core/API/DataTablePostProcessor.php391
-rw-r--r--core/API/DocumentationGenerator.php16
-rw-r--r--core/API/Inconsistencies.php42
-rw-r--r--core/API/Request.php32
-rw-r--r--core/API/ResponseBuilder.php98
-rw-r--r--core/Archive.php5
-rw-r--r--core/Archive/DataTableFactory.php29
-rw-r--r--core/CronArchive.php20
-rw-r--r--core/DataTable.php9
-rw-r--r--core/DataTable/Filter/AddColumnsProcessedMetrics.php37
-rw-r--r--core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php119
-rwxr-xr-xcore/DataTable/Filter/CalculateEvolutionFilter.php1
-rw-r--r--core/DataTable/Filter/ColumnCallbackReplace.php1
-rw-r--r--core/DataTable/Filter/ExcludeLowPopulation.php2
-rw-r--r--core/DataTable/Filter/PivotByDimension.php2
-rw-r--r--core/DataTable/Filter/Sort.php2
-rw-r--r--core/Error.php2
-rw-r--r--core/Metrics.php22
-rw-r--r--core/Metrics/Base.php61
-rw-r--r--core/Metrics/Formatter.php305
-rw-r--r--core/Metrics/Formatter/Html.php43
-rw-r--r--core/Metrics/Processed.php68
-rw-r--r--core/Metrics/ProcessedGoals.php123
-rw-r--r--core/MetricsFormatter.php236
-rw-r--r--core/Piwik.php15
-rw-r--r--core/Plugin/AggregatedMetric.php22
-rw-r--r--core/Plugin/ComponentFactory.php2
-rw-r--r--core/Plugin/ControllerAdmin.php2
-rw-r--r--core/Plugin/Metric.php179
-rw-r--r--core/Plugin/ProcessedMetric.php70
-rw-r--r--core/Plugin/Report.php163
-rw-r--r--core/Plugin/Visualization.php62
-rw-r--r--core/Session.php2
-rw-r--r--core/Timer.php6
-rwxr-xr-xcore/Twig.php15
-rw-r--r--core/Updates/0.6-rc1.php4
-rw-r--r--core/Updates/1.2-rc1.php2
-rw-r--r--core/testMinimumPhpVersion.php14
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 &gt; 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 &gt; 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(' ', '&nbsp;', $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 `'&nbsp;'`.
- * @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(' ', '&nbsp;', $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 `'&nbsp;'`.
- * @return string
- */
public static function getPrettyMoney($value, $idSite, $isHtml = true)
{
- $currencyBefore = MetricsFormatter::getCurrencySymbol($idSite);
-
- $space = ' ';
- if ($isHtml) {
- $space = '&nbsp;';
- }
-
- $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 `'&nbsp;'`.
- * @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) {