PageRenderTime 49ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/MultiSites/API.php

https://github.com/CodeYellowBV/piwik
PHP | 495 lines | 321 code | 60 blank | 114 comment | 39 complexity | 74399ba56463226e767ea8b92a8fd200 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\MultiSites;
  10. use Exception;
  11. use Piwik\API\Request;
  12. use Piwik\Archive;
  13. use Piwik\Common;
  14. use Piwik\DataTable;
  15. use Piwik\Period\Range;
  16. use Piwik\Piwik;
  17. use Piwik\Plugins\Goals\Archiver;
  18. use Piwik\Plugins\SitesManager\API as APISitesManager;
  19. use Piwik\Site;
  20. use Piwik\TaskScheduler;
  21. /**
  22. * The MultiSites API lets you request the key metrics (visits, page views, revenue) for all Websites in Piwik.
  23. * @method static \Piwik\Plugins\MultiSites\API getInstance()
  24. */
  25. class API extends \Piwik\Plugin\API
  26. {
  27. const METRIC_TRANSLATION_KEY = 'translation';
  28. const METRIC_EVOLUTION_COL_NAME_KEY = 'evolution_column_name';
  29. const METRIC_RECORD_NAME_KEY = 'record_name';
  30. const METRIC_IS_ECOMMERCE_KEY = 'is_ecommerce';
  31. const NB_VISITS_METRIC = 'nb_visits';
  32. const NB_ACTIONS_METRIC = 'nb_actions';
  33. const NB_PAGEVIEWS_LABEL = 'nb_pageviews';
  34. const NB_PAGEVIEWS_METRIC = 'Actions_nb_pageviews';
  35. const GOAL_REVENUE_METRIC = 'revenue';
  36. const GOAL_CONVERSION_METRIC = 'nb_conversions';
  37. const ECOMMERCE_ORDERS_METRIC = 'orders';
  38. const ECOMMERCE_REVENUE_METRIC = 'ecommerce_revenue';
  39. static private $baseMetrics = array(
  40. self::NB_VISITS_METRIC => array(
  41. self::METRIC_TRANSLATION_KEY => 'General_ColumnNbVisits',
  42. self::METRIC_EVOLUTION_COL_NAME_KEY => 'visits_evolution',
  43. self::METRIC_RECORD_NAME_KEY => self::NB_VISITS_METRIC,
  44. self::METRIC_IS_ECOMMERCE_KEY => false,
  45. ),
  46. self::NB_ACTIONS_METRIC => array(
  47. self::METRIC_TRANSLATION_KEY => 'General_ColumnNbActions',
  48. self::METRIC_EVOLUTION_COL_NAME_KEY => 'actions_evolution',
  49. self::METRIC_RECORD_NAME_KEY => self::NB_ACTIONS_METRIC,
  50. self::METRIC_IS_ECOMMERCE_KEY => false,
  51. )
  52. );
  53. /**
  54. * Returns a report displaying the total visits, actions and revenue, as
  55. * well as the evolution of these values, of all existing sites over a
  56. * specified period of time.
  57. *
  58. * If the specified period is not a 'range', this function will calculcate
  59. * evolution metrics. Evolution metrics are metrics that display the
  60. * percent increase/decrease of another metric since the last period.
  61. *
  62. * This function will merge the result of the archive query so each
  63. * row in the result DataTable will correspond to the metrics of a single
  64. * site. If a date range is specified, the result will be a
  65. * DataTable\Map, but it will still be merged.
  66. *
  67. * @param string $period The period type to get data for.
  68. * @param string $date The date(s) to get data for.
  69. * @param bool|string $segment The segments to get data for.
  70. * @param bool|string $_restrictSitesToLogin Hack used to enforce we restrict the returned data to the specified username
  71. * Only used when a scheduled task is running
  72. * @param bool|string $enhanced When true, return additional goal & ecommerce metrics
  73. * @param bool|string $pattern If specified, only the website which names (or site ID) match the pattern will be returned using SitesManager.getPatternMatchSites
  74. * @return DataTable
  75. */
  76. public function getAll($period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false, $pattern = false)
  77. {
  78. Piwik::checkUserHasSomeViewAccess();
  79. $idSites = $this->getSitesIdFromPattern($pattern);
  80. if (empty($idSites)) {
  81. return new DataTable();
  82. }
  83. return $this->buildDataTable(
  84. $idSites,
  85. $period,
  86. $date,
  87. $segment,
  88. $_restrictSitesToLogin,
  89. $enhanced,
  90. $multipleWebsitesRequested = true
  91. );
  92. }
  93. /**
  94. * Fetches the list of sites which names match the string pattern
  95. *
  96. * @param $pattern
  97. * @return array|string
  98. */
  99. private function getSitesIdFromPattern($pattern)
  100. {
  101. $idSites = 'all';
  102. if (empty($pattern)) {
  103. return $idSites;
  104. }
  105. $idSites = array();
  106. $sites = Request::processRequest('SitesManager.getPatternMatchSites',
  107. array('pattern' => $pattern,
  108. // added because caller could overwrite these
  109. 'serialize' => 0,
  110. 'format' => 'original'));
  111. if (!empty($sites)) {
  112. foreach ($sites as $site) {
  113. $idSites[] = $site['idsite'];
  114. }
  115. }
  116. return $idSites;
  117. }
  118. /**
  119. * Same as getAll but for a unique Piwik site
  120. * @see Piwik\Plugins\MultiSites\API::getAll()
  121. *
  122. * @param int $idSite Id of the Piwik site
  123. * @param string $period The period type to get data for.
  124. * @param string $date The date(s) to get data for.
  125. * @param bool|string $segment The segments to get data for.
  126. * @param bool|string $_restrictSitesToLogin Hack used to enforce we restrict the returned data to the specified username
  127. * Only used when a scheduled task is running
  128. * @param bool|string $enhanced When true, return additional goal & ecommerce metrics
  129. * @return DataTable
  130. */
  131. public function getOne($idSite, $period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false)
  132. {
  133. Piwik::checkUserHasViewAccess($idSite);
  134. return $this->buildDataTable(
  135. $idSite,
  136. $period,
  137. $date,
  138. $segment,
  139. $_restrictSitesToLogin,
  140. $enhanced,
  141. $multipleWebsitesRequested = false
  142. );
  143. }
  144. private function buildDataTable($idSitesOrIdSite, $period, $date, $segment, $_restrictSitesToLogin, $enhanced, $multipleWebsitesRequested)
  145. {
  146. $allWebsitesRequested = ($idSitesOrIdSite == 'all');
  147. if ($allWebsitesRequested) {
  148. // First clear cache
  149. Site::clearCache();
  150. // Then, warm the cache with only the data we should have access to
  151. if (Piwik::hasUserSuperUserAccess()
  152. // Hack: when this API function is called as a Scheduled Task, Super User status is enforced.
  153. // This means this function would return ALL websites in all cases.
  154. // Instead, we make sure that only the right set of data is returned
  155. && !TaskScheduler::isTaskBeingExecuted()
  156. ) {
  157. APISitesManager::getInstance()->getAllSites();
  158. } else {
  159. APISitesManager::getInstance()->getSitesWithAtLeastViewAccess($limit = false, $_restrictSitesToLogin);
  160. }
  161. // Both calls above have called Site::setSitesFromArray. We now get these sites:
  162. $sitesToProblablyAdd = Site::getSites();
  163. } else {
  164. $sitesToProblablyAdd = array(APISitesManager::getInstance()->getSiteFromId($idSitesOrIdSite));
  165. }
  166. // build the archive type used to query archive data
  167. $archive = Archive::build(
  168. $idSitesOrIdSite,
  169. $period,
  170. $date,
  171. $segment,
  172. $_restrictSitesToLogin
  173. );
  174. // determine what data will be displayed
  175. $fieldsToGet = array();
  176. $columnNameRewrites = array();
  177. $apiECommerceMetrics = array();
  178. $apiMetrics = API::getApiMetrics($enhanced);
  179. foreach ($apiMetrics as $metricName => $metricSettings) {
  180. $fieldsToGet[] = $metricSettings[self::METRIC_RECORD_NAME_KEY];
  181. $columnNameRewrites[$metricSettings[self::METRIC_RECORD_NAME_KEY]] = $metricName;
  182. if ($metricSettings[self::METRIC_IS_ECOMMERCE_KEY]) {
  183. $apiECommerceMetrics[$metricName] = $metricSettings;
  184. }
  185. }
  186. // get the data
  187. // $dataTable instanceOf Set
  188. $dataTable = $archive->getDataTableFromNumeric($fieldsToGet);
  189. $dataTable = $this->mergeDataTableMapAndPopulateLabel($idSitesOrIdSite, $multipleWebsitesRequested, $dataTable);
  190. if ($dataTable instanceof DataTable\Map) {
  191. foreach ($dataTable->getDataTables() as $table) {
  192. $this->addMissingWebsites($table, $fieldsToGet, $sitesToProblablyAdd);
  193. }
  194. } else {
  195. $this->addMissingWebsites($dataTable, $fieldsToGet, $sitesToProblablyAdd);
  196. }
  197. // calculate total visits/actions/revenue
  198. $this->setMetricsTotalsMetadata($dataTable, $apiMetrics);
  199. // if the period isn't a range & a lastN/previousN date isn't used, we get the same
  200. // data for the last period to show the evolution of visits/actions/revenue
  201. list($strLastDate, $lastPeriod) = Range::getLastDate($date, $period);
  202. if ($strLastDate !== false) {
  203. if ($lastPeriod !== false) {
  204. // NOTE: no easy way to set last period date metadata when range of dates is requested.
  205. // will be easier if DataTable\Map::metadata is removed, and metadata that is
  206. // put there is put directly in DataTable::metadata.
  207. $dataTable->setMetadata(self::getLastPeriodMetadataName('date'), $lastPeriod);
  208. }
  209. $pastArchive = Archive::build($idSitesOrIdSite, $period, $strLastDate, $segment, $_restrictSitesToLogin);
  210. $pastData = $pastArchive->getDataTableFromNumeric($fieldsToGet);
  211. $pastData = $this->mergeDataTableMapAndPopulateLabel($idSitesOrIdSite, $multipleWebsitesRequested, $pastData);
  212. // use past data to calculate evolution percentages
  213. $this->calculateEvolutionPercentages($dataTable, $pastData, $apiMetrics);
  214. Common::destroy($pastData);
  215. }
  216. // remove eCommerce related metrics on non eCommerce Piwik sites
  217. // note: this is not optimal in terms of performance: those metrics should not be retrieved in the first place
  218. if ($enhanced) {
  219. if ($dataTable instanceof DataTable\Map) {
  220. foreach ($dataTable->getDataTables() as $table) {
  221. $this->removeEcommerceRelatedMetricsOnNonEcommercePiwikSites($table, $apiECommerceMetrics);
  222. }
  223. } else {
  224. $this->removeEcommerceRelatedMetricsOnNonEcommercePiwikSites($dataTable, $apiECommerceMetrics);
  225. }
  226. }
  227. // move the site id to a metadata column
  228. $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'group', array('\Piwik\Site', 'getGroupFor'), array()));
  229. $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'main_url', array('\Piwik\Site', 'getMainUrlFor'), array()));
  230. $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'idsite'));
  231. // set the label of each row to the site name
  232. if ($multipleWebsitesRequested) {
  233. $dataTable->filter('ColumnCallbackReplace', array('label', '\Piwik\Site::getNameFor'));
  234. } else {
  235. $dataTable->filter('ColumnDelete', array('label'));
  236. }
  237. Site::clearCache();
  238. // replace record names with user friendly metric names
  239. $dataTable->filter('ReplaceColumnNames', array($columnNameRewrites));
  240. // Ensures data set sorted, for Metadata output
  241. $dataTable->filter('Sort', array(self::NB_VISITS_METRIC, 'desc', $naturalSort = false));
  242. // filter rows without visits
  243. // note: if only one website is queried and there are no visits, we can not remove the row otherwise
  244. // ResponseBuilder throws 'Call to a member function getColumns() on a non-object'
  245. if ($multipleWebsitesRequested
  246. // We don't delete the 0 visits row, if "Enhanced" mode is on.
  247. && !$enhanced
  248. ) {
  249. $dataTable->filter(
  250. 'ColumnCallbackDeleteRow',
  251. array(
  252. self::NB_VISITS_METRIC,
  253. function ($value) {
  254. return $value == 0;
  255. }
  256. )
  257. );
  258. }
  259. return $dataTable;
  260. }
  261. /**
  262. * Performs a binary filter of two
  263. * DataTables in order to correctly calculate evolution metrics.
  264. *
  265. * @param DataTable|DataTable\Map $currentData
  266. * @param DataTable|DataTable\Map $pastData
  267. * @param array $apiMetrics The array of string fields to calculate evolution
  268. * metrics for.
  269. * @throws Exception
  270. */
  271. private function calculateEvolutionPercentages($currentData, $pastData, $apiMetrics)
  272. {
  273. if (get_class($currentData) != get_class($pastData)) { // sanity check for regressions
  274. throw new Exception("Expected \$pastData to be of type " . get_class($currentData) . " - got "
  275. . get_class($pastData) . ".");
  276. }
  277. if ($currentData instanceof DataTable\Map) {
  278. $pastArray = $pastData->getDataTables();
  279. foreach ($currentData->getDataTables() as $subTable) {
  280. $this->calculateEvolutionPercentages($subTable, current($pastArray), $apiMetrics);
  281. next($pastArray);
  282. }
  283. } else {
  284. foreach ($apiMetrics as $metricSettings) {
  285. $currentData->filter(
  286. 'CalculateEvolutionFilter',
  287. array(
  288. $pastData,
  289. $metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY],
  290. $metricSettings[self::METRIC_RECORD_NAME_KEY],
  291. $quotientPrecision = 1)
  292. );
  293. }
  294. }
  295. }
  296. /**
  297. * @ignore
  298. */
  299. public static function getApiMetrics($enhanced)
  300. {
  301. $metrics = self::$baseMetrics;
  302. if(Common::isActionsPluginEnabled()) {
  303. $metrics[self::NB_PAGEVIEWS_LABEL] = array(
  304. self::METRIC_TRANSLATION_KEY => 'General_ColumnPageviews',
  305. self::METRIC_EVOLUTION_COL_NAME_KEY => 'pageviews_evolution',
  306. self::METRIC_RECORD_NAME_KEY => self::NB_PAGEVIEWS_METRIC,
  307. self::METRIC_IS_ECOMMERCE_KEY => false,
  308. );
  309. }
  310. if (Common::isGoalPluginEnabled()) {
  311. // goal revenue metric
  312. $metrics[self::GOAL_REVENUE_METRIC] = array(
  313. self::METRIC_TRANSLATION_KEY => 'General_ColumnRevenue',
  314. self::METRIC_EVOLUTION_COL_NAME_KEY => self::GOAL_REVENUE_METRIC . '_evolution',
  315. self::METRIC_RECORD_NAME_KEY => Archiver::getRecordName(self::GOAL_REVENUE_METRIC),
  316. self::METRIC_IS_ECOMMERCE_KEY => false,
  317. );
  318. if ($enhanced) {
  319. // number of goal conversions metric
  320. $metrics[self::GOAL_CONVERSION_METRIC] = array(
  321. self::METRIC_TRANSLATION_KEY => 'Goals_ColumnConversions',
  322. self::METRIC_EVOLUTION_COL_NAME_KEY => self::GOAL_CONVERSION_METRIC . '_evolution',
  323. self::METRIC_RECORD_NAME_KEY => Archiver::getRecordName(self::GOAL_CONVERSION_METRIC),
  324. self::METRIC_IS_ECOMMERCE_KEY => false,
  325. );
  326. // number of orders
  327. $metrics[self::ECOMMERCE_ORDERS_METRIC] = array(
  328. self::METRIC_TRANSLATION_KEY => 'General_EcommerceOrders',
  329. self::METRIC_EVOLUTION_COL_NAME_KEY => self::ECOMMERCE_ORDERS_METRIC . '_evolution',
  330. self::METRIC_RECORD_NAME_KEY => Archiver::getRecordName(self::GOAL_CONVERSION_METRIC, 0),
  331. self::METRIC_IS_ECOMMERCE_KEY => true,
  332. );
  333. // eCommerce revenue
  334. $metrics[self::ECOMMERCE_REVENUE_METRIC] = array(
  335. self::METRIC_TRANSLATION_KEY => 'General_ProductRevenue',
  336. self::METRIC_EVOLUTION_COL_NAME_KEY => self::ECOMMERCE_REVENUE_METRIC . '_evolution',
  337. self::METRIC_RECORD_NAME_KEY => Archiver::getRecordName(self::GOAL_REVENUE_METRIC, 0),
  338. self::METRIC_IS_ECOMMERCE_KEY => true,
  339. );
  340. }
  341. }
  342. return $metrics;
  343. }
  344. /**
  345. * Sets the total visits, actions & revenue for a DataTable returned by
  346. * $this->buildDataTable.
  347. *
  348. * @param DataTable $dataTable
  349. * @param array $apiMetrics Metrics info.
  350. * @return array Array of three values: total visits, total actions, total revenue
  351. */
  352. private function setMetricsTotalsMetadata($dataTable, $apiMetrics)
  353. {
  354. if ($dataTable instanceof DataTable\Map) {
  355. foreach ($dataTable->getDataTables() as $table) {
  356. $this->setMetricsTotalsMetadata($table, $apiMetrics);
  357. }
  358. } else {
  359. $revenueMetric = '';
  360. if (Common::isGoalPluginEnabled()) {
  361. $revenueMetric = Archiver::getRecordName(self::GOAL_REVENUE_METRIC);
  362. }
  363. $totals = array();
  364. foreach ($apiMetrics as $label => $metricInfo) {
  365. $totalMetadataName = self::getTotalMetadataName($label);
  366. $totals[$totalMetadataName] = 0;
  367. }
  368. foreach ($dataTable->getRows() as $row) {
  369. foreach ($apiMetrics as $label => $metricInfo) {
  370. $totalMetadataName = self::getTotalMetadataName($label);
  371. $totals[$totalMetadataName] += $row->getColumn($metricInfo[self::METRIC_RECORD_NAME_KEY]);
  372. }
  373. }
  374. foreach ($totals as $name => $value) {
  375. $dataTable->setMetadata($name, $value);
  376. }
  377. }
  378. }
  379. private static function getTotalMetadataName($name)
  380. {
  381. return 'total_' . $name;
  382. }
  383. private static function getLastPeriodMetadataName($name)
  384. {
  385. return 'last_period_' . $name;
  386. }
  387. /**
  388. * @param DataTable|DataTable\Map $dataTable
  389. * @param $fieldsToGet
  390. * @param $sitesToProblablyAdd
  391. */
  392. private function addMissingWebsites($dataTable, $fieldsToGet, $sitesToProblablyAdd)
  393. {
  394. $siteIdsInDataTable = array();
  395. foreach ($dataTable->getRows() as $row) {
  396. /** @var DataTable\Row $row */
  397. $siteIdsInDataTable[] = $row->getColumn('label');
  398. }
  399. foreach ($sitesToProblablyAdd as $site) {
  400. if (!in_array($site['idsite'], $siteIdsInDataTable)) {
  401. $siteRow = array_combine($fieldsToGet, array_pad(array(), count($fieldsToGet), 0));
  402. $siteRow['label'] = (int) $site['idsite'];
  403. $dataTable->addRowFromSimpleArray($siteRow);
  404. }
  405. }
  406. }
  407. private function removeEcommerceRelatedMetricsOnNonEcommercePiwikSites($dataTable, $apiECommerceMetrics)
  408. {
  409. // $dataTableRows instanceOf Row[]
  410. $dataTableRows = $dataTable->getRows();
  411. foreach ($dataTableRows as $dataTableRow) {
  412. $siteId = $dataTableRow->getColumn('label');
  413. if (!Site::isEcommerceEnabledFor($siteId)) {
  414. foreach ($apiECommerceMetrics as $metricSettings) {
  415. $dataTableRow->deleteColumn($metricSettings[self::METRIC_RECORD_NAME_KEY]);
  416. $dataTableRow->deleteColumn($metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY]);
  417. }
  418. }
  419. }
  420. }
  421. private function mergeDataTableMapAndPopulateLabel($idSitesOrIdSite, $multipleWebsitesRequested, $dataTable)
  422. {
  423. // get rid of the DataTable\Map that is created by the IndexedBySite archive type
  424. if ($dataTable instanceof DataTable\Map && $multipleWebsitesRequested) {
  425. return $dataTable->mergeChildren();
  426. } else {
  427. if (!$dataTable instanceof DataTable\Map && $dataTable->getRowsCount() > 0) {
  428. $firstSite = is_array($idSitesOrIdSite) ? reset($idSitesOrIdSite) : $idSitesOrIdSite;
  429. $firstDataTableRow = $dataTable->getFirstRow();
  430. $firstDataTableRow->setColumn('label', $firstSite);
  431. }
  432. }
  433. return $dataTable;
  434. }
  435. }