archiveInvalidator = $invalidator ?: StaticContainer::get('Piwik\Archive\ArchiveInvalidator'); $this->duplicateActionRemover = $duplicateActionRemover ?: new DuplicateActionRemover(); $this->actionsAccess = $actionsAccess ?: new Actions(); $this->logger = $logger ?: StaticContainer::get(LoggerInterface::class); } protected function configure() { $this->setName('core:fix-duplicate-log-actions'); $this->addNoValueOption('invalidate-archives', null, "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 doExecute(): int { $input = $this->getInput(); $output = $this->getOutput(); $invalidateArchives = $input->getOption('invalidate-archives'); $timer = new Timer(); $duplicateActions = $this->duplicateActionRemover->getDuplicateIdActions(); if (empty($duplicateActions)) { $output->writeln("Found no duplicate actions."); return self::SUCCESS; } $output->writeln("Found " . count($duplicateActions) . " actions with duplicates."); list($numberRemoved, $allArchivesAffected) = $this->fixDuplicateActionReferences($duplicateActions); $this->deleteDuplicatesFromLogAction($duplicateActions); if ($invalidateArchives) { $this->invalidateArchivesUsingActionDuplicates($allArchivesAffected); } else { $this->printAffectedArchives($allArchivesAffected); } $logActionTable = Common::prefixTable('log_action'); $this->writeSuccessMessage(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(), )); return self::SUCCESS; } private function invalidateArchivesUsingActionDuplicates($archivesAffected) { $this->getOutput()->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); } $this->getOutput()->writeln("Done."); } private function printAffectedArchives($allArchivesAffected) { $this->getOutput()->writeln("The following archives used duplicate actions and should be invalidated if you want correct reports:"); foreach ($allArchivesAffected as $archiveInfo) { $this->getOutput()->writeln("\t[ idSite = {$archiveInfo['idsite']}, date = {$archiveInfo['server_time']} ]"); } } private function fixDuplicateActionReferences($duplicateActions) { $dupeCount = count($duplicateActions); $numberRemoved = 0; $allArchivesAffected = array(); foreach ($duplicateActions as $index => $dupeInfo) { $name = $dupeInfo['name']; $toIdAction = $dupeInfo['idaction']; $fromIdActions = $dupeInfo['duplicateIdActions']; $numberRemoved += count($fromIdActions); $this->getOutput()->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($table, $toIdAction, $fromIdActions); $allArchivesAffected = array_merge($allArchivesAffected, $archivesAffected); } } $allArchivesAffected = array_values(array_unique($allArchivesAffected, SORT_REGULAR)); return array($numberRemoved, $allArchivesAffected); } private function fixDuplicateActionsInTable($table, $toIdAction, $fromIdActions) { $timer = new Timer(); $archivesAffected = $this->duplicateActionRemover->getSitesAndDatesOfRowsUsingDuplicates($table, $fromIdActions); $this->duplicateActionRemover->fixDuplicateActionsInTable($table, $toIdAction, $fromIdActions); $this->getOutput()->writeln("\tFixed duplicates in " . Common::prefixTable($table) . ". " . $timer->__toString() . "."); return $archivesAffected; } private function deleteDuplicatesFromLogAction($duplicateActions) { $logActionTable = Common::prefixTable('log_action'); $this->getOutput()->writeln("Deleting duplicate actions from $logActionTable..."); $idActions = []; foreach ($duplicateActions as $dupeInfo) { $idActions = array_merge($idActions, $dupeInfo['duplicateIdActions']); } $this->actionsAccess->delete($idActions); } }