| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Archive; |
|
|
| use Piwik\Archive\ArchiveInvalidator\InvalidationResult; |
| use Piwik\ArchiveProcessor\Rules; |
| use Piwik\Common; |
| use Piwik\Container\StaticContainer; |
| use Piwik\CronArchive\ReArchiveList; |
| use Piwik\CronArchive\SegmentArchiving; |
| use Piwik\DataAccess\ArchiveTableCreator; |
| use Piwik\DataAccess\Model; |
| use Piwik\Date; |
| use Piwik\Db; |
| use Piwik\Option; |
| use Piwik\Period; |
| use Piwik\Piwik; |
| use Piwik\Plugin\Manager; |
| use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList; |
| use Piwik\Plugins\PrivacyManager\PrivacyManager; |
| use Piwik\Segment; |
| use Piwik\SettingsServer; |
| use Piwik\Site; |
| use Piwik\Tracker\Cache; |
| use Piwik\Log\LoggerInterface; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class ArchiveInvalidator |
| { |
| public const TRACKER_CACHE_KEY = 'ArchiveInvalidator.rememberToInvalidate'; |
|
|
| public const INVALIDATION_STATUS_QUEUED = 0; |
| public const INVALIDATION_STATUS_IN_PROGRESS = 1; |
|
|
| private $rememberArchivedReportIdStart = 'report_to_invalidate_'; |
|
|
| |
| |
| |
| private $model; |
|
|
| |
| |
| |
| private $segmentArchiving; |
|
|
| |
| |
| |
| private $logger; |
|
|
| |
| |
| |
| private $allIdSitesCache; |
|
|
| public function __construct(Model $model, LoggerInterface $logger) |
| { |
| $this->model = $model; |
| $this->segmentArchiving = null; |
| $this->logger = $logger; |
| } |
|
|
| public function getAllRememberToInvalidateArchivedReportsLater() |
| { |
| |
| |
| |
| $values = Option::getLike('%' . str_replace('_', '\_', $this->rememberArchivedReportIdStart) . '%'); |
|
|
| $all = []; |
| foreach ($values as $name => $value) { |
| $suffix = substr($name, strpos($name, $this->rememberArchivedReportIdStart)); |
| $suffix = str_replace($this->rememberArchivedReportIdStart, '', $suffix); |
| [$idSite, $dateStr] = explode('_', $suffix); |
|
|
| $all[$idSite][$dateStr] = $value; |
| } |
| return $all; |
| } |
|
|
| public function rememberToInvalidateArchivedReportsLater($idSite, Date $date) |
| { |
| if (SettingsServer::isTrackerApiRequest()) { |
| $value = $this->getRememberedArchivedReportsOptionFromTracker($idSite, $date->toString()); |
| } else { |
| |
| |
| $key = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date->toString()); |
|
|
| |
| |
| |
| $value = Option::getLike('%' . str_replace('_', '\_', $key) . '%'); |
| } |
|
|
| |
| if (empty($value)) { |
| |
| |
| |
| |
| |
| |
| |
| $mykey = $this->buildRememberArchivedReportIdProcessSafe($idSite, $date->toString()); |
| Option::set($mykey, '1'); |
| Cache::clearCacheGeneral(); |
| return $mykey; |
| } |
| } |
|
|
| private function getRememberedArchivedReportsOptionFromTracker($idSite, $dateStr) |
| { |
| $cacheKey = self::TRACKER_CACHE_KEY; |
|
|
| $generalCache = Cache::getCacheGeneral(); |
| if (empty($generalCache[$cacheKey][$idSite][$dateStr])) { |
| return []; |
| } |
|
|
| return $generalCache[$cacheKey][$idSite][$dateStr]; |
| } |
|
|
| public function getDaysWithRememberedInvalidationsForSite(int $idSite): array |
| { |
| return array_keys($this->getRememberedArchivedReportsThatShouldBeInvalidated($idSite)); |
| } |
|
|
| public function getRememberedArchivedReportsThatShouldBeInvalidated(?int $idSite = null) |
| { |
| if (null === $idSite) { |
| $optionName = $this->rememberArchivedReportIdStart . '%'; |
| } else { |
| $optionName = $this->buildRememberArchivedReportIdForSite($idSite); |
| } |
|
|
| $reports = Option::getLike('%' . str_replace('_', '\_', $optionName) . '\_%'); |
| $sitesPerDay = []; |
|
|
| foreach ($reports as $report => $value) { |
| $report = substr($report, strpos($report, $this->rememberArchivedReportIdStart)); |
| $report = str_replace($this->rememberArchivedReportIdStart, '', $report); |
| $report = explode('_', $report); |
| $siteId = (int) $report[0]; |
| $date = $report[1]; |
|
|
| if (empty($siteId)) { |
| continue; |
| } |
|
|
| if (empty($sitesPerDay[$date])) { |
| $sitesPerDay[$date] = []; |
| } |
|
|
| $sitesPerDay[$date][] = $siteId; |
| } |
|
|
| return $sitesPerDay; |
| } |
|
|
| private function buildRememberArchivedReportIdForSite($idSite) |
| { |
| return $this->rememberArchivedReportIdStart . (int) $idSite; |
| } |
|
|
| private function buildRememberArchivedReportIdForSiteAndDate($idSite, $date) |
| { |
| $id = $this->buildRememberArchivedReportIdForSite($idSite); |
| $id .= '_' . trim($date); |
|
|
| return $id; |
| } |
|
|
| |
| private function buildRememberArchivedReportIdProcessSafe($idSite, $date) |
| { |
| $id = Common::getRandomString(4, 'abcdefghijklmnoprstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') . '_'; |
| $id .= $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date); |
| $id .= '_' . Common::getProcessId(); |
|
|
| return $id; |
| } |
|
|
| public function forgetRememberedArchivedReportsToInvalidateForSite($idSite) |
| { |
| $id = $this->buildRememberArchivedReportIdForSite($idSite) . '_'; |
| $hasDeletedSomething = $this->deleteOptionLike($id); |
| if ($hasDeletedSomething) { |
| Cache::clearCacheGeneral(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| public function forgetRememberedArchivedReportsToInvalidate($idSite, Date $date) |
| { |
| $id = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date->toString()); |
|
|
| |
| |
| return $this->deleteOptionLike($id); |
| } |
|
|
| |
| |
| |
| |
| |
| private function deleteOptionLike($id) |
| { |
| |
| |
| $keys = Option::getLike('%' . str_replace('_', '\_', $id) . '%'); |
|
|
| if (empty($keys)) { |
| return false; |
| } |
|
|
| $keys = array_keys($keys); |
|
|
| $placeholders = Common::getSqlStringFieldsArray($keys); |
|
|
| $table = Common::prefixTable('option'); |
| $db = Db::query('DELETE FROM `' . $table . '` WHERE `option_name` IN (' . $placeholders . ')', $keys); |
| return (bool) $db->rowCount(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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 |
| ) { |
| $plugin = null; |
| if ($name && strpos($name, '.') !== false) { |
| [$plugin] = explode('.', $name); |
| } elseif ($name) { |
| $plugin = $name; |
| } |
|
|
| if ( |
| $plugin |
| && !Manager::getInstance()->isPluginActivated($plugin) |
| ) { |
| throw new \Exception("Plugin is not activated: '$plugin'"); |
| } |
|
|
| $invalidationInfo = new InvalidationResult(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent('Archiving.getIdSitesToMarkArchivesAsInvalidated', array(&$idSites, $dates, $period, $segment, $name, $isPrivacyDeleteData = false)); |
| |
| |
| |
| |
|
|
| $datesToInvalidate = $this->removeDatesThatHaveBeenPurged($dates, $period, $invalidationInfo, $ignorePurgeLogDataDate); |
|
|
| $allPeriodsToInvalidate = $this->getAllPeriodsByYearMonth($period, $datesToInvalidate, $cascadeDown); |
|
|
| $this->markArchivesInvalidated($idSites, $allPeriodsToInvalidate, $segment, $period != 'range', $forceInvalidateNonexistentRanges, $name, $doNotCreateInvalidations); |
|
|
| $isInvalidatingDays = $period == 'day' || $cascadeDown || empty($period); |
| $isNotInvalidatingSegment = empty($segment) || empty($segment->getString()); |
|
|
| if ( |
| $isInvalidatingDays |
| && $isNotInvalidatingSegment |
| ) { |
| $hasDeletedAny = false; |
|
|
| foreach ($idSites as $idSite) { |
| foreach ($dates as $date) { |
| if (is_string($date)) { |
| $date = Date::factory($date); |
| } |
|
|
| $hasDeletedAny = $this->forgetRememberedArchivedReportsToInvalidate($idSite, $date) || $hasDeletedAny; |
| } |
| } |
|
|
| if ($hasDeletedAny) { |
| Cache::clearCacheGeneral(); |
| } |
| } |
|
|
| return $invalidationInfo; |
| } |
|
|
| private function getAllPeriodsByYearMonth($periodOrAll, $dates, $cascadeDown, &$result = []) |
| { |
| $periods = $periodOrAll ? [$periodOrAll] : ['day']; |
| foreach ($periods as $period) { |
| foreach ($dates as $date) { |
| $periodObj = $this->makePeriod($date, $period); |
|
|
| $result[$this->getYearMonth($periodObj)][$this->getUniquePeriodId($periodObj)] = $periodObj; |
|
|
| |
| if ( |
| $cascadeDown |
| && $period != 'range' |
| ) { |
| $this->addChildPeriodsByYearMonth($result, $periodObj); |
| } |
|
|
| |
| |
| |
| if ( |
| $this->shouldPropagateUp($periodObj) |
| && $period != 'range' |
| ) { |
| $this->addParentPeriodsByYearMonth($result, $periodObj); |
| } |
| } |
| } |
|
|
| return $result; |
| } |
|
|
| private function shouldPropagateUp(Period $periodObj) |
| { |
| return $periodObj->getDateStart()->toString('Y') == $periodObj->getDateEnd()->toString('Y') |
| && $periodObj->getDateStart()->toString('m') == $periodObj->getDateEnd()->toString('m'); |
| } |
|
|
| private function addChildPeriodsByYearMonth(&$result, Period $period) |
| { |
| if ($period->getLabel() == 'range') { |
| return; |
| } elseif ( |
| $period->getLabel() == 'day' |
| && $this->shouldPropagateUp($period) |
| ) { |
| $this->addParentPeriodsByYearMonth($result, $period); |
| return; |
| } |
|
|
| foreach ($period->getSubperiods() as $subperiod) { |
| $result[$this->getYearMonth($subperiod)][$this->getUniquePeriodId($subperiod)] = $subperiod; |
| $this->addChildPeriodsByYearMonth($result, $subperiod); |
| } |
| } |
|
|
| private function addParentPeriodsByYearMonth(&$result, Period $period, ?Date $originalDate = null) |
| { |
| if ( |
| $period->getLabel() == 'year' |
| || $period->getLabel() == 'range' |
| || !Period\Factory::isPeriodEnabledForAPI($period->getParentPeriodLabel()) |
| ) { |
| return; |
| } |
|
|
| $originalDate = $originalDate ?? $period->getDateStart(); |
|
|
| $parentPeriod = Period\Factory::build($period->getParentPeriodLabel(), $originalDate); |
| $result[$this->getYearMonth($parentPeriod)][$this->getUniquePeriodId($parentPeriod)] = $parentPeriod; |
| $this->addParentPeriodsByYearMonth($result, $parentPeriod, $originalDate); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function markArchivesOverlappingRangeAsInvalidated(array $idSites, array $dates, ?Segment $segment = null, ?string $name = null) |
| { |
| $invalidationInfo = new InvalidationResult(); |
|
|
| $ranges = array(); |
| foreach ($dates as $dateRange) { |
| $ranges[] = Period\Factory::build('range', $dateRange[0] . ',' . $dateRange[1]); |
| } |
|
|
| $archiveNumericTables = ArchiveTableCreator::getTablesArchivesInstalled($type = ArchiveTableCreator::NUMERIC_TABLE); |
| foreach ($archiveNumericTables as $table) { |
| $this->model->updateArchiveAsInvalidated($table, $idSites, $ranges, $segment, false, $name); |
| } |
|
|
| foreach ($idSites as $idSite) { |
| foreach ($dates as $dateRange) { |
| $this->forgetRememberedArchivedReportsToInvalidate($idSite, $dateRange[0]); |
| $invalidationInfo->processedDates[] = $dateRange[0]; |
| } |
| } |
|
|
| Cache::clearCacheGeneral(); |
|
|
| return $invalidationInfo; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public function reArchiveReport($idSites, ?string $plugin = null, ?string $report = null, ?Date $startDate = null, ?Segment $segment = null) |
| { |
| $date2 = Date::today(); |
|
|
| $earliestDateToRearchive = Piwik::getEarliestDateToRearchive(); |
| if (empty($startDate)) { |
| if (empty($earliestDateToRearchive)) { |
| return null; |
| } |
|
|
| $startDate = $earliestDateToRearchive; |
| } elseif (!empty($earliestDateToRearchive)) { |
| |
| $startDate = $startDate->isEarlier($earliestDateToRearchive) ? $earliestDateToRearchive : $startDate; |
| } |
|
|
| if ($idSites === 'all') { |
| $idSites = $this->getAllSitesId(); |
| } |
|
|
| $dates = []; |
| $date = $startDate; |
| while ($date->isEarlier($date2)) { |
| $dates[] = $date; |
| $date = $date->addDay(1); |
| } |
|
|
| if (empty($dates)) { |
| return; |
| } |
|
|
| $name = $plugin; |
| if (!empty($report)) { |
| $name .= '.' . $report; |
| } |
|
|
| $this->markArchivesAsInvalidated($idSites, $dates, 'day', $segment, $cascadeDown = false, $forceInvalidateRanges = false, $name); |
| if ( |
| empty($segment) |
| && Rules::shouldProcessSegmentsWhenReArchivingReports() |
| ) { |
| foreach ($idSites as $idSite) { |
| foreach (Rules::getSegmentsToProcess([$idSite]) as $segment) { |
| $this->markArchivesAsInvalidated( |
| [$idSite], |
| $dates, |
| 'day', |
| new Segment($segment, [$idSite]), |
| $cascadeDown = false, |
| $forceInvalidateRanges = false, |
| $name |
| ); |
| } |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function removeInvalidations($idSite, $plugin, $report = null) |
| { |
| if (empty($report)) { |
| $this->model->removeInvalidationsLike($idSite, $plugin); |
| } else { |
| $this->model->removeInvalidations($idSite, $plugin, $report); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function scheduleReArchiving( |
| $idSites, |
| ?string $pluginName = null, |
| $report = null, |
| ?Date $startDate = null, |
| ?Segment $segment = null |
| ) { |
| if (!empty($report)) { |
| $this->removeInvalidationsSafely($idSites, $pluginName, $report); |
| } |
| try { |
| $reArchiveList = new ReArchiveList($this->logger); |
| $reArchiveList->add(json_encode([ |
| 'idSites' => $idSites, |
| 'pluginName' => $pluginName, |
| 'report' => $report, |
| 'startDate' => $startDate ? $startDate->getTimestamp() : null, |
| 'segment' => $segment ? $segment->getOriginalString() : null, |
| ])); |
| } catch (\Throwable $ex) { |
| $this->logger->info("Failed to schedule rearchiving of past reports for $pluginName plugin."); |
| } |
| } |
|
|
| |
| |
| |
| public function applyScheduledReArchiving() |
| { |
| $reArchiveList = new ReArchiveList($this->logger); |
| $items = $reArchiveList->getAll(); |
|
|
| foreach ($items as $item) { |
| try { |
| $entry = @json_decode($item, true); |
| if (empty($entry)) { |
| continue; |
| } |
|
|
| $idSites = Site::getIdSitesFromIdSitesString($entry['idSites']); |
|
|
| $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 |
| ); |
| } catch (\Throwable $ex) { |
| $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, |
| ]); |
| } finally { |
| $reArchiveList->remove([$item]); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function removeInvalidationsSafely($idSites, $pluginName, $report = null) |
| { |
| try { |
| $this->removeInvalidations($idSites, $pluginName, $report); |
| $this->removeInvalidationsFromDistributedList($idSites, $pluginName, $report); |
| } catch (\Throwable $ex) { |
| $logger = StaticContainer::get(LoggerInterface::class); |
| $logger->debug("Failed to remove invalidations the for $pluginName plugin."); |
| } |
| } |
|
|
| public function removeInvalidationsFromDistributedList($idSites, $pluginName = null, $report = null) |
| { |
| $list = new ReArchiveList(); |
| $entries = $list->getAll(); |
|
|
| if ($idSites === 'all') { |
| $idSites = $this->getAllSitesId(); |
| } |
|
|
| |
| $idSites = is_array($idSites) ? $idSites : ($idSites !== true ? [$idSites] : []); |
|
|
| foreach ($entries as $index => $entry) { |
| $entry = @json_decode($entry, true); |
| if (empty($entry)) { |
| unset($entries[$index]); |
| continue; |
| } |
|
|
| $entryPluginName = $entry['pluginName']; |
| if ( |
| !empty($pluginName) |
| && $pluginName != $entryPluginName |
| ) { |
| continue; |
| } |
|
|
| $entryReport = $entry['report']; |
| if ( |
| !empty($pluginName) |
| && !empty($report) |
| && $report != $entryReport |
| ) { |
| continue; |
| } |
|
|
| $sitesInEntry = $entry['idSites']; |
| if ($sitesInEntry === 'all') { |
| $sitesInEntry = $this->getAllSitesId(); |
| } |
|
|
| $diffSites = array_diff($sitesInEntry, $idSites); |
| if (empty($diffSites)) { |
| unset($entries[$index]); |
| continue; |
| } |
|
|
| $entry['idSites'] = $diffSites; |
|
|
| $entries[$index] = json_encode($entry); |
| } |
|
|
| $list->setAll(array_values($entries)); |
| } |
|
|
| |
| |
| |
| |
| |
| private function markArchivesInvalidated( |
| $idSites, |
| $dates, |
| ?Segment $segment = null, |
| bool $removeRanges = false, |
| bool $forceInvalidateNonexistentRanges = false, |
| ?string $name = null, |
| bool $doNotCreateInvalidations = false |
| ) { |
| $idSites = array_map('intval', $idSites); |
|
|
| $yearMonths = []; |
|
|
| foreach ($dates as $tableDate => $datesForTable) { |
| $tableDateObj = Date::factory($tableDate); |
|
|
| $table = ArchiveTableCreator::getNumericTable($tableDateObj, false); |
| $yearMonths[] = $tableDateObj->toString('Y_m'); |
|
|
| $this->model->updateArchiveAsInvalidated($table, $idSites, $datesForTable, $segment, $forceInvalidateNonexistentRanges, $name, $doNotCreateInvalidations); |
|
|
| if ($removeRanges && !empty($table)) { |
| $this->model->updateRangeArchiveAsInvalidated($table, $idSites, $datesForTable, $segment); |
| } |
| } |
|
|
| $this->markInvalidatedArchivesForReprocessAndPurge($yearMonths); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private function removeDatesThatHaveBeenPurged($dates, $period, InvalidationResult $invalidationInfo, $ignorePurgeLogDataDate) |
| { |
| $this->findOlderDateWithLogs($invalidationInfo); |
|
|
| $result = array(); |
| foreach ($dates as $date) { |
| $periodObj = $this->makePeriod($date, $period ?: 'day'); |
|
|
| |
| if ( |
| $invalidationInfo->minimumDateWithLogs |
| && !$ignorePurgeLogDataDate |
| && ($periodObj->getDateEnd()->isEarlier($invalidationInfo->minimumDateWithLogs) |
| || $periodObj->getDateStart()->isEarlier($invalidationInfo->minimumDateWithLogs)) |
| ) { |
| $invalidationInfo->warningDates[] = $date; |
| continue; |
| } |
|
|
| $result[] = $date; |
| $invalidationInfo->processedDates[] = $date; |
| } |
| return $result; |
| } |
|
|
| private function findOlderDateWithLogs(InvalidationResult $info) |
| { |
| |
| $purgeDataSettings = PrivacyManager::getPurgeDataSettings(); |
| $logsDeletedWhenOlderThanDays = (int)$purgeDataSettings['delete_logs_older_than']; |
| $logsDeleteEnabled = $purgeDataSettings['delete_logs_enable']; |
|
|
| if ( |
| $logsDeleteEnabled |
| && $logsDeletedWhenOlderThanDays |
| ) { |
| $info->minimumDateWithLogs = Date::factory('today')->subDay($logsDeletedWhenOlderThanDays); |
| } |
| } |
|
|
| |
| |
| |
| private function markInvalidatedArchivesForReprocessAndPurge($yearMonths) |
| { |
| $archivesToPurge = new ArchivesToPurgeDistributedList(); |
| $archivesToPurge->add($yearMonths); |
| } |
|
|
| private function getYearMonth(Period $period) |
| { |
| return $period->getDateStart()->toString('Y-m-01'); |
| } |
|
|
| private function getUniquePeriodId(Period $period) |
| { |
| return $period->getId() . '.' . $period->getRangeString(); |
| } |
|
|
| private function makePeriod($date, $period) |
| { |
| if ( |
| $period === 'range' |
| && strpos($date, ',') === false |
| ) { |
| $date = $date . ',' . $date; |
| return new Period\Range('range', $date); |
| } else { |
| return Period\Factory::build($period, $date); |
| } |
| } |
|
|
| private function getSegmentArchiving() |
| { |
| if (empty($this->segmentArchiving)) { |
| $this->segmentArchiving = new SegmentArchiving(); |
| } |
| return $this->segmentArchiving; |
| } |
|
|
| private function getAllSitesId() |
| { |
| if (isset($this->allIdSitesCache)) { |
| return $this->allIdSitesCache; |
| } |
|
|
| $model = new \Piwik\Plugins\SitesManager\Model(); |
| $this->allIdSitesCache = $model->getSitesId(); |
| return $this->allIdSitesCache; |
| } |
| } |
|
|