diff --git a/bdi b/bdi index d636fab..4cd468f 100755 --- a/bdi +++ b/bdi @@ -10,11 +10,13 @@ use DBrekelmans\BrowserDriverInstaller\Browser; use DBrekelmans\BrowserDriverInstaller\Browser\Chromium; use DBrekelmans\BrowserDriverInstaller\Browser\Firefox; use DBrekelmans\BrowserDriverInstaller\Browser\GoogleChrome; +use DBrekelmans\BrowserDriverInstaller\Browser\MsEdge; use DBrekelmans\BrowserDriverInstaller\Command\DetectCommand; use DBrekelmans\BrowserDriverInstaller\CommandLine\ShellCommandLineEnvironment; use DBrekelmans\BrowserDriverInstaller\Driver; use DBrekelmans\BrowserDriverInstaller\Driver\ChromeDriver; use DBrekelmans\BrowserDriverInstaller\Driver\GeckoDriver; +use DBrekelmans\BrowserDriverInstaller\Driver\MsEdgeDriver; use Symfony\Component\Console\Application; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpClient\NativeHttpClient; @@ -37,23 +39,23 @@ $browserPathResolverFactory = new Browser\PathResolverFactory(); $browserPathResolverFactory->register(new GoogleChrome\PathResolver()); $browserPathResolverFactory->register(new Chromium\PathResolver()); $browserPathResolverFactory->register(new Firefox\PathResolver()); +$browserPathResolverFactory->register(new MsEdge\PathResolver()); $browserVersionResolverFactory = new Browser\VersionResolverFactory(); $browserVersionResolverFactory->register(new GoogleChrome\VersionResolver($shellCommandLineEnv)); $browserVersionResolverFactory->register(new Chromium\VersionResolver($shellCommandLineEnv)); $browserVersionResolverFactory->register(new Firefox\VersionResolver($shellCommandLineEnv)); +$browserVersionResolverFactory->register(new MsEdge\VersionResolver($shellCommandLineEnv)); $driverVersionResolverFactory = new Driver\VersionResolverFactory(); -$chromeDriverVersionResolver = new ChromeDriver\VersionResolver($httpClient); -$geckoDriverVersionResolver = new GeckoDriver\VersionResolver($httpClient); -$driverVersionResolverFactory->register($chromeDriverVersionResolver); -$driverVersionResolverFactory->register($geckoDriverVersionResolver); - -$chromeDriverDownloadUrlResolver = new ChromeDriver\DownloadUrlResolver($httpClient); +$driverVersionResolverFactory->register($chromeDriverVersionResolver = new ChromeDriver\VersionResolver($httpClient)); +$driverVersionResolverFactory->register($geckoDriverVersionResolver = new GeckoDriver\VersionResolver($httpClient)); +$driverVersionResolverFactory->register($msedgeDriverVersionResolver = new MsEdgeDriver\VersionResolver($httpClient)); $driverDownloaderFactory = new Driver\DownloaderFactory(); -$driverDownloaderFactory->register(new ChromeDriver\Downloader($filesystem, $httpClient, $multiExtractor, $chromeDriverDownloadUrlResolver)); +$driverDownloaderFactory->register(new ChromeDriver\Downloader($filesystem, $httpClient, $multiExtractor, $chromeDriverDownloadUrlResolver = new ChromeDriver\DownloadUrlResolver($httpClient))); $driverDownloaderFactory->register(new GeckoDriver\Downloader($filesystem, $httpClient, $multiExtractor)); +$driverDownloaderFactory->register(new MsEdgeDriver\Downloader($filesystem, $httpClient, $multiExtractor, $msedgeDriverDownloadUrlResolver = new MsEdgeDriver\DownloadUrlResolver())); $browserFactory = new Browser\BrowserFactory($browserPathResolverFactory, $browserVersionResolverFactory); $driverFactory = new Driver\DriverFactory($driverVersionResolverFactory); @@ -64,6 +66,7 @@ $application->add(new Chromium\Command($filesystem, $browserFactory, $driverFact $application->add(new Firefox\Command($filesystem, $browserFactory, $driverFactory, $driverDownloaderFactory)); $application->add(new ChromeDriver\Command($chromeDriverVersionResolver, $driverDownloaderFactory)); $application->add(new GeckoDriver\Command($geckoDriverVersionResolver, $driverDownloaderFactory)); +$application->add(new MsEdge\Command($filesystem, $browserFactory, $driverFactory, $driverDownloaderFactory)); $application->setDefaultCommand(DetectCommand::NAME); diff --git a/src/Browser/BrowserName.php b/src/Browser/BrowserName.php index ab7d55d..f9af067 100644 --- a/src/Browser/BrowserName.php +++ b/src/Browser/BrowserName.php @@ -9,4 +9,5 @@ enum BrowserName: string case GOOGLE_CHROME = 'google-chrome'; case CHROMIUM = 'chromium'; case FIREFOX = 'firefox'; + case MSEDGE = 'msedge'; } diff --git a/src/Browser/MsEdge/Command.php b/src/Browser/MsEdge/Command.php new file mode 100644 index 0000000..b54f4c0 --- /dev/null +++ b/src/Browser/MsEdge/Command.php @@ -0,0 +1,16 @@ + throw new RuntimeException('Not implemented yet.'), + OperatingSystem::MACOS => '/Applications/Microsoft\ Edge.app', + OperatingSystem::WINDOWS => 'C:\Program Files (x86)\Microsoft\Edge\Application', + }; + } + + public function supports(BrowserName $browserName): bool + { + return $browserName === BrowserName::MSEDGE; + } +} diff --git a/src/Browser/MsEdge/VersionResolver.php b/src/Browser/MsEdge/VersionResolver.php new file mode 100644 index 0000000..61f90fb --- /dev/null +++ b/src/Browser/MsEdge/VersionResolver.php @@ -0,0 +1,87 @@ + throw new RuntimeException('Not implemented yet.'), + OperatingSystem::MACOS => $this->getVersionFromCommandLine(sprintf('%s/Contents/MacOS/Microsoft\ Edge --version', $path)), + OperatingSystem::WINDOWS => $this->getVersionFromWindows(), + }; + } + + public function supports(BrowserName $browserName): bool + { + return $browserName === BrowserName::MSEDGE; + } + + private function getVersionFromWindows(): Version + { + $previousException = null; + foreach (self::getWindowsCommandsForVersion() as $possibleCommand) { + try { + return $this->getVersionFromCommandLine($possibleCommand); + } catch (InvalidArgumentException $exception) { + $previousException = $exception; + } + } + + throw new InvalidArgumentException('Version could not be determined.', 0, $previousException); + } + + /** + * Provide potential commands to determine Edge Version on Windows + * + * @return string[] + */ + private static function getWindowsCommandsForVersion(): array + { + $versions = [ + 'Edge', + 'Edge Beta', + 'Edge Dev', + 'Edge SxS', + ]; + + $commands = []; + foreach ($versions as $version) { + $commands[] = sprintf('reg query HKCU\Software\Microsoft\%s\BLBeacon /v version', $version); + } + + return $commands; + } + + private function getVersionFromCommandLine(string $command): Version + { + try { + $commandOutput = $this->commandLineEnvironment->getCommandLineSuccessfulOutput($command); + + return Version::fromString($commandOutput); + } catch (RuntimeException $exception) { + throw new RuntimeException( + 'Version could not be determined.', + 0, + $exception, + ); + } + } +} diff --git a/src/Driver/DriverFactory.php b/src/Driver/DriverFactory.php index d676c83..55d2872 100644 --- a/src/Driver/DriverFactory.php +++ b/src/Driver/DriverFactory.php @@ -28,6 +28,7 @@ private function getDriverNameForBrowser(Browser $browser): DriverName return match ($browser->name) { BrowserName::GOOGLE_CHROME, BrowserName::CHROMIUM => DriverName::CHROME, BrowserName::FIREFOX => DriverName::GECKO, + BrowserName::MSEDGE => DriverName::MSEDGE, }; } } diff --git a/src/Driver/DriverName.php b/src/Driver/DriverName.php index 8184fdd..3c3e115 100644 --- a/src/Driver/DriverName.php +++ b/src/Driver/DriverName.php @@ -8,4 +8,5 @@ enum DriverName: string { case CHROME = 'chromedriver'; case GECKO = 'geckodriver'; + case MSEDGE = 'msedgedriver'; } diff --git a/src/Driver/MsEdgeDriver/Command.php b/src/Driver/MsEdgeDriver/Command.php new file mode 100644 index 0000000..f424936 --- /dev/null +++ b/src/Driver/MsEdgeDriver/Command.php @@ -0,0 +1,16 @@ +version->toBuildString(), + $this->getBinaryName($driver), + ); + } + + private function getBinaryName(Driver $driver): string + { + // https://msedgewebdriverstorage.z22.web.core.windows.net/?prefix=131.0.2903.112/ + return match ($driver->operatingSystem) { + OperatingSystem::LINUX => 'edgedriver_linux64', + OperatingSystem::MACOS => 'edgedriver_mac64', + OperatingSystem::WINDOWS => 'edgedriver_win64', // TODO: 32bit? + }; + } +} diff --git a/src/Driver/MsEdgeDriver/Downloader.php b/src/Driver/MsEdgeDriver/Downloader.php new file mode 100644 index 0000000..f60f2db --- /dev/null +++ b/src/Driver/MsEdgeDriver/Downloader.php @@ -0,0 +1,197 @@ +tempDir = sys_get_temp_dir(); + } + + public function supports(Driver $driver): bool + { + return $driver->name === DriverName::MSEDGE; + } + + /** @throws RuntimeException */ + public function download(Driver $driver, string $location): string + { + try { + $archive = $this->downloadArchive($driver); + } catch (NotImplemented | FilesystemException | IOException | TransportExceptionInterface $exception) { + throw new RuntimeException('Something went wrong downloading the msedgedriver archive.', 0, $exception); + } + + try { + $binary = $this->extractArchive($archive, $driver); + } catch (IOException | RuntimeException $exception) { + throw new RuntimeException('Something went wrong extracting the msedgedriver archive.', 0, $exception); + } + + $filePath = $this->getFilePath($location, $driver->operatingSystem); + + if (! $this->filesystem->exists($location)) { + $this->filesystem->mkdir($location); + } + + try { + $this->filesystem->rename($binary, $filePath, true); + } catch (IOException $exception) { + throw new RuntimeException( + sprintf('Something went wrong moving the msedgedriver to %s.', $location), + 0, + $exception, + ); + } + + $mode = 0755; + try { + $this->filesystem->chmod($filePath, $mode); + } catch (IOException $exception) { + throw new RuntimeException( + sprintf('Something went wrong setting the permissions of the msedgedriver to %d.', $mode), + 0, + $exception, + ); + } + + return $filePath; + } + + /** + * @throws NotImplemented + * @throws TransportExceptionInterface + * @throws FilesystemException + * @throws IOException + */ + private function downloadArchive(Driver $driver): string + { + $temporaryFile = $this->filesystem->tempnam($this->tempDir, 'msedgedriver', '.zip'); + + $response = $this->httpClient->request( + 'GET', + $this->downloadUrlResolver->byDriver($driver), + ); + + $fileHandler = fopen($temporaryFile, 'wb'); + + try { + foreach ($this->httpClient->stream($response) as $chunk) { + fwrite($fileHandler, $chunk->getContent()); + } + } catch (TransportExceptionInterface $exception) { + throw $exception; + } finally { + fclose($fileHandler); + } + + return $temporaryFile; + } + + /** + * @throws RuntimeException + * @throws IOException + */ + private function extractArchive(string $archive, Driver $driver): string + { + $unzipLocation = $this->tempDir . DIRECTORY_SEPARATOR . 'msedgedriver'; + $extractedFiles = $this->archiveExtractor->extract($archive, $unzipLocation); + $filePath = $this->getFilePath($unzipLocation, $driver->operatingSystem); + + if ( + ! in_array( + $filePath, + $extractedFiles, + true, + ) + ) { + throw new UnexpectedValueException(sprintf('Could not find "%s" in the extracted files.', $filePath)); + } + + $file = $this->filesystem->readlink($filePath, true); + if ($file === null) { + throw new RuntimeException(sprintf('Could not read link %s', $filePath)); + } + + $this->filesystem->remove($archive); + + return $file; + } + + private function getFilePath(string $location, OperatingSystem $operatingSystem): string + { + return $location . DIRECTORY_SEPARATOR . $this->getFileName($operatingSystem); + } + + private function getFileName(OperatingSystem $operatingSystem): string + { + $fileName = 'msedgedriver'; + + if ($operatingSystem === OperatingSystem::WINDOWS) { + $fileName .= '.exe'; + } + + return $fileName; + } + + /** + * @param string[] $extractedFiles + * + * @return string[] + */ + public function cleanArchiveStructure(Driver $driver, string $unzipLocation, array $extractedFiles): array + { + $archiveDirectory = $this->getArchiveDirectory($driver->operatingSystem); + $filename = $this->getFileName($driver->operatingSystem); + $this->filesystem->rename( + $unzipLocation . DIRECTORY_SEPARATOR . $archiveDirectory . $filename, + $unzipLocation . DIRECTORY_SEPARATOR . $filename, + true, + ); + + return str_replace($archiveDirectory, '', $extractedFiles); + } + + private function getArchiveDirectory(OperatingSystem $operatingSystem): string + { + return match ($operatingSystem) { + OperatingSystem::LINUX => 'msedgedriver-linux64/', + OperatingSystem::WINDOWS => 'msedgedriver-win32/', // This weirdly contains a forward slash on windows + OperatingSystem::MACOS => 'msedgedriver-mac-x64/', + }; + } +} diff --git a/src/Driver/MsEdgeDriver/VersionResolver.php b/src/Driver/MsEdgeDriver/VersionResolver.php new file mode 100644 index 0000000..628f924 --- /dev/null +++ b/src/Driver/MsEdgeDriver/VersionResolver.php @@ -0,0 +1,134 @@ +supports($browser)) { + throw new Unsupported(sprintf('%s is not supported.', $browser->name->value)); + } + + try { + $versionString = $this->getVersionString($browser); + } catch ( + ClientExceptionInterface + | RedirectionExceptionInterface + | ServerExceptionInterface + | TransportExceptionInterface + $exception + ) { + throw new UnexpectedValueException( + 'Something went wrong getting the driver version from the msedgedriver API.', + 0, + $exception, + ); + } + + try { + return Version::fromString($versionString); + } catch (InvalidArgumentException $exception) { + throw new UnexpectedValueException( + 'Content received from msedgedriver API could not be parsed into a version.', + 0, + $exception, + ); + } + } + + public function supports(Browser $browser): bool + { + return $browser->name === BrowserName::MSEDGE; + } + + /** + * @throws RedirectionExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + * @throws DecodingExceptionInterface + */ + private function getVersionString(Browser $browser): string + { + $latestStable = $this->latestStableVersion(); + $latestBeta = $this->latestBetaVersion(); + $latestDev = $this->latestDevVersion(); + $latestCanary = $this->latestCanaryVersion(); + + $version = $browser->version; + // In this case we're dealing with a Dev or Canary version, so we will take the last Beta version. + $versions = [$latestStable, $latestBeta, $latestDev, $latestCanary]; + foreach ($versions as $ver) { + if (version_compare($version->toString(), $ver->toString(), '>=')) { + $version = $ver; + } + } + + return $version->toBuildString(); + } + + public function latestStableVersion(): Version + { + return $this->request(self::LATEST_STABLE_VERSION_ENDPOINT); + } + + private function latestBetaVersion(): Version + { + return $this->request(self::LATEST_BETA_VERSION_ENDPOINT); + } + + private function latestDevVersion(): Version + { + return $this->request(self::LATEST_DEV_VERSION_ENDPOINT); + } + + private function latestCanaryVersion(): Version + { + return $this->request(self::LATEST_CANARY_VERSION_ENDPOINT); + } + + public function request(string $url): Version + { + $response = $this->httpClient->request('GET', $url); + $version = $response->getContent(); + $version = str_replace(chr(0), '', substr($version, 2)); + + return Version::fromString($version); + } + + public function latest(): Version + { + return $this->latestStableVersion(); + } +}