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