Skip to content
Extraits de code Groupes Projets
DataTablePostProcessor.php 15,1 ko
Newer Older
  • Learn to ignore specific revisions
  • <?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;
    
    
    /**
     * 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';
    
    
    diosmosis's avatar
    diosmosis a validé
        /**
         * @var null|Report
         */
        private $report;
    
        /**
         * @var string[]
         */
        private $request;
    
        /**
         * @var string
         */
        private $apiModule;
    
        /**
         * @var string
         */
        private $apiMethod;
    
    
        /**
         * @var Inconsistencies
         */
        private $apiInconsistencies;
    
    
        private $callbackBeforeGenericFilters;
        private $callbackAfterGenericFilters;
    
    
    diosmosis's avatar
    diosmosis a validé
        /**
         * Constructor.
         */
        public function __construct($apiModule, $apiMethod, $request)
        {
            $this->apiModule = $apiModule;
            $this->apiMethod = $apiMethod;
    
    diosmosis's avatar
    diosmosis a validé
    
    
            $this->report = ReportsProvider::factory($apiModule, $apiMethod);
    
            $this->apiInconsistencies = new Inconsistencies();
    
            $this->setFormatter(new Formatter());
        }
    
        public function setFormatter(Formatter $formatter)
        {
            $this->formatter = $formatter;
        }
    
        public function setRequest($request)
        {
            $this->request = $request;
        }
    
        public function setCallbackBeforeGenericFilters($callbackBeforeGenericFilters)
        {
            $this->callbackBeforeGenericFilters = $callbackBeforeGenericFilters;
        }
    
        public function setCallbackAfterGenericFilters($callbackAfterGenericFilters)
        {
            $this->callbackAfterGenericFilters = $callbackAfterGenericFilters;
    
        /**
         * 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);
    
    diosmosis's avatar
    diosmosis a validé
            $dataTable = $this->applyTotalsCalculator($dataTable);
    
            $dataTable = $this->applyFlattener($dataTable);
    
            if ($this->callbackBeforeGenericFilters) {
                call_user_func($this->callbackBeforeGenericFilters, $dataTable);
            }
    
    diosmosis's avatar
    diosmosis a validé
    
    
            $dataTable = $this->applyGenericFilters($dataTable);
    
    diosmosis's avatar
    diosmosis a validé
            $this->applyComputeProcessedMetrics($dataTable);
    
    
            if ($this->callbackAfterGenericFilters) {
                call_user_func($this->callbackAfterGenericFilters, $dataTable);
            }
    
    
            // we automatically safe decode all datatable labels (against xss)
            $dataTable->queueFilter('SafeDecodeLabel');
    
            $dataTable = $this->convertSegmentValueToSegment($dataTable);
    
    diosmosis's avatar
    diosmosis a validé
            $dataTable = $this->applyQueuedFilters($dataTable);
            $dataTable = $this->applyRequestedColumnDeletion($dataTable);
    
    diosmosis's avatar
    diosmosis a validé
            $dataTable = $this->applyLabelFilter($dataTable);
    
            $dataTable = $this->applyMetricsFormatting($dataTable);
    
        private function convertSegmentValueToSegment(DataTableInterface $dataTable)
    
            $dataTable->filter('AddSegmentBySegmentValue', array($this->report));
    
            $dataTable->filter('ColumnCallbackDeleteMetadata', array('segmentValue'));
    
    
            return $dataTable;
        }
    
        /**
         * @param DataTableInterface $dataTable
         * @return DataTableInterface
         */
    
    diosmosis's avatar
    diosmosis a validé
        public function applyPivotByFilter(DataTableInterface $dataTable)
    
    diosmosis's avatar
    diosmosis a validé
            $pivotBy = Common::getRequestVar('pivotBy', false, 'string', $this->request);
    
    diosmosis's avatar
    diosmosis a validé
                $this->applyComputeProcessedMetrics($dataTable);
    
    
    diosmosis's avatar
    diosmosis a validé
                $reportId = $this->apiModule . '.' . $this->apiMethod;
                $pivotByColumn = Common::getRequestVar('pivotByColumn', false, 'string', $this->request);
                $pivotByColumnLimit = Common::getRequestVar('pivotByColumnLimit', false, 'int', $this->request);
    
                $dataTable->filter('ColumnCallbackDeleteMetadata', array('segmentValue'));
    
                $dataTable->filter('ColumnCallbackDeleteMetadata', array('segment'));
    
                $dataTable->filter('PivotByDimension', array($reportId, $pivotBy, $pivotByColumn, $pivotByColumnLimit,
                    PivotByDimension::isSegmentFetchingEnabledInConfig()));
            }
            return $dataTable;
        }
    
        /**
         * @param DataTableInterface $dataTable
         * @return DataTable|DataTableInterface|DataTable\Map
         */
    
    diosmosis's avatar
    diosmosis a validé
        public function applyFlattener($dataTable)
    
    diosmosis's avatar
    diosmosis a validé
            if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') {
    
                // skip flattening if not supported by report and remove subtables only
                if ($this->report && !$this->report->supportsFlatten()) {
                    $dataTable->filter('RemoveSubtables');
                    return $dataTable;
                }
    
    
    diosmosis's avatar
    diosmosis a validé
                $flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request);
                if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') {
    
    
                $recursiveLabelSeparator = ' - ';
                if ($this->report) {
                    $recursiveLabelSeparator = $this->report->getRecursiveLabelSeparator();
                }
    
                $dataTable = $flattener->flatten($dataTable, $recursiveLabelSeparator);
    
            }
            return $dataTable;
        }
    
        /**
         * @param DataTableInterface $dataTable
         * @return DataTableInterface
         */
    
    diosmosis's avatar
    diosmosis a validé
        public function applyTotalsCalculator($dataTable)
    
    diosmosis's avatar
    diosmosis a validé
            if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) {
    
                $calculator = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request, $this->report);
                $dataTable  = $calculator->calculate($dataTable);
    
            }
            return $dataTable;
        }
    
        /**
         * @param DataTableInterface $dataTable
         * @return DataTableInterface
         */
    
    diosmosis's avatar
    diosmosis a validé
        public function applyGenericFilters($dataTable)
    
        {
            // if the flag disable_generic_filters is defined we skip the generic filters
    
    diosmosis's avatar
    diosmosis a validé
            if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) {
    
                $this->applyProcessedMetricsGenericFilters($dataTable);
    
    diosmosis's avatar
    diosmosis a validé
    
    
                $genericFilter = new DataTableGenericFilter($this->request, $this->report);
    
    diosmosis's avatar
    diosmosis a validé
                $self = $this;
                $report = $this->report;
    
                $dataTable->filter(function (DataTable $table) use ($genericFilter, $report, $self) {
                    $processedMetrics = Report::getProcessedMetricsForTable($table, $report);
    
    diosmosis's avatar
    diosmosis a validé
                    if ($genericFilter->areProcessedMetricsNeededFor($processedMetrics)) {
    
    diosmosis's avatar
    diosmosis a validé
                        $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
         */
    
    diosmosis's avatar
    diosmosis a validé
        public function applyQueuedFilters($dataTable)
    
        {
            // if the flag disable_queued_filters is defined we skip the filters that were queued
    
    diosmosis's avatar
    diosmosis a validé
            if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) {
    
                $dataTable->applyQueuedFilters();
            }
            return $dataTable;
        }
    
        /**
         * @param DataTableInterface $dataTable
         * @return DataTableInterface
         */
    
    diosmosis's avatar
    diosmosis a validé
        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)
    
    diosmosis's avatar
    diosmosis a validé
            $hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request);
            $showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request);
    
            $showRawMetrics = Common::getRequestVar('showRawMetrics', 0, 'int', $this->request);
    
            if (!empty($hideColumns)
                || !empty($showColumns)
            ) {
                $dataTable->filter('ColumnDelete', array($hideColumns, $showColumns));
    
                $this->removeTemporaryMetrics($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) {
    
    diosmosis's avatar
    diosmosis a validé
                $processedMetrics = Report::getProcessedMetricsForTable($table, $report);
    
    
                $allTemporaryMetrics = array();
                foreach ($processedMetrics as $metric) {
                    $allTemporaryMetrics = array_merge($allTemporaryMetrics, $metric->getTemporaryMetrics());
                }
    
    
    diosmosis's avatar
    diosmosis a validé
                if (!empty($allTemporaryMetrics)) {
                    $table->filter('ColumnDelete', array($allTemporaryMetrics));
                }
    
        /**
         * @param DataTableInterface $dataTable
         * @return DataTableInterface
         */
    
    diosmosis's avatar
    diosmosis a validé
        public function applyLabelFilter($dataTable)
    
    diosmosis's avatar
    diosmosis a validé
            $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)) {
    
    diosmosis's avatar
    diosmosis a validé
                $addLabelIndex = Common::getRequestVar('labelFilterAddLabelIndex', 0, 'int', $this->request) == 1;
    
    diosmosis's avatar
    diosmosis a validé
                $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 = $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 = str_replace( htmlentities('>', ENT_COMPAT | ENT_HTML401, 'UTF-8'), '>', $label);
    
    diosmosis's avatar
    diosmosis a validé
        public function computeProcessedMetrics(DataTable $dataTable)
    
        {
            if ($dataTable->getMetadata(self::PROCESSED_METRICS_COMPUTED_FLAG)) {
                return;
            }
    
    
            /** @var ProcessedMetric[] $processedMetrics */
    
            $processedMetrics = Report::getProcessedMetricsForTable($dataTable, $this->report);
    
    diosmosis's avatar
    diosmosis a validé
            $dataTable->setMetadata(self::PROCESSED_METRICS_COMPUTED_FLAG, true);
    
    
            foreach ($processedMetrics as $name => $processedMetric) {
    
    diosmosis's avatar
    diosmosis a validé
                if (!$processedMetric->beforeCompute($this->report, $dataTable)) {
    
                    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);
                        }
    
                            $this->computeProcessedMetrics($subtable);
    
    diosmosis's avatar
    diosmosis a validé
        public function applyComputeProcessedMetrics(DataTableInterface $dataTable)
    
    diosmosis's avatar
    diosmosis a validé
        {
            $dataTable->filter(array($this, 'computeProcessedMetrics'));
        }