buildRememberArchivedReportId($idSite, $date->toString()); $value = Option::get($key); // we do not really have to get the value first. we could simply always try to call set() and it would update or // insert the record if needed but we do not want to lock the table (especially since there are still some // MyISAM installations) if (false === $value) { Option::set($key, '1'); } } public function getRememberedArchivedReportsThatShouldBeInvalidated() { $reports = Option::getLike($this->rememberArchivedReportIdStart . '%_%'); $sitesPerDay = array(); foreach ($reports as $report => $value) { $report = str_replace($this->rememberArchivedReportIdStart, '', $report); $report = explode('_', $report); $siteId = (int) $report[0]; $date = $report[1]; if (empty($sitesPerDay[$date])) { $sitesPerDay[$date] = array(); } $sitesPerDay[$date][] = $siteId; } return $sitesPerDay; } private function buildRememberArchivedReportId($idSite, $date) { $id = $this->buildRememberArchivedReportIdForSite($idSite); $id .= '_' . trim($date); return $id; } private function buildRememberArchivedReportIdForSite($idSite) { return $this->rememberArchivedReportIdStart . (int) $idSite; } public function forgetRememberedArchivedReportsToInvalidateForSite($idSite) { $id = $this->buildRememberArchivedReportIdForSite($idSite) . '_%'; Option::deleteLike($id); } /** * @internal */ public function forgetRememberedArchivedReportsToInvalidate($idSite, Date $date) { $id = $this->buildRememberArchivedReportId($idSite, $date->toString()); Option::delete($id); } /** * @param $idSites array * @param $dates string * @param $period string * @return array * @throws \Exception */ public function markArchivesAsInvalidated(array $idSites, $dates, $period) { $this->findOlderDateWithLogs(); $datesToInvalidate = $this->getDatesToInvalidateFromString($dates); $minDate = $this->getMinimumDateToInvalidate($datesToInvalidate); $this->updateSiteCreatedTime($idSites, $minDate); $datesByMonth = $this->getDatesByYearMonth($datesToInvalidate); $this->markArchivesInvalidatedFor($idSites, $period, $datesByMonth); $this->persistInvalidatedArchives($idSites, $datesByMonth); foreach ($idSites as $idSite) { foreach ($datesToInvalidate as $date) { $this->forgetRememberedArchivedReportsToInvalidate($idSite, $date); } } return $this->makeOutputLogs(); } private function updateSiteCreatedTime($idSites, Date $minDate) { $idSites = Site::getIdSitesFromIdSitesString($idSites); $minDateSql = $minDate->subDay(1)->getDatetime(); $model = new SitesManagerModel(); $model->updateSiteCreatedTime($idSites, $minDateSql); } /** * @param $toInvalidate * @return bool|Date * @throws \Exception */ private function getMinimumDateToInvalidate($toInvalidate) { /* @var $date Date */ $minDate = false; foreach ($toInvalidate as $date) { // Keep track of the minimum date for each website if ($minDate === false || $date->isEarlier($minDate) ) { $minDate = $date; } } if (empty($minDate)) { throw new \Exception("Check the 'dates' parameter is a valid date."); } return $minDate; } /** * @param $idSites * @param $period string * @param $datesByMonth array * @throws \Exception */ private function markArchivesInvalidatedFor($idSites, $period, $datesByMonth) { $invalidateForPeriodId = $this->getPeriodId($period); // In each table, invalidate day/week/month/year containing this date $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled(); $archiveNumericTables = array_filter($archiveTables, function($name) { return ArchiveTableCreator::getTypeFromTableName($name) == ArchiveTableCreator::NUMERIC_TABLE; }); foreach ($archiveNumericTables as $table) { // Extract Y_m from table name $suffix = ArchiveTableCreator::getDateFromTableName($table); if (!isset($datesByMonth[$suffix])) { continue; } // Dates which are to be deleted from this table $datesToDelete = $datesByMonth[$suffix]; self::getModel()->updateArchiveAsInvalidated($table, $idSites, $invalidateForPeriodId, $datesToDelete); } } /** * Ensure the specified dates are valid. * Store invalid date so we can log them * @param array $dates * @return Date[] */ private function getDatesToInvalidateFromString($dates) { $toInvalidate = array(); $dates = explode(',', trim($dates)); $dates = array_unique($dates); foreach ($dates as $theDate) { $theDate = trim($theDate); try { $date = Date::factory($theDate); } catch (\Exception $e) { $this->invalidDates[] = $theDate; continue; } if ($date->toString() == $theDate) { $toInvalidate[] = $date; } else { $this->invalidDates[] = $theDate; } } return $toInvalidate; } private function findOlderDateWithLogs() { // If using the feature "Delete logs older than N days"... $purgeDataSettings = PrivacyManager::getPurgeDataSettings(); $logsDeletedWhenOlderThanDays = $purgeDataSettings['delete_logs_older_than']; $logsDeleteEnabled = $purgeDataSettings['delete_logs_enable']; if ($logsDeleteEnabled && $logsDeletedWhenOlderThanDays ) { $this->minimumDateWithLogs = Date::factory('today')->subDay($logsDeletedWhenOlderThanDays); } } /** * Given the list of dates, process which tables YYYY_MM we should delete from * * @param $datesToInvalidate Date[] * @return array */ private function getDatesByYearMonth($datesToInvalidate) { $datesByMonth = array(); foreach ($datesToInvalidate as $date) { // we should only delete reports for dates that are more recent than N days if ($this->minimumDateWithLogs && $date->isEarlier($this->minimumDateWithLogs) ) { $this->warningDates[] = $date->toString(); continue; } $this->processedDates[] = $date->toString(); $month = $date->toString('Y_m'); // For a given date, we must invalidate in the monthly archive table $datesByMonth[$month][] = $date->toString(); // But also the year stored in January $year = $date->toString('Y_01'); $datesByMonth[$year][] = $date->toString(); // but also weeks overlapping several months stored in the month where the week is starting /* @var $week Week */ $week = Period\Factory::build('week', $date); $weekAsString = $week->getDateStart()->toString('Y_m'); $datesByMonth[$weekAsString][] = $date->toString(); } return $datesByMonth; } /** * @return array */ private function makeOutputLogs() { $output = array(); if ($this->warningDates) { $output[] = 'Warning: the following Dates have not been invalidated, because they are earlier than your Log Deletion limit: ' . implode(", ", $this->warningDates) . "\n The last day with logs is " . $this->minimumDateWithLogs . ". " . "\n Please disable 'Delete old Logs' or set it to a higher deletion threshold (eg. 180 days or 365 years).'."; } if ($this->invalidDates) { $output[] = 'Warning: some of the Dates to invalidate were invalid: ' . implode(", ", $this->invalidDates) . ". Piwik simply ignored those and proceeded with the others."; } $output[] = "Success. The following dates were invalidated successfully: " . implode(", ", $this->processedDates); return $output; } /** * @param $period * @return bool|int */ private function getPeriodId($period) { if (!empty($period)) { $period = Period\Factory::build($period, Date::today()); } $invalidateForPeriod = $period ? $period->getId() : false; return $invalidateForPeriod; } /** * @param array $idSites * @param $datesByMonth */ private function persistInvalidatedArchives(array $idSites, $datesByMonth) { $yearMonths = array_keys($datesByMonth); $yearMonths = array_unique($yearMonths); $store = new SitesToReprocessDistributedList(); $store->add($idSites); $archivesToPurge = new ArchivesToPurgeDistributedList(); $archivesToPurge->add($yearMonths); } private static function getModel() { return new Model(); } }