/core/Tracker/GoalManager.php
PHP | 905 lines | 624 code | 88 blank | 193 comment | 94 complexity | f8eeacadbdb6d130cadb05067b684a03 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
- <?php
- /**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
- namespace Piwik\Tracker;
- use Exception;
- use Piwik\Common;
- use Piwik\Config;
- use Piwik\Piwik;
- use Piwik\Plugins\CustomVariables\CustomVariables;
- use Piwik\Tracker;
- /**
- */
- class GoalManager
- {
- // log_visit.visit_goal_buyer
- const TYPE_BUYER_NONE = 0;
- const TYPE_BUYER_ORDERED = 1;
- const TYPE_BUYER_OPEN_CART = 2;
- const TYPE_BUYER_ORDERED_AND_OPEN_CART = 3;
- // log_conversion.idorder is NULLable, but not log_conversion_item which defaults to zero for carts
- const ITEM_IDORDER_ABANDONED_CART = 0;
- // log_conversion.idgoal special values
- const IDGOAL_CART = -1;
- const IDGOAL_ORDER = 0;
- const REVENUE_PRECISION = 2;
- const MAXIMUM_PRODUCT_CATEGORIES = 5;
- public $idGoal;
- public $requestIsEcommerce;
- public $isGoalAnOrder;
- /**
- * @var Action
- */
- protected $action = null;
- protected $convertedGoals = array();
- protected $isThereExistingCartInVisit = false;
- /**
- * @var Request
- */
- protected $request;
- protected $orderId;
- /**
- * Constructor
- * @param Request $request
- */
- public function __construct(Request $request)
- {
- $this->request = $request;
- $this->init();
- }
- function init()
- {
- $this->orderId = $this->request->getParam('ec_id');
- $this->isGoalAnOrder = !empty($this->orderId);
- $this->idGoal = $this->request->getParam('idgoal');
- $this->requestIsEcommerce = ($this->idGoal == 0);
- }
- function getBuyerType($existingType = GoalManager::TYPE_BUYER_NONE)
- {
- // Was there a Cart for this visit prior to the order?
- $this->isThereExistingCartInVisit = in_array($existingType,
- array(GoalManager::TYPE_BUYER_OPEN_CART,
- GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART));
- if (!$this->requestIsEcommerce) {
- return $existingType;
- }
- if ($this->isGoalAnOrder) {
- return self::TYPE_BUYER_ORDERED;
- }
- // request is Add to Cart
- if ($existingType == self::TYPE_BUYER_ORDERED
- || $existingType == self::TYPE_BUYER_ORDERED_AND_OPEN_CART
- ) {
- return self::TYPE_BUYER_ORDERED_AND_OPEN_CART;
- }
- return self::TYPE_BUYER_OPEN_CART;
- }
- static public function getGoalDefinitions($idSite)
- {
- $websiteAttributes = Cache::getCacheWebsiteAttributes($idSite);
- if (isset($websiteAttributes['goals'])) {
- return $websiteAttributes['goals'];
- }
- return array();
- }
- static public function getGoalDefinition($idSite, $idGoal)
- {
- $goals = self::getGoalDefinitions($idSite);
- foreach ($goals as $goal) {
- if ($goal['idgoal'] == $idGoal) {
- return $goal;
- }
- }
- throw new Exception('Goal not found');
- }
- static public function getGoalIds($idSite)
- {
- $goals = self::getGoalDefinitions($idSite);
- $goalIds = array();
- foreach ($goals as $goal) {
- $goalIds[] = $goal['idgoal'];
- }
- return $goalIds;
- }
- /**
- * Look at the URL or Page Title and sees if it matches any existing Goal definition
- *
- * @param int $idSite
- * @param Action $action
- * @throws Exception
- * @return int Number of goals matched
- */
- function detectGoalsMatchingUrl($idSite, $action)
- {
- if (!Common::isGoalPluginEnabled()) {
- return false;
- }
- $decodedActionUrl = $action->getActionUrl();
- $actionType = $action->getActionType();
- $goals = $this->getGoalDefinitions($idSite);
- foreach ($goals as $goal) {
- $attribute = $goal['match_attribute'];
- // if the attribute to match is not the type of the current action
- if ( (($attribute == 'url' || $attribute == 'title') && $actionType != Action::TYPE_PAGE_URL)
- || ($attribute == 'file' && $actionType != Action::TYPE_DOWNLOAD)
- || ($attribute == 'external_website' && $actionType != Action::TYPE_OUTLINK)
- || ($attribute == 'manually')
- ) {
- continue;
- }
- $url = $decodedActionUrl;
- // Matching on Page Title
- if ($attribute == 'title') {
- $url = $action->getActionName();
- }
- $pattern_type = $goal['pattern_type'];
- $match = $this->isUrlMatchingGoal($goal, $pattern_type, $url);
- if ($match) {
- $goal['url'] = $decodedActionUrl;
- $this->convertedGoals[] = $goal;
- }
- }
- return count($this->convertedGoals) > 0;
- }
- function detectGoalId($idSite)
- {
- if (!Common::isGoalPluginEnabled()) {
- return false;
- }
- $goals = $this->getGoalDefinitions($idSite);
- if (!isset($goals[$this->idGoal])) {
- return false;
- }
- $goal = $goals[$this->idGoal];
- $url = $this->request->getParam('url');
- $goal['url'] = PageUrl::excludeQueryParametersFromUrl($url, $idSite);
- $goal['revenue'] = $this->getRevenue($this->request->getGoalRevenue($goal['revenue']));
- $this->convertedGoals[] = $goal;
- return true;
- }
- /**
- * Records one or several goals matched in this request.
- *
- * @param int $idSite
- * @param array $visitorInformation
- * @param array $visitCustomVariables
- * @param Action $action
- */
- public function recordGoals($idSite, $visitorInformation, $visitCustomVariables, $action)
- {
- $referrerTimestamp = $this->request->getParam('_refts');
- $referrerUrl = $this->request->getParam('_ref');
- $referrerCampaignName = trim(urldecode($this->request->getParam('_rcn')));
- $referrerCampaignKeyword = trim(urldecode($this->request->getParam('_rck')));
- $browserLanguage = $this->request->getBrowserLanguage();
- $location_country = isset($visitorInformation['location_country'])
- ? $visitorInformation['location_country']
- : Common::getCountry(
- $browserLanguage,
- $enableLanguageToCountryGuess = Config::getInstance()->Tracker['enable_language_to_country_guess'],
- $visitorInformation['location_ip']
- );
- $goal = array(
- 'idvisit' => $visitorInformation['idvisit'],
- 'idsite' => $idSite,
- 'idvisitor' => $visitorInformation['idvisitor'],
- 'server_time' => Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']),
- 'location_country' => $location_country,
- 'visitor_returning' => $visitorInformation['visitor_returning'],
- 'visitor_days_since_first' => $visitorInformation['visitor_days_since_first'],
- 'visitor_days_since_order' => $visitorInformation['visitor_days_since_order'],
- 'visitor_count_visits' => $visitorInformation['visitor_count_visits'],
- );
- $extraLocationCols = array('location_region', 'location_city', 'location_latitude', 'location_longitude');
- foreach ($extraLocationCols as $col) {
- if (isset($visitorInformation[$col])) {
- $goal[$col] = $visitorInformation[$col];
- }
- }
- // Copy Custom Variables from Visit row to the Goal conversion
- // Otherwise, set the Custom Variables found in the cookie sent with this request
- $goal += $visitCustomVariables;
- $maxCustomVariables = CustomVariables::getMaxCustomVariables();
- for ($i = 1; $i <= $maxCustomVariables; $i++) {
- if (isset($visitorInformation['custom_var_k' . $i])
- && strlen($visitorInformation['custom_var_k' . $i])
- ) {
- $goal['custom_var_k' . $i] = $visitorInformation['custom_var_k' . $i];
- }
- if (isset($visitorInformation['custom_var_v' . $i])
- && strlen($visitorInformation['custom_var_v' . $i])
- ) {
- $goal['custom_var_v' . $i] = $visitorInformation['custom_var_v' . $i];
- }
- }
- // Attributing the correct Referrer to this conversion.
- // Priority order is as follows:
- // 0) In some cases, the campaign is not passed from the JS so we look it up from the current visit
- // 1) Campaign name/kwd parsed in the JS
- // 2) Referrer URL stored in the _ref cookie
- // 3) If no info from the cookie, attribute to the current visit referrer
- // 3) Default values: current referrer
- $type = $visitorInformation['referer_type'];
- $name = $visitorInformation['referer_name'];
- $keyword = $visitorInformation['referer_keyword'];
- $time = $visitorInformation['visit_first_action_time'];
- // 0) In some (unknown!?) cases the campaign is not found in the attribution cookie, but the URL ref was found.
- // In this case we look up if the current visit is credited to a campaign and will credit this campaign rather than the URL ref (since campaigns have higher priority)
- if (empty($referrerCampaignName)
- && $type == Common::REFERRER_TYPE_CAMPAIGN
- && !empty($name)
- ) {
- // Use default values per above
- } // 1) Campaigns from 1st party cookie
- elseif (!empty($referrerCampaignName)) {
- $type = Common::REFERRER_TYPE_CAMPAIGN;
- $name = $referrerCampaignName;
- $keyword = $referrerCampaignKeyword;
- $time = $referrerTimestamp;
- } // 2) Referrer URL parsing
- elseif (!empty($referrerUrl)) {
- $referrer = new Referrer();
- $referrer = $referrer->getReferrerInformation($referrerUrl, $currentUrl = '', $idSite);
- // if the parsed referrer is interesting enough, ie. website or search engine
- if (in_array($referrer['referer_type'], array(Common::REFERRER_TYPE_SEARCH_ENGINE, Common::REFERRER_TYPE_WEBSITE))) {
- $type = $referrer['referer_type'];
- $name = $referrer['referer_name'];
- $keyword = $referrer['referer_keyword'];
- $time = $referrerTimestamp;
- }
- }
- $this->setCampaignValuesToLowercase($type, $name, $keyword);
- $goal += array(
- 'referer_type' => $type,
- 'referer_name' => $name,
- 'referer_keyword' => $keyword,
- // this field is currently unused
- 'referer_visit_server_date' => date("Y-m-d", $time),
- );
- // some goals are converted, so must be ecommerce Order or Cart Update
- if ($this->requestIsEcommerce) {
- $this->recordEcommerceGoal($goal, $visitorInformation);
- } else {
- $this->recordStandardGoals($goal, $action, $visitorInformation);
- }
- }
- /**
- * Returns rounded decimal revenue, or if revenue is integer, then returns as is.
- *
- * @param int|float $revenue
- * @return int|float
- */
- protected function getRevenue($revenue)
- {
- if (round($revenue) == $revenue) {
- return $revenue;
- }
- return round($revenue, self::REVENUE_PRECISION);
- }
- /**
- * Records an Ecommerce conversion in the DB. Deals with Items found in the request.
- * Will deal with 2 types of conversions: Ecommerce Order and Ecommerce Cart update (Add to cart, Update Cart etc).
- *
- * @param array $conversion
- * @param array $visitInformation
- */
- protected function recordEcommerceGoal($conversion, $visitInformation)
- {
- if ($this->isThereExistingCartInVisit) {
- Common::printDebug("There is an existing cart for this visit");
- }
- if ($this->isGoalAnOrder) {
- $conversion['idgoal'] = self::IDGOAL_ORDER;
- $conversion['idorder'] = $this->orderId;
- $conversion['buster'] = Common::hashStringToInt($this->orderId);
- $conversion['revenue_subtotal'] = $this->getRevenue($this->request->getParam('ec_st'));
- $conversion['revenue_tax'] = $this->getRevenue($this->request->getParam('ec_tx'));
- $conversion['revenue_shipping'] = $this->getRevenue($this->request->getParam('ec_sh'));
- $conversion['revenue_discount'] = $this->getRevenue($this->request->getParam('ec_dt'));
- $debugMessage = 'The conversion is an Ecommerce order';
- } // If Cart update, select current items in the previous Cart
- else {
- $conversion['buster'] = 0;
- $conversion['idgoal'] = self::IDGOAL_CART;
- $debugMessage = 'The conversion is an Ecommerce Cart Update';
- }
- $conversion['revenue'] = $this->getRevenue($this->request->getGoalRevenue($defaultRevenue = 0));
- Common::printDebug($debugMessage . ':' . var_export($conversion, true));
- // INSERT or Sync items in the Cart / Order for this visit & order
- $items = $this->getEcommerceItemsFromRequest();
- if ($items === false) {
- return;
- }
- $itemsCount = 0;
- foreach ($items as $item) {
- $itemsCount += $item[self::INTERNAL_ITEM_QUANTITY];
- }
- $conversion['items'] = $itemsCount;
- if($this->isThereExistingCartInVisit) {
- $updateWhere = array(
- 'idvisit' => $visitInformation['idvisit'],
- 'idgoal' => self::IDGOAL_CART,
- 'buster' => 0,
- );
- $recorded = $this->updateExistingConversion($conversion, $updateWhere);
- } else {
- $recorded = $this->insertNewConversion($conversion, $visitInformation);
- }
- if ($recorded) {
- $this->recordEcommerceItems($conversion, $items, $visitInformation);
- }
- /**
- * Triggered after successfully persisting an ecommerce conversion.
- *
- * _Note: Subscribers should be wary of doing any expensive computation here as it may slow
- * the tracker down._
- *
- * @param array $conversion The conversion entity that was just persisted. See what information
- * it contains [here](/guides/persistence-and-the-mysql-backend#conversions).
- * @param array $visitInformation The visit entity that we are tracking a conversion for. See what
- * information it contains [here](/guides/persistence-and-the-mysql-backend#visits).
- */
- Piwik::postEvent('Tracker.recordEcommerceGoal', array($conversion, $visitInformation));
- }
- /**
- * Returns Items read from the request string
- * @return array|bool
- */
- protected function getEcommerceItemsFromRequest()
- {
- $items = Common::unsanitizeInputValue($this->request->getParam('ec_items'));
- if (empty($items)) {
- Common::printDebug("There are no Ecommerce items in the request");
- // we still record an Ecommerce order without any item in it
- return array();
- }
- $items = Common::json_decode($items, $assoc = true);
- if (!is_array($items)) {
- Common::printDebug("Error while json_decode the Ecommerce items = " . var_export($items, true));
- return false;
- }
- $cleanedItems = $this->getCleanedEcommerceItems($items);
- return $cleanedItems;
- }
- /**
- * Loads the Ecommerce items from the request and records them in the DB
- *
- * @param array $goal
- * @param array $items
- * @throws Exception
- * @return int Number of items in the cart
- */
- protected function recordEcommerceItems($goal, $items)
- {
- $itemInCartBySku = array();
- foreach ($items as $item) {
- $itemInCartBySku[$item[0]] = $item;
- }
- // Select all items currently in the Cart if any
- $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value
- FROM " . Common::prefixTable('log_conversion_item') . "
- WHERE idvisit = ?
- AND (idorder = ? OR idorder = ?)";
- $bind = array($goal['idvisit'],
- isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART,
- self::ITEM_IDORDER_ABANDONED_CART
- );
- $itemsInDb = Tracker::getDatabase()->fetchAll($sql, $bind);
- Common::printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true));
- Common::printDebug($itemsInDb);
- // Look at which items need to be deleted, which need to be added or updated, based on the SKU
- $skuFoundInDb = $itemsToUpdate = array();
- foreach ($itemsInDb as $itemInDb) {
- $skuFoundInDb[] = $itemInDb['idaction_sku'];
- // Ensure price comparisons will have the same assumption
- $itemInDb['price'] = $this->getRevenue($itemInDb['price']);
- $itemInDbOriginal = $itemInDb;
- $itemInDb = array_values($itemInDb);
- // Cast all as string, because what comes out of the fetchAll() are strings
- $itemInDb = $this->getItemRowCast($itemInDb);
- //Item in the cart in the DB, but not anymore in the cart
- if (!isset($itemInCartBySku[$itemInDb[0]])) {
- $itemToUpdate = array_merge($itemInDb,
- array('deleted' => 1,
- 'idorder_original_value' => $itemInDbOriginal['idorder_original_value']
- )
- );
- $itemsToUpdate[] = $itemToUpdate;
- Common::printDebug("Item found in the previous Cart, but no in the current cart/order");
- Common::printDebug($itemToUpdate);
- continue;
- }
- $newItem = $itemInCartBySku[$itemInDb[0]];
- $newItem = $this->getItemRowCast($newItem);
- if (count($itemInDb) != count($newItem)) {
- Common::printDebug("ERROR: Different format in items from cart and DB");
- 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));
- }
- Common::printDebug("Item has changed since the last cart. Previous item stored in cart in database:");
- Common::printDebug($itemInDb);
- Common::printDebug("New item to UPDATE the previous row:");
- $newItem['idorder_original_value'] = $itemInDbOriginal['idorder_original_value'];
- Common::printDebug($newItem);
- $itemsToUpdate[] = $newItem;
- }
- // Items to UPDATE
- $this->updateEcommerceItems($goal, $itemsToUpdate);
- // Items to INSERT
- $itemsToInsert = array();
- foreach ($items as $item) {
- if (!in_array($item[0], $skuFoundInDb)) {
- $itemsToInsert[] = $item;
- }
- }
- $this->insertEcommerceItems($goal, $itemsToInsert);
- }
- // In the GET items parameter, each item has the following array of information
- const INDEX_ITEM_SKU = 0;
- const INDEX_ITEM_NAME = 1;
- const INDEX_ITEM_CATEGORY = 2;
- const INDEX_ITEM_PRICE = 3;
- const INDEX_ITEM_QUANTITY = 4;
- // Used in the array of items, internally to this class
- const INTERNAL_ITEM_SKU = 0;
- const INTERNAL_ITEM_NAME = 1;
- const INTERNAL_ITEM_CATEGORY = 2;
- const INTERNAL_ITEM_CATEGORY2 = 3;
- const INTERNAL_ITEM_CATEGORY3 = 4;
- const INTERNAL_ITEM_CATEGORY4 = 5;
- const INTERNAL_ITEM_CATEGORY5 = 6;
- const INTERNAL_ITEM_PRICE = 7;
- const INTERNAL_ITEM_QUANTITY = 8;
- /**
- * Reads items from the request, then looks up the names from the lookup table
- * and returns a clean array of items ready for the database.
- *
- * @param array $items
- * @return array $cleanedItems
- */
- protected function getCleanedEcommerceItems($items)
- {
- // Clean up the items array
- $cleanedItems = array();
- foreach ($items as $item) {
- $name = $category = $category2 = $category3 = $category4 = $category5 = false;
- $price = 0;
- $quantity = 1;
- // items are passed in the request as an array: ( $sku, $name, $category, $price, $quantity )
- if (empty($item[self::INDEX_ITEM_SKU])) {
- continue;
- }
- $sku = $item[self::INDEX_ITEM_SKU];
- if (!empty($item[self::INDEX_ITEM_NAME])) {
- $name = $item[self::INDEX_ITEM_NAME];
- }
- if (!empty($item[self::INDEX_ITEM_CATEGORY])) {
- $category = $item[self::INDEX_ITEM_CATEGORY];
- }
- if (isset($item[self::INDEX_ITEM_PRICE])
- && is_numeric($item[self::INDEX_ITEM_PRICE])
- ) {
- $price = $this->getRevenue($item[self::INDEX_ITEM_PRICE]);
- }
- if (!empty($item[self::INDEX_ITEM_QUANTITY])
- && is_numeric($item[self::INDEX_ITEM_QUANTITY])
- ) {
- $quantity = (int)$item[self::INDEX_ITEM_QUANTITY];
- }
- // self::INDEX_ITEM_* are in order
- $cleanedItems[] = array(
- self::INTERNAL_ITEM_SKU => $sku,
- self::INTERNAL_ITEM_NAME => $name,
- self::INTERNAL_ITEM_CATEGORY => $category,
- self::INTERNAL_ITEM_CATEGORY2 => $category2,
- self::INTERNAL_ITEM_CATEGORY3 => $category3,
- self::INTERNAL_ITEM_CATEGORY4 => $category4,
- self::INTERNAL_ITEM_CATEGORY5 => $category5,
- self::INTERNAL_ITEM_PRICE => $price,
- self::INTERNAL_ITEM_QUANTITY => $quantity
- );
- }
- // Lookup Item SKUs, Names & Categories Ids
- $actionsToLookupAllItems = array();
- // Each item has 7 potential "ids" to lookup in the lookup table
- $columnsInEachRow = 1 + 1 + self::MAXIMUM_PRODUCT_CATEGORIES;
- foreach ($cleanedItems as $item) {
- $actionsToLookup = array();
- list($sku, $name, $category, $price, $quantity) = $item;
- $actionsToLookup[] = array(trim($sku), Action::TYPE_ECOMMERCE_ITEM_SKU);
- $actionsToLookup[] = array(trim($name), Action::TYPE_ECOMMERCE_ITEM_NAME);
- // Only one category
- if (!is_array($category)) {
- $actionsToLookup[] = array(trim($category), Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
- } // Multiple categories
- else {
- $countCategories = 0;
- foreach ($category as $productCategory) {
- $productCategory = trim($productCategory);
- if (empty($productCategory)) {
- continue;
- }
- $countCategories++;
- if ($countCategories > self::MAXIMUM_PRODUCT_CATEGORIES) {
- break;
- }
- $actionsToLookup[] = array($productCategory, Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
- }
- }
- // Ensure that each row has the same number of columns, fill in the blanks
- for ($i = count($actionsToLookup); $i < $columnsInEachRow; $i++) {
- $actionsToLookup[] = array(false, Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
- }
- $actionsToLookupAllItems = array_merge($actionsToLookupAllItems, $actionsToLookup);
- }
- $actionsLookedUp = TableLogAction::loadIdsAction($actionsToLookupAllItems);
- // Replace SKU, name & category by their ID action
- foreach ($cleanedItems as $index => &$item) {
- // SKU
- $item[0] = $actionsLookedUp[$index * $columnsInEachRow + 0];
- // Name
- $item[1] = $actionsLookedUp[$index * $columnsInEachRow + 1];
- // Categories
- $item[2] = $actionsLookedUp[$index * $columnsInEachRow + 2];
- $item[3] = $actionsLookedUp[$index * $columnsInEachRow + 3];
- $item[4] = $actionsLookedUp[$index * $columnsInEachRow + 4];
- $item[5] = $actionsLookedUp[$index * $columnsInEachRow + 5];
- $item[6] = $actionsLookedUp[$index * $columnsInEachRow + 6];
- }
- return $cleanedItems;
- }
- /**
- * Updates the cart items in the DB
- * that have been modified since the last cart update
- *
- * @param array $goal
- * @param array $itemsToUpdate
- *
- * @return void
- */
- protected function updateEcommerceItems($goal, $itemsToUpdate)
- {
- if (empty($itemsToUpdate)) {
- return;
- }
- Common::printDebug("Goal data used to update ecommerce items:");
- Common::printDebug($goal);
- foreach ($itemsToUpdate as $item) {
- $newRow = $this->getItemRowEnriched($goal, $item);
- Common::printDebug($newRow);
- $updateParts = $sqlBind = array();
- foreach ($newRow AS $name => $value) {
- $updateParts[] = $name . " = ?";
- $sqlBind[] = $value;
- }
- $sql = 'UPDATE ' . Common::prefixTable('log_conversion_item') . "
- SET " . implode($updateParts, ', ') . "
- WHERE idvisit = ?
- AND idorder = ?
- AND idaction_sku = ?";
- $sqlBind[] = $newRow['idvisit'];
- $sqlBind[] = $item['idorder_original_value'];
- $sqlBind[] = $newRow['idaction_sku'];
- Tracker::getDatabase()->query($sql, $sqlBind);
- }
- }
- /**
- * Inserts in the cart in the DB the new items
- * that were not previously in the cart
- *
- * @param array $goal
- * @param array $itemsToInsert
- *
- * @return void
- */
- protected function insertEcommerceItems($goal, $itemsToInsert)
- {
- if (empty($itemsToInsert)) {
- return;
- }
- Common::printDebug("Ecommerce items that are added to the cart/order");
- Common::printDebug($itemsToInsert);
- $sql = "INSERT INTO " . Common::prefixTable('log_conversion_item') . "
- (idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted,
- idorder, idsite, idvisitor, server_time, idvisit)
- VALUES ";
- $i = 0;
- $bind = array();
- foreach ($itemsToInsert as $item) {
- if ($i > 0) {
- $sql .= ',';
- }
- $newRow = array_values($this->getItemRowEnriched($goal, $item));
- $sql .= " ( " . Common::getSqlStringFieldsArray($newRow) . " ) ";
- $i++;
- $bind = array_merge($bind, $newRow);
- }
- Tracker::getDatabase()->query($sql, $bind);
- Common::printDebug($sql);
- Common::printDebug($bind);
- }
- protected function getItemRowEnriched($goal, $item)
- {
- $newRow = array(
- 'idaction_sku' => (int)$item[self::INTERNAL_ITEM_SKU],
- 'idaction_name' => (int)$item[self::INTERNAL_ITEM_NAME],
- 'idaction_category' => (int)$item[self::INTERNAL_ITEM_CATEGORY],
- 'idaction_category2' => (int)$item[self::INTERNAL_ITEM_CATEGORY2],
- 'idaction_category3' => (int)$item[self::INTERNAL_ITEM_CATEGORY3],
- 'idaction_category4' => (int)$item[self::INTERNAL_ITEM_CATEGORY4],
- 'idaction_category5' => (int)$item[self::INTERNAL_ITEM_CATEGORY5],
- 'price' => $item[self::INTERNAL_ITEM_PRICE],
- 'quantity' => $item[self::INTERNAL_ITEM_QUANTITY],
- 'deleted' => isset($item['deleted']) ? $item['deleted'] : 0, //deleted
- 'idorder' => isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, //idorder = 0 in log_conversion_item for carts
- 'idsite' => $goal['idsite'],
- 'idvisitor' => $goal['idvisitor'],
- 'server_time' => $goal['server_time'],
- 'idvisit' => $goal['idvisit']
- );
- return $newRow;
- }
- /**
- * Records a standard non-Ecommerce goal in the DB (URL/Title matching),
- * linking the conversion to the action that triggered it
- * @param $goal
- * @param Action $action
- * @param $visitorInformation
- */
- protected function recordStandardGoals($goal, $action, $visitorInformation)
- {
- foreach ($this->convertedGoals as $convertedGoal) {
- Common::printDebug("- Goal " . $convertedGoal['idgoal'] . " matched. Recording...");
- $conversion = $goal;
- $conversion['idgoal'] = $convertedGoal['idgoal'];
- $conversion['url'] = $convertedGoal['url'];
- $conversion['revenue'] = $this->getRevenue($convertedGoal['revenue']);
- if (!is_null($action)) {
- $conversion['idaction_url'] = $action->getIdActionUrl();
- $conversion['idlink_va'] = $action->getIdLinkVisitAction();
- }
- // If multiple Goal conversions per visit, set a cache buster
- $conversion['buster'] = $convertedGoal['allow_multiple'] == 0
- ? '0'
- : $visitorInformation['visit_last_action_time'];
- $this->insertNewConversion($conversion, $visitorInformation);
- /**
- * Triggered after successfully recording a non-ecommerce conversion.
- *
- * _Note: Subscribers should be wary of doing any expensive computation here as it may slow
- * the tracker down._
- *
- * @param array $conversion The conversion entity that was just persisted. See what information
- * it contains [here](/guides/persistence-and-the-mysql-backend#conversions).
- */
- Piwik::postEvent('Tracker.recordStandardGoals', array($conversion));
- }
- }
- /**
- * Helper function used by other record* methods which will INSERT or UPDATE the conversion in the DB
- *
- * @param array $conversion
- * @param array $visitInformation
- * @return bool
- */
- protected function insertNewConversion($conversion, $visitInformation)
- {
- /**
- * Triggered before persisting a new [conversion entity](/guides/persistence-and-the-mysql-backend#conversions).
- *
- * This event can be used to modify conversion information or to add new information to be persisted.
- *
- * @param array $conversion The conversion entity. Read [this](/guides/persistence-and-the-mysql-backend#conversions)
- * to see what it contains.
- * @param array $visitInformation The visit entity that we are tracking a conversion for. See what
- * information it contains [here](/guides/persistence-and-the-mysql-backend#visits).
- * @param \Piwik\Tracker\Request $request An object describing the tracking request being processed.
- */
- Piwik::postEvent('Tracker.newConversionInformation', array(&$conversion, $visitInformation, $this->request));
- $newGoalDebug = $conversion;
- $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']);
- Common::printDebug($newGoalDebug);
- $fields = implode(", ", array_keys($conversion));
- $bindFields = Common::getSqlStringFieldsArray($conversion);
- $sql = 'INSERT IGNORE INTO ' . Common::prefixTable('log_conversion') . "
- ($fields) VALUES ($bindFields) ";
- $bind = array_values($conversion);
- $result = Tracker::getDatabase()->query($sql, $bind);
- // If a record was inserted, we return true
- return Tracker::getDatabase()->rowCount($result) > 0;
- }
- /**
- * Casts the item array so that array comparisons work nicely
- * @param array $row
- * @return array
- */
- protected function getItemRowCast($row)
- {
- return array(
- (string)(int)$row[self::INTERNAL_ITEM_SKU],
- (string)(int)$row[self::INTERNAL_ITEM_NAME],
- (string)(int)$row[self::INTERNAL_ITEM_CATEGORY],
- (string)(int)$row[self::INTERNAL_ITEM_CATEGORY2],
- (string)(int)$row[self::INTERNAL_ITEM_CATEGORY3],
- (string)(int)$row[self::INTERNAL_ITEM_CATEGORY4],
- (string)(int)$row[self::INTERNAL_ITEM_CATEGORY5],
- (string)$row[self::INTERNAL_ITEM_PRICE],
- (string)$row[self::INTERNAL_ITEM_QUANTITY],
- );
- }
- protected function updateExistingConversion($newGoal, $updateWhere)
- {
- $updateParts = $sqlBind = $updateWhereParts = array();
- foreach ($newGoal AS $name => $value) {
- $updateParts[] = $name . " = ?";
- $sqlBind[] = $value;
- }
- foreach ($updateWhere as $name => $value) {
- $updateWhereParts[] = $name . " = ?";
- $sqlBind[] = $value;
- }
- $sql = 'UPDATE ' . Common::prefixTable('log_conversion') . "
- SET " . implode($updateParts, ', ') . "
- WHERE " . implode($updateWhereParts, ' AND ');
- try {
- Tracker::getDatabase()->query($sql, $sqlBind);
- } catch(Exception $e){
- Common::printDebug("There was an error while updating the Conversion: " . $e->getMessage());
- return false;
- }
- return true;
- }
- /**
- * @param $type
- * @param $name
- * @param $keyword
- */
- protected function setCampaignValuesToLowercase($type, &$name, &$keyword)
- {
- if ($type === Common::REFERRER_TYPE_CAMPAIGN) {
- if (!empty($name)) {
- $name = Common::mb_strtolower($name);
- }
- if (!empty($keyword)) {
- $keyword = Common::mb_strtolower($keyword);
- }
- }
- }
- /**
- * @param $goal
- * @param $pattern_type
- * @param $url
- * @return bool
- * @throws \Exception
- */
- protected function isUrlMatchingGoal($goal, $pattern_type, $url)
- {
- switch ($pattern_type) {
- case 'regex':
- $pattern = $goal['pattern'];
- if (strpos($pattern, '/') !== false
- && strpos($pattern, '\\/') === false
- ) {
- $pattern = str_replace('/', '\\/', $pattern);
- }
- $pattern = '/' . $pattern . '/';
- if (!$goal['case_sensitive']) {
- $pattern .= 'i';
- }
- $match = (@preg_match($pattern, $url) == 1);
- break;
- case 'contains':
- if ($goal['case_sensitive']) {
- $matched = strpos($url, $goal['pattern']);
- } else {
- $matched = stripos($url, $goal['pattern']);
- }
- $match = ($matched !== false);
- break;
- case 'exact':
- if ($goal['case_sensitive']) {
- $matched = strcmp($goal['pattern'], $url);
- } else {
- $matched = strcasecmp($goal['pattern'], $url);
- }
- $match = ($matched == 0);
- break;
- default:
- throw new Exception(Piwik::translate('General_ExceptionInvalidGoalPattern', array($pattern_type)));
- break;
- }
- return $match;
- }
- }