PageRenderTime 42ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/API/RowEvolution.php

https://github.com/CodeYellowBV/piwik
PHP | 529 lines | 347 code | 67 blank | 115 comment | 62 complexity | bf054fe26c5e40b31ae474212fd22191 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugins\API;
  10. use Exception;
  11. use Piwik\API\DataTableManipulator\LabelFilter;
  12. use Piwik\API\Request;
  13. use Piwik\API\ResponseBuilder;
  14. use Piwik\Common;
  15. use Piwik\DataTable;
  16. use Piwik\DataTable\Filter\CalculateEvolutionFilter;
  17. use Piwik\DataTable\Filter\SafeDecodeLabel;
  18. use Piwik\DataTable\Row;
  19. use Piwik\Period;
  20. use Piwik\Piwik;
  21. use Piwik\Site;
  22. use Piwik\Url;
  23. /**
  24. * This class generates a Row evolution dataset, from input request
  25. *
  26. */
  27. class RowEvolution
  28. {
  29. private static $actionsUrlReports = array(
  30. 'getPageUrls',
  31. 'getPageUrlsFollowingSiteSearch',
  32. 'getEntryPageUrls',
  33. 'getExitPageUrls',
  34. 'getPageUrl'
  35. );
  36. public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true)
  37. {
  38. // validation of requested $period & $date
  39. if ($period == 'range') {
  40. // load days in the range
  41. $period = 'day';
  42. }
  43. if (!Period::isMultiplePeriod($date, $period)) {
  44. throw new Exception("Row evolutions can not be processed with this combination of \'date\' and \'period\' parameters.");
  45. }
  46. $label = ResponseBuilder::unsanitizeLabelParameter($label);
  47. $labels = Piwik::getArrayFromApiParameter($label);
  48. $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal);
  49. $dataTable = $this->loadRowEvolutionDataFromAPI($metadata, $idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $idGoal);
  50. if (empty($labels)) {
  51. $labels = $this->getLabelsFromDataTable($dataTable, $labels);
  52. $dataTable = $this->enrichRowAddMetadataLabelIndex($labels, $dataTable);
  53. }
  54. if (count($labels) != 1) {
  55. $data = $this->getMultiRowEvolution(
  56. $dataTable,
  57. $metadata,
  58. $apiModule,
  59. $apiAction,
  60. $labels,
  61. $column,
  62. $legendAppendMetric,
  63. $labelUseAbsoluteUrl
  64. );
  65. } else {
  66. $data = $this->getSingleRowEvolution(
  67. $idSite,
  68. $dataTable,
  69. $metadata,
  70. $apiModule,
  71. $apiAction,
  72. $labels[0],
  73. $labelUseAbsoluteUrl
  74. );
  75. }
  76. return $data;
  77. }
  78. /**
  79. * @param array $labels
  80. * @param DataTable\Map $dataTable
  81. * @return mixed
  82. */
  83. protected function enrichRowAddMetadataLabelIndex($labels, $dataTable)
  84. {
  85. // set label index metadata
  86. $labelsToIndex = array_flip($labels);
  87. foreach ($dataTable->getDataTables() as $table) {
  88. foreach ($table->getRows() as $row) {
  89. $label = $row->getColumn('label');
  90. if (isset($labelsToIndex[$label])) {
  91. $row->setMetadata(LabelFilter::FLAG_IS_ROW_EVOLUTION, $labelsToIndex[$label]);
  92. }
  93. }
  94. }
  95. return $dataTable;
  96. }
  97. /**
  98. * @param DataTable\Map $dataTable
  99. * @param array $labels
  100. * @return array
  101. */
  102. protected function getLabelsFromDataTable($dataTable, $labels)
  103. {
  104. // if no labels specified, use all possible labels as list
  105. foreach ($dataTable->getDataTables() as $table) {
  106. $labels = array_merge($labels, $table->getColumn('label'));
  107. }
  108. $labels = array_values(array_unique($labels));
  109. // if the filter_limit query param is set, treat it as a request to limit
  110. // the number of labels used
  111. $limit = Common::getRequestVar('filter_limit', false);
  112. if ($limit != false
  113. && $limit >= 0
  114. ) {
  115. $labels = array_slice($labels, 0, $limit);
  116. }
  117. return $labels;
  118. }
  119. /**
  120. * Get row evolution for a single label
  121. * @param DataTable\Map $dataTable
  122. * @param array $metadata
  123. * @param string $apiModule
  124. * @param string $apiAction
  125. * @param string $label
  126. * @param bool $labelUseAbsoluteUrl
  127. * @return array containing report data, metadata, label, logo
  128. */
  129. private function getSingleRowEvolution($idSite, $dataTable, $metadata, $apiModule, $apiAction, $label, $labelUseAbsoluteUrl = true)
  130. {
  131. $metricNames = array_keys($metadata['metrics']);
  132. $logo = $actualLabel = false;
  133. $urlFound = false;
  134. foreach ($dataTable->getDataTables() as $date => $subTable) {
  135. /** @var $subTable DataTable */
  136. $subTable->applyQueuedFilters();
  137. if ($subTable->getRowsCount() > 0) {
  138. /** @var $row Row */
  139. $row = $subTable->getFirstRow();
  140. if (!$actualLabel) {
  141. $logo = $row->getMetadata('logo');
  142. $actualLabel = $this->getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl);
  143. $urlFound = $actualLabel !== false;
  144. if (empty($actualLabel)) {
  145. $actualLabel = $row->getColumn('label');
  146. }
  147. }
  148. // remove all columns that are not in the available metrics.
  149. // this removes the label as well (which is desired for two reasons: (1) it was passed
  150. // in the request, (2) it would cause the evolution graph to show the label in the legend).
  151. foreach ($row->getColumns() as $column => $value) {
  152. if (!in_array($column, $metricNames) && $column != 'label_html') {
  153. $row->deleteColumn($column);
  154. }
  155. }
  156. $row->deleteMetadata();
  157. }
  158. }
  159. $this->enhanceRowEvolutionMetaData($metadata, $dataTable);
  160. // if we have a recursive label and no url, use the path
  161. if (!$urlFound) {
  162. $actualLabel = $this->formatQueryLabelForDisplay($idSite, $apiModule, $apiAction, $label);
  163. }
  164. $return = array(
  165. 'label' => SafeDecodeLabel::decodeLabelSafe($actualLabel),
  166. 'reportData' => $dataTable,
  167. 'metadata' => $metadata
  168. );
  169. if (!empty($logo)) {
  170. $return['logo'] = $logo;
  171. }
  172. return $return;
  173. }
  174. private function formatQueryLabelForDisplay($idSite, $apiModule, $apiAction, $label)
  175. {
  176. // rows with subtables do not contain URL metadata. this hack makes sure the label titles in row
  177. // evolution popovers look like URLs.
  178. if ($apiModule == 'Actions'
  179. && in_array($apiAction, self::$actionsUrlReports)
  180. ) {
  181. $mainUrl = Site::getMainUrlFor($idSite);
  182. $mainUrlHost = @parse_url($mainUrl, PHP_URL_HOST);
  183. $replaceRegex = "/\\s*" . preg_quote(LabelFilter::SEPARATOR_RECURSIVE_LABEL) . "\\s*/";
  184. $cleanLabel = preg_replace($replaceRegex, '/', $label);
  185. return $mainUrlHost . '/' . $cleanLabel . '/';
  186. } else {
  187. return str_replace(LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label);
  188. }
  189. }
  190. /**
  191. * @param Row $row
  192. * @param string $apiModule
  193. * @param string $apiAction
  194. * @param bool $labelUseAbsoluteUrl
  195. * @return bool|string
  196. */
  197. private function getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl)
  198. {
  199. $url = $row->getMetadata('url');
  200. if ($url
  201. && ($apiModule == 'Actions'
  202. || ($apiModule == 'Referrers'
  203. && $apiAction == 'getWebsites'))
  204. && $labelUseAbsoluteUrl
  205. ) {
  206. $actualLabel = preg_replace(';^http(s)?://(www.)?;i', '', $url);
  207. return $actualLabel;
  208. }
  209. return false;
  210. }
  211. /**
  212. * @param array $metadata see getRowEvolutionMetaData()
  213. * @param int $idSite
  214. * @param string $period
  215. * @param string $date
  216. * @param string $apiModule
  217. * @param string $apiAction
  218. * @param string|bool $label
  219. * @param string|bool $segment
  220. * @param int|bool $idGoal
  221. * @throws Exception
  222. * @return DataTable\Map|DataTable
  223. */
  224. private function loadRowEvolutionDataFromAPI($metadata, $idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $idGoal = false)
  225. {
  226. if (!is_array($label)) {
  227. $label = array($label);
  228. }
  229. $label = array_map('rawurlencode', $label);
  230. $parameters = array(
  231. 'method' => $apiModule . '.' . $apiAction,
  232. 'label' => $label,
  233. 'idSite' => $idSite,
  234. 'period' => $period,
  235. 'date' => $date,
  236. 'format' => 'original',
  237. 'serialize' => '0',
  238. 'segment' => $segment,
  239. 'idGoal' => $idGoal,
  240. // data for row evolution should NOT be limited
  241. 'filter_limit' => -1,
  242. // if more than one label is used, we add metadata to ensure we know which
  243. // row corresponds with which label (since the labels can change, and rows
  244. // can be sorted in a different order)
  245. 'labelFilterAddLabelIndex' => count($label) > 1 ? 1 : 0,
  246. );
  247. // add "processed metrics" like actions per visit or bounce rate
  248. // note: some reports should not be filtered with AddColumnProcessedMetrics
  249. // specifically, reports without the Metrics::INDEX_NB_VISITS metric such as Goals.getVisitsUntilConversion & Goal.getDaysToConversion
  250. // this is because the AddColumnProcessedMetrics filter removes all datable rows lacking this metric
  251. if( isset($metadata['metrics']['nb_visits'])
  252. && !empty($label)) {
  253. $parameters['filter_add_columns_when_show_all_columns'] = '1';
  254. }
  255. $url = Url::getQueryStringFromParameters($parameters);
  256. $request = new Request($url);
  257. try {
  258. $dataTable = $request->process();
  259. } catch (Exception $e) {
  260. throw new Exception("API returned an error: " . $e->getMessage() . "\n");
  261. }
  262. return $dataTable;
  263. }
  264. /**
  265. * For a given API report, returns a simpler version
  266. * of the metadata (will return only the metrics and the dimension name)
  267. * @param $idSite
  268. * @param $period
  269. * @param $date
  270. * @param $apiModule
  271. * @param $apiAction
  272. * @param $language
  273. * @param $idGoal
  274. * @throws Exception
  275. * @return array
  276. */
  277. private function getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal = false)
  278. {
  279. $apiParameters = array();
  280. if (!empty($idGoal) && $idGoal > 0) {
  281. $apiParameters = array('idGoal' => $idGoal);
  282. }
  283. $reportMetadata = API::getInstance()->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language,
  284. $period, $date, $hideMetricsDoc = false, $showSubtableReports = true);
  285. if (empty($reportMetadata)) {
  286. throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite "
  287. . "not found in the list of available reports. \n");
  288. }
  289. $reportMetadata = reset($reportMetadata);
  290. $metrics = $reportMetadata['metrics'];
  291. if (isset($reportMetadata['processedMetrics']) && is_array($reportMetadata['processedMetrics'])) {
  292. $metrics = $metrics + $reportMetadata['processedMetrics'];
  293. }
  294. $dimension = $reportMetadata['dimension'];
  295. return compact('metrics', 'dimension');
  296. }
  297. /**
  298. * Given the Row evolution dataTable, and the associated metadata,
  299. * enriches the metadata with min/max values, and % change between the first period and the last one
  300. * @param array $metadata
  301. * @param DataTable\Map $dataTable
  302. */
  303. private function enhanceRowEvolutionMetaData(&$metadata, $dataTable)
  304. {
  305. // prepare result array for metrics
  306. $metricsResult = array();
  307. foreach ($metadata['metrics'] as $metric => $name) {
  308. $metricsResult[$metric] = array('name' => $name);
  309. if (!empty($metadata['logos'][$metric])) {
  310. $metricsResult[$metric]['logo'] = $metadata['logos'][$metric];
  311. }
  312. }
  313. unset($metadata['logos']);
  314. $subDataTables = $dataTable->getDataTables();
  315. $firstDataTable = reset($subDataTables);
  316. $firstDataTableRow = $firstDataTable->getFirstRow();
  317. $lastDataTable = end($subDataTables);
  318. $lastDataTableRow = $lastDataTable->getFirstRow();
  319. // Process min/max values
  320. $firstNonZeroFound = array();
  321. foreach ($subDataTables as $subDataTable) {
  322. // $subDataTable is the report for one period, it has only one row
  323. $firstRow = $subDataTable->getFirstRow();
  324. foreach ($metadata['metrics'] as $metric => $label) {
  325. $value = $firstRow ? floatval($firstRow->getColumn($metric)) : 0;
  326. if ($value > 0) {
  327. $firstNonZeroFound[$metric] = true;
  328. } else if (!isset($firstNonZeroFound[$metric])) {
  329. continue;
  330. }
  331. if (!isset($metricsResult[$metric]['min'])
  332. || $metricsResult[$metric]['min'] > $value
  333. ) {
  334. $metricsResult[$metric]['min'] = $value;
  335. }
  336. if (!isset($metricsResult[$metric]['max'])
  337. || $metricsResult[$metric]['max'] < $value
  338. ) {
  339. $metricsResult[$metric]['max'] = $value;
  340. }
  341. }
  342. }
  343. // Process % change between first/last values
  344. foreach ($metadata['metrics'] as $metric => $label) {
  345. $first = $firstDataTableRow ? floatval($firstDataTableRow->getColumn($metric)) : 0;
  346. $last = $lastDataTableRow ? floatval($lastDataTableRow->getColumn($metric)) : 0;
  347. // do not calculate evolution if the first value is 0 (to avoid divide-by-zero)
  348. if ($first == 0) {
  349. continue;
  350. }
  351. $change = CalculateEvolutionFilter::calculate($last, $first, $quotientPrecision = 0);
  352. $change = CalculateEvolutionFilter::prependPlusSignToNumber($change);
  353. $metricsResult[$metric]['change'] = $change;
  354. }
  355. $metadata['metrics'] = $metricsResult;
  356. }
  357. /** Get row evolution for a multiple labels */
  358. private function getMultiRowEvolution(DataTable\Map $dataTable, $metadata, $apiModule, $apiAction, $labels, $column,
  359. $legendAppendMetric = true,
  360. $labelUseAbsoluteUrl = true)
  361. {
  362. if (!isset($metadata['metrics'][$column])) {
  363. // invalid column => use the first one that's available
  364. $metrics = array_keys($metadata['metrics']);
  365. $column = reset($metrics);
  366. }
  367. // get the processed label and logo (if any) for every requested label
  368. $actualLabels = $logos = array();
  369. foreach ($labels as $labelIdx => $label) {
  370. foreach ($dataTable->getDataTables() as $table) {
  371. $labelRow = $this->getRowEvolutionRowFromLabelIdx($table, $labelIdx);
  372. if ($labelRow) {
  373. $actualLabels[$labelIdx] = $this->getRowUrlForEvolutionLabel(
  374. $labelRow, $apiModule, $apiAction, $labelUseAbsoluteUrl);
  375. $prettyLabel = $labelRow->getColumn('label_html');
  376. if($prettyLabel !== false) {
  377. $actualLabels[$labelIdx] = $prettyLabel;
  378. }
  379. $logos[$labelIdx] = $labelRow->getMetadata('logo');
  380. if (!empty($actualLabels[$labelIdx])) {
  381. break;
  382. }
  383. }
  384. }
  385. if (empty($actualLabels[$labelIdx])) {
  386. $cleanLabel = $this->cleanOriginalLabel($label);
  387. $actualLabels[$labelIdx] = $cleanLabel;
  388. }
  389. }
  390. // convert rows to be array($column.'_'.$labelIdx => $value) as opposed to
  391. // array('label' => $label, 'column' => $value).
  392. $dataTableMulti = $dataTable->getEmptyClone();
  393. foreach ($dataTable->getDataTables() as $tableLabel => $table) {
  394. $newRow = new Row();
  395. foreach ($labels as $labelIdx => $label) {
  396. $row = $this->getRowEvolutionRowFromLabelIdx($table, $labelIdx);
  397. $value = 0;
  398. if ($row) {
  399. $value = $row->getColumn($column);
  400. $value = floatVal(str_replace(',', '.', $value));
  401. }
  402. if ($value == '') {
  403. $value = 0;
  404. }
  405. $newLabel = $column . '_' . (int)$labelIdx;
  406. $newRow->addColumn($newLabel, $value);
  407. }
  408. $newTable = $table->getEmptyClone();
  409. if (!empty($labels)) { // only add a row if the row has data (no labels === no data)
  410. $newTable->addRow($newRow);
  411. }
  412. $dataTableMulti->addTable($newTable, $tableLabel);
  413. }
  414. // the available metrics for the report are returned as metadata / columns
  415. $metadata['columns'] = $metadata['metrics'];
  416. // metadata / metrics should document the rows that are compared
  417. // this way, UI code can be reused
  418. $metadata['metrics'] = array();
  419. foreach ($actualLabels as $labelIndex => $label) {
  420. if ($legendAppendMetric) {
  421. $label .= ' (' . $metadata['columns'][$column] . ')';
  422. }
  423. $metricName = $column . '_' . $labelIndex;
  424. $metadata['metrics'][$metricName] = $label;
  425. if (!empty($logos[$labelIndex])) {
  426. $metadata['logos'][$metricName] = $logos[$labelIndex];
  427. }
  428. }
  429. $this->enhanceRowEvolutionMetaData($metadata, $dataTableMulti);
  430. return array(
  431. 'column' => $column,
  432. 'reportData' => $dataTableMulti,
  433. 'metadata' => $metadata
  434. );
  435. }
  436. /**
  437. * Returns the row in a datatable by its LabelFilter::FLAG_IS_ROW_EVOLUTION metadata.
  438. *
  439. * @param DataTable $table
  440. * @param int $labelIdx
  441. * @return Row|false
  442. */
  443. private function getRowEvolutionRowFromLabelIdx($table, $labelIdx)
  444. {
  445. $labelIdx = (int)$labelIdx;
  446. foreach ($table->getRows() as $row) {
  447. if ($row->getMetadata(LabelFilter::FLAG_IS_ROW_EVOLUTION) === $labelIdx) {
  448. return $row;
  449. }
  450. }
  451. return false;
  452. }
  453. /**
  454. * Returns a prettier, more comprehensible version of a row evolution label for display.
  455. */
  456. private function cleanOriginalLabel($label)
  457. {
  458. $label = str_replace(LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label);
  459. $label = SafeDecodeLabel::decodeLabelSafe($label);
  460. return $label;
  461. }
  462. }