forked from rebillar/site-accueil-insa
979 lines
36 KiB
PHP
979 lines
36 KiB
PHP
<?php
|
|
/**
|
|
* Matomo - free/libre analytics platform
|
|
*
|
|
* @link https://matomo.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\Container\StaticContainer;
|
|
use Piwik\Date;
|
|
use Piwik\Exception\InvalidRequestParameterException;
|
|
use Piwik\Piwik;
|
|
use Piwik\Plugin\Dimension\ConversionDimension;
|
|
use Piwik\Plugin\Dimension\VisitDimension;
|
|
use Piwik\Plugin\Manager;
|
|
use Piwik\Plugins\CustomVariables\CustomVariables;
|
|
use Piwik\Plugins\Events\Actions\ActionEvent;
|
|
use Piwik\Tracker\Visit\VisitProperties;
|
|
|
|
/**
|
|
*/
|
|
class GoalManager
|
|
{
|
|
// log_visit.visit_goal_buyer
|
|
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;
|
|
|
|
// 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;
|
|
|
|
public static $NUMERIC_MATCH_ATTRIBUTES = [
|
|
'visit_duration',
|
|
];
|
|
|
|
/**
|
|
* TODO: should remove this, but it is used by getGoalColumn which is used by dimensions. should replace w/ value object.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $currentGoal = array();
|
|
|
|
public function detectIsThereExistingCartInVisit($visitInformation)
|
|
{
|
|
if (empty($visitInformation['visit_goal_buyer'])) {
|
|
return false;
|
|
}
|
|
|
|
$goalBuyer = $visitInformation['visit_goal_buyer'];
|
|
$types = array(GoalManager::TYPE_BUYER_OPEN_CART, GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART);
|
|
|
|
// Was there a Cart for this visit prior to the order?
|
|
return in_array($goalBuyer, $types);
|
|
}
|
|
|
|
public static function getGoalDefinitions($idSite)
|
|
{
|
|
$websiteAttributes = Cache::getCacheWebsiteAttributes($idSite);
|
|
|
|
if (isset($websiteAttributes['goals'])) {
|
|
return $websiteAttributes['goals'];
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
public static function getGoalDefinition($idSite, $idGoal)
|
|
{
|
|
$goals = self::getGoalDefinitions($idSite);
|
|
|
|
foreach ($goals as $goal) {
|
|
if ($goal['idgoal'] == $idGoal) {
|
|
return $goal;
|
|
}
|
|
}
|
|
|
|
throw new Exception('Goal not found');
|
|
}
|
|
|
|
public static 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
|
|
* @param VisitProperties $visitor
|
|
* @param Request $request
|
|
* @throws Exception
|
|
* @return array[] Goals matched
|
|
*/
|
|
public function detectGoalsMatchingUrl($idSite, $action, VisitProperties $visitor, Request $request)
|
|
{
|
|
if (!Common::isGoalPluginEnabled()) {
|
|
return array();
|
|
}
|
|
|
|
$goals = $this->getGoalDefinitions($idSite);
|
|
|
|
$convertedGoals = array();
|
|
foreach ($goals as $goal) {
|
|
$convertedUrl = $this->detectGoalMatch($goal, $action, $visitor, $request);
|
|
if (!is_null($convertedUrl)) {
|
|
$convertedGoals[] = array('url' => $convertedUrl) + $goal;
|
|
}
|
|
}
|
|
return $convertedGoals;
|
|
}
|
|
|
|
/**
|
|
* Detects if an Action matches a given goal. If it does, the URL that triggered the goal
|
|
* is returned. Otherwise null is returned.
|
|
*
|
|
* @param array $goal
|
|
* @param Action $action
|
|
* @param VisitProperties $visitor
|
|
* @param Request $request
|
|
* @return bool|null if a goal is matched, a string of the Action URL is returned, or if no goal was matched it returns null
|
|
*/
|
|
public function detectGoalMatch($goal, Action $action, VisitProperties $visitor, Request $request)
|
|
{
|
|
$actionType = $action->getActionType();
|
|
|
|
$attribute = $goal['match_attribute'];
|
|
|
|
// handle numeric match attributes specifically
|
|
if (in_array($attribute, self::$NUMERIC_MATCH_ATTRIBUTES)) {
|
|
return $this->detectNumericGoalMatch($goal, $action, $visitor, $request);
|
|
}
|
|
|
|
// 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')
|
|
|| self::isEventMatchingGoal($goal) && $actionType != Action::TYPE_EVENT
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
switch ($attribute) {
|
|
case 'title':
|
|
// Matching on Page Title
|
|
$actionToMatch = $action->getActionName();
|
|
break;
|
|
case 'event_action':
|
|
$actionToMatch = $action->getEventAction();
|
|
break;
|
|
case 'event_name':
|
|
$actionToMatch = $action->getEventName();
|
|
break;
|
|
case 'event_category':
|
|
$actionToMatch = $action->getEventCategory();
|
|
break;
|
|
// url, external_website, file, manually...
|
|
default:
|
|
$actionToMatch = $action->getActionUrlRaw();
|
|
break;
|
|
}
|
|
|
|
$pattern_type = $goal['pattern_type'];
|
|
|
|
$match = $this->isUrlMatchingGoal($goal, $pattern_type, $actionToMatch);
|
|
if (!$match) {
|
|
return null;
|
|
}
|
|
|
|
return $action->getActionUrl();
|
|
}
|
|
|
|
private function detectNumericGoalMatch($goal, Action $action, VisitProperties $visitProperties, Request $request)
|
|
{
|
|
switch ($goal['match_attribute']) {
|
|
case 'visit_duration':
|
|
$firstActionTime = $visitProperties->getProperty('visit_first_action_time');
|
|
if (empty($firstActionTime)) {
|
|
return null;
|
|
}
|
|
|
|
$visitDurationInSecs = $request->getCurrentTimestamp() - ((int) $firstActionTime);
|
|
$valueToMatchAgainst = $visitDurationInSecs / 60;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
$pattern = (float) $goal['pattern'];
|
|
|
|
Common::printDebug("Matching {$goal['match_attribute']} (current value = $valueToMatchAgainst, idGoal = {$goal['idgoal']}) {$goal['pattern_type']} $pattern.");
|
|
|
|
switch ($goal['pattern_type']) {
|
|
case 'greater_than':
|
|
$matches = $valueToMatchAgainst > $pattern;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if ($matches) {
|
|
Common::printDebug("Conversion detected for idGoal = , idGoal = {$goal['idgoal']}.");
|
|
return $action->getActionUrl();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public function detectGoalId($idSite, Request $request)
|
|
{
|
|
if (!Common::isGoalPluginEnabled()) {
|
|
return null;
|
|
}
|
|
|
|
$idGoal = $request->getParam('idgoal');
|
|
|
|
$goals = $this->getGoalDefinitions($idSite);
|
|
|
|
if (!isset($goals[$idGoal])) {
|
|
throw new InvalidRequestParameterException('idGoal ' . $idGoal . ' does not exist');
|
|
}
|
|
|
|
$goal = $goals[$idGoal];
|
|
|
|
$url = $request->getParam('url');
|
|
$goal['url'] = PageUrl::excludeQueryParametersFromUrl($url, $idSite);
|
|
return $goal;
|
|
}
|
|
|
|
/**
|
|
* Records one or several goals matched in this request.
|
|
*
|
|
* @param Visitor $visitor
|
|
* @param array $visitorInformation
|
|
* @param array $visitCustomVariables
|
|
* @param Action $action
|
|
*/
|
|
public function recordGoals(VisitProperties $visitProperties, Request $request)
|
|
{
|
|
$visitorInformation = $visitProperties->getProperties();
|
|
|
|
/** @var Action $action */
|
|
$action = $request->getMetadata('Actions', 'action');
|
|
|
|
$goal = $this->getGoalFromVisitor($visitProperties, $request, $action);
|
|
|
|
if (Manager::getInstance()->isPluginActivated('CustomVariables')) {
|
|
// @todo move this to CustomVariables plugin if possible
|
|
// Copy Custom Variables from Visit row to the Goal conversion
|
|
// Otherwise, set the Custom Variables found in the cookie sent with this request
|
|
$visitCustomVariables = $request->getMetadata('CustomVariables', 'visitCustomVariables') ?: array();
|
|
$goal += $visitCustomVariables;
|
|
$maxCustomVariables = CustomVariables::getNumUsableCustomVariables();
|
|
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
|
|
// some goals are converted, so must be ecommerce Order or Cart Update
|
|
$isRequestEcommerce = $request->getMetadata('Ecommerce', 'isRequestEcommerce');
|
|
if ($isRequestEcommerce) {
|
|
$this->recordEcommerceGoal($visitProperties, $request, $goal, $action);
|
|
} else {
|
|
$this->recordStandardGoals($visitProperties, $request, $goal, $action);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
$revenue = round($revenue, self::REVENUE_PRECISION);
|
|
}
|
|
|
|
$revenue = Common::forceDotAsSeparatorForDecimalPoint($revenue);
|
|
|
|
return $revenue;
|
|
}
|
|
|
|
/**
|
|
* 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 Visitor $visitor
|
|
* @param Action $action
|
|
* @param array $visitInformation
|
|
*/
|
|
protected function recordEcommerceGoal(VisitProperties $visitProperties, Request $request, $conversion, $action)
|
|
{
|
|
$isThereExistingCartInVisit = $request->getMetadata('Goals', 'isThereExistingCartInVisit');
|
|
if ($isThereExistingCartInVisit) {
|
|
Common::printDebug("There is an existing cart for this visit");
|
|
}
|
|
|
|
$visitor = Visitor::makeFromVisitProperties($visitProperties, $request);
|
|
|
|
$isGoalAnOrder = $request->getMetadata('Ecommerce', 'isGoalAnOrder');
|
|
if ($isGoalAnOrder) {
|
|
$debugMessage = 'The conversion is an Ecommerce order';
|
|
|
|
$orderId = $request->getParam('ec_id');
|
|
|
|
$conversion['idorder'] = $orderId;
|
|
$conversion['idgoal'] = self::IDGOAL_ORDER;
|
|
$conversion['buster'] = Common::hashStringToInt($orderId);
|
|
|
|
$conversionDimensions = ConversionDimension::getAllDimensions();
|
|
$conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onEcommerceOrderConversion', $visitor, $action, $conversion);
|
|
} // If Cart update, select current items in the previous Cart
|
|
else {
|
|
$debugMessage = 'The conversion is an Ecommerce Cart Update';
|
|
|
|
$conversion['buster'] = 0;
|
|
$conversion['idgoal'] = self::IDGOAL_CART;
|
|
|
|
$conversionDimensions = ConversionDimension::getAllDimensions();
|
|
$conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onEcommerceCartUpdateConversion', $visitor, $action, $conversion);
|
|
}
|
|
|
|
Common::printDebug($debugMessage . ':' . var_export($conversion, true));
|
|
|
|
// INSERT or Sync items in the Cart / Order for this visit & order
|
|
$items = $this->getEcommerceItemsFromRequest($request);
|
|
|
|
if (false === $items) {
|
|
return;
|
|
}
|
|
|
|
$itemsCount = 0;
|
|
foreach ($items as $item) {
|
|
$itemsCount += $item[GoalManager::INTERNAL_ITEM_QUANTITY];
|
|
}
|
|
|
|
$conversion['items'] = $itemsCount;
|
|
|
|
if ($isThereExistingCartInVisit) {
|
|
$recorded = $this->getModel()->updateConversion(
|
|
$visitProperties->getProperty('idvisit'), self::IDGOAL_CART, $conversion);
|
|
} else {
|
|
$recorded = $this->insertNewConversion($conversion, $visitProperties->getProperties(), $request, $action);
|
|
}
|
|
|
|
if ($recorded) {
|
|
$this->recordEcommerceItems($conversion, $items);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns Items read from the request string
|
|
* @return array|bool
|
|
*/
|
|
private function getEcommerceItemsFromRequest(Request $request)
|
|
{
|
|
$items = $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();
|
|
}
|
|
|
|
if (!is_array($items)) {
|
|
Common::printDebug("Error while json_decode the Ecommerce items = " . var_export($items, true));
|
|
return false;
|
|
}
|
|
|
|
$items = Common::unsanitizeInputValues($items);
|
|
|
|
$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;
|
|
}
|
|
|
|
$itemsInDb = $this->getModel()->getAllItemsCurrentlyInTheCart($goal, self::ITEM_IDORDER_ABANDONED_CART);
|
|
|
|
// 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);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
private 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_check, $name_check, $category, $price, $quantity) = $item;
|
|
$sku = is_array($sku_check) ? join(',', $sku_check) : $sku_check;
|
|
$actionsToLookup[] = array(trim($sku), Action::TYPE_ECOMMERCE_ITEM_SKU);
|
|
$name = is_array($name_check) ? join(',', $name_check) : $name_check;
|
|
$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);
|
|
|
|
$this->getModel()->updateEcommerceItem($item['idorder_original_value'], $newRow);
|
|
}
|
|
}
|
|
|
|
private function getModel()
|
|
{
|
|
return new Model();
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
$items = array();
|
|
|
|
foreach ($itemsToInsert as $item) {
|
|
$items[] = $this->getItemRowEnriched($goal, $item);
|
|
}
|
|
|
|
$this->getModel()->createEcommerceItems($items);
|
|
}
|
|
|
|
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' => Common::forceDotAsSeparatorForDecimalPoint($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;
|
|
}
|
|
|
|
public function getGoalColumn($column)
|
|
{
|
|
if (array_key_exists($column, $this->currentGoal)) {
|
|
return $this->currentGoal[$column];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Records a standard non-Ecommerce goal in the DB (URL/Title matching),
|
|
* linking the conversion to the action that triggered it
|
|
* @param $goal
|
|
* @param Visitor $visitor
|
|
* @param Action $action
|
|
* @param $visitorInformation
|
|
*/
|
|
protected function recordStandardGoals(VisitProperties $visitProperties, Request $request, $goal, $action)
|
|
{
|
|
$visitor = Visitor::makeFromVisitProperties($visitProperties, $request);
|
|
|
|
$convertedGoals = $request->getMetadata('Goals', 'goalsConverted') ?: array();
|
|
foreach ($convertedGoals as $convertedGoal) {
|
|
$this->currentGoal = $convertedGoal;
|
|
Common::printDebug("- Goal " . $convertedGoal['idgoal'] . " matched. Recording...");
|
|
$conversion = $goal;
|
|
$conversion['idgoal'] = $convertedGoal['idgoal'];
|
|
$conversion['url'] = $convertedGoal['url'];
|
|
|
|
if (!is_null($action)) {
|
|
$conversion['idaction_url'] = $action->getIdActionUrl();
|
|
$conversion['idlink_va'] = $action->getIdLinkVisitAction();
|
|
}
|
|
|
|
// If multiple Goal conversions per visit, set a cache buster
|
|
if ($convertedGoal['allow_multiple'] == 0) {
|
|
$conversion['buster'] = 0;
|
|
} else {
|
|
$lastActionTime = $visitProperties->getProperty('visit_last_action_time');
|
|
if (empty($lastActionTime)) {
|
|
$conversion['buster'] = $this->makeRandomMySqlUnsignedInt(10);
|
|
} else {
|
|
$conversion['buster'] = $this->makeRandomMySqlUnsignedInt(2) . mb_substr($visitProperties->getProperty('visit_last_action_time'), 2);
|
|
}
|
|
}
|
|
|
|
$conversionDimensions = ConversionDimension::getAllDimensions();
|
|
$conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onGoalConversion', $visitor, $action, $conversion);
|
|
|
|
$this->insertNewConversion($conversion, $visitProperties->getProperties(), $request, $action, $convertedGoal);
|
|
}
|
|
}
|
|
|
|
private function makeRandomMySqlUnsignedInt($length)
|
|
{
|
|
// mysql int unsgined max value is 4294967295 so we want to allow max 39999...
|
|
$randomInt = Common::getRandomString(1, '123');
|
|
$randomInt .= Common::getRandomString($length - 1, '0123456789');
|
|
return $randomInt;
|
|
}
|
|
|
|
/**
|
|
* Helper function used by other record* methods which will INSERT or UPDATE the conversion in the DB
|
|
*
|
|
* @param array $conversion
|
|
* @param array $visitInformation
|
|
* @param Request $request
|
|
* @param Action|null $action
|
|
* @return bool
|
|
*/
|
|
protected function insertNewConversion($conversion, $visitInformation, Request $request, $action, $convertedGoal = null)
|
|
{
|
|
/**
|
|
* 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.
|
|
*
|
|
* This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead.
|
|
*
|
|
* @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.
|
|
* @param Action|null $action An action object like ActionPageView or ActionDownload, or null if no action is
|
|
* supposed to be processed.
|
|
* @deprecated
|
|
* @ignore
|
|
*/
|
|
Piwik::postEvent('Tracker.newConversionInformation', array(&$conversion, $visitInformation, $request, $action));
|
|
|
|
if (!empty($convertedGoal)
|
|
&& $this->isEventMatchingGoal($convertedGoal)
|
|
&& !empty($convertedGoal['event_value_as_revenue'])
|
|
) {
|
|
$eventValue = ActionEvent::getEventValue($request);
|
|
if ($eventValue != '') {
|
|
$conversion['revenue'] = $eventValue;
|
|
}
|
|
}
|
|
|
|
$newGoalDebug = $conversion;
|
|
$newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']);
|
|
Common::printDebug($newGoalDebug);
|
|
|
|
$idorder = $request->getParam('ec_id');
|
|
|
|
$wasInserted = $this->getModel()->createConversion($conversion);
|
|
if (!$wasInserted
|
|
&& !empty($idorder)
|
|
) {
|
|
$idSite = $request->getIdSite();
|
|
throw new InvalidRequestParameterException("Invalid non-unique idsite/idorder combination ($idSite, $idorder), conversion was not inserted.");
|
|
}
|
|
|
|
return $wasInserted;
|
|
}
|
|
|
|
/**
|
|
* 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],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param $goal
|
|
* @param $pattern_type
|
|
* @param $url
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
protected function isUrlMatchingGoal($goal, $pattern_type, $url)
|
|
{
|
|
$url = Common::unsanitizeInputValue($url);
|
|
$goal['pattern'] = Common::unsanitizeInputValue($goal['pattern']);
|
|
|
|
$match = $this->isGoalPatternMatchingUrl($goal, $pattern_type, $url);
|
|
|
|
if (!$match) {
|
|
// Users may set Goal matching URL as URL encoded
|
|
$goal['pattern'] = urldecode($goal['pattern']);
|
|
|
|
$match = $this->isGoalPatternMatchingUrl($goal, $pattern_type, $url);
|
|
}
|
|
return $match;
|
|
}
|
|
|
|
/**
|
|
* @param ConversionDimension[] $dimensions
|
|
* @param string $hook
|
|
* @param Visitor $visitor
|
|
* @param Action|null $action
|
|
* @param array|null $valuesToUpdate If null, $this->visitorInfo will be updated
|
|
*
|
|
* @return array|null The updated $valuesToUpdate or null if no $valuesToUpdate given
|
|
*/
|
|
private function triggerHookOnDimensions(Request $request, $dimensions, $hook, $visitor, $action, $valuesToUpdate)
|
|
{
|
|
foreach ($dimensions as $dimension) {
|
|
$value = $dimension->$hook($request, $visitor, $action, $this);
|
|
|
|
if (false !== $value) {
|
|
if (is_float($value)) {
|
|
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
|
|
}
|
|
|
|
$fieldName = $dimension->getColumnName();
|
|
$visitor->setVisitorColumn($fieldName, $value);
|
|
|
|
$valuesToUpdate[$fieldName] = $value;
|
|
}
|
|
}
|
|
|
|
return $valuesToUpdate;
|
|
}
|
|
|
|
private function getGoalFromVisitor(VisitProperties $visitProperties, Request $request, $action)
|
|
{
|
|
$lastVisitTime = $visitProperties->getProperty('visit_last_action_time');
|
|
if (!$lastVisitTime) {
|
|
$lastVisitTime = $request->getCurrentTimestamp(); // fallback in case visit_last_action_time is not set
|
|
}
|
|
|
|
if (!empty($lastVisitTime) && is_numeric($lastVisitTime)) {
|
|
// visit last action time might be 2020-05-05 00:00:00
|
|
// we want it to prevent this being converted to a timestamp of 2020
|
|
// resulting in some day in 1970
|
|
$lastVisitTime = Date::getDatetimeFromTimestamp($lastVisitTime);
|
|
}
|
|
|
|
$goal = array(
|
|
'idvisit' => $visitProperties->getProperty('idvisit'),
|
|
'idvisitor' => $visitProperties->getProperty('idvisitor'),
|
|
'server_time' => $lastVisitTime,
|
|
);
|
|
|
|
$visitDimensions = VisitDimension::getAllDimensions();
|
|
|
|
$visit = Visitor::makeFromVisitProperties($visitProperties, $request);
|
|
foreach ($visitDimensions as $dimension) {
|
|
$value = $dimension->onAnyGoalConversion($request, $visit, $action);
|
|
if (false !== $value) {
|
|
$goal[$dimension->getColumnName()] = $value;
|
|
}
|
|
}
|
|
|
|
return $goal;
|
|
}
|
|
|
|
/**
|
|
* @param $goal
|
|
* @param $pattern_type
|
|
* @param $url
|
|
* @return bool
|
|
*/
|
|
protected function isGoalPatternMatchingUrl($goal, $pattern_type, $url)
|
|
{
|
|
switch ($pattern_type) {
|
|
case 'regex':
|
|
$pattern = self::formatRegex($goal['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:
|
|
try {
|
|
StaticContainer::get('Psr\Log\LoggerInterface')->warning(Piwik::translate('General_ExceptionInvalidGoalPattern', array($pattern_type)));
|
|
} catch (\Exception $e) {
|
|
}
|
|
$match = false;
|
|
break;
|
|
}
|
|
return $match;
|
|
}
|
|
|
|
/**
|
|
* Formats a goal regex pattern to a usable regex
|
|
*
|
|
* @param string $pattern
|
|
* @return string
|
|
*/
|
|
public static function formatRegex($pattern)
|
|
{
|
|
if (strpos($pattern, '/') !== false
|
|
&& strpos($pattern, '\\/') === false
|
|
) {
|
|
$pattern = str_replace('/', '\\/', $pattern);
|
|
}
|
|
return '/' . $pattern . '/';
|
|
}
|
|
|
|
public static function isEventMatchingGoal($goal)
|
|
{
|
|
return in_array($goal['match_attribute'], array('event_action', 'event_name', 'event_category'));
|
|
}
|
|
}
|