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