PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.13.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.13.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 / Archive / ArchiveInvalidator.php
matomo / app / core / Archive Last commit date
ArchiveInvalidator 3 years ago ArchiveInvalidator.php 3 years ago ArchivePurger.php 5 years ago ArchiveQuery.php 5 years ago ArchiveQueryFactory.php 5 years ago Chunk.php 5 years ago DataCollection.php 3 years ago DataTableFactory.php 4 years ago Parameters.php 5 years ago
ArchiveInvalidator.php
798 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
10 namespace Piwik\Archive;
11
12 use Piwik\Archive\ArchiveInvalidator\InvalidationResult;
13 use Piwik\ArchiveProcessor\Rules;
14 use Piwik\Common;
15 use Piwik\Container\StaticContainer;
16 use Piwik\CronArchive\ReArchiveList;
17 use Piwik\CronArchive\SegmentArchiving;
18 use Piwik\DataAccess\ArchiveTableCreator;
19 use Piwik\DataAccess\Model;
20 use Piwik\Date;
21 use Piwik\Db;
22 use Piwik\Option;
23 use Piwik\Period;
24 use Piwik\Piwik;
25 use Piwik\Plugin\Manager;
26 use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList;
27 use Piwik\Plugins\PrivacyManager\PrivacyManager;
28 use Piwik\Segment;
29 use Piwik\SettingsServer;
30 use Piwik\Site;
31 use Piwik\Tracker\Cache;
32 use Psr\Log\LoggerInterface;
33
34 /**
35 * Service that can be used to invalidate archives or add archive references to a list so they will
36 * be invalidated later.
37 *
38 * Archives are put in an "invalidated" state by setting the done flag to `ArchiveWriter::DONE_INVALIDATED`.
39 * This class also adds the archive's associated site to the a distributed list and adding the archive's year month to another
40 * distributed list.
41 *
42 * CronArchive will reprocess the archive data for all sites in the first list, and a scheduled task
43 * will purge the old, invalidated data in archive tables identified by the second list.
44 *
45 * Until CronArchive, or browser triggered archiving, re-processes data for an invalidated archive, the invalidated
46 * archive data will still be displayed in the UI and API.
47 *
48 * ### Deferred Invalidation
49 *
50 * Invalidating archives means running queries on one or more archive tables. In some situations, like during
51 * tracking, this is not desired. In such cases, archive references can be added to a list via the
52 * rememberToInvalidateArchivedReportsLater method, which will add the reference to a distributed list
53 *
54 * Later, during Piwik's normal execution, the list will be read and every archive it references will
55 * be invalidated.
56 */
57 class ArchiveInvalidator
58 {
59 const TRACKER_CACHE_KEY = 'ArchiveInvalidator.rememberToInvalidate';
60
61 const INVALIDATION_STATUS_QUEUED = 0;
62 const INVALIDATION_STATUS_IN_PROGRESS = 1;
63
64 private $rememberArchivedReportIdStart = 'report_to_invalidate_';
65
66 /**
67 * @var Model
68 */
69 private $model;
70
71 /**
72 * @var SegmentArchiving
73 */
74 private $segmentArchiving;
75
76 /**
77 * @var LoggerInterface
78 */
79 private $logger;
80
81 /**
82 * @var int[]
83 */
84 private $allIdSitesCache;
85
86 public function __construct(Model $model, LoggerInterface $logger)
87 {
88 $this->model = $model;
89 $this->segmentArchiving = null;
90 $this->logger = $logger;
91 }
92
93 public function getAllRememberToInvalidateArchivedReportsLater()
94 {
95 // we do not really have to get the value first. we could simply always try to call set() and it would update or
96 // insert the record if needed but we do not want to lock the table (especially since there are still some
97 // MyISAM installations)
98 $values = Option::getLike('%' . str_replace('_', '\_', $this->rememberArchivedReportIdStart) . '%');
99
100 $all = [];
101 foreach ($values as $name => $value) {
102 $suffix = substr($name, strpos($name, $this->rememberArchivedReportIdStart));
103 $suffix = str_replace($this->rememberArchivedReportIdStart, '', $suffix);
104 list($idSite, $dateStr) = explode('_', $suffix);
105
106 $all[$idSite][$dateStr] = $value;
107 }
108 return $all;
109 }
110
111 public function rememberToInvalidateArchivedReportsLater($idSite, Date $date)
112 {
113 if (SettingsServer::isTrackerApiRequest()) {
114 $value = $this->getRememberedArchivedReportsOptionFromTracker($idSite, $date->toString());
115 } else {
116 // To support multiple transactions at once, look for any other process to have set (and committed)
117 // this report to be invalidated.
118 $key = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date->toString());
119
120 // we do not really have to get the value first. we could simply always try to call set() and it would update or
121 // insert the record if needed but we do not want to lock the table (especially since there are still some
122 // MyISAM installations)
123 $value = Option::getLike('%' . str_replace('_', '\_', $key) . '%');
124 }
125
126 // getLike() returns an empty array rather than 'false'
127 if (empty($value)) {
128 // In order to support multiple concurrent transactions, add our pid to the end of the key so that it will just insert
129 // rather than waiting on some other process to commit before proceeding.The issue is that with out this, more than
130 // one process is trying to add the exact same value to the table, which causes contention. With the pid suffixed to
131 // the value, each process can successfully enter its own row in the table. The net result will be the same. We could
132 // always just set this, but it would result in a lot of rows in the options table.. more than needed. With this
133 // change you'll have at most N rows per date/site, where N is the number of parallel requests on this same idsite/date
134 // that happen to run in overlapping transactions.
135 $mykey = $this->buildRememberArchivedReportIdProcessSafe($idSite, $date->toString());
136 Option::set($mykey, '1');
137 Cache::clearCacheGeneral();
138 return $mykey;
139 }
140 }
141
142 private function getRememberedArchivedReportsOptionFromTracker($idSite, $dateStr)
143 {
144 $cacheKey = self::TRACKER_CACHE_KEY;
145
146 $generalCache = Cache::getCacheGeneral();
147 if (empty($generalCache[$cacheKey][$idSite][$dateStr])) {
148 return [];
149 }
150
151 return $generalCache[$cacheKey][$idSite][$dateStr];
152 }
153
154 public function getRememberedArchivedReportsThatShouldBeInvalidated()
155 {
156 $reports = Option::getLike('%' . str_replace('_', '\_', $this->rememberArchivedReportIdStart) . '%\_%');
157
158 $sitesPerDay = array();
159
160 foreach ($reports as $report => $value) {
161 $report = substr($report, strpos($report, $this->rememberArchivedReportIdStart));
162 $report = str_replace($this->rememberArchivedReportIdStart, '', $report);
163 $report = explode('_', $report);
164 $siteId = (int) $report[0];
165 $date = $report[1];
166
167 if (empty($siteId)) {
168 continue;
169 }
170
171 if (empty($sitesPerDay[$date])) {
172 $sitesPerDay[$date] = array();
173 }
174
175 $sitesPerDay[$date][] = $siteId;
176 }
177
178 return $sitesPerDay;
179 }
180
181 private function buildRememberArchivedReportIdForSite($idSite)
182 {
183 return $this->rememberArchivedReportIdStart . (int) $idSite;
184 }
185
186 private function buildRememberArchivedReportIdForSiteAndDate($idSite, $date)
187 {
188 $id = $this->buildRememberArchivedReportIdForSite($idSite);
189 $id .= '_' . trim($date);
190
191 return $id;
192 }
193
194 // This version is multi process safe on the insert of a new date to invalidate.
195 private function buildRememberArchivedReportIdProcessSafe($idSite, $date)
196 {
197 $id = Common::getRandomString(4, 'abcdefghijklmnoprstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') . '_';
198 $id .= $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date);
199 $id .= '_' . Common::getProcessId();
200
201 return $id;
202 }
203
204 public function forgetRememberedArchivedReportsToInvalidateForSite($idSite)
205 {
206 $id = $this->buildRememberArchivedReportIdForSite($idSite) . '_';
207 $hasDeletedSomething = $this->deleteOptionLike($id);
208 if ($hasDeletedSomething) {
209 Cache::clearCacheGeneral();
210 }
211 }
212
213 /**
214 * @internal
215 * After calling this method, make sure to call Cache::clearCacheGeneral(); For performance reasons we don't call
216 * this here immediately in case there are multiple invalidations.
217 */
218 public function forgetRememberedArchivedReportsToInvalidate($idSite, Date $date)
219 {
220 $id = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date->toString());
221
222 // The process pid is added to the end of the entry in order to support multiple concurrent transactions.
223 // So this must be a deleteLike call to get all the entries, where there used to only be one.
224 return $this->deleteOptionLike($id);
225 }
226
227 /**
228 * @param $id
229 * @return bool true if a record was deleted, false otherwise.
230 * @throws \Zend_Db_Statement_Exception
231 */
232 private function deleteOptionLike($id)
233 {
234 // we're not using deleteLike since it maybe could cause deadlocks see https://github.com/matomo-org/matomo/issues/15545
235 // we want to reduce number of rows scanned and only delete specific primary key
236 $keys = Option::getLike('%' . str_replace('_', '\_', $id) . '%');
237
238 if (empty($keys)) {
239 return false;
240 }
241
242 $keys = array_keys($keys);
243
244 $placeholders = Common::getSqlStringFieldsArray($keys);
245
246 $table = Common::prefixTable('option');
247 $db = Db::query('DELETE FROM `' . $table . '` WHERE `option_name` IN (' . $placeholders . ')', $keys);
248 return (bool) $db->rowCount();
249 }
250
251 /**
252 * @param $idSites int[]
253 * @param $dates Date[]|string[]
254 * @param $period string
255 * @param $segment Segment
256 * @param bool $cascadeDown
257 * @param bool $forceInvalidateNonexistentRanges set true to force inserting rows for ranges in archive_invalidations
258 * @param string $name null to make sure every plugin is archived when this invalidation is processed by core:archive,
259 * or a plugin name to only archive the specific plugin.
260 * @param bool $ignorePurgeLogDataDate
261 * @return InvalidationResult
262 * @throws \Exception
263 */
264 public function markArchivesAsInvalidated(array $idSites, array $dates, $period, Segment $segment = null, $cascadeDown = false,
265 $forceInvalidateNonexistentRanges = false, $name = null, $ignorePurgeLogDataDate = false)
266 {
267 $plugin = null;
268 if ($name && strpos($name, '.') !== false) {
269 list($plugin) = explode('.', $name);
270 }
271
272 if ($plugin
273 && !Manager::getInstance()->isPluginActivated($plugin)
274 ) {
275 throw new \Exception("Plugin is not activated: '$plugin'");
276 }
277
278 $invalidationInfo = new InvalidationResult();
279
280 // quick fix for #15086, if we're only invalidating today's date for a site, don't add the site to the list of sites
281 // to reprocess.
282 $hasMoreThanJustToday = [];
283 foreach ($idSites as $idSite) {
284 $hasMoreThanJustToday[$idSite] = true;
285 $tz = Site::getTimezoneFor($idSite);
286
287 if (($period == 'day' || $period === false)
288 && count($dates) == 1
289 && ((string)$dates[0]) == ((string)Date::factoryInTimezone('today', $tz))
290 ) {
291 // date is for today
292 $hasMoreThanJustToday[$idSite] = false;
293 }
294 }
295
296 /**
297 * Triggered when a Matomo user requested the invalidation of some reporting archives. Using this event, plugin
298 * developers can automatically invalidate another site, when a site is being invalidated. A plugin may even
299 * remove an idSite from the list of sites that should be invalidated to prevent it from ever being
300 * invalidated.
301 *
302 * **Example**
303 *
304 * public function getIdSitesToMarkArchivesAsInvalidates(&$idSites)
305 * {
306 * if (in_array(1, $idSites)) {
307 * $idSites[] = 5; // when idSite 1 is being invalidated, also invalidate idSite 5
308 * }
309 * }
310 *
311 * @param array &$idSites An array containing a list of site IDs which are requested to be invalidated.
312 */
313 Piwik::postEvent('Archiving.getIdSitesToMarkArchivesAsInvalidated', array(&$idSites));
314 // we trigger above event on purpose here and it is good that the segment was created like
315 // `new Segment($segmentString, $idSites)` because when a user adds a site via this event, the added idSite
316 // might not have this segment meaning we avoid a possible error. For the workflow to work, any added or removed
317 // idSite does not need to be added to $segment.
318
319 $datesToInvalidate = $this->removeDatesThatHaveBeenPurged($dates, $period, $invalidationInfo, $ignorePurgeLogDataDate);
320
321 $allPeriodsToInvalidate = $this->getAllPeriodsByYearMonth($period, $datesToInvalidate, $cascadeDown);
322
323 $this->markArchivesInvalidated($idSites, $allPeriodsToInvalidate, $segment, $period != 'range', $forceInvalidateNonexistentRanges, $name);
324
325 $isInvalidatingDays = $period == 'day' || $cascadeDown || empty($period);
326 $isNotInvalidatingSegment = empty($segment) || empty($segment->getString());
327
328 if ($isInvalidatingDays
329 && $isNotInvalidatingSegment
330 ) {
331
332 $hasDeletedAny = false;
333
334 foreach ($idSites as $idSite) {
335 foreach ($dates as $date) {
336 if (is_string($date)) {
337 $date = Date::factory($date);
338 }
339
340 $hasDeletedAny = $this->forgetRememberedArchivedReportsToInvalidate($idSite, $date) || $hasDeletedAny;
341 }
342 }
343
344 if ($hasDeletedAny) {
345 Cache::clearCacheGeneral();
346 }
347 }
348
349 return $invalidationInfo;
350 }
351
352 private function getAllPeriodsByYearMonth($periodOrAll, $dates, $cascadeDown, &$result = [])
353 {
354 $periods = $periodOrAll ? [$periodOrAll] : ['day'];
355 foreach ($periods as $period) {
356 foreach ($dates as $date) {
357 $periodObj = $this->makePeriod($date, $period);
358
359 $result[$this->getYearMonth($periodObj)][$this->getUniquePeriodId($periodObj)] = $periodObj;
360
361 // cascade down
362 if ($cascadeDown
363 && $period != 'range'
364 ) {
365 $this->addChildPeriodsByYearMonth($result, $periodObj);
366 }
367
368 // cascade up
369 // if the period spans multiple years or months, it won't be used when aggregating parent periods, so
370 // we can avoid invalidating it
371 if ($this->shouldPropagateUp($periodObj)
372 && $period != 'range'
373 ) {
374 $this->addParentPeriodsByYearMonth($result, $periodObj);
375 }
376 }
377 }
378
379 return $result;
380 }
381
382 private function shouldPropagateUp(Period $periodObj)
383 {
384 return $periodObj->getDateStart()->toString('Y') == $periodObj->getDateEnd()->toString('Y')
385 && $periodObj->getDateStart()->toString('m') == $periodObj->getDateEnd()->toString('m');
386 }
387
388 private function addChildPeriodsByYearMonth(&$result, Period $period)
389 {
390 if ($period->getLabel() == 'range') {
391 return;
392 } else if ($period->getLabel() == 'day'
393 && $this->shouldPropagateUp($period)
394 ) {
395 $this->addParentPeriodsByYearMonth($result, $period);
396 return;
397 }
398
399 foreach ($period->getSubperiods() as $subperiod) {
400 $result[$this->getYearMonth($subperiod)][$this->getUniquePeriodId($subperiod)] = $subperiod;
401 $this->addChildPeriodsByYearMonth($result, $subperiod);
402 }
403 }
404
405 private function addParentPeriodsByYearMonth(&$result, Period $period, Date $originalDate = null)
406 {
407 if ($period->getLabel() == 'year'
408 || $period->getLabel() == 'range'
409 || !Period\Factory::isPeriodEnabledForAPI($period->getParentPeriodLabel())
410 ) {
411 return;
412 }
413
414 $originalDate = $originalDate ?? $period->getDateStart();
415
416 $parentPeriod = Period\Factory::build($period->getParentPeriodLabel(), $originalDate);
417 $result[$this->getYearMonth($parentPeriod)][$this->getUniquePeriodId($parentPeriod)] = $parentPeriod;
418 $this->addParentPeriodsByYearMonth($result, $parentPeriod, $originalDate);
419 }
420
421 /**
422 * @param $idSites int[]
423 * @param $dates Date[]
424 * @param $period string
425 * @param $segment Segment
426 * @param bool $cascadeDown
427 * @return InvalidationResult
428 * @throws \Exception
429 */
430 public function markArchivesOverlappingRangeAsInvalidated(array $idSites, array $dates, Segment $segment = null)
431 {
432 $invalidationInfo = new InvalidationResult();
433
434 $ranges = array();
435 foreach ($dates as $dateRange) {
436 $ranges[] = Period\Factory::build('range', $dateRange[0] . ',' . $dateRange[1]);
437 }
438
439 $invalidatedMonths = array();
440 $archiveNumericTables = ArchiveTableCreator::getTablesArchivesInstalled($type = ArchiveTableCreator::NUMERIC_TABLE);
441 foreach ($archiveNumericTables as $table) {
442 $tableDate = ArchiveTableCreator::getDateFromTableName($table);
443
444 $rowsAffected = $this->model->updateArchiveAsInvalidated($table, $idSites, $ranges, $segment);
445 if ($rowsAffected > 0) {
446 $invalidatedMonths[] = $tableDate;
447 }
448 }
449
450 foreach ($idSites as $idSite) {
451 foreach ($dates as $dateRange) {
452 $this->forgetRememberedArchivedReportsToInvalidate($idSite, $dateRange[0]);
453 $invalidationInfo->processedDates[] = $dateRange[0];
454 }
455 }
456
457 Cache::clearCacheGeneral();
458
459 return $invalidationInfo;
460 }
461
462 /**
463 * Schedule rearchiving of reports for a single plugin or single report for N months in the past. The next time
464 * core:archive is run, they will be processed.
465 *
466 * @param int[]|string $idSites A list of idSites or 'all'
467 * @param string $plugin
468 * @param string|null $report
469 * @param Date|null $startDate
470 * @throws \Exception
471 * @api
472 */
473 public function reArchiveReport($idSites, string $plugin = null, string $report = null, Date $startDate = null, Segment $segment = null)
474 {
475 $date2 = Date::today();
476
477 $earliestDateToRearchive = Piwik::getEarliestDateToRearchive();
478 if (empty($startDate)) {
479 if (empty($earliestDateToRearchive)) {
480 return null; // INI setting set to 0 months so no rearchiving
481 }
482
483 $startDate = $earliestDateToRearchive;
484 } else if (!empty($earliestDateToRearchive)) {
485 // don't allow archiving further back than the rearchive_reports_in_past_last_n_months date allows
486 $startDate = $startDate->isEarlier($earliestDateToRearchive) ? $earliestDateToRearchive : $startDate;
487 }
488
489 if ($idSites === 'all') {
490 $idSites = $this->getAllSitesId();
491 }
492
493 $dates = [];
494 $date = $startDate;
495 while ($date->isEarlier($date2)) {
496 $dates[] = $date;
497 $date = $date->addDay(1);
498 }
499
500 if (empty($dates)) {
501 return;
502 }
503
504 $name = $plugin;
505 if (!empty($report)) {
506 $name .= '.' . $report;
507 }
508
509 $this->markArchivesAsInvalidated($idSites, $dates, 'day', $segment, $cascadeDown = false, $forceInvalidateRanges = false, $name);
510 if (empty($segment)
511 && Rules::shouldProcessSegmentsWhenReArchivingReports()
512 ) {
513 foreach ($idSites as $idSite) {
514 foreach (Rules::getSegmentsToProcess([$idSite]) as $segment) {
515 $this->markArchivesAsInvalidated($idSites, $dates, 'day', new Segment($segment, [$idSite]),
516 $cascadeDown = false, $forceInvalidateRanges = false, $name);
517 }
518 }
519 }
520 }
521
522 /**
523 * Remove invalidations for a specific report or all invalidations for a specific plugin. If your plugin supports
524 * archiving data in the past, you may want to call this method to remove any pending invalidations if, for example,
525 * your plugin is deactivated or a report deleted.
526 *
527 * @param int|int[] $idSite one or more site IDs or 'all' for all site IDs
528 * @param string $string
529 * @param string|null $report
530 */
531 public function removeInvalidations($idSite, $plugin, $report = null)
532 {
533 if (empty($report)) {
534 $this->model->removeInvalidationsLike($idSite, $plugin);
535 } else {
536 $this->model->removeInvalidations($idSite, $plugin, $report);
537 }
538 }
539
540 /**
541 * Schedules a re-archiving reports without propagating exceptions. This is scheduled
542 * since adding invalidations can take a long time and delay UI response times.
543 *
544 * @param int|int[]|'all' $idSites
545 * @param string|int $pluginName
546 * @param string|null $report
547 * @param Date|null $startDate
548 */
549 public function scheduleReArchiving($idSites, string $pluginName = null, $report = null, Date $startDate = null,
550 Segment $segment = null)
551 {
552 if (!empty($report)) {
553 $this->removeInvalidationsSafely($idSites, $pluginName, $report);
554 }
555 try {
556 $reArchiveList = new ReArchiveList($this->logger);
557 $reArchiveList->add(json_encode([
558 'idSites' => $idSites,
559 'pluginName' => $pluginName,
560 'report' => $report,
561 'startDate' => $startDate ? $startDate->getTimestamp() : null,
562 'segment' => $segment ? $segment->getOriginalString() : null,
563 ]));
564 } catch (\Throwable $ex) {
565 $this->logger->info("Failed to schedule rearchiving of past reports for $pluginName plugin.");
566 }
567 }
568
569 /**
570 * Applies the queued archiving rearchiving entries.
571 */
572 public function applyScheduledReArchiving()
573 {
574 $reArchiveList = new ReArchiveList($this->logger);
575 $items = $reArchiveList->getAll();
576
577 foreach ($items as $item) {
578 try {
579 $entry = @json_decode($item, true);
580 if (empty($entry)) {
581 continue;
582 }
583
584 $idSites = Site::getIdSitesFromIdSitesString($entry['idSites']);
585
586 $this->reArchiveReport(
587 $idSites,
588 $entry['pluginName'],
589 $entry['report'],
590 !empty($entry['startDate']) ? Date::factory((int) $entry['startDate']) : null,
591 !empty($entry['segment']) ? new Segment($entry['segment'], $idSites) : null
592 );
593 } catch (\Throwable $ex) {
594 $this->logger->info("Failed to create invalidations for report re-archiving (idSites = {idSites}, pluginName = {pluginName}, report = {report}, startDate = {startDateTs}): {ex}", [
595 'idSites' => json_encode($entry['idSites']),
596 'pluginName' => $entry['pluginName'],
597 'report' => $entry['report'],
598 'startDateTs' => $entry['startDate'],
599 'ex' => $ex,
600 ]);
601 } finally {
602 $reArchiveList->remove([$item]);
603 }
604 }
605 }
606
607 /**
608 * Calls removeInvalidations() without propagating exceptions.
609 *
610 * @param int|int[]|'all' $idSites
611 * @param string $pluginName
612 * @param string|null $report
613 */
614 public function removeInvalidationsSafely($idSites, $pluginName, $report = null)
615 {
616 try {
617 $this->removeInvalidations($idSites, $pluginName, $report);
618 $this->removeInvalidationsFromDistributedList($idSites, $pluginName, $report);
619 } catch (\Throwable $ex) {
620 $logger = StaticContainer::get(LoggerInterface::class);
621 $logger->debug("Failed to remove invalidations the for $pluginName plugin.");
622 }
623 }
624
625 public function removeInvalidationsFromDistributedList($idSites, $pluginName = null, $report = null)
626 {
627 $list = new ReArchiveList();
628 $entries = $list->getAll();
629
630 if ($idSites === 'all') {
631 $idSites = $this->getAllSitesId();
632 }
633
634 foreach ($entries as $index => $entry) {
635 $entry = @json_decode($entry, true);
636 if (empty($entry)) {
637 unset($entries[$index]);
638 continue;
639 }
640
641 $entryPluginName = $entry['pluginName'];
642 if (!empty($pluginName)
643 && $pluginName != $entryPluginName
644 ) {
645 continue;
646 }
647
648 $entryReport = $entry['report'];
649 if (!empty($pluginName)
650 && !empty($report)
651 && $report != $entryReport
652 ) {
653 continue;
654 }
655
656 $sitesInEntry = $entry['idSites'];
657 if ($sitesInEntry === 'all') {
658 $sitesInEntry = $this->getAllSitesId();
659 }
660
661 $diffSites = array_diff($sitesInEntry, $idSites);
662 if (empty($diffSites)) {
663 unset($entries[$index]);
664 continue;
665 }
666
667 $entry['idSites'] = $diffSites;
668
669 $entries[$index] = json_encode($entry);
670 }
671
672 $list->setAll(array_values($entries));
673 }
674
675 /**
676 * @param int[] $idSites
677 * @param string[][][] $dates
678 * @throws \Exception
679 */
680 private function markArchivesInvalidated($idSites, $dates, Segment $segment = null, $removeRanges = false,
681 $forceInvalidateNonexistentRanges = false, $name = null)
682 {
683 $idSites = array_map('intval', $idSites);
684
685 $yearMonths = [];
686
687 foreach ($dates as $tableDate => $datesForTable) {
688 $tableDateObj = Date::factory($tableDate);
689
690 $table = ArchiveTableCreator::getNumericTable($tableDateObj);
691 $yearMonths[] = $tableDateObj->toString('Y_m');
692
693 $this->model->updateArchiveAsInvalidated($table, $idSites, $datesForTable, $segment, $forceInvalidateNonexistentRanges, $name);
694
695 if ($removeRanges) {
696 $this->model->updateRangeArchiveAsInvalidated($table, $idSites, $datesForTable, $segment);
697 }
698 }
699
700 $this->markInvalidatedArchivesForReprocessAndPurge($yearMonths);
701 }
702
703 /**
704 * @param Date[] $dates
705 * @param InvalidationResult $invalidationInfo
706 * @return \Piwik\Date[]
707 */
708 private function removeDatesThatHaveBeenPurged($dates, $period, InvalidationResult $invalidationInfo, $ignorePurgeLogDataDate)
709 {
710 $this->findOlderDateWithLogs($invalidationInfo);
711
712 $result = array();
713 foreach ($dates as $date) {
714 $periodObj = $this->makePeriod($date, $period ?: 'day');
715
716 // we should only delete reports for dates that are more recent than N days
717 if ($invalidationInfo->minimumDateWithLogs
718 && !$ignorePurgeLogDataDate
719 && ($periodObj->getDateEnd()->isEarlier($invalidationInfo->minimumDateWithLogs)
720 || $periodObj->getDateStart()->isEarlier($invalidationInfo->minimumDateWithLogs))
721 ) {
722 $invalidationInfo->warningDates[] = $date;
723 continue;
724 }
725
726 $result[] = $date;
727 $invalidationInfo->processedDates[] = $date;
728 }
729 return $result;
730 }
731
732 private function findOlderDateWithLogs(InvalidationResult $info)
733 {
734 // If using the feature "Delete logs older than N days"...
735 $purgeDataSettings = PrivacyManager::getPurgeDataSettings();
736 $logsDeletedWhenOlderThanDays = (int)$purgeDataSettings['delete_logs_older_than'];
737 $logsDeleteEnabled = $purgeDataSettings['delete_logs_enable'];
738
739 if ($logsDeleteEnabled
740 && $logsDeletedWhenOlderThanDays
741 ) {
742 $info->minimumDateWithLogs = Date::factory('today')->subDay($logsDeletedWhenOlderThanDays);
743 }
744 }
745
746 /**
747 * @param array $idSites
748 * @param array $yearMonths
749 */
750 private function markInvalidatedArchivesForReprocessAndPurge($yearMonths)
751 {
752 $archivesToPurge = new ArchivesToPurgeDistributedList();
753 $archivesToPurge->add($yearMonths);
754 }
755
756 private function getYearMonth(Period $period)
757 {
758 return $period->getDateStart()->toString('Y-m-01');
759 }
760
761 private function getUniquePeriodId(Period $period)
762 {
763 return $period->getId() . '.' . $period->getRangeString();
764 }
765
766 private function makePeriod($date, $period)
767 {
768 if ($period === 'range'
769 && strpos($date, ',') === false
770 ) {
771 $date = $date . ',' . $date;
772 return new Period\Range('range', $date);
773 } else {
774 return Period\Factory::build($period, $date);
775 }
776 }
777
778 private function getSegmentArchiving()
779 {
780 if (empty($this->segmentArchiving)) {
781 $this->segmentArchiving = new SegmentArchiving();
782 }
783 return $this->segmentArchiving;
784 }
785
786 private function getAllSitesId()
787 {
788 if (isset($this->allIdSitesCache)) {
789 return $this->allIdSitesCache;
790 }
791
792 $model = new \Piwik\Plugins\SitesManager\Model();
793 $this->allIdSitesCache = $model->getSitesId();
794 return $this->allIdSitesCache;
795 }
796
797 }
798