PageRenderTime 72ms CodeModel.GetById 18ms RepoModel.GetById 2ms app.codeStats 0ms

/plugins/Actions/Actions.php

https://github.com/quarkness/piwik
PHP | 802 lines | 591 code | 97 blank | 114 comment | 39 complexity | 2b88062efc94edfcaadcc43c8d090a9e MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik_Plugins
  10. * @package Piwik_Actions
  11. */
  12. /**
  13. * Actions plugin
  14. *
  15. * Reports about the page views, the outlinks and downloads.
  16. *
  17. * @package Piwik_Actions
  18. */
  19. class Piwik_Actions extends Piwik_Plugin
  20. {
  21. static protected $actionUrlCategoryDelimiter = null;
  22. static protected $actionTitleCategoryDelimiter = null;
  23. static protected $defaultActionName = null;
  24. static protected $defaultActionNameWhenNotDefined = null;
  25. static protected $defaultActionUrlWhenNotDefined = null;
  26. static protected $limitLevelSubCategory = 10; // must be less than Piwik_DataTable::MAXIMUM_DEPTH_LEVEL_ALLOWED
  27. protected $maximumRowsInDataTableLevelZero;
  28. protected $maximumRowsInSubDataTable;
  29. protected $columnToSortByBeforeTruncation;
  30. public function getInformation()
  31. {
  32. $info = array(
  33. 'description' => Piwik_Translate('Actions_PluginDescription'),
  34. 'author' => 'Piwik',
  35. 'author_homepage' => 'http://piwik.org/',
  36. 'version' => Piwik_Version::VERSION,
  37. );
  38. return $info;
  39. }
  40. function getListHooksRegistered()
  41. {
  42. $hooks = array(
  43. 'ArchiveProcessing_Day.compute' => 'archiveDay',
  44. 'ArchiveProcessing_Period.compute' => 'archivePeriod',
  45. 'WidgetsList.add' => 'addWidgets',
  46. 'Menu.add' => 'addMenus',
  47. 'API.getReportMetadata' => 'getReportMetadata',
  48. 'API.getSegmentsMetadata' => 'getSegmentsMetadata',
  49. );
  50. return $hooks;
  51. }
  52. public function getSegmentsMetadata($notification)
  53. {
  54. $segments =& $notification->getNotificationObject();
  55. $sqlFilter = array($this, 'getIdActionFromString');
  56. // entry and exit pages of visit
  57. $segments[] = array(
  58. 'type' => 'dimension',
  59. 'category' => 'Actions_Actions',
  60. 'name' => 'Actions_ColumnEntryPageURL',
  61. 'segment' => 'entryPageUrl',
  62. 'sqlSegment' => 'log_visit.visit_entry_idaction_url',
  63. 'sqlFilter' => $sqlFilter,
  64. );
  65. $segments[] = array(
  66. 'type' => 'dimension',
  67. 'category' => 'Actions_Actions',
  68. 'name' => 'Actions_ColumnEntryPageTitle',
  69. 'segment' => 'entryPageTitle',
  70. 'sqlSegment' => 'log_visit.visit_entry_idaction_name',
  71. 'sqlFilter' => $sqlFilter,
  72. );
  73. $segments[] = array(
  74. 'type' => 'dimension',
  75. 'category' => 'Actions_Actions',
  76. 'name' => 'Actions_ColumnExitPageURL',
  77. 'segment' => 'exitPageUrl',
  78. 'sqlSegment' => 'log_visit.visit_exit_idaction_url',
  79. 'sqlFilter' => $sqlFilter,
  80. );
  81. $segments[] = array(
  82. 'type' => 'dimension',
  83. 'category' => 'Actions_Actions',
  84. 'name' => 'Actions_ColumnExitPageTitle',
  85. 'segment' => 'exitPageTitle',
  86. 'sqlSegment' => 'log_visit.visit_exit_idaction_name',
  87. 'sqlFilter' => $sqlFilter,
  88. );
  89. // single pages
  90. $segments[] = array(
  91. 'type' => 'dimension',
  92. 'category' => 'Actions_Actions',
  93. 'name' => 'Actions_ColumnPageURL',
  94. 'segment' => 'pageUrl',
  95. 'sqlSegment' => 'log_link_visit_action.idaction_url',
  96. 'sqlFilter' => $sqlFilter,
  97. 'acceptedValues' => "All these segments must be URL encoded, for example: ".urlencode('http://example.com/path/page?query'),
  98. );
  99. $segments[] = array(
  100. 'type' => 'dimension',
  101. 'category' => 'Actions_Actions',
  102. 'name' => 'Actions_ColumnPageName',
  103. 'segment' => 'pageTitle',
  104. 'sqlSegment' => 'log_link_visit_action.idaction_name',
  105. 'sqlFilter' => $sqlFilter,
  106. );
  107. }
  108. function getIdActionFromString($string, $sqlField)
  109. {
  110. // Field is visit_*_idaction_url or visit_*_idaction_name
  111. $actionType = strpos($sqlField, '_name') === false
  112. ? Piwik_Tracker_Action::TYPE_ACTION_URL
  113. : Piwik_Tracker_Action::TYPE_ACTION_NAME;
  114. $sql = Piwik_Tracker_Action::getSqlSelectActionId();
  115. $bind = array($string, $string, $actionType);
  116. $idAction = Zend_Registry::get('db')->fetchOne($sql, $bind);
  117. // if the action is not found, we hack -1 to ensure it tries to match against an integer
  118. // otherwise binding idaction_name to "false" returns some rows for some reasons (in case &segment=pageTitle==Větrnásssssss)
  119. if(empty($idAction))
  120. {
  121. $idAction = -1;
  122. }
  123. return $idAction;
  124. }
  125. public function getReportMetadata($notification)
  126. {
  127. $reports = &$notification->getNotificationObject();
  128. $reports[] = array(
  129. 'category' => Piwik_Translate('Actions_Actions'),
  130. 'name' => Piwik_Translate('Actions_Actions'),
  131. 'module' => 'Actions',
  132. 'action' => 'get',
  133. 'metrics' => array(
  134. 'nb_pageviews' => Piwik_Translate('General_ColumnPageviews'),
  135. 'nb_uniq_pageviews' => Piwik_Translate('General_ColumnUniquePageviews'),
  136. 'nb_downloads' => Piwik_Translate('Actions_ColumnDownloads'),
  137. 'nb_uniq_downloads' => Piwik_Translate('Actions_ColumnUniqueDownloads'),
  138. 'nb_outlinks' => Piwik_Translate('Actions_ColumnOutlinks'),
  139. 'nb_uniq_outlinks' => Piwik_Translate('Actions_ColumnUniqueOutlinks')
  140. ),
  141. 'metricsDocumentation' => array(
  142. 'nb_pageviews' => Piwik_Translate('General_ColumnPageviewsDocumentation'),
  143. 'nb_uniq_pageviews' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'),
  144. 'nb_downloads' => Piwik_Translate('Actions_ColumnClicksDocumentation'),
  145. 'nb_uniq_downloads' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation'),
  146. 'nb_outlinks' => Piwik_Translate('Actions_ColumnClicksDocumentation'),
  147. 'nb_uniq_outlinks' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation')
  148. ),
  149. 'processedMetrics' => false,
  150. 'order' => 1
  151. );
  152. $metrics = array(
  153. 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviews'),
  154. 'nb_hits' => Piwik_Translate('General_ColumnPageviews'),
  155. 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrances'),
  156. 'avg_time_on_page' => Piwik_Translate('General_ColumnAverageTimeOnPage'),
  157. 'bounce_rate' => Piwik_Translate('General_ColumnBounceRate'),
  158. 'exit_nb_visits' => Piwik_Translate('General_ColumnExits'),
  159. 'exit_rate' => Piwik_Translate('General_ColumnExitRate'),
  160. // 'entry_bounce_count' => Piwik_Translate('General_ColumnBounces'),
  161. );
  162. $documentation = array(
  163. 'nb_hits' => 'General_ColumnPageviewsDocumentation',
  164. 'nb_visits' => 'General_ColumnUniquePageviewsDocumentation',
  165. 'bounce_rate' => 'General_ColumnPageBounceRateDocumentation',
  166. 'avg_time_on_page' => 'General_ColumnAverageTimeOnPageDocumentation',
  167. 'exit_rate' => 'General_ColumnExitRateDocumentation'
  168. );
  169. $documentation = array_map('Piwik_Translate', $documentation);
  170. // Page views URLs and Page titles have the full set of metrics
  171. $reports[] = array(
  172. 'category' => Piwik_Translate('Actions_Actions'),
  173. 'name' => Piwik_Translate('Actions_SubmenuPages'),
  174. 'module' => 'Actions',
  175. 'action' => 'getPageUrls',
  176. 'dimension' => Piwik_Translate('Actions_ColumnPageURL'),
  177. 'metrics' => $metrics,
  178. 'metricsDocumentation' => $documentation,
  179. 'documentation' => Piwik_Translate('Actions_PagesReportDocumentation', '<br />')
  180. .'<br />'.Piwik_Translate('General_UsePlusMinusIconsDocumentation'),
  181. 'processedMetrics' => false,
  182. 'order' => 2,
  183. );
  184. $reports[] = array(
  185. 'category' => Piwik_Translate('Actions_Actions'),
  186. 'name' => Piwik_Translate('Actions_SubmenuPageTitles'),
  187. 'module' => 'Actions',
  188. 'action' => 'getPageTitles',
  189. 'dimension' => Piwik_Translate('Actions_ColumnPageName'),
  190. 'metrics' => $metrics,
  191. 'metricsDocumentation' => $documentation,
  192. 'documentation' => Piwik_Translate('Actions_PageTitlesReportDocumentation', array('<br />', htmlentities('<title>'))),
  193. 'processedMetrics' => false,
  194. 'order' => 3,
  195. );
  196. // Outlinks and downloads only report basic metrics
  197. $metrics = array( 'nb_hits' => Piwik_Translate('General_ColumnPageviews'),
  198. 'nb_visits',
  199. );
  200. $documentation = array(
  201. 'nb_hits' => Piwik_Translate('Actions_ColumnClicksDocumentation'),
  202. 'nb_visits' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation')
  203. );
  204. $reports[] = array(
  205. 'category' => Piwik_Translate('Actions_Actions'),
  206. 'name' => Piwik_Translate('Actions_SubmenuOutlinks'),
  207. 'module' => 'Actions',
  208. 'action' => 'getOutlinks',
  209. 'dimension' => Piwik_Translate('Actions_ColumnClickedURL'),
  210. 'metrics' => $metrics,
  211. 'metricsDocumentation' => $documentation,
  212. 'documentation' => Piwik_Translate('Actions_OutlinksReportDocumentation').' '
  213. .Piwik_Translate('Actions_OutlinkDocumentation').'<br />'
  214. .Piwik_Translate('General_UsePlusMinusIconsDocumentation'),
  215. 'processedMetrics' => false,
  216. 'order' => 5,
  217. );
  218. $reports[] = array(
  219. 'category' => Piwik_Translate('Actions_Actions'),
  220. 'name' => Piwik_Translate('Actions_SubmenuDownloads'),
  221. 'module' => 'Actions',
  222. 'action' => 'getDownloads',
  223. 'dimension' => Piwik_Translate('Actions_ColumnDownloadURL'),
  224. 'metrics' => $metrics,
  225. 'metricsDocumentation' => $documentation,
  226. 'documentation' => Piwik_Translate('Actions_DownloadsReportDocumentation', '<br />'),
  227. 'processedMetrics' => false,
  228. 'order' => 7,
  229. );
  230. }
  231. function addWidgets()
  232. {
  233. Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuPagesEntry', 'Actions', 'getEntryPageUrls');
  234. Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuPagesExit', 'Actions', 'getExitPageUrls');
  235. Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuPages', 'Actions', 'getPageUrls');
  236. Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuPageTitles', 'Actions', 'getPageTitles');
  237. Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuOutlinks', 'Actions', 'getOutlinks');
  238. Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuDownloads', 'Actions', 'getDownloads');
  239. }
  240. function addMenus()
  241. {
  242. Piwik_AddMenu('Actions_Actions', '', array('module' => 'Actions', 'action' => 'indexPageUrls'), true, 15);
  243. Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPages', array('module' => 'Actions', 'action' => 'indexPageUrls'), true, 1);
  244. Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPagesEntry', array('module' => 'Actions', 'action' => 'indexEntryPageUrls'), true, 2);
  245. Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPagesExit', array('module' => 'Actions', 'action' => 'indexExitPageUrls'), true, 3);
  246. Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPageTitles', array('module' => 'Actions', 'action' => 'indexPageTitles'), true, 4);
  247. Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuOutlinks', array('module' => 'Actions', 'action' => 'indexOutlinks'), true, 5);
  248. Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuDownloads', array('module' => 'Actions', 'action' => 'indexDownloads'), true, 6);
  249. }
  250. static protected $invalidSummedColumnNameToRenamedNameForPeriodArchive = array(
  251. Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS,
  252. Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS,
  253. Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS,
  254. );
  255. protected static $invalidSummedColumnNameToDeleteFromDayArchive = array(
  256. Piwik_Archive::INDEX_NB_UNIQ_VISITORS,
  257. Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS,
  258. Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS,
  259. );
  260. public function __construct()
  261. {
  262. // for BC, we read the old style delimiter first (see #1067)
  263. $actionDelimiter = Zend_Registry::get('config')->General->action_category_delimiter;
  264. if(empty($actionDelimiter))
  265. {
  266. self::$actionUrlCategoryDelimiter = Zend_Registry::get('config')->General->action_url_category_delimiter;
  267. self::$actionTitleCategoryDelimiter = Zend_Registry::get('config')->General->action_title_category_delimiter;
  268. }
  269. else
  270. {
  271. self::$actionUrlCategoryDelimiter = self::$actionTitleCategoryDelimiter = $actionDelimiter;
  272. }
  273. self::$defaultActionName = Zend_Registry::get('config')->General->action_default_name;
  274. $this->columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS;
  275. $this->maximumRowsInDataTableLevelZero = Zend_Registry::get('config')->General->datatable_archiving_maximum_rows_actions;
  276. $this->maximumRowsInSubDataTable = Zend_Registry::get('config')->General->datatable_archiving_maximum_rows_subtable_actions;
  277. }
  278. function archivePeriod( $notification )
  279. {
  280. $archiveProcessing = $notification->getNotificationObject();
  281. if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;
  282. $dataTableToSum = array(
  283. 'Actions_actions',
  284. 'Actions_downloads',
  285. 'Actions_outlink',
  286. 'Actions_actions_url',
  287. );
  288. $archiveProcessing->archiveDataTable($dataTableToSum, self::$invalidSummedColumnNameToRenamedNameForPeriodArchive, $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation);
  289. $archiveProcessing->archiveNumericValuesSum(array(
  290. 'Actions_nb_pageviews',
  291. 'Actions_nb_uniq_pageviews',
  292. 'Actions_nb_downloads',
  293. 'Actions_nb_uniq_downloads',
  294. 'Actions_nb_outlinks',
  295. 'Actions_nb_uniq_outlinks'
  296. ));
  297. }
  298. /**
  299. * Compute all the actions along with their hierarchies.
  300. *
  301. * For each action we process the "interest statistics" :
  302. * visits, unique visitors, bouce count, sum visit length.
  303. *
  304. *
  305. */
  306. public function archiveDay( $notification )
  307. {
  308. /* @var $archiveProcessing Piwik_ArchiveProcessing_Day */
  309. $archiveProcessing = $notification->getNotificationObject();
  310. if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;
  311. $this->actionsTablesByType = array(
  312. Piwik_Tracker_Action::TYPE_ACTION_URL => array(),
  313. Piwik_Tracker_Action::TYPE_DOWNLOAD => array(),
  314. Piwik_Tracker_Action::TYPE_OUTLINK => array(),
  315. Piwik_Tracker_Action::TYPE_ACTION_NAME => array(),
  316. );
  317. // This row is used in the case where an action is know as an exit_action
  318. // but this action was not properly recorded when it was hit in the first place
  319. // so we add this fake row information to make sure there is a nb_hits, etc. column for every action
  320. $this->defaultRow = new Piwik_DataTable_Row(array(
  321. Piwik_DataTable_Row::COLUMNS => array(
  322. Piwik_Archive::INDEX_NB_VISITS => 1,
  323. Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 1,
  324. Piwik_Archive::INDEX_PAGE_NB_HITS => 1,
  325. )));
  326. /*
  327. * Page URLs and Page names, general stats
  328. */
  329. $select = "log_action.name,
  330. log_action.type,
  331. log_action.idaction,
  332. count(distinct log_link_visit_action.idvisit) as `". Piwik_Archive::INDEX_NB_VISITS ."`,
  333. count(distinct log_link_visit_action.idvisitor) as `". Piwik_Archive::INDEX_NB_UNIQ_VISITORS ."`,
  334. count(*) as `". Piwik_Archive::INDEX_PAGE_NB_HITS ."`";
  335. $from = array(
  336. "log_link_visit_action",
  337. array(
  338. "table" => "log_action",
  339. "joinOn" => "log_link_visit_action.%s = log_action.idaction"
  340. )
  341. );
  342. $where = "log_link_visit_action.server_time >= ?
  343. AND log_link_visit_action.server_time <= ?
  344. AND log_link_visit_action.idsite = ?
  345. AND log_link_visit_action.%s IS NOT NULL";
  346. $groupBy = "log_action.idaction";
  347. $orderBy = "`". Piwik_Archive::INDEX_PAGE_NB_HITS ."` DESC";
  348. $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy,
  349. "idaction_url", $archiveProcessing);
  350. $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy,
  351. "idaction_name", $archiveProcessing);
  352. /*
  353. * Entry actions for Page URLs and Page names
  354. */
  355. $select = "log_visit.%s as idaction,
  356. count(distinct log_visit.idvisitor) as `". Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS ."`,
  357. count(*) as `". Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS ."`,
  358. sum(log_visit.visit_total_actions) as `". Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS ."`,
  359. sum(log_visit.visit_total_time) as `". Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH ."`,
  360. sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `". Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT ."`";
  361. $from = "log_visit";
  362. $where = "log_visit.visit_last_action_time >= ?
  363. AND log_visit.visit_last_action_time <= ?
  364. AND log_visit.idsite = ?
  365. AND log_visit.%s > 0";
  366. $groupBy = "log_visit.%s, idaction";
  367. $this->archiveDayQueryProcess($select, $from, $where, $orderBy=false, $groupBy,
  368. "visit_entry_idaction_url", $archiveProcessing);
  369. $this->archiveDayQueryProcess($select, $from, $where, $orderBy=false, $groupBy,
  370. "visit_entry_idaction_name", $archiveProcessing);
  371. /*
  372. * Exit actions
  373. */
  374. $select = "log_visit.%s as idaction,
  375. count(distinct log_visit.idvisitor) as `". Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS ."`,
  376. count(*) as `". Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS ."`";
  377. $from = "log_visit";
  378. $where = "log_visit.visit_last_action_time >= ?
  379. AND log_visit.visit_last_action_time <= ?
  380. AND log_visit.idsite = ?
  381. AND log_visit.%s > 0";
  382. $groupBy = "log_visit.%s, idaction";
  383. $this->archiveDayQueryProcess($select, $from, $where, $orderBy=false, $groupBy,
  384. "visit_exit_idaction_url", $archiveProcessing);
  385. $this->archiveDayQueryProcess($select, $from, $where, $orderBy=false, $groupBy,
  386. "visit_exit_idaction_name", $archiveProcessing);
  387. /*
  388. * Time per action
  389. */
  390. $select = "log_link_visit_action.%s as idaction,
  391. sum(log_link_visit_action.time_spent_ref_action) as `".Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT."`";
  392. $from = "log_link_visit_action";
  393. $where = "log_link_visit_action.server_time >= ?
  394. AND log_link_visit_action.server_time <= ?
  395. AND log_link_visit_action.idsite = ?
  396. AND log_link_visit_action.time_spent_ref_action > 0
  397. AND log_link_visit_action.%s > 0";
  398. $groupBy = "log_link_visit_action.%s, idaction";
  399. $this->archiveDayQueryProcess($select, $from, $where, $orderBy=false, $groupBy,
  400. "idaction_url_ref", $archiveProcessing);
  401. $this->archiveDayQueryProcess($select, $from, $where, $orderBy=false, $groupBy,
  402. "idaction_name_ref", $archiveProcessing);
  403. // Empty static cache
  404. self::$cacheParsedAction = array();
  405. // Record the final datasets
  406. $this->archiveDayRecordInDatabase($archiveProcessing);
  407. }
  408. protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy,
  409. $sprintfField, $archiveProcessing)
  410. {
  411. // idaction field needs to be set in select clause before calling getSelectQuery().
  412. // if a complex segmentation join is needed, the field needs to be propagated
  413. // to the outer select. therefore, $segment needs to know about it.
  414. $select = sprintf($select, $sprintfField);
  415. // get query with segmentation
  416. $bind = array();
  417. $orderBy = false;
  418. $query = $archiveProcessing->getSegment()->getSelectQuery(
  419. $select, $from, $where, $bind, $orderBy, $groupBy);
  420. // replace the rest of the %s
  421. $querySql = str_replace("%s", $sprintfField, $query['sql']);
  422. // extend bindings
  423. $bind = array_merge(array($archiveProcessing->getStartDatetimeUTC(),
  424. $archiveProcessing->getEndDatetimeUTC(), $archiveProcessing->idsite), $query['bind']);
  425. // get result
  426. $resultSet = $archiveProcessing->db->query($querySql, $bind);
  427. $modified = $this->updateActionsTableWithRowQuery($resultSet, $sprintfField);
  428. return $modified;
  429. }
  430. protected function archiveDayRecordInDatabase($archiveProcessing)
  431. {
  432. $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_URL]);
  433. $this->deleteInvalidSummedColumnsFromDataTable($dataTable);
  434. $s = $dataTable->getSerialized( $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation );
  435. $archiveProcessing->insertBlobRecord('Actions_actions_url', $s);
  436. $archiveProcessing->insertNumericRecord('Actions_nb_pageviews', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS)));
  437. $archiveProcessing->insertNumericRecord('Actions_nb_uniq_pageviews', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS)));
  438. destroy($dataTable);
  439. $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_DOWNLOAD]);
  440. $this->deleteInvalidSummedColumnsFromDataTable($dataTable);
  441. $s = $dataTable->getSerialized($this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation );
  442. $archiveProcessing->insertBlobRecord('Actions_downloads', $s);
  443. $archiveProcessing->insertNumericRecord('Actions_nb_downloads', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS)));
  444. $archiveProcessing->insertNumericRecord('Actions_nb_uniq_downloads', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS)));
  445. destroy($dataTable);
  446. $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_OUTLINK]);
  447. $this->deleteInvalidSummedColumnsFromDataTable($dataTable);
  448. $s = $dataTable->getSerialized( $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation );
  449. $archiveProcessing->insertBlobRecord('Actions_outlink', $s);
  450. $archiveProcessing->insertNumericRecord('Actions_nb_outlinks', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS)));
  451. $archiveProcessing->insertNumericRecord('Actions_nb_uniq_outlinks', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS)));
  452. destroy($dataTable);
  453. $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_NAME]);
  454. $this->deleteInvalidSummedColumnsFromDataTable($dataTable);
  455. $s = $dataTable->getSerialized( $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation );
  456. $archiveProcessing->insertBlobRecord('Actions_actions', $s);
  457. destroy($dataTable);
  458. destroy($this->actionsTablesByType);
  459. }
  460. protected function deleteInvalidSummedColumnsFromDataTable($dataTable)
  461. {
  462. foreach($dataTable->getRows() as $row)
  463. {
  464. if(($idSubtable = $row->getIdSubDataTable()) !== null)
  465. {
  466. foreach(self::$invalidSummedColumnNameToDeleteFromDayArchive as $name)
  467. {
  468. $row->deleteColumn($name);
  469. }
  470. $this->deleteInvalidSummedColumnsFromDataTable(Piwik_DataTable_Manager::getInstance()->getTable($idSubtable));
  471. }
  472. }
  473. }
  474. /**
  475. * Explodes action name into an array of elements.
  476. *
  477. * for downloads:
  478. * we explode link http://piwik.org/some/path/piwik.zip into an array( 'piwik.org', '/some/path/piwik.zip' );
  479. *
  480. * for outlinks:
  481. * we explode link http://dev.piwik.org/some/path into an array( 'dev.piwik.org', '/some/path' );
  482. *
  483. * for action urls:
  484. * we explode link http://piwik.org/some/path into an array( 'some', 'path' );
  485. *
  486. * for action names:
  487. * we explode name 'Piwik / Category 1 / Category 2' into an array('Piwik', 'Category 1', 'Category 2');
  488. *
  489. * @param string action name
  490. * @param int action type
  491. * @return array of exploded elements from $name
  492. */
  493. static public function getActionExplodedNames($name, $type)
  494. {
  495. $matches = array();
  496. $isUrl = false;
  497. $name = str_replace("\n", "", $name);
  498. preg_match('@^http[s]?://([^/]+)[/]?([^#]*)[#]?(.*)$@i', $name, $matches);
  499. if( count($matches) )
  500. {
  501. $isUrl = true;
  502. $urlHost = $matches[1];
  503. $urlPath = $matches[2];
  504. $urlAnchor = $matches[3];
  505. }
  506. if($type == Piwik_Tracker_Action::TYPE_DOWNLOAD
  507. || $type == Piwik_Tracker_Action::TYPE_OUTLINK)
  508. {
  509. if( $isUrl )
  510. {
  511. return array(trim($urlHost), '/' . trim($urlPath));
  512. }
  513. }
  514. if( $isUrl )
  515. {
  516. $name = $urlPath;
  517. if( empty($name) || substr($name, -1) == '/' )
  518. {
  519. $name .= self::$defaultActionName;
  520. }
  521. }
  522. if($type == Piwik_Tracker_Action::TYPE_ACTION_NAME)
  523. {
  524. $categoryDelimiter = self::$actionTitleCategoryDelimiter;
  525. }
  526. else
  527. {
  528. $categoryDelimiter = self::$actionUrlCategoryDelimiter;
  529. }
  530. if(empty($categoryDelimiter))
  531. {
  532. return array( trim($name) );
  533. }
  534. $split = explode($categoryDelimiter, $name, self::$limitLevelSubCategory);
  535. // trim every category and remove empty categories
  536. $split = array_map('trim', $split);
  537. $split = array_filter($split, 'strlen');
  538. // forces array key to start at 0
  539. $split = array_values($split);
  540. if( empty($split) )
  541. {
  542. $defaultName = self::getUnknownActionName($type);
  543. return array( trim($defaultName) );
  544. }
  545. $lastPageName = end($split);
  546. // we are careful to prefix the page URL / name with some value
  547. // so that if a page has the same name as a category
  548. // we don't merge both entries
  549. if($type != Piwik_Tracker_Action::TYPE_ACTION_NAME )
  550. {
  551. $lastPageName = '/' . $lastPageName;
  552. }
  553. else
  554. {
  555. $lastPageName = ' ' . $lastPageName;
  556. }
  557. $split[count($split)-1] = $lastPageName;
  558. return array_values( $split );
  559. }
  560. static protected function getUnknownActionName($type)
  561. {
  562. if(empty(self::$defaultActionNameWhenNotDefined))
  563. {
  564. self::$defaultActionNameWhenNotDefined = Piwik_Translate('General_NotDefined', Piwik_Translate('Actions_ColumnPageName'));
  565. self::$defaultActionUrlWhenNotDefined = Piwik_Translate('General_NotDefined', Piwik_Translate('Actions_ColumnPageURL'));
  566. }
  567. if($type == Piwik_Tracker_Action::TYPE_ACTION_NAME) {
  568. return self::$defaultActionNameWhenNotDefined;
  569. }
  570. return self::$defaultActionUrlWhenNotDefined;
  571. }
  572. const CACHE_PARSED_INDEX_NAME = 0;
  573. const CACHE_PARSED_INDEX_TYPE = 1;
  574. static $cacheParsedAction = array();
  575. protected function updateActionsTableWithRowQuery($query, $fieldQueried = false)
  576. {
  577. $rowsProcessed = 0;
  578. while( $row = $query->fetch() )
  579. {
  580. if(empty($row['idaction']))
  581. {
  582. $row['type'] = ($fieldQueried == 'idaction_url' ? Piwik_Tracker_Action::TYPE_ACTION_URL : Piwik_Tracker_Action::TYPE_ACTION_NAME);
  583. // This will be replaced with 'X not defined' later
  584. $row['name'] = '';
  585. // Yes, this is kind of a hack, so we don't mix 'page url not defined' with 'page title not defined' etc.
  586. $row['idaction'] = -$row['type'];
  587. }
  588. // Only the first query will contain the name and type of actions, for performance reasons
  589. if(isset($row['name'])
  590. && isset($row['type']))
  591. {
  592. $actionName = $row['name'];
  593. $actionType = $row['type'];
  594. // in some unknown case, the type field is NULL, as reported in #1082 - we ignore this page view
  595. if(empty($actionType))
  596. {
  597. self::$cacheParsedAction[$row['idaction']] = false;
  598. continue;
  599. }
  600. $currentTable = $this->parseActionNameCategoriesInDataTable($actionName, $actionType);
  601. self::$cacheParsedAction[$row['idaction']] = $currentTable;
  602. }
  603. else
  604. {
  605. if(!isset(self::$cacheParsedAction[$row['idaction']]))
  606. {
  607. // This can happen when
  608. // - We select an entry page ID that was only seen yesterday, so wasn't selected in the first query
  609. // - We count time spent on a page, when this page was only seen yesterday
  610. continue;
  611. }
  612. $currentTable = self::$cacheParsedAction[$row['idaction']];
  613. // Action processed as "to skip" for some reasons
  614. if($currentTable === false)
  615. {
  616. continue;
  617. }
  618. }
  619. unset($row['name']);
  620. unset($row['type']);
  621. unset($row['idaction']);
  622. foreach($row as $name => $value)
  623. {
  624. // in some edge cases, we have twice the same action name with 2 different idaction
  625. // this happens when 2 visitors visit the same new page at the same time, there is a SELECT and an INSERT for each new page,
  626. // and in between the two the other visitor comes.
  627. // here we handle the case where there is already a row for this action name, if this is the case we add the value
  628. if(($alreadyValue = $currentTable->getColumn($name)) !== false)
  629. {
  630. $currentTable->setColumn($name, $alreadyValue+$value);
  631. }
  632. else
  633. {
  634. $currentTable->addColumn($name, $value);
  635. }
  636. }
  637. // if the exit_action was not recorded properly in the log_link_visit_action
  638. // there would be an error message when getting the nb_hits column
  639. // we must fake the record and add the columns
  640. if($currentTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS) === false)
  641. {
  642. // to test this code: delete the entries in log_link_action_visit for
  643. // a given exit_idaction_url
  644. foreach($this->defaultRow->getColumns() as $name => $value)
  645. {
  646. $currentTable->addColumn($name, $value);
  647. }
  648. }
  649. $rowsProcessed++;
  650. }
  651. // just to make sure php copies the last $currentTable in the $parentTable array
  652. $currentTable =& $this->actionsTablesByType;
  653. return $rowsProcessed;
  654. }
  655. /**
  656. * Given a page name and type, builds a recursive datatable where
  657. * each level of the tree is a category, based on the page name split by a delimiter (slash / by default)
  658. *
  659. * @param string $actionName
  660. * @param int $actionType
  661. * @return Piwik_DataTable
  662. */
  663. protected function parseActionNameCategoriesInDataTable($actionName, $actionType)
  664. {
  665. // we work on the root table of the given TYPE (either ACTION_URL or DOWNLOAD or OUTLINK etc.)
  666. $currentTable =& $this->actionsTablesByType[$actionType];
  667. // go to the level of the subcategory
  668. $actionExplodedNames = $this->getActionExplodedNames($actionName, $actionType);
  669. $end = count($actionExplodedNames)-1;
  670. for($level = 0 ; $level < $end; $level++)
  671. {
  672. $actionCategory = $actionExplodedNames[$level];
  673. $currentTable =& $currentTable[$actionCategory];
  674. }
  675. $actionShortName = $actionExplodedNames[$end];
  676. // currentTable is now the array element corresponding the the action
  677. // at this point we may be for example at the 4th level of depth in the hierarchy
  678. $currentTable =& $currentTable[$actionShortName];
  679. // add the row to the matching sub category subtable
  680. if(!($currentTable instanceof Piwik_DataTable_Row))
  681. {
  682. $defaultColumnsNewRow = array(
  683. 'label' => (string)$actionShortName,
  684. Piwik_Archive::INDEX_NB_VISITS => 0,
  685. Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0,
  686. Piwik_Archive::INDEX_PAGE_NB_HITS => 0,
  687. Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT => 0,
  688. );
  689. if( $actionType == Piwik_Tracker_Action::TYPE_ACTION_NAME )
  690. {
  691. $currentTable = new Piwik_DataTable_Row(array(
  692. Piwik_DataTable_Row::COLUMNS => $defaultColumnsNewRow,
  693. ));
  694. }
  695. else
  696. {
  697. $currentTable = new Piwik_DataTable_Row(array(
  698. Piwik_DataTable_Row::COLUMNS => $defaultColumnsNewRow,
  699. Piwik_DataTable_Row::METADATA => array('url' => (string)$actionName),
  700. ));
  701. }
  702. }
  703. return $currentTable;
  704. }
  705. }