| /** | |
| * Matomo - free/libre analytics platform | |
| * | |
| * @link https://matomo.org | |
| * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later | |
| */ | |
| namespace Piwik\Plugin; | |
| use Piwik\API\Proxy; | |
| use Piwik\API\Request; | |
| use Piwik\Columns\Dimension; | |
| use Piwik\Common; | |
| use Piwik\DataTable; | |
| use Piwik\DataTable\Filter\Sort; | |
| use Piwik\Metrics; | |
| use Piwik\Piwik; | |
| use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable; | |
| use Piwik\ViewDataTable\Factory as ViewDataTableFactory; | |
| use Exception; | |
| use Piwik\Widget\WidgetsList; | |
| use Piwik\Report\ReportWidgetFactory; | |
| /** | |
| * Defines a new report. This class contains all information a report defines except the corresponding API method which | |
| * needs to be defined in the 'API.php'. You can define the name of the report, a documentation, the supported metrics, | |
| * how the report should be displayed, which features the report has (eg search) and much more. | |
| * | |
| * You can create a new report using the console command `./console generate:report`. The generated report will guide | |
| * you through the creation of a report. | |
| * | |
| * @since 2.5.0 | |
| * @api | |
| */ | |
| class Report | |
| { | |
| /** | |
| * The sub-namespace name in a plugin where Report components are stored. | |
| */ | |
| public const COMPONENT_SUBNAMESPACE = 'Reports'; | |
| /** | |
| * When added to the menu, a given report eg 'getCampaigns' | |
| * will be routed as &action=menuGetCampaigns | |
| */ | |
| public const PREFIX_ACTION_IN_MENU = 'menu'; | |
| /** | |
| * The name of the module which is supposed to be equal to the name of the plugin. The module is detected | |
| * automatically. | |
| * @var string | |
| */ | |
| protected $module; | |
| /** | |
| * The name of the action. The action is detected automatically depending on the file name. A corresponding action | |
| * should exist in the API as well. | |
| * @var string | |
| */ | |
| protected $action; | |
| /** | |
| * The translated name of the report. The name will be used for instance in the mobile app or if another report | |
| * defines this report as a related report. | |
| * @var string | |
| * @api | |
| */ | |
| protected $name; | |
| /** | |
| * A translated documentation which explains the report. | |
| * @var string | |
| */ | |
| protected $documentation; | |
| /** | |
| * URL linking to an online guide for this report or plugin. | |
| * @var string | |
| */ | |
| protected $onlineGuideUrl; | |
| /** | |
| * The translation key of the category the report belongs to. | |
| * @var string|null | |
| * @api | |
| */ | |
| protected $categoryId; | |
| /** | |
| * The translation key of the subcategory the report belongs to. | |
| * @var string|null | |
| * @api | |
| */ | |
| protected $subcategoryId; | |
| /** | |
| * An array of supported metrics. Eg `array('nb_visits', 'nb_actions', ...)`. Defaults to the platform default | |
| * metrics see {@link Metrics::getDefaultProcessedMetrics()}. | |
| * @var array | |
| * @api | |
| */ | |
| protected $metrics = array('nb_visits', 'nb_uniq_visitors', 'nb_actions', 'nb_users'); | |
| // for a little performance improvement we avoid having to call Metrics::getDefaultMetrics for each report | |
| /** | |
| * The processed metrics this report supports, eg `avg_time_on_site` or `nb_actions_per_visit`. Defaults to the | |
| * platform default processed metrics, see {@link Metrics::getDefaultProcessedMetrics()}. Set it to boolean `false` | |
| * if your report does not support any processed metrics at all. Otherwise an array of metric names. | |
| * Eg `array('avg_time_on_site', 'nb_actions_per_visit', ...)` | |
| * @var array | |
| * @api | |
| */ | |
| protected $processedMetrics = array('nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate', 'conversion_rate'); | |
| // for a little performance improvement we avoid having to call Metrics::getDefaultProcessedMetrics for each report | |
| /** | |
| * The semantic types for all metrics this report displays (including processed metrics). | |
| * | |
| * If set to null, the defaults from the `Metrics.getDefaultMetricSemanticTypes` event are used. | |
| * | |
| * @var null|(string|null)[] | |
| */ | |
| protected $metricSemanticTypes = null; | |
| /** | |
| * Set this property to true in case your report supports goal metrics. In this case, the goal metrics will be | |
| * automatically added to the report metadata and the report will be displayed in the Goals UI. | |
| * @var bool | |
| * @api | |
| */ | |
| protected $hasGoalMetrics = false; | |
| /** | |
| * Set this property to false in case your report can't/shouldn't be flattened. | |
| * In this case, flattener won't be applied even if parameter is provided in a request | |
| * @var bool | |
| * @api | |
| */ | |
| protected $supportsFlatten = true; | |
| /** | |
| * Set it to boolean `true` if your report always returns a constant count of rows, for instance always 24 rows | |
| * for 1-24 hours. | |
| * @var bool | |
| * @api | |
| */ | |
| protected $constantRowsCount = false; | |
| /** | |
| * Set it to boolean `true` if this report is a subtable report and won't be used as a standalone report. | |
| * @var bool | |
| * @api | |
| */ | |
| protected $isSubtableReport = false; | |
| /** | |
| * Some reports may require additional URL parameters that need to be sent when a report is requested. For instance | |
| * a "goal" report might need a "goalId": `array('idgoal' => 5)`. | |
| * @var null|array | |
| * @api | |
| */ | |
| protected $parameters = null; | |
| /** | |
| * An instance of a dimension if the report has one. You can create a new dimension using the Piwik console CLI tool | |
| * if needed. | |
| * @var \Piwik\Columns\Dimension | |
| */ | |
| protected $dimension; | |
| /** | |
| * The name of the API action to load a subtable if supported. The action has to be of the same module. For instance | |
| * a report "getKeywords" might support a subtable "getSearchEngines" which shows how often a keyword was searched | |
| * via a specific search engine. | |
| * @var string | |
| * @api | |
| */ | |
| protected $actionToLoadSubTables = ''; | |
| /** | |
| * The order of the report. Depending on the order the report gets a different position in the list of widgets, | |
| * the menu and the mobile app. | |
| * @var int | |
| * @api | |
| */ | |
| protected $order = 1; | |
| /** | |
| * Separator for building recursive labels (or paths) | |
| * @var string | |
| * @api | |
| */ | |
| protected $recursiveLabelSeparator = ' - '; | |
| /** | |
| * Default sort column. Either a column name or a column id. | |
| * | |
| * @var string|int | |
| */ | |
| protected $defaultSortColumn = 'nb_visits'; | |
| /** | |
| * Default sort desc. If true will sort by default desc, if false will sort by default asc | |
| * | |
| * @var bool | |
| */ | |
| protected $defaultSortOrderDesc = true; | |
| /** | |
| * The column that uniquely identifies a row in this report. Normally | |
| * this is the 'label' column, but it is sometimes the case that the label column is | |
| * not unique. In this case, another column or metadata is used to uniquely identify a row, but | |
| * we don't want to display it to the user, perhaps because it is a numeric ID and not a human | |
| * readable value. | |
| * | |
| * This property is used by features like Row Evolution which compares the same row in | |
| * multiple instances of a report. Being able to find corresponding rows in reports for other | |
| * periods/sites/etc. is required for such features. | |
| * | |
| * @var string | |
| */ | |
| protected $rowIdentifier = 'label'; | |
| /** | |
| * The constructor initializes the module, action and the default metrics. If you want to overwrite any of those | |
| * values or if you want to do any work during initializing overwrite the method {@link init()}. | |
| * @ignore | |
| */ | |
| final public function __construct() | |
| { | |
| $classname = get_class($this); | |
| $parts = explode('\\', $classname); | |
| if (5 === count($parts)) { | |
| $this->module = $parts[2]; | |
| $this->action = lcfirst($parts[4]); | |
| } | |
| $this->init(); | |
| } | |
| /** | |
| * Here you can do any instance initialization and overwrite any default values. You should avoid doing time | |
| * consuming initialization here and if possible delay as long as possible. An instance of this report will be | |
| * created in most page requests. | |
| * @api | |
| */ | |
| protected function init() | |
| { | |
| } | |
| /** | |
| * Defines whether a report is enabled or not. For instance some reports might not be available to every user or | |
| * might depend on a setting (such as Ecommerce) of a site. In such a case you can perform any checks and then | |
| * return `true` or `false`. If your report is only available to users having super user access you can do the | |
| * following: `return Piwik::hasUserSuperUserAccess();` | |
| * @return bool | |
| * @api | |
| */ | |
| public function isEnabled() | |
| { | |
| return true; | |
| } | |
| /** | |
| * This method checks whether the report is available, see {@isEnabled()}. If not, it triggers an exception | |
| * containing a message that will be displayed to the user. You can overwrite this message in case you want to | |
| * customize the error message. Eg. | |
| * ``` | |
| * if (!$this->isEnabled()) { | |
| * throw new Exception('Setting XYZ is not enabled or the user has not enough permission'); | |
| * } | |
| * ``` | |
| * @throws \Exception | |
| * @api | |
| */ | |
| public function checkIsEnabled() | |
| { | |
| if (!$this->isEnabled()) { | |
| throw new Exception(Piwik::translate('General_ExceptionReportNotEnabled')); | |
| } | |
| } | |
| /** | |
| * Returns the id of the default visualization for this report. Eg 'table' or 'pie'. Defaults to the HTML table. | |
| * @return string | |
| * @api | |
| */ | |
| public function getDefaultTypeViewDataTable() | |
| { | |
| return HtmlTable::ID; | |
| } | |
| /** | |
| * Returns if the default viewDataTable type should always be used. e.g. the type won't be changeable through config or url params. | |
| * Defaults to false | |
| * @return bool | |
| */ | |
| public function alwaysUseDefaultViewDataTable() | |
| { | |
| return false; | |
| } | |
| /** | |
| * Here you can configure how your report should be displayed and which capabilities your report has. For instance | |
| * whether your report supports a "search" or not. EG `$view->config->show_search = false`. You can also change the | |
| * default request config. For instance you can change how many rows are displayed by default: | |
| * `$view->requestConfig->filter_limit = 10;`. See {@link ViewDataTable} for more information. | |
| * @api | |
| */ | |
| public function configureView(ViewDataTable $view) | |
| { | |
| } | |
| /** | |
| * Renders a report depending on the configured ViewDataTable see {@link configureView()} and | |
| * {@link getDefaultTypeViewDataTable()}. If you want to customize the render process or just render any custom view | |
| * you can overwrite this method. | |
| * | |
| * @return string | |
| * @throws \Exception In case the given API action does not exist yet. | |
| * @api | |
| */ | |
| public function render() | |
| { | |
| $viewDataTable = Common::getRequestVar('viewDataTable', false, 'string'); | |
| $fixed = Common::getRequestVar('forceView', 0, 'int'); | |
| $module = $this->getModule(); | |
| $action = $this->getAction(); | |
| $apiProxy = Proxy::getInstance(); | |
| if (!$apiProxy->isExistingApiAction($module, $action)) { | |
| throw new Exception("Invalid action name '$action' for '$module' plugin."); | |
| } | |
| $apiAction = $apiProxy->buildApiActionName($module, $action); | |
| $view = ViewDataTableFactory::build($viewDataTable, $apiAction, $module . '.' . $action, $fixed); | |
| return $view->render(); | |
| } | |
| /** | |
| * Processing a uniqueId for each report, can be used by UIs as a key to match a given report | |
| * @return string | |
| */ | |
| public function getId() | |
| { | |
| $params = $this->getParameters(); | |
| $paramsKey = $this->getModule() . '.' . $this->getAction(); | |
| if (!empty($params)) { | |
| foreach ($params as $key => $value) { | |
| $paramsKey .= '_' . $key . '--' . $value; | |
| } | |
| } | |
| return $paramsKey; | |
| } | |
| /** | |
| * lets you add any amount of widgets for this report. If a report defines a {@link $categoryId} and a | |
| * {@link $subcategoryId} a widget will be generated automatically. | |
| * | |
| * Example to add a widget manually by overwriting this method in your report: | |
| * $widgetsList->addWidgetConfig($factory->createWidget()); | |
| * | |
| * If you want to have the name and the order of the widget differently to the name and order of the report you can | |
| * do the following: | |
| * $widgetsList->addWidgetConfig($factory->createWidget()->setName('Custom')->setOrder(5)); | |
| * | |
| * If you want to add a widget to any container defined by your plugin or by another plugin you can do | |
| * this: | |
| * $widgetsList->addToContainerWidget($containerId = 'Products', $factory->createWidget()); | |
| * | |
| * @api | |
| */ | |
| public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) | |
| { | |
| if ($this->categoryId && $this->subcategoryId) { | |
| $widgetsList->addWidgetConfig($factory->createWidget()); | |
| } | |
| } | |
| /** | |
| * @ignore | |
| * @see $recursiveLabelSeparator | |
| */ | |
| public function getRecursiveLabelSeparator() | |
| { | |
| return $this->recursiveLabelSeparator; | |
| } | |
| /** | |
| * Returns an array of supported metrics and their corresponding translations. Eg `array('nb_visits' => 'Visits')`. | |
| * By default the given {@link $metrics} are used and their corresponding translations are looked up automatically. | |
| * If a metric is not translated, you should add the default metric translation for this metric using | |
| * the {@hook Metrics.getDefaultMetricTranslations} event. If you want to overwrite any default metric translation | |
| * you should overwrite this method, call this parent method to get all default translations and overwrite any | |
| * custom metric translations. | |
| * @return array | |
| * @api | |
| */ | |
| public function getMetrics() | |
| { | |
| return $this->getMetricTranslations($this->metrics); | |
| } | |
| /** | |
| * Returns the list of metrics required at minimum for a report factoring in the columns requested by | |
| * the report requester. | |
| * | |
| * This will return all the metrics requested (or all the metrics in the report if nothing is requested) | |
| * **plus** the metrics required to calculate the requested processed metrics. | |
| * | |
| * This method should be used in **Plugin.get** API methods. | |
| * | |
| * @param string[]|null $allMetrics The list of all available unprocessed metrics. Defaults to this report's | |
| * metrics. | |
| * @param string[]|null $restrictToColumns The requested columns. | |
| * @return string[] | |
| */ | |
| public function getMetricsRequiredForReport($allMetrics = null, $restrictToColumns = null) | |
| { | |
| if (empty($allMetrics)) { | |
| $allMetrics = $this->metrics; | |
| } | |
| if (empty($restrictToColumns)) { | |
| $restrictToColumns = array_merge($allMetrics, array_keys($this->getProcessedMetrics())); | |
| } | |
| $restrictToColumns = array_unique($restrictToColumns); | |
| $processedMetricsById = $this->getProcessedMetricsById(); | |
| $metricsSet = array_flip($allMetrics); | |
| $metrics = array(); | |
| foreach ($restrictToColumns as $column) { | |
| if (isset($processedMetricsById[$column])) { | |
| $metrics = array_merge($metrics, $processedMetricsById[$column]->getDependentMetrics()); | |
| } elseif (isset($metricsSet[$column])) { | |
| $metrics[] = $column; | |
| } | |
| } | |
| return array_unique($metrics); | |
| } | |
| /** | |
| * Returns an array of supported processed metrics and their corresponding translations. Eg | |
| * `array('nb_visits' => 'Visits')`. By default the given {@link $processedMetrics} are used and their | |
| * corresponding translations are looked up automatically. If a metric is not translated, you should add the | |
| * default metric translation for this metric using the {@hook Metrics.getDefaultMetricTranslations} event. If you | |
| * want to overwrite any default metric translation you should overwrite this method, call this parent method to | |
| * get all default translations and overwrite any custom metric translations. | |
| * @return array|mixed | |
| * @api | |
| */ | |
| public function getProcessedMetrics() | |
| { | |
| if (!is_array($this->processedMetrics)) { | |
| return $this->processedMetrics; | |
| } | |
| return $this->getMetricTranslations($this->processedMetrics); | |
| } | |
| /** | |
| * Returns the semantic types for metrics this report displays. | |
| * | |
| * If the semantic type is not defined by the derived Report class, it defaults to | |
| * the value returned by {@link Metrics::getDefaultMetricSemanticTypes()} or | |
| * {@link Metric::getSemanticType()}. If the semantic type cannot be found this way, | |
| * this method tries to deduce it from the metric name, though this process will | |
| * not identify the semantic type for most metrics. | |
| * | |
| * @return string[] maps metric name => semantic type | |
| * @api | |
| */ | |
| public function getMetricSemanticTypes(): array | |
| { | |
| $metricTypes = $this->metricSemanticTypes ?: []; | |
| $allMetrics = array_merge($this->metrics ?: [], $this->processedMetrics ?: []); | |
| foreach ($allMetrics as $metric) { | |
| $metricName = $metric instanceof Metric ? $metric->getName() : $metric; | |
| if ( | |
| $metricName == 'label' | |
| || !empty($metricTypes[$metricName]) | |
| ) { | |
| continue; | |
| } | |
| $metricTypes[$metricName] = $this->deduceMetricTypeFromName($metric); | |
| } | |
| return $metricTypes; | |
| } | |
| /** | |
| * Returns the array of all metrics displayed by this report. | |
| * | |
| * @return array | |
| * @api | |
| */ | |
| public function getAllMetrics() | |
| { | |
| $processedMetrics = $this->getProcessedMetrics() ?: array(); | |
| return array_keys(array_merge($this->getMetrics(), $processedMetrics)); | |
| } | |
| /** | |
| * Use this method to register metrics to process report totals. | |
| * | |
| * When a metric is registered, it will process the report total values and as a result show percentage values | |
| * in the HTML Table reporting visualization. | |
| * | |
| * @return string[] metricId => metricColumn, if the report has only column names and no IDs, it should return | |
| * metricColumn => metricColumn, eg array('13' => 'nb_pageviews') or array('mymetric' => 'mymetric') | |
| */ | |
| public function getMetricNamesToProcessReportTotals() | |
| { | |
| return array(); | |
| } | |
| /** | |
| * Returns an array of metric documentations and their corresponding translations. Eg | |
| * `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...')`. | |
| * By default the given {@link $metrics} are used and their corresponding translations are looked up automatically. | |
| * If there is a metric documentation not found, you should add the default metric documentation translation for | |
| * this metric using the {@hook Metrics.getDefaultMetricDocumentationTranslations} event. If you want to overwrite | |
| * any default metric translation you should overwrite this method, call this parent method to get all default | |
| * translations and overwrite any custom metric translations. | |
| * @return array | |
| * @api | |
| */ | |
| protected function getMetricsDocumentation() | |
| { | |
| $translations = Metrics::getDefaultMetricsDocumentation(); | |
| $documentation = array(); | |
| foreach ($this->metrics as $metric) { | |
| if (is_string($metric) && !empty($translations[$metric])) { | |
| $documentation[$metric] = $translations[$metric]; | |
| } elseif ($metric instanceof Metric) { | |
| $name = $metric->getName(); | |
| $metricDocs = $metric->getDocumentation(); | |
| if (empty($metricDocs) && !empty($translations[$name])) { | |
| $metricDocs = $translations[$name]; | |
| } | |
| if (!empty($metricDocs)) { | |
| $documentation[$name] = $metricDocs; | |
| } | |
| } | |
| } | |
| $processedMetrics = $this->processedMetrics ?: array(); | |
| foreach ($processedMetrics as $processedMetric) { | |
| if (is_string($processedMetric) && !empty($translations[$processedMetric])) { | |
| $documentation[$processedMetric] = $translations[$processedMetric]; | |
| } elseif ($processedMetric instanceof Metric) { | |
| $name = $processedMetric->getName(); | |
| $metricDocs = $processedMetric->getDocumentation(); | |
| if (empty($metricDocs) && !empty($translations[$name])) { | |
| $metricDocs = $translations[$name]; | |
| } | |
| if (!empty($metricDocs)) { | |
| $documentation[$name] = $metricDocs; | |
| } | |
| } | |
| } | |
| return $documentation; | |
| } | |
| /** | |
| * @return bool | |
| * @ignore | |
| */ | |
| public function hasGoalMetrics() | |
| { | |
| return $this->hasGoalMetrics; | |
| } | |
| /** | |
| * @return bool | |
| * @ignore | |
| */ | |
| public function supportsFlatten() | |
| { | |
| return $this->supportsFlatten; | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| public function hasConstantRowsCount(): bool | |
| { | |
| return $this->constantRowsCount; | |
| } | |
| /** | |
| * If the report is enabled the report metadata for this report will be built and added to the list of available | |
| * reports. Overwrite this method and leave it empty in case you do not want your report to be added to the report | |
| * metadata. In this case your report won't be visible for instance in the mobile app and scheduled reports | |
| * generator. We recommend to change this behavior only if you are familiar with the Piwik core. `$infos` contains | |
| * the current requested date, period and site. | |
| * @param $availableReports | |
| * @param $infos | |
| * @api | |
| */ | |
| public function configureReportMetadata(&$availableReports, $infos) | |
| { | |
| if (!$this->isEnabled()) { | |
| return; | |
| } | |
| $report = $this->buildReportMetadata(); | |
| if (!empty($report)) { | |
| $availableReports[] = $report; | |
| } | |
| } | |
| /** | |
| * Get report documentation. | |
| * @return string | |
| */ | |
| public function getDocumentation() | |
| { | |
| return $this->documentation; | |
| } | |
| /** | |
| * Builts the report metadata for this report. Can be useful in case you want to change the behavior of | |
| * {@link configureReportMetadata()}. | |
| * @return ?array | |
| * @ignore | |
| * | |
| * TODO we should move this out to API::getReportMetadata | |
| */ | |
| protected function buildReportMetadata() | |
| { | |
| $report = array( | |
| 'category' => $this->getCategoryId(), | |
| 'subcategory' => $this->getSubcategoryId(), | |
| 'name' => $this->getName(), | |
| 'module' => $this->getModule(), | |
| 'action' => $this->getAction(), | |
| ); | |
| if (null !== $this->parameters) { | |
| $report['parameters'] = $this->parameters; | |
| } | |
| if (!empty($this->dimension)) { | |
| $report['dimension'] = $this->dimension->getName(); | |
| } | |
| if (!empty($this->documentation)) { | |
| $report['documentation'] = $this->documentation; | |
| } | |
| if (!empty($this->onlineGuideUrl)) { | |
| $report['onlineGuideUrl'] = $this->onlineGuideUrl; | |
| } | |
| if (true === $this->isSubtableReport) { | |
| $report['isSubtableReport'] = $this->isSubtableReport; | |
| } | |
| $dimensions = $this->getDimensions(); | |
| if (count($dimensions) > 1) { | |
| $report['dimensions'] = $dimensions; | |
| } | |
| $report['metrics'] = $this->getMetrics(); | |
| $report['metricsDocumentation'] = $this->getMetricsDocumentation(); | |
| $report['processedMetrics'] = $this->getProcessedMetrics(); | |
| $report['metricTypes'] = $this->getMetricSemanticTypes(); | |
| $report['metricTypes'] = array_map(function ($t) { | |
| return $t ?: 'unspecified'; | |
| }, $report['metricTypes']); | |
| if (!empty($this->actionToLoadSubTables)) { | |
| $report['actionToLoadSubTables'] = $this->actionToLoadSubTables; | |
| } | |
| if (true === $this->constantRowsCount) { | |
| $report['constantRowsCount'] = $this->constantRowsCount; | |
| } | |
| $relatedReports = $this->getRelatedReports(); | |
| if (!empty($relatedReports)) { | |
| $report['relatedReports'] = array(); | |
| foreach ($relatedReports as $relatedReport) { | |
| if (!empty($relatedReport)) { | |
| $report['relatedReports'][] = [ | |
| 'name' => $relatedReport->getName(), | |
| 'module' => $relatedReport->getModule(), | |
| 'action' => $relatedReport->getAction(), | |
| ]; | |
| } | |
| } | |
| } | |
| $report['order'] = $this->order; | |
| return $report; | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| public function getDefaultSortColumn() | |
| { | |
| return $this->defaultSortColumn; | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| public function getDefaultSortOrder() | |
| { | |
| if ($this->defaultSortOrderDesc) { | |
| return Sort::ORDER_DESC; | |
| } | |
| return Sort::ORDER_ASC; | |
| } | |
| /** | |
| * Allows to define a callback that will be used to determine the secondary column to sort by | |
| * | |
| * ``` | |
| * public function getSecondarySortColumnCallback() | |
| * { | |
| * return function ($primaryColumn) { | |
| * switch ($primaryColumn) { | |
| * case Metrics::NB_CLICKS: | |
| * return Metrics::NB_IMPRESSIONS; | |
| * case 'label': | |
| * default: | |
| * return Metrics::NB_CLICKS; | |
| * } | |
| * }; | |
| * } | |
| * ``` | |
| * @return null|callable | |
| */ | |
| public function getSecondarySortColumnCallback() | |
| { | |
| return null; | |
| } | |
| /** | |
| * Get the list of related reports if there are any. They will be displayed for instance below a report as a | |
| * recommended related report. | |
| * | |
| * @return (Report|null)[] | |
| * @api | |
| */ | |
| public function getRelatedReports() | |
| { | |
| return array(); | |
| } | |
| /** | |
| * Get the name of the report | |
| * @return string | |
| * @ignore | |
| */ | |
| public function getName() | |
| { | |
| return $this->name; | |
| } | |
| /** | |
| * Get the name of the module. | |
| * @return string | |
| * @ignore | |
| */ | |
| public function getModule() | |
| { | |
| return $this->module; | |
| } | |
| /** | |
| * Get the name of the action. | |
| * @return string | |
| * @ignore | |
| */ | |
| public function getAction() | |
| { | |
| return $this->action; | |
| } | |
| public function getParameters() | |
| { | |
| return $this->parameters; | |
| } | |
| /** | |
| * Get the translated name of the category the report belongs to. | |
| * @return string|null | |
| * @ignore | |
| */ | |
| public function getCategoryId() | |
| { | |
| return $this->categoryId; | |
| } | |
| /** | |
| * Get the translated name of the subcategory the report belongs to. | |
| * @return string|null | |
| * @ignore | |
| */ | |
| public function getSubcategoryId() | |
| { | |
| return $this->subcategoryId; | |
| } | |
| /** | |
| * @return \Piwik\Columns\Dimension|null | |
| * @ignore | |
| */ | |
| public function getDimension() | |
| { | |
| return $this->dimension; | |
| } | |
| /** | |
| * Get dimensions used for current report and its subreports | |
| * | |
| * @return array [dimensionId => dimensionName] | |
| * @ignore | |
| */ | |
| public function getDimensions() | |
| { | |
| $dimensions = []; | |
| $tableDimensionId = null; | |
| if (!empty($this->getDimension())) { | |
| $tableDimensionId = $this->getDimension()->getId(); | |
| $dimensionId = str_replace('.', '_', $tableDimensionId); | |
| $dimensions[$dimensionId] = $this->getDimension()->getName(); | |
| } | |
| for ($level = 1; $level <= 100; $level++) { | |
| $subDimension = $this->getNthLevelTableDimension($level); | |
| if (empty($subDimension) || $tableDimensionId === $subDimension->getId()) { | |
| break; | |
| } | |
| $tableDimensionId = $subDimension->getId(); | |
| $subDimensionId = str_replace('.', '_', $tableDimensionId); | |
| $dimensions[$subDimensionId] = $subDimension->getName(); | |
| }; | |
| return $dimensions; | |
| } | |
| /** | |
| * Returns the order of the report | |
| * @return int | |
| * @ignore | |
| */ | |
| public function getOrder() | |
| { | |
| return $this->order; | |
| } | |
| /** | |
| * Get the action to load sub tables if one is defined. | |
| * @return string | |
| * @ignore | |
| */ | |
| public function getActionToLoadSubTables() | |
| { | |
| return $this->actionToLoadSubTables; | |
| } | |
| /** | |
| * Returns the Dimension instance of this report's subtable report. | |
| * | |
| * @return Dimension|null The subtable report's dimension or null if there is subtable report or | |
| * no dimension for the subtable report. | |
| * @api | |
| */ | |
| public function getSubtableDimension() | |
| { | |
| return $this->getNthLevelTableDimension($level = 1); | |
| } | |
| /** | |
| * Returns the Dimension instance of the subtable report of this report's subtable report. | |
| * | |
| * @return Dimension|null The subtable report's dimension or null if there is no subtable report or | |
| * no dimension for the subtable report. | |
| * @deprecated since 5.3.0, use getNthLevelTableDimension(2) instead | |
| * @api | |
| */ | |
| public function getThirdLeveltableDimension() | |
| { | |
| return $this->getNthLevelTableDimension($level = 2); | |
| } | |
| /** | |
| * Returns the Dimension instance of the subtable report of this report's subtable report based on level. | |
| * | |
| * @param int $level The subTable level for which dimension is to be determined, zero-based | |
| * @return Dimension|null The subtable report's dimension or null if there is no subtable report or | |
| * no dimension for the subtable report. | |
| * @api | |
| */ | |
| public function getNthLevelTableDimension(int $level): ?Dimension | |
| { | |
| if (empty($this->actionToLoadSubTables)) { | |
| return null; | |
| } | |
| $subTableReport = $this; | |
| for ($i = 1; $i <= $level; $i++) { | |
| [$subTableReportModule, $subTableReportAction] = $subTableReport->getSubtableApiMethod(); | |
| $subTableReport = ReportsProvider::factory($subTableReportModule, $subTableReportAction); | |
| if (empty($subTableReport)) { | |
| return null; | |
| } | |
| } | |
| return $subTableReport->getDimension(); | |
| } | |
| /** | |
| * Returns true if the report is for another report's subtable, false if otherwise. | |
| * | |
| * @return bool | |
| */ | |
| public function isSubtableReport() | |
| { | |
| return $this->isSubtableReport; | |
| } | |
| /** | |
| * Fetches the report represented by this instance. | |
| * | |
| * @param array $paramOverride Query parameter overrides. | |
| * @return DataTable | |
| * @api | |
| */ | |
| public function fetch($paramOverride = array()) | |
| { | |
| return Request::processRequest($this->module . '.' . $this->action, $paramOverride); | |
| } | |
| /** | |
| * Fetches a subtable for the report represented by this instance. | |
| * | |
| * @param int $idSubtable The subtable ID. | |
| * @param array $paramOverride Query parameter overrides. | |
| * @return DataTable | |
| * @api | |
| */ | |
| public function fetchSubtable($idSubtable, $paramOverride = array()) | |
| { | |
| $paramOverride = array('idSubtable' => $idSubtable) + $paramOverride; | |
| [$module, $action] = $this->getSubtableApiMethod(); | |
| return Request::processRequest($module . '.' . $action, $paramOverride); | |
| } | |
| private function getMetricTranslations($metricsToTranslate) | |
| { | |
| $translations = Metrics::getDefaultMetricTranslations(); | |
| $metrics = array(); | |
| foreach ($metricsToTranslate as $metric) { | |
| if ($metric instanceof Metric) { | |
| $metricName = $metric->getName(); | |
| $translation = $metric->getTranslatedName(); | |
| } else { | |
| $metricName = $metric; | |
| $translation = $translations[$metric] ?? null; | |
| } | |
| $metrics[$metricName] = $translation ?: $metricName; | |
| } | |
| return $metrics; | |
| } | |
| private function getSubtableApiMethod() | |
| { | |
| if (strpos($this->actionToLoadSubTables, '.') !== false) { | |
| return explode('.', $this->actionToLoadSubTables); | |
| } else { | |
| return array($this->module, $this->actionToLoadSubTables); | |
| } | |
| } | |
| /** | |
| * Finds a top level report that provides stats for a specific Dimension. | |
| * | |
| * @param Dimension $dimension The dimension whose report we're looking for. | |
| * @return Report|null The | |
| * @api | |
| */ | |
| public static function getForDimension(Dimension $dimension) | |
| { | |
| $provider = new ReportsProvider(); | |
| $reports = $provider->getAllReports(); | |
| foreach ($reports as $report) { | |
| if ( | |
| !$report->isSubtableReport() | |
| && $report->getDimension() | |
| && $report->getDimension()->getId() == $dimension->getId() | |
| ) { | |
| return $report; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Returns an array mapping the ProcessedMetrics served by this report by their string names. | |
| * | |
| * @return ProcessedMetric[] | |
| */ | |
| public function getProcessedMetricsById() | |
| { | |
| $processedMetrics = $this->processedMetrics ?: array(); | |
| $result = array(); | |
| foreach ($processedMetrics as $processedMetric) { | |
| if ($processedMetric instanceof ProcessedMetric) { // instanceof check for backwards compatibility | |
| $result[$processedMetric->getName()] = $processedMetric; | |
| } elseif ( | |
| $processedMetric instanceof ArchivedMetric | |
| && $processedMetric->getType() !== Dimension::TYPE_NUMBER | |
| && $processedMetric->getType() !== Dimension::TYPE_FLOAT | |
| && $processedMetric->getType() !== Dimension::TYPE_BOOL | |
| && $processedMetric->getType() !== Dimension::TYPE_ENUM | |
| ) { | |
| // we do not format regular numbers from regular archived metrics here because when they are rendered | |
| // in a visualisation (eg HtmlTable) they would be formatted again in the regular number filter. | |
| // These metrics aren't "processed metrics". Eventually could maybe format them when "&format_metrics=all" | |
| // is used but may not be needed. It caused a problem when eg language==de. Then eg 555444 would be formatted | |
| // to "555.444" (which is the German version of the English "555,444") in the data table post processor | |
| // when formatting metrics. Then when rendering the visualisation it would check "is_numeric()" which is | |
| // true for German formatting but false for English formatting. Meaning for English formatting the number | |
| // would be correctly printed as is but for the German formatting it would format it again and it would think | |
| // it would be assumed the dot is a decimal separator and therefore the number be formatted to "555,44" which | |
| // is the English version of "555.44" (because we only show 2 fractions). | |
| $result[$processedMetric->getName()] = $processedMetric; | |
| } | |
| } | |
| return $result; | |
| } | |
| /** | |
| * Returns the Metrics that are displayed by a DataTable of a certain Report type. | |
| * | |
| * Includes ProcessedMetrics and Metrics. | |
| * | |
| * @param string $baseType The base type each metric class needs to be of. | |
| * @return Metric[] | |
| * @api | |
| */ | |
| public static function getMetricsForTable(DataTable $dataTable, ?Report $report = null, $baseType = 'Piwik\\Plugin\\Metric') | |
| { | |
| $metrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME) ?: array(); | |
| if (!empty($report)) { | |
| $metrics = array_merge($metrics, $report->getProcessedMetricsById()); | |
| } | |
| $result = array(); | |
| /** @var Metric $metric */ | |
| foreach ($metrics as $metric) { | |
| if (!($metric instanceof $baseType)) { | |
| continue; | |
| } | |
| $result[$metric->getName()] = $metric; | |
| } | |
| return $result; | |
| } | |
| /** | |
| * Returns the ProcessedMetrics that should be computed and formatted for a DataTable of a | |
| * certain report. The ProcessedMetrics returned are those specified by the Report metadata | |
| * as well as the DataTable metadata. | |
| * | |
| * @return ProcessedMetric[] | |
| * @api | |
| */ | |
| public static function getProcessedMetricsForTable(DataTable $dataTable, ?Report $report = null) | |
| { | |
| /** @var ProcessedMetric[] $metrics */ | |
| $metrics = self::getMetricsForTable($dataTable, $report, 'Piwik\\Plugin\\ProcessedMetric'); | |
| // sort metrics w/ dependent metrics calculated before the metrics that depend on them | |
| $result = []; | |
| self::processedMetricDfs($metrics, function ($metricName) use (&$result, $metrics) { | |
| $result[$metricName] = $metrics[$metricName]; | |
| }); | |
| return $result; | |
| } | |
| /** | |
| * @param ProcessedMetric[] $metrics | |
| * @param $callback | |
| * @param array $visited | |
| */ | |
| private static function processedMetricDfs($metrics, $callback, &$visited = [], $toVisit = null) | |
| { | |
| $toVisit = $toVisit === null ? $metrics : $toVisit; | |
| foreach ($toVisit as $name => $metric) { | |
| if (!empty($visited[$name])) { | |
| continue; | |
| } | |
| $visited[$name] = true; | |
| $dependentMetrics = []; | |
| foreach ($metric->getDependentMetrics() as $metricName) { | |
| if (!empty($metrics[$metricName])) { | |
| $dependentMetrics[$metricName] = $metrics[$metricName]; | |
| } | |
| } | |
| self::processedMetricDfs($metrics, $callback, $visited, $dependentMetrics); | |
| $callback($name); | |
| } | |
| } | |
| /** | |
| * Returns the name of the column/metadata that uniquely identifies rows in this report. See | |
| * {@link self::$rowIdentifier} for more information. | |
| */ | |
| public function getRowIdentifier(): string | |
| { | |
| return $this->rowIdentifier; | |
| } | |
| private function deduceMetricTypeFromName($metric): ?string | |
| { | |
| $metricName = $metric instanceof Metric ? $metric->getName() : $metric; | |
| $metricType = null; | |
| if ($metric instanceof Metric) { | |
| $metricType = $metric->getSemanticType(); | |
| } | |
| if (empty($metricType)) { | |
| if (preg_match('/_(evolution|rate|percentage)(_|$)/', $metricName)) { | |
| $metricType = Dimension::TYPE_PERCENT; | |
| } else { | |
| $allMetricTypes = Metrics::getDefaultMetricSemanticTypes(); | |
| $metricType = $allMetricTypes[$metricName] ?? null; | |
| } | |
| } | |
| return $metricType; | |
| } | |
| } | |