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 constructur 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. * @param ViewDataTable $view * @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()); * * @param WidgetsList $widgetsList * @param ReportWidgetFactory $factory * @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 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; } /** * 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(); 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'][] = array( '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[] * @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 * @ignore */ public function getCategoryId() { return $this->categoryId; } /** * Get the translated name of the subcategory the report belongs to. * @return string * @ignore */ public function getSubcategoryId() { return $this->subcategoryId; } /** * @return \Piwik\Columns\Dimension * @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 = []; if (!empty($this->getDimension())) { $dimensionId = str_replace('.', '_', $this->getDimension()->getId()); $dimensions[$dimensionId] = $this->getDimension()->getName(); } if (!empty($this->getSubtableDimension())) { $subDimensionId = str_replace('.', '_', $this->getSubtableDimension()->getId()); $dimensions[$subDimensionId] = $this->getSubtableDimension()->getName(); } if (!empty($this->getThirdLeveltableDimension())) { $subDimensionId = str_replace('.', '_', $this->getThirdLeveltableDimension()->getId()); $dimensions[$subDimensionId] = $this->getThirdLeveltableDimension()->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() { if (empty($this->actionToLoadSubTables)) { return null; } list($subtableReportModule, $subtableReportAction) = $this->getSubtableApiMethod(); $subtableReport = ReportsProvider::factory($subtableReportModule, $subtableReportAction); if (empty($subtableReport)) { return null; } return $subtableReport->getDimension(); } /** * 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. * @api */ public function getThirdLeveltableDimension() { if (empty($this->actionToLoadSubTables)) { return null; } list($subtableReportModule, $subtableReportAction) = $this->getSubtableApiMethod(); $subtableReport = ReportsProvider::factory($subtableReportModule, $subtableReportAction); if (empty($subtableReport) || empty($subtableReport->actionToLoadSubTables)) { return null; } list($subSubtableReportModule, $subSubtableReportAction) = $subtableReport->getSubtableApiMethod(); $subSubtableReport = ReportsProvider::factory($subSubtableReportModule, $subSubtableReportAction); if (empty($subSubtableReport)) { return null; } return $subSubtableReport->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; list($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]; } $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 DataTable $dataTable * @param Report|null $report * @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. * * @param DataTable $dataTable * @param Report|null $report * @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); } } }