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\Proxy; 12use Piwik\API\Request; 13use Piwik\Columns\Dimension; 14use Piwik\Common; 15use Piwik\DataTable; 16use Piwik\DataTable\Filter\Sort; 17use Piwik\Metrics; 18use Piwik\Piwik; 19use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable; 20use Piwik\ViewDataTable\Factory as ViewDataTableFactory; 21use Exception; 22use Piwik\Widget\WidgetsList; 23use Piwik\Report\ReportWidgetFactory; 24 25/** 26 * Defines a new report. This class contains all information a report defines except the corresponding API method which 27 * needs to be defined in the 'API.php'. You can define the name of the report, a documentation, the supported metrics, 28 * how the report should be displayed, which features the report has (eg search) and much more. 29 * 30 * You can create a new report using the console command `./console generate:report`. The generated report will guide 31 * you through the creation of a report. 32 * 33 * @since 2.5.0 34 * @api 35 */ 36class Report 37{ 38 /** 39 * The sub-namespace name in a plugin where Report components are stored. 40 */ 41 const COMPONENT_SUBNAMESPACE = 'Reports'; 42 43 /** 44 * When added to the menu, a given report eg 'getCampaigns' 45 * will be routed as &action=menuGetCampaigns 46 */ 47 const PREFIX_ACTION_IN_MENU = 'menu'; 48 49 /** 50 * The name of the module which is supposed to be equal to the name of the plugin. The module is detected 51 * automatically. 52 * @var string 53 */ 54 protected $module; 55 56 /** 57 * The name of the action. The action is detected automatically depending on the file name. A corresponding action 58 * should exist in the API as well. 59 * @var string 60 */ 61 protected $action; 62 63 /** 64 * The translated name of the report. The name will be used for instance in the mobile app or if another report 65 * defines this report as a related report. 66 * @var string 67 * @api 68 */ 69 protected $name; 70 71 /** 72 * A translated documentation which explains the report. 73 * @var string 74 */ 75 protected $documentation; 76 77 /** 78 * URL linking to an online guide for this report or plugin. 79 * @var string 80 */ 81 protected $onlineGuideUrl; 82 83 /** 84 * The translation key of the category the report belongs to. 85 * @var string 86 * @api 87 */ 88 protected $categoryId; 89 90 /** 91 * The translation key of the subcategory the report belongs to. 92 * @var string 93 * @api 94 */ 95 protected $subcategoryId; 96 97 /** 98 * An array of supported metrics. Eg `array('nb_visits', 'nb_actions', ...)`. Defaults to the platform default 99 * metrics see {@link Metrics::getDefaultProcessedMetrics()}. 100 * @var array 101 * @api 102 */ 103 protected $metrics = array('nb_visits', 'nb_uniq_visitors', 'nb_actions', 'nb_users'); 104 // for a little performance improvement we avoid having to call Metrics::getDefaultMetrics for each report 105 106 /** 107 * The processed metrics this report supports, eg `avg_time_on_site` or `nb_actions_per_visit`. Defaults to the 108 * platform default processed metrics, see {@link Metrics::getDefaultProcessedMetrics()}. Set it to boolean `false` 109 * if your report does not support any processed metrics at all. Otherwise an array of metric names. 110 * Eg `array('avg_time_on_site', 'nb_actions_per_visit', ...)` 111 * @var array 112 * @api 113 */ 114 protected $processedMetrics = array('nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate', 'conversion_rate'); 115 // for a little performance improvement we avoid having to call Metrics::getDefaultProcessedMetrics for each report 116 117 /** 118 * Set this property to true in case your report supports goal metrics. In this case, the goal metrics will be 119 * automatically added to the report metadata and the report will be displayed in the Goals UI. 120 * @var bool 121 * @api 122 */ 123 protected $hasGoalMetrics = false; 124 125 /** 126 * Set this property to false in case your report can't/shouldn't be flattened. 127 * In this case, flattener won't be applied even if parameter is provided in a request 128 * @var bool 129 * @api 130 */ 131 protected $supportsFlatten = true; 132 133 /** 134 * Set it to boolean `true` if your report always returns a constant count of rows, for instance always 24 rows 135 * for 1-24 hours. 136 * @var bool 137 * @api 138 */ 139 protected $constantRowsCount = false; 140 141 /** 142 * Set it to boolean `true` if this report is a subtable report and won't be used as a standalone report. 143 * @var bool 144 * @api 145 */ 146 protected $isSubtableReport = false; 147 148 /** 149 * Some reports may require additional URL parameters that need to be sent when a report is requested. For instance 150 * a "goal" report might need a "goalId": `array('idgoal' => 5)`. 151 * @var null|array 152 * @api 153 */ 154 protected $parameters = null; 155 156 /** 157 * An instance of a dimension if the report has one. You can create a new dimension using the Piwik console CLI tool 158 * if needed. 159 * @var \Piwik\Columns\Dimension 160 */ 161 protected $dimension; 162 163 /** 164 * The name of the API action to load a subtable if supported. The action has to be of the same module. For instance 165 * a report "getKeywords" might support a subtable "getSearchEngines" which shows how often a keyword was searched 166 * via a specific search engine. 167 * @var string 168 * @api 169 */ 170 protected $actionToLoadSubTables = ''; 171 172 /** 173 * The order of the report. Depending on the order the report gets a different position in the list of widgets, 174 * the menu and the mobile app. 175 * @var int 176 * @api 177 */ 178 protected $order = 1; 179 180 /** 181 * Separator for building recursive labels (or paths) 182 * @var string 183 * @api 184 */ 185 protected $recursiveLabelSeparator = ' - '; 186 187 /** 188 * Default sort column. Either a column name or a column id. 189 * 190 * @var string|int 191 */ 192 protected $defaultSortColumn = 'nb_visits'; 193 194 /** 195 * Default sort desc. If true will sort by default desc, if false will sort by default asc 196 * 197 * @var bool 198 */ 199 protected $defaultSortOrderDesc = true; 200 201 /** 202 * The constructur initializes the module, action and the default metrics. If you want to overwrite any of those 203 * values or if you want to do any work during initializing overwrite the method {@link init()}. 204 * @ignore 205 */ 206 final public function __construct() 207 { 208 $classname = get_class($this); 209 $parts = explode('\\', $classname); 210 211 if (5 === count($parts)) { 212 $this->module = $parts[2]; 213 $this->action = lcfirst($parts[4]); 214 } 215 216 $this->init(); 217 } 218 219 /** 220 * Here you can do any instance initialization and overwrite any default values. You should avoid doing time 221 * consuming initialization here and if possible delay as long as possible. An instance of this report will be 222 * created in most page requests. 223 * @api 224 */ 225 protected function init() 226 { 227 } 228 229 /** 230 * Defines whether a report is enabled or not. For instance some reports might not be available to every user or 231 * might depend on a setting (such as Ecommerce) of a site. In such a case you can perform any checks and then 232 * return `true` or `false`. If your report is only available to users having super user access you can do the 233 * following: `return Piwik::hasUserSuperUserAccess();` 234 * @return bool 235 * @api 236 */ 237 public function isEnabled() 238 { 239 return true; 240 } 241 242 /** 243 * This method checks whether the report is available, see {@isEnabled()}. If not, it triggers an exception 244 * containing a message that will be displayed to the user. You can overwrite this message in case you want to 245 * customize the error message. Eg. 246 * ``` 247 * if (!$this->isEnabled()) { 248 * throw new Exception('Setting XYZ is not enabled or the user has not enough permission'); 249 * } 250 * ``` 251 * @throws \Exception 252 * @api 253 */ 254 public function checkIsEnabled() 255 { 256 if (!$this->isEnabled()) { 257 throw new Exception(Piwik::translate('General_ExceptionReportNotEnabled')); 258 } 259 } 260 261 /** 262 * Returns the id of the default visualization for this report. Eg 'table' or 'pie'. Defaults to the HTML table. 263 * @return string 264 * @api 265 */ 266 public function getDefaultTypeViewDataTable() 267 { 268 return HtmlTable::ID; 269 } 270 271 /** 272 * Returns if the default viewDataTable type should always be used. e.g. the type won't be changeable through config or url params. 273 * Defaults to false 274 * @return bool 275 */ 276 public function alwaysUseDefaultViewDataTable() 277 { 278 return false; 279 } 280 281 /** 282 * Here you can configure how your report should be displayed and which capabilities your report has. For instance 283 * whether your report supports a "search" or not. EG `$view->config->show_search = false`. You can also change the 284 * default request config. For instance you can change how many rows are displayed by default: 285 * `$view->requestConfig->filter_limit = 10;`. See {@link ViewDataTable} for more information. 286 * @param ViewDataTable $view 287 * @api 288 */ 289 public function configureView(ViewDataTable $view) 290 { 291 } 292 293 /** 294 * Renders a report depending on the configured ViewDataTable see {@link configureView()} and 295 * {@link getDefaultTypeViewDataTable()}. If you want to customize the render process or just render any custom view 296 * you can overwrite this method. 297 * 298 * @return string 299 * @throws \Exception In case the given API action does not exist yet. 300 * @api 301 */ 302 public function render() 303 { 304 $viewDataTable = Common::getRequestVar('viewDataTable', false, 'string'); 305 $fixed = Common::getRequestVar('forceView', 0, 'int'); 306 307 $module = $this->getModule(); 308 $action = $this->getAction(); 309 310 $apiProxy = Proxy::getInstance(); 311 312 if (!$apiProxy->isExistingApiAction($module, $action)) { 313 throw new Exception("Invalid action name '$action' for '$module' plugin."); 314 } 315 316 $apiAction = $apiProxy->buildApiActionName($module, $action); 317 318 $view = ViewDataTableFactory::build($viewDataTable, $apiAction, $module . '.' . $action, $fixed); 319 320 return $view->render(); 321 } 322 323 /** 324 * 325 * Processing a uniqueId for each report, can be used by UIs as a key to match a given report 326 * @return string 327 */ 328 public function getId() 329 { 330 $params = $this->getParameters(); 331 332 $paramsKey = $this->getModule() . '.' . $this->getAction(); 333 334 if (!empty($params)) { 335 foreach ($params as $key => $value) { 336 $paramsKey .= '_' . $key . '--' . $value; 337 } 338 } 339 340 return $paramsKey; 341 } 342 343 /** 344 * lets you add any amount of widgets for this report. If a report defines a {@link $categoryId} and a 345 * {@link $subcategoryId} a widget will be generated automatically. 346 * 347 * Example to add a widget manually by overwriting this method in your report: 348 * $widgetsList->addWidgetConfig($factory->createWidget()); 349 * 350 * If you want to have the name and the order of the widget differently to the name and order of the report you can 351 * do the following: 352 * $widgetsList->addWidgetConfig($factory->createWidget()->setName('Custom')->setOrder(5)); 353 * 354 * If you want to add a widget to any container defined by your plugin or by another plugin you can do 355 * this: 356 * $widgetsList->addToContainerWidget($containerId = 'Products', $factory->createWidget()); 357 * 358 * @param WidgetsList $widgetsList 359 * @param ReportWidgetFactory $factory 360 * @api 361 */ 362 public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) 363 { 364 if ($this->categoryId && $this->subcategoryId) { 365 $widgetsList->addWidgetConfig($factory->createWidget()); 366 } 367 } 368 369 /** 370 * @ignore 371 * @see $recursiveLabelSeparator 372 */ 373 public function getRecursiveLabelSeparator() 374 { 375 return $this->recursiveLabelSeparator; 376 } 377 378 /** 379 * Returns an array of supported metrics and their corresponding translations. Eg `array('nb_visits' => 'Visits')`. 380 * By default the given {@link $metrics} are used and their corresponding translations are looked up automatically. 381 * If a metric is not translated, you should add the default metric translation for this metric using 382 * the {@hook Metrics.getDefaultMetricTranslations} event. If you want to overwrite any default metric translation 383 * you should overwrite this method, call this parent method to get all default translations and overwrite any 384 * custom metric translations. 385 * @return array 386 * @api 387 */ 388 public function getMetrics() 389 { 390 return $this->getMetricTranslations($this->metrics); 391 } 392 393 /** 394 * Returns the list of metrics required at minimum for a report factoring in the columns requested by 395 * the report requester. 396 * 397 * This will return all the metrics requested (or all the metrics in the report if nothing is requested) 398 * **plus** the metrics required to calculate the requested processed metrics. 399 * 400 * This method should be used in **Plugin.get** API methods. 401 * 402 * @param string[]|null $allMetrics The list of all available unprocessed metrics. Defaults to this report's 403 * metrics. 404 * @param string[]|null $restrictToColumns The requested columns. 405 * @return string[] 406 */ 407 public function getMetricsRequiredForReport($allMetrics = null, $restrictToColumns = null) 408 { 409 if (empty($allMetrics)) { 410 $allMetrics = $this->metrics; 411 } 412 413 if (empty($restrictToColumns)) { 414 $restrictToColumns = array_merge($allMetrics, array_keys($this->getProcessedMetrics())); 415 } 416 $restrictToColumns = array_unique($restrictToColumns); 417 418 $processedMetricsById = $this->getProcessedMetricsById(); 419 $metricsSet = array_flip($allMetrics); 420 421 $metrics = array(); 422 foreach ($restrictToColumns as $column) { 423 if (isset($processedMetricsById[$column])) { 424 $metrics = array_merge($metrics, $processedMetricsById[$column]->getDependentMetrics()); 425 } elseif (isset($metricsSet[$column])) { 426 $metrics[] = $column; 427 } 428 } 429 return array_unique($metrics); 430 } 431 432 /** 433 * Returns an array of supported processed metrics and their corresponding translations. Eg 434 * `array('nb_visits' => 'Visits')`. By default the given {@link $processedMetrics} are used and their 435 * corresponding translations are looked up automatically. If a metric is not translated, you should add the 436 * default metric translation for this metric using the {@hook Metrics.getDefaultMetricTranslations} event. If you 437 * want to overwrite any default metric translation you should overwrite this method, call this parent method to 438 * get all default translations and overwrite any custom metric translations. 439 * @return array|mixed 440 * @api 441 */ 442 public function getProcessedMetrics() 443 { 444 if (!is_array($this->processedMetrics)) { 445 return $this->processedMetrics; 446 } 447 448 return $this->getMetricTranslations($this->processedMetrics); 449 } 450 451 /** 452 * Returns the array of all metrics displayed by this report. 453 * 454 * @return array 455 * @api 456 */ 457 public function getAllMetrics() 458 { 459 $processedMetrics = $this->getProcessedMetrics() ?: array(); 460 return array_keys(array_merge($this->getMetrics(), $processedMetrics)); 461 } 462 463 /** 464 * Use this method to register metrics to process report totals. 465 * 466 * When a metric is registered, it will process the report total values and as a result show percentage values 467 * in the HTML Table reporting visualization. 468 * 469 * @return string[] metricId => metricColumn, if the report has only column names and no IDs, it should return 470 * metricColumn => metricColumn, eg array('13' => 'nb_pageviews') or array('mymetric' => 'mymetric') 471 */ 472 public function getMetricNamesToProcessReportTotals() 473 { 474 return array(); 475 } 476 477 /** 478 * Returns an array of metric documentations and their corresponding translations. Eg 479 * `array('nb_visits' => 'If a visitor comes to your website for the first time or if they visit a page more than 30 minutes after...')`. 480 * By default the given {@link $metrics} are used and their corresponding translations are looked up automatically. 481 * If there is a metric documentation not found, you should add the default metric documentation translation for 482 * this metric using the {@hook Metrics.getDefaultMetricDocumentationTranslations} event. If you want to overwrite 483 * any default metric translation you should overwrite this method, call this parent method to get all default 484 * translations and overwrite any custom metric translations. 485 * @return array 486 * @api 487 */ 488 protected function getMetricsDocumentation() 489 { 490 $translations = Metrics::getDefaultMetricsDocumentation(); 491 $documentation = array(); 492 493 foreach ($this->metrics as $metric) { 494 if (is_string($metric) && !empty($translations[$metric])) { 495 $documentation[$metric] = $translations[$metric]; 496 } elseif ($metric instanceof Metric) { 497 $name = $metric->getName(); 498 $metricDocs = $metric->getDocumentation(); 499 if (empty($metricDocs) && !empty($translations[$name])) { 500 $metricDocs = $translations[$name]; 501 } 502 503 if (!empty($metricDocs)) { 504 $documentation[$name] = $metricDocs; 505 } 506 } 507 } 508 509 $processedMetrics = $this->processedMetrics ?: array(); 510 foreach ($processedMetrics as $processedMetric) { 511 if (is_string($processedMetric) && !empty($translations[$processedMetric])) { 512 $documentation[$processedMetric] = $translations[$processedMetric]; 513 } elseif ($processedMetric instanceof Metric) { 514 $name = $processedMetric->getName(); 515 $metricDocs = $processedMetric->getDocumentation(); 516 if (empty($metricDocs) && !empty($translations[$name])) { 517 $metricDocs = $translations[$name]; 518 } 519 520 if (!empty($metricDocs)) { 521 $documentation[$name] = $metricDocs; 522 } 523 } 524 } 525 526 return $documentation; 527 } 528 529 /** 530 * @return bool 531 * @ignore 532 */ 533 public function hasGoalMetrics() 534 { 535 return $this->hasGoalMetrics; 536 } 537 538 /** 539 * @return bool 540 * @ignore 541 */ 542 public function supportsFlatten() 543 { 544 return $this->supportsFlatten; 545 } 546 547 /** 548 * If the report is enabled the report metadata for this report will be built and added to the list of available 549 * reports. Overwrite this method and leave it empty in case you do not want your report to be added to the report 550 * metadata. In this case your report won't be visible for instance in the mobile app and scheduled reports 551 * generator. We recommend to change this behavior only if you are familiar with the Piwik core. `$infos` contains 552 * the current requested date, period and site. 553 * @param $availableReports 554 * @param $infos 555 * @api 556 */ 557 public function configureReportMetadata(&$availableReports, $infos) 558 { 559 if (!$this->isEnabled()) { 560 return; 561 } 562 563 $report = $this->buildReportMetadata(); 564 565 if (!empty($report)) { 566 $availableReports[] = $report; 567 } 568 } 569 570 /** 571 * Get report documentation. 572 * @return string 573 */ 574 public function getDocumentation() 575 { 576 return $this->documentation; 577 } 578 579 /** 580 * Builts the report metadata for this report. Can be useful in case you want to change the behavior of 581 * {@link configureReportMetadata()}. 582 * @return array 583 * @ignore 584 * 585 * TODO we should move this out to API::getReportMetadata 586 */ 587 protected function buildReportMetadata() 588 { 589 $report = array( 590 'category' => $this->getCategoryId(), 591 'subcategory' => $this->getSubcategoryId(), 592 'name' => $this->getName(), 593 'module' => $this->getModule(), 594 'action' => $this->getAction() 595 ); 596 597 if (null !== $this->parameters) { 598 $report['parameters'] = $this->parameters; 599 } 600 601 if (!empty($this->dimension)) { 602 $report['dimension'] = $this->dimension->getName(); 603 } 604 605 if (!empty($this->documentation)) { 606 $report['documentation'] = $this->documentation; 607 } 608 609 if (!empty($this->onlineGuideUrl)) { 610 $report['onlineGuideUrl'] = $this->onlineGuideUrl; 611 } 612 613 if (true === $this->isSubtableReport) { 614 $report['isSubtableReport'] = $this->isSubtableReport; 615 } 616 617 $dimensions = $this->getDimensions(); 618 619 if (count($dimensions) > 1) { 620 $report['dimensions'] = $dimensions; 621 } 622 623 $report['metrics'] = $this->getMetrics(); 624 $report['metricsDocumentation'] = $this->getMetricsDocumentation(); 625 $report['processedMetrics'] = $this->getProcessedMetrics(); 626 627 if (!empty($this->actionToLoadSubTables)) { 628 $report['actionToLoadSubTables'] = $this->actionToLoadSubTables; 629 } 630 631 if (true === $this->constantRowsCount) { 632 $report['constantRowsCount'] = $this->constantRowsCount; 633 } 634 635 $relatedReports = $this->getRelatedReports(); 636 if (!empty($relatedReports)) { 637 $report['relatedReports'] = array(); 638 foreach ($relatedReports as $relatedReport) { 639 if (!empty($relatedReport)) { 640 $report['relatedReports'][] = array( 641 'name' => $relatedReport->getName(), 642 'module' => $relatedReport->getModule(), 643 'action' => $relatedReport->getAction() 644 ); 645 } 646 } 647 } 648 649 $report['order'] = $this->order; 650 651 return $report; 652 } 653 654 /** 655 * @ignore 656 */ 657 public function getDefaultSortColumn() 658 { 659 return $this->defaultSortColumn; 660 } 661 662 /** 663 * @ignore 664 */ 665 public function getDefaultSortOrder() 666 { 667 if ($this->defaultSortOrderDesc) { 668 return Sort::ORDER_DESC; 669 } 670 671 return Sort::ORDER_ASC; 672 } 673 674 /** 675 * Allows to define a callback that will be used to determine the secondary column to sort by 676 * 677 * ``` 678 * public function getSecondarySortColumnCallback() 679 * { 680 * return function ($primaryColumn) { 681 * switch ($primaryColumn) { 682 * case Metrics::NB_CLICKS: 683 * return Metrics::NB_IMPRESSIONS; 684 * case 'label': 685 * default: 686 * return Metrics::NB_CLICKS; 687 * } 688 * }; 689 * } 690 * ``` 691 * @return null|callable 692 */ 693 public function getSecondarySortColumnCallback() 694 { 695 return null; 696 } 697 698 /** 699 * Get the list of related reports if there are any. They will be displayed for instance below a report as a 700 * recommended related report. 701 * 702 * @return Report[] 703 * @api 704 */ 705 public function getRelatedReports() 706 { 707 return array(); 708 } 709 710 /** 711 * Get the name of the report 712 * @return string 713 * @ignore 714 */ 715 public function getName() 716 { 717 return $this->name; 718 } 719 720 /** 721 * Get the name of the module. 722 * @return string 723 * @ignore 724 */ 725 public function getModule() 726 { 727 return $this->module; 728 } 729 730 /** 731 * Get the name of the action. 732 * @return string 733 * @ignore 734 */ 735 public function getAction() 736 { 737 return $this->action; 738 } 739 740 public function getParameters() 741 { 742 return $this->parameters; 743 } 744 745 /** 746 * Get the translated name of the category the report belongs to. 747 * @return string 748 * @ignore 749 */ 750 public function getCategoryId() 751 { 752 return $this->categoryId; 753 } 754 755 /** 756 * Get the translated name of the subcategory the report belongs to. 757 * @return string 758 * @ignore 759 */ 760 public function getSubcategoryId() 761 { 762 return $this->subcategoryId; 763 } 764 765 /** 766 * @return \Piwik\Columns\Dimension 767 * @ignore 768 */ 769 public function getDimension() 770 { 771 return $this->dimension; 772 } 773 774 /** 775 * Get dimensions used for current report and its subreports 776 * 777 * @return array [dimensionId => dimensionName] 778 * @ignore 779 */ 780 public function getDimensions() 781 { 782 $dimensions = []; 783 784 if (!empty($this->getDimension())) { 785 $dimensionId = str_replace('.', '_', $this->getDimension()->getId()); 786 $dimensions[$dimensionId] = $this->getDimension()->getName(); 787 } 788 789 if (!empty($this->getSubtableDimension())) { 790 $subDimensionId = str_replace('.', '_', $this->getSubtableDimension()->getId()); 791 $dimensions[$subDimensionId] = $this->getSubtableDimension()->getName(); 792 } 793 794 if (!empty($this->getThirdLeveltableDimension())) { 795 $subDimensionId = str_replace('.', '_', $this->getThirdLeveltableDimension()->getId()); 796 $dimensions[$subDimensionId] = $this->getThirdLeveltableDimension()->getName(); 797 } 798 799 return $dimensions; 800 } 801 802 /** 803 * Returns the order of the report 804 * @return int 805 * @ignore 806 */ 807 public function getOrder() 808 { 809 return $this->order; 810 } 811 812 /** 813 * Get the action to load sub tables if one is defined. 814 * @return string 815 * @ignore 816 */ 817 public function getActionToLoadSubTables() 818 { 819 return $this->actionToLoadSubTables; 820 } 821 822 /** 823 * Returns the Dimension instance of this report's subtable report. 824 * 825 * @return Dimension|null The subtable report's dimension or null if there is subtable report or 826 * no dimension for the subtable report. 827 * @api 828 */ 829 public function getSubtableDimension() 830 { 831 if (empty($this->actionToLoadSubTables)) { 832 return null; 833 } 834 835 list($subtableReportModule, $subtableReportAction) = $this->getSubtableApiMethod(); 836 837 $subtableReport = ReportsProvider::factory($subtableReportModule, $subtableReportAction); 838 if (empty($subtableReport)) { 839 return null; 840 } 841 842 return $subtableReport->getDimension(); 843 } 844 845 /** 846 * Returns the Dimension instance of the subtable report of this report's subtable report. 847 * 848 * @return Dimension|null The subtable report's dimension or null if there is no subtable report or 849 * no dimension for the subtable report. 850 * @api 851 */ 852 public function getThirdLeveltableDimension() 853 { 854 if (empty($this->actionToLoadSubTables)) { 855 return null; 856 } 857 858 list($subtableReportModule, $subtableReportAction) = $this->getSubtableApiMethod(); 859 860 $subtableReport = ReportsProvider::factory($subtableReportModule, $subtableReportAction); 861 if (empty($subtableReport) || empty($subtableReport->actionToLoadSubTables)) { 862 return null; 863 } 864 865 list($subSubtableReportModule, $subSubtableReportAction) = $subtableReport->getSubtableApiMethod(); 866 867 $subSubtableReport = ReportsProvider::factory($subSubtableReportModule, $subSubtableReportAction); 868 if (empty($subSubtableReport)) { 869 return null; 870 } 871 872 return $subSubtableReport->getDimension(); 873 } 874 875 /** 876 * Returns true if the report is for another report's subtable, false if otherwise. 877 * 878 * @return bool 879 */ 880 public function isSubtableReport() 881 { 882 return $this->isSubtableReport; 883 } 884 885 /** 886 * Fetches the report represented by this instance. 887 * 888 * @param array $paramOverride Query parameter overrides. 889 * @return DataTable 890 * @api 891 */ 892 public function fetch($paramOverride = array()) 893 { 894 return Request::processRequest($this->module . '.' . $this->action, $paramOverride); 895 } 896 897 /** 898 * Fetches a subtable for the report represented by this instance. 899 * 900 * @param int $idSubtable The subtable ID. 901 * @param array $paramOverride Query parameter overrides. 902 * @return DataTable 903 * @api 904 */ 905 public function fetchSubtable($idSubtable, $paramOverride = array()) 906 { 907 $paramOverride = array('idSubtable' => $idSubtable) + $paramOverride; 908 909 list($module, $action) = $this->getSubtableApiMethod(); 910 return Request::processRequest($module . '.' . $action, $paramOverride); 911 } 912 913 private function getMetricTranslations($metricsToTranslate) 914 { 915 $translations = Metrics::getDefaultMetricTranslations(); 916 $metrics = array(); 917 918 foreach ($metricsToTranslate as $metric) { 919 if ($metric instanceof Metric) { 920 $metricName = $metric->getName(); 921 $translation = $metric->getTranslatedName(); 922 } else { 923 $metricName = $metric; 924 $translation = @$translations[$metric]; 925 } 926 927 $metrics[$metricName] = $translation ?: $metricName; 928 } 929 930 return $metrics; 931 } 932 933 private function getSubtableApiMethod() 934 { 935 if (strpos($this->actionToLoadSubTables, '.') !== false) { 936 return explode('.', $this->actionToLoadSubTables); 937 } else { 938 return array($this->module, $this->actionToLoadSubTables); 939 } 940 } 941 942 /** 943 * Finds a top level report that provides stats for a specific Dimension. 944 * 945 * @param Dimension $dimension The dimension whose report we're looking for. 946 * @return Report|null The 947 * @api 948 */ 949 public static function getForDimension(Dimension $dimension) 950 { 951 $provider = new ReportsProvider(); 952 $reports = $provider->getAllReports(); 953 foreach ($reports as $report) { 954 if (!$report->isSubtableReport() 955 && $report->getDimension() 956 && $report->getDimension()->getId() == $dimension->getId() 957 ) { 958 return $report; 959 } 960 } 961 return null; 962 } 963 964 /** 965 * Returns an array mapping the ProcessedMetrics served by this report by their string names. 966 * 967 * @return ProcessedMetric[] 968 */ 969 public function getProcessedMetricsById() 970 { 971 $processedMetrics = $this->processedMetrics ?: array(); 972 973 $result = array(); 974 foreach ($processedMetrics as $processedMetric) { 975 if ($processedMetric instanceof ProcessedMetric) { // instanceof check for backwards compatibility 976 $result[$processedMetric->getName()] = $processedMetric; 977 } elseif ($processedMetric instanceof ArchivedMetric 978 && $processedMetric->getType() !== Dimension::TYPE_NUMBER 979 && $processedMetric->getType() !== Dimension::TYPE_FLOAT 980 && $processedMetric->getType() !== Dimension::TYPE_BOOL 981 && $processedMetric->getType() !== Dimension::TYPE_ENUM 982 ) { 983 // we do not format regular numbers from regular archived metrics here because when they are rendered 984 // in a visualisation (eg HtmlTable) they would be formatted again in the regular number filter. 985 // These metrics aren't "processed metrics". Eventually could maybe format them when "&format_metrics=all" 986 // is used but may not be needed. It caused a problem when eg language==de. Then eg 555444 would be formatted 987 // to "555.444" (which is the German version of the English "555,444") in the data table post processor 988 // when formatting metrics. Then when rendering the visualisation it would check "is_numeric()" which is 989 // true for German formatting but false for English formatting. Meaning for English formatting the number 990 // would be correctly printed as is but for the German formatting it would format it again and it would think 991 // it would be assumed the dot is a decimal separator and therefore the number be formatted to "555,44" which 992 // is the English version of "555.44" (because we only show 2 fractions). 993 $result[$processedMetric->getName()] = $processedMetric; 994 } 995 } 996 return $result; 997 } 998 999 /** 1000 * Returns the Metrics that are displayed by a DataTable of a certain Report type. 1001 * 1002 * Includes ProcessedMetrics and Metrics. 1003 * 1004 * @param DataTable $dataTable 1005 * @param Report|null $report 1006 * @param string $baseType The base type each metric class needs to be of. 1007 * @return Metric[] 1008 * @api 1009 */ 1010 public static function getMetricsForTable(DataTable $dataTable, Report $report = null, $baseType = 'Piwik\\Plugin\\Metric') 1011 { 1012 $metrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME) ?: array(); 1013 1014 if (!empty($report)) { 1015 $metrics = array_merge($metrics, $report->getProcessedMetricsById()); 1016 } 1017 1018 $result = array(); 1019 1020 /** @var Metric $metric */ 1021 foreach ($metrics as $metric) { 1022 if (!($metric instanceof $baseType)) { 1023 continue; 1024 } 1025 1026 $result[$metric->getName()] = $metric; 1027 } 1028 1029 return $result; 1030 } 1031 1032 /** 1033 * Returns the ProcessedMetrics that should be computed and formatted for a DataTable of a 1034 * certain report. The ProcessedMetrics returned are those specified by the Report metadata 1035 * as well as the DataTable metadata. 1036 * 1037 * @param DataTable $dataTable 1038 * @param Report|null $report 1039 * @return ProcessedMetric[] 1040 * @api 1041 */ 1042 public static function getProcessedMetricsForTable(DataTable $dataTable, Report $report = null) 1043 { 1044 /** @var ProcessedMetric[] $metrics */ 1045 $metrics = self::getMetricsForTable($dataTable, $report, 'Piwik\\Plugin\\ProcessedMetric'); 1046 1047 // sort metrics w/ dependent metrics calculated before the metrics that depend on them 1048 $result = []; 1049 self::processedMetricDfs($metrics, function ($metricName) use (&$result, $metrics) { 1050 $result[$metricName] = $metrics[$metricName]; 1051 }); 1052 return $result; 1053 } 1054 1055 /** 1056 * @param ProcessedMetric[] $metrics 1057 * @param $callback 1058 * @param array $visited 1059 */ 1060 private static function processedMetricDfs($metrics, $callback, &$visited = [], $toVisit = null) 1061 { 1062 $toVisit = $toVisit === null ? $metrics : $toVisit; 1063 foreach ($toVisit as $name => $metric) { 1064 if (!empty($visited[$name])) { 1065 continue; 1066 } 1067 1068 $visited[$name] = true; 1069 1070 $dependentMetrics = []; 1071 foreach ($metric->getDependentMetrics() as $metricName) { 1072 if (!empty($metrics[$metricName])) { 1073 $dependentMetrics[$metricName] = $metrics[$metricName]; 1074 } 1075 } 1076 1077 self::processedMetricDfs($metrics, $callback, $visited, $dependentMetrics); 1078 1079 $callback($name); 1080 } 1081 } 1082} 1083