1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik\API;
10
11use Exception;
12use Piwik\Common;
13use Piwik\DataTable;
14use Piwik\Plugin\ProcessedMetric;
15use Piwik\Plugin\Report;
16
17class DataTableGenericFilter
18{
19    /**
20     * List of filter names not to run.
21     *
22     * @var string[]
23     */
24    private $disabledFilters = array();
25
26    /**
27     * @var Report
28     */
29    private $report;
30
31    /**
32     * @var array
33     */
34    private $request;
35
36    /**
37     * Constructor
38     *
39     * @param $request
40     */
41    public function __construct($request, $report)
42    {
43        $this->request = $request;
44        $this->report  = $report;
45    }
46
47    /**
48     * Filters the given data table
49     *
50     * @param DataTable $table
51     */
52    public function filter($table)
53    {
54        $this->applyGenericFilters($table);
55    }
56
57    /**
58     * Makes sure a set of filters are not run.
59     *
60     * @param string[] $filterNames The name of each filter to disable.
61     */
62    public function disableFilters($filterNames)
63    {
64        $this->disabledFilters = array_unique(array_merge($this->disabledFilters, $filterNames));
65    }
66
67    /**
68     * Returns an array containing the information of the generic Filter
69     * to be applied automatically to the data resulting from the API calls.
70     *
71     * Order to apply the filters:
72     * 1 - Filter that remove filtered rows
73     * 2 - Filter that sort the remaining rows
74     * 3 - Filter that keep only a subset of the results
75     * 4 - Presentation filters
76     *
77     * @return array  See the code for spec
78     */
79    public static function getGenericFiltersInformation()
80    {
81        return array(
82            array('Pattern',
83                  array(
84                      'filter_column'  => array('string', 'label'),
85                      'filter_pattern' => array('string')
86                  )),
87            array('PatternRecursive',
88                  array(
89                      'filter_column_recursive'  => array('string', 'label'),
90                      'filter_pattern_recursive' => array('string'),
91                  )),
92            array('ExcludeLowPopulation',
93                  array(
94                      'filter_excludelowpop'       => array('string'),
95                      'filter_excludelowpop_value' => array('float', '0'),
96                  )),
97            array('Sort',
98                  array(
99                      'filter_sort_column' => array('string'),
100                      'filter_sort_order'  => array('string', 'desc'),
101                      $naturalSort = true,
102                      $recursiveSort = true,
103                      'filter_sort_column_secondary' => true
104                  )),
105            array('Truncate',
106                  array(
107                      'filter_truncate' => array('integer'),
108                  )),
109            array('Limit',
110                  array(
111                      'filter_offset'    => array('integer', '0'),
112                      'filter_limit'     => array('integer'),
113                      'keep_summary_row' => array('integer', '0'),
114                  ))
115        );
116    }
117
118    private function getGenericFiltersHavingDefaultValues()
119    {
120        $filters = self::getGenericFiltersInformation();
121
122        if ($this->report && $this->report->getDefaultSortColumn()) {
123            foreach ($filters as $index => $filter) {
124                if ($filter[0] === 'Sort') {
125                    $filters[$index][1]['filter_sort_column'] = array('string', $this->report->getDefaultSortColumn());
126                    $filters[$index][1]['filter_sort_order']  = array('string', $this->report->getDefaultSortOrder());
127
128                    $callback = $this->report->getSecondarySortColumnCallback();
129
130                    if (is_callable($callback)) {
131                        $filters[$index][1]['filter_sort_column_secondary'] = $callback;
132                    }
133
134                }
135            }
136        }
137
138        return $filters;
139    }
140
141    /**
142     * Apply generic filters to the DataTable object resulting from the API Call.
143     * Disable this feature by setting the parameter disable_generic_filters to 1 in the API call request.
144     *
145     * @param DataTable $datatable
146     * @return bool
147     */
148    protected function applyGenericFilters($datatable)
149    {
150        if ($datatable instanceof DataTable\Map) {
151            $tables = $datatable->getDataTables();
152            foreach ($tables as $table) {
153                $this->applyGenericFilters($table);
154            }
155            return;
156        }
157
158        $tableDisabledFilters = $datatable->getMetadata(DataTable::GENERIC_FILTERS_TO_DISABLE_METADATA_NAME) ?: [];
159        $genericFilters = $this->getGenericFiltersHavingDefaultValues();
160
161        $filterApplied = false;
162        foreach ($genericFilters as $filterMeta) {
163            $filterName = $filterMeta[0];
164            $filterParams = $filterMeta[1];
165            $filterParameters = array();
166            $exceptionRaised = false;
167
168            if (in_array($filterName, $this->disabledFilters)
169                || in_array($filterName, $tableDisabledFilters)
170            ) {
171                continue;
172            }
173
174            foreach ($filterParams as $name => $info) {
175                if (!is_array($info)) {
176                    // hard coded value that cannot be changed via API, see eg $naturalSort = true in 'Sort'
177                    $filterParameters[] = $info;
178                } else {
179                    // parameter type to cast to
180                    $type = $info[0];
181
182                    // default value if specified, when the parameter doesn't have a value
183                    $defaultValue = null;
184                    if (isset($info[1])) {
185                        $defaultValue = $info[1];
186                    }
187
188                    try {
189                        $value = Common::getRequestVar($name, $defaultValue, $type, $this->request);
190                        settype($value, $type);
191                        $filterParameters[] = $value;
192                    } catch (Exception $e) {
193                        $exceptionRaised = true;
194                        break;
195                    }
196                }
197            }
198
199            if (!$exceptionRaised) {
200                $datatable->filter($filterName, $filterParameters);
201                $filterApplied = true;
202            }
203        }
204
205        return $filterApplied;
206    }
207
208    public function areProcessedMetricsNeededFor($metrics)
209    {
210        $columnQueryParameters = array(
211            'filter_column',
212            'filter_column_recursive',
213            'filter_excludelowpop',
214            'filter_sort_column'
215        );
216
217        foreach ($columnQueryParameters as $queryParamName) {
218            $queryParamValue = Common::getRequestVar($queryParamName, false, $type = null, $this->request);
219            if (!empty($queryParamValue)
220                && $this->containsProcessedMetric($metrics, $queryParamValue)
221            ) {
222                return true;
223            }
224        }
225
226        return false;
227    }
228
229    /**
230     * @param ProcessedMetric[] $metrics
231     * @param string $name
232     * @return bool
233     */
234    private function containsProcessedMetric($metrics, $name)
235    {
236        foreach ($metrics as $metric) {
237            if ($metric instanceof ProcessedMetric
238                && $metric->getName() == $name
239            ) {
240                return true;
241            }
242        }
243        return false;
244    }
245}
246