PageRenderTime 28ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Tracker/GoalManager.php

https://github.com/quarkness/piwik
PHP | 835 lines | 625 code | 77 blank | 133 comment | 74 complexity | 458c9f71fb1f598d6f70096aec76a46e 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
  10. * @package Piwik
  11. */
  12. /**
  13. * @package Piwik
  14. * @subpackage Piwik_Tracker
  15. */
  16. class Piwik_Tracker_GoalManager
  17. {
  18. // log_visit.visit_goal_buyer
  19. const TYPE_BUYER_NONE = 0;
  20. const TYPE_BUYER_ORDERED = 1;
  21. const TYPE_BUYER_OPEN_CART = 2;
  22. const TYPE_BUYER_ORDERED_AND_OPEN_CART = 3;
  23. // log_conversion.idorder is NULLable, but not log_conversion_item which defaults to zero for carts
  24. const ITEM_IDORDER_ABANDONED_CART = 0;
  25. // log_conversion.idgoal special values
  26. const IDGOAL_CART = -1;
  27. const IDGOAL_ORDER = 0;
  28. const REVENUE_PRECISION = 2;
  29. const MAXIMUM_PRODUCT_CATEGORIES = 5;
  30. public $idGoal;
  31. public $requestIsEcommerce;
  32. public $isGoalAnOrder;
  33. /**
  34. * @var Piwik_Tracker_Action
  35. */
  36. protected $action = null;
  37. protected $convertedGoals = array();
  38. protected $isThereExistingCartInVisit = false;
  39. protected $request;
  40. protected $orderId;
  41. function init($request)
  42. {
  43. $this->request = $request;
  44. $this->orderId = Piwik_Common::getRequestVar('ec_id', false, 'string', $this->request);
  45. $this->isGoalAnOrder = !empty($this->orderId);
  46. $this->idGoal = Piwik_Common::getRequestVar('idgoal', -1, 'int', $this->request);
  47. $this->requestIsEcommerce = ($this->idGoal == 0);
  48. }
  49. function getBuyerType($existingType = self::TYPE_BUYER_NONE)
  50. {
  51. // Was there a Cart for this visit prior to the order?
  52. $this->isThereExistingCartInVisit = in_array($existingType,
  53. array( Piwik_Tracker_GoalManager::TYPE_BUYER_OPEN_CART,
  54. Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART));
  55. if(!$this->requestIsEcommerce)
  56. {
  57. return $existingType;
  58. }
  59. if($this->isGoalAnOrder)
  60. {
  61. return self::TYPE_BUYER_ORDERED;
  62. }
  63. // request is Add to Cart
  64. if($existingType == self::TYPE_BUYER_ORDERED
  65. || $existingType == self::TYPE_BUYER_ORDERED_AND_OPEN_CART)
  66. {
  67. return self::TYPE_BUYER_ORDERED_AND_OPEN_CART;
  68. }
  69. return self::TYPE_BUYER_OPEN_CART;
  70. }
  71. static public function getGoalDefinitions( $idSite )
  72. {
  73. $websiteAttributes = Piwik_Common::getCacheWebsiteAttributes( $idSite );
  74. if(isset($websiteAttributes['goals']))
  75. {
  76. return $websiteAttributes['goals'];
  77. }
  78. return array();
  79. }
  80. static public function getGoalDefinition( $idSite, $idGoal )
  81. {
  82. $goals = self::getGoalDefinitions( $idSite );
  83. foreach($goals as $goal)
  84. {
  85. if($goal['idgoal'] == $idGoal)
  86. {
  87. return $goal;
  88. }
  89. }
  90. throw new Exception('Goal not found');
  91. }
  92. static public function getGoalIds( $idSite )
  93. {
  94. $goals = self::getGoalDefinitions( $idSite );
  95. $goalIds = array();
  96. foreach($goals as $goal)
  97. {
  98. $goalIds[] = $goal['idgoal'];
  99. }
  100. return $goalIds;
  101. }
  102. /**
  103. * Look at the URL or Page Title and sees if it matches any existing Goal definition
  104. *
  105. * @param int $idSite
  106. * @param Piwik_Tracker_Action $action
  107. * @return int Number of goals matched
  108. */
  109. function detectGoalsMatchingUrl($idSite, $action)
  110. {
  111. if(!Piwik_Common::isGoalPluginEnabled())
  112. {
  113. return false;
  114. }
  115. $decodedActionUrl = $action->getActionUrl();
  116. $actionType = $action->getActionType();
  117. $goals = $this->getGoalDefinitions($idSite);
  118. foreach($goals as $goal)
  119. {
  120. $attribute = $goal['match_attribute'];
  121. // if the attribute to match is not the type of the current action
  122. if( ($actionType == Piwik_Tracker_Action::TYPE_ACTION_URL && $attribute != 'url' && $attribute != 'title')
  123. || ($actionType == Piwik_Tracker_Action::TYPE_DOWNLOAD && $attribute != 'file')
  124. || ($actionType == Piwik_Tracker_Action::TYPE_OUTLINK && $attribute != 'external_website')
  125. || ($attribute == 'manually')
  126. )
  127. {
  128. continue;
  129. }
  130. $url = $decodedActionUrl;
  131. // Matching on Page Title
  132. if($attribute == 'title')
  133. {
  134. $url = $action->getActionName();
  135. }
  136. $pattern_type = $goal['pattern_type'];
  137. switch($pattern_type)
  138. {
  139. case 'regex':
  140. $pattern = $goal['pattern'];
  141. if(strpos($pattern, '/') !== false
  142. && strpos($pattern, '\\/') === false)
  143. {
  144. $pattern = str_replace('/', '\\/', $pattern);
  145. }
  146. $pattern = '/' . $pattern . '/';
  147. if(!$goal['case_sensitive'])
  148. {
  149. $pattern .= 'i';
  150. }
  151. $match = (@preg_match($pattern, $url) == 1);
  152. break;
  153. case 'contains':
  154. if($goal['case_sensitive'])
  155. {
  156. $matched = strpos($url, $goal['pattern']);
  157. }
  158. else
  159. {
  160. $matched = stripos($url, $goal['pattern']);
  161. }
  162. $match = ($matched !== false);
  163. break;
  164. case 'exact':
  165. if($goal['case_sensitive'])
  166. {
  167. $matched = strcmp($goal['pattern'], $url);
  168. }
  169. else
  170. {
  171. $matched = strcasecmp($goal['pattern'], $url);
  172. }
  173. $match = ($matched == 0);
  174. break;
  175. default:
  176. throw new Exception(Piwik_TranslateException('General_ExceptionInvalidGoalPattern', array($pattern_type)));
  177. break;
  178. }
  179. if($match)
  180. {
  181. $goal['url'] = $decodedActionUrl;
  182. $this->convertedGoals[] = $goal;
  183. }
  184. }
  185. // var_dump($this->convertedGoals);exit;
  186. return count($this->convertedGoals) > 0;
  187. }
  188. function detectGoalId($idSite)
  189. {
  190. if(!Piwik_Common::isGoalPluginEnabled())
  191. {
  192. return false;
  193. }
  194. $goals = $this->getGoalDefinitions($idSite);
  195. if(!isset($goals[$this->idGoal]))
  196. {
  197. return false;
  198. }
  199. $goal = $goals[$this->idGoal];
  200. $url = Piwik_Common::getRequestVar( 'url', '', 'string', $this->request);
  201. $goal['url'] = Piwik_Tracker_Action::excludeQueryParametersFromUrl($url, $idSite);
  202. $goal['revenue'] = $this->getRevenue(Piwik_Common::getRequestVar('revenue', $goal['revenue'], 'float', $this->request));
  203. $this->convertedGoals[] = $goal;
  204. return true;
  205. }
  206. /**
  207. * Records one or several goals matched in this request.
  208. */
  209. public function recordGoals($idSite, $visitorInformation, $visitCustomVariables, $action, $referrerTimestamp, $referrerUrl, $referrerCampaignName, $referrerCampaignKeyword)
  210. {
  211. $location_country = isset($visitorInformation['location_country'])
  212. ? $visitorInformation['location_country']
  213. : Piwik_Common::getCountry(
  214. Piwik_Common::getBrowserLanguage(),
  215. $enableLanguageToCountryGuess = Piwik_Tracker_Config::getInstance()->Tracker['enable_language_to_country_guess'], $visitorInformation['location_ip']
  216. );
  217. $location_continent = isset($visitorInformation['location_continent'])
  218. ? $visitorInformation['location_continent']
  219. : Piwik_Common::getContinent($location_country);
  220. $goal = array(
  221. 'idvisit' => $visitorInformation['idvisit'],
  222. 'idsite' => $idSite,
  223. 'idvisitor' => $visitorInformation['idvisitor'],
  224. 'server_time' => Piwik_Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']),
  225. 'location_country' => $location_country,
  226. 'location_continent'=> $location_continent,
  227. 'visitor_returning' => $visitorInformation['visitor_returning'],
  228. 'visitor_days_since_first' => $visitorInformation['visitor_days_since_first'],
  229. 'visitor_days_since_order' => $visitorInformation['visitor_days_since_order'],
  230. 'visitor_count_visits' => $visitorInformation['visitor_count_visits'],
  231. );
  232. // Copy Custom Variables from Visit row to the Goal conversion
  233. for($i=1; $i<=Piwik_Tracker::MAX_CUSTOM_VARIABLES; $i++)
  234. {
  235. if(!empty($visitorInformation['custom_var_k'.$i]))
  236. {
  237. $goal['custom_var_k'.$i] = $visitorInformation['custom_var_k'.$i];
  238. }
  239. if(!empty($visitorInformation['custom_var_v'.$i]))
  240. {
  241. $goal['custom_var_v'.$i] = $visitorInformation['custom_var_v'.$i];
  242. }
  243. }
  244. // Otherwise, set the Custom Variables found in the cookie sent with this request
  245. $goal += $visitCustomVariables;
  246. // Attributing the correct Referrer to this conversion.
  247. // Priority order is as follows:
  248. // 1) Campaign name/kwd parsed in the JS
  249. // 2) Referrer URL stored in the _ref cookie
  250. // 3) If no info from the cookie, attribute to the current visit referrer
  251. // 3) Default values: current referrer
  252. $type = $visitorInformation['referer_type'];
  253. $name = $visitorInformation['referer_name'];
  254. $keyword = $visitorInformation['referer_keyword'];
  255. $time = $visitorInformation['visit_first_action_time'];
  256. // 1) Campaigns from 1st party cookie
  257. if(!empty($referrerCampaignName))
  258. {
  259. $type = Piwik_Common::REFERER_TYPE_CAMPAIGN;
  260. $name = $referrerCampaignName;
  261. $keyword = $referrerCampaignKeyword;
  262. $time = $referrerTimestamp;
  263. }
  264. // 2) Referrer URL parsing
  265. elseif(!empty($referrerUrl))
  266. {
  267. $referrer = new Piwik_Tracker_Visit_Referer();
  268. $referrer = $referrer->getRefererInformation($referrerUrl, $currentUrl = '', $idSite);
  269. // if the parsed referer is interesting enough, ie. website or search engine
  270. if(in_array($referrer['referer_type'], array(Piwik_Common::REFERER_TYPE_SEARCH_ENGINE, Piwik_Common::REFERER_TYPE_WEBSITE)))
  271. {
  272. $type = $referrer['referer_type'];
  273. $name = $referrer['referer_name'];
  274. $keyword = $referrer['referer_keyword'];
  275. $time = $referrerTimestamp;
  276. }
  277. }
  278. $goal += array(
  279. 'referer_type' => $type,
  280. 'referer_name' => $name,
  281. 'referer_keyword' => $keyword,
  282. // this field is currently unused
  283. 'referer_visit_server_date' => date("Y-m-d", $time),
  284. );
  285. // some goals are converted, so must be ecommerce Order or Cart Update
  286. if($this->requestIsEcommerce)
  287. {
  288. $this->recordEcommerceGoal($goal, $visitorInformation);
  289. }
  290. else
  291. {
  292. $this->recordStandardGoals($goal, $action, $visitorInformation);
  293. }
  294. }
  295. /**
  296. * Returns rounded decimal revenue, or if revenue is integer, then returns as is.
  297. *
  298. * @param int|float $revenue
  299. * @return int|float
  300. */
  301. protected function getRevenue($revenue)
  302. {
  303. if(round($revenue) == $revenue)
  304. {
  305. return $revenue;
  306. }
  307. return round($revenue, self::REVENUE_PRECISION);
  308. }
  309. /**
  310. * Records an Ecommerce conversion in the DB. Deals with Items found in the request.
  311. * Will deal with 2 types of conversions: Ecommerce Order and Ecommerce Cart update (Add to cart, Update Cart etc).
  312. *
  313. * @param array $goal
  314. * @param array $visitorInformation
  315. */
  316. protected function recordEcommerceGoal($goal, $visitorInformation)
  317. {
  318. // Is the transaction a Cart Update or an Ecommerce order?
  319. $updateWhere = array(
  320. 'idvisit' => $visitorInformation['idvisit'],
  321. 'idgoal' => self::IDGOAL_CART,
  322. 'buster' => 0,
  323. );
  324. if($this->isThereExistingCartInVisit)
  325. {
  326. printDebug("There is an existing cart for this visit");
  327. }
  328. if($this->isGoalAnOrder)
  329. {
  330. $orderIdNumeric = Piwik_Common::hashStringToInt($this->orderId);
  331. $goal['idgoal'] = self::IDGOAL_ORDER;
  332. $goal['idorder'] = $this->orderId;
  333. $goal['buster'] = $orderIdNumeric;
  334. $goal['revenue_subtotal'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_st', false, 'float', $this->request));
  335. $goal['revenue_tax'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_tx', false, 'float', $this->request));
  336. $goal['revenue_shipping'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_sh', false, 'float', $this->request));
  337. $goal['revenue_discount'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_dt', false, 'float', $this->request));
  338. $debugMessage = 'The conversion is an Ecommerce order';
  339. }
  340. // If Cart update, select current items in the previous Cart
  341. else
  342. {
  343. $goal['buster'] = 0;
  344. $goal['idgoal'] = self::IDGOAL_CART;
  345. $debugMessage = 'The conversion is an Ecommerce Cart Update';
  346. }
  347. $goal['revenue'] = $this->getRevenue(Piwik_Common::getRequestVar('revenue', 0, 'float', $this->request));
  348. printDebug($debugMessage . ':' . var_export($goal, true));
  349. // INSERT or Sync items in the Cart / Order for this visit & order
  350. $items = $this->getEcommerceItemsFromRequest();
  351. if($items === false)
  352. {
  353. return;
  354. }
  355. $itemsCount = 0;
  356. foreach($items as $item)
  357. {
  358. $itemsCount += $item[self::INTERNAL_ITEM_QUANTITY];
  359. }
  360. $goal['items'] = $itemsCount;
  361. // If there is already a cart for this visit
  362. // 1) If conversion is Order, we update the cart into an Order
  363. // 2) If conversion is Cart Update, we update the cart
  364. $recorded = $this->recordGoal($goal, $this->isThereExistingCartInVisit, $updateWhere);
  365. if($recorded)
  366. {
  367. $this->recordEcommerceItems($goal, $items);
  368. }
  369. }
  370. /**
  371. * Returns Items read from the request string
  372. * @return array|false
  373. */
  374. protected function getEcommerceItemsFromRequest()
  375. {
  376. $items = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('ec_items', '', 'string', $this->request));
  377. if(empty($items))
  378. {
  379. printDebug("There are no Ecommerce items in the request");
  380. // we still record an Ecommerce order without any item in it
  381. return array();
  382. }
  383. $items = Piwik_Common::json_decode($items, $assoc = true);
  384. if(!is_array($items))
  385. {
  386. printDebug("Error while json_decode the Ecommerce items = ".var_export($items, true));
  387. return false;
  388. }
  389. $cleanedItems = $this->getCleanedEcommerceItems($items);
  390. return $cleanedItems;
  391. }
  392. /**
  393. * Loads the Ecommerce items from the request and records them in the DB
  394. *
  395. * @param array $goal
  396. * @return int $items Number of items in the cart
  397. */
  398. protected function recordEcommerceItems($goal, $items)
  399. {
  400. $itemInCartBySku = array();
  401. foreach($items as $item)
  402. {
  403. $itemInCartBySku[$item[0]] = $item;
  404. }
  405. // var_dump($items); echo "Items by SKU:";var_dump($itemInCartBySku);
  406. // Select all items currently in the Cart if any
  407. $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value
  408. FROM ". Piwik_Common::prefixTable('log_conversion_item') . "
  409. WHERE idvisit = ?
  410. AND (idorder = ? OR idorder = ?)";
  411. $bind = array( $goal['idvisit'],
  412. isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART,
  413. self::ITEM_IDORDER_ABANDONED_CART
  414. );
  415. $itemsInDb = Piwik_Tracker::getDatabase()->fetchAll($sql, $bind);
  416. printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind,true));
  417. printDebug($itemsInDb);
  418. // Look at which items need to be deleted, which need to be added or updated, based on the SKU
  419. $skuFoundInDb = $itemsToUpdate = array();
  420. foreach($itemsInDb as $itemInDb)
  421. {
  422. $skuFoundInDb[] = $itemInDb['idaction_sku'];
  423. // Ensure price comparisons will have the same assumption
  424. $itemInDb['price'] = $this->getRevenue($itemInDb['price']);
  425. $itemInDbOriginal = $itemInDb;
  426. $itemInDb = array_values($itemInDb);
  427. // Cast all as string, because what comes out of the fetchAll() are strings
  428. $itemInDb = $this->getItemRowCast($itemInDb);
  429. //Item in the cart in the DB, but not anymore in the cart
  430. if(!isset($itemInCartBySku[$itemInDb[0]]))
  431. {
  432. $itemToUpdate = array_merge($itemInDb,
  433. array( 'deleted' => 1,
  434. 'idorder_original_value' => $itemInDbOriginal['idorder_original_value']
  435. )
  436. );
  437. $itemsToUpdate[] = $itemToUpdate;
  438. printDebug("Item found in the previous Cart, but no in the current cart/order");
  439. printDebug($itemToUpdate);
  440. continue;
  441. }
  442. $newItem = $itemInCartBySku[$itemInDb[0]];
  443. $newItem = $this->getItemRowCast($newItem);
  444. if(count($itemInDb) != count($newItem))
  445. {
  446. printDebug("ERROR: Different format in items from cart and DB");
  447. throw new Exception(" Item in DB and Item in cart have a different format, this is not expected... ".var_export($itemInDb, true) . var_export($newItem, true));
  448. }
  449. printDebug("Item has changed since the last cart. Previous item stored in cart in database:");
  450. printDebug($itemInDb);
  451. printDebug("New item to UPDATE the previous row:");
  452. $newItem['idorder_original_value'] = $itemInDbOriginal['idorder_original_value'];
  453. printDebug($newItem);
  454. $itemsToUpdate[] = $newItem;
  455. }
  456. // Items to UPDATE
  457. //var_dump($itemsToUpdate);
  458. $this->updateEcommerceItems($goal, $itemsToUpdate);
  459. // Items to INSERT
  460. $itemsToInsert = array();
  461. foreach($items as $item)
  462. {
  463. if(!in_array($item[0], $skuFoundInDb))
  464. {
  465. $itemsToInsert[] = $item;
  466. }
  467. }
  468. $this->insertEcommerceItems($goal, $itemsToInsert);
  469. }
  470. // In the GET items parameter, each item has the following array of information
  471. const INDEX_ITEM_SKU = 0;
  472. const INDEX_ITEM_NAME = 1;
  473. const INDEX_ITEM_CATEGORY = 2;
  474. const INDEX_ITEM_PRICE = 3;
  475. const INDEX_ITEM_QUANTITY = 4;
  476. // Used in the array of items, internally to this class
  477. const INTERNAL_ITEM_SKU = 0;
  478. const INTERNAL_ITEM_NAME = 1;
  479. const INTERNAL_ITEM_CATEGORY = 2;
  480. const INTERNAL_ITEM_CATEGORY2 = 3;
  481. const INTERNAL_ITEM_CATEGORY3 = 4;
  482. const INTERNAL_ITEM_CATEGORY4 = 5;
  483. const INTERNAL_ITEM_CATEGORY5 = 6;
  484. const INTERNAL_ITEM_PRICE = 7;
  485. const INTERNAL_ITEM_QUANTITY = 8;
  486. /**
  487. * Reads items from the request, then looks up the names from the lookup table
  488. * and returns a clean array of items ready for the database.
  489. *
  490. * @param array $items
  491. * @return array $cleanedItems
  492. */
  493. protected function getCleanedEcommerceItems($items)
  494. {
  495. // Clean up the items array
  496. $cleanedItems = array();
  497. foreach($items as $item)
  498. {
  499. $name = $category = $category2 = $category3 = $category4 = $category5 = false;
  500. $price = 0;
  501. $quantity = 1;
  502. // items are passed in the request as an array: ( $sku, $name, $category, $price, $quantity )
  503. if(empty($item[self::INDEX_ITEM_SKU])) {
  504. continue;
  505. }
  506. $sku = $item[self::INDEX_ITEM_SKU];
  507. if(!empty($item[self::INDEX_ITEM_NAME])) {
  508. $name = $item[self::INDEX_ITEM_NAME];
  509. }
  510. if(!empty($item[self::INDEX_ITEM_CATEGORY])) {
  511. $category = $item[self::INDEX_ITEM_CATEGORY];
  512. }
  513. if(!empty($item[self::INDEX_ITEM_PRICE])
  514. && is_numeric($item[self::INDEX_ITEM_PRICE])) {
  515. $price = $this->getRevenue($item[self::INDEX_ITEM_PRICE]);
  516. }
  517. if(!empty($item[self::INDEX_ITEM_QUANTITY])
  518. && is_numeric($item[self::INDEX_ITEM_QUANTITY])) {
  519. $quantity = (int)$item[self::INDEX_ITEM_QUANTITY];
  520. }
  521. // self::INDEX_ITEM_* are in order
  522. $cleanedItems[] = array(
  523. self::INTERNAL_ITEM_SKU => $sku,
  524. self::INTERNAL_ITEM_NAME => $name,
  525. self::INTERNAL_ITEM_CATEGORY => $category,
  526. self::INTERNAL_ITEM_CATEGORY2 => $category2,
  527. self::INTERNAL_ITEM_CATEGORY3 => $category3,
  528. self::INTERNAL_ITEM_CATEGORY4 => $category4,
  529. self::INTERNAL_ITEM_CATEGORY5 => $category5,
  530. self::INTERNAL_ITEM_PRICE => $price,
  531. self::INTERNAL_ITEM_QUANTITY => $quantity
  532. );
  533. }
  534. // Lookup Item SKUs, Names & Categories Ids
  535. $actionsToLookupAllItems = array();
  536. // Each item has 7 potential "ids" to lookup in the lookup table
  537. $columnsInEachRow = 1 + 1 + self::MAXIMUM_PRODUCT_CATEGORIES;
  538. foreach($cleanedItems as $item)
  539. {
  540. $actionsToLookup = array();
  541. list($sku, $name, $category, $price, $quantity) = $item;
  542. $actionsToLookup[] = array(trim($sku), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_SKU);
  543. $actionsToLookup[] = array(trim($name), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_NAME);
  544. // Only one category
  545. if(!is_array($category))
  546. {
  547. $actionsToLookup[] = array(trim($category), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
  548. }
  549. // Multiple categories
  550. else
  551. {
  552. $countCategories = 0;
  553. foreach($category as $productCategory) {
  554. $productCategory = trim($productCategory);
  555. if(empty($productCategory)) {
  556. continue;
  557. }
  558. $countCategories++;
  559. if($countCategories > self::MAXIMUM_PRODUCT_CATEGORIES) {
  560. break;
  561. }
  562. $actionsToLookup[] = array($productCategory, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
  563. }
  564. }
  565. // Ensure that each row has the same number of columns, fill in the blanks
  566. for($i = count($actionsToLookup); $i < $columnsInEachRow; $i++) {
  567. $actionsToLookup[] = array(false, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
  568. }
  569. $actionsToLookupAllItems = array_merge($actionsToLookupAllItems, $actionsToLookup);
  570. }
  571. $actionsLookedUp = Piwik_Tracker_Action::loadActionId($actionsToLookupAllItems);
  572. // var_dump($actionsLookedUp);
  573. // Replace SKU, name & category by their ID action
  574. foreach($cleanedItems as $index => &$item)
  575. {
  576. list($sku, $name, $category, $price, $quantity) = $item;
  577. // SKU
  578. $item[0] = $actionsLookedUp[ $index * $columnsInEachRow + 0][2];
  579. // Name
  580. $item[1] = $actionsLookedUp[ $index * $columnsInEachRow + 1][2];
  581. // Categories
  582. $item[2] = $actionsLookedUp[ $index * $columnsInEachRow + 2][2];
  583. $item[3] = $actionsLookedUp[ $index * $columnsInEachRow + 3][2];
  584. $item[4] = $actionsLookedUp[ $index * $columnsInEachRow + 4][2];
  585. $item[5] = $actionsLookedUp[ $index * $columnsInEachRow + 5][2];
  586. $item[6] = $actionsLookedUp[ $index * $columnsInEachRow + 6][2];
  587. }
  588. return $cleanedItems;
  589. }
  590. /**
  591. * Updates the cart items in the DB
  592. * that have been modified since the last cart update
  593. */
  594. protected function updateEcommerceItems($goal, $itemsToUpdate)
  595. {
  596. if(empty($itemsToUpdate))
  597. {
  598. return;
  599. }
  600. printDebug("Goal data used to update ecommerce items:");
  601. printDebug($goal);
  602. foreach($itemsToUpdate as $item)
  603. {
  604. $newRow = $this->getItemRowEnriched($goal, $item);
  605. printDebug($newRow);
  606. $updateParts = $sqlBind = array();
  607. foreach($newRow AS $name => $value)
  608. {
  609. $updateParts[] = $name." = ?";
  610. $sqlBind[] = $value;
  611. }
  612. $sql = 'UPDATE ' . Piwik_Common::prefixTable('log_conversion_item') . "
  613. SET ".implode($updateParts, ', ')."
  614. WHERE idvisit = ?
  615. AND idorder = ?
  616. AND idaction_sku = ?";
  617. $sqlBind[] = $newRow['idvisit'];
  618. $sqlBind[] = $item['idorder_original_value'];
  619. $sqlBind[] = $newRow['idaction_sku'];
  620. Piwik_Tracker::getDatabase()->query($sql, $sqlBind);
  621. }
  622. }
  623. /**
  624. * Inserts in the cart in the DB the new items
  625. * that were not previously in the cart
  626. */
  627. protected function insertEcommerceItems($goal, $itemsToInsert)
  628. {
  629. if(empty($itemsToInsert))
  630. {
  631. return;
  632. }
  633. printDebug("Ecommerce items that are added to the cart/order");
  634. printDebug($itemsToInsert);
  635. $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_conversion_item') . "
  636. (idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted,
  637. idorder, idsite, idvisitor, server_time, idvisit)
  638. VALUES ";
  639. $i = 0;
  640. $bind = array();
  641. foreach($itemsToInsert as $item)
  642. {
  643. if($i > 0) { $sql .= ','; }
  644. $newRow = array_values($this->getItemRowEnriched($goal, $item));
  645. $sql .= " ( ". Piwik_Common::getSqlStringFieldsArray($newRow) . " ) ";
  646. $i++;
  647. $bind = array_merge($bind, $newRow);
  648. }
  649. Piwik_Tracker::getDatabase()->query($sql, $bind);
  650. printDebug($sql);printDebug($bind);
  651. }
  652. protected function getItemRowEnriched($goal, $item)
  653. {
  654. $newRow = array(
  655. 'idaction_sku' => (int)$item[self::INTERNAL_ITEM_SKU],
  656. 'idaction_name' => (int)$item[self::INTERNAL_ITEM_NAME],
  657. 'idaction_category' => (int)$item[self::INTERNAL_ITEM_CATEGORY],
  658. 'idaction_category2' => (int)$item[self::INTERNAL_ITEM_CATEGORY2],
  659. 'idaction_category3' => (int)$item[self::INTERNAL_ITEM_CATEGORY3],
  660. 'idaction_category4' => (int)$item[self::INTERNAL_ITEM_CATEGORY4],
  661. 'idaction_category5' => (int)$item[self::INTERNAL_ITEM_CATEGORY5],
  662. 'price' => $item[self::INTERNAL_ITEM_PRICE],
  663. 'quantity' => $item[self::INTERNAL_ITEM_QUANTITY],
  664. 'deleted' => isset($item['deleted']) ? $item['deleted'] : 0, //deleted
  665. 'idorder' => isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, //idorder = 0 in log_conversion_item for carts
  666. 'idsite' => $goal['idsite'],
  667. 'idvisitor' => $goal['idvisitor'],
  668. 'server_time' => $goal['server_time'],
  669. 'idvisit' => $goal['idvisit']
  670. );
  671. return $newRow;
  672. }
  673. /**
  674. * Records a standard non-Ecommerce goal in the DB (URL/Title matching),
  675. * linking the conversion to the action that triggered it
  676. */
  677. protected function recordStandardGoals($goal, $action, $visitorInformation)
  678. {
  679. foreach($this->convertedGoals as $convertedGoal)
  680. {
  681. printDebug("- Goal ".$convertedGoal['idgoal'] ." matched. Recording...");
  682. $newGoal = $goal;
  683. $newGoal['idgoal'] = $convertedGoal['idgoal'];
  684. $newGoal['url'] = $convertedGoal['url'];
  685. $newGoal['revenue'] = $this->getRevenue($convertedGoal['revenue']);
  686. if(!is_null($action))
  687. {
  688. $newGoal['idaction_url'] = (int)$action->getIdActionUrl();
  689. $newGoal['idlink_va'] = $action->getIdLinkVisitAction();
  690. }
  691. // If multiple Goal conversions per visit, set a cache buster
  692. $newGoal['buster'] = $convertedGoal['allow_multiple'] == 0
  693. ? '0'
  694. : $visitorInformation['visit_last_action_time'];
  695. $this->recordGoal($newGoal);
  696. }
  697. }
  698. /**
  699. * Helper function used by other record* methods which will INSERT or UPDATE the conversion in the DB
  700. *
  701. * @param array $newGoal
  702. * @param bool $mustUpdateNotInsert If set to true, the previous conversion will be UPDATEd. This is used for the Cart Update conversion (only one cart per visit)
  703. * @param array $updateWhere
  704. */
  705. protected function recordGoal($newGoal, $mustUpdateNotInsert = false, $updateWhere = array())
  706. {
  707. $newGoalDebug = $newGoal;
  708. $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']);
  709. printDebug($newGoalDebug);
  710. $fields = implode(", ", array_keys($newGoal));
  711. $bindFields = Piwik_Common::getSqlStringFieldsArray($newGoal);
  712. if($mustUpdateNotInsert)
  713. {
  714. $updateParts = $sqlBind = $updateWhereParts = array();
  715. foreach($newGoal AS $name => $value)
  716. {
  717. $updateParts[] = $name." = ?";
  718. $sqlBind[] = $value;
  719. }
  720. foreach($updateWhere as $name => $value)
  721. {
  722. $updateWhereParts[] = $name." = ?";
  723. $sqlBind[] = $value;
  724. }
  725. $sql = 'UPDATE ' . Piwik_Common::prefixTable('log_conversion') . "
  726. SET ".implode($updateParts, ', ')."
  727. WHERE ".implode($updateWhereParts, ' AND ');
  728. Piwik_Tracker::getDatabase()->query($sql, $sqlBind);
  729. return true;
  730. }
  731. else
  732. {
  733. $sql = 'INSERT IGNORE INTO ' . Piwik_Common::prefixTable('log_conversion') . "
  734. ($fields) VALUES ($bindFields) ";
  735. $bind = array_values($newGoal);
  736. $result = Piwik_Tracker::getDatabase()->query($sql, $bind);
  737. // If a record was inserted, we return true
  738. return Piwik_Tracker::getDatabase()->rowCount($result) > 0;
  739. }
  740. }
  741. /**
  742. * Casts the item array so that array comparisons work nicely
  743. */
  744. protected function getItemRowCast($row)
  745. {
  746. return array(
  747. (string)(int)$row[self::INTERNAL_ITEM_SKU],
  748. (string)(int)$row[self::INTERNAL_ITEM_NAME],
  749. (string)(int)$row[self::INTERNAL_ITEM_CATEGORY],
  750. (string)(int)$row[self::INTERNAL_ITEM_CATEGORY2],
  751. (string)(int)$row[self::INTERNAL_ITEM_CATEGORY3],
  752. (string)(int)$row[self::INTERNAL_ITEM_CATEGORY4],
  753. (string)(int)$row[self::INTERNAL_ITEM_CATEGORY5],
  754. (string)$row[self::INTERNAL_ITEM_PRICE],
  755. (string)$row[self::INTERNAL_ITEM_QUANTITY],
  756. );
  757. }
  758. }