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);
}
}