PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.3.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.3.0
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 5 years ago Handler 5 years ago TableLogAction 5 years ago Visit 5 years ago Action.php 5 years ago ActionPageview.php 5 years ago Cache.php 5 years ago Db.php 5 years ago Failures.php 6 years ago FingerprintSalt.php 6 years ago GoalManager.php 5 years ago Handler.php 5 years ago IgnoreCookie.php 5 years ago LogTable.php 5 years ago Model.php 5 years ago PageUrl.php 5 years ago Request.php 5 years ago RequestProcessor.php 5 years ago RequestSet.php 5 years ago Response.php 5 years ago ScheduledTasksRunner.php 5 years ago Settings.php 5 years ago TableLogAction.php 5 years ago TrackerCodeGenerator.php 5 years ago TrackerConfig.php 5 years ago Visit.php 5 years ago VisitExcluded.php 5 years ago VisitInterface.php 5 years ago Visitor.php 5 years ago VisitorNotFoundInDb.php 5 years ago VisitorRecognizer.php 5 years ago
Model.php
516 lines
1 <?php
2 /**
3 * Matomo - 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 $selectFields = implode(', ', $fieldsToRead);
373
374 $select = "SELECT $selectFields ";
375 $from = "FROM " . Common::prefixTable('log_visit');
376
377 // Two use cases:
378 // 1) there is no visitor ID so we try to match only on config_id (heuristics)
379 // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed,
380 // importing server access logs with import_logs.py, etc.
381 // In this case we use config_id heuristics to try find the visitor in tahhhe past. There is a risk to assign
382 // this page view to the wrong visitor, but this is better than creating artificial visits.
383 // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API),
384 // and in these cases, we force to look up this visitor id
385 $configIdWhere = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?";
386 $configIdbindSql = array(
387 $timeLookBack,
388 $timeLookAhead,
389 $idSite
390 );
391
392 $visitorIdWhere = 'idsite = ? AND visit_last_action_time <= ?';
393 $visitorIdbindSql = [$idSite, $timeLookAhead];
394
395 if ($shouldMatchOneFieldOnly && $isVisitorIdToLookup) {
396 $visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $visitorIdWhere, $visitorIdbindSql);
397 } elseif ($shouldMatchOneFieldOnly) {
398 $visitRow = $this->findVisitorByConfigId($configId, $select, $from, $configIdWhere, $configIdbindSql);
399 } else {
400 if (!empty($idVisitor)) {
401 $visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $visitorIdWhere, $visitorIdbindSql);
402 } else {
403 $visitRow = false;
404 }
405
406 if (empty($visitRow)) {
407 if (!empty($userId)) {
408 $configIdWhere .= ' AND ( user_id IS NULL OR user_id = ? )';
409 $configIdbindSql[] = $userId;
410 }
411 $visitRow = $this->findVisitorByConfigId($configId, $select, $from, $configIdWhere, $configIdbindSql);
412 }
413 }
414
415 return $visitRow;
416 }
417
418 public function hasVisit($idSite, $idVisit)
419 {
420 // will use INDEX index_idsite_idvisitor (idsite, idvisitor)
421 $sql = 'SELECT idsite FROM ' . Common::prefixTable('log_visit') . ' WHERE idvisit = ? LIMIT 1';
422 $bindSql = array($idVisit);
423
424 $val = $this->getDb()->fetchOne($sql, $bindSql);
425 return $val == $idSite;
426 }
427
428 private function findVisitorByVisitorId($idVisitor, $select, $from, $where, $bindSql)
429 {
430 $cache = Cache::getCacheGeneral();
431
432 // use INDEX index_idsite_idvisitor (idsite, idvisitor) if available
433 if (array_key_exists(self::CACHE_KEY_INDEX_IDSITE_IDVISITOR, $cache) && true === $cache[self::CACHE_KEY_INDEX_IDSITE_IDVISITOR]) {
434 $from .= ' FORCE INDEX (index_idsite_idvisitor) ';
435 }
436
437 $where .= ' AND idvisitor = ?';
438 $bindSql[] = $idVisitor;
439
440 return $this->fetchVisitor($select, $from, $where, $bindSql);
441 }
442
443 private function findVisitorByConfigId($configId, $select, $from, $where, $bindSql)
444 {
445 // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time)
446 $where .= ' AND config_id = ?';
447 $bindSql[] = $configId;
448
449 return $this->fetchVisitor($select, $from, $where, $bindSql);
450 }
451
452 private function fetchVisitor($select, $from, $where, $bindSql)
453 {
454 $sql = "$select $from WHERE " . $where . "
455 ORDER BY visit_last_action_time DESC
456 LIMIT 1";
457
458 $visitRow = $this->getDb()->fetch($sql, $bindSql);
459
460 return $visitRow;
461 }
462
463 /**
464 * Returns true if the site doesn't have raw data.
465 *
466 * @param int $siteId
467 * @return bool
468 */
469 public function isSiteEmpty($siteId)
470 {
471 $sql = sprintf('SELECT idsite FROM %s WHERE idsite = ? limit 1', Common::prefixTable('log_visit'));
472
473 $result = \Piwik\Db::fetchOne($sql, array($siteId));
474
475 return $result == null;
476 }
477
478 private function fieldsToQuery($valuesToUpdate)
479 {
480 $updateParts = array();
481 $sqlBind = array();
482
483 foreach ($valuesToUpdate as $name => $value) {
484 // Case where bind parameters don't work
485 if ($value === $name . ' + 1') {
486 //$name = 'visit_total_events'
487 //$value = 'visit_total_events + 1';
488 $updateParts[] = " $name = $value ";
489 } else {
490 $updateParts[] = $name . " = ?";
491 $sqlBind[] = $value;
492 }
493 }
494
495 return array($updateParts, $sqlBind);
496 }
497
498 private function deleteDuplicateAction($newActionId)
499 {
500 $sql = "DELETE FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?";
501
502 $db = $this->getDb();
503 $db->query($sql, array($newActionId));
504 }
505
506 private function getDb()
507 {
508 return Tracker::getDatabase();
509 }
510
511 private function getSqlConditionToMatchSingleAction()
512 {
513 return "( hash = CRC32(?) AND name = ? AND type = ? )";
514 }
515 }
516