PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.13.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.13.2
5.12.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 / DataAccess / RawLogDao.php
matomo / app / core / DataAccess Last commit date
LogQueryBuilder 3 years ago Actions.php 5 years ago ArchiveSelector.php 3 years ago ArchiveTableCreator.php 5 years ago ArchiveTableDao.php 5 years ago ArchiveWriter.php 3 years ago ArchivingDbAdapter.php 4 years ago LogAggregator.php 3 years ago LogQueryBuilder.php 5 years ago LogTableTemporary.php 5 years ago Model.php 3 years ago RawLogDao.php 4 years ago TableMetadata.php 5 years ago
RawLogDao.php
440 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\DataAccess;
10
11 use Piwik\Common;
12 use Piwik\Config as PiwikConfig;
13 use Piwik\Container\StaticContainer;
14 use Piwik\Db;
15 use Piwik\Plugin\Dimension\DimensionMetadataProvider;
16 use Piwik\Plugin\LogTablesProvider;
17
18 /**
19 * DAO that queries log tables.
20 */
21 class RawLogDao
22 {
23 const DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME = 'tmp_log_actions_to_keep';
24
25 /**
26 * @var DimensionMetadataProvider
27 */
28 private $dimensionMetadataProvider;
29
30 /**
31 * @var LogTablesProvider
32 */
33 private $logTablesProvider;
34
35 public function __construct(DimensionMetadataProvider $provider = null, LogTablesProvider $logTablesProvider = null)
36 {
37 $this->dimensionMetadataProvider = $provider ?: StaticContainer::get('Piwik\Plugin\Dimension\DimensionMetadataProvider');
38 $this->logTablesProvider = $logTablesProvider ?: StaticContainer::get('Piwik\Plugin\LogTablesProvider');
39 }
40
41 /**
42 * @param array $values
43 * @param string $idVisit
44 */
45 public function updateVisits(array $values, $idVisit)
46 {
47 $sql = "UPDATE " . Common::prefixTable('log_visit')
48 . " SET " . $this->getColumnSetExpressions(array_keys($values))
49 . " WHERE idvisit = ?";
50
51 $this->update($sql, $values, $idVisit);
52 }
53
54 /**
55 * @param array $values
56 * @param string $idVisit
57 */
58 public function updateConversions(array $values, $idVisit)
59 {
60 $sql = "UPDATE " . Common::prefixTable('log_conversion')
61 . " SET " . $this->getColumnSetExpressions(array_keys($values))
62 . " WHERE idvisit = ?";
63
64 $this->update($sql, $values, $idVisit);
65 }
66
67 /**
68 * @param string $from
69 * @param string $to
70 * @return int
71 */
72 public function countVisitsWithDatesLimit($from, $to)
73 {
74 $sql = "SELECT COUNT(*) AS num_rows"
75 . " FROM " . Common::prefixTable('log_visit')
76 . " WHERE visit_last_action_time >= ? AND visit_last_action_time < ?";
77
78 $bind = array($from, $to);
79
80 return (int) Db::fetchOne($sql, $bind);
81 }
82
83 /**
84 * Iterates over logs in a log table in chunks. Parameters to this function are as backend agnostic
85 * as possible w/o dramatically increasing code complexity.
86 *
87 * @param string $logTable The log table name. Unprefixed, eg, `log_visit`.
88 * @param array[] $conditions An array describing the conditions logs must match in the query. Translates to
89 * the WHERE part of a SELECT statement. Each element must contain three elements:
90 *
91 * * the column name
92 * * the operator (ie, '=', '<>', '<', etc.)
93 * * the operand (ie, a value)
94 *
95 * The elements are AND-ed together.
96 *
97 * Example:
98 *
99 * ```
100 * array(
101 * array('visit_first_action_time', '>=', ...),
102 * array('visit_first_action_time', '<', ...)
103 * )
104 * ```
105 * @param int $iterationStep The number of rows to query at a time.
106 * @param callable $callback The callback that processes each chunk of rows.
107 * @param string $willDelete Set to true if you will make sure to delete all rows that were fetched. If you are in
108 * doubt and not sure if to set true or false, use "false". Setting it to true will
109 * enable an internal performance improvement but it can result in an endless loop if not
110 * used properly.
111 */
112 public function forAllLogs($logTable, $fields, $conditions, $iterationStep, $callback, $willDelete)
113 {
114 $lastId = 0;
115
116 if ($willDelete) {
117 // we don't want to look at eg idvisit so the query will be mostly index covered as the
118 // "where idvisit > 0 ... ORDER BY idvisit ASC" will be gone... meaning we don't need to look at a huge range
119 // of visits...
120 $idField = null;
121 $bindFunction = function ($bind, $lastId) {
122 return $bind;
123 };
124 } else {
125 // when we are not deleting, we need to ensure to iterate over each visitor step by step... meaning we
126 // need to remember which visit we have already looked at and which one not. Therefore we need to apply
127 // "where idvisit > $lastId" in the query and "order by idvisit ASC"
128 $idField = $this->getIdFieldForLogTable($logTable);
129 $bindFunction = function ($bind, $lastId) {
130 return array_merge(array($lastId), $bind);
131 };
132 }
133
134 list($query, $bind) = $this->createLogIterationQuery($logTable, $idField, $fields, $conditions, $iterationStep);
135
136 do {
137 $rows = Db::fetchAll($query, call_user_func($bindFunction, $bind, $lastId));
138 if (!empty($rows)) {
139 if ($idField) {
140 $lastId = $rows[count($rows) - 1][$idField];
141 }
142 $callback($rows);
143 }
144 } while (count($rows) == $iterationStep);
145 }
146
147 /**
148 * Deletes conversion items for the supplied visit IDs from log_conversion_item.
149 *
150 * @param int[] $visitIds
151 * @return int The number of deleted rows.
152 */
153 public function deleteConversionItems($visitIds)
154 {
155 $sql = "DELETE FROM `" . Common::prefixTable('log_conversion_item') . "` WHERE idvisit IN "
156 . $this->getInFieldExpressionWithInts($visitIds);
157
158 $statement = Db::query($sql);
159 return $statement->rowCount();
160 }
161
162 /**
163 * Deletes all unused entries from the log_action table. This method uses a temporary table to store used
164 * actions, and then deletes rows from log_action that are not in this temporary table.
165 *
166 * Table locking is required to avoid concurrency issues.
167 *
168 * @throws \Exception If table locking permission is not granted to the current MySQL user.
169 */
170 public function deleteUnusedLogActions()
171 {
172 if (!Db::isLockPrivilegeGranted()) {
173 throw new \Exception("RawLogDao.deleteUnusedLogActions() requires table locking permission in order to complete without error.");
174 }
175
176 // get current max ID in log tables w/ idaction references.
177 $maxIds = $this->getMaxIdsInLogTables();
178
179 // get max rows to analyze
180 $max_rows_per_query = PiwikConfig::getInstance()->Deletelogs['delete_logs_unused_actions_max_rows_per_query'];
181
182 $this->createTempTableForStoringUsedActions();
183
184 // do large insert (inserting everything before maxIds) w/o locking tables...
185 $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = true, $max_rows_per_query);
186
187 // ... then do small insert w/ locked tables to minimize the amount of time tables are locked.
188 $this->lockLogTables();
189 $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = false, $max_rows_per_query);
190
191 // delete before unlocking tables so there's no chance a new log row that references an
192 // unused action will be inserted.
193 $this->deleteUnusedActions();
194
195 Db::unlockAllTables();
196
197 $this->dropTempTableForStoringUsedActions();
198 }
199
200 /**
201 * Returns the list of the website IDs that received some visits between the specified timestamp. The
202 * start date and the end date is included in the time frame.
203 *
204 * @param string $fromDateTime
205 * @param string $toDateTime
206 * @return bool true if there are visits for this site between the given timeframe, false if not
207 */
208 public function hasSiteVisitsBetweenTimeframe($fromDateTime, $toDateTime, $idSite)
209 {
210 $sites = Db::fetchOne("SELECT 1
211 FROM " . Common::prefixTable('log_visit') . "
212 WHERE idsite = ?
213 AND visit_last_action_time >= ?
214 AND visit_last_action_time <= ?
215 LIMIT 1", array($idSite, $fromDateTime, $toDateTime));
216
217 return (bool) $sites;
218 }
219
220 /**
221 * @param array $columnsToSet
222 * @return string
223 */
224 protected function getColumnSetExpressions(array $columnsToSet)
225 {
226 $columnsToSet = array_map(
227 function ($column) {
228 return $column . ' = ?';
229 },
230 $columnsToSet
231 );
232
233 return implode(', ', $columnsToSet);
234 }
235
236 /**
237 * @param array $values
238 * @param $idVisit
239 * @param $sql
240 * @return \Zend_Db_Statement
241 * @throws \Exception
242 */
243 protected function update($sql, array $values, $idVisit)
244 {
245 return Db::query($sql, array_merge(array_values($values), array($idVisit)));
246 }
247
248 protected function getIdFieldForLogTable($logTable)
249 {
250 $idColumns = $this->getTableIdColumns();
251
252 if (isset($idColumns[$logTable])) {
253 return $idColumns[$logTable];
254 }
255
256 throw new \InvalidArgumentException("Unknown log table '$logTable'.");
257 }
258
259 // TODO: instead of creating a log query like this, we should re-use segments. to do this, however, there must be a 1-1
260 // mapping for dimensions => segments, and each dimension should automatically have a segment.
261 private function createLogIterationQuery($logTable, $idField, $fields, $conditions, $iterationStep)
262 {
263 $bind = array();
264
265 $sql = "SELECT " . implode(', ', $fields) . " FROM `" . Common::prefixTable($logTable) . "` WHERE ";
266
267 $parts = array();
268
269 if ($idField) {
270 $parts[] = "$idField > ?";
271 }
272
273 foreach ($conditions as $condition) {
274 list($column, $operator, $value) = $condition;
275
276 if (is_array($value)) {
277 $parts[] = "$column IN (" . Common::getSqlStringFieldsArray($value) . ")";
278
279 $bind = array_merge($bind, $value);
280 } else {
281 $parts[]= "$column $operator ?";
282
283 $bind[] = $value;
284 }
285 }
286 $sql .= implode(' AND ', $parts);
287
288 if ($idField) {
289 $sql .= " ORDER BY $idField ASC";
290 }
291
292 $sql .= " LIMIT " . (int)$iterationStep;
293
294 return array($sql, $bind);
295 }
296
297 private function getInFieldExpressionWithInts($idVisits)
298 {
299 $sql = "(";
300
301 $isFirst = true;
302 foreach ($idVisits as $idVisit) {
303 if ($isFirst) {
304 $isFirst = false;
305 } else {
306 $sql .= ', ';
307 }
308
309 $sql .= (int)$idVisit;
310 }
311
312 $sql .= ")";
313
314 return $sql;
315 }
316
317 protected function getMaxIdsInLogTables()
318 {
319 $idColumns = $this->getTableIdColumns();
320 $tables = array_keys($idColumns);
321
322 $result = array();
323 foreach ($tables as $table) {
324 $idCol = $idColumns[$table];
325 $result[$table] = Db::fetchOne("SELECT MAX($idCol) FROM " . Common::prefixTable($table));
326 }
327
328 return $result;
329 }
330
331 private function createTempTableForStoringUsedActions()
332 {
333 $sql = "CREATE TEMPORARY TABLE " . Common::prefixTable(self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME) . " (
334 idaction INT(11),
335 PRIMARY KEY (idaction)
336 )";
337 Db::query($sql);
338 }
339
340 private function dropTempTableForStoringUsedActions()
341 {
342 $sql = "DROP TABLE " . Common::prefixTable(self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME);
343 Db::query($sql);
344 }
345
346 // protected for testing purposes
347 protected function insertActionsToKeep($maxIds, $olderThan = true, $insertIntoTempIterationStep = 100000)
348 {
349 $tempTableName = Common::prefixTable(self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME);
350
351 $idColumns = $this->getTableIdColumns();
352 foreach ($this->dimensionMetadataProvider->getActionReferenceColumnsByTable() as $table => $columns) {
353 $idCol = $idColumns[$table];
354 // Create select query for requesting ALL needed fields at once
355 $sql = "SELECT " . implode(',' ,$columns) . " FROM " . Common::prefixTable($table) . " WHERE $idCol >= ? AND $idCol < ?";
356
357 if ($olderThan) {
358 // Why start on zero? When running for a couple of months, this will generate about 10000+ queries with zero result. Use the lowest value instead.... saves a LOT of waiting time!
359 $start = (int) Db::fetchOne("SELECT MIN($idCol) FROM " . Common::prefixTable($table));;
360 $finish = $maxIds[$table];
361 } else {
362 $start = $maxIds[$table];
363 $finish = (int) Db::fetchOne("SELECT MAX($idCol) FROM " . Common::prefixTable($table));
364 }
365 // Borrowed from Db::segmentedFetchAll
366 // Request records per $insertIntoTempIterationStep amount
367 // Loop over the result set, mapping all numeric fields in a single insert query
368
369 // Insert query would be: INSERT IGNORE INTO [temp_table] VALUES (X),(Y),(Z) depending on the amount of fields requested per row
370 for ($i = $start; $i <= $finish; $i += $insertIntoTempIterationStep) {
371 $currentParams = array($i, $i + $insertIntoTempIterationStep);
372 $result = Db::fetchAll($sql, $currentParams);
373 // Now we loop over the result set of max $insertIntoTempIterationStep rows and create insert queries
374 $keepValues = [];
375 foreach ($result as $row) {
376 $keepValues = array_merge($keepValues, array_filter(array_values($row), "is_numeric"));
377 if (count($keepValues) >= 1000) {
378 $insert = 'INSERT IGNORE INTO ' . $tempTableName .' VALUES (';
379 $insert .= implode('),(', $keepValues);
380 $insert .= ')';
381
382 Db::exec($insert);
383 $keepValues = [];
384 }
385 }
386
387 $insert = 'INSERT IGNORE INTO ' . $tempTableName .' VALUES (';
388 $insert .= implode('),(', $keepValues);
389 $insert .= ')';
390
391 Db::exec($insert);
392 }
393 }
394 }
395
396 private function lockLogTables()
397 {
398 $tables = $this->getTableIdColumns();
399 unset($tables['log_action']); // we write lock it
400 $tableNames = array_keys($tables);
401
402 $readLocks = array();
403 foreach ($tableNames as $tableName) {
404 $readLocks[] = Common::prefixTable($tableName);
405 }
406
407 Db::lockTables(
408 $readLocks,
409 $writeLocks = Common::prefixTables('log_action')
410 );
411 }
412
413 private function deleteUnusedActions()
414 {
415 list($logActionTable, $tempTableName) = Common::prefixTables("log_action", self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME);
416
417 $deleteSql = "DELETE LOW_PRIORITY QUICK IGNORE $logActionTable
418 FROM $logActionTable
419 LEFT JOIN $tempTableName tmp ON tmp.idaction = $logActionTable.idaction
420 WHERE tmp.idaction IS NULL";
421
422 Db::query($deleteSql);
423 }
424
425 protected function getTableIdColumns()
426 {
427 $columns = array();
428
429 foreach ($this->logTablesProvider->getAllLogTables() as $logTable) {
430 $idColumn = $logTable->getIdColumn();
431
432 if (!empty($idColumn)) {
433 $columns[$logTable->getName()] = $idColumn;
434 }
435 }
436
437 return $columns;
438 }
439 }
440