PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.3.1
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Tracker / Model.php
matomo / app / core / Tracker Last commit date
Db 6 years ago Handler 6 years ago TableLogAction 6 years ago Visit 6 years ago Action.php 6 years ago ActionPageview.php 6 years ago Cache.php 6 years ago Db.php 6 years ago Failures.php 6 years ago FingerprintSalt.php 6 years ago GoalManager.php 6 years ago Handler.php 6 years ago IgnoreCookie.php 6 years ago LogTable.php 6 years ago Model.php 6 years ago PageUrl.php 6 years ago Request.php 5 years ago RequestProcessor.php 6 years ago RequestSet.php 6 years ago Response.php 6 years ago ScheduledTasksRunner.php 6 years ago Settings.php 5 years ago TableLogAction.php 6 years ago TrackerCodeGenerator.php 6 years ago TrackerConfig.php 6 years ago Visit.php 5 years ago VisitExcluded.php 6 years ago VisitInterface.php 6 years ago Visitor.php 6 years ago VisitorNotFoundInDb.php 6 years ago VisitorRecognizer.php 6 years ago
Model.php
508 lines
1 <?php
2 /**
3 * Piwik - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9 namespace Piwik\Tracker;
10
11 use Exception;
12 use Piwik\Common;
13 use Piwik\Container\StaticContainer;
14 use Piwik\Tracker;
15 use Psr\Log\LoggerInterface;
16
17 class Model
18 {
19 const CACHE_KEY_INDEX_IDSITE_IDVISITOR = 'log_visit_has_index_idsite_idvisitor';
20
21 public function createAction($visitAction)
22 {
23 $fields = implode(", ", array_keys($visitAction));
24 $values = Common::getSqlStringFieldsArray($visitAction);
25 $table = Common::prefixTable('log_link_visit_action');
26
27 $sql = "INSERT INTO $table ($fields) VALUES ($values)";
28 $bind = array_values($visitAction);
29
30 $db = $this->getDb();
31 $db->query($sql, $bind);
32
33 $id = $db->lastInsertId();
34
35 return $id;
36 }
37
38 public function createConversion($conversion)
39 {
40 $fields = implode(", ", array_keys($conversion));
41 $bindFields = Common::getSqlStringFieldsArray($conversion);
42 $table = Common::prefixTable('log_conversion');
43
44 $sql = "INSERT IGNORE INTO $table ($fields) VALUES ($bindFields) ";
45 $bind = array_values($conversion);
46
47 $db = $this->getDb();
48 $result = $db->query($sql, $bind);
49
50 // If a record was inserted, we return true
51 return $db->rowCount($result) > 0;
52 }
53
54 public function updateConversion($idVisit, $idGoal, $newConversion)
55 {
56 $updateWhere = array(
57 'idvisit' => $idVisit,
58 'idgoal' => $idGoal,
59 'buster' => 0,
60 );
61
62 $updateParts = $sqlBind = $updateWhereParts = array();
63
64 foreach ($newConversion as $name => $value) {
65 $updateParts[] = $name . " = ?";
66 $sqlBind[] = $value;
67 }
68
69 foreach ($updateWhere as $name => $value) {
70 $updateWhereParts[] = $name . " = ?";
71 $sqlBind[] = $value;
72 }
73
74 $parts = implode(', ', $updateParts);
75 $table = Common::prefixTable('log_conversion');
76
77 $sql = "UPDATE $table SET $parts WHERE " . implode(' AND ', $updateWhereParts);
78
79 try {
80 $this->getDb()->query($sql, $sqlBind);
81 } catch (Exception $e) {
82 StaticContainer::get(LoggerInterface::class)->error("There was an error while updating the Conversion: {exception}", [
83 'exception' => $e,
84 ]);
85
86 return false;
87 }
88
89 return true;
90 }
91
92
93 /**
94 * Loads the Ecommerce items from the request and records them in the DB
95 *
96 * @param array $goal
97 * @param int $defaultIdOrder
98 * @throws Exception
99 * @return array
100 */
101 public function getAllItemsCurrentlyInTheCart($goal, $defaultIdOrder)
102 {
103 $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value
104 FROM " . Common::prefixTable('log_conversion_item') . "
105 WHERE idvisit = ? AND (idorder = ? OR idorder = ?)";
106
107 $bind = array(
108 $goal['idvisit'],
109 isset($goal['idorder']) ? $goal['idorder'] : $defaultIdOrder,
110 $defaultIdOrder
111 );
112
113 $itemsInDb = $this->getDb()->fetchAll($sql, $bind);
114
115 Common::printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true));
116 Common::printDebug($itemsInDb);
117
118 return $itemsInDb;
119 }
120
121 public function createEcommerceItems($ecommerceItems)
122 {
123 $sql = "INSERT IGNORE INTO " . Common::prefixTable('log_conversion_item');
124 $i = 0;
125 $bind = array();
126
127 foreach ($ecommerceItems as $item) {
128 if ($i === 0) {
129 $fields = implode(', ', array_keys($item));
130 $sql .= ' (' . $fields . ') VALUES ';
131 } elseif ($i > 0) {
132 $sql .= ',';
133 }
134
135 $newRow = array_values($item);
136 $sql .= " ( " . Common::getSqlStringFieldsArray($newRow) . " ) ";
137 $bind = array_merge($bind, $newRow);
138 $i++;
139 }
140
141 Common::printDebug($sql);
142 Common::printDebug($bind);
143
144 try {
145 $this->getDb()->query($sql, $bind);
146 } catch (Exception $e) {
147 if ($e->getCode() == 23000 ||
148 false !== strpos($e->getMessage(), 'Duplicate entry') ||
149 false !== strpos($e->getMessage(), 'Integrity constraint violation')) {
150 Common::printDebug('Did not create ecommerce item as item was already created');
151 } else {
152 throw $e;
153 }
154 }
155 }
156
157 /**
158 * Inserts a new action into the log_action table. If there is an existing action that was inserted
159 * due to another request pre-empting this one, the newly inserted action is deleted.
160 *
161 * @param string $name
162 * @param int $type
163 * @param int $urlPrefix
164 * @return int The ID of the action (can be for an existing action or new action).
165 */
166 public function createNewIdAction($name, $type, $urlPrefix)
167 {
168 $newActionId = $this->insertNewAction($name, $type, $urlPrefix);
169
170 $realFirstActionId = $this->getIdActionMatchingNameAndType($name, $type);
171
172 // if the inserted action ID is not the same as the queried action ID, then that means we inserted
173 // a duplicate, so remove it now
174 if ($realFirstActionId != $newActionId) {
175 $this->deleteDuplicateAction($newActionId);
176 }
177
178 return $realFirstActionId;
179 }
180
181 private function insertNewAction($name, $type, $urlPrefix)
182 {
183 $table = Common::prefixTable('log_action');
184 $sql = "INSERT INTO $table (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)";
185
186 $db = $this->getDb();
187 $db->query($sql, array($name, $name, $type, $urlPrefix));
188
189 $actionId = $db->lastInsertId();
190
191 return $actionId;
192 }
193
194 private function getSqlSelectActionId()
195 {
196 // it is possible for multiple actions to exist in the DB (due to rare concurrency issues), so the ORDER BY and
197 // LIMIT are important
198 $sql = "SELECT idaction, type, name FROM " . Common::prefixTable('log_action')
199 . " WHERE " . $this->getSqlConditionToMatchSingleAction() . " "
200 . "ORDER BY idaction ASC LIMIT 1";
201
202 return $sql;
203 }
204
205 public function getIdActionMatchingNameAndType($name, $type)
206 {
207 $sql = $this->getSqlSelectActionId();
208 $bind = array($name, $name, $type);
209
210 $idAction = $this->getDb()->fetchOne($sql, $bind);
211
212 return $idAction;
213 }
214
215 /**
216 * Returns the IDs for multiple actions based on name + type values.
217 *
218 * @param array $actionsNameAndType Array like `array( array('name' => '...', 'type' => 1), ... )`
219 * @return array|false Array of DB rows w/ columns: **idaction**, **type**, **name**.
220 */
221 public function getIdsAction($actionsNameAndType)
222 {
223 $sql = "SELECT `idaction`, `type`, `name` FROM " . Common::prefixTable('log_action') . " WHERE";
224 $bind = array();
225
226 $i = 0;
227 foreach ($actionsNameAndType as $actionNameType) {
228 $name = $actionNameType['name'];
229
230 if (empty($name)) {
231 continue;
232 }
233
234 if ($i > 0) {
235 $sql .= " OR";
236 }
237
238 $sql .= " " . $this->getSqlConditionToMatchSingleAction() . " ";
239
240 $bind[] = $name;
241 $bind[] = $name;
242 $bind[] = $actionNameType['type'];
243 $i++;
244 }
245
246 // Case URL & Title are empty
247 if (empty($bind)) {
248 return false;
249 }
250
251 $rows = $this->getDb()->fetchAll($sql, $bind);
252
253 $actionsPerType = array();
254
255 foreach ($rows as $row) {
256 $name = $row['name'];
257 $type = $row['type'];
258
259 if (!isset($actionsPerType[$type])) {
260 $actionsPerType[$type] = array();
261 }
262
263 if (!isset($actionsPerType[$type][$name])) {
264 $actionsPerType[$type][$name] = $row;
265 } elseif ($row['idaction'] < $actionsPerType[$type][$name]['idaction']) {
266 // keep the lowest idaction for this type, name
267 $actionsPerType[$type][$name] = $row;
268 }
269 }
270
271 $actionsToReturn = array();
272 foreach ($actionsPerType as $type => $actionsPerName) {
273 foreach ($actionsPerName as $actionPerName) {
274 $actionsToReturn[] = $actionPerName;
275 }
276 }
277
278 return $actionsToReturn;
279 }
280
281 public function updateEcommerceItem($originalIdOrder, $newItem)
282 {
283 $updateParts = $sqlBind = array();
284 foreach ($newItem as $name => $value) {
285 $updateParts[] = $name . " = ?";
286 $sqlBind[] = $value;
287 }
288
289 $parts = implode(', ', $updateParts);
290 $table = Common::prefixTable('log_conversion_item');
291
292 $sql = "UPDATE $table SET $parts WHERE idvisit = ? AND idorder = ? AND idaction_sku = ?";
293
294 $sqlBind[] = $newItem['idvisit'];
295 $sqlBind[] = $originalIdOrder;
296 $sqlBind[] = $newItem['idaction_sku'];
297
298 $this->getDb()->query($sql, $sqlBind);
299 }
300
301 public function createVisit($visit)
302 {
303 $fields = array_keys($visit);
304 $fields = implode(", ", $fields);
305 $values = Common::getSqlStringFieldsArray($visit);
306 $table = Common::prefixTable('log_visit');
307
308 $sql = "INSERT INTO $table ($fields) VALUES ($values)";
309 $bind = array_values($visit);
310
311 $db = $this->getDb();
312 $db->query($sql, $bind);
313
314 return $db->lastInsertId();
315 }
316
317 public function updateVisit($idSite, $idVisit, $valuesToUpdate)
318 {
319 list($updateParts, $sqlBind) = $this->fieldsToQuery($valuesToUpdate);
320
321 $parts = implode(', ',$updateParts);
322 $table = Common::prefixTable('log_visit');
323
324 $sqlQuery = "UPDATE $table SET $parts WHERE idsite = ? AND idvisit = ?";
325
326 $sqlBind[] = $idSite;
327 $sqlBind[] = $idVisit;
328
329 $db = $this->getDb();
330 $result = $db->query($sqlQuery, $sqlBind);
331 $wasInserted = $db->rowCount($result) != 0;
332
333 if (!$wasInserted) {
334 Common::printDebug("Visitor with this idvisit wasn't found in the DB.");
335 Common::printDebug("$sqlQuery --- ");
336 Common::printDebug($sqlBind);
337 }
338
339 return $wasInserted;
340 }
341
342 public function updateAction($idLinkVa, $valuesToUpdate)
343 {
344 if (empty($idLinkVa)) {
345 return;
346 }
347
348 list($updateParts, $sqlBind) = $this->fieldsToQuery($valuesToUpdate);
349
350 $parts = implode(', ', $updateParts);
351 $table = Common::prefixTable('log_link_visit_action');
352
353 $sqlQuery = "UPDATE $table SET $parts WHERE idlink_va = ?";
354
355 $sqlBind[] = $idLinkVa;
356
357 $db = $this->getDb();
358 $result = $db->query($sqlQuery, $sqlBind);
359 $wasInserted = $db->rowCount($result) != 0;
360
361 if (!$wasInserted) {
362 Common::printDebug("Action with this idLinkVa wasn't found in the DB.");
363 Common::printDebug("$sqlQuery --- ");
364 Common::printDebug($sqlBind);
365 }
366
367 return $wasInserted;
368 }
369
370 public function findVisitor($idSite, $configId, $idVisitor, $userId, $fieldsToRead, $shouldMatchOneFieldOnly, $isVisitorIdToLookup, $timeLookBack, $timeLookAhead)
371 {
372 $selectCustomVariables = '';
373
374 $selectFields = implode(', ', $fieldsToRead);
375
376 $select = "SELECT $selectFields $selectCustomVariables ";
377 $from = "FROM " . Common::prefixTable('log_visit');
378
379 // Two use cases:
380 // 1) there is no visitor ID so we try to match only on config_id (heuristics)
381 // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed,
382 // importing server access logs with import_logs.py, etc.
383 // In this case we use config_id heuristics to try find the visitor in tahhhe past. There is a risk to assign
384 // this page view to the wrong visitor, but this is better than creating artificial visits.
385 // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API),
386 // and in these cases, we force to look up this visitor id
387 $configIdWhere = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?";
388 $configIdbindSql = array(
389 $timeLookBack,
390 $timeLookAhead,
391 $idSite
392 );
393
394 $visitorIdWhere = 'idsite = ? AND visit_last_action_time <= ?';
395 $visitorIdbindSql = [$idSite, $timeLookAhead];
396
397 if ($shouldMatchOneFieldOnly && $isVisitorIdToLookup) {
398 $visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $visitorIdWhere, $visitorIdbindSql);
399 } elseif ($shouldMatchOneFieldOnly) {
400 $visitRow = $this->findVisitorByConfigId($configId, $select, $from, $configIdWhere, $configIdbindSql);
401 } else {
402 if (!empty($idVisitor)) {
403 $visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $visitorIdWhere, $visitorIdbindSql);
404 } else {
405 $visitRow = false;
406 }
407
408 if (empty($visitRow)) {
409 if (!empty($userId)) {
410 $configIdWhere .= ' AND ( user_id IS NULL OR user_id = ? )';
411 $configIdbindSql[] = $userId;
412 }
413 $visitRow = $this->findVisitorByConfigId($configId, $select, $from, $configIdWhere, $configIdbindSql);
414 }
415 }
416
417 return $visitRow;
418 }
419
420 private function findVisitorByVisitorId($idVisitor, $select, $from, $where, $bindSql)
421 {
422 $cache = Cache::getCacheGeneral();
423
424 // use INDEX index_idsite_idvisitor (idsite, idvisitor) if available
425 if (array_key_exists(self::CACHE_KEY_INDEX_IDSITE_IDVISITOR, $cache) && true === $cache[self::CACHE_KEY_INDEX_IDSITE_IDVISITOR]) {
426 $from .= ' FORCE INDEX (index_idsite_idvisitor) ';
427 }
428
429 $where .= ' AND idvisitor = ?';
430 $bindSql[] = $idVisitor;
431
432 return $this->fetchVisitor($select, $from, $where, $bindSql);
433 }
434
435 private function findVisitorByConfigId($configId, $select, $from, $where, $bindSql)
436 {
437 // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time)
438 $where .= ' AND config_id = ?';
439 $bindSql[] = $configId;
440
441 return $this->fetchVisitor($select, $from, $where, $bindSql);
442 }
443
444 private function fetchVisitor($select, $from, $where, $bindSql)
445 {
446 $sql = "$select $from WHERE " . $where . "
447 ORDER BY visit_last_action_time DESC
448 LIMIT 1";
449
450 $visitRow = $this->getDb()->fetch($sql, $bindSql);
451
452 return $visitRow;
453 }
454
455 /**
456 * Returns true if the site doesn't have raw data.
457 *
458 * @param int $siteId
459 * @return bool
460 */
461 public function isSiteEmpty($siteId)
462 {
463 $sql = sprintf('SELECT idsite FROM %s WHERE idsite = ? limit 1', Common::prefixTable('log_visit'));
464
465 $result = \Piwik\Db::fetchOne($sql, array($siteId));
466
467 return $result == null;
468 }
469
470 private function fieldsToQuery($valuesToUpdate)
471 {
472 $updateParts = array();
473 $sqlBind = array();
474
475 foreach ($valuesToUpdate as $name => $value) {
476 // Case where bind parameters don't work
477 if ($value === $name . ' + 1') {
478 //$name = 'visit_total_events'
479 //$value = 'visit_total_events + 1';
480 $updateParts[] = " $name = $value ";
481 } else {
482 $updateParts[] = $name . " = ?";
483 $sqlBind[] = $value;
484 }
485 }
486
487 return array($updateParts, $sqlBind);
488 }
489
490 private function deleteDuplicateAction($newActionId)
491 {
492 $sql = "DELETE FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?";
493
494 $db = $this->getDb();
495 $db->query($sql, array($newActionId));
496 }
497
498 private function getDb()
499 {
500 return Tracker::getDatabase();
501 }
502
503 private function getSqlConditionToMatchSingleAction()
504 {
505 return "( hash = CRC32(?) AND name = ? AND type = ? )";
506 }
507 }
508