| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Plugins\CoreAdminHome\Commands; |
|
|
| use Exception; |
| use Piwik\Archive\ArchiveInvalidator; |
| use Piwik\ArchiveProcessor\Rules; |
| use Piwik\Container\StaticContainer; |
| use Piwik\Period; |
| use Piwik\Period\Range; |
| use Piwik\Piwik; |
| use Piwik\Plugins\SegmentEditor\API; |
| use Piwik\Segment; |
| use Piwik\Plugin\ConsoleCommand; |
| use Piwik\Plugins\SitesManager\API as SitesManagerAPI; |
| use Piwik\Site; |
| use Piwik\Period\Factory as PeriodFactory; |
| use Piwik\Log\LoggerInterface; |
|
|
| |
| |
| |
| |
| |
| class InvalidateReportData extends ConsoleCommand |
| { |
| public const ALL_OPTION_VALUE = 'all'; |
|
|
| |
| |
| |
| |
| private $allSegments = null; |
|
|
| |
| |
| |
| private $logger; |
|
|
| |
| |
| |
| private $invalidator; |
|
|
| public function __construct() |
| { |
| parent::__construct(); |
|
|
| $this->logger = StaticContainer::get(LoggerInterface::class); |
| $this->invalidator = StaticContainer::get(ArchiveInvalidator::class); |
| } |
|
|
| protected function configure() |
| { |
| $this->setName('core:invalidate-report-data'); |
| $this->setDescription('Invalidate archived report data by date range, site and period.'); |
| $this->addRequiredValueOption( |
| 'dates', |
| null, |
| 'List of dates or date ranges to invalidate report data for, eg, 2015-01-03 or 2015-01-05,2015-02-12.', |
| null, |
| true |
| ); |
| $this->addRequiredValueOption( |
| 'sites', |
| null, |
| 'List of site IDs to invalidate report data for, eg, "1,2,3,4" or "all" for all sites.', |
| self::ALL_OPTION_VALUE |
| ); |
| $this->addRequiredValueOption( |
| 'periods', |
| null, |
| 'List of period types to invalidate report data for. Can be one or more of the following values: day, ' |
| . 'week, month, year, range or "all" for all of them.', |
| self::ALL_OPTION_VALUE |
| ); |
| $this->addRequiredValueOption( |
| 'segment', |
| null, |
| 'List of segments to invalidate report data for. This can be the segment string itself, the segment name from the UI or the ID of the segment.' |
| . ' If specifying the segment definition, make sure it is encoded properly (it should be the same as the segment parameter in the URL).' |
| . ' If no segment is provided, all segments (including all visits) will be invalidated. To specifically invalidate all visits --segment="" can be provided.', |
| null, |
| true |
| ); |
| $this->addNoValueOption( |
| 'cascade', |
| null, |
| 'If supplied, invalidation will cascade, invalidating child period types even if they aren\'t specified in' |
| . ' --periods. For example, if --periods=week, --cascade will cause the days within those weeks to be ' |
| . 'invalidated as well. If --periods=month, then weeks and days will be invalidated. Note: if a period ' |
| . 'falls partly outside of a date range, then --cascade will also invalidate data for child periods ' |
| . 'outside the date range. For example, if --dates=2015-09-14,2015-09-15 & --periods=week, --cascade will' |
| . ' also invalidate all days within 2015-09-13,2015-09-19, even those outside the date range.' |
| ); |
| $this->addNoValueOption('dry-run', null, 'For tests. Runs the command w/o actually ' |
| . 'invalidating anything.'); |
| $this->addRequiredValueOption('plugin', null, 'To invalidate data for a specific plugin only.'); |
| $this->addNoValueOption( |
| 'ignore-log-deletion-limit', |
| null, |
| 'Ignore the log purging limit when invalidating archives. If a date is older than the log purging threshold (which means ' |
| . 'there should be no log data for it), we normally skip invalidating it in order to prevent losing any report data. In some cases, ' |
| . 'however it is useful, if, for example, your site was imported from Google, and there is never any log data.' |
| ); |
| $this->setHelp('Invalidate archived report data by date range, site and period. Invalidated archive data will ' |
| . 'be re-archived during the next core:archive run. If your log data has changed for some reason, this ' |
| . 'command can be used to make sure reports are generated using the new, changed log data.'); |
| } |
|
|
| protected function doExecute(): int |
| { |
| $sites = $this->getSitesToInvalidateFor(); |
| $periodTypes = $this->getPeriodTypesToInvalidateFor(); |
| $dateRanges = $this->getDateRangesToInvalidateFor(); |
| $segments = $this->getSegmentsToInvalidateFor($sites); |
|
|
| foreach ($periodTypes as $periodType) { |
| if ($periodType === 'range') { |
| $this->invalidateRangePeriods($dateRanges, $segments, $sites); |
| } else { |
| $this->invalidateNonRangePeriods($periodType, $dateRanges, $segments, $sites); |
| } |
| } |
|
|
| return self::SUCCESS; |
| } |
|
|
| private function getSegmentsWithSitesToProcess( |
| array $segments, |
| array $sites, |
| bool $ignoreInvalidSegments = false |
| ): array { |
| $segmentDetails = []; |
|
|
| |
| foreach ($segments as $segmentStr) { |
| |
| $sitesToProcess = $this->getSitesForSegment($segmentStr, $sites); |
| $segment = null; |
|
|
| try { |
| $segment = new Segment($segmentStr, $sitesToProcess); |
| } catch (Exception $e) { |
| if (!$ignoreInvalidSegments) { |
| throw $e; |
| } |
| } |
|
|
| if (empty($sitesToProcess)) { |
| |
| $this->logger->info("Segment [$segmentStr] not available for any site, skipping it..."); |
| continue; |
| } |
|
|
| $sitesDiff = array_diff($sites, $sitesToProcess); |
|
|
| if (count($sitesDiff)) { |
| $this->logger->info( |
| "Segment [$segmentStr] not available for all sites, skipping this segment for sites [ " |
| . implode(', ', $sitesDiff) . " ]." |
| ); |
| } |
|
|
| $segmentDetails[$segmentStr] = [ |
| 'segment' => $segment, |
| 'sites' => $sitesToProcess, |
| ]; |
| } |
|
|
| return $segmentDetails; |
| } |
|
|
| private function invalidateNonRangePeriods( |
| string $periodType, |
| array $dateRangesToInvalidate, |
| array $segments, |
| array $sites |
| ): void { |
| $ignoreLogDeletionLimit = $this->getInput()->getOption('ignore-log-deletion-limit'); |
| $cascade = $this->getInput()->getOption('cascade'); |
| $dryRun = $this->getInput()->getOption('dry-run'); |
| $plugin = $this->getInput()->getOption('plugin'); |
|
|
| foreach ($dateRangesToInvalidate as $dateRange) { |
| foreach ($segments as $segmentStr => $segmentData) { |
| $sitesToProcess = $segmentData['sites']; |
| $segment = $segmentData['segment']; |
|
|
| $this->logger->info("Invalidating $periodType periods in $dateRange for site = [ " |
| . implode(', ', $sitesToProcess) . " ], segment = [ $segmentStr ]..."); |
|
|
| $dates = $this->getPeriodDates($periodType, $dateRange); |
|
|
| if ($dryRun) { |
| $message = "[Dry-run] invalidating archives for site = [ " . implode(', ', $sitesToProcess) |
| . " ], dates = [ " . implode(', ', $dates) . " ], period = [ $periodType ], segment = [ " |
| . "$segmentStr ], cascade = [ " . (int)$cascade . " ]"; |
| if (!empty($plugin)) { |
| $message .= ", plugin = [ $plugin ]"; |
| } |
| $this->logger->info($message); |
| } else { |
| $invalidationResult = $this->invalidator->markArchivesAsInvalidated( |
| $sitesToProcess, |
| $dates, |
| $periodType, |
| $segment, |
| $cascade, |
| false, |
| $plugin, |
| $ignoreLogDeletionLimit |
| ); |
|
|
| if ($this->getOutput()->getVerbosity() > $this->getOutput()::VERBOSITY_NORMAL) { |
| foreach ($invalidationResult->makeOutputLogs() as $outputLog) { |
| $this->logger->info($outputLog); |
| } |
| } |
| } |
| } |
| } |
| } |
|
|
| private function invalidateRangePeriods(array $dateRangesToInvalidate, array $segments, array $sites): void |
| { |
| $dryRun = $this->getInput()->getOption('dry-run'); |
| $plugin = $this->getInput()->getOption('plugin'); |
| $periods = trim($this->getInput()->getOption('periods')); |
| $isUsingAllOption = $periods === self::ALL_OPTION_VALUE; |
| $rangeDates = []; |
|
|
| foreach ($dateRangesToInvalidate as $dateRange) { |
| if ( |
| $isUsingAllOption |
| && !Period::isMultiplePeriod($dateRange, 'day') |
| ) { |
| continue; |
| } |
|
|
| $rangeDates[] = $this->getPeriodDates('range', $dateRange); |
| } |
|
|
| if (!empty($rangeDates)) { |
| foreach ($segments as $segmentStr => $segmentData) { |
| $sitesToProcess = $segmentData['sites']; |
| $segment = $segmentData['segment']; |
|
|
| if ($dryRun) { |
| $dateRangeStr = implode(';', $dateRangesToInvalidate); |
| $this->logger->info("Invalidating range periods overlapping $dateRangeStr for " |
| . "site = [ " . implode(', ', $sitesToProcess) . " ], segment = [ $segmentStr ]..."); |
| } else { |
| $this->invalidator->markArchivesOverlappingRangeAsInvalidated($sitesToProcess, $rangeDates, $segment, $plugin); |
| } |
| } |
| } |
| } |
|
|
| |
| |
| |
| private function getSitesForSegment(string $segmentStr, array $idSites): array |
| { |
| $sitesToProcess = []; |
|
|
| foreach ($idSites as $idSite) { |
| try { |
| $segment = new Segment($segmentStr, $idSite); |
| $sitesToProcess[] = $idSite; |
| } catch (Exception $e) { |
| continue; |
| } |
| } |
|
|
| return $sitesToProcess; |
| } |
|
|
| |
| |
| |
| private function getSitesToInvalidateFor(): array |
| { |
| $sites = $this->getInput()->getOption('sites'); |
|
|
| $siteIds = Site::getIdSitesFromIdSitesString($sites); |
| if (empty($siteIds)) { |
| throw new \InvalidArgumentException("Invalid --sites value: '$sites'."); |
| } |
|
|
| $allSiteIds = SitesManagerAPI::getInstance()->getAllSitesId(); |
| foreach ($siteIds as $idSite) { |
| if (!in_array($idSite, $allSiteIds)) { |
| throw new \InvalidArgumentException("Invalid --sites value: '$sites', there are no sites with IDs = $idSite"); |
| } |
| } |
|
|
| return $siteIds; |
| } |
|
|
| |
| |
| |
| |
| private function getPeriodTypesToInvalidateFor(): array |
| { |
| $periods = $this->getInput()->getOption('periods'); |
| if (empty($periods)) { |
| throw new \InvalidArgumentException("The --periods argument is required."); |
| } |
|
|
| if ($periods == self::ALL_OPTION_VALUE) { |
| $result = array_keys(Piwik::$idPeriods); |
| return $result; |
| } |
|
|
| $periods = explode(',', $periods); |
| $periods = array_map('trim', $periods); |
|
|
| foreach ($periods as $periodIdentifier) { |
| if (!isset(Piwik::$idPeriods[$periodIdentifier])) { |
| throw new \InvalidArgumentException("Invalid period type '$periodIdentifier' supplied in --periods."); |
| } |
| } |
|
|
| return $periods; |
| } |
|
|
| |
| |
| |
| private function getDateRangesToInvalidateFor(): array |
| { |
| $dateRanges = $this->getInput()->getOption('dates'); |
| if (empty($dateRanges)) { |
| throw new \InvalidArgumentException("The --dates option is required."); |
| } |
|
|
| return is_array($dateRanges) ? $dateRanges : [$dateRanges]; |
| } |
|
|
| private function getPeriodDates($periodType, $dateRange) |
| { |
| if (!isset(Piwik::$idPeriods[$periodType])) { |
| throw new \InvalidArgumentException("Invalid period type '$periodType'."); |
| } |
|
|
| try { |
| $period = PeriodFactory::build($periodType, $dateRange); |
| } catch (\Exception $ex) { |
| throw new \InvalidArgumentException("Invalid date or date range specifier '$dateRange'", $code = 0, $ex); |
| } |
|
|
| $result = []; |
| if ($periodType === 'range') { |
| $result[] = $period->getDateStart(); |
| $result[] = $period->getDateEnd(); |
| } elseif ($period instanceof Range) { |
| foreach ($period->getSubperiods() as $subperiod) { |
| $result[] = $subperiod->getDateStart(); |
| } |
| } else { |
| $result[] = $period->getDateStart(); |
| } |
| return $result; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private function getSegmentsToInvalidateFor(array $idSites): array |
| { |
| $input = $this->getInput(); |
| $segments = $input->getOption('segment'); |
| $segments = array_map('trim', $segments); |
| $segments = array_unique($segments); |
|
|
| if (empty($segments)) { |
| $this->logger->debug("No segment provided. Invalidating all stored segments."); |
| $segments = Rules::getSegmentsToProcess($idSites); |
| array_unshift($segments, ""); |
| return $this->getSegmentsWithSitesToProcess($segments, $idSites, true); |
| } |
|
|
| $result = []; |
|
|
| foreach ($segments as $segmentOptionValue) { |
| if ($segmentOptionValue === "") { |
| $result[] = ""; |
| continue; |
| } |
|
|
| $segmentDefinition = $this->findSegment($segmentOptionValue, $idSites); |
|
|
| if (empty($segmentDefinition)) { |
| continue; |
| } |
|
|
| $result[] = $segmentDefinition; |
| } |
|
|
| return $this->getSegmentsWithSitesToProcess($result, $idSites); |
| } |
|
|
| |
| |
| |
| private function findSegment(string $segmentOptionValue, array $idSites) |
| { |
| $allSegments = $this->getAllSegments($idSites); |
|
|
| foreach ($allSegments as $segment) { |
| if ( |
| !empty($segment['enable_only_idsite']) |
| && !in_array($segment['enable_only_idsite'], $idSites) |
| ) { |
| continue; |
| } |
|
|
| if ($segmentOptionValue == $segment['idsegment']) { |
| $this->logger->debug("Matching '$segmentOptionValue' by idsegment with segment {segment}.", ['segment' => json_encode($segment)]); |
| return $segment['definition']; |
| } |
|
|
| if (strtolower($segmentOptionValue) == strtolower($segment['name'])) { |
| $this->logger->debug("Matching '$segmentOptionValue' by name with segment {segment}.", ['segment' => json_encode($segment)]); |
| return $segment['definition']; |
| } |
|
|
| if ( |
| $segment['definition'] == $segmentOptionValue |
| || $segment['definition'] == urldecode($segmentOptionValue) |
| ) { |
| $this->logger->debug("Matching '{value}' by definition with segment {segment}.", ['value' => $segmentOptionValue, 'segment' => json_encode($segment)]); |
| return $segment['definition']; |
| } |
| } |
|
|
| $this->logger->warning("'$segmentOptionValue' did not match any stored segment, but invalidating it anyway."); |
| return $segmentOptionValue; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private function getAllSegments(array $idSites): array |
| { |
| if ($this->allSegments === null) { |
| $segmentsByDefinition = []; |
|
|
| if ([] === $idSites) { |
| $idSites = [null]; |
| } |
|
|
| foreach ($idSites as $idSite) { |
| $siteSegments = API::getInstance()->getAll($idSite); |
|
|
| $siteSegmentsByDefinition = array_combine( |
| array_column($siteSegments, 'definition'), |
| $siteSegments |
| ); |
|
|
| $segmentsByDefinition = array_merge( |
| $segmentsByDefinition, |
| $siteSegmentsByDefinition |
| ); |
| } |
|
|
| $this->allSegments = array_values($segmentsByDefinition); |
| } |
|
|
| return $this->allSegments; |
| } |
| } |
|
|