PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.0.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.0.3
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 / Model.php
matomo / app / core / DataAccess Last commit date
LogQueryBuilder 5 years ago Actions.php 5 years ago ArchiveSelector.php 5 years ago ArchiveTableCreator.php 5 years ago ArchiveTableDao.php 5 years ago ArchiveWriter.php 5 years ago ArchivingDbAdapter.php 5 years ago LogAggregator.php 5 years ago LogQueryBuilder.php 5 years ago LogTableTemporary.php 5 years ago Model.php 5 years ago RawLogDao.php 5 years ago TableMetadata.php 5 years ago
Model.php
916 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 Exception;
12 use Piwik\Archive\ArchiveInvalidator;
13 use Piwik\ArchiveProcessor\ArchivingStatus;
14 use Piwik\ArchiveProcessor\Parameters;
15 use Piwik\ArchiveProcessor\Rules;
16 use Piwik\Common;
17 use Piwik\Container\StaticContainer;
18 use Piwik\Date;
19 use Piwik\Db;
20 use Piwik\DbHelper;
21 use Piwik\Period;
22 use Piwik\Segment;
23 use Piwik\Sequence;
24 use Piwik\Site;
25 use Psr\Log\LoggerInterface;
26
27 /**
28 * Cleans up outdated archives
29 */
30 class Model
31 {
32 /**
33 * @var LoggerInterface
34 */
35 private $logger;
36
37 /**
38 * @var ArchivingStatus
39 */
40 private $archivingStatus;
41
42 public function __construct(LoggerInterface $logger = null)
43 {
44 $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
45 $this->archivingStatus = StaticContainer::get(ArchivingStatus::class);
46 }
47
48 /**
49 * Returns the archives IDs that have already been invalidated and have been since re-processed.
50 *
51 * These archives { archive name (includes segment hash) , idsite, date, period } will be deleted.
52 *
53 * @param string $archiveTable
54 * @param array $idSites
55 * @return array
56 * @throws Exception
57 */
58 public function getInvalidatedArchiveIdsSafeToDelete($archiveTable)
59 {
60 try {
61 Db::get()->query('SET SESSION group_concat_max_len=' . (128 * 1024));
62 } catch (\Exception $ex) {
63 $this->logger->info("Could not set group_concat_max_len MySQL session variable.");
64 }
65
66 $sql = "SELECT idsite, date1, date2, period, name,
67 GROUP_CONCAT(idarchive, '.', value ORDER BY ts_archived DESC) as archives
68 FROM `$archiveTable`
69 WHERE name LIKE 'done%'
70 AND ts_archived IS NOT NULL
71 AND `value` NOT IN (" . ArchiveWriter::DONE_ERROR . ")
72 GROUP BY idsite, date1, date2, period, name HAVING count(*) > 1";
73
74 $archiveIds = array();
75
76 $rows = Db::fetchAll($sql);
77 foreach ($rows as $row) {
78 $duplicateArchives = explode(',', $row['archives']);
79
80 // do not consider purging partial archives, if they are the latest archive,
81 // and we don't want to delete the latest archive if it is usable
82 while (!empty($duplicateArchives)) {
83 $pair = $duplicateArchives[0];
84 if (strpos($pair, '.') === false) {
85 continue; // see below
86 }
87
88 list($idarchive, $value) = explode('.', $pair);
89
90 array_shift($duplicateArchives);
91
92 if ($value != ArchiveWriter::DONE_PARTIAL) {
93 break;
94 }
95 }
96
97 // if there is more than one archive, the older invalidated ones can be deleted
98 if (!empty($duplicateArchives)) {
99 foreach ($duplicateArchives as $pair) {
100 if (strpos($pair, '.') === false) {
101 $this->logger->info("GROUP_CONCAT cut off the query result, you may have to purge archives again.");
102 break;
103 }
104
105 list($idarchive, $value) = explode('.', $pair);
106 $archiveIds[] = $idarchive; // does not matter what the value is, the latest is usable so older archives can be purged
107 }
108 }
109 }
110
111 return $archiveIds;
112 }
113
114 public function getPlaceholderArchiveIds($archiveTable)
115 {
116 $sql = "SELECT DISTINCT idarchive FROM `$archiveTable` WHERE ts_archived IS NULL";
117 $result = Db::fetchAll($sql);
118 $result = array_column($result, 'idarchive');
119 return $result;
120 }
121
122 public function updateArchiveAsInvalidated($archiveTable, $idSites, $allPeriodsToInvalidate, Segment $segment = null,
123 $forceInvalidateNonexistantRanges = false, $name = null)
124 {
125 if (empty($idSites)) {
126 return 0;
127 }
128
129 // select all idarchive/name pairs we want to invalidate
130 $sql = "SELECT idarchive, idsite, period, date1, date2, `name`, `value`
131 FROM `$archiveTable`
132 WHERE idsite IN (" . implode(',', $idSites) . ")";
133
134 $periodCondition = '';
135 if (!empty($allPeriodsToInvalidate)) {
136 $periodCondition .= " AND (";
137
138 $isFirst = true;
139 /** @var Period $period */
140 foreach ($allPeriodsToInvalidate as $period) {
141 if ($isFirst) {
142 $isFirst = false;
143 } else {
144 $periodCondition .= " OR ";
145 }
146
147 if ($period->getLabel() == 'range') { // for ranges, we delete all ranges that contain the given date(s)
148 $periodCondition .= "(period = " . (int)$period->getId()
149 . " AND date2 >= '" . $period->getDateStart()->getDatetime()
150 . "' AND date1 <= '" . $period->getDateEnd()->getDatetime() . "')";
151 } else {
152 $periodCondition .= "(period = " . (int)$period->getId()
153 . " AND date1 = '" . $period->getDateStart()->getDatetime() . "'"
154 . " AND date2 = '" . $period->getDateEnd()->getDatetime() . "')";
155 }
156 }
157 $periodCondition .= ")";
158 }
159 $sql .= $periodCondition;
160
161 if (!empty($name)) {
162 if (strpos($name, '.') !== false) {
163 list($plugin, $name) = explode('.', $name, 2);
164 } else {
165 $plugin = $name;
166 $name = null;
167 }
168 }
169
170 if (empty($plugin)) {
171 $doneFlag = Rules::getDoneFlagArchiveContainsAllPlugins($segment ?: new Segment('', []));
172 } else {
173 $doneFlag = Rules::getDoneFlagArchiveContainsOnePlugin($segment ?: new Segment('', []), $plugin);
174 }
175
176 $nameCondition = "name LIKE '$doneFlag%'";
177
178 $sql .= " AND $nameCondition";
179
180 $archivesToInvalidate = Db::fetchAll($sql);
181 $idArchives = array_column($archivesToInvalidate, 'idarchive');
182
183 // update each archive as invalidated
184 if (!empty($idArchives)) {
185 $idArchives = array_map('intval', $idArchives);
186
187 $sql = "UPDATE `$archiveTable` SET `value` = " . ArchiveWriter::DONE_INVALIDATED . " WHERE idarchive IN ("
188 . implode(',', $idArchives) . ") AND $nameCondition";
189
190 Db::query($sql);
191 }
192
193 // we add every archive we need to invalidate + the archives that do not already exist to archive_invalidations.
194 // except for archives that are DONE_IN_PROGRESS.
195 $archivesToCreateInvalidationRowsFor = [];
196 foreach ($archivesToInvalidate as $row) {
197 if ($row['name'] != $doneFlag) { // only look at done flags that equal the one we are explicitly adding
198 continue;
199 }
200
201 $archivesToCreateInvalidationRowsFor[$row['idsite']][$row['period']][$row['date1']][$row['date2']] = $row['idarchive'];
202 }
203
204 $now = Date::now()->getDatetime();
205
206 $existingInvalidations = $this->getExistingInvalidations($idSites, $periodCondition, $nameCondition);
207
208 $dummyArchives = [];
209 foreach ($idSites as $idSite) {
210 try {
211 $siteCreationTime = Site::getCreationDateFor($idSite);
212 } catch (\Exception $ex) {
213 continue;
214 }
215
216 $siteCreationTime = Date::factory($siteCreationTime);
217 foreach ($allPeriodsToInvalidate as $period) {
218 if ($period->getLabel() == 'range'
219 && !$forceInvalidateNonexistantRanges
220 ) {
221 continue; // range
222 }
223
224 if ($period->getDateEnd()->isEarlier($siteCreationTime)) {
225 continue; // don't add entries if it is before the time the site was created
226 }
227
228 $date1 = $period->getDateStart()->toString();
229 $date2 = $period->getDateEnd()->toString();
230
231 $key = $this->makeExistingInvalidationArrayKey($idSite, $date1, $date2, $period->getId(), $doneFlag);
232 if (!empty($existingInvalidations[$key])) {
233 continue; // avoid adding duplicates where possible
234 }
235
236 $idArchive = $archivesToCreateInvalidationRowsFor[$idSite][$period->getId()][$date1][$date2] ?? null;
237
238 $dummyArchives[] = [
239 'idarchive' => $idArchive,
240 'name' => $doneFlag,
241 'report' => $name,
242 'idsite' => $idSite,
243 'date1' => $period->getDateStart()->getDatetime(),
244 'date2' => $period->getDateEnd()->getDatetime(),
245 'period' => $period->getId(),
246 'ts_invalidated' => $now,
247 ];
248 }
249 }
250
251 if (!empty($dummyArchives)) {
252 $fields = ['idarchive', 'name', 'report', 'idsite', 'date1', 'date2', 'period', 'ts_invalidated'];
253 Db\BatchInsert::tableInsertBatch(Common::prefixTable('archive_invalidations'), $fields, $dummyArchives);
254 }
255
256 return count($idArchives);
257 }
258
259 private function getExistingInvalidations($idSites, $periodCondition, $nameCondition)
260 {
261 $table = Common::prefixTable('archive_invalidations');
262
263 $idSites = array_map('intval', $idSites);
264
265 $sql = "SELECT idsite, date1, date2, period, name, COUNT(*) as `count` FROM `$table`
266 WHERE idsite IN (" . implode(',', $idSites) . ") AND status = " . ArchiveInvalidator::INVALIDATION_STATUS_QUEUED . "
267 $periodCondition AND $nameCondition
268 GROUP BY idsite, date1, date2, period, name";
269 $rows = Db::fetchAll($sql);
270
271 $invalidations = [];
272 foreach ($rows as $row) {
273 $key = $this->makeExistingInvalidationArrayKey($row['idsite'], $row['date1'], $row['date2'], $row['period'], $row['name']);
274 $invalidations[$key] = $row['count'];
275 }
276 return $invalidations;
277 }
278
279 private function makeExistingInvalidationArrayKey($idSite, $date1, $date2, $period, $name)
280 {
281 return implode('.', [$idSite, $date1, $date2, $period, $name]);
282 }
283
284 /**
285 * @param string $archiveTable Prefixed table name
286 * @param int[] $idSites
287 * @param string[][] $datesByPeriodType
288 * @param Segment $segment
289 * @return \Zend_Db_Statement
290 * @throws Exception
291 */
292 public function updateRangeArchiveAsInvalidated($archiveTable, $idSites, $allPeriodsToInvalidate, Segment $segment = null)
293 {
294 if (empty($idSites)) {
295 return;
296 }
297
298 $bind = array();
299
300 $periodConditions = array();
301 if (!empty($allPeriodsToInvalidate)) {
302 foreach ($allPeriodsToInvalidate as $period) {
303 $dateConditions = array();
304
305 /** @var Period $period */
306 $dateConditions[] = "(date1 <= ? AND ? <= date2)";
307 $bind[] = $period->getDateStart();
308 $bind[] = $period->getDateEnd();
309
310 $dateConditionsSql = implode(" OR ", $dateConditions);
311 $periodConditions[] = "(period = 5 AND ($dateConditionsSql))";
312 }
313 }
314
315 if ($segment) {
316 $nameCondition = "name LIKE '" . Rules::getDoneFlagArchiveContainsAllPlugins($segment) . "%'";
317 } else {
318 $nameCondition = "name LIKE 'done%'";
319 }
320
321 $sql = "UPDATE $archiveTable SET value = " . ArchiveWriter::DONE_INVALIDATED
322 . " WHERE $nameCondition
323 AND idsite IN (" . implode(", ", $idSites) . ")
324 AND (" . implode(" OR ", $periodConditions) . ")";
325
326 return Db::query($sql, $bind);
327 }
328
329 public function getTemporaryArchivesOlderThan($archiveTable, $purgeArchivesOlderThan)
330 {
331 $query = "SELECT idarchive FROM " . $archiveTable . "
332 WHERE name LIKE 'done%'
333 AND (( value = " . ArchiveWriter::DONE_OK_TEMPORARY . "
334 AND ts_archived < ?)
335 OR value = " . ArchiveWriter::DONE_ERROR . ")";
336
337 return Db::fetchAll($query, array($purgeArchivesOlderThan));
338 }
339
340 public function deleteArchivesWithPeriod($numericTable, $blobTable, $period, $date)
341 {
342 $query = "DELETE FROM %s WHERE period = ? AND ts_archived < ?";
343 $bind = array($period, $date);
344
345 $queryObj = Db::query(sprintf($query, $numericTable), $bind);
346 $deletedRows = $queryObj->rowCount();
347
348 try {
349 $queryObj = Db::query(sprintf($query, $blobTable), $bind);
350 $deletedRows += $queryObj->rowCount();
351 } catch (Exception $e) {
352 // Individual blob tables could be missing
353 $this->logger->debug("Unable to delete archives by period from {blobTable}.", array(
354 'blobTable' => $blobTable,
355 'exception' => $e,
356 ));
357 }
358
359 return $deletedRows;
360 }
361
362 public function getInvalidatedArchiveIdsAsOldOrOlderThan($archive)
363 {
364 $table = ArchiveTableCreator::getNumericTable(Date::factory($archive['date1']));
365 $sql = "SELECT idarchive FROM `$table` WHERE idsite = ? AND period = ? AND date1 = ? AND date2 = ? AND `name` = ? AND `value` IN ("
366 . ArchiveWriter::DONE_INVALIDATED . ") AND idarchive <= ?";
367 $bind = [
368 $archive['idsite'],
369 $archive['period'],
370 $archive['date1'],
371 $archive['date2'],
372 $archive['name'],
373 $archive['idarchive'],
374 ];
375
376 $result = Db::fetchAll($sql, $bind);
377 $result = array_column($result, 'idarchive');
378
379 return $result;
380 }
381
382 public function deleteArchiveIds($numericTable, $blobTable, $idsToDelete)
383 {
384 $idsToDelete = array_values($idsToDelete);
385
386 $idsToDelete = array_map('intval', $idsToDelete);
387 $query = "DELETE FROM %s WHERE idarchive IN (" . implode(',', $idsToDelete) . ")";
388
389 $queryObj = Db::query(sprintf($query, $numericTable), array());
390 $deletedRows = $queryObj->rowCount();
391
392 try {
393 $queryObj = Db::query(sprintf($query, $blobTable), array());
394 $deletedRows += $queryObj->rowCount();
395 } catch (Exception $e) {
396 // Individual blob tables could be missing
397 $this->logger->debug("Unable to delete archive IDs from {blobTable}.", array(
398 'blobTable' => $blobTable,
399 'exception' => $e,
400 ));
401 }
402
403 return $deletedRows;
404 }
405
406 public function deleteOlderArchives(Parameters $params, $name, $tsArchived, $idArchive)
407 {
408 $dateStart = $params->getPeriod()->getDateStart();
409 $dateEnd = $params->getPeriod()->getDateEnd();
410
411 $numericTable = ArchiveTableCreator::getNumericTable($dateStart);
412 $blobTable = ArchiveTableCreator::getBlobTable($dateStart);
413
414 $sql = "SELECT idarchive FROM `$numericTable` WHERE idsite = ? AND date1 = ? AND date2 = ? AND period = ? AND name = ? AND ts_archived < ? AND idarchive < ?";
415
416 $idArchives = Db::fetchAll($sql, [$params->getSite()->getId(), $dateStart->getDatetime(), $dateEnd->getDatetime(), $params->getPeriod()->getId(), $name, $tsArchived, $idArchive]);
417 $idArchives = array_column($idArchives, 'idarchive');
418 if (empty($idArchives)) {
419 return;
420 }
421
422 $this->deleteArchiveIds($numericTable, $blobTable, $idArchives);
423 }
424
425 public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC,
426 $doneFlags, $doneFlagValues = null)
427 {
428 $bindSQL = array($idSite,
429 $dateStartIso,
430 $dateEndIso,
431 $period,
432 );
433
434 $sqlWhereArchiveName = self::getNameCondition($doneFlags, $doneFlagValues);
435
436 $timeStampWhere = '';
437 if ($minDatetimeIsoArchiveProcessedUTC) {
438 $timeStampWhere = " AND arc1.ts_archived >= ? ";
439 $bindSQL[] = $minDatetimeIsoArchiveProcessedUTC;
440 }
441
442 // NOTE: we can't predict how many segments there will be so there could be lots of nb_visits/nb_visits_converted rows... have to select everything.
443 $sqlQuery = "SELECT arc1.idarchive, arc1.value, arc1.name, arc1.ts_archived, arc1.date1 as startDate, arc2.value as " . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . ", arc3.value as " . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "
444 FROM $numericTable arc1
445 LEFT JOIN $numericTable arc2 on arc2.idarchive = arc1.idarchive and (arc2.name = '" . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . "')
446 LEFT JOIN $numericTable arc3 on arc3.idarchive = arc1.idarchive and (arc3.name = '" . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "')
447 WHERE arc1.idsite = ?
448 AND arc1.date1 = ?
449 AND arc1.date2 = ?
450 AND arc1.period = ?
451 AND ($sqlWhereArchiveName)
452 $timeStampWhere
453 AND arc1.ts_archived IS NOT NULL
454 ORDER BY arc1.ts_archived DESC, arc1.idarchive DESC";
455
456 $results = Db::fetchAll($sqlQuery, $bindSQL);
457
458 return $results;
459 }
460
461 public function createArchiveTable($tableName, $tableNamePrefix)
462 {
463 $db = Db::get();
464 $sql = DbHelper::getTableCreateSql($tableNamePrefix);
465
466 // replace table name template by real name
467 $tableNamePrefix = Common::prefixTable($tableNamePrefix);
468 $sql = str_replace($tableNamePrefix, $tableName, $sql);
469
470 try {
471 $db->query($sql);
472 } catch (Exception $e) {
473 // accept mysql error 1050: table already exists, throw otherwise
474 if (!$db->isErrNo($e, '1050')) {
475 throw $e;
476 }
477 }
478
479 try {
480 if (ArchiveTableCreator::NUMERIC_TABLE === ArchiveTableCreator::getTypeFromTableName($tableName)) {
481 $sequence = new Sequence($tableName);
482 $sequence->create();
483 }
484 } catch (Exception $e) {
485 }
486 }
487
488 public function getInstalledArchiveTables()
489 {
490 $allArchiveNumeric = Db::get()->fetchCol("SHOW TABLES LIKE '" . Common::prefixTable('archive_numeric%') . "'");
491 $allArchiveBlob = Db::get()->fetchCol("SHOW TABLES LIKE '" . Common::prefixTable('archive_blob%') ."'");
492
493 return array_merge($allArchiveBlob, $allArchiveNumeric);
494 }
495
496 public function allocateNewArchiveId($numericTable)
497 {
498 $sequence = new Sequence($numericTable);
499
500 try {
501 $idarchive = $sequence->getNextId();
502 } catch (Exception $e) {
503 // edge case: sequence was not found, create it now
504 $sequence->create();
505
506 $idarchive = $sequence->getNextId();
507 }
508
509 return $idarchive;
510 }
511
512 public function updateArchiveStatus($numericTable, $archiveId, $doneFlag, $value)
513 {
514 Db::query("UPDATE $numericTable SET `value` = ? WHERE idarchive = ? and `name` = ?",
515 array($value, $archiveId, $doneFlag)
516 );
517 }
518
519 public function insertRecord($tableName, $fields, $record, $name, $value)
520 {
521 // duplicate idarchives are Ignored, see https://github.com/piwik/piwik/issues/987
522 $query = "INSERT IGNORE INTO " . $tableName . " (" . implode(", ", $fields) . ")
523 VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE " . end($fields) . " = ?";
524
525 $bindSql = $record;
526 $bindSql[] = $name;
527 $bindSql[] = $value;
528 $bindSql[] = $value;
529
530 Db::query($query, $bindSql);
531
532 return true;
533 }
534
535 /**
536 * Returns the site IDs for invalidated archives in an archive table.
537 *
538 * @param string $numericTable The numeric table to search through.
539 * @return int[]
540 */
541 public function getSitesWithInvalidatedArchive($numericTable)
542 {
543 $rows = Db::fetchAll("SELECT DISTINCT idsite FROM `$numericTable` WHERE `name` LIKE 'done%' AND `value` IN ("
544 . ArchiveWriter::DONE_INVALIDATED . ")");
545
546 $result = array();
547 foreach ($rows as $row) {
548 $result[] = $row['idsite'];
549 }
550 return $result;
551 }
552
553 /**
554 * Get a list of IDs of archives that don't have any matching rows in the site table. Excludes temporary archives
555 * that may still be in use, as specified by the $oldestToKeep passed in.
556 * @param string $archiveTableName
557 * @param string $oldestToKeep Datetime string
558 * @return array of IDs
559 */
560 public function getArchiveIdsForDeletedSites($archiveTableName)
561 {
562 $sql = "SELECT DISTINCT idsite FROM " . $archiveTableName;
563 $rows = Db::getReader()->fetchAll($sql, array());
564
565 if (empty($rows)) {
566 return array(); // nothing to delete
567 }
568
569 $idSitesUsed = array_column($rows, 'idsite');
570
571 $model = new \Piwik\Plugins\SitesManager\Model();
572 $idSitesExisting = $model->getSitesId();
573
574 $deletedSites = array_diff($idSitesUsed, $idSitesExisting);
575
576 if (empty($deletedSites)) {
577 return array();
578 }
579 $deletedSites = array_values($deletedSites);
580 $deletedSites = array_map('intval', $deletedSites);
581
582 $sql = "SELECT DISTINCT idarchive FROM " . $archiveTableName . " WHERE idsite IN (".implode(',',$deletedSites).")";
583
584 $rows = Db::getReader()->fetchAll($sql, array());
585
586 return array_column($rows, 'idarchive');
587 }
588
589 /**
590 * Get a list of IDs of archives with segments that no longer exist in the DB. Excludes temporary archives that
591 * may still be in use, as specified by the $oldestToKeep passed in.
592 * @param string $archiveTableName
593 * @param array $segments List of segments to match against
594 * @param string $oldestToKeep Datetime string
595 * @return array With keys idarchive, name, idsite
596 */
597 public function getArchiveIdsForSegments($archiveTableName, array $segments, $oldestToKeep)
598 {
599 $segmentClauses = [];
600 foreach ($segments as $segment) {
601 if (!empty($segment['definition'])) {
602 $segmentClauses[] = $this->getDeletedSegmentWhereClause($segment);
603 }
604 }
605
606 if (empty($segmentClauses)) {
607 return array();
608 }
609
610 $segmentClauses = implode(' OR ', $segmentClauses);
611
612 $sql = 'SELECT idarchive FROM ' . $archiveTableName
613 . ' WHERE ts_archived < ?'
614 . ' AND (' . $segmentClauses . ')';
615
616 $rows = Db::fetchAll($sql, array($oldestToKeep));
617
618 return array_column($rows, 'idarchive');
619 }
620
621 private function getDeletedSegmentWhereClause(array $segment)
622 {
623 $idSite = (int)$segment['enable_only_idsite'];
624 $segmentHash = Segment::getSegmentHash($segment['definition']);
625 // Valid segment hashes are md5 strings - just confirm that it is so it's safe for SQL injection
626 if (!ctype_xdigit($segmentHash)) {
627 throw new Exception($segment . ' expected to be an md5 hash');
628 }
629
630 $nameClause = 'name LIKE "done' . $segmentHash . '%"';
631 $idSiteClause = '';
632 if ($idSite > 0) {
633 $idSiteClause = ' AND idsite = ' . $idSite;
634 } elseif (! empty($segment['idsites_to_preserve'])) {
635 // A segment for all sites was deleted, but there are segments for a single site with the same definition
636 $idSitesToPreserve = array_map('intval', $segment['idsites_to_preserve']);
637 $idSiteClause = ' AND idsite NOT IN (' . implode(',', $idSitesToPreserve) . ')';
638 }
639
640 return "($nameClause $idSiteClause)";
641 }
642
643 /**
644 * Returns the SQL condition used to find successfully completed archives that
645 * this instance is querying for.
646 */
647 private static function getNameCondition($doneFlags, $possibleValues)
648 {
649 $allDoneFlags = "'" . implode("','", $doneFlags) . "'";
650
651 // create the SQL to find archives that are DONE
652 $result = "((arc1.name IN ($allDoneFlags))";
653
654 if (!empty($possibleValues)) {
655 $result .= " AND (arc1.value IN (" . implode(',', $possibleValues) . ")))";
656 }
657 $result .= ')';
658
659 return $result;
660 }
661
662 /**
663 * Marks an archive as in progress if it has not been already. This method must be thread
664 * safe.
665 */
666 public function startArchive($invalidation)
667 {
668 $table = Common::prefixTable('archive_invalidations');
669
670 // set archive value to in progress if not set already
671 $statement = Db::query("UPDATE `$table` SET `status` = ?, ts_started = NOW() WHERE idinvalidation = ? AND status = ?", [
672 ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS,
673 $invalidation['idinvalidation'],
674 ArchiveInvalidator::INVALIDATION_STATUS_QUEUED,
675 ]);
676
677 if ($statement->rowCount() > 0) { // if we updated, then we've marked the archive as started
678 return true;
679 }
680
681 // if we didn't get anything, some process either got there first, OR
682 // the archive was started previously and failed in a way that kept it's done value
683 // set to DONE_IN_PROGRESS. try to acquire the lock and if acquired, archiving isn' in process
684 // so we can claim it.
685 $lock = $this->archivingStatus->acquireArchiveInProgressLock($invalidation['idsite'], $invalidation['date1'],
686 $invalidation['date2'], $invalidation['period'], $invalidation['name']);
687 if (!$lock->isLocked()) {
688 return false; // we couldn't claim the lock, archive is in progress
689 }
690
691 // remove similar invalidations w/ lesser idinvalidation values
692 $bind = [
693 $invalidation['idsite'],
694 $invalidation['period'],
695 $invalidation['date1'],
696 $invalidation['date2'],
697 $invalidation['name'],
698 ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS,
699 ];
700
701 if (empty($invalidation['report'])) {
702 $reportClause = "(report IS NULL OR report = '')";
703 } else {
704 $reportClause = "report = ?";
705 $bind[] = $invalidation['report'];
706 }
707
708 $sql = "DELETE FROM " . Common::prefixTable('archive_invalidations') . " WHERE idinvalidation < ? AND idsite = ? AND "
709 . "date1 = ? AND date2 = ? AND `period` = ? AND `name` = ? AND $reportClause";
710 Db::query($sql, $bind);
711
712 return true;
713 }
714
715 public function isSimilarArchiveInProgress($invalidation)
716 {
717 $table = Common::prefixTable('archive_invalidations');
718
719 $bind = [
720 $invalidation['idsite'],
721 $invalidation['period'],
722 $invalidation['date1'],
723 $invalidation['date2'],
724 $invalidation['name'],
725 ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS,
726 ];
727
728 if (empty($invalidation['report'])) {
729 $reportClause = "(report IS NULL OR report = '')";
730 } else {
731 $reportClause = "report = ?";
732 $bind[] = $invalidation['report'];
733 }
734
735 $sql = "SELECT idinvalidation FROM `$table` WHERE idsite = ? AND `period` = ? AND date1 = ? AND date2 = ? AND `name` = ? AND `status` = ? AND ts_started IS NOT NULL AND $reportClause LIMIT 1";
736 $result = Db::fetchOne($sql, $bind);
737
738 return !empty($result);
739 }
740
741 /**
742 * Gets the next invalidated archive that should be archived in a table.
743 *
744 * @param int $idSite
745 * @param string $archivingStartTime
746 * @param int[]|null $idInvalidationsToExclude
747 * @param bool $useLimit Whether to limit the result set to one result or not. Used in tests only.
748 */
749 public function getNextInvalidatedArchive($idSite, $archivingStartTime, $idInvalidationsToExclude = null, $useLimit = true)
750 {
751 $table = Common::prefixTable('archive_invalidations');
752 $sql = "SELECT idinvalidation, idarchive, idsite, date1, date2, period, `name`, report, ts_invalidated
753 FROM `$table`
754 WHERE idsite = ? AND status != ? AND ts_invalidated <= ?";
755 $bind = [
756 $idSite,
757 ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS,
758 $archivingStartTime,
759 ];
760
761 if (!empty($idInvalidationsToExclude)) {
762 $idInvalidationsToExclude = array_map('intval', $idInvalidationsToExclude);
763 $sql .= " AND idinvalidation NOT IN (" . implode(',', $idInvalidationsToExclude) . ')';
764 }
765
766 // NOTE: order here is very important to ensure we process lower period archives first, and general 'all' archives before
767 // segment archives, and so we use the latest idinvalidation
768 $sql .= " ORDER BY date1 DESC, period ASC, CHAR_LENGTH(name) ASC, idinvalidation DESC";
769
770 if ($useLimit) {
771 $sql .= " LIMIT 1";
772 return Db::fetchRow($sql, $bind);
773 } else {
774 return Db::fetchAll($sql, $bind);
775 }
776 }
777
778 public function deleteInvalidations($archiveInvalidations)
779 {
780 $ids = array_column($archiveInvalidations, 'idinvalidation');
781 $ids = array_map('intval', $ids);
782
783 $table = Common::prefixTable('archive_invalidations');
784 $sql = "DELETE FROM `$table` WHERE idinvalidation IN (" . implode(', ', $ids) . ")";
785
786 Db::query($sql);
787 }
788
789 public function removeInvalidationsLike($idSite, $start)
790 {
791 $idSitesClause = $this->getRemoveInvalidationsIdSitesClause($idSite);
792
793 $table = Common::prefixTable('archive_invalidations');
794 $sql = "DELETE FROM `$table` WHERE $idSitesClause `name` LIKE ?";
795
796 Db::query($sql, ['done%.' . str_replace('_', "\\_", $start)]);
797 }
798
799 public function removeInvalidations($idSite, $plugin, $report)
800 {
801 $idSitesClause = $this->getRemoveInvalidationsIdSitesClause($idSite);
802
803 $table = Common::prefixTable('archive_invalidations');
804 $sql = "DELETE FROM `$table` WHERE $idSitesClause `name` LIKE ? AND report = ?";
805
806 Db::query($sql, ['done%.' . str_replace('_', "\\_", $plugin), $report]);
807 }
808
809 public function isArchiveAlreadyInProgress($invalidatedArchive)
810 {
811 $table = Common::prefixTable('archive_invalidations');
812
813 $bind = [
814 $invalidatedArchive['idsite'],
815 $invalidatedArchive['date1'],
816 $invalidatedArchive['date2'],
817 $invalidatedArchive['period'],
818 $invalidatedArchive['name'],
819 ];
820
821 $reportClause = "(report = '' OR report IS NULL)";
822 if (!empty($invalidatedArchive['report'])) {
823 $reportClause = "report = ?";
824 $bind[] = $invalidatedArchive['report'];
825 }
826
827 $sql = "SELECT MAX(idinvalidation) FROM `$table` WHERE idsite = ? AND date1 = ? AND date2 = ? AND `period` = ? AND `name` = ? AND status = 1 AND $reportClause";
828
829 $inProgressInvalidation = Db::fetchOne($sql, $bind);
830 return $inProgressInvalidation;
831 }
832
833 /**
834 * Returns true if there is an archive that exists that can be used when aggregating an archive for $period.
835 *
836 * @param $idSite
837 * @param Period $period
838 * @return bool
839 * @throws Exception
840 */
841 public function hasChildArchivesInPeriod($idSite, Period $period)
842 {
843 $date = $period->getDateStart();
844 while ($date->isEarlier($period->getDateEnd()->addPeriod(1, 'month'))) {
845 $archiveTable = ArchiveTableCreator::getNumericTable($date);
846
847 $sql = "SELECT idarchive
848 FROM `$archiveTable`
849 WHERE idsite = ? AND date1 >= ? AND date2 <= ? AND period < ? AND `name` LIKE 'done%' AND `value` = " . ArchiveWriter::DONE_OK . "
850 LIMIT 1";
851 $bind = [$idSite, $period->getDateStart()->getDatetime(), $period->getDateEnd()->getDatetime(), $period->getId()];
852
853 $result = (bool) Db::fetchOne($sql, $bind);
854 if ($result) {
855 return true;
856 }
857
858 $date = $date->addPeriod(1, 'month'); // move to next archive table
859 }
860 return false;
861 }
862
863 public function deleteInvalidationsForSites(array $idSites)
864 {
865 $idSites = array_map('intval', $idSites);
866
867 $table = Common::prefixTable('archive_invalidations');
868 $sql = "DELETE FROM `$table` WHERE idsite IN (" . implode(',', $idSites) . ")";
869
870 Db::query($sql);
871 }
872
873 public function deleteInvalidationsForDeletedSites()
874 {
875 $siteTable = Common::prefixTable('site');
876 $table = Common::prefixTable('archive_invalidations');
877 $sql = "DELETE a FROM `$table` a LEFT JOIN `$siteTable` s ON a.idsite = s.idsite WHERE s.idsite IS NULL";
878 Db::query($sql);
879 }
880
881 private function getRemoveInvalidationsIdSitesClause($idSite)
882 {
883 if ($idSite === 'all') {
884 return '';
885 }
886
887 $idSites = is_array($idSite) ? $idSite : [$idSite];
888 $idSites = array_map('intval', $idSites);
889 $idSitesStr = implode(',', $idSites);
890
891 return "idsite IN ($idSitesStr) AND";
892 }
893
894 public function releaseInProgressInvalidation($idinvalidation)
895 {
896 $table = Common::prefixTable('archive_invalidations');
897 $sql = "UPDATE $table SET status = " . ArchiveInvalidator::INVALIDATION_STATUS_QUEUED . ", ts_started = NULL WHERE idinvalidation = ?";
898 Db::query($sql, [$idinvalidation]);
899 }
900
901 public function resetFailedArchivingJobs()
902 {
903 $table = Common::prefixTable('archive_invalidations');
904 $sql = "UPDATE $table SET status = ? WHERE status = ? AND (ts_started IS NULL OR ts_started < ?)";
905
906 $bind = [
907 ArchiveInvalidator::INVALIDATION_STATUS_QUEUED,
908 ArchiveInvalidator::INVALIDATION_STATUS_IN_PROGRESS,
909 Date::now()->subDay(1)->getDatetime(),
910 ];
911
912 $query = Db::query($sql, $bind);
913 return $query->rowCount();
914 }
915 }
916