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