Skip to content
Extraits de code Groupes Projets
Valider 6bc50776 rédigé par diosmosis's avatar diosmosis
Parcourir les fichiers

Filling out TODO items and refactoring ResponseBuilder + computing/formatting of ProcessedMetrics.

parent 4aee52a3
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
......@@ -12,7 +12,6 @@ use Exception;
use Piwik\Common;
use Piwik\DataTable\Filter\AddColumnsProcessedMetricsGoal;
use Piwik\DataTable;
use Piwik\Plugin\ProcessedMetric;
use Piwik\Plugin\Report;
class DataTableGenericFilter
......@@ -24,22 +23,14 @@ class DataTableGenericFilter
*/
private $disabledFilters = array();
/**
* TODO
*
* @var Report|null-
*/
private $report;
/**
* Constructor
*
* @param $request
*/
function __construct($request, $report = null)
function __construct($request)
{
$this->request = $request;
$this->report = $report;
}
/**
......@@ -178,27 +169,7 @@ class DataTableGenericFilter
return $filterApplied;
}
public function computeProcessedMetricsIfNeeded(DataTable $dataTable)
{
if (!$this->doesColumnQueryParamReferenceProcessedMetric()) {
return false;
}
$this->computeProcessedMetrics($dataTable);
return true;
}
public function computeProcessedMetrics(DataTable $dataTable)
{
if (empty($this->report)) {
return;
}
$this->report->computeProcessedMetrics($dataTable);
}
private function doesColumnQueryParamReferenceProcessedMetric()
public function areProcessedMetricsNeededFor(Report $report)
{
$columnQueryParameters = array(
'filter_column',
......@@ -208,9 +179,9 @@ class DataTableGenericFilter
);
foreach ($columnQueryParameters as $queryParamName) {
$queryParamValue = Common::getRequestVar($queryParamName, false);
$queryParamValue = Common::getRequestVar($queryParamName, false, $type = null, $this->request);
if (!empty($queryParamValue)
&& $this->report->hasProcessedMetric($queryParamValue)
&& $report->hasProcessedMetric($queryParamValue)
) {
return true;
}
......
<?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 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\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 formates (eg, 'xml').
*/
class DataTablePostProcessor
{
const PROCESSED_METRICS_FORMATTED_FLAG = 'processed_metrics_formatted';
const PROCESSED_METRICS_COMPUTED_FLAG = 'processed_metrics_computed';
/**
* Apply post-processing logic to a DataTable of a report for an API request.
*
* @param DataTableInterface $dataTable The data table to process.
* @param Report|null $report The Report metadata class for the DataTable's report, or null if
* there is none.
* @param string[] $request The API request that
* @param bool $applyFormatting Whether to format processed metrics or not.
* @return DataTableInterface A new data table.
*/
public function process(DataTableInterface $dataTable, $report, $request, $applyFormatting = true)
{
$label = self::getLabelFromRequest($request);
$dataTable = $this->applyPivotByFilter($dataTable, $report, $request);
$dataTable = $this->applyFlattener($dataTable, $report, $request);
$dataTable = $this->applyTotalsCalculator($dataTable, $report, $request);
$dataTable = $this->applyGenericFilters($label, $dataTable, $report, $request);
$dataTable->filter(array($this, 'computeProcessedMetrics'), array($report));
// we automatically safe decode all datatable labels (against xss)
$dataTable->queueFilter('SafeDecodeLabel');
$dataTable = $this->applyQueuedFilters($dataTable, $request);
$dataTable = $this->applyRequestedColumnDeletion($dataTable, $request);
$dataTable = $this->applyLabelFilter($label, $dataTable, $report, $request);
$dataTable = $this->applyProcessedMetricsFormatting($dataTable, $report, $applyFormatting);
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @param Report|null $report
* @param $request
* @return DataTableInterface
*/
private function applyPivotByFilter(DataTableInterface $dataTable, $report, $request)
{
$pivotBy = Common::getRequestVar('pivotBy', false, 'string', $request);
if (!empty($pivotBy)) {
$reportId = $report->getModule() . '.' . $report->getAction();
$pivotByColumn = Common::getRequestVar('pivotByColumn', false, 'string', $request);
$pivotByColumnLimit = Common::getRequestVar('pivotByColumnLimit', false, 'int', $request);
$dataTable->filter('PivotByDimension', array($reportId, $pivotBy, $pivotByColumn, $pivotByColumnLimit,
PivotByDimension::isSegmentFetchingEnabledInConfig()));
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @param Report|null $report
* @param $request
* @return DataTable|DataTableInterface|DataTable\Map
*/
private function applyFlattener($dataTable, $report, $request)
{
if (Common::getRequestVar('flat', '0', 'string', $request) == '1') {
$flattener = new Flattener($report->getModule(), $report->getAction(), $request);
if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $request) == '1') {
$flattener->includeAggregateRows();
}
$dataTable = $flattener->flatten($dataTable);
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @param Report|null $report
* @param $request
* @return DataTableInterface
*/
private function applyTotalsCalculator($dataTable, $report, $request)
{
if (1 == Common::getRequestVar('totals', '1', 'integer', $request)) {
$reportTotalsCalculator = new ReportTotalsCalculator($report->getModule(), $report->getAction(), $request);
$dataTable = $reportTotalsCalculator->calculate($dataTable);
}
return $dataTable;
}
/**
* @param string $label
* @param DataTableInterface $dataTable
* @param Report|null $report
* @param $request
* @return DataTableInterface
*/
private function applyGenericFilters($label, $dataTable, $report, $request)
{
// if the flag disable_generic_filters is defined we skip the generic filters
if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $request)) {
$self = $this;
$genericFilter = new DataTableGenericFilter($request, $report);
if ($genericFilter->areProcessedMetricsNeededFor($report)) {
$dataTable->filter(function (DataTable $table) use ($self, $report) {
$self->computeProcessedMetrics($table, $report);
});
}
if (!empty($label)) {
$genericFilter->disableFilters(array('Limit', 'Truncate'));
}
$genericFilter->filter($dataTable);
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @param $request
* @return DataTableInterface
*/
private function applyQueuedFilters($dataTable, $request)
{
// if the flag disable_queued_filters is defined we skip the filters that were queued
if (Common::getRequestVar('disable_queued_filters', 0, 'int', $request) == 0) {
$dataTable->applyQueuedFilters();
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @param $request
* @return DataTableInterface
*/
private function applyRequestedColumnDeletion($dataTable, $request)
{
// 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', $request);
$showColumns = Common::getRequestVar('showColumns', '', 'string', $request);
if (empty($showColumns)) {
// if 'columns' is used, we remove all temporary metrics by showing only the columns specified in
// 'columns'
$showColumns = Common::getRequestVar('columns', '', 'string', $request);
}
if (!empty($hideColumns)
|| !empty($showColumns)
) {
$dataTable->filter('ColumnDelete', array($hideColumns, $showColumns));
}
return $dataTable;
}
/**
* @param string $label
* @param DataTableInterface $dataTable
* @param Report $report
* @return DataTableInterface
*/
private function applyLabelFilter($label, $dataTable, $report, $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', $request) == 1;
$filter = new LabelFilter($report->getModule(), $report->getAction(), $request);
$dataTable = $filter->filter($label, $dataTable, $addLabelIndex);
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @param Report $report
* @return DataTableInterface
*/
private function applyProcessedMetricsFormatting($dataTable, $report, $applyFormatting)
{
if ($applyFormatting) {
$dataTable->filter(array($this, 'formatProcessedMetrics'), array($report));
} else {
$dataTable->queueFilter(array($this, 'formatProcessedMetrics'), array($report)); // TODO: queuing does not always work.
}
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;
}
private function computeProcessedMetrics(DataTable $dataTable, $report)
{
if ($dataTable->getMetadata(self::PROCESSED_METRICS_COMPUTED_FLAG)) {
return;
}
$dataTable->setMetadata(self::PROCESSED_METRICS_COMPUTED_FLAG, true);
$processedMetrics = $this->getProcessedMetricsFor($dataTable, $report);
if (empty($processedMetrics)) {
return;
}
foreach ($processedMetrics as $name => $processedMetric) {
if (!$processedMetric->beforeCompute($this, $dataTable)) {
continue;
}
foreach ($dataTable->getRows() as $row) {
if ($row->getColumn($name) === false) { // do not compute the metric if it has been computed already
$row->addColumn($name, $processedMetric->compute($row));
$subtable = $row->getSubtable();
if (!empty($subtable)) {
$this->computeProcessedMetrics($subtable, $report);
}
}
}
}
}
/**
* public for use as callback.
*/
public function formatProcessedMetrics(DataTable $dataTable, $report)
{
if ($dataTable->getMetadata(self::PROCESSED_METRICS_FORMATTED_FLAG)) {
return;
}
$dataTable->setMetadata(self::PROCESSED_METRICS_FORMATTED_FLAG, true);
$processedMetrics = $this->getProcessedMetricsFor($dataTable, $report);
if (empty($processedMetrics)) {
return;
}
foreach ($dataTable->getRows() as $row) {
foreach ($processedMetrics as $name => $processedMetric) {
$columnValue = $row->getColumn($name);
if ($columnValue !== false) {
$row->setColumn($name, $processedMetric->format($columnValue));
}
$subtable = $row->getSubtable();
if (!empty($subtable)) {
$this->formatProcessedMetrics($subtable, $report);
}
}
}
}
/**
* @param DataTable $dataTable
* @param Report $report
* @return ProcessedMetric[]
*/
private function getProcessedMetricsFor(DataTable $dataTable, $report)
{
$processedMetrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME) ?: array();
if (!empty($report)) {
$processedMetrics = array_merge($processedMetrics, $report->processedMetrics ?: array());
}
$result = array();
foreach ($processedMetrics as $metric) {
if (!($metric instanceof ProcessedMetric)) {
continue;
}
$result[$metric->getName()] = $metric;
}
return $result;
}
}
\ No newline at end of file
......@@ -9,12 +9,8 @@
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;
......@@ -33,6 +29,8 @@ class ResponseBuilder
private $apiModule = false;
private $apiMethod = false;
private $postProcessor;
/**
* @param string $outputFormat
* @param array $request
......@@ -42,6 +40,7 @@ class ResponseBuilder
$this->outputFormat = $outputFormat;
$this->request = $request;
$this->apiRenderer = ApiRenderer::factory($outputFormat, $request);
$this->postProcessor = new DataTablePostProcessor();
}
public function disableSendHeader()
......@@ -168,90 +167,10 @@ class ResponseBuilder
private function handleDataTable(DataTableInterface $datatable)
{
$label = $this->getLabelFromRequest($this->request);
$report = Report::factory($this->apiModule, $this->apiMethod);
$applyFormatting = !($this->apiRenderer instanceof Original);
$genericFilter = new DataTableGenericFilter($this->request, $report);
// 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)) {
$reportTotalsCalculator = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request);
$datatable = $reportTotalsCalculator->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)) {
$datatable->filter(function (DataTable $table) use ($genericFilter) {
$genericFilter->computeProcessedMetricsIfNeeded($table);
});
if (!empty($label)) {
$genericFilter->disableFilters(array('Limit', 'Truncate'));
}
$genericFilter->filter($datatable);
}
$datatable->filter(function (DataTable $table) use ($genericFilter) {
$genericFilter->computeProcessedMetrics($table);
});
// 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 (empty($showColumns)) {
$showColumns = Common::getRequestVar('columns', '', 'string', $this->request); // TODO: note backwards compatibility
}
if (!empty($hideColumns)
|| !empty($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);
}
if (!empty($report)) {
if (!($this->apiRenderer instanceof Original)) {
$datatable->filter(array($report, 'formatProcessedMetrics'));
} else {
$datatable->queueFilter(array($report, 'formatProcessedMetrics')); // TODO: queuing does not always work.
}
}
$datatable = $this->postProcessor->process($datatable, $report, $this->request, $applyFormatting);
return $this->apiRenderer->renderDataTable($datatable);
}
......@@ -286,36 +205,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) {
......
......@@ -121,10 +121,8 @@ class Report
* Eg `array('avg_time_on_site', 'nb_actions_per_visit', ...)`
* @var array|false
* @api
*
* TODO: shouldn't be public
*/
public $processedMetrics = array('nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate', 'conversion_rate');
protected $processedMetrics = array('nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate', 'conversion_rate');
// for a little performance improvement we avoid having to call Metrics::getDefaultProcessedMetrics for each report
/**
......@@ -361,7 +359,18 @@ class Report
}
/**
* TODO
* 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 $allColumns The list of all available columns. Defaults to this report's metrics
* and processed metrics.
* @param string[]|null $restrictToColumns The requested columns.
* @return string[]
*/
public function getMetricsRequiredForReport($allColumns = null, $restrictToColumns = null)
{
......@@ -697,97 +706,13 @@ class Report
}
/**
* TODO
* Returns true if this report has a processed metric with the `$name` name.
*
* @return ProcessedMetric[]
*/
public function getProcessedMetricsFor(DataTable $dataTable)
{
$dataTableProcessedMetrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME) ?: array();
$processedMetrics = $this->processedMetrics ?: array();
$processedMetrics = array_merge($processedMetrics, $dataTableProcessedMetrics);
$result = array();
foreach ($processedMetrics as $metric) {
if (!($metric instanceof ProcessedMetric)) {
continue;
}
$result[$metric->getName()] = $metric;
}
return $result;
}
/**
* TODO
* Will only search through {@link Piwik\Plugin\ProcessedMetric} instances, so string entries
* in {@link $processedMetrics} will be ignored.
*
* TODO: put in new non-filter class. do not mark w/ @api.
*/
public function computeProcessedMetrics(DataTable $dataTable)
{
if ($dataTable->getMetadata('processed_metrics_computed')) {
return;
}
$dataTable->setMetadata('processed_metrics_computed', true); // TODO: metadataname should be const
$processedMetrics = $this->getProcessedMetricsFor($dataTable);
if (empty($processedMetrics)) {
return;
}
foreach ($processedMetrics as $name => $processedMetric) {
if (!$processedMetric->beforeCompute($this, $dataTable)) {
continue;
}
foreach ($dataTable->getRows() as $row) {
if ($row->getColumn($name) === false) { // do not compute the metric if it has been computed already
$row->addColumn($name, $processedMetric->compute($row));
$subtable = $row->getSubtable();
if (!empty($subtable)) {
$this->computeProcessedMetrics($subtable);
}
}
}
}
}
/**
* TODO
*/
public function formatProcessedMetrics(DataTable $dataTable)
{
if ($dataTable->getMetadata('processed_metrics_formatted')) {
return;
}
$dataTable->setMetadata('processed_metrics_formatted', true); // TODO: metadataname should be const
$processedMetrics = $this->getProcessedMetricsFor($dataTable);
if (empty($processedMetrics)) {
return;
}
foreach ($dataTable->getRows() as $row) {
foreach ($processedMetrics as $name => $processedMetric) {
$columnValue = $row->getColumn($name);
if ($columnValue !== false) {
$row->setColumn($name, $processedMetric->format($columnValue));
}
$subtable = $row->getSubtable();
if (!empty($subtable)) {
$this->formatProcessedMetrics($subtable);
}
}
}
}
/**
* TODO
* @param string $name
* @return bool
*/
public function hasProcessedMetric($name)
{
......
......@@ -10,6 +10,7 @@ namespace Piwik\Plugins\API;
use Exception;
use Piwik\API\DataTableManipulator\LabelFilter;
use Piwik\API\DataTablePostProcessor;
use Piwik\API\Request;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
......@@ -48,7 +49,7 @@ class RowEvolution
throw new Exception("Row evolutions can not be processed with this combination of \'date\' and \'period\' parameters.");
}
$label = ResponseBuilder::unsanitizeLabelParameter($label);
$label = DataTablePostProcessor::unsanitizeLabelParameter($label);
$labels = Piwik::getArrayFromApiParameter($label);
$metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal);
......
......@@ -9,6 +9,7 @@
namespace Piwik\Plugins\CoreHome\DataTableRowAction;
use Exception;
use Piwik\API\DataTablePostProcessor;
use Piwik\API\Request;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
......@@ -85,7 +86,7 @@ class RowEvolution
$this->apiMethod = Common::getRequestVar('apiMethod', '', 'string');
if (empty($this->apiMethod)) throw new Exception("Parameter apiMethod not set.");
$this->label = ResponseBuilder::getLabelFromRequest($_GET);
$this->label = DataTablePostProcessor::getLabelFromRequest($_GET);
if (!is_array($this->label)) {
throw new Exception("Expected label to be an array, got instead: " . $this->label);
}
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter