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