Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

API.php 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  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 Exception;
  11. use Piwik\Common;
  12. use Piwik\Config;
  13. use Piwik\DataTable;
  14. use Piwik\DataTable\Row;
  15. use Piwik\Date;
  16. use Piwik\Db;
  17. use Piwik\MetricsFormatter;
  18. use Piwik\Period\Range;
  19. use Piwik\Period;
  20. use Piwik\Piwik;
  21. use Piwik\Plugins\Referrers\API as APIReferrers;
  22. use Piwik\Plugins\SitesManager\API as APISitesManager;
  23. use Piwik\Segment;
  24. use Piwik\Site;
  25. use Piwik\Tracker;
  26. /**
  27. * @see plugins/Live/Visitor.php
  28. */
  29. require_once PIWIK_INCLUDE_PATH . '/plugins/Live/Visitor.php';
  30. require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/functions.php';
  31. /**
  32. * The Live! API lets you access complete visit level information about your visitors. Combined with the power of <a href='http://piwik.org/docs/analytics-api/segmentation/' target='_blank'>Segmentation</a>,
  33. * you will be able to request visits filtered by any criteria.
  34. *
  35. * The method "getLastVisitsDetails" will return extensive data for each visit, which includes: server time, visitId, visitorId,
  36. * visitorType (new or returning), number of pages, list of all pages (and events, file downloaded and outlinks clicked),
  37. * custom variables names and values set to this visit, number of goal conversions (and list of all Goal conversions for this visit,
  38. * with time of conversion, revenue, URL, etc.), but also other attributes such as: days since last visit, days since first visit,
  39. * country, continent, visitor IP,
  40. * provider, referrer used (referrer name, keyword if it was a search engine, full URL), campaign name and keyword, operating system,
  41. * browser, type of screen, resolution, supported browser plugins (flash, java, silverlight, pdf, etc.), various dates & times format to make
  42. * it easier for API users... and more!
  43. *
  44. * With the parameter <a href='http://piwik.org/docs/analytics-api/segmentation/' target='_blank'>'&segment='</a> you can filter the
  45. * returned visits by any criteria (visitor IP, visitor ID, country, keyword used, time of day, etc.).
  46. *
  47. * The method "getCounters" is used to return a simple counter: visits, number of actions, number of converted visits, in the last N minutes.
  48. *
  49. * See also the documentation about <a href='http://piwik.org/docs/real-time/' target='_blank'>Real time widget and visitor level reports</a> in Piwik.
  50. * @method static \Piwik\Plugins\Live\API getInstance()
  51. */
  52. class API extends \Piwik\Plugin\API
  53. {
  54. const VISITOR_PROFILE_MAX_VISITS_TO_AGGREGATE = 100;
  55. const VISITOR_PROFILE_MAX_VISITS_TO_SHOW = 10;
  56. const VISITOR_PROFILE_DATE_FORMAT = '%day% %shortMonth% %longYear%';
  57. /**
  58. * This will return simple counters, for a given website ID, for visits over the last N minutes
  59. *
  60. * @param int $idSite Id Site
  61. * @param int $lastMinutes Number of minutes to look back at
  62. * @param bool|string $segment
  63. * @return array( visits => N, actions => M, visitsConverted => P )
  64. */
  65. public function getCounters($idSite, $lastMinutes, $segment = false)
  66. {
  67. Piwik::checkUserHasViewAccess($idSite);
  68. $lastMinutes = (int) $lastMinutes;
  69. $counters = array(
  70. 'visits' => 0,
  71. 'actions' => 0,
  72. 'visitors' => 0,
  73. 'visitsConverted' => 0,
  74. );
  75. if (empty($lastMinutes)) {
  76. return array($counters);
  77. }
  78. list($whereIdSites, $idSites) = $this->getIdSitesWhereClause($idSite);
  79. $select = "count(*) as visits, COUNT(DISTINCT log_visit.idvisitor) as visitors";
  80. $where = $whereIdSites . "AND log_visit.visit_last_action_time >= ?";
  81. $bind = $idSites;
  82. $bind[] = Date::factory(time() - $lastMinutes * 60)->toString('Y-m-d H:i:s');
  83. $segment = new Segment($segment, $idSite);
  84. $query = $segment->getSelectQuery($select, 'log_visit', $where, $bind);
  85. $data = Db::fetchAll($query['sql'], $query['bind']);
  86. $counters['visits'] = $data[0]['visits'];
  87. $counters['visitors'] = $data[0]['visitors'];
  88. $select = "count(*)";
  89. $from = 'log_link_visit_action';
  90. list($whereIdSites) = $this->getIdSitesWhereClause($idSite, $from);
  91. $where = $whereIdSites . "AND log_link_visit_action.server_time >= ?";
  92. $query = $segment->getSelectQuery($select, $from, $where, $bind);
  93. $counters['actions'] = Db::fetchOne($query['sql'], $query['bind']);
  94. $select = "count(*)";
  95. $from = 'log_conversion';
  96. list($whereIdSites) = $this->getIdSitesWhereClause($idSite, $from);
  97. $where = $whereIdSites . "AND log_conversion.server_time >= ?";
  98. $query = $segment->getSelectQuery($select, $from, $where, $bind);
  99. $counters['visitsConverted'] = Db::fetchOne($query['sql'], $query['bind']);
  100. return array($counters);
  101. }
  102. /**
  103. * The same functionnality can be obtained using segment=visitorId==$visitorId with getLastVisitsDetails
  104. *
  105. * @deprecated
  106. * @ignore
  107. * @param int $visitorId
  108. * @param int $idSite
  109. * @param int $filter_limit
  110. * @param bool $flat Whether to flatten the visitor details array
  111. *
  112. * @return DataTable
  113. */
  114. public function getLastVisitsForVisitor($visitorId, $idSite, $filter_limit = 10, $flat = false)
  115. {
  116. Piwik::checkUserHasViewAccess($idSite);
  117. $countVisitorsToFetch = $filter_limit;
  118. $table = $this->loadLastVisitorDetailsFromDatabase($idSite, $period = false, $date = false, $segment = false, $countVisitorsToFetch, $visitorId);
  119. $this->addFilterToCleanVisitors($table, $idSite, $flat);
  120. return $table;
  121. }
  122. /**
  123. * Returns the last visits tracked in the specified website
  124. * You can define any number of filters: none, one, many or all parameters can be defined
  125. *
  126. * @param int $idSite Site ID
  127. * @param bool|string $period Period to restrict to when looking at the logs
  128. * @param bool|string $date Date to restrict to
  129. * @param bool|int $segment (optional) Number of visits rows to return
  130. * @param bool|int $countVisitorsToFetch (optional) Only return the last X visits. By default the last GET['filter_offset']+GET['filter_limit'] are returned.
  131. * @param bool|int $minTimestamp (optional) Minimum timestamp to restrict the query to (useful when paginating or refreshing visits)
  132. * @param bool $flat
  133. * @param bool $doNotFetchActions
  134. * @return DataTable
  135. */
  136. public function getLastVisitsDetails($idSite, $period = false, $date = false, $segment = false, $countVisitorsToFetch = false, $minTimestamp = false, $flat = false, $doNotFetchActions = false)
  137. {
  138. if (false === $countVisitorsToFetch) {
  139. $filter_limit = Common::getRequestVar('filter_limit', 10, 'int');
  140. $filter_offset = Common::getRequestVar('filter_offset', 0, 'int');
  141. $countVisitorsToFetch = $filter_limit + $filter_offset;
  142. }
  143. $filterSortOrder = Common::getRequestVar('filter_sort_order', false, 'string');
  144. Piwik::checkUserHasViewAccess($idSite);
  145. $dataTable = $this->loadLastVisitorDetailsFromDatabase($idSite, $period, $date, $segment, $countVisitorsToFetch, $visitorId = false, $minTimestamp, $filterSortOrder);
  146. $this->addFilterToCleanVisitors($dataTable, $idSite, $flat, $doNotFetchActions);
  147. $filterSortColumn = Common::getRequestVar('filter_sort_column', false, 'string');
  148. $filterSortOrder = Common::getRequestVar('filter_sort_order', 'desc', 'string');
  149. if ($filterSortColumn) {
  150. $dataTable->queueFilter('Sort', array($filterSortColumn, $filterSortOrder));
  151. }
  152. return $dataTable;
  153. }
  154. /**
  155. * Returns an array describing a visitor using her last visits (uses a maximum of 100).
  156. *
  157. * @param int $idSite Site ID
  158. * @param bool|false|string $visitorId The ID of the visitor whose profile to retrieve.
  159. * @param bool|false|string $segment
  160. * @param bool $checkForLatLong If true, hasLatLong will appear in the output and be true if
  161. * one of the first 100 visits has a latitude/longitude.
  162. * @return array
  163. */
  164. public function getVisitorProfile($idSite, $visitorId = false, $segment = false, $checkForLatLong = false)
  165. {
  166. Piwik::checkUserHasViewAccess($idSite);
  167. if ($visitorId === false) {
  168. $visitorId = $this->getMostRecentVisitorId($idSite, $segment);
  169. }
  170. $newSegment = ($segment === false ? '' : $segment . ';') . 'visitorId==' . $visitorId;
  171. $visits = $this->loadLastVisitorDetailsFromDatabase($idSite, $period = false, $date = false, $newSegment,
  172. $numVisitorsToFetch = self::VISITOR_PROFILE_MAX_VISITS_TO_AGGREGATE,
  173. $overrideVisitorId = false,
  174. $minTimestamp = false);
  175. $this->addFilterToCleanVisitors($visits, $idSite, $flat = false, $doNotFetchActions = false, $filterNow = true);
  176. if ($visits->getRowsCount() == 0) {
  177. return array();
  178. }
  179. $isEcommerceEnabled = Site::isEcommerceEnabledFor($idSite);
  180. $result = array();
  181. $result['totalVisits'] = 0;
  182. $result['totalVisitDuration'] = 0;
  183. $result['totalActions'] = 0;
  184. $result['totalSearches'] = 0;
  185. $result['totalPageViews'] = 0;
  186. $result['totalGoalConversions'] = 0;
  187. $result['totalConversionsByGoal'] = array();
  188. if ($isEcommerceEnabled) {
  189. $result['totalEcommerceConversions'] = 0;
  190. $result['totalEcommerceRevenue'] = 0;
  191. $result['totalEcommerceItems'] = 0;
  192. $result['totalAbandonedCarts'] = 0;
  193. $result['totalAbandonedCartsRevenue'] = 0;
  194. $result['totalAbandonedCartsItems'] = 0;
  195. }
  196. $countries = array();
  197. $continents = array();
  198. $cities = array();
  199. $siteSearchKeywords = array();
  200. $pageGenerationTimeTotal = 0;
  201. // aggregate all requested visits info for total_* info
  202. foreach ($visits->getRows() as $visit) {
  203. ++$result['totalVisits'];
  204. $result['totalVisitDuration'] += $visit->getColumn('visitDuration');
  205. $result['totalActions'] += $visit->getColumn('actions');
  206. $result['totalGoalConversions'] += $visit->getColumn('goalConversions');
  207. // individual goal conversions are stored in action details
  208. foreach ($visit->getColumn('actionDetails') as $action) {
  209. if ($action['type'] == 'goal') {
  210. // handle goal conversion
  211. $idGoal = $action['goalId'];
  212. $idGoalKey = 'idgoal=' . $idGoal;
  213. if (!isset($result['totalConversionsByGoal'][$idGoalKey])) {
  214. $result['totalConversionsByGoal'][$idGoalKey] = 0;
  215. }
  216. ++$result['totalConversionsByGoal'][$idGoalKey];
  217. if (!empty($action['revenue'])) {
  218. if (!isset($result['totalRevenueByGoal'][$idGoalKey])) {
  219. $result['totalRevenueByGoal'][$idGoalKey] = 0;
  220. }
  221. $result['totalRevenueByGoal'][$idGoalKey] += $action['revenue'];
  222. }
  223. } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER // handle ecommerce order
  224. && $isEcommerceEnabled
  225. ) {
  226. ++$result['totalEcommerceConversions'];
  227. $result['totalEcommerceRevenue'] += $action['revenue'];
  228. $result['totalEcommerceItems'] += $action['items'];
  229. } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART // handler abandoned cart
  230. && $isEcommerceEnabled
  231. ) {
  232. ++$result['totalAbandonedCarts'];
  233. $result['totalAbandonedCartsRevenue'] += $action['revenue'];
  234. $result['totalAbandonedCartsItems'] += $action['items'];
  235. }
  236. if (isset($action['siteSearchKeyword'])) {
  237. $keyword = $action['siteSearchKeyword'];
  238. if (!isset($siteSearchKeywords[$keyword])) {
  239. $siteSearchKeywords[$keyword] = 0;
  240. ++$result['totalSearches'];
  241. }
  242. ++$siteSearchKeywords[$keyword];
  243. }
  244. if (isset($action['generationTime'])) {
  245. $pageGenerationTimeTotal += $action['generationTime'];
  246. ++$result['totalPageViews'];
  247. }
  248. }
  249. $countryCode = $visit->getColumn('countryCode');
  250. if (!isset($countries[$countryCode])) {
  251. $countries[$countryCode] = 0;
  252. }
  253. ++$countries[$countryCode];
  254. $continentCode = $visit->getColumn('continentCode');
  255. if (!isset($continents[$continentCode])) {
  256. $continents[$continentCode] = 0;
  257. }
  258. ++$continents[$continentCode];
  259. if ($countryCode && !array_key_exists($countryCode, $cities)) {
  260. $cities[$countryCode] = array();
  261. }
  262. $city = $visit->getColumn('city');
  263. if(!empty($city)) {
  264. $cities[$countryCode][] = $city;
  265. }
  266. }
  267. // sort countries/continents/search keywords by visit/action
  268. asort($countries);
  269. asort($continents);
  270. arsort($siteSearchKeywords);
  271. // transform country/continents/search keywords into something that will look good in XML
  272. $result['countries'] = $result['continents'] = $result['searches'] = array();
  273. foreach ($countries as $countryCode => $nbVisits) {
  274. $countryInfo = array('country' => $countryCode,
  275. 'nb_visits' => $nbVisits,
  276. 'flag' => \Piwik\Plugins\UserCountry\getFlagFromCode($countryCode),
  277. 'prettyName' => \Piwik\Plugins\UserCountry\countryTranslate($countryCode));
  278. if(!empty($cities[$countryCode])) {
  279. $countryInfo['cities'] = array_unique($cities[$countryCode]);
  280. }
  281. $result['countries'][] = $countryInfo;
  282. }
  283. foreach ($continents as $continentCode => $nbVisits) {
  284. $result['continents'][] = array('continent' => $continentCode,
  285. 'nb_visits' => $nbVisits,
  286. 'prettyName' => \Piwik\Plugins\UserCountry\continentTranslate($continentCode));
  287. }
  288. foreach ($siteSearchKeywords as $keyword => $searchCount) {
  289. $result['searches'][] = array('keyword' => $keyword,
  290. 'searches' => $searchCount);
  291. }
  292. if ($result['totalPageViews']) {
  293. $result['averagePageGenerationTime'] =
  294. round($pageGenerationTimeTotal / $result['totalPageViews'], $precision = 2);
  295. }
  296. $result['totalVisitDurationPretty'] = MetricsFormatter::getPrettyTimeFromSeconds($result['totalVisitDuration']);
  297. // use requested visits for first/last visit info
  298. $rows = $visits->getRows();
  299. $result['firstVisit'] = $this->getVisitorProfileVisitSummary(end($rows));
  300. $result['lastVisit'] = $this->getVisitorProfileVisitSummary(reset($rows));
  301. // check if requested visits have lat/long
  302. if ($checkForLatLong) {
  303. $result['hasLatLong'] = false;
  304. foreach ($rows as $visit) {
  305. if ($visit->getColumn('latitude') !== false) { // realtime map only checks for latitude
  306. $result['hasLatLong'] = true;
  307. break;
  308. }
  309. }
  310. }
  311. // save count of visits we queries
  312. $result['visitsAggregated'] = count($rows);
  313. // use N most recent visits for last_visits
  314. $visits->deleteRowsOffset(self::VISITOR_PROFILE_MAX_VISITS_TO_SHOW);
  315. $result['lastVisits'] = $visits;
  316. // use the right date format for the pretty server date
  317. $timezone = Site::getTimezoneFor($idSite);
  318. foreach ($result['lastVisits']->getRows() as $visit) {
  319. $dateTimeVisitFirstAction = Date::factory($visit->getColumn('firstActionTimestamp'), $timezone);
  320. $datePretty = $dateTimeVisitFirstAction->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT);
  321. $visit->setColumn('serverDatePrettyFirstAction', $datePretty);
  322. $dateTimePretty = $datePretty . ' ' . $visit->getColumn('serverTimePrettyFirstAction');
  323. $visit->setColumn('serverDateTimePrettyFirstAction', $dateTimePretty);
  324. }
  325. $result['userId'] = $visit->getColumn('userId');
  326. // get visitor IDs that are adjacent to this one in log_visit
  327. // TODO: make sure order of visitor ids is not changed if a returning visitor visits while the user is
  328. // looking at the popup.
  329. $latestVisitTime = reset($rows)->getColumn('lastActionDateTime');
  330. $result['nextVisitorId'] = $this->getAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = true);
  331. $result['previousVisitorId'] = $this->getAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = false);
  332. /**
  333. * Triggered in the Live.getVisitorProfile API method. Plugins can use this event
  334. * to discover and add extra data to visitor profiles.
  335. *
  336. * For example, if an email address is found in a custom variable, a plugin could load the
  337. * gravatar for the email and add it to the visitor profile, causing it to display in the
  338. * visitor profile popup.
  339. *
  340. * The following visitor profile elements can be set to augment the visitor profile popup:
  341. *
  342. * - **visitorAvatar**: A URL to an image to display in the top left corner of the popup.
  343. * - **visitorDescription**: Text to be used as the tooltip of the avatar image.
  344. *
  345. * @param array &$visitorProfile The unaugmented visitor profile info.
  346. */
  347. Piwik::postEvent('Live.getExtraVisitorDetails', array(&$result));
  348. return $result;
  349. }
  350. /**
  351. * Returns the visitor ID of the most recent visit.
  352. *
  353. * @param int $idSite
  354. * @param bool|string $segment
  355. * @return string
  356. */
  357. public function getMostRecentVisitorId($idSite, $segment = false)
  358. {
  359. Piwik::checkUserHasViewAccess($idSite);
  360. $dataTable = $this->loadLastVisitorDetailsFromDatabase(
  361. $idSite, $period = false, $date = false, $segment, $numVisitorsToFetch = 1,
  362. $visitorId = false, $minTimestamp = false
  363. );
  364. if (0 >= $dataTable->getRowsCount()) {
  365. return false;
  366. }
  367. $visitorFactory = new VisitorFactory();
  368. $visitDetails = $dataTable->getFirstRow()->getColumns();
  369. $visitor = $visitorFactory->create($visitDetails);
  370. return $visitor->getVisitorId();
  371. }
  372. /**
  373. * Returns the ID of a visitor that is adjacent to another visitor (by time of last action)
  374. * in the log_visit table.
  375. *
  376. * @param int $idSite The ID of the site whose visits should be looked at.
  377. * @param string $visitorId The ID of the visitor to get an adjacent visitor for.
  378. * @param string $visitLastActionTime The last action time of the latest visit for $visitorId.
  379. * @param string $segment
  380. * @param bool $getNext Whether to retrieve the next visitor or the previous visitor. The next
  381. * visitor will be the visitor that appears chronologically later in the
  382. * log_visit table. The previous visitor will be the visitor that appears
  383. * earlier.
  384. * @return string The hex visitor ID.
  385. */
  386. private function getAdjacentVisitorId($idSite, $visitorId, $visitLastActionTime, $segment, $getNext)
  387. {
  388. if ($getNext) {
  389. $visitLastActionTimeCondition = "sub.visit_last_action_time <= ?";
  390. $orderByDir = "DESC";
  391. } else {
  392. $visitLastActionTimeCondition = "sub.visit_last_action_time >= ?";
  393. $orderByDir = "ASC";
  394. }
  395. $visitLastActionDate = Date::factory($visitLastActionTime);
  396. $dateOneDayAgo = $visitLastActionDate->subDay(1);
  397. $dateOneDayInFuture = $visitLastActionDate->addDay(1);
  398. $select = "log_visit.idvisitor, MAX(log_visit.visit_last_action_time) as visit_last_action_time";
  399. $from = "log_visit";
  400. $where = "log_visit.idsite = ? AND log_visit.idvisitor <> ? AND visit_last_action_time >= ? and visit_last_action_time <= ?";
  401. $whereBind = array($idSite, @Common::hex2bin($visitorId), $dateOneDayAgo->toString('Y-m-d H:i:s'), $dateOneDayInFuture->toString('Y-m-d H:i:s'));
  402. $orderBy = "MAX(log_visit.visit_last_action_time) $orderByDir";
  403. $groupBy = "log_visit.idvisitor";
  404. $segment = new Segment($segment, $idSite);
  405. $queryInfo = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy);
  406. $sql = "SELECT sub.idvisitor, sub.visit_last_action_time
  407. FROM ({$queryInfo['sql']}) as sub
  408. WHERE $visitLastActionTimeCondition
  409. LIMIT 1";
  410. $bind = array_merge($queryInfo['bind'], array($visitLastActionTime));
  411. $visitorId = Db::fetchOne($sql, $bind);
  412. if (!empty($visitorId)) {
  413. $visitorId = bin2hex($visitorId);
  414. }
  415. return $visitorId;
  416. }
  417. /**
  418. * Returns a summary for an important visit. Used to describe the first & last visits of a visitor.
  419. *
  420. * @param Row $visit
  421. * @return array
  422. */
  423. private function getVisitorProfileVisitSummary($visit)
  424. {
  425. $today = Date::today();
  426. $serverDate = $visit->getColumn('firstActionTimestamp');
  427. return array(
  428. 'date' => $serverDate,
  429. 'prettyDate' => Date::factory($serverDate)->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT),
  430. 'daysAgo' => (int)Date::secondsToDays($today->getTimestamp() - Date::factory($serverDate)->getTimestamp()),
  431. 'referrerType' => $visit->getColumn('referrerType'),
  432. 'referralSummary' => self::getReferrerSummaryForVisit($visit),
  433. );
  434. }
  435. /**
  436. * Returns a summary for a visit's referral.
  437. *
  438. * @param Row $visit
  439. * @return bool|mixed|string
  440. * @ignore
  441. */
  442. public static function getReferrerSummaryForVisit($visit)
  443. {
  444. $referrerType = $visit->getColumn('referrerType');
  445. if ($referrerType === false
  446. || $referrerType == 'direct'
  447. ) {
  448. $result = Piwik::translate('Referrers_DirectEntry');
  449. } else if ($referrerType == 'search') {
  450. $result = $visit->getColumn('referrerName');
  451. $keyword = $visit->getColumn('referrerKeyword');
  452. if ($keyword !== false
  453. && $keyword != APIReferrers::getKeywordNotDefinedString()
  454. ) {
  455. $result .= ' (' . $keyword . ')';
  456. }
  457. } else if ($referrerType == 'campaign') {
  458. $result = Piwik::translate('Referrers_ColumnCampaign') . ' (' . $visit->getColumn('referrerName') . ')';
  459. } else {
  460. $result = $visit->getColumn('referrerName');
  461. }
  462. return $result;
  463. }
  464. /**
  465. * @deprecated
  466. */
  467. public function getLastVisits($idSite, $filter_limit = 10, $minTimestamp = false)
  468. {
  469. return $this->getLastVisitsDetails($idSite, $period = false, $date = false, $segment = false, $countVisitorsToFetch = $filter_limit, $minTimestamp, $flat = false);
  470. }
  471. /**
  472. * For an array of visits, query the list of pages for this visit
  473. * as well as make the data human readable
  474. * @param DataTable $dataTable
  475. * @param int $idSite
  476. * @param bool $flat whether to flatten the array (eg. 'customVariables' names/values will appear in the root array rather than in 'customVariables' key
  477. * @param bool $doNotFetchActions If set to true, we only fetch visit info and not actions (much faster)
  478. * @param bool $filterNow If true, the visitors will be cleaned immediately
  479. */
  480. private function addFilterToCleanVisitors(DataTable $dataTable, $idSite, $flat = false, $doNotFetchActions = false, $filterNow = false)
  481. {
  482. $filter = 'queueFilter';
  483. if ($filterNow) {
  484. $filter = 'filter';
  485. }
  486. $dataTable->$filter(function ($table) use ($idSite, $flat, $doNotFetchActions) {
  487. /** @var DataTable $table */
  488. $actionsLimit = (int)Config::getInstance()->General['visitor_log_maximum_actions_per_visit'];
  489. $visitorFactory = new VisitorFactory();
  490. $website = new Site($idSite);
  491. $timezone = $website->getTimezone();
  492. $currency = $website->getCurrency();
  493. $currencies = APISitesManager::getInstance()->getCurrencySymbols();
  494. // live api is not summable, prevents errors like "Unexpected ECommerce status value"
  495. $table->deleteRow(DataTable::ID_SUMMARY_ROW);
  496. foreach ($table->getRows() as $visitorDetailRow) {
  497. $visitorDetailsArray = Visitor::cleanVisitorDetails($visitorDetailRow->getColumns());
  498. $visitor = $visitorFactory->create($visitorDetailsArray);
  499. $visitorDetailsArray = $visitor->getAllVisitorDetails();
  500. $visitorDetailsArray['siteCurrency'] = $currency;
  501. $visitorDetailsArray['siteCurrencySymbol'] = @$currencies[$visitorDetailsArray['siteCurrency']];
  502. $visitorDetailsArray['serverTimestamp'] = $visitorDetailsArray['lastActionTimestamp'];
  503. $dateTimeVisit = Date::factory($visitorDetailsArray['lastActionTimestamp'], $timezone);
  504. if($dateTimeVisit) {
  505. $visitorDetailsArray['serverTimePretty'] = $dateTimeVisit->getLocalized('%time%');
  506. $visitorDetailsArray['serverDatePretty'] = $dateTimeVisit->getLocalized(Piwik::translate('CoreHome_ShortDateFormat'));
  507. }
  508. $dateTimeVisitFirstAction = Date::factory($visitorDetailsArray['firstActionTimestamp'], $timezone);
  509. $visitorDetailsArray['serverDatePrettyFirstAction'] = $dateTimeVisitFirstAction->getLocalized(Piwik::translate('CoreHome_ShortDateFormat'));
  510. $visitorDetailsArray['serverTimePrettyFirstAction'] = $dateTimeVisitFirstAction->getLocalized('%time%');
  511. $visitorDetailsArray['actionDetails'] = array();
  512. if (!$doNotFetchActions) {
  513. $visitorDetailsArray = Visitor::enrichVisitorArrayWithActions($visitorDetailsArray, $actionsLimit, $timezone);
  514. }
  515. if ($flat) {
  516. $visitorDetailsArray = Visitor::flattenVisitorDetailsArray($visitorDetailsArray);
  517. }
  518. $visitorDetailRow->setColumns($visitorDetailsArray);
  519. }
  520. });
  521. }
  522. private function loadLastVisitorDetailsFromDatabase($idSite, $period, $date, $segment = false, $countVisitorsToFetch = 100, $visitorId = false, $minTimestamp = false, $filterSortOrder = false)
  523. {
  524. $where = $whereBind = array();
  525. list($whereClause, $idSites) = $this->getIdSitesWhereClause($idSite);
  526. $where[] = $whereClause;
  527. $whereBind = $idSites;
  528. if (strtolower($filterSortOrder) !== 'asc') {
  529. $filterSortOrder = 'DESC';
  530. }
  531. $orderBy = "idsite, visit_last_action_time " . $filterSortOrder;
  532. $orderByParent = "sub.visit_last_action_time " . $filterSortOrder;
  533. if (!empty($visitorId)) {
  534. $where[] = "log_visit.idvisitor = ? ";
  535. $whereBind[] = @Common::hex2bin($visitorId);
  536. }
  537. if (!empty($minTimestamp)) {
  538. $where[] = "log_visit.visit_last_action_time > ? ";
  539. $whereBind[] = date("Y-m-d H:i:s", $minTimestamp);
  540. }
  541. // If no other filter, only look at the last 24 hours of stats
  542. if (empty($visitorId)
  543. && empty($countVisitorsToFetch)
  544. && empty($period)
  545. && empty($date)
  546. ) {
  547. $period = 'day';
  548. $date = 'yesterdaySameTime';
  549. }
  550. // SQL Filter with provided period
  551. if (!empty($period) && !empty($date)) {
  552. $currentSite = new Site($idSite);
  553. $currentTimezone = $currentSite->getTimezone();
  554. $dateString = $date;
  555. if ($period == 'range') {
  556. $processedPeriod = new Range('range', $date);
  557. if ($parsedDate = Range::parseDateRange($date)) {
  558. $dateString = $parsedDate[2];
  559. }
  560. } else {
  561. $processedDate = Date::factory($date);
  562. if ($date == 'today'
  563. || $date == 'now'
  564. || $processedDate->toString() == Date::factory('now', $currentTimezone)->toString()
  565. ) {
  566. $processedDate = $processedDate->subDay(1);
  567. }
  568. $processedPeriod = Period\Factory::build($period, $processedDate);
  569. }
  570. $dateStart = $processedPeriod->getDateStart()->setTimezone($currentTimezone);
  571. $where[] = "log_visit.visit_last_action_time >= ?";
  572. $whereBind[] = $dateStart->toString('Y-m-d H:i:s');
  573. if (!in_array($date, array('now', 'today', 'yesterdaySameTime'))
  574. && strpos($date, 'last') === false
  575. && strpos($date, 'previous') === false
  576. && Date::factory($dateString)->toString('Y-m-d') != Date::factory('now', $currentTimezone)->toString()
  577. ) {
  578. $dateEnd = $processedPeriod->getDateEnd()->setTimezone($currentTimezone);
  579. $where[] = " log_visit.visit_last_action_time <= ?";
  580. $dateEndString = $dateEnd->addDay(1)->toString('Y-m-d H:i:s');
  581. $whereBind[] = $dateEndString;
  582. }
  583. }
  584. if (count($where) > 0) {
  585. $where = join("
  586. AND ", $where);
  587. } else {
  588. $where = false;
  589. }
  590. $segment = new Segment($segment, $idSite);
  591. // Subquery to use the indexes for ORDER BY
  592. $select = "log_visit.*";
  593. $from = "log_visit";
  594. $subQuery = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy);
  595. $sqlLimit = $countVisitorsToFetch >= 1 ? " LIMIT 0, " . (int)$countVisitorsToFetch : "";
  596. // Group by idvisit so that a visitor converting 2 goals only appears once
  597. $sql = "
  598. SELECT sub.*
  599. FROM (
  600. " . $subQuery['sql'] . "
  601. $sqlLimit
  602. ) AS sub
  603. GROUP BY sub.idvisit
  604. ORDER BY $orderByParent
  605. ";
  606. try {
  607. $data = Db::fetchAll($sql, $subQuery['bind']);
  608. } catch (Exception $e) {
  609. echo $e->getMessage();
  610. exit;
  611. }
  612. $dataTable = new DataTable();
  613. $dataTable->addRowsFromSimpleArray($data);
  614. // $dataTable->disableFilter('Truncate');
  615. if (!empty($data[0])) {
  616. $columnsToNotAggregate = array_map(function () {
  617. return 'skip';
  618. }, $data[0]);
  619. $dataTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnsToNotAggregate);
  620. }
  621. return $dataTable;
  622. }
  623. /**
  624. * @param $idSite
  625. * @param string $table
  626. * @return array
  627. */
  628. private function getIdSitesWhereClause($idSite, $table = 'log_visit')
  629. {
  630. $idSites = array($idSite);
  631. Piwik::postEvent('Live.API.getIdSitesString', array(&$idSites));
  632. $idSitesBind = Common::getSqlStringFieldsArray($idSites);
  633. $whereClause = $table . ".idsite in ($idSitesBind) ";
  634. return array($whereClause, $idSites);
  635. }
  636. }