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