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