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