archiveInvalidator = $invalidator ?: StaticContainer::get('Piwik\Archive\ArchiveInvalidator'); $this->duplicateActionRemover = $duplicateActionRemover ?: new DuplicateActionRemover(); $this->actionsAccess = $actionsAccess ?: new Actions(); $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface'); } protected function configure() { $this->setName('core:fix-duplicate-log-actions'); $this->addOption('invalidate-archives', null, InputOption::VALUE_NONE, "If supplied, archives for logs that use duplicate actions will be invalidated." . " On the next cron archive run, the reports for those dates will be re-processed."); $this->setDescription('Removes duplicates in the log action table and fixes references to the duplicates in ' . 'related tables. NOTE: This action can take a long time to run!'); } protected function execute(InputInterface $input, OutputInterface $output) { $invalidateArchives = $input->getOption('invalidate-archives'); $timer = new Timer(); $duplicateActions = $this->duplicateActionRemover->getDuplicateIdActions(); if (empty($duplicateActions)) { $output->writeln("Found no duplicate actions."); return; } $output->writeln("Found " . count($duplicateActions) . " actions with duplicates."); list($numberRemoved, $allArchivesAffected) = $this->fixDuplicateActionReferences($duplicateActions, $output); $this->deleteDuplicatesFromLogAction($output, $duplicateActions); if ($invalidateArchives) { $this->invalidateArchivesUsingActionDuplicates($allArchivesAffected, $output); } else { $this->printAffectedArchives($allArchivesAffected, $output); } $logActionTable = Common::prefixTable('log_action'); $this->writeSuccessMessage($output, array( "Found and deleted $numberRemoved duplicate action entries in the $logActionTable table.", "References in log_link_visit_action, log_conversion and log_conversion_item were corrected.", $timer->__toString() )); } private function invalidateArchivesUsingActionDuplicates($archivesAffected, OutputInterface $output) { $output->write("Invalidating archives affected by duplicates fixed..."); foreach ($archivesAffected as $archiveInfo) { $dates = array(Date::factory($archiveInfo['server_time'])); $this->archiveInvalidator->markArchivesAsInvalidated(array($archiveInfo['idsite']), $dates, $period = false); } $output->writeln("Done."); } private function printAffectedArchives($allArchivesAffected, OutputInterface $output) { $output->writeln("The following archives used duplicate actions and should be invalidated if you want correct reports:"); foreach ($allArchivesAffected as $archiveInfo) { $output->writeln("\t[ idSite = {$archiveInfo['idsite']}, date = {$archiveInfo['server_time']} ]"); } } private function fixDuplicateActionReferences($duplicateActions, OutputInterface $output) { $dupeCount = count($duplicateActions); $numberRemoved = 0; $allArchivesAffected = array(); foreach ($duplicateActions as $index => $dupeInfo) { $name = $dupeInfo['name']; $toIdAction = $dupeInfo['idaction']; $fromIdActions = $dupeInfo['duplicateIdActions']; $numberRemoved += count($fromIdActions); $output->writeln("[$index / $dupeCount] Fixing duplicates for '$name'"); $this->logger->debug(" idaction = {idaction}, duplicate idactions = {duplicateIdActions}", array( 'idaction' => $toIdAction, 'duplicateIdActions' => $fromIdActions )); foreach (DuplicateActionRemover::$tablesWithIdActionColumns as $table) { $archivesAffected = $this->fixDuplicateActionsInTable($output, $table, $toIdAction, $fromIdActions); $allArchivesAffected = array_merge($allArchivesAffected, $archivesAffected); } } $allArchivesAffected = array_values(array_unique($allArchivesAffected, SORT_REGULAR)); return array($numberRemoved, $allArchivesAffected); } private function fixDuplicateActionsInTable(OutputInterface $output, $table, $toIdAction, $fromIdActions) { $timer = new Timer(); $archivesAffected = $this->duplicateActionRemover->getSitesAndDatesOfRowsUsingDuplicates($table, $fromIdActions); $this->duplicateActionRemover->fixDuplicateActionsInTable($table, $toIdAction, $fromIdActions); $output->writeln("\tFixed duplicates in " . Common::prefixTable($table) . ". " . $timer->__toString() . "."); return $archivesAffected; } private function deleteDuplicatesFromLogAction(OutputInterface $output, $duplicateActions) { $logActionTable = Common::prefixTable('log_action'); $output->writeln("Deleting duplicate actions from $logActionTable..."); $idActions = array(); foreach ($duplicateActions as $dupeInfo) { $idActions = array_merge($idActions, $dupeInfo['duplicateIdActions']); } $this->actionsAccess->delete($idActions); } }