diff options
author | Thomas ZILLIOX <thomas@zilliox.me> | 2013-07-02 20:08:38 +0400 |
---|---|---|
committer | Thomas ZILLIOX <thomas@zilliox.me> | 2013-07-02 20:08:38 +0400 |
commit | 287dcbbe9eecd9d6e4c5cd89a5a2fa7fe14bb09a (patch) | |
tree | 3ca1c8b4a1fd14cca3c5f492cf6ea8391f1a1060 /plugins/API | |
parent | 3b367dd3afffa6d44fdf0069f62b00c2bb487a5a (diff) |
Merge with master
Diffstat (limited to 'plugins/API')
-rw-r--r-- | plugins/API/API.php | 1161 | ||||
-rw-r--r-- | plugins/API/ProcessedReport.php | 543 | ||||
-rw-r--r-- | plugins/API/RowEvolution.php | 468 | ||||
-rw-r--r-- | plugins/API/templates/listAllAPI.twig | 3 |
4 files changed, 1057 insertions, 1118 deletions
diff --git a/plugins/API/API.php b/plugins/API/API.php index d52ae41f0a..9843a2ce05 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -1,5 +1,4 @@ <?php - /** * Piwik - Open source web analytics * @@ -36,7 +35,7 @@ class Piwik_API extends Piwik_Plugin public function addTopMenu() { - $apiUrlParams = array('module' => 'API', 'action' => 'listAllAPI'); + $apiUrlParams = array('module' => 'API', 'action' => 'listAllAPI', 'segment' => false); $tooltip = Piwik_Translate('API_TopLinkTooltip'); Piwik_AddTopMenu('General_API', $apiUrlParams, true, 7, $isHTML = false, $tooltip); @@ -67,7 +66,6 @@ class Piwik_API extends Piwik_Plugin } } - /** * This API is the <a href='http://piwik.org/docs/analytics-api/metadata/' target='_blank'>Metadata API</a>: it gives information about all other available APIs methods, as well as providing * human readable and more complete outputs than normal API methods. @@ -121,52 +119,6 @@ class Piwik_API_API } /** - * Derive the unit name from a column name - * @param $column - * @param $idSite - * @return string - * @ignore - */ - static public function getUnit($column, $idSite) - { - $nameToUnit = array( - '_rate' => '%', - 'revenue' => Piwik::getCurrency($idSite), - '_time_' => 's' - ); - - foreach ($nameToUnit as $pattern => $type) { - if (strpos($column, $pattern) !== false) { - return $type; - } - } - - return ''; - } - - /** - * Is a lower value for a given column better? - * @param $column - * @return bool - * - * @ignore - */ - static public function isLowerValueBetter($column) - { - $lowerIsBetterPatterns = array( - 'bounce', 'exit' - ); - - foreach ($lowerIsBetterPatterns as $pattern) { - if (strpos($column, $pattern) !== false) { - return true; - } - } - - return false; - } - - /** * Default translations for many core metrics. * This is used for exports with translated labels. The exports contain columns that * are not visible in the UI and not present in the API meta data. These columns are @@ -175,93 +127,7 @@ class Piwik_API_API */ static public function getDefaultMetricTranslations() { - $trans = array( - 'label' => 'General_ColumnLabel', - 'date' => 'General_Date', - 'avg_time_on_page' => 'General_ColumnAverageTimeOnPage', - 'sum_time_spent' => 'General_ColumnSumVisitLength', - 'sum_visit_length' => 'General_ColumnSumVisitLength', - 'bounce_count' => 'General_ColumnBounces', - 'bounce_count_returning' => 'VisitFrequency_ColumnBounceCountForReturningVisits', - 'max_actions' => 'General_ColumnMaxActions', - 'max_actions_returning' => 'VisitFrequency_ColumnMaxActionsInReturningVisit', - 'nb_visits_converted_returning' => 'VisitFrequency_ColumnNbReturningVisitsConverted', - 'sum_visit_length_returning' => 'VisitFrequency_ColumnSumVisitLengthReturning', - 'nb_visits_converted' => 'General_ColumnVisitsWithConversions', - 'nb_conversions' => 'Goals_ColumnConversions', - 'revenue' => 'Goals_ColumnRevenue', - 'nb_hits' => 'General_ColumnPageviews', - 'entry_nb_visits' => 'General_ColumnEntrances', - 'entry_nb_uniq_visitors' => 'General_ColumnUniqueEntrances', - 'exit_nb_visits' => 'General_ColumnExits', - 'exit_nb_uniq_visitors' => 'General_ColumnUniqueExits', - 'entry_bounce_count' => 'General_ColumnBounces', - 'exit_bounce_count' => 'General_ColumnBounces', - 'exit_rate' => 'General_ColumnExitRate' - ); - - $trans = array_map('Piwik_Translate', $trans); - - $dailySum = ' (' . Piwik_Translate('General_DailySum') . ')'; - $afterEntry = ' ' . Piwik_Translate('General_AfterEntry'); - - $trans['sum_daily_nb_uniq_visitors'] = Piwik_Translate('General_ColumnNbUniqVisitors') . $dailySum; - $trans['sum_daily_entry_nb_uniq_visitors'] = Piwik_Translate('General_ColumnUniqueEntrances') . $dailySum; - $trans['sum_daily_exit_nb_uniq_visitors'] = Piwik_Translate('General_ColumnUniqueExits') . $dailySum; - $trans['entry_nb_actions'] = Piwik_Translate('General_ColumnNbActions') . $afterEntry; - $trans['entry_sum_visit_length'] = Piwik_Translate('General_ColumnSumVisitLength') . $afterEntry; - - $api = self::getInstance(); - $trans = array_merge($api->getDefaultMetrics(), $api->getDefaultProcessedMetrics(), $trans); - - return $trans; - } - - public function getDefaultMetrics() - { - $translations = array( - // Standard metrics - 'nb_visits' => 'General_ColumnNbVisits', - 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitors', - 'nb_actions' => 'General_ColumnNbActions', -// Do not display these in reports, as they are not so relevant -// They are used to process metrics below -// 'nb_visits_converted' => 'General_ColumnVisitsWithConversions', -// 'max_actions' => 'General_ColumnMaxActions', -// 'sum_visit_length' => 'General_ColumnSumVisitLength', -// 'bounce_count' - ); - $translations = array_map('Piwik_Translate', $translations); - return $translations; - } - - public function getDefaultProcessedMetrics() - { - $translations = array( - // Processed in AddColumnsProcessedMetrics - 'nb_actions_per_visit' => 'General_ColumnActionsPerVisit', - 'avg_time_on_site' => 'General_ColumnAvgTimeOnSite', - 'bounce_rate' => 'General_ColumnBounceRate', - 'conversion_rate' => 'General_ColumnConversionRate', - ); - return array_map('Piwik_Translate', $translations); - } - - public function getDefaultMetricsDocumentation() - { - $documentation = array( - 'nb_visits' => 'General_ColumnNbVisitsDocumentation', - 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitorsDocumentation', - 'nb_actions' => 'General_ColumnNbActionsDocumentation', - 'nb_actions_per_visit' => 'General_ColumnActionsPerVisitDocumentation', - 'avg_time_on_site' => 'General_ColumnAvgTimeOnSiteDocumentation', - 'bounce_rate' => 'General_ColumnBounceRateDocumentation', - 'conversion_rate' => 'General_ColumnConversionRateDocumentation', - 'avg_time_on_page' => 'General_ColumnAverageTimeOnPageDocumentation', - 'nb_hits' => 'General_ColumnPageviewsDocumentation', - 'exit_rate' => 'General_ColumnExitRateDocumentation' - ); - return array_map('Piwik_Translate', $documentation); + return Piwik_Metrics::getDefaultMetricTranslations(); } public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true) @@ -269,35 +135,38 @@ class Piwik_API_API $segments = array(); Piwik_PostEvent('API.getSegmentsMetadata', $segments, $idSites); + $isAuthenticatedWithViewAccess = Piwik::isUserHasViewAccess($idSites) && !Piwik::isUserIsAnonymous(); + $segments[] = array( 'type' => 'dimension', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_VisitorIP', 'segment' => 'visitIp', 'acceptedValues' => '13.54.122.1, etc.', 'sqlSegment' => 'log_visit.location_ip', 'sqlFilter' => array('Piwik_IP', 'P2N'), - 'permission' => Piwik::isUserHasAdminAccess($idSites), + 'permission' => $isAuthenticatedWithViewAccess, ); $segments[] = array( 'type' => 'dimension', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_VisitorID', 'segment' => 'visitorId', 'acceptedValues' => '34c31e04394bdc63 - any 16 Hexadecimal chars ID, which can be fetched using the Tracking API function getVisitorId()', 'sqlSegment' => 'log_visit.idvisitor', 'sqlFilter' => array('Piwik_Common', 'convertVisitorIdToBin'), + 'permission' => $isAuthenticatedWithViewAccess, ); $segments[] = array( 'type' => 'metric', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_NbActions', 'segment' => 'actions', 'sqlSegment' => 'log_visit.visit_total_actions', ); $segments[] = array( 'type' => 'metric', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_NbSearches', 'segment' => 'searches', 'sqlSegment' => 'log_visit.visit_total_searches', @@ -305,15 +174,15 @@ class Piwik_API_API ); $segments[] = array( 'type' => 'metric', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_ColumnVisitDuration', 'segment' => 'visitDuration', 'sqlSegment' => 'log_visit.visit_total_time', ); $segments[] = array( 'type' => 'dimension', - 'category' => 'Visit', - 'name' => Piwik_Translate('General_VisitType') , + 'category' => Piwik_Translate('General_Visit'), + 'name' => Piwik_Translate('General_VisitType'), 'segment' => 'visitorType', 'acceptedValues' => 'new, returning, returningCustomer' . ". " . Piwik_Translate('General_VisitTypeExample', '"&segment=visitorType==returning,visitorType==returningCustomer"'), 'sqlSegment' => 'log_visit.visitor_returning', @@ -321,21 +190,21 @@ class Piwik_API_API ); $segments[] = array( 'type' => 'metric', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_DaysSinceLastVisit', 'segment' => 'daysSinceLastVisit', 'sqlSegment' => 'log_visit.visitor_days_since_last', ); $segments[] = array( 'type' => 'metric', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_DaysSinceFirstVisit', 'segment' => 'daysSinceFirstVisit', 'sqlSegment' => 'log_visit.visitor_days_since_first', ); $segments[] = array( 'type' => 'metric', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_NumberOfVisits', 'segment' => 'visitCount', 'sqlSegment' => 'log_visit.visitor_count_visits', @@ -343,7 +212,7 @@ class Piwik_API_API $segments[] = array( 'type' => 'dimension', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_VisitConvertedGoal', 'segment' => 'visitConverted', 'acceptedValues' => '0, 1', @@ -352,18 +221,18 @@ class Piwik_API_API $segments[] = array( 'type' => 'dimension', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => Piwik_Translate('General_EcommerceVisitStatusDesc'), 'segment' => 'visitEcommerceStatus', 'acceptedValues' => implode(", ", self::$visitEcommerceStatus) - . '. '. Piwik_Translate('General_EcommerceVisitStatusEg', '"&segment=visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart"'), + . '. ' . Piwik_Translate('General_EcommerceVisitStatusEg', '"&segment=visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart"'), 'sqlSegment' => 'log_visit.visit_goal_buyer', 'sqlFilter' => array('Piwik_API_API', 'getVisitEcommerceStatus'), ); $segments[] = array( 'type' => 'metric', - 'category' => 'Visit', + 'category' => Piwik_Translate('General_Visit'), 'name' => 'General_DaysSinceLastEcommerceOrder', 'segment' => 'daysSinceLastEcommerceOrder', 'sqlSegment' => 'log_visit.visitor_days_since_order', @@ -524,32 +393,9 @@ class Piwik_API_API $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false) { Piwik_Translate::getInstance()->reloadLanguage($language); - $reportsMetadata = $this->getReportMetadata($idSite, $period, $date, $hideMetricsDoc, $showSubtableReports); - - foreach ($reportsMetadata as $report) { - // See ArchiveProcessing/Period.php - unique visitors are not processed for period != day - if (($period && $period != 'day') && !($apiModule == 'VisitsSummary' && $apiAction == 'get')) { - unset($report['metrics']['nb_uniq_visitors']); - } - if ($report['module'] == $apiModule - && $report['action'] == $apiAction - ) { - // No custom parameters - if (empty($apiParameters) - && empty($report['parameters']) - ) { - return array($report); - } - if (empty($report['parameters'])) { - continue; - } - $diff = array_diff($report['parameters'], $apiParameters); - if (empty($diff)) { - return array($report); - } - } - } - return false; + $reporter = new Piwik_API_ProcessedReport(); + $metadata = $reporter->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, $period, $date, $hideMetricsDoc, $showSubtableReports); + return $metadata; } /** @@ -562,485 +408,21 @@ class Piwik_API_API public function getReportMetadata($idSites = '', $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false) { - $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); - if (!empty($idSites)) { - Piwik::checkUserHasViewAccess($idSites); - } - - $parameters = array('idSites' => $idSites, 'period' => $period, 'date' => $date); - - $availableReports = array(); - Piwik_PostEvent('API.getReportMetadata', $availableReports, $parameters); - foreach ($availableReports as &$availableReport) { - if (!isset($availableReport['metrics'])) { - $availableReport['metrics'] = $this->getDefaultMetrics(); - } - if (!isset($availableReport['processedMetrics'])) { - $availableReport['processedMetrics'] = $this->getDefaultProcessedMetrics(); - } - - if ($hideMetricsDoc) // remove metric documentation if it's not wanted - { - unset($availableReport['metricsDocumentation']); - } else if (!isset($availableReport['metricsDocumentation'])) { - // set metric documentation to default if it's not set - $availableReport['metricsDocumentation'] = $this->getDefaultMetricsDocumentation(); - } - } - - // Some plugins need to add custom metrics after all plugins hooked in - Piwik_PostEvent('API.getReportMetadata.end', $availableReports, $parameters); - // Oh this is not pretty! Until we have event listeners order parameter... - Piwik_PostEvent('API.getReportMetadata.end.end', $availableReports, $parameters); - - // Sort results to ensure consistent order - usort($availableReports, array($this, 'sort')); - - // Add the magic API.get report metadata aggregating all plugins API.get API calls automatically - $this->addApiGetMetdata($availableReports); - - $knownMetrics = array_merge($this->getDefaultMetrics(), $this->getDefaultProcessedMetrics()); - foreach ($availableReports as &$availableReport) { - // Ensure all metrics have a translation - $metrics = $availableReport['metrics']; - $cleanedMetrics = array(); - foreach ($metrics as $metricId => $metricTranslation) { - // When simply the column name was given, ie 'metric' => array( 'nb_visits' ) - // $metricTranslation is in this case nb_visits. We look for a known translation. - if (is_numeric($metricId) - && isset($knownMetrics[$metricTranslation]) - ) { - $metricId = $metricTranslation; - $metricTranslation = $knownMetrics[$metricTranslation]; - } - $cleanedMetrics[$metricId] = $metricTranslation; - } - $availableReport['metrics'] = $cleanedMetrics; - - // if hide/show columns specified, hide/show metrics & docs - $availableReport['metrics'] = $this->hideShowMetrics($availableReport['metrics']); - if (isset($availableReport['processedMetrics'])) { - $availableReport['processedMetrics'] = $this->hideShowMetrics($availableReport['processedMetrics']); - } - if (isset($availableReport['metricsDocumentation'])) { - $availableReport['metricsDocumentation'] = - $this->hideShowMetrics($availableReport['metricsDocumentation']); - } - - // Remove array elements that are false (to clean up API output) - foreach ($availableReport as $attributeName => $attributeValue) { - if (empty($attributeValue)) { - unset($availableReport[$attributeName]); - } - } - // when there are per goal metrics, don't display conversion_rate since it can differ from per goal sum - if (isset($availableReport['metricsGoal'])) { - unset($availableReport['processedMetrics']['conversion_rate']); - unset($availableReport['metricsGoal']['conversion_rate']); - } - - // Processing a uniqueId for each report, - // can be used by UIs as a key to match a given report - $uniqueId = $availableReport['module'] . '_' . $availableReport['action']; - if (!empty($availableReport['parameters'])) { - foreach ($availableReport['parameters'] as $key => $value) { - $uniqueId .= '_' . $key . '--' . $value; - } - } - $availableReport['uniqueId'] = $uniqueId; - - // Order is used to order reports internally, but not meant to be used outside - unset($availableReport['order']); - } - - // remove subtable reports - if (!$showSubtableReports) { - foreach ($availableReports as $idx => $report) { - if (isset($report['isSubtableReport']) && $report['isSubtableReport']) { - unset($availableReports[$idx]); - } - } - } - - return array_values($availableReports); // make sure array has contiguous key values - } - - - /** - * Add the metadata for the API.get report - * In other plugins, this would hook on 'API.getReportMetadata' - */ - private function addApiGetMetdata(&$availableReports) - { - $metadata = array( - 'category' => Piwik_Translate('General_API'), - 'name' => Piwik_Translate('General_MainMetrics'), - 'module' => 'API', - 'action' => 'get', - 'metrics' => array(), - 'processedMetrics' => array(), - 'metricsDocumentation' => array(), - 'order' => 1 - ); - - $indexesToMerge = array('metrics', 'processedMetrics', 'metricsDocumentation'); - - foreach ($availableReports as $report) { - if ($report['action'] == 'get') { - foreach ($indexesToMerge as $index) { - if (isset($report[$index]) - && is_array($report[$index]) - ) { - $metadata[$index] = array_merge($metadata[$index], $report[$index]); - } - } - } - } + $reporter = new Piwik_API_ProcessedReport(); + $metadata = $reporter->getReportMetadata($idSites, $period, $date, $hideMetricsDoc, $showSubtableReports); + return $metadata; - $availableReports[] = $metadata; } public function getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment = false, $apiParameters = false, $idGoal = false, $language = false, $showTimer = true, $hideMetricsDoc = false, $idSubtable = false, $showRawMetrics = false) { - $timer = new Piwik_Timer(); - if ($apiParameters === false) { - $apiParameters = array(); - } - if (!empty($idGoal) - && empty($apiParameters['idGoal']) - ) { - $apiParameters['idGoal'] = $idGoal; - } - // Is this report found in the Metadata available reports? - $reportMetadata = $this->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, - $period, $date, $hideMetricsDoc, $showSubtableReports = true); - if (empty($reportMetadata)) { - throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite not found in the list of available reports. \n"); - } - $reportMetadata = reset($reportMetadata); - - // Generate Api call URL passing custom parameters - $parameters = array_merge($apiParameters, array( - 'method' => $apiModule . '.' . $apiAction, - 'idSite' => $idSite, - 'period' => $period, - 'date' => $date, - 'format' => 'original', - 'serialize' => '0', - 'language' => $language, - 'idSubtable' => $idSubtable, - )); - if (!empty($segment)) $parameters['segment'] = $segment; - - $url = Piwik_Url::getQueryStringFromParameters($parameters); - $request = new Piwik_API_Request($url); - try { - /** @var Piwik_DataTable */ - $dataTable = $request->process(); - } catch (Exception $e) { - throw new Exception("API returned an error: " . $e->getMessage() . "\n"); - } - - list($newReport, $columns, $rowsMetadata) = $this->handleTableReport($idSite, $dataTable, $reportMetadata, isset($reportMetadata['dimension']), $showRawMetrics); - foreach ($columns as $columnId => &$name) { - $name = ucfirst($name); - } - $website = new Piwik_Site($idSite); -// $segment = new Piwik_Segment($segment, $idSite); - - $period = Piwik_Period::advancedFactory($period, $date); - $period = $period->getLocalizedLongString(); - - $return = array( - 'website' => $website->getName(), - 'prettyDate' => $period, -// 'prettySegment' => $segment->getPrettyString(), - 'metadata' => $reportMetadata, - 'columns' => $columns, - 'reportData' => $newReport, - 'reportMetadata' => $rowsMetadata, - ); - if ($showTimer) { - $return['timerMillis'] = $timer->getTimeMs(0); - } - return $return; - } - - /** - * Enhance a $dataTable using metadata : - * - * - remove metrics based on $reportMetadata['metrics'] - * - add 0 valued metrics if $dataTable doesn't provide all $reportMetadata['metrics'] - * - format metric values to a 'human readable' format - * - extract row metadata to a separate Piwik_DataTable_Simple|Piwik_DataTable_Array : $rowsMetadata - * - translate metric names to a separate array : $columns - * - * @param int $idSite enables monetary value formatting based on site currency - * @param Piwik_DataTable|Piwik_DataTable_Array $dataTable - * @param array $reportMetadata - * @param boolean $hasDimension - * @return array Piwik_DataTable_Simple|Piwik_DataTable_Array $newReport with human readable format & array $columns list of translated column names & Piwik_DataTable_Simple|Piwik_DataTable_Array $rowsMetadata - **/ - private function handleTableReport($idSite, $dataTable, &$reportMetadata, $hasDimension, $showRawMetrics = false) - { - $columns = $reportMetadata['metrics']; - - if ($hasDimension) { - $columns = array_merge( - array('label' => $reportMetadata['dimension']), - $columns - ); - - if (isset($reportMetadata['processedMetrics'])) { - $processedMetricsAdded = $this->getDefaultProcessedMetrics(); - foreach ($processedMetricsAdded as $processedMetricId => $processedMetricTranslation) { - // this processed metric can be displayed for this report - if (isset($reportMetadata['processedMetrics'][$processedMetricId])) { - $columns[$processedMetricId] = $processedMetricTranslation; - } - } - } - - // Display the global Goal metrics - if (isset($reportMetadata['metricsGoal'])) { - $metricsGoalDisplay = array('revenue'); - // Add processed metrics to be displayed for this report - foreach ($metricsGoalDisplay as $goalMetricId) { - if (isset($reportMetadata['metricsGoal'][$goalMetricId])) { - $columns[$goalMetricId] = $reportMetadata['metricsGoal'][$goalMetricId]; - } - } - } - - if (isset($reportMetadata['processedMetrics'])) { - // Add processed metrics - $dataTable->filter('AddColumnsProcessedMetrics', array($deleteRowsWithNoVisit = false)); - } - } - - $columns = $this->hideShowMetrics($columns); - - // $dataTable is an instance of Piwik_DataTable_Array when multiple periods requested - if ($dataTable instanceof Piwik_DataTable_Array) { - // Need a new Piwik_DataTable_Array to store the 'human readable' values - $newReport = new Piwik_DataTable_Array(); - $newReport->setKeyName("prettyDate"); - - // Need a new Piwik_DataTable_Array to store report metadata - $rowsMetadata = new Piwik_DataTable_Array(); - $rowsMetadata->setKeyName("prettyDate"); - - // Process each Piwik_DataTable_Simple entry - foreach ($dataTable->getArray() as $label => $simpleDataTable) { - $this->removeEmptyColumns($columns, $reportMetadata, $simpleDataTable); - - list($enhancedSimpleDataTable, $rowMetadata) = $this->handleSimpleDataTable($idSite, $simpleDataTable, $columns, $hasDimension, $showRawMetrics); - $enhancedSimpleDataTable->metadata = $simpleDataTable->metadata; - - $period = $simpleDataTable->metadata['period']->getLocalizedLongString(); - $newReport->addTable($enhancedSimpleDataTable, $period); - $rowsMetadata->addTable($rowMetadata, $period); - } - } else { - $this->removeEmptyColumns($columns, $reportMetadata, $dataTable); - list($newReport, $rowsMetadata) = $this->handleSimpleDataTable($idSite, $dataTable, $columns, $hasDimension, $showRawMetrics); - } - - return array( - $newReport, - $columns, - $rowsMetadata - ); - } - - /** - * Removes metrics from the list of columns and the report meta data if they are marked empty - * in the data table meta data. - */ - private function removeEmptyColumns(&$columns, &$reportMetadata, $dataTable) - { - $emptyColumns = $dataTable->getMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); - - if (!is_array($emptyColumns)) { - return; - } - - $columns = $this->hideShowMetrics($columns, $emptyColumns); - - if (isset($reportMetadata['metrics'])) { - $reportMetadata['metrics'] = $this->hideShowMetrics($reportMetadata['metrics'], $emptyColumns); - } - - if (isset($reportMetadata['metricsDocumentation'])) { - $reportMetadata['metricsDocumentation'] = $this->hideShowMetrics($reportMetadata['metricsDocumentation'], $emptyColumns); - } - } - - /** - * Removes column names from an array based on the values in the hideColumns, - * showColumns query parameters. This is a hack that provides the ColumnDelete - * filter functionality in processed reports. - * - * @param array $columns List of metrics shown in a processed report. - * @param array $emptyColumns Empty columns from the data table meta data. - * @return array Filtered list of metrics. - */ - private function hideShowMetrics($columns, $emptyColumns = array()) - { - if (!is_array($columns)) { - return $columns; - } - - // remove columns if hideColumns query parameters exist - $columnsToRemove = Piwik_Common::getRequestVar('hideColumns', ''); - if ($columnsToRemove != '') { - $columnsToRemove = explode(',', $columnsToRemove); - foreach ($columnsToRemove as $name) { - // if a column to remove is in the column list, remove it - if (isset($columns[$name])) { - unset($columns[$name]); - } - } - } - - // remove columns if showColumns query parameters exist - $columnsToKeep = Piwik_Common::getRequestVar('showColumns', ''); - if ($columnsToKeep != '') { - $columnsToKeep = explode(',', $columnsToKeep); - $columnsToKeep[] = 'label'; - - foreach ($columns as $name => $ignore) { - // if the current column should not be kept, remove it - $idx = array_search($name, $columnsToKeep); - if ($idx === false) // if $name is not in $columnsToKeep - { - unset($columns[$name]); - } - } - } - - // remove empty columns - if (is_array($emptyColumns)) { - foreach ($emptyColumns as $column) { - if (isset($columns[$column])) { - unset($columns[$column]); - } - } - } - - return $columns; - } - - /** - * Enhance $simpleDataTable using metadata : - * - * - remove metrics based on $reportMetadata['metrics'] - * - add 0 valued metrics if $simpleDataTable doesn't provide all $reportMetadata['metrics'] - * - format metric values to a 'human readable' format - * - extract row metadata to a separate Piwik_DataTable_Simple $rowsMetadata - * - * @param int $idSite enables monetary value formatting based on site currency - * @param Piwik_DataTable_Simple $simpleDataTable - * @param array $metadataColumns - * @param boolean $hasDimension - * @param bool $returnRawMetrics If set to true, the original metrics will be returned - * - * @return array Piwik_DataTable $enhancedDataTable filtered metrics with human readable format & Piwik_DataTable_Simple $rowsMetadata - */ - private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false) - { - // new DataTable to store metadata - $rowsMetadata = new Piwik_DataTable(); - - // new DataTable to store 'human readable' values - if ($hasDimension) { - $enhancedDataTable = new Piwik_DataTable(); - } else { - $enhancedDataTable = new Piwik_DataTable_Simple(); - } - - // add missing metrics - foreach ($simpleDataTable->getRows() as $row) { - $rowMetrics = $row->getColumns(); - foreach ($metadataColumns as $id => $name) { - if (!isset($rowMetrics[$id])) { - $row->addColumn($id, 0); - } - } - } - - foreach ($simpleDataTable->getRows() as $row) { - $enhancedRow = new Piwik_DataTable_Row(); - $enhancedDataTable->addRow($enhancedRow); - $rowMetrics = $row->getColumns(); - foreach ($rowMetrics as $columnName => $columnValue) { - // filter metrics according to metadata definition - if (isset($metadataColumns[$columnName])) { - // generate 'human readable' metric values - $prettyValue = Piwik::getPrettyValue($idSite, $columnName, $columnValue, $htmlAllowed = false); - $enhancedRow->addColumn($columnName, $prettyValue); - } // For example the Maps Widget requires the raw metrics to do advanced datavis - elseif ($returnRawMetrics) { - $enhancedRow->addColumn($columnName, $columnValue); - } - } - - // If report has a dimension, extract metadata into a distinct DataTable - if ($hasDimension) { - $rowMetadata = $row->getMetadata(); - $idSubDataTable = $row->getIdSubDataTable(); - - // Create a row metadata only if there are metadata to insert - if (count($rowMetadata) > 0 || !is_null($idSubDataTable)) { - $metadataRow = new Piwik_DataTable_Row(); - $rowsMetadata->addRow($metadataRow); - - foreach ($rowMetadata as $metadataKey => $metadataValue) { - $metadataRow->addColumn($metadataKey, $metadataValue); - } - - if (!is_null($idSubDataTable)) { - $metadataRow->addColumn('idsubdatatable', $idSubDataTable); - } - } - } - } + $reporter = new Piwik_API_ProcessedReport(); + $processed = $reporter->getProcessedReport( $idSite, $period, $date, $apiModule, $apiAction, $segment, + $apiParameters, $idGoal, $language, $showTimer, $hideMetricsDoc, $idSubtable, $showRawMetrics); - return array( - $enhancedDataTable, - $rowsMetadata - ); - } - - /** - * API metadata are sorted by category/name, - * with a little tweak to replicate the standard Piwik category ordering - * - * @param string $a - * @param string $b - * @return int - */ - private function sort($a, $b) - { - static $order = null; - if (is_null($order)) { - $order = array( - Piwik_Translate('General_MultiSitesSummary'), - Piwik_Translate('VisitsSummary_VisitsSummary'), - Piwik_Translate('Goals_Ecommerce'), - Piwik_Translate('Actions_Actions'), - Piwik_Translate('Actions_SubmenuSitesearch'), - Piwik_Translate('Referers_Referers'), - Piwik_Translate('Goals_Goals'), - Piwik_Translate('General_Visitors'), - Piwik_Translate('UserSettings_VisitorSettings'), - ); - } - return ($category = strcmp(array_search($a['category'], $order), array_search($b['category'], $order))) == 0 - ? (@$a['order'] < @$b['order'] ? -1 : 1) - : $category; + return $processed; } /** @@ -1115,7 +497,6 @@ class Piwik_API_API return $mergedDataTable; } - /** * Merge the columns of two data tables. * Manipulates the first table. @@ -1141,7 +522,6 @@ class Piwik_API_API } } - /** * Given an API report to query (eg. "Referers.getKeywords", and a Label (eg. "free%20software"), * this function will query the API for the previous days/weeks/etc. and will return @@ -1151,460 +531,9 @@ class Piwik_API_API */ public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true) { - // validation of requested $period & $date - if ($period == 'range') { - // load days in the range - $period = 'day'; - } - - if (!Piwik_Archive::isMultiplePeriod($date, $period)) { - throw new Exception("Row evolutions can not be processed with this combination of \'date\' and \'period\' parameters."); - } - - // this is needed because Piwik_API_Proxy uses Piwik_Common::getRequestVar which in turn - // uses Piwik_Common::sanitizeInputValue. This causes the > that separates recursive labels - // to become > and we need to undo that here. - $label = Piwik_Common::unsanitizeInputValue($label); - - if ($label) { - $labels = explode(',', $label); - $labels = array_unique($labels); - } else { - $labels = array(); - } - - $dataTable = $this->loadRowEvolutionDataFromAPI($idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $idGoal); - - if (empty($labels)) { - // if no labels specified, use all possible labels as list - foreach ($dataTable->getArray() as $table) { - $labels = array_merge($labels, $table->getColumn('label')); - } - $labels = array_values(array_unique($labels)); - - // if the filter_limit query param is set, treat it as a request to limit - // the number of labels used - $limit = Piwik_Common::getRequestVar('filter_limit', false); - if ($limit != false - && $limit >= 0 - ) { - $labels = array_slice($labels, 0, $limit); - } - - // set label index metadata - $labelsToIndex = array_flip($labels); - foreach ($dataTable->getArray() as $table) { - foreach ($table->getRows() as $row) { - $label = $row->getColumn('label'); - if (isset($labelsToIndex[$label])) { - $row->setMetadata('label_index', $labelsToIndex[$label]); - } - } - } - } - - if (count($labels) != 1) { - $data = $this->getMultiRowEvolution( - $dataTable, - $idSite, - $period, - $date, - $apiModule, - $apiAction, - $labels, - $segment, - $column, - $language, - $idGoal, - $legendAppendMetric, - $labelUseAbsoluteUrl - ); - } else { - $data = $this->getSingleRowEvolution( - $dataTable, - $idSite, - $period, - $date, - $apiModule, - $apiAction, - $labels[0], - $segment, - $language, - $idGoal, - $labelUseAbsoluteUrl - ); - } - return $data; - } - - /** - * Get row evolution for a single label - * @return array containing report data, metadata, label, logo - */ - private function getSingleRowEvolution($dataTable, $idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $language = false, $idGoal = false, $labelUseAbsoluteUrl = true) - { - $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal); - $metricNames = array_keys($metadata['metrics']); - - $logo = $actualLabel = false; - $urlFound = false; - foreach ($dataTable->getArray() as $date => $subTable) { - /** @var $subTable Piwik_DataTable */ - $subTable->applyQueuedFilters(); - if ($subTable->getRowsCount() > 0) { - /** @var $row Piwik_DataTable_Row */ - $row = $subTable->getFirstRow(); - - if (!$actualLabel) { - $logo = $row->getMetadata('logo'); - - $actualLabel = $this->getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl); - $urlFound = $actualLabel !== false; - if (empty($actualLabel)) { - $actualLabel = $row->getColumn('label'); - } - - } - - // remove all columns that are not in the available metrics. - // this removes the label as well (which is desired for two reasons: (1) it was passed - // in the request, (2) it would cause the evolution graph to show the label in the legend). - foreach ($row->getColumns() as $column => $value) { - if (!in_array($column, $metricNames)) { - $row->deleteColumn($column); - } - } - - $row->deleteMetadata(); - } - } - - $this->enhanceRowEvolutionMetaData($metadata, $dataTable); - - // if we have a recursive label and no url, use the path - if (!$urlFound) { - $actualLabel = str_replace(Piwik_API_DataTableManipulator_LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label); - } - - $return = array( - 'label' => Piwik_DataTable_Filter_SafeDecodeLabel::safeDecodeLabel($actualLabel), - 'reportData' => $dataTable, - 'metadata' => $metadata - ); - if (!empty($logo)) { - $return['logo'] = $logo; - } - return $return; - } - - private function getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl) - { - $url = $row->getMetadata('url'); - if ($url - && ($apiModule == 'Actions' - || ($apiModule == 'Referers' - && $apiAction == 'getWebsites')) - && $labelUseAbsoluteUrl - ) { - $actualLabel = preg_replace(';^http(s)?://(www.)?;i', '', $url); - return $actualLabel; - } - return false; - } - - /** - * @param $idSite - * @param $period - * @param $date - * @param $apiModule - * @param $apiAction - * @param $label - * @param $segment - * @param $idGoal - * @throws Exception - * @return Piwik_DataTable_Array|Piwik_DataTable - */ - private function loadRowEvolutionDataFromAPI($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $idGoal = false) - { - if (!is_array($label)) { - $label = array($label); - } - $label = array_map('rawurlencode', $label); - - $parameters = array( - 'method' => $apiModule . '.' . $apiAction, - 'label' => $label, - 'idSite' => $idSite, - 'period' => $period, - 'date' => $date, - 'format' => 'original', - 'serialize' => '0', - 'segment' => $segment, - 'idGoal' => $idGoal, - - // data for row evolution should NOT be limited - 'filter_limit' => -1, - - // if more than one label is used, we add metadata to ensure we know which - // row corresponds with which label (since the labels can change, and rows - // can be sorted in a different order) - 'labelFilterAddLabelIndex' => count($label) > 1 ? 1 : 0, - ); - - // add "processed metrics" like actions per visit or bounce rate - // note: some reports should not be filtered with AddColumnProcessedMetrics - // specifically, reports without the Piwik_Archive::INDEX_NB_VISITS metric such as Goals.getVisitsUntilConversion & Goal.getDaysToConversion - // this is because the AddColumnProcessedMetrics filter removes all datable rows lacking this metric - if - ( - $apiModule != 'Actions' - && - ($apiModule != 'Goals' || ($apiAction != 'getVisitsUntilConversion' && $apiAction != 'getDaysToConversion')) - && !empty($label) - ) { - $parameters['filter_add_columns_when_show_all_columns'] = '1'; - } - - $url = Piwik_Url::getQueryStringFromParameters($parameters); - - $request = new Piwik_API_Request($url); - - try { - $dataTable = $request->process(); - } catch (Exception $e) { - throw new Exception("API returned an error: " . $e->getMessage() . "\n"); - } - - return $dataTable; - } - - /** - * For a given API report, returns a simpler version - * of the metadata (will return only the metrics and the dimension name) - * @param $idSite - * @param $period - * @param $date - * @param $apiModule - * @param $apiAction - * @param $language - * @param $idGoal - * @throws Exception - * @return array - */ - private function getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal = false) - { - $apiParameters = array(); - if (!empty($idGoal) && $idGoal > 0) { - $apiParameters = array('idGoal' => $idGoal); - } - $reportMetadata = $this->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, $period, $date, $hideMetricsDoc = false, $showSubtableReports = true); - - if (empty($reportMetadata)) { - throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite " - . "not found in the list of available reports. \n"); - } - - $reportMetadata = reset($reportMetadata); - - $metrics = $reportMetadata['metrics']; - if (isset($reportMetadata['processedMetrics']) && is_array($reportMetadata['processedMetrics'])) { - $metrics = $metrics + $reportMetadata['processedMetrics']; - } - - $dimension = $reportMetadata['dimension']; - - return compact('metrics', 'dimension'); - } - - /** - * Given the Row evolution dataTable, and the associated metadata, - * enriches the metadata with min/max values, and % change between the first period and the last one - * @param array $metadata - * @param Piwik_DataTable_Array $dataTable - */ - private function enhanceRowEvolutionMetaData(&$metadata, $dataTable) - { - // prepare result array for metrics - $metricsResult = array(); - foreach ($metadata['metrics'] as $metric => $name) { - $metricsResult[$metric] = array('name' => $name); - - if (!empty($metadata['logos'][$metric])) { - $metricsResult[$metric]['logo'] = $metadata['logos'][$metric]; - } - } - unset($metadata['logos']); - - $subDataTables = $dataTable->getArray(); - $firstDataTable = reset($subDataTables); - $firstDataTableRow = $firstDataTable->getFirstRow(); - $lastDataTable = end($subDataTables); - $lastDataTableRow = $lastDataTable->getFirstRow(); - - // Process min/max values - $firstNonZeroFound = array(); - foreach ($subDataTables as $subDataTable) { - // $subDataTable is the report for one period, it has only one row - $firstRow = $subDataTable->getFirstRow(); - foreach ($metadata['metrics'] as $metric => $label) { - $value = $firstRow ? floatval($firstRow->getColumn($metric)) : 0; - if ($value > 0) { - $firstNonZeroFound[$metric] = true; - } else if (!isset($firstNonZeroFound[$metric])) { - continue; - } - if (!isset($metricsResult[$metric]['min']) - || $metricsResult[$metric]['min'] > $value - ) { - $metricsResult[$metric]['min'] = $value; - } - if (!isset($metricsResult[$metric]['max']) - || $metricsResult[$metric]['max'] < $value - ) { - $metricsResult[$metric]['max'] = $value; - } - } - } - - // Process % change between first/last values - foreach ($metadata['metrics'] as $metric => $label) { - $first = $firstDataTableRow ? floatval($firstDataTableRow->getColumn($metric)) : 0; - $last = $lastDataTableRow ? floatval($lastDataTableRow->getColumn($metric)) : 0; - - // do not calculate evolution if the first value is 0 (to avoid divide-by-zero) - if ($first == 0) { - continue; - } - - $change = Piwik_DataTable_Filter_CalculateEvolutionFilter::calculate($last, $first, $quotientPrecision = 0); - $change = Piwik_DataTable_Filter_CalculateEvolutionFilter::prependPlusSignToNumber($change); - $metricsResult[$metric]['change'] = $change; - } - - $metadata['metrics'] = $metricsResult; - } - - /** Get row evolution for a multiple labels */ - private function getMultiRowEvolution($dataTable, $idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $column, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true) - { - $actualLabels = $logos = array(); - - $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal); - - if (!isset($metadata['metrics'][$column])) { - // invalid column => use the first one that's available - $metrics = array_keys($metadata['metrics']); - $column = reset($metrics); - } - - // get the processed label and logo (if any) for every requested label - $actualLabels = $logos = array(); - foreach ($labels as $labelIdx => $label) { - foreach ($dataTable->getArray() as $table) { - $labelRow = $this->getRowEvolutionRowFromLabelIdx($table, $labelIdx); - - if ($labelRow) { - $actualLabels[$labelIdx] = $this->getRowUrlForEvolutionLabel( - $labelRow, $apiModule, $apiAction, $labelUseAbsoluteUrl); - - $logos[$labelIdx] = $labelRow->getMetadata('logo'); - - if (!empty($actualLabels[$labelIdx])) { - break; - } - } - } - - if (empty($actualLabels[$labelIdx])) { - $actualLabels[$labelIdx] = $this->cleanOriginalLabel($label); - } - } - - // convert rows to be array($column.'_'.$labelIdx => $value) as opposed to - // array('label' => $label, 'column' => $value). - $dataTableMulti = $dataTable->getEmptyClone(); - foreach ($dataTable->getArray() as $tableLabel => $table) { - $newRow = new Piwik_DataTable_Row(); - - foreach ($labels as $labelIdx => $label) { - $row = $this->getRowEvolutionRowFromLabelIdx($table, $labelIdx); - - $value = 0; - if ($row) { - $value = $row->getColumn($column); - $value = floatVal(str_replace(',', '.', $value)); - } - - if ($value == '') { - $value = 0; - } - - $newLabel = $column . '_' . (int)$labelIdx; - $newRow->addColumn($newLabel, $value); - } - - $newTable = $table->getEmptyClone(); - if (!empty($labels)) { // only add a row if the row has data (no labels === no data) - $newTable->addRow($newRow); - } - - $dataTableMulti->addTable($newTable, $tableLabel); - } - - // the available metrics for the report are returned as metadata / columns - $metadata['columns'] = $metadata['metrics']; - - // metadata / metrics should document the rows that are compared - // this way, UI code can be reused - $metadata['metrics'] = array(); - foreach ($actualLabels as $labelIndex => $label) { - if ($legendAppendMetric) { - $label .= ' (' . $metadata['columns'][$column] . ')'; - } - $metricName = $column . '_' . $labelIndex; - $metadata['metrics'][$metricName] = Piwik_DataTable_Filter_SafeDecodeLabel::safeDecodeLabel($label); - - if (!empty($logos[$labelIndex])) { - $metadata['logos'][$metricName] = $logos[$labelIndex]; - } - } - - $this->enhanceRowEvolutionMetaData($metadata, $dataTableMulti); - - return array( - 'column' => $column, - 'reportData' => $dataTableMulti, - 'metadata' => $metadata - ); - } - - /** - * Returns the row in a datatable by its label_index metadata. - * - * @param Piwik_DataTable $table - * @param int $labelIdx - * @return Piwik_DataTable_Row|false - */ - private function getRowEvolutionRowFromLabelIdx($table, $labelIdx) - { - $labelIdx = (int)$labelIdx; - foreach ($table->getRows() as $row) - { - if ($row->getMetadata('label_index') === $labelIdx) - { - return $row; - } - } - return false; - } - - /** - * Returns a prettier, more comprehensible version of a row evolution label - * for display. - */ - private function cleanOriginalLabel($label) - { - return str_replace(Piwik_API_DataTableManipulator_LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label); + $rowEvolution = new Piwik_API_RowEvolution(); + return $rowEvolution->getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $column, + $language, $idGoal, $legendAppendMetric, $labelUseAbsoluteUrl); } /** @@ -1618,11 +547,12 @@ class Piwik_API_API return array(); } - $urls = Piwik_Common::unsanitizeInputValues($urls); + $urls = array_map('urldecode', $urls); + $urls = array_map(array('Piwik_Common', 'unsanitizeInputValue'), $urls); $result = array(); foreach ($urls as $url) { - $req = new Piwik_API_Request($url); + $req = new Piwik_API_Request($url . '&format=php&serialize=0'); $result[] = $req->process(); } return $result; @@ -1641,13 +571,13 @@ class Piwik_API_API $segmentsMetadata = $this->getSegmentsMetadata($idSite, $_hideImplementationData = false); $segmentFound = false; - foreach($segmentsMetadata as $segmentMetadata) { - if($segmentMetadata['segment'] == $segmentName) { + foreach ($segmentsMetadata as $segmentMetadata) { + if ($segmentMetadata['segment'] == $segmentName) { $segmentFound = $segmentMetadata; break; } } - if(empty($segmentFound)) { + if (empty($segmentFound)) { throw new Exception("Requested segment not found."); } @@ -1662,10 +592,10 @@ class Piwik_API_API // Select non empty fields only // Note: this optimization has only a very minor impact - $requestLastVisits.= "&segment=$segmentName" . Piwik_SegmentExpression::MATCH_IS_NOT_NULL . "null"; + $requestLastVisits .= "&segment=$segmentName" . urlencode('!='); // By default Live fetches all actions for all visitors, but we'd rather do this only when required - if($this->doesSegmentNeedActionsData($segmentName)) { + if ($this->doesSegmentNeedActionsData($segmentName)) { $requestLastVisits .= "&filter_limit=500"; } else { $requestLastVisits .= "&doNotFetchActions=1"; @@ -1674,7 +604,7 @@ class Piwik_API_API $request = new Piwik_API_Request($requestLastVisits); $table = $request->process(); - if(empty($table)) { + if (empty($table)) { throw new Exception("There was no data to suggest for $segmentName"); } @@ -1686,14 +616,14 @@ class Piwik_API_API $values = array_merge($values, $valuesBis); // remove false values (while keeping zeros) - $values = array_filter( $values, 'strlen' ); + $values = array_filter($values, 'strlen'); // we have a list of all values. let's show the most frequently used first. - $values = array_count_values( $values ); + $values = array_count_values($values); arsort($values); $values = array_keys($values); - $values = array_map( array('Piwik_Common', 'unsanitizeInputValue'), $values); + $values = array_map(array('Piwik_Common', 'unsanitizeInputValue'), $values); $values = array_slice($values, 0, $maxSuggestionsToReturn); return $values; @@ -1712,5 +642,4 @@ class Piwik_API_API $doesSegmentNeedActionsInfo = in_array($segmentName, $segmentsNeedActionsInfo) || $isCustomVariablePage; return $doesSegmentNeedActionsInfo; } - } diff --git a/plugins/API/ProcessedReport.php b/plugins/API/ProcessedReport.php new file mode 100644 index 0000000000..f37a2c6527 --- /dev/null +++ b/plugins/API/ProcessedReport.php @@ -0,0 +1,543 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik_Plugins + * @package Piwik_API + */ + + +class Piwik_API_ProcessedReport +{ + + /** + * Loads reports metadata, then return the requested one, + * matching optional API parameters. + */ + public function getMetadata($idSite, $apiModule, $apiAction, $apiParameters = array(), $language = false, + $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false) + { + $reportsMetadata = $this->getReportMetadata($idSite, $period, $date, $hideMetricsDoc, $showSubtableReports); + + foreach ($reportsMetadata as $report) { + // See ArchiveProcessor/Period.php - unique visitors are not processed for period != day + if (($period && $period != 'day') && !($apiModule == 'VisitsSummary' && $apiAction == 'get')) { + unset($report['metrics']['nb_uniq_visitors']); + } + if ($report['module'] == $apiModule + && $report['action'] == $apiAction + ) { + // No custom parameters + if (empty($apiParameters) + && empty($report['parameters']) + ) { + return array($report); + } + if (empty($report['parameters'])) { + continue; + } + $diff = array_diff($report['parameters'], $apiParameters); + if (empty($diff)) { + return array($report); + } + } + } + return false; + } + + /** + * Triggers a hook to ask plugins for available Reports. + * Returns metadata information about each report (category, name, dimension, metrics, etc.) + * + * @param string $idSites Comma separated list of website Ids + * @return array + */ + public function getReportMetadata($idSites, $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false) + { + $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); + if (!empty($idSites)) { + Piwik::checkUserHasViewAccess($idSites); + } + + $parameters = array('idSites' => $idSites, 'period' => $period, 'date' => $date); + + $availableReports = array(); + Piwik_PostEvent('API.getReportMetadata', $availableReports, $parameters); + foreach ($availableReports as &$availableReport) { + if (!isset($availableReport['metrics'])) { + $availableReport['metrics'] = Piwik_Metrics::getDefaultMetrics(); + } + if (!isset($availableReport['processedMetrics'])) { + $availableReport['processedMetrics'] = Piwik_Metrics::getDefaultProcessedMetrics(); + } + + if ($hideMetricsDoc) // remove metric documentation if it's not wanted + { + unset($availableReport['metricsDocumentation']); + } else if (!isset($availableReport['metricsDocumentation'])) { + // set metric documentation to default if it's not set + $availableReport['metricsDocumentation'] = Piwik_Metrics::getDefaultMetricsDocumentation(); + } + } + + // Some plugins need to add custom metrics after all plugins hooked in + Piwik_PostEvent('API.getReportMetadata.end', $availableReports, $parameters); + // Oh this is not pretty! Until we have event listeners order parameter... + Piwik_PostEvent('API.getReportMetadata.end.end', $availableReports, $parameters); + + // Sort results to ensure consistent order + usort($availableReports, array($this, 'sort')); + + // Add the magic API.get report metadata aggregating all plugins API.get API calls automatically + $this->addApiGetMetdata($availableReports); + + $knownMetrics = array_merge(Piwik_Metrics::getDefaultMetrics(), Piwik_Metrics::getDefaultProcessedMetrics()); + foreach ($availableReports as &$availableReport) { + // Ensure all metrics have a translation + $metrics = $availableReport['metrics']; + $cleanedMetrics = array(); + foreach ($metrics as $metricId => $metricTranslation) { + // When simply the column name was given, ie 'metric' => array( 'nb_visits' ) + // $metricTranslation is in this case nb_visits. We look for a known translation. + if (is_numeric($metricId) + && isset($knownMetrics[$metricTranslation]) + ) { + $metricId = $metricTranslation; + $metricTranslation = $knownMetrics[$metricTranslation]; + } + $cleanedMetrics[$metricId] = $metricTranslation; + } + $availableReport['metrics'] = $cleanedMetrics; + + // if hide/show columns specified, hide/show metrics & docs + $availableReport['metrics'] = $this->hideShowMetrics($availableReport['metrics']); + if (isset($availableReport['processedMetrics'])) { + $availableReport['processedMetrics'] = $this->hideShowMetrics($availableReport['processedMetrics']); + } + if (isset($availableReport['metricsDocumentation'])) { + $availableReport['metricsDocumentation'] = + $this->hideShowMetrics($availableReport['metricsDocumentation']); + } + + // Remove array elements that are false (to clean up API output) + foreach ($availableReport as $attributeName => $attributeValue) { + if (empty($attributeValue)) { + unset($availableReport[$attributeName]); + } + } + // when there are per goal metrics, don't display conversion_rate since it can differ from per goal sum + if (isset($availableReport['metricsGoal'])) { + unset($availableReport['processedMetrics']['conversion_rate']); + unset($availableReport['metricsGoal']['conversion_rate']); + } + + // Processing a uniqueId for each report, + // can be used by UIs as a key to match a given report + $uniqueId = $availableReport['module'] . '_' . $availableReport['action']; + if (!empty($availableReport['parameters'])) { + foreach ($availableReport['parameters'] as $key => $value) { + $uniqueId .= '_' . $key . '--' . $value; + } + } + $availableReport['uniqueId'] = $uniqueId; + + // Order is used to order reports internally, but not meant to be used outside + unset($availableReport['order']); + } + + // remove subtable reports + if (!$showSubtableReports) { + foreach ($availableReports as $idx => $report) { + if (isset($report['isSubtableReport']) && $report['isSubtableReport']) { + unset($availableReports[$idx]); + } + } + } + + return array_values($availableReports); // make sure array has contiguous key values + } + + + /** + * API metadata are sorted by category/name, + * with a little tweak to replicate the standard Piwik category ordering + * + * @param string $a + * @param string $b + * @return int + */ + private function sort($a, $b) + { + static $order = null; + if (is_null($order)) { + $order = array( + Piwik_Translate('General_MultiSitesSummary'), + Piwik_Translate('VisitsSummary_VisitsSummary'), + Piwik_Translate('Goals_Ecommerce'), + Piwik_Translate('Actions_Actions'), + Piwik_Translate('Actions_SubmenuSitesearch'), + Piwik_Translate('Referers_Referers'), + Piwik_Translate('Goals_Goals'), + Piwik_Translate('General_Visitors'), + Piwik_Translate('DevicesDetection_DevicesDetection'), + Piwik_Translate('UserSettings_VisitorSettings'), + ); + } + return ($category = strcmp(array_search($a['category'], $order), array_search($b['category'], $order))) == 0 + ? (@$a['order'] < @$b['order'] ? -1 : 1) + : $category; + } + + /** + * Add the metadata for the API.get report + * In other plugins, this would hook on 'API.getReportMetadata' + */ + private function addApiGetMetdata(&$availableReports) + { + $metadata = array( + 'category' => Piwik_Translate('General_API'), + 'name' => Piwik_Translate('General_MainMetrics'), + 'module' => 'API', + 'action' => 'get', + 'metrics' => array(), + 'processedMetrics' => array(), + 'metricsDocumentation' => array(), + 'order' => 1 + ); + + $indexesToMerge = array('metrics', 'processedMetrics', 'metricsDocumentation'); + + foreach ($availableReports as $report) { + if ($report['action'] == 'get') { + foreach ($indexesToMerge as $index) { + if (isset($report[$index]) + && is_array($report[$index]) + ) { + $metadata[$index] = array_merge($metadata[$index], $report[$index]); + } + } + } + } + + $availableReports[] = $metadata; + } + + + + public function getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment = false, + $apiParameters = false, $idGoal = false, $language = false, + $showTimer = true, $hideMetricsDoc = false, $idSubtable = false, $showRawMetrics = false) + { + $timer = new Piwik_Timer(); + if (empty($apiParameters)) { + $apiParameters = array(); + } + if (!empty($idGoal) + && empty($apiParameters['idGoal']) + ) { + $apiParameters['idGoal'] = $idGoal; + } + // Is this report found in the Metadata available reports? + $reportMetadata = $this->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, + $period, $date, $hideMetricsDoc, $showSubtableReports = true); + if (empty($reportMetadata)) { + throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite not found in the list of available reports. \n"); + } + $reportMetadata = reset($reportMetadata); + + // Generate Api call URL passing custom parameters + $parameters = array_merge($apiParameters, array( + 'method' => $apiModule . '.' . $apiAction, + 'idSite' => $idSite, + 'period' => $period, + 'date' => $date, + 'format' => 'original', + 'serialize' => '0', + 'language' => $language, + 'idSubtable' => $idSubtable, + )); + if (!empty($segment)) $parameters['segment'] = $segment; + + $url = Piwik_Url::getQueryStringFromParameters($parameters); + $request = new Piwik_API_Request($url); + try { + /** @var Piwik_DataTable */ + $dataTable = $request->process(); + } catch (Exception $e) { + throw new Exception("API returned an error: " . $e->getMessage() . " at " . basename($e->getFile()) . ":" . $e->getLine() . "\n"); + } + + list($newReport, $columns, $rowsMetadata) = $this->handleTableReport($idSite, $dataTable, $reportMetadata, $showRawMetrics); + foreach ($columns as $columnId => &$name) { + $name = ucfirst($name); + } + $website = new Piwik_Site($idSite); + + $period = Piwik_Period::advancedFactory($period, $date); + $period = $period->getLocalizedLongString(); + + $return = array( + 'website' => $website->getName(), + 'prettyDate' => $period, + 'metadata' => $reportMetadata, + 'columns' => $columns, + 'reportData' => $newReport, + 'reportMetadata' => $rowsMetadata, + ); + if ($showTimer) { + $return['timerMillis'] = $timer->getTimeMs(0); + } + return $return; + } + + /** + * Enhance a $dataTable using metadata : + * + * - remove metrics based on $reportMetadata['metrics'] + * - add 0 valued metrics if $dataTable doesn't provide all $reportMetadata['metrics'] + * - format metric values to a 'human readable' format + * - extract row metadata to a separate Piwik_DataTable_Simple|Piwik_DataTable_Array : $rowsMetadata + * - translate metric names to a separate array : $columns + * + * @param int $idSite enables monetary value formatting based on site currency + * @param Piwik_DataTable|Piwik_DataTable_Array $dataTable + * @param array $reportMetadata + * @param boolean $hasDimension + * @return array Piwik_DataTable_Simple|Piwik_DataTable_Array $newReport with human readable format & array $columns list of translated column names & Piwik_DataTable_Simple|Piwik_DataTable_Array $rowsMetadata + **/ + private function handleTableReport($idSite, $dataTable, &$reportMetadata, $showRawMetrics = false) + { + $hasDimension = isset($reportMetadata['dimension']); + $columns = $reportMetadata['metrics']; + + if ($hasDimension) { + $columns = array_merge( + array('label' => $reportMetadata['dimension']), + $columns + ); + + if (isset($reportMetadata['processedMetrics'])) { + $processedMetricsAdded = Piwik_Metrics::getDefaultProcessedMetrics(); + foreach ($processedMetricsAdded as $processedMetricId => $processedMetricTranslation) { + // this processed metric can be displayed for this report + if (isset($reportMetadata['processedMetrics'][$processedMetricId])) { + $columns[$processedMetricId] = $processedMetricTranslation; + } + } + } + + // Display the global Goal metrics + if (isset($reportMetadata['metricsGoal'])) { + $metricsGoalDisplay = array('revenue'); + // Add processed metrics to be displayed for this report + foreach ($metricsGoalDisplay as $goalMetricId) { + if (isset($reportMetadata['metricsGoal'][$goalMetricId])) { + $columns[$goalMetricId] = $reportMetadata['metricsGoal'][$goalMetricId]; + } + } + } + + if (isset($reportMetadata['processedMetrics'])) { + // Add processed metrics + $dataTable->filter('AddColumnsProcessedMetrics', array($deleteRowsWithNoVisit = false)); + } + } + + $columns = $this->hideShowMetrics($columns); + + // $dataTable is an instance of Piwik_DataTable_Array when multiple periods requested + if ($dataTable instanceof Piwik_DataTable_Array) { + // Need a new Piwik_DataTable_Array to store the 'human readable' values + $newReport = new Piwik_DataTable_Array(); + $newReport->setKeyName("prettyDate"); + + // Need a new Piwik_DataTable_Array to store report metadata + $rowsMetadata = new Piwik_DataTable_Array(); + $rowsMetadata->setKeyName("prettyDate"); + + // Process each Piwik_DataTable_Simple entry + foreach ($dataTable->getArray() as $label => $simpleDataTable) { + $this->removeEmptyColumns($columns, $reportMetadata, $simpleDataTable); + + list($enhancedSimpleDataTable, $rowMetadata) = $this->handleSimpleDataTable($idSite, $simpleDataTable, $columns, $hasDimension, $showRawMetrics); + $enhancedSimpleDataTable->metadata = $simpleDataTable->metadata; + + $period = $simpleDataTable->metadata['period']->getLocalizedLongString(); + $newReport->addTable($enhancedSimpleDataTable, $period); + $rowsMetadata->addTable($rowMetadata, $period); + } + } else { + $this->removeEmptyColumns($columns, $reportMetadata, $dataTable); + list($newReport, $rowsMetadata) = $this->handleSimpleDataTable($idSite, $dataTable, $columns, $hasDimension, $showRawMetrics); + } + + return array( + $newReport, + $columns, + $rowsMetadata + ); + } + + /** + * Removes metrics from the list of columns and the report meta data if they are marked empty + * in the data table meta data. + */ + private function removeEmptyColumns(&$columns, &$reportMetadata, $dataTable) + { + $emptyColumns = $dataTable->getMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); + + if (!is_array($emptyColumns)) { + return; + } + + $columns = $this->hideShowMetrics($columns, $emptyColumns); + + if (isset($reportMetadata['metrics'])) { + $reportMetadata['metrics'] = $this->hideShowMetrics($reportMetadata['metrics'], $emptyColumns); + } + + if (isset($reportMetadata['metricsDocumentation'])) { + $reportMetadata['metricsDocumentation'] = $this->hideShowMetrics($reportMetadata['metricsDocumentation'], $emptyColumns); + } + } + + /** + * Removes column names from an array based on the values in the hideColumns, + * showColumns query parameters. This is a hack that provides the ColumnDelete + * filter functionality in processed reports. + * + * @param array $columns List of metrics shown in a processed report. + * @param array $emptyColumns Empty columns from the data table meta data. + * @return array Filtered list of metrics. + */ + private function hideShowMetrics($columns, $emptyColumns = array()) + { + if (!is_array($columns)) { + return $columns; + } + + // remove columns if hideColumns query parameters exist + $columnsToRemove = Piwik_Common::getRequestVar('hideColumns', ''); + if ($columnsToRemove != '') { + $columnsToRemove = explode(',', $columnsToRemove); + foreach ($columnsToRemove as $name) { + // if a column to remove is in the column list, remove it + if (isset($columns[$name])) { + unset($columns[$name]); + } + } + } + + // remove columns if showColumns query parameters exist + $columnsToKeep = Piwik_Common::getRequestVar('showColumns', ''); + if ($columnsToKeep != '') { + $columnsToKeep = explode(',', $columnsToKeep); + $columnsToKeep[] = 'label'; + + foreach ($columns as $name => $ignore) { + // if the current column should not be kept, remove it + $idx = array_search($name, $columnsToKeep); + if ($idx === false) // if $name is not in $columnsToKeep + { + unset($columns[$name]); + } + } + } + + // remove empty columns + if (is_array($emptyColumns)) { + foreach ($emptyColumns as $column) { + if (isset($columns[$column])) { + unset($columns[$column]); + } + } + } + + return $columns; + } + + /** + * Enhance $simpleDataTable using metadata : + * + * - remove metrics based on $reportMetadata['metrics'] + * - add 0 valued metrics if $simpleDataTable doesn't provide all $reportMetadata['metrics'] + * - format metric values to a 'human readable' format + * - extract row metadata to a separate Piwik_DataTable_Simple $rowsMetadata + * + * @param int $idSite enables monetary value formatting based on site currency + * @param Piwik_DataTable_Simple $simpleDataTable + * @param array $metadataColumns + * @param boolean $hasDimension + * @param bool $returnRawMetrics If set to true, the original metrics will be returned + * + * @return array Piwik_DataTable $enhancedDataTable filtered metrics with human readable format & Piwik_DataTable_Simple $rowsMetadata + */ + private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false) + { + // new DataTable to store metadata + $rowsMetadata = new Piwik_DataTable(); + + // new DataTable to store 'human readable' values + if ($hasDimension) { + $enhancedDataTable = new Piwik_DataTable(); + } else { + $enhancedDataTable = new Piwik_DataTable_Simple(); + } + + // add missing metrics + foreach ($simpleDataTable->getRows() as $row) { + $rowMetrics = $row->getColumns(); + foreach ($metadataColumns as $id => $name) { + if (!isset($rowMetrics[$id])) { + $row->addColumn($id, 0); + } + } + } + + foreach ($simpleDataTable->getRows() as $row) { + $enhancedRow = new Piwik_DataTable_Row(); + $enhancedDataTable->addRow($enhancedRow); + $rowMetrics = $row->getColumns(); + foreach ($rowMetrics as $columnName => $columnValue) { + // filter metrics according to metadata definition + if (isset($metadataColumns[$columnName])) { + // generate 'human readable' metric values + $prettyValue = Piwik::getPrettyValue($idSite, $columnName, $columnValue, $htmlAllowed = false); + $enhancedRow->addColumn($columnName, $prettyValue); + } // For example the Maps Widget requires the raw metrics to do advanced datavis + elseif ($returnRawMetrics) { + $enhancedRow->addColumn($columnName, $columnValue); + } + } + + // If report has a dimension, extract metadata into a distinct DataTable + if ($hasDimension) { + $rowMetadata = $row->getMetadata(); + $idSubDataTable = $row->getIdSubDataTable(); + + // Create a row metadata only if there are metadata to insert + if (count($rowMetadata) > 0 || !is_null($idSubDataTable)) { + $metadataRow = new Piwik_DataTable_Row(); + $rowsMetadata->addRow($metadataRow); + + foreach ($rowMetadata as $metadataKey => $metadataValue) { + $metadataRow->addColumn($metadataKey, $metadataValue); + } + + if (!is_null($idSubDataTable)) { + $metadataRow->addColumn('idsubdatatable', $idSubDataTable); + } + } + } + } + + return array( + $enhancedDataTable, + $rowsMetadata + ); + } + +}
\ No newline at end of file diff --git a/plugins/API/RowEvolution.php b/plugins/API/RowEvolution.php new file mode 100644 index 0000000000..2daac7083b --- /dev/null +++ b/plugins/API/RowEvolution.php @@ -0,0 +1,468 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + * @category Piwik_Plugins + * @package Piwik_API + */ + +/** + * This class generates a Row evolution dataset, from input request + * + * @package Piwik_API + */ +class Piwik_API_RowEvolution +{ + + public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true) + { + // validation of requested $period & $date + if ($period == 'range') { + // load days in the range + $period = 'day'; + } + + if (!Piwik_Period::isMultiplePeriod($date, $period)) { + throw new Exception("Row evolutions can not be processed with this combination of \'date\' and \'period\' parameters."); + } + + $label = Piwik_API_ResponseBuilder::unsanitizeLabelParameter($label); + $labels = Piwik::getArrayFromApiParameter($label); + + + $dataTable = $this->loadRowEvolutionDataFromAPI($idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $idGoal); + + if (empty($labels)) { + $labels = $this->getLabelsFromDataTable($dataTable, $labels); + $dataTable = $this->enrichRowAddMetadataLabelIndex($labels, $dataTable); + } + $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal); + + if (count($labels) != 1) { + $data = $this->getMultiRowEvolution( + $dataTable, + $metadata, + $apiModule, + $apiAction, + $labels, + $column, + $legendAppendMetric, + $labelUseAbsoluteUrl + ); + } else { + $data = $this->getSingleRowEvolution( + $dataTable, + $metadata, + $apiModule, + $apiAction, + $labels[0], + $labelUseAbsoluteUrl + ); + } + return $data; + } + + protected function enrichRowAddMetadataLabelIndex($labels, $dataTable) + { + // set label index metadata + $labelsToIndex = array_flip($labels); + foreach ($dataTable->getArray() as $table) { + foreach ($table->getRows() as $row) { + $label = $row->getColumn('label'); + if (isset($labelsToIndex[$label])) { + $row->setMetadata(Piwik_API_DataTableManipulator_LabelFilter::FLAG_IS_ROW_EVOLUTION, $labelsToIndex[$label]); + } + } + } + return $dataTable; + } + + protected function getLabelsFromDataTable($dataTable, $labels) + { + // if no labels specified, use all possible labels as list + foreach ($dataTable->getArray() as $table) { + $labels = array_merge($labels, $table->getColumn('label')); + } + $labels = array_values(array_unique($labels)); + + // if the filter_limit query param is set, treat it as a request to limit + // the number of labels used + $limit = Piwik_Common::getRequestVar('filter_limit', false); + if ($limit != false + && $limit >= 0 + ) { + $labels = array_slice($labels, 0, $limit); + } + return $labels; + } + + /** + * Get row evolution for a single label + * @return array containing report data, metadata, label, logo + */ + private function getSingleRowEvolution($dataTable, $metadata, $apiModule, $apiAction, $label, $labelUseAbsoluteUrl = true) + { + $metricNames = array_keys($metadata['metrics']); + + $logo = $actualLabel = false; + $urlFound = false; + foreach ($dataTable->getArray() as $date => $subTable) { + /** @var $subTable Piwik_DataTable */ + $subTable->applyQueuedFilters(); + if ($subTable->getRowsCount() > 0) { + /** @var $row Piwik_DataTable_Row */ + $row = $subTable->getFirstRow(); + + if (!$actualLabel) { + $logo = $row->getMetadata('logo'); + + $actualLabel = $this->getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl); + $urlFound = $actualLabel !== false; + if (empty($actualLabel)) { + $actualLabel = $row->getColumn('label'); + } + } + + // remove all columns that are not in the available metrics. + // this removes the label as well (which is desired for two reasons: (1) it was passed + // in the request, (2) it would cause the evolution graph to show the label in the legend). + foreach ($row->getColumns() as $column => $value) { + if (!in_array($column, $metricNames)) { + $row->deleteColumn($column); + } + } + $row->deleteMetadata(); + } + } + + $this->enhanceRowEvolutionMetaData($metadata, $dataTable); + + // if we have a recursive label and no url, use the path + if (!$urlFound) { + $actualLabel = str_replace(Piwik_API_DataTableManipulator_LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label); + } + + $return = array( + 'label' => Piwik_DataTable_Filter_SafeDecodeLabel::safeDecodeLabel($actualLabel), + 'reportData' => $dataTable, + 'metadata' => $metadata + ); + if (!empty($logo)) { + $return['logo'] = $logo; + } + return $return; + } + + private function getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl) + { + $url = $row->getMetadata('url'); + if ($url + && ($apiModule == 'Actions' + || ($apiModule == 'Referers' + && $apiAction == 'getWebsites')) + && $labelUseAbsoluteUrl + ) { + $actualLabel = preg_replace(';^http(s)?://(www.)?;i', '', $url); + return $actualLabel; + } + return false; + } + + /** + * @param $idSite + * @param $period + * @param $date + * @param $apiModule + * @param $apiAction + * @param $label + * @param $segment + * @param $idGoal + * @throws Exception + * @return Piwik_DataTable_Array|Piwik_DataTable + */ + private function loadRowEvolutionDataFromAPI($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $idGoal = false) + { + if (!is_array($label)) { + $label = array($label); + } + $label = array_map('rawurlencode', $label); + + $parameters = array( + 'method' => $apiModule . '.' . $apiAction, + 'label' => $label, + 'idSite' => $idSite, + 'period' => $period, + 'date' => $date, + 'format' => 'original', + 'serialize' => '0', + 'segment' => $segment, + 'idGoal' => $idGoal, + + // data for row evolution should NOT be limited + 'filter_limit' => -1, + + // if more than one label is used, we add metadata to ensure we know which + // row corresponds with which label (since the labels can change, and rows + // can be sorted in a different order) + 'labelFilterAddLabelIndex' => count($label) > 1 ? 1 : 0, + ); + + // add "processed metrics" like actions per visit or bounce rate + // note: some reports should not be filtered with AddColumnProcessedMetrics + // specifically, reports without the Piwik_Metrics::INDEX_NB_VISITS metric such as Goals.getVisitsUntilConversion & Goal.getDaysToConversion + // this is because the AddColumnProcessedMetrics filter removes all datable rows lacking this metric + if + ( + $apiModule != 'Actions' + && + ($apiModule != 'Goals' || ($apiAction != 'getVisitsUntilConversion' && $apiAction != 'getDaysToConversion')) + && !empty($label) + ) { + $parameters['filter_add_columns_when_show_all_columns'] = '1'; + } + + $url = Piwik_Url::getQueryStringFromParameters($parameters); + + $request = new Piwik_API_Request($url); + + try { + $dataTable = $request->process(); + } catch (Exception $e) { + throw new Exception("API returned an error: " . $e->getMessage() . "\n"); + } + + return $dataTable; + } + + /** + * For a given API report, returns a simpler version + * of the metadata (will return only the metrics and the dimension name) + * @param $idSite + * @param $period + * @param $date + * @param $apiModule + * @param $apiAction + * @param $language + * @param $idGoal + * @throws Exception + * @return array + */ + private function getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal = false) + { + $apiParameters = array(); + if (!empty($idGoal) && $idGoal > 0) { + $apiParameters = array('idGoal' => $idGoal); + } + $reportMetadata = Piwik_API_API::getInstance()->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, + $period, $date, $hideMetricsDoc = false, $showSubtableReports = true); + + if (empty($reportMetadata)) { + throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite " + . "not found in the list of available reports. \n"); + } + + $reportMetadata = reset($reportMetadata); + + $metrics = $reportMetadata['metrics']; + if (isset($reportMetadata['processedMetrics']) && is_array($reportMetadata['processedMetrics'])) { + $metrics = $metrics + $reportMetadata['processedMetrics']; + } + + $dimension = $reportMetadata['dimension']; + + return compact('metrics', 'dimension'); + } + + /** + * Given the Row evolution dataTable, and the associated metadata, + * enriches the metadata with min/max values, and % change between the first period and the last one + * @param array $metadata + * @param Piwik_DataTable_Array $dataTable + */ + private function enhanceRowEvolutionMetaData(&$metadata, $dataTable) + { + // prepare result array for metrics + $metricsResult = array(); + foreach ($metadata['metrics'] as $metric => $name) { + $metricsResult[$metric] = array('name' => $name); + + if (!empty($metadata['logos'][$metric])) { + $metricsResult[$metric]['logo'] = $metadata['logos'][$metric]; + } + } + unset($metadata['logos']); + + $subDataTables = $dataTable->getArray(); + $firstDataTable = reset($subDataTables); + $firstDataTableRow = $firstDataTable->getFirstRow(); + $lastDataTable = end($subDataTables); + $lastDataTableRow = $lastDataTable->getFirstRow(); + + // Process min/max values + $firstNonZeroFound = array(); + foreach ($subDataTables as $subDataTable) { + // $subDataTable is the report for one period, it has only one row + $firstRow = $subDataTable->getFirstRow(); + foreach ($metadata['metrics'] as $metric => $label) { + $value = $firstRow ? floatval($firstRow->getColumn($metric)) : 0; + if ($value > 0) { + $firstNonZeroFound[$metric] = true; + } else if (!isset($firstNonZeroFound[$metric])) { + continue; + } + if (!isset($metricsResult[$metric]['min']) + || $metricsResult[$metric]['min'] > $value + ) { + $metricsResult[$metric]['min'] = $value; + } + if (!isset($metricsResult[$metric]['max']) + || $metricsResult[$metric]['max'] < $value + ) { + $metricsResult[$metric]['max'] = $value; + } + } + } + + // Process % change between first/last values + foreach ($metadata['metrics'] as $metric => $label) { + $first = $firstDataTableRow ? floatval($firstDataTableRow->getColumn($metric)) : 0; + $last = $lastDataTableRow ? floatval($lastDataTableRow->getColumn($metric)) : 0; + + // do not calculate evolution if the first value is 0 (to avoid divide-by-zero) + if ($first == 0) { + continue; + } + + $change = Piwik_DataTable_Filter_CalculateEvolutionFilter::calculate($last, $first, $quotientPrecision = 0); + $change = Piwik_DataTable_Filter_CalculateEvolutionFilter::prependPlusSignToNumber($change); + $metricsResult[$metric]['change'] = $change; + } + + $metadata['metrics'] = $metricsResult; + } + + /** Get row evolution for a multiple labels */ + private function getMultiRowEvolution($dataTable, $metadata, $apiModule, $apiAction, $labels, $column, + $legendAppendMetric = true, + $labelUseAbsoluteUrl = true) + { + if (!isset($metadata['metrics'][$column])) { + // invalid column => use the first one that's available + $metrics = array_keys($metadata['metrics']); + $column = reset($metrics); + } + + // get the processed label and logo (if any) for every requested label + $actualLabels = $logos = array(); + foreach ($labels as $labelIdx => $label) { + foreach ($dataTable->getArray() as $table) { + $labelRow = $this->getRowEvolutionRowFromLabelIdx($table, $labelIdx); + + if ($labelRow) { + $actualLabels[$labelIdx] = $this->getRowUrlForEvolutionLabel( + $labelRow, $apiModule, $apiAction, $labelUseAbsoluteUrl); + + $logos[$labelIdx] = $labelRow->getMetadata('logo'); + + if (!empty($actualLabels[$labelIdx])) { + break; + } + } + } + + if (empty($actualLabels[$labelIdx])) { + $actualLabels[$labelIdx] = $this->cleanOriginalLabel($label); + } + } + + // convert rows to be array($column.'_'.$labelIdx => $value) as opposed to + // array('label' => $label, 'column' => $value). + $dataTableMulti = $dataTable->getEmptyClone(); + foreach ($dataTable->getArray() as $tableLabel => $table) { + $newRow = new Piwik_DataTable_Row(); + + foreach ($labels as $labelIdx => $label) { + $row = $this->getRowEvolutionRowFromLabelIdx($table, $labelIdx); + + $value = 0; + if ($row) { + $value = $row->getColumn($column); + $value = floatVal(str_replace(',', '.', $value)); + } + + if ($value == '') { + $value = 0; + } + + $newLabel = $column . '_' . (int)$labelIdx; + $newRow->addColumn($newLabel, $value); + } + + $newTable = $table->getEmptyClone(); + if (!empty($labels)) { // only add a row if the row has data (no labels === no data) + $newTable->addRow($newRow); + } + + $dataTableMulti->addTable($newTable, $tableLabel); + } + + // the available metrics for the report are returned as metadata / columns + $metadata['columns'] = $metadata['metrics']; + + // metadata / metrics should document the rows that are compared + // this way, UI code can be reused + $metadata['metrics'] = array(); + foreach ($actualLabels as $labelIndex => $label) { + if ($legendAppendMetric) { + $label .= ' (' . $metadata['columns'][$column] . ')'; + } + $metricName = $column . '_' . $labelIndex; + $metadata['metrics'][$metricName] = Piwik_DataTable_Filter_SafeDecodeLabel::safeDecodeLabel($label); + + if (!empty($logos[$labelIndex])) { + $metadata['logos'][$metricName] = $logos[$labelIndex]; + } + } + + $this->enhanceRowEvolutionMetaData($metadata, $dataTableMulti); + + return array( + 'column' => $column, + 'reportData' => $dataTableMulti, + 'metadata' => $metadata + ); + } + + /** + * Returns the row in a datatable by its Piwik_API_DataTableManipulator_LabelFilter::FLAG_IS_ROW_EVOLUTION metadata. + * + * @param Piwik_DataTable $table + * @param int $labelIdx + * @return Piwik_DataTable_Row|false + */ + private function getRowEvolutionRowFromLabelIdx($table, $labelIdx) + { + $labelIdx = (int)$labelIdx; + foreach ($table->getRows() as $row) + { + if ($row->getMetadata(Piwik_API_DataTableManipulator_LabelFilter::FLAG_IS_ROW_EVOLUTION) === $labelIdx) + { + return $row; + } + } + return false; + } + + + /** + * Returns a prettier, more comprehensible version of a row evolution label + * for display. + */ + private function cleanOriginalLabel($label) + { + return str_replace(Piwik_API_DataTableManipulator_LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label); + } +} diff --git a/plugins/API/templates/listAllAPI.twig b/plugins/API/templates/listAllAPI.twig index 439b8b5747..87d9eee178 100644 --- a/plugins/API/templates/listAllAPI.twig +++ b/plugins/API/templates/listAllAPI.twig @@ -25,8 +25,7 @@ <p> {{ 'API_UsingTokenAuth'|translate('<b>','</b>',"")|raw }}<br/> <span id='token_auth'>&token_auth=<b>{{ token_auth }}</b></span><br/> - {{ 'API_KeepTokenSecret'|translate('<b>','</b>') }} - <!-- {'API_LoadedAPIs'|translate:$countLoadedAPI} --> + {{ 'API_KeepTokenSecret'|translate('<b>','</b>')|raw }} {{ list_api_methods_with_links|raw }} <br/> </div> |