Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Visitor.php 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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\Live;
  10. use Piwik\Common;
  11. use Piwik\DataAccess\LogAggregator;
  12. use Piwik\DataTable\Filter\ColumnDelete;
  13. use Piwik\Date;
  14. use Piwik\Db;
  15. use Piwik\IP;
  16. use Piwik\Piwik;
  17. use Piwik\Plugins\CustomVariables\CustomVariables;
  18. use Piwik\Plugins\UserCountry\LocationProvider\GeoIp;
  19. use Piwik\Plugins\Actions\Actions\ActionSiteSearch;
  20. use Piwik\Tracker;
  21. use Piwik\Tracker\Action;
  22. use Piwik\Tracker\GoalManager;
  23. class Visitor implements VisitorInterface
  24. {
  25. const EVENT_VALUE_PRECISION = 3;
  26. private $details = array();
  27. function __construct($visitorRawData)
  28. {
  29. $this->details = $visitorRawData;
  30. }
  31. function getAllVisitorDetails()
  32. {
  33. $visitor = array(
  34. 'idSite' => $this->getIdSite(),
  35. 'idVisit' => $this->getIdVisit(),
  36. 'visitIp' => $this->getIp(),
  37. 'visitorId' => $this->getVisitorId(),
  38. // => false are placeholders to be filled in API later
  39. 'actionDetails' => false,
  40. 'goalConversions' => false,
  41. 'siteCurrency' => false,
  42. 'siteCurrencySymbol' => false,
  43. // all time entries
  44. 'serverDate' => $this->getServerDate(),
  45. 'visitServerHour' => $this->getVisitServerHour(),
  46. 'lastActionTimestamp' => $this->getTimestampLastAction(),
  47. 'lastActionDateTime' => $this->getDateTimeLastAction(),
  48. );
  49. /**
  50. * This event can be used to add any details to a visitor. The visitor's details are for instance used in
  51. * API requests like 'Live.getVisitorProfile' and 'Live.getLastVisitDetails'. This can be useful for instance
  52. * in case your plugin defines any visit dimensions and you want to add the value of your dimension to a user.
  53. * It can be also useful if you want to enrich a visitor with custom fields based on other fields or if you
  54. * want to change or remove any fields from the user.
  55. *
  56. * **Example**
  57. *
  58. * Piwik::addAction('Live.getAllVisitorDetails', function (&visitor, $details) {
  59. * $visitor['userPoints'] = $details['actions'] + $details['events'] + $details['searches'];
  60. * unset($visitor['anyFieldYouWantToRemove']);
  61. * });
  62. *
  63. * @param array &visitor You can add or remove fields to the visitor array and it will reflected in the API output
  64. * @param array $details The details array contains all visit dimensions (columns of log_visit table)
  65. */
  66. Piwik::postEvent('Live.getAllVisitorDetails', array(&$visitor, $this->details));
  67. return $visitor;
  68. }
  69. function getVisitorId()
  70. {
  71. if (isset($this->details['idvisitor'])) {
  72. return bin2hex($this->details['idvisitor']);
  73. }
  74. return false;
  75. }
  76. function getVisitServerHour()
  77. {
  78. return date('G', strtotime($this->details['visit_last_action_time']));
  79. }
  80. function getServerDate()
  81. {
  82. return date('Y-m-d', strtotime($this->details['visit_last_action_time']));
  83. }
  84. function getIp()
  85. {
  86. if (isset($this->details['location_ip'])) {
  87. return IP::N2P($this->details['location_ip']);
  88. }
  89. return false;
  90. }
  91. function getIdVisit()
  92. {
  93. return $this->details['idvisit'];
  94. }
  95. function getIdSite()
  96. {
  97. return $this->details['idsite'];
  98. }
  99. function getTimestampLastAction()
  100. {
  101. return strtotime($this->details['visit_last_action_time']);
  102. }
  103. function getDateTimeLastAction()
  104. {
  105. return date('Y-m-d H:i:s', strtotime($this->details['visit_last_action_time']));
  106. }
  107. /**
  108. * Removes fields that are not meant to be displayed (md5 config hash)
  109. * Or that the user should only access if he is Super User or admin (cookie, IP)
  110. *
  111. * @param array $visitorDetails
  112. * @return array
  113. */
  114. public static function cleanVisitorDetails($visitorDetails)
  115. {
  116. $toUnset = array('config_id');
  117. if (Piwik::isUserIsAnonymous()) {
  118. $toUnset[] = 'idvisitor';
  119. $toUnset[] = 'user_id';
  120. $toUnset[] = 'location_ip';
  121. }
  122. foreach ($toUnset as $keyName) {
  123. if (isset($visitorDetails[$keyName])) {
  124. unset($visitorDetails[$keyName]);
  125. }
  126. }
  127. return $visitorDetails;
  128. }
  129. /**
  130. * The &flat=1 feature is used by API.getSuggestedValuesForSegment
  131. *
  132. * @param $visitorDetailsArray
  133. * @return array
  134. */
  135. public static function flattenVisitorDetailsArray($visitorDetailsArray)
  136. {
  137. // NOTE: if you flatten more fields from the "actionDetails" array
  138. // ==> also update API/API.php getSuggestedValuesForSegment(), the $segmentsNeedActionsInfo array
  139. // flatten visit custom variables
  140. if (is_array($visitorDetailsArray['customVariables'])) {
  141. foreach ($visitorDetailsArray['customVariables'] as $thisCustomVar) {
  142. $visitorDetailsArray = array_merge($visitorDetailsArray, $thisCustomVar);
  143. }
  144. unset($visitorDetailsArray['customVariables']);
  145. }
  146. // flatten page views custom variables
  147. $count = 1;
  148. foreach ($visitorDetailsArray['actionDetails'] as $action) {
  149. if (!empty($action['customVariables'])) {
  150. foreach ($action['customVariables'] as $thisCustomVar) {
  151. foreach ($thisCustomVar as $cvKey => $cvValue) {
  152. $flattenedKeyName = $cvKey . ColumnDelete::APPEND_TO_COLUMN_NAME_TO_KEEP . $count;
  153. $visitorDetailsArray[$flattenedKeyName] = $cvValue;
  154. $count++;
  155. }
  156. }
  157. }
  158. }
  159. // Flatten Goals
  160. $count = 1;
  161. foreach ($visitorDetailsArray['actionDetails'] as $action) {
  162. if (!empty($action['goalId'])) {
  163. $flattenedKeyName = 'visitConvertedGoalId' . ColumnDelete::APPEND_TO_COLUMN_NAME_TO_KEEP . $count;
  164. $visitorDetailsArray[$flattenedKeyName] = $action['goalId'];
  165. $count++;
  166. }
  167. }
  168. // Flatten Page Titles/URLs
  169. $count = 1;
  170. foreach ($visitorDetailsArray['actionDetails'] as $action) {
  171. if (!empty($action['url'])) {
  172. $flattenedKeyName = 'pageUrl' . ColumnDelete::APPEND_TO_COLUMN_NAME_TO_KEEP . $count;
  173. $visitorDetailsArray[$flattenedKeyName] = $action['url'];
  174. }
  175. // API.getSuggestedValuesForSegment
  176. $flatten = array( 'pageTitle', 'siteSearchKeyword', 'eventCategory', 'eventAction', 'eventName', 'eventValue');
  177. foreach($flatten as $toFlatten) {
  178. if (!empty($action[$toFlatten])) {
  179. $flattenedKeyName = $toFlatten . ColumnDelete::APPEND_TO_COLUMN_NAME_TO_KEEP . $count;
  180. $visitorDetailsArray[$flattenedKeyName] = $action[$toFlatten];
  181. }
  182. }
  183. $count++;
  184. }
  185. // Entry/exit pages
  186. $firstAction = $lastAction = false;
  187. foreach ($visitorDetailsArray['actionDetails'] as $action) {
  188. if ($action['type'] == 'action') {
  189. if (empty($firstAction)) {
  190. $firstAction = $action;
  191. }
  192. $lastAction = $action;
  193. }
  194. }
  195. if (!empty($firstAction['pageTitle'])) {
  196. $visitorDetailsArray['entryPageTitle'] = $firstAction['pageTitle'];
  197. }
  198. if (!empty($firstAction['url'])) {
  199. $visitorDetailsArray['entryPageUrl'] = $firstAction['url'];
  200. }
  201. if (!empty($lastAction['pageTitle'])) {
  202. $visitorDetailsArray['exitPageTitle'] = $lastAction['pageTitle'];
  203. }
  204. if (!empty($lastAction['url'])) {
  205. $visitorDetailsArray['exitPageUrl'] = $lastAction['url'];
  206. }
  207. return $visitorDetailsArray;
  208. }
  209. /**
  210. * @param $visitorDetailsArray
  211. * @param $actionsLimit
  212. * @param $timezone
  213. * @return array
  214. */
  215. public static function enrichVisitorArrayWithActions($visitorDetailsArray, $actionsLimit, $timezone)
  216. {
  217. $idVisit = $visitorDetailsArray['idVisit'];
  218. $maxCustomVariables = CustomVariables::getMaxCustomVariables();
  219. $sqlCustomVariables = '';
  220. for ($i = 1; $i <= $maxCustomVariables; $i++) {
  221. $sqlCustomVariables .= ', custom_var_k' . $i . ', custom_var_v' . $i;
  222. }
  223. // The second join is a LEFT join to allow returning records that don't have a matching page title
  224. // eg. Downloads, Outlinks. For these, idaction_name is set to 0
  225. $sql = "
  226. SELECT
  227. COALESCE(log_action_event_category.type, log_action.type, log_action_title.type) AS type,
  228. log_action.name AS url,
  229. log_action.url_prefix,
  230. log_action_title.name AS pageTitle,
  231. log_action.idaction AS pageIdAction,
  232. log_link_visit_action.server_time as serverTimePretty,
  233. log_link_visit_action.time_spent_ref_action as timeSpentRef,
  234. log_link_visit_action.idlink_va AS pageId,
  235. log_link_visit_action.custom_float
  236. ". $sqlCustomVariables . ",
  237. log_action_event_category.name AS eventCategory,
  238. log_action_event_action.name as eventAction
  239. FROM " . Common::prefixTable('log_link_visit_action') . " AS log_link_visit_action
  240. LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action
  241. ON log_link_visit_action.idaction_url = log_action.idaction
  242. LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_title
  243. ON log_link_visit_action.idaction_name = log_action_title.idaction
  244. LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_event_category
  245. ON log_link_visit_action.idaction_event_category = log_action_event_category.idaction
  246. LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_event_action
  247. ON log_link_visit_action.idaction_event_action = log_action_event_action.idaction
  248. WHERE log_link_visit_action.idvisit = ?
  249. ORDER BY server_time ASC
  250. LIMIT 0, $actionsLimit
  251. ";
  252. $actionDetails = Db::fetchAll($sql, array($idVisit));
  253. foreach ($actionDetails as $actionIdx => &$actionDetail) {
  254. $actionDetail =& $actionDetails[$actionIdx];
  255. $customVariablesPage = array();
  256. for ($i = 1; $i <= $maxCustomVariables; $i++) {
  257. if (!empty($actionDetail['custom_var_k' . $i])) {
  258. $cvarKey = $actionDetail['custom_var_k' . $i];
  259. $cvarKey = static::getCustomVariablePrettyKey($cvarKey);
  260. $customVariablesPage[$i] = array(
  261. 'customVariablePageName' . $i => $cvarKey,
  262. 'customVariablePageValue' . $i => $actionDetail['custom_var_v' . $i],
  263. );
  264. }
  265. unset($actionDetail['custom_var_k' . $i]);
  266. unset($actionDetail['custom_var_v' . $i]);
  267. }
  268. if (!empty($customVariablesPage)) {
  269. $actionDetail['customVariables'] = $customVariablesPage;
  270. }
  271. if ($actionDetail['type'] == Action::TYPE_CONTENT) {
  272. unset($actionDetails[$actionIdx]);
  273. continue;
  274. } elseif ($actionDetail['type'] == Action::TYPE_EVENT_CATEGORY) {
  275. // Handle Event
  276. if(strlen($actionDetail['pageTitle']) > 0) {
  277. $actionDetail['eventName'] = $actionDetail['pageTitle'];
  278. }
  279. unset($actionDetail['pageTitle']);
  280. } else if ($actionDetail['type'] == Action::TYPE_SITE_SEARCH) {
  281. // Handle Site Search
  282. $actionDetail['siteSearchKeyword'] = $actionDetail['pageTitle'];
  283. unset($actionDetail['pageTitle']);
  284. }
  285. // Event value / Generation time
  286. if($actionDetail['type'] == Action::TYPE_EVENT_CATEGORY) {
  287. if(strlen($actionDetail['custom_float']) > 0) {
  288. $actionDetail['eventValue'] = round($actionDetail['custom_float'], self::EVENT_VALUE_PRECISION);
  289. }
  290. } elseif ($actionDetail['custom_float'] > 0) {
  291. $actionDetail['generationTime'] = \Piwik\MetricsFormatter::getPrettyTimeFromSeconds($actionDetail['custom_float'] / 1000);
  292. }
  293. unset($actionDetail['custom_float']);
  294. if ($actionDetail['type'] != Action::TYPE_EVENT_CATEGORY) {
  295. unset($actionDetail['eventCategory']);
  296. unset($actionDetail['eventAction']);
  297. }
  298. // Reconstruct url from prefix
  299. $actionDetail['url'] = Tracker\PageUrl::reconstructNormalizedUrl($actionDetail['url'], $actionDetail['url_prefix']);
  300. unset($actionDetail['url_prefix']);
  301. // Set the time spent for this action (which is the timeSpentRef of the next action)
  302. if (isset($actionDetails[$actionIdx + 1])) {
  303. $actionDetail['timeSpent'] = $actionDetails[$actionIdx + 1]['timeSpentRef'];
  304. $actionDetail['timeSpentPretty'] = \Piwik\MetricsFormatter::getPrettyTimeFromSeconds($actionDetail['timeSpent']);
  305. }
  306. unset($actionDetails[$actionIdx]['timeSpentRef']); // not needed after timeSpent is added
  307. }
  308. // If the visitor converted a goal, we shall select all Goals
  309. $sql = "
  310. SELECT
  311. 'goal' as type,
  312. goal.name as goalName,
  313. goal.idgoal as goalId,
  314. goal.revenue as revenue,
  315. log_conversion.idlink_va as goalPageId,
  316. log_conversion.server_time as serverTimePretty,
  317. log_conversion.url as url
  318. FROM " . Common::prefixTable('log_conversion') . " AS log_conversion
  319. LEFT JOIN " . Common::prefixTable('goal') . " AS goal
  320. ON (goal.idsite = log_conversion.idsite
  321. AND
  322. goal.idgoal = log_conversion.idgoal)
  323. AND goal.deleted = 0
  324. WHERE log_conversion.idvisit = ?
  325. AND log_conversion.idgoal > 0
  326. ORDER BY server_time ASC
  327. LIMIT 0, $actionsLimit
  328. ";
  329. $goalDetails = Db::fetchAll($sql, array($idVisit));
  330. $sql = "SELECT
  331. case idgoal when " . GoalManager::IDGOAL_CART . " then '" . Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART . "' else '" . Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER . "' end as type,
  332. idorder as orderId,
  333. " . LogAggregator::getSqlRevenue('revenue') . " as revenue,
  334. " . LogAggregator::getSqlRevenue('revenue_subtotal') . " as revenueSubTotal,
  335. " . LogAggregator::getSqlRevenue('revenue_tax') . " as revenueTax,
  336. " . LogAggregator::getSqlRevenue('revenue_shipping') . " as revenueShipping,
  337. " . LogAggregator::getSqlRevenue('revenue_discount') . " as revenueDiscount,
  338. items as items,
  339. log_conversion.server_time as serverTimePretty
  340. FROM " . Common::prefixTable('log_conversion') . " AS log_conversion
  341. WHERE idvisit = ?
  342. AND idgoal <= " . GoalManager::IDGOAL_ORDER . "
  343. ORDER BY server_time ASC
  344. LIMIT 0, $actionsLimit";
  345. $ecommerceDetails = Db::fetchAll($sql, array($idVisit));
  346. foreach ($ecommerceDetails as &$ecommerceDetail) {
  347. if ($ecommerceDetail['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) {
  348. unset($ecommerceDetail['orderId']);
  349. unset($ecommerceDetail['revenueSubTotal']);
  350. unset($ecommerceDetail['revenueTax']);
  351. unset($ecommerceDetail['revenueShipping']);
  352. unset($ecommerceDetail['revenueDiscount']);
  353. }
  354. // 25.00 => 25
  355. foreach ($ecommerceDetail as $column => $value) {
  356. if (strpos($column, 'revenue') !== false) {
  357. if ($value == round($value)) {
  358. $ecommerceDetail[$column] = round($value);
  359. }
  360. }
  361. }
  362. }
  363. // Enrich ecommerce carts/orders with the list of products
  364. usort($ecommerceDetails, array('static', 'sortByServerTime'));
  365. foreach ($ecommerceDetails as &$ecommerceConversion) {
  366. $sql = "SELECT
  367. log_action_sku.name as itemSKU,
  368. log_action_name.name as itemName,
  369. log_action_category.name as itemCategory,
  370. " . LogAggregator::getSqlRevenue('price') . " as price,
  371. quantity as quantity
  372. FROM " . Common::prefixTable('log_conversion_item') . "
  373. INNER JOIN " . Common::prefixTable('log_action') . " AS log_action_sku
  374. ON idaction_sku = log_action_sku.idaction
  375. LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_name
  376. ON idaction_name = log_action_name.idaction
  377. LEFT JOIN " . Common::prefixTable('log_action') . " AS log_action_category
  378. ON idaction_category = log_action_category.idaction
  379. WHERE idvisit = ?
  380. AND idorder = ?
  381. AND deleted = 0
  382. LIMIT 0, $actionsLimit
  383. ";
  384. $bind = array($idVisit, isset($ecommerceConversion['orderId'])
  385. ? $ecommerceConversion['orderId']
  386. : GoalManager::ITEM_IDORDER_ABANDONED_CART
  387. );
  388. $itemsDetails = Db::fetchAll($sql, $bind);
  389. foreach ($itemsDetails as &$detail) {
  390. if ($detail['price'] == round($detail['price'])) {
  391. $detail['price'] = round($detail['price']);
  392. }
  393. }
  394. $ecommerceConversion['itemDetails'] = $itemsDetails;
  395. }
  396. $actions = array_merge($actionDetails, $goalDetails, $ecommerceDetails);
  397. usort($actions, array('static', 'sortByServerTime'));
  398. $visitorDetailsArray['actionDetails'] = $actions;
  399. foreach ($visitorDetailsArray['actionDetails'] as &$details) {
  400. switch ($details['type']) {
  401. case 'goal':
  402. $details['icon'] = 'plugins/Morpheus/images/goal.png';
  403. break;
  404. case Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER:
  405. case Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART:
  406. $details['icon'] = 'plugins/Morpheus/images/' . $details['type'] . '.gif';
  407. break;
  408. case Action::TYPE_DOWNLOAD:
  409. $details['type'] = 'download';
  410. $details['icon'] = 'plugins/Morpheus/images/download.png';
  411. break;
  412. case Action::TYPE_OUTLINK:
  413. $details['type'] = 'outlink';
  414. $details['icon'] = 'plugins/Morpheus/images/link.gif';
  415. break;
  416. case Action::TYPE_SITE_SEARCH:
  417. $details['type'] = 'search';
  418. $details['icon'] = 'plugins/Morpheus/images/search_ico.png';
  419. break;
  420. case Action::TYPE_EVENT_CATEGORY:
  421. $details['type'] = 'event';
  422. $details['icon'] = 'plugins/Morpheus/images/event.png';
  423. break;
  424. default:
  425. $details['type'] = 'action';
  426. $details['icon'] = null;
  427. break;
  428. }
  429. // Convert datetimes to the site timezone
  430. $dateTimeVisit = Date::factory($details['serverTimePretty'], $timezone);
  431. $details['serverTimePretty'] = $dateTimeVisit->getLocalized(Piwik::translate('CoreHome_ShortDateFormat') . ' %time%');
  432. }
  433. $visitorDetailsArray['goalConversions'] = count($goalDetails);
  434. return $visitorDetailsArray;
  435. }
  436. private static function getCustomVariablePrettyKey($key)
  437. {
  438. $rename = array(
  439. ActionSiteSearch::CVAR_KEY_SEARCH_CATEGORY => Piwik::translate('Actions_ColumnSearchCategory'),
  440. ActionSiteSearch::CVAR_KEY_SEARCH_COUNT => Piwik::translate('Actions_ColumnSearchResultsCount'),
  441. );
  442. if (isset($rename[$key])) {
  443. return $rename[$key];
  444. }
  445. return $key;
  446. }
  447. private static function sortByServerTime($a, $b)
  448. {
  449. $ta = strtotime($a['serverTimePretty']);
  450. $tb = strtotime($b['serverTimePretty']);
  451. return $ta < $tb
  452. ? -1
  453. : ($ta == $tb
  454. ? 0
  455. : 1);
  456. }
  457. }