shlinkio /
shlink
| 1 | <?php |
||||
| 2 | |||||
| 3 | declare(strict_types=1); |
||||
| 4 | |||||
| 5 | namespace Shlinkio\Shlink\CLI\Command\Visit; |
||||
| 6 | |||||
| 7 | use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand; |
||||
| 8 | use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig; |
||||
| 9 | use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; |
||||
| 10 | use Shlinkio\Shlink\CLI\Util\ExitCodes; |
||||
| 11 | use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; |
||||
| 12 | use Shlinkio\Shlink\Common\Util\IpAddress; |
||||
| 13 | use Shlinkio\Shlink\Core\Entity\Visit; |
||||
| 14 | use Shlinkio\Shlink\Core\Entity\VisitLocation; |
||||
| 15 | use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; |
||||
| 16 | use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface; |
||||
| 17 | use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface; |
||||
| 18 | use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; |
||||
| 19 | use Shlinkio\Shlink\IpGeolocation\Model\Location; |
||||
| 20 | use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; |
||||
| 21 | use Symfony\Component\Console\Exception\RuntimeException; |
||||
| 22 | use Symfony\Component\Console\Helper\ProgressBar; |
||||
| 23 | use Symfony\Component\Console\Input\InputInterface; |
||||
| 24 | use Symfony\Component\Console\Input\InputOption; |
||||
| 25 | use Symfony\Component\Console\Output\OutputInterface; |
||||
| 26 | use Symfony\Component\Console\Style\SymfonyStyle; |
||||
| 27 | use Symfony\Component\Lock\LockFactory; |
||||
| 28 | use Throwable; |
||||
| 29 | |||||
| 30 | use function sprintf; |
||||
| 31 | |||||
| 32 | class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocationHelperInterface |
||||
| 33 | { |
||||
| 34 | public const NAME = 'visit:locate'; |
||||
| 35 | |||||
| 36 | private VisitLocatorInterface $visitLocator; |
||||
| 37 | private IpLocationResolverInterface $ipLocationResolver; |
||||
| 38 | private GeolocationDbUpdaterInterface $dbUpdater; |
||||
| 39 | |||||
| 40 | private SymfonyStyle $io; |
||||
| 41 | private ?ProgressBar $progressBar = null; |
||||
| 42 | |||||
| 43 | 14 | public function __construct( |
|||
| 44 | VisitLocatorInterface $visitLocator, |
||||
| 45 | IpLocationResolverInterface $ipLocationResolver, |
||||
| 46 | LockFactory $locker, |
||||
| 47 | GeolocationDbUpdaterInterface $dbUpdater |
||||
| 48 | ) { |
||||
| 49 | 14 | parent::__construct($locker); |
|||
| 50 | 14 | $this->visitLocator = $visitLocator; |
|||
| 51 | 14 | $this->ipLocationResolver = $ipLocationResolver; |
|||
| 52 | 14 | $this->dbUpdater = $dbUpdater; |
|||
| 53 | 14 | } |
|||
| 54 | |||||
| 55 | 14 | protected function configure(): void |
|||
| 56 | { |
||||
| 57 | $this |
||||
| 58 | 14 | ->setName(self::NAME) |
|||
| 59 | 14 | ->setDescription('Resolves visits origin locations.') |
|||
| 60 | 14 | ->addOption( |
|||
| 61 | 14 | 'retry', |
|||
| 62 | 14 | 'r', |
|||
| 63 | 14 | InputOption::VALUE_NONE, |
|||
| 64 | 'Will retry the location of visits that were located with a not-found location, in case it was due to ' |
||||
| 65 | 14 | . 'a temporal issue.', |
|||
| 66 | ) |
||||
| 67 | 14 | ->addOption( |
|||
| 68 | 14 | 'all', |
|||
| 69 | 14 | 'a', |
|||
| 70 | 14 | InputOption::VALUE_NONE, |
|||
| 71 | 'When provided together with --retry, will locate all existing visits, regardless the fact that they ' |
||||
| 72 | 14 | . 'have already been located.', |
|||
| 73 | ); |
||||
| 74 | 14 | } |
|||
| 75 | |||||
| 76 | 14 | protected function initialize(InputInterface $input, OutputInterface $output): void |
|||
| 77 | { |
||||
| 78 | 14 | $this->io = new SymfonyStyle($input, $output); |
|||
| 79 | 14 | } |
|||
| 80 | |||||
| 81 | 14 | protected function interact(InputInterface $input, OutputInterface $output): void |
|||
| 82 | { |
||||
| 83 | 14 | $retry = $input->getOption('retry'); |
|||
| 84 | 14 | $all = $input->getOption('all'); |
|||
| 85 | |||||
| 86 | 14 | if ($all && !$retry) { |
|||
| 87 | 1 | $this->io->writeln( |
|||
| 88 | '<comment>The <fg=yellow;options=bold>--all</> flag has no effect on its own. You have to provide it ' |
||||
| 89 | 1 | . 'together with <fg=yellow;options=bold>--retry</>.</comment>', |
|||
| 90 | ); |
||||
| 91 | } |
||||
| 92 | |||||
| 93 | 14 | if ($all && $retry && ! $this->warnAndVerifyContinue()) { |
|||
| 94 | 3 | throw new RuntimeException('Execution aborted'); |
|||
| 95 | } |
||||
| 96 | 11 | } |
|||
| 97 | |||||
| 98 | 4 | private function warnAndVerifyContinue(): bool |
|||
| 99 | { |
||||
| 100 | 4 | $this->io->warning([ |
|||
| 101 | 4 | 'You are about to process the location of all existing visits your short URLs received.', |
|||
| 102 | 'Since shlink saves visitors IP addresses anonymized, you could end up losing precision on some of ' |
||||
| 103 | . 'your visits.', |
||||
| 104 | 'Also, if you have a large amount of visits, this can be a very time consuming process. ' |
||||
| 105 | . 'Continue at your own risk.', |
||||
| 106 | ]); |
||||
| 107 | 4 | return $this->io->confirm('Do you want to proceed?', false); |
|||
| 108 | } |
||||
| 109 | |||||
| 110 | 10 | protected function lockedExecute(InputInterface $input, OutputInterface $output): int |
|||
| 111 | { |
||||
| 112 | 10 | $retry = $input->getOption('retry'); |
|||
| 113 | 10 | $all = $retry && $input->getOption('all'); |
|||
| 114 | |||||
| 115 | try { |
||||
| 116 | 10 | $this->checkDbUpdate(); |
|||
| 117 | |||||
| 118 | 9 | if ($all) { |
|||
| 119 | 1 | $this->visitLocator->locateAllVisits($this); |
|||
| 120 | } else { |
||||
| 121 | 8 | $this->visitLocator->locateUnlocatedVisits($this); |
|||
| 122 | 4 | if ($retry) { |
|||
| 123 | 1 | $this->visitLocator->locateVisitsWithEmptyLocation($this); |
|||
| 124 | } |
||||
| 125 | } |
||||
| 126 | |||||
| 127 | 5 | $this->io->success('Finished locating visits'); |
|||
| 128 | 5 | return ExitCodes::EXIT_SUCCESS; |
|||
| 129 | 5 | } catch (Throwable $e) { |
|||
| 130 | 5 | $this->io->error($e->getMessage()); |
|||
| 131 | 5 | if ($e instanceof Throwable && $this->io->isVerbose()) { |
|||
| 132 | 4 | $this->getApplication()->renderThrowable($e, $this->io); |
|||
| 133 | } |
||||
| 134 | |||||
| 135 | 5 | return ExitCodes::EXIT_FAILURE; |
|||
| 136 | } |
||||
| 137 | } |
||||
| 138 | |||||
| 139 | /** |
||||
| 140 | * @throws IpCannotBeLocatedException |
||||
| 141 | */ |
||||
| 142 | 7 | public function geolocateVisit(Visit $visit): Location |
|||
| 143 | { |
||||
| 144 | 7 | if (! $visit->hasRemoteAddr()) { |
|||
| 145 | 2 | $this->io->writeln( |
|||
| 146 | 2 | '<comment>Ignored visit with no IP address</comment>', |
|||
| 147 | 2 | OutputInterface::VERBOSITY_VERBOSE, |
|||
| 148 | ); |
||||
| 149 | 2 | throw IpCannotBeLocatedException::forEmptyAddress(); |
|||
| 150 | } |
||||
| 151 | |||||
| 152 | 5 | $ipAddr = $visit->getRemoteAddr(); |
|||
| 153 | 5 | $this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr)); |
|||
| 154 | 5 | if ($ipAddr === IpAddress::LOCALHOST) { |
|||
| 155 | 1 | $this->io->writeln(' [<comment>Ignored localhost address</comment>]'); |
|||
| 156 | 1 | throw IpCannotBeLocatedException::forLocalhost(); |
|||
| 157 | } |
||||
| 158 | |||||
| 159 | try { |
||||
| 160 | 4 | return $this->ipLocationResolver->resolveIpLocation($ipAddr); |
|||
| 161 | 1 | } catch (WrongIpException $e) { |
|||
| 162 | 1 | $this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]'); |
|||
| 163 | 1 | if ($this->io->isVerbose()) { |
|||
| 164 | 1 | $this->getApplication()->renderThrowable($e, $this->io); |
|||
| 165 | } |
||||
| 166 | |||||
| 167 | 1 | throw IpCannotBeLocatedException::forError($e); |
|||
| 168 | } |
||||
| 169 | } |
||||
| 170 | |||||
| 171 | 3 | public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void |
|||
| 172 | { |
||||
| 173 | 3 | $message = ! $visitLocation->isEmpty() |
|||
| 174 | ? sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->getCountryName()) |
||||
| 175 | 3 | : ' [<comment>Address not found</comment>]'; |
|||
| 176 | 3 | $this->io->writeln($message); |
|||
| 177 | 3 | } |
|||
| 178 | |||||
| 179 | 10 | private function checkDbUpdate(): void |
|||
| 180 | { |
||||
| 181 | try { |
||||
| 182 | $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists): void { |
||||
| 183 | 2 | $this->io->writeln( |
|||
| 184 | 2 | sprintf('<fg=blue>%s GeoLite2 database...</>', $olderDbExists ? 'Updating' : 'Downloading'), |
|||
| 185 | ); |
||||
| 186 | 2 | $this->progressBar = new ProgressBar($this->io); |
|||
| 187 | }, function (int $total, int $downloaded): void { |
||||
| 188 | 2 | $this->progressBar->setMaxSteps($total); |
|||
|
0 ignored issues
–
show
|
|||||
| 189 | 2 | $this->progressBar->setProgress($downloaded); |
|||
| 190 | 10 | }); |
|||
| 191 | |||||
| 192 | 8 | if ($this->progressBar !== null) { |
|||
| 193 | $this->progressBar->finish(); |
||||
| 194 | 8 | $this->io->newLine(); |
|||
| 195 | } |
||||
| 196 | 2 | } catch (GeolocationDbUpdateFailedException $e) { |
|||
| 197 | 2 | if (! $e->olderDbExists()) { |
|||
| 198 | 1 | $this->io->error('GeoLite2 database download failed. It is not possible to locate visits.'); |
|||
| 199 | 1 | throw $e; |
|||
| 200 | } |
||||
| 201 | |||||
| 202 | 1 | $this->io->newLine(); |
|||
| 203 | 1 | $this->io->writeln( |
|||
| 204 | 1 | '<fg=yellow;options=bold>[Warning] GeoLite2 database update failed. Proceeding with old version.</>', |
|||
| 205 | ); |
||||
| 206 | } |
||||
| 207 | 9 | } |
|||
| 208 | |||||
| 209 | 11 | protected function getLockConfig(): LockedCommandConfig |
|||
| 210 | { |
||||
| 211 | 11 | return new LockedCommandConfig($this->getName()); |
|||
|
0 ignored issues
–
show
It seems like
$this->getName() can also be of type null; however, parameter $lockName of Shlinkio\Shlink\CLI\Comm...ndConfig::__construct() does only seem to accept string, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 212 | } |
||||
| 213 | } |
||||
| 214 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.