PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Plugin / ViewDataTable.php
matomo / app / core / Plugin Last commit date
ConsoleCommand 2 years ago Dimension 1 year ago API.php 1 year ago AggregatedMetric.php 2 years ago ArchivedMetric.php 1 year ago Archiver.php 1 year ago Categories.php 2 years ago ComponentFactory.php 2 years ago ComputedMetric.php 1 year ago ConsoleCommand.php 1 year ago Controller.php 1 year ago ControllerAdmin.php 1 year ago Dependency.php 1 year ago LogTablesProvider.php 2 years ago Manager.php 1 year ago Menu.php 1 year ago MetadataLoader.php 1 year ago Metric.php 1 year ago PluginException.php 1 year ago ProcessedMetric.php 1 year ago ReleaseChannels.php 2 years ago Report.php 1 year ago ReportsProvider.php 2 years ago RequestProcessors.php 2 years ago Segment.php 1 year ago SettingsProvider.php 2 years ago Tasks.php 1 year ago ThemeStyles.php 2 years ago ViewDataTable.php 1 year ago Visualization.php 1 year ago WidgetsProvider.php 2 years ago
ViewDataTable.php
568 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Plugin;
10
11 use Piwik\API\Request;
12 use Piwik\API\Request as ApiRequest;
13 use Piwik\Common;
14 use Piwik\DataTable;
15 use Piwik\Period;
16 use Piwik\Piwik;
17 use Piwik\Plugins\API\Filter\DataComparisonFilter;
18 use Piwik\View\ViewInterface;
19 use Piwik\ViewDataTable\Config as VizConfig;
20 use Piwik\ViewDataTable\Manager as ViewDataTableManager;
21 use Piwik\ViewDataTable\Request as ViewDataTableRequest;
22 use Piwik\ViewDataTable\RequestConfig as VizRequest;
23 /**
24 * The base class of all report visualizations.
25 *
26 * ViewDataTable instances load analytics data via Piwik's Reporting API and then output some
27 * type of visualization of that data.
28 *
29 * Visualizations can be in any format. HTML-based visualizations should extend
30 * {@link Visualization}. Visualizations that use other formats, such as visualizations
31 * that output an image, should extend ViewDataTable directly.
32 *
33 * ### Creating ViewDataTables
34 *
35 * ViewDataTable instances are not created via the new operator, instead the {@link Piwik\ViewDataTable\Factory}
36 * class is used.
37 *
38 * The specific subclass to create is determined, first, by the **viewDataTable** query parameter.
39 * If this parameter is not set, then the default visualization type for the report being
40 * displayed is used.
41 *
42 * ### Configuring ViewDataTables
43 *
44 * **Display properties**
45 *
46 * ViewDataTable output can be customized by setting one of many available display
47 * properties. Display properties are stored as fields in {@link Piwik\ViewDataTable\Config} objects.
48 * ViewDataTables store a {@link Piwik\ViewDataTable\Config} object in the {@link $config} field.
49 *
50 * Display properties can be set at any time before rendering.
51 *
52 * **Request properties**
53 *
54 * Request properties are similar to display properties in the way they are set. They are,
55 * however, not used to customize ViewDataTable instances, but in the request to Piwik's
56 * API when loading analytics data.
57 *
58 * Request properties are set by setting the fields of a {@link Piwik\ViewDataTable\RequestConfig} object stored in
59 * the {@link $requestConfig} field. They can be set at any time before rendering.
60 * Setting them after data is loaded will have no effect.
61 *
62 * **Customizing how reports are displayed**
63 *
64 * Each individual report should be rendered in its own controller method. There are two
65 * ways to render a report within its controller method. You can either:
66 *
67 * 1. manually create and configure a ViewDataTable instance
68 * 2. invoke {@link Piwik\Plugin\Controller::renderReport} and configure the ViewDataTable instance
69 * in the {@hook ViewDataTable.configure} event.
70 *
71 * ViewDataTable instances are configured by setting and modifying display properties and request
72 * properties.
73 *
74 * ### Creating new visualizations
75 *
76 * New visualizations can be created by extending the ViewDataTable class or one of its
77 * descendants. To learn more [read our guide on creating new visualizations](/guides/visualizing-report-data#creating-new-visualizations).
78 *
79 * ### Examples
80 *
81 * **Manually configuring a ViewDataTable**
82 *
83 * // a controller method that displays a single report
84 * public function myReport()
85 * {
86 * $view = \Piwik\ViewDataTable\Factory::build('table', 'MyPlugin.myReport');
87 * $view->config->show_limit_control = true;
88 * $view->config->translations['myFancyMetric'] = "My Fancy Metric";
89 * // ...
90 * return $view->render();
91 * }
92 *
93 * **Using {@link Piwik\Plugin\Controller::renderReport}**
94 *
95 * First, a controller method that displays a single report:
96 *
97 * public function myReport()
98 * {
99 * return $this->renderReport(__FUNCTION__);`
100 * }
101 *
102 * Then the event handler for the {@hook ViewDataTable.configure} event:
103 *
104 * public function configureViewDataTable(ViewDataTable $view)
105 * {
106 * switch ($view->requestConfig->apiMethodToRequestDataTable) {
107 * case 'MyPlugin.myReport':
108 * $view->config->show_limit_control = true;
109 * $view->config->translations['myFancyMetric'] = "My Fancy Metric";
110 * // ...
111 * break;
112 * }
113 * }
114 *
115 * **Using custom configuration objects in a new visualization**
116 *
117 * class MyVisualizationConfig extends Piwik\ViewDataTable\Config
118 * {
119 * public $my_new_property = true;
120 * }
121 *
122 * class MyVisualizationRequestConfig extends Piwik\ViewDataTable\RequestConfig
123 * {
124 * public $my_new_property = false;
125 * }
126 *
127 * class MyVisualization extends Piwik\Plugin\ViewDataTable
128 * {
129 * public static function getDefaultConfig()
130 * {
131 * return new MyVisualizationConfig();
132 * }
133 *
134 * public static function getDefaultRequestConfig()
135 * {
136 * return new MyVisualizationRequestConfig();
137 * }
138 * }
139 *
140 *
141 * @api
142 */
143 abstract class ViewDataTable implements ViewInterface
144 {
145 public const ID = '';
146 /**
147 * DataTable loaded from the API for this ViewDataTable.
148 *
149 * @var DataTable
150 */
151 protected $dataTable = null;
152 /**
153 * Contains display properties for this visualization.
154 *
155 * @var \Piwik\ViewDataTable\Config
156 */
157 public $config;
158 /**
159 * Contains request properties for this visualization.
160 *
161 * @var \Piwik\ViewDataTable\RequestConfig
162 */
163 public $requestConfig;
164 /**
165 * @var ViewDataTableRequest
166 */
167 protected $request;
168 private $isComparing = null;
169 /**
170 * Constructor. Initializes display and request properties to their default values.
171 * Posts the {@hook ViewDataTable.configure} event which plugins can use to configure the
172 * way reports are displayed.
173 */
174 public function __construct($controllerAction, $apiMethodToRequestDataTable, $overrideParams = array())
175 {
176 if (strpos($controllerAction, '.') === \false) {
177 $controllerName = '';
178 $controllerAction = '';
179 } else {
180 list($controllerName, $controllerAction) = explode('.', $controllerAction);
181 }
182 $this->requestConfig = static::getDefaultRequestConfig();
183 $this->config = static::getDefaultConfig();
184 $this->config->subtable_controller_action = $controllerAction;
185 $this->config->setController($controllerName, $controllerAction);
186 $this->request = new ViewDataTableRequest($this->requestConfig);
187 $this->requestConfig->idSubtable = Common::getRequestVar('idSubtable', \false, 'int');
188 $this->config->self_url = Request::getBaseReportUrl($controllerName, $controllerAction);
189 $this->requestConfig->apiMethodToRequestDataTable = $apiMethodToRequestDataTable;
190 $report = \Piwik\Plugin\ReportsProvider::factory($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest());
191 if (!empty($report)) {
192 /** @var Report $report */
193 $subtable = $report->getActionToLoadSubTables();
194 if (!empty($subtable)) {
195 $this->config->subtable_controller_action = $subtable;
196 }
197 $this->config->show_goals = $report->hasGoalMetrics();
198 $relatedReports = $report->getRelatedReports();
199 if (!empty($relatedReports)) {
200 foreach ($relatedReports as $relatedReport) {
201 if (!$relatedReport) {
202 continue;
203 }
204 $relatedReportName = $relatedReport->getName();
205 $this->config->addRelatedReport($relatedReport->getModule() . '.' . $relatedReport->getAction(), $relatedReportName);
206 }
207 }
208 $metrics = $report->getMetrics();
209 if (!empty($metrics)) {
210 $this->config->addTranslations($metrics);
211 }
212 $processedMetrics = $report->getProcessedMetrics();
213 if (!empty($processedMetrics)) {
214 $this->config->addTranslations($processedMetrics);
215 }
216 $dimension = $report->getDimension();
217 if (!empty($dimension)) {
218 $this->config->addTranslations(['label' => $dimension->getName()]);
219 }
220 $this->config->title = $report->getName();
221 $report->configureView($this);
222 }
223 /**
224 * Triggered during {@link ViewDataTable} construction. Subscribers should customize
225 * the view based on the report that is being displayed.
226 *
227 * This event is triggered before view configuration properties are overwritten by saved settings or request
228 * parameters. Use this to define default values.
229 *
230 * Plugins that define their own reports must subscribe to this event in order to
231 * specify how the Piwik UI should display the report.
232 *
233 * **Example**
234 *
235 * // event handler
236 * public function configureViewDataTable(ViewDataTable $view)
237 * {
238 * switch ($view->requestConfig->apiMethodToRequestDataTable) {
239 * case 'VisitTime.getVisitInformationPerServerTime':
240 * $view->config->enable_sort = true;
241 * $view->requestConfig->filter_limit = 10;
242 * break;
243 * }
244 * }
245 *
246 * @param ViewDataTable $view The instance to configure.
247 */
248 Piwik::postEvent('ViewDataTable.configure', array($this));
249 $this->assignRelatedReportsTitle();
250 $this->config->show_footer_icons = \false == $this->requestConfig->idSubtable;
251 // the exclude low population threshold value is sometimes obtained by requesting data.
252 // to avoid issuing unnecessary requests when display properties are determined by metadata,
253 // we allow it to be a closure.
254 if (isset($this->requestConfig->filter_excludelowpop_value) && $this->requestConfig->filter_excludelowpop_value instanceof \Closure) {
255 $function = $this->requestConfig->filter_excludelowpop_value;
256 $this->requestConfig->filter_excludelowpop_value = $function();
257 }
258 $this->overrideViewPropertiesWithParams($overrideParams);
259 $this->overrideViewPropertiesWithQueryParams();
260 /**
261 * Triggered after {@link ViewDataTable} construction. Subscribers should customize
262 * the view based on the report that is being displayed.
263 *
264 * This event is triggered after all view configuration values have been overwritten by saved settings or
265 * request parameters. Use this if you need to work with the final configuration values.
266 *
267 * Plugins that define their own reports can subscribe to this event in order to
268 * specify how the Piwik UI should display the report.
269 *
270 * **Example**
271 *
272 * // event handler
273 * public function configureViewDataTableEnd(ViewDataTable $view)
274 * {
275 * if ($view->requestConfig->apiMethodToRequestDataTable == 'VisitTime.getVisitInformationPerServerTime'
276 * && $view->requestConfig->flat == 1) {
277 * $view->config->show_header_message = 'You are viewing this report flattened';
278 * }
279 * }
280 *
281 * @param ViewDataTable $view The instance to configure.
282 */
283 Piwik::postEvent('ViewDataTable.configure.end', array($this));
284 }
285 private function assignRelatedReportsTitle()
286 {
287 if (!empty($this->config->related_reports_title)) {
288 // title already assigned by a plugin
289 return;
290 }
291 if (count($this->config->related_reports) == 1) {
292 $this->config->related_reports_title = Piwik::translate('General_RelatedReport') . ':';
293 } else {
294 $this->config->related_reports_title = Piwik::translate('General_RelatedReports') . ':';
295 }
296 }
297 /**
298 * Returns the default config instance.
299 *
300 * Visualizations that define their own display properties should override this method and
301 * return an instance of their new {@link Piwik\ViewDataTable\Config} descendant.
302 *
303 * See the last example {@link ViewDataTable here} for more information.
304 *
305 * @return \Piwik\ViewDataTable\Config
306 */
307 public static function getDefaultConfig()
308 {
309 return new VizConfig();
310 }
311 /**
312 * Returns the default request config instance.
313 *
314 * Visualizations that define their own request properties should override this method and
315 * return an instance of their new {@link Piwik\ViewDataTable\RequestConfig} descendant.
316 *
317 * See the last example {@link ViewDataTable here} for more information.
318 *
319 * @return \Piwik\ViewDataTable\RequestConfig
320 */
321 public static function getDefaultRequestConfig()
322 {
323 return new VizRequest();
324 }
325 protected function loadDataTableFromAPI()
326 {
327 if (!is_null($this->dataTable)) {
328 // data table is already there
329 // this happens when setDataTable has been used
330 return $this->dataTable;
331 }
332 $extraParams = [];
333 if ($this->isComparing()) {
334 $extraParams['compare'] = '1';
335 }
336 $this->dataTable = $this->request->loadDataTableFromAPI($extraParams);
337 return $this->dataTable;
338 }
339 /**
340 * Returns the viewDataTable ID for this DataTable visualization.
341 *
342 * Derived classes should not override this method. They should instead declare a const ID field
343 * with the viewDataTable ID.
344 *
345 * @throws \Exception
346 * @return string
347 */
348 public static function getViewDataTableId()
349 {
350 $id = static::ID;
351 if (empty($id)) {
352 $message = sprintf('ViewDataTable %s does not define an ID. Set the ID constant to fix this issue', get_called_class());
353 throw new \Exception($message);
354 }
355 return $id;
356 }
357 /**
358 * Returns `true` if this instance's or any of its ancestors' viewDataTable IDs equals the supplied ID,
359 * `false` if otherwise.
360 *
361 * Can be used to test whether a ViewDataTable object is an instance of a certain visualization or not,
362 * without having to know where that visualization is.
363 *
364 * @param string $viewDataTableId The viewDataTable ID to check for, eg, `'table'`.
365 * @return bool
366 */
367 public function isViewDataTableId($viewDataTableId)
368 {
369 $myIds = ViewDataTableManager::getIdsWithInheritance(get_called_class());
370 return in_array($viewDataTableId, $myIds);
371 }
372 /**
373 * Returns the DataTable loaded from the API.
374 *
375 * @return DataTable
376 * @throws \Exception if not yet loaded.
377 */
378 public function getDataTable()
379 {
380 if (is_null($this->dataTable)) {
381 throw new \Exception("The DataTable object has not yet been created");
382 }
383 return $this->dataTable;
384 }
385 /**
386 * To prevent calling an API multiple times, the DataTable can be set directly.
387 * It won't be loaded from the API in this case.
388 *
389 * @param DataTable $dataTable The DataTable to use.
390 * @return void
391 */
392 public function setDataTable($dataTable)
393 {
394 $this->dataTable = $dataTable;
395 }
396 /**
397 * Checks that the API returned a normal DataTable (as opposed to DataTable\Map)
398 * @throws \Exception
399 * @return void
400 */
401 protected function checkStandardDataTable()
402 {
403 Piwik::checkObjectTypeIs($this->dataTable, array('\\Piwik\\DataTable'));
404 }
405 /**
406 * Requests all needed data and renders the view.
407 *
408 * @return string The result of rendering.
409 */
410 public function render()
411 {
412 return '';
413 }
414 protected function getDefaultDataTableCssClass()
415 {
416 return 'dataTableViz' . Piwik::getUnnamespacedClassName(get_class($this));
417 }
418 /**
419 * Returns the list of view properties that can be overridden by query parameters.
420 *
421 * @return array
422 */
423 protected function getOverridableProperties()
424 {
425 return array_merge($this->config->overridableProperties, $this->requestConfig->overridableProperties);
426 }
427 private function overrideViewPropertiesWithQueryParams()
428 {
429 $properties = $this->getOverridableProperties();
430 foreach ($properties as $name) {
431 if (property_exists($this->requestConfig, $name)) {
432 $this->requestConfig->{$name} = $this->getPropertyFromQueryParam($name, $this->requestConfig->{$name});
433 } elseif (property_exists($this->config, $name)) {
434 $this->config->{$name} = $this->getPropertyFromQueryParam($name, $this->config->{$name});
435 }
436 }
437 // handle special 'columns' query parameter
438 $columns = Common::getRequestVar('columns', \false);
439 if (\false !== $columns) {
440 $this->config->columns_to_display = Piwik::getArrayFromApiParameter($columns);
441 array_unshift($this->config->columns_to_display, 'label');
442 }
443 }
444 protected function getPropertyFromQueryParam($name, $defaultValue)
445 {
446 $type = is_numeric($defaultValue) ? 'int' : null;
447 $value = Common::getRequestVar($name, $defaultValue, $type);
448 // convert comma separated values to arrays if needed
449 if (is_array($defaultValue)) {
450 $value = Piwik::getArrayFromApiParameter($value);
451 }
452 return $value;
453 }
454 /**
455 * Returns `true` if this instance will request a single DataTable, `false` if requesting
456 * more than one.
457 *
458 * @return bool
459 */
460 public function isRequestingSingleDataTable()
461 {
462 $requestArray = $this->request->getRequestArray() + $_GET + $_POST;
463 $date = Common::getRequestVar('date', null, 'string', $requestArray);
464 $period = Common::getRequestVar('period', null, 'string', $requestArray);
465 $idSite = Common::getRequestVar('idSite', null, 'string', $requestArray);
466 if (Period::isMultiplePeriod($date, $period) || strpos($idSite, ',') !== \false || $idSite == 'all') {
467 return \false;
468 }
469 return \true;
470 }
471 /**
472 * Returns `true` if this visualization can display some type of data or not.
473 *
474 * New visualization classes should override this method if they can only visualize certain
475 * types of data. The evolution graph visualization, for example, can only visualize
476 * sets of DataTables. If the API method used results in a single DataTable, the evolution
477 * graph footer icon should not be displayed.
478 *
479 * @param ViewDataTable $view Contains the API request being checked.
480 * @return bool
481 */
482 public static function canDisplayViewDataTable(\Piwik\Plugin\ViewDataTable $view)
483 {
484 return $view->config->show_all_views_icons;
485 }
486 private function overrideViewPropertiesWithParams($overrideParams)
487 {
488 if (empty($overrideParams)) {
489 return;
490 }
491 foreach ($overrideParams as $key => $value) {
492 if (property_exists($this->requestConfig, $key)) {
493 $this->requestConfig->{$key} = $value;
494 } elseif (property_exists($this->config, $key)) {
495 $this->config->{$key} = $value;
496 } elseif ($key != 'enable_filter_excludelowpop') {
497 $this->config->custom_parameters[$key] = $value;
498 }
499 }
500 }
501 /**
502 * Display a meaningful error message when any invalid parameter is being set.
503 *
504 * @param $overrideParams
505 * @throws
506 */
507 public function throwWhenSettingNonOverridableParameter($overrideParams)
508 {
509 $nonOverridableParams = $this->getNonOverridableParams($overrideParams);
510 if (count($nonOverridableParams) > 0) {
511 throw new \Exception(sprintf("Setting parameters %s is not allowed. Please report this bug to the Matomo team.", implode(" and ", $nonOverridableParams)));
512 }
513 }
514 /**
515 * @param $overrideParams
516 * @return array
517 */
518 public function getNonOverridableParams($overrideParams)
519 {
520 $paramsCannotBeOverridden = array();
521 foreach ($overrideParams as $paramName => $paramValue) {
522 if (property_exists($this->requestConfig, $paramName)) {
523 $allowedParams = $this->requestConfig->overridableProperties;
524 } elseif (property_exists($this->config, $paramName)) {
525 $allowedParams = $this->config->overridableProperties;
526 } else {
527 // setting Config.custom_parameters is always allowed
528 continue;
529 }
530 if (!in_array($paramName, $allowedParams)) {
531 $paramsCannotBeOverridden[] = $paramName;
532 }
533 }
534 return $paramsCannotBeOverridden;
535 }
536 /**
537 * Returns true if both this current visualization supports comparison, and if comparison query parameters
538 * are present in the URL.
539 *
540 * @return bool
541 */
542 public function isComparing()
543 {
544 if (!$this->supportsComparison() || $this->config->disable_comparison) {
545 return \false;
546 }
547 $request = $this->request->getRequestArray();
548 $request = ApiRequest::getRequestArrayFromString($request);
549 $result = DataComparisonFilter::isCompareParamsPresent($request);
550 return $result;
551 }
552 /**
553 * Implementations should override this method if they support a special comparison view. By
554 * default, it is assumed visualizations do not support comparison.
555 *
556 * @return bool
557 */
558 public function supportsComparison()
559 {
560 return \false;
561 }
562 public function getRequestArray()
563 {
564 $requestArray = $this->request->getRequestArray();
565 return ApiRequest::getRequestArrayFromString($requestArray);
566 }
567 }
568