From c803fcdb2569a2e264d6a24d217961e2f133a9ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 3 Mar 2026 08:33:23 +0100 Subject: [PATCH 01/60] Removed dependency on ReactPHP ReactPHP event loop, promise and child process have been removed and replaced with native PHP io stream functions --- library/Pdfexport/HeadlessChrome.php | 286 +++++++++++++-------------- 1 file changed, 140 insertions(+), 146 deletions(-) diff --git a/library/Pdfexport/HeadlessChrome.php b/library/Pdfexport/HeadlessChrome.php index f40b6a2..8348d81 100644 --- a/library/Pdfexport/HeadlessChrome.php +++ b/library/Pdfexport/HeadlessChrome.php @@ -12,11 +12,6 @@ use Icinga\File\Storage\TemporaryLocalFileStorage; use ipl\Html\HtmlString; use LogicException; -use React\ChildProcess\Process; -use React\EventLoop\Loop; -use React\EventLoop\TimerInterface; -use React\Promise; -use React\Promise\PromiseInterface; use Throwable; use WebSocket\Client; @@ -242,170 +237,169 @@ public function fromHtml($html, $asFile = false) return $this; } - /** - * Generate a PDF raw string asynchronously. - * - * @return PromiseInterface - */ - public function asyncToPdf(): PromiseInterface + public function toPdf(): string { - $deferred = new Promise\Deferred(); - Loop::futureTick(function () use ($deferred) { - switch (true) { - case $this->remote !== null: - try { - $result = $this->jsonVersion($this->remote[0], $this->remote[1]); - if (is_array($result)) { - $parts = explode('/', $result['webSocketDebuggerUrl']); - $pdf = $this->printToPDF( - join(':', $this->remote), - end($parts), - ! $this->document->isEmpty() ? $this->document->getPrintParameters() : [] - ); - break; - } - } catch (Exception $e) { - if ($this->binary == null) { - $deferred->reject($e); - return; - } - - Logger::warning( - 'Failed to connect to remote chrome: %s:%d (%s)', - $this->remote[0], - $this->remote[1], - $e + switch (true) { + case $this->remote !== null: + try { + $result = $this->jsonVersion($this->remote[0], $this->remote[1]); + if (is_array($result)) { + $parts = explode('/', $result['webSocketDebuggerUrl']); + $pdf = $this->printToPDF( + join(':', $this->remote), + end($parts), + ! $this->document->isEmpty() ? $this->document->getPrintParameters() : [] ); + break; } + } catch (Exception $e) { + Logger::warning( + 'Failed to connect to remote chrome: %s:%d (%s)', + $this->remote[0], + $this->remote[1], + $e + ); - // Reject the promise if we didn't get the expected output from the /json/version endpoint. - if ($this->binary === null) { - $deferred->reject( - new Exception('Failed to determine remote chrome version via the /json/version endpoint.') - ); - return; - } + throw $e; + } - // Fallback to the local binary if a remote chrome is unavailable - case $this->binary !== null: - $browserHome = $this->getFileStorage()->resolvePath('HOME'); - $commandLine = join(' ', [ - escapeshellarg($this->getBinary()), - static::renderArgumentList([ - '--bwsi', - '--headless', - '--disable-gpu', - '--no-sandbox', - '--no-first-run', - '--disable-dev-shm-usage', - '--remote-debugging-port=0', - '--homedir=' => $browserHome, - '--user-data-dir=' => $browserHome - ]) - ]); - - if (Platform::isLinux()) { - Logger::debug('Starting browser process: HOME=%s exec %s', $browserHome, $commandLine); - $chrome = new Process('exec ' . $commandLine, null, ['HOME' => $browserHome]); - } else { - Logger::debug('Starting browser process: %s', $commandLine); - $chrome = new Process($commandLine); - } + // Reject the promise if we didn't get the expected output from the /json/version endpoint. + if ($this->binary === null) { + throw new Exception('Failed to determine remote chrome version via the /json/version endpoint.'); + } - $killer = Loop::addTimer(10, function (TimerInterface $timer) use ($chrome, $deferred) { - $chrome->terminate(6); // SIGABRT + break; - Logger::error( - 'Browser timed out after %d seconds without the expected output', - $timer->getInterval() - ); + // Fallback to the local binary if a remote chrome is unavailable + case $this->binary !== null: + $descriptors = [ + 0 => ['pipe', 'r'], // stdin + 1 => ['pipe', 'w'], // stdout + 2 => ['pipe', 'w'], // stderr + ]; + + + $browserHome = $this->getFileStorage()->resolvePath('HOME'); + $commandLine = join(' ', [ + escapeshellarg($this->getBinary()), + static::renderArgumentList([ + '--bwsi', + '--headless', + '--disable-gpu', + '--no-sandbox', + '--no-first-run', + '--disable-dev-shm-usage', + '--remote-debugging-port=0', + '--homedir=' => $browserHome, + '--user-data-dir=' => $browserHome + ]) + ]); + + $env = null; + if (Platform::isLinux()) { + Logger::debug('Starting browser process: HOME=%s exec %s', $browserHome, $commandLine); + $env = array_merge($_ENV, ['HOME' => $browserHome]); + $commandLine = 'exec ' . $commandLine; + } else { + Logger::debug('Starting browser process: %s', $commandLine); + } - $deferred->reject( - new Exception( - 'Received empty response or none at all from browser.' - . ' Please check the logs for further details.' - ) - ); - }); + $process = proc_open($commandLine, $descriptors, $pipes, null, $env); - $chrome->start(); + if (! is_resource($process)) { + throw new Exception('Could not start browser process.'); + } - $chrome->stderr->on('data', function ($chunk) use ($chrome, $deferred, $killer) { - Logger::debug('Caught browser output: %s', $chunk); + // Non-blocking mode + stream_set_blocking($pipes[2], false); - if (preg_match(self::DEBUG_ADDR_PATTERN, trim($chunk), $matches)) { - Loop::cancelTimer($killer); + $timeoutSeconds = 10; + $startTime = time(); + $pdf = null; - try { - $pdf = $this->printToPDF( - $matches[1], - $matches[2], - ! $this->document->isEmpty() ? $this->document->getPrintParameters() : [] - ); - } catch (Exception $e) { - Logger::error('Failed to print PDF. An error occurred: %s', $e); - } + while (true) { + $status = proc_get_status($process); + + // Timeout handling + if ((time() - $startTime) > $timeoutSeconds) { + proc_terminate($process, 6); // SIGABRT + Logger::error( + 'Browser timed out after %d seconds without the expected output', + $timeoutSeconds + ); - $chrome->terminate(); + throw new Exception( + 'Received empty response or none at all from browser.' + . ' Please check the logs for further details.' + ); + } - if (! empty($pdf)) { - $deferred->resolve($pdf); - } else { - $deferred->reject( - new Exception( - 'Received empty response or none at all from browser.' - . ' Please check the logs for further details.' - ) + $chunkSize = 8192; + $streamWaitTime = 200000; + $idleTime = 100000; + $read = [$pipes[2]]; + $write = null; + $except = null; + + if (stream_select($read, $write, $except, 0, $streamWaitTime)) { + $chunk = fread($pipes[2], $chunkSize); + + if ($chunk !== false && $chunk !== '') { + Logger::debug('Caught browser output: %s', $chunk); + + if (preg_match(self::DEBUG_ADDR_PATTERN, trim($chunk), $matches)) { + + try { + $pdf = $this->printToPDF( + $matches[1], + $matches[2], + ! $this->document->isEmpty() + ? $this->document->getPrintParameters() + : [] + ); + } catch (Exception $e) { + Logger::error('Failed to print PDF. An error occurred: %s', $e->getMessage()); + } + + proc_terminate($process); + + if (! empty($pdf)) { + break; + } + + throw new Exception( + 'Received empty response or none at all from browser.' + . ' Please check the logs for further details.' ); } } - }); - - $chrome->on('exit', function ($exitCode, $signal) use ($killer) { - Loop::cancelTimer($killer); - - Logger::debug('Browser terminated by signal %d and exited with code %d', $signal, $exitCode); - - // Browser is either timed out (after 10s) and the promise should have already been rejected, - // or it is terminated using its terminate() method, in which case the promise is also already - // resolved/rejected. So, we don't need to resolve/reject the promise here. - }); - - return; - } + } - if (! empty($pdf)) { - $deferred->resolve($pdf); - } else { - $deferred->reject( - new Exception( - 'Received empty response or none at all from browser.' - . ' Please check the logs for further details.' - ) - ); - } - }); + if (! $status['running']) { + break; + } - return $deferred->promise(); - } + usleep($idleTime); + } - /** - * Export to PDF - * - * @return string - * @throws Exception - */ - public function toPdf() - { - $pdf = ''; + // Cleanup + foreach ($pipes as $pipe) { + fclose($pipe); + } - $this->asyncToPdf()->then(function (string $newPdf) use (&$pdf) { - $pdf = $newPdf; - }); + proc_close($process); - Loop::run(); + return $pdf; + } - return $pdf; + if (! empty($pdf)) { + return $pdf; + } else { + throw new Exception( + 'Received empty response or none at all from browser.' + . ' Please check the logs for further details.', + ); + } } /** From b04bf63327633ee8c4052b05e386cab69ea7251e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 13:47:22 +0100 Subject: [PATCH 02/60] Install php-webdriver --- composer.json | 1 + composer.lock | 220 ++- vendor/autoload.php | 5 +- vendor/composer/InstalledVersions.php | 38 +- vendor/composer/autoload_files.php | 11 + vendor/composer/autoload_psr4.php | 5 +- vendor/composer/autoload_real.php | 12 + vendor/composer/autoload_static.php | 52 +- vendor/composer/installed.json | 225 +++ vendor/composer/installed.php | 37 +- vendor/composer/platform_check.php | 5 +- vendor/php-webdriver/webdriver/CHANGELOG.md | 266 +++ vendor/php-webdriver/webdriver/LICENSE.md | 22 + vendor/php-webdriver/webdriver/README.md | 228 +++ vendor/php-webdriver/webdriver/composer.json | 97 + .../lib/AbstractWebDriverCheckboxOrRadio.php | 240 +++ .../lib/Chrome/ChromeDevToolsDriver.php | 46 + .../webdriver/lib/Chrome/ChromeDriver.php | 107 ++ .../lib/Chrome/ChromeDriverService.php | 37 + .../webdriver/lib/Chrome/ChromeOptions.php | 182 ++ vendor/php-webdriver/webdriver/lib/Cookie.php | 278 +++ .../Exception/DetachedShadowRootException.php | 10 + .../ElementClickInterceptedException.php | 11 + .../ElementNotInteractableException.php | 10 + .../ElementNotSelectableException.php | 10 + .../Exception/ElementNotVisibleException.php | 10 + .../lib/Exception/ExpectedException.php | 10 + .../IMEEngineActivationFailedException.php | 10 + .../Exception/IMENotAvailableException.php | 10 + .../Exception/IndexOutOfBoundsException.php | 10 + .../InsecureCertificateException.php | 11 + .../Internal/DriverServerDiedException.php | 16 + .../lib/Exception/Internal/IOException.php | 16 + .../lib/Exception/Internal/LogicException.php | 29 + .../Exception/Internal/RuntimeException.php | 28 + .../Internal/UnexpectedResponseException.php | 51 + .../Internal/WebDriverCurlException.php | 22 + .../Exception/InvalidArgumentException.php | 10 + .../InvalidCookieDomainException.php | 10 + .../Exception/InvalidCoordinatesException.php | 10 + .../InvalidElementStateException.php | 11 + .../Exception/InvalidSelectorException.php | 10 + .../Exception/InvalidSessionIdException.php | 11 + .../Exception/JavascriptErrorException.php | 10 + .../MoveTargetOutOfBoundsException.php | 10 + .../lib/Exception/NoAlertOpenException.php | 10 + .../lib/Exception/NoCollectionException.php | 10 + .../lib/Exception/NoScriptResultException.php | 10 + .../lib/Exception/NoStringException.php | 10 + .../lib/Exception/NoStringLengthException.php | 10 + .../Exception/NoStringWrapperException.php | 10 + .../lib/Exception/NoSuchAlertException.php | 10 + .../Exception/NoSuchCollectionException.php | 10 + .../lib/Exception/NoSuchCookieException.php | 11 + .../lib/Exception/NoSuchDocumentException.php | 10 + .../lib/Exception/NoSuchDriverException.php | 10 + .../lib/Exception/NoSuchElementException.php | 10 + .../lib/Exception/NoSuchFrameException.php | 10 + .../Exception/NoSuchShadowRootException.php | 10 + .../lib/Exception/NoSuchWindowException.php | 10 + .../lib/Exception/NullPointerException.php | 10 + .../PhpWebDriverExceptionInterface.php | 10 + .../lib/Exception/ScriptTimeoutException.php | 10 + .../Exception/SessionNotCreatedException.php | 10 + .../StaleElementReferenceException.php | 10 + .../lib/Exception/TimeoutException.php | 10 + .../UnableToCaptureScreenException.php | 10 + .../Exception/UnableToSetCookieException.php | 10 + .../UnexpectedAlertOpenException.php | 10 + .../UnexpectedJavascriptException.php | 10 + .../Exception/UnexpectedTagNameException.php | 23 + .../lib/Exception/UnknownCommandException.php | 10 + .../lib/Exception/UnknownErrorException.php | 10 + .../lib/Exception/UnknownMethodException.php | 10 + .../lib/Exception/UnknownServerException.php | 10 + .../UnrecognizedExceptionException.php | 7 + .../UnsupportedOperationException.php | 10 + .../lib/Exception/WebDriverException.php | 229 +++ .../lib/Exception/XPathLookupException.php | 10 + .../webdriver/lib/Firefox/FirefoxDriver.php | 68 + .../lib/Firefox/FirefoxDriverService.php | 34 + .../webdriver/lib/Firefox/FirefoxOptions.php | 133 ++ .../lib/Firefox/FirefoxPreferences.php | 25 + .../webdriver/lib/Firefox/FirefoxProfile.php | 318 ++++ .../Internal/WebDriverButtonReleaseAction.php | 16 + .../Internal/WebDriverClickAction.php | 13 + .../Internal/WebDriverClickAndHoldAction.php | 16 + .../Internal/WebDriverContextClickAction.php | 16 + .../Internal/WebDriverCoordinates.php | 77 + .../Internal/WebDriverDoubleClickAction.php | 13 + .../Internal/WebDriverKeyDownAction.php | 12 + .../Internal/WebDriverKeyUpAction.php | 12 + .../Internal/WebDriverKeysRelatedAction.php | 43 + .../Internal/WebDriverMouseAction.php | 44 + .../Internal/WebDriverMouseMoveAction.php | 13 + .../Internal/WebDriverMoveToOffsetAction.php | 43 + .../Internal/WebDriverSendKeysAction.php | 35 + .../Internal/WebDriverSingleKeyAction.php | 54 + .../Touch/WebDriverDoubleTapAction.php | 13 + .../Touch/WebDriverDownAction.php | 33 + .../Touch/WebDriverFlickAction.php | 33 + .../Touch/WebDriverFlickFromElementAction.php | 50 + .../Touch/WebDriverLongPressAction.php | 13 + .../Touch/WebDriverMoveAction.php | 27 + .../Touch/WebDriverScrollAction.php | 27 + .../WebDriverScrollFromElementAction.php | 36 + .../Interactions/Touch/WebDriverTapAction.php | 13 + .../Touch/WebDriverTouchAction.php | 38 + .../Touch/WebDriverTouchScreen.php | 109 ++ .../lib/Interactions/WebDriverActions.php | 253 +++ .../Interactions/WebDriverCompositeAction.php | 48 + .../Interactions/WebDriverTouchActions.php | 175 ++ .../lib/Internal/WebDriverLocatable.php | 16 + .../webdriver/lib/JavaScriptExecutor.php | 35 + .../webdriver/lib/Local/LocalWebDriver.php | 37 + .../webdriver/lib/Net/URLChecker.php | 73 + .../lib/Remote/CustomWebDriverCommand.php | 82 + .../lib/Remote/DesiredCapabilities.php | 428 +++++ .../webdriver/lib/Remote/DriverCommand.php | 153 ++ .../webdriver/lib/Remote/ExecuteMethod.php | 12 + .../webdriver/lib/Remote/FileDetector.php | 16 + .../lib/Remote/HttpCommandExecutor.php | 414 ++++ .../webdriver/lib/Remote/JsonWireCompat.php | 98 + .../lib/Remote/LocalFileDetector.php | 20 + .../lib/Remote/RemoteExecuteMethod.php | 25 + .../webdriver/lib/Remote/RemoteKeyboard.php | 105 ++ .../webdriver/lib/Remote/RemoteMouse.php | 290 +++ .../webdriver/lib/Remote/RemoteStatus.php | 79 + .../lib/Remote/RemoteTargetLocator.php | 149 ++ .../lib/Remote/RemoteTouchScreen.php | 177 ++ .../webdriver/lib/Remote/RemoteWebDriver.php | 760 ++++++++ .../webdriver/lib/Remote/RemoteWebElement.php | 650 +++++++ .../Remote/Service/DriverCommandExecutor.php | 53 + .../lib/Remote/Service/DriverService.php | 183 ++ .../webdriver/lib/Remote/ShadowRoot.php | 98 + .../lib/Remote/UselessFileDetector.php | 11 + .../lib/Remote/WebDriverBrowserType.php | 40 + .../lib/Remote/WebDriverCapabilityType.php | 32 + .../webdriver/lib/Remote/WebDriverCommand.php | 60 + .../lib/Remote/WebDriverResponse.php | 84 + .../Support/Events/EventFiringWebDriver.php | 394 ++++ .../Events/EventFiringWebDriverNavigation.php | 135 ++ .../Support/Events/EventFiringWebElement.php | 413 ++++ .../lib/Support/IsElementDisplayedAtom.php | 71 + .../lib/Support/ScreenshotHelper.php | 81 + .../webdriver/lib/Support/XPathEscaper.php | 32 + .../php-webdriver/webdriver/lib/WebDriver.php | 143 ++ .../webdriver/lib/WebDriverAction.php | 11 + .../webdriver/lib/WebDriverAlert.php | 72 + .../webdriver/lib/WebDriverBy.php | 134 ++ .../webdriver/lib/WebDriverCapabilities.php | 46 + .../webdriver/lib/WebDriverCheckboxes.php | 53 + .../lib/WebDriverCommandExecutor.php | 17 + .../webdriver/lib/WebDriverDimension.php | 59 + .../webdriver/lib/WebDriverDispatcher.php | 75 + .../webdriver/lib/WebDriverElement.php | 154 ++ .../webdriver/lib/WebDriverEventListener.php | 52 + .../lib/WebDriverExpectedCondition.php | 584 ++++++ .../lib/WebDriverHasInputDevices.php | 19 + .../webdriver/lib/WebDriverKeyboard.php | 32 + .../webdriver/lib/WebDriverKeys.php | 132 ++ .../webdriver/lib/WebDriverMouse.php | 47 + .../webdriver/lib/WebDriverNavigation.php | 45 + .../lib/WebDriverNavigationInterface.php | 43 + .../webdriver/lib/WebDriverOptions.php | 180 ++ .../webdriver/lib/WebDriverPlatform.php | 25 + .../webdriver/lib/WebDriverPoint.php | 80 + .../webdriver/lib/WebDriverRadios.php | 52 + .../webdriver/lib/WebDriverSearchContext.php | 28 + .../webdriver/lib/WebDriverSelect.php | 250 +++ .../lib/WebDriverSelectInterface.php | 128 ++ .../webdriver/lib/WebDriverTargetLocator.php | 69 + .../webdriver/lib/WebDriverTimeouts.php | 102 + .../webdriver/lib/WebDriverUpAction.php | 28 + .../webdriver/lib/WebDriverWait.php | 73 + .../webdriver/lib/WebDriverWindow.php | 188 ++ .../lib/scripts/isElementDisplayed.js | 219 +++ vendor/symfony/polyfill-mbstring/LICENSE | 19 + vendor/symfony/polyfill-mbstring/Mbstring.php | 1045 ++++++++++ vendor/symfony/polyfill-mbstring/README.md | 13 + .../Resources/unidata/caseFolding.php | 119 ++ .../Resources/unidata/lowerCase.php | 1397 ++++++++++++++ .../Resources/unidata/titleCaseRegexp.php | 5 + .../Resources/unidata/upperCase.php | 1489 +++++++++++++++ .../symfony/polyfill-mbstring/bootstrap.php | 172 ++ .../symfony/polyfill-mbstring/bootstrap80.php | 167 ++ .../symfony/polyfill-mbstring/composer.json | 39 + vendor/symfony/process/CHANGELOG.md | 134 ++ .../process/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 21 + .../process/Exception/LogicException.php | 21 + .../Exception/ProcessFailedException.php | 53 + .../Exception/ProcessSignaledException.php | 38 + .../Exception/ProcessStartFailedException.php | 43 + .../Exception/ProcessTimedOutException.php | 60 + .../Exception/RunProcessFailedException.php | 25 + .../process/Exception/RuntimeException.php | 21 + vendor/symfony/process/ExecutableFinder.php | 103 + vendor/symfony/process/InputStream.php | 91 + vendor/symfony/process/LICENSE | 19 + .../process/Messenger/RunProcessContext.php | 33 + .../process/Messenger/RunProcessMessage.php | 47 + .../Messenger/RunProcessMessageHandler.php | 36 + .../symfony/process/PhpExecutableFinder.php | 98 + vendor/symfony/process/PhpProcess.php | 69 + vendor/symfony/process/PhpSubprocess.php | 167 ++ .../symfony/process/Pipes/AbstractPipes.php | 204 ++ .../symfony/process/Pipes/PipesInterface.php | 61 + vendor/symfony/process/Pipes/UnixPipes.php | 144 ++ vendor/symfony/process/Pipes/WindowsPipes.php | 185 ++ vendor/symfony/process/Process.php | 1676 +++++++++++++++++ vendor/symfony/process/ProcessUtils.php | 64 + vendor/symfony/process/README.md | 13 + vendor/symfony/process/composer.json | 28 + 214 files changed, 21212 insertions(+), 57 deletions(-) create mode 100644 vendor/composer/autoload_files.php create mode 100644 vendor/php-webdriver/webdriver/CHANGELOG.md create mode 100644 vendor/php-webdriver/webdriver/LICENSE.md create mode 100644 vendor/php-webdriver/webdriver/README.md create mode 100644 vendor/php-webdriver/webdriver/composer.json create mode 100644 vendor/php-webdriver/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php create mode 100644 vendor/php-webdriver/webdriver/lib/Chrome/ChromeDevToolsDriver.php create mode 100644 vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriver.php create mode 100644 vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriverService.php create mode 100644 vendor/php-webdriver/webdriver/lib/Chrome/ChromeOptions.php create mode 100644 vendor/php-webdriver/webdriver/lib/Cookie.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/DetachedShadowRootException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ElementClickInterceptedException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ElementNotInteractableException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ElementNotSelectableException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ElementNotVisibleException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ExpectedException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/IMEEngineActivationFailedException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/IMENotAvailableException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/IndexOutOfBoundsException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InsecureCertificateException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/DriverServerDiedException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/IOException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/LogicException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/RuntimeException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/UnexpectedResponseException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/WebDriverCurlException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidArgumentException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidCookieDomainException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidCoordinatesException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidElementStateException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidSelectorException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidSessionIdException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/JavascriptErrorException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/MoveTargetOutOfBoundsException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoAlertOpenException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoCollectionException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoScriptResultException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoStringException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoStringLengthException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoStringWrapperException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchAlertException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchCollectionException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchCookieException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchDocumentException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchDriverException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchElementException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchFrameException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchShadowRootException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchWindowException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NullPointerException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/PhpWebDriverExceptionInterface.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ScriptTimeoutException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/SessionNotCreatedException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/StaleElementReferenceException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/TimeoutException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnableToCaptureScreenException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnableToSetCookieException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnexpectedAlertOpenException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnexpectedJavascriptException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnexpectedTagNameException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnknownCommandException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnknownErrorException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnknownMethodException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnknownServerException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnrecognizedExceptionException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnsupportedOperationException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/WebDriverException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Exception/XPathLookupException.php create mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriver.php create mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriverService.php create mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxOptions.php create mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxPreferences.php create mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxProfile.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverCoordinates.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDownAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTapAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/WebDriverActions.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/WebDriverCompositeAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/WebDriverTouchActions.php create mode 100644 vendor/php-webdriver/webdriver/lib/Internal/WebDriverLocatable.php create mode 100644 vendor/php-webdriver/webdriver/lib/JavaScriptExecutor.php create mode 100644 vendor/php-webdriver/webdriver/lib/Local/LocalWebDriver.php create mode 100644 vendor/php-webdriver/webdriver/lib/Net/URLChecker.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/CustomWebDriverCommand.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/DesiredCapabilities.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/DriverCommand.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/ExecuteMethod.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/FileDetector.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/HttpCommandExecutor.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/JsonWireCompat.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/LocalFileDetector.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteExecuteMethod.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteKeyboard.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteMouse.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteStatus.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteTargetLocator.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteTouchScreen.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/Service/DriverCommandExecutor.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/Service/DriverService.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/ShadowRoot.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/UselessFileDetector.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/WebDriverBrowserType.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/WebDriverCapabilityType.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/WebDriverCommand.php create mode 100644 vendor/php-webdriver/webdriver/lib/Remote/WebDriverResponse.php create mode 100644 vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriver.php create mode 100644 vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php create mode 100644 vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebElement.php create mode 100644 vendor/php-webdriver/webdriver/lib/Support/IsElementDisplayedAtom.php create mode 100644 vendor/php-webdriver/webdriver/lib/Support/ScreenshotHelper.php create mode 100644 vendor/php-webdriver/webdriver/lib/Support/XPathEscaper.php create mode 100755 vendor/php-webdriver/webdriver/lib/WebDriver.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverAlert.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverBy.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverCapabilities.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverCheckboxes.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverCommandExecutor.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverDimension.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverDispatcher.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverElement.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverEventListener.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverExpectedCondition.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverHasInputDevices.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverKeyboard.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverKeys.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverMouse.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverNavigation.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverNavigationInterface.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverOptions.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverPlatform.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverPoint.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverRadios.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverSearchContext.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverSelect.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverSelectInterface.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverTargetLocator.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverTimeouts.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverUpAction.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverWait.php create mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverWindow.php create mode 100644 vendor/php-webdriver/webdriver/lib/scripts/isElementDisplayed.js create mode 100644 vendor/symfony/polyfill-mbstring/LICENSE create mode 100644 vendor/symfony/polyfill-mbstring/Mbstring.php create mode 100644 vendor/symfony/polyfill-mbstring/README.md create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php create mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php create mode 100644 vendor/symfony/polyfill-mbstring/bootstrap.php create mode 100644 vendor/symfony/polyfill-mbstring/bootstrap80.php create mode 100644 vendor/symfony/polyfill-mbstring/composer.json create mode 100644 vendor/symfony/process/CHANGELOG.md create mode 100644 vendor/symfony/process/Exception/ExceptionInterface.php create mode 100644 vendor/symfony/process/Exception/InvalidArgumentException.php create mode 100644 vendor/symfony/process/Exception/LogicException.php create mode 100644 vendor/symfony/process/Exception/ProcessFailedException.php create mode 100644 vendor/symfony/process/Exception/ProcessSignaledException.php create mode 100644 vendor/symfony/process/Exception/ProcessStartFailedException.php create mode 100644 vendor/symfony/process/Exception/ProcessTimedOutException.php create mode 100644 vendor/symfony/process/Exception/RunProcessFailedException.php create mode 100644 vendor/symfony/process/Exception/RuntimeException.php create mode 100644 vendor/symfony/process/ExecutableFinder.php create mode 100644 vendor/symfony/process/InputStream.php create mode 100644 vendor/symfony/process/LICENSE create mode 100644 vendor/symfony/process/Messenger/RunProcessContext.php create mode 100644 vendor/symfony/process/Messenger/RunProcessMessage.php create mode 100644 vendor/symfony/process/Messenger/RunProcessMessageHandler.php create mode 100644 vendor/symfony/process/PhpExecutableFinder.php create mode 100644 vendor/symfony/process/PhpProcess.php create mode 100644 vendor/symfony/process/PhpSubprocess.php create mode 100644 vendor/symfony/process/Pipes/AbstractPipes.php create mode 100644 vendor/symfony/process/Pipes/PipesInterface.php create mode 100644 vendor/symfony/process/Pipes/UnixPipes.php create mode 100644 vendor/symfony/process/Pipes/WindowsPipes.php create mode 100644 vendor/symfony/process/Process.php create mode 100644 vendor/symfony/process/ProcessUtils.php create mode 100644 vendor/symfony/process/README.md create mode 100644 vendor/symfony/process/composer.json diff --git a/composer.json b/composer.json index 79d6d79..e390362 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "require": { "php": ">=8.2", "karriere/pdf-merge": "dev-master", + "php-webdriver/webdriver": "1.15.2", "phrity/websocket": "^3.6" }, "config": { diff --git a/composer.lock b/composer.lock index f749ab2..ee808dd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5c3df7bceb770cd13e1f582b6dcc2ff5", + "content-hash": "5936242128deac4d5d29d07f8df8a2a5", "packages": [ { "name": "karriere/pdf-merge", @@ -92,6 +92,72 @@ }, "time": "2026-01-19T15:39:37+00:00" }, + { + "name": "php-webdriver/webdriver", + "version": "1.15.2", + "source": { + "type": "git", + "url": "https://github.com/php-webdriver/php-webdriver.git", + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf", + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-zip": "*", + "php": "^7.3 || ^8.0", + "symfony/polyfill-mbstring": "^1.12", + "symfony/process": "^5.0 || ^6.0 || ^7.0" + }, + "replace": { + "facebook/webdriver": "*" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.20.0", + "ondram/ci-detector": "^4.0", + "php-coveralls/php-coveralls": "^2.4", + "php-mock/php-mock-phpunit": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Exception/TimeoutException.php" + ], + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", + "homepage": "https://github.com/php-webdriver/php-webdriver", + "keywords": [ + "Chromedriver", + "geckodriver", + "php", + "selenium", + "webdriver" + ], + "support": { + "issues": "https://github.com/php-webdriver/php-webdriver/issues", + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" + }, + "time": "2024-11-21T15:12:59+00:00" + }, { "name": "phrity/comparison", "version": "1.4.1", @@ -602,6 +668,156 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "608476f4604102976d687c483ac63a79ba18cc97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:07:59+00:00" + }, { "name": "tecnickcom/tcpdf", "version": "6.10.0", @@ -686,5 +902,5 @@ "php": ">=8.2" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/vendor/autoload.php b/vendor/autoload.php index 5f84171..7642d2c 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -14,7 +14,10 @@ echo $err; } } - throw new RuntimeException($err); + trigger_error( + $err, + E_USER_ERROR + ); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index 2052022..07b32ed 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -26,23 +26,12 @@ */ class InstalledVersions { - /** - * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to - * @internal - */ - private static $selfDir = null; - /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null */ private static $installed; - /** - * @var bool - */ - private static $installedIsLocalDir; - /** * @var bool|null */ @@ -320,24 +309,6 @@ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); - - // when using reload, we disable the duplicate protection to ensure that self::$installed data is - // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, - // so we have to assume it does not, and that may result in duplicate data being returned when listing - // all installed packages for example - self::$installedIsLocalDir = false; - } - - /** - * @return string - */ - private static function getSelfDir() - { - if (self::$selfDir === null) { - self::$selfDir = strtr(__DIR__, '\\', '/'); - } - - return self::$selfDir; } /** @@ -354,9 +325,7 @@ private static function getInstalled() $copiedLocalDir = false; if (self::$canGetVendors) { - $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { - $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { @@ -364,14 +333,11 @@ private static function getInstalled() $required = require $vendorDir.'/composer/installed.php'; self::$installedByVendor[$vendorDir] = $required; $installed[] = $required; - if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $required; - self::$installedIsLocalDir = true; + $copiedLocalDir = true; } } - if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { - $copiedLocalDir = true; - } } } diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php new file mode 100644 index 0000000..4314571 --- /dev/null +++ b/vendor/composer/autoload_files.php @@ -0,0 +1,11 @@ + $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '2a3c2110e8e0295330dc3d11a4cbc4cb' => $vendorDir . '/php-webdriver/webdriver/lib/Exception/TimeoutException.php', +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 6e32a62..81b5069 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -7,11 +7,14 @@ return array( 'WebSocket\\' => array($vendorDir . '/phrity/websocket/src'), + 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), + 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'), 'Phrity\\Util\\' => array($vendorDir . '/phrity/util-errorhandler/src'), - 'Phrity\\Net\\' => array($vendorDir . '/phrity/net-uri/src', $vendorDir . '/phrity/net-stream/src'), + 'Phrity\\Net\\' => array($vendorDir . '/phrity/net-stream/src', $vendorDir . '/phrity/net-uri/src'), 'Phrity\\Http\\' => array($vendorDir . '/phrity/http/src'), 'Phrity\\Comparison\\' => array($vendorDir . '/phrity/comparison/src'), 'Karriere\\PdfMerge\\' => array($vendorDir . '/karriere/pdf-merge/src'), + 'Facebook\\WebDriver\\' => array($vendorDir . '/php-webdriver/webdriver/lib'), ); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 9468cad..936217f 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -33,6 +33,18 @@ public static function getLoader() $loader->register(true); + $filesToLoad = \Composer\Autoload\ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + return $loader; } } diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 5af240e..fffe6ec 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -6,12 +6,22 @@ class ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776 { + public static $files = array ( + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '2a3c2110e8e0295330dc3d11a4cbc4cb' => __DIR__ . '/..' . '/php-webdriver/webdriver/lib/Exception/TimeoutException.php', + ); + public static $prefixLengthsPsr4 = array ( - 'W' => + 'W' => array ( 'WebSocket\\' => 10, ), - 'P' => + 'S' => + array ( + 'Symfony\\Polyfill\\Mbstring\\' => 26, + 'Symfony\\Component\\Process\\' => 26, + ), + 'P' => array ( 'Psr\\Log\\' => 8, 'Psr\\Http\\Message\\' => 17, @@ -20,47 +30,63 @@ class ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776 'Phrity\\Http\\' => 12, 'Phrity\\Comparison\\' => 18, ), - 'K' => + 'K' => array ( 'Karriere\\PdfMerge\\' => 18, ), + 'F' => + array ( + 'Facebook\\WebDriver\\' => 19, + ), ); public static $prefixDirsPsr4 = array ( - 'WebSocket\\' => + 'WebSocket\\' => array ( 0 => __DIR__ . '/..' . '/phrity/websocket/src', ), - 'Psr\\Log\\' => + 'Symfony\\Polyfill\\Mbstring\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', + ), + 'Symfony\\Component\\Process\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/process', + ), + 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/src', ), - 'Psr\\Http\\Message\\' => + 'Psr\\Http\\Message\\' => array ( 0 => __DIR__ . '/..' . '/psr/http-factory/src', 1 => __DIR__ . '/..' . '/psr/http-message/src', ), - 'Phrity\\Util\\' => + 'Phrity\\Util\\' => array ( 0 => __DIR__ . '/..' . '/phrity/util-errorhandler/src', ), - 'Phrity\\Net\\' => + 'Phrity\\Net\\' => array ( - 0 => __DIR__ . '/..' . '/phrity/net-uri/src', - 1 => __DIR__ . '/..' . '/phrity/net-stream/src', + 0 => __DIR__ . '/..' . '/phrity/net-stream/src', + 1 => __DIR__ . '/..' . '/phrity/net-uri/src', ), - 'Phrity\\Http\\' => + 'Phrity\\Http\\' => array ( 0 => __DIR__ . '/..' . '/phrity/http/src', ), - 'Phrity\\Comparison\\' => + 'Phrity\\Comparison\\' => array ( 0 => __DIR__ . '/..' . '/phrity/comparison/src', ), - 'Karriere\\PdfMerge\\' => + 'Karriere\\PdfMerge\\' => array ( 0 => __DIR__ . '/..' . '/karriere/pdf-merge/src', ), + 'Facebook\\WebDriver\\' => + array ( + 0 => __DIR__ . '/..' . '/php-webdriver/webdriver/lib', + ), ); public static $classMap = array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index a87ff1f..0d56a51 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -89,6 +89,75 @@ }, "install-path": "../karriere/pdf-merge" }, + { + "name": "php-webdriver/webdriver", + "version": "1.15.2", + "version_normalized": "1.15.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-webdriver/php-webdriver.git", + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf", + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-zip": "*", + "php": "^7.3 || ^8.0", + "symfony/polyfill-mbstring": "^1.12", + "symfony/process": "^5.0 || ^6.0 || ^7.0" + }, + "replace": { + "facebook/webdriver": "*" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.20.0", + "ondram/ci-detector": "^4.0", + "php-coveralls/php-coveralls": "^2.4", + "php-mock/php-mock-phpunit": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "time": "2024-11-21T15:12:59+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/Exception/TimeoutException.php" + ], + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", + "homepage": "https://github.com/php-webdriver/php-webdriver", + "keywords": [ + "Chromedriver", + "geckodriver", + "php", + "selenium", + "webdriver" + ], + "support": { + "issues": "https://github.com/php-webdriver/php-webdriver/issues", + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" + }, + "install-path": "../php-webdriver/webdriver" + }, { "name": "phrity/comparison", "version": "1.4.1", @@ -626,6 +695,162 @@ }, "install-path": "../psr/log" }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "version_normalized": "1.33.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "time": "2024-12-23T08:48:59+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-mbstring" + }, + { + "name": "symfony/process", + "version": "v7.4.5", + "version_normalized": "7.4.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "608476f4604102976d687c483ac63a79ba18cc97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "time": "2026-01-26T15:07:59+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/process" + }, { "name": "tecnickcom/tcpdf", "version": "6.10.0", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 61e4d33..c248751 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '9ce3413e78d2809619557cbe6ea79fafc5dd70c5', + 'reference' => '9468d76922e728ff5aa16d55fc4ca472755e07e5', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -13,12 +13,18 @@ '__root__' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '9ce3413e78d2809619557cbe6ea79fafc5dd70c5', + 'reference' => '9468d76922e728ff5aa16d55fc4ca472755e07e5', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), + 'facebook/webdriver' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), 'karriere/pdf-merge' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', @@ -30,6 +36,15 @@ ), 'dev_requirement' => false, ), + 'php-webdriver/webdriver' => array( + 'pretty_version' => '1.15.2', + 'version' => '1.15.2.0', + 'reference' => '998e499b786805568deaf8cbf06f4044f05d91bf', + 'type' => 'library', + 'install_path' => __DIR__ . '/../php-webdriver/webdriver', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'phrity/comparison' => array( 'pretty_version' => '1.4.1', 'version' => '1.4.1.0', @@ -111,6 +126,24 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/polyfill-mbstring' => array( + 'pretty_version' => 'v1.33.0', + 'version' => '1.33.0.0', + 'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/process' => array( + 'pretty_version' => 'v7.4.5', + 'version' => '7.4.5.0', + 'reference' => '608476f4604102976d687c483ac63a79ba18cc97', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/process', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'tecnickcom/tcpdf' => array( 'pretty_version' => '6.10.0', 'version' => '6.10.0.0', diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index 14bf88d..d32d90c 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -19,7 +19,8 @@ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - throw new \RuntimeException( - 'Composer detected issues in your platform: ' . implode(' ', $issues) + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR ); } diff --git a/vendor/php-webdriver/webdriver/CHANGELOG.md b/vendor/php-webdriver/webdriver/CHANGELOG.md new file mode 100644 index 0000000..a468c26 --- /dev/null +++ b/vendor/php-webdriver/webdriver/CHANGELOG.md @@ -0,0 +1,266 @@ +# Changelog +This project versioning adheres to [Semantic Versioning](http://semver.org/). + +## Unreleased + +## 1.15.2 - 2024-11-21 +### Fixed +- PHP 8.4 deprecation notices, especially in nullable type-hints. +- Docs: Fix static return types in RemoteWebElement phpDoc. +- Tests: Disable chrome 127+ search engine pop-up in tests +- Tests: Enable Shadow DOM tests in Geckodriver + +### Added +- Tests: Allow running tests in headfull (not headless) mode using `DISABLE_HEADLESS` environment variable. + +### Changed +- Docs: Update selenium server host URL in example. + +## 1.15.1 - 2023-10-20 +- Update `symfony/process` dependency to support upcoming Symfony 7. + +## 1.15.0 - 2023-08-29 +### Changed +- Capability key `ChromeOptions::CAPABILITY_W3C` used to set ChromeOptions is now deprecated in favor of `ChromeOptions::CAPABILITY`, which now also contains the W3C compatible value (`goog:chromeOptions`). +- ChromeOptions are now passed to the driver always as a W3C compatible key `goog:chromeOptions`, even in the deprecated OSS JsonWire payload (as ChromeDriver [supports](https://bugs.chromium.org/p/chromedriver/issues/detail?id=1786) this since 2017). +- Improve Safari compatibility for `` by its partial text (using `selectByVisiblePartialText()`). +- `XPathEscaper` helper class to quote XPaths containing both single and double quotes. +- `WebDriverSelectInterface`, to allow implementation of custom select-like components, eg. those not built around and actual select tag. + +### Changed +- `Symfony\Process` is used to start local WebDriver processes (when browsers are run directly, without Selenium server) to workaround some PHP bugs and improve portability. +- Clarified meaning of selenium server URL variable in methods of `RemoteWebDriver` class. +- Deprecated `setSessionID()` and `setCommandExecutor()` methods of `RemoteWebDriver` class; these values should be immutable and thus passed only via constructor. +- Deprecated `WebDriverExpectedCondition::textToBePresentInElement()` in favor of `elementTextContains()`. +- Throw an exception when attempting to deselect options of non-multiselect (it already didn't have any effect, but was silently ignored). +- Optimize performance of `(de)selectByIndex()` and `getAllSelectedOptions()` methods of `WebDriverSelect` when used with non-multiple select element. + +### Fixed +- XPath escaping in `select*()` and `deselect*()` methods of `WebDriverSelect`. + +## 1.2.0 - 2016-10-14 +- Added initial support of remote Microsoft Edge browser (but starting local EdgeDriver is still not supported). +- Utilize late static binding to make eg. `WebDriverBy` and `DesiredCapabilities` classes easily extensible. +- PHP version at least 5.5 is required. +- Fixed incompatibility with Appium, caused by redundant params present in requests to Selenium server. + +## 1.1.3 - 2016-08-10 +- Fixed FirefoxProfile to support installation of extensions with custom namespace prefix in their manifest file. +- Comply codestyle with [PSR-2](http://www.php-fig.org/psr/psr-2/). + +## 1.1.2 - 2016-06-04 +- Added ext-curl to composer.json. +- Added CHANGELOG.md. +- Added CONTRIBUTING.md with information and rules for contributors. + +## 1.1.1 - 2015-12-31 +- Fixed strict standards error in `ChromeDriver`. +- Added unit tests for `WebDriverCommand` and `DesiredCapabilities`. +- Fixed retrieving temporary path name in `FirefoxDriver` when `open_basedir` restriction is in effect. + +## 1.1.0 - 2015-12-08 +- FirefoxProfile improved - added possibility to set RDF file and to add datas for extensions. +- Fixed setting 0 second timeout of `WebDriverWait`. diff --git a/vendor/php-webdriver/webdriver/LICENSE.md b/vendor/php-webdriver/webdriver/LICENSE.md new file mode 100644 index 0000000..611fa73 --- /dev/null +++ b/vendor/php-webdriver/webdriver/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2004-2020 Facebook +Copyright (c) 2020-present [open-source contributors](https://github.com/php-webdriver/php-webdriver/graphs/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/php-webdriver/webdriver/README.md b/vendor/php-webdriver/webdriver/README.md new file mode 100644 index 0000000..f3e8d38 --- /dev/null +++ b/vendor/php-webdriver/webdriver/README.md @@ -0,0 +1,228 @@ +# php-webdriver – Selenium WebDriver bindings for PHP + +[![Latest stable version](https://img.shields.io/packagist/v/php-webdriver/webdriver.svg?style=flat-square&label=Packagist)](https://packagist.org/packages/php-webdriver/webdriver) +[![GitHub Actions build status](https://img.shields.io/github/actions/workflow/status/php-webdriver/php-webdriver/tests.yaml?style=flat-square&label=GitHub%20Actions)](https://github.com/php-webdriver/php-webdriver/actions) +[![SauceLabs test status](https://img.shields.io/github/actions/workflow/status/php-webdriver/php-webdriver/sauce-labs.yaml?style=flat-square&label=SauceLabs)](https://saucelabs.com/u/php-webdriver) +[![Total downloads](https://img.shields.io/packagist/dd/php-webdriver/webdriver.svg?style=flat-square&label=Downloads)](https://packagist.org/packages/php-webdriver/webdriver) + +## Description +Php-webdriver library is PHP language binding for Selenium WebDriver, which allows you to control web browsers from PHP. + +This library is compatible with Selenium server version 2.x, 3.x and 4.x. + +The library supports modern [W3C WebDriver](https://w3c.github.io/webdriver/) protocol, as well +as legacy [JsonWireProtocol](https://www.selenium.dev/documentation/legacy/json_wire_protocol/). + +The concepts of this library are very similar to the "official" Java, JavaScript, .NET, Python and Ruby libraries +which are developed as part of the [Selenium project](https://github.com/SeleniumHQ/selenium/). + +## Installation + +Installation is possible using [Composer](https://getcomposer.org/). + +If you don't already use Composer, you can download the `composer.phar` binary: + + curl -sS https://getcomposer.org/installer | php + +Then install the library: + + php composer.phar require php-webdriver/webdriver + +## Upgrade from version <1.8.0 + +Starting from version 1.8.0, the project has been renamed from `facebook/php-webdriver` to `php-webdriver/webdriver`. + +In order to receive the new version and future updates, **you need to rename it in your composer.json**: + +```diff +"require": { +- "facebook/webdriver": "(version you use)", ++ "php-webdriver/webdriver": "(version you use)", +} +``` + +and run `composer update`. + +## Getting started + +### 1. Start server (aka. remote end) + +To control a browser, you need to start a *remote end* (server), which will listen to the commands sent +from this library and will execute them in the respective browser. + +This could be Selenium standalone server, but for local development, you can send them directly to so-called "browser driver" like Chromedriver or Geckodriver. + +#### a) Chromedriver + +📙 Below you will find a simple example. Make sure to read our wiki for [more information on Chrome/Chromedriver](https://github.com/php-webdriver/php-webdriver/wiki/Chrome). + +Install the latest Chrome and [Chromedriver](https://sites.google.com/chromium.org/driver/downloads). +Make sure to have a compatible version of Chromedriver and Chrome! + +Run `chromedriver` binary, you can pass `port` argument, so that it listens on port 4444: + +```sh +chromedriver --port=4444 +``` + +#### b) Geckodriver + +📙 Below you will find a simple example. Make sure to read our wiki for [more information on Firefox/Geckodriver](https://github.com/php-webdriver/php-webdriver/wiki/Firefox). + +Install the latest Firefox and [Geckodriver](https://github.com/mozilla/geckodriver/releases). +Make sure to have a compatible version of Geckodriver and Firefox! + +Run `geckodriver` binary (it start to listen on port 4444 by default): + +```sh +geckodriver +``` + +#### c) Selenium standalone server + +Selenium server can be useful when you need to execute multiple tests at once, +when you run tests in several different browsers (like on your CI server), or when you need to distribute tests amongst +several machines in grid mode (where one Selenium server acts as a hub, and others connect to it as nodes). + +Selenium server then act like a proxy and takes care of distributing commands to the respective nodes. + +The latest version can be found on the [Selenium download page](https://www.selenium.dev/downloads/). + +📙 You can find [further Selenium server information](https://github.com/php-webdriver/php-webdriver/wiki/Selenium-server) +in our wiki. + +#### d) Docker + +Selenium server could also be started inside Docker container - see [docker-selenium project](https://github.com/SeleniumHQ/docker-selenium). + +### 2. Create a Browser Session + +When creating a browser session, be sure to pass the url of your running server. + +For example: + +```php +// Chromedriver (if started using --port=4444 as above) +$serverUrl = 'http://localhost:4444'; +// Geckodriver +$serverUrl = 'http://localhost:4444'; +// selenium-server-standalone-#.jar (version 2.x or 3.x) +$serverUrl = 'http://localhost:4444/wd/hub'; +// selenium-server-standalone-#.jar (version 4.x) +$serverUrl = 'http://localhost:4444'; +``` + +Now you can start browser of your choice: + +```php +use Facebook\WebDriver\Remote\RemoteWebDriver; + +// Chrome +$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::chrome()); +// Firefox +$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::firefox()); +// Microsoft Edge +$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::microsoftEdge()); +``` + +### 3. Customize Desired Capabilities + +Desired capabilities define properties of the browser you are about to start. + +They can be customized: + +```php +use Facebook\WebDriver\Firefox\FirefoxOptions; +use Facebook\WebDriver\Remote\DesiredCapabilities; + +$desiredCapabilities = DesiredCapabilities::firefox(); + +// Disable accepting SSL certificates +$desiredCapabilities->setCapability('acceptSslCerts', false); + +// Add arguments via FirefoxOptions to start headless firefox +$firefoxOptions = new FirefoxOptions(); +$firefoxOptions->addArguments(['-headless']); +$desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); + +$driver = RemoteWebDriver::create($serverUrl, $desiredCapabilities); +``` + +Capabilities can also be used to [📙 configure a proxy server](https://github.com/php-webdriver/php-webdriver/wiki/HowTo-Work-with-proxy) which the browser should use. + +To configure browser-specific capabilities, you may use [📙 ChromeOptions](https://github.com/php-webdriver/php-webdriver/wiki/Chrome#chromeoptions) +or [📙 FirefoxOptions](https://github.com/php-webdriver/php-webdriver/wiki/Firefox#firefoxoptions). + +* See [legacy JsonWire protocol](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities) documentation or [W3C WebDriver specification](https://w3c.github.io/webdriver/#capabilities) for more details. + +### 4. Control your browser + +```php +// Go to URL +$driver->get('https://en.wikipedia.org/wiki/Selenium_(software)'); + +// Find search element by its id, write 'PHP' inside and submit +$driver->findElement(WebDriverBy::id('searchInput')) // find search input element + ->sendKeys('PHP') // fill the search box + ->submit(); // submit the whole form + +// Find element of 'History' item in menu by its css selector +$historyButton = $driver->findElement( + WebDriverBy::cssSelector('#ca-history a') +); +// Read text of the element and print it to output +echo 'About to click to a button with text: ' . $historyButton->getText(); + +// Click the element to navigate to revision history page +$historyButton->click(); + +// Make sure to always call quit() at the end to terminate the browser session +$driver->quit(); +``` + +See [example.php](example.php) for full example scenario. +Visit our GitHub wiki for [📙 php-webdriver command reference](https://github.com/php-webdriver/php-webdriver/wiki/Example-command-reference) and further examples. + +**NOTE:** Above snippets are not intended to be a working example by simply copy-pasting. See [example.php](example.php) for a working example. + +## Changelog +For latest changes see [CHANGELOG.md](CHANGELOG.md) file. + +## More information + +Some basic usage example is provided in [example.php](example.php) file. + +How-tos are provided right here in [📙 our GitHub wiki](https://github.com/php-webdriver/php-webdriver/wiki). + +If you don't use IDE, you may use [API documentation of php-webdriver](https://php-webdriver.github.io/php-webdriver/latest/). + +You may also want to check out the Selenium project [docs](https://selenium.dev/documentation/en/) and [wiki](https://github.com/SeleniumHQ/selenium/wiki). + +## Testing framework integration + +To take advantage of automatized testing you may want to integrate php-webdriver to your testing framework. +There are some projects already providing this: + +- [Symfony Panther](https://github.com/symfony/panther) uses php-webdriver and integrates with PHPUnit using `PantherTestCase` +- [Laravel Dusk](https://laravel.com/docs/dusk) is another project using php-webdriver, could be used for testing via `DuskTestCase` +- [Steward](https://github.com/lmc-eu/steward) integrates php-webdriver directly to [PHPUnit](https://phpunit.de/), and provides parallelization +- [Codeception](https://codeception.com/) testing framework provides BDD-layer on top of php-webdriver in its [WebDriver module](https://codeception.com/docs/modules/WebDriver) +- You can also check out this [blogpost](https://codeception.com/11-12-2013/working-with-phpunit-and-selenium-webdriver.html) + [demo project](https://github.com/DavertMik/php-webdriver-demo), describing simple [PHPUnit](https://phpunit.de/) integration + +## Support + +We have a great community willing to help you! + +❓ Do you have a **question, idea or some general feedback**? Visit our [Discussions](https://github.com/php-webdriver/php-webdriver/discussions) page. +(Alternatively, you can [look for many answered questions also on StackOverflow](https://stackoverflow.com/questions/tagged/php+selenium-webdriver)). + +🐛 Something isn't working, and you want to **report a bug**? [Submit it here](https://github.com/php-webdriver/php-webdriver/issues/new) as a new issue. + +📙 Looking for a **how-to** or **reference documentation**? See [our wiki](https://github.com/php-webdriver/php-webdriver/wiki). + +## Contributing ❤️ + +We love to have your help to make php-webdriver better. See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for more information about contributing and developing php-webdriver. + +Php-webdriver is community project - if you want to join the effort with maintaining and developing this library, the best is to look on [issues marked with "help wanted"](https://github.com/php-webdriver/php-webdriver/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) +label. Let us know in the issue comments if you want to contribute and if you want any guidance, and we will be delighted to help you to prepare your pull request. diff --git a/vendor/php-webdriver/webdriver/composer.json b/vendor/php-webdriver/webdriver/composer.json new file mode 100644 index 0000000..bd21842 --- /dev/null +++ b/vendor/php-webdriver/webdriver/composer.json @@ -0,0 +1,97 @@ +{ + "name": "php-webdriver/webdriver", + "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", + "license": "MIT", + "type": "library", + "keywords": [ + "webdriver", + "selenium", + "php", + "geckodriver", + "chromedriver" + ], + "homepage": "https://github.com/php-webdriver/php-webdriver", + "require": { + "php": "^7.3 || ^8.0", + "ext-curl": "*", + "ext-json": "*", + "ext-zip": "*", + "symfony/polyfill-mbstring": "^1.12", + "symfony/process": "^5.0 || ^6.0 || ^7.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.20.0", + "ondram/ci-detector": "^4.0", + "php-coveralls/php-coveralls": "^2.4", + "php-mock/php-mock-phpunit": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" + }, + "replace": { + "facebook/webdriver": "*" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + }, + "files": [ + "lib/Exception/TimeoutException.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Facebook\\WebDriver\\": [ + "tests/unit", + "tests/functional" + ] + }, + "classmap": [ + "tests/functional/" + ] + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + }, + "sort-packages": true + }, + "scripts": { + "post-install-cmd": [ + "@composer install --working-dir=tools/php-cs-fixer --no-progress --no-interaction", + "@composer install --working-dir=tools/phpstan --no-progress --no-interaction" + ], + "post-update-cmd": [ + "@composer update --working-dir=tools/php-cs-fixer --no-progress --no-interaction", + "@composer update --working-dir=tools/phpstan --no-progress --no-interaction" + ], + "all": [ + "@lint", + "@analyze", + "@test" + ], + "analyze": [ + "@php tools/phpstan/vendor/bin/phpstan analyze -c phpstan.neon --ansi", + "@php tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff --dry-run -vvv --ansi", + "@php vendor/bin/phpcs --standard=PSR2 --ignore=*.js ./lib/ ./tests/" + ], + "fix": [ + "@composer normalize", + "@php tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff -vvv || exit 0", + "@php vendor/bin/phpcbf --standard=PSR2 --ignore=*.js ./lib/ ./tests/" + ], + "lint": [ + "@php vendor/bin/parallel-lint -j 10 ./lib ./tests example.php", + "@composer validate", + "@composer normalize --dry-run" + ], + "test": [ + "@php vendor/bin/phpunit --colors=always" + ] + } +} diff --git a/vendor/php-webdriver/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php b/vendor/php-webdriver/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php new file mode 100644 index 0000000..00aee24 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php @@ -0,0 +1,240 @@ +getTagName(); + if ($tagName !== 'input') { + throw new UnexpectedTagNameException('input', $tagName); + } + + $this->name = $element->getAttribute('name'); + if ($this->name === null) { + throw new InvalidElementStateException('The input does not have a "name" attribute.'); + } + + $this->element = $element; + } + + public function getOptions() + { + return $this->getRelatedElements(); + } + + public function getAllSelectedOptions() + { + $selectedElement = []; + foreach ($this->getRelatedElements() as $element) { + if ($element->isSelected()) { + $selectedElement[] = $element; + + if (!$this->isMultiple()) { + return $selectedElement; + } + } + } + + return $selectedElement; + } + + public function getFirstSelectedOption() + { + foreach ($this->getRelatedElements() as $element) { + if ($element->isSelected()) { + return $element; + } + } + + throw new NoSuchElementException( + sprintf('No %s are selected', $this->type === 'radio' ? 'radio buttons' : 'checkboxes') + ); + } + + public function selectByIndex($index) + { + $this->byIndex($index); + } + + public function selectByValue($value) + { + $this->byValue($value); + } + + public function selectByVisibleText($text) + { + $this->byVisibleText($text); + } + + public function selectByVisiblePartialText($text) + { + $this->byVisibleText($text, true); + } + + /** + * Selects or deselects a checkbox or a radio button by its value. + * + * @param string $value + * @param bool $select + * @throws NoSuchElementException + */ + protected function byValue($value, $select = true) + { + $matched = false; + foreach ($this->getRelatedElements($value) as $element) { + $select ? $this->selectOption($element) : $this->deselectOption($element); + if (!$this->isMultiple()) { + return; + } + + $matched = true; + } + + if (!$matched) { + throw new NoSuchElementException( + sprintf('Cannot locate %s with value: %s', $this->type, $value) + ); + } + } + + /** + * Selects or deselects a checkbox or a radio button by its index. + * + * @param int $index + * @param bool $select + * @throws NoSuchElementException + */ + protected function byIndex($index, $select = true) + { + $elements = $this->getRelatedElements(); + if (!isset($elements[$index])) { + throw new NoSuchElementException(sprintf('Cannot locate %s with index: %d', $this->type, $index)); + } + + $select ? $this->selectOption($elements[$index]) : $this->deselectOption($elements[$index]); + } + + /** + * Selects or deselects a checkbox or a radio button by its visible text. + * + * @param string $text + * @param bool $partial + * @param bool $select + */ + protected function byVisibleText($text, $partial = false, $select = true) + { + foreach ($this->getRelatedElements() as $element) { + $normalizeFilter = sprintf( + $partial ? 'contains(normalize-space(.), %s)' : 'normalize-space(.) = %s', + XPathEscaper::escapeQuotes($text) + ); + + $xpath = 'ancestor::label'; + $xpathNormalize = sprintf('%s[%s]', $xpath, $normalizeFilter); + + $id = $element->getAttribute('id'); + if ($id !== null) { + $idFilter = sprintf('@for = %s', XPathEscaper::escapeQuotes($id)); + + $xpath .= sprintf(' | //label[%s]', $idFilter); + $xpathNormalize .= sprintf(' | //label[%s and %s]', $idFilter, $normalizeFilter); + } + + try { + $element->findElement(WebDriverBy::xpath($xpathNormalize)); + } catch (NoSuchElementException $e) { + if ($partial) { + continue; + } + + try { + // Since the mechanism of getting the text in xpath is not the same as + // webdriver, use the expensive getText() to check if nothing is matched. + if ($text !== $element->findElement(WebDriverBy::xpath($xpath))->getText()) { + continue; + } + } catch (NoSuchElementException $e) { + continue; + } + } + + $select ? $this->selectOption($element) : $this->deselectOption($element); + if (!$this->isMultiple()) { + return; + } + } + } + + /** + * Gets checkboxes or radio buttons with the same name. + * + * @param string|null $value + * @return WebDriverElement[] + */ + protected function getRelatedElements($value = null) + { + $valueSelector = $value ? sprintf(' and @value = %s', XPathEscaper::escapeQuotes($value)) : ''; + $formId = $this->element->getAttribute('form'); + if ($formId === null) { + $form = $this->element->findElement(WebDriverBy::xpath('ancestor::form')); + + $formId = $form->getAttribute('id'); + if ($formId === '' || $formId === null) { + return $form->findElements(WebDriverBy::xpath( + sprintf('.//input[@name = %s%s]', XPathEscaper::escapeQuotes($this->name), $valueSelector) + )); + } + } + + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form + return $this->element->findElements( + WebDriverBy::xpath(sprintf( + '//form[@id = %1$s]//input[@name = %2$s%3$s' + . ' and ((boolean(@form) = true() and @form = %1$s) or boolean(@form) = false())]' + . ' | //input[@form = %1$s and @name = %2$s%3$s]', + XPathEscaper::escapeQuotes($formId), + XPathEscaper::escapeQuotes($this->name), + $valueSelector + )) + ); + } + + /** + * Selects a checkbox or a radio button. + */ + protected function selectOption(WebDriverElement $element) + { + if (!$element->isSelected()) { + $element->click(); + } + } + + /** + * Deselects a checkbox or a radio button. + */ + protected function deselectOption(WebDriverElement $element) + { + if ($element->isSelected()) { + $element->click(); + } + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDevToolsDriver.php b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDevToolsDriver.php new file mode 100644 index 0000000..2d95d27 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDevToolsDriver.php @@ -0,0 +1,46 @@ + 'POST', + 'url' => '/session/:sessionId/goog/cdp/execute', + ]; + + /** + * @var RemoteWebDriver + */ + private $driver; + + public function __construct(RemoteWebDriver $driver) + { + $this->driver = $driver; + } + + /** + * Executes a Chrome DevTools command + * + * @param string $command The DevTools command to execute + * @param array $parameters Optional parameters to the command + * @return array The result of the command + */ + public function execute($command, array $parameters = []) + { + $params = ['cmd' => $command, 'params' => (object) $parameters]; + + return $this->driver->executeCustomCommand( + self::SEND_COMMAND['url'], + self::SEND_COMMAND['method'], + $params + ); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriver.php b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriver.php new file mode 100644 index 0000000..e947a49 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriver.php @@ -0,0 +1,107 @@ + [ + 'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()], + ], + 'desiredCapabilities' => (object) $capabilities->toArray(), + ] + ); + + $response = $executor->execute($newSessionCommand); + + /* + * TODO: in next major version we may not need to use this method, because without OSS compatibility the + * driver creation is straightforward. + */ + return static::createFromResponse($response, $executor); + } + + /** + * @todo Remove in next major version. The class is internally no longer used and is kept only to keep BC. + * @deprecated Use start or startUsingDriverService method instead. + * @codeCoverageIgnore + * @internal + */ + public function startSession(DesiredCapabilities $desired_capabilities) + { + $command = WebDriverCommand::newSession( + [ + 'capabilities' => [ + 'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()], + ], + 'desiredCapabilities' => (object) $desired_capabilities->toArray(), + ] + ); + $response = $this->executor->execute($command); + $value = $response->getValue(); + + if (!$this->isW3cCompliant = isset($value['capabilities'])) { + $this->executor->disableW3cCompliance(); + } + + $this->sessionID = $response->getSessionID(); + } + + /** + * @return ChromeDevToolsDriver + */ + public function getDevTools() + { + if ($this->devTools === null) { + $this->devTools = new ChromeDevToolsDriver($this); + } + + return $this->devTools; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriverService.php b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriverService.php new file mode 100644 index 0000000..902a48d --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriverService.php @@ -0,0 +1,37 @@ +toArray(); + } + + /** + * Sets the path of the Chrome executable. The path should be either absolute + * or relative to the location running ChromeDriver server. + * + * @param string $path + * @return ChromeOptions + */ + public function setBinary($path) + { + $this->binary = $path; + + return $this; + } + + /** + * @return ChromeOptions + */ + public function addArguments(array $arguments) + { + $this->arguments = array_merge($this->arguments, $arguments); + + return $this; + } + + /** + * Add a Chrome extension to install on browser startup. Each path should be + * a packed Chrome extension. + * + * @return ChromeOptions + */ + public function addExtensions(array $paths) + { + foreach ($paths as $path) { + $this->addExtension($path); + } + + return $this; + } + + /** + * @param array $encoded_extensions An array of base64 encoded of the extensions. + * @return ChromeOptions + */ + public function addEncodedExtensions(array $encoded_extensions) + { + foreach ($encoded_extensions as $encoded_extension) { + $this->addEncodedExtension($encoded_extension); + } + + return $this; + } + + /** + * Sets an experimental option which has not exposed officially. + * + * When using "prefs" to set Chrome preferences, please be aware they are so far not supported by + * Chrome running in headless mode, see https://bugs.chromium.org/p/chromium/issues/detail?id=775911 + * + * @param string $name + * @param mixed $value + * @return ChromeOptions + */ + public function setExperimentalOption($name, $value) + { + $this->experimentalOptions[$name] = $value; + + return $this; + } + + /** + * @return DesiredCapabilities The DesiredCapabilities for Chrome with this options. + */ + public function toCapabilities() + { + $capabilities = DesiredCapabilities::chrome(); + $capabilities->setCapability(self::CAPABILITY, $this); + + return $capabilities; + } + + /** + * @return \ArrayObject|array + */ + public function toArray() + { + // The selenium server expects a 'dictionary' instead of a 'list' when + // reading the chrome option. However, an empty array in PHP will be + // converted to a 'list' instead of a 'dictionary'. To fix it, we work + // with `ArrayObject` + $options = new \ArrayObject($this->experimentalOptions); + + if (!empty($this->binary)) { + $options['binary'] = $this->binary; + } + + if (!empty($this->arguments)) { + $options['args'] = $this->arguments; + } + + if (!empty($this->extensions)) { + $options['extensions'] = $this->extensions; + } + + return $options; + } + + /** + * Add a Chrome extension to install on browser startup. Each path should be a + * packed Chrome extension. + * + * @param string $path + * @return ChromeOptions + */ + private function addExtension($path) + { + $this->addEncodedExtension(base64_encode(file_get_contents($path))); + + return $this; + } + + /** + * @param string $encoded_extension Base64 encoded of the extension. + * @return ChromeOptions + */ + private function addEncodedExtension($encoded_extension) + { + $this->extensions[] = $encoded_extension; + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Cookie.php b/vendor/php-webdriver/webdriver/lib/Cookie.php new file mode 100644 index 0000000..2ae257b --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Cookie.php @@ -0,0 +1,278 @@ +validateCookieName($name); + $this->validateCookieValue($value); + + $this->cookie['name'] = $name; + $this->cookie['value'] = $value; + } + + /** + * @param array $cookieArray The cookie fields; must contain name and value. + * @return Cookie + */ + public static function createFromArray(array $cookieArray) + { + if (!isset($cookieArray['name'])) { + throw LogicException::forError('Cookie name should be set'); + } + if (!isset($cookieArray['value'])) { + throw LogicException::forError('Cookie value should be set'); + } + $cookie = new self($cookieArray['name'], $cookieArray['value']); + + if (isset($cookieArray['path'])) { + $cookie->setPath($cookieArray['path']); + } + if (isset($cookieArray['domain'])) { + $cookie->setDomain($cookieArray['domain']); + } + if (isset($cookieArray['expiry'])) { + $cookie->setExpiry($cookieArray['expiry']); + } + if (isset($cookieArray['secure'])) { + $cookie->setSecure($cookieArray['secure']); + } + if (isset($cookieArray['httpOnly'])) { + $cookie->setHttpOnly($cookieArray['httpOnly']); + } + if (isset($cookieArray['sameSite'])) { + $cookie->setSameSite($cookieArray['sameSite']); + } + + return $cookie; + } + + /** + * @return string + */ + public function getName() + { + return $this->offsetGet('name'); + } + + /** + * @return string + */ + public function getValue() + { + return $this->offsetGet('value'); + } + + /** + * The path the cookie is visible to. Defaults to "/" if omitted. + * + * @param string $path + */ + public function setPath($path) + { + $this->offsetSet('path', $path); + } + + /** + * @return string|null + */ + public function getPath() + { + return $this->offsetGet('path'); + } + + /** + * The domain the cookie is visible to. Defaults to the current browsing context's document's URL domain if omitted. + * + * @param string $domain + */ + public function setDomain($domain) + { + if (mb_strpos($domain, ':') !== false) { + throw LogicException::forError(sprintf('Cookie domain "%s" should not contain a port', $domain)); + } + + $this->offsetSet('domain', $domain); + } + + /** + * @return string|null + */ + public function getDomain() + { + return $this->offsetGet('domain'); + } + + /** + * The cookie's expiration date, specified in seconds since Unix Epoch. + * + * @param int $expiry + */ + public function setExpiry($expiry) + { + $this->offsetSet('expiry', (int) $expiry); + } + + /** + * @return int|null + */ + public function getExpiry() + { + return $this->offsetGet('expiry'); + } + + /** + * Whether this cookie requires a secure connection (https). Defaults to false if omitted. + * + * @param bool $secure + */ + public function setSecure($secure) + { + $this->offsetSet('secure', $secure); + } + + /** + * @return bool|null + */ + public function isSecure() + { + return $this->offsetGet('secure'); + } + + /** + * Whether the cookie is an HTTP only cookie. Defaults to false if omitted. + * + * @param bool $httpOnly + */ + public function setHttpOnly($httpOnly) + { + $this->offsetSet('httpOnly', $httpOnly); + } + + /** + * @return bool|null + */ + public function isHttpOnly() + { + return $this->offsetGet('httpOnly'); + } + + /** + * The cookie's same-site value. + * + * @param string $sameSite + */ + public function setSameSite($sameSite) + { + $this->offsetSet('sameSite', $sameSite); + } + + /** + * @return string|null + */ + public function getSameSite() + { + return $this->offsetGet('sameSite'); + } + + /** + * @return array + */ + public function toArray() + { + $cookie = $this->cookie; + if (!isset($cookie['secure'])) { + // Passing a boolean value for the "secure" flag is mandatory when using geckodriver + $cookie['secure'] = false; + } + + return $cookie; + } + + /** + * @param mixed $offset + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return isset($this->cookie[$offset]); + } + + /** + * @param mixed $offset + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->offsetExists($offset) ? $this->cookie[$offset] : null; + } + + /** + * @param mixed $offset + * @param mixed $value + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if ($value === null) { + unset($this->cookie[$offset]); + } else { + $this->cookie[$offset] = $value; + } + } + + /** + * @param mixed $offset + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->cookie[$offset]); + } + + /** + * @param string $name + */ + protected function validateCookieName($name) + { + if ($name === null || $name === '') { + throw LogicException::forError('Cookie name should be non-empty'); + } + + if (mb_strpos($name, ';') !== false) { + throw LogicException::forError('Cookie name should not contain a ";"'); + } + } + + /** + * @param string $value + */ + protected function validateCookieValue($value) + { + if ($value === null) { + throw LogicException::forError('Cookie value is required when setting a cookie'); + } + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Exception/DetachedShadowRootException.php b/vendor/php-webdriver/webdriver/lib/Exception/DetachedShadowRootException.php new file mode 100644 index 0000000..b9bfb39 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Exception/DetachedShadowRootException.php @@ -0,0 +1,10 @@ +getCommandLine(), + $process->getErrorOutput() + ) + ); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Exception/Internal/UnexpectedResponseException.php b/vendor/php-webdriver/webdriver/lib/Exception/Internal/UnexpectedResponseException.php new file mode 100644 index 0000000..18cdc88 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Exception/Internal/UnexpectedResponseException.php @@ -0,0 +1,51 @@ +getMessage() + ) + ); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Exception/Internal/WebDriverCurlException.php b/vendor/php-webdriver/webdriver/lib/Exception/Internal/WebDriverCurlException.php new file mode 100644 index 0000000..ac81f97 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Exception/Internal/WebDriverCurlException.php @@ -0,0 +1,22 @@ +results = $results; + } + + /** + * @return mixed + */ + public function getResults() + { + return $this->results; + } + + /** + * Throw WebDriverExceptions based on WebDriver status code. + * + * @param int|string $status_code + * @param string $message + * @param mixed $results + * + * @throws ElementClickInterceptedException + * @throws ElementNotInteractableException + * @throws ElementNotSelectableException + * @throws ElementNotVisibleException + * @throws ExpectedException + * @throws IMEEngineActivationFailedException + * @throws IMENotAvailableException + * @throws IndexOutOfBoundsException + * @throws InsecureCertificateException + * @throws InvalidArgumentException + * @throws InvalidCookieDomainException + * @throws InvalidCoordinatesException + * @throws InvalidElementStateException + * @throws InvalidSelectorException + * @throws InvalidSessionIdException + * @throws JavascriptErrorException + * @throws MoveTargetOutOfBoundsException + * @throws NoAlertOpenException + * @throws NoCollectionException + * @throws NoScriptResultException + * @throws NoStringException + * @throws NoStringLengthException + * @throws NoStringWrapperException + * @throws NoSuchAlertException + * @throws NoSuchCollectionException + * @throws NoSuchCookieException + * @throws NoSuchDocumentException + * @throws NoSuchDriverException + * @throws NoSuchElementException + * @throws NoSuchFrameException + * @throws NoSuchWindowException + * @throws NullPointerException + * @throws ScriptTimeoutException + * @throws SessionNotCreatedException + * @throws StaleElementReferenceException + * @throws TimeoutException + * @throws UnableToCaptureScreenException + * @throws UnableToSetCookieException + * @throws UnexpectedAlertOpenException + * @throws UnexpectedJavascriptException + * @throws UnknownCommandException + * @throws UnknownErrorException + * @throws UnknownMethodException + * @throws UnknownServerException + * @throws UnrecognizedExceptionException + * @throws UnsupportedOperationException + * @throws XPathLookupException + */ + public static function throwException($status_code, $message, $results) + { + if (is_string($status_code)) { + // @see https://w3c.github.io/webdriver/#errors + switch ($status_code) { + case 'element click intercepted': + throw new ElementClickInterceptedException($message, $results); + case 'element not interactable': + throw new ElementNotInteractableException($message, $results); + case 'insecure certificate': + throw new InsecureCertificateException($message, $results); + case 'invalid argument': + throw new InvalidArgumentException($message, $results); + case 'invalid cookie domain': + throw new InvalidCookieDomainException($message, $results); + case 'invalid element state': + throw new InvalidElementStateException($message, $results); + case 'invalid selector': + throw new InvalidSelectorException($message, $results); + case 'invalid session id': + throw new InvalidSessionIdException($message, $results); + case 'javascript error': + throw new JavascriptErrorException($message, $results); + case 'move target out of bounds': + throw new MoveTargetOutOfBoundsException($message, $results); + case 'no such alert': + throw new NoSuchAlertException($message, $results); + case 'no such cookie': + throw new NoSuchCookieException($message, $results); + case 'no such element': + throw new NoSuchElementException($message, $results); + case 'no such frame': + throw new NoSuchFrameException($message, $results); + case 'no such window': + throw new NoSuchWindowException($message, $results); + case 'no such shadow root': + throw new NoSuchShadowRootException($message, $results); + case 'script timeout': + throw new ScriptTimeoutException($message, $results); + case 'session not created': + throw new SessionNotCreatedException($message, $results); + case 'stale element reference': + throw new StaleElementReferenceException($message, $results); + case 'detached shadow root': + throw new DetachedShadowRootException($message, $results); + case 'timeout': + throw new TimeoutException($message, $results); + case 'unable to set cookie': + throw new UnableToSetCookieException($message, $results); + case 'unable to capture screen': + throw new UnableToCaptureScreenException($message, $results); + case 'unexpected alert open': + throw new UnexpectedAlertOpenException($message, $results); + case 'unknown command': + throw new UnknownCommandException($message, $results); + case 'unknown error': + throw new UnknownErrorException($message, $results); + case 'unknown method': + throw new UnknownMethodException($message, $results); + case 'unsupported operation': + throw new UnsupportedOperationException($message, $results); + default: + throw new UnrecognizedExceptionException($message, $results); + } + } + + switch ($status_code) { + case 1: + throw new IndexOutOfBoundsException($message, $results); + case 2: + throw new NoCollectionException($message, $results); + case 3: + throw new NoStringException($message, $results); + case 4: + throw new NoStringLengthException($message, $results); + case 5: + throw new NoStringWrapperException($message, $results); + case 6: + throw new NoSuchDriverException($message, $results); + case 7: + throw new NoSuchElementException($message, $results); + case 8: + throw new NoSuchFrameException($message, $results); + case 9: + throw new UnknownCommandException($message, $results); + case 10: + throw new StaleElementReferenceException($message, $results); + case 11: + throw new ElementNotVisibleException($message, $results); + case 12: + throw new InvalidElementStateException($message, $results); + case 13: + throw new UnknownServerException($message, $results); + case 14: + throw new ExpectedException($message, $results); + case 15: + throw new ElementNotSelectableException($message, $results); + case 16: + throw new NoSuchDocumentException($message, $results); + case 17: + throw new UnexpectedJavascriptException($message, $results); + case 18: + throw new NoScriptResultException($message, $results); + case 19: + throw new XPathLookupException($message, $results); + case 20: + throw new NoSuchCollectionException($message, $results); + case 21: + throw new TimeoutException($message, $results); + case 22: + throw new NullPointerException($message, $results); + case 23: + throw new NoSuchWindowException($message, $results); + case 24: + throw new InvalidCookieDomainException($message, $results); + case 25: + throw new UnableToSetCookieException($message, $results); + case 26: + throw new UnexpectedAlertOpenException($message, $results); + case 27: + throw new NoAlertOpenException($message, $results); + case 28: + throw new ScriptTimeoutException($message, $results); + case 29: + throw new InvalidCoordinatesException($message, $results); + case 30: + throw new IMENotAvailableException($message, $results); + case 31: + throw new IMEEngineActivationFailedException($message, $results); + case 32: + throw new InvalidSelectorException($message, $results); + case 33: + throw new SessionNotCreatedException($message, $results); + case 34: + throw new MoveTargetOutOfBoundsException($message, $results); + default: + throw new UnrecognizedExceptionException($message, $results); + } + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Exception/XPathLookupException.php b/vendor/php-webdriver/webdriver/lib/Exception/XPathLookupException.php new file mode 100644 index 0000000..86513db --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Exception/XPathLookupException.php @@ -0,0 +1,10 @@ +setProfile($profile->encode()); + * $capabilities = DesiredCapabilities::firefox(); + * $capabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); + */ + public const PROFILE = 'firefox_profile'; + + /** + * Creates a new FirefoxDriver using default configuration. + * This includes starting a new geckodriver process each time this method is called. However this may be + * unnecessary overhead - instead, you can start the process once using FirefoxDriverService and pass + * this instance to startUsingDriverService() method. + * + * @return static + */ + public static function start(?DesiredCapabilities $capabilities = null) + { + $service = FirefoxDriverService::createDefaultService(); + + return static::startUsingDriverService($service, $capabilities); + } + + /** + * Creates a new FirefoxDriver using given FirefoxDriverService. + * This is usable when you for example don't want to start new geckodriver process for each individual test + * and want to reuse the already started geckodriver, which will lower the overhead associated with spinning up + * a new process. + * + * @return static + */ + public static function startUsingDriverService( + FirefoxDriverService $service, + ?DesiredCapabilities $capabilities = null + ) { + if ($capabilities === null) { + $capabilities = DesiredCapabilities::firefox(); + } + + $executor = new DriverCommandExecutor($service); + $newSessionCommand = WebDriverCommand::newSession( + [ + 'capabilities' => [ + 'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()], + ], + ] + ); + + $response = $executor->execute($newSessionCommand); + + $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($response->getValue()['capabilities']); + $sessionId = $response->getSessionID(); + + return new static($executor, $sessionId, $returnedCapabilities, true); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriverService.php b/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriverService.php new file mode 100644 index 0000000..83c6a28 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriverService.php @@ -0,0 +1,34 @@ +setPreference(FirefoxPreferences::READER_PARSE_ON_LOAD_ENABLED, false); + // disable JSON viewer and let JSON be rendered as raw data + $this->setPreference(FirefoxPreferences::DEVTOOLS_JSONVIEW, false); + } + + /** + * Directly set firefoxOptions. + * Use `addArguments` to add command line arguments and `setPreference` to set Firefox about:config entry. + * + * @param string $name + * @param mixed $value + * @return self + */ + public function setOption($name, $value) + { + if ($name === self::OPTION_PREFS) { + throw LogicException::forError('Use setPreference() method to set Firefox preferences'); + } + if ($name === self::OPTION_ARGS) { + throw LogicException::forError('Use addArguments() method to add Firefox arguments'); + } + if ($name === self::OPTION_PROFILE) { + throw LogicException::forError('Use setProfile() method to set Firefox profile'); + } + + $this->options[$name] = $value; + + return $this; + } + + /** + * Command line arguments to pass to the Firefox binary. + * These must include the leading dash (-) where required, e.g. ['-headless']. + * + * @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#args + * @param string[] $arguments + * @return self + */ + public function addArguments(array $arguments) + { + $this->arguments = array_merge($this->arguments, $arguments); + + return $this; + } + + /** + * Set Firefox preference (about:config entry). + * + * @see http://kb.mozillazine.org/About:config_entries + * @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#prefs + * @param string $name + * @param string|bool|int $value + * @return self + */ + public function setPreference($name, $value) + { + $this->preferences[$name] = $value; + + return $this; + } + + /** + * @see https://github.com/php-webdriver/php-webdriver/wiki/Firefox#firefox-profile + * @return self + */ + public function setProfile(FirefoxProfile $profile) + { + $this->profile = $profile; + + return $this; + } + + /** + * @return array + */ + public function toArray() + { + $array = $this->options; + if (!empty($this->arguments)) { + $array[self::OPTION_ARGS] = $this->arguments; + } + if (!empty($this->preferences)) { + $array[self::OPTION_PREFS] = $this->preferences; + } + if (!empty($this->profile)) { + $array[self::OPTION_PROFILE] = $this->profile->encode(); + } + + return $array; + } + + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return new \ArrayObject($this->toArray()); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxPreferences.php b/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxPreferences.php new file mode 100644 index 0000000..2a33fb0 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxPreferences.php @@ -0,0 +1,25 @@ +extensions[] = $extension; + + return $this; + } + + /** + * @param string $extension_datas The path to the folder containing the datas to add to the extension + * @return FirefoxProfile + */ + public function addExtensionDatas($extension_datas) + { + if (!is_dir($extension_datas)) { + return null; + } + + $this->extensions_datas[basename($extension_datas)] = $extension_datas; + + return $this; + } + + /** + * @param string $rdf_file The path to the rdf file + * @return FirefoxProfile + */ + public function setRdfFile($rdf_file) + { + if (!is_file($rdf_file)) { + return null; + } + + $this->rdf_file = $rdf_file; + + return $this; + } + + /** + * @param string $key + * @param string|bool|int $value + * @throws LogicException + * @return FirefoxProfile + */ + public function setPreference($key, $value) + { + if (is_string($value)) { + $value = sprintf('"%s"', $value); + } else { + if (is_int($value)) { + $value = sprintf('%d', $value); + } else { + if (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } else { + throw LogicException::forError( + 'The value of the preference should be either a string, int or bool.' + ); + } + } + } + $this->preferences[$key] = $value; + + return $this; + } + + /** + * @param mixed $key + * @return mixed + */ + public function getPreference($key) + { + if (array_key_exists($key, $this->preferences)) { + return $this->preferences[$key]; + } + + return null; + } + + /** + * @return string + */ + public function encode() + { + $temp_dir = $this->createTempDirectory('WebDriverFirefoxProfile'); + + if (isset($this->rdf_file)) { + copy($this->rdf_file, $temp_dir . DIRECTORY_SEPARATOR . 'mimeTypes.rdf'); + } + + foreach ($this->extensions as $extension) { + $this->installExtension($extension, $temp_dir); + } + + foreach ($this->extensions_datas as $dirname => $extension_datas) { + mkdir($temp_dir . DIRECTORY_SEPARATOR . $dirname); + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($extension_datas, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $item) { + $target_dir = $temp_dir . DIRECTORY_SEPARATOR . $dirname . DIRECTORY_SEPARATOR + . $iterator->getSubPathName(); + + if ($item->isDir()) { + mkdir($target_dir); + } else { + copy($item, $target_dir); + } + } + } + + $content = ''; + foreach ($this->preferences as $key => $value) { + $content .= sprintf("user_pref(\"%s\", %s);\n", $key, $value); + } + file_put_contents($temp_dir . '/user.js', $content); + + // Intentionally do not use `tempnam()`, as it creates empty file which zip extension may not handle. + $temp_zip = sys_get_temp_dir() . '/' . uniqid('WebDriverFirefoxProfileZip', false); + + $zip = new ZipArchive(); + $zip->open($temp_zip, ZipArchive::CREATE); + + $dir = new RecursiveDirectoryIterator($temp_dir); + $files = new RecursiveIteratorIterator($dir); + + $dir_prefix = preg_replace( + '#\\\\#', + '\\\\\\\\', + $temp_dir . DIRECTORY_SEPARATOR + ); + + foreach ($files as $name => $object) { + if (is_dir($name)) { + continue; + } + + $path = preg_replace("#^{$dir_prefix}#", '', $name); + $zip->addFile($name, $path); + } + $zip->close(); + + $profile = base64_encode(file_get_contents($temp_zip)); + + // clean up + $this->deleteDirectory($temp_dir); + unlink($temp_zip); + + return $profile; + } + + /** + * @param string $extension The path to the extension. + * @param string $profileDir The path to the profile directory. + * @throws IOException + */ + private function installExtension($extension, $profileDir) + { + $extensionCommonName = $this->parseExtensionName($extension); + + // install extension to profile directory + $extensionDir = $profileDir . '/extensions/'; + if (!is_dir($extensionDir) && !mkdir($extensionDir, 0777, true) && !is_dir($extensionDir)) { + throw IOException::forFileError( + 'Cannot install Firefox extension - cannot create directory', + $extensionDir + ); + } + + if (!copy($extension, $extensionDir . $extensionCommonName . '.xpi')) { + throw IOException::forFileError( + 'Cannot install Firefox extension - cannot copy file', + $extension + ); + } + } + + /** + * @param string $prefix Prefix of the temp directory. + * + * @throws IOException + * @return string The path to the temp directory created. + */ + private function createTempDirectory($prefix = '') + { + $temp_dir = tempnam(sys_get_temp_dir(), $prefix); + if (file_exists($temp_dir)) { + unlink($temp_dir); + mkdir($temp_dir); + if (!is_dir($temp_dir)) { + throw IOException::forFileError( + 'Cannot install Firefox extension - cannot create directory', + $temp_dir + ); + } + } + + return $temp_dir; + } + + /** + * @param string $directory The path to the directory. + */ + private function deleteDirectory($directory) + { + $dir = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS); + $paths = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($paths as $path) { + if ($path->isDir() && !$path->isLink()) { + rmdir($path->getPathname()); + } else { + unlink($path->getPathname()); + } + } + + rmdir($directory); + } + + /** + * @param string $xpi The path to the .xpi extension. + * @param string $target_dir The path to the unzip directory. + * + * @throws IOException + * @return FirefoxProfile + */ + private function extractTo($xpi, $target_dir) + { + $zip = new ZipArchive(); + if (file_exists($xpi)) { + if ($zip->open($xpi)) { + $zip->extractTo($target_dir); + $zip->close(); + } else { + throw IOException::forFileError('Failed to open the firefox extension.', $xpi); + } + } else { + throw IOException::forFileError('Firefox extension doesn\'t exist.', $xpi); + } + + return $this; + } + + private function parseExtensionName($extensionPath) + { + $temp_dir = $this->createTempDirectory(); + + $this->extractTo($extensionPath, $temp_dir); + + $mozillaRsaPath = $temp_dir . '/META-INF/mozilla.rsa'; + $mozillaRsaBinaryData = file_get_contents($mozillaRsaPath); + $mozillaRsaHex = bin2hex($mozillaRsaBinaryData); + + //We need to find the plugin id. This is the second occurrence of object identifier "2.5.4.3 commonName". + + //That is marker "2.5.4.3 commonName" in hex: + $objectIdentifierHexMarker = '0603550403'; + + $firstMarkerPosInHex = strpos($mozillaRsaHex, $objectIdentifierHexMarker); // phpcs:ignore + + $secondMarkerPosInHexString = + strpos($mozillaRsaHex, $objectIdentifierHexMarker, $firstMarkerPosInHex + 2); // phpcs:ignore + + if ($secondMarkerPosInHexString === false) { + throw RuntimeException::forError('Cannot install extension. Cannot fetch extension commonName'); + } + + // phpcs:ignore + $commonNameStringPositionInBinary = ($secondMarkerPosInHexString + strlen($objectIdentifierHexMarker)) / 2; + + $commonNameStringLength = ord($mozillaRsaBinaryData[$commonNameStringPositionInBinary + 1]); + // phpcs:ignore + $extensionCommonName = substr( + $mozillaRsaBinaryData, + $commonNameStringPositionInBinary + 2, + $commonNameStringLength + ); + + $this->deleteDirectory($temp_dir); + + return $extensionCommonName; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php new file mode 100644 index 0000000..91cad24 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php @@ -0,0 +1,16 @@ +mouse->mouseUp($this->getActionLocation()); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAction.php new file mode 100644 index 0000000..e21b883 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAction.php @@ -0,0 +1,13 @@ +mouse->click($this->getActionLocation()); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php new file mode 100644 index 0000000..5f5042c --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php @@ -0,0 +1,16 @@ +mouse->mouseDown($this->getActionLocation()); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php new file mode 100644 index 0000000..493978b --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php @@ -0,0 +1,16 @@ +mouse->contextClick($this->getActionLocation()); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverCoordinates.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverCoordinates.php new file mode 100644 index 0000000..3a92f0a --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverCoordinates.php @@ -0,0 +1,77 @@ +onScreen = $on_screen; + $this->inViewPort = $in_view_port; + $this->onPage = $on_page; + $this->auxiliary = $auxiliary; + } + + /** + * @throws UnsupportedOperationException + * @return WebDriverPoint + */ + public function onScreen() + { + throw new UnsupportedOperationException( + 'onScreen is planned but not yet supported by Selenium' + ); + } + + /** + * @return WebDriverPoint + */ + public function inViewPort() + { + return call_user_func($this->inViewPort); + } + + /** + * @return WebDriverPoint + */ + public function onPage() + { + return call_user_func($this->onPage); + } + + /** + * @return string The attached object id. + */ + public function getAuxiliary() + { + return $this->auxiliary; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php new file mode 100644 index 0000000..386c496 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php @@ -0,0 +1,13 @@ +mouse->doubleClick($this->getActionLocation()); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php new file mode 100644 index 0000000..415ebe7 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php @@ -0,0 +1,12 @@ +focusOnElement(); + $this->keyboard->pressKey($this->key); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php new file mode 100644 index 0000000..0cdb3a8 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php @@ -0,0 +1,12 @@ +focusOnElement(); + $this->keyboard->releaseKey($this->key); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php new file mode 100644 index 0000000..a5ba087 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php @@ -0,0 +1,43 @@ +keyboard = $keyboard; + $this->mouse = $mouse; + $this->locationProvider = $location_provider; + } + + protected function focusOnElement() + { + if ($this->locationProvider) { + $this->mouse->click($this->locationProvider->getCoordinates()); + } + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php new file mode 100644 index 0000000..ecb1127 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php @@ -0,0 +1,44 @@ +mouse = $mouse; + $this->locationProvider = $location_provider; + } + + /** + * @return null|WebDriverCoordinates + */ + protected function getActionLocation() + { + if ($this->locationProvider !== null) { + return $this->locationProvider->getCoordinates(); + } + + return null; + } + + protected function moveToLocation() + { + $this->mouse->mouseMove($this->locationProvider); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php new file mode 100644 index 0000000..1969f01 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php @@ -0,0 +1,13 @@ +mouse->mouseMove($this->getActionLocation()); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php new file mode 100644 index 0000000..c865da4 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php @@ -0,0 +1,43 @@ +xOffset = $x_offset; + $this->yOffset = $y_offset; + } + + public function perform() + { + $this->mouse->mouseMove( + $this->getActionLocation(), + $this->xOffset, + $this->yOffset + ); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php new file mode 100644 index 0000000..2ed3cfd --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php @@ -0,0 +1,35 @@ +keys = $keys; + } + + public function perform() + { + $this->focusOnElement(); + $this->keyboard->sendKeys($this->keys); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.php new file mode 100644 index 0000000..6efe938 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.php @@ -0,0 +1,54 @@ +key = $key; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php new file mode 100644 index 0000000..25a1761 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php @@ -0,0 +1,13 @@ +touchScreen->doubleTap($this->locationProvider); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDownAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDownAction.php new file mode 100644 index 0000000..225726f --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDownAction.php @@ -0,0 +1,33 @@ +x = $x; + $this->y = $y; + parent::__construct($touch_screen); + } + + public function perform() + { + $this->touchScreen->down($this->x, $this->y); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php new file mode 100644 index 0000000..acdb7fc --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php @@ -0,0 +1,33 @@ +x = $x; + $this->y = $y; + parent::__construct($touch_screen); + } + + public function perform() + { + $this->touchScreen->flick($this->x, $this->y); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php new file mode 100644 index 0000000..28d3597 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php @@ -0,0 +1,50 @@ +x = $x; + $this->y = $y; + $this->speed = $speed; + parent::__construct($touch_screen, $element); + } + + public function perform() + { + $this->touchScreen->flickFromElement( + $this->locationProvider, + $this->x, + $this->y, + $this->speed + ); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php new file mode 100644 index 0000000..7c1a165 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php @@ -0,0 +1,13 @@ +touchScreen->longPress($this->locationProvider); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php new file mode 100644 index 0000000..d0a5f85 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php @@ -0,0 +1,27 @@ +x = $x; + $this->y = $y; + parent::__construct($touch_screen); + } + + public function perform() + { + $this->touchScreen->move($this->x, $this->y); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php new file mode 100644 index 0000000..952d57e --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php @@ -0,0 +1,27 @@ +x = $x; + $this->y = $y; + parent::__construct($touch_screen); + } + + public function perform() + { + $this->touchScreen->scroll($this->x, $this->y); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.php new file mode 100644 index 0000000..217564d --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.php @@ -0,0 +1,36 @@ +x = $x; + $this->y = $y; + parent::__construct($touch_screen, $element); + } + + public function perform() + { + $this->touchScreen->scrollFromElement( + $this->locationProvider, + $this->x, + $this->y + ); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTapAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTapAction.php new file mode 100644 index 0000000..63527e8 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTapAction.php @@ -0,0 +1,13 @@ +touchScreen->tap($this->locationProvider); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchAction.php new file mode 100644 index 0000000..10100ea --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchAction.php @@ -0,0 +1,38 @@ +touchScreen = $touch_screen; + $this->locationProvider = $location_provider; + } + + /** + * @return null|WebDriverCoordinates + */ + protected function getActionLocation() + { + return $this->locationProvider !== null + ? $this->locationProvider->getCoordinates() : null; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php new file mode 100644 index 0000000..ff9f9c4 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php @@ -0,0 +1,109 @@ +driver = $driver; + $this->keyboard = $driver->getKeyboard(); + $this->mouse = $driver->getMouse(); + $this->action = new WebDriverCompositeAction(); + } + + /** + * A convenience method for performing the actions without calling build(). + */ + public function perform() + { + $this->action->perform(); + } + + /** + * Mouse click. + * If $element is provided, move to the middle of the element first. + * + * @return WebDriverActions + */ + public function click(?WebDriverElement $element = null) + { + $this->action->addAction( + new WebDriverClickAction($this->mouse, $element) + ); + + return $this; + } + + /** + * Mouse click and hold. + * If $element is provided, move to the middle of the element first. + * + * @return WebDriverActions + */ + public function clickAndHold(?WebDriverElement $element = null) + { + $this->action->addAction( + new WebDriverClickAndHoldAction($this->mouse, $element) + ); + + return $this; + } + + /** + * Context-click (right click). + * If $element is provided, move to the middle of the element first. + * + * @return WebDriverActions + */ + public function contextClick(?WebDriverElement $element = null) + { + $this->action->addAction( + new WebDriverContextClickAction($this->mouse, $element) + ); + + return $this; + } + + /** + * Double click. + * If $element is provided, move to the middle of the element first. + * + * @return WebDriverActions + */ + public function doubleClick(?WebDriverElement $element = null) + { + $this->action->addAction( + new WebDriverDoubleClickAction($this->mouse, $element) + ); + + return $this; + } + + /** + * Drag and drop from $source to $target. + * + * @return WebDriverActions + */ + public function dragAndDrop(WebDriverElement $source, WebDriverElement $target) + { + $this->action->addAction( + new WebDriverClickAndHoldAction($this->mouse, $source) + ); + $this->action->addAction( + new WebDriverMouseMoveAction($this->mouse, $target) + ); + $this->action->addAction( + new WebDriverButtonReleaseAction($this->mouse, $target) + ); + + return $this; + } + + /** + * Drag $source and drop by offset ($x_offset, $y_offset). + * + * @param int $x_offset + * @param int $y_offset + * @return WebDriverActions + */ + public function dragAndDropBy(WebDriverElement $source, $x_offset, $y_offset) + { + $this->action->addAction( + new WebDriverClickAndHoldAction($this->mouse, $source) + ); + $this->action->addAction( + new WebDriverMoveToOffsetAction($this->mouse, null, $x_offset, $y_offset) + ); + $this->action->addAction( + new WebDriverButtonReleaseAction($this->mouse, null) + ); + + return $this; + } + + /** + * Mouse move by offset. + * + * @param int $x_offset + * @param int $y_offset + * @return WebDriverActions + */ + public function moveByOffset($x_offset, $y_offset) + { + $this->action->addAction( + new WebDriverMoveToOffsetAction($this->mouse, null, $x_offset, $y_offset) + ); + + return $this; + } + + /** + * Move to the middle of the given WebDriverElement. + * Extra shift, calculated from the top-left corner of the element, can be set by passing $x_offset and $y_offset + * parameters. + * + * @param int $x_offset + * @param int $y_offset + * @return WebDriverActions + */ + public function moveToElement(WebDriverElement $element, $x_offset = null, $y_offset = null) + { + $this->action->addAction(new WebDriverMoveToOffsetAction( + $this->mouse, + $element, + $x_offset, + $y_offset + )); + + return $this; + } + + /** + * Release the mouse button. + * If $element is provided, move to the middle of the element first. + * + * @return WebDriverActions + */ + public function release(?WebDriverElement $element = null) + { + $this->action->addAction( + new WebDriverButtonReleaseAction($this->mouse, $element) + ); + + return $this; + } + + /** + * Press a key on keyboard. + * If $element is provided, focus on that element first. + * + * @see WebDriverKeys for special keys like CONTROL, ALT, etc. + * @param string $key + * @return WebDriverActions + */ + public function keyDown(?WebDriverElement $element = null, $key = null) + { + $this->action->addAction( + new WebDriverKeyDownAction($this->keyboard, $this->mouse, $element, $key) + ); + + return $this; + } + + /** + * Release a key on keyboard. + * If $element is provided, focus on that element first. + * + * @see WebDriverKeys for special keys like CONTROL, ALT, etc. + * @param string $key + * @return WebDriverActions + */ + public function keyUp(?WebDriverElement $element = null, $key = null) + { + $this->action->addAction( + new WebDriverKeyUpAction($this->keyboard, $this->mouse, $element, $key) + ); + + return $this; + } + + /** + * Send keys by keyboard. + * If $element is provided, focus on that element first (using single mouse click). + * + * @see WebDriverKeys for special keys like CONTROL, ALT, etc. + * @param string $keys + * @return WebDriverActions + */ + public function sendKeys(?WebDriverElement $element = null, $keys = null) + { + $this->action->addAction( + new WebDriverSendKeysAction( + $this->keyboard, + $this->mouse, + $element, + $keys + ) + ); + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverCompositeAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverCompositeAction.php new file mode 100644 index 0000000..955d619 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverCompositeAction.php @@ -0,0 +1,48 @@ +actions[] = $action; + + return $this; + } + + /** + * Get the number of actions in the sequence. + * + * @return int The number of actions. + */ + public function getNumberOfActions() + { + return count($this->actions); + } + + /** + * Perform the sequence of actions. + */ + public function perform() + { + foreach ($this->actions as $action) { + $action->perform(); + } + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverTouchActions.php b/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverTouchActions.php new file mode 100644 index 0000000..fd32984 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverTouchActions.php @@ -0,0 +1,175 @@ +touchScreen = $driver->getTouch(); + } + + /** + * @return WebDriverTouchActions + */ + public function tap(WebDriverElement $element) + { + $this->action->addAction( + new WebDriverTapAction($this->touchScreen, $element) + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * @return WebDriverTouchActions + */ + public function down($x, $y) + { + $this->action->addAction( + new WebDriverDownAction($this->touchScreen, $x, $y) + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * @return WebDriverTouchActions + */ + public function up($x, $y) + { + $this->action->addAction( + new WebDriverUpAction($this->touchScreen, $x, $y) + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * @return WebDriverTouchActions + */ + public function move($x, $y) + { + $this->action->addAction( + new WebDriverMoveAction($this->touchScreen, $x, $y) + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * @return WebDriverTouchActions + */ + public function scroll($x, $y) + { + $this->action->addAction( + new WebDriverScrollAction($this->touchScreen, $x, $y) + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * @return WebDriverTouchActions + */ + public function scrollFromElement(WebDriverElement $element, $x, $y) + { + $this->action->addAction( + new WebDriverScrollFromElementAction($this->touchScreen, $element, $x, $y) + ); + + return $this; + } + + /** + * @return WebDriverTouchActions + */ + public function doubleTap(WebDriverElement $element) + { + $this->action->addAction( + new WebDriverDoubleTapAction($this->touchScreen, $element) + ); + + return $this; + } + + /** + * @return WebDriverTouchActions + */ + public function longPress(WebDriverElement $element) + { + $this->action->addAction( + new WebDriverLongPressAction($this->touchScreen, $element) + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * @return WebDriverTouchActions + */ + public function flick($x, $y) + { + $this->action->addAction( + new WebDriverFlickAction($this->touchScreen, $x, $y) + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * @param int $speed + * @return WebDriverTouchActions + */ + public function flickFromElement(WebDriverElement $element, $x, $y, $speed) + { + $this->action->addAction( + new WebDriverFlickFromElementAction( + $this->touchScreen, + $element, + $x, + $y, + $speed + ) + ); + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Internal/WebDriverLocatable.php b/vendor/php-webdriver/webdriver/lib/Internal/WebDriverLocatable.php new file mode 100644 index 0000000..225a10d --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Internal/WebDriverLocatable.php @@ -0,0 +1,16 @@ + microtime(true)) { + if ($this->getHTTPResponseCode($url) === 200) { + return $this; + } + usleep(self::POLL_INTERVAL_MS); + } + + throw new TimeoutException(sprintf( + 'Timed out waiting for %s to become available after %d ms.', + $url, + $timeout_in_ms + )); + } + + public function waitUntilUnavailable($timeout_in_ms, $url) + { + $end = microtime(true) + $timeout_in_ms / 1000; + + while ($end > microtime(true)) { + if ($this->getHTTPResponseCode($url) !== 200) { + return $this; + } + usleep(self::POLL_INTERVAL_MS); + } + + throw new TimeoutException(sprintf( + 'Timed out waiting for %s to become unavailable after %d ms.', + $url, + $timeout_in_ms + )); + } + + private function getHTTPResponseCode($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + // The PHP doc indicates that CURLOPT_CONNECTTIMEOUT_MS constant is added in cURL 7.16.2 + // available since PHP 5.2.3. + if (!defined('CURLOPT_CONNECTTIMEOUT_MS')) { + define('CURLOPT_CONNECTTIMEOUT_MS', 156); // default value for CURLOPT_CONNECTTIMEOUT_MS + } + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, self::CONNECT_TIMEOUT_MS); + + $code = null; + + try { + curl_exec($ch); + $info = curl_getinfo($ch); + $code = $info['http_code']; + } catch (Exception $e) { + } + curl_close($ch); + + return $code; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/CustomWebDriverCommand.php b/vendor/php-webdriver/webdriver/lib/Remote/CustomWebDriverCommand.php new file mode 100644 index 0000000..cef2c95 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/CustomWebDriverCommand.php @@ -0,0 +1,82 @@ +setCustomRequestParameters($url, $method); + + parent::__construct($session_id, DriverCommand::CUSTOM_COMMAND, $parameters); + } + + /** + * @throws WebDriverException + * @return string + */ + public function getCustomUrl() + { + if ($this->customUrl === null) { + throw LogicException::forError('URL of custom command is not set'); + } + + return $this->customUrl; + } + + /** + * @throws WebDriverException + * @return string + */ + public function getCustomMethod() + { + if ($this->customMethod === null) { + throw LogicException::forError('Method of custom command is not set'); + } + + return $this->customMethod; + } + + /** + * @param string $custom_url + * @param string $custom_method + * @throws WebDriverException + */ + protected function setCustomRequestParameters($custom_url, $custom_method) + { + $allowedMethods = [static::METHOD_GET, static::METHOD_POST]; + if (!in_array($custom_method, $allowedMethods, true)) { + throw LogicException::forError( + sprintf( + 'Invalid custom method "%s", must be one of [%s]', + $custom_method, + implode(', ', $allowedMethods) + ) + ); + } + $this->customMethod = $custom_method; + + if (mb_strpos($custom_url, '/') !== 0) { + throw LogicException::forError( + sprintf('URL of custom command has to start with / but is "%s"', $custom_url) + ); + } + $this->customUrl = $custom_url; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/DesiredCapabilities.php b/vendor/php-webdriver/webdriver/lib/Remote/DesiredCapabilities.php new file mode 100644 index 0000000..88aa6b1 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/DesiredCapabilities.php @@ -0,0 +1,428 @@ + 'platformName', + WebDriverCapabilityType::VERSION => 'browserVersion', + WebDriverCapabilityType::ACCEPT_SSL_CERTS => 'acceptInsecureCerts', + ]; + + public function __construct(array $capabilities = []) + { + $this->capabilities = $capabilities; + } + + public static function createFromW3cCapabilities(array $capabilities = []) + { + $w3cToOss = array_flip(self::$ossToW3c); + + foreach ($w3cToOss as $w3cCapability => $ossCapability) { + // Copy W3C capabilities to OSS ones + if (array_key_exists($w3cCapability, $capabilities)) { + $capabilities[$ossCapability] = $capabilities[$w3cCapability]; + } + } + + return new self($capabilities); + } + + /** + * @return string The name of the browser. + */ + public function getBrowserName() + { + return $this->get(WebDriverCapabilityType::BROWSER_NAME, ''); + } + + /** + * @param string $browser_name + * @return DesiredCapabilities + */ + public function setBrowserName($browser_name) + { + $this->set(WebDriverCapabilityType::BROWSER_NAME, $browser_name); + + return $this; + } + + /** + * @return string The version of the browser. + */ + public function getVersion() + { + return $this->get(WebDriverCapabilityType::VERSION, ''); + } + + /** + * @param string $version + * @return DesiredCapabilities + */ + public function setVersion($version) + { + $this->set(WebDriverCapabilityType::VERSION, $version); + + return $this; + } + + /** + * @param string $name + * @return mixed The value of a capability. + */ + public function getCapability($name) + { + return $this->get($name); + } + + /** + * @param string $name + * @param mixed $value + * @return DesiredCapabilities + */ + public function setCapability($name, $value) + { + // When setting 'moz:firefoxOptions' from an array and not from instance of FirefoxOptions, we must merge + // it with default FirefoxOptions to keep previous behavior (where the default preferences were added + // using FirefoxProfile, thus not overwritten by adding 'moz:firefoxOptions') + // TODO: remove in next major version, once FirefoxOptions are only accepted as object instance and not as array + if ($name === FirefoxOptions::CAPABILITY && is_array($value)) { + $defaultOptions = (new FirefoxOptions())->toArray(); + $value = array_merge($defaultOptions, $value); + } + + $this->set($name, $value); + + return $this; + } + + /** + * @return string The name of the platform. + */ + public function getPlatform() + { + return $this->get(WebDriverCapabilityType::PLATFORM, ''); + } + + /** + * @param string $platform + * @return DesiredCapabilities + */ + public function setPlatform($platform) + { + $this->set(WebDriverCapabilityType::PLATFORM, $platform); + + return $this; + } + + /** + * @param string $capability_name + * @return bool Whether the value is not null and not false. + */ + public function is($capability_name) + { + return (bool) $this->get($capability_name); + } + + /** + * @todo Remove in next major release (BC) + * @deprecated All browsers are always JS enabled except HtmlUnit and it's not meaningful to disable JS execution. + * @return bool Whether javascript is enabled. + */ + public function isJavascriptEnabled() + { + return $this->get(WebDriverCapabilityType::JAVASCRIPT_ENABLED, false); + } + + /** + * This is a htmlUnit-only option. + * + * @param bool $enabled + * @throws UnsupportedOperationException + * @return DesiredCapabilities + * @see https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#read-write-capabilities + */ + public function setJavascriptEnabled($enabled) + { + $browser = $this->getBrowserName(); + if ($browser && $browser !== WebDriverBrowserType::HTMLUNIT) { + throw new UnsupportedOperationException( + 'isJavascriptEnabled() is a htmlunit-only option. ' . + 'See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#read-write-capabilities.' + ); + } + + $this->set(WebDriverCapabilityType::JAVASCRIPT_ENABLED, $enabled); + + return $this; + } + + /** + * @todo Remove side-effects - not change eg. ChromeOptions::CAPABILITY from instance of ChromeOptions to an array + * @return array + */ + public function toArray() + { + if (isset($this->capabilities[ChromeOptions::CAPABILITY]) && + $this->capabilities[ChromeOptions::CAPABILITY] instanceof ChromeOptions + ) { + $this->capabilities[ChromeOptions::CAPABILITY] = + $this->capabilities[ChromeOptions::CAPABILITY]->toArray(); + } + + if (isset($this->capabilities[FirefoxOptions::CAPABILITY]) && + $this->capabilities[FirefoxOptions::CAPABILITY] instanceof FirefoxOptions + ) { + $this->capabilities[FirefoxOptions::CAPABILITY] = + $this->capabilities[FirefoxOptions::CAPABILITY]->toArray(); + } + + if (isset($this->capabilities[FirefoxDriver::PROFILE]) && + $this->capabilities[FirefoxDriver::PROFILE] instanceof FirefoxProfile + ) { + $this->capabilities[FirefoxDriver::PROFILE] = + $this->capabilities[FirefoxDriver::PROFILE]->encode(); + } + + return $this->capabilities; + } + + /** + * @return array + */ + public function toW3cCompatibleArray() + { + $allowedW3cCapabilities = [ + 'browserName', + 'browserVersion', + 'platformName', + 'acceptInsecureCerts', + 'pageLoadStrategy', + 'proxy', + 'setWindowRect', + 'timeouts', + 'strictFileInteractability', + 'unhandledPromptBehavior', + ]; + + $ossCapabilities = $this->toArray(); + $w3cCapabilities = []; + + foreach ($ossCapabilities as $capabilityKey => $capabilityValue) { + // Copy already W3C compatible capabilities + if (in_array($capabilityKey, $allowedW3cCapabilities, true)) { + $w3cCapabilities[$capabilityKey] = $capabilityValue; + } + + // Convert capabilities with changed name + if (array_key_exists($capabilityKey, self::$ossToW3c)) { + if ($capabilityKey === WebDriverCapabilityType::PLATFORM) { + $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue); + + // Remove platformName if it is set to "any" + if ($w3cCapabilities[self::$ossToW3c[$capabilityKey]] === 'any') { + unset($w3cCapabilities[self::$ossToW3c[$capabilityKey]]); + } + } else { + $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = $capabilityValue; + } + } + + // Copy vendor extensions + if (mb_strpos($capabilityKey, ':') !== false) { + $w3cCapabilities[$capabilityKey] = $capabilityValue; + } + } + + // Convert ChromeOptions + if (array_key_exists(ChromeOptions::CAPABILITY, $ossCapabilities)) { + $w3cCapabilities[ChromeOptions::CAPABILITY] = $ossCapabilities[ChromeOptions::CAPABILITY]; + } + + // Convert Firefox profile + if (array_key_exists(FirefoxDriver::PROFILE, $ossCapabilities)) { + // Convert profile only if not already set in moz:firefoxOptions + if (!array_key_exists(FirefoxOptions::CAPABILITY, $ossCapabilities) + || !array_key_exists('profile', $ossCapabilities[FirefoxOptions::CAPABILITY])) { + $w3cCapabilities[FirefoxOptions::CAPABILITY]['profile'] = $ossCapabilities[FirefoxDriver::PROFILE]; + } + } + + return $w3cCapabilities; + } + + /** + * @return static + */ + public static function android() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::ANDROID, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANDROID, + ]); + } + + /** + * @return static + */ + public static function chrome() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::CHROME, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, + ]); + } + + /** + * @return static + */ + public static function firefox() + { + $caps = new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::FIREFOX, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, + ]); + + $caps->setCapability(FirefoxOptions::CAPABILITY, new FirefoxOptions()); // to add default options + + return $caps; + } + + /** + * @return static + */ + public static function htmlUnit() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::HTMLUNIT, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, + ]); + } + + /** + * @return static + */ + public static function htmlUnitWithJS() + { + $caps = new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::HTMLUNIT, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, + ]); + + return $caps->setJavascriptEnabled(true); + } + + /** + * @return static + */ + public static function internetExplorer() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IE, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::WINDOWS, + ]); + } + + /** + * @return static + */ + public static function microsoftEdge() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::MICROSOFT_EDGE, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::WINDOWS, + ]); + } + + /** + * @return static + */ + public static function iphone() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IPHONE, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::MAC, + ]); + } + + /** + * @return static + */ + public static function ipad() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IPAD, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::MAC, + ]); + } + + /** + * @return static + */ + public static function opera() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::OPERA, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, + ]); + } + + /** + * @return static + */ + public static function safari() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::SAFARI, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, + ]); + } + + /** + * @deprecated PhantomJS is no longer developed and its support will be removed in next major version. + * Use headless Chrome or Firefox instead. + * @return static + */ + public static function phantomjs() + { + return new static([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::PHANTOMJS, + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, + ]); + } + + /** + * @param string $key + * @param mixed $value + * @return DesiredCapabilities + */ + private function set($key, $value) + { + $this->capabilities[$key] = $value; + + return $this; + } + + /** + * @param string $key + * @param mixed $default + * @return mixed + */ + private function get($key, $default = null) + { + return $this->capabilities[$key] ?? $default; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/DriverCommand.php b/vendor/php-webdriver/webdriver/lib/Remote/DriverCommand.php new file mode 100644 index 0000000..a3a230b --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/DriverCommand.php @@ -0,0 +1,153 @@ + ['method' => 'POST', 'url' => '/session/:sessionId/accept_alert'], + DriverCommand::ADD_COOKIE => ['method' => 'POST', 'url' => '/session/:sessionId/cookie'], + DriverCommand::CLEAR_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/clear'], + DriverCommand::CLICK_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/click'], + DriverCommand::CLOSE => ['method' => 'DELETE', 'url' => '/session/:sessionId/window'], + DriverCommand::DELETE_ALL_COOKIES => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie'], + DriverCommand::DELETE_COOKIE => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie/:name'], + DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/dismiss_alert'], + DriverCommand::ELEMENT_EQUALS => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/equals/:other'], + DriverCommand::FIND_CHILD_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/element'], + DriverCommand::FIND_CHILD_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/elements'], + DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute'], + DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute_async'], + DriverCommand::FIND_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element'], + DriverCommand::FIND_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/elements'], + DriverCommand::SWITCH_TO_FRAME => ['method' => 'POST', 'url' => '/session/:sessionId/frame'], + DriverCommand::SWITCH_TO_PARENT_FRAME => ['method' => 'POST', 'url' => '/session/:sessionId/frame/parent'], + DriverCommand::SWITCH_TO_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window'], + DriverCommand::GET => ['method' => 'POST', 'url' => '/session/:sessionId/url'], + DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/active'], + DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert_text'], + DriverCommand::GET_ALL_COOKIES => ['method' => 'GET', 'url' => '/session/:sessionId/cookie'], + DriverCommand::GET_NAMED_COOKIE => ['method' => 'GET', 'url' => '/session/:sessionId/cookie/:name'], + DriverCommand::GET_ALL_SESSIONS => ['method' => 'GET', 'url' => '/sessions'], + DriverCommand::GET_AVAILABLE_LOG_TYPES => ['method' => 'GET', 'url' => '/session/:sessionId/log/types'], + DriverCommand::GET_CURRENT_URL => ['method' => 'GET', 'url' => '/session/:sessionId/url'], + DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window_handle'], + DriverCommand::GET_ELEMENT_ATTRIBUTE => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/attribute/:name', + ], + DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/css/:propertyName', + ], + DriverCommand::GET_ELEMENT_LOCATION => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/location', + ], + DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/location_in_view', + ], + DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/size'], + DriverCommand::GET_ELEMENT_TAG_NAME => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/name'], + DriverCommand::GET_ELEMENT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/text'], + DriverCommand::GET_LOG => ['method' => 'POST', 'url' => '/session/:sessionId/log'], + DriverCommand::GET_PAGE_SOURCE => ['method' => 'GET', 'url' => '/session/:sessionId/source'], + DriverCommand::GET_SCREEN_ORIENTATION => ['method' => 'GET', 'url' => '/session/:sessionId/orientation'], + DriverCommand::GET_CAPABILITIES => ['method' => 'GET', 'url' => '/session/:sessionId'], + DriverCommand::GET_TITLE => ['method' => 'GET', 'url' => '/session/:sessionId/title'], + DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window_handles'], + DriverCommand::GET_WINDOW_POSITION => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/window/:windowHandle/position', + ], + DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/:windowHandle/size'], + DriverCommand::GO_BACK => ['method' => 'POST', 'url' => '/session/:sessionId/back'], + DriverCommand::GO_FORWARD => ['method' => 'POST', 'url' => '/session/:sessionId/forward'], + DriverCommand::IS_ELEMENT_DISPLAYED => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/displayed', + ], + DriverCommand::IS_ELEMENT_ENABLED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/enabled'], + DriverCommand::IS_ELEMENT_SELECTED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/selected'], + DriverCommand::MAXIMIZE_WINDOW => [ + 'method' => 'POST', + 'url' => '/session/:sessionId/window/:windowHandle/maximize', + ], + DriverCommand::MOUSE_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/buttondown'], + DriverCommand::MOUSE_UP => ['method' => 'POST', 'url' => '/session/:sessionId/buttonup'], + DriverCommand::CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/click'], + DriverCommand::DOUBLE_CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/doubleclick'], + DriverCommand::MOVE_TO => ['method' => 'POST', 'url' => '/session/:sessionId/moveto'], + DriverCommand::NEW_SESSION => ['method' => 'POST', 'url' => '/session'], + DriverCommand::QUIT => ['method' => 'DELETE', 'url' => '/session/:sessionId'], + DriverCommand::REFRESH => ['method' => 'POST', 'url' => '/session/:sessionId/refresh'], + DriverCommand::UPLOAD_FILE => ['method' => 'POST', 'url' => '/session/:sessionId/file'], // undocumented + DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/keys'], + DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert_text'], + DriverCommand::SEND_KEYS_TO_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/value'], + DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/implicit_wait'], + DriverCommand::SET_SCREEN_ORIENTATION => ['method' => 'POST', 'url' => '/session/:sessionId/orientation'], + DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/async_script'], + DriverCommand::SET_WINDOW_POSITION => [ + 'method' => 'POST', + 'url' => '/session/:sessionId/window/:windowHandle/position', + ], + DriverCommand::SET_WINDOW_SIZE => [ + 'method' => 'POST', + 'url' => '/session/:sessionId/window/:windowHandle/size', + ], + DriverCommand::STATUS => ['method' => 'GET', 'url' => '/status'], + DriverCommand::SUBMIT_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/submit'], + DriverCommand::SCREENSHOT => ['method' => 'GET', 'url' => '/session/:sessionId/screenshot'], + DriverCommand::TAKE_ELEMENT_SCREENSHOT => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/screenshot', + ], + DriverCommand::TOUCH_SINGLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/click'], + DriverCommand::TOUCH_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/touch/down'], + DriverCommand::TOUCH_DOUBLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/doubleclick'], + DriverCommand::TOUCH_FLICK => ['method' => 'POST', 'url' => '/session/:sessionId/touch/flick'], + DriverCommand::TOUCH_LONG_PRESS => ['method' => 'POST', 'url' => '/session/:sessionId/touch/longclick'], + DriverCommand::TOUCH_MOVE => ['method' => 'POST', 'url' => '/session/:sessionId/touch/move'], + DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'], + DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'], + DriverCommand::CUSTOM_COMMAND => [], + ]; + /** + * @var array Will be merged with $commands + */ + protected static $w3cCompliantCommands = [ + DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/accept'], + DriverCommand::ACTIONS => ['method' => 'POST', 'url' => '/session/:sessionId/actions'], + DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'], + DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'], + DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'], + DriverCommand::FIND_ELEMENT_FROM_SHADOW_ROOT => [ + 'method' => 'POST', + 'url' => '/session/:sessionId/shadow/:id/element', + ], + DriverCommand::FIND_ELEMENTS_FROM_SHADOW_ROOT => [ + 'method' => 'POST', + 'url' => '/session/:sessionId/shadow/:id/elements', + ], + DriverCommand::FULLSCREEN_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/fullscreen'], + DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'GET', 'url' => '/session/:sessionId/element/active'], + DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'], + DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'], + DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], + DriverCommand::GET_ELEMENT_PROPERTY => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/property/:name', + ], + DriverCommand::GET_ELEMENT_SHADOW_ROOT => [ + 'method' => 'GET', + 'url' => '/session/:sessionId/element/:id/shadow', + ], + DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], + DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window/handles'], + DriverCommand::GET_WINDOW_POSITION => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'], + DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'], + DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::MAXIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/maximize'], + DriverCommand::MINIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/minimize'], + DriverCommand::NEW_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/new'], + DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert/text'], + DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::SET_WINDOW_SIZE => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'], + DriverCommand::SET_WINDOW_POSITION => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'], + ]; + /** + * @var string + */ + protected $url; + /** + * @var resource + */ + protected $curl; + /** + * @var bool + */ + protected $isW3cCompliant = true; + + /** + * @param string $url + * @param string|null $http_proxy + * @param int|null $http_proxy_port + */ + public function __construct($url, $http_proxy = null, $http_proxy_port = null) + { + self::$w3cCompliantCommands = array_merge(self::$commands, self::$w3cCompliantCommands); + + $this->url = $url; + $this->curl = curl_init(); + + if (!empty($http_proxy)) { + curl_setopt($this->curl, CURLOPT_PROXY, $http_proxy); + if ($http_proxy_port !== null) { + curl_setopt($this->curl, CURLOPT_PROXYPORT, $http_proxy_port); + } + } + + // Get credentials from $url (if any) + $matches = null; + if (preg_match("/^(https?:\/\/)(.*):(.*)@(.*?)/U", $url, $matches)) { + $this->url = $matches[1] . $matches[4]; + $auth_creds = $matches[2] . ':' . $matches[3]; + curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_setopt($this->curl, CURLOPT_USERPWD, $auth_creds); + } + + curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS); + + $this->setConnectionTimeout(30 * 1000); // 30 seconds + $this->setRequestTimeout(180 * 1000); // 3 minutes + } + + public function disableW3cCompliance() + { + $this->isW3cCompliant = false; + } + + /** + * Set timeout for the connect phase + * + * @param int $timeout_in_ms Timeout in milliseconds + * @return HttpCommandExecutor + */ + public function setConnectionTimeout($timeout_in_ms) + { + // There is a PHP bug in some versions which didn't define the constant. + curl_setopt( + $this->curl, + /* CURLOPT_CONNECTTIMEOUT_MS */ + 156, + $timeout_in_ms + ); + + return $this; + } + + /** + * Set the maximum time of a request + * + * @param int $timeout_in_ms Timeout in milliseconds + * @return HttpCommandExecutor + */ + public function setRequestTimeout($timeout_in_ms) + { + // There is a PHP bug in some versions (at least for PHP 5.3.3) which + // didn't define the constant. + curl_setopt( + $this->curl, + /* CURLOPT_TIMEOUT_MS */ + 155, + $timeout_in_ms + ); + + return $this; + } + + /** + * @return WebDriverResponse + */ + public function execute(WebDriverCommand $command) + { + $http_options = $this->getCommandHttpOptions($command); + $http_method = $http_options['method']; + $url = $http_options['url']; + + $sessionID = $command->getSessionID(); + $url = str_replace(':sessionId', $sessionID ?? '', $url); + $params = $command->getParameters(); + foreach ($params as $name => $value) { + if ($name[0] === ':') { + $url = str_replace($name, $value, $url); + unset($params[$name]); + } + } + + if (is_array($params) && !empty($params) && $http_method !== 'POST') { + throw LogicException::forInvalidHttpMethod($url, $http_method, $params); + } + + curl_setopt($this->curl, CURLOPT_URL, $this->url . $url); + + // https://github.com/facebook/php-webdriver/issues/173 + if ($command->getName() === DriverCommand::NEW_SESSION) { + curl_setopt($this->curl, CURLOPT_POST, 1); + } else { + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $http_method); + } + + if (in_array($http_method, ['POST', 'PUT'], true)) { + // Disable sending 'Expect: 100-Continue' header, as it is causing issues with eg. squid proxy + // https://tools.ietf.org/html/rfc7231#section-5.1.1 + curl_setopt($this->curl, CURLOPT_HTTPHEADER, array_merge(static::DEFAULT_HTTP_HEADERS, ['Expect:'])); + } else { + curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS); + } + + $encoded_params = null; + + if ($http_method === 'POST') { + if (is_array($params) && !empty($params)) { + $encoded_params = json_encode($params); + } elseif ($this->isW3cCompliant) { + // POST body must be valid JSON in W3C, even if empty: https://www.w3.org/TR/webdriver/#processing-model + $encoded_params = '{}'; + } + } + + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params); + + $raw_results = trim(curl_exec($this->curl)); + + if ($error = curl_error($this->curl)) { + throw WebDriverCurlException::forCurlError($http_method, $url, $error, is_array($params) ? $params : null); + } + + $results = json_decode($raw_results, true); + + if ($results === null && json_last_error() !== JSON_ERROR_NONE) { + throw UnexpectedResponseException::forJsonDecodingError(json_last_error(), $raw_results); + } + + $value = null; + if (is_array($results) && array_key_exists('value', $results)) { + $value = $results['value']; + } + + $message = null; + if (is_array($value) && array_key_exists('message', $value)) { + $message = $value['message']; + } + + $sessionId = null; + if (is_array($value) && array_key_exists('sessionId', $value)) { + // W3C's WebDriver + $sessionId = $value['sessionId']; + } elseif (is_array($results) && array_key_exists('sessionId', $results)) { + // Legacy JsonWire + $sessionId = $results['sessionId']; + } + + // @see https://w3c.github.io/webdriver/#errors + if (isset($value['error'])) { + // W3C's WebDriver + WebDriverException::throwException($value['error'], $message, $results); + } + + $status = $results['status'] ?? 0; + if ($status !== 0) { + // Legacy JsonWire + WebDriverException::throwException($status, $message, $results); + } + + $response = new WebDriverResponse($sessionId); + + return $response + ->setStatus($status) + ->setValue($value); + } + + /** + * @return string + */ + public function getAddressOfRemoteServer() + { + return $this->url; + } + + /** + * @return array + */ + protected function getCommandHttpOptions(WebDriverCommand $command) + { + $commandName = $command->getName(); + if (!isset(self::$commands[$commandName])) { + if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) { + throw LogicException::forError($command->getName() . ' is not a valid command.'); + } + } + + if ($this->isW3cCompliant) { + $raw = self::$w3cCompliantCommands[$command->getName()]; + } else { + $raw = self::$commands[$command->getName()]; + } + + if ($command instanceof CustomWebDriverCommand) { + $url = $command->getCustomUrl(); + $method = $command->getCustomMethod(); + } else { + $url = $raw['url']; + $method = $raw['method']; + } + + return [ + 'url' => $url, + 'method' => $method, + ]; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/JsonWireCompat.php b/vendor/php-webdriver/webdriver/lib/Remote/JsonWireCompat.php new file mode 100644 index 0000000..b9e1b5e --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/JsonWireCompat.php @@ -0,0 +1,98 @@ +getMechanism(); + $value = $by->getValue(); + + if ($isW3cCompliant) { + switch ($mechanism) { + // Convert to CSS selectors + case 'class name': + $mechanism = 'css selector'; + $value = sprintf('.%s', self::escapeSelector($value)); + break; + case 'id': + $mechanism = 'css selector'; + $value = sprintf('#%s', self::escapeSelector($value)); + break; + case 'name': + $mechanism = 'css selector'; + $value = sprintf('[name=\'%s\']', self::escapeSelector($value)); + break; + } + } + + return ['using' => $mechanism, 'value' => $value]; + } + + /** + * Escapes a CSS selector. + * + * Code adapted from the Zend Escaper project. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @see https://github.com/zendframework/zend-escaper/blob/master/src/Escaper.php + * + * @param string $selector + * @return string + */ + private static function escapeSelector($selector) + { + return preg_replace_callback('/[^a-z0-9]/iSu', function ($matches) { + $chr = $matches[0]; + if (mb_strlen($chr) === 1) { + $ord = ord($chr); + } else { + $chr = mb_convert_encoding($chr, 'UTF-32BE', 'UTF-8'); + $ord = hexdec(bin2hex($chr)); + } + + return sprintf('\\%X ', $ord); + }, $selector); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/LocalFileDetector.php b/vendor/php-webdriver/webdriver/lib/Remote/LocalFileDetector.php new file mode 100644 index 0000000..ea7e85e --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/LocalFileDetector.php @@ -0,0 +1,20 @@ +driver = $driver; + } + + /** + * @param string $command_name + * @return mixed + */ + public function execute($command_name, array $parameters = []) + { + return $this->driver->execute($command_name, $parameters); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteKeyboard.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteKeyboard.php new file mode 100644 index 0000000..095b0c5 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/RemoteKeyboard.php @@ -0,0 +1,105 @@ +executor = $executor; + $this->driver = $driver; + $this->isW3cCompliant = $isW3cCompliant; + } + + /** + * Send keys to active element + * @param string|array $keys + * @return $this + */ + public function sendKeys($keys) + { + if ($this->isW3cCompliant) { + $activeElement = $this->driver->switchTo()->activeElement(); + $activeElement->sendKeys($keys); + } else { + $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ + 'value' => WebDriverKeys::encode($keys), + ]); + } + + return $this; + } + + /** + * Press a modifier key + * + * @see WebDriverKeys + * @param string $key + * @return $this + */ + public function pressKey($key) + { + if ($this->isW3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'key', + 'id' => 'keyboard', + 'actions' => [['type' => 'keyDown', 'value' => $key]], + ], + ], + ]); + } else { + $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ + 'value' => [(string) $key], + ]); + } + + return $this; + } + + /** + * Release a modifier key + * + * @see WebDriverKeys + * @param string $key + * @return $this + */ + public function releaseKey($key) + { + if ($this->isW3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'key', + 'id' => 'keyboard', + 'actions' => [['type' => 'keyUp', 'value' => $key]], + ], + ], + ]); + } else { + $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ + 'value' => [(string) $key], + ]); + } + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteMouse.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteMouse.php new file mode 100644 index 0000000..fee209f --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/RemoteMouse.php @@ -0,0 +1,290 @@ +executor = $executor; + $this->isW3cCompliant = $isW3cCompliant; + } + + /** + * @return RemoteMouse + */ + public function click(?WebDriverCoordinates $where = null) + { + if ($this->isW3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, $this->createClickActions()), + ], + ], + ]); + + return $this; + } + + $this->moveIfNeeded($where); + $this->executor->execute(DriverCommand::CLICK, [ + 'button' => self::BUTTON_LEFT, + ]); + + return $this; + } + + /** + * @return RemoteMouse + */ + public function contextClick(?WebDriverCoordinates $where = null) + { + if ($this->isW3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, [ + [ + 'type' => 'pointerDown', + 'button' => self::BUTTON_RIGHT, + ], + [ + 'type' => 'pointerUp', + 'button' => self::BUTTON_RIGHT, + ], + ]), + ], + ], + ]); + + return $this; + } + + $this->moveIfNeeded($where); + $this->executor->execute(DriverCommand::CLICK, [ + 'button' => self::BUTTON_RIGHT, + ]); + + return $this; + } + + /** + * @return RemoteMouse + */ + public function doubleClick(?WebDriverCoordinates $where = null) + { + if ($this->isW3cCompliant) { + $clickActions = $this->createClickActions(); + $moveAction = $where === null ? [] : [$this->createMoveAction($where)]; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, $clickActions, $clickActions), + ], + ], + ]); + + return $this; + } + + $this->moveIfNeeded($where); + $this->executor->execute(DriverCommand::DOUBLE_CLICK); + + return $this; + } + + /** + * @return RemoteMouse + */ + public function mouseDown(?WebDriverCoordinates $where = null) + { + if ($this->isW3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => [ + $this->createMoveAction($where), + [ + 'type' => 'pointerDown', + 'button' => self::BUTTON_LEFT, + ], + ], + ], + ], + ]); + + return $this; + } + + $this->moveIfNeeded($where); + $this->executor->execute(DriverCommand::MOUSE_DOWN); + + return $this; + } + + /** + * @param int|null $x_offset + * @param int|null $y_offset + * + * @return RemoteMouse + */ + public function mouseMove( + ?WebDriverCoordinates $where = null, + $x_offset = null, + $y_offset = null + ) { + if ($this->isW3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => [$this->createMoveAction($where, $x_offset, $y_offset)], + ], + ], + ]); + + return $this; + } + + $params = []; + if ($where !== null) { + $params['element'] = $where->getAuxiliary(); + } + if ($x_offset !== null) { + $params['xoffset'] = $x_offset; + } + if ($y_offset !== null) { + $params['yoffset'] = $y_offset; + } + + $this->executor->execute(DriverCommand::MOVE_TO, $params); + + return $this; + } + + /** + * @return RemoteMouse + */ + public function mouseUp(?WebDriverCoordinates $where = null) + { + if ($this->isW3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, [ + [ + 'type' => 'pointerUp', + 'button' => self::BUTTON_LEFT, + ], + ]), + ], + ], + ]); + + return $this; + } + + $this->moveIfNeeded($where); + $this->executor->execute(DriverCommand::MOUSE_UP); + + return $this; + } + + protected function moveIfNeeded(?WebDriverCoordinates $where = null) + { + if ($where) { + $this->mouseMove($where); + } + } + + /** + * @param int|null $x_offset + * @param int|null $y_offset + * + * @return array + */ + private function createMoveAction( + ?WebDriverCoordinates $where = null, + $x_offset = null, + $y_offset = null + ) { + $move_action = [ + 'type' => 'pointerMove', + 'duration' => 100, // to simulate human delay + 'x' => $x_offset ?? 0, + 'y' => $y_offset ?? 0, + ]; + + if ($where !== null) { + $move_action['origin'] = [JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $where->getAuxiliary()]; + } else { + $move_action['origin'] = 'pointer'; + } + + return $move_action; + } + + /** + * @return array + */ + private function createClickActions() + { + return [ + [ + 'type' => 'pointerDown', + 'button' => self::BUTTON_LEFT, + ], + [ + 'type' => 'pointerUp', + 'button' => self::BUTTON_LEFT, + ], + ]; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteStatus.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteStatus.php new file mode 100644 index 0000000..73ebeba --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/RemoteStatus.php @@ -0,0 +1,79 @@ +isReady = (bool) $isReady; + $this->message = (string) $message; + + $this->setMeta($meta); + } + + /** + * @return RemoteStatus + */ + public static function createFromResponse(array $responseBody) + { + $object = new static($responseBody['ready'], $responseBody['message'], $responseBody); + + return $object; + } + + /** + * The remote end's readiness state. + * False if an attempt to create a session at the current time would fail. + * However, the value true does not guarantee that a New Session command will succeed. + * + * @return bool + */ + public function isReady() + { + return $this->isReady; + } + + /** + * An implementation-defined string explaining the remote end's readiness state. + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Arbitrary meta information specific to remote-end implementation. + * + * @return array + */ + public function getMeta() + { + return $this->meta; + } + + protected function setMeta(array $meta) + { + unset($meta['ready'], $meta['message']); + + $this->meta = $meta; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteTargetLocator.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteTargetLocator.php new file mode 100644 index 0000000..979b9c4 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/RemoteTargetLocator.php @@ -0,0 +1,149 @@ +executor = $executor; + $this->driver = $driver; + $this->isW3cCompliant = $isW3cCompliant; + } + + /** + * @return RemoteWebDriver + */ + public function defaultContent() + { + $params = ['id' => null]; + $this->executor->execute(DriverCommand::SWITCH_TO_FRAME, $params); + + return $this->driver; + } + + /** + * @param WebDriverElement|null|int|string $frame The WebDriverElement, the id or the name of the frame. + * When null, switch to the current top-level browsing context When int, switch to the WindowProxy identified + * by the value. When an Element, switch to that Element. + * @return RemoteWebDriver + */ + public function frame($frame) + { + if ($this->isW3cCompliant) { + if ($frame instanceof WebDriverElement) { + $id = [JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $frame->getID()]; + } elseif ($frame === null) { + $id = null; + } elseif (is_int($frame)) { + $id = $frame; + } else { + throw LogicException::forError( + 'In W3C compliance mode frame must be either instance of WebDriverElement, integer or null' + ); + } + } else { + if ($frame instanceof WebDriverElement) { + $id = ['ELEMENT' => $frame->getID()]; + } elseif ($frame === null) { + $id = null; + } elseif (is_int($frame)) { + $id = $frame; + } else { + $id = (string) $frame; + } + } + + $params = ['id' => $id]; + $this->executor->execute(DriverCommand::SWITCH_TO_FRAME, $params); + + return $this->driver; + } + + /** + * Switch to the parent iframe. + * + * @return RemoteWebDriver This driver focused on the parent frame + */ + public function parent() + { + $this->executor->execute(DriverCommand::SWITCH_TO_PARENT_FRAME, []); + + return $this->driver; + } + + /** + * @param string $handle The handle of the window to be focused on. + * @return RemoteWebDriver + */ + public function window($handle) + { + if ($this->isW3cCompliant) { + $params = ['handle' => (string) $handle]; + } else { + $params = ['name' => (string) $handle]; + } + + $this->executor->execute(DriverCommand::SWITCH_TO_WINDOW, $params); + + return $this->driver; + } + + /** + * Creates a new browser window and switches the focus for future commands of this driver to the new window. + * + * @see https://w3c.github.io/webdriver/#new-window + * @param string $windowType The type of a new browser window that should be created. One of [tab, window]. + * The created window is not guaranteed to be of the requested type; if the driver does not support the requested + * type, a new browser window will be created of whatever type the driver does support. + * @throws LogicException + * @return RemoteWebDriver This driver focused on the given window + */ + public function newWindow($windowType = self::WINDOW_TYPE_TAB) + { + if ($windowType !== self::WINDOW_TYPE_TAB && $windowType !== self::WINDOW_TYPE_WINDOW) { + throw LogicException::forError('Window type must by either "tab" or "window"'); + } + + if (!$this->isW3cCompliant) { + throw LogicException::forError('New window is only supported in W3C mode'); + } + + $response = $this->executor->execute(DriverCommand::NEW_WINDOW, ['type' => $windowType]); + + $this->window($response['handle']); + + return $this->driver; + } + + public function alert() + { + return new WebDriverAlert($this->executor); + } + + /** + * @return RemoteWebElement + */ + public function activeElement() + { + $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); + $method = new RemoteExecuteMethod($this->driver); + + return new RemoteWebElement($method, JsonWireCompat::getElement($response), $this->isW3cCompliant); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteTouchScreen.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteTouchScreen.php new file mode 100644 index 0000000..951c861 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/RemoteTouchScreen.php @@ -0,0 +1,177 @@ +executor = $executor; + } + + /** + * @return RemoteTouchScreen The instance. + */ + public function tap(WebDriverElement $element) + { + $this->executor->execute( + DriverCommand::TOUCH_SINGLE_TAP, + ['element' => $element->getID()] + ); + + return $this; + } + + /** + * @return RemoteTouchScreen The instance. + */ + public function doubleTap(WebDriverElement $element) + { + $this->executor->execute( + DriverCommand::TOUCH_DOUBLE_TAP, + ['element' => $element->getID()] + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * + * @return RemoteTouchScreen The instance. + */ + public function down($x, $y) + { + $this->executor->execute(DriverCommand::TOUCH_DOWN, [ + 'x' => $x, + 'y' => $y, + ]); + + return $this; + } + + /** + * @param int $xspeed + * @param int $yspeed + * + * @return RemoteTouchScreen The instance. + */ + public function flick($xspeed, $yspeed) + { + $this->executor->execute(DriverCommand::TOUCH_FLICK, [ + 'xspeed' => $xspeed, + 'yspeed' => $yspeed, + ]); + + return $this; + } + + /** + * @param int $xoffset + * @param int $yoffset + * @param int $speed + * + * @return RemoteTouchScreen The instance. + */ + public function flickFromElement(WebDriverElement $element, $xoffset, $yoffset, $speed) + { + $this->executor->execute(DriverCommand::TOUCH_FLICK, [ + 'xoffset' => $xoffset, + 'yoffset' => $yoffset, + 'element' => $element->getID(), + 'speed' => $speed, + ]); + + return $this; + } + + /** + * @return RemoteTouchScreen The instance. + */ + public function longPress(WebDriverElement $element) + { + $this->executor->execute( + DriverCommand::TOUCH_LONG_PRESS, + ['element' => $element->getID()] + ); + + return $this; + } + + /** + * @param int $x + * @param int $y + * + * @return RemoteTouchScreen The instance. + */ + public function move($x, $y) + { + $this->executor->execute(DriverCommand::TOUCH_MOVE, [ + 'x' => $x, + 'y' => $y, + ]); + + return $this; + } + + /** + * @param int $xoffset + * @param int $yoffset + * + * @return RemoteTouchScreen The instance. + */ + public function scroll($xoffset, $yoffset) + { + $this->executor->execute(DriverCommand::TOUCH_SCROLL, [ + 'xoffset' => $xoffset, + 'yoffset' => $yoffset, + ]); + + return $this; + } + + /** + * @param int $xoffset + * @param int $yoffset + * + * @return RemoteTouchScreen The instance. + */ + public function scrollFromElement(WebDriverElement $element, $xoffset, $yoffset) + { + $this->executor->execute(DriverCommand::TOUCH_SCROLL, [ + 'element' => $element->getID(), + 'xoffset' => $xoffset, + 'yoffset' => $yoffset, + ]); + + return $this; + } + + /** + * @param int $x + * @param int $y + * + * @return RemoteTouchScreen The instance. + */ + public function up($x, $y) + { + $this->executor->execute(DriverCommand::TOUCH_UP, [ + 'x' => $x, + 'y' => $y, + ]); + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php new file mode 100644 index 0000000..3d65aaf --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php @@ -0,0 +1,760 @@ +executor = $commandExecutor; + $this->sessionID = $sessionId; + $this->isW3cCompliant = $isW3cCompliant; + $this->capabilities = $capabilities; + } + + /** + * Construct the RemoteWebDriver by a desired capabilities. + * + * @param string $selenium_server_url The url of the remote Selenium WebDriver server + * @param DesiredCapabilities|array $desired_capabilities The desired capabilities + * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server + * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server + * @param string|null $http_proxy The proxy to tunnel requests to the remote Selenium WebDriver through + * @param int|null $http_proxy_port The proxy port to tunnel requests to the remote Selenium WebDriver through + * @param DesiredCapabilities $required_capabilities The required capabilities + * + * @return static + */ + public static function create( + $selenium_server_url = 'http://localhost:4444/wd/hub', + $desired_capabilities = null, + $connection_timeout_in_ms = null, + $request_timeout_in_ms = null, + $http_proxy = null, + $http_proxy_port = null, + ?DesiredCapabilities $required_capabilities = null + ) { + $selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url); + + $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); + + $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port); + if ($connection_timeout_in_ms !== null) { + $executor->setConnectionTimeout($connection_timeout_in_ms); + } + if ($request_timeout_in_ms !== null) { + $executor->setRequestTimeout($request_timeout_in_ms); + } + + // W3C + $parameters = [ + 'capabilities' => [ + 'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()], + ], + ]; + + if ($required_capabilities !== null && !empty($required_capabilities->toArray())) { + $parameters['capabilities']['alwaysMatch'] = (object) $required_capabilities->toW3cCompatibleArray(); + } + + // Legacy protocol + if ($required_capabilities !== null) { + // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities. + // This has changed with the W3C WebDriver spec, but is the only way how to pass these + // values with the legacy protocol. + $desired_capabilities->setCapability('requiredCapabilities', (object) $required_capabilities->toArray()); + } + + $parameters['desiredCapabilities'] = (object) $desired_capabilities->toArray(); + + $command = WebDriverCommand::newSession($parameters); + + $response = $executor->execute($command); + + return static::createFromResponse($response, $executor); + } + + /** + * [Experimental] Construct the RemoteWebDriver by an existing session. + * + * This constructor can boost the performance by reusing the same browser for the whole test suite. On the other + * hand, because the browser is not pristine, this may lead to flaky and dependent tests. So carefully + * consider the tradeoffs. + * + * To create the instance, we need to know Capabilities of the previously created session. You can either + * pass them in $existingCapabilities parameter, or we will attempt to receive them from the Selenium Grid server. + * However, if Capabilities were not provided and the attempt to get them was not successful, + * exception will be thrown. + * + * @param string $session_id The existing session id + * @param string $selenium_server_url The url of the remote Selenium WebDriver server + * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server + * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server + * @param bool $isW3cCompliant True to use W3C WebDriver (default), false to use the legacy JsonWire protocol + * @param WebDriverCapabilities|null $existingCapabilities Provide capabilities of the existing previously created + * session. If not provided, we will attempt to read them, but this will only work when using Selenium Grid. + * @return static + */ + public static function createBySessionID( + $session_id, + $selenium_server_url = 'http://localhost:4444/wd/hub', + $connection_timeout_in_ms = null, + $request_timeout_in_ms = null + ) { + // BC layer to not break the method signature + $isW3cCompliant = func_num_args() > 4 ? func_get_arg(4) : true; + $existingCapabilities = func_num_args() > 5 ? func_get_arg(5) : null; + + $executor = new HttpCommandExecutor($selenium_server_url, null, null); + if ($connection_timeout_in_ms !== null) { + $executor->setConnectionTimeout($connection_timeout_in_ms); + } + if ($request_timeout_in_ms !== null) { + $executor->setRequestTimeout($request_timeout_in_ms); + } + + if (!$isW3cCompliant) { + $executor->disableW3cCompliance(); + } + + // if capabilities were not provided, attempt to read them from the Selenium Grid API + if ($existingCapabilities === null) { + $existingCapabilities = self::readExistingCapabilitiesFromSeleniumGrid($session_id, $executor); + } + + return new static($executor, $session_id, $existingCapabilities, $isW3cCompliant); + } + + /** + * Close the current window. + * + * @return RemoteWebDriver The current instance. + */ + public function close() + { + $this->execute(DriverCommand::CLOSE, []); + + return $this; + } + + /** + * Create a new top-level browsing context. + * + * @codeCoverageIgnore + * @deprecated Use $driver->switchTo()->newWindow() + * @return WebDriver The current instance. + */ + public function newWindow() + { + return $this->switchTo()->newWindow(); + } + + /** + * Find the first WebDriverElement using the given mechanism. + * + * @return RemoteWebElement NoSuchElementException is thrown in HttpCommandExecutor if no element is found. + * @see WebDriverBy + */ + public function findElement(WebDriverBy $by) + { + $raw_element = $this->execute( + DriverCommand::FIND_ELEMENT, + JsonWireCompat::getUsing($by, $this->isW3cCompliant) + ); + + return $this->newElement(JsonWireCompat::getElement($raw_element)); + } + + /** + * Find all WebDriverElements within the current page using the given mechanism. + * + * @return RemoteWebElement[] A list of all WebDriverElements, or an empty array if nothing matches + * @see WebDriverBy + */ + public function findElements(WebDriverBy $by) + { + $raw_elements = $this->execute( + DriverCommand::FIND_ELEMENTS, + JsonWireCompat::getUsing($by, $this->isW3cCompliant) + ); + + if (!is_array($raw_elements)) { + throw UnexpectedResponseException::forError('Server response to findElements command is not an array'); + } + + $elements = []; + foreach ($raw_elements as $raw_element) { + $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); + } + + return $elements; + } + + /** + * Load a new web page in the current browser window. + * + * @param string $url + * + * @return RemoteWebDriver The current instance. + */ + public function get($url) + { + $params = ['url' => (string) $url]; + $this->execute(DriverCommand::GET, $params); + + return $this; + } + + /** + * Get a string representing the current URL that the browser is looking at. + * + * @return string The current URL. + */ + public function getCurrentURL() + { + return $this->execute(DriverCommand::GET_CURRENT_URL); + } + + /** + * Get the source of the last loaded page. + * + * @return string The current page source. + */ + public function getPageSource() + { + return $this->execute(DriverCommand::GET_PAGE_SOURCE); + } + + /** + * Get the title of the current page. + * + * @return string The title of the current page. + */ + public function getTitle() + { + return $this->execute(DriverCommand::GET_TITLE); + } + + /** + * Return an opaque handle to this window that uniquely identifies it within this driver instance. + * + * @return string The current window handle. + */ + public function getWindowHandle() + { + return $this->execute( + DriverCommand::GET_CURRENT_WINDOW_HANDLE, + [] + ); + } + + /** + * Get all window handles available to the current session. + * + * Note: Do not use `end($driver->getWindowHandles())` to find the last open window, for proper solution see: + * https://github.com/php-webdriver/php-webdriver/wiki/Alert,-tabs,-frames,-iframes#switch-to-the-new-window + * + * @return array An array of string containing all available window handles. + */ + public function getWindowHandles() + { + return $this->execute(DriverCommand::GET_WINDOW_HANDLES, []); + } + + /** + * Quits this driver, closing every associated window. + */ + public function quit() + { + $this->execute(DriverCommand::QUIT); + $this->executor = null; + } + + /** + * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. + * The executed script is assumed to be synchronous and the result of evaluating the script will be returned. + * + * @param string $script The script to inject. + * @param array $arguments The arguments of the script. + * @return mixed The return value of the script. + */ + public function executeScript($script, array $arguments = []) + { + $params = [ + 'script' => $script, + 'args' => $this->prepareScriptArguments($arguments), + ]; + + return $this->execute(DriverCommand::EXECUTE_SCRIPT, $params); + } + + /** + * Inject a snippet of JavaScript into the page for asynchronous execution in the context of the currently selected + * frame. + * + * The driver will pass a callback as the last argument to the snippet, and block until the callback is invoked. + * + * You may need to define script timeout using `setScriptTimeout()` method of `WebDriverTimeouts` first. + * + * @param string $script The script to inject. + * @param array $arguments The arguments of the script. + * @return mixed The value passed by the script to the callback. + */ + public function executeAsyncScript($script, array $arguments = []) + { + $params = [ + 'script' => $script, + 'args' => $this->prepareScriptArguments($arguments), + ]; + + return $this->execute( + DriverCommand::EXECUTE_ASYNC_SCRIPT, + $params + ); + } + + /** + * Take a screenshot of the current page. + * + * @param string $save_as The path of the screenshot to be saved. + * @return string The screenshot in PNG format. + */ + public function takeScreenshot($save_as = null) + { + return (new ScreenshotHelper($this->getExecuteMethod()))->takePageScreenshot($save_as); + } + + /** + * Status returns information about whether a remote end is in a state in which it can create new sessions. + */ + public function getStatus() + { + $response = $this->execute(DriverCommand::STATUS); + + return RemoteStatus::createFromResponse($response); + } + + /** + * Construct a new WebDriverWait by the current WebDriver instance. + * Sample usage: + * + * ``` + * $driver->wait(20, 1000)->until( + * WebDriverExpectedCondition::titleIs('WebDriver Page') + * ); + * ``` + * @param int $timeout_in_second + * @param int $interval_in_millisecond + * + * @return WebDriverWait + */ + public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) + { + return new WebDriverWait( + $this, + $timeout_in_second, + $interval_in_millisecond + ); + } + + /** + * An abstraction for managing stuff you would do in a browser menu. For example, adding and deleting cookies. + * + * @return WebDriverOptions + */ + public function manage() + { + return new WebDriverOptions($this->getExecuteMethod(), $this->isW3cCompliant); + } + + /** + * An abstraction allowing the driver to access the browser's history and to navigate to a given URL. + * + * @return WebDriverNavigation + * @see WebDriverNavigation + */ + public function navigate() + { + return new WebDriverNavigation($this->getExecuteMethod()); + } + + /** + * Switch to a different window or frame. + * + * @return RemoteTargetLocator + * @see RemoteTargetLocator + */ + public function switchTo() + { + return new RemoteTargetLocator($this->getExecuteMethod(), $this, $this->isW3cCompliant); + } + + /** + * @return RemoteMouse + */ + public function getMouse() + { + if (!$this->mouse) { + $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->isW3cCompliant); + } + + return $this->mouse; + } + + /** + * @return RemoteKeyboard + */ + public function getKeyboard() + { + if (!$this->keyboard) { + $this->keyboard = new RemoteKeyboard($this->getExecuteMethod(), $this, $this->isW3cCompliant); + } + + return $this->keyboard; + } + + /** + * @return RemoteTouchScreen + */ + public function getTouch() + { + if (!$this->touch) { + $this->touch = new RemoteTouchScreen($this->getExecuteMethod()); + } + + return $this->touch; + } + + /** + * Construct a new action builder. + * + * @return WebDriverActions + */ + public function action() + { + return new WebDriverActions($this); + } + + /** + * Set the command executor of this RemoteWebdriver + * + * @deprecated To be removed in the future. Executor should be passed in the constructor. + * @internal + * @codeCoverageIgnore + * @param WebDriverCommandExecutor $executor Despite the typehint, it have be an instance of HttpCommandExecutor. + * @return RemoteWebDriver + */ + public function setCommandExecutor(WebDriverCommandExecutor $executor) + { + $this->executor = $executor; + + return $this; + } + + /** + * Get the command executor of this RemoteWebdriver + * + * @return HttpCommandExecutor + */ + public function getCommandExecutor() + { + return $this->executor; + } + + /** + * Set the session id of the RemoteWebDriver. + * + * @deprecated To be removed in the future. Session ID should be passed in the constructor. + * @internal + * @codeCoverageIgnore + * @param string $session_id + * @return RemoteWebDriver + */ + public function setSessionID($session_id) + { + $this->sessionID = $session_id; + + return $this; + } + + /** + * Get current selenium sessionID + * + * @return string + */ + public function getSessionID() + { + return $this->sessionID; + } + + /** + * Get capabilities of the RemoteWebDriver. + * + * @return WebDriverCapabilities|null + */ + public function getCapabilities() + { + return $this->capabilities; + } + + /** + * Returns a list of the currently active sessions. + * + * @deprecated Removed in W3C WebDriver. + * @param string $selenium_server_url The url of the remote Selenium WebDriver server + * @param int $timeout_in_ms + * @return array + */ + public static function getAllSessions($selenium_server_url = 'http://localhost:4444/wd/hub', $timeout_in_ms = 30000) + { + $executor = new HttpCommandExecutor($selenium_server_url, null, null); + $executor->setConnectionTimeout($timeout_in_ms); + + $command = new WebDriverCommand( + null, + DriverCommand::GET_ALL_SESSIONS, + [] + ); + + return $executor->execute($command)->getValue(); + } + + public function execute($command_name, $params = []) + { + // As we so far only use atom for IS_ELEMENT_DISPLAYED, this condition is hardcoded here. In case more atoms + // are used, this should be rewritten and separated from this class (e.g. to some abstract matcher logic). + if ($command_name === DriverCommand::IS_ELEMENT_DISPLAYED + && ( + // When capabilities are missing in php-webdriver 1.13.x, always fallback to use the atom + $this->getCapabilities() === null + // If capabilities are present, use the atom only if condition matches + || IsElementDisplayedAtom::match($this->getCapabilities()->getBrowserName()) + ) + ) { + return (new IsElementDisplayedAtom($this))->execute($params); + } + + $command = new WebDriverCommand( + $this->sessionID, + $command_name, + $params + ); + + if ($this->executor) { + $response = $this->executor->execute($command); + + return $response->getValue(); + } + + return null; + } + + /** + * Execute custom commands on remote end. + * For example vendor-specific commands or other commands not implemented by php-webdriver. + * + * @see https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands + * @param string $endpointUrl + * @param string $method + * @param array $params + * @return mixed|null + */ + public function executeCustomCommand($endpointUrl, $method = 'GET', $params = []) + { + $command = new CustomWebDriverCommand( + $this->sessionID, + $endpointUrl, + $method, + $params + ); + + if ($this->executor) { + $response = $this->executor->execute($command); + + return $response->getValue(); + } + + return null; + } + + /** + * @internal + * @return bool + */ + public function isW3cCompliant() + { + return $this->isW3cCompliant; + } + + /** + * Create instance based on response to NEW_SESSION command. + * Also detect W3C/OSS dialect and setup the driver/executor accordingly. + * + * @internal + * @return static + */ + protected static function createFromResponse(WebDriverResponse $response, HttpCommandExecutor $commandExecutor) + { + $responseValue = $response->getValue(); + + if (!$isW3cCompliant = isset($responseValue['capabilities'])) { + $commandExecutor->disableW3cCompliance(); + } + + if ($isW3cCompliant) { + $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($responseValue['capabilities']); + } else { + $returnedCapabilities = new DesiredCapabilities($responseValue); + } + + return new static($commandExecutor, $response->getSessionID(), $returnedCapabilities, $isW3cCompliant); + } + + /** + * Prepare arguments for JavaScript injection + * + * @return array + */ + protected function prepareScriptArguments(array $arguments) + { + $args = []; + foreach ($arguments as $key => $value) { + if ($value instanceof WebDriverElement) { + $args[$key] = [ + $this->isW3cCompliant ? + JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER + : 'ELEMENT' => $value->getID(), + ]; + } else { + if (is_array($value)) { + $value = $this->prepareScriptArguments($value); + } + $args[$key] = $value; + } + } + + return $args; + } + + /** + * @return RemoteExecuteMethod + */ + protected function getExecuteMethod() + { + if (!$this->executeMethod) { + $this->executeMethod = new RemoteExecuteMethod($this); + } + + return $this->executeMethod; + } + + /** + * Return the WebDriverElement with the given id. + * + * @param string $id The id of the element to be created. + * @return RemoteWebElement + */ + protected function newElement($id) + { + return new RemoteWebElement($this->getExecuteMethod(), $id, $this->isW3cCompliant); + } + + /** + * Cast legacy types (array or null) to DesiredCapabilities object. To be removed in future when instance of + * DesiredCapabilities will be required. + * + * @param array|DesiredCapabilities|null $desired_capabilities + * @return DesiredCapabilities + */ + protected static function castToDesiredCapabilitiesObject($desired_capabilities = null) + { + if ($desired_capabilities === null) { + return new DesiredCapabilities(); + } + + if (is_array($desired_capabilities)) { + return new DesiredCapabilities($desired_capabilities); + } + + return $desired_capabilities; + } + + protected static function readExistingCapabilitiesFromSeleniumGrid( + string $session_id, + HttpCommandExecutor $executor + ): DesiredCapabilities { + $getCapabilitiesCommand = new CustomWebDriverCommand($session_id, '/se/grid/session/:sessionId', 'GET', []); + + try { + $capabilitiesResponse = $executor->execute($getCapabilitiesCommand); + + $existingCapabilities = DesiredCapabilities::createFromW3cCapabilities( + $capabilitiesResponse->getValue()['capabilities'] + ); + if ($existingCapabilities === null) { + throw UnexpectedResponseException::forError('Empty capabilities received'); + } + } catch (\Exception $e) { + throw UnexpectedResponseException::forCapabilitiesRetrievalError($e); + } + + return $existingCapabilities; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php new file mode 100644 index 0000000..e0ce43b --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php @@ -0,0 +1,650 @@ +executor = $executor; + $this->id = $id; + $this->fileDetector = new UselessFileDetector(); + $this->isW3cCompliant = $isW3cCompliant; + } + + /** + * Clear content editable or resettable element + * + * @return $this The current instance. + */ + public function clear() + { + $this->executor->execute( + DriverCommand::CLEAR_ELEMENT, + [':id' => $this->id] + ); + + return $this; + } + + /** + * Click this element. + * + * @return $this The current instance. + */ + public function click() + { + try { + $this->executor->execute( + DriverCommand::CLICK_ELEMENT, + [':id' => $this->id] + ); + } catch (ElementNotInteractableException $e) { + // An issue with geckodriver (https://github.com/mozilla/geckodriver/issues/653) prevents clicking on a link + // if the first child is a block-level element. + // The workaround in this case is to click on a child element. + $this->clickChildElement($e); + } + + return $this; + } + + /** + * Find the first WebDriverElement within this element using the given mechanism. + * + * When using xpath be aware that webdriver follows standard conventions: a search prefixed with "//" will + * search the entire document from the root, not just the children (relative context) of this current node. + * Use ".//" to limit your search to the children of this element. + * + * @return static NoSuchElementException is thrown in HttpCommandExecutor if no element is found. + * @see WebDriverBy + */ + public function findElement(WebDriverBy $by) + { + $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); + $params[':id'] = $this->id; + + $raw_element = $this->executor->execute( + DriverCommand::FIND_CHILD_ELEMENT, + $params + ); + + return $this->newElement(JsonWireCompat::getElement($raw_element)); + } + + /** + * Find all WebDriverElements within this element using the given mechanism. + * + * When using xpath be aware that webdriver follows standard conventions: a search prefixed with "//" will + * search the entire document from the root, not just the children (relative context) of this current node. + * Use ".//" to limit your search to the children of this element. + * + * @return static[] A list of all WebDriverElements, or an empty + * array if nothing matches + * @see WebDriverBy + */ + public function findElements(WebDriverBy $by) + { + $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); + $params[':id'] = $this->id; + $raw_elements = $this->executor->execute( + DriverCommand::FIND_CHILD_ELEMENTS, + $params + ); + + if (!is_array($raw_elements)) { + throw UnexpectedResponseException::forError('Server response to findChildElements command is not an array'); + } + + $elements = []; + foreach ($raw_elements as $raw_element) { + $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); + } + + return $elements; + } + + /** + * Get the value of the given attribute of the element. + * Attribute is meant what is declared in the HTML markup of the element. + * To read a value of a IDL "JavaScript" property (like `innerHTML`), use `getDomProperty()` method. + * + * @param string $attribute_name The name of the attribute. + * @return string|true|null The value of the attribute. If this is boolean attribute, return true if the element + * has it, otherwise return null. + */ + public function getAttribute($attribute_name) + { + $params = [ + ':name' => $attribute_name, + ':id' => $this->id, + ]; + + if ($this->isW3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { + $value = $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); + + if ($value === true) { + return 'true'; + } + + if ($value === false) { + return 'false'; + } + + if ($value !== null) { + return (string) $value; + } + } + + return $this->executor->execute(DriverCommand::GET_ELEMENT_ATTRIBUTE, $params); + } + + /** + * Gets the value of a IDL JavaScript property of this element (for example `innerHTML`, `tagName` etc.). + * + * @see https://developer.mozilla.org/en-US/docs/Glossary/IDL + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element#properties + * @param string $propertyName + * @return mixed|null The property's current value or null if the value is not set or the property does not exist. + */ + public function getDomProperty($propertyName) + { + if (!$this->isW3cCompliant) { + throw new UnsupportedOperationException('This method is only supported in W3C mode'); + } + + $params = [ + ':name' => $propertyName, + ':id' => $this->id, + ]; + + return $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); + } + + /** + * Get the value of a given CSS property. + * + * @param string $css_property_name The name of the CSS property. + * @return string The value of the CSS property. + */ + public function getCSSValue($css_property_name) + { + $params = [ + ':propertyName' => $css_property_name, + ':id' => $this->id, + ]; + + return $this->executor->execute( + DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY, + $params + ); + } + + /** + * Get the location of element relative to the top-left corner of the page. + * + * @return WebDriverPoint The location of the element. + */ + public function getLocation() + { + $location = $this->executor->execute( + DriverCommand::GET_ELEMENT_LOCATION, + [':id' => $this->id] + ); + + return new WebDriverPoint($location['x'], $location['y']); + } + + /** + * Try scrolling the element into the view port and return the location of + * element relative to the top-left corner of the page afterwards. + * + * @return WebDriverPoint The location of the element. + */ + public function getLocationOnScreenOnceScrolledIntoView() + { + if ($this->isW3cCompliant) { + $script = <<executor->execute(DriverCommand::EXECUTE_SCRIPT, [ + 'script' => $script, + 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]], + ]); + $location = ['x' => $result['x'], 'y' => $result['y']]; + } else { + $location = $this->executor->execute( + DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW, + [':id' => $this->id] + ); + } + + return new WebDriverPoint($location['x'], $location['y']); + } + + /** + * @return WebDriverCoordinates + */ + public function getCoordinates() + { + $element = $this; + + $on_screen = null; // planned but not yet implemented + $in_view_port = static function () use ($element) { + return $element->getLocationOnScreenOnceScrolledIntoView(); + }; + $on_page = static function () use ($element) { + return $element->getLocation(); + }; + $auxiliary = $this->getID(); + + return new WebDriverCoordinates( + $on_screen, + $in_view_port, + $on_page, + $auxiliary + ); + } + + /** + * Get the size of element. + * + * @return WebDriverDimension The dimension of the element. + */ + public function getSize() + { + $size = $this->executor->execute( + DriverCommand::GET_ELEMENT_SIZE, + [':id' => $this->id] + ); + + return new WebDriverDimension($size['width'], $size['height']); + } + + /** + * Get the (lowercase) tag name of this element. + * + * @return string The tag name. + */ + public function getTagName() + { + // Force tag name to be lowercase as expected by JsonWire protocol for Opera driver + // until this issue is not resolved : + // https://github.com/operasoftware/operadriver/issues/102 + // Remove it when fixed to be consistent with the protocol. + return mb_strtolower($this->executor->execute( + DriverCommand::GET_ELEMENT_TAG_NAME, + [':id' => $this->id] + )); + } + + /** + * Get the visible (i.e. not hidden by CSS) innerText of this element, + * including sub-elements, without any leading or trailing whitespace. + * + * @return string The visible innerText of this element. + */ + public function getText() + { + return $this->executor->execute( + DriverCommand::GET_ELEMENT_TEXT, + [':id' => $this->id] + ); + } + + /** + * Is this element displayed or not? This method avoids the problem of having + * to parse an element's "style" attribute. + * + * @return bool + */ + public function isDisplayed() + { + return $this->executor->execute( + DriverCommand::IS_ELEMENT_DISPLAYED, + [':id' => $this->id] + ); + } + + /** + * Is the element currently enabled or not? This will generally return true + * for everything but disabled input elements. + * + * @return bool + */ + public function isEnabled() + { + return $this->executor->execute( + DriverCommand::IS_ELEMENT_ENABLED, + [':id' => $this->id] + ); + } + + /** + * Determine whether this element is selected or not. + * + * @return bool + */ + public function isSelected() + { + return $this->executor->execute( + DriverCommand::IS_ELEMENT_SELECTED, + [':id' => $this->id] + ); + } + + /** + * Simulate typing into an element, which may set its value. + * + * @param mixed $value The data to be typed. + * @return static The current instance. + */ + public function sendKeys($value) + { + $local_file = $this->fileDetector->getLocalFile($value); + + $params = []; + if ($local_file === null) { + if ($this->isW3cCompliant) { + // Work around the Geckodriver NULL issue by splitting on NULL and calling sendKeys multiple times. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1494661. + $encodedValues = explode(WebDriverKeys::NULL, WebDriverKeys::encode($value, true)); + foreach ($encodedValues as $encodedValue) { + $params[] = [ + 'text' => $encodedValue, + ':id' => $this->id, + ]; + } + } else { + $params[] = [ + 'value' => WebDriverKeys::encode($value), + ':id' => $this->id, + ]; + } + } else { + if ($this->isW3cCompliant) { + try { + // Attempt to upload the file to the remote browser. + // This is so far non-W3C compliant method, so it may fail - if so, we just ignore the exception. + // @see https://github.com/w3c/webdriver/issues/1355 + $fileName = $this->upload($local_file); + } catch (PhpWebDriverExceptionInterface $e) { + $fileName = $local_file; + } + + $params[] = [ + 'text' => $fileName, + ':id' => $this->id, + ]; + } else { + $params[] = [ + 'value' => WebDriverKeys::encode($this->upload($local_file)), + ':id' => $this->id, + ]; + } + } + + foreach ($params as $param) { + $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $param); + } + + return $this; + } + + /** + * Set the fileDetector in order to let the RemoteWebElement to know that you are going to upload a file. + * + * Basically, if you want WebDriver trying to send a file, set the fileDetector + * to be LocalFileDetector. Otherwise, keep it UselessFileDetector. + * + * eg. `$element->setFileDetector(new LocalFileDetector);` + * + * @return $this + * @see FileDetector + * @see LocalFileDetector + * @see UselessFileDetector + */ + public function setFileDetector(FileDetector $detector) + { + $this->fileDetector = $detector; + + return $this; + } + + /** + * If this current element is a form, or an element within a form, then this will be submitted to the remote server. + * + * @return $this The current instance. + */ + public function submit() + { + if ($this->isW3cCompliant) { + // Submit method cannot be called directly in case an input of this form is named "submit". + // We use this polyfill to trigger 'submit' event using form.dispatchEvent(). + $submitPolyfill = <<executor->execute(DriverCommand::EXECUTE_SCRIPT, [ + 'script' => $submitPolyfill, + 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]], + ]); + + return $this; + } + + $this->executor->execute( + DriverCommand::SUBMIT_ELEMENT, + [':id' => $this->id] + ); + + return $this; + } + + /** + * Get the opaque ID of the element. + * + * @return string The opaque ID. + */ + public function getID() + { + return $this->id; + } + + /** + * Take a screenshot of a specific element. + * + * @param string $save_as The path of the screenshot to be saved. + * @return string The screenshot in PNG format. + */ + public function takeElementScreenshot($save_as = null) + { + return (new ScreenshotHelper($this->executor))->takeElementScreenshot($this->id, $save_as); + } + + /** + * Test if two elements IDs refer to the same DOM element. + * + * @return bool + */ + public function equals(WebDriverElement $other) + { + if ($this->isW3cCompliant) { + return $this->getID() === $other->getID(); + } + + return $this->executor->execute(DriverCommand::ELEMENT_EQUALS, [ + ':id' => $this->id, + ':other' => $other->getID(), + ]); + } + + /** + * Get representation of an element's shadow root for accessing the shadow DOM of a web component. + * + * @return ShadowRoot + */ + public function getShadowRoot() + { + if (!$this->isW3cCompliant) { + throw new UnsupportedOperationException('This method is only supported in W3C mode'); + } + + $response = $this->executor->execute( + DriverCommand::GET_ELEMENT_SHADOW_ROOT, + [ + ':id' => $this->id, + ] + ); + + return ShadowRoot::createFromResponse($this->executor, $response); + } + + /** + * Attempt to click on a child level element. + * + * This provides a workaround for geckodriver bug 653 whereby a link whose first element is a block-level element + * throws an ElementNotInteractableException could not scroll into view exception. + * + * The workaround provided here attempts to click on a child node of the element. + * In case the first child is hidden, other elements are processed until we run out of elements. + * + * @param ElementNotInteractableException $originalException The exception to throw if unable to click on any child + * @see https://github.com/mozilla/geckodriver/issues/653 + * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1374283 + */ + protected function clickChildElement(ElementNotInteractableException $originalException) + { + $children = $this->findElements(WebDriverBy::xpath('./*')); + foreach ($children as $child) { + try { + // Note: This does not use $child->click() as this would cause recursion into all children. + // Where the element is hidden, all children will also be hidden. + $this->executor->execute( + DriverCommand::CLICK_ELEMENT, + [':id' => $child->id] + ); + + return; + } catch (ElementNotInteractableException $e) { + // Ignore the ElementNotInteractableException exception on this node. Try the next child instead. + } + } + + throw $originalException; + } + + /** + * Return the WebDriverElement with $id + * + * @param string $id + * + * @return static + */ + protected function newElement($id) + { + return new static($this->executor, $id, $this->isW3cCompliant); + } + + /** + * Upload a local file to the server + * + * @param string $local_file + * + * @throws LogicException + * @return string The remote path of the file. + */ + protected function upload($local_file) + { + if (!is_file($local_file)) { + throw LogicException::forError('You may only upload files: ' . $local_file); + } + + $temp_zip_path = $this->createTemporaryZipArchive($local_file); + + $remote_path = $this->executor->execute( + DriverCommand::UPLOAD_FILE, + ['file' => base64_encode(file_get_contents($temp_zip_path))] + ); + + unlink($temp_zip_path); + + return $remote_path; + } + + /** + * @param string $fileToZip + * @return string + */ + protected function createTemporaryZipArchive($fileToZip) + { + // Create a temporary file in the system temp directory. + // Intentionally do not use `tempnam()`, as it creates empty file which zip extension may not handle. + $tempZipPath = sys_get_temp_dir() . '/' . uniqid('WebDriverZip', false); + + $zip = new ZipArchive(); + if (($errorCode = $zip->open($tempZipPath, ZipArchive::CREATE)) !== true) { + throw IOException::forFileError(sprintf('Error creating zip archive: %s', $errorCode), $tempZipPath); + } + + $info = pathinfo($fileToZip); + $file_name = $info['basename']; + $zip->addFile($fileToZip, $file_name); + $zip->close(); + + return $tempZipPath; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverCommandExecutor.php b/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverCommandExecutor.php new file mode 100644 index 0000000..5e3ef83 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverCommandExecutor.php @@ -0,0 +1,53 @@ +getURL()); + $this->service = $service; + } + + /** + * @throws \Exception + * @throws WebDriverException + * @return WebDriverResponse + */ + public function execute(WebDriverCommand $command) + { + if ($command->getName() === DriverCommand::NEW_SESSION) { + $this->service->start(); + } + + try { + $value = parent::execute($command); + if ($command->getName() === DriverCommand::QUIT) { + $this->service->stop(); + } + + return $value; + } catch (\Exception $e) { + if (!$this->service->isRunning()) { + throw new DriverServerDiedException($e); + } + throw $e; + } + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverService.php b/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverService.php new file mode 100644 index 0000000..028b8cd --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverService.php @@ -0,0 +1,183 @@ +setExecutable($executable); + $this->url = sprintf('http://localhost:%d', $port); + $this->args = $args; + $this->environment = $environment ?: $_ENV; + } + + /** + * @return string + */ + public function getURL() + { + return $this->url; + } + + /** + * @return DriverService + */ + public function start() + { + if ($this->process !== null) { + return $this; + } + + $this->process = $this->createProcess(); + $this->process->start(); + + $this->checkWasStarted($this->process); + + $checker = new URLChecker(); + $checker->waitUntilAvailable(20 * 1000, $this->url . '/status'); + + return $this; + } + + /** + * @return DriverService + */ + public function stop() + { + if ($this->process === null) { + return $this; + } + + $this->process->stop(); + $this->process = null; + + $checker = new URLChecker(); + $checker->waitUntilUnavailable(3 * 1000, $this->url . '/shutdown'); + + return $this; + } + + /** + * @return bool + */ + public function isRunning() + { + if ($this->process === null) { + return false; + } + + return $this->process->isRunning(); + } + + /** + * @deprecated Has no effect. Will be removed in next major version. Executable is now checked + * when calling setExecutable(). + * @param string $executable + * @return string + */ + protected static function checkExecutable($executable) + { + return $executable; + } + + /** + * @param string $executable + * @throws IOException + */ + protected function setExecutable($executable) + { + if ($this->isExecutable($executable)) { + $this->executable = $executable; + + return; + } + + throw IOException::forFileError( + 'File is not executable. Make sure the path is correct or use environment variable to specify' + . ' location of the executable.', + $executable + ); + } + + /** + * @param Process $process + */ + protected function checkWasStarted($process) + { + usleep(10000); // wait 10ms, otherwise the asynchronous process failure may not yet be propagated + + if (!$process->isRunning()) { + throw RuntimeException::forDriverError($process); + } + } + + private function createProcess(): Process + { + $commandLine = array_merge([$this->executable], $this->args); + + return new Process($commandLine, null, $this->environment); + } + + /** + * Check whether given file is executable directly or using system PATH + */ + private function isExecutable(string $filename): bool + { + if (is_executable($filename)) { + return true; + } + if ($filename !== basename($filename)) { // $filename is an absolute path, do no try to search it in PATH + return false; + } + + $paths = explode(PATH_SEPARATOR, getenv('PATH')); + foreach ($paths as $path) { + if (is_executable($path . DIRECTORY_SEPARATOR . $filename)) { + return true; + } + } + + return false; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/ShadowRoot.php b/vendor/php-webdriver/webdriver/lib/Remote/ShadowRoot.php new file mode 100644 index 0000000..5d419b1 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/ShadowRoot.php @@ -0,0 +1,98 @@ +executor = $executor; + $this->id = $id; + } + + /** + * @return self + */ + public static function createFromResponse(RemoteExecuteMethod $executor, array $response) + { + if (empty($response[self::SHADOW_ROOT_IDENTIFIER])) { + throw new UnknownErrorException('Shadow root is missing in server response'); + } + + return new self($executor, $response[self::SHADOW_ROOT_IDENTIFIER]); + } + + /** + * @return RemoteWebElement + */ + public function findElement(WebDriverBy $locator) + { + $params = JsonWireCompat::getUsing($locator, true); + $params[':id'] = $this->id; + + $rawElement = $this->executor->execute( + DriverCommand::FIND_ELEMENT_FROM_SHADOW_ROOT, + $params + ); + + return new RemoteWebElement($this->executor, JsonWireCompat::getElement($rawElement), true); + } + + /** + * @return WebDriverElement[] + */ + public function findElements(WebDriverBy $locator) + { + $params = JsonWireCompat::getUsing($locator, true); + $params[':id'] = $this->id; + + $rawElements = $this->executor->execute( + DriverCommand::FIND_ELEMENTS_FROM_SHADOW_ROOT, + $params + ); + + if (!is_array($rawElements)) { + throw UnexpectedResponseException::forError( + 'Server response to findElementsFromShadowRoot command is not an array' + ); + } + + $elements = []; + foreach ($rawElements as $rawElement) { + $elements[] = new RemoteWebElement($this->executor, JsonWireCompat::getElement($rawElement), true); + } + + return $elements; + } + + /** + * @return string + */ + public function getID() + { + return $this->id; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/UselessFileDetector.php b/vendor/php-webdriver/webdriver/lib/Remote/UselessFileDetector.php new file mode 100644 index 0000000..6bce0e0 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/UselessFileDetector.php @@ -0,0 +1,11 @@ +sessionID = $session_id; + $this->name = $name; + $this->parameters = $parameters; + } + + /** + * @return self + */ + public static function newSession(array $parameters) + { + // TODO: In 2.0 call empty constructor and assign properties directly. + return new self(null, DriverCommand::NEW_SESSION, $parameters); + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string|null Could be null for newSession command + */ + public function getSessionID() + { + return $this->sessionID; + } + + /** + * @return array + */ + public function getParameters() + { + return $this->parameters; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/WebDriverResponse.php b/vendor/php-webdriver/webdriver/lib/Remote/WebDriverResponse.php new file mode 100644 index 0000000..39b19bf --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Remote/WebDriverResponse.php @@ -0,0 +1,84 @@ +sessionID = $session_id; + } + + /** + * @return null|int + */ + public function getStatus() + { + return $this->status; + } + + /** + * @param int $status + * @return WebDriverResponse + */ + public function setStatus($status) + { + $this->status = $status; + + return $this; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @param mixed $value + * @return WebDriverResponse + */ + public function setValue($value) + { + $this->value = $value; + + return $this; + } + + /** + * @return null|string + */ + public function getSessionID() + { + return $this->sessionID; + } + + /** + * @param mixed $session_id + * @return WebDriverResponse + */ + public function setSessionID($session_id) + { + $this->sessionID = $session_id; + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriver.php b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriver.php new file mode 100644 index 0000000..21e5a68 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriver.php @@ -0,0 +1,394 @@ +dispatcher = $dispatcher ?: new WebDriverDispatcher(); + if (!$this->dispatcher->getDefaultDriver()) { + $this->dispatcher->setDefaultDriver($this); + } + $this->driver = $driver; + } + + /** + * @return WebDriverDispatcher + */ + public function getDispatcher() + { + return $this->dispatcher; + } + + /** + * @return WebDriver + */ + public function getWebDriver() + { + return $this->driver; + } + + /** + * @param mixed $url + * @throws WebDriverException + * @return $this + */ + public function get($url) + { + $this->dispatch('beforeNavigateTo', $url, $this); + + try { + $this->driver->get($url); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + $this->dispatch('afterNavigateTo', $url, $this); + + return $this; + } + + /** + * @throws WebDriverException + * @return array + */ + public function findElements(WebDriverBy $by) + { + $this->dispatch('beforeFindBy', $by, null, $this); + $elements = []; + + try { + foreach ($this->driver->findElements($by) as $element) { + $elements[] = $this->newElement($element); + } + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + + $this->dispatch('afterFindBy', $by, null, $this); + + return $elements; + } + + /** + * @throws WebDriverException + * @return EventFiringWebElement + */ + public function findElement(WebDriverBy $by) + { + $this->dispatch('beforeFindBy', $by, null, $this); + + try { + $element = $this->newElement($this->driver->findElement($by)); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + + $this->dispatch('afterFindBy', $by, null, $this); + + return $element; + } + + /** + * @param string $script + * @throws WebDriverException + * @return mixed + */ + public function executeScript($script, array $arguments = []) + { + if (!$this->driver instanceof JavaScriptExecutor) { + throw new UnsupportedOperationException( + 'driver does not implement JavaScriptExecutor' + ); + } + + $this->dispatch('beforeScript', $script, $this); + + try { + $result = $this->driver->executeScript($script, $arguments); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + + $this->dispatch('afterScript', $script, $this); + + return $result; + } + + /** + * @param string $script + * @throws WebDriverException + * @return mixed + */ + public function executeAsyncScript($script, array $arguments = []) + { + if (!$this->driver instanceof JavaScriptExecutor) { + throw new UnsupportedOperationException( + 'driver does not implement JavaScriptExecutor' + ); + } + + $this->dispatch('beforeScript', $script, $this); + + try { + $result = $this->driver->executeAsyncScript($script, $arguments); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + $this->dispatch('afterScript', $script, $this); + + return $result; + } + + /** + * @throws WebDriverException + * @return $this + */ + public function close() + { + try { + $this->driver->close(); + + return $this; + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return string + */ + public function getCurrentURL() + { + try { + return $this->driver->getCurrentURL(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return string + */ + public function getPageSource() + { + try { + return $this->driver->getPageSource(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return string + */ + public function getTitle() + { + try { + return $this->driver->getTitle(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return string + */ + public function getWindowHandle() + { + try { + return $this->driver->getWindowHandle(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return array + */ + public function getWindowHandles() + { + try { + return $this->driver->getWindowHandles(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + */ + public function quit() + { + try { + $this->driver->quit(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @param null|string $save_as + * @throws WebDriverException + * @return string + */ + public function takeScreenshot($save_as = null) + { + try { + return $this->driver->takeScreenshot($save_as); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @param int $timeout_in_second + * @param int $interval_in_millisecond + * @throws WebDriverException + * @return WebDriverWait + */ + public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) + { + try { + return $this->driver->wait($timeout_in_second, $interval_in_millisecond); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return WebDriverOptions + */ + public function manage() + { + try { + return $this->driver->manage(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return EventFiringWebDriverNavigation + */ + public function navigate() + { + try { + return new EventFiringWebDriverNavigation( + $this->driver->navigate(), + $this->getDispatcher() + ); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return WebDriverTargetLocator + */ + public function switchTo() + { + try { + return $this->driver->switchTo(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return WebDriverTouchScreen + */ + public function getTouch() + { + try { + return $this->driver->getTouch(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + public function execute($name, $params) + { + try { + return $this->driver->execute($name, $params); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @return EventFiringWebElement + */ + protected function newElement(WebDriverElement $element) + { + return new EventFiringWebElement($element, $this->getDispatcher()); + } + + /** + * @param mixed $method + * @param mixed ...$arguments + */ + protected function dispatch($method, ...$arguments) + { + if (!$this->dispatcher) { + return; + } + + $this->dispatcher->dispatch($method, $arguments); + } + + protected function dispatchOnException(WebDriverException $exception) + { + $this->dispatch('onException', $exception, $this); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php new file mode 100644 index 0000000..27ea342 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php @@ -0,0 +1,135 @@ +navigator = $navigator; + $this->dispatcher = $dispatcher; + } + + /** + * @return WebDriverDispatcher + */ + public function getDispatcher() + { + return $this->dispatcher; + } + + /** + * @return WebDriverNavigationInterface + */ + public function getNavigator() + { + return $this->navigator; + } + + public function back() + { + $this->dispatch( + 'beforeNavigateBack', + $this->getDispatcher()->getDefaultDriver() + ); + + try { + $this->navigator->back(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + } + $this->dispatch( + 'afterNavigateBack', + $this->getDispatcher()->getDefaultDriver() + ); + + return $this; + } + + public function forward() + { + $this->dispatch( + 'beforeNavigateForward', + $this->getDispatcher()->getDefaultDriver() + ); + + try { + $this->navigator->forward(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + } + $this->dispatch( + 'afterNavigateForward', + $this->getDispatcher()->getDefaultDriver() + ); + + return $this; + } + + public function refresh() + { + try { + $this->navigator->refresh(); + + return $this; + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + public function to($url) + { + $this->dispatch( + 'beforeNavigateTo', + $url, + $this->getDispatcher()->getDefaultDriver() + ); + + try { + $this->navigator->to($url); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + + $this->dispatch( + 'afterNavigateTo', + $url, + $this->getDispatcher()->getDefaultDriver() + ); + + return $this; + } + + /** + * @param mixed $method + * @param mixed ...$arguments + */ + protected function dispatch($method, ...$arguments) + { + if (!$this->dispatcher) { + return; + } + + $this->dispatcher->dispatch($method, $arguments); + } + + protected function dispatchOnException(WebDriverException $exception) + { + $this->dispatch('onException', $exception); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebElement.php b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebElement.php new file mode 100644 index 0000000..6caa086 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebElement.php @@ -0,0 +1,413 @@ +element = $element; + $this->dispatcher = $dispatcher; + } + + /** + * @return WebDriverDispatcher + */ + public function getDispatcher() + { + return $this->dispatcher; + } + + /** + * @return WebDriverElement + */ + public function getElement() + { + return $this->element; + } + + /** + * @param mixed $value + * @throws WebDriverException + * @return $this + */ + public function sendKeys($value) + { + $this->dispatch('beforeChangeValueOf', $this); + + try { + $this->element->sendKeys($value); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + $this->dispatch('afterChangeValueOf', $this); + + return $this; + } + + /** + * @throws WebDriverException + * @return $this + */ + public function click() + { + $this->dispatch('beforeClickOn', $this); + + try { + $this->element->click(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + $this->dispatch('afterClickOn', $this); + + return $this; + } + + /** + * @throws WebDriverException + * @return EventFiringWebElement + */ + public function findElement(WebDriverBy $by) + { + $this->dispatch( + 'beforeFindBy', + $by, + $this, + $this->dispatcher->getDefaultDriver() + ); + + try { + $element = $this->newElement($this->element->findElement($by)); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + + $this->dispatch( + 'afterFindBy', + $by, + $this, + $this->dispatcher->getDefaultDriver() + ); + + return $element; + } + + /** + * @throws WebDriverException + * @return array + */ + public function findElements(WebDriverBy $by) + { + $this->dispatch( + 'beforeFindBy', + $by, + $this, + $this->dispatcher->getDefaultDriver() + ); + + try { + $elements = []; + foreach ($this->element->findElements($by) as $element) { + $elements[] = $this->newElement($element); + } + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + $this->dispatch( + 'afterFindBy', + $by, + $this, + $this->dispatcher->getDefaultDriver() + ); + + return $elements; + } + + /** + * @throws WebDriverException + * @return $this + */ + public function clear() + { + try { + $this->element->clear(); + + return $this; + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @param string $attribute_name + * @throws WebDriverException + * @return string + */ + public function getAttribute($attribute_name) + { + try { + return $this->element->getAttribute($attribute_name); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @param string $css_property_name + * @throws WebDriverException + * @return string + */ + public function getCSSValue($css_property_name) + { + try { + return $this->element->getCSSValue($css_property_name); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return WebDriverPoint + */ + public function getLocation() + { + try { + return $this->element->getLocation(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return WebDriverPoint + */ + public function getLocationOnScreenOnceScrolledIntoView() + { + try { + return $this->element->getLocationOnScreenOnceScrolledIntoView(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @return WebDriverCoordinates + */ + public function getCoordinates() + { + try { + return $this->element->getCoordinates(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return WebDriverDimension + */ + public function getSize() + { + try { + return $this->element->getSize(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return string + */ + public function getTagName() + { + try { + return $this->element->getTagName(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return string + */ + public function getText() + { + try { + return $this->element->getText(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return bool + */ + public function isDisplayed() + { + try { + return $this->element->isDisplayed(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return bool + */ + public function isEnabled() + { + try { + return $this->element->isEnabled(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return bool + */ + public function isSelected() + { + try { + return $this->element->isSelected(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return $this + */ + public function submit() + { + try { + $this->element->submit(); + + return $this; + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * @throws WebDriverException + * @return string + */ + public function getID() + { + try { + return $this->element->getID(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + /** + * Test if two element IDs refer to the same DOM element. + * + * @return bool + */ + public function equals(WebDriverElement $other) + { + try { + return $this->element->equals($other); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + public function takeElementScreenshot($save_as = null) + { + try { + return $this->element->takeElementScreenshot($save_as); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + public function getShadowRoot() + { + try { + return $this->element->getShadowRoot(); + } catch (WebDriverException $exception) { + $this->dispatchOnException($exception); + throw $exception; + } + } + + protected function dispatchOnException(WebDriverException $exception) + { + $this->dispatch( + 'onException', + $exception, + $this->dispatcher->getDefaultDriver() + ); + } + + /** + * @param mixed $method + * @param mixed ...$arguments + */ + protected function dispatch($method, ...$arguments) + { + if (!$this->dispatcher) { + return; + } + + $this->dispatcher->dispatch($method, $arguments); + } + + /** + * @return static + */ + protected function newElement(WebDriverElement $element) + { + return new static($element, $this->getDispatcher()); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Support/IsElementDisplayedAtom.php b/vendor/php-webdriver/webdriver/lib/Support/IsElementDisplayedAtom.php new file mode 100644 index 0000000..d95e4f0 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Support/IsElementDisplayedAtom.php @@ -0,0 +1,71 @@ +driver = $driver; + } + + public static function match($browserName) + { + return !in_array($browserName, self::BROWSERS_WITH_ENDPOINT_SUPPORT, true); + } + + public function execute($params) + { + $element = new RemoteWebElement( + new RemoteExecuteMethod($this->driver), + $params[':id'], + $this->driver->isW3cCompliant() + ); + + return $this->executeAtom('isElementDisplayed', $element); + } + + protected function executeAtom($atomName, ...$params) + { + return $this->driver->executeScript( + sprintf('%s; return (%s).apply(null, arguments);', $this->loadAtomScript($atomName), $atomName), + $params + ); + } + + private function loadAtomScript($atomName) + { + return file_get_contents(__DIR__ . '/../scripts/' . $atomName . '.js'); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Support/ScreenshotHelper.php b/vendor/php-webdriver/webdriver/lib/Support/ScreenshotHelper.php new file mode 100644 index 0000000..956f561 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Support/ScreenshotHelper.php @@ -0,0 +1,81 @@ +executor = $executor; + } + + /** + * @param string|null $saveAs + * @throws WebDriverException + * @return string + */ + public function takePageScreenshot($saveAs = null) + { + $commandToExecute = [DriverCommand::SCREENSHOT]; + + return $this->takeScreenshot($commandToExecute, $saveAs); + } + + public function takeElementScreenshot($elementId, $saveAs = null) + { + $commandToExecute = [DriverCommand::TAKE_ELEMENT_SCREENSHOT, [':id' => $elementId]]; + + return $this->takeScreenshot($commandToExecute, $saveAs); + } + + private function takeScreenshot(array $commandToExecute, $saveAs = null) + { + $response = $this->executor->execute(...$commandToExecute); + + if (!is_string($response)) { + throw UnexpectedResponseException::forError( + 'Error taking screenshot, no data received from the remote end' + ); + } + + $screenshot = base64_decode($response, true); + + if ($screenshot === false) { + throw UnexpectedResponseException::forError('Error decoding screenshot data'); + } + + if ($saveAs !== null) { + $this->saveScreenshotToPath($screenshot, $saveAs); + } + + return $screenshot; + } + + private function saveScreenshotToPath($screenshot, $path) + { + $this->createDirectoryIfNotExists(dirname($path)); + + file_put_contents($path, $screenshot); + } + + private function createDirectoryIfNotExists($directoryPath) + { + if (!file_exists($directoryPath)) { + if (!mkdir($directoryPath, 0777, true) && !is_dir($directoryPath)) { + throw IOException::forFileError('Directory cannot be not created', $directoryPath); + } + } + } +} diff --git a/vendor/php-webdriver/webdriver/lib/Support/XPathEscaper.php b/vendor/php-webdriver/webdriver/lib/Support/XPathEscaper.php new file mode 100644 index 0000000..eb85081 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/Support/XPathEscaper.php @@ -0,0 +1,32 @@ + `concat('foo', "'" ,'"bar')` + * + * @param string $xpathToEscape The xpath to be converted. + * @return string The escaped string. + */ + public static function escapeQuotes($xpathToEscape) + { + // Single quotes not present => we can quote in them + if (mb_strpos($xpathToEscape, "'") === false) { + return sprintf("'%s'", $xpathToEscape); + } + + // Double quotes not present => we can quote in them + if (mb_strpos($xpathToEscape, '"') === false) { + return sprintf('"%s"', $xpathToEscape); + } + + // Both single and double quotes are present + return sprintf( + "concat('%s')", + str_replace("'", "', \"'\" ,'", $xpathToEscape) + ); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriver.php b/vendor/php-webdriver/webdriver/lib/WebDriver.php new file mode 100755 index 0000000..52120a7 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriver.php @@ -0,0 +1,143 @@ +wait(20, 1000)->until( + * WebDriverExpectedCondition::titleIs('WebDriver Page') + * ); + * + * @param int $timeout_in_second + * @param int $interval_in_millisecond + * @return WebDriverWait + */ + public function wait( + $timeout_in_second = 30, + $interval_in_millisecond = 250 + ); + + /** + * An abstraction for managing stuff you would do in a browser menu. For + * example, adding and deleting cookies. + * + * @return WebDriverOptions + */ + public function manage(); + + /** + * An abstraction allowing the driver to access the browser's history and to + * navigate to a given URL. + * + * @return WebDriverNavigationInterface + * @see WebDriverNavigation + */ + public function navigate(); + + /** + * Switch to a different window or frame. + * + * @return WebDriverTargetLocator + * @see WebDriverTargetLocator + */ + public function switchTo(); + + // TODO: Add in next major release (BC) + ///** + // * @return WebDriverTouchScreen + // */ + //public function getTouch(); + + /** + * @param string $name + * @param array $params + * @return mixed + */ + public function execute($name, $params); + + // TODO: Add in next major release (BC) + ///** + // * Execute custom commands on remote end. + // * For example vendor-specific commands or other commands not implemented by php-webdriver. + // * + // * @see https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands + // * @param string $endpointUrl + // * @param string $method + // * @param array $params + // * @return mixed|null + // */ + //public function executeCustomCommand($endpointUrl, $method = 'GET', $params = []); +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverAction.php b/vendor/php-webdriver/webdriver/lib/WebDriverAction.php new file mode 100644 index 0000000..3b3a784 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverAction.php @@ -0,0 +1,11 @@ +executor = $executor; + } + + /** + * Accept alert + * + * @return WebDriverAlert The instance. + */ + public function accept() + { + $this->executor->execute(DriverCommand::ACCEPT_ALERT); + + return $this; + } + + /** + * Dismiss alert + * + * @return WebDriverAlert The instance. + */ + public function dismiss() + { + $this->executor->execute(DriverCommand::DISMISS_ALERT); + + return $this; + } + + /** + * Get alert text + * + * @return string + */ + public function getText() + { + return $this->executor->execute(DriverCommand::GET_ALERT_TEXT); + } + + /** + * Send keystrokes to javascript prompt() dialog + * + * @param string $value + * @return WebDriverAlert + */ + public function sendKeys($value) + { + $this->executor->execute( + DriverCommand::SET_ALERT_VALUE, + ['text' => $value] + ); + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverBy.php b/vendor/php-webdriver/webdriver/lib/WebDriverBy.php new file mode 100644 index 0000000..4bead67 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverBy.php @@ -0,0 +1,134 @@ +mechanism = $mechanism; + $this->value = $value; + } + + /** + * @return string + */ + public function getMechanism() + { + return $this->mechanism; + } + + /** + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Locates elements whose class name contains the search value; compound class + * names are not permitted. + * + * @param string $class_name + * @return static + */ + public static function className($class_name) + { + return new static('class name', $class_name); + } + + /** + * Locates elements matching a CSS selector. + * + * @param string $css_selector + * @return static + */ + public static function cssSelector($css_selector) + { + return new static('css selector', $css_selector); + } + + /** + * Locates elements whose ID attribute matches the search value. + * + * @param string $id + * @return static + */ + public static function id($id) + { + return new static('id', $id); + } + + /** + * Locates elements whose NAME attribute matches the search value. + * + * @param string $name + * @return static + */ + public static function name($name) + { + return new static('name', $name); + } + + /** + * Locates anchor elements whose visible text matches the search value. + * + * @param string $link_text + * @return static + */ + public static function linkText($link_text) + { + return new static('link text', $link_text); + } + + /** + * Locates anchor elements whose visible text partially matches the search + * value. + * + * @param string $partial_link_text + * @return static + */ + public static function partialLinkText($partial_link_text) + { + return new static('partial link text', $partial_link_text); + } + + /** + * Locates elements whose tag name matches the search value. + * + * @param string $tag_name + * @return static + */ + public static function tagName($tag_name) + { + return new static('tag name', $tag_name); + } + + /** + * Locates elements matching an XPath expression. + * + * @param string $xpath + * @return static + */ + public static function xpath($xpath) + { + return new static('xpath', $xpath); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverCapabilities.php b/vendor/php-webdriver/webdriver/lib/WebDriverCapabilities.php new file mode 100644 index 0000000..75cb99d --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverCapabilities.php @@ -0,0 +1,46 @@ +type = $element->getAttribute('type'); + if ($this->type !== 'checkbox') { + throw new InvalidElementStateException('The input must be of type "checkbox".'); + } + } + + public function isMultiple() + { + return true; + } + + public function deselectAll() + { + foreach ($this->getRelatedElements() as $checkbox) { + $this->deselectOption($checkbox); + } + } + + public function deselectByIndex($index) + { + $this->byIndex($index, false); + } + + public function deselectByValue($value) + { + $this->byValue($value, false); + } + + public function deselectByVisibleText($text) + { + $this->byVisibleText($text, false, false); + } + + public function deselectByVisiblePartialText($text) + { + $this->byVisibleText($text, true, false); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverCommandExecutor.php b/vendor/php-webdriver/webdriver/lib/WebDriverCommandExecutor.php new file mode 100644 index 0000000..7f6bb3e --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverCommandExecutor.php @@ -0,0 +1,17 @@ +width = $width; + $this->height = $height; + } + + /** + * Get the height. + * + * @return int The height. + */ + public function getHeight() + { + return (int) $this->height; + } + + /** + * Get the width. + * + * @return int The width. + */ + public function getWidth() + { + return (int) $this->width; + } + + /** + * Check whether the given dimension is the same as the instance. + * + * @param WebDriverDimension $dimension The dimension to be compared with. + * @return bool Whether the height and the width are the same as the instance. + */ + public function equals(self $dimension) + { + return $this->height === $dimension->getHeight() && $this->width === $dimension->getWidth(); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverDispatcher.php b/vendor/php-webdriver/webdriver/lib/WebDriverDispatcher.php new file mode 100644 index 0000000..fe1ecb0 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverDispatcher.php @@ -0,0 +1,75 @@ +driver = $driver; + + return $this; + } + + /** + * @return null|EventFiringWebDriver + */ + public function getDefaultDriver() + { + return $this->driver; + } + + /** + * @return $this + */ + public function register(WebDriverEventListener $listener) + { + $this->listeners[] = $listener; + + return $this; + } + + /** + * @return $this + */ + public function unregister(WebDriverEventListener $listener) + { + $key = array_search($listener, $this->listeners, true); + if ($key !== false) { + unset($this->listeners[$key]); + } + + return $this; + } + + /** + * @param mixed $method + * @param mixed $arguments + * @return $this + */ + public function dispatch($method, $arguments) + { + foreach ($this->listeners as $listener) { + call_user_func_array([$listener, $method], $arguments); + } + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverElement.php b/vendor/php-webdriver/webdriver/lib/WebDriverElement.php new file mode 100644 index 0000000..8ffa183 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverElement.php @@ -0,0 +1,154 @@ +apply = $apply; + } + + /** + * @return callable A callable function to be executed by WebDriverWait + */ + public function getApply() + { + return $this->apply; + } + + /** + * An expectation for checking the title of a page. + * + * @param string $title The expected title, which must be an exact match. + * @return static Condition returns whether current page title equals given string. + */ + public static function titleIs($title) + { + return new static( + function (WebDriver $driver) use ($title) { + return $title === $driver->getTitle(); + } + ); + } + + /** + * An expectation for checking substring of a page Title. + * + * @param string $title The expected substring of Title. + * @return static Condition returns whether current page title contains given string. + */ + public static function titleContains($title) + { + return new static( + function (WebDriver $driver) use ($title) { + return mb_strpos($driver->getTitle(), $title) !== false; + } + ); + } + + /** + * An expectation for checking current page title matches the given regular expression. + * + * @param string $titleRegexp The regular expression to test against. + * @return static Condition returns whether current page title matches the regular expression. + */ + public static function titleMatches($titleRegexp) + { + return new static( + function (WebDriver $driver) use ($titleRegexp) { + return (bool) preg_match($titleRegexp, $driver->getTitle()); + } + ); + } + + /** + * An expectation for checking the URL of a page. + * + * @param string $url The expected URL, which must be an exact match. + * @return static Condition returns whether current URL equals given one. + */ + public static function urlIs($url) + { + return new static( + function (WebDriver $driver) use ($url) { + return $url === $driver->getCurrentURL(); + } + ); + } + + /** + * An expectation for checking substring of the URL of a page. + * + * @param string $url The expected substring of the URL + * @return static Condition returns whether current URL contains given string. + */ + public static function urlContains($url) + { + return new static( + function (WebDriver $driver) use ($url) { + return mb_strpos($driver->getCurrentURL(), $url) !== false; + } + ); + } + + /** + * An expectation for checking current page URL matches the given regular expression. + * + * @param string $urlRegexp The regular expression to test against. + * @return static Condition returns whether current URL matches the regular expression. + */ + public static function urlMatches($urlRegexp) + { + return new static( + function (WebDriver $driver) use ($urlRegexp) { + return (bool) preg_match($urlRegexp, $driver->getCurrentURL()); + } + ); + } + + /** + * An expectation for checking that an element is present on the DOM of a page. + * This does not necessarily mean that the element is visible. + * + * @param WebDriverBy $by The locator used to find the element. + * @return static Condition returns the WebDriverElement which is located. + */ + public static function presenceOfElementLocated(WebDriverBy $by) + { + return new static( + function (WebDriver $driver) use ($by) { + try { + return $driver->findElement($by); + } catch (NoSuchElementException $e) { + return false; + } + } + ); + } + + /** + * An expectation for checking that there is at least one element present on a web page. + * + * @param WebDriverBy $by The locator used to find the element. + * @return static Condition return an array of WebDriverElement once they are located. + */ + public static function presenceOfAllElementsLocatedBy(WebDriverBy $by) + { + return new static( + function (WebDriver $driver) use ($by) { + $elements = $driver->findElements($by); + + return count($elements) > 0 ? $elements : null; + } + ); + } + + /** + * An expectation for checking that an element is present on the DOM of a page and visible. + * Visibility means that the element is not only displayed but also has a height and width that is greater than 0. + * + * @param WebDriverBy $by The locator used to find the element. + * @return static Condition returns the WebDriverElement which is located and visible. + */ + public static function visibilityOfElementLocated(WebDriverBy $by) + { + return new static( + function (WebDriver $driver) use ($by) { + try { + $element = $driver->findElement($by); + + return $element->isDisplayed() ? $element : null; + } catch (StaleElementReferenceException $e) { + return null; + } + } + ); + } + + /** + * An expectation for checking than at least one element in an array of elements is present on the + * DOM of a page and visible. + * Visibility means that the element is not only displayed but also has a height and width that is greater than 0. + * + * @param WebDriverBy $by The located used to find the element. + * @return static Condition returns the array of WebDriverElement that are located and visible. + */ + public static function visibilityOfAnyElementLocated(WebDriverBy $by) + { + return new static( + function (WebDriver $driver) use ($by) { + $elements = $driver->findElements($by); + $visibleElements = []; + + foreach ($elements as $element) { + try { + if ($element->isDisplayed()) { + $visibleElements[] = $element; + } + } catch (StaleElementReferenceException $e) { + } + } + + return count($visibleElements) > 0 ? $visibleElements : null; + } + ); + } + + /** + * An expectation for checking that an element, known to be present on the DOM of a page, is visible. + * Visibility means that the element is not only displayed but also has a height and width that is greater than 0. + * + * @param WebDriverElement $element The element to be checked. + * @return static Condition returns the same WebDriverElement once it is visible. + */ + public static function visibilityOf(WebDriverElement $element) + { + return new static( + function () use ($element) { + return $element->isDisplayed() ? $element : null; + } + ); + } + + /** + * An expectation for checking if the given text is present in the specified element. + * To check exact text match use elementTextIs() condition. + * + * @codeCoverageIgnore + * @deprecated Use WebDriverExpectedCondition::elementTextContains() instead + * @param WebDriverBy $by The locator used to find the element. + * @param string $text The text to be presented in the element. + * @return static Condition returns whether the text is present in the element. + */ + public static function textToBePresentInElement(WebDriverBy $by, $text) + { + return self::elementTextContains($by, $text); + } + + /** + * An expectation for checking if the given text is present in the specified element. + * To check exact text match use elementTextIs() condition. + * + * @param WebDriverBy $by The locator used to find the element. + * @param string $text The text to be presented in the element. + * @return static Condition returns whether the partial text is present in the element. + */ + public static function elementTextContains(WebDriverBy $by, $text) + { + return new static( + function (WebDriver $driver) use ($by, $text) { + try { + $element_text = $driver->findElement($by)->getText(); + + return mb_strpos($element_text, $text) !== false; + } catch (StaleElementReferenceException $e) { + return null; + } + } + ); + } + + /** + * An expectation for checking if the given text exactly equals the text in specified element. + * To check only partial substring of the text use elementTextContains() condition. + * + * @param WebDriverBy $by The locator used to find the element. + * @param string $text The expected text of the element. + * @return static Condition returns whether the element has text value equal to given one. + */ + public static function elementTextIs(WebDriverBy $by, $text) + { + return new static( + function (WebDriver $driver) use ($by, $text) { + try { + return $driver->findElement($by)->getText() == $text; + } catch (StaleElementReferenceException $e) { + return null; + } + } + ); + } + + /** + * An expectation for checking if the given regular expression matches the text in specified element. + * + * @param WebDriverBy $by The locator used to find the element. + * @param string $regexp The regular expression to test against. + * @return static Condition returns whether the element has text value equal to given one. + */ + public static function elementTextMatches(WebDriverBy $by, $regexp) + { + return new static( + function (WebDriver $driver) use ($by, $regexp) { + try { + return (bool) preg_match($regexp, $driver->findElement($by)->getText()); + } catch (StaleElementReferenceException $e) { + return null; + } + } + ); + } + + /** + * An expectation for checking if the given text is present in the specified elements value attribute. + * + * @codeCoverageIgnore + * @deprecated Use WebDriverExpectedCondition::elementValueContains() instead + * @param WebDriverBy $by The locator used to find the element. + * @param string $text The text to be presented in the element value. + * @return static Condition returns whether the text is present in value attribute. + */ + public static function textToBePresentInElementValue(WebDriverBy $by, $text) + { + return self::elementValueContains($by, $text); + } + + /** + * An expectation for checking if the given text is present in the specified elements value attribute. + * + * @param WebDriverBy $by The locator used to find the element. + * @param string $text The text to be presented in the element value. + * @return static Condition returns whether the text is present in value attribute. + */ + public static function elementValueContains(WebDriverBy $by, $text) + { + return new static( + function (WebDriver $driver) use ($by, $text) { + try { + $element_text = $driver->findElement($by)->getAttribute('value'); + + return mb_strpos($element_text, $text) !== false; + } catch (StaleElementReferenceException $e) { + return null; + } + } + ); + } + + /** + * Expectation for checking if iFrame exists. If iFrame exists switches driver's focus to the iFrame. + * + * @param string $frame_locator The locator used to find the iFrame + * expected to be either the id or name value of the i/frame + * @return static Condition returns object focused on new frame when frame is found, false otherwise. + */ + public static function frameToBeAvailableAndSwitchToIt($frame_locator) + { + return new static( + function (WebDriver $driver) use ($frame_locator) { + try { + return $driver->switchTo()->frame($frame_locator); + } catch (NoSuchFrameException $e) { + return false; + } + } + ); + } + + /** + * An expectation for checking that an element is either invisible or not present on the DOM. + * + * @param WebDriverBy $by The locator used to find the element. + * @return static Condition returns whether no visible element located. + */ + public static function invisibilityOfElementLocated(WebDriverBy $by) + { + return new static( + function (WebDriver $driver) use ($by) { + try { + return !$driver->findElement($by)->isDisplayed(); + } catch (NoSuchElementException|StaleElementReferenceException $e) { + return true; + } + } + ); + } + + /** + * An expectation for checking that an element with text is either invisible or not present on the DOM. + * + * @param WebDriverBy $by The locator used to find the element. + * @param string $text The text of the element. + * @return static Condition returns whether the text is found in the element located. + */ + public static function invisibilityOfElementWithText(WebDriverBy $by, $text) + { + return new static( + function (WebDriver $driver) use ($by, $text) { + try { + return !($driver->findElement($by)->getText() === $text); + } catch (NoSuchElementException|StaleElementReferenceException $e) { + return true; + } + } + ); + } + + /** + * An expectation for checking an element is visible and enabled such that you can click it. + * + * @param WebDriverBy $by The locator used to find the element + * @return static Condition return the WebDriverElement once it is located, visible and clickable. + */ + public static function elementToBeClickable(WebDriverBy $by) + { + $visibility_of_element_located = self::visibilityOfElementLocated($by); + + return new static( + function (WebDriver $driver) use ($visibility_of_element_located) { + $element = call_user_func( + $visibility_of_element_located->getApply(), + $driver + ); + + try { + if ($element !== null && $element->isEnabled()) { + return $element; + } + + return null; + } catch (StaleElementReferenceException $e) { + return null; + } + } + ); + } + + /** + * Wait until an element is no longer attached to the DOM. + * + * @param WebDriverElement $element The element to wait for. + * @return static Condition returns whether the element is still attached to the DOM. + */ + public static function stalenessOf(WebDriverElement $element) + { + return new static( + function () use ($element) { + try { + $element->isEnabled(); + + return false; + } catch (StaleElementReferenceException $e) { + return true; + } + } + ); + } + + /** + * Wrapper for a condition, which allows for elements to update by redrawing. + * + * This works around the problem of conditions which have two parts: find an element and then check for some + * condition on it. For these conditions it is possible that an element is located and then subsequently it is + * redrawn on the client. When this happens a StaleElementReferenceException is thrown when the second part of + * the condition is checked. + * + * @param WebDriverExpectedCondition $condition The condition wrapped. + * @return static Condition returns the return value of the getApply() of the given condition. + */ + public static function refreshed(self $condition) + { + return new static( + function (WebDriver $driver) use ($condition) { + try { + return call_user_func($condition->getApply(), $driver); + } catch (StaleElementReferenceException $e) { + return null; + } + } + ); + } + + /** + * An expectation for checking if the given element is selected. + * + * @param mixed $element_or_by Either the element or the locator. + * @return static Condition returns whether the element is selected. + */ + public static function elementToBeSelected($element_or_by) + { + return self::elementSelectionStateToBe( + $element_or_by, + true + ); + } + + /** + * An expectation for checking if the given element is selected. + * + * @param mixed $element_or_by Either the element or the locator. + * @param bool $selected The required state. + * @return static Condition returns whether the element is selected. + */ + public static function elementSelectionStateToBe($element_or_by, $selected) + { + if ($element_or_by instanceof WebDriverElement) { + return new static( + function () use ($element_or_by, $selected) { + return $element_or_by->isSelected() === $selected; + } + ); + } + + if ($element_or_by instanceof WebDriverBy) { + return new static( + function (WebDriver $driver) use ($element_or_by, $selected) { + try { + $element = $driver->findElement($element_or_by); + + return $element->isSelected() === $selected; + } catch (StaleElementReferenceException $e) { + return null; + } + } + ); + } + + throw LogicException::forError('Instance of either WebDriverElement or WebDriverBy must be given'); + } + + /** + * An expectation for whether an alert() box is present. + * + * @return static Condition returns WebDriverAlert if alert() is present, null otherwise. + */ + public static function alertIsPresent() + { + return new static( + function (WebDriver $driver) { + try { + // Unlike the Java code, we get a WebDriverAlert object regardless + // of whether there is an alert. Calling getText() will throw + // an exception if it is not really there. + $alert = $driver->switchTo()->alert(); + $alert->getText(); + + return $alert; + } catch (NoSuchAlertException $e) { + return null; + } + } + ); + } + + /** + * An expectation checking the number of opened windows. + * + * @param int $expectedNumberOfWindows + * @return static + */ + public static function numberOfWindowsToBe($expectedNumberOfWindows) + { + return new static( + function (WebDriver $driver) use ($expectedNumberOfWindows) { + return count($driver->getWindowHandles()) == $expectedNumberOfWindows; + } + ); + } + + /** + * An expectation with the logical opposite condition of the given condition. + * + * @param WebDriverExpectedCondition $condition The condition to be negated. + * @return mixed The negation of the result of the given condition. + */ + public static function not(self $condition) + { + return new static( + function (WebDriver $driver) use ($condition) { + $result = call_user_func($condition->getApply(), $driver); + + return !$result; + } + ); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverHasInputDevices.php b/vendor/php-webdriver/webdriver/lib/WebDriverHasInputDevices.php new file mode 100644 index 0000000..efe41ae --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverHasInputDevices.php @@ -0,0 +1,19 @@ +executor = $executor; + } + + public function back() + { + $this->executor->execute(DriverCommand::GO_BACK); + + return $this; + } + + public function forward() + { + $this->executor->execute(DriverCommand::GO_FORWARD); + + return $this; + } + + public function refresh() + { + $this->executor->execute(DriverCommand::REFRESH); + + return $this; + } + + public function to($url) + { + $params = ['url' => (string) $url]; + $this->executor->execute(DriverCommand::GET, $params); + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverNavigationInterface.php b/vendor/php-webdriver/webdriver/lib/WebDriverNavigationInterface.php new file mode 100644 index 0000000..6fcd06e --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverNavigationInterface.php @@ -0,0 +1,43 @@ +executor = $executor; + $this->isW3cCompliant = $isW3cCompliant; + } + + /** + * Add a specific cookie. + * + * @see Cookie for description of possible cookie properties + * @param Cookie|array $cookie Cookie object. May be also created from array for compatibility reasons. + * @return WebDriverOptions The current instance. + */ + public function addCookie($cookie) + { + if (is_array($cookie)) { // @todo @deprecated remove in 2.0 + $cookie = Cookie::createFromArray($cookie); + } + if (!$cookie instanceof Cookie) { + throw LogicException::forError('Cookie must be set from instance of Cookie class or from array.'); + } + + $this->executor->execute( + DriverCommand::ADD_COOKIE, + ['cookie' => $cookie->toArray()] + ); + + return $this; + } + + /** + * Delete all the cookies that are currently visible. + * + * @return WebDriverOptions The current instance. + */ + public function deleteAllCookies() + { + $this->executor->execute(DriverCommand::DELETE_ALL_COOKIES); + + return $this; + } + + /** + * Delete the cookie with the given name. + * + * @param string $name + * @return WebDriverOptions The current instance. + */ + public function deleteCookieNamed($name) + { + $this->executor->execute( + DriverCommand::DELETE_COOKIE, + [':name' => $name] + ); + + return $this; + } + + /** + * Get the cookie with a given name. + * + * @param string $name + * @throws NoSuchCookieException In W3C compliant mode if no cookie with the given name is present + * @return Cookie|null The cookie, or null in JsonWire mode if no cookie with the given name is present + */ + public function getCookieNamed($name) + { + if ($this->isW3cCompliant) { + $cookieArray = $this->executor->execute( + DriverCommand::GET_NAMED_COOKIE, + [':name' => $name] + ); + + if (!is_array($cookieArray)) { // Microsoft Edge returns null even in W3C mode => emulate proper behavior + throw new NoSuchCookieException('no such cookie'); + } + + return Cookie::createFromArray($cookieArray); + } + + $cookies = $this->getCookies(); + foreach ($cookies as $cookie) { + if ($cookie['name'] === $name) { + return $cookie; + } + } + + return null; + } + + /** + * Get all the cookies for the current domain. + * + * @return Cookie[] The array of cookies presented. + */ + public function getCookies() + { + $cookieArrays = $this->executor->execute(DriverCommand::GET_ALL_COOKIES); + if (!is_array($cookieArrays)) { // Microsoft Edge returns null if there are no cookies... + return []; + } + + $cookies = []; + foreach ($cookieArrays as $cookieArray) { + $cookies[] = Cookie::createFromArray($cookieArray); + } + + return $cookies; + } + + /** + * Return the interface for managing driver timeouts. + * + * @return WebDriverTimeouts + */ + public function timeouts() + { + return new WebDriverTimeouts($this->executor, $this->isW3cCompliant); + } + + /** + * An abstraction allowing the driver to manipulate the browser's window + * + * @return WebDriverWindow + * @see WebDriverWindow + */ + public function window() + { + return new WebDriverWindow($this->executor, $this->isW3cCompliant); + } + + /** + * Get the log for a given log type. Log buffer is reset after each request. + * + * @param string $log_type The log type. + * @return array The list of log entries. + * @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#log-type + */ + public function getLog($log_type) + { + return $this->executor->execute( + DriverCommand::GET_LOG, + ['type' => $log_type] + ); + } + + /** + * Get available log types. + * + * @return array The list of available log types. + * @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#log-type + */ + public function getAvailableLogTypes() + { + return $this->executor->execute(DriverCommand::GET_AVAILABLE_LOG_TYPES); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverPlatform.php b/vendor/php-webdriver/webdriver/lib/WebDriverPlatform.php new file mode 100644 index 0000000..a589f30 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverPlatform.php @@ -0,0 +1,25 @@ +x = $x; + $this->y = $y; + } + + /** + * Get the x-coordinate. + * + * @return int The x-coordinate of the point. + */ + public function getX() + { + return (int) $this->x; + } + + /** + * Get the y-coordinate. + * + * @return int The y-coordinate of the point. + */ + public function getY() + { + return (int) $this->y; + } + + /** + * Set the point to a new position. + * + * @param int $new_x + * @param int $new_y + * @return WebDriverPoint The same instance with updated coordinates. + */ + public function move($new_x, $new_y) + { + $this->x = $new_x; + $this->y = $new_y; + + return $this; + } + + /** + * Move the current by offsets. + * + * @param int $x_offset + * @param int $y_offset + * @return WebDriverPoint The same instance with updated coordinates. + */ + public function moveBy($x_offset, $y_offset) + { + $this->x += $x_offset; + $this->y += $y_offset; + + return $this; + } + + /** + * Check whether the given point is the same as the instance. + * + * @param WebDriverPoint $point The point to be compared with. + * @return bool Whether the x and y coordinates are the same as the instance. + */ + public function equals(self $point) + { + return $this->x === $point->getX() && + $this->y === $point->getY(); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverRadios.php b/vendor/php-webdriver/webdriver/lib/WebDriverRadios.php new file mode 100644 index 0000000..aeaaaec --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverRadios.php @@ -0,0 +1,52 @@ +type = $element->getAttribute('type'); + if ($this->type !== 'radio') { + throw new InvalidElementStateException('The input must be of type "radio".'); + } + } + + public function isMultiple() + { + return false; + } + + public function deselectAll() + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } + + public function deselectByIndex($index) + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } + + public function deselectByValue($value) + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } + + public function deselectByVisibleText($text) + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } + + public function deselectByVisiblePartialText($text) + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverSearchContext.php b/vendor/php-webdriver/webdriver/lib/WebDriverSearchContext.php new file mode 100644 index 0000000..5fb1daa --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverSearchContext.php @@ -0,0 +1,28 @@ +` tag, providing helper methods to select and deselect options. + */ +class WebDriverSelect implements WebDriverSelectInterface +{ + /** @var WebDriverElement */ + private $element; + /** @var bool */ + private $isMulti; + + public function __construct(WebDriverElement $element) + { + $tag_name = $element->getTagName(); + + if ($tag_name !== 'select') { + throw new UnexpectedTagNameException('select', $tag_name); + } + $this->element = $element; + $value = $element->getAttribute('multiple'); + + /** + * There is a bug in safari webdriver that returns 'multiple' instead of 'true' which does not match the spec. + * Apple Feedback #FB12760673 + * + * @see https://www.w3.org/TR/webdriver2/#get-element-attribute + */ + $this->isMulti = $value === 'true' || $value === 'multiple'; + } + + public function isMultiple() + { + return $this->isMulti; + } + + public function getOptions() + { + return $this->element->findElements(WebDriverBy::tagName('option')); + } + + public function getAllSelectedOptions() + { + $selected_options = []; + foreach ($this->getOptions() as $option) { + if ($option->isSelected()) { + $selected_options[] = $option; + + if (!$this->isMultiple()) { + return $selected_options; + } + } + } + + return $selected_options; + } + + public function getFirstSelectedOption() + { + foreach ($this->getOptions() as $option) { + if ($option->isSelected()) { + return $option; + } + } + + throw new NoSuchElementException('No options are selected'); + } + + public function selectByIndex($index) + { + foreach ($this->getOptions() as $option) { + if ($option->getAttribute('index') === (string) $index) { + $this->selectOption($option); + + return; + } + } + + throw new NoSuchElementException(sprintf('Cannot locate option with index: %d', $index)); + } + + public function selectByValue($value) + { + $matched = false; + $xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']'; + $options = $this->element->findElements(WebDriverBy::xpath($xpath)); + + foreach ($options as $option) { + $this->selectOption($option); + if (!$this->isMultiple()) { + return; + } + $matched = true; + } + + if (!$matched) { + throw new NoSuchElementException( + sprintf('Cannot locate option with value: %s', $value) + ); + } + } + + public function selectByVisibleText($text) + { + $matched = false; + $xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']'; + $options = $this->element->findElements(WebDriverBy::xpath($xpath)); + + foreach ($options as $option) { + $this->selectOption($option); + if (!$this->isMultiple()) { + return; + } + $matched = true; + } + + // Since the mechanism of getting the text in xpath is not the same as + // webdriver, use the expensive getText() to check if nothing is matched. + if (!$matched) { + foreach ($this->getOptions() as $option) { + if ($option->getText() === $text) { + $this->selectOption($option); + if (!$this->isMultiple()) { + return; + } + $matched = true; + } + } + } + + if (!$matched) { + throw new NoSuchElementException( + sprintf('Cannot locate option with text: %s', $text) + ); + } + } + + public function selectByVisiblePartialText($text) + { + $matched = false; + $xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]'; + $options = $this->element->findElements(WebDriverBy::xpath($xpath)); + + foreach ($options as $option) { + $this->selectOption($option); + if (!$this->isMultiple()) { + return; + } + $matched = true; + } + + if (!$matched) { + throw new NoSuchElementException( + sprintf('Cannot locate option with text: %s', $text) + ); + } + } + + public function deselectAll() + { + if (!$this->isMultiple()) { + throw new UnsupportedOperationException('You may only deselect all options of a multi-select'); + } + + foreach ($this->getOptions() as $option) { + $this->deselectOption($option); + } + } + + public function deselectByIndex($index) + { + if (!$this->isMultiple()) { + throw new UnsupportedOperationException('You may only deselect options of a multi-select'); + } + + foreach ($this->getOptions() as $option) { + if ($option->getAttribute('index') === (string) $index) { + $this->deselectOption($option); + + return; + } + } + } + + public function deselectByValue($value) + { + if (!$this->isMultiple()) { + throw new UnsupportedOperationException('You may only deselect options of a multi-select'); + } + + $xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']'; + $options = $this->element->findElements(WebDriverBy::xpath($xpath)); + foreach ($options as $option) { + $this->deselectOption($option); + } + } + + public function deselectByVisibleText($text) + { + if (!$this->isMultiple()) { + throw new UnsupportedOperationException('You may only deselect options of a multi-select'); + } + + $xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']'; + $options = $this->element->findElements(WebDriverBy::xpath($xpath)); + foreach ($options as $option) { + $this->deselectOption($option); + } + } + + public function deselectByVisiblePartialText($text) + { + if (!$this->isMultiple()) { + throw new UnsupportedOperationException('You may only deselect options of a multi-select'); + } + + $xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]'; + $options = $this->element->findElements(WebDriverBy::xpath($xpath)); + foreach ($options as $option) { + $this->deselectOption($option); + } + } + + /** + * Mark option selected + */ + protected function selectOption(WebDriverElement $option) + { + if (!$option->isSelected()) { + $option->click(); + } + } + + /** + * Mark option not selected + */ + protected function deselectOption(WebDriverElement $option) + { + if ($option->isSelected()) { + $option->click(); + } + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverSelectInterface.php b/vendor/php-webdriver/webdriver/lib/WebDriverSelectInterface.php new file mode 100644 index 0000000..030a783 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverSelectInterface.php @@ -0,0 +1,128 @@ +Bar` + * + * @param string $value The value to match against. + * + * @throws NoSuchElementException + */ + public function selectByValue($value); + + /** + * Select all options that display text matching the argument. That is, when given "Bar" this would + * select an option like: + * + * `` + * + * @param string $text The visible text to match against. + * + * @throws NoSuchElementException + */ + public function selectByVisibleText($text); + + /** + * Select all options that display text partially matching the argument. That is, when given "Bar" this would + * select an option like: + * + * `` + * + * @param string $text The visible text to match against. + * + * @throws NoSuchElementException + */ + public function selectByVisiblePartialText($text); + + /** + * Deselect all options in multiple select tag. + * + * @throws UnsupportedOperationException If the SELECT does not support multiple selections + */ + public function deselectAll(); + + /** + * Deselect the option at the given index. + * + * @param int $index The index of the option. (0-based) + * @throws UnsupportedOperationException If the SELECT does not support multiple selections + */ + public function deselectByIndex($index); + + /** + * Deselect all options that have value attribute matching the argument. That is, when given "foo" this would + * deselect an option like: + * + * `` + * + * @param string $value The value to match against. + * @throws UnsupportedOperationException If the SELECT does not support multiple selections + */ + public function deselectByValue($value); + + /** + * Deselect all options that display text matching the argument. That is, when given "Bar" this would + * deselect an option like: + * + * `` + * + * @param string $text The visible text to match against. + * @throws UnsupportedOperationException If the SELECT does not support multiple selections + */ + public function deselectByVisibleText($text); + + /** + * Deselect all options that display text matching the argument. That is, when given "Bar" this would + * deselect an option like: + * + * `` + * + * @param string $text The visible text to match against. + * @throws UnsupportedOperationException If the SELECT does not support multiple selections + */ + public function deselectByVisiblePartialText($text); +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverTargetLocator.php b/vendor/php-webdriver/webdriver/lib/WebDriverTargetLocator.php new file mode 100644 index 0000000..8787f66 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverTargetLocator.php @@ -0,0 +1,69 @@ +executor = $executor; + $this->isW3cCompliant = $isW3cCompliant; + } + + /** + * Specify the amount of time the driver should wait when searching for an element if it is not immediately present. + * + * @param int $seconds Wait time in second. + * @return WebDriverTimeouts The current instance. + */ + public function implicitlyWait($seconds) + { + if ($this->isW3cCompliant) { + $this->executor->execute( + DriverCommand::IMPLICITLY_WAIT, + ['implicit' => $seconds * 1000] + ); + + return $this; + } + + $this->executor->execute( + DriverCommand::IMPLICITLY_WAIT, + ['ms' => $seconds * 1000] + ); + + return $this; + } + + /** + * Set the amount of time to wait for an asynchronous script to finish execution before throwing an error. + * + * @param int $seconds Wait time in second. + * @return WebDriverTimeouts The current instance. + */ + public function setScriptTimeout($seconds) + { + if ($this->isW3cCompliant) { + $this->executor->execute( + DriverCommand::SET_SCRIPT_TIMEOUT, + ['script' => $seconds * 1000] + ); + + return $this; + } + + $this->executor->execute( + DriverCommand::SET_SCRIPT_TIMEOUT, + ['ms' => $seconds * 1000] + ); + + return $this; + } + + /** + * Set the amount of time to wait for a page load to complete before throwing an error. + * + * @param int $seconds Wait time in second. + * @return WebDriverTimeouts The current instance. + */ + public function pageLoadTimeout($seconds) + { + if ($this->isW3cCompliant) { + $this->executor->execute( + DriverCommand::SET_SCRIPT_TIMEOUT, + ['pageLoad' => $seconds * 1000] + ); + + return $this; + } + + $this->executor->execute(DriverCommand::SET_TIMEOUT, [ + 'type' => 'page load', + 'ms' => $seconds * 1000, + ]); + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverUpAction.php b/vendor/php-webdriver/webdriver/lib/WebDriverUpAction.php new file mode 100644 index 0000000..3b045df --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverUpAction.php @@ -0,0 +1,28 @@ +x = $x; + $this->y = $y; + parent::__construct($touch_screen); + } + + public function perform() + { + $this->touchScreen->up($this->x, $this->y); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverWait.php b/vendor/php-webdriver/webdriver/lib/WebDriverWait.php new file mode 100644 index 0000000..d2176b9 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverWait.php @@ -0,0 +1,73 @@ +driver = $driver; + $this->timeout = $timeout_in_second ?? 30; + $this->interval = $interval_in_millisecond ?: 250; + } + + /** + * Calls the function provided with the driver as an argument until the return value is not falsey. + * + * @param callable|WebDriverExpectedCondition $func_or_ec + * @param string $message + * + * @throws \Exception + * @throws NoSuchElementException + * @throws TimeoutException + * @return mixed The return value of $func_or_ec + */ + public function until($func_or_ec, $message = '') + { + $end = microtime(true) + $this->timeout; + $last_exception = null; + + while ($end > microtime(true)) { + try { + if ($func_or_ec instanceof WebDriverExpectedCondition) { + $ret_val = call_user_func($func_or_ec->getApply(), $this->driver); + } else { + $ret_val = call_user_func($func_or_ec, $this->driver); + } + if ($ret_val) { + return $ret_val; + } + } catch (NoSuchElementException $e) { + $last_exception = $e; + } + usleep($this->interval * 1000); + } + + if ($last_exception) { + throw $last_exception; + } + + throw new TimeoutException($message); + } +} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverWindow.php b/vendor/php-webdriver/webdriver/lib/WebDriverWindow.php new file mode 100644 index 0000000..2a69fe2 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/WebDriverWindow.php @@ -0,0 +1,188 @@ +executor = $executor; + $this->isW3cCompliant = $isW3cCompliant; + } + + /** + * Get the position of the current window, relative to the upper left corner + * of the screen. + * + * @return WebDriverPoint The current window position. + */ + public function getPosition() + { + $position = $this->executor->execute( + DriverCommand::GET_WINDOW_POSITION, + [':windowHandle' => 'current'] + ); + + return new WebDriverPoint( + $position['x'], + $position['y'] + ); + } + + /** + * Get the size of the current window. This will return the outer window + * dimension, not just the view port. + * + * @return WebDriverDimension The current window size. + */ + public function getSize() + { + $size = $this->executor->execute( + DriverCommand::GET_WINDOW_SIZE, + [':windowHandle' => 'current'] + ); + + return new WebDriverDimension( + $size['width'], + $size['height'] + ); + } + + /** + * Minimizes the current window if it is not already minimized. + * + * @return WebDriverWindow The instance. + */ + public function minimize() + { + if (!$this->isW3cCompliant) { + throw new UnsupportedOperationException('Minimize window is only supported in W3C mode'); + } + + $this->executor->execute(DriverCommand::MINIMIZE_WINDOW, []); + + return $this; + } + + /** + * Maximizes the current window if it is not already maximized + * + * @return WebDriverWindow The instance. + */ + public function maximize() + { + if ($this->isW3cCompliant) { + $this->executor->execute(DriverCommand::MAXIMIZE_WINDOW, []); + } else { + $this->executor->execute( + DriverCommand::MAXIMIZE_WINDOW, + [':windowHandle' => 'current'] + ); + } + + return $this; + } + + /** + * Makes the current window full screen. + * + * @return WebDriverWindow The instance. + */ + public function fullscreen() + { + if (!$this->isW3cCompliant) { + throw new UnsupportedOperationException('The Fullscreen window command is only supported in W3C mode'); + } + + $this->executor->execute(DriverCommand::FULLSCREEN_WINDOW, []); + + return $this; + } + + /** + * Set the size of the current window. This will change the outer window + * dimension, not just the view port. + * + * @return WebDriverWindow The instance. + */ + public function setSize(WebDriverDimension $size) + { + $params = [ + 'width' => $size->getWidth(), + 'height' => $size->getHeight(), + ':windowHandle' => 'current', + ]; + $this->executor->execute(DriverCommand::SET_WINDOW_SIZE, $params); + + return $this; + } + + /** + * Set the position of the current window. This is relative to the upper left + * corner of the screen. + * + * @return WebDriverWindow The instance. + */ + public function setPosition(WebDriverPoint $position) + { + $params = [ + 'x' => $position->getX(), + 'y' => $position->getY(), + ':windowHandle' => 'current', + ]; + $this->executor->execute(DriverCommand::SET_WINDOW_POSITION, $params); + + return $this; + } + + /** + * Get the current browser orientation. + * + * @return string Either LANDSCAPE|PORTRAIT + */ + public function getScreenOrientation() + { + return $this->executor->execute(DriverCommand::GET_SCREEN_ORIENTATION); + } + + /** + * Set the browser orientation. The orientation should either + * LANDSCAPE|PORTRAIT + * + * @param string $orientation + * @throws IndexOutOfBoundsException + * @return WebDriverWindow The instance. + */ + public function setScreenOrientation($orientation) + { + $orientation = mb_strtoupper($orientation); + if (!in_array($orientation, ['PORTRAIT', 'LANDSCAPE'], true)) { + throw LogicException::forError('Orientation must be either PORTRAIT, or LANDSCAPE'); + } + + $this->executor->execute( + DriverCommand::SET_SCREEN_ORIENTATION, + ['orientation' => $orientation] + ); + + return $this; + } +} diff --git a/vendor/php-webdriver/webdriver/lib/scripts/isElementDisplayed.js b/vendor/php-webdriver/webdriver/lib/scripts/isElementDisplayed.js new file mode 100644 index 0000000..f24bfa5 --- /dev/null +++ b/vendor/php-webdriver/webdriver/lib/scripts/isElementDisplayed.js @@ -0,0 +1,219 @@ +/* + * Imported from WebdriverIO project. + * https://github.com/webdriverio/webdriverio/blob/main/packages/webdriverio/src/scripts/isElementDisplayed.ts + * + * Copyright (C) 2017 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * check if element is visible + * @param {HTMLElement} elem element to check + * @return {Boolean} true if element is within viewport + */ +function isElementDisplayed(element) { + function nodeIsElement(node) { + if (!node) { + return false; + } + + switch (node.nodeType) { + case Node.ELEMENT_NODE: + case Node.DOCUMENT_NODE: + case Node.DOCUMENT_FRAGMENT_NODE: + return true; + default: + return false; + } + } + function parentElementForElement(element) { + if (!element) { + return null; + } + return enclosingNodeOrSelfMatchingPredicate(element.parentNode, nodeIsElement); + } + function enclosingNodeOrSelfMatchingPredicate(targetNode, predicate) { + for (let node = targetNode; node && node !== targetNode.ownerDocument; node = node.parentNode) + if (predicate(node)) { + return node; + } + return null; + } + function enclosingElementOrSelfMatchingPredicate(targetElement, predicate) { + for (let element = targetElement; element && element !== targetElement.ownerDocument; element = parentElementForElement(element)) + if (predicate(element)) { + return element; + } + return null; + } + function cascadedStylePropertyForElement(element, property) { + if (!element || !property) { + return null; + } + // if document-fragment, skip it and use element.host instead. This happens + // when the element is inside a shadow root. + // window.getComputedStyle errors on document-fragment. + if (element instanceof ShadowRoot) { + element = element.host; + } + let computedStyle = window.getComputedStyle(element); + let computedStyleProperty = computedStyle.getPropertyValue(property); + if (computedStyleProperty && computedStyleProperty !== 'inherit') { + return computedStyleProperty; + } + // Ideally getPropertyValue would return the 'used' or 'actual' value, but + // it doesn't for legacy reasons. So we need to do our own poor man's cascade. + // Fall back to the first non-'inherit' value found in an ancestor. + // In any case, getPropertyValue will not return 'initial'. + // FIXME: will this incorrectly inherit non-inheritable CSS properties? + // I think all important non-inheritable properties (width, height, etc.) + // for our purposes here are specially resolved, so this may not be an issue. + // Specification is here: https://drafts.csswg.org/cssom/#resolved-values + let parentElement = parentElementForElement(element); + return cascadedStylePropertyForElement(parentElement, property); + } + function elementSubtreeHasNonZeroDimensions(element) { + let boundingBox = element.getBoundingClientRect(); + if (boundingBox.width > 0 && boundingBox.height > 0) { + return true; + } + // Paths can have a zero width or height. Treat them as shown if the stroke width is positive. + if (element.tagName.toUpperCase() === 'PATH' && boundingBox.width + boundingBox.height > 0) { + let strokeWidth = cascadedStylePropertyForElement(element, 'stroke-width'); + return !!strokeWidth && (parseInt(strokeWidth, 10) > 0); + } + let cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow'); + if (cascadedOverflow === 'hidden') { + return false; + } + // If the container's overflow is not hidden and it has zero size, consider the + // container to have non-zero dimensions if a child node has non-zero dimensions. + return Array.from(element.childNodes).some((childNode) => { + if (childNode.nodeType === Node.TEXT_NODE) { + return true; + } + if (nodeIsElement(childNode)) { + return elementSubtreeHasNonZeroDimensions(childNode); + } + return false; + }); + } + function elementOverflowsContainer(element) { + let cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow'); + if (cascadedOverflow !== 'hidden') { + return false; + } + // FIXME: this needs to take into account the scroll position of the element, + // the display modes of it and its ancestors, and the container it overflows. + // See Selenium's bot.dom.getOverflowState atom for an exhaustive list of edge cases. + return true; + } + function isElementSubtreeHiddenByOverflow(element) { + if (!element) { + return false; + } + if (!elementOverflowsContainer(element)) { + return false; + } + if (!element.childNodes.length) { + return false; + } + // This element's subtree is hidden by overflow if all child subtrees are as well. + return Array.from(element.childNodes).every((childNode) => { + // Returns true if the child node is overflowed or otherwise hidden. + // Base case: not an element, has zero size, scrolled out, or doesn't overflow container. + // Visibility of text nodes is controlled by parent + if (childNode.nodeType === Node.TEXT_NODE) { + return false; + } + if (!nodeIsElement(childNode)) { + return true; + } + if (!elementSubtreeHasNonZeroDimensions(childNode)) { + return true; + } + // Recurse. + return isElementSubtreeHiddenByOverflow(childNode); + }); + } + // walk up the tree testing for a shadow root + function isElementInsideShadowRoot(element) { + if (!element) { + return false; + } + if (element.parentNode && element.parentNode.host) { + return true; + } + return isElementInsideShadowRoot(element.parentNode); + } + // This is a partial reimplementation of Selenium's "element is displayed" algorithm. + // When the W3C specification's algorithm stabilizes, we should implement that. + // If this command is misdirected to the wrong document (and is NOT inside a shadow root), treat it as not shown. + if (!isElementInsideShadowRoot(element) && !document.contains(element)) { + return false; + } + // Special cases for specific tag names. + switch (element.tagName.toUpperCase()) { + case 'BODY': + return true; + case 'SCRIPT': + case 'NOSCRIPT': + return false; + case 'OPTGROUP': + case 'OPTION': { + // Option/optgroup are considered shown if the containing is considered not shown. + if (element.type === 'hidden') { + return false; + } + break; + // case 'MAP': + // FIXME: Selenium has special handling for elements. We don't do anything now. + default: + break; + } + if (cascadedStylePropertyForElement(element, 'visibility') !== 'visible') { + return false; + } + let hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element, (e) => { + return Number(cascadedStylePropertyForElement(e, 'opacity')) === 0; + }); + let hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element, (e) => { + return cascadedStylePropertyForElement(e, 'display') === 'none'; + }); + if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone) { + return false; + } + if (!elementSubtreeHasNonZeroDimensions(element)) { + return false; + } + if (isElementSubtreeHiddenByOverflow(element)) { + return false; + } + return true; +} + diff --git a/vendor/symfony/polyfill-mbstring/LICENSE b/vendor/symfony/polyfill-mbstring/LICENSE new file mode 100644 index 0000000..6e3afce --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-mbstring/Mbstring.php b/vendor/symfony/polyfill-mbstring/Mbstring.php new file mode 100644 index 0000000..31e36a3 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Mbstring.php @@ -0,0 +1,1045 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Mbstring; + +/** + * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. + * + * Implemented: + * - mb_chr - Returns a specific character from its Unicode code point + * - mb_convert_encoding - Convert character encoding + * - mb_convert_variables - Convert character code in variable(s) + * - mb_decode_mimeheader - Decode string in MIME header field + * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED + * - mb_decode_numericentity - Decode HTML numeric string reference to character + * - mb_encode_numericentity - Encode character to HTML numeric string reference + * - mb_convert_case - Perform case folding on a string + * - mb_detect_encoding - Detect character encoding + * - mb_get_info - Get internal settings of mbstring + * - mb_http_input - Detect HTTP input character encoding + * - mb_http_output - Set/Get HTTP output character encoding + * - mb_internal_encoding - Set/Get internal character encoding + * - mb_list_encodings - Returns an array of all supported encodings + * - mb_ord - Returns the Unicode code point of a character + * - mb_output_handler - Callback function converts character encoding in output buffer + * - mb_scrub - Replaces ill-formed byte sequences with substitute characters + * - mb_strlen - Get string length + * - mb_strpos - Find position of first occurrence of string in a string + * - mb_strrpos - Find position of last occurrence of a string in a string + * - mb_str_split - Convert a string to an array + * - mb_strtolower - Make a string lowercase + * - mb_strtoupper - Make a string uppercase + * - mb_substitute_character - Set/Get substitution character + * - mb_substr - Get part of string + * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive + * - mb_stristr - Finds first occurrence of a string within another, case insensitive + * - mb_strrchr - Finds the last occurrence of a character in a string within another + * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive + * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive + * - mb_strstr - Finds first occurrence of a string within another + * - mb_strwidth - Return width of string + * - mb_substr_count - Count the number of substring occurrences + * - mb_ucfirst - Make a string's first character uppercase + * - mb_lcfirst - Make a string's first character lowercase + * - mb_trim - Strip whitespace (or other characters) from the beginning and end of a string + * - mb_ltrim - Strip whitespace (or other characters) from the beginning of a string + * - mb_rtrim - Strip whitespace (or other characters) from the end of a string + * + * Not implemented: + * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) + * - mb_ereg_* - Regular expression with multibyte support + * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable + * - mb_preferred_mime_name - Get MIME charset string + * - mb_regex_encoding - Returns current encoding for multibyte regex as string + * - mb_regex_set_options - Set/Get the default options for mbregex functions + * - mb_send_mail - Send encoded mail + * - mb_split - Split multibyte string using regular expression + * - mb_strcut - Get part of string + * - mb_strimwidth - Get truncated string with specified width + * + * @author Nicolas Grekas + * + * @internal + */ +final class Mbstring +{ + public const MB_CASE_FOLD = \PHP_INT_MAX; + + private const SIMPLE_CASE_FOLD = [ + ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], + ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], + ]; + + private static $encodingList = ['ASCII', 'UTF-8']; + private static $language = 'neutral'; + private static $internalEncoding = 'UTF-8'; + + public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) + { + if (\is_array($s)) { + $r = []; + foreach ($s as $str) { + $r[] = self::mb_convert_encoding($str, $toEncoding, $fromEncoding); + } + + return $r; + } + + if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) { + $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); + } else { + $fromEncoding = self::getEncoding($fromEncoding); + } + + $toEncoding = self::getEncoding($toEncoding); + + if ('BASE64' === $fromEncoding) { + $s = base64_decode($s); + $fromEncoding = $toEncoding; + } + + if ('BASE64' === $toEncoding) { + return base64_encode($s); + } + + if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { + if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { + $fromEncoding = 'Windows-1252'; + } + if ('UTF-8' !== $fromEncoding) { + $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); + } + + return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); + } + + if ('HTML-ENTITIES' === $fromEncoding) { + $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8'); + $fromEncoding = 'UTF-8'; + } + + return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); + } + + public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars) + { + $ok = true; + array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { + if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { + $ok = false; + } + }); + + return $ok ? $fromEncoding : false; + } + + public static function mb_decode_mimeheader($s) + { + return iconv_mime_decode($s, 2, self::$internalEncoding); + } + + public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) + { + trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING); + } + + public static function mb_decode_numericentity($s, $convmap, $encoding = null) + { + if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { + trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { + return false; + } + + if (null !== $encoding && !\is_scalar($encoding)) { + trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return ''; // Instead of null (cf. mb_encode_numericentity). + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $cnt = floor(\count($convmap) / 4) * 4; + + for ($i = 0; $i < $cnt; $i += 4) { + // collector_decode_htmlnumericentity ignores $convmap[$i + 3] + $convmap[$i] += $convmap[$i + 2]; + $convmap[$i + 1] += $convmap[$i + 2]; + } + + $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { + $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; + for ($i = 0; $i < $cnt; $i += 4) { + if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { + return self::mb_chr($c - $convmap[$i + 2]); + } + } + + return $m[0]; + }, $s); + + if (null === $encoding) { + return $s; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) + { + if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { + trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { + return false; + } + + if (null !== $encoding && !\is_scalar($encoding)) { + trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; // Instead of '' (cf. mb_decode_numericentity). + } + + if (null !== $is_hex && !\is_scalar($is_hex)) { + trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + + $cnt = floor(\count($convmap) / 4) * 4; + $i = 0; + $len = \strlen($s); + $result = ''; + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + $c = self::mb_ord($uchr); + + for ($j = 0; $j < $cnt; $j += 4) { + if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { + $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; + $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; + continue 2; + } + } + $result .= $uchr; + } + + if (null === $encoding) { + return $result; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $result); + } + + public static function mb_convert_case($s, $mode, $encoding = null) + { + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + if (\MB_CASE_TITLE == $mode) { + static $titleRegexp = null; + if (null === $titleRegexp) { + $titleRegexp = self::getData('titleCaseRegexp'); + } + $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); + } else { + if (\MB_CASE_UPPER == $mode) { + static $upper = null; + if (null === $upper) { + $upper = self::getData('upperCase'); + } + $map = $upper; + } else { + if (self::MB_CASE_FOLD === $mode) { + static $caseFolding = null; + if (null === $caseFolding) { + $caseFolding = self::getData('caseFolding'); + } + $s = strtr($s, $caseFolding); + } + + static $lower = null; + if (null === $lower) { + $lower = self::getData('lowerCase'); + } + $map = $lower; + } + + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + + $i = 0; + $len = \strlen($s); + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + + if (isset($map[$uchr])) { + $uchr = $map[$uchr]; + $nlen = \strlen($uchr); + + if ($nlen == $ulen) { + $nlen = $i; + do { + $s[--$nlen] = $uchr[--$ulen]; + } while ($ulen); + } else { + $s = substr_replace($s, $uchr, $i - $ulen, $ulen); + $len += $nlen - $ulen; + $i += $nlen - $ulen; + } + } + } + } + + if (null === $encoding) { + return $s; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_internal_encoding($encoding = null) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + $normalizedEncoding = self::getEncoding($encoding); + + if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) { + self::$internalEncoding = $normalizedEncoding; + + return true; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + public static function mb_language($lang = null) + { + if (null === $lang) { + return self::$language; + } + + switch ($normalizedLang = strtolower($lang)) { + case 'uni': + case 'neutral': + self::$language = $normalizedLang; + + return true; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang)); + } + + public static function mb_list_encodings() + { + return ['UTF-8']; + } + + public static function mb_encoding_aliases($encoding) + { + switch (strtoupper($encoding)) { + case 'UTF8': + case 'UTF-8': + return ['utf8']; + } + + return false; + } + + public static function mb_check_encoding($var = null, $encoding = null) + { + if (null === $encoding) { + if (null === $var) { + return false; + } + $encoding = self::$internalEncoding; + } + + if (!\is_array($var)) { + return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var); + } + + foreach ($var as $key => $value) { + if (!self::mb_check_encoding($key, $encoding)) { + return false; + } + if (!self::mb_check_encoding($value, $encoding)) { + return false; + } + } + + return true; + } + + public static function mb_detect_encoding($str, $encodingList = null, $strict = false) + { + if (null === $encodingList) { + $encodingList = self::$encodingList; + } else { + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + } + + foreach ($encodingList as $enc) { + switch ($enc) { + case 'ASCII': + if (!preg_match('/[\x80-\xFF]/', $str)) { + return $enc; + } + break; + + case 'UTF8': + case 'UTF-8': + if (preg_match('//u', $str)) { + return 'UTF-8'; + } + break; + + default: + if (0 === strncmp($enc, 'ISO-8859-', 9)) { + return $enc; + } + } + } + + return false; + } + + public static function mb_detect_order($encodingList = null) + { + if (null === $encodingList) { + return self::$encodingList; + } + + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + + foreach ($encodingList as $enc) { + switch ($enc) { + default: + if (strncmp($enc, 'ISO-8859-', 9)) { + return false; + } + // no break + case 'ASCII': + case 'UTF8': + case 'UTF-8': + } + } + + self::$encodingList = $encodingList; + + return true; + } + + public static function mb_strlen($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return \strlen($s); + } + + return @iconv_strlen($s, $encoding); + } + + public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strpos($haystack, $needle, $offset); + } + + $needle = (string) $needle; + if ('' === $needle) { + if (80000 > \PHP_VERSION_ID) { + trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING); + + return false; + } + + return 0; + } + + return iconv_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strrpos($haystack, $needle, $offset); + } + + if ($offset != (int) $offset) { + $offset = 0; + } elseif ($offset = (int) $offset) { + if ($offset < 0) { + if (0 > $offset += self::mb_strlen($needle)) { + $haystack = self::mb_substr($haystack, 0, $offset, $encoding); + } + $offset = 0; + } else { + $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); + } + } + + $pos = '' !== $needle || 80000 > \PHP_VERSION_ID + ? iconv_strrpos($haystack, $needle, $encoding) + : self::mb_strlen($haystack, $encoding); + + return false !== $pos ? $offset + $pos : false; + } + + public static function mb_str_split($string, $split_length = 1, $encoding = null) + { + if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { + trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING); + + return null; + } + + if (1 > $split_length = (int) $split_length) { + if (80000 > \PHP_VERSION_ID) { + trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); + + return false; + } + + throw new \ValueError('Argument #2 ($length) must be greater than 0'); + } + + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + if ('UTF-8' === $encoding = self::getEncoding($encoding)) { + $rx = '/('; + while (65535 < $split_length) { + $rx .= '.{65535}'; + $split_length -= 65535; + } + $rx .= '.{'.$split_length.'})/us'; + + return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); + } + + $result = []; + $length = mb_strlen($string, $encoding); + + for ($i = 0; $i < $length; $i += $split_length) { + $result[] = mb_substr($string, $i, $split_length, $encoding); + } + + return $result; + } + + public static function mb_strtolower($s, $encoding = null) + { + return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding); + } + + public static function mb_strtoupper($s, $encoding = null) + { + return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding); + } + + public static function mb_substitute_character($c = null) + { + if (null === $c) { + return 'none'; + } + if (0 === strcasecmp($c, 'none')) { + return true; + } + if (80000 > \PHP_VERSION_ID) { + return false; + } + if (\is_int($c) || 'long' === $c || 'entity' === $c) { + return false; + } + + throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint'); + } + + public static function mb_substr($s, $start, $length = null, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return (string) substr($s, $start, null === $length ? 2147483647 : $length); + } + + if ($start < 0) { + $start = iconv_strlen($s, $encoding) + $start; + if ($start < 0) { + $start = 0; + } + } + + if (null === $length) { + $length = 2147483647; + } elseif ($length < 0) { + $length = iconv_strlen($s, $encoding) + $length - $start; + if ($length < 0) { + return ''; + } + } + + return (string) iconv_substr($s, $start, $length, $encoding); + } + + public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) + { + [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [ + self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding), + self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding), + ]); + + return self::mb_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) + { + $pos = self::mb_stripos($haystack, $needle, 0, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + $pos = strrpos($haystack, $needle); + } else { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = iconv_strrpos($haystack, $needle, $encoding); + } + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) + { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = self::mb_strripos($haystack, $needle, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) + { + $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding); + $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding); + + $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack); + $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle); + + return self::mb_strrpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) + { + $pos = strpos($haystack, $needle); + if (false === $pos) { + return false; + } + if ($part) { + return substr($haystack, 0, $pos); + } + + return substr($haystack, $pos); + } + + public static function mb_get_info($type = 'all') + { + $info = [ + 'internal_encoding' => self::$internalEncoding, + 'http_output' => 'pass', + 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', + 'func_overload' => 0, + 'func_overload_list' => 'no overload', + 'mail_charset' => 'UTF-8', + 'mail_header_encoding' => 'BASE64', + 'mail_body_encoding' => 'BASE64', + 'illegal_chars' => 0, + 'encoding_translation' => 'Off', + 'language' => self::$language, + 'detect_order' => self::$encodingList, + 'substitute_character' => 'none', + 'strict_detection' => 'Off', + ]; + + if ('all' === $type) { + return $info; + } + if (isset($info[$type])) { + return $info[$type]; + } + + return false; + } + + public static function mb_http_input($type = '') + { + return false; + } + + public static function mb_http_output($encoding = null) + { + return null !== $encoding ? 'pass' === $encoding : 'pass'; + } + + public static function mb_strwidth($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + + if ('UTF-8' !== $encoding) { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); + + return ($wide << 1) + iconv_strlen($s, 'UTF-8'); + } + + public static function mb_substr_count($haystack, $needle, $encoding = null) + { + return substr_count($haystack, $needle); + } + + public static function mb_output_handler($contents, $status) + { + return $contents; + } + + public static function mb_chr($code, $encoding = null) + { + if (0x80 > $code %= 0x200000) { + $s = \chr($code); + } elseif (0x800 > $code) { + $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } else { + $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } + + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, $encoding, 'UTF-8'); + } + + return $s; + } + + public static function mb_ord($s, $encoding = null) + { + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, 'UTF-8', $encoding); + } + + if (1 === \strlen($s)) { + return \ord($s); + } + + $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; + if (0xF0 <= $code) { + return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; + } + if (0xE0 <= $code) { + return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; + } + if (0xC0 <= $code) { + return (($code - 0xC0) << 6) + $s[2] - 0x80; + } + + return $code; + } + + public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string + { + if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) { + throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH'); + } + + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } else { + self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given'); + } + + if (self::mb_strlen($pad_string, $encoding) <= 0) { + throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string'); + } + + $paddingRequired = $length - self::mb_strlen($string, $encoding); + + if ($paddingRequired < 1) { + return $string; + } + + switch ($pad_type) { + case \STR_PAD_LEFT: + return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string; + case \STR_PAD_RIGHT: + return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding); + default: + $leftPaddingLength = floor($paddingRequired / 2); + $rightPaddingLength = $paddingRequired - $leftPaddingLength; + + return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding); + } + } + + public static function mb_ucfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } else { + self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding); + + return $firstChar.mb_substr($string, 1, null, $encoding); + } + + public static function mb_lcfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } else { + self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding); + + return $firstChar.mb_substr($string, 1, null, $encoding); + } + + private static function getSubpart($pos, $part, $haystack, $encoding) + { + if (false === $pos) { + return false; + } + if ($part) { + return self::mb_substr($haystack, 0, $pos, $encoding); + } + + return self::mb_substr($haystack, $pos, null, $encoding); + } + + private static function html_encoding_callback(array $m) + { + $i = 1; + $entities = ''; + $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8')); + + while (isset($m[$i])) { + if (0x80 > $m[$i]) { + $entities .= \chr($m[$i++]); + continue; + } + if (0xF0 <= $m[$i]) { + $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } elseif (0xE0 <= $m[$i]) { + $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } else { + $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; + } + + $entities .= '&#'.$c.';'; + } + + return $entities; + } + + private static function title_case(array $s) + { + return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); + } + + private static function getData($file) + { + if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { + return require $file; + } + + return false; + } + + private static function getEncoding($encoding) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + if ('UTF-8' === $encoding) { + return 'UTF-8'; + } + + $encoding = strtoupper($encoding); + + if ('8BIT' === $encoding || 'BINARY' === $encoding) { + return 'CP850'; + } + + if ('UTF8' === $encoding) { + return 'UTF-8'; + } + + return $encoding; + } + + public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__); + } + + public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__); + } + + public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__); + } + + private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string + { + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } else { + self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given'); + } + + if ('' === $characters) { + return null === $encoding ? $string : self::mb_convert_encoding($string, $encoding); + } + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $string)) { + $string = @iconv('UTF-8', 'UTF-8//IGNORE', $string); + } + if (null !== $characters && !preg_match('//u', $characters)) { + $characters = @iconv('UTF-8', 'UTF-8//IGNORE', $characters); + } + } else { + $string = iconv($encoding, 'UTF-8//IGNORE', $string); + + if (null !== $characters) { + $characters = iconv($encoding, 'UTF-8//IGNORE', $characters); + } + } + + if (null === $characters) { + $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}"; + } else { + $characters = preg_quote($characters); + } + + $string = preg_replace(sprintf($regex, $characters), '', $string); + + if (null === $encoding) { + return $string; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $string); + } + + private static function assertEncoding(string $encoding, string $errorFormat): void + { + try { + $validEncoding = @self::mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf($errorFormat, $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf($errorFormat, $encoding)); + } + } +} diff --git a/vendor/symfony/polyfill-mbstring/README.md b/vendor/symfony/polyfill-mbstring/README.md new file mode 100644 index 0000000..478b40d --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/README.md @@ -0,0 +1,13 @@ +Symfony Polyfill / Mbstring +=========================== + +This component provides a partial, native PHP implementation for the +[Mbstring](https://php.net/mbstring) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php new file mode 100644 index 0000000..512bba0 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php @@ -0,0 +1,119 @@ + 'i̇', + 'µ' => 'μ', + 'ſ' => 's', + 'ͅ' => 'ι', + 'ς' => 'σ', + 'ϐ' => 'β', + 'ϑ' => 'θ', + 'ϕ' => 'φ', + 'ϖ' => 'π', + 'ϰ' => 'κ', + 'ϱ' => 'ρ', + 'ϵ' => 'ε', + 'ẛ' => 'ṡ', + 'ι' => 'ι', + 'ß' => 'ss', + 'ʼn' => 'ʼn', + 'ǰ' => 'ǰ', + 'ΐ' => 'ΐ', + 'ΰ' => 'ΰ', + 'և' => 'եւ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẚ' => 'aʾ', + 'ẞ' => 'ss', + 'ὐ' => 'ὐ', + 'ὒ' => 'ὒ', + 'ὔ' => 'ὔ', + 'ὖ' => 'ὖ', + 'ᾀ' => 'ἀι', + 'ᾁ' => 'ἁι', + 'ᾂ' => 'ἂι', + 'ᾃ' => 'ἃι', + 'ᾄ' => 'ἄι', + 'ᾅ' => 'ἅι', + 'ᾆ' => 'ἆι', + 'ᾇ' => 'ἇι', + 'ᾈ' => 'ἀι', + 'ᾉ' => 'ἁι', + 'ᾊ' => 'ἂι', + 'ᾋ' => 'ἃι', + 'ᾌ' => 'ἄι', + 'ᾍ' => 'ἅι', + 'ᾎ' => 'ἆι', + 'ᾏ' => 'ἇι', + 'ᾐ' => 'ἠι', + 'ᾑ' => 'ἡι', + 'ᾒ' => 'ἢι', + 'ᾓ' => 'ἣι', + 'ᾔ' => 'ἤι', + 'ᾕ' => 'ἥι', + 'ᾖ' => 'ἦι', + 'ᾗ' => 'ἧι', + 'ᾘ' => 'ἠι', + 'ᾙ' => 'ἡι', + 'ᾚ' => 'ἢι', + 'ᾛ' => 'ἣι', + 'ᾜ' => 'ἤι', + 'ᾝ' => 'ἥι', + 'ᾞ' => 'ἦι', + 'ᾟ' => 'ἧι', + 'ᾠ' => 'ὠι', + 'ᾡ' => 'ὡι', + 'ᾢ' => 'ὢι', + 'ᾣ' => 'ὣι', + 'ᾤ' => 'ὤι', + 'ᾥ' => 'ὥι', + 'ᾦ' => 'ὦι', + 'ᾧ' => 'ὧι', + 'ᾨ' => 'ὠι', + 'ᾩ' => 'ὡι', + 'ᾪ' => 'ὢι', + 'ᾫ' => 'ὣι', + 'ᾬ' => 'ὤι', + 'ᾭ' => 'ὥι', + 'ᾮ' => 'ὦι', + 'ᾯ' => 'ὧι', + 'ᾲ' => 'ὰι', + 'ᾳ' => 'αι', + 'ᾴ' => 'άι', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾶι', + 'ᾼ' => 'αι', + 'ῂ' => 'ὴι', + 'ῃ' => 'ηι', + 'ῄ' => 'ήι', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῆι', + 'ῌ' => 'ηι', + 'ῒ' => 'ῒ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'ῢ' => 'ῢ', + 'ῤ' => 'ῤ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'ῲ' => 'ὼι', + 'ῳ' => 'ωι', + 'ῴ' => 'ώι', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῶι', + 'ῼ' => 'ωι', + 'ff' => 'ff', + 'fi' => 'fi', + 'fl' => 'fl', + 'ffi' => 'ffi', + 'ffl' => 'ffl', + 'ſt' => 'st', + 'st' => 'st', + 'ﬓ' => 'մն', + 'ﬔ' => 'մե', + 'ﬕ' => 'մի', + 'ﬖ' => 'վն', + 'ﬗ' => 'մխ', +]; diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php new file mode 100644 index 0000000..fac60b0 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php @@ -0,0 +1,1397 @@ + 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + 'À' => 'à', + 'Á' => 'á', + 'Â' => 'â', + 'Ã' => 'ã', + 'Ä' => 'ä', + 'Å' => 'å', + 'Æ' => 'æ', + 'Ç' => 'ç', + 'È' => 'è', + 'É' => 'é', + 'Ê' => 'ê', + 'Ë' => 'ë', + 'Ì' => 'ì', + 'Í' => 'í', + 'Î' => 'î', + 'Ï' => 'ï', + 'Ð' => 'ð', + 'Ñ' => 'ñ', + 'Ò' => 'ò', + 'Ó' => 'ó', + 'Ô' => 'ô', + 'Õ' => 'õ', + 'Ö' => 'ö', + 'Ø' => 'ø', + 'Ù' => 'ù', + 'Ú' => 'ú', + 'Û' => 'û', + 'Ü' => 'ü', + 'Ý' => 'ý', + 'Þ' => 'þ', + 'Ā' => 'ā', + 'Ă' => 'ă', + 'Ą' => 'ą', + 'Ć' => 'ć', + 'Ĉ' => 'ĉ', + 'Ċ' => 'ċ', + 'Č' => 'č', + 'Ď' => 'ď', + 'Đ' => 'đ', + 'Ē' => 'ē', + 'Ĕ' => 'ĕ', + 'Ė' => 'ė', + 'Ę' => 'ę', + 'Ě' => 'ě', + 'Ĝ' => 'ĝ', + 'Ğ' => 'ğ', + 'Ġ' => 'ġ', + 'Ģ' => 'ģ', + 'Ĥ' => 'ĥ', + 'Ħ' => 'ħ', + 'Ĩ' => 'ĩ', + 'Ī' => 'ī', + 'Ĭ' => 'ĭ', + 'Į' => 'į', + 'İ' => 'i̇', + 'IJ' => 'ij', + 'Ĵ' => 'ĵ', + 'Ķ' => 'ķ', + 'Ĺ' => 'ĺ', + 'Ļ' => 'ļ', + 'Ľ' => 'ľ', + 'Ŀ' => 'ŀ', + 'Ł' => 'ł', + 'Ń' => 'ń', + 'Ņ' => 'ņ', + 'Ň' => 'ň', + 'Ŋ' => 'ŋ', + 'Ō' => 'ō', + 'Ŏ' => 'ŏ', + 'Ő' => 'ő', + 'Œ' => 'œ', + 'Ŕ' => 'ŕ', + 'Ŗ' => 'ŗ', + 'Ř' => 'ř', + 'Ś' => 'ś', + 'Ŝ' => 'ŝ', + 'Ş' => 'ş', + 'Š' => 'š', + 'Ţ' => 'ţ', + 'Ť' => 'ť', + 'Ŧ' => 'ŧ', + 'Ũ' => 'ũ', + 'Ū' => 'ū', + 'Ŭ' => 'ŭ', + 'Ů' => 'ů', + 'Ű' => 'ű', + 'Ų' => 'ų', + 'Ŵ' => 'ŵ', + 'Ŷ' => 'ŷ', + 'Ÿ' => 'ÿ', + 'Ź' => 'ź', + 'Ż' => 'ż', + 'Ž' => 'ž', + 'Ɓ' => 'ɓ', + 'Ƃ' => 'ƃ', + 'Ƅ' => 'ƅ', + 'Ɔ' => 'ɔ', + 'Ƈ' => 'ƈ', + 'Ɖ' => 'ɖ', + 'Ɗ' => 'ɗ', + 'Ƌ' => 'ƌ', + 'Ǝ' => 'ǝ', + 'Ə' => 'ə', + 'Ɛ' => 'ɛ', + 'Ƒ' => 'ƒ', + 'Ɠ' => 'ɠ', + 'Ɣ' => 'ɣ', + 'Ɩ' => 'ɩ', + 'Ɨ' => 'ɨ', + 'Ƙ' => 'ƙ', + 'Ɯ' => 'ɯ', + 'Ɲ' => 'ɲ', + 'Ɵ' => 'ɵ', + 'Ơ' => 'ơ', + 'Ƣ' => 'ƣ', + 'Ƥ' => 'ƥ', + 'Ʀ' => 'ʀ', + 'Ƨ' => 'ƨ', + 'Ʃ' => 'ʃ', + 'Ƭ' => 'ƭ', + 'Ʈ' => 'ʈ', + 'Ư' => 'ư', + 'Ʊ' => 'ʊ', + 'Ʋ' => 'ʋ', + 'Ƴ' => 'ƴ', + 'Ƶ' => 'ƶ', + 'Ʒ' => 'ʒ', + 'Ƹ' => 'ƹ', + 'Ƽ' => 'ƽ', + 'DŽ' => 'dž', + 'Dž' => 'dž', + 'LJ' => 'lj', + 'Lj' => 'lj', + 'NJ' => 'nj', + 'Nj' => 'nj', + 'Ǎ' => 'ǎ', + 'Ǐ' => 'ǐ', + 'Ǒ' => 'ǒ', + 'Ǔ' => 'ǔ', + 'Ǖ' => 'ǖ', + 'Ǘ' => 'ǘ', + 'Ǚ' => 'ǚ', + 'Ǜ' => 'ǜ', + 'Ǟ' => 'ǟ', + 'Ǡ' => 'ǡ', + 'Ǣ' => 'ǣ', + 'Ǥ' => 'ǥ', + 'Ǧ' => 'ǧ', + 'Ǩ' => 'ǩ', + 'Ǫ' => 'ǫ', + 'Ǭ' => 'ǭ', + 'Ǯ' => 'ǯ', + 'DZ' => 'dz', + 'Dz' => 'dz', + 'Ǵ' => 'ǵ', + 'Ƕ' => 'ƕ', + 'Ƿ' => 'ƿ', + 'Ǹ' => 'ǹ', + 'Ǻ' => 'ǻ', + 'Ǽ' => 'ǽ', + 'Ǿ' => 'ǿ', + 'Ȁ' => 'ȁ', + 'Ȃ' => 'ȃ', + 'Ȅ' => 'ȅ', + 'Ȇ' => 'ȇ', + 'Ȉ' => 'ȉ', + 'Ȋ' => 'ȋ', + 'Ȍ' => 'ȍ', + 'Ȏ' => 'ȏ', + 'Ȑ' => 'ȑ', + 'Ȓ' => 'ȓ', + 'Ȕ' => 'ȕ', + 'Ȗ' => 'ȗ', + 'Ș' => 'ș', + 'Ț' => 'ț', + 'Ȝ' => 'ȝ', + 'Ȟ' => 'ȟ', + 'Ƞ' => 'ƞ', + 'Ȣ' => 'ȣ', + 'Ȥ' => 'ȥ', + 'Ȧ' => 'ȧ', + 'Ȩ' => 'ȩ', + 'Ȫ' => 'ȫ', + 'Ȭ' => 'ȭ', + 'Ȯ' => 'ȯ', + 'Ȱ' => 'ȱ', + 'Ȳ' => 'ȳ', + 'Ⱥ' => 'ⱥ', + 'Ȼ' => 'ȼ', + 'Ƚ' => 'ƚ', + 'Ⱦ' => 'ⱦ', + 'Ɂ' => 'ɂ', + 'Ƀ' => 'ƀ', + 'Ʉ' => 'ʉ', + 'Ʌ' => 'ʌ', + 'Ɇ' => 'ɇ', + 'Ɉ' => 'ɉ', + 'Ɋ' => 'ɋ', + 'Ɍ' => 'ɍ', + 'Ɏ' => 'ɏ', + 'Ͱ' => 'ͱ', + 'Ͳ' => 'ͳ', + 'Ͷ' => 'ͷ', + 'Ϳ' => 'ϳ', + 'Ά' => 'ά', + 'Έ' => 'έ', + 'Ή' => 'ή', + 'Ί' => 'ί', + 'Ό' => 'ό', + 'Ύ' => 'ύ', + 'Ώ' => 'ώ', + 'Α' => 'α', + 'Β' => 'β', + 'Γ' => 'γ', + 'Δ' => 'δ', + 'Ε' => 'ε', + 'Ζ' => 'ζ', + 'Η' => 'η', + 'Θ' => 'θ', + 'Ι' => 'ι', + 'Κ' => 'κ', + 'Λ' => 'λ', + 'Μ' => 'μ', + 'Ν' => 'ν', + 'Ξ' => 'ξ', + 'Ο' => 'ο', + 'Π' => 'π', + 'Ρ' => 'ρ', + 'Σ' => 'σ', + 'Τ' => 'τ', + 'Υ' => 'υ', + 'Φ' => 'φ', + 'Χ' => 'χ', + 'Ψ' => 'ψ', + 'Ω' => 'ω', + 'Ϊ' => 'ϊ', + 'Ϋ' => 'ϋ', + 'Ϗ' => 'ϗ', + 'Ϙ' => 'ϙ', + 'Ϛ' => 'ϛ', + 'Ϝ' => 'ϝ', + 'Ϟ' => 'ϟ', + 'Ϡ' => 'ϡ', + 'Ϣ' => 'ϣ', + 'Ϥ' => 'ϥ', + 'Ϧ' => 'ϧ', + 'Ϩ' => 'ϩ', + 'Ϫ' => 'ϫ', + 'Ϭ' => 'ϭ', + 'Ϯ' => 'ϯ', + 'ϴ' => 'θ', + 'Ϸ' => 'ϸ', + 'Ϲ' => 'ϲ', + 'Ϻ' => 'ϻ', + 'Ͻ' => 'ͻ', + 'Ͼ' => 'ͼ', + 'Ͽ' => 'ͽ', + 'Ѐ' => 'ѐ', + 'Ё' => 'ё', + 'Ђ' => 'ђ', + 'Ѓ' => 'ѓ', + 'Є' => 'є', + 'Ѕ' => 'ѕ', + 'І' => 'і', + 'Ї' => 'ї', + 'Ј' => 'ј', + 'Љ' => 'љ', + 'Њ' => 'њ', + 'Ћ' => 'ћ', + 'Ќ' => 'ќ', + 'Ѝ' => 'ѝ', + 'Ў' => 'ў', + 'Џ' => 'џ', + 'А' => 'а', + 'Б' => 'б', + 'В' => 'в', + 'Г' => 'г', + 'Д' => 'д', + 'Е' => 'е', + 'Ж' => 'ж', + 'З' => 'з', + 'И' => 'и', + 'Й' => 'й', + 'К' => 'к', + 'Л' => 'л', + 'М' => 'м', + 'Н' => 'н', + 'О' => 'о', + 'П' => 'п', + 'Р' => 'р', + 'С' => 'с', + 'Т' => 'т', + 'У' => 'у', + 'Ф' => 'ф', + 'Х' => 'х', + 'Ц' => 'ц', + 'Ч' => 'ч', + 'Ш' => 'ш', + 'Щ' => 'щ', + 'Ъ' => 'ъ', + 'Ы' => 'ы', + 'Ь' => 'ь', + 'Э' => 'э', + 'Ю' => 'ю', + 'Я' => 'я', + 'Ѡ' => 'ѡ', + 'Ѣ' => 'ѣ', + 'Ѥ' => 'ѥ', + 'Ѧ' => 'ѧ', + 'Ѩ' => 'ѩ', + 'Ѫ' => 'ѫ', + 'Ѭ' => 'ѭ', + 'Ѯ' => 'ѯ', + 'Ѱ' => 'ѱ', + 'Ѳ' => 'ѳ', + 'Ѵ' => 'ѵ', + 'Ѷ' => 'ѷ', + 'Ѹ' => 'ѹ', + 'Ѻ' => 'ѻ', + 'Ѽ' => 'ѽ', + 'Ѿ' => 'ѿ', + 'Ҁ' => 'ҁ', + 'Ҋ' => 'ҋ', + 'Ҍ' => 'ҍ', + 'Ҏ' => 'ҏ', + 'Ґ' => 'ґ', + 'Ғ' => 'ғ', + 'Ҕ' => 'ҕ', + 'Җ' => 'җ', + 'Ҙ' => 'ҙ', + 'Қ' => 'қ', + 'Ҝ' => 'ҝ', + 'Ҟ' => 'ҟ', + 'Ҡ' => 'ҡ', + 'Ң' => 'ң', + 'Ҥ' => 'ҥ', + 'Ҧ' => 'ҧ', + 'Ҩ' => 'ҩ', + 'Ҫ' => 'ҫ', + 'Ҭ' => 'ҭ', + 'Ү' => 'ү', + 'Ұ' => 'ұ', + 'Ҳ' => 'ҳ', + 'Ҵ' => 'ҵ', + 'Ҷ' => 'ҷ', + 'Ҹ' => 'ҹ', + 'Һ' => 'һ', + 'Ҽ' => 'ҽ', + 'Ҿ' => 'ҿ', + 'Ӏ' => 'ӏ', + 'Ӂ' => 'ӂ', + 'Ӄ' => 'ӄ', + 'Ӆ' => 'ӆ', + 'Ӈ' => 'ӈ', + 'Ӊ' => 'ӊ', + 'Ӌ' => 'ӌ', + 'Ӎ' => 'ӎ', + 'Ӑ' => 'ӑ', + 'Ӓ' => 'ӓ', + 'Ӕ' => 'ӕ', + 'Ӗ' => 'ӗ', + 'Ә' => 'ә', + 'Ӛ' => 'ӛ', + 'Ӝ' => 'ӝ', + 'Ӟ' => 'ӟ', + 'Ӡ' => 'ӡ', + 'Ӣ' => 'ӣ', + 'Ӥ' => 'ӥ', + 'Ӧ' => 'ӧ', + 'Ө' => 'ө', + 'Ӫ' => 'ӫ', + 'Ӭ' => 'ӭ', + 'Ӯ' => 'ӯ', + 'Ӱ' => 'ӱ', + 'Ӳ' => 'ӳ', + 'Ӵ' => 'ӵ', + 'Ӷ' => 'ӷ', + 'Ӹ' => 'ӹ', + 'Ӻ' => 'ӻ', + 'Ӽ' => 'ӽ', + 'Ӿ' => 'ӿ', + 'Ԁ' => 'ԁ', + 'Ԃ' => 'ԃ', + 'Ԅ' => 'ԅ', + 'Ԇ' => 'ԇ', + 'Ԉ' => 'ԉ', + 'Ԋ' => 'ԋ', + 'Ԍ' => 'ԍ', + 'Ԏ' => 'ԏ', + 'Ԑ' => 'ԑ', + 'Ԓ' => 'ԓ', + 'Ԕ' => 'ԕ', + 'Ԗ' => 'ԗ', + 'Ԙ' => 'ԙ', + 'Ԛ' => 'ԛ', + 'Ԝ' => 'ԝ', + 'Ԟ' => 'ԟ', + 'Ԡ' => 'ԡ', + 'Ԣ' => 'ԣ', + 'Ԥ' => 'ԥ', + 'Ԧ' => 'ԧ', + 'Ԩ' => 'ԩ', + 'Ԫ' => 'ԫ', + 'Ԭ' => 'ԭ', + 'Ԯ' => 'ԯ', + 'Ա' => 'ա', + 'Բ' => 'բ', + 'Գ' => 'գ', + 'Դ' => 'դ', + 'Ե' => 'ե', + 'Զ' => 'զ', + 'Է' => 'է', + 'Ը' => 'ը', + 'Թ' => 'թ', + 'Ժ' => 'ժ', + 'Ի' => 'ի', + 'Լ' => 'լ', + 'Խ' => 'խ', + 'Ծ' => 'ծ', + 'Կ' => 'կ', + 'Հ' => 'հ', + 'Ձ' => 'ձ', + 'Ղ' => 'ղ', + 'Ճ' => 'ճ', + 'Մ' => 'մ', + 'Յ' => 'յ', + 'Ն' => 'ն', + 'Շ' => 'շ', + 'Ո' => 'ո', + 'Չ' => 'չ', + 'Պ' => 'պ', + 'Ջ' => 'ջ', + 'Ռ' => 'ռ', + 'Ս' => 'ս', + 'Վ' => 'վ', + 'Տ' => 'տ', + 'Ր' => 'ր', + 'Ց' => 'ց', + 'Ւ' => 'ւ', + 'Փ' => 'փ', + 'Ք' => 'ք', + 'Օ' => 'օ', + 'Ֆ' => 'ֆ', + 'Ⴀ' => 'ⴀ', + 'Ⴁ' => 'ⴁ', + 'Ⴂ' => 'ⴂ', + 'Ⴃ' => 'ⴃ', + 'Ⴄ' => 'ⴄ', + 'Ⴅ' => 'ⴅ', + 'Ⴆ' => 'ⴆ', + 'Ⴇ' => 'ⴇ', + 'Ⴈ' => 'ⴈ', + 'Ⴉ' => 'ⴉ', + 'Ⴊ' => 'ⴊ', + 'Ⴋ' => 'ⴋ', + 'Ⴌ' => 'ⴌ', + 'Ⴍ' => 'ⴍ', + 'Ⴎ' => 'ⴎ', + 'Ⴏ' => 'ⴏ', + 'Ⴐ' => 'ⴐ', + 'Ⴑ' => 'ⴑ', + 'Ⴒ' => 'ⴒ', + 'Ⴓ' => 'ⴓ', + 'Ⴔ' => 'ⴔ', + 'Ⴕ' => 'ⴕ', + 'Ⴖ' => 'ⴖ', + 'Ⴗ' => 'ⴗ', + 'Ⴘ' => 'ⴘ', + 'Ⴙ' => 'ⴙ', + 'Ⴚ' => 'ⴚ', + 'Ⴛ' => 'ⴛ', + 'Ⴜ' => 'ⴜ', + 'Ⴝ' => 'ⴝ', + 'Ⴞ' => 'ⴞ', + 'Ⴟ' => 'ⴟ', + 'Ⴠ' => 'ⴠ', + 'Ⴡ' => 'ⴡ', + 'Ⴢ' => 'ⴢ', + 'Ⴣ' => 'ⴣ', + 'Ⴤ' => 'ⴤ', + 'Ⴥ' => 'ⴥ', + 'Ⴧ' => 'ⴧ', + 'Ⴭ' => 'ⴭ', + 'Ꭰ' => 'ꭰ', + 'Ꭱ' => 'ꭱ', + 'Ꭲ' => 'ꭲ', + 'Ꭳ' => 'ꭳ', + 'Ꭴ' => 'ꭴ', + 'Ꭵ' => 'ꭵ', + 'Ꭶ' => 'ꭶ', + 'Ꭷ' => 'ꭷ', + 'Ꭸ' => 'ꭸ', + 'Ꭹ' => 'ꭹ', + 'Ꭺ' => 'ꭺ', + 'Ꭻ' => 'ꭻ', + 'Ꭼ' => 'ꭼ', + 'Ꭽ' => 'ꭽ', + 'Ꭾ' => 'ꭾ', + 'Ꭿ' => 'ꭿ', + 'Ꮀ' => 'ꮀ', + 'Ꮁ' => 'ꮁ', + 'Ꮂ' => 'ꮂ', + 'Ꮃ' => 'ꮃ', + 'Ꮄ' => 'ꮄ', + 'Ꮅ' => 'ꮅ', + 'Ꮆ' => 'ꮆ', + 'Ꮇ' => 'ꮇ', + 'Ꮈ' => 'ꮈ', + 'Ꮉ' => 'ꮉ', + 'Ꮊ' => 'ꮊ', + 'Ꮋ' => 'ꮋ', + 'Ꮌ' => 'ꮌ', + 'Ꮍ' => 'ꮍ', + 'Ꮎ' => 'ꮎ', + 'Ꮏ' => 'ꮏ', + 'Ꮐ' => 'ꮐ', + 'Ꮑ' => 'ꮑ', + 'Ꮒ' => 'ꮒ', + 'Ꮓ' => 'ꮓ', + 'Ꮔ' => 'ꮔ', + 'Ꮕ' => 'ꮕ', + 'Ꮖ' => 'ꮖ', + 'Ꮗ' => 'ꮗ', + 'Ꮘ' => 'ꮘ', + 'Ꮙ' => 'ꮙ', + 'Ꮚ' => 'ꮚ', + 'Ꮛ' => 'ꮛ', + 'Ꮜ' => 'ꮜ', + 'Ꮝ' => 'ꮝ', + 'Ꮞ' => 'ꮞ', + 'Ꮟ' => 'ꮟ', + 'Ꮠ' => 'ꮠ', + 'Ꮡ' => 'ꮡ', + 'Ꮢ' => 'ꮢ', + 'Ꮣ' => 'ꮣ', + 'Ꮤ' => 'ꮤ', + 'Ꮥ' => 'ꮥ', + 'Ꮦ' => 'ꮦ', + 'Ꮧ' => 'ꮧ', + 'Ꮨ' => 'ꮨ', + 'Ꮩ' => 'ꮩ', + 'Ꮪ' => 'ꮪ', + 'Ꮫ' => 'ꮫ', + 'Ꮬ' => 'ꮬ', + 'Ꮭ' => 'ꮭ', + 'Ꮮ' => 'ꮮ', + 'Ꮯ' => 'ꮯ', + 'Ꮰ' => 'ꮰ', + 'Ꮱ' => 'ꮱ', + 'Ꮲ' => 'ꮲ', + 'Ꮳ' => 'ꮳ', + 'Ꮴ' => 'ꮴ', + 'Ꮵ' => 'ꮵ', + 'Ꮶ' => 'ꮶ', + 'Ꮷ' => 'ꮷ', + 'Ꮸ' => 'ꮸ', + 'Ꮹ' => 'ꮹ', + 'Ꮺ' => 'ꮺ', + 'Ꮻ' => 'ꮻ', + 'Ꮼ' => 'ꮼ', + 'Ꮽ' => 'ꮽ', + 'Ꮾ' => 'ꮾ', + 'Ꮿ' => 'ꮿ', + 'Ᏸ' => 'ᏸ', + 'Ᏹ' => 'ᏹ', + 'Ᏺ' => 'ᏺ', + 'Ᏻ' => 'ᏻ', + 'Ᏼ' => 'ᏼ', + 'Ᏽ' => 'ᏽ', + 'Ა' => 'ა', + 'Ბ' => 'ბ', + 'Გ' => 'გ', + 'Დ' => 'დ', + 'Ე' => 'ე', + 'Ვ' => 'ვ', + 'Ზ' => 'ზ', + 'Თ' => 'თ', + 'Ი' => 'ი', + 'Კ' => 'კ', + 'Ლ' => 'ლ', + 'Მ' => 'მ', + 'Ნ' => 'ნ', + 'Ო' => 'ო', + 'Პ' => 'პ', + 'Ჟ' => 'ჟ', + 'Რ' => 'რ', + 'Ს' => 'ს', + 'Ტ' => 'ტ', + 'Უ' => 'უ', + 'Ფ' => 'ფ', + 'Ქ' => 'ქ', + 'Ღ' => 'ღ', + 'Ყ' => 'ყ', + 'Შ' => 'შ', + 'Ჩ' => 'ჩ', + 'Ც' => 'ც', + 'Ძ' => 'ძ', + 'Წ' => 'წ', + 'Ჭ' => 'ჭ', + 'Ხ' => 'ხ', + 'Ჯ' => 'ჯ', + 'Ჰ' => 'ჰ', + 'Ჱ' => 'ჱ', + 'Ჲ' => 'ჲ', + 'Ჳ' => 'ჳ', + 'Ჴ' => 'ჴ', + 'Ჵ' => 'ჵ', + 'Ჶ' => 'ჶ', + 'Ჷ' => 'ჷ', + 'Ჸ' => 'ჸ', + 'Ჹ' => 'ჹ', + 'Ჺ' => 'ჺ', + 'Ჽ' => 'ჽ', + 'Ჾ' => 'ჾ', + 'Ჿ' => 'ჿ', + 'Ḁ' => 'ḁ', + 'Ḃ' => 'ḃ', + 'Ḅ' => 'ḅ', + 'Ḇ' => 'ḇ', + 'Ḉ' => 'ḉ', + 'Ḋ' => 'ḋ', + 'Ḍ' => 'ḍ', + 'Ḏ' => 'ḏ', + 'Ḑ' => 'ḑ', + 'Ḓ' => 'ḓ', + 'Ḕ' => 'ḕ', + 'Ḗ' => 'ḗ', + 'Ḙ' => 'ḙ', + 'Ḛ' => 'ḛ', + 'Ḝ' => 'ḝ', + 'Ḟ' => 'ḟ', + 'Ḡ' => 'ḡ', + 'Ḣ' => 'ḣ', + 'Ḥ' => 'ḥ', + 'Ḧ' => 'ḧ', + 'Ḩ' => 'ḩ', + 'Ḫ' => 'ḫ', + 'Ḭ' => 'ḭ', + 'Ḯ' => 'ḯ', + 'Ḱ' => 'ḱ', + 'Ḳ' => 'ḳ', + 'Ḵ' => 'ḵ', + 'Ḷ' => 'ḷ', + 'Ḹ' => 'ḹ', + 'Ḻ' => 'ḻ', + 'Ḽ' => 'ḽ', + 'Ḿ' => 'ḿ', + 'Ṁ' => 'ṁ', + 'Ṃ' => 'ṃ', + 'Ṅ' => 'ṅ', + 'Ṇ' => 'ṇ', + 'Ṉ' => 'ṉ', + 'Ṋ' => 'ṋ', + 'Ṍ' => 'ṍ', + 'Ṏ' => 'ṏ', + 'Ṑ' => 'ṑ', + 'Ṓ' => 'ṓ', + 'Ṕ' => 'ṕ', + 'Ṗ' => 'ṗ', + 'Ṙ' => 'ṙ', + 'Ṛ' => 'ṛ', + 'Ṝ' => 'ṝ', + 'Ṟ' => 'ṟ', + 'Ṡ' => 'ṡ', + 'Ṣ' => 'ṣ', + 'Ṥ' => 'ṥ', + 'Ṧ' => 'ṧ', + 'Ṩ' => 'ṩ', + 'Ṫ' => 'ṫ', + 'Ṭ' => 'ṭ', + 'Ṯ' => 'ṯ', + 'Ṱ' => 'ṱ', + 'Ṳ' => 'ṳ', + 'Ṵ' => 'ṵ', + 'Ṷ' => 'ṷ', + 'Ṹ' => 'ṹ', + 'Ṻ' => 'ṻ', + 'Ṽ' => 'ṽ', + 'Ṿ' => 'ṿ', + 'Ẁ' => 'ẁ', + 'Ẃ' => 'ẃ', + 'Ẅ' => 'ẅ', + 'Ẇ' => 'ẇ', + 'Ẉ' => 'ẉ', + 'Ẋ' => 'ẋ', + 'Ẍ' => 'ẍ', + 'Ẏ' => 'ẏ', + 'Ẑ' => 'ẑ', + 'Ẓ' => 'ẓ', + 'Ẕ' => 'ẕ', + 'ẞ' => 'ß', + 'Ạ' => 'ạ', + 'Ả' => 'ả', + 'Ấ' => 'ấ', + 'Ầ' => 'ầ', + 'Ẩ' => 'ẩ', + 'Ẫ' => 'ẫ', + 'Ậ' => 'ậ', + 'Ắ' => 'ắ', + 'Ằ' => 'ằ', + 'Ẳ' => 'ẳ', + 'Ẵ' => 'ẵ', + 'Ặ' => 'ặ', + 'Ẹ' => 'ẹ', + 'Ẻ' => 'ẻ', + 'Ẽ' => 'ẽ', + 'Ế' => 'ế', + 'Ề' => 'ề', + 'Ể' => 'ể', + 'Ễ' => 'ễ', + 'Ệ' => 'ệ', + 'Ỉ' => 'ỉ', + 'Ị' => 'ị', + 'Ọ' => 'ọ', + 'Ỏ' => 'ỏ', + 'Ố' => 'ố', + 'Ồ' => 'ồ', + 'Ổ' => 'ổ', + 'Ỗ' => 'ỗ', + 'Ộ' => 'ộ', + 'Ớ' => 'ớ', + 'Ờ' => 'ờ', + 'Ở' => 'ở', + 'Ỡ' => 'ỡ', + 'Ợ' => 'ợ', + 'Ụ' => 'ụ', + 'Ủ' => 'ủ', + 'Ứ' => 'ứ', + 'Ừ' => 'ừ', + 'Ử' => 'ử', + 'Ữ' => 'ữ', + 'Ự' => 'ự', + 'Ỳ' => 'ỳ', + 'Ỵ' => 'ỵ', + 'Ỷ' => 'ỷ', + 'Ỹ' => 'ỹ', + 'Ỻ' => 'ỻ', + 'Ỽ' => 'ỽ', + 'Ỿ' => 'ỿ', + 'Ἀ' => 'ἀ', + 'Ἁ' => 'ἁ', + 'Ἂ' => 'ἂ', + 'Ἃ' => 'ἃ', + 'Ἄ' => 'ἄ', + 'Ἅ' => 'ἅ', + 'Ἆ' => 'ἆ', + 'Ἇ' => 'ἇ', + 'Ἐ' => 'ἐ', + 'Ἑ' => 'ἑ', + 'Ἒ' => 'ἒ', + 'Ἓ' => 'ἓ', + 'Ἔ' => 'ἔ', + 'Ἕ' => 'ἕ', + 'Ἠ' => 'ἠ', + 'Ἡ' => 'ἡ', + 'Ἢ' => 'ἢ', + 'Ἣ' => 'ἣ', + 'Ἤ' => 'ἤ', + 'Ἥ' => 'ἥ', + 'Ἦ' => 'ἦ', + 'Ἧ' => 'ἧ', + 'Ἰ' => 'ἰ', + 'Ἱ' => 'ἱ', + 'Ἲ' => 'ἲ', + 'Ἳ' => 'ἳ', + 'Ἴ' => 'ἴ', + 'Ἵ' => 'ἵ', + 'Ἶ' => 'ἶ', + 'Ἷ' => 'ἷ', + 'Ὀ' => 'ὀ', + 'Ὁ' => 'ὁ', + 'Ὂ' => 'ὂ', + 'Ὃ' => 'ὃ', + 'Ὄ' => 'ὄ', + 'Ὅ' => 'ὅ', + 'Ὑ' => 'ὑ', + 'Ὓ' => 'ὓ', + 'Ὕ' => 'ὕ', + 'Ὗ' => 'ὗ', + 'Ὠ' => 'ὠ', + 'Ὡ' => 'ὡ', + 'Ὢ' => 'ὢ', + 'Ὣ' => 'ὣ', + 'Ὤ' => 'ὤ', + 'Ὥ' => 'ὥ', + 'Ὦ' => 'ὦ', + 'Ὧ' => 'ὧ', + 'ᾈ' => 'ᾀ', + 'ᾉ' => 'ᾁ', + 'ᾊ' => 'ᾂ', + 'ᾋ' => 'ᾃ', + 'ᾌ' => 'ᾄ', + 'ᾍ' => 'ᾅ', + 'ᾎ' => 'ᾆ', + 'ᾏ' => 'ᾇ', + 'ᾘ' => 'ᾐ', + 'ᾙ' => 'ᾑ', + 'ᾚ' => 'ᾒ', + 'ᾛ' => 'ᾓ', + 'ᾜ' => 'ᾔ', + 'ᾝ' => 'ᾕ', + 'ᾞ' => 'ᾖ', + 'ᾟ' => 'ᾗ', + 'ᾨ' => 'ᾠ', + 'ᾩ' => 'ᾡ', + 'ᾪ' => 'ᾢ', + 'ᾫ' => 'ᾣ', + 'ᾬ' => 'ᾤ', + 'ᾭ' => 'ᾥ', + 'ᾮ' => 'ᾦ', + 'ᾯ' => 'ᾧ', + 'Ᾰ' => 'ᾰ', + 'Ᾱ' => 'ᾱ', + 'Ὰ' => 'ὰ', + 'Ά' => 'ά', + 'ᾼ' => 'ᾳ', + 'Ὲ' => 'ὲ', + 'Έ' => 'έ', + 'Ὴ' => 'ὴ', + 'Ή' => 'ή', + 'ῌ' => 'ῃ', + 'Ῐ' => 'ῐ', + 'Ῑ' => 'ῑ', + 'Ὶ' => 'ὶ', + 'Ί' => 'ί', + 'Ῠ' => 'ῠ', + 'Ῡ' => 'ῡ', + 'Ὺ' => 'ὺ', + 'Ύ' => 'ύ', + 'Ῥ' => 'ῥ', + 'Ὸ' => 'ὸ', + 'Ό' => 'ό', + 'Ὼ' => 'ὼ', + 'Ώ' => 'ώ', + 'ῼ' => 'ῳ', + 'Ω' => 'ω', + 'K' => 'k', + 'Å' => 'å', + 'Ⅎ' => 'ⅎ', + 'Ⅰ' => 'ⅰ', + 'Ⅱ' => 'ⅱ', + 'Ⅲ' => 'ⅲ', + 'Ⅳ' => 'ⅳ', + 'Ⅴ' => 'ⅴ', + 'Ⅵ' => 'ⅵ', + 'Ⅶ' => 'ⅶ', + 'Ⅷ' => 'ⅷ', + 'Ⅸ' => 'ⅸ', + 'Ⅹ' => 'ⅹ', + 'Ⅺ' => 'ⅺ', + 'Ⅻ' => 'ⅻ', + 'Ⅼ' => 'ⅼ', + 'Ⅽ' => 'ⅽ', + 'Ⅾ' => 'ⅾ', + 'Ⅿ' => 'ⅿ', + 'Ↄ' => 'ↄ', + 'Ⓐ' => 'ⓐ', + 'Ⓑ' => 'ⓑ', + 'Ⓒ' => 'ⓒ', + 'Ⓓ' => 'ⓓ', + 'Ⓔ' => 'ⓔ', + 'Ⓕ' => 'ⓕ', + 'Ⓖ' => 'ⓖ', + 'Ⓗ' => 'ⓗ', + 'Ⓘ' => 'ⓘ', + 'Ⓙ' => 'ⓙ', + 'Ⓚ' => 'ⓚ', + 'Ⓛ' => 'ⓛ', + 'Ⓜ' => 'ⓜ', + 'Ⓝ' => 'ⓝ', + 'Ⓞ' => 'ⓞ', + 'Ⓟ' => 'ⓟ', + 'Ⓠ' => 'ⓠ', + 'Ⓡ' => 'ⓡ', + 'Ⓢ' => 'ⓢ', + 'Ⓣ' => 'ⓣ', + 'Ⓤ' => 'ⓤ', + 'Ⓥ' => 'ⓥ', + 'Ⓦ' => 'ⓦ', + 'Ⓧ' => 'ⓧ', + 'Ⓨ' => 'ⓨ', + 'Ⓩ' => 'ⓩ', + 'Ⰰ' => 'ⰰ', + 'Ⰱ' => 'ⰱ', + 'Ⰲ' => 'ⰲ', + 'Ⰳ' => 'ⰳ', + 'Ⰴ' => 'ⰴ', + 'Ⰵ' => 'ⰵ', + 'Ⰶ' => 'ⰶ', + 'Ⰷ' => 'ⰷ', + 'Ⰸ' => 'ⰸ', + 'Ⰹ' => 'ⰹ', + 'Ⰺ' => 'ⰺ', + 'Ⰻ' => 'ⰻ', + 'Ⰼ' => 'ⰼ', + 'Ⰽ' => 'ⰽ', + 'Ⰾ' => 'ⰾ', + 'Ⰿ' => 'ⰿ', + 'Ⱀ' => 'ⱀ', + 'Ⱁ' => 'ⱁ', + 'Ⱂ' => 'ⱂ', + 'Ⱃ' => 'ⱃ', + 'Ⱄ' => 'ⱄ', + 'Ⱅ' => 'ⱅ', + 'Ⱆ' => 'ⱆ', + 'Ⱇ' => 'ⱇ', + 'Ⱈ' => 'ⱈ', + 'Ⱉ' => 'ⱉ', + 'Ⱊ' => 'ⱊ', + 'Ⱋ' => 'ⱋ', + 'Ⱌ' => 'ⱌ', + 'Ⱍ' => 'ⱍ', + 'Ⱎ' => 'ⱎ', + 'Ⱏ' => 'ⱏ', + 'Ⱐ' => 'ⱐ', + 'Ⱑ' => 'ⱑ', + 'Ⱒ' => 'ⱒ', + 'Ⱓ' => 'ⱓ', + 'Ⱔ' => 'ⱔ', + 'Ⱕ' => 'ⱕ', + 'Ⱖ' => 'ⱖ', + 'Ⱗ' => 'ⱗ', + 'Ⱘ' => 'ⱘ', + 'Ⱙ' => 'ⱙ', + 'Ⱚ' => 'ⱚ', + 'Ⱛ' => 'ⱛ', + 'Ⱜ' => 'ⱜ', + 'Ⱝ' => 'ⱝ', + 'Ⱞ' => 'ⱞ', + 'Ⱡ' => 'ⱡ', + 'Ɫ' => 'ɫ', + 'Ᵽ' => 'ᵽ', + 'Ɽ' => 'ɽ', + 'Ⱨ' => 'ⱨ', + 'Ⱪ' => 'ⱪ', + 'Ⱬ' => 'ⱬ', + 'Ɑ' => 'ɑ', + 'Ɱ' => 'ɱ', + 'Ɐ' => 'ɐ', + 'Ɒ' => 'ɒ', + 'Ⱳ' => 'ⱳ', + 'Ⱶ' => 'ⱶ', + 'Ȿ' => 'ȿ', + 'Ɀ' => 'ɀ', + 'Ⲁ' => 'ⲁ', + 'Ⲃ' => 'ⲃ', + 'Ⲅ' => 'ⲅ', + 'Ⲇ' => 'ⲇ', + 'Ⲉ' => 'ⲉ', + 'Ⲋ' => 'ⲋ', + 'Ⲍ' => 'ⲍ', + 'Ⲏ' => 'ⲏ', + 'Ⲑ' => 'ⲑ', + 'Ⲓ' => 'ⲓ', + 'Ⲕ' => 'ⲕ', + 'Ⲗ' => 'ⲗ', + 'Ⲙ' => 'ⲙ', + 'Ⲛ' => 'ⲛ', + 'Ⲝ' => 'ⲝ', + 'Ⲟ' => 'ⲟ', + 'Ⲡ' => 'ⲡ', + 'Ⲣ' => 'ⲣ', + 'Ⲥ' => 'ⲥ', + 'Ⲧ' => 'ⲧ', + 'Ⲩ' => 'ⲩ', + 'Ⲫ' => 'ⲫ', + 'Ⲭ' => 'ⲭ', + 'Ⲯ' => 'ⲯ', + 'Ⲱ' => 'ⲱ', + 'Ⲳ' => 'ⲳ', + 'Ⲵ' => 'ⲵ', + 'Ⲷ' => 'ⲷ', + 'Ⲹ' => 'ⲹ', + 'Ⲻ' => 'ⲻ', + 'Ⲽ' => 'ⲽ', + 'Ⲿ' => 'ⲿ', + 'Ⳁ' => 'ⳁ', + 'Ⳃ' => 'ⳃ', + 'Ⳅ' => 'ⳅ', + 'Ⳇ' => 'ⳇ', + 'Ⳉ' => 'ⳉ', + 'Ⳋ' => 'ⳋ', + 'Ⳍ' => 'ⳍ', + 'Ⳏ' => 'ⳏ', + 'Ⳑ' => 'ⳑ', + 'Ⳓ' => 'ⳓ', + 'Ⳕ' => 'ⳕ', + 'Ⳗ' => 'ⳗ', + 'Ⳙ' => 'ⳙ', + 'Ⳛ' => 'ⳛ', + 'Ⳝ' => 'ⳝ', + 'Ⳟ' => 'ⳟ', + 'Ⳡ' => 'ⳡ', + 'Ⳣ' => 'ⳣ', + 'Ⳬ' => 'ⳬ', + 'Ⳮ' => 'ⳮ', + 'Ⳳ' => 'ⳳ', + 'Ꙁ' => 'ꙁ', + 'Ꙃ' => 'ꙃ', + 'Ꙅ' => 'ꙅ', + 'Ꙇ' => 'ꙇ', + 'Ꙉ' => 'ꙉ', + 'Ꙋ' => 'ꙋ', + 'Ꙍ' => 'ꙍ', + 'Ꙏ' => 'ꙏ', + 'Ꙑ' => 'ꙑ', + 'Ꙓ' => 'ꙓ', + 'Ꙕ' => 'ꙕ', + 'Ꙗ' => 'ꙗ', + 'Ꙙ' => 'ꙙ', + 'Ꙛ' => 'ꙛ', + 'Ꙝ' => 'ꙝ', + 'Ꙟ' => 'ꙟ', + 'Ꙡ' => 'ꙡ', + 'Ꙣ' => 'ꙣ', + 'Ꙥ' => 'ꙥ', + 'Ꙧ' => 'ꙧ', + 'Ꙩ' => 'ꙩ', + 'Ꙫ' => 'ꙫ', + 'Ꙭ' => 'ꙭ', + 'Ꚁ' => 'ꚁ', + 'Ꚃ' => 'ꚃ', + 'Ꚅ' => 'ꚅ', + 'Ꚇ' => 'ꚇ', + 'Ꚉ' => 'ꚉ', + 'Ꚋ' => 'ꚋ', + 'Ꚍ' => 'ꚍ', + 'Ꚏ' => 'ꚏ', + 'Ꚑ' => 'ꚑ', + 'Ꚓ' => 'ꚓ', + 'Ꚕ' => 'ꚕ', + 'Ꚗ' => 'ꚗ', + 'Ꚙ' => 'ꚙ', + 'Ꚛ' => 'ꚛ', + 'Ꜣ' => 'ꜣ', + 'Ꜥ' => 'ꜥ', + 'Ꜧ' => 'ꜧ', + 'Ꜩ' => 'ꜩ', + 'Ꜫ' => 'ꜫ', + 'Ꜭ' => 'ꜭ', + 'Ꜯ' => 'ꜯ', + 'Ꜳ' => 'ꜳ', + 'Ꜵ' => 'ꜵ', + 'Ꜷ' => 'ꜷ', + 'Ꜹ' => 'ꜹ', + 'Ꜻ' => 'ꜻ', + 'Ꜽ' => 'ꜽ', + 'Ꜿ' => 'ꜿ', + 'Ꝁ' => 'ꝁ', + 'Ꝃ' => 'ꝃ', + 'Ꝅ' => 'ꝅ', + 'Ꝇ' => 'ꝇ', + 'Ꝉ' => 'ꝉ', + 'Ꝋ' => 'ꝋ', + 'Ꝍ' => 'ꝍ', + 'Ꝏ' => 'ꝏ', + 'Ꝑ' => 'ꝑ', + 'Ꝓ' => 'ꝓ', + 'Ꝕ' => 'ꝕ', + 'Ꝗ' => 'ꝗ', + 'Ꝙ' => 'ꝙ', + 'Ꝛ' => 'ꝛ', + 'Ꝝ' => 'ꝝ', + 'Ꝟ' => 'ꝟ', + 'Ꝡ' => 'ꝡ', + 'Ꝣ' => 'ꝣ', + 'Ꝥ' => 'ꝥ', + 'Ꝧ' => 'ꝧ', + 'Ꝩ' => 'ꝩ', + 'Ꝫ' => 'ꝫ', + 'Ꝭ' => 'ꝭ', + 'Ꝯ' => 'ꝯ', + 'Ꝺ' => 'ꝺ', + 'Ꝼ' => 'ꝼ', + 'Ᵹ' => 'ᵹ', + 'Ꝿ' => 'ꝿ', + 'Ꞁ' => 'ꞁ', + 'Ꞃ' => 'ꞃ', + 'Ꞅ' => 'ꞅ', + 'Ꞇ' => 'ꞇ', + 'Ꞌ' => 'ꞌ', + 'Ɥ' => 'ɥ', + 'Ꞑ' => 'ꞑ', + 'Ꞓ' => 'ꞓ', + 'Ꞗ' => 'ꞗ', + 'Ꞙ' => 'ꞙ', + 'Ꞛ' => 'ꞛ', + 'Ꞝ' => 'ꞝ', + 'Ꞟ' => 'ꞟ', + 'Ꞡ' => 'ꞡ', + 'Ꞣ' => 'ꞣ', + 'Ꞥ' => 'ꞥ', + 'Ꞧ' => 'ꞧ', + 'Ꞩ' => 'ꞩ', + 'Ɦ' => 'ɦ', + 'Ɜ' => 'ɜ', + 'Ɡ' => 'ɡ', + 'Ɬ' => 'ɬ', + 'Ɪ' => 'ɪ', + 'Ʞ' => 'ʞ', + 'Ʇ' => 'ʇ', + 'Ʝ' => 'ʝ', + 'Ꭓ' => 'ꭓ', + 'Ꞵ' => 'ꞵ', + 'Ꞷ' => 'ꞷ', + 'Ꞹ' => 'ꞹ', + 'Ꞻ' => 'ꞻ', + 'Ꞽ' => 'ꞽ', + 'Ꞿ' => 'ꞿ', + 'Ꟃ' => 'ꟃ', + 'Ꞔ' => 'ꞔ', + 'Ʂ' => 'ʂ', + 'Ᶎ' => 'ᶎ', + 'Ꟈ' => 'ꟈ', + 'Ꟊ' => 'ꟊ', + 'Ꟶ' => 'ꟶ', + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + '𐐀' => '𐐨', + '𐐁' => '𐐩', + '𐐂' => '𐐪', + '𐐃' => '𐐫', + '𐐄' => '𐐬', + '𐐅' => '𐐭', + '𐐆' => '𐐮', + '𐐇' => '𐐯', + '𐐈' => '𐐰', + '𐐉' => '𐐱', + '𐐊' => '𐐲', + '𐐋' => '𐐳', + '𐐌' => '𐐴', + '𐐍' => '𐐵', + '𐐎' => '𐐶', + '𐐏' => '𐐷', + '𐐐' => '𐐸', + '𐐑' => '𐐹', + '𐐒' => '𐐺', + '𐐓' => '𐐻', + '𐐔' => '𐐼', + '𐐕' => '𐐽', + '𐐖' => '𐐾', + '𐐗' => '𐐿', + '𐐘' => '𐑀', + '𐐙' => '𐑁', + '𐐚' => '𐑂', + '𐐛' => '𐑃', + '𐐜' => '𐑄', + '𐐝' => '𐑅', + '𐐞' => '𐑆', + '𐐟' => '𐑇', + '𐐠' => '𐑈', + '𐐡' => '𐑉', + '𐐢' => '𐑊', + '𐐣' => '𐑋', + '𐐤' => '𐑌', + '𐐥' => '𐑍', + '𐐦' => '𐑎', + '𐐧' => '𐑏', + '𐒰' => '𐓘', + '𐒱' => '𐓙', + '𐒲' => '𐓚', + '𐒳' => '𐓛', + '𐒴' => '𐓜', + '𐒵' => '𐓝', + '𐒶' => '𐓞', + '𐒷' => '𐓟', + '𐒸' => '𐓠', + '𐒹' => '𐓡', + '𐒺' => '𐓢', + '𐒻' => '𐓣', + '𐒼' => '𐓤', + '𐒽' => '𐓥', + '𐒾' => '𐓦', + '𐒿' => '𐓧', + '𐓀' => '𐓨', + '𐓁' => '𐓩', + '𐓂' => '𐓪', + '𐓃' => '𐓫', + '𐓄' => '𐓬', + '𐓅' => '𐓭', + '𐓆' => '𐓮', + '𐓇' => '𐓯', + '𐓈' => '𐓰', + '𐓉' => '𐓱', + '𐓊' => '𐓲', + '𐓋' => '𐓳', + '𐓌' => '𐓴', + '𐓍' => '𐓵', + '𐓎' => '𐓶', + '𐓏' => '𐓷', + '𐓐' => '𐓸', + '𐓑' => '𐓹', + '𐓒' => '𐓺', + '𐓓' => '𐓻', + '𐲀' => '𐳀', + '𐲁' => '𐳁', + '𐲂' => '𐳂', + '𐲃' => '𐳃', + '𐲄' => '𐳄', + '𐲅' => '𐳅', + '𐲆' => '𐳆', + '𐲇' => '𐳇', + '𐲈' => '𐳈', + '𐲉' => '𐳉', + '𐲊' => '𐳊', + '𐲋' => '𐳋', + '𐲌' => '𐳌', + '𐲍' => '𐳍', + '𐲎' => '𐳎', + '𐲏' => '𐳏', + '𐲐' => '𐳐', + '𐲑' => '𐳑', + '𐲒' => '𐳒', + '𐲓' => '𐳓', + '𐲔' => '𐳔', + '𐲕' => '𐳕', + '𐲖' => '𐳖', + '𐲗' => '𐳗', + '𐲘' => '𐳘', + '𐲙' => '𐳙', + '𐲚' => '𐳚', + '𐲛' => '𐳛', + '𐲜' => '𐳜', + '𐲝' => '𐳝', + '𐲞' => '𐳞', + '𐲟' => '𐳟', + '𐲠' => '𐳠', + '𐲡' => '𐳡', + '𐲢' => '𐳢', + '𐲣' => '𐳣', + '𐲤' => '𐳤', + '𐲥' => '𐳥', + '𐲦' => '𐳦', + '𐲧' => '𐳧', + '𐲨' => '𐳨', + '𐲩' => '𐳩', + '𐲪' => '𐳪', + '𐲫' => '𐳫', + '𐲬' => '𐳬', + '𐲭' => '𐳭', + '𐲮' => '𐳮', + '𐲯' => '𐳯', + '𐲰' => '𐳰', + '𐲱' => '𐳱', + '𐲲' => '𐳲', + '𑢠' => '𑣀', + '𑢡' => '𑣁', + '𑢢' => '𑣂', + '𑢣' => '𑣃', + '𑢤' => '𑣄', + '𑢥' => '𑣅', + '𑢦' => '𑣆', + '𑢧' => '𑣇', + '𑢨' => '𑣈', + '𑢩' => '𑣉', + '𑢪' => '𑣊', + '𑢫' => '𑣋', + '𑢬' => '𑣌', + '𑢭' => '𑣍', + '𑢮' => '𑣎', + '𑢯' => '𑣏', + '𑢰' => '𑣐', + '𑢱' => '𑣑', + '𑢲' => '𑣒', + '𑢳' => '𑣓', + '𑢴' => '𑣔', + '𑢵' => '𑣕', + '𑢶' => '𑣖', + '𑢷' => '𑣗', + '𑢸' => '𑣘', + '𑢹' => '𑣙', + '𑢺' => '𑣚', + '𑢻' => '𑣛', + '𑢼' => '𑣜', + '𑢽' => '𑣝', + '𑢾' => '𑣞', + '𑢿' => '𑣟', + '𖹀' => '𖹠', + '𖹁' => '𖹡', + '𖹂' => '𖹢', + '𖹃' => '𖹣', + '𖹄' => '𖹤', + '𖹅' => '𖹥', + '𖹆' => '𖹦', + '𖹇' => '𖹧', + '𖹈' => '𖹨', + '𖹉' => '𖹩', + '𖹊' => '𖹪', + '𖹋' => '𖹫', + '𖹌' => '𖹬', + '𖹍' => '𖹭', + '𖹎' => '𖹮', + '𖹏' => '𖹯', + '𖹐' => '𖹰', + '𖹑' => '𖹱', + '𖹒' => '𖹲', + '𖹓' => '𖹳', + '𖹔' => '𖹴', + '𖹕' => '𖹵', + '𖹖' => '𖹶', + '𖹗' => '𖹷', + '𖹘' => '𖹸', + '𖹙' => '𖹹', + '𖹚' => '𖹺', + '𖹛' => '𖹻', + '𖹜' => '𖹼', + '𖹝' => '𖹽', + '𖹞' => '𖹾', + '𖹟' => '𖹿', + '𞤀' => '𞤢', + '𞤁' => '𞤣', + '𞤂' => '𞤤', + '𞤃' => '𞤥', + '𞤄' => '𞤦', + '𞤅' => '𞤧', + '𞤆' => '𞤨', + '𞤇' => '𞤩', + '𞤈' => '𞤪', + '𞤉' => '𞤫', + '𞤊' => '𞤬', + '𞤋' => '𞤭', + '𞤌' => '𞤮', + '𞤍' => '𞤯', + '𞤎' => '𞤰', + '𞤏' => '𞤱', + '𞤐' => '𞤲', + '𞤑' => '𞤳', + '𞤒' => '𞤴', + '𞤓' => '𞤵', + '𞤔' => '𞤶', + '𞤕' => '𞤷', + '𞤖' => '𞤸', + '𞤗' => '𞤹', + '𞤘' => '𞤺', + '𞤙' => '𞤻', + '𞤚' => '𞤼', + '𞤛' => '𞤽', + '𞤜' => '𞤾', + '𞤝' => '𞤿', + '𞤞' => '𞥀', + '𞤟' => '𞥁', + '𞤠' => '𞥂', + '𞤡' => '𞥃', +); diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php new file mode 100644 index 0000000..2a8f6e7 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php @@ -0,0 +1,5 @@ + 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + 'µ' => 'Μ', + 'à' => 'À', + 'á' => 'Á', + 'â' => 'Â', + 'ã' => 'Ã', + 'ä' => 'Ä', + 'å' => 'Å', + 'æ' => 'Æ', + 'ç' => 'Ç', + 'è' => 'È', + 'é' => 'É', + 'ê' => 'Ê', + 'ë' => 'Ë', + 'ì' => 'Ì', + 'í' => 'Í', + 'î' => 'Î', + 'ï' => 'Ï', + 'ð' => 'Ð', + 'ñ' => 'Ñ', + 'ò' => 'Ò', + 'ó' => 'Ó', + 'ô' => 'Ô', + 'õ' => 'Õ', + 'ö' => 'Ö', + 'ø' => 'Ø', + 'ù' => 'Ù', + 'ú' => 'Ú', + 'û' => 'Û', + 'ü' => 'Ü', + 'ý' => 'Ý', + 'þ' => 'Þ', + 'ÿ' => 'Ÿ', + 'ā' => 'Ā', + 'ă' => 'Ă', + 'ą' => 'Ą', + 'ć' => 'Ć', + 'ĉ' => 'Ĉ', + 'ċ' => 'Ċ', + 'č' => 'Č', + 'ď' => 'Ď', + 'đ' => 'Đ', + 'ē' => 'Ē', + 'ĕ' => 'Ĕ', + 'ė' => 'Ė', + 'ę' => 'Ę', + 'ě' => 'Ě', + 'ĝ' => 'Ĝ', + 'ğ' => 'Ğ', + 'ġ' => 'Ġ', + 'ģ' => 'Ģ', + 'ĥ' => 'Ĥ', + 'ħ' => 'Ħ', + 'ĩ' => 'Ĩ', + 'ī' => 'Ī', + 'ĭ' => 'Ĭ', + 'į' => 'Į', + 'ı' => 'I', + 'ij' => 'IJ', + 'ĵ' => 'Ĵ', + 'ķ' => 'Ķ', + 'ĺ' => 'Ĺ', + 'ļ' => 'Ļ', + 'ľ' => 'Ľ', + 'ŀ' => 'Ŀ', + 'ł' => 'Ł', + 'ń' => 'Ń', + 'ņ' => 'Ņ', + 'ň' => 'Ň', + 'ŋ' => 'Ŋ', + 'ō' => 'Ō', + 'ŏ' => 'Ŏ', + 'ő' => 'Ő', + 'œ' => 'Œ', + 'ŕ' => 'Ŕ', + 'ŗ' => 'Ŗ', + 'ř' => 'Ř', + 'ś' => 'Ś', + 'ŝ' => 'Ŝ', + 'ş' => 'Ş', + 'š' => 'Š', + 'ţ' => 'Ţ', + 'ť' => 'Ť', + 'ŧ' => 'Ŧ', + 'ũ' => 'Ũ', + 'ū' => 'Ū', + 'ŭ' => 'Ŭ', + 'ů' => 'Ů', + 'ű' => 'Ű', + 'ų' => 'Ų', + 'ŵ' => 'Ŵ', + 'ŷ' => 'Ŷ', + 'ź' => 'Ź', + 'ż' => 'Ż', + 'ž' => 'Ž', + 'ſ' => 'S', + 'ƀ' => 'Ƀ', + 'ƃ' => 'Ƃ', + 'ƅ' => 'Ƅ', + 'ƈ' => 'Ƈ', + 'ƌ' => 'Ƌ', + 'ƒ' => 'Ƒ', + 'ƕ' => 'Ƕ', + 'ƙ' => 'Ƙ', + 'ƚ' => 'Ƚ', + 'ƞ' => 'Ƞ', + 'ơ' => 'Ơ', + 'ƣ' => 'Ƣ', + 'ƥ' => 'Ƥ', + 'ƨ' => 'Ƨ', + 'ƭ' => 'Ƭ', + 'ư' => 'Ư', + 'ƴ' => 'Ƴ', + 'ƶ' => 'Ƶ', + 'ƹ' => 'Ƹ', + 'ƽ' => 'Ƽ', + 'ƿ' => 'Ƿ', + 'Dž' => 'DŽ', + 'dž' => 'DŽ', + 'Lj' => 'LJ', + 'lj' => 'LJ', + 'Nj' => 'NJ', + 'nj' => 'NJ', + 'ǎ' => 'Ǎ', + 'ǐ' => 'Ǐ', + 'ǒ' => 'Ǒ', + 'ǔ' => 'Ǔ', + 'ǖ' => 'Ǖ', + 'ǘ' => 'Ǘ', + 'ǚ' => 'Ǚ', + 'ǜ' => 'Ǜ', + 'ǝ' => 'Ǝ', + 'ǟ' => 'Ǟ', + 'ǡ' => 'Ǡ', + 'ǣ' => 'Ǣ', + 'ǥ' => 'Ǥ', + 'ǧ' => 'Ǧ', + 'ǩ' => 'Ǩ', + 'ǫ' => 'Ǫ', + 'ǭ' => 'Ǭ', + 'ǯ' => 'Ǯ', + 'Dz' => 'DZ', + 'dz' => 'DZ', + 'ǵ' => 'Ǵ', + 'ǹ' => 'Ǹ', + 'ǻ' => 'Ǻ', + 'ǽ' => 'Ǽ', + 'ǿ' => 'Ǿ', + 'ȁ' => 'Ȁ', + 'ȃ' => 'Ȃ', + 'ȅ' => 'Ȅ', + 'ȇ' => 'Ȇ', + 'ȉ' => 'Ȉ', + 'ȋ' => 'Ȋ', + 'ȍ' => 'Ȍ', + 'ȏ' => 'Ȏ', + 'ȑ' => 'Ȑ', + 'ȓ' => 'Ȓ', + 'ȕ' => 'Ȕ', + 'ȗ' => 'Ȗ', + 'ș' => 'Ș', + 'ț' => 'Ț', + 'ȝ' => 'Ȝ', + 'ȟ' => 'Ȟ', + 'ȣ' => 'Ȣ', + 'ȥ' => 'Ȥ', + 'ȧ' => 'Ȧ', + 'ȩ' => 'Ȩ', + 'ȫ' => 'Ȫ', + 'ȭ' => 'Ȭ', + 'ȯ' => 'Ȯ', + 'ȱ' => 'Ȱ', + 'ȳ' => 'Ȳ', + 'ȼ' => 'Ȼ', + 'ȿ' => 'Ȿ', + 'ɀ' => 'Ɀ', + 'ɂ' => 'Ɂ', + 'ɇ' => 'Ɇ', + 'ɉ' => 'Ɉ', + 'ɋ' => 'Ɋ', + 'ɍ' => 'Ɍ', + 'ɏ' => 'Ɏ', + 'ɐ' => 'Ɐ', + 'ɑ' => 'Ɑ', + 'ɒ' => 'Ɒ', + 'ɓ' => 'Ɓ', + 'ɔ' => 'Ɔ', + 'ɖ' => 'Ɖ', + 'ɗ' => 'Ɗ', + 'ə' => 'Ə', + 'ɛ' => 'Ɛ', + 'ɜ' => 'Ɜ', + 'ɠ' => 'Ɠ', + 'ɡ' => 'Ɡ', + 'ɣ' => 'Ɣ', + 'ɥ' => 'Ɥ', + 'ɦ' => 'Ɦ', + 'ɨ' => 'Ɨ', + 'ɩ' => 'Ɩ', + 'ɪ' => 'Ɪ', + 'ɫ' => 'Ɫ', + 'ɬ' => 'Ɬ', + 'ɯ' => 'Ɯ', + 'ɱ' => 'Ɱ', + 'ɲ' => 'Ɲ', + 'ɵ' => 'Ɵ', + 'ɽ' => 'Ɽ', + 'ʀ' => 'Ʀ', + 'ʂ' => 'Ʂ', + 'ʃ' => 'Ʃ', + 'ʇ' => 'Ʇ', + 'ʈ' => 'Ʈ', + 'ʉ' => 'Ʉ', + 'ʊ' => 'Ʊ', + 'ʋ' => 'Ʋ', + 'ʌ' => 'Ʌ', + 'ʒ' => 'Ʒ', + 'ʝ' => 'Ʝ', + 'ʞ' => 'Ʞ', + 'ͅ' => 'Ι', + 'ͱ' => 'Ͱ', + 'ͳ' => 'Ͳ', + 'ͷ' => 'Ͷ', + 'ͻ' => 'Ͻ', + 'ͼ' => 'Ͼ', + 'ͽ' => 'Ͽ', + 'ά' => 'Ά', + 'έ' => 'Έ', + 'ή' => 'Ή', + 'ί' => 'Ί', + 'α' => 'Α', + 'β' => 'Β', + 'γ' => 'Γ', + 'δ' => 'Δ', + 'ε' => 'Ε', + 'ζ' => 'Ζ', + 'η' => 'Η', + 'θ' => 'Θ', + 'ι' => 'Ι', + 'κ' => 'Κ', + 'λ' => 'Λ', + 'μ' => 'Μ', + 'ν' => 'Ν', + 'ξ' => 'Ξ', + 'ο' => 'Ο', + 'π' => 'Π', + 'ρ' => 'Ρ', + 'ς' => 'Σ', + 'σ' => 'Σ', + 'τ' => 'Τ', + 'υ' => 'Υ', + 'φ' => 'Φ', + 'χ' => 'Χ', + 'ψ' => 'Ψ', + 'ω' => 'Ω', + 'ϊ' => 'Ϊ', + 'ϋ' => 'Ϋ', + 'ό' => 'Ό', + 'ύ' => 'Ύ', + 'ώ' => 'Ώ', + 'ϐ' => 'Β', + 'ϑ' => 'Θ', + 'ϕ' => 'Φ', + 'ϖ' => 'Π', + 'ϗ' => 'Ϗ', + 'ϙ' => 'Ϙ', + 'ϛ' => 'Ϛ', + 'ϝ' => 'Ϝ', + 'ϟ' => 'Ϟ', + 'ϡ' => 'Ϡ', + 'ϣ' => 'Ϣ', + 'ϥ' => 'Ϥ', + 'ϧ' => 'Ϧ', + 'ϩ' => 'Ϩ', + 'ϫ' => 'Ϫ', + 'ϭ' => 'Ϭ', + 'ϯ' => 'Ϯ', + 'ϰ' => 'Κ', + 'ϱ' => 'Ρ', + 'ϲ' => 'Ϲ', + 'ϳ' => 'Ϳ', + 'ϵ' => 'Ε', + 'ϸ' => 'Ϸ', + 'ϻ' => 'Ϻ', + 'а' => 'А', + 'б' => 'Б', + 'в' => 'В', + 'г' => 'Г', + 'д' => 'Д', + 'е' => 'Е', + 'ж' => 'Ж', + 'з' => 'З', + 'и' => 'И', + 'й' => 'Й', + 'к' => 'К', + 'л' => 'Л', + 'м' => 'М', + 'н' => 'Н', + 'о' => 'О', + 'п' => 'П', + 'р' => 'Р', + 'с' => 'С', + 'т' => 'Т', + 'у' => 'У', + 'ф' => 'Ф', + 'х' => 'Х', + 'ц' => 'Ц', + 'ч' => 'Ч', + 'ш' => 'Ш', + 'щ' => 'Щ', + 'ъ' => 'Ъ', + 'ы' => 'Ы', + 'ь' => 'Ь', + 'э' => 'Э', + 'ю' => 'Ю', + 'я' => 'Я', + 'ѐ' => 'Ѐ', + 'ё' => 'Ё', + 'ђ' => 'Ђ', + 'ѓ' => 'Ѓ', + 'є' => 'Є', + 'ѕ' => 'Ѕ', + 'і' => 'І', + 'ї' => 'Ї', + 'ј' => 'Ј', + 'љ' => 'Љ', + 'њ' => 'Њ', + 'ћ' => 'Ћ', + 'ќ' => 'Ќ', + 'ѝ' => 'Ѝ', + 'ў' => 'Ў', + 'џ' => 'Џ', + 'ѡ' => 'Ѡ', + 'ѣ' => 'Ѣ', + 'ѥ' => 'Ѥ', + 'ѧ' => 'Ѧ', + 'ѩ' => 'Ѩ', + 'ѫ' => 'Ѫ', + 'ѭ' => 'Ѭ', + 'ѯ' => 'Ѯ', + 'ѱ' => 'Ѱ', + 'ѳ' => 'Ѳ', + 'ѵ' => 'Ѵ', + 'ѷ' => 'Ѷ', + 'ѹ' => 'Ѹ', + 'ѻ' => 'Ѻ', + 'ѽ' => 'Ѽ', + 'ѿ' => 'Ѿ', + 'ҁ' => 'Ҁ', + 'ҋ' => 'Ҋ', + 'ҍ' => 'Ҍ', + 'ҏ' => 'Ҏ', + 'ґ' => 'Ґ', + 'ғ' => 'Ғ', + 'ҕ' => 'Ҕ', + 'җ' => 'Җ', + 'ҙ' => 'Ҙ', + 'қ' => 'Қ', + 'ҝ' => 'Ҝ', + 'ҟ' => 'Ҟ', + 'ҡ' => 'Ҡ', + 'ң' => 'Ң', + 'ҥ' => 'Ҥ', + 'ҧ' => 'Ҧ', + 'ҩ' => 'Ҩ', + 'ҫ' => 'Ҫ', + 'ҭ' => 'Ҭ', + 'ү' => 'Ү', + 'ұ' => 'Ұ', + 'ҳ' => 'Ҳ', + 'ҵ' => 'Ҵ', + 'ҷ' => 'Ҷ', + 'ҹ' => 'Ҹ', + 'һ' => 'Һ', + 'ҽ' => 'Ҽ', + 'ҿ' => 'Ҿ', + 'ӂ' => 'Ӂ', + 'ӄ' => 'Ӄ', + 'ӆ' => 'Ӆ', + 'ӈ' => 'Ӈ', + 'ӊ' => 'Ӊ', + 'ӌ' => 'Ӌ', + 'ӎ' => 'Ӎ', + 'ӏ' => 'Ӏ', + 'ӑ' => 'Ӑ', + 'ӓ' => 'Ӓ', + 'ӕ' => 'Ӕ', + 'ӗ' => 'Ӗ', + 'ә' => 'Ә', + 'ӛ' => 'Ӛ', + 'ӝ' => 'Ӝ', + 'ӟ' => 'Ӟ', + 'ӡ' => 'Ӡ', + 'ӣ' => 'Ӣ', + 'ӥ' => 'Ӥ', + 'ӧ' => 'Ӧ', + 'ө' => 'Ө', + 'ӫ' => 'Ӫ', + 'ӭ' => 'Ӭ', + 'ӯ' => 'Ӯ', + 'ӱ' => 'Ӱ', + 'ӳ' => 'Ӳ', + 'ӵ' => 'Ӵ', + 'ӷ' => 'Ӷ', + 'ӹ' => 'Ӹ', + 'ӻ' => 'Ӻ', + 'ӽ' => 'Ӽ', + 'ӿ' => 'Ӿ', + 'ԁ' => 'Ԁ', + 'ԃ' => 'Ԃ', + 'ԅ' => 'Ԅ', + 'ԇ' => 'Ԇ', + 'ԉ' => 'Ԉ', + 'ԋ' => 'Ԋ', + 'ԍ' => 'Ԍ', + 'ԏ' => 'Ԏ', + 'ԑ' => 'Ԑ', + 'ԓ' => 'Ԓ', + 'ԕ' => 'Ԕ', + 'ԗ' => 'Ԗ', + 'ԙ' => 'Ԙ', + 'ԛ' => 'Ԛ', + 'ԝ' => 'Ԝ', + 'ԟ' => 'Ԟ', + 'ԡ' => 'Ԡ', + 'ԣ' => 'Ԣ', + 'ԥ' => 'Ԥ', + 'ԧ' => 'Ԧ', + 'ԩ' => 'Ԩ', + 'ԫ' => 'Ԫ', + 'ԭ' => 'Ԭ', + 'ԯ' => 'Ԯ', + 'ա' => 'Ա', + 'բ' => 'Բ', + 'գ' => 'Գ', + 'դ' => 'Դ', + 'ե' => 'Ե', + 'զ' => 'Զ', + 'է' => 'Է', + 'ը' => 'Ը', + 'թ' => 'Թ', + 'ժ' => 'Ժ', + 'ի' => 'Ի', + 'լ' => 'Լ', + 'խ' => 'Խ', + 'ծ' => 'Ծ', + 'կ' => 'Կ', + 'հ' => 'Հ', + 'ձ' => 'Ձ', + 'ղ' => 'Ղ', + 'ճ' => 'Ճ', + 'մ' => 'Մ', + 'յ' => 'Յ', + 'ն' => 'Ն', + 'շ' => 'Շ', + 'ո' => 'Ո', + 'չ' => 'Չ', + 'պ' => 'Պ', + 'ջ' => 'Ջ', + 'ռ' => 'Ռ', + 'ս' => 'Ս', + 'վ' => 'Վ', + 'տ' => 'Տ', + 'ր' => 'Ր', + 'ց' => 'Ց', + 'ւ' => 'Ւ', + 'փ' => 'Փ', + 'ք' => 'Ք', + 'օ' => 'Օ', + 'ֆ' => 'Ֆ', + 'ა' => 'Ა', + 'ბ' => 'Ბ', + 'გ' => 'Გ', + 'დ' => 'Დ', + 'ე' => 'Ე', + 'ვ' => 'Ვ', + 'ზ' => 'Ზ', + 'თ' => 'Თ', + 'ი' => 'Ი', + 'კ' => 'Კ', + 'ლ' => 'Ლ', + 'მ' => 'Მ', + 'ნ' => 'Ნ', + 'ო' => 'Ო', + 'პ' => 'Პ', + 'ჟ' => 'Ჟ', + 'რ' => 'Რ', + 'ს' => 'Ს', + 'ტ' => 'Ტ', + 'უ' => 'Უ', + 'ფ' => 'Ფ', + 'ქ' => 'Ქ', + 'ღ' => 'Ღ', + 'ყ' => 'Ყ', + 'შ' => 'Შ', + 'ჩ' => 'Ჩ', + 'ც' => 'Ც', + 'ძ' => 'Ძ', + 'წ' => 'Წ', + 'ჭ' => 'Ჭ', + 'ხ' => 'Ხ', + 'ჯ' => 'Ჯ', + 'ჰ' => 'Ჰ', + 'ჱ' => 'Ჱ', + 'ჲ' => 'Ჲ', + 'ჳ' => 'Ჳ', + 'ჴ' => 'Ჴ', + 'ჵ' => 'Ჵ', + 'ჶ' => 'Ჶ', + 'ჷ' => 'Ჷ', + 'ჸ' => 'Ჸ', + 'ჹ' => 'Ჹ', + 'ჺ' => 'Ჺ', + 'ჽ' => 'Ჽ', + 'ჾ' => 'Ჾ', + 'ჿ' => 'Ჿ', + 'ᏸ' => 'Ᏸ', + 'ᏹ' => 'Ᏹ', + 'ᏺ' => 'Ᏺ', + 'ᏻ' => 'Ᏻ', + 'ᏼ' => 'Ᏼ', + 'ᏽ' => 'Ᏽ', + 'ᲀ' => 'В', + 'ᲁ' => 'Д', + 'ᲂ' => 'О', + 'ᲃ' => 'С', + 'ᲄ' => 'Т', + 'ᲅ' => 'Т', + 'ᲆ' => 'Ъ', + 'ᲇ' => 'Ѣ', + 'ᲈ' => 'Ꙋ', + 'ᵹ' => 'Ᵹ', + 'ᵽ' => 'Ᵽ', + 'ᶎ' => 'Ᶎ', + 'ḁ' => 'Ḁ', + 'ḃ' => 'Ḃ', + 'ḅ' => 'Ḅ', + 'ḇ' => 'Ḇ', + 'ḉ' => 'Ḉ', + 'ḋ' => 'Ḋ', + 'ḍ' => 'Ḍ', + 'ḏ' => 'Ḏ', + 'ḑ' => 'Ḑ', + 'ḓ' => 'Ḓ', + 'ḕ' => 'Ḕ', + 'ḗ' => 'Ḗ', + 'ḙ' => 'Ḙ', + 'ḛ' => 'Ḛ', + 'ḝ' => 'Ḝ', + 'ḟ' => 'Ḟ', + 'ḡ' => 'Ḡ', + 'ḣ' => 'Ḣ', + 'ḥ' => 'Ḥ', + 'ḧ' => 'Ḧ', + 'ḩ' => 'Ḩ', + 'ḫ' => 'Ḫ', + 'ḭ' => 'Ḭ', + 'ḯ' => 'Ḯ', + 'ḱ' => 'Ḱ', + 'ḳ' => 'Ḳ', + 'ḵ' => 'Ḵ', + 'ḷ' => 'Ḷ', + 'ḹ' => 'Ḹ', + 'ḻ' => 'Ḻ', + 'ḽ' => 'Ḽ', + 'ḿ' => 'Ḿ', + 'ṁ' => 'Ṁ', + 'ṃ' => 'Ṃ', + 'ṅ' => 'Ṅ', + 'ṇ' => 'Ṇ', + 'ṉ' => 'Ṉ', + 'ṋ' => 'Ṋ', + 'ṍ' => 'Ṍ', + 'ṏ' => 'Ṏ', + 'ṑ' => 'Ṑ', + 'ṓ' => 'Ṓ', + 'ṕ' => 'Ṕ', + 'ṗ' => 'Ṗ', + 'ṙ' => 'Ṙ', + 'ṛ' => 'Ṛ', + 'ṝ' => 'Ṝ', + 'ṟ' => 'Ṟ', + 'ṡ' => 'Ṡ', + 'ṣ' => 'Ṣ', + 'ṥ' => 'Ṥ', + 'ṧ' => 'Ṧ', + 'ṩ' => 'Ṩ', + 'ṫ' => 'Ṫ', + 'ṭ' => 'Ṭ', + 'ṯ' => 'Ṯ', + 'ṱ' => 'Ṱ', + 'ṳ' => 'Ṳ', + 'ṵ' => 'Ṵ', + 'ṷ' => 'Ṷ', + 'ṹ' => 'Ṹ', + 'ṻ' => 'Ṻ', + 'ṽ' => 'Ṽ', + 'ṿ' => 'Ṿ', + 'ẁ' => 'Ẁ', + 'ẃ' => 'Ẃ', + 'ẅ' => 'Ẅ', + 'ẇ' => 'Ẇ', + 'ẉ' => 'Ẉ', + 'ẋ' => 'Ẋ', + 'ẍ' => 'Ẍ', + 'ẏ' => 'Ẏ', + 'ẑ' => 'Ẑ', + 'ẓ' => 'Ẓ', + 'ẕ' => 'Ẕ', + 'ẛ' => 'Ṡ', + 'ạ' => 'Ạ', + 'ả' => 'Ả', + 'ấ' => 'Ấ', + 'ầ' => 'Ầ', + 'ẩ' => 'Ẩ', + 'ẫ' => 'Ẫ', + 'ậ' => 'Ậ', + 'ắ' => 'Ắ', + 'ằ' => 'Ằ', + 'ẳ' => 'Ẳ', + 'ẵ' => 'Ẵ', + 'ặ' => 'Ặ', + 'ẹ' => 'Ẹ', + 'ẻ' => 'Ẻ', + 'ẽ' => 'Ẽ', + 'ế' => 'Ế', + 'ề' => 'Ề', + 'ể' => 'Ể', + 'ễ' => 'Ễ', + 'ệ' => 'Ệ', + 'ỉ' => 'Ỉ', + 'ị' => 'Ị', + 'ọ' => 'Ọ', + 'ỏ' => 'Ỏ', + 'ố' => 'Ố', + 'ồ' => 'Ồ', + 'ổ' => 'Ổ', + 'ỗ' => 'Ỗ', + 'ộ' => 'Ộ', + 'ớ' => 'Ớ', + 'ờ' => 'Ờ', + 'ở' => 'Ở', + 'ỡ' => 'Ỡ', + 'ợ' => 'Ợ', + 'ụ' => 'Ụ', + 'ủ' => 'Ủ', + 'ứ' => 'Ứ', + 'ừ' => 'Ừ', + 'ử' => 'Ử', + 'ữ' => 'Ữ', + 'ự' => 'Ự', + 'ỳ' => 'Ỳ', + 'ỵ' => 'Ỵ', + 'ỷ' => 'Ỷ', + 'ỹ' => 'Ỹ', + 'ỻ' => 'Ỻ', + 'ỽ' => 'Ỽ', + 'ỿ' => 'Ỿ', + 'ἀ' => 'Ἀ', + 'ἁ' => 'Ἁ', + 'ἂ' => 'Ἂ', + 'ἃ' => 'Ἃ', + 'ἄ' => 'Ἄ', + 'ἅ' => 'Ἅ', + 'ἆ' => 'Ἆ', + 'ἇ' => 'Ἇ', + 'ἐ' => 'Ἐ', + 'ἑ' => 'Ἑ', + 'ἒ' => 'Ἒ', + 'ἓ' => 'Ἓ', + 'ἔ' => 'Ἔ', + 'ἕ' => 'Ἕ', + 'ἠ' => 'Ἠ', + 'ἡ' => 'Ἡ', + 'ἢ' => 'Ἢ', + 'ἣ' => 'Ἣ', + 'ἤ' => 'Ἤ', + 'ἥ' => 'Ἥ', + 'ἦ' => 'Ἦ', + 'ἧ' => 'Ἧ', + 'ἰ' => 'Ἰ', + 'ἱ' => 'Ἱ', + 'ἲ' => 'Ἲ', + 'ἳ' => 'Ἳ', + 'ἴ' => 'Ἴ', + 'ἵ' => 'Ἵ', + 'ἶ' => 'Ἶ', + 'ἷ' => 'Ἷ', + 'ὀ' => 'Ὀ', + 'ὁ' => 'Ὁ', + 'ὂ' => 'Ὂ', + 'ὃ' => 'Ὃ', + 'ὄ' => 'Ὄ', + 'ὅ' => 'Ὅ', + 'ὑ' => 'Ὑ', + 'ὓ' => 'Ὓ', + 'ὕ' => 'Ὕ', + 'ὗ' => 'Ὗ', + 'ὠ' => 'Ὠ', + 'ὡ' => 'Ὡ', + 'ὢ' => 'Ὢ', + 'ὣ' => 'Ὣ', + 'ὤ' => 'Ὤ', + 'ὥ' => 'Ὥ', + 'ὦ' => 'Ὦ', + 'ὧ' => 'Ὧ', + 'ὰ' => 'Ὰ', + 'ά' => 'Ά', + 'ὲ' => 'Ὲ', + 'έ' => 'Έ', + 'ὴ' => 'Ὴ', + 'ή' => 'Ή', + 'ὶ' => 'Ὶ', + 'ί' => 'Ί', + 'ὸ' => 'Ὸ', + 'ό' => 'Ό', + 'ὺ' => 'Ὺ', + 'ύ' => 'Ύ', + 'ὼ' => 'Ὼ', + 'ώ' => 'Ώ', + 'ᾀ' => 'ἈΙ', + 'ᾁ' => 'ἉΙ', + 'ᾂ' => 'ἊΙ', + 'ᾃ' => 'ἋΙ', + 'ᾄ' => 'ἌΙ', + 'ᾅ' => 'ἍΙ', + 'ᾆ' => 'ἎΙ', + 'ᾇ' => 'ἏΙ', + 'ᾐ' => 'ἨΙ', + 'ᾑ' => 'ἩΙ', + 'ᾒ' => 'ἪΙ', + 'ᾓ' => 'ἫΙ', + 'ᾔ' => 'ἬΙ', + 'ᾕ' => 'ἭΙ', + 'ᾖ' => 'ἮΙ', + 'ᾗ' => 'ἯΙ', + 'ᾠ' => 'ὨΙ', + 'ᾡ' => 'ὩΙ', + 'ᾢ' => 'ὪΙ', + 'ᾣ' => 'ὫΙ', + 'ᾤ' => 'ὬΙ', + 'ᾥ' => 'ὭΙ', + 'ᾦ' => 'ὮΙ', + 'ᾧ' => 'ὯΙ', + 'ᾰ' => 'Ᾰ', + 'ᾱ' => 'Ᾱ', + 'ᾳ' => 'ΑΙ', + 'ι' => 'Ι', + 'ῃ' => 'ΗΙ', + 'ῐ' => 'Ῐ', + 'ῑ' => 'Ῑ', + 'ῠ' => 'Ῠ', + 'ῡ' => 'Ῡ', + 'ῥ' => 'Ῥ', + 'ῳ' => 'ΩΙ', + 'ⅎ' => 'Ⅎ', + 'ⅰ' => 'Ⅰ', + 'ⅱ' => 'Ⅱ', + 'ⅲ' => 'Ⅲ', + 'ⅳ' => 'Ⅳ', + 'ⅴ' => 'Ⅴ', + 'ⅵ' => 'Ⅵ', + 'ⅶ' => 'Ⅶ', + 'ⅷ' => 'Ⅷ', + 'ⅸ' => 'Ⅸ', + 'ⅹ' => 'Ⅹ', + 'ⅺ' => 'Ⅺ', + 'ⅻ' => 'Ⅻ', + 'ⅼ' => 'Ⅼ', + 'ⅽ' => 'Ⅽ', + 'ⅾ' => 'Ⅾ', + 'ⅿ' => 'Ⅿ', + 'ↄ' => 'Ↄ', + 'ⓐ' => 'Ⓐ', + 'ⓑ' => 'Ⓑ', + 'ⓒ' => 'Ⓒ', + 'ⓓ' => 'Ⓓ', + 'ⓔ' => 'Ⓔ', + 'ⓕ' => 'Ⓕ', + 'ⓖ' => 'Ⓖ', + 'ⓗ' => 'Ⓗ', + 'ⓘ' => 'Ⓘ', + 'ⓙ' => 'Ⓙ', + 'ⓚ' => 'Ⓚ', + 'ⓛ' => 'Ⓛ', + 'ⓜ' => 'Ⓜ', + 'ⓝ' => 'Ⓝ', + 'ⓞ' => 'Ⓞ', + 'ⓟ' => 'Ⓟ', + 'ⓠ' => 'Ⓠ', + 'ⓡ' => 'Ⓡ', + 'ⓢ' => 'Ⓢ', + 'ⓣ' => 'Ⓣ', + 'ⓤ' => 'Ⓤ', + 'ⓥ' => 'Ⓥ', + 'ⓦ' => 'Ⓦ', + 'ⓧ' => 'Ⓧ', + 'ⓨ' => 'Ⓨ', + 'ⓩ' => 'Ⓩ', + 'ⰰ' => 'Ⰰ', + 'ⰱ' => 'Ⰱ', + 'ⰲ' => 'Ⰲ', + 'ⰳ' => 'Ⰳ', + 'ⰴ' => 'Ⰴ', + 'ⰵ' => 'Ⰵ', + 'ⰶ' => 'Ⰶ', + 'ⰷ' => 'Ⰷ', + 'ⰸ' => 'Ⰸ', + 'ⰹ' => 'Ⰹ', + 'ⰺ' => 'Ⰺ', + 'ⰻ' => 'Ⰻ', + 'ⰼ' => 'Ⰼ', + 'ⰽ' => 'Ⰽ', + 'ⰾ' => 'Ⰾ', + 'ⰿ' => 'Ⰿ', + 'ⱀ' => 'Ⱀ', + 'ⱁ' => 'Ⱁ', + 'ⱂ' => 'Ⱂ', + 'ⱃ' => 'Ⱃ', + 'ⱄ' => 'Ⱄ', + 'ⱅ' => 'Ⱅ', + 'ⱆ' => 'Ⱆ', + 'ⱇ' => 'Ⱇ', + 'ⱈ' => 'Ⱈ', + 'ⱉ' => 'Ⱉ', + 'ⱊ' => 'Ⱊ', + 'ⱋ' => 'Ⱋ', + 'ⱌ' => 'Ⱌ', + 'ⱍ' => 'Ⱍ', + 'ⱎ' => 'Ⱎ', + 'ⱏ' => 'Ⱏ', + 'ⱐ' => 'Ⱐ', + 'ⱑ' => 'Ⱑ', + 'ⱒ' => 'Ⱒ', + 'ⱓ' => 'Ⱓ', + 'ⱔ' => 'Ⱔ', + 'ⱕ' => 'Ⱕ', + 'ⱖ' => 'Ⱖ', + 'ⱗ' => 'Ⱗ', + 'ⱘ' => 'Ⱘ', + 'ⱙ' => 'Ⱙ', + 'ⱚ' => 'Ⱚ', + 'ⱛ' => 'Ⱛ', + 'ⱜ' => 'Ⱜ', + 'ⱝ' => 'Ⱝ', + 'ⱞ' => 'Ⱞ', + 'ⱡ' => 'Ⱡ', + 'ⱥ' => 'Ⱥ', + 'ⱦ' => 'Ⱦ', + 'ⱨ' => 'Ⱨ', + 'ⱪ' => 'Ⱪ', + 'ⱬ' => 'Ⱬ', + 'ⱳ' => 'Ⱳ', + 'ⱶ' => 'Ⱶ', + 'ⲁ' => 'Ⲁ', + 'ⲃ' => 'Ⲃ', + 'ⲅ' => 'Ⲅ', + 'ⲇ' => 'Ⲇ', + 'ⲉ' => 'Ⲉ', + 'ⲋ' => 'Ⲋ', + 'ⲍ' => 'Ⲍ', + 'ⲏ' => 'Ⲏ', + 'ⲑ' => 'Ⲑ', + 'ⲓ' => 'Ⲓ', + 'ⲕ' => 'Ⲕ', + 'ⲗ' => 'Ⲗ', + 'ⲙ' => 'Ⲙ', + 'ⲛ' => 'Ⲛ', + 'ⲝ' => 'Ⲝ', + 'ⲟ' => 'Ⲟ', + 'ⲡ' => 'Ⲡ', + 'ⲣ' => 'Ⲣ', + 'ⲥ' => 'Ⲥ', + 'ⲧ' => 'Ⲧ', + 'ⲩ' => 'Ⲩ', + 'ⲫ' => 'Ⲫ', + 'ⲭ' => 'Ⲭ', + 'ⲯ' => 'Ⲯ', + 'ⲱ' => 'Ⲱ', + 'ⲳ' => 'Ⲳ', + 'ⲵ' => 'Ⲵ', + 'ⲷ' => 'Ⲷ', + 'ⲹ' => 'Ⲹ', + 'ⲻ' => 'Ⲻ', + 'ⲽ' => 'Ⲽ', + 'ⲿ' => 'Ⲿ', + 'ⳁ' => 'Ⳁ', + 'ⳃ' => 'Ⳃ', + 'ⳅ' => 'Ⳅ', + 'ⳇ' => 'Ⳇ', + 'ⳉ' => 'Ⳉ', + 'ⳋ' => 'Ⳋ', + 'ⳍ' => 'Ⳍ', + 'ⳏ' => 'Ⳏ', + 'ⳑ' => 'Ⳑ', + 'ⳓ' => 'Ⳓ', + 'ⳕ' => 'Ⳕ', + 'ⳗ' => 'Ⳗ', + 'ⳙ' => 'Ⳙ', + 'ⳛ' => 'Ⳛ', + 'ⳝ' => 'Ⳝ', + 'ⳟ' => 'Ⳟ', + 'ⳡ' => 'Ⳡ', + 'ⳣ' => 'Ⳣ', + 'ⳬ' => 'Ⳬ', + 'ⳮ' => 'Ⳮ', + 'ⳳ' => 'Ⳳ', + 'ⴀ' => 'Ⴀ', + 'ⴁ' => 'Ⴁ', + 'ⴂ' => 'Ⴂ', + 'ⴃ' => 'Ⴃ', + 'ⴄ' => 'Ⴄ', + 'ⴅ' => 'Ⴅ', + 'ⴆ' => 'Ⴆ', + 'ⴇ' => 'Ⴇ', + 'ⴈ' => 'Ⴈ', + 'ⴉ' => 'Ⴉ', + 'ⴊ' => 'Ⴊ', + 'ⴋ' => 'Ⴋ', + 'ⴌ' => 'Ⴌ', + 'ⴍ' => 'Ⴍ', + 'ⴎ' => 'Ⴎ', + 'ⴏ' => 'Ⴏ', + 'ⴐ' => 'Ⴐ', + 'ⴑ' => 'Ⴑ', + 'ⴒ' => 'Ⴒ', + 'ⴓ' => 'Ⴓ', + 'ⴔ' => 'Ⴔ', + 'ⴕ' => 'Ⴕ', + 'ⴖ' => 'Ⴖ', + 'ⴗ' => 'Ⴗ', + 'ⴘ' => 'Ⴘ', + 'ⴙ' => 'Ⴙ', + 'ⴚ' => 'Ⴚ', + 'ⴛ' => 'Ⴛ', + 'ⴜ' => 'Ⴜ', + 'ⴝ' => 'Ⴝ', + 'ⴞ' => 'Ⴞ', + 'ⴟ' => 'Ⴟ', + 'ⴠ' => 'Ⴠ', + 'ⴡ' => 'Ⴡ', + 'ⴢ' => 'Ⴢ', + 'ⴣ' => 'Ⴣ', + 'ⴤ' => 'Ⴤ', + 'ⴥ' => 'Ⴥ', + 'ⴧ' => 'Ⴧ', + 'ⴭ' => 'Ⴭ', + 'ꙁ' => 'Ꙁ', + 'ꙃ' => 'Ꙃ', + 'ꙅ' => 'Ꙅ', + 'ꙇ' => 'Ꙇ', + 'ꙉ' => 'Ꙉ', + 'ꙋ' => 'Ꙋ', + 'ꙍ' => 'Ꙍ', + 'ꙏ' => 'Ꙏ', + 'ꙑ' => 'Ꙑ', + 'ꙓ' => 'Ꙓ', + 'ꙕ' => 'Ꙕ', + 'ꙗ' => 'Ꙗ', + 'ꙙ' => 'Ꙙ', + 'ꙛ' => 'Ꙛ', + 'ꙝ' => 'Ꙝ', + 'ꙟ' => 'Ꙟ', + 'ꙡ' => 'Ꙡ', + 'ꙣ' => 'Ꙣ', + 'ꙥ' => 'Ꙥ', + 'ꙧ' => 'Ꙧ', + 'ꙩ' => 'Ꙩ', + 'ꙫ' => 'Ꙫ', + 'ꙭ' => 'Ꙭ', + 'ꚁ' => 'Ꚁ', + 'ꚃ' => 'Ꚃ', + 'ꚅ' => 'Ꚅ', + 'ꚇ' => 'Ꚇ', + 'ꚉ' => 'Ꚉ', + 'ꚋ' => 'Ꚋ', + 'ꚍ' => 'Ꚍ', + 'ꚏ' => 'Ꚏ', + 'ꚑ' => 'Ꚑ', + 'ꚓ' => 'Ꚓ', + 'ꚕ' => 'Ꚕ', + 'ꚗ' => 'Ꚗ', + 'ꚙ' => 'Ꚙ', + 'ꚛ' => 'Ꚛ', + 'ꜣ' => 'Ꜣ', + 'ꜥ' => 'Ꜥ', + 'ꜧ' => 'Ꜧ', + 'ꜩ' => 'Ꜩ', + 'ꜫ' => 'Ꜫ', + 'ꜭ' => 'Ꜭ', + 'ꜯ' => 'Ꜯ', + 'ꜳ' => 'Ꜳ', + 'ꜵ' => 'Ꜵ', + 'ꜷ' => 'Ꜷ', + 'ꜹ' => 'Ꜹ', + 'ꜻ' => 'Ꜻ', + 'ꜽ' => 'Ꜽ', + 'ꜿ' => 'Ꜿ', + 'ꝁ' => 'Ꝁ', + 'ꝃ' => 'Ꝃ', + 'ꝅ' => 'Ꝅ', + 'ꝇ' => 'Ꝇ', + 'ꝉ' => 'Ꝉ', + 'ꝋ' => 'Ꝋ', + 'ꝍ' => 'Ꝍ', + 'ꝏ' => 'Ꝏ', + 'ꝑ' => 'Ꝑ', + 'ꝓ' => 'Ꝓ', + 'ꝕ' => 'Ꝕ', + 'ꝗ' => 'Ꝗ', + 'ꝙ' => 'Ꝙ', + 'ꝛ' => 'Ꝛ', + 'ꝝ' => 'Ꝝ', + 'ꝟ' => 'Ꝟ', + 'ꝡ' => 'Ꝡ', + 'ꝣ' => 'Ꝣ', + 'ꝥ' => 'Ꝥ', + 'ꝧ' => 'Ꝧ', + 'ꝩ' => 'Ꝩ', + 'ꝫ' => 'Ꝫ', + 'ꝭ' => 'Ꝭ', + 'ꝯ' => 'Ꝯ', + 'ꝺ' => 'Ꝺ', + 'ꝼ' => 'Ꝼ', + 'ꝿ' => 'Ꝿ', + 'ꞁ' => 'Ꞁ', + 'ꞃ' => 'Ꞃ', + 'ꞅ' => 'Ꞅ', + 'ꞇ' => 'Ꞇ', + 'ꞌ' => 'Ꞌ', + 'ꞑ' => 'Ꞑ', + 'ꞓ' => 'Ꞓ', + 'ꞔ' => 'Ꞔ', + 'ꞗ' => 'Ꞗ', + 'ꞙ' => 'Ꞙ', + 'ꞛ' => 'Ꞛ', + 'ꞝ' => 'Ꞝ', + 'ꞟ' => 'Ꞟ', + 'ꞡ' => 'Ꞡ', + 'ꞣ' => 'Ꞣ', + 'ꞥ' => 'Ꞥ', + 'ꞧ' => 'Ꞧ', + 'ꞩ' => 'Ꞩ', + 'ꞵ' => 'Ꞵ', + 'ꞷ' => 'Ꞷ', + 'ꞹ' => 'Ꞹ', + 'ꞻ' => 'Ꞻ', + 'ꞽ' => 'Ꞽ', + 'ꞿ' => 'Ꞿ', + 'ꟃ' => 'Ꟃ', + 'ꟈ' => 'Ꟈ', + 'ꟊ' => 'Ꟊ', + 'ꟶ' => 'Ꟶ', + 'ꭓ' => 'Ꭓ', + 'ꭰ' => 'Ꭰ', + 'ꭱ' => 'Ꭱ', + 'ꭲ' => 'Ꭲ', + 'ꭳ' => 'Ꭳ', + 'ꭴ' => 'Ꭴ', + 'ꭵ' => 'Ꭵ', + 'ꭶ' => 'Ꭶ', + 'ꭷ' => 'Ꭷ', + 'ꭸ' => 'Ꭸ', + 'ꭹ' => 'Ꭹ', + 'ꭺ' => 'Ꭺ', + 'ꭻ' => 'Ꭻ', + 'ꭼ' => 'Ꭼ', + 'ꭽ' => 'Ꭽ', + 'ꭾ' => 'Ꭾ', + 'ꭿ' => 'Ꭿ', + 'ꮀ' => 'Ꮀ', + 'ꮁ' => 'Ꮁ', + 'ꮂ' => 'Ꮂ', + 'ꮃ' => 'Ꮃ', + 'ꮄ' => 'Ꮄ', + 'ꮅ' => 'Ꮅ', + 'ꮆ' => 'Ꮆ', + 'ꮇ' => 'Ꮇ', + 'ꮈ' => 'Ꮈ', + 'ꮉ' => 'Ꮉ', + 'ꮊ' => 'Ꮊ', + 'ꮋ' => 'Ꮋ', + 'ꮌ' => 'Ꮌ', + 'ꮍ' => 'Ꮍ', + 'ꮎ' => 'Ꮎ', + 'ꮏ' => 'Ꮏ', + 'ꮐ' => 'Ꮐ', + 'ꮑ' => 'Ꮑ', + 'ꮒ' => 'Ꮒ', + 'ꮓ' => 'Ꮓ', + 'ꮔ' => 'Ꮔ', + 'ꮕ' => 'Ꮕ', + 'ꮖ' => 'Ꮖ', + 'ꮗ' => 'Ꮗ', + 'ꮘ' => 'Ꮘ', + 'ꮙ' => 'Ꮙ', + 'ꮚ' => 'Ꮚ', + 'ꮛ' => 'Ꮛ', + 'ꮜ' => 'Ꮜ', + 'ꮝ' => 'Ꮝ', + 'ꮞ' => 'Ꮞ', + 'ꮟ' => 'Ꮟ', + 'ꮠ' => 'Ꮠ', + 'ꮡ' => 'Ꮡ', + 'ꮢ' => 'Ꮢ', + 'ꮣ' => 'Ꮣ', + 'ꮤ' => 'Ꮤ', + 'ꮥ' => 'Ꮥ', + 'ꮦ' => 'Ꮦ', + 'ꮧ' => 'Ꮧ', + 'ꮨ' => 'Ꮨ', + 'ꮩ' => 'Ꮩ', + 'ꮪ' => 'Ꮪ', + 'ꮫ' => 'Ꮫ', + 'ꮬ' => 'Ꮬ', + 'ꮭ' => 'Ꮭ', + 'ꮮ' => 'Ꮮ', + 'ꮯ' => 'Ꮯ', + 'ꮰ' => 'Ꮰ', + 'ꮱ' => 'Ꮱ', + 'ꮲ' => 'Ꮲ', + 'ꮳ' => 'Ꮳ', + 'ꮴ' => 'Ꮴ', + 'ꮵ' => 'Ꮵ', + 'ꮶ' => 'Ꮶ', + 'ꮷ' => 'Ꮷ', + 'ꮸ' => 'Ꮸ', + 'ꮹ' => 'Ꮹ', + 'ꮺ' => 'Ꮺ', + 'ꮻ' => 'Ꮻ', + 'ꮼ' => 'Ꮼ', + 'ꮽ' => 'Ꮽ', + 'ꮾ' => 'Ꮾ', + 'ꮿ' => 'Ꮿ', + 'a' => 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + '𐐨' => '𐐀', + '𐐩' => '𐐁', + '𐐪' => '𐐂', + '𐐫' => '𐐃', + '𐐬' => '𐐄', + '𐐭' => '𐐅', + '𐐮' => '𐐆', + '𐐯' => '𐐇', + '𐐰' => '𐐈', + '𐐱' => '𐐉', + '𐐲' => '𐐊', + '𐐳' => '𐐋', + '𐐴' => '𐐌', + '𐐵' => '𐐍', + '𐐶' => '𐐎', + '𐐷' => '𐐏', + '𐐸' => '𐐐', + '𐐹' => '𐐑', + '𐐺' => '𐐒', + '𐐻' => '𐐓', + '𐐼' => '𐐔', + '𐐽' => '𐐕', + '𐐾' => '𐐖', + '𐐿' => '𐐗', + '𐑀' => '𐐘', + '𐑁' => '𐐙', + '𐑂' => '𐐚', + '𐑃' => '𐐛', + '𐑄' => '𐐜', + '𐑅' => '𐐝', + '𐑆' => '𐐞', + '𐑇' => '𐐟', + '𐑈' => '𐐠', + '𐑉' => '𐐡', + '𐑊' => '𐐢', + '𐑋' => '𐐣', + '𐑌' => '𐐤', + '𐑍' => '𐐥', + '𐑎' => '𐐦', + '𐑏' => '𐐧', + '𐓘' => '𐒰', + '𐓙' => '𐒱', + '𐓚' => '𐒲', + '𐓛' => '𐒳', + '𐓜' => '𐒴', + '𐓝' => '𐒵', + '𐓞' => '𐒶', + '𐓟' => '𐒷', + '𐓠' => '𐒸', + '𐓡' => '𐒹', + '𐓢' => '𐒺', + '𐓣' => '𐒻', + '𐓤' => '𐒼', + '𐓥' => '𐒽', + '𐓦' => '𐒾', + '𐓧' => '𐒿', + '𐓨' => '𐓀', + '𐓩' => '𐓁', + '𐓪' => '𐓂', + '𐓫' => '𐓃', + '𐓬' => '𐓄', + '𐓭' => '𐓅', + '𐓮' => '𐓆', + '𐓯' => '𐓇', + '𐓰' => '𐓈', + '𐓱' => '𐓉', + '𐓲' => '𐓊', + '𐓳' => '𐓋', + '𐓴' => '𐓌', + '𐓵' => '𐓍', + '𐓶' => '𐓎', + '𐓷' => '𐓏', + '𐓸' => '𐓐', + '𐓹' => '𐓑', + '𐓺' => '𐓒', + '𐓻' => '𐓓', + '𐳀' => '𐲀', + '𐳁' => '𐲁', + '𐳂' => '𐲂', + '𐳃' => '𐲃', + '𐳄' => '𐲄', + '𐳅' => '𐲅', + '𐳆' => '𐲆', + '𐳇' => '𐲇', + '𐳈' => '𐲈', + '𐳉' => '𐲉', + '𐳊' => '𐲊', + '𐳋' => '𐲋', + '𐳌' => '𐲌', + '𐳍' => '𐲍', + '𐳎' => '𐲎', + '𐳏' => '𐲏', + '𐳐' => '𐲐', + '𐳑' => '𐲑', + '𐳒' => '𐲒', + '𐳓' => '𐲓', + '𐳔' => '𐲔', + '𐳕' => '𐲕', + '𐳖' => '𐲖', + '𐳗' => '𐲗', + '𐳘' => '𐲘', + '𐳙' => '𐲙', + '𐳚' => '𐲚', + '𐳛' => '𐲛', + '𐳜' => '𐲜', + '𐳝' => '𐲝', + '𐳞' => '𐲞', + '𐳟' => '𐲟', + '𐳠' => '𐲠', + '𐳡' => '𐲡', + '𐳢' => '𐲢', + '𐳣' => '𐲣', + '𐳤' => '𐲤', + '𐳥' => '𐲥', + '𐳦' => '𐲦', + '𐳧' => '𐲧', + '𐳨' => '𐲨', + '𐳩' => '𐲩', + '𐳪' => '𐲪', + '𐳫' => '𐲫', + '𐳬' => '𐲬', + '𐳭' => '𐲭', + '𐳮' => '𐲮', + '𐳯' => '𐲯', + '𐳰' => '𐲰', + '𐳱' => '𐲱', + '𐳲' => '𐲲', + '𑣀' => '𑢠', + '𑣁' => '𑢡', + '𑣂' => '𑢢', + '𑣃' => '𑢣', + '𑣄' => '𑢤', + '𑣅' => '𑢥', + '𑣆' => '𑢦', + '𑣇' => '𑢧', + '𑣈' => '𑢨', + '𑣉' => '𑢩', + '𑣊' => '𑢪', + '𑣋' => '𑢫', + '𑣌' => '𑢬', + '𑣍' => '𑢭', + '𑣎' => '𑢮', + '𑣏' => '𑢯', + '𑣐' => '𑢰', + '𑣑' => '𑢱', + '𑣒' => '𑢲', + '𑣓' => '𑢳', + '𑣔' => '𑢴', + '𑣕' => '𑢵', + '𑣖' => '𑢶', + '𑣗' => '𑢷', + '𑣘' => '𑢸', + '𑣙' => '𑢹', + '𑣚' => '𑢺', + '𑣛' => '𑢻', + '𑣜' => '𑢼', + '𑣝' => '𑢽', + '𑣞' => '𑢾', + '𑣟' => '𑢿', + '𖹠' => '𖹀', + '𖹡' => '𖹁', + '𖹢' => '𖹂', + '𖹣' => '𖹃', + '𖹤' => '𖹄', + '𖹥' => '𖹅', + '𖹦' => '𖹆', + '𖹧' => '𖹇', + '𖹨' => '𖹈', + '𖹩' => '𖹉', + '𖹪' => '𖹊', + '𖹫' => '𖹋', + '𖹬' => '𖹌', + '𖹭' => '𖹍', + '𖹮' => '𖹎', + '𖹯' => '𖹏', + '𖹰' => '𖹐', + '𖹱' => '𖹑', + '𖹲' => '𖹒', + '𖹳' => '𖹓', + '𖹴' => '𖹔', + '𖹵' => '𖹕', + '𖹶' => '𖹖', + '𖹷' => '𖹗', + '𖹸' => '𖹘', + '𖹹' => '𖹙', + '𖹺' => '𖹚', + '𖹻' => '𖹛', + '𖹼' => '𖹜', + '𖹽' => '𖹝', + '𖹾' => '𖹞', + '𖹿' => '𖹟', + '𞤢' => '𞤀', + '𞤣' => '𞤁', + '𞤤' => '𞤂', + '𞤥' => '𞤃', + '𞤦' => '𞤄', + '𞤧' => '𞤅', + '𞤨' => '𞤆', + '𞤩' => '𞤇', + '𞤪' => '𞤈', + '𞤫' => '𞤉', + '𞤬' => '𞤊', + '𞤭' => '𞤋', + '𞤮' => '𞤌', + '𞤯' => '𞤍', + '𞤰' => '𞤎', + '𞤱' => '𞤏', + '𞤲' => '𞤐', + '𞤳' => '𞤑', + '𞤴' => '𞤒', + '𞤵' => '𞤓', + '𞤶' => '𞤔', + '𞤷' => '𞤕', + '𞤸' => '𞤖', + '𞤹' => '𞤗', + '𞤺' => '𞤘', + '𞤻' => '𞤙', + '𞤼' => '𞤚', + '𞤽' => '𞤛', + '𞤾' => '𞤜', + '𞤿' => '𞤝', + '𞥀' => '𞤞', + '𞥁' => '𞤟', + '𞥂' => '𞤠', + '𞥃' => '𞤡', + 'ß' => 'SS', + 'ff' => 'FF', + 'fi' => 'FI', + 'fl' => 'FL', + 'ffi' => 'FFI', + 'ffl' => 'FFL', + 'ſt' => 'ST', + 'st' => 'ST', + 'և' => 'ԵՒ', + 'ﬓ' => 'ՄՆ', + 'ﬔ' => 'ՄԵ', + 'ﬕ' => 'ՄԻ', + 'ﬖ' => 'ՎՆ', + 'ﬗ' => 'ՄԽ', + 'ʼn' => 'ʼN', + 'ΐ' => 'Ϊ́', + 'ΰ' => 'Ϋ́', + 'ǰ' => 'J̌', + 'ẖ' => 'H̱', + 'ẗ' => 'T̈', + 'ẘ' => 'W̊', + 'ẙ' => 'Y̊', + 'ẚ' => 'Aʾ', + 'ὐ' => 'Υ̓', + 'ὒ' => 'Υ̓̀', + 'ὔ' => 'Υ̓́', + 'ὖ' => 'Υ̓͂', + 'ᾶ' => 'Α͂', + 'ῆ' => 'Η͂', + 'ῒ' => 'Ϊ̀', + 'ΐ' => 'Ϊ́', + 'ῖ' => 'Ι͂', + 'ῗ' => 'Ϊ͂', + 'ῢ' => 'Ϋ̀', + 'ΰ' => 'Ϋ́', + 'ῤ' => 'Ρ̓', + 'ῦ' => 'Υ͂', + 'ῧ' => 'Ϋ͂', + 'ῶ' => 'Ω͂', + 'ᾈ' => 'ἈΙ', + 'ᾉ' => 'ἉΙ', + 'ᾊ' => 'ἊΙ', + 'ᾋ' => 'ἋΙ', + 'ᾌ' => 'ἌΙ', + 'ᾍ' => 'ἍΙ', + 'ᾎ' => 'ἎΙ', + 'ᾏ' => 'ἏΙ', + 'ᾘ' => 'ἨΙ', + 'ᾙ' => 'ἩΙ', + 'ᾚ' => 'ἪΙ', + 'ᾛ' => 'ἫΙ', + 'ᾜ' => 'ἬΙ', + 'ᾝ' => 'ἭΙ', + 'ᾞ' => 'ἮΙ', + 'ᾟ' => 'ἯΙ', + 'ᾨ' => 'ὨΙ', + 'ᾩ' => 'ὩΙ', + 'ᾪ' => 'ὪΙ', + 'ᾫ' => 'ὫΙ', + 'ᾬ' => 'ὬΙ', + 'ᾭ' => 'ὭΙ', + 'ᾮ' => 'ὮΙ', + 'ᾯ' => 'ὯΙ', + 'ᾼ' => 'ΑΙ', + 'ῌ' => 'ΗΙ', + 'ῼ' => 'ΩΙ', + 'ᾲ' => 'ᾺΙ', + 'ᾴ' => 'ΆΙ', + 'ῂ' => 'ῊΙ', + 'ῄ' => 'ΉΙ', + 'ῲ' => 'ῺΙ', + 'ῴ' => 'ΏΙ', + 'ᾷ' => 'Α͂Ι', + 'ῇ' => 'Η͂Ι', + 'ῷ' => 'Ω͂Ι', +); diff --git a/vendor/symfony/polyfill-mbstring/bootstrap.php b/vendor/symfony/polyfill-mbstring/bootstrap.php new file mode 100644 index 0000000..ff51ae0 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/bootstrap.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('mb_convert_encoding')) { + function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } +} +if (!function_exists('mb_decode_mimeheader')) { + function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } +} +if (!function_exists('mb_encode_mimeheader')) { + function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } +} +if (!function_exists('mb_decode_numericentity')) { + function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } +} +if (!function_exists('mb_encode_numericentity')) { + function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } +} +if (!function_exists('mb_convert_case')) { + function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } +} +if (!function_exists('mb_internal_encoding')) { + function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } +} +if (!function_exists('mb_language')) { + function mb_language($language = null) { return p\Mbstring::mb_language($language); } +} +if (!function_exists('mb_list_encodings')) { + function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } +} +if (!function_exists('mb_encoding_aliases')) { + function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } +} +if (!function_exists('mb_check_encoding')) { + function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } +} +if (!function_exists('mb_detect_encoding')) { + function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } +} +if (!function_exists('mb_detect_order')) { + function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } +} +if (!function_exists('mb_parse_str')) { + function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; } +} +if (!function_exists('mb_strlen')) { + function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } +} +if (!function_exists('mb_strpos')) { + function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strtolower')) { + function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } +} +if (!function_exists('mb_strtoupper')) { + function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } +} +if (!function_exists('mb_substitute_character')) { + function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } +} +if (!function_exists('mb_substr')) { + function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } +} +if (!function_exists('mb_stripos')) { + function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_stristr')) { + function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strrchr')) { + function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strrichr')) { + function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strripos')) { + function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strrpos')) { + function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strstr')) { + function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_get_info')) { + function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } +} +if (!function_exists('mb_http_output')) { + function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } +} +if (!function_exists('mb_strwidth')) { + function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } +} +if (!function_exists('mb_substr_count')) { + function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } +} +if (!function_exists('mb_output_handler')) { + function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } +} +if (!function_exists('mb_http_input')) { + function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); } +} + +if (!function_exists('mb_convert_variables')) { + function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } +} + +if (!function_exists('mb_ord')) { + function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } +} +if (!function_exists('mb_chr')) { + function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } +} +if (!function_exists('mb_scrub')) { + function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } +} +if (!function_exists('mb_str_split')) { + function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } +} + +if (!function_exists('mb_str_pad')) { + function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } +} + +if (!function_exists('mb_ucfirst')) { + function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } +} + +if (!function_exists('mb_lcfirst')) { + function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } +} + +if (!function_exists('mb_trim')) { + function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } +} + +if (!function_exists('mb_ltrim')) { + function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } +} + +if (!function_exists('mb_rtrim')) { + function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } +} + + +if (extension_loaded('mbstring')) { + return; +} + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); +} +if (!defined('MB_CASE_LOWER')) { + define('MB_CASE_LOWER', 1); +} +if (!defined('MB_CASE_TITLE')) { + define('MB_CASE_TITLE', 2); +} diff --git a/vendor/symfony/polyfill-mbstring/bootstrap80.php b/vendor/symfony/polyfill-mbstring/bootstrap80.php new file mode 100644 index 0000000..5236e6d --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/bootstrap80.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (!function_exists('mb_convert_encoding')) { + function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); } +} +if (!function_exists('mb_decode_mimeheader')) { + function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); } +} +if (!function_exists('mb_encode_mimeheader')) { + function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); } +} +if (!function_exists('mb_decode_numericentity')) { + function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); } +} +if (!function_exists('mb_encode_numericentity')) { + function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); } +} +if (!function_exists('mb_convert_case')) { + function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); } +} +if (!function_exists('mb_internal_encoding')) { + function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); } +} +if (!function_exists('mb_language')) { + function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); } +} +if (!function_exists('mb_list_encodings')) { + function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); } +} +if (!function_exists('mb_encoding_aliases')) { + function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); } +} +if (!function_exists('mb_check_encoding')) { + function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); } +} +if (!function_exists('mb_detect_encoding')) { + function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); } +} +if (!function_exists('mb_detect_order')) { + function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); } +} +if (!function_exists('mb_parse_str')) { + function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; } +} +if (!function_exists('mb_strlen')) { + function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); } +} +if (!function_exists('mb_strpos')) { + function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strtolower')) { + function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); } +} +if (!function_exists('mb_strtoupper')) { + function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); } +} +if (!function_exists('mb_substitute_character')) { + function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); } +} +if (!function_exists('mb_substr')) { + function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); } +} +if (!function_exists('mb_stripos')) { + function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_stristr')) { + function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strrchr')) { + function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strrichr')) { + function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strripos')) { + function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strrpos')) { + function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strstr')) { + function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_get_info')) { + function mb_get_info(?string $type = 'all'): array|string|int|false|null { return p\Mbstring::mb_get_info((string) $type); } +} +if (!function_exists('mb_http_output')) { + function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); } +} +if (!function_exists('mb_strwidth')) { + function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); } +} +if (!function_exists('mb_substr_count')) { + function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); } +} +if (!function_exists('mb_output_handler')) { + function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); } +} +if (!function_exists('mb_http_input')) { + function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); } +} + +if (!function_exists('mb_convert_variables')) { + function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); } +} + +if (!function_exists('mb_ord')) { + function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); } +} +if (!function_exists('mb_chr')) { + function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); } +} +if (!function_exists('mb_scrub')) { + function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); } +} +if (!function_exists('mb_str_split')) { + function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); } +} + +if (!function_exists('mb_str_pad')) { + function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } +} + +if (!function_exists('mb_ucfirst')) { + function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } +} + +if (!function_exists('mb_lcfirst')) { + function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } +} + +if (!function_exists('mb_trim')) { + function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } +} + +if (!function_exists('mb_ltrim')) { + function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } +} + +if (!function_exists('mb_rtrim')) { + function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } +} + +if (extension_loaded('mbstring')) { + return; +} + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); +} +if (!defined('MB_CASE_LOWER')) { + define('MB_CASE_LOWER', 1); +} +if (!defined('MB_CASE_TITLE')) { + define('MB_CASE_TITLE', 2); +} diff --git a/vendor/symfony/polyfill-mbstring/composer.json b/vendor/symfony/polyfill-mbstring/composer.json new file mode 100644 index 0000000..daa07f8 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/composer.json @@ -0,0 +1,39 @@ +{ + "name": "symfony/polyfill-mbstring", + "type": "library", + "description": "Symfony polyfill for the Mbstring extension", + "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2", + "ext-iconv": "*" + }, + "provide": { + "ext-mbstring": "*" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/process/CHANGELOG.md b/vendor/symfony/process/CHANGELOG.md new file mode 100644 index 0000000..d730856 --- /dev/null +++ b/vendor/symfony/process/CHANGELOG.md @@ -0,0 +1,134 @@ +CHANGELOG +========= + + +7.3 +--- + + * Add `RunProcessMessage::fromShellCommandline()` to instantiate a Process via the fromShellCommandline method + +7.1 +--- + + * Add `Process::setIgnoredSignals()` to disable signal propagation to the child process + +6.4 +--- + + * Add `PhpSubprocess` to handle PHP subprocesses that take over the + configuration from their parent + * Add `RunProcessMessage` and `RunProcessMessageHandler` + +5.2.0 +----- + + * added `Process::setOptions()` to set `Process` specific options + * added option `create_new_console` to allow a subprocess to continue + to run after the main script exited, both on Linux and on Windows + +5.1.0 +----- + + * added `Process::getStartTime()` to retrieve the start time of the process as float + +5.0.0 +----- + + * removed `Process::inheritEnvironmentVariables()` + * removed `PhpProcess::setPhpBinary()` + * `Process` must be instantiated with a command array, use `Process::fromShellCommandline()` when the command should be parsed by the shell + * removed `Process::setCommandLine()` + +4.4.0 +----- + + * deprecated `Process::inheritEnvironmentVariables()`: env variables are always inherited. + * added `Process::getLastOutputTime()` method + +4.2.0 +----- + + * added the `Process::fromShellCommandline()` to run commands in a shell wrapper + * deprecated passing a command as string when creating a `Process` instance + * deprecated the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods + * added the `Process::waitUntil()` method to wait for the process only for a + specific output, then continue the normal execution of your application + +4.1.0 +----- + + * added the `Process::isTtySupported()` method that allows to check for TTY support + * made `PhpExecutableFinder` look for the `PHP_BINARY` env var when searching the php binary + * added the `ProcessSignaledException` class to properly catch signaled process errors + +4.0.0 +----- + + * environment variables will always be inherited + * added a second `array $env = []` argument to the `start()`, `run()`, + `mustRun()`, and `restart()` methods of the `Process` class + * added a second `array $env = []` argument to the `start()` method of the + `PhpProcess` class + * the `ProcessUtils::escapeArgument()` method has been removed + * the `areEnvironmentVariablesInherited()`, `getOptions()`, and `setOptions()` + methods of the `Process` class have been removed + * support for passing `proc_open()` options has been removed + * removed the `ProcessBuilder` class, use the `Process` class instead + * removed the `getEnhanceWindowsCompatibility()` and `setEnhanceWindowsCompatibility()` methods of the `Process` class + * passing a not existing working directory to the constructor of the `Symfony\Component\Process\Process` class is not + supported anymore + +3.4.0 +----- + + * deprecated the ProcessBuilder class + * deprecated calling `Process::start()` without setting a valid working directory beforehand (via `setWorkingDirectory()` or constructor) + +3.3.0 +----- + + * added command line arrays in the `Process` class + * added `$env` argument to `Process::start()`, `run()`, `mustRun()` and `restart()` methods + * deprecated the `ProcessUtils::escapeArgument()` method + * deprecated not inheriting environment variables + * deprecated configuring `proc_open()` options + * deprecated configuring enhanced Windows compatibility + * deprecated configuring enhanced sigchild compatibility + +2.5.0 +----- + + * added support for PTY mode + * added the convenience method "mustRun" + * deprecation: Process::setStdin() is deprecated in favor of Process::setInput() + * deprecation: Process::getStdin() is deprecated in favor of Process::getInput() + * deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types + +2.4.0 +----- + + * added the ability to define an idle timeout + +2.3.0 +----- + + * added ProcessUtils::escapeArgument() to fix the bug in escapeshellarg() function on Windows + * added Process::signal() + * added Process::getPid() + * added support for a TTY mode + +2.2.0 +----- + + * added ProcessBuilder::setArguments() to reset the arguments on a builder + * added a way to retrieve the standard and error output incrementally + * added Process:restart() + +2.1.0 +----- + + * added support for non-blocking processes (start(), wait(), isRunning(), stop()) + * enhanced Windows compatibility + * added Process::getExitCodeText() that returns a string representation for + the exit code returned by the process + * added ProcessBuilder diff --git a/vendor/symfony/process/Exception/ExceptionInterface.php b/vendor/symfony/process/Exception/ExceptionInterface.php new file mode 100644 index 0000000..bd4a604 --- /dev/null +++ b/vendor/symfony/process/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * Marker Interface for the Process Component. + * + * @author Johannes M. Schmitt + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/vendor/symfony/process/Exception/InvalidArgumentException.php b/vendor/symfony/process/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..926ee21 --- /dev/null +++ b/vendor/symfony/process/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * InvalidArgumentException for the Process Component. + * + * @author Romain Neutron + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/process/Exception/LogicException.php b/vendor/symfony/process/Exception/LogicException.php new file mode 100644 index 0000000..be3d490 --- /dev/null +++ b/vendor/symfony/process/Exception/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * LogicException for the Process Component. + * + * @author Romain Neutron + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/process/Exception/ProcessFailedException.php b/vendor/symfony/process/Exception/ProcessFailedException.php new file mode 100644 index 0000000..de8a9e9 --- /dev/null +++ b/vendor/symfony/process/Exception/ProcessFailedException.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for failed processes. + * + * @author Johannes M. Schmitt + */ +class ProcessFailedException extends RuntimeException +{ + public function __construct( + private Process $process, + ) { + if ($process->isSuccessful()) { + throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = \sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", + $process->getCommandLine(), + $process->getExitCode(), + $process->getExitCodeText(), + $process->getWorkingDirectory() + ); + + if (!$process->isOutputDisabled()) { + $error .= \sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", + $process->getOutput(), + $process->getErrorOutput() + ); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess(): Process + { + return $this->process; + } +} diff --git a/vendor/symfony/process/Exception/ProcessSignaledException.php b/vendor/symfony/process/Exception/ProcessSignaledException.php new file mode 100644 index 0000000..3fd13e5 --- /dev/null +++ b/vendor/symfony/process/Exception/ProcessSignaledException.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception that is thrown when a process has been signaled. + * + * @author Sullivan Senechal + */ +final class ProcessSignaledException extends RuntimeException +{ + public function __construct( + private Process $process, + ) { + parent::__construct(\sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); + } + + public function getProcess(): Process + { + return $this->process; + } + + public function getSignal(): int + { + return $this->getProcess()->getTermSignal(); + } +} diff --git a/vendor/symfony/process/Exception/ProcessStartFailedException.php b/vendor/symfony/process/Exception/ProcessStartFailedException.php new file mode 100644 index 0000000..3725472 --- /dev/null +++ b/vendor/symfony/process/Exception/ProcessStartFailedException.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for processes failed during startup. + */ +class ProcessStartFailedException extends ProcessFailedException +{ + public function __construct( + private Process $process, + ?string $message, + ) { + if ($process->isStarted()) { + throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.'); + } + + $error = \sprintf('The command "%s" failed.'."\n\nWorking directory: %s\n\nError: %s", + $process->getCommandLine(), + $process->getWorkingDirectory(), + $message ?? 'unknown' + ); + + // Skip parent constructor + RuntimeException::__construct($error); + } + + public function getProcess(): Process + { + return $this->process; + } +} diff --git a/vendor/symfony/process/Exception/ProcessTimedOutException.php b/vendor/symfony/process/Exception/ProcessTimedOutException.php new file mode 100644 index 0000000..d3fe493 --- /dev/null +++ b/vendor/symfony/process/Exception/ProcessTimedOutException.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception that is thrown when a process times out. + * + * @author Johannes M. Schmitt + */ +class ProcessTimedOutException extends RuntimeException +{ + public const TYPE_GENERAL = 1; + public const TYPE_IDLE = 2; + + public function __construct( + private Process $process, + private int $timeoutType, + ) { + parent::__construct(\sprintf( + 'The process "%s" exceeded the timeout of %s seconds.', + $process->getCommandLine(), + $this->getExceededTimeout() + )); + } + + public function getProcess(): Process + { + return $this->process; + } + + public function isGeneralTimeout(): bool + { + return self::TYPE_GENERAL === $this->timeoutType; + } + + public function isIdleTimeout(): bool + { + return self::TYPE_IDLE === $this->timeoutType; + } + + public function getExceededTimeout(): ?float + { + return match ($this->timeoutType) { + self::TYPE_GENERAL => $this->process->getTimeout(), + self::TYPE_IDLE => $this->process->getIdleTimeout(), + default => throw new \LogicException(\sprintf('Unknown timeout type "%d".', $this->timeoutType)), + }; + } +} diff --git a/vendor/symfony/process/Exception/RunProcessFailedException.php b/vendor/symfony/process/Exception/RunProcessFailedException.php new file mode 100644 index 0000000..e7219d3 --- /dev/null +++ b/vendor/symfony/process/Exception/RunProcessFailedException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Messenger\RunProcessContext; + +/** + * @author Kevin Bond + */ +final class RunProcessFailedException extends RuntimeException +{ + public function __construct(ProcessFailedException $exception, public readonly RunProcessContext $context) + { + parent::__construct($exception->getMessage(), $exception->getCode()); + } +} diff --git a/vendor/symfony/process/Exception/RuntimeException.php b/vendor/symfony/process/Exception/RuntimeException.php new file mode 100644 index 0000000..adead25 --- /dev/null +++ b/vendor/symfony/process/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * RuntimeException for the Process Component. + * + * @author Johannes M. Schmitt + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/process/ExecutableFinder.php b/vendor/symfony/process/ExecutableFinder.php new file mode 100644 index 0000000..204558b --- /dev/null +++ b/vendor/symfony/process/ExecutableFinder.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * Generic executable finder. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class ExecutableFinder +{ + private const CMD_BUILTINS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; + + private array $suffixes = []; + + /** + * Replaces default suffixes of executable. + */ + public function setSuffixes(array $suffixes): void + { + $this->suffixes = $suffixes; + } + + /** + * Adds new possible suffix to check for executable, including the dot (.). + * + * $finder = new ExecutableFinder(); + * $finder->addSuffix('.foo'); + */ + public function addSuffix(string $suffix): void + { + $this->suffixes[] = $suffix; + } + + /** + * Finds an executable by name. + * + * @param string $name The executable name (without the extension) + * @param string|null $default The default to return if no executable is found + * @param array $extraDirs Additional dirs to check into + */ + public function find(string $name, ?string $default = null, array $extraDirs = []): ?string + { + // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes + if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) { + return $name; + } + + $dirs = array_merge( + explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path') ?: ''), + $extraDirs + ); + + $suffixes = $this->suffixes; + if ('\\' === \DIRECTORY_SEPARATOR) { + $pathExt = getenv('PATHEXT') ?: ''; + $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); + } + $suffixes = '' !== pathinfo($name, \PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); + foreach ($suffixes as $suffix) { + foreach ($dirs as $dir) { + if ('' === $dir) { + $dir = '.'; + } + if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { + return $file; + } + + if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) { + return $dir; + } + } + } + + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + return $default; + } + + $execResult = exec('command -v -- '.escapeshellarg($name)); + + if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { + return $executablePath; + } + + return $default; + } +} diff --git a/vendor/symfony/process/InputStream.php b/vendor/symfony/process/InputStream.php new file mode 100644 index 0000000..586e742 --- /dev/null +++ b/vendor/symfony/process/InputStream.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * Provides a way to continuously write to the input of a Process until the InputStream is closed. + * + * @author Nicolas Grekas + * + * @implements \IteratorAggregate + */ +class InputStream implements \IteratorAggregate +{ + private ?\Closure $onEmpty = null; + private array $input = []; + private bool $open = true; + + /** + * Sets a callback that is called when the write buffer becomes empty. + */ + public function onEmpty(?callable $onEmpty = null): void + { + $this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null; + } + + /** + * Appends an input to the write buffer. + * + * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, + * stream resource or \Traversable + */ + public function write(mixed $input): void + { + if (null === $input) { + return; + } + if ($this->isClosed()) { + throw new RuntimeException(\sprintf('"%s" is closed.', static::class)); + } + $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); + } + + /** + * Closes the write buffer. + */ + public function close(): void + { + $this->open = false; + } + + /** + * Tells whether the write buffer is closed or not. + */ + public function isClosed(): bool + { + return !$this->open; + } + + public function getIterator(): \Traversable + { + $this->open = true; + + while ($this->open || $this->input) { + if (!$this->input) { + yield ''; + continue; + } + $current = array_shift($this->input); + + if ($current instanceof \Iterator) { + yield from $current; + } else { + yield $current; + } + if (!$this->input && $this->open && null !== $onEmpty = $this->onEmpty) { + $this->write($onEmpty($this)); + } + } + } +} diff --git a/vendor/symfony/process/LICENSE b/vendor/symfony/process/LICENSE new file mode 100644 index 0000000..0138f8f --- /dev/null +++ b/vendor/symfony/process/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/process/Messenger/RunProcessContext.php b/vendor/symfony/process/Messenger/RunProcessContext.php new file mode 100644 index 0000000..5e22304 --- /dev/null +++ b/vendor/symfony/process/Messenger/RunProcessContext.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class RunProcessContext +{ + public readonly ?int $exitCode; + public readonly ?string $output; + public readonly ?string $errorOutput; + + public function __construct( + public readonly RunProcessMessage $message, + Process $process, + ) { + $this->exitCode = $process->getExitCode(); + $this->output = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getOutput(); + $this->errorOutput = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getErrorOutput(); + } +} diff --git a/vendor/symfony/process/Messenger/RunProcessMessage.php b/vendor/symfony/process/Messenger/RunProcessMessage.php new file mode 100644 index 0000000..d14ac23 --- /dev/null +++ b/vendor/symfony/process/Messenger/RunProcessMessage.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +/** + * @author Kevin Bond + */ +class RunProcessMessage implements \Stringable +{ + public ?string $commandLine = null; + + public function __construct( + public readonly array $command, + public readonly ?string $cwd = null, + public readonly ?array $env = null, + public readonly mixed $input = null, + public readonly ?float $timeout = 60.0, + ) { + } + + public function __toString(): string + { + return $this->commandLine ?? implode(' ', $this->command); + } + + /** + * Create a process message instance that will instantiate a Process using the fromShellCommandline method. + * + * @see Process::fromShellCommandline + */ + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): self + { + $message = new self([], $cwd, $env, $input, $timeout); + $message->commandLine = $command; + + return $message; + } +} diff --git a/vendor/symfony/process/Messenger/RunProcessMessageHandler.php b/vendor/symfony/process/Messenger/RunProcessMessageHandler.php new file mode 100644 index 0000000..69bfa6a --- /dev/null +++ b/vendor/symfony/process/Messenger/RunProcessMessageHandler.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\RunProcessFailedException; +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class RunProcessMessageHandler +{ + public function __invoke(RunProcessMessage $message): RunProcessContext + { + $process = match ($message->commandLine) { + null => new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout), + default => Process::fromShellCommandline($message->commandLine, $message->cwd, $message->env, $message->input, $message->timeout), + }; + + try { + return new RunProcessContext($message, $process->mustRun()); + } catch (ProcessFailedException $e) { + throw new RunProcessFailedException($e, new RunProcessContext($message, $e->getProcess())); + } + } +} diff --git a/vendor/symfony/process/PhpExecutableFinder.php b/vendor/symfony/process/PhpExecutableFinder.php new file mode 100644 index 0000000..f9ed79e --- /dev/null +++ b/vendor/symfony/process/PhpExecutableFinder.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * An executable finder specifically designed for the PHP executable. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class PhpExecutableFinder +{ + private ExecutableFinder $executableFinder; + + public function __construct() + { + $this->executableFinder = new ExecutableFinder(); + } + + /** + * Finds The PHP executable. + */ + public function find(bool $includeArgs = true): string|false + { + if ($php = getenv('PHP_BINARY')) { + if (!is_executable($php) && !$php = $this->executableFinder->find($php)) { + return false; + } + + if (@is_dir($php)) { + return false; + } + + return $php; + } + + $args = $this->findArguments(); + $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; + + // PHP_BINARY return the current sapi executable + if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) { + return \PHP_BINARY.$args; + } + + if ($php = getenv('PHP_PATH')) { + if (!@is_executable($php) || @is_dir($php)) { + return false; + } + + return $php; + } + + if ($php = getenv('PHP_PEAR_PHP_BIN')) { + if (@is_executable($php) && !@is_dir($php)) { + return $php; + } + } + + if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) { + return $php; + } + + $dirs = [\PHP_BINDIR]; + if ('\\' === \DIRECTORY_SEPARATOR) { + $dirs[] = 'C:\xampp\php\\'; + } + + if ($herdPath = getenv('HERD_HOME')) { + $dirs[] = $herdPath.\DIRECTORY_SEPARATOR.'bin'; + } + + return $this->executableFinder->find('php', false, $dirs); + } + + /** + * Finds the PHP executable arguments. + * + * @return list + */ + public function findArguments(): array + { + $arguments = []; + if ('phpdbg' === \PHP_SAPI) { + $arguments[] = '-qrr'; + } + + return $arguments; + } +} diff --git a/vendor/symfony/process/PhpProcess.php b/vendor/symfony/process/PhpProcess.php new file mode 100644 index 0000000..930f591 --- /dev/null +++ b/vendor/symfony/process/PhpProcess.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpProcess runs a PHP script in an independent process. + * + * $p = new PhpProcess(''); + * $p->run(); + * print $p->getOutput()."\n"; + * + * @author Fabien Potencier + */ +class PhpProcess extends Process +{ + /** + * @param string $script The PHP script to run (as a string) + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int $timeout The timeout in seconds + * @param array|null $php Path to the PHP binary to use with any additional arguments + */ + public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) + { + if (null === $php) { + $executableFinder = new PhpExecutableFinder(); + $php = $executableFinder->find(false); + $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); + } + if ('phpdbg' === \PHP_SAPI) { + $file = tempnam(sys_get_temp_dir(), 'dbg'); + file_put_contents($file, $script); + register_shutdown_function('unlink', $file); + $php[] = $file; + $script = null; + } + + parent::__construct($php, $cwd, $env, $script, $timeout); + } + + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static + { + throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + } + + /** + * @param (callable('out'|'err', string):void)|null $callback + */ + public function start(?callable $callback = null, array $env = []): void + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + + parent::start($callback, $env); + } +} diff --git a/vendor/symfony/process/PhpSubprocess.php b/vendor/symfony/process/PhpSubprocess.php new file mode 100644 index 0000000..8282f93 --- /dev/null +++ b/vendor/symfony/process/PhpSubprocess.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpSubprocess runs a PHP command as a subprocess while keeping the original php.ini settings. + * + * For this, it generates a temporary php.ini file taking over all the current settings and disables + * loading additional .ini files. Basically, your command gets prefixed using "php -n -c /tmp/temp.ini". + * + * Given your php.ini contains "memory_limit=-1" and you have a "MemoryTest.php" with the following content: + * + * run(); + * print $p->getOutput()."\n"; + * + * This will output "string(2) "-1", because the process is started with the default php.ini settings. + * + * $p = new PhpSubprocess(['MemoryTest.php'], null, null, 60, ['php', '-d', 'memory_limit=256M']); + * $p->run(); + * print $p->getOutput()."\n"; + * + * This will output "string(4) "256M"", because the process is started with the temporarily created php.ini settings. + * + * @author Yanick Witschi + * @author Partially copied and heavily inspired from composer/xdebug-handler by John Stevenson + */ +class PhpSubprocess extends Process +{ + /** + * @param array $command The command to run and its arguments listed as separate entries. They will automatically + * get prefixed with the PHP binary + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int $timeout The timeout in seconds + * @param array|null $php Path to the PHP binary to use with any additional arguments + */ + public function __construct(array $command, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) + { + if (null === $php) { + $executableFinder = new PhpExecutableFinder(); + $php = $executableFinder->find(false); + $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); + } + + if (null === $php) { + throw new RuntimeException('Unable to find PHP binary.'); + } + + $tmpIni = $this->writeTmpIni($this->getAllIniFiles(), sys_get_temp_dir()); + + $php = array_merge($php, ['-n', '-c', $tmpIni]); + register_shutdown_function('unlink', $tmpIni); + + $command = array_merge($php, $command); + + parent::__construct($command, $cwd, $env, null, $timeout); + } + + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static + { + throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + } + + /** + * @param (callable('out'|'err', string):void)|null $callback + */ + public function start(?callable $callback = null, array $env = []): void + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + + parent::start($callback, $env); + } + + private function writeTmpIni(array $iniFiles, string $tmpDir): string + { + if (false === $tmpfile = @tempnam($tmpDir, '')) { + throw new RuntimeException('Unable to create temporary ini file.'); + } + + // $iniFiles has at least one item and it may be empty + if ('' === $iniFiles[0]) { + array_shift($iniFiles); + } + + $content = ''; + + foreach ($iniFiles as $file) { + // Check for inaccessible ini files + if (($data = @file_get_contents($file)) === false) { + throw new RuntimeException('Unable to read ini: '.$file); + } + // Check and remove directives after HOST and PATH sections + if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) { + $data = substr($data, 0, $matches[0][1]); + } + + $content .= $data."\n"; + } + + // Merge loaded settings into our ini content, if it is valid + $config = parse_ini_string($content); + $loaded = ini_get_all(null, false); + + if (false === $config || false === $loaded) { + throw new RuntimeException('Unable to parse ini data.'); + } + + $content .= $this->mergeLoadedConfig($loaded, $config); + + // Work-around for https://bugs.php.net/bug.php?id=75932 + $content .= "opcache.enable_cli=0\n"; + + if (false === @file_put_contents($tmpfile, $content)) { + throw new RuntimeException('Unable to write temporary ini file.'); + } + + return $tmpfile; + } + + private function mergeLoadedConfig(array $loadedConfig, array $iniConfig): string + { + $content = ''; + + foreach ($loadedConfig as $name => $value) { + if (!\is_string($value)) { + continue; + } + + if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { + // Double-quote escape each value + $content .= $name.'="'.addcslashes($value, '\\"')."\"\n"; + } + } + + return $content; + } + + private function getAllIniFiles(): array + { + $paths = [(string) php_ini_loaded_file()]; + + if (false !== $scanned = php_ini_scanned_files()) { + $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); + } + + return $paths; + } +} diff --git a/vendor/symfony/process/Pipes/AbstractPipes.php b/vendor/symfony/process/Pipes/AbstractPipes.php new file mode 100644 index 0000000..49a14da --- /dev/null +++ b/vendor/symfony/process/Pipes/AbstractPipes.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * @author Romain Neutron + * + * @internal + */ +abstract class AbstractPipes implements PipesInterface +{ + public array $pipes = []; + + private string $inputBuffer = ''; + /** @var resource|string|\Iterator */ + private $input; + private bool $blocked = true; + private ?string $lastError = null; + + /** + * @param resource|string|\Iterator $input + */ + public function __construct($input) + { + if (\is_resource($input) || $input instanceof \Iterator) { + $this->input = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + public function close(): void + { + foreach ($this->pipes as $pipe) { + if (\is_resource($pipe)) { + fclose($pipe); + } + } + $this->pipes = []; + } + + /** + * Returns true if a system call has been interrupted. + * + * stream_select() returns false when the `select` system call is interrupted by an incoming signal. + */ + protected function hasSystemCallBeenInterrupted(): bool + { + $lastError = $this->lastError; + $this->lastError = null; + + if (null === $lastError) { + return false; + } + + if (false !== stripos($lastError, 'interrupted system call')) { + return true; + } + + // on applications with a different locale than english, the message above is not found because + // it's translated. So we also check for the SOCKET_EINTR constant which is defined under + // Windows and UNIX-like platforms (if available on the platform). + return \defined('SOCKET_EINTR') && str_starts_with($lastError, 'stream_select(): Unable to select ['.\SOCKET_EINTR.']'); + } + + /** + * Unblocks streams. + */ + protected function unblock(): void + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, false); + } + if (\is_resource($this->input)) { + stream_set_blocking($this->input, false); + } + + $this->blocked = false; + } + + /** + * Writes input to stdin. + * + * @throws InvalidArgumentException When an input iterator yields a non supported value + */ + protected function write(): ?array + { + if (!isset($this->pipes[0])) { + return null; + } + $input = $this->input; + + if ($input instanceof \Iterator) { + if (!$input->valid()) { + $input = null; + } elseif (\is_resource($input = $input->current())) { + stream_set_blocking($input, false); + } elseif (!isset($this->inputBuffer[0])) { + if (!\is_string($input)) { + if (!\is_scalar($input)) { + throw new InvalidArgumentException(\sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input))); + } + $input = (string) $input; + } + $this->inputBuffer = $input; + $this->input->next(); + $input = null; + } else { + $input = null; + } + } + + $r = $e = []; + $w = [$this->pipes[0]]; + + // let's have a look if something changed in streams + if (false === @stream_select($r, $w, $e, 0, 0)) { + return null; + } + + foreach ($w as $stdin) { + if (isset($this->inputBuffer[0])) { + if (false === $written = @fwrite($stdin, $this->inputBuffer)) { + return $this->closeBrokenInputPipe(); + } + $this->inputBuffer = substr($this->inputBuffer, $written); + if (isset($this->inputBuffer[0]) && isset($this->pipes[0])) { + return [$this->pipes[0]]; + } + } + + if ($input) { + while (true) { + $data = fread($input, self::CHUNK_SIZE); + if (!isset($data[0])) { + break; + } + if (false === $written = @fwrite($stdin, $data)) { + return $this->closeBrokenInputPipe(); + } + $data = substr($data, $written); + if (isset($data[0])) { + $this->inputBuffer = $data; + + return isset($this->pipes[0]) ? [$this->pipes[0]] : null; + } + } + if (feof($input)) { + if ($this->input instanceof \Iterator) { + $this->input->next(); + } else { + $this->input = null; + } + } + } + } + + // no input to read on resource, buffer is empty + if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) { + $this->input = null; + fclose($this->pipes[0]); + unset($this->pipes[0]); + } elseif (!$w) { + return [$this->pipes[0]]; + } + + return null; + } + + private function closeBrokenInputPipe(): void + { + $this->lastError = error_get_last()['message'] ?? null; + if (\is_resource($this->pipes[0] ?? null)) { + fclose($this->pipes[0]); + } + unset($this->pipes[0]); + + $this->input = null; + $this->inputBuffer = ''; + } + + /** + * @internal + */ + public function handleError(int $type, string $msg): void + { + $this->lastError = $msg; + } +} diff --git a/vendor/symfony/process/Pipes/PipesInterface.php b/vendor/symfony/process/Pipes/PipesInterface.php new file mode 100644 index 0000000..967f8de --- /dev/null +++ b/vendor/symfony/process/Pipes/PipesInterface.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * PipesInterface manages descriptors and pipes for the use of proc_open. + * + * @author Romain Neutron + * + * @internal + */ +interface PipesInterface +{ + public const CHUNK_SIZE = 16384; + + /** + * Returns an array of descriptors for the use of proc_open. + */ + public function getDescriptors(): array; + + /** + * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. + * + * @return string[] + */ + public function getFiles(): array; + + /** + * Reads data in file handles and pipes. + * + * @param bool $blocking Whether to use blocking calls or not + * @param bool $close Whether to close pipes if they've reached EOF + * + * @return string[] An array of read data indexed by their fd + */ + public function readAndWrite(bool $blocking, bool $close = false): array; + + /** + * Returns if the current state has open file handles or pipes. + */ + public function areOpen(): bool; + + /** + * Returns if pipes are able to read output. + */ + public function haveReadSupport(): bool; + + /** + * Closes file handles and pipes. + */ + public function close(): void; +} diff --git a/vendor/symfony/process/Pipes/UnixPipes.php b/vendor/symfony/process/Pipes/UnixPipes.php new file mode 100644 index 0000000..5fbab21 --- /dev/null +++ b/vendor/symfony/process/Pipes/UnixPipes.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; + +/** + * UnixPipes implementation uses unix pipes as handles. + * + * @author Romain Neutron + * + * @internal + */ +class UnixPipes extends AbstractPipes +{ + public function __construct( + private ?bool $ttyMode, + private bool $ptyMode, + mixed $input, + private bool $haveReadSupport, + ) { + parent::__construct($input); + } + + public function __serialize(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __unserialize(array $data): void + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->close(); + } + + public function getDescriptors(): array + { + if (!$this->haveReadSupport) { + $nullstream = fopen('/dev/null', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + if ($this->ttyMode) { + return [ + ['file', '/dev/tty', 'r'], + ['file', '/dev/tty', 'w'], + ['file', '/dev/tty', 'w'], + ]; + } + + if ($this->ptyMode && Process::isPtySupported()) { + return [ + ['pty'], + ['pty'], + ['pipe', 'w'], // stderr needs to be in a pipe to correctly split error and output, since PHP will use the same stream for both + ]; + } + + return [ + ['pipe', 'r'], + ['pipe', 'w'], // stdout + ['pipe', 'w'], // stderr + ]; + } + + public function getFiles(): array + { + return []; + } + + public function readAndWrite(bool $blocking, bool $close = false): array + { + $this->unblock(); + $w = $this->write(); + + $read = $e = []; + $r = $this->pipes; + unset($r[0]); + + // let's have a look if something changed in streams + set_error_handler($this->handleError(...)); + if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + restore_error_handler(); + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return $read; + } + restore_error_handler(); + + foreach ($r as $pipe) { + // prior PHP 5.4 the array passed to stream_select is modified and + // lose key association, we have to find back the key + $read[$type = array_search($pipe, $this->pipes, true)] = ''; + + do { + $data = @fread($pipe, self::CHUNK_SIZE); + $read[$type] .= $data; + } while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1]))); + + if (!isset($read[$type][0])) { + unset($read[$type]); + } + + if ($close && feof($pipe)) { + fclose($pipe); + unset($this->pipes[$type]); + } + } + + return $read; + } + + public function haveReadSupport(): bool + { + return $this->haveReadSupport; + } + + public function areOpen(): bool + { + return (bool) $this->pipes; + } +} diff --git a/vendor/symfony/process/Pipes/WindowsPipes.php b/vendor/symfony/process/Pipes/WindowsPipes.php new file mode 100644 index 0000000..f4ab195 --- /dev/null +++ b/vendor/symfony/process/Pipes/WindowsPipes.php @@ -0,0 +1,185 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Process; + +/** + * WindowsPipes implementation uses temporary files as handles. + * + * @see https://bugs.php.net/51800 + * @see https://bugs.php.net/65650 + * + * @author Romain Neutron + * + * @internal + */ +class WindowsPipes extends AbstractPipes +{ + private array $files = []; + private array $fileHandles = []; + private array $lockHandles = []; + private array $readBytes = [ + Process::STDOUT => 0, + Process::STDERR => 0, + ]; + + public function __construct( + mixed $input, + private bool $haveReadSupport, + ) { + if ($this->haveReadSupport) { + // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. + // Workaround for this problem is to use temporary files instead of pipes on Windows platform. + // + // @see https://bugs.php.net/51800 + $pipes = [ + Process::STDOUT => Process::OUT, + Process::STDERR => Process::ERR, + ]; + $tmpDir = sys_get_temp_dir(); + $lastError = 'unknown reason'; + set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); + for ($i = 0;; ++$i) { + foreach ($pipes as $pipe => $name) { + $file = \sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); + + if (!$h = fopen($file.'.lock', 'w')) { + if (file_exists($file.'.lock')) { + continue 2; + } + restore_error_handler(); + throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError); + } + if (!flock($h, \LOCK_EX | \LOCK_NB)) { + continue 2; + } + if (isset($this->lockHandles[$pipe])) { + flock($this->lockHandles[$pipe], \LOCK_UN); + fclose($this->lockHandles[$pipe]); + } + $this->lockHandles[$pipe] = $h; + + if (!($h = fopen($file, 'w')) || !fclose($h) || !$h = fopen($file, 'r')) { + flock($this->lockHandles[$pipe], \LOCK_UN); + fclose($this->lockHandles[$pipe]); + unset($this->lockHandles[$pipe]); + continue 2; + } + $this->fileHandles[$pipe] = $h; + $this->files[$pipe] = $file; + } + break; + } + restore_error_handler(); + } + + parent::__construct($input); + } + + public function __serialize(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __unserialize(array $data): void + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->close(); + } + + public function getDescriptors(): array + { + if (!$this->haveReadSupport) { + $nullstream = fopen('NUL', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/51800) + // We're not using file handles as it can produce corrupted output https://bugs.php.net/65650 + // So we redirect output within the commandline and pass the nul device to the process + return [ + ['pipe', 'r'], + ['file', 'NUL', 'w'], + ['file', 'NUL', 'w'], + ]; + } + + public function getFiles(): array + { + return $this->files; + } + + public function readAndWrite(bool $blocking, bool $close = false): array + { + $this->unblock(); + $w = $this->write(); + $read = $r = $e = []; + + if ($blocking) { + if ($w) { + @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); + } elseif ($this->fileHandles) { + usleep((int) (Process::TIMEOUT_PRECISION * 1E6)); + } + } + foreach ($this->fileHandles as $type => $fileHandle) { + $data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]); + + if (isset($data[0])) { + $this->readBytes[$type] += \strlen($data); + $read[$type] = $data; + } + if ($close) { + ftruncate($fileHandle, 0); + fclose($fileHandle); + flock($this->lockHandles[$type], \LOCK_UN); + fclose($this->lockHandles[$type]); + unset($this->fileHandles[$type], $this->lockHandles[$type]); + } + } + + return $read; + } + + public function haveReadSupport(): bool + { + return $this->haveReadSupport; + } + + public function areOpen(): bool + { + return $this->pipes && $this->fileHandles; + } + + public function close(): void + { + parent::close(); + foreach ($this->fileHandles as $type => $handle) { + ftruncate($handle, 0); + fclose($handle); + flock($this->lockHandles[$type], \LOCK_UN); + fclose($this->lockHandles[$type]); + } + $this->fileHandles = $this->lockHandles = []; + } +} diff --git a/vendor/symfony/process/Process.php b/vendor/symfony/process/Process.php new file mode 100644 index 0000000..75d6d50 --- /dev/null +++ b/vendor/symfony/process/Process.php @@ -0,0 +1,1676 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\ProcessSignaledException; +use Symfony\Component\Process\Exception\ProcessStartFailedException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Pipes\UnixPipes; +use Symfony\Component\Process\Pipes\WindowsPipes; + +/** + * Process is a thin wrapper around proc_* functions to easily + * start independent PHP processes. + * + * @author Fabien Potencier + * @author Romain Neutron + * + * @implements \IteratorAggregate + */ +class Process implements \IteratorAggregate +{ + public const ERR = 'err'; + public const OUT = 'out'; + + public const STATUS_READY = 'ready'; + public const STATUS_STARTED = 'started'; + public const STATUS_TERMINATED = 'terminated'; + + public const STDIN = 0; + public const STDOUT = 1; + public const STDERR = 2; + + // Timeout Precision in seconds. + public const TIMEOUT_PRECISION = 0.2; + + public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking + public const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory + public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating + public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating + + /** + * @var \Closure('out'|'err', string):bool|null + */ + private ?\Closure $callback = null; + private array|string $commandline; + private ?string $cwd; + private array $env = []; + /** @var resource|string|\Iterator|null */ + private $input; + private ?float $starttime = null; + private ?float $lastOutputTime = null; + private ?float $timeout = null; + private ?float $idleTimeout = null; + private ?int $exitcode = null; + private array $fallbackStatus = []; + private array $processInformation; + private bool $outputDisabled = false; + /** @var resource */ + private $stdout; + /** @var resource */ + private $stderr; + /** @var resource|null */ + private $process; + private string $status = self::STATUS_READY; + private int $incrementalOutputOffset = 0; + private int $incrementalErrorOutputOffset = 0; + private bool $tty = false; + private bool $pty; + private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; + private array $ignoredSignals = []; + + private WindowsPipes|UnixPipes $processPipes; + + private ?int $latestSignal = null; + + private static ?bool $sigchild = null; + private static array $executables = []; + + /** + * Exit codes translation table. + * + * User-defined errors must use exit codes in the 64-113 range. + */ + public static array $exitCodes = [ + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ]; + + /** + * @param array $command The command to run and its arguments listed as separate entries + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input + * @param int|float|null $timeout The timeout in seconds or null to disable + * + * @throws LogicException When proc_open is not installed + */ + public function __construct(array $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60) + { + if (!\function_exists('proc_open')) { + throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $command; + $this->cwd = $cwd; + + // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started + // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected + // @see : https://bugs.php.net/51800 + // @see : https://bugs.php.net/50524 + if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->setInput($input); + $this->setTimeout($timeout); + $this->pty = false; + } + + /** + * Creates a Process instance as a command-line to be run in a shell wrapper. + * + * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.) + * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the + * shell wrapper and not to your commands. + * + * In order to inject dynamic values into command-lines, we strongly recommend using placeholders. + * This will save escaping values, which is not portable nor secure anyway: + * + * $process = Process::fromShellCommandline('my_command "${:MY_VAR}"'); + * $process->run(null, ['MY_VAR' => $theValue]); + * + * @param string $command The command line to pass to the shell of the OS + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input + * @param int|float|null $timeout The timeout in seconds or null to disable + * + * @throws LogicException When proc_open is not installed + */ + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static + { + $process = new static([], $cwd, $env, $input, $timeout); + $process->commandline = $command; + + return $process; + } + + public function __serialize(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __unserialize(array $data): void + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + if ($this->options['create_new_console'] ?? false) { + $this->processPipes->close(); + } else { + $this->stop(0); + } + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * Runs the process. + * + * The callback receives the type of output (out or err) and + * some bytes from the output in real-time. It allows to have feedback + * from the independent process during execution. + * + * The STDOUT and STDERR are also available after the process is finished + * via the getOutput() and getErrorOutput() methods. + * + * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return int The exit status code + * + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled + * + * @final + */ + public function run(?callable $callback = null, array $env = []): int + { + $this->start($callback, $env); + + return $this->wait(); + } + + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return $this + * + * @throws ProcessFailedException When process didn't terminate successfully + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled + * + * @final + */ + public function mustRun(?callable $callback = null, array $env = []): static + { + if (0 !== $this->run($callback, $env)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * Starts the process and returns after writing the input to STDIN. + * + * This method blocks until all STDIN data is sent to the process then it + * returns while the process runs in the background. + * + * The termination of the process can be awaited with wait(). + * + * The callback receives the type of output (out or err) and some bytes from + * the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running + * @throws LogicException In case a callback is provided and output has been disabled + */ + public function start(?callable $callback = null, array $env = []): void + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running.'); + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $descriptors = $this->getDescriptors(null !== $callback); + + if ($this->env) { + $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; + } + + $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); + + if (\is_array($commandline = $this->commandline)) { + $commandline = array_values(array_map(strval(...), $commandline)); + } else { + $commandline = $this->replacePlaceholders($commandline, $env); + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + $commandline = $this->prepareWindowsCommandLine($commandline, $env); + } elseif ($this->isSigchildEnabled()) { + // last exit code is output on the fourth pipe and caught to work around --enable-sigchild + $descriptors[3] = ['pipe', 'w']; + + if (\is_array($commandline)) { + // exec is mandatory to deal with sending a signal to the process + $commandline = 'exec '.$this->buildShellCommandline($commandline); + } + + // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input + $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; + $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; + } + + $envPairs = []; + foreach ($env as $k => $v) { + if (false !== $v && !\in_array($k = (string) $k, ['', 'argc', 'argv', 'ARGC', 'ARGV'], true) && !str_contains($k, '=') && !str_contains($k, "\0")) { + $envPairs[] = $k.'='.$v; + } + } + + if (!is_dir($this->cwd)) { + throw new RuntimeException(\sprintf('The provided cwd "%s" does not exist.', $this->cwd)); + } + + $lastError = null; + set_error_handler(function ($type, $msg) use (&$lastError) { + $lastError = $msg; + + return true; + }); + + $oldMask = []; + + if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { + // we block signals we want to ignore, as proc_open will use fork / posix_spawn which will copy the signal mask this allow to block + // signals in the child process + pcntl_sigprocmask(\SIG_BLOCK, $this->ignoredSignals, $oldMask); + } + + try { + $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + + // Ensure array vs string commands behave the same + if (!$process && \is_array($commandline)) { + $process = @proc_open('exec '.$this->buildShellCommandline($commandline), $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + } + } finally { + if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { + // we restore the signal mask here to avoid any side effects + pcntl_sigprocmask(\SIG_SETMASK, $oldMask); + } + + restore_error_handler(); + } + + if (!$process) { + throw new ProcessStartFailedException($this, $lastError); + } + $this->process = $process; + $this->status = self::STATUS_STARTED; + + if (isset($descriptors[3])) { + $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); + } + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * Restarts the process. + * + * Be warned that the process is cloned before being started. + * + * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running + * + * @see start() + * + * @final + */ + public function restart(?callable $callback = null, array $env = []): static + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running.'); + } + + $process = clone $this; + $process->start($callback, $env); + + return $process; + } + + /** + * Waits for the process to terminate. + * + * The callback receives the type of output (out or err) and some bytes + * from the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return int The exitcode of the process + * + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException When process is not yet started + */ + public function wait(?callable $callback = null): int + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + + if (null !== $callback) { + if (!$this->processPipes->haveReadSupport()) { + $this->stop(0); + throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".'); + } + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen()); + $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); + } while ($running); + + while ($this->isRunning()) { + $this->checkTimeout(); + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new ProcessSignaledException($this); + } + + return $this->exitcode; + } + + /** + * Waits until the callback returns true. + * + * The callback receives the type of output (out or err) and some bytes + * from the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param (callable('out'|'err', string):bool)|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws RuntimeException When process timed out + * @throws LogicException When process is not yet started + * @throws ProcessTimedOutException In case the timeout was reached + */ + public function waitUntil(callable $callback): bool + { + $this->requireProcessIsStarted(__FUNCTION__); + $this->updateStatus(false); + + if (!$this->processPipes->haveReadSupport()) { + $this->stop(0); + throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".'); + } + $callback = $this->buildCallback($callback); + + $ready = false; + while (true) { + $this->checkTimeout(); + $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); + + foreach ($output as $type => $data) { + if (3 !== $type) { + $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready; + } elseif (!isset($this->fallbackStatus['signaled'])) { + $this->fallbackStatus['exitcode'] = (int) $data; + } + } + if ($ready) { + return true; + } + if (!$running) { + return false; + } + + usleep(1000); + } + } + + /** + * Returns the Pid (process identifier), if applicable. + * + * @return int|null The process id if running, null otherwise + */ + public function getPid(): ?int + { + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) + * + * @return $this + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed + * @throws RuntimeException In case of failure + */ + public function signal(int $signal): static + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * Disables fetching output and error output from the underlying process. + * + * @return $this + * + * @throws RuntimeException In case the process is already running + * @throws LogicException if an idle timeout is set + */ + public function disableOutput(): static + { + if ($this->isRunning()) { + throw new RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new LogicException('Output cannot be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * Enables fetching output and error output from the underlying process. + * + * @return $this + * + * @throws RuntimeException In case the process is already running + */ + public function enableOutput(): static + { + if ($this->isRunning()) { + throw new RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * Returns true in case the output is disabled, false otherwise. + */ + public function isOutputDisabled(): bool + { + return $this->outputDisabled; + } + + /** + * Returns the current output of the process (STDOUT). + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getOutput(): string + { + $this->readPipesForOutput(__FUNCTION__); + + if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { + return ''; + } + + return $ret; + } + + /** + * Returns the output incrementally. + * + * In comparison with the getOutput method which always return the whole + * output, this one returns the new output since the last call. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIncrementalOutput(): string + { + $this->readPipesForOutput(__FUNCTION__); + + $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); + $this->incrementalOutputOffset = ftell($this->stdout); + + if (false === $latest) { + return ''; + } + + return $latest; + } + + /** + * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). + * + * @param int $flags A bit field of Process::ITER_* flags + * + * @return \Generator + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIterator(int $flags = 0): \Generator + { + $this->readPipesForOutput(__FUNCTION__, false); + + $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); + $blocking = !(self::ITER_NON_BLOCKING & $flags); + $yieldOut = !(self::ITER_SKIP_OUT & $flags); + $yieldErr = !(self::ITER_SKIP_ERR & $flags); + + while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { + if ($yieldOut) { + $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); + + if (isset($out[0])) { + if ($clearOutput) { + $this->clearOutput(); + } else { + $this->incrementalOutputOffset = ftell($this->stdout); + } + + yield self::OUT => $out; + } + } + + if ($yieldErr) { + $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); + + if (isset($err[0])) { + if ($clearOutput) { + $this->clearErrorOutput(); + } else { + $this->incrementalErrorOutputOffset = ftell($this->stderr); + } + + yield self::ERR => $err; + } + } + + if (!$blocking && !isset($out[0]) && !isset($err[0])) { + yield self::OUT => ''; + } + + $this->checkTimeout(); + $this->readPipesForOutput(__FUNCTION__, $blocking); + } + } + + /** + * Clears the process output. + * + * @return $this + */ + public function clearOutput(): static + { + ftruncate($this->stdout, 0); + fseek($this->stdout, 0); + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * Returns the current error output of the process (STDERR). + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getErrorOutput(): string + { + $this->readPipesForOutput(__FUNCTION__); + + if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { + return ''; + } + + return $ret; + } + + /** + * Returns the errorOutput incrementally. + * + * In comparison with the getErrorOutput method which always return the + * whole error output, this one returns the new error output since the last + * call. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIncrementalErrorOutput(): string + { + $this->readPipesForOutput(__FUNCTION__); + + $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); + $this->incrementalErrorOutputOffset = ftell($this->stderr); + + if (false === $latest) { + return ''; + } + + return $latest; + } + + /** + * Clears the process output. + * + * @return $this + */ + public function clearErrorOutput(): static + { + ftruncate($this->stderr, 0); + fseek($this->stderr, 0); + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * Returns the exit code returned by the process. + * + * @return int|null The exit status code, null if the Process is not terminated + */ + public function getExitCode(): ?int + { + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * Returns a string representation for the exit code returned by the process. + * + * This method relies on the Unix exit code status standardization + * and might not be relevant for other operating systems. + * + * @return string|null A string representation for the exit status code, null if the Process is not terminated + * + * @see http://tldp.org/LDP/abs/html/exitcodes.html + * @see http://en.wikipedia.org/wiki/Unix_signal + */ + public function getExitCodeText(): ?string + { + if (null === $exitcode = $this->getExitCode()) { + return null; + } + + return self::$exitCodes[$exitcode] ?? 'Unknown error'; + } + + /** + * Checks if the process ended successfully. + */ + public function isSuccessful(): bool + { + return 0 === $this->getExitCode(); + } + + /** + * Returns true if the child process has been terminated by an uncaught signal. + * + * It always returns false on Windows. + * + * @throws LogicException In case the process is not terminated + */ + public function hasBeenSignaled(): bool + { + $this->requireProcessIsTerminated(__FUNCTION__); + + return $this->processInformation['signaled']; + } + + /** + * Returns the number of the signal that caused the child process to terminate its execution. + * + * It is only meaningful if hasBeenSignaled() returns true. + * + * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated + */ + public function getTermSignal(): int + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.'); + } + + return $this->processInformation['termsig']; + } + + /** + * Returns true if the child process has been stopped by a signal. + * + * It always returns false on Windows. + * + * @throws LogicException In case the process is not terminated + */ + public function hasBeenStopped(): bool + { + $this->requireProcessIsTerminated(__FUNCTION__); + + return $this->processInformation['stopped']; + } + + /** + * Returns the number of the signal that caused the child process to stop its execution. + * + * It is only meaningful if hasBeenStopped() returns true. + * + * @throws LogicException In case the process is not terminated + */ + public function getStopSignal(): int + { + $this->requireProcessIsTerminated(__FUNCTION__); + + return $this->processInformation['stopsig']; + } + + /** + * Checks if the process is currently running. + */ + public function isRunning(): bool + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * Checks if the process has been started with no regard to the current state. + */ + public function isStarted(): bool + { + return self::STATUS_READY != $this->status; + } + + /** + * Checks if the process is terminated. + */ + public function isTerminated(): bool + { + $this->updateStatus(false); + + return self::STATUS_TERMINATED == $this->status; + } + + /** + * Gets the process status. + * + * The status is one of: ready, started, terminated. + */ + public function getStatus(): string + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * Stops the process. + * + * @param int|float $timeout The timeout in seconds + * @param int|null $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) + * + * @return int|null The exit-code of the process or null if it's not running + */ + public function stop(float $timeout = 10, ?int $signal = null): ?int + { + $timeoutMicro = microtime(true) + $timeout; + if ($this->isRunning()) { + // given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here + $this->doSignal(15, false); + do { + usleep(1000); + } while ($this->isRunning() && microtime(true) < $timeoutMicro); + + if ($this->isRunning()) { + // Avoid exception here: process is supposed to be running, but it might have stopped just + // after this line. In any case, let's silently discard the error, we cannot do anything. + $this->doSignal($signal ?: 9, false); + } + } + + if ($this->isRunning()) { + if (isset($this->fallbackStatus['pid'])) { + unset($this->fallbackStatus['pid']); + + return $this->stop(0, $signal); + } + $this->close(); + } + + return $this->exitcode; + } + + /** + * Adds a line to the STDOUT stream. + * + * @internal + */ + public function addOutput(string $line): void + { + $this->lastOutputTime = microtime(true); + + fseek($this->stdout, 0, \SEEK_END); + fwrite($this->stdout, $line); + fseek($this->stdout, $this->incrementalOutputOffset); + } + + /** + * Adds a line to the STDERR stream. + * + * @internal + */ + public function addErrorOutput(string $line): void + { + $this->lastOutputTime = microtime(true); + + fseek($this->stderr, 0, \SEEK_END); + fwrite($this->stderr, $line); + fseek($this->stderr, $this->incrementalErrorOutputOffset); + } + + /** + * Gets the last output time in seconds. + */ + public function getLastOutputTime(): ?float + { + return $this->lastOutputTime; + } + + /** + * Gets the command line to be executed. + */ + public function getCommandLine(): string + { + return $this->buildShellCommandline($this->commandline); + } + + /** + * Gets the process timeout in seconds (max. runtime). + */ + public function getTimeout(): ?float + { + return $this->timeout; + } + + /** + * Gets the process idle timeout in seconds (max. time since last output). + */ + public function getIdleTimeout(): ?float + { + return $this->idleTimeout; + } + + /** + * Sets the process timeout (max. runtime) in seconds. + * + * To disable the timeout, set this value to null. + * + * @return $this + * + * @throws InvalidArgumentException if the timeout is negative + */ + public function setTimeout(?float $timeout): static + { + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Sets the process idle timeout (max. time since last output) in seconds. + * + * To disable the timeout, set this value to null. + * + * @return $this + * + * @throws LogicException if the output is disabled + * @throws InvalidArgumentException if the timeout is negative + */ + public function setIdleTimeout(?float $timeout): static + { + if (null !== $timeout && $this->outputDisabled) { + throw new LogicException('Idle timeout cannot be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Enables or disables the TTY mode. + * + * @return $this + * + * @throws RuntimeException In case the TTY mode is not supported + */ + public function setTty(bool $tty): static + { + if ('\\' === \DIRECTORY_SEPARATOR && $tty) { + throw new RuntimeException('TTY mode is not supported on Windows platform.'); + } + + if ($tty && !self::isTtySupported()) { + throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); + } + + $this->tty = $tty; + + return $this; + } + + /** + * Checks if the TTY mode is enabled. + */ + public function isTty(): bool + { + return $this->tty; + } + + /** + * Sets PTY mode. + * + * @return $this + */ + public function setPty(bool $bool): static + { + $this->pty = $bool; + + return $this; + } + + /** + * Returns PTY state. + */ + public function isPty(): bool + { + return $this->pty; + } + + /** + * Gets the working directory. + */ + public function getWorkingDirectory(): ?string + { + if (null === $this->cwd) { + // getcwd() will return false if any one of the parent directories does not have + // the readable or search mode set, even if the current directory does + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * Sets the current working directory. + * + * @return $this + */ + public function setWorkingDirectory(string $cwd): static + { + $this->cwd = $cwd; + + return $this; + } + + /** + * Gets the environment variables. + */ + public function getEnv(): array + { + return $this->env; + } + + /** + * Sets the environment variables. + * + * @param array $env The new environment variables + * + * @return $this + */ + public function setEnv(array $env): static + { + $this->env = $env; + + return $this; + } + + /** + * Gets the Process input. + * + * @return resource|string|\Iterator|null + */ + public function getInput() + { + return $this->input; + } + + /** + * Sets the input. + * + * This content will be passed to the underlying process standard input. + * + * @param string|resource|\Traversable|self|null $input The content + * + * @return $this + * + * @throws LogicException In case the process is running + */ + public function setInput(mixed $input): static + { + if ($this->isRunning()) { + throw new LogicException('Input cannot be set while the process is running.'); + } + + $this->input = ProcessUtils::validateInput(__METHOD__, $input); + + return $this; + } + + /** + * Performs a check between the timeout definition and the time the process started. + * + * In case you run a background process (with the start method), you should + * trigger this method regularly to ensure the process timeout + * + * @throws ProcessTimedOutException In case the timeout was reached + */ + public function checkTimeout(): void + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); + } + } + + /** + * @throws LogicException in case process is not started + */ + public function getStartTime(): float + { + if (!$this->isStarted()) { + throw new LogicException('Start time is only available after process start.'); + } + + return $this->starttime; + } + + /** + * Defines options to pass to the underlying proc_open(). + * + * @see https://php.net/proc_open for the options supported by PHP. + * + * Enabling the "create_new_console" option allows a subprocess to continue + * to run after the main process exited, on both Windows and *nix + */ + public function setOptions(array $options): void + { + if ($this->isRunning()) { + throw new RuntimeException('Setting options while the process is running is not possible.'); + } + + $defaultOptions = $this->options; + $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console']; + + foreach ($options as $key => $value) { + if (!\in_array($key, $existingOptions)) { + $this->options = $defaultOptions; + throw new LogicException(\sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); + } + $this->options[$key] = $value; + } + } + + /** + * Defines a list of posix signals that will not be propagated to the process. + * + * @param list<\SIG*> $signals + */ + public function setIgnoredSignals(array $signals): void + { + if ($this->isRunning()) { + throw new RuntimeException('Setting ignored signals while the process is running is not possible.'); + } + + $this->ignoredSignals = $signals; + } + + /** + * Returns whether TTY is supported on the current operating system. + */ + public static function isTtySupported(): bool + { + static $isTtySupported; + + return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty')); + } + + /** + * Returns whether PTY is supported on the current operating system. + */ + public static function isPtySupported(): bool + { + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + return $result = false; + } + + return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes); + } + + /** + * Creates the descriptors needed by the proc_open. + */ + private function getDescriptors(bool $hasCallback): array + { + if ($this->input instanceof \Iterator) { + $this->input->rewind(); + } + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $hasCallback); + } else { + $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $hasCallback); + } + + return $this->processPipes->getDescriptors(); + } + + /** + * Builds up the callback used by wait(). + * + * The callbacks adds all occurred output to the specific buffer and calls + * the user callback (if present) with the received output. + * + * @param callable('out'|'err', string)|null $callback + * + * @return \Closure('out'|'err', string):bool + */ + protected function buildCallback(?callable $callback = null): \Closure + { + if ($this->outputDisabled) { + return fn ($type, $data): bool => null !== $callback && $callback($type, $data); + } + + return function ($type, $data) use ($callback): bool { + match ($type) { + self::OUT => $this->addOutput($data), + self::ERR => $this->addErrorOutput($data), + }; + + return null !== $callback && $callback($type, $data); + }; + } + + /** + * Updates the status of the process, reads pipes. + * + * @param bool $blocking Whether to use a blocking read call + */ + protected function updateStatus(bool $blocking): void + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + if ($this->processInformation['running'] ?? true) { + $this->processInformation = proc_get_status($this->process); + } + $running = $this->processInformation['running']; + + $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); + + if ($this->fallbackStatus && $this->isSigchildEnabled()) { + $this->processInformation = $this->fallbackStatus + $this->processInformation; + } + + if (!$running) { + $this->close(); + } + } + + /** + * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. + */ + protected function isSigchildEnabled(): bool + { + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!\function_exists('phpinfo')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(\INFO_GENERAL); + + return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild'); + } + + /** + * Reads pipes for the freshest output. + * + * @param string $caller The name of the method that needs fresh outputs + * @param bool $blocking Whether to use blocking calls or not + * + * @throws LogicException in case output has been disabled or process is not started + */ + private function readPipesForOutput(string $caller, bool $blocking = false): void + { + if ($this->outputDisabled) { + throw new LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted($caller); + + $this->updateStatus($blocking); + } + + /** + * Validates and returns the filtered timeout. + * + * @throws InvalidArgumentException if the given timeout is a negative number + */ + private function validateTimeout(?float $timeout): ?float + { + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * Reads pipes, executes callback. + * + * @param bool $blocking Whether to use blocking calls or not + * @param bool $close Whether to close file handles or not + */ + private function readPipes(bool $blocking, bool $close): void + { + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 !== $type) { + $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); + } elseif (!isset($this->fallbackStatus['signaled'])) { + $this->fallbackStatus['exitcode'] = (int) $data; + } + } + } + + /** + * Closes process resource, closes file handles, sets the exitcode. + * + * @return int The exitcode + */ + private function close(): int + { + $this->processPipes->close(); + if ($this->process) { + proc_close($this->process); + $this->process = null; + } + $this->exitcode = $this->processInformation['exitcode']; + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode) { + if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { + // if process has been signaled, no exitcode but a valid termsig, apply Unix convention + $this->exitcode = 128 + $this->processInformation['termsig']; + } elseif ($this->isSigchildEnabled()) { + $this->processInformation['signaled'] = true; + $this->processInformation['termsig'] = -1; + } + } + + // Free memory from self-reference callback created by buildCallback + // Doing so in other contexts like __destruct or by garbage collector is ineffective + // Now pipes are closed, so the callback is no longer necessary + $this->callback = null; + + return $this->exitcode; + } + + /** + * Resets data related to the latest run of the process. + */ + private function resetProcessData(): void + { + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackStatus = []; + $this->processInformation = []; + $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); + $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) + * @param bool $throwException Whether to throw exception in case signal failed + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed + * @throws RuntimeException In case of failure + */ + private function doSignal(int $signal, bool $throwException): bool + { + // Signal seems to be send when sigchild is enable, this allow blocking the signal correctly in this case + if ($this->isSigchildEnabled() && \in_array($signal, $this->ignoredSignals)) { + return false; + } + + if (null === $pid = $this->getPid()) { + if ($throwException) { + throw new LogicException('Cannot send signal on a non running process.'); + } + + return false; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + exec(\sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); + if ($exitCode && $this->isRunning()) { + if ($throwException) { + throw new RuntimeException(\sprintf('Unable to kill the process (%s).', implode(' ', $output))); + } + + return false; + } + } else { + if (!$this->isSigchildEnabled()) { + $ok = @proc_terminate($this->process, $signal); + } elseif (\function_exists('posix_kill')) { + $ok = @posix_kill($pid, $signal); + } elseif ($ok = proc_open(\sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { + $ok = false === fgets($pipes[2]); + } + if (!$ok) { + if ($throwException) { + throw new RuntimeException(\sprintf('Error while sending signal "%s".', $signal)); + } + + return false; + } + } + + $this->latestSignal = $signal; + $this->fallbackStatus['signaled'] = true; + $this->fallbackStatus['exitcode'] = -1; + $this->fallbackStatus['termsig'] = $this->latestSignal; + + return true; + } + + private function buildShellCommandline(string|array $commandline): string + { + if (\is_string($commandline)) { + return $commandline; + } + + if ('\\' === \DIRECTORY_SEPARATOR && isset($commandline[0][0]) && \strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) { + // On Windows, we don't rely on the OS to find the executable if possible to avoid lookups + // in the current directory which could be untrusted. Instead we use the ExecutableFinder. + $commandline[0] = (self::$executables[$commandline[0]] ??= (new ExecutableFinder())->find($commandline[0])) ?? $commandline[0]; + } + + return implode(' ', array_map($this->escapeArgument(...), $commandline)); + } + + private function prepareWindowsCommandLine(string|array $cmd, array &$env): string + { + $cmd = $this->buildShellCommandline($cmd); + $uid = bin2hex(random_bytes(4)); + $cmd = preg_replace_callback( + '/"(?:( + [^"%!^]*+ + (?: + (?: !LF! | "(?:\^[%!^])?+" ) + [^"%!^]*+ + )++ + ) | [^"]*+ )"/x', + function ($m) use (&$env, $uid) { + static $varCount = 0; + static $varCache = []; + if (!isset($m[1])) { + return $m[0]; + } + if (isset($varCache[$m[0]])) { + return $varCache[$m[0]]; + } + if (str_contains($value = $m[1], "\0")) { + $value = str_replace("\0", '?', $value); + } + if (false === strpbrk($value, "\"%!\n")) { + return '"'.$value.'"'; + } + + $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value); + $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; + $var = $uid.++$varCount; + + $env[$var] = $value; + + return $varCache[$m[0]] = '!'.$var.'!'; + }, + $cmd + ); + + static $comSpec; + + if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { + // Escape according to CommandLineToArgvW rules + $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec).'"'; + } + + $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $cmd .= ' '.$offset.'>"'.$filename.'"'; + } + + return $cmd; + } + + /** + * Ensures the process is running or terminated, throws a LogicException if the process has a not started. + * + * @throws LogicException if the process has not run + */ + private function requireProcessIsStarted(string $functionName): void + { + if (!$this->isStarted()) { + throw new LogicException(\sprintf('Process must be started before calling "%s()".', $functionName)); + } + } + + /** + * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated". + * + * @throws LogicException if the process is not yet terminated + */ + private function requireProcessIsTerminated(string $functionName): void + { + if (!$this->isTerminated()) { + throw new LogicException(\sprintf('Process must be terminated before calling "%s()".', $functionName)); + } + } + + /** + * Escapes a string to be used as a shell argument. + */ + private function escapeArgument(?string $argument): string + { + if ('' === $argument || null === $argument) { + return '""'; + } + if ('\\' !== \DIRECTORY_SEPARATOR) { + return "'".str_replace("'", "'\\''", $argument)."'"; + } + if (str_contains($argument, "\0")) { + $argument = str_replace("\0", '?', $argument); + } + if (!preg_match('/[()%!^"<>&|\s[\]=;*?\'$]/', $argument)) { + return $argument; + } + $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); + + return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; + } + + private function replacePlaceholders(string $commandline, array $env): string + { + return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { + if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { + throw new InvalidArgumentException(\sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); + } + + return $this->escapeArgument($env[$matches[1]]); + }, $commandline); + } + + private function getDefaultEnv(): array + { + $env = getenv(); + $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env; + + return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env); + } +} diff --git a/vendor/symfony/process/ProcessUtils.php b/vendor/symfony/process/ProcessUtils.php new file mode 100644 index 0000000..a2dbde9 --- /dev/null +++ b/vendor/symfony/process/ProcessUtils.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * ProcessUtils is a bunch of utility methods. + * + * This class contains static methods only and is not meant to be instantiated. + * + * @author Martin Hasoň + */ +class ProcessUtils +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Validates and normalizes a Process input. + * + * @param string $caller The name of method call that validates the input + * @param mixed $input The input to validate + * + * @throws InvalidArgumentException In case the input is not valid + */ + public static function validateInput(string $caller, mixed $input): mixed + { + if (null !== $input) { + if (\is_resource($input)) { + return $input; + } + if (\is_scalar($input)) { + return (string) $input; + } + if ($input instanceof Process) { + return $input->getIterator($input::ITER_SKIP_ERR); + } + if ($input instanceof \Iterator) { + return $input; + } + if ($input instanceof \Traversable) { + return new \IteratorIterator($input); + } + + throw new InvalidArgumentException(\sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); + } + + return $input; + } +} diff --git a/vendor/symfony/process/README.md b/vendor/symfony/process/README.md new file mode 100644 index 0000000..afce5e4 --- /dev/null +++ b/vendor/symfony/process/README.md @@ -0,0 +1,13 @@ +Process Component +================= + +The Process component executes commands in sub-processes. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/process.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/process/composer.json b/vendor/symfony/process/composer.json new file mode 100644 index 0000000..dda5575 --- /dev/null +++ b/vendor/symfony/process/composer.json @@ -0,0 +1,28 @@ +{ + "name": "symfony/process", + "type": "library", + "description": "Executes commands in sub-processes", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Process\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} From 529a1c7356a031de7a467ba3dbf342a00909c532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 14:06:34 +0100 Subject: [PATCH 03/60] Add types to printable html document --- library/Pdfexport/PrintableHtmlDocument.php | 88 +++++++-------------- 1 file changed, 30 insertions(+), 58 deletions(-) diff --git a/library/Pdfexport/PrintableHtmlDocument.php b/library/Pdfexport/PrintableHtmlDocument.php index 4cd59f5..a9ee0b3 100644 --- a/library/Pdfexport/PrintableHtmlDocument.php +++ b/library/Pdfexport/PrintableHtmlDocument.php @@ -55,98 +55,80 @@ class PrintableHtmlDocument extends BaseHtmlElement } CSS; - /** @var string Document title */ - protected $title; + /** Document title */ + protected ?string $title = null; /** * Paper orientation * * Defaults to false. - * - * @var ?bool */ - protected $landscape; + protected ?bool $landscape = null; /** * Print background graphics * * Defaults to false. - * - * @var ?bool */ - protected $printBackground; + protected ?bool $printBackground = null; /** * Scale of the webpage rendering * * Defaults to 1. - * - * @var ?float */ - protected $scale; + protected ?float $scale = null; /** * Paper width in inches * * Defaults to 8.5 inches. - * - * @var ?float */ - protected $paperWidth; + protected ?float $paperWidth = null; /** * Paper height in inches * * Defaults to 11 inches. - * - * @var ?float */ - protected $paperHeight; + protected ?float $paperHeight = null; /** * Top margin in inches * * Defaults to 1cm (~0.4 inches). - * - * @var ?float */ - protected $marginTop; + protected ?float $marginTop = null; /** * Bottom margin in inches * * Defaults to 1cm (~0.4 inches). - * - * @var ?float */ - protected $marginBottom; + protected ?float $marginBottom = null; /** * Left margin in inches * * Defaults to 1cm (~0.4 inches). - * - * @var ?float */ - protected $marginLeft; + protected ?float $marginLeft = null; /** * Right margin in inches * * Defaults to 1cm (~0.4 inches). - * - * @var ?float */ - protected $marginRight; + protected ?float $marginRight = null; /** * Paper ranges to print, e.g., '1-5, 8, 11-13' * * Defaults to the empty string, which means print all pages * - * @var ?string + * @var string */ - protected $pageRanges; + protected string $pageRanges = ""; /** * Page height in pixels @@ -154,15 +136,13 @@ class PrintableHtmlDocument extends BaseHtmlElement * Minus the default vertical margins, this is 1035. * If the vertical margins are zero, it's 1160. * Whether there's a header or footer doesn't matter in any case. - * - * @var int */ - protected $pagePixelHeight = 1035; + protected int $pagePixelHeight = 1035; /** * HTML template for the print header * - * Should be valid HTML markup with following classes used to inject printing values into them: + * Should be valid HTML markup with the following classes used to inject printing values into them: * * date: formatted print date * * title: document title * * url: document location @@ -174,15 +154,13 @@ class PrintableHtmlDocument extends BaseHtmlElement * Note that the header cannot exceed a height of 21px regardless of the margin's height or document's scale. * With the default style, this height is separated by three lines, each accommodating 7px. * Use `span`'s for single line text and `p`'s for multiline text. - * - * @var ?ValidHtml */ - protected $headerTemplate; + protected ?ValidHtml $headerTemplate = null; /** * HTML template for the print footer * - * Should be valid HTML markup with following classes used to inject printing values into them: + * Should be valid HTML markup with the following classes used to inject printing values into them: * * date: formatted print date * * title: document title * * url: document location @@ -194,26 +172,20 @@ class PrintableHtmlDocument extends BaseHtmlElement * Note that the footer cannot exceed a height of 21px regardless of the margin's height or document's scale. * With the default style, this height is separated by three lines, each accommodating 7px. * Use `span`'s for single line text and `p`'s for multiline text. - * - * @var ?ValidHtml */ - protected $footerTemplate; + protected ?ValidHtml $footerTemplate = null; /** * HTML for the cover page - * - * @var ValidHtml */ - protected $coverPage; + protected ?ValidHtml $coverPage = null; /** - * Whether or not to prefer page size as defined by css + * Whether to prefer page size as defined by CSS * * Defaults to false, in which case the content will be scaled to fit the paper size. - * - * @var ?bool */ - protected $preferCSSPageSize; + protected ?bool $preferCSSPageSize = null; protected $tag = 'body'; @@ -222,7 +194,7 @@ class PrintableHtmlDocument extends BaseHtmlElement * * @return string */ - public function getTitle() + public function getTitle(): string { return $this->title; } @@ -234,7 +206,7 @@ public function getTitle() * * @return $this */ - public function setTitle($title) + public function setTitle(string $title): static { $this->title = $title; @@ -248,7 +220,7 @@ public function setTitle($title) * * @return $this */ - public function setHeader(ValidHtml $header) + public function setHeader(ValidHtml $header): static { $this->headerTemplate = $header; @@ -262,7 +234,7 @@ public function setHeader(ValidHtml $header) * * @return $this */ - public function setFooter(ValidHtml $footer) + public function setFooter(ValidHtml $footer): static { $this->footerTemplate = $footer; @@ -274,7 +246,7 @@ public function setFooter(ValidHtml $footer) * * @return ValidHtml|null */ - public function getCoverPage() + public function getCoverPage(): ?ValidHtml { return $this->coverPage; } @@ -286,7 +258,7 @@ public function getCoverPage() * * @return $this */ - public function setCoverPage(ValidHtml $coverPage) + public function setCoverPage(ValidHtml $coverPage): static { $this->coverPage = $coverPage; @@ -298,7 +270,7 @@ public function setCoverPage(ValidHtml $coverPage) * * @return $this */ - public function removeMargins() + public function removeMargins(): static { $this->marginBottom = 0; $this->marginLeft = 0; @@ -311,7 +283,7 @@ public function removeMargins() /** * Finalize document to be printed */ - protected function assemble() + protected function assemble(): void { $this->setWrapper(new HtmlElement( 'html', @@ -342,7 +314,7 @@ protected function assemble() * * @return array */ - public function getPrintParameters() + public function getPrintParameters(): array { $parameters = []; From ec4097ffd99353ba541f095e858403d55a689dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 14:06:49 +0100 Subject: [PATCH 04/60] Use str_starts_with whereever possible --- library/Pdfexport/PrintableHtmlDocument.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/Pdfexport/PrintableHtmlDocument.php b/library/Pdfexport/PrintableHtmlDocument.php index a9ee0b3..be8c0af 100644 --- a/library/Pdfexport/PrintableHtmlDocument.php +++ b/library/Pdfexport/PrintableHtmlDocument.php @@ -399,17 +399,17 @@ protected function createStylesheet(): ValidHtml $css = preg_replace_callback( '~(?<=url\()[\'"]?([^(\'"]*)[\'"]?(?=\))~', function ($matches) use ($app) { - if (substr($matches[1], 0, 3) !== '../') { + if (! str_starts_with($matches[1], '../')) { return $matches[1]; } $path = substr($matches[1], 3); - if (substr($path, 0, 4) === 'lib/') { + if (str_starts_with($path, 'lib/')) { $assetPath = substr($path, 4); $library = null; foreach ($app->getLibraries() as $candidate) { - if (substr($assetPath, 0, strlen($candidate->getName())) === $candidate->getName()) { + if (str_starts_with($assetPath, $candidate->getName())) { $library = $candidate; $assetPath = ltrim(substr($assetPath, strlen($candidate->getName())), '/'); break; @@ -421,8 +421,8 @@ function ($matches) use ($app) { } $path = $library->getStaticAssetPath() . DIRECTORY_SEPARATOR . $assetPath; - } elseif (substr($matches[1], 0, 14) === '../static/img?') { - list($_, $query) = explode('?', $matches[1], 2); + } elseif (str_starts_with($matches[1], '../static/img?')) { + [$_, $query] = explode('?', $matches[1], 2); $params = UrlParams::fromQueryString($query); if (! $app->getModuleManager()->hasEnabled($params->get('module_name'))) { return $matches[1]; From a8900866c15f7466830d4e64e3439cdf82e5ad77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 14:21:30 +0100 Subject: [PATCH 05/60] Add method to create parameters for printing with the webdriver --- library/Pdfexport/PrintableHtmlDocument.php | 60 +++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/library/Pdfexport/PrintableHtmlDocument.php b/library/Pdfexport/PrintableHtmlDocument.php index be8c0af..9984918 100644 --- a/library/Pdfexport/PrintableHtmlDocument.php +++ b/library/Pdfexport/PrintableHtmlDocument.php @@ -309,6 +309,66 @@ protected function assemble(): void }); } + /** + * Get the parameters for Page.printToPDF + * + * @return array + */ + public function getPrintParametersForWebdriver(): array + { + $parameters = []; + + if (isset($this->landscape)) { + $parameters['landscape'] = $this->landscape ? 'landscape' : 'portrait'; + } + + if (isset($this->printBackground)) { + $parameters['background'] = $this->printBackground; + } + + if (isset($this->scale)) { + $parameters['scale'] = $this->scale; + } + + // TODO: Validate width & height + if (isset($this->paperWidth)) { + $parameters['paperWidth'] = $this->paperWidth; + } + + if (isset($this->paperHeight)) { + $parameters['paperHeight'] = $this->paperHeight; + } + + // TODO: Validate margins + if (isset($this->marginTop)) { + $parameters['marginTop'] = $this->marginTop; + } + + if (isset($this->marginBottom)) { + $parameters['marginBottom'] = $this->marginBottom; + } + + if (isset($this->marginLeft)) { + $parameters['marginLeft'] = $this->marginLeft; + } + + if (isset($this->marginRight)) { + $parameters['marginRight'] = $this->marginRight; + } + + if (isset($this->pageRanges)) { + $parameters['pageRanges'] = explode(',', $this->pageRanges); + } + + // Note: Header and footer aren't supported for webdriver + + if (isset($this->preferCSSPageSize)) { + $parameters['preferCSSPageSize'] = $this->preferCSSPageSize; + } + + return $parameters; + } + /** * Get the parameters for Page.printToPDF * From 16078079d417d510990422fbfda43daeafb04f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 14:22:15 +0100 Subject: [PATCH 06/60] Add webdrivers for chrome and gecko (firefox) --- library/Pdfexport/Driver/Chromedriver.php | 33 +++++++++++ library/Pdfexport/Driver/Geckodriver.php | 13 ++++ library/Pdfexport/Driver/Webdriver.php | 72 +++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 library/Pdfexport/Driver/Chromedriver.php create mode 100644 library/Pdfexport/Driver/Geckodriver.php create mode 100644 library/Pdfexport/Driver/Webdriver.php diff --git a/library/Pdfexport/Driver/Chromedriver.php b/library/Pdfexport/Driver/Chromedriver.php new file mode 100644 index 0000000..31fe4fa --- /dev/null +++ b/library/Pdfexport/Driver/Chromedriver.php @@ -0,0 +1,33 @@ +execute( +// 'Page.printToPDF', +// ); +// +// return base64_decode($result['data']); + return parent::printToPdf($printParameters); + } +} diff --git a/library/Pdfexport/Driver/Geckodriver.php b/library/Pdfexport/Driver/Geckodriver.php new file mode 100644 index 0000000..5ff5e20 --- /dev/null +++ b/library/Pdfexport/Driver/Geckodriver.php @@ -0,0 +1,13 @@ +driver = RemoteWebDriver::create($url, $capabilities); + } + + protected function setContent(PrintableHtmlDocument $document): void + { + // This is horribly ugly, but it works for all browser backends + $encoded = base64_encode($document); + $this->driver->executeScript("document.body.innerHTML = atob('$encoded');"); + + // Wait for the body element to ensure the page has fully loaded + $wait = new WebDriverWait($this->driver, 10); + $wait->until(WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::tagName('body'))); + } + + protected function getPrintParameters(PrintableHtmlDocument $document): array + { + $parameters = [ + 'background' => true, + ]; + + return array_merge( + $parameters, + $document->getPrintParametersForWebdriver(), + ); + } + + protected function printToPdf(array $printParameters): string + { + $result = $this->driver->executeCustomCommand( + '/session/:sessionId/print', + 'POST', + $printParameters, + ); + + return base64_decode($result); + } + + public function close(): void + { + $this->driver->quit(); + } + + public function toPdf(PrintableHtmlDocument $document): string + { + try { + $this->setContent($document); + $printParameters = $this->getPrintParameters($document); + return $this->printToPdf($printParameters); + } finally { + $this->close(); + } + } +} From 42fb2174305f78b713af29b27bb9da03c5086c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 14:33:18 +0100 Subject: [PATCH 07/60] Use destructor instead of close method --- library/Pdfexport/Driver/PfdPrintDriver.php | 10 ++++++++++ library/Pdfexport/Driver/Webdriver.php | 22 +++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 library/Pdfexport/Driver/PfdPrintDriver.php diff --git a/library/Pdfexport/Driver/PfdPrintDriver.php b/library/Pdfexport/Driver/PfdPrintDriver.php new file mode 100644 index 0000000..7641227 --- /dev/null +++ b/library/Pdfexport/Driver/PfdPrintDriver.php @@ -0,0 +1,10 @@ +driver = RemoteWebDriver::create($url, $capabilities); } + function __destruct() + { + $this->driver->quit(); + } + protected function setContent(PrintableHtmlDocument $document): void { // This is horribly ugly, but it works for all browser backends @@ -54,19 +59,10 @@ protected function printToPdf(array $printParameters): string return base64_decode($result); } - public function close(): void - { - $this->driver->quit(); - } - public function toPdf(PrintableHtmlDocument $document): string { - try { - $this->setContent($document); - $printParameters = $this->getPrintParameters($document); - return $this->printToPdf($printParameters); - } finally { - $this->close(); - } + $this->setContent($document); + $printParameters = $this->getPrintParameters($document); + return $this->printToPdf($printParameters); } } From 380b66ac8d5cb6ceb7a41da55d3413ea8b0328a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 16:42:24 +0100 Subject: [PATCH 08/60] Rewrite Pdfexport hook --- library/Pdfexport/Driver/PfdPrintDriver.php | 2 + library/Pdfexport/Driver/Webdriver.php | 6 + library/Pdfexport/ProvidedHook/Pdfexport.php | 144 ++++++------------- 3 files changed, 55 insertions(+), 97 deletions(-) diff --git a/library/Pdfexport/Driver/PfdPrintDriver.php b/library/Pdfexport/Driver/PfdPrintDriver.php index 7641227..3f413cc 100644 --- a/library/Pdfexport/Driver/PfdPrintDriver.php +++ b/library/Pdfexport/Driver/PfdPrintDriver.php @@ -7,4 +7,6 @@ interface PfdPrintDriver { function toPdf(PrintableHtmlDocument $document): string; + + function isSupported(): bool; } diff --git a/library/Pdfexport/Driver/Webdriver.php b/library/Pdfexport/Driver/Webdriver.php index aa21cfa..3754297 100644 --- a/library/Pdfexport/Driver/Webdriver.php +++ b/library/Pdfexport/Driver/Webdriver.php @@ -65,4 +65,10 @@ public function toPdf(PrintableHtmlDocument $document): string $printParameters = $this->getPrintParameters($document); return $this->printToPdf($printParameters); } + + function isSupported(): bool + { + // TODO: Come up with a check + return true; + } } diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index a4903e6..4fa5a45 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -10,12 +10,15 @@ use Icinga\Application\Hook; use Icinga\Application\Hook\PdfexportHook; use Icinga\Application\Icinga; -use Icinga\Application\Web; use Icinga\File\Storage\TemporaryLocalFileStorage; +use Icinga\Module\Pdfexport\Driver\PfdPrintDriver; use Icinga\Module\Pdfexport\HeadlessChrome; use Icinga\Module\Pdfexport\PrintableHtmlDocument; +use Icinga\Module\Pdfexport\Driver\Webdriver; +use Icinga\Module\Pdfexport\Driver\Geckodriver; +use Icinga\Module\Pdfexport\Driver\Chromedriver; +use ipl\Html\HtmlString; use Karriere\PdfMerge\PdfMerge; -use React\Promise\PromiseInterface; class Pdfexport extends PdfexportHook { @@ -40,102 +43,47 @@ public static function first() return $pdfexport; } - public static function getBinary() - { - return Config::module('pdfexport')->get('chrome', 'binary', '/usr/bin/google-chrome'); - } - - public static function getForceTempStorage() - { - return (bool) Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0'); - } - - public static function getHost() - { - return Config::module('pdfexport')->get('chrome', 'host'); - } - - public static function getPort() - { - return Config::module('pdfexport')->get('chrome', 'port', 9222); - } - - public function isSupported() + public function isSupported(): bool { try { - return $this->chrome()->getVersion() >= 59; + // FIXME: This seems very strange + $driver = $this->getDriver(); + return $driver->isSupported(); } catch (Exception $e) { return false; } } - public function htmlToPdf($html) + public function streamPdfFromHtml($html, $filename): void { - // Keep reference to the chrome object because it is using temp files which are automatically removed when - // the object is destructed - $chrome = $this->chrome(); - - $pdf = $chrome->fromHtml($html, static::getForceTempStorage())->toPdf(); - - if ($html instanceof PrintableHtmlDocument && ($coverPage = $html->getCoverPage()) !== null) { - $coverPagePdf = $chrome - ->fromHtml( - (new PrintableHtmlDocument()) - ->add($coverPage) - ->addAttributes($html->getAttributes()) - ->removeMargins(), - static::getForceTempStorage() - ) - ->toPdf(); - - $pdf = $this->mergePdfs($coverPagePdf, $pdf); - } + $filename = basename($filename, '.pdf') . '.pdf'; - return $pdf; - } + $document = $this->getPrintableHtmlDocument($html); - /** - * Transforms the given printable html document/string asynchronously to PDF. - * - * @param PrintableHtmlDocument|string $html - * - * @return PromiseInterface - */ - public function asyncHtmlToPdf($html): PromiseInterface - { - // Keep reference to the chrome object because it is using temp files which are automatically removed when - // the object is destructed - $chrome = $this->chrome(); - - $pdfPromise = $chrome->fromHtml($html, static::getForceTempStorage())->asyncToPdf(); - - if ($html instanceof PrintableHtmlDocument && ($coverPage = $html->getCoverPage()) !== null) { - /** @var PromiseInterface $pdfPromise */ - $pdfPromise = $pdfPromise->then(function (string $pdf) use ($chrome, $html, $coverPage) { - return $chrome->fromHtml( - (new PrintableHtmlDocument()) - ->add($coverPage) - ->addAttributes($html->getAttributes()) - ->removeMargins(), - static::getForceTempStorage() - )->asyncToPdf()->then( - function (string $coverPagePdf) use ($pdf) { - return $this->mergePdfs($coverPagePdf, $pdf); - } - ); - }); + $driver = $this->getDriver(); + + $pdf = $driver->toPdf($document); + + if ($html instanceof PrintableHtmlDocument) { + $coverPage = $html->getCoverPage(); + if ($coverPage !== null) { + $coverPageDocument = $this->getPrintableHtmlDocument($coverPage); + $coverPageDocument->addAttributes($html->getAttributes()); + $coverPageDocument->removeMargins(); + + $coverPagePdf = $driver->toPdf($coverPage); + + $pdf = $this->mergePdfs($coverPagePdf, $pdf); + } } - return $pdfPromise; + $this->emit($pdf, $filename); + + exit; } - public function streamPdfFromHtml($html, $filename) + protected function emit(string $pdf, string $filename): void { - $filename = basename($filename, '.pdf') . '.pdf'; - - // Generate the PDF before changing the response headers to properly handle and display errors in the UI. - $pdf = $this->htmlToPdf($html); - /** @var Web $app */ $app = Icinga::app(); $app->getResponse() @@ -143,25 +91,27 @@ public function streamPdfFromHtml($html, $filename) ->setHeader('Content-Disposition', "inline; filename=\"$filename\"", true) ->setBody($pdf) ->sendResponse(); - - exit; } - /** - * Create an instance of HeadlessChrome from configuration - * - * @return HeadlessChrome - */ - protected function chrome() + protected function getDriver(): PfdPrintDriver { - $chrome = new HeadlessChrome(); - $chrome->setBinary(static::getBinary()); +// return new Chromedriver('http://selenium-chrome:4444'); +// return new Geckodriver('http://selenium-firefox:4444'); + return HeadlessChrome::createLocal( + Config::module('pdfexport')->get('chrome', 'binary', '/usr/bin/google-chrome') + ); +// $serverUrl = 'http://selenium-chrome:4444'; +// $serverUrl = 'http://chromedriver:9515'; +// $serverUrl = 'http://selenium-firefox:4444'; + } - if (($host = static::getHost()) !== null) { - $chrome->setRemote($host, static::getPort()); + protected function getPrintableHtmlDocument($html): PrintableHtmlDocument + { + if (! $html instanceof PrintableHtmlDocument) { + $html = (new PrintableHtmlDocument()) + ->setContent(HtmlString::create($html)); } - - return $chrome; + return $html; } protected function mergePdfs(string ...$pdfs): string From 7a28a6ad378e1045223c191f533178076240bbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 16:42:58 +0100 Subject: [PATCH 09/60] Convert the HeadlessChrome class into a PdfPrintDriver --- library/Pdfexport/HeadlessChrome.php | 639 ++++++++++++--------------- 1 file changed, 275 insertions(+), 364 deletions(-) diff --git a/library/Pdfexport/HeadlessChrome.php b/library/Pdfexport/HeadlessChrome.php index 8348d81..ba83817 100644 --- a/library/Pdfexport/HeadlessChrome.php +++ b/library/Pdfexport/HeadlessChrome.php @@ -6,21 +6,24 @@ namespace Icinga\Module\Pdfexport; use Exception; +use GuzzleHttp\Client as HttpClient; +use GuzzleHttp\Exception\ServerException; use Icinga\Application\Logger; use Icinga\Application\Platform; use Icinga\File\Storage\StorageInterface; use Icinga\File\Storage\TemporaryLocalFileStorage; +use Icinga\Module\Pdfexport\Driver\PfdPrintDriver; use ipl\Html\HtmlString; use LogicException; use Throwable; use WebSocket\Client; -class HeadlessChrome +class HeadlessChrome implements PfdPrintDriver { /** * Line of stderr output identifying the websocket url * - * First matching group is the used port and the second one the browser id. + * The first matching group is the used port, and the second one the browser id. */ public const DEBUG_ADDR_PATTERN = '/DevTools listening on ws:\/\/((?>\d+\.?){4}:\d+)\/devtools\/browser\/([\w-]+)/'; @@ -47,102 +50,167 @@ class HeadlessChrome }) JS; - /** @var string Path to the Chrome binary */ - protected $binary; + protected ?StorageInterface $fileStorage = null; - /** @var array Host and port to the remote Chrome */ - protected $remote; + protected ?Client $browser = null; - /** - * The document to print - * - * @var PrintableHtmlDocument - */ - protected $document; + protected ?Client $page = null; - /** @var string Target Url */ - protected $url; + protected ?string $targetId; - /** @var StorageInterface */ - protected $fileStorage; + private array $interceptedRequests = []; - /** @var array */ - private $interceptedRequests = []; + private array $interceptedEvents = []; - /** @var array */ - private $interceptedEvents = []; + protected $process; - /** - * Get the path to the Chrome binary - * - * @return string - */ - public function getBinary() - { - return $this->binary; - } + protected array $pipes = []; - /** - * Set the path to the Chrome binary - * - * @param string $binary - * - * @return $this - */ - public function setBinary($binary) - { - $this->binary = $binary; + protected ?string $socket = null; - return $this; - } + protected ?string $browserId = null; - /** - * Get host and port combination of the remote chrome - * - * @return array - */ - public function getRemote() + protected ?string $frameId = null; + + public function __destruct() { - return $this->remote; + $this->closeBrowser(); + $this->closeBrowser(); + $this->closeLocal(); } - /** - * Set host and port combination of a remote chrome - * - * @param string $host - * @param int $port - * - * @return $this - */ - public function setRemote($host, $port) + public static function createRemote(string $host, int $port): static { - $this->remote = [$host, $port]; + $instance = new self(); + $instance->socket = "$host:$port"; + try { + $result = $instance->getVersion(); + if (! is_array($result)) { + throw new Exception('Failed to determine remote chrome version via the /json/version endpoint.'); + } - return $this; + $parts = explode('/', $result['webSocketDebuggerUrl']); + $instance->browserId = end($parts); + } catch (Exception $e) { + Logger::warning( + 'Failed to connect to remote chrome: %s (%s)', + $instance->socket, + $e + ); + + throw $e; + } + + return $instance; } - /** - * Get the target Url - * - * @return string - */ - public function getUrl() + public static function createLocal(string $path): static { - return $this->url; + $instance = new self(); + + $browserHome = $instance->getFileStorage()->resolvePath('HOME'); + $descriptors = [ + 0 => ['pipe', 'r'], // stdin + 1 => ['pipe', 'w'], // stdout + 2 => ['pipe', 'w'], // stderr + ]; + + $commandLine = join(' ', [ + escapeshellarg($path), + static::renderArgumentList([ + '--bwsi', + '--headless', + '--disable-gpu', + '--no-sandbox', + '--no-first-run', + '--disable-dev-shm-usage', + '--remote-debugging-port=0', + '--homedir=' => $browserHome, + '--user-data-dir=' => $browserHome + ]) + ]); + + $env = null; + if (Platform::isLinux()) { + Logger::debug('Starting browser process: HOME=%s exec %s', $browserHome, $commandLine); + $env = array_merge($_ENV, ['HOME' => $browserHome]); + $commandLine = 'exec ' . $commandLine; + } else { + Logger::debug('Starting browser process: %s', $commandLine); + } + + $instance->process = proc_open($commandLine, $descriptors, $instance->pipes, null, $env); + + if (! is_resource($instance->process)) { + throw new Exception('Could not start browser process.'); + } + + // Non-blocking mode + stream_set_blocking($instance->pipes[2], false); + + $timeoutSeconds = 10; + $startTime = time(); + + while (true) { + $status = proc_get_status($instance->process); + + // Timeout handling + if ((time() - $startTime) > $timeoutSeconds) { + proc_terminate($instance->process, 6); // SIGABRT + Logger::error( + 'Browser timed out after %d seconds without the expected output', + $timeoutSeconds + ); + + throw new Exception( + 'Received empty response or none at all from browser.' + . ' Please check the logs for further details.' + ); + } + + $chunkSize = 8192; + $streamWaitTime = 200000; + $idleTime = 100000; + $read = [$instance->pipes[2]]; + $write = null; + $except = null; + + if (stream_select($read, $write, $except, 0, $streamWaitTime)) { + $chunk = fread($instance->pipes[2], $chunkSize); + + if ($chunk !== false && $chunk !== '') { + Logger::debug('Caught browser output: %s', $chunk); + + if (preg_match(self::DEBUG_ADDR_PATTERN, trim($chunk), $matches)) { + $instance->socket = $matches[1]; + $instance->browserId = $matches[2]; + break; + } + } + } + + if (! $status['running']) { + break; + } + + usleep($idleTime); + } + + return $instance; } - /** - * Set the target Url - * - * @param string $url - * - * @return $this - */ - public function setUrl($url) + protected function closeLocal(): void { - $this->url = $url; + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = []; - return $this; + if ($this->process !== null) { + proc_terminate($this->process); + proc_close($this->process); + $this->process = null; + } } /** @@ -175,12 +243,8 @@ public function setFileStorage($fileStorage) /** * Render the given argument name-value pairs as shell-escaped string - * - * @param array $arguments - * - * @return string */ - public static function renderArgumentList(array $arguments) + public static function renderArgumentList(array $arguments): string { $list = []; @@ -189,7 +253,7 @@ public static function renderArgumentList(array $arguments) $value = escapeshellarg($value); if (! is_int($name)) { - if (substr($name, -1) === '=') { + if (str_ends_with($name, '=')) { $glue = ''; } else { $glue = ' '; @@ -214,7 +278,7 @@ public static function renderArgumentList(array $arguments) * @param bool $asFile * @return $this */ - public function fromHtml($html, $asFile = false) + public function fromHtml($html, $asFile = false): static { if ($html instanceof PrintableHtmlDocument) { $this->document = $html; @@ -237,169 +301,24 @@ public function fromHtml($html, $asFile = false) return $this; } - public function toPdf(): string + protected function getPrintParameters(PrintableHtmlDocument $document): array { - switch (true) { - case $this->remote !== null: - try { - $result = $this->jsonVersion($this->remote[0], $this->remote[1]); - if (is_array($result)) { - $parts = explode('/', $result['webSocketDebuggerUrl']); - $pdf = $this->printToPDF( - join(':', $this->remote), - end($parts), - ! $this->document->isEmpty() ? $this->document->getPrintParameters() : [] - ); - break; - } - } catch (Exception $e) { - Logger::warning( - 'Failed to connect to remote chrome: %s:%d (%s)', - $this->remote[0], - $this->remote[1], - $e - ); - - throw $e; - } - - // Reject the promise if we didn't get the expected output from the /json/version endpoint. - if ($this->binary === null) { - throw new Exception('Failed to determine remote chrome version via the /json/version endpoint.'); - } - - break; - - // Fallback to the local binary if a remote chrome is unavailable - case $this->binary !== null: - $descriptors = [ - 0 => ['pipe', 'r'], // stdin - 1 => ['pipe', 'w'], // stdout - 2 => ['pipe', 'w'], // stderr - ]; - - - $browserHome = $this->getFileStorage()->resolvePath('HOME'); - $commandLine = join(' ', [ - escapeshellarg($this->getBinary()), - static::renderArgumentList([ - '--bwsi', - '--headless', - '--disable-gpu', - '--no-sandbox', - '--no-first-run', - '--disable-dev-shm-usage', - '--remote-debugging-port=0', - '--homedir=' => $browserHome, - '--user-data-dir=' => $browserHome - ]) - ]); - - $env = null; - if (Platform::isLinux()) { - Logger::debug('Starting browser process: HOME=%s exec %s', $browserHome, $commandLine); - $env = array_merge($_ENV, ['HOME' => $browserHome]); - $commandLine = 'exec ' . $commandLine; - } else { - Logger::debug('Starting browser process: %s', $commandLine); - } - - $process = proc_open($commandLine, $descriptors, $pipes, null, $env); - - if (! is_resource($process)) { - throw new Exception('Could not start browser process.'); - } - - // Non-blocking mode - stream_set_blocking($pipes[2], false); - - $timeoutSeconds = 10; - $startTime = time(); - $pdf = null; - - while (true) { - $status = proc_get_status($process); - - // Timeout handling - if ((time() - $startTime) > $timeoutSeconds) { - proc_terminate($process, 6); // SIGABRT - Logger::error( - 'Browser timed out after %d seconds without the expected output', - $timeoutSeconds - ); - - throw new Exception( - 'Received empty response or none at all from browser.' - . ' Please check the logs for further details.' - ); - } - - $chunkSize = 8192; - $streamWaitTime = 200000; - $idleTime = 100000; - $read = [$pipes[2]]; - $write = null; - $except = null; - - if (stream_select($read, $write, $except, 0, $streamWaitTime)) { - $chunk = fread($pipes[2], $chunkSize); - - if ($chunk !== false && $chunk !== '') { - Logger::debug('Caught browser output: %s', $chunk); - - if (preg_match(self::DEBUG_ADDR_PATTERN, trim($chunk), $matches)) { - - try { - $pdf = $this->printToPDF( - $matches[1], - $matches[2], - ! $this->document->isEmpty() - ? $this->document->getPrintParameters() - : [] - ); - } catch (Exception $e) { - Logger::error('Failed to print PDF. An error occurred: %s', $e->getMessage()); - } - - proc_terminate($process); - - if (! empty($pdf)) { - break; - } - - throw new Exception( - 'Received empty response or none at all from browser.' - . ' Please check the logs for further details.' - ); - } - } - } - - if (! $status['running']) { - break; - } - - usleep($idleTime); - } - - // Cleanup - foreach ($pipes as $pipe) { - fclose($pipe); - } - - proc_close($process); + $parameters = [ + 'printBackground' => true, + 'transferMode' => 'ReturnAsBase64', + ]; - return $pdf; - } + return array_merge( + $parameters, + $document->getPrintParameters(), + ); + } - if (! empty($pdf)) { - return $pdf; - } else { - throw new Exception( - 'Received empty response or none at all from browser.' - . ' Please check the logs for further details.', - ); - } + public function toPdf(PrintableHtmlDocument $document): string + { + $this->setContent($document); + $printParameters = $this->getPrintParameters($document); + return $this->printToPdf($printParameters); } /** @@ -420,64 +339,117 @@ public function savePdf() return $path; } - private function printToPDF($socket, $browserId, array $parameters) + protected function getBrowser(): Client { - $browser = new Client(sprintf('ws://%s/devtools/browser/%s', $socket, $browserId)); - - // Open new tab, get its id - $result = $this->communicate($browser, 'Target.createTarget', [ - 'url' => 'about:blank' - ]); - if (isset($result['targetId'])) { - $targetId = $result['targetId']; - } else { - throw new Exception('Expected target id. Got instead: ' . json_encode($result)); + if ($this->browser === null) { + $this->browser = new Client(sprintf('ws://%s/devtools/browser/%s', $this->socket, $this->browserId)); } + return $this->browser; + } - $page = new Client(sprintf('ws://%s/devtools/page/%s', $socket, $targetId), ['timeout' => 300]); + protected function closeBrowser(): void + { + if ($this->browser === null) { + return; + } - // enable various events - $this->communicate($page, 'Log.enable'); - $this->communicate($page, 'Network.enable'); - $this->communicate($page, 'Page.enable'); + $this->closePage(); try { - $this->communicate($page, 'Console.enable'); - } catch (Exception $_) { - // Deprecated, might fail + $this->browser->close(); + $this->browser = null; + } catch (Throwable $e) { + // For some reason, the browser doesn't send a response + Logger::debug(sprintf('Failed to close browser connection: ' . $e->getMessage())); } + } + + public function getPage(): Client + { + if ($this->page === null) { + $browser = $this->getBrowser(); - if (($url = $this->getUrl()) !== null) { - // Navigate to target - $result = $this->communicate($page, 'Page.navigate', [ - 'url' => $url + // Open new tab, get its id + $result = $this->communicate($browser, 'Target.createTarget', [ + 'url' => 'about:blank' ]); - if (isset($result['frameId'])) { - $frameId = $result['frameId']; + if (isset($result['targetId'])) { + $this->targetId = $result['targetId']; } else { - throw new Exception('Expected navigation frame. Got instead: ' . json_encode($result)); + throw new Exception('Expected target id. Got instead: ' . json_encode($result)); } - // wait for page to fully load - $this->waitFor($page, 'Page.frameStoppedLoading', ['frameId' => $frameId]); - } elseif (! $this->document->isEmpty()) { - // If there's no url to load transfer the document's content directly - $this->communicate($page, 'Page.setDocumentContent', [ - 'frameId' => $targetId, - 'html' => $this->document->render() - ]); + $this->page = new Client(sprintf('ws://%s/devtools/page/%s', $this->socket, $this->targetId)); - // wait for page to fully load - $this->waitFor($page, 'Page.loadEventFired'); - } else { + // enable various events + $this->communicate($this->page, 'Log.enable'); + $this->communicate($this->page, 'Network.enable'); + $this->communicate($this->page, 'Page.enable'); + + try { + $this->communicate($this->page, 'Console.enable'); + } catch (Exception $_) { + // Deprecated, might fail + } + } + return $this->page; + } + + public function closePage(): void + { + if ($this->browser === null || $this->page === null) { + return; + } + + // close tab + $result = $this->communicate($this->browser, 'Target.closeTarget', [ + 'targetId' => $this->targetId + ]); + + if (! isset($result['success'])) { + throw new Exception('Expected close confirmation. Got instead: ' . json_encode($result)); + } + + $this->page = null; + $this->targetId = null; + } + + protected function setContent(PrintableHtmlDocument $document): void + { + $page = $this->getPage(); + + // TODO: Reimplement +// if (($url = $this->getUrl()) !== null) { +// // Navigate to target +// $result = $this->communicate($page, 'Page.navigate', [ +// 'url' => $url +// ]); +// if (isset($result['frameId'])) { +// $frameId = $result['frameId']; +// } else { +// throw new Exception('Expected navigation frame. Got instead: ' . json_encode($result)); +// } +// +// // wait for page to fully load +// $this->waitFor($page, 'Page.frameStoppedLoading', ['frameId' => $frameId]); + if ($document->isEmpty()) { throw new LogicException('Nothing to print'); } + // Transfer the document's content directly + $this->communicate($page, 'Page.setDocumentContent', [ + 'frameId' => $this->targetId, + 'html' => $document->render() + ]); + + // wait for the page to fully load + $this->waitFor($page, 'Page.loadEventFired'); + // Wait for network activity to finish $this->waitFor($page, self::WAIT_FOR_NETWORK); - // Wait for layout to initialize - if (! $this->document->isEmpty()) { + // Wait for the layout to initialize + if (! $document->isEmpty()) { // Ensure layout scripts work in the same environment as the pdf printing itself $this->communicate($page, 'Emulation.setEmulatedMedia', ['media' => 'print']); @@ -489,7 +461,7 @@ private function printToPDF($socket, $browserId, array $parameters) $promisedResult = $this->communicate($page, 'Runtime.evaluate', [ 'awaitPromise' => true, 'returnByValue' => true, - 'timeout' => 1000, // Failsafe, doesn't apply to `await` it seems + 'timeout' => 1000, // Failsafe: doesn't apply to `await` it seems 'expression' => static::WAIT_FOR_LAYOUT ]); if (isset($promisedResult['exceptionDetails'])) { @@ -506,10 +478,15 @@ private function printToPDF($socket, $browserId, array $parameters) // Reset media emulation, this may prevent the real media from coming into effect? $this->communicate($page, 'Emulation.setEmulatedMedia', ['media' => '']); } + } + + protected function printToPdf(array $printParameters): string + { + $page = $this->getPage(); // print pdf $result = $this->communicate($page, 'Page.printToPDF', array_merge( - $parameters, + $printParameters, ['transferMode' => 'ReturnAsBase64', 'printBackground' => true] )); if (! empty($result['data'])) { @@ -518,21 +495,6 @@ private function printToPDF($socket, $browserId, array $parameters) throw new Exception('Expected base64 data. Got instead: ' . json_encode($result)); } - // close tab - $result = $this->communicate($browser, 'Target.closeTarget', [ - 'targetId' => $targetId - ]); - if (! isset($result['success'])) { - throw new Exception('Expected close confirmation. Got instead: ' . json_encode($result)); - } - - try { - $browser->close(); - } catch (Throwable $e) { - // For some reason, the browser doesn't send a response - Logger::debug(sprintf('Failed to close browser connection: ' . $e->getMessage())); - } - return $pdf; } @@ -683,81 +645,22 @@ private function waitFor(Client $ws, $eventName, ?array $expectedParams = null) return $params; } - /** - * Get the major version number of Chrome or false on failure - * - * @return int|false - * - * @throws Exception - */ - public function getVersion() - { - switch (true) { - case $this->remote !== null: - try { - $result = $this->jsonVersion($this->remote[0], $this->remote[1]); - $version = $result['Browser']; - break; - } catch (Exception $e) { - if ($this->binary === null) { - throw $e; - } else { - Logger::warning( - 'Failed to connect to remote chrome: %s:%d (%s)', - $this->remote[0], - $this->remote[1], - $e - ); - } - } - - // Fallback to the local binary if a remote chrome is unavailable - case $this->binary !== null: - $command = new ShellCommand( - escapeshellarg($this->getBinary()) . ' ' . static::renderArgumentList(['--version']), - false - ); - - $output = $command->execute(); - - if ($command->getExitCode() !== 0) { - throw new \Exception($output->stderr); - } - - $version = $output->stdout; - break; - default: - throw new LogicException('Set a binary or remote first'); - } - - if (preg_match('/(\d+)\.[\d.]+/', $version, $match)) { - return (int) $match[1]; - } - - return false; - } - /** * Fetch result from the /json/version API endpoint - * - * @param string $host - * @param int $port - * - * @return bool|array */ - protected function jsonVersion($host, $port) + protected function getVersion(): bool|array { - $client = new \GuzzleHttp\Client(); + $client = new HttpClient(); try { - $response = $client->request('GET', sprintf('http://%s:%s/json/version', $host, $port)); - } catch (\GuzzleHttp\Exception\ServerException $e) { + $response = $client->request('GET', sprintf('http://%s/json/version', $this->socket)); + } catch (ServerException $e) { // Check if we've run into the host header security change, and re-run the request with no host header. // ref: https://issues.chromium.org/issues/40090537 - if (strstr($e->getMessage(), 'Host header is specified and is not an IP address or localhost.')) { + if (str_contains($e->getMessage(), 'Host header is specified and is not an IP address or localhost.')) { $response = $client->request( 'GET', - sprintf('http://%s:%s/json/version', $host, $port), + sprintf('http://%s/json/version', $this->socket), ['headers' => ['Host' => null]] ); } else { @@ -771,4 +674,12 @@ protected function jsonVersion($host, $port) return json_decode($response->getBody(), true); } + + function isSupported(): bool + { + $version = $this->getVersion(); + preg_match('/Chrome\/([0-9]+)/', $version['Browser'], $matches); + $number = (int)$matches[1]; + return $number >= 59; + } } From a63419393b021ca0242cd30d692a67ffc058d47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 4 Mar 2026 16:46:39 +0100 Subject: [PATCH 10/60] Move and rename HeadlessChrome to HeadlessChromeDriver --- application/forms/ChromeBinaryForm.php | 6 +++--- .../{HeadlessChrome.php => Driver/HeadlessChromeDriver.php} | 6 +++--- library/Pdfexport/ProvidedHook/Pdfexport.php | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename library/Pdfexport/{HeadlessChrome.php => Driver/HeadlessChromeDriver.php} (99%) diff --git a/application/forms/ChromeBinaryForm.php b/application/forms/ChromeBinaryForm.php index 70f8751..bdaff9f 100644 --- a/application/forms/ChromeBinaryForm.php +++ b/application/forms/ChromeBinaryForm.php @@ -7,7 +7,7 @@ use Exception; use Icinga\Forms\ConfigForm; -use Icinga\Module\Pdfexport\HeadlessChrome; +use Icinga\Module\Pdfexport\HeadlessChromeDriver; use Zend_Validate_Callback; class ChromeBinaryForm extends ConfigForm @@ -24,7 +24,7 @@ public function createElements(array $formData) 'label' => $this->translate('Local Binary'), 'placeholder' => '/usr/bin/google-chrome', 'validators' => [new Zend_Validate_Callback(function ($value) { - $chrome = (new HeadlessChrome()) + $chrome = (new HeadlessChromeDriver()) ->setBinary($value); try { @@ -61,7 +61,7 @@ public function createElements(array $formData) $port = $this->getValue('chrome_port') ?: 9222; - $chrome = (new HeadlessChrome()) + $chrome = (new HeadlessChromeDriver()) ->setRemote($value, $port); try { diff --git a/library/Pdfexport/HeadlessChrome.php b/library/Pdfexport/Driver/HeadlessChromeDriver.php similarity index 99% rename from library/Pdfexport/HeadlessChrome.php rename to library/Pdfexport/Driver/HeadlessChromeDriver.php index ba83817..8b6eaea 100644 --- a/library/Pdfexport/HeadlessChrome.php +++ b/library/Pdfexport/Driver/HeadlessChromeDriver.php @@ -3,7 +3,7 @@ // SPDX-FileCopyrightText: 2018 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later -namespace Icinga\Module\Pdfexport; +namespace Icinga\Module\Pdfexport\Driver; use Exception; use GuzzleHttp\Client as HttpClient; @@ -12,13 +12,13 @@ use Icinga\Application\Platform; use Icinga\File\Storage\StorageInterface; use Icinga\File\Storage\TemporaryLocalFileStorage; -use Icinga\Module\Pdfexport\Driver\PfdPrintDriver; +use Icinga\Module\Pdfexport\PrintableHtmlDocument; use ipl\Html\HtmlString; use LogicException; use Throwable; use WebSocket\Client; -class HeadlessChrome implements PfdPrintDriver +class HeadlessChromeDriver implements PfdPrintDriver { /** * Line of stderr output identifying the websocket url diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 4fa5a45..3142d64 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -12,10 +12,10 @@ use Icinga\Application\Icinga; use Icinga\File\Storage\TemporaryLocalFileStorage; use Icinga\Module\Pdfexport\Driver\PfdPrintDriver; -use Icinga\Module\Pdfexport\HeadlessChrome; use Icinga\Module\Pdfexport\PrintableHtmlDocument; use Icinga\Module\Pdfexport\Driver\Webdriver; use Icinga\Module\Pdfexport\Driver\Geckodriver; +use Icinga\Module\Pdfexport\Driver\HeadlessChromeDriver; use Icinga\Module\Pdfexport\Driver\Chromedriver; use ipl\Html\HtmlString; use Karriere\PdfMerge\PdfMerge; @@ -97,7 +97,7 @@ protected function getDriver(): PfdPrintDriver { // return new Chromedriver('http://selenium-chrome:4444'); // return new Geckodriver('http://selenium-firefox:4444'); - return HeadlessChrome::createLocal( + return HeadlessChromeDriver::createLocal( Config::module('pdfexport')->get('chrome', 'binary', '/usr/bin/google-chrome') ); // $serverUrl = 'http://selenium-chrome:4444'; From 1e255ef1fb9c502c6c9205427c2a704466ab6d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 09:39:01 +0100 Subject: [PATCH 11/60] Rename $targetId -> $frameId --- .../Pdfexport/Driver/HeadlessChromeDriver.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/library/Pdfexport/Driver/HeadlessChromeDriver.php b/library/Pdfexport/Driver/HeadlessChromeDriver.php index 8b6eaea..83bdb27 100644 --- a/library/Pdfexport/Driver/HeadlessChromeDriver.php +++ b/library/Pdfexport/Driver/HeadlessChromeDriver.php @@ -56,7 +56,7 @@ class HeadlessChromeDriver implements PfdPrintDriver protected ?Client $page = null; - protected ?string $targetId; + protected ?string $frameId; private array $interceptedRequests = []; @@ -70,8 +70,6 @@ class HeadlessChromeDriver implements PfdPrintDriver protected ?string $browserId = null; - protected ?string $frameId = null; - public function __destruct() { $this->closeBrowser(); @@ -85,6 +83,7 @@ public static function createRemote(string $host, int $port): static $instance->socket = "$host:$port"; try { $result = $instance->getVersion(); + if (! is_array($result)) { throw new Exception('Failed to determine remote chrome version via the /json/version endpoint.'); } @@ -374,12 +373,12 @@ public function getPage(): Client 'url' => 'about:blank' ]); if (isset($result['targetId'])) { - $this->targetId = $result['targetId']; + $this->frameId = $result['targetId']; } else { throw new Exception('Expected target id. Got instead: ' . json_encode($result)); } - $this->page = new Client(sprintf('ws://%s/devtools/page/%s', $this->socket, $this->targetId)); + $this->page = new Client(sprintf('ws://%s/devtools/page/%s', $this->socket, $this->frameId)); // enable various events $this->communicate($this->page, 'Log.enable'); @@ -403,7 +402,7 @@ public function closePage(): void // close tab $result = $this->communicate($this->browser, 'Target.closeTarget', [ - 'targetId' => $this->targetId + 'targetId' => $this->frameId ]); if (! isset($result['success'])) { @@ -411,7 +410,7 @@ public function closePage(): void } $this->page = null; - $this->targetId = null; + $this->frameId = null; } protected function setContent(PrintableHtmlDocument $document): void @@ -425,20 +424,20 @@ protected function setContent(PrintableHtmlDocument $document): void // 'url' => $url // ]); // if (isset($result['frameId'])) { -// $frameId = $result['frameId']; +// $this->targetId = $result['frameId']; // } else { // throw new Exception('Expected navigation frame. Got instead: ' . json_encode($result)); // } // // // wait for page to fully load -// $this->waitFor($page, 'Page.frameStoppedLoading', ['frameId' => $frameId]); +// $this->waitFor($page, 'Page.frameStoppedLoading', ['frameId' => $this->targetId]); if ($document->isEmpty()) { throw new LogicException('Nothing to print'); } // Transfer the document's content directly $this->communicate($page, 'Page.setDocumentContent', [ - 'frameId' => $this->targetId, + 'frameId' => $this->frameId, 'html' => $document->render() ]); From 835e92bce4a5ca689f53d6eac0f1ac47b250d3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 09:39:28 +0100 Subject: [PATCH 12/60] Always waitForPageLoad after setting content --- library/Pdfexport/Driver/Webdriver.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/Pdfexport/Driver/Webdriver.php b/library/Pdfexport/Driver/Webdriver.php index 3754297..316eed1 100644 --- a/library/Pdfexport/Driver/Webdriver.php +++ b/library/Pdfexport/Driver/Webdriver.php @@ -30,7 +30,10 @@ protected function setContent(PrintableHtmlDocument $document): void // This is horribly ugly, but it works for all browser backends $encoded = base64_encode($document); $this->driver->executeScript("document.body.innerHTML = atob('$encoded');"); + } + protected function waitForPageLoad(): void + { // Wait for the body element to ensure the page has fully loaded $wait = new WebDriverWait($this->driver, 10); $wait->until(WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::tagName('body'))); @@ -62,7 +65,9 @@ protected function printToPdf(array $printParameters): string public function toPdf(PrintableHtmlDocument $document): string { $this->setContent($document); + $this->waitForPageLoad(); $printParameters = $this->getPrintParameters($document); + return $this->printToPdf($printParameters); } From 059642c2fcf8a64fa07c55dca72c5c157e3f32ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 09:57:45 +0100 Subject: [PATCH 13/60] Implement basic driver selection --- library/Pdfexport/ProvidedHook/Pdfexport.php | 70 +++++++++++++++++--- library/Pdfexport/WebDriverType.php | 9 +++ 2 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 library/Pdfexport/WebDriverType.php diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 3142d64..8d99d3a 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -6,6 +6,7 @@ namespace Icinga\Module\Pdfexport\ProvidedHook; use Exception; +use Facebook\WebDriver\Firefox\FirefoxDriver; use Icinga\Application\Config; use Icinga\Application\Hook; use Icinga\Application\Hook\PdfexportHook; @@ -13,10 +14,10 @@ use Icinga\File\Storage\TemporaryLocalFileStorage; use Icinga\Module\Pdfexport\Driver\PfdPrintDriver; use Icinga\Module\Pdfexport\PrintableHtmlDocument; -use Icinga\Module\Pdfexport\Driver\Webdriver; use Icinga\Module\Pdfexport\Driver\Geckodriver; use Icinga\Module\Pdfexport\Driver\HeadlessChromeDriver; use Icinga\Module\Pdfexport\Driver\Chromedriver; +use Icinga\Module\Pdfexport\WebDriverType; use ipl\Html\HtmlString; use Karriere\PdfMerge\PdfMerge; @@ -54,6 +55,42 @@ public function isSupported(): bool } } + public static function getBinary(): string + { + return Config::module('pdfexport')->get('chrome', 'binary', '/usr/bin/google-chrome'); + } + + public static function getForceTempStorage(): bool + { + return (bool) Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0'); + } + + public static function getHost(): ?string + { + return Config::module('pdfexport')->get('chrome', 'host'); + } + + public static function getPort(): int + { + return Config::module('pdfexport')->get('chrome', 'port', 9222); + } + + public static function getWebDriverHost(): ?string + { + return Config::module('pdfexport')->get('webdriver', 'host'); + } + + public static function getWebDriverPort(): int + { + return (int)Config::module('pdfexport')->get('webdriver', 'port', 4444); + } + + public static function getWebDriverType(): WebDriverType + { + $str = Config::module('pdfexport')->get('webdriver', 'type', 'chrome'); + return WebDriverType::from($str); + } + public function streamPdfFromHtml($html, $filename): void { $filename = basename($filename, '.pdf') . '.pdf'; @@ -95,14 +132,29 @@ protected function emit(string $pdf, string $filename): void protected function getDriver(): PfdPrintDriver { -// return new Chromedriver('http://selenium-chrome:4444'); -// return new Geckodriver('http://selenium-firefox:4444'); - return HeadlessChromeDriver::createLocal( - Config::module('pdfexport')->get('chrome', 'binary', '/usr/bin/google-chrome') - ); -// $serverUrl = 'http://selenium-chrome:4444'; -// $serverUrl = 'http://chromedriver:9515'; -// $serverUrl = 'http://selenium-firefox:4444'; + if (($host = $this->getWebDriverHost()) !== null) { + $port = $this->getWebDriverPort(); + $url = "$host:$port"; + $type = $this->getWebDriverType(); + return match ($type) { + WebDriverType::Chrome => new Chromedriver($url), + WebDriverType::Firefox => new Geckodriver($url), + default => throw new Exception("Invalid webdriver type $type->value"), + }; + } + + if (($binary = $this->getBinary()) !== null) { + return HeadlessChromeDriver::createLocal($binary); + } + + if (($host = $this->getHost()) !== null) { + return HeadlessChromeDriver::createRemote( + $host, + $this->getPort(), + ); + } + + throw new Exception("No PDF print backend available."); } protected function getPrintableHtmlDocument($html): PrintableHtmlDocument diff --git a/library/Pdfexport/WebDriverType.php b/library/Pdfexport/WebDriverType.php new file mode 100644 index 0000000..ad5d897 --- /dev/null +++ b/library/Pdfexport/WebDriverType.php @@ -0,0 +1,9 @@ + Date: Thu, 5 Mar 2026 12:53:16 +0100 Subject: [PATCH 14/60] Fix form connection validation --- application/forms/ChromeBinaryForm.php | 22 +++++++------- .../Pdfexport/Driver/HeadlessChromeDriver.php | 30 +++++++++++++++---- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/application/forms/ChromeBinaryForm.php b/application/forms/ChromeBinaryForm.php index bdaff9f..92dd405 100644 --- a/application/forms/ChromeBinaryForm.php +++ b/application/forms/ChromeBinaryForm.php @@ -7,7 +7,7 @@ use Exception; use Icinga\Forms\ConfigForm; -use Icinga\Module\Pdfexport\HeadlessChromeDriver; +use Icinga\Module\Pdfexport\Driver\HeadlessChromeDriver; use Zend_Validate_Callback; class ChromeBinaryForm extends ConfigForm @@ -24,22 +24,25 @@ public function createElements(array $formData) 'label' => $this->translate('Local Binary'), 'placeholder' => '/usr/bin/google-chrome', 'validators' => [new Zend_Validate_Callback(function ($value) { - $chrome = (new HeadlessChromeDriver()) - ->setBinary($value); + if (empty($value)) { + return true; + } try { + $chrome = (HeadlessChromeDriver::createLocal($value)); $version = $chrome->getVersion(); } catch (Exception $e) { $this->getElement('chrome_binary')->addError($e->getMessage()); return true; } - if ($version < 59) { + if ($version < HeadlessChromeDriver::MIN_SUPPORTED_CHROME_VERSION) { $this->getElement('chrome_binary')->addError(sprintf( $this->translate( 'Chrome/Chromium supporting headless mode required' - . ' which is provided since version 59. Version detected: %s' + . ' which is provided since version %s. Version detected: %s' ), + HeadlessChromeDriver::MIN_SUPPORTED_CHROME_VERSION, $version )); } @@ -61,22 +64,21 @@ public function createElements(array $formData) $port = $this->getValue('chrome_port') ?: 9222; - $chrome = (new HeadlessChromeDriver()) - ->setRemote($value, $port); - try { + $chrome = HeadlessChromeDriver::createRemote($value, $port); $version = $chrome->getVersion(); } catch (Exception $e) { $this->getElement('chrome_host')->addError($e->getMessage()); return true; } - if ($version < 59) { + if ($version < HeadlessChromeDriver::MIN_SUPPORTED_CHROME_VERSION) { $this->getElement('chrome_host')->addError(sprintf( $this->translate( 'Chrome/Chromium supporting headless mode required' - . ' which is provided since version 59. Version detected: %s' + . ' which is provided since version %s. Version detected: %s' ), + HeadlessChromeDriver::MIN_SUPPORTED_CHROME_VERSION, $version )); } diff --git a/library/Pdfexport/Driver/HeadlessChromeDriver.php b/library/Pdfexport/Driver/HeadlessChromeDriver.php index 83bdb27..7418930 100644 --- a/library/Pdfexport/Driver/HeadlessChromeDriver.php +++ b/library/Pdfexport/Driver/HeadlessChromeDriver.php @@ -20,6 +20,8 @@ class HeadlessChromeDriver implements PfdPrintDriver { + /** @var int */ + public const MIN_SUPPORTED_CHROME_VERSION = 59; /** * Line of stderr output identifying the websocket url * @@ -82,7 +84,7 @@ public static function createRemote(string $host, int $port): static $instance = new self(); $instance->socket = "$host:$port"; try { - $result = $instance->getVersion(); + $result = $instance->getJsonVersion(); if (! is_array($result)) { throw new Exception('Failed to determine remote chrome version via the /json/version endpoint.'); @@ -195,6 +197,10 @@ public static function createLocal(string $path): static usleep($idleTime); } + if ($instance->socket === null || $instance->browserId === null) { + throw new Exception('Could not start browser process.'); + } + return $instance; } @@ -674,11 +680,25 @@ protected function getVersion(): bool|array return json_decode($response->getBody(), true); } - function isSupported(): bool + public function getVersion(): int { - $version = $this->getVersion(); + $version = $this->getJsonVersion(); + + if (! isset($version['Browser'])) { + throw new Exception("Invalid Version Json"); + } + preg_match('/Chrome\/([0-9]+)/', $version['Browser'], $matches); - $number = (int)$matches[1]; - return $number >= 59; + + if (! isset($matches[1])) { + throw new Exception("Malformed Chrome Version String: " . $version['Browser']); + } + + return (int)$matches[1]; + } + + function isSupported(): bool + { + return $this->getVersion() >= self::MIN_SUPPORTED_CHROME_VERSION; } } From c66980db475b3b8258e4a1fb293ff483de011e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 12:55:03 +0100 Subject: [PATCH 15/60] Use chrome Page.printToPDF This allows us to set headers and footers --- library/Pdfexport/Driver/Chromedriver.php | 51 +++++++++++++++++------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/library/Pdfexport/Driver/Chromedriver.php b/library/Pdfexport/Driver/Chromedriver.php index 31fe4fa..9bb951e 100644 --- a/library/Pdfexport/Driver/Chromedriver.php +++ b/library/Pdfexport/Driver/Chromedriver.php @@ -2,32 +2,59 @@ namespace Icinga\Module\Pdfexport\Driver; +use Facebook\WebDriver\Chrome\ChromeDevToolsDriver; use Facebook\WebDriver\Remote\DesiredCapabilities; use Icinga\Module\Pdfexport\PrintableHtmlDocument; class Chromedriver extends Webdriver { + protected ?ChromeDevToolsDriver $dcp = null; + public function __construct(string $url) { parent::__construct($url, DesiredCapabilities::chrome()); } - protected function setContent(PrintableHtmlDocument $document): void + protected function getChromeDeveloperTools(): ChromeDevToolsDriver { - // TODO: Replace with CDP - parent::setContent($document); + if ($this->dcp === null) { + $this->dcp = new ChromeDevToolsDriver($this->driver); + } + return $this->dcp; + } + +// protected function setContent(PrintableHtmlDocument $document): void +// { +// $devTools = $this->getChromeDeveloperTools(); +// $devTools->execute( +// 'Page.setDocumentContent', +// [ +// 'frameId' => 'TODO', +// 'html' => $document->render() +// ] +// ); +// } + + protected function getPrintParameters(PrintableHtmlDocument $document): array + { + $parameters = [ + 'printBackground' => true, + ]; + + return array_merge( + $parameters, + $document->getPrintParameters(), + ); } protected function printToPdf(array $printParameters): string { - // TODO: Implement -// // This only works for chrome -// $devTools = new ChromeDevToolsDriver($driver); -// $result = $devTools->execute( -// 'Page.printToPDF', -// ); -// -// return base64_decode($result['data']); - return parent::printToPdf($printParameters); + $devTools = $this->getChromeDeveloperTools(); + $result = $devTools->execute( + 'Page.printToPDF', + $printParameters, + ); + + return base64_decode($result['data']); } } From 48d78291aced39b83c8a45553b1801d0b756da52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 12:56:28 +0100 Subject: [PATCH 16/60] Fix coverpage creation --- library/Pdfexport/ProvidedHook/Pdfexport.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 8d99d3a..b5887d2 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -108,7 +108,7 @@ public function streamPdfFromHtml($html, $filename): void $coverPageDocument->addAttributes($html->getAttributes()); $coverPageDocument->removeMargins(); - $coverPagePdf = $driver->toPdf($coverPage); + $coverPagePdf = $driver->toPdf($coverPageDocument); $pdf = $this->mergePdfs($coverPagePdf, $pdf); } @@ -159,11 +159,11 @@ protected function getDriver(): PfdPrintDriver protected function getPrintableHtmlDocument($html): PrintableHtmlDocument { - if (! $html instanceof PrintableHtmlDocument) { - $html = (new PrintableHtmlDocument()) - ->setContent(HtmlString::create($html)); + if ($html instanceof PrintableHtmlDocument) { + return $html; } - return $html; + return (new PrintableHtmlDocument()) + ->setContent(HtmlString::create($html)); } protected function mergePdfs(string ...$pdfs): string From 4161126c7d7e742a93df634c0acbd900a5cec5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 12:59:44 +0100 Subject: [PATCH 17/60] FIXUP: remove comment --- library/Pdfexport/ProvidedHook/Pdfexport.php | 1 - 1 file changed, 1 deletion(-) diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index b5887d2..f4b42db 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -47,7 +47,6 @@ public static function first() public function isSupported(): bool { try { - // FIXME: This seems very strange $driver = $this->getDriver(); return $driver->isSupported(); } catch (Exception $e) { From b863a7abb776184deb01ca76f36cefbd5e8bfca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 13:03:27 +0100 Subject: [PATCH 18/60] Use try catch --- library/Pdfexport/ProvidedHook/Pdfexport.php | 45 +++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index f4b42db..fcee4cf 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -11,6 +11,7 @@ use Icinga\Application\Hook; use Icinga\Application\Hook\PdfexportHook; use Icinga\Application\Icinga; +use Icinga\Application\Logger; use Icinga\File\Storage\TemporaryLocalFileStorage; use Icinga\Module\Pdfexport\Driver\PfdPrintDriver; use Icinga\Module\Pdfexport\PrintableHtmlDocument; @@ -131,26 +132,38 @@ protected function emit(string $pdf, string $filename): void protected function getDriver(): PfdPrintDriver { - if (($host = $this->getWebDriverHost()) !== null) { - $port = $this->getWebDriverPort(); - $url = "$host:$port"; - $type = $this->getWebDriverType(); - return match ($type) { - WebDriverType::Chrome => new Chromedriver($url), - WebDriverType::Firefox => new Geckodriver($url), - default => throw new Exception("Invalid webdriver type $type->value"), - }; + try { + if (($host = $this->getWebDriverHost()) !== null) { + $port = $this->getWebDriverPort(); + $url = "$host:$port"; + $type = $this->getWebDriverType(); + return match ($type) { + WebDriverType::Chrome => new Chromedriver($url), + WebDriverType::Firefox => new Geckodriver($url), + default => throw new Exception("Invalid webdriver type $type->value"), + }; + } + } catch (Exception $e) { + Logger::error("Error while creating WebDriver backend: " . $e->getMessage()); } - if (($binary = $this->getBinary()) !== null) { - return HeadlessChromeDriver::createLocal($binary); + try { + if (($host = $this->getHost()) !== null) { + return HeadlessChromeDriver::createRemote( + $host, + $this->getPort(), + ); + } + } catch (Exception $e) { + Logger::error("Error while creating remote HeadlessChrome backend: " . $e->getMessage()); } - if (($host = $this->getHost()) !== null) { - return HeadlessChromeDriver::createRemote( - $host, - $this->getPort(), - ); + try { + if (($binary = $this->getBinary()) !== null) { + return HeadlessChromeDriver::createLocal($binary); + } + } catch (Exception $e) { + Logger::error("Error while creating local HeadlessChrome backend: " . $e->getMessage()); } throw new Exception("No PDF print backend available."); From fb9f48375c9216f20fcaac0ff963a01aa05c8975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 13:05:32 +0100 Subject: [PATCH 19/60] Format changes --- .../Pdfexport/Driver/HeadlessChromeDriver.php | 51 ++++++++++--------- library/Pdfexport/Driver/Webdriver.php | 4 +- library/Pdfexport/PrintStyleSheet.php | 2 +- library/Pdfexport/PrintableHtmlDocument.php | 18 +++---- library/Pdfexport/ProvidedHook/Pdfexport.php | 11 ++-- library/Pdfexport/ShellCommand.php | 26 +++++----- library/Pdfexport/WebDriverType.php | 4 +- 7 files changed, 58 insertions(+), 58 deletions(-) diff --git a/library/Pdfexport/Driver/HeadlessChromeDriver.php b/library/Pdfexport/Driver/HeadlessChromeDriver.php index 7418930..234a82e 100644 --- a/library/Pdfexport/Driver/HeadlessChromeDriver.php +++ b/library/Pdfexport/Driver/HeadlessChromeDriver.php @@ -96,7 +96,7 @@ public static function createRemote(string $host, int $port): static Logger::warning( 'Failed to connect to remote chrome: %s (%s)', $instance->socket, - $e + $e, ); throw $e; @@ -127,8 +127,8 @@ public static function createLocal(string $path): static '--disable-dev-shm-usage', '--remote-debugging-port=0', '--homedir=' => $browserHome, - '--user-data-dir=' => $browserHome - ]) + '--user-data-dir=' => $browserHome, + ]), ]); $env = null; @@ -160,12 +160,12 @@ public static function createLocal(string $path): static proc_terminate($instance->process, 6); // SIGABRT Logger::error( 'Browser timed out after %d seconds without the expected output', - $timeoutSeconds + $timeoutSeconds, ); throw new Exception( 'Received empty response or none at all from browser.' - . ' Please check the logs for further details.' + . ' Please check the logs for further details.', ); } @@ -235,7 +235,7 @@ public function getFileStorage() /** * Set the file storage * - * @param StorageInterface $fileStorage + * @param StorageInterface $fileStorage * * @return $this */ @@ -281,6 +281,7 @@ public static function renderArgumentList(array $arguments): string * * @param string|PrintableHtmlDocument $html * @param bool $asFile + * * @return $this */ public function fromHtml($html, $asFile = false): static @@ -310,7 +311,7 @@ protected function getPrintParameters(PrintableHtmlDocument $document): array { $parameters = [ 'printBackground' => true, - 'transferMode' => 'ReturnAsBase64', + 'transferMode' => 'ReturnAsBase64', ]; return array_merge( @@ -365,7 +366,7 @@ protected function closeBrowser(): void $this->browser = null; } catch (Throwable $e) { // For some reason, the browser doesn't send a response - Logger::debug(sprintf('Failed to close browser connection: ' . $e->getMessage())); + Logger::debug('Failed to close browser connection: ' . $e->getMessage()); } } @@ -376,7 +377,7 @@ public function getPage(): Client // Open new tab, get its id $result = $this->communicate($browser, 'Target.createTarget', [ - 'url' => 'about:blank' + 'url' => 'about:blank', ]); if (isset($result['targetId'])) { $this->frameId = $result['targetId']; @@ -408,7 +409,7 @@ public function closePage(): void // close tab $result = $this->communicate($this->browser, 'Target.closeTarget', [ - 'targetId' => $this->frameId + 'targetId' => $this->frameId, ]); if (! isset($result['success'])) { @@ -443,8 +444,8 @@ protected function setContent(PrintableHtmlDocument $document): void // Transfer the document's content directly $this->communicate($page, 'Page.setDocumentContent', [ - 'frameId' => $this->frameId, - 'html' => $document->render() + 'frameId' => $this->frameId, + 'html' => $document->render(), ]); // wait for the page to fully load @@ -459,21 +460,21 @@ protected function setContent(PrintableHtmlDocument $document): void $this->communicate($page, 'Emulation.setEmulatedMedia', ['media' => 'print']); $this->communicate($page, 'Runtime.evaluate', [ - 'timeout' => 1000, - 'expression' => 'setTimeout(() => new Layout().apply(), 0)' + 'timeout' => 1000, + 'expression' => 'setTimeout(() => new Layout().apply(), 0)', ]); $promisedResult = $this->communicate($page, 'Runtime.evaluate', [ 'awaitPromise' => true, 'returnByValue' => true, 'timeout' => 1000, // Failsafe: doesn't apply to `await` it seems - 'expression' => static::WAIT_FOR_LAYOUT + 'expression' => static::WAIT_FOR_LAYOUT, ]); if (isset($promisedResult['exceptionDetails'])) { if (isset($promisedResult['exceptionDetails']['exception']['description'])) { Logger::error( 'PDF layout failed to initialize: %s', - $promisedResult['exceptionDetails']['exception']['description'] + $promisedResult['exceptionDetails']['exception']['description'], ); } else { Logger::warning('PDF layout failed to initialize. Pages might look skewed.'); @@ -492,7 +493,7 @@ protected function printToPdf(array $printParameters): string // print pdf $result = $this->communicate($page, 'Page.printToPDF', array_merge( $printParameters, - ['transferMode' => 'ReturnAsBase64', 'printBackground' => true] + ['transferMode' => 'ReturnAsBase64', 'printBackground' => true], )); if (! empty($result['data'])) { $pdf = base64_decode($result['data']); @@ -506,9 +507,9 @@ protected function printToPdf(array $printParameters): string private function renderApiCall($method, $options = null): string { $data = [ - 'id' => time(), + 'id' => time(), 'method' => $method, - 'params' => $options ?: [] + 'params' => $options ?: [], ]; return json_encode($data, JSON_FORCE_OBJECT); @@ -523,7 +524,7 @@ private function parseApiResponse(string $payload) throw new Exception(sprintf( 'Error response (%s): %s', $data['error']['code'], - $data['error']['message'] + $data['error']['message'], )); } else { throw new Exception(sprintf('Unknown response received: %s', $payload)); @@ -554,7 +555,7 @@ private function registerEvent($method, $params) $method, join(',', array_map(function ($param) use ($shortenedParams) { return $param . '=' . json_encode($shortenedParams[$param]); - }, array_keys($shortenedParams))) + }, array_keys($shortenedParams))), ); } @@ -569,7 +570,7 @@ private function registerEvent($method, $params) Logger::error( 'Headless Chrome was unable to complete a request to "%s". Error: %s', $requestData['request']['url'], - $params['errorText'] + $params['errorText'], ); } else { $this->interceptedEvents[] = ['method' => $method, 'params' => $params]; @@ -603,7 +604,7 @@ private function waitFor(Client $ws, $eventName, ?array $expectedParams = null) Logger::debug( 'Awaiting CDP event: %s(%s)', $eventName, - $expectedParams ? join(',', array_keys($expectedParams)) : '' + $expectedParams ? join(',', array_keys($expectedParams)) : '', ); } elseif (empty($this->interceptedRequests)) { return null; @@ -653,7 +654,7 @@ private function waitFor(Client $ws, $eventName, ?array $expectedParams = null) /** * Fetch result from the /json/version API endpoint */ - protected function getVersion(): bool|array + protected function getJsonVersion(): bool|array { $client = new HttpClient(); @@ -666,7 +667,7 @@ protected function getVersion(): bool|array $response = $client->request( 'GET', sprintf('http://%s/json/version', $this->socket), - ['headers' => ['Host' => null]] + ['headers' => ['Host' => null]], ); } else { throw $e; diff --git a/library/Pdfexport/Driver/Webdriver.php b/library/Pdfexport/Driver/Webdriver.php index 316eed1..2e7b1fc 100644 --- a/library/Pdfexport/Driver/Webdriver.php +++ b/library/Pdfexport/Driver/Webdriver.php @@ -14,8 +14,8 @@ class Webdriver implements PfdPrintDriver protected RemoteWebDriver $driver; public function __construct( - string $url, - DesiredCapabilities $capabilities + string $url, + DesiredCapabilities $capabilities, ) { $this->driver = RemoteWebDriver::create($url, $capabilities); } diff --git a/library/Pdfexport/PrintStyleSheet.php b/library/Pdfexport/PrintStyleSheet.php index 48ec2f2..34039ad 100644 --- a/library/Pdfexport/PrintStyleSheet.php +++ b/library/Pdfexport/PrintStyleSheet.php @@ -16,7 +16,7 @@ protected function collect() $this->lessCompiler->setTheme(join(DIRECTORY_SEPARATOR, [ Icinga::app()->getModuleManager()->getModule('pdfexport')->getCssDir(), - 'print.less' + 'print.less', ])); if (method_exists($this->lessCompiler, 'setThemeMode')) { diff --git a/library/Pdfexport/PrintableHtmlDocument.php b/library/Pdfexport/PrintableHtmlDocument.php index 9984918..f0b294e 100644 --- a/library/Pdfexport/PrintableHtmlDocument.php +++ b/library/Pdfexport/PrintableHtmlDocument.php @@ -294,11 +294,11 @@ protected function assemble(): void new HtmlElement( 'title', null, - Text::create($this->title) + Text::create($this->title), ), $this->createStylesheet(), - $this->createLayoutScript() - ) + $this->createLayoutScript(), + ), )); $this->getAttributes()->registerAttributeCallback('data-content-height', function () { @@ -511,7 +511,7 @@ function ($matches) use ($app) { return "'data:$mimeType; base64, " . base64_encode($fileContent) . "'"; }, - (new PrintStyleSheet())->render(true) + (new PrintStyleSheet())->render(true), ); return new HtmlElement('style', null, HtmlString::create($css)); @@ -537,7 +537,7 @@ protected function createLayoutScript(): ValidHtml return new HtmlElement( 'script', Attributes::create(['type' => 'application/javascript']), - HtmlString::create($layoutJS) + HtmlString::create($layoutJS), ); } @@ -551,9 +551,9 @@ protected function createHeader(): ValidHtml return (new HtmlDocument()) ->addHtml( new HtmlElement('style', null, HtmlString::create( - static::DEFAULT_HEADER_FOOTER_STYLE + static::DEFAULT_HEADER_FOOTER_STYLE, )), - new HtmlElement('header', null, $this->headerTemplate) + new HtmlElement('header', null, $this->headerTemplate), ); } @@ -567,9 +567,9 @@ protected function createFooter(): ValidHtml return (new HtmlDocument()) ->addHtml( new HtmlElement('style', null, HtmlString::create( - static::DEFAULT_HEADER_FOOTER_STYLE + static::DEFAULT_HEADER_FOOTER_STYLE, )), - new HtmlElement('footer', null, $this->footerTemplate) + new HtmlElement('footer', null, $this->footerTemplate), ); } } diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index fcee4cf..ea2139a 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -6,18 +6,17 @@ namespace Icinga\Module\Pdfexport\ProvidedHook; use Exception; -use Facebook\WebDriver\Firefox\FirefoxDriver; use Icinga\Application\Config; use Icinga\Application\Hook; use Icinga\Application\Hook\PdfexportHook; use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\File\Storage\TemporaryLocalFileStorage; -use Icinga\Module\Pdfexport\Driver\PfdPrintDriver; -use Icinga\Module\Pdfexport\PrintableHtmlDocument; +use Icinga\Module\Pdfexport\Driver\Chromedriver; use Icinga\Module\Pdfexport\Driver\Geckodriver; use Icinga\Module\Pdfexport\Driver\HeadlessChromeDriver; -use Icinga\Module\Pdfexport\Driver\Chromedriver; +use Icinga\Module\Pdfexport\Driver\PfdPrintDriver; +use Icinga\Module\Pdfexport\PrintableHtmlDocument; use Icinga\Module\Pdfexport\WebDriverType; use ipl\Html\HtmlString; use Karriere\PdfMerge\PdfMerge; @@ -33,7 +32,7 @@ public static function first() if (! $pdfexport->isSupported()) { throw new Exception( - sprintf("Can't export: %s does not support exporting PDFs", get_class($pdfexport)) + sprintf("Can't export: %s does not support exporting PDFs", get_class($pdfexport)), ); } } @@ -62,7 +61,7 @@ public static function getBinary(): string public static function getForceTempStorage(): bool { - return (bool) Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0'); + return (bool)Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0'); } public static function getHost(): ?string diff --git a/library/Pdfexport/ShellCommand.php b/library/Pdfexport/ShellCommand.php index 9025560..5e7e47c 100644 --- a/library/Pdfexport/ShellCommand.php +++ b/library/Pdfexport/ShellCommand.php @@ -19,12 +19,12 @@ class ShellCommand /** * Create a new command * - * @param string $command The command to execute - * @param bool $escape Whether to escape the command + * @param string $command The command to execute + * @param bool $escape Whether to escape the command */ public function __construct($command, $escape = true) { - $command = (string) $command; + $command = (string)$command; $this->command = $escape ? escapeshellcmd($command) : $command; } @@ -46,7 +46,7 @@ public function getExitCode() */ public function getStatus() { - $status = (object) proc_get_status($this->resource); + $status = (object)proc_get_status($this->resource); if ($status->running === false && $this->exitCode === null) { // The exit code is only valid the first time proc_get_status is // called in terms of running false, hence we capture it @@ -72,26 +72,26 @@ public function execute() $descriptors = [ ['pipe', 'r'], // stdin ['pipe', 'w'], // stdout - ['pipe', 'w'] // stderr + ['pipe', 'w'], // stderr ]; $this->resource = proc_open( $this->command, $descriptors, - $pipes + $pipes, ); if (! is_resource($this->resource)) { throw new \Exception(sprintf( "Can't fork '%s'", - $this->command + $this->command, )); } - $namedpipes = (object) [ - 'stdin' => &$pipes[0], - 'stdout' => &$pipes[1], - 'stderr' => &$pipes[2] + $namedpipes = (object)[ + 'stdin' => &$pipes[0], + 'stdout' => &$pipes[1], + 'stderr' => &$pipes[2], ]; fclose($namedpipes->stdin); @@ -141,9 +141,9 @@ public function execute() $this->resource = null; - return (object) [ + return (object)[ 'stdout' => $stdout, - 'stderr' => $stderr + 'stderr' => $stderr, ]; } } diff --git a/library/Pdfexport/WebDriverType.php b/library/Pdfexport/WebDriverType.php index ad5d897..4f587bd 100644 --- a/library/Pdfexport/WebDriverType.php +++ b/library/Pdfexport/WebDriverType.php @@ -2,8 +2,8 @@ namespace Icinga\Module\Pdfexport; -enum WebDriverType : string +enum WebDriverType: string { - case Chrome = 'chrome'; + case Chrome = 'chrome'; case Firefox = 'firefox'; } From df37ddb2bb027cab844b5711d3a087dfff7aa605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 13:10:44 +0100 Subject: [PATCH 20/60] Rename Driver to Backend --- application/forms/ChromeBinaryForm.php | 14 +++++++------- .../{Driver => Backend}/Chromedriver.php | 4 ++-- .../{Driver => Backend}/Geckodriver.php | 4 ++-- .../HeadlessChromeBackend.php} | 4 ++-- .../PfdPrintBackend.php} | 4 ++-- .../WebdriverBackend.php} | 4 ++-- library/Pdfexport/ProvidedHook/Pdfexport.php | 18 +++++++++--------- 7 files changed, 26 insertions(+), 26 deletions(-) rename library/Pdfexport/{Driver => Backend}/Chromedriver.php (94%) rename library/Pdfexport/{Driver => Backend}/Geckodriver.php (68%) rename library/Pdfexport/{Driver/HeadlessChromeDriver.php => Backend/HeadlessChromeBackend.php} (99%) rename library/Pdfexport/{Driver/PfdPrintDriver.php => Backend/PfdPrintBackend.php} (69%) rename library/Pdfexport/{Driver/Webdriver.php => Backend/WebdriverBackend.php} (95%) diff --git a/application/forms/ChromeBinaryForm.php b/application/forms/ChromeBinaryForm.php index 92dd405..cb7c576 100644 --- a/application/forms/ChromeBinaryForm.php +++ b/application/forms/ChromeBinaryForm.php @@ -7,7 +7,7 @@ use Exception; use Icinga\Forms\ConfigForm; -use Icinga\Module\Pdfexport\Driver\HeadlessChromeDriver; +use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; use Zend_Validate_Callback; class ChromeBinaryForm extends ConfigForm @@ -29,20 +29,20 @@ public function createElements(array $formData) } try { - $chrome = (HeadlessChromeDriver::createLocal($value)); + $chrome = (HeadlessChromeBackend::createLocal($value)); $version = $chrome->getVersion(); } catch (Exception $e) { $this->getElement('chrome_binary')->addError($e->getMessage()); return true; } - if ($version < HeadlessChromeDriver::MIN_SUPPORTED_CHROME_VERSION) { + if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { $this->getElement('chrome_binary')->addError(sprintf( $this->translate( 'Chrome/Chromium supporting headless mode required' . ' which is provided since version %s. Version detected: %s' ), - HeadlessChromeDriver::MIN_SUPPORTED_CHROME_VERSION, + HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION, $version )); } @@ -65,20 +65,20 @@ public function createElements(array $formData) $port = $this->getValue('chrome_port') ?: 9222; try { - $chrome = HeadlessChromeDriver::createRemote($value, $port); + $chrome = HeadlessChromeBackend::createRemote($value, $port); $version = $chrome->getVersion(); } catch (Exception $e) { $this->getElement('chrome_host')->addError($e->getMessage()); return true; } - if ($version < HeadlessChromeDriver::MIN_SUPPORTED_CHROME_VERSION) { + if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { $this->getElement('chrome_host')->addError(sprintf( $this->translate( 'Chrome/Chromium supporting headless mode required' . ' which is provided since version %s. Version detected: %s' ), - HeadlessChromeDriver::MIN_SUPPORTED_CHROME_VERSION, + HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION, $version )); } diff --git a/library/Pdfexport/Driver/Chromedriver.php b/library/Pdfexport/Backend/Chromedriver.php similarity index 94% rename from library/Pdfexport/Driver/Chromedriver.php rename to library/Pdfexport/Backend/Chromedriver.php index 9bb951e..2a659c9 100644 --- a/library/Pdfexport/Driver/Chromedriver.php +++ b/library/Pdfexport/Backend/Chromedriver.php @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later -namespace Icinga\Module\Pdfexport\Driver; +namespace Icinga\Module\Pdfexport\Backend; use Exception; use GuzzleHttp\Client as HttpClient; @@ -18,7 +18,7 @@ use Throwable; use WebSocket\Client; -class HeadlessChromeDriver implements PfdPrintDriver +class HeadlessChromeBackend implements PfdPrintBackend { /** @var int */ public const MIN_SUPPORTED_CHROME_VERSION = 59; diff --git a/library/Pdfexport/Driver/PfdPrintDriver.php b/library/Pdfexport/Backend/PfdPrintBackend.php similarity index 69% rename from library/Pdfexport/Driver/PfdPrintDriver.php rename to library/Pdfexport/Backend/PfdPrintBackend.php index 3f413cc..04ca9dd 100644 --- a/library/Pdfexport/Driver/PfdPrintDriver.php +++ b/library/Pdfexport/Backend/PfdPrintBackend.php @@ -1,10 +1,10 @@ getDriver(); + $driver = $this->getBackend(); return $driver->isSupported(); } catch (Exception $e) { return false; @@ -96,7 +96,7 @@ public function streamPdfFromHtml($html, $filename): void $document = $this->getPrintableHtmlDocument($html); - $driver = $this->getDriver(); + $driver = $this->getBackend(); $pdf = $driver->toPdf($document); @@ -129,7 +129,7 @@ protected function emit(string $pdf, string $filename): void ->sendResponse(); } - protected function getDriver(): PfdPrintDriver + protected function getBackend(): PfdPrintBackend { try { if (($host = $this->getWebDriverHost()) !== null) { @@ -148,7 +148,7 @@ protected function getDriver(): PfdPrintDriver try { if (($host = $this->getHost()) !== null) { - return HeadlessChromeDriver::createRemote( + return HeadlessChromeBackend::createRemote( $host, $this->getPort(), ); @@ -159,7 +159,7 @@ protected function getDriver(): PfdPrintDriver try { if (($binary = $this->getBinary()) !== null) { - return HeadlessChromeDriver::createLocal($binary); + return HeadlessChromeBackend::createLocal($binary); } } catch (Exception $e) { Logger::error("Error while creating local HeadlessChrome backend: " . $e->getMessage()); From 3457afc1acc082ba781877f9612092008130c460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 5 Mar 2026 13:36:44 +0100 Subject: [PATCH 21/60] Allow for filesystem based html transfer for HeadlesChromeBackend --- .../Backend/HeadlessChromeBackend.php | 81 +++++++++++-------- library/Pdfexport/ProvidedHook/Pdfexport.php | 5 +- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index 386468c..b592265 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -54,6 +54,8 @@ class HeadlessChromeBackend implements PfdPrintBackend protected ?StorageInterface $fileStorage = null; + protected bool $useFilesystemTransfer = false; + protected ?Client $browser = null; protected ?Client $page = null; @@ -105,9 +107,10 @@ public static function createRemote(string $host, int $port): static return $instance; } - public static function createLocal(string $path): static + public static function createLocal(string $path, bool $useFile = false): static { $instance = new self(); + $instance->useFilesystemTransfer = $useFile; $browserHome = $instance->getFileStorage()->resolvePath('HOME'); $descriptors = [ @@ -232,20 +235,6 @@ public function getFileStorage() return $this->fileStorage; } - /** - * Set the file storage - * - * @param StorageInterface $fileStorage - * - * @return $this - */ - public function setFileStorage($fileStorage) - { - $this->fileStorage = $fileStorage; - - return $this; - } - /** * Render the given argument name-value pairs as shell-escaped string */ @@ -424,29 +413,53 @@ protected function setContent(PrintableHtmlDocument $document): void { $page = $this->getPage(); - // TODO: Reimplement -// if (($url = $this->getUrl()) !== null) { -// // Navigate to target -// $result = $this->communicate($page, 'Page.navigate', [ -// 'url' => $url -// ]); -// if (isset($result['frameId'])) { -// $this->targetId = $result['frameId']; -// } else { -// throw new Exception('Expected navigation frame. Got instead: ' . json_encode($result)); -// } -// -// // wait for page to fully load -// $this->waitFor($page, 'Page.frameStoppedLoading', ['frameId' => $this->targetId]); if ($document->isEmpty()) { throw new LogicException('Nothing to print'); } - // Transfer the document's content directly - $this->communicate($page, 'Page.setDocumentContent', [ - 'frameId' => $this->frameId, - 'html' => $document->render(), - ]); + if ($this->useFilesystemTransfer) { + $path = uniqid('icingaweb2-pdfexport-') . '.html'; + $storage = $this->getFileStorage(); + + $storage->create($path, $document->render()); + + $absPath = $storage->resolvePath($path, true); + + Logger::debug('Using filesystem transfer to local chrome instance. Path: ' . $absPath); + + $url = "file://$absPath"; + + // Navigate to target + $result = $this->communicate($page, 'Page.navigate', [ + 'url' => $url, + ]); + + if (isset($result['frameId'])) { + $this->frameId = $result['frameId']; + } else { + throw new Exception('Expected navigation frame. Got instead: ' . json_encode($result)); + } + + // wait for the page to fully load + $this->waitFor( + $page, + 'Page.frameStoppedLoading', + [ + 'frameId' => $this->frameId + ], + ); + + try { + $storage->delete($path); + } catch (Exception $e) { + Logger::warning('Failed to delete file: ' . $e->getMessage()); + } + } else { + $this->communicate($page, 'Page.setDocumentContent', [ + 'frameId' => $this->frameId, + 'html' => $document->render(), + ]); + } // wait for the page to fully load $this->waitFor($page, 'Page.loadEventFired'); diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 0819495..89297de 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -159,7 +159,10 @@ protected function getBackend(): PfdPrintBackend try { if (($binary = $this->getBinary()) !== null) { - return HeadlessChromeBackend::createLocal($binary); + return HeadlessChromeBackend::createLocal( + $binary, + $this->getForceTempStorage(), + ); } } catch (Exception $e) { Logger::error("Error while creating local HeadlessChrome backend: " . $e->getMessage()); From 3becc7f18fe6e668f56d2f181f5f383f0de31fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 6 Mar 2026 08:53:02 +0100 Subject: [PATCH 22/60] Change ConfigController to CompatController --- application/controllers/ConfigController.php | 23 +++++--- ...meBinaryForm.php => BackendConfigForm.php} | 58 ++++++++++++++++++- application/views/scripts/config/chrome.phtml | 6 -- configuration.php | 8 +-- 4 files changed, 77 insertions(+), 18 deletions(-) rename application/forms/{ChromeBinaryForm.php => BackendConfigForm.php} (60%) delete mode 100644 application/views/scripts/config/chrome.phtml diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 6df82d7..5202b1e 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -6,10 +6,12 @@ namespace Icinga\Module\Pdfexport\Controllers; use Icinga\Application\Config; -use Icinga\Module\Pdfexport\Forms\ChromeBinaryForm; -use Icinga\Web\Controller; +use Icinga\Module\Pdfexport\Forms\BackendConfigForm; +use ipl\Html\HtmlString; +use ipl\Web\Compat\CompatController; +use Icinga\Web\Widget\Tabs; -class ConfigController extends Controller +class ConfigController extends CompatController { public function init() { @@ -18,14 +20,21 @@ public function init() parent::init(); } - public function chromeAction() + public function backendAction() { - $form = (new ChromeBinaryForm()) + $form = (new BackendConfigForm()) ->setIniConfig(Config::module('pdfexport')); $form->handleRequest(); - $this->view->tabs = $this->Module()->getConfigTabs()->activate('chrome'); - $this->view->form = $form; + $this->mergeTabs($this->Module()->getConfigTabs()->activate('backend')); + $this->addContent(HtmlString::create($form->render())); + } + + protected function mergeTabs(Tabs $tabs): void + { + foreach ($tabs->getTabs() as $tab) { + $this->tabs->add($tab->getName(), $tab); + } } } diff --git a/application/forms/ChromeBinaryForm.php b/application/forms/BackendConfigForm.php similarity index 60% rename from application/forms/ChromeBinaryForm.php rename to application/forms/BackendConfigForm.php index cb7c576..31bf34e 100644 --- a/application/forms/ChromeBinaryForm.php +++ b/application/forms/BackendConfigForm.php @@ -7,10 +7,13 @@ use Exception; use Icinga\Forms\ConfigForm; +use Icinga\Module\Pdfexport\Backend\Chromedriver; +use Icinga\Module\Pdfexport\Backend\Geckodriver; use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; +use Icinga\Module\Pdfexport\WebDriverType; use Zend_Validate_Callback; -class ChromeBinaryForm extends ConfigForm +class BackendConfigForm extends ConfigForm { public function init() { @@ -81,6 +84,7 @@ public function createElements(array $formData) HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION, $version )); + return true; } return true; @@ -93,5 +97,57 @@ public function createElements(array $formData) 'min' => 1, 'max' => 65535 ]); + + $this->addElement('text', 'webdriver_host', [ + 'label' => $this->translate('WebDriver Host'), + 'validators' => [new Zend_Validate_Callback(function ($value) { + if ($value === null) { + return true; + } + + $port = $this->getValue('webdriver_port') ?: 4444; + $type = $this->getValue('webdriver_type') ?: 'chrome'; + + try { + $url = "$value:$port"; + $backend = match (WebDriverType::from($type)) { + WebDriverType::Chrome => new Chromedriver($url), + WebDriverType::Firefox => new Geckodriver($url), + default => throw new Exception("Invalid webdriver type $type"), + }; + + if (! $backend->isSupported()) { + $this->getElement('webdriver_host') + ->addError($this->translate( + 'The webdriver server reports that it is unable to generate PDFs' + )); + return false; + } + + } catch (Exception $e) { + $this->getElement('webdriver_host')->addError($e->getMessage()); + return false; + } + return true; + })] + ]); + + $this->addElement('number', 'webdriver_port', [ + 'label' => $this->translate('WebDriver Port'), + 'placeholder' => 4444, + 'min' => 1, + 'max' => 65535, + ]); + + $this->addElement('select', 'webdriver_type', [ + 'label' => $this->translate('WebDriver Type'), + 'multiOptions' => array_merge( + ['' => sprintf(' - %s - ', t('Please choose'))], + [ + 'firefox' => t('Firefox'), + 'chrome' => t('Chrome'), + ], + ), + ]); } } diff --git a/application/views/scripts/config/chrome.phtml b/application/views/scripts/config/chrome.phtml deleted file mode 100644 index 46daf07..0000000 --- a/application/views/scripts/config/chrome.phtml +++ /dev/null @@ -1,6 +0,0 @@ -
- -
-
- -
diff --git a/configuration.php b/configuration.php index e219f38..897fbda 100644 --- a/configuration.php +++ b/configuration.php @@ -5,8 +5,8 @@ /** @var \Icinga\Application\Modules\Module $this */ -$this->provideConfigTab('chrome', array( - 'title' => $this->translate('Configure the Chrome/Chromium connection'), - 'label' => $this->translate('Chrome'), - 'url' => 'config/chrome' +$this->provideConfigTab('backend', array( + 'title' => $this->translate('Configure the Chrome/WebDriver connection'), + 'label' => $this->translate('Backend'), + 'url' => 'config/backend' )); From ea4ce28aba962e52cce8941bdbb3956e2df6e9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 6 Mar 2026 10:12:22 +0100 Subject: [PATCH 23/60] Move BackendConfigForm away from ZendForms --- application/controllers/ConfigController.php | 5 +- application/forms/BackendConfigForm.php | 84 +++++------ .../Backend/HeadlessChromeBackend.php | 4 + library/Pdfexport/Form/ConfigForm.php | 135 ++++++++++++++++++ library/Pdfexport/ProvidedHook/Pdfexport.php | 3 +- library/Pdfexport/Web/ShowConfiguration.php | 87 +++++++++++ 6 files changed, 267 insertions(+), 51 deletions(-) create mode 100644 library/Pdfexport/Form/ConfigForm.php create mode 100644 library/Pdfexport/Web/ShowConfiguration.php diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 5202b1e..251176f 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -22,10 +22,9 @@ public function init() public function backendAction() { - $form = (new BackendConfigForm()) - ->setIniConfig(Config::module('pdfexport')); + $form = new BackendConfigForm(Config::module('pdfexport')); - $form->handleRequest(); + $form->handleRequest($this->getServerRequest()); $this->mergeTabs($this->Module()->getConfigTabs()->activate('backend')); $this->addContent(HtmlString::create($form->render())); diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 31bf34e..52e7c7d 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -6,52 +6,44 @@ namespace Icinga\Module\Pdfexport\Forms; use Exception; -use Icinga\Forms\ConfigForm; use Icinga\Module\Pdfexport\Backend\Chromedriver; use Icinga\Module\Pdfexport\Backend\Geckodriver; use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; +use Icinga\Module\Pdfexport\Form\ConfigForm; use Icinga\Module\Pdfexport\WebDriverType; -use Zend_Validate_Callback; +use ipl\Validator\CallbackValidator; class BackendConfigForm extends ConfigForm { - public function init() - { - $this->setName('pdfexport_binary'); - $this->setSubmitLabel($this->translate('Save Changes')); - } - - public function createElements(array $formData) + public function assemble() { $this->addElement('text', 'chrome_binary', [ 'label' => $this->translate('Local Binary'), 'placeholder' => '/usr/bin/google-chrome', - 'validators' => [new Zend_Validate_Callback(function ($value) { - if (empty($value)) { - return true; - } + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { + if (empty($value)) { + return true; + } - try { - $chrome = (HeadlessChromeBackend::createLocal($value)); - $version = $chrome->getVersion(); - } catch (Exception $e) { - $this->getElement('chrome_binary')->addError($e->getMessage()); - return true; - } + try { + $chrome = (HeadlessChromeBackend::createLocal($value)); + $version = $chrome->getVersion(); + } catch (Exception $e) { + $validator->addMessage($e->getMessage()); + return false; + } - if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { - $this->getElement('chrome_binary')->addError(sprintf( - $this->translate( + if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { + $validator->addMessage(t( 'Chrome/Chromium supporting headless mode required' . ' which is provided since version %s. Version detected: %s' - ), - HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION, - $version - )); - } + )); + } - return true; - })] + return true; + }), + ], ]); $this->addElement('checkbox', 'chrome_force_temp_storage', [ @@ -60,7 +52,8 @@ public function createElements(array $formData) $this->addElement('text', 'chrome_host', [ 'label' => $this->translate('Remote Host'), - 'validators' => [new Zend_Validate_Callback(function ($value) { + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { if ($value === null) { return true; } @@ -71,20 +64,16 @@ public function createElements(array $formData) $chrome = HeadlessChromeBackend::createRemote($value, $port); $version = $chrome->getVersion(); } catch (Exception $e) { - $this->getElement('chrome_host')->addError($e->getMessage()); - return true; + $validator->addMessage($e->getMessage()); + return false; } if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { - $this->getElement('chrome_host')->addError(sprintf( - $this->translate( - 'Chrome/Chromium supporting headless mode required' - . ' which is provided since version %s. Version detected: %s' - ), - HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION, - $version + $validator->addMessage(t( + 'Chrome/Chromium supporting headless mode required' + . ' which is provided since version %s. Version detected: %s' )); - return true; + return false; } return true; @@ -100,7 +89,7 @@ public function createElements(array $formData) $this->addElement('text', 'webdriver_host', [ 'label' => $this->translate('WebDriver Host'), - 'validators' => [new Zend_Validate_Callback(function ($value) { + 'validators' => [new CallbackValidator(function ($value, CallbackValidator $validator) { if ($value === null) { return true; } @@ -117,15 +106,12 @@ public function createElements(array $formData) }; if (! $backend->isSupported()) { - $this->getElement('webdriver_host') - ->addError($this->translate( - 'The webdriver server reports that it is unable to generate PDFs' - )); + $validator->addMessage(t('The webdriver server reports that it is unable to generate PDFs')); return false; } } catch (Exception $e) { - $this->getElement('webdriver_host')->addError($e->getMessage()); + $validator->addMessage($e->getMessage()); return false; } return true; @@ -149,5 +135,9 @@ public function createElements(array $formData) ], ), ]); + + $this->addElement('submit', 'submit', [ + 'label' => $this->translate('Store') + ]); } } diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index b592265..e209324 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -112,6 +112,10 @@ public static function createLocal(string $path, bool $useFile = false): static $instance = new self(); $instance->useFilesystemTransfer = $useFile; + if (! file_exists($path)) { + throw new Exception('Local chrome binary not found: ' . $path); + } + $browserHome = $instance->getFileStorage()->resolvePath('HOME'); $descriptors = [ 0 => ['pipe', 'r'], // stdin diff --git a/library/Pdfexport/Form/ConfigForm.php b/library/Pdfexport/Form/ConfigForm.php new file mode 100644 index 0000000..5b61bb5 --- /dev/null +++ b/library/Pdfexport/Form/ConfigForm.php @@ -0,0 +1,135 @@ +hasBeenAssembled) { + parent::ensureAssembled(); + $this->populateFromConfig(); + } + + return $this; + } + + protected function populateFromConfig(): void + { + foreach ($this->getElements() as $element) { + [$section, $key] = $this->getIniKeyFromName($element->getName()); + if ($section === null && $key === null) { + continue; + } + $value = $this->getPopulatedValue($element->getName()) ?? $this->config->get($section, $key); + $this->populate([ + $element->getName() => $value, + ]); + } + } + + protected function getIniKeyFromName(string $name): ?array + { + if ($this->section !== null) { + return [$this->section, $name]; + } + + $parts = explode('_', $name, 2); + if (count($parts) !== 2) { + return [null, null]; + } + + return $parts; + } + + public function getConfigValue(string $name, $default = null): mixed + { + if (! $this->hasElement($name)) { + return $default; + } + + if (($value = $this->getPopulatedValue($name)) !== null) { + return $value; + } + + [$section, $key] = $this->getIniKeyFromName($name); + if ($section === null && $key === null) { + return $default; + } + + if (! $this->config->hasSection($section)) { + return $default; + } + + return $this->config->get($section, $key, $default); + } + + public function getConfigValues(): array + { + $values = []; + foreach ($this->getElements() as $element) { + if ($element->isIgnored()) { + continue; + } + + $values[$element->getName()] = $this->getConfigValue($element->getName()); + } + + return $values; + } + + protected function onSuccess(): void + { + foreach ($this->getElements() as $element) { + if (in_array($element->getName(), $this->ignoredElements)) { + continue; + } + [$section, $key] = $this->getIniKeyFromName($element->getName()); + if ($section === null || $key === null) { + continue; + } + $value = $this->getConfigValue($element->getName()); + + $configSection = $this->config->getSection($section); + if (empty($value)) { + unset($configSection[$key]); + } else { + $configSection->$key = $value; + } + + if ($configSection->isEmpty()) { + $this->config->removeSection($section); + } else { + $this->config->setSection($section, $configSection); + } + } + + try { + $this->config->saveIni(); + } catch (Exception $e) { + $content = $this->getContent(); + array_unshift( + $content, + new ShowConfiguration( + $this->config->getConfigFile(), + $this->config, + ) + ); + $this->setContent($content); + throw $e; + } + } +} diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 89297de..98cfe4f 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -61,7 +61,8 @@ public static function getBinary(): string public static function getForceTempStorage(): bool { - return (bool)Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0'); + $value = Config::module('pdfexport')->get('chrome', 'force_temp_storage', 'n'); + return in_array($value, ['1', 'y']); } public static function getHost(): ?string diff --git a/library/Pdfexport/Web/ShowConfiguration.php b/library/Pdfexport/Web/ShowConfiguration.php new file mode 100644 index 0000000..a67028d --- /dev/null +++ b/library/Pdfexport/Web/ShowConfiguration.php @@ -0,0 +1,87 @@ +addHtml(HtmlElement::create( + 'h4', + null, + t('Saving Configuration Failed!'), + )); + + $this->addHtml(HtmlElement::create( + 'p', + null, + [ + sprintf( + t("The file %s couldn't be stored."), + $this->filePath, + ), + HtmlString::create('
'), + t('This could have one or more of the following reasons:'), + ], + )); + + $this->addHtml(HtmlElement::create( + 'ul', + null, + [ + HtmlElement::create('li', null, t("You don't have file-system permissions to write to the file")), + HtmlElement::create('li', null, t('Something went wrong while writing the file')), + HtmlElement::create( + 'li', + null, + t("There's an application error preventing you from persisting the configuration"), + ), + ], + )); + + $this->addHtml(HtmlElement::create( + 'p', + null, + [ + t( + 'Details can be found in the application log. ' . + "(If you don't have access to this log, call your administrator in this case)" + ), + HtmlString::create('
'), + t('In case you can access the file by yourself, you can open it and insert the config manually:'), + ], + )); + + $this->addHtml( + HtmlElement::create( + 'p', + null, + HtmlElement::create( + 'pre', + null, + HtmlElement::create( + 'code', + null, + (string)$this->config, + ), + ), + ), + ); + } +} From 751d5642ae0aa958cb02abf5f2cdc870a2499392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 6 Mar 2026 10:14:54 +0100 Subject: [PATCH 24/60] Change element order to reflect precedence --- application/forms/BackendConfigForm.php | 145 +++++++++++++----------- 1 file changed, 76 insertions(+), 69 deletions(-) diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 52e7c7d..052351e 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -11,47 +11,68 @@ use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; use Icinga\Module\Pdfexport\Form\ConfigForm; use Icinga\Module\Pdfexport\WebDriverType; +use ipl\Html\Html; use ipl\Validator\CallbackValidator; class BackendConfigForm extends ConfigForm { - public function assemble() + public function assemble(): void { - $this->addElement('text', 'chrome_binary', [ - 'label' => $this->translate('Local Binary'), - 'placeholder' => '/usr/bin/google-chrome', - 'validators' => [ - new CallbackValidator(function ($value, CallbackValidator $validator) { - if (empty($value)) { - return true; - } + $this->add(Html::tag('h2', t("WebDriver"))); - try { - $chrome = (HeadlessChromeBackend::createLocal($value)); - $version = $chrome->getVersion(); - } catch (Exception $e) { - $validator->addMessage($e->getMessage()); + $this->addElement('text', 'webdriver_host', [ + 'label' => $this->translate('Host'), + 'validators' => [new CallbackValidator(function ($value, CallbackValidator $validator) { + if ($value === null) { + return true; + } + + $port = $this->getValue('webdriver_port') ?: 4444; + $type = $this->getValue('webdriver_type') ?: 'chrome'; + + try { + $url = "$value:$port"; + $backend = match (WebDriverType::from($type)) { + WebDriverType::Chrome => new Chromedriver($url), + WebDriverType::Firefox => new Geckodriver($url), + default => throw new Exception("Invalid webdriver type $type"), + }; + + if (! $backend->isSupported()) { + $validator->addMessage(t('The webdriver server reports that it is unable to generate PDFs')); return false; } - if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { - $validator->addMessage(t( - 'Chrome/Chromium supporting headless mode required' - . ' which is provided since version %s. Version detected: %s' - )); - } + } catch (Exception $e) { + $validator->addMessage($e->getMessage()); + return false; + } + return true; + })] + ]); - return true; - }), - ], + $this->addElement('number', 'webdriver_port', [ + 'label' => $this->translate('Port'), + 'placeholder' => 4444, + 'min' => 1, + 'max' => 65535, ]); - $this->addElement('checkbox', 'chrome_force_temp_storage', [ - 'label' => $this->translate('Force local temp storage') + $this->addElement('select', 'webdriver_type', [ + 'label' => $this->translate('Type'), + 'multiOptions' => array_merge( + ['' => sprintf(' - %s - ', t('Please choose'))], + [ + 'firefox' => t('Firefox'), + 'chrome' => t('Chrome'), + ], + ), ]); + $this->add(Html::tag('h2', t("Remote Chrome"))); + $this->addElement('text', 'chrome_host', [ - 'label' => $this->translate('Remote Host'), + 'label' => $this->translate('Host'), 'validators' => [ new CallbackValidator(function ($value, CallbackValidator $validator) { if ($value === null) { @@ -81,59 +102,45 @@ public function assemble() ]); $this->addElement('number', 'chrome_port', [ - 'label' => $this->translate('Remote Port'), - 'placeholder' => 9222, - 'min' => 1, - 'max' => 65535 + 'label' => $this->translate('Port'), + 'placeholder' => 9222, + 'min' => 1, + 'max' => 65535 ]); - $this->addElement('text', 'webdriver_host', [ - 'label' => $this->translate('WebDriver Host'), - 'validators' => [new CallbackValidator(function ($value, CallbackValidator $validator) { - if ($value === null) { - return true; - } - - $port = $this->getValue('webdriver_port') ?: 4444; - $type = $this->getValue('webdriver_type') ?: 'chrome'; + $this->add(Html::tag('h2', t("Local Chrome"))); - try { - $url = "$value:$port"; - $backend = match (WebDriverType::from($type)) { - WebDriverType::Chrome => new Chromedriver($url), - WebDriverType::Firefox => new Geckodriver($url), - default => throw new Exception("Invalid webdriver type $type"), - }; + $this->addElement('text', 'chrome_binary', [ + 'label' => $this->translate('Binary'), + 'placeholder' => '/usr/bin/google-chrome', + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { + if (empty($value)) { + return true; + } - if (! $backend->isSupported()) { - $validator->addMessage(t('The webdriver server reports that it is unable to generate PDFs')); + try { + $chrome = (HeadlessChromeBackend::createLocal($value)); + $version = $chrome->getVersion(); + } catch (Exception $e) { + $validator->addMessage($e->getMessage()); return false; } - } catch (Exception $e) { - $validator->addMessage($e->getMessage()); - return false; - } - return true; - })] - ]); + if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { + $validator->addMessage(t( + 'Chrome/Chromium supporting headless mode required' + . ' which is provided since version %s. Version detected: %s' + )); + } - $this->addElement('number', 'webdriver_port', [ - 'label' => $this->translate('WebDriver Port'), - 'placeholder' => 4444, - 'min' => 1, - 'max' => 65535, + return true; + }), + ], ]); - $this->addElement('select', 'webdriver_type', [ - 'label' => $this->translate('WebDriver Type'), - 'multiOptions' => array_merge( - ['' => sprintf(' - %s - ', t('Please choose'))], - [ - 'firefox' => t('Firefox'), - 'chrome' => t('Chrome'), - ], - ), + $this->addElement('checkbox', 'chrome_force_temp_storage', [ + 'label' => $this->translate('Use temp storage') ]); $this->addElement('submit', 'submit', [ From f2fc458ec0ca75143a2758fb8ea360b7313c1e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 6 Mar 2026 11:23:13 +0100 Subject: [PATCH 25/60] Add headers and short descriptions --- application/forms/BackendConfigForm.php | 29 ++++++++++++++++++++++++- public/css/module.less | 6 +++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 public/css/module.less diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 052351e..0122dd6 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -12,16 +12,31 @@ use Icinga\Module\Pdfexport\Form\ConfigForm; use Icinga\Module\Pdfexport\WebDriverType; use ipl\Html\Html; +use ipl\Html\HtmlElement; use ipl\Validator\CallbackValidator; class BackendConfigForm extends ConfigForm { public function assemble(): void { + $this->add(HtmlElement::create( + 'div', + ['class' => 'note'], + t( + 'The precedence for the chosen backend is the same as in this configuration form. ' . + 'Backends that are not configured are skipped and backends further down the list act as a fallback.' + ), + )); + $this->add(Html::tag('h2', t("WebDriver"))); + $this->add(Html::tag('p', t( + 'WebDriver is a API that allows software to automatically control and interact with a web browser, ' . + 'commonly used for automating website testing through tools like Selenium WebDriver.' + ))); $this->addElement('text', 'webdriver_host', [ 'label' => $this->translate('Host'), + 'description' => $this->translate('Host address of the webdriver server'), 'validators' => [new CallbackValidator(function ($value, CallbackValidator $validator) { if ($value === null) { return true; @@ -53,6 +68,7 @@ public function assemble(): void $this->addElement('number', 'webdriver_port', [ 'label' => $this->translate('Port'), + 'description' => $this->translate('Port of the webdriver instance. (Default: 4444)'), 'placeholder' => 4444, 'min' => 1, 'max' => 65535, @@ -60,6 +76,7 @@ public function assemble(): void $this->addElement('select', 'webdriver_type', [ 'label' => $this->translate('Type'), + 'description' => $this->translate('The type of webdriver server.'), 'multiOptions' => array_merge( ['' => sprintf(' - %s - ', t('Please choose'))], [ @@ -70,9 +87,13 @@ public function assemble(): void ]); $this->add(Html::tag('h2', t("Remote Chrome"))); + $this->add(Html::tag('p', t( + 'A remote chrome instance and it\'s debug interface can be used to create PDFs.' + ))); $this->addElement('text', 'chrome_host', [ 'label' => $this->translate('Host'), + 'description' => $this->translate('Host address of the server with the running web browser.'), 'validators' => [ new CallbackValidator(function ($value, CallbackValidator $validator) { if ($value === null) { @@ -103,16 +124,21 @@ public function assemble(): void $this->addElement('number', 'chrome_port', [ 'label' => $this->translate('Port'), + 'description' => $this->translate('Port of the chrome developer tools. (Default: 9222)'), 'placeholder' => 9222, 'min' => 1, 'max' => 65535 ]); $this->add(Html::tag('h2', t("Local Chrome"))); + $this->add(Html::tag('p', t( + 'Start a chrome instance on the same server as icingaweb2. This is always attempted as a fallback.', + ))); $this->addElement('text', 'chrome_binary', [ 'label' => $this->translate('Binary'), 'placeholder' => '/usr/bin/google-chrome', + 'description' => $this->translate('Path to the binary of the web browser.'), 'validators' => [ new CallbackValidator(function ($value, CallbackValidator $validator) { if (empty($value)) { @@ -140,7 +166,8 @@ public function assemble(): void ]); $this->addElement('checkbox', 'chrome_force_temp_storage', [ - 'label' => $this->translate('Use temp storage') + 'label' => $this->translate('Use temp storage'), + 'description' => $this->translate('Use temp storage to transfer the html to the local chrome instance.'), ]); $this->addElement('submit', 'submit', [ diff --git a/public/css/module.less b/public/css/module.less new file mode 100644 index 0000000..aa4e397 --- /dev/null +++ b/public/css/module.less @@ -0,0 +1,6 @@ +.note { + background-color: @empty-state-bar-bg; + color: @default-text-color; + padding: @vertical-padding @horizontal-padding; + border-radius: .4em; +} From 408175c7bb7a01271f3a6e27195b5e61b88b9583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 6 Mar 2026 14:00:36 +0100 Subject: [PATCH 26/60] Require two underscores to divide section and key --- application/forms/BackendConfigForm.php | 20 ++++++++++---------- library/Pdfexport/Form/ConfigForm.php | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 0122dd6..3c2b7fa 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -34,7 +34,7 @@ public function assemble(): void 'commonly used for automating website testing through tools like Selenium WebDriver.' ))); - $this->addElement('text', 'webdriver_host', [ + $this->addElement('text', 'webdriver__host', [ 'label' => $this->translate('Host'), 'description' => $this->translate('Host address of the webdriver server'), 'validators' => [new CallbackValidator(function ($value, CallbackValidator $validator) { @@ -42,8 +42,8 @@ public function assemble(): void return true; } - $port = $this->getValue('webdriver_port') ?: 4444; - $type = $this->getValue('webdriver_type') ?: 'chrome'; + $port = $this->getValue('webdriver__port') ?: 4444; + $type = $this->getValue('webdriver__type') ?: 'chrome'; try { $url = "$value:$port"; @@ -66,7 +66,7 @@ public function assemble(): void })] ]); - $this->addElement('number', 'webdriver_port', [ + $this->addElement('number', 'webdriver__port', [ 'label' => $this->translate('Port'), 'description' => $this->translate('Port of the webdriver instance. (Default: 4444)'), 'placeholder' => 4444, @@ -74,7 +74,7 @@ public function assemble(): void 'max' => 65535, ]); - $this->addElement('select', 'webdriver_type', [ + $this->addElement('select', 'webdriver__type', [ 'label' => $this->translate('Type'), 'description' => $this->translate('The type of webdriver server.'), 'multiOptions' => array_merge( @@ -91,7 +91,7 @@ public function assemble(): void 'A remote chrome instance and it\'s debug interface can be used to create PDFs.' ))); - $this->addElement('text', 'chrome_host', [ + $this->addElement('text', 'remote_chrome_host', [ 'label' => $this->translate('Host'), 'description' => $this->translate('Host address of the server with the running web browser.'), 'validators' => [ @@ -100,7 +100,7 @@ public function assemble(): void return true; } - $port = $this->getValue('chrome_port') ?: 9222; + $port = $this->getValue('remote_chrome__port') ?: 9222; try { $chrome = HeadlessChromeBackend::createRemote($value, $port); @@ -122,7 +122,7 @@ public function assemble(): void })] ]); - $this->addElement('number', 'chrome_port', [ + $this->addElement('number', 'remote_chrome__port', [ 'label' => $this->translate('Port'), 'description' => $this->translate('Port of the chrome developer tools. (Default: 9222)'), 'placeholder' => 9222, @@ -135,7 +135,7 @@ public function assemble(): void 'Start a chrome instance on the same server as icingaweb2. This is always attempted as a fallback.', ))); - $this->addElement('text', 'chrome_binary', [ + $this->addElement('text', 'local_chrome__binary', [ 'label' => $this->translate('Binary'), 'placeholder' => '/usr/bin/google-chrome', 'description' => $this->translate('Path to the binary of the web browser.'), @@ -165,7 +165,7 @@ public function assemble(): void ], ]); - $this->addElement('checkbox', 'chrome_force_temp_storage', [ + $this->addElement('checkbox', 'local_chrome__force_temp_storage', [ 'label' => $this->translate('Use temp storage'), 'description' => $this->translate('Use temp storage to transfer the html to the local chrome instance.'), ]); diff --git a/library/Pdfexport/Form/ConfigForm.php b/library/Pdfexport/Form/ConfigForm.php index 5b61bb5..b1b6aa1 100644 --- a/library/Pdfexport/Form/ConfigForm.php +++ b/library/Pdfexport/Form/ConfigForm.php @@ -47,7 +47,7 @@ protected function getIniKeyFromName(string $name): ?array return [$this->section, $name]; } - $parts = explode('_', $name, 2); + $parts = explode('__', $name, 2); if (count($parts) !== 2) { return [null, null]; } From fbdc9dcc446fa9fa7fd410fe045eece275b9d264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 6 Mar 2026 14:01:21 +0100 Subject: [PATCH 27/60] Move initialization of the backend to the backend locator --- application/forms/BackendConfigForm.php | 7 +- library/Pdfexport/BackendLocator.php | 130 +++++++++++++++++++ library/Pdfexport/ProvidedHook/Pdfexport.php | 103 ++------------- library/Pdfexport/WebDriverType.php | 9 -- 4 files changed, 146 insertions(+), 103 deletions(-) create mode 100644 library/Pdfexport/BackendLocator.php delete mode 100644 library/Pdfexport/WebDriverType.php diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 3c2b7fa..0154dee 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -10,7 +10,6 @@ use Icinga\Module\Pdfexport\Backend\Geckodriver; use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; use Icinga\Module\Pdfexport\Form\ConfigForm; -use Icinga\Module\Pdfexport\WebDriverType; use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Validator\CallbackValidator; @@ -47,9 +46,9 @@ public function assemble(): void try { $url = "$value:$port"; - $backend = match (WebDriverType::from($type)) { - WebDriverType::Chrome => new Chromedriver($url), - WebDriverType::Firefox => new Geckodriver($url), + $backend = match ($type) { + 'chrome' => new Chromedriver($url), + 'firefox' => new Geckodriver($url), default => throw new Exception("Invalid webdriver type $type"), }; diff --git a/library/Pdfexport/BackendLocator.php b/library/Pdfexport/BackendLocator.php new file mode 100644 index 0000000..53ccf34 --- /dev/null +++ b/library/Pdfexport/BackendLocator.php @@ -0,0 +1,130 @@ +getSingleBackend($section); + if ($backend === null) { + continue; + } + return $backend; + } + + return null; + } + + protected function connectToWebDriver(string $section): ?PfdPrintBackend + { + $config = Config::module('pdfexport'); + try { + $host = $config->get($section, 'host'); + if ($host === null) { + return null; + } + $port = $config->get($section, 'port', 4444); + $url = "$host:$port"; + $type = $config->get($section, 'type'); + $backend = match ($type) { + 'chrome' => new Chromedriver($url), + 'firefox' => new Geckodriver($url), + default => throw new Exception("Invalid webdriver type $type"), + }; + Logger::info("Connected WebDriver Backend: $section"); + return $backend; + } catch (Exception $e) { + Logger::warning("Webdriver connection failed! backend: $section, error: " . $e->getMessage()); + } + return null; + } + + protected function connectToRemoteChrome(string $section): ?PfdPrintBackend + { + $config = Config::module('pdfexport'); + try { + $host = $config->get($section, 'host'); + if ($host === null) { + return null; + } + $port = $config->get($section, 'port', 9222); + $backend = HeadlessChromeBackend::createRemote( + $host, + $port, + ); + Logger::info("Connected WebDriver Backend: $section"); + return $backend; + } catch (Exception $e) { + Logger::warning("Error while creating remote HeadlessChrome! backend: $section, error: " . $e->getMessage()); + } + return null; + } + + protected function connectToLocalChrome(string $section): ?PfdPrintBackend + { + $config = Config::module('pdfexport'); + $binary = $config->get($section, 'binary', '/usr/bin/google-chrome'); + try { + if ($binary === null) { + return null; + } + $backend = HeadlessChromeBackend::createLocal( + $binary, + $this->getForceTempStorage(), + ); + Logger::info("Connected WebDriver Backend: $section"); + return $backend; + } catch (Exception $e) { + Logger::warning( + "Error while creating HeadlessChrome backend: $section, path: $binary, error:" . $e->getMessage(), + ); + } + return null; + } + + protected function getSingleBackend($section): ?PfdPrintBackend + { + $config = Config::module('pdfexport'); + if (! $config->hasSection($section)) { + return null; + } + + Logger::info("Connecting to backend $section."); + + $backend = match($section) { + 'local_chrome' => $this->connectToLocalChrome($section), + 'remote_chrome' => $this->connectToRemoteChrome($section), + default => $this->connectToWebDriver($section), + }; + + if ($backend === null) { + Logger::warning("Failed to connect to backend: $section"); + return null; + } + + if (! $backend->isSupported()) { + Logger::info("Connected backend $section reports that it doesn't support generating PDFs"); + return null; + } + + return $backend; + } + + public static function getForceTempStorage(): bool + { + $value = Config::module('pdfexport')->get('chrome', 'force_temp_storage', 'n'); + return in_array($value, ['1', 'y']); + } +} diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 98cfe4f..4e5447d 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -6,18 +6,14 @@ namespace Icinga\Module\Pdfexport\ProvidedHook; use Exception; -use Icinga\Application\Config; use Icinga\Application\Hook; use Icinga\Application\Hook\PdfexportHook; use Icinga\Application\Icinga; use Icinga\Application\Logger; +use Icinga\Application\Web; use Icinga\File\Storage\TemporaryLocalFileStorage; -use Icinga\Module\Pdfexport\Backend\Chromedriver; -use Icinga\Module\Pdfexport\Backend\Geckodriver; -use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; -use Icinga\Module\Pdfexport\Backend\PfdPrintBackend; +use Icinga\Module\Pdfexport\BackendLocator; use Icinga\Module\Pdfexport\PrintableHtmlDocument; -use Icinga\Module\Pdfexport\WebDriverType; use ipl\Html\HtmlString; use Karriere\PdfMerge\PdfMerge; @@ -46,60 +42,29 @@ public static function first() public function isSupported(): bool { + $locator = new BackendLocator(); try { - $driver = $this->getBackend(); - return $driver->isSupported(); + $backend = $locator->getFirstSupportedBackend(); + return $backend !== null; } catch (Exception $e) { + Logger::warning("No supported PDF backend available."); return false; } } - public static function getBinary(): string - { - return Config::module('pdfexport')->get('chrome', 'binary', '/usr/bin/google-chrome'); - } - - public static function getForceTempStorage(): bool - { - $value = Config::module('pdfexport')->get('chrome', 'force_temp_storage', 'n'); - return in_array($value, ['1', 'y']); - } - - public static function getHost(): ?string - { - return Config::module('pdfexport')->get('chrome', 'host'); - } - - public static function getPort(): int - { - return Config::module('pdfexport')->get('chrome', 'port', 9222); - } - - public static function getWebDriverHost(): ?string - { - return Config::module('pdfexport')->get('webdriver', 'host'); - } - - public static function getWebDriverPort(): int - { - return (int)Config::module('pdfexport')->get('webdriver', 'port', 4444); - } - - public static function getWebDriverType(): WebDriverType - { - $str = Config::module('pdfexport')->get('webdriver', 'type', 'chrome'); - return WebDriverType::from($str); - } - public function streamPdfFromHtml($html, $filename): void { $filename = basename($filename, '.pdf') . '.pdf'; $document = $this->getPrintableHtmlDocument($html); - $driver = $this->getBackend(); + $locator = new BackendLocator(); + $backend = $locator->getFirstSupportedBackend(); + if ($backend === null) { + Logger::warning("No supported PDF backend available."); + } - $pdf = $driver->toPdf($document); + $pdf = $backend->toPdf($document); if ($html instanceof PrintableHtmlDocument) { $coverPage = $html->getCoverPage(); @@ -108,7 +73,7 @@ public function streamPdfFromHtml($html, $filename): void $coverPageDocument->addAttributes($html->getAttributes()); $coverPageDocument->removeMargins(); - $coverPagePdf = $driver->toPdf($coverPageDocument); + $coverPagePdf = $backend->toPdf($coverPageDocument); $pdf = $this->mergePdfs($coverPagePdf, $pdf); } @@ -130,48 +95,6 @@ protected function emit(string $pdf, string $filename): void ->sendResponse(); } - protected function getBackend(): PfdPrintBackend - { - try { - if (($host = $this->getWebDriverHost()) !== null) { - $port = $this->getWebDriverPort(); - $url = "$host:$port"; - $type = $this->getWebDriverType(); - return match ($type) { - WebDriverType::Chrome => new Chromedriver($url), - WebDriverType::Firefox => new Geckodriver($url), - default => throw new Exception("Invalid webdriver type $type->value"), - }; - } - } catch (Exception $e) { - Logger::error("Error while creating WebDriver backend: " . $e->getMessage()); - } - - try { - if (($host = $this->getHost()) !== null) { - return HeadlessChromeBackend::createRemote( - $host, - $this->getPort(), - ); - } - } catch (Exception $e) { - Logger::error("Error while creating remote HeadlessChrome backend: " . $e->getMessage()); - } - - try { - if (($binary = $this->getBinary()) !== null) { - return HeadlessChromeBackend::createLocal( - $binary, - $this->getForceTempStorage(), - ); - } - } catch (Exception $e) { - Logger::error("Error while creating local HeadlessChrome backend: " . $e->getMessage()); - } - - throw new Exception("No PDF print backend available."); - } - protected function getPrintableHtmlDocument($html): PrintableHtmlDocument { if ($html instanceof PrintableHtmlDocument) { diff --git a/library/Pdfexport/WebDriverType.php b/library/Pdfexport/WebDriverType.php deleted file mode 100644 index 4f587bd..0000000 --- a/library/Pdfexport/WebDriverType.php +++ /dev/null @@ -1,9 +0,0 @@ - Date: Fri, 6 Mar 2026 14:04:27 +0100 Subject: [PATCH 28/60] Add license headers --- library/Pdfexport/Backend/Chromedriver.php | 2 ++ library/Pdfexport/Backend/Geckodriver.php | 2 ++ library/Pdfexport/Backend/PfdPrintBackend.php | 2 ++ library/Pdfexport/Backend/WebdriverBackend.php | 2 ++ library/Pdfexport/Form/ConfigForm.php | 2 ++ library/Pdfexport/Web/ShowConfiguration.php | 2 ++ public/css/module.less | 2 ++ 7 files changed, 14 insertions(+) diff --git a/library/Pdfexport/Backend/Chromedriver.php b/library/Pdfexport/Backend/Chromedriver.php index 2a659c9..368356e 100644 --- a/library/Pdfexport/Backend/Chromedriver.php +++ b/library/Pdfexport/Backend/Chromedriver.php @@ -1,5 +1,7 @@ Date: Mon, 9 Mar 2026 11:35:07 +0100 Subject: [PATCH 29/60] Use outerHTML instead of innerHTML to preseve class names on the body element --- library/Pdfexport/Backend/WebdriverBackend.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index 72ac266..81d08c3 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -9,6 +9,7 @@ use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverExpectedCondition; use Facebook\WebDriver\WebDriverWait; +use Icinga\Application\Logger; use Icinga\Module\Pdfexport\PrintableHtmlDocument; class WebdriverBackend implements PfdPrintBackend @@ -31,7 +32,7 @@ protected function setContent(PrintableHtmlDocument $document): void { // This is horribly ugly, but it works for all browser backends $encoded = base64_encode($document); - $this->driver->executeScript("document.body.innerHTML = atob('$encoded');"); + $this->driver->executeScript("document.body.outerHTML = atob('$encoded');"); } protected function waitForPageLoad(): void From 5e61ca1d7cc0698e4d0a00da989c93dbbbaf09a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 9 Mar 2026 11:39:04 +0100 Subject: [PATCH 30/60] Remove unused methods --- .../Backend/HeadlessChromeBackend.php | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index e209324..3adbf33 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -269,37 +269,6 @@ public static function renderArgumentList(array $arguments): string return implode(' ', $list); } - /** - * Use the given HTML as input - * - * @param string|PrintableHtmlDocument $html - * @param bool $asFile - * - * @return $this - */ - public function fromHtml($html, $asFile = false): static - { - if ($html instanceof PrintableHtmlDocument) { - $this->document = $html; - } else { - $this->document = (new PrintableHtmlDocument()) - ->setContent(HtmlString::create($html)); - } - - if ($asFile) { - $path = uniqid('icingaweb2-pdfexport-') . '.html'; - $storage = $this->getFileStorage(); - - $storage->create($path, $this->document->render()); - - $path = $storage->resolvePath($path, true); - - $this->setUrl("file://$path"); - } - - return $this; - } - protected function getPrintParameters(PrintableHtmlDocument $document): array { $parameters = [ @@ -320,24 +289,6 @@ public function toPdf(PrintableHtmlDocument $document): string return $this->printToPdf($printParameters); } - /** - * Export to PDF and save as file on disk - * - * @return string The path to the file on disk - */ - public function savePdf() - { - $path = uniqid('icingaweb2-pdfexport-') . '.pdf'; - - $storage = $this->getFileStorage(); - $storage->create($path, ''); - - $path = $storage->resolvePath($path, true); - file_put_contents($path, $this->toPdf()); - - return $path; - } - protected function getBrowser(): Client { if ($this->browser === null) { From cafe3a7a8a4be250e4b29b80af30c40bbc9b63c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 9 Mar 2026 13:39:20 +0100 Subject: [PATCH 31/60] Explicitly call close on the backend This fixes an issue where the webdriver server is never closed because PDF merging causes OOM. --- library/Pdfexport/Backend/HeadlessChromeBackend.php | 12 ++++++++---- library/Pdfexport/Backend/PfdPrintBackend.php | 2 ++ library/Pdfexport/Backend/WebdriverBackend.php | 7 ++++++- library/Pdfexport/ProvidedHook/Pdfexport.php | 5 +++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index 3adbf33..2724c99 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -13,7 +13,6 @@ use Icinga\File\Storage\StorageInterface; use Icinga\File\Storage\TemporaryLocalFileStorage; use Icinga\Module\Pdfexport\PrintableHtmlDocument; -use ipl\Html\HtmlString; use LogicException; use Throwable; use WebSocket\Client; @@ -76,9 +75,7 @@ class HeadlessChromeBackend implements PfdPrintBackend public function __destruct() { - $this->closeBrowser(); - $this->closeBrowser(); - $this->closeLocal(); + $this->close(); } public static function createRemote(string $host, int $port): static @@ -670,4 +667,11 @@ function isSupported(): bool { return $this->getVersion() >= self::MIN_SUPPORTED_CHROME_VERSION; } + + function close(): void + { + $this->closeBrowser(); + $this->closeBrowser(); + $this->closeLocal(); + } } diff --git a/library/Pdfexport/Backend/PfdPrintBackend.php b/library/Pdfexport/Backend/PfdPrintBackend.php index e579ee2..00f5baa 100644 --- a/library/Pdfexport/Backend/PfdPrintBackend.php +++ b/library/Pdfexport/Backend/PfdPrintBackend.php @@ -11,4 +11,6 @@ interface PfdPrintBackend function toPdf(PrintableHtmlDocument $document): string; function isSupported(): bool; + + function close(): void; } diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index 81d08c3..401b154 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -25,7 +25,7 @@ public function __construct( function __destruct() { - $this->driver->quit(); + $this->close(); } protected function setContent(PrintableHtmlDocument $document): void @@ -79,4 +79,9 @@ function isSupported(): bool // TODO: Come up with a check return true; } + + function close(): void + { + $this->driver->quit(); + } } diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 4e5447d..b02d2ac 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -75,10 +75,15 @@ public function streamPdfFromHtml($html, $filename): void $coverPagePdf = $backend->toPdf($coverPageDocument); + $backend->close(); + $pdf = $this->mergePdfs($coverPagePdf, $pdf); } } + $backend->close(); + unset($coverPage); + $this->emit($pdf, $filename); exit; From bf2c0769a9e12df02cf7449ec456194bc8be5b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 10 Mar 2026 08:46:31 +0100 Subject: [PATCH 32/60] Fixup: clear header & set transfer mode --- library/Pdfexport/Backend/Chromedriver.php | 1 + library/Pdfexport/Backend/WebdriverBackend.php | 1 + 2 files changed, 2 insertions(+) diff --git a/library/Pdfexport/Backend/Chromedriver.php b/library/Pdfexport/Backend/Chromedriver.php index 368356e..bf86fcb 100644 --- a/library/Pdfexport/Backend/Chromedriver.php +++ b/library/Pdfexport/Backend/Chromedriver.php @@ -41,6 +41,7 @@ protected function getPrintParameters(PrintableHtmlDocument $document): array { $parameters = [ 'printBackground' => true, + 'transferMode' => 'ReturnAsBase64', ]; return array_merge( diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index 401b154..f6cedde 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -32,6 +32,7 @@ protected function setContent(PrintableHtmlDocument $document): void { // This is horribly ugly, but it works for all browser backends $encoded = base64_encode($document); + $this->driver->executeScript('document.head.remove()'); $this->driver->executeScript("document.body.outerHTML = atob('$encoded');"); } From f4d2fc33a193880200e3f847e1e3e379778a7a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 10 Mar 2026 10:03:04 +0100 Subject: [PATCH 33/60] Manually close filestorage --- library/Pdfexport/Backend/HeadlessChromeBackend.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index 2724c99..670ff64 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -220,6 +220,15 @@ protected function closeLocal(): void proc_close($this->process); $this->process = null; } + + try { + if ($this->fileStorage !== null) { + unset($this->fileStorage); + $this->fileStorage = null; + } + } catch (Exception $exception) { + Logger::error("Failed to close local temporary file storage: " . $exception->getMessage()); + } } /** From eb13c9bb36b905f90fece163d3b0f40bc157ce42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 10 Mar 2026 10:03:42 +0100 Subject: [PATCH 34/60] Wait for chrome to close and force close it after a timeout --- .../Backend/HeadlessChromeBackend.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index 670ff64..b0ef909 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -217,6 +217,27 @@ protected function closeLocal(): void if ($this->process !== null) { proc_terminate($this->process); + + $start = time(); + $running = true; + + while ($running && (time() - $start) < 5) { + $status = proc_get_status($this->process); + $running = $status['running']; + + if ($running) { + usleep(100000); + } + } + + // If still running after 5 seconds, force kills the entire process group + if ($running) { + $status = proc_get_status($this->process); + if (! empty($status['pid'])) { + posix_kill(-$status['pid'], SIGKILL); + } + } + proc_close($this->process); $this->process = null; } From 059f19718d5ff71add414320522adb6de79f0ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 10 Mar 2026 10:37:24 +0100 Subject: [PATCH 35/60] Use constants instead of magic numbers --- .../Backend/HeadlessChromeBackend.php | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index b0ef909..c633b4e 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -21,6 +21,22 @@ class HeadlessChromeBackend implements PfdPrintBackend { /** @var int */ public const MIN_SUPPORTED_CHROME_VERSION = 59; + + /** @var int */ + protected const CHROME_START_MAX_WAIT_TIME = 10; + + /** @var int */ + protected const CHROME_CLOSE_MAX_WAIT_TIME = 5; + + /** @var int */ + protected const PROCESS_IDLE_TIME = 100000; + + /** @var int */ + protected const STREAM_WAIT_TIME = 200000; + + /** @var int */ + protected const STREAM_CHUNK_SIZE = 8192; + /** * Line of stderr output identifying the websocket url * @@ -153,18 +169,17 @@ public static function createLocal(string $path, bool $useFile = false): static // Non-blocking mode stream_set_blocking($instance->pipes[2], false); - $timeoutSeconds = 10; $startTime = time(); while (true) { $status = proc_get_status($instance->process); // Timeout handling - if ((time() - $startTime) > $timeoutSeconds) { + if ((time() - $startTime) > self::CHROME_START_MAX_WAIT_TIME) { proc_terminate($instance->process, 6); // SIGABRT Logger::error( 'Browser timed out after %d seconds without the expected output', - $timeoutSeconds, + self::CHROME_CLOSE_MAX_WAIT_TIME, ); throw new Exception( @@ -173,15 +188,12 @@ public static function createLocal(string $path, bool $useFile = false): static ); } - $chunkSize = 8192; - $streamWaitTime = 200000; - $idleTime = 100000; $read = [$instance->pipes[2]]; $write = null; $except = null; - if (stream_select($read, $write, $except, 0, $streamWaitTime)) { - $chunk = fread($instance->pipes[2], $chunkSize); + if (stream_select($read, $write, $except, 0, self::STREAM_WAIT_TIME)) { + $chunk = fread($instance->pipes[2], self::STREAM_CHUNK_SIZE); if ($chunk !== false && $chunk !== '') { Logger::debug('Caught browser output: %s', $chunk); @@ -198,7 +210,7 @@ public static function createLocal(string $path, bool $useFile = false): static break; } - usleep($idleTime); + usleep(self::PROCESS_IDLE_TIME); } if ($instance->socket === null || $instance->browserId === null) { @@ -221,16 +233,16 @@ protected function closeLocal(): void $start = time(); $running = true; - while ($running && (time() - $start) < 5) { + while ($running && (time() - $start) < self::CHROME_CLOSE_MAX_WAIT_TIME) { $status = proc_get_status($this->process); $running = $status['running']; if ($running) { - usleep(100000); + usleep(self::PROCESS_IDLE_TIME); } } - // If still running after 5 seconds, force kills the entire process group + // If still running after wait time seconds, force kills the entire process group if ($running) { $status = proc_get_status($this->process); if (! empty($status['pid'])) { @@ -254,10 +266,8 @@ protected function closeLocal(): void /** * Get the file storage - * - * @return StorageInterface */ - public function getFileStorage() + public function getFileStorage(): StorageInterface { if ($this->fileStorage === null) { $this->fileStorage = new TemporaryLocalFileStorage(); From 93766d0e6aca1acd5f9ecb5129ebd3fa49266e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 13 Mar 2026 11:25:05 +0100 Subject: [PATCH 36/60] Relicense to GPL-3.0-only and add SPDX license headers Relicense this module to `GPL-3.0-only`. Add `GPL-3.0-or-later` SPDX license headers to source files, allowing relicensing under future GPL versions. This ensures compatibility with third-party dependencies (e.g. `Apache-2.0`) incompatible with `GPL-2.0-only`. --- library/Pdfexport/Backend/Chromedriver.php | 3 ++- library/Pdfexport/Backend/Geckodriver.php | 3 ++- library/Pdfexport/Backend/PfdPrintBackend.php | 3 ++- library/Pdfexport/Backend/WebdriverBackend.php | 3 ++- library/Pdfexport/BackendLocator.php | 3 ++- library/Pdfexport/Form/ConfigForm.php | 3 ++- library/Pdfexport/Web/ShowConfiguration.php | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/library/Pdfexport/Backend/Chromedriver.php b/library/Pdfexport/Backend/Chromedriver.php index bf86fcb..6e14fbe 100644 --- a/library/Pdfexport/Backend/Chromedriver.php +++ b/library/Pdfexport/Backend/Chromedriver.php @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Pdfexport\Backend; diff --git a/library/Pdfexport/Backend/Geckodriver.php b/library/Pdfexport/Backend/Geckodriver.php index 6e6e512..a0befdc 100644 --- a/library/Pdfexport/Backend/Geckodriver.php +++ b/library/Pdfexport/Backend/Geckodriver.php @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Pdfexport\Backend; diff --git a/library/Pdfexport/Backend/PfdPrintBackend.php b/library/Pdfexport/Backend/PfdPrintBackend.php index 00f5baa..5ed1b04 100644 --- a/library/Pdfexport/Backend/PfdPrintBackend.php +++ b/library/Pdfexport/Backend/PfdPrintBackend.php @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Pdfexport\Backend; diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index f6cedde..efc2292 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Pdfexport\Backend; diff --git a/library/Pdfexport/BackendLocator.php b/library/Pdfexport/BackendLocator.php index 53ccf34..e3dca74 100644 --- a/library/Pdfexport/BackendLocator.php +++ b/library/Pdfexport/BackendLocator.php @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Pdfexport; diff --git a/library/Pdfexport/Form/ConfigForm.php b/library/Pdfexport/Form/ConfigForm.php index 86603b0..9205159 100644 --- a/library/Pdfexport/Form/ConfigForm.php +++ b/library/Pdfexport/Form/ConfigForm.php @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Pdfexport\Form; diff --git a/library/Pdfexport/Web/ShowConfiguration.php b/library/Pdfexport/Web/ShowConfiguration.php index a1e8c4f..755077f 100644 --- a/library/Pdfexport/Web/ShowConfiguration.php +++ b/library/Pdfexport/Web/ShowConfiguration.php @@ -1,6 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Pdfexport\Web; From 7df8e4b76ec82b479a5b829c7ac230476347343e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 13 Mar 2026 12:05:45 +0100 Subject: [PATCH 37/60] Code style changes --- application/forms/BackendConfigForm.php | 129 +++++++++--------- library/Pdfexport/Backend/Chromedriver.php | 20 +++ .../Backend/HeadlessChromeBackend.php | 8 +- library/Pdfexport/Backend/PfdPrintBackend.php | 6 +- .../Pdfexport/Backend/WebdriverBackend.php | 8 +- library/Pdfexport/BackendLocator.php | 6 +- library/Pdfexport/ShellCommand.php | 8 +- 7 files changed, 106 insertions(+), 79 deletions(-) diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 0154dee..6eadd05 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -23,46 +23,50 @@ public function assemble(): void ['class' => 'note'], t( 'The precedence for the chosen backend is the same as in this configuration form. ' . - 'Backends that are not configured are skipped and backends further down the list act as a fallback.' + 'Backends that are not configured are skipped and backends further down the list act as a fallback.', ), )); $this->add(Html::tag('h2', t("WebDriver"))); $this->add(Html::tag('p', t( 'WebDriver is a API that allows software to automatically control and interact with a web browser, ' . - 'commonly used for automating website testing through tools like Selenium WebDriver.' + 'commonly used for automating website testing through tools like Selenium WebDriver.', ))); $this->addElement('text', 'webdriver__host', [ - 'label' => $this->translate('Host'), - 'description' => $this->translate('Host address of the webdriver server'), - 'validators' => [new CallbackValidator(function ($value, CallbackValidator $validator) { - if ($value === null) { - return true; - } + 'label' => $this->translate('Host'), + 'description' => $this->translate('Host address of the webdriver server'), + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { + if ($value === null) { + return true; + } - $port = $this->getValue('webdriver__port') ?: 4444; - $type = $this->getValue('webdriver__type') ?: 'chrome'; + $port = $this->getValue('webdriver__port') ?: 4444; + $type = $this->getValue('webdriver__type') ?: 'chrome'; - try { - $url = "$value:$port"; - $backend = match ($type) { - 'chrome' => new Chromedriver($url), - 'firefox' => new Geckodriver($url), - default => throw new Exception("Invalid webdriver type $type"), - }; + try { + $url = "$value:$port"; + $backend = match ($type) { + 'chrome' => new Chromedriver($url), + 'firefox' => new Geckodriver($url), + default => throw new Exception("Invalid webdriver type $type"), + }; + + if (! $backend->isSupported()) { + $validator->addMessage( + t('The webdriver server reports that it is unable to generate PDFs'), + ); + return false; + } - if (! $backend->isSupported()) { - $validator->addMessage(t('The webdriver server reports that it is unable to generate PDFs')); + } catch (Exception $e) { + $validator->addMessage($e->getMessage()); return false; } - - } catch (Exception $e) { - $validator->addMessage($e->getMessage()); - return false; - } - return true; - })] + return true; + }), + ], ]); $this->addElement('number', 'webdriver__port', [ @@ -74,51 +78,52 @@ public function assemble(): void ]); $this->addElement('select', 'webdriver__type', [ - 'label' => $this->translate('Type'), - 'description' => $this->translate('The type of webdriver server.'), - 'multiOptions' => array_merge( + 'label' => $this->translate('Type'), + 'description' => $this->translate('The type of webdriver server.'), + 'multiOptions' => array_merge( ['' => sprintf(' - %s - ', t('Please choose'))], [ 'firefox' => t('Firefox'), - 'chrome' => t('Chrome'), + 'chrome' => t('Chrome'), ], ), ]); $this->add(Html::tag('h2', t("Remote Chrome"))); $this->add(Html::tag('p', t( - 'A remote chrome instance and it\'s debug interface can be used to create PDFs.' + 'A remote chrome instance and it\'s debug interface can be used to create PDFs.', ))); $this->addElement('text', 'remote_chrome_host', [ - 'label' => $this->translate('Host'), - 'description' => $this->translate('Host address of the server with the running web browser.'), - 'validators' => [ + 'label' => $this->translate('Host'), + 'description' => $this->translate('Host address of the server with the running web browser.'), + 'validators' => [ new CallbackValidator(function ($value, CallbackValidator $validator) { - if ($value === null) { + if ($value === null) { + return true; + } + + $port = $this->getValue('remote_chrome__port') ?: 9222; + + try { + $chrome = HeadlessChromeBackend::createRemote($value, $port); + $version = $chrome->getVersion(); + } catch (Exception $e) { + $validator->addMessage($e->getMessage()); + return false; + } + + if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { + $validator->addMessage(t( + 'Chrome/Chromium supporting headless mode required' + . ' which is provided since version %s. Version detected: %s', + )); + return false; + } + return true; - } - - $port = $this->getValue('remote_chrome__port') ?: 9222; - - try { - $chrome = HeadlessChromeBackend::createRemote($value, $port); - $version = $chrome->getVersion(); - } catch (Exception $e) { - $validator->addMessage($e->getMessage()); - return false; - } - - if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { - $validator->addMessage(t( - 'Chrome/Chromium supporting headless mode required' - . ' which is provided since version %s. Version detected: %s' - )); - return false; - } - - return true; - })] + }), + ], ]); $this->addElement('number', 'remote_chrome__port', [ @@ -126,7 +131,7 @@ public function assemble(): void 'description' => $this->translate('Port of the chrome developer tools. (Default: 9222)'), 'placeholder' => 9222, 'min' => 1, - 'max' => 65535 + 'max' => 65535, ]); $this->add(Html::tag('h2', t("Local Chrome"))); @@ -138,7 +143,7 @@ public function assemble(): void 'label' => $this->translate('Binary'), 'placeholder' => '/usr/bin/google-chrome', 'description' => $this->translate('Path to the binary of the web browser.'), - 'validators' => [ + 'validators' => [ new CallbackValidator(function ($value, CallbackValidator $validator) { if (empty($value)) { return true; @@ -155,7 +160,7 @@ public function assemble(): void if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { $validator->addMessage(t( 'Chrome/Chromium supporting headless mode required' - . ' which is provided since version %s. Version detected: %s' + . ' which is provided since version %s. Version detected: %s', )); } @@ -165,12 +170,12 @@ public function assemble(): void ]); $this->addElement('checkbox', 'local_chrome__force_temp_storage', [ - 'label' => $this->translate('Use temp storage'), + 'label' => $this->translate('Use temp storage'), 'description' => $this->translate('Use temp storage to transfer the html to the local chrome instance.'), ]); $this->addElement('submit', 'submit', [ - 'label' => $this->translate('Store') + 'label' => $this->translate('Store'), ]); } } diff --git a/library/Pdfexport/Backend/Chromedriver.php b/library/Pdfexport/Backend/Chromedriver.php index 6e14fbe..1f1204d 100644 --- a/library/Pdfexport/Backend/Chromedriver.php +++ b/library/Pdfexport/Backend/Chromedriver.php @@ -5,8 +5,10 @@ namespace Icinga\Module\Pdfexport\Backend; +use Exception; use Facebook\WebDriver\Chrome\ChromeDevToolsDriver; use Facebook\WebDriver\Remote\DesiredCapabilities; +use Icinga\Application\Logger; use Icinga\Module\Pdfexport\PrintableHtmlDocument; class Chromedriver extends WebdriverBackend @@ -54,6 +56,24 @@ protected function getPrintParameters(PrintableHtmlDocument $document): array protected function printToPdf(array $printParameters): string { $devTools = $this->getChromeDeveloperTools(); + + $png = base64_decode($devTools->execute( + 'Page.captureScreenshot', + [ + 'format' => 'png', + ], + )['data']); + + $path = '/tmp/png-' . time() . '.png'; + file_put_contents($path, $png); + Logger::debug("Wrote PNG: " . $path); + + try { + $devTools->execute('Console.enable'); + } catch (Exception $_) { + // Deprecated, might fail + } + $result = $devTools->execute( 'Page.printToPDF', $printParameters, diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index c633b4e..b09ad4c 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -437,7 +437,7 @@ protected function setContent(PrintableHtmlDocument $document): void $page, 'Page.frameStoppedLoading', [ - 'frameId' => $this->frameId + 'frameId' => $this->frameId, ], ); @@ -700,15 +700,15 @@ public function getVersion(): int throw new Exception("Malformed Chrome Version String: " . $version['Browser']); } - return (int)$matches[1]; + return (int) $matches[1]; } - function isSupported(): bool + public function isSupported(): bool { return $this->getVersion() >= self::MIN_SUPPORTED_CHROME_VERSION; } - function close(): void + public function close(): void { $this->closeBrowser(); $this->closeBrowser(); diff --git a/library/Pdfexport/Backend/PfdPrintBackend.php b/library/Pdfexport/Backend/PfdPrintBackend.php index 5ed1b04..03af0bd 100644 --- a/library/Pdfexport/Backend/PfdPrintBackend.php +++ b/library/Pdfexport/Backend/PfdPrintBackend.php @@ -9,9 +9,9 @@ interface PfdPrintBackend { - function toPdf(PrintableHtmlDocument $document): string; + public function toPdf(PrintableHtmlDocument $document): string; - function isSupported(): bool; + public function isSupported(): bool; - function close(): void; + public function close(): void; } diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index efc2292..c6f3d5a 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -24,7 +24,7 @@ public function __construct( $this->driver = RemoteWebDriver::create($url, $capabilities); } - function __destruct() + protected function __destruct() { $this->close(); } @@ -33,7 +33,7 @@ protected function setContent(PrintableHtmlDocument $document): void { // This is horribly ugly, but it works for all browser backends $encoded = base64_encode($document); - $this->driver->executeScript('document.head.remove()'); + $this->driver->executeScript('document.head.remove();'); $this->driver->executeScript("document.body.outerHTML = atob('$encoded');"); } @@ -76,13 +76,13 @@ public function toPdf(PrintableHtmlDocument $document): string return $this->printToPdf($printParameters); } - function isSupported(): bool + public function isSupported(): bool { // TODO: Come up with a check return true; } - function close(): void + public function close(): void { $this->driver->quit(); } diff --git a/library/Pdfexport/BackendLocator.php b/library/Pdfexport/BackendLocator.php index e3dca74..d0cd196 100644 --- a/library/Pdfexport/BackendLocator.php +++ b/library/Pdfexport/BackendLocator.php @@ -68,7 +68,9 @@ protected function connectToRemoteChrome(string $section): ?PfdPrintBackend Logger::info("Connected WebDriver Backend: $section"); return $backend; } catch (Exception $e) { - Logger::warning("Error while creating remote HeadlessChrome! backend: $section, error: " . $e->getMessage()); + Logger::warning( + "Error while creating remote HeadlessChrome! backend: $section, error: " . $e->getMessage(), + ); } return null; } @@ -104,7 +106,7 @@ protected function getSingleBackend($section): ?PfdPrintBackend Logger::info("Connecting to backend $section."); - $backend = match($section) { + $backend = match ($section) { 'local_chrome' => $this->connectToLocalChrome($section), 'remote_chrome' => $this->connectToRemoteChrome($section), default => $this->connectToWebDriver($section), diff --git a/library/Pdfexport/ShellCommand.php b/library/Pdfexport/ShellCommand.php index 5e7e47c..bd79fee 100644 --- a/library/Pdfexport/ShellCommand.php +++ b/library/Pdfexport/ShellCommand.php @@ -24,7 +24,7 @@ class ShellCommand */ public function __construct($command, $escape = true) { - $command = (string)$command; + $command = (string) $command; $this->command = $escape ? escapeshellcmd($command) : $command; } @@ -46,7 +46,7 @@ public function getExitCode() */ public function getStatus() { - $status = (object)proc_get_status($this->resource); + $status = (object) proc_get_status($this->resource); if ($status->running === false && $this->exitCode === null) { // The exit code is only valid the first time proc_get_status is // called in terms of running false, hence we capture it @@ -88,7 +88,7 @@ public function execute() )); } - $namedpipes = (object)[ + $namedpipes = (object) [ 'stdin' => &$pipes[0], 'stdout' => &$pipes[1], 'stderr' => &$pipes[2], @@ -141,7 +141,7 @@ public function execute() $this->resource = null; - return (object)[ + return (object) [ 'stdout' => $stdout, 'stderr' => $stderr, ]; From 0d7a86b6d023d45e87518670479530e00c018814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 13 Mar 2026 12:15:45 +0100 Subject: [PATCH 38/60] fixup! Code style changes --- application/forms/BackendConfigForm.php | 1 - library/Pdfexport/Backend/WebdriverBackend.php | 10 ++++++++-- library/Pdfexport/Web/ShowConfiguration.php | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 6eadd05..0c428ae 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -59,7 +59,6 @@ public function assemble(): void ); return false; } - } catch (Exception $e) { $validator->addMessage($e->getMessage()); return false; diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index c6f3d5a..7303313 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -17,14 +17,15 @@ class WebdriverBackend implements PfdPrintBackend { protected RemoteWebDriver $driver; + public function __construct( - string $url, + string $url, DesiredCapabilities $capabilities, ) { $this->driver = RemoteWebDriver::create($url, $capabilities); } - protected function __destruct() + public function __destruct() { $this->close(); } @@ -71,6 +72,11 @@ public function toPdf(PrintableHtmlDocument $document): string { $this->setContent($document); $this->waitForPageLoad(); + + $path = '/tmp/chromedriver-' . time() . '.html'; + file_put_contents($path, $this->driver->getPageSource()); + Logger::info("Printing page $path."); + $printParameters = $this->getPrintParameters($document); return $this->printToPdf($printParameters); diff --git a/library/Pdfexport/Web/ShowConfiguration.php b/library/Pdfexport/Web/ShowConfiguration.php index 755077f..9eb7ee7 100644 --- a/library/Pdfexport/Web/ShowConfiguration.php +++ b/library/Pdfexport/Web/ShowConfiguration.php @@ -64,7 +64,7 @@ protected function assemble(): void [ t( 'Details can be found in the application log. ' . - "(If you don't have access to this log, call your administrator in this case)" + "(If you don't have access to this log, call your administrator in this case)", ), HtmlString::create('
'), t('In case you can access the file by yourself, you can open it and insert the config manually:'), @@ -81,7 +81,7 @@ protected function assemble(): void HtmlElement::create( 'code', null, - (string)$this->config, + (string) $this->config, ), ), ), From 1b1e3e7ca012a8024d47dc0ee8ed23617deff398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 13 Mar 2026 15:52:11 +0100 Subject: [PATCH 39/60] Use Callout instead of building something custom --- application/forms/BackendConfigForm.php | 13 +++++++------ public/css/module.less | 6 ------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 0c428ae..9186dd1 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -11,20 +11,21 @@ use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; use Icinga\Module\Pdfexport\Form\ConfigForm; use ipl\Html\Html; -use ipl\Html\HtmlElement; use ipl\Validator\CallbackValidator; +use ipl\Web\Common\CalloutType; +use ipl\Web\Widget\Callout; class BackendConfigForm extends ConfigForm { public function assemble(): void { - $this->add(HtmlElement::create( - 'div', - ['class' => 'note'], + $this->addHtml(new Callout( + CalloutType::Info, t( - 'The precedence for the chosen backend is the same as in this configuration form. ' . - 'Backends that are not configured are skipped and backends further down the list act as a fallback.', + 'Backends are chosen in the order of this from.' + . ' Backends that are not configured are skipped and ones further down the list act as a fallback.', ), + t('Info: Backend precedence'), )); $this->add(Html::tag('h2', t("WebDriver"))); diff --git a/public/css/module.less b/public/css/module.less index 12bf56c..aad35f8 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -1,8 +1,2 @@ /* Icinga PDF Export | (c) 2026 Icinga GmbH | GPLv2 */ -.note { - background-color: @empty-state-bar-bg; - color: @default-text-color; - padding: @vertical-padding @horizontal-padding; - border-radius: .4em; -} From ef93d1656c53e962755cada1fee5a192abe423cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 18 Mar 2026 10:17:45 +0100 Subject: [PATCH 40/60] Implement WebDriver Protocol with GuzzlePHP --- .../ChromeDevTools/ChromeDevTools.php | 30 +++++ library/Pdfexport/ChromeDevTools/Command.php | 32 ++++++ library/Pdfexport/WebDriver/Capabilities.php | 101 +++++++++++++++++ library/Pdfexport/WebDriver/Command.php | 74 ++++++++++++ .../Pdfexport/WebDriver/CommandExecutor.php | 106 ++++++++++++++++++ .../Pdfexport/WebDriver/CommandInterface.php | 12 ++ .../WebDriver/ConditionInterface.php | 8 ++ library/Pdfexport/WebDriver/CustomCommand.php | 30 +++++ library/Pdfexport/WebDriver/DriverCommand.php | 43 +++++++ .../WebDriver/ElementPresentCondition.php | 66 +++++++++++ library/Pdfexport/WebDriver/Response.php | 28 +++++ library/Pdfexport/WebDriver/WebDriver.php | 85 ++++++++++++++ 12 files changed, 615 insertions(+) create mode 100644 library/Pdfexport/ChromeDevTools/ChromeDevTools.php create mode 100644 library/Pdfexport/ChromeDevTools/Command.php create mode 100644 library/Pdfexport/WebDriver/Capabilities.php create mode 100644 library/Pdfexport/WebDriver/Command.php create mode 100644 library/Pdfexport/WebDriver/CommandExecutor.php create mode 100644 library/Pdfexport/WebDriver/CommandInterface.php create mode 100644 library/Pdfexport/WebDriver/ConditionInterface.php create mode 100644 library/Pdfexport/WebDriver/CustomCommand.php create mode 100644 library/Pdfexport/WebDriver/DriverCommand.php create mode 100644 library/Pdfexport/WebDriver/ElementPresentCondition.php create mode 100644 library/Pdfexport/WebDriver/Response.php create mode 100644 library/Pdfexport/WebDriver/WebDriver.php diff --git a/library/Pdfexport/ChromeDevTools/ChromeDevTools.php b/library/Pdfexport/ChromeDevTools/ChromeDevTools.php new file mode 100644 index 0000000..20366ef --- /dev/null +++ b/library/Pdfexport/ChromeDevTools/ChromeDevTools.php @@ -0,0 +1,30 @@ + $command->getName(), + 'params' => $command->getParameters(), + ]; + + $customCommand = new CustomCommand( + 'POST', + '/session/:sessionId/goog/cdp/execute', + $params, + ); + + return $this->driver->execute($customCommand); + } +} diff --git a/library/Pdfexport/ChromeDevTools/Command.php b/library/Pdfexport/ChromeDevTools/Command.php new file mode 100644 index 0000000..5599800 --- /dev/null +++ b/library/Pdfexport/ChromeDevTools/Command.php @@ -0,0 +1,32 @@ +name; + } + + public function getParameters(): array + { + return $this->parameters; + } + + public static function enableConsole(): static + { + return new static('Console.enable'); + } + + public static function printToPdf(array $printParameters): static + { + return new static('Page.printToPDF', $printParameters); + } +} diff --git a/library/Pdfexport/WebDriver/Capabilities.php b/library/Pdfexport/WebDriver/Capabilities.php new file mode 100644 index 0000000..23c0d12 --- /dev/null +++ b/library/Pdfexport/WebDriver/Capabilities.php @@ -0,0 +1,101 @@ + 'platformName', + 'version' => 'browserVersion', + 'acceptSslCerts' => 'acceptInsecureCerts', + ]; + + public function __construct( + protected array $capabilities = [], + ) { + } + + public static function chrome(): static + { + return new static([ + 'browserName' => 'chrome', + 'platform' => 'ANY', + ]); + } + + public static function firefox(): static + { + return new static([ + 'browserName' => 'firefox', + 'platform' => 'ANY', + 'moz:firefoxOptions' => [ + 'prefs' => [ + 'reader.parse-on-load.enabled' => false, + 'devtools.jsonview.enabled' => false, + ], + ], + ]); + } + + public function toW3cCompatibleArray(): array + { + $allowedW3cCapabilities = [ + 'browserName', + 'browserVersion', + 'platformName', + 'acceptInsecureCerts', + 'pageLoadStrategy', + 'proxy', + 'setWindowRect', + 'timeouts', + 'strictFileInteractability', + 'unhandledPromptBehavior', + ]; + + $capabilities = $this->toArray(); + $w3cCapabilities = []; + + foreach ($capabilities as $capabilityKey => $capabilityValue) { + if (in_array($capabilityKey, $allowedW3cCapabilities, true)) { + $w3cCapabilities[$capabilityKey] = $capabilityValue; + continue; + } + + if (array_key_exists($capabilityKey, self::$ossToW3c)) { + if ($capabilityKey === 'platform') { + if ($capabilityValue === 'ANY') { + continue; + } + + $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue); + } else { + $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = $capabilityValue; + } + } + + if (mb_strpos($capabilityKey, ':') !== false) { + $w3cCapabilities[$capabilityKey] = $capabilityValue; + } + } + + if (array_key_exists('goog:chromeOptions', $capabilities)) { + $w3cCapabilities['goog:chromeOptions'] = $capabilities['goog:chromeOptions']; + } + + if (array_key_exists('firefox_profile', $capabilities)) { + if (! array_key_exists('moz:firefoxOptions', $capabilities) + || ! array_key_exists('profile', $capabilities['moz:firefoxOptions'])) { + $w3cCapabilities['moz:firefoxOptions']['profile'] = $capabilities['firefox_profile']; + } + } + + return $w3cCapabilities; + } + + public function toArray(): array + { + return $this->capabilities; + } +} diff --git a/library/Pdfexport/WebDriver/Command.php b/library/Pdfexport/WebDriver/Command.php new file mode 100644 index 0000000..2e55650 --- /dev/null +++ b/library/Pdfexport/WebDriver/Command.php @@ -0,0 +1,74 @@ + $script, + 'args' => static::prepareScriptArguments($arguments), + ]; + + return new static(DriverCommand::ExecuteScript, $params); + } + + public static function getPageSource(): static + { + return new static(DriverCommand::GetPageSource); + } + + public static function findElement(string $method, string $value): static + { + return new static(DriverCommand::FindElement, [ + 'using' => $method, + 'value' => $value, + ]); + } + + protected static function prepareScriptArguments(array $arguments): array + { + $args = []; + foreach ($arguments as $key => $value) { + if (is_array($value)) { + $args[$key] = static::prepareScriptArguments($value); + } else { + $args[$key] = $value; + } + } + + return $args; + } + + public static function printPage(array $printParameters): static + { + return new static(DriverCommand::PrintPage, $printParameters); + } + + public function getPath(): string + { + return $this->name->getPath(); + } + + public function getMethod(): string + { + return $this->name->getMethod(); + } + + public function getParameters(): array + { + return $this->parameters; + } + + public function getName(): DriverCommand + { + return $this->name; + } +} diff --git a/library/Pdfexport/WebDriver/CommandExecutor.php b/library/Pdfexport/WebDriver/CommandExecutor.php new file mode 100644 index 0000000..29d3ea4 --- /dev/null +++ b/library/Pdfexport/WebDriver/CommandExecutor.php @@ -0,0 +1,106 @@ + 'application/json;charset=UTF-8', + 'Accept' => 'application/json', + ]; + + protected ?Client $client = null; + + public function __construct( + protected string $url, + protected ?float $timeout = 10, + ) { + $this->client = new Client(); + } + + public function execute(?string $sessionId, CommandInterface $command): Response + { + $method = $command->getMethod(); + $path = $command->getPath(); + if (str_contains($path, ':sessionId')) { + if ($sessionId === null) { + throw new RuntimeException('Session ID is not set'); + } + $path = str_replace(':sessionId', $sessionId, $path); + } + $params = $command->getParameters(); + foreach ($params as $name => $value) { + if (str_starts_with($name, ':')) { + $path = str_replace($name, $value, $path); + unset($params[$name]); + } + } + + if (is_array($params) && ! empty($params) && $method !== 'POST') { + throw new RuntimeException('Invalid HTTP method'); + } + + if ($command instanceof Command + && $command->getName() === DriverCommand::NewSession) { + $method = 'POST'; + } + + $headers = static::DEFAULT_HEADERS; + + if (in_array($method, ['POST', 'PUT'], true)) { + unset ($headers['expect']); + } + + if (is_array($params) && ! empty($params)) { + $body = json_encode($params); + } else { + $body = '{}'; + } + + $options = [ + 'headers' => $headers, + 'expect' => false, + 'body' => $body, + 'http_errors' => false, + 'timeout' => $this->timeout ?? 0, + ]; + + $response = $this->client->request($method, $this->url . $path, $options); + + $results = json_decode($response->getBody()->getContents(), true); + + if ($results === null && json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException(json_last_error_msg()); + } + + if (! is_array($results)) { + throw new RuntimeException('Invalid response'); + } + + $value = $results['value'] ?? null; + $message = $results['message'] ?? null; + + if (is_array($value) && array_key_exists('sessionId', $value)) { + $sessionId = $value['sessionId']; + } else if (isset($results['sessionId'])) { + $sessionId = $results['sessionId']; + } + + if (isset($value['error'])) { + throw new Exception(sprintf( + "Error in command response: %s - %s", $value['error'], $value['message'] ?? "Unknown error" + )); + } + + $status = $results['status'] ?? 0; + if ($status !== 0) { + throw new Exception($message, $status); + } + + return new Response($sessionId, $status, $value); + } +} diff --git a/library/Pdfexport/WebDriver/CommandInterface.php b/library/Pdfexport/WebDriver/CommandInterface.php new file mode 100644 index 0000000..1edad6c --- /dev/null +++ b/library/Pdfexport/WebDriver/CommandInterface.php @@ -0,0 +1,12 @@ +path; + } + + public function getMethod(): string + { + return $this->method; + } + + public function getParameters(): array + { + return $this->parameters; + } +} diff --git a/library/Pdfexport/WebDriver/DriverCommand.php b/library/Pdfexport/WebDriver/DriverCommand.php new file mode 100644 index 0000000..51683d8 --- /dev/null +++ b/library/Pdfexport/WebDriver/DriverCommand.php @@ -0,0 +1,43 @@ + '/session', + self::Status => '/status', + self::Close => '/session/:sessionId/window', + self::Quit => '/session/:sessionId', + self::ExecuteScript => '/session/:sessionId/execute/sync', + self::GetPageSource => '/session/:sessionId/source', + self::PrintPage => '/session/:sessionId/print', + self::FindElement => '/session/:sessionId/element', + }; + } + + public function getMethod(): string + { + return match($this) { + self::NewSession => 'POST', + self::Status => 'GET', + self::Close => 'DELETE', + self::Quit => 'DELETE', + self::ExecuteScript => 'POST', + self::GetPageSource => 'GET', + self::PrintPage => 'POST', + self::FindElement => 'POST', + }; + } +} diff --git a/library/Pdfexport/WebDriver/ElementPresentCondition.php b/library/Pdfexport/WebDriver/ElementPresentCondition.php new file mode 100644 index 0000000..9236d49 --- /dev/null +++ b/library/Pdfexport/WebDriver/ElementPresentCondition.php @@ -0,0 +1,66 @@ +execute( + Command::findElement($this->mechanism, $this->value), + ); + + if (isset($response['ELEMENT']) || isset($response[self::WEBDRIVER_ELEMENT_IDENTIFIER])) { + return true; + } + + return false; + } + + public static function byCssSelector(string $selector): static + { + return new static('css selector', $selector); + } + + public static function byLinkText(string $linkText): static + { + return new static('link text', $linkText); + } + + public static function byPartialLinkText(string $partialLinkText): static + { + return new static('partial link text', $partialLinkText); + } + + public static function byId(string $id): static + { + return static::byCssSelector('#' . $id); + } + + public static function byClassName(string $className): static + { + return static::byCssSelector('.' . $className); + } + + public static function byName(string $name): static + { + return static::byCssSelector('[name="' . $name . '"]'); + } + + public static function byTagName(string $tagName): static + { + return new static('tag name', $tagName); + } + + public static function byXPath(string $xpath): static + { + return new static('xpath', $xpath); + } +} diff --git a/library/Pdfexport/WebDriver/Response.php b/library/Pdfexport/WebDriver/Response.php new file mode 100644 index 0000000..5880d86 --- /dev/null +++ b/library/Pdfexport/WebDriver/Response.php @@ -0,0 +1,28 @@ +sessionId; + } + + public function getStatus(): int + { + return $this->status; + } + + public function getValue(): mixed + { + return $this->value; + } +} diff --git a/library/Pdfexport/WebDriver/WebDriver.php b/library/Pdfexport/WebDriver/WebDriver.php new file mode 100644 index 0000000..b1511fd --- /dev/null +++ b/library/Pdfexport/WebDriver/WebDriver.php @@ -0,0 +1,85 @@ +quit(); + } + + public static function create(string $url, Capabilities $capabilities): static + { + $executor = new CommandExecutor($url); + + $params = [ + 'capabilities' => [ + 'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()], + ], + 'desiredCapabilities' => (object) $capabilities->toArray(), + ]; + + $cmd = new Command(DriverCommand::NewSession, $params); + + $response = $executor->execute(null, $cmd); + + return new static($executor, $response->getSessionID()); + } + + public function execute(CommandInterface $command): mixed + { + if ($this->sessionId === null) { + throw new Exception('Session is not active'); + } + + $response = $this->executor->execute($this->sessionId, $command); + + return $response->getValue(); + } + + public function wait( + ConditionInterface $condition, + int $timeoutSeconds = 10, + int $intervalMs = 250 + ): mixed { + $end = microtime(true) + $timeoutSeconds; + $lastException = null; + + while ($end > microtime(true)) { + try { + $result = $condition->apply($this); + if ($result !== false) { + return $result; + } + } catch (Exception $e) { + $lastException = $e; + } + usleep($intervalMs * 1000); + } + + if ($lastException !== null) { + throw $lastException; + } + + return false; + } + + public function quit(): void + { + if ($this->executor !== null) { + $this->execute(new Command(DriverCommand::Quit)); + $this->executor = null; + } + + $this->sessionId = null; + } +} From e48d4087dcc15ce1ef0b2c1174f0dc8791dd740c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 18 Mar 2026 11:04:48 +0100 Subject: [PATCH 41/60] Use new WebDriver implementation --- library/Pdfexport/Backend/Chromedriver.php | 32 +++++---------- library/Pdfexport/Backend/Geckodriver.php | 4 +- .../Pdfexport/Backend/WebdriverBackend.php | 39 ++++++++----------- 3 files changed, 28 insertions(+), 47 deletions(-) diff --git a/library/Pdfexport/Backend/Chromedriver.php b/library/Pdfexport/Backend/Chromedriver.php index 1f1204d..435948e 100644 --- a/library/Pdfexport/Backend/Chromedriver.php +++ b/library/Pdfexport/Backend/Chromedriver.php @@ -6,24 +6,24 @@ namespace Icinga\Module\Pdfexport\Backend; use Exception; -use Facebook\WebDriver\Chrome\ChromeDevToolsDriver; -use Facebook\WebDriver\Remote\DesiredCapabilities; -use Icinga\Application\Logger; +use Icinga\Module\Pdfexport\ChromeDevTools\ChromeDevTools; +use Icinga\Module\Pdfexport\ChromeDevTools\Command; use Icinga\Module\Pdfexport\PrintableHtmlDocument; +use Icinga\Module\Pdfexport\WebDriver\Capabilities; class Chromedriver extends WebdriverBackend { - protected ?ChromeDevToolsDriver $dcp = null; + protected ?ChromeDevTools $dcp = null; public function __construct(string $url) { - parent::__construct($url, DesiredCapabilities::chrome()); + parent::__construct($url, Capabilities::chrome()); } - protected function getChromeDeveloperTools(): ChromeDevToolsDriver + protected function getChromeDeveloperTools(): ChromeDevTools { if ($this->dcp === null) { - $this->dcp = new ChromeDevToolsDriver($this->driver); + $this->dcp = new ChromeDevTools($this->driver); } return $this->dcp; } @@ -57,27 +57,13 @@ protected function printToPdf(array $printParameters): string { $devTools = $this->getChromeDeveloperTools(); - $png = base64_decode($devTools->execute( - 'Page.captureScreenshot', - [ - 'format' => 'png', - ], - )['data']); - - $path = '/tmp/png-' . time() . '.png'; - file_put_contents($path, $png); - Logger::debug("Wrote PNG: " . $path); - try { - $devTools->execute('Console.enable'); + $devTools->execute(Command::enableConsole()); } catch (Exception $_) { // Deprecated, might fail } - $result = $devTools->execute( - 'Page.printToPDF', - $printParameters, - ); + $result = $devTools->execute(Command::printToPdf($printParameters)); return base64_decode($result['data']); } diff --git a/library/Pdfexport/Backend/Geckodriver.php b/library/Pdfexport/Backend/Geckodriver.php index a0befdc..ba8e83e 100644 --- a/library/Pdfexport/Backend/Geckodriver.php +++ b/library/Pdfexport/Backend/Geckodriver.php @@ -5,12 +5,12 @@ namespace Icinga\Module\Pdfexport\Backend; -use Facebook\WebDriver\Remote\DesiredCapabilities; +use Icinga\Module\Pdfexport\WebDriver\Capabilities; class Geckodriver extends WebdriverBackend { public function __construct(string $rul) { - parent::__construct($rul, DesiredCapabilities::firefox()); + parent::__construct($rul, Capabilities::firefox()); } } diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index 7303313..1e5831a 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -5,24 +5,23 @@ namespace Icinga\Module\Pdfexport\Backend; -use Facebook\WebDriver\Remote\DesiredCapabilities; -use Facebook\WebDriver\Remote\RemoteWebDriver; -use Facebook\WebDriver\WebDriverBy; -use Facebook\WebDriver\WebDriverExpectedCondition; -use Facebook\WebDriver\WebDriverWait; -use Icinga\Application\Logger; use Icinga\Module\Pdfexport\PrintableHtmlDocument; +use Icinga\Module\Pdfexport\WebDriver\Capabilities; +use Icinga\Module\Pdfexport\WebDriver\ElementPresentCondition; +use Icinga\Module\Pdfexport\WebDriver\WebDriver; +use Icinga\Module\Pdfexport\WebDriver\Command; class WebdriverBackend implements PfdPrintBackend { - protected RemoteWebDriver $driver; + protected WebDriver $driver; + public function __construct( string $url, - DesiredCapabilities $capabilities, + Capabilities $capabilities, ) { - $this->driver = RemoteWebDriver::create($url, $capabilities); + $this->driver = WebDriver::create($url, $capabilities); } public function __destruct() @@ -34,15 +33,17 @@ protected function setContent(PrintableHtmlDocument $document): void { // This is horribly ugly, but it works for all browser backends $encoded = base64_encode($document); - $this->driver->executeScript('document.head.remove();'); - $this->driver->executeScript("document.body.outerHTML = atob('$encoded');"); + $this->driver->execute( + Command::executeScript('document.head.remove();'), + ); + $this->driver->execute( + Command::executeScript("document.body.outerHTML = atob('$encoded');"), + ); } protected function waitForPageLoad(): void { - // Wait for the body element to ensure the page has fully loaded - $wait = new WebDriverWait($this->driver, 10); - $wait->until(WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::tagName('body'))); + $this->driver->wait(ElementPresentCondition::byTagName('body')); } protected function getPrintParameters(PrintableHtmlDocument $document): array @@ -59,10 +60,8 @@ protected function getPrintParameters(PrintableHtmlDocument $document): array protected function printToPdf(array $printParameters): string { - $result = $this->driver->executeCustomCommand( - '/session/:sessionId/print', - 'POST', - $printParameters, + $result = $this->driver->execute( + Command::printPage($printParameters), ); return base64_decode($result); @@ -73,10 +72,6 @@ public function toPdf(PrintableHtmlDocument $document): string $this->setContent($document); $this->waitForPageLoad(); - $path = '/tmp/chromedriver-' . time() . '.html'; - file_put_contents($path, $this->driver->getPageSource()); - Logger::info("Printing page $path."); - $printParameters = $this->getPrintParameters($document); return $this->printToPdf($printParameters); From 77ae3873319c93e748188012feafc9afefc52942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 19 Mar 2026 08:51:18 +0100 Subject: [PATCH 42/60] Add support for layout plugins to ChromeWebdriver --- library/Pdfexport/Backend/Chromedriver.php | 71 +++++++++++++++---- .../Pdfexport/Backend/WebdriverBackend.php | 2 - 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/library/Pdfexport/Backend/Chromedriver.php b/library/Pdfexport/Backend/Chromedriver.php index 435948e..d0ff712 100644 --- a/library/Pdfexport/Backend/Chromedriver.php +++ b/library/Pdfexport/Backend/Chromedriver.php @@ -7,19 +7,72 @@ use Exception; use Icinga\Module\Pdfexport\ChromeDevTools\ChromeDevTools; -use Icinga\Module\Pdfexport\ChromeDevTools\Command; +use Icinga\Module\Pdfexport\ChromeDevTools\Command as DevToolsCommand; +use Icinga\Module\Pdfexport\WebDriver\Command; use Icinga\Module\Pdfexport\PrintableHtmlDocument; use Icinga\Module\Pdfexport\WebDriver\Capabilities; +use Icinga\Module\Pdfexport\WebDriver\ElementPresentCondition; class Chromedriver extends WebdriverBackend { protected ?ChromeDevTools $dcp = null; + public const ACTIVATE_SCRIPTS = <<driver->execute( + Command::executeScript(self::ACTIVATE_SCRIPTS), + ); + $this->driver->execute( + Command::executeScript('new Layout().apply();'), + ); + } + + protected function waitForPageLoad(): void + { + parent::waitForPageLoad(); + + $this->driver->wait(ElementPresentCondition::byCssSelector('[data-layout-ready=yes]')); + } + protected function getChromeDeveloperTools(): ChromeDevTools { if ($this->dcp === null) { @@ -28,18 +81,6 @@ protected function getChromeDeveloperTools(): ChromeDevTools return $this->dcp; } -// protected function setContent(PrintableHtmlDocument $document): void -// { -// $devTools = $this->getChromeDeveloperTools(); -// $devTools->execute( -// 'Page.setDocumentContent', -// [ -// 'frameId' => 'TODO', -// 'html' => $document->render() -// ] -// ); -// } - protected function getPrintParameters(PrintableHtmlDocument $document): array { $parameters = [ @@ -58,12 +99,12 @@ protected function printToPdf(array $printParameters): string $devTools = $this->getChromeDeveloperTools(); try { - $devTools->execute(Command::enableConsole()); + $devTools->execute(DevToolsCommand::enableConsole()); } catch (Exception $_) { // Deprecated, might fail } - $result = $devTools->execute(Command::printToPdf($printParameters)); + $result = $devTools->execute(DevToolsCommand::printToPdf($printParameters)); return base64_decode($result['data']); } diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index 1e5831a..c26651f 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -15,8 +15,6 @@ class WebdriverBackend implements PfdPrintBackend { protected WebDriver $driver; - - public function __construct( string $url, Capabilities $capabilities, From 40e04e1a32ad12acc6c08c73eb50bc20b14d6893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 19 Mar 2026 09:21:45 +0100 Subject: [PATCH 43/60] Code style changes --- library/Pdfexport/WebDriver/Capabilities.php | 9 +++++---- library/Pdfexport/WebDriver/Command.php | 12 ++++++------ library/Pdfexport/WebDriver/CommandExecutor.php | 16 +++++++++++----- .../{DriverCommand.php => CommandName.php} | 6 +++--- .../WebDriver/ElementPresentCondition.php | 5 +++-- library/Pdfexport/WebDriver/WebDriver.php | 4 ++-- 6 files changed, 30 insertions(+), 22 deletions(-) rename library/Pdfexport/WebDriver/{DriverCommand.php => CommandName.php} (93%) diff --git a/library/Pdfexport/WebDriver/Capabilities.php b/library/Pdfexport/WebDriver/Capabilities.php index 23c0d12..67dfd61 100644 --- a/library/Pdfexport/WebDriver/Capabilities.php +++ b/library/Pdfexport/WebDriver/Capabilities.php @@ -4,9 +4,8 @@ class Capabilities { - /** @var array */ - private static $ossToW3c = [ + private static array $ossToW3c = [ 'platform' => 'platformName', 'version' => 'browserVersion', 'acceptSslCerts' => 'acceptInsecureCerts', @@ -85,8 +84,10 @@ public function toW3cCompatibleArray(): array } if (array_key_exists('firefox_profile', $capabilities)) { - if (! array_key_exists('moz:firefoxOptions', $capabilities) - || ! array_key_exists('profile', $capabilities['moz:firefoxOptions'])) { + if ( + ! array_key_exists('moz:firefoxOptions', $capabilities) + || ! array_key_exists('profile', $capabilities['moz:firefoxOptions']) + ) { $w3cCapabilities['moz:firefoxOptions']['profile'] = $capabilities['firefox_profile']; } } diff --git a/library/Pdfexport/WebDriver/Command.php b/library/Pdfexport/WebDriver/Command.php index 2e55650..de5c80f 100644 --- a/library/Pdfexport/WebDriver/Command.php +++ b/library/Pdfexport/WebDriver/Command.php @@ -5,7 +5,7 @@ class Command implements CommandInterface { public function __construct( - protected DriverCommand $name, + protected CommandName $name, protected array $parameters = [], ) { } @@ -17,17 +17,17 @@ public static function executeScript(string $script, array $arguments = []): sta 'args' => static::prepareScriptArguments($arguments), ]; - return new static(DriverCommand::ExecuteScript, $params); + return new static(CommandName::ExecuteScript, $params); } public static function getPageSource(): static { - return new static(DriverCommand::GetPageSource); + return new static(CommandName::GetPageSource); } public static function findElement(string $method, string $value): static { - return new static(DriverCommand::FindElement, [ + return new static(CommandName::FindElement, [ 'using' => $method, 'value' => $value, ]); @@ -49,7 +49,7 @@ protected static function prepareScriptArguments(array $arguments): array public static function printPage(array $printParameters): static { - return new static(DriverCommand::PrintPage, $printParameters); + return new static(CommandName::PrintPage, $printParameters); } public function getPath(): string @@ -67,7 +67,7 @@ public function getParameters(): array return $this->parameters; } - public function getName(): DriverCommand + public function getName(): CommandName { return $this->name; } diff --git a/library/Pdfexport/WebDriver/CommandExecutor.php b/library/Pdfexport/WebDriver/CommandExecutor.php index 29d3ea4..20561f4 100644 --- a/library/Pdfexport/WebDriver/CommandExecutor.php +++ b/library/Pdfexport/WebDriver/CommandExecutor.php @@ -4,6 +4,7 @@ use Exception; use GuzzleHttp\Client; +use Icinga\Application\Logger; use RuntimeException; class CommandExecutor @@ -44,15 +45,17 @@ public function execute(?string $sessionId, CommandInterface $command): Response throw new RuntimeException('Invalid HTTP method'); } - if ($command instanceof Command - && $command->getName() === DriverCommand::NewSession) { + if ( + $command instanceof Command + && $command->getName() === CommandName::NewSession + ) { $method = 'POST'; } $headers = static::DEFAULT_HEADERS; if (in_array($method, ['POST', 'PUT'], true)) { - unset ($headers['expect']); + unset($headers['expect']); } if (is_array($params) && ! empty($params)) { @@ -86,13 +89,16 @@ public function execute(?string $sessionId, CommandInterface $command): Response if (is_array($value) && array_key_exists('sessionId', $value)) { $sessionId = $value['sessionId']; - } else if (isset($results['sessionId'])) { + } elseif (isset($results['sessionId'])) { $sessionId = $results['sessionId']; } if (isset($value['error'])) { + Logger::error(print_r($value, true)); throw new Exception(sprintf( - "Error in command response: %s - %s", $value['error'], $value['message'] ?? "Unknown error" + "Error in command response: %s - %s", + $value['error'], + $value['message'] ?? "Unknown error", )); } diff --git a/library/Pdfexport/WebDriver/DriverCommand.php b/library/Pdfexport/WebDriver/CommandName.php similarity index 93% rename from library/Pdfexport/WebDriver/DriverCommand.php rename to library/Pdfexport/WebDriver/CommandName.php index 51683d8..a0190db 100644 --- a/library/Pdfexport/WebDriver/DriverCommand.php +++ b/library/Pdfexport/WebDriver/CommandName.php @@ -2,7 +2,7 @@ namespace Icinga\Module\Pdfexport\WebDriver; -enum DriverCommand: string +enum CommandName: string { case NewSession = 'newSession'; case Status = 'status'; @@ -15,7 +15,7 @@ enum DriverCommand: string public function getPath(): string { - return match($this) { + return match ($this) { self::NewSession => '/session', self::Status => '/status', self::Close => '/session/:sessionId/window', @@ -29,7 +29,7 @@ public function getPath(): string public function getMethod(): string { - return match($this) { + return match ($this) { self::NewSession => 'POST', self::Status => 'GET', self::Close => 'DELETE', diff --git a/library/Pdfexport/WebDriver/ElementPresentCondition.php b/library/Pdfexport/WebDriver/ElementPresentCondition.php index 9236d49..1f0ee58 100644 --- a/library/Pdfexport/WebDriver/ElementPresentCondition.php +++ b/library/Pdfexport/WebDriver/ElementPresentCondition.php @@ -4,12 +4,13 @@ class ElementPresentCondition implements ConditionInterface { - const WEBDRIVER_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf'; + protected const WEBDRIVER_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf'; protected function __construct( protected string $mechanism, protected string $value, - ) {} + ) { + } public function apply(WebDriver $driver): mixed { diff --git a/library/Pdfexport/WebDriver/WebDriver.php b/library/Pdfexport/WebDriver/WebDriver.php index b1511fd..59a9962 100644 --- a/library/Pdfexport/WebDriver/WebDriver.php +++ b/library/Pdfexport/WebDriver/WebDriver.php @@ -28,7 +28,7 @@ public static function create(string $url, Capabilities $capabilities): static 'desiredCapabilities' => (object) $capabilities->toArray(), ]; - $cmd = new Command(DriverCommand::NewSession, $params); + $cmd = new Command(CommandName::NewSession, $params); $response = $executor->execute(null, $cmd); @@ -76,7 +76,7 @@ public function wait( public function quit(): void { if ($this->executor !== null) { - $this->execute(new Command(DriverCommand::Quit)); + $this->execute(new Command(CommandName::Quit)); $this->executor = null; } From 55a5a447f6c6afe8fc98468f11b2a3327c0dc7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 19 Mar 2026 09:34:58 +0100 Subject: [PATCH 44/60] Revert "Install php-webdriver" This reverts commit 80f967c52e840dcd2b0f7dc70896d702568c085c. --- composer.json | 1 - composer.lock | 220 +-- vendor/autoload.php | 5 +- vendor/composer/InstalledVersions.php | 38 +- vendor/composer/autoload_files.php | 11 - vendor/composer/autoload_psr4.php | 5 +- vendor/composer/autoload_real.php | 12 - vendor/composer/autoload_static.php | 52 +- vendor/composer/installed.json | 225 --- vendor/composer/installed.php | 37 +- vendor/composer/platform_check.php | 5 +- vendor/php-webdriver/webdriver/CHANGELOG.md | 266 --- vendor/php-webdriver/webdriver/LICENSE.md | 22 - vendor/php-webdriver/webdriver/README.md | 228 --- vendor/php-webdriver/webdriver/composer.json | 97 - .../lib/AbstractWebDriverCheckboxOrRadio.php | 240 --- .../lib/Chrome/ChromeDevToolsDriver.php | 46 - .../webdriver/lib/Chrome/ChromeDriver.php | 107 -- .../lib/Chrome/ChromeDriverService.php | 37 - .../webdriver/lib/Chrome/ChromeOptions.php | 182 -- vendor/php-webdriver/webdriver/lib/Cookie.php | 278 --- .../Exception/DetachedShadowRootException.php | 10 - .../ElementClickInterceptedException.php | 11 - .../ElementNotInteractableException.php | 10 - .../ElementNotSelectableException.php | 10 - .../Exception/ElementNotVisibleException.php | 10 - .../lib/Exception/ExpectedException.php | 10 - .../IMEEngineActivationFailedException.php | 10 - .../Exception/IMENotAvailableException.php | 10 - .../Exception/IndexOutOfBoundsException.php | 10 - .../InsecureCertificateException.php | 11 - .../Internal/DriverServerDiedException.php | 16 - .../lib/Exception/Internal/IOException.php | 16 - .../lib/Exception/Internal/LogicException.php | 29 - .../Exception/Internal/RuntimeException.php | 28 - .../Internal/UnexpectedResponseException.php | 51 - .../Internal/WebDriverCurlException.php | 22 - .../Exception/InvalidArgumentException.php | 10 - .../InvalidCookieDomainException.php | 10 - .../Exception/InvalidCoordinatesException.php | 10 - .../InvalidElementStateException.php | 11 - .../Exception/InvalidSelectorException.php | 10 - .../Exception/InvalidSessionIdException.php | 11 - .../Exception/JavascriptErrorException.php | 10 - .../MoveTargetOutOfBoundsException.php | 10 - .../lib/Exception/NoAlertOpenException.php | 10 - .../lib/Exception/NoCollectionException.php | 10 - .../lib/Exception/NoScriptResultException.php | 10 - .../lib/Exception/NoStringException.php | 10 - .../lib/Exception/NoStringLengthException.php | 10 - .../Exception/NoStringWrapperException.php | 10 - .../lib/Exception/NoSuchAlertException.php | 10 - .../Exception/NoSuchCollectionException.php | 10 - .../lib/Exception/NoSuchCookieException.php | 11 - .../lib/Exception/NoSuchDocumentException.php | 10 - .../lib/Exception/NoSuchDriverException.php | 10 - .../lib/Exception/NoSuchElementException.php | 10 - .../lib/Exception/NoSuchFrameException.php | 10 - .../Exception/NoSuchShadowRootException.php | 10 - .../lib/Exception/NoSuchWindowException.php | 10 - .../lib/Exception/NullPointerException.php | 10 - .../PhpWebDriverExceptionInterface.php | 10 - .../lib/Exception/ScriptTimeoutException.php | 10 - .../Exception/SessionNotCreatedException.php | 10 - .../StaleElementReferenceException.php | 10 - .../lib/Exception/TimeoutException.php | 10 - .../UnableToCaptureScreenException.php | 10 - .../Exception/UnableToSetCookieException.php | 10 - .../UnexpectedAlertOpenException.php | 10 - .../UnexpectedJavascriptException.php | 10 - .../Exception/UnexpectedTagNameException.php | 23 - .../lib/Exception/UnknownCommandException.php | 10 - .../lib/Exception/UnknownErrorException.php | 10 - .../lib/Exception/UnknownMethodException.php | 10 - .../lib/Exception/UnknownServerException.php | 10 - .../UnrecognizedExceptionException.php | 7 - .../UnsupportedOperationException.php | 10 - .../lib/Exception/WebDriverException.php | 229 --- .../lib/Exception/XPathLookupException.php | 10 - .../webdriver/lib/Firefox/FirefoxDriver.php | 68 - .../lib/Firefox/FirefoxDriverService.php | 34 - .../webdriver/lib/Firefox/FirefoxOptions.php | 133 -- .../lib/Firefox/FirefoxPreferences.php | 25 - .../webdriver/lib/Firefox/FirefoxProfile.php | 318 ---- .../Internal/WebDriverButtonReleaseAction.php | 16 - .../Internal/WebDriverClickAction.php | 13 - .../Internal/WebDriverClickAndHoldAction.php | 16 - .../Internal/WebDriverContextClickAction.php | 16 - .../Internal/WebDriverCoordinates.php | 77 - .../Internal/WebDriverDoubleClickAction.php | 13 - .../Internal/WebDriverKeyDownAction.php | 12 - .../Internal/WebDriverKeyUpAction.php | 12 - .../Internal/WebDriverKeysRelatedAction.php | 43 - .../Internal/WebDriverMouseAction.php | 44 - .../Internal/WebDriverMouseMoveAction.php | 13 - .../Internal/WebDriverMoveToOffsetAction.php | 43 - .../Internal/WebDriverSendKeysAction.php | 35 - .../Internal/WebDriverSingleKeyAction.php | 54 - .../Touch/WebDriverDoubleTapAction.php | 13 - .../Touch/WebDriverDownAction.php | 33 - .../Touch/WebDriverFlickAction.php | 33 - .../Touch/WebDriverFlickFromElementAction.php | 50 - .../Touch/WebDriverLongPressAction.php | 13 - .../Touch/WebDriverMoveAction.php | 27 - .../Touch/WebDriverScrollAction.php | 27 - .../WebDriverScrollFromElementAction.php | 36 - .../Interactions/Touch/WebDriverTapAction.php | 13 - .../Touch/WebDriverTouchAction.php | 38 - .../Touch/WebDriverTouchScreen.php | 109 -- .../lib/Interactions/WebDriverActions.php | 253 --- .../Interactions/WebDriverCompositeAction.php | 48 - .../Interactions/WebDriverTouchActions.php | 175 -- .../lib/Internal/WebDriverLocatable.php | 16 - .../webdriver/lib/JavaScriptExecutor.php | 35 - .../webdriver/lib/Local/LocalWebDriver.php | 37 - .../webdriver/lib/Net/URLChecker.php | 73 - .../lib/Remote/CustomWebDriverCommand.php | 82 - .../lib/Remote/DesiredCapabilities.php | 428 ----- .../webdriver/lib/Remote/DriverCommand.php | 153 -- .../webdriver/lib/Remote/ExecuteMethod.php | 12 - .../webdriver/lib/Remote/FileDetector.php | 16 - .../lib/Remote/HttpCommandExecutor.php | 414 ---- .../webdriver/lib/Remote/JsonWireCompat.php | 98 - .../lib/Remote/LocalFileDetector.php | 20 - .../lib/Remote/RemoteExecuteMethod.php | 25 - .../webdriver/lib/Remote/RemoteKeyboard.php | 105 -- .../webdriver/lib/Remote/RemoteMouse.php | 290 --- .../webdriver/lib/Remote/RemoteStatus.php | 79 - .../lib/Remote/RemoteTargetLocator.php | 149 -- .../lib/Remote/RemoteTouchScreen.php | 177 -- .../webdriver/lib/Remote/RemoteWebDriver.php | 760 -------- .../webdriver/lib/Remote/RemoteWebElement.php | 650 ------- .../Remote/Service/DriverCommandExecutor.php | 53 - .../lib/Remote/Service/DriverService.php | 183 -- .../webdriver/lib/Remote/ShadowRoot.php | 98 - .../lib/Remote/UselessFileDetector.php | 11 - .../lib/Remote/WebDriverBrowserType.php | 40 - .../lib/Remote/WebDriverCapabilityType.php | 32 - .../webdriver/lib/Remote/WebDriverCommand.php | 60 - .../lib/Remote/WebDriverResponse.php | 84 - .../Support/Events/EventFiringWebDriver.php | 394 ---- .../Events/EventFiringWebDriverNavigation.php | 135 -- .../Support/Events/EventFiringWebElement.php | 413 ---- .../lib/Support/IsElementDisplayedAtom.php | 71 - .../lib/Support/ScreenshotHelper.php | 81 - .../webdriver/lib/Support/XPathEscaper.php | 32 - .../php-webdriver/webdriver/lib/WebDriver.php | 143 -- .../webdriver/lib/WebDriverAction.php | 11 - .../webdriver/lib/WebDriverAlert.php | 72 - .../webdriver/lib/WebDriverBy.php | 134 -- .../webdriver/lib/WebDriverCapabilities.php | 46 - .../webdriver/lib/WebDriverCheckboxes.php | 53 - .../lib/WebDriverCommandExecutor.php | 17 - .../webdriver/lib/WebDriverDimension.php | 59 - .../webdriver/lib/WebDriverDispatcher.php | 75 - .../webdriver/lib/WebDriverElement.php | 154 -- .../webdriver/lib/WebDriverEventListener.php | 52 - .../lib/WebDriverExpectedCondition.php | 584 ------ .../lib/WebDriverHasInputDevices.php | 19 - .../webdriver/lib/WebDriverKeyboard.php | 32 - .../webdriver/lib/WebDriverKeys.php | 132 -- .../webdriver/lib/WebDriverMouse.php | 47 - .../webdriver/lib/WebDriverNavigation.php | 45 - .../lib/WebDriverNavigationInterface.php | 43 - .../webdriver/lib/WebDriverOptions.php | 180 -- .../webdriver/lib/WebDriverPlatform.php | 25 - .../webdriver/lib/WebDriverPoint.php | 80 - .../webdriver/lib/WebDriverRadios.php | 52 - .../webdriver/lib/WebDriverSearchContext.php | 28 - .../webdriver/lib/WebDriverSelect.php | 250 --- .../lib/WebDriverSelectInterface.php | 128 -- .../webdriver/lib/WebDriverTargetLocator.php | 69 - .../webdriver/lib/WebDriverTimeouts.php | 102 - .../webdriver/lib/WebDriverUpAction.php | 28 - .../webdriver/lib/WebDriverWait.php | 73 - .../webdriver/lib/WebDriverWindow.php | 188 -- .../lib/scripts/isElementDisplayed.js | 219 --- vendor/symfony/polyfill-mbstring/LICENSE | 19 - vendor/symfony/polyfill-mbstring/Mbstring.php | 1045 ---------- vendor/symfony/polyfill-mbstring/README.md | 13 - .../Resources/unidata/caseFolding.php | 119 -- .../Resources/unidata/lowerCase.php | 1397 -------------- .../Resources/unidata/titleCaseRegexp.php | 5 - .../Resources/unidata/upperCase.php | 1489 --------------- .../symfony/polyfill-mbstring/bootstrap.php | 172 -- .../symfony/polyfill-mbstring/bootstrap80.php | 167 -- .../symfony/polyfill-mbstring/composer.json | 39 - vendor/symfony/process/CHANGELOG.md | 134 -- .../process/Exception/ExceptionInterface.php | 21 - .../Exception/InvalidArgumentException.php | 21 - .../process/Exception/LogicException.php | 21 - .../Exception/ProcessFailedException.php | 53 - .../Exception/ProcessSignaledException.php | 38 - .../Exception/ProcessStartFailedException.php | 43 - .../Exception/ProcessTimedOutException.php | 60 - .../Exception/RunProcessFailedException.php | 25 - .../process/Exception/RuntimeException.php | 21 - vendor/symfony/process/ExecutableFinder.php | 103 - vendor/symfony/process/InputStream.php | 91 - vendor/symfony/process/LICENSE | 19 - .../process/Messenger/RunProcessContext.php | 33 - .../process/Messenger/RunProcessMessage.php | 47 - .../Messenger/RunProcessMessageHandler.php | 36 - .../symfony/process/PhpExecutableFinder.php | 98 - vendor/symfony/process/PhpProcess.php | 69 - vendor/symfony/process/PhpSubprocess.php | 167 -- .../symfony/process/Pipes/AbstractPipes.php | 204 -- .../symfony/process/Pipes/PipesInterface.php | 61 - vendor/symfony/process/Pipes/UnixPipes.php | 144 -- vendor/symfony/process/Pipes/WindowsPipes.php | 185 -- vendor/symfony/process/Process.php | 1676 ----------------- vendor/symfony/process/ProcessUtils.php | 64 - vendor/symfony/process/README.md | 13 - vendor/symfony/process/composer.json | 28 - 214 files changed, 57 insertions(+), 21212 deletions(-) delete mode 100644 vendor/composer/autoload_files.php delete mode 100644 vendor/php-webdriver/webdriver/CHANGELOG.md delete mode 100644 vendor/php-webdriver/webdriver/LICENSE.md delete mode 100644 vendor/php-webdriver/webdriver/README.md delete mode 100644 vendor/php-webdriver/webdriver/composer.json delete mode 100644 vendor/php-webdriver/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Chrome/ChromeDevToolsDriver.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriver.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriverService.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Chrome/ChromeOptions.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Cookie.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/DetachedShadowRootException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ElementClickInterceptedException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ElementNotInteractableException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ElementNotSelectableException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ElementNotVisibleException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ExpectedException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/IMEEngineActivationFailedException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/IMENotAvailableException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/IndexOutOfBoundsException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InsecureCertificateException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/DriverServerDiedException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/IOException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/LogicException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/RuntimeException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/UnexpectedResponseException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/Internal/WebDriverCurlException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidArgumentException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidCookieDomainException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidCoordinatesException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidElementStateException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidSelectorException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/InvalidSessionIdException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/JavascriptErrorException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/MoveTargetOutOfBoundsException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoAlertOpenException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoCollectionException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoScriptResultException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoStringException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoStringLengthException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoStringWrapperException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchAlertException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchCollectionException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchCookieException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchDocumentException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchDriverException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchElementException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchFrameException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchShadowRootException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NoSuchWindowException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/NullPointerException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/PhpWebDriverExceptionInterface.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/ScriptTimeoutException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/SessionNotCreatedException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/StaleElementReferenceException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/TimeoutException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnableToCaptureScreenException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnableToSetCookieException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnexpectedAlertOpenException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnexpectedJavascriptException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnexpectedTagNameException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnknownCommandException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnknownErrorException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnknownMethodException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnknownServerException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnrecognizedExceptionException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/UnsupportedOperationException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/WebDriverException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Exception/XPathLookupException.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriver.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriverService.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxOptions.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxPreferences.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Firefox/FirefoxProfile.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverCoordinates.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDownAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTapAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/WebDriverActions.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/WebDriverCompositeAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Interactions/WebDriverTouchActions.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Internal/WebDriverLocatable.php delete mode 100644 vendor/php-webdriver/webdriver/lib/JavaScriptExecutor.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Local/LocalWebDriver.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Net/URLChecker.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/CustomWebDriverCommand.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/DesiredCapabilities.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/DriverCommand.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/ExecuteMethod.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/FileDetector.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/HttpCommandExecutor.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/JsonWireCompat.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/LocalFileDetector.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteExecuteMethod.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteKeyboard.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteMouse.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteStatus.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteTargetLocator.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteTouchScreen.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/Service/DriverCommandExecutor.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/Service/DriverService.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/ShadowRoot.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/UselessFileDetector.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/WebDriverBrowserType.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/WebDriverCapabilityType.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/WebDriverCommand.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Remote/WebDriverResponse.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriver.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebElement.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Support/IsElementDisplayedAtom.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Support/ScreenshotHelper.php delete mode 100644 vendor/php-webdriver/webdriver/lib/Support/XPathEscaper.php delete mode 100755 vendor/php-webdriver/webdriver/lib/WebDriver.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverAlert.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverBy.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverCapabilities.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverCheckboxes.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverCommandExecutor.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverDimension.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverDispatcher.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverElement.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverEventListener.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverExpectedCondition.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverHasInputDevices.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverKeyboard.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverKeys.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverMouse.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverNavigation.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverNavigationInterface.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverOptions.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverPlatform.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverPoint.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverRadios.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverSearchContext.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverSelect.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverSelectInterface.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverTargetLocator.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverTimeouts.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverUpAction.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverWait.php delete mode 100644 vendor/php-webdriver/webdriver/lib/WebDriverWindow.php delete mode 100644 vendor/php-webdriver/webdriver/lib/scripts/isElementDisplayed.js delete mode 100644 vendor/symfony/polyfill-mbstring/LICENSE delete mode 100644 vendor/symfony/polyfill-mbstring/Mbstring.php delete mode 100644 vendor/symfony/polyfill-mbstring/README.md delete mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php delete mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php delete mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php delete mode 100644 vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php delete mode 100644 vendor/symfony/polyfill-mbstring/bootstrap.php delete mode 100644 vendor/symfony/polyfill-mbstring/bootstrap80.php delete mode 100644 vendor/symfony/polyfill-mbstring/composer.json delete mode 100644 vendor/symfony/process/CHANGELOG.md delete mode 100644 vendor/symfony/process/Exception/ExceptionInterface.php delete mode 100644 vendor/symfony/process/Exception/InvalidArgumentException.php delete mode 100644 vendor/symfony/process/Exception/LogicException.php delete mode 100644 vendor/symfony/process/Exception/ProcessFailedException.php delete mode 100644 vendor/symfony/process/Exception/ProcessSignaledException.php delete mode 100644 vendor/symfony/process/Exception/ProcessStartFailedException.php delete mode 100644 vendor/symfony/process/Exception/ProcessTimedOutException.php delete mode 100644 vendor/symfony/process/Exception/RunProcessFailedException.php delete mode 100644 vendor/symfony/process/Exception/RuntimeException.php delete mode 100644 vendor/symfony/process/ExecutableFinder.php delete mode 100644 vendor/symfony/process/InputStream.php delete mode 100644 vendor/symfony/process/LICENSE delete mode 100644 vendor/symfony/process/Messenger/RunProcessContext.php delete mode 100644 vendor/symfony/process/Messenger/RunProcessMessage.php delete mode 100644 vendor/symfony/process/Messenger/RunProcessMessageHandler.php delete mode 100644 vendor/symfony/process/PhpExecutableFinder.php delete mode 100644 vendor/symfony/process/PhpProcess.php delete mode 100644 vendor/symfony/process/PhpSubprocess.php delete mode 100644 vendor/symfony/process/Pipes/AbstractPipes.php delete mode 100644 vendor/symfony/process/Pipes/PipesInterface.php delete mode 100644 vendor/symfony/process/Pipes/UnixPipes.php delete mode 100644 vendor/symfony/process/Pipes/WindowsPipes.php delete mode 100644 vendor/symfony/process/Process.php delete mode 100644 vendor/symfony/process/ProcessUtils.php delete mode 100644 vendor/symfony/process/README.md delete mode 100644 vendor/symfony/process/composer.json diff --git a/composer.json b/composer.json index e390362..79d6d79 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,6 @@ "require": { "php": ">=8.2", "karriere/pdf-merge": "dev-master", - "php-webdriver/webdriver": "1.15.2", "phrity/websocket": "^3.6" }, "config": { diff --git a/composer.lock b/composer.lock index ee808dd..f749ab2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5936242128deac4d5d29d07f8df8a2a5", + "content-hash": "5c3df7bceb770cd13e1f582b6dcc2ff5", "packages": [ { "name": "karriere/pdf-merge", @@ -92,72 +92,6 @@ }, "time": "2026-01-19T15:39:37+00:00" }, - { - "name": "php-webdriver/webdriver", - "version": "1.15.2", - "source": { - "type": "git", - "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf", - "reference": "998e499b786805568deaf8cbf06f4044f05d91bf", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-zip": "*", - "php": "^7.3 || ^8.0", - "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^5.0 || ^6.0 || ^7.0" - }, - "replace": { - "facebook/webdriver": "*" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.20.0", - "ondram/ci-detector": "^4.0", - "php-coveralls/php-coveralls": "^2.4", - "php-mock/php-mock-phpunit": "^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpunit/phpunit": "^9.3", - "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "type": "library", - "autoload": { - "files": [ - "lib/Exception/TimeoutException.php" - ], - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", - "homepage": "https://github.com/php-webdriver/php-webdriver", - "keywords": [ - "Chromedriver", - "geckodriver", - "php", - "selenium", - "webdriver" - ], - "support": { - "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" - }, - "time": "2024-11-21T15:12:59+00:00" - }, { "name": "phrity/comparison", "version": "1.4.1", @@ -668,156 +602,6 @@ }, "time": "2024-09-11T13:17:53+00:00" }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-23T08:48:59+00:00" - }, - { - "name": "symfony/process", - "version": "v7.4.5", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "608476f4604102976d687c483ac63a79ba18cc97" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", - "reference": "608476f4604102976d687c483ac63a79ba18cc97", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v7.4.5" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-01-26T15:07:59+00:00" - }, { "name": "tecnickcom/tcpdf", "version": "6.10.0", @@ -902,5 +686,5 @@ "php": ">=8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/vendor/autoload.php b/vendor/autoload.php index 7642d2c..5f84171 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -14,10 +14,7 @@ echo $err; } } - trigger_error( - $err, - E_USER_ERROR - ); + throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index 07b32ed..2052022 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -26,12 +26,23 @@ */ class InstalledVersions { + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null */ private static $installed; + /** + * @var bool + */ + private static $installedIsLocalDir; + /** * @var bool|null */ @@ -309,6 +320,24 @@ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; } /** @@ -325,7 +354,9 @@ private static function getInstalled() $copiedLocalDir = false; if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { @@ -333,11 +364,14 @@ private static function getInstalled() $required = require $vendorDir.'/composer/installed.php'; self::$installedByVendor[$vendorDir] = $required; $installed[] = $required; - if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { self::$installed = $required; - $copiedLocalDir = true; + self::$installedIsLocalDir = true; } } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } } } diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php deleted file mode 100644 index 4314571..0000000 --- a/vendor/composer/autoload_files.php +++ /dev/null @@ -1,11 +0,0 @@ - $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', - '2a3c2110e8e0295330dc3d11a4cbc4cb' => $vendorDir . '/php-webdriver/webdriver/lib/Exception/TimeoutException.php', -); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 81b5069..6e32a62 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -7,14 +7,11 @@ return array( 'WebSocket\\' => array($vendorDir . '/phrity/websocket/src'), - 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), - 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'), 'Phrity\\Util\\' => array($vendorDir . '/phrity/util-errorhandler/src'), - 'Phrity\\Net\\' => array($vendorDir . '/phrity/net-stream/src', $vendorDir . '/phrity/net-uri/src'), + 'Phrity\\Net\\' => array($vendorDir . '/phrity/net-uri/src', $vendorDir . '/phrity/net-stream/src'), 'Phrity\\Http\\' => array($vendorDir . '/phrity/http/src'), 'Phrity\\Comparison\\' => array($vendorDir . '/phrity/comparison/src'), 'Karriere\\PdfMerge\\' => array($vendorDir . '/karriere/pdf-merge/src'), - 'Facebook\\WebDriver\\' => array($vendorDir . '/php-webdriver/webdriver/lib'), ); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 936217f..9468cad 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -33,18 +33,6 @@ public static function getLoader() $loader->register(true); - $filesToLoad = \Composer\Autoload\ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776::$files; - $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { - if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { - $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; - - require $file; - } - }, null, null); - foreach ($filesToLoad as $fileIdentifier => $file) { - $requireFile($fileIdentifier, $file); - } - return $loader; } } diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index fffe6ec..5af240e 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -6,22 +6,12 @@ class ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776 { - public static $files = array ( - '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', - '2a3c2110e8e0295330dc3d11a4cbc4cb' => __DIR__ . '/..' . '/php-webdriver/webdriver/lib/Exception/TimeoutException.php', - ); - public static $prefixLengthsPsr4 = array ( - 'W' => + 'W' => array ( 'WebSocket\\' => 10, ), - 'S' => - array ( - 'Symfony\\Polyfill\\Mbstring\\' => 26, - 'Symfony\\Component\\Process\\' => 26, - ), - 'P' => + 'P' => array ( 'Psr\\Log\\' => 8, 'Psr\\Http\\Message\\' => 17, @@ -30,63 +20,47 @@ class ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776 'Phrity\\Http\\' => 12, 'Phrity\\Comparison\\' => 18, ), - 'K' => + 'K' => array ( 'Karriere\\PdfMerge\\' => 18, ), - 'F' => - array ( - 'Facebook\\WebDriver\\' => 19, - ), ); public static $prefixDirsPsr4 = array ( - 'WebSocket\\' => + 'WebSocket\\' => array ( 0 => __DIR__ . '/..' . '/phrity/websocket/src', ), - 'Symfony\\Polyfill\\Mbstring\\' => - array ( - 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', - ), - 'Symfony\\Component\\Process\\' => - array ( - 0 => __DIR__ . '/..' . '/symfony/process', - ), - 'Psr\\Log\\' => + 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/src', ), - 'Psr\\Http\\Message\\' => + 'Psr\\Http\\Message\\' => array ( 0 => __DIR__ . '/..' . '/psr/http-factory/src', 1 => __DIR__ . '/..' . '/psr/http-message/src', ), - 'Phrity\\Util\\' => + 'Phrity\\Util\\' => array ( 0 => __DIR__ . '/..' . '/phrity/util-errorhandler/src', ), - 'Phrity\\Net\\' => + 'Phrity\\Net\\' => array ( - 0 => __DIR__ . '/..' . '/phrity/net-stream/src', - 1 => __DIR__ . '/..' . '/phrity/net-uri/src', + 0 => __DIR__ . '/..' . '/phrity/net-uri/src', + 1 => __DIR__ . '/..' . '/phrity/net-stream/src', ), - 'Phrity\\Http\\' => + 'Phrity\\Http\\' => array ( 0 => __DIR__ . '/..' . '/phrity/http/src', ), - 'Phrity\\Comparison\\' => + 'Phrity\\Comparison\\' => array ( 0 => __DIR__ . '/..' . '/phrity/comparison/src', ), - 'Karriere\\PdfMerge\\' => + 'Karriere\\PdfMerge\\' => array ( 0 => __DIR__ . '/..' . '/karriere/pdf-merge/src', ), - 'Facebook\\WebDriver\\' => - array ( - 0 => __DIR__ . '/..' . '/php-webdriver/webdriver/lib', - ), ); public static $classMap = array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 0d56a51..a87ff1f 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -89,75 +89,6 @@ }, "install-path": "../karriere/pdf-merge" }, - { - "name": "php-webdriver/webdriver", - "version": "1.15.2", - "version_normalized": "1.15.2.0", - "source": { - "type": "git", - "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf", - "reference": "998e499b786805568deaf8cbf06f4044f05d91bf", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-zip": "*", - "php": "^7.3 || ^8.0", - "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^5.0 || ^6.0 || ^7.0" - }, - "replace": { - "facebook/webdriver": "*" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.20.0", - "ondram/ci-detector": "^4.0", - "php-coveralls/php-coveralls": "^2.4", - "php-mock/php-mock-phpunit": "^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpunit/phpunit": "^9.3", - "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "time": "2024-11-21T15:12:59+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "files": [ - "lib/Exception/TimeoutException.php" - ], - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", - "homepage": "https://github.com/php-webdriver/php-webdriver", - "keywords": [ - "Chromedriver", - "geckodriver", - "php", - "selenium", - "webdriver" - ], - "support": { - "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" - }, - "install-path": "../php-webdriver/webdriver" - }, { "name": "phrity/comparison", "version": "1.4.1", @@ -695,162 +626,6 @@ }, "install-path": "../psr/log" }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", - "version_normalized": "1.33.0.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "time": "2024-12-23T08:48:59+00:00", - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "installation-source": "dist", - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "install-path": "../symfony/polyfill-mbstring" - }, - { - "name": "symfony/process", - "version": "v7.4.5", - "version_normalized": "7.4.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "608476f4604102976d687c483ac63a79ba18cc97" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", - "reference": "608476f4604102976d687c483ac63a79ba18cc97", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "time": "2026-01-26T15:07:59+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v7.4.5" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "install-path": "../symfony/process" - }, { "name": "tecnickcom/tcpdf", "version": "6.10.0", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index c248751..61e4d33 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '9468d76922e728ff5aa16d55fc4ca472755e07e5', + 'reference' => '9ce3413e78d2809619557cbe6ea79fafc5dd70c5', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -13,18 +13,12 @@ '__root__' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '9468d76922e728ff5aa16d55fc4ca472755e07e5', + 'reference' => '9ce3413e78d2809619557cbe6ea79fafc5dd70c5', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), - 'facebook/webdriver' => array( - 'dev_requirement' => false, - 'replaced' => array( - 0 => '*', - ), - ), 'karriere/pdf-merge' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', @@ -36,15 +30,6 @@ ), 'dev_requirement' => false, ), - 'php-webdriver/webdriver' => array( - 'pretty_version' => '1.15.2', - 'version' => '1.15.2.0', - 'reference' => '998e499b786805568deaf8cbf06f4044f05d91bf', - 'type' => 'library', - 'install_path' => __DIR__ . '/../php-webdriver/webdriver', - 'aliases' => array(), - 'dev_requirement' => false, - ), 'phrity/comparison' => array( 'pretty_version' => '1.4.1', 'version' => '1.4.1.0', @@ -126,24 +111,6 @@ 'aliases' => array(), 'dev_requirement' => false, ), - 'symfony/polyfill-mbstring' => array( - 'pretty_version' => 'v1.33.0', - 'version' => '1.33.0.0', - 'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493', - 'type' => 'library', - 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', - 'aliases' => array(), - 'dev_requirement' => false, - ), - 'symfony/process' => array( - 'pretty_version' => 'v7.4.5', - 'version' => '7.4.5.0', - 'reference' => '608476f4604102976d687c483ac63a79ba18cc97', - 'type' => 'library', - 'install_path' => __DIR__ . '/../symfony/process', - 'aliases' => array(), - 'dev_requirement' => false, - ), 'tecnickcom/tcpdf' => array( 'pretty_version' => '6.10.0', 'version' => '6.10.0.0', diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index d32d90c..14bf88d 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -19,8 +19,7 @@ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - trigger_error( - 'Composer detected issues in your platform: ' . implode(' ', $issues), - E_USER_ERROR + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) ); } diff --git a/vendor/php-webdriver/webdriver/CHANGELOG.md b/vendor/php-webdriver/webdriver/CHANGELOG.md deleted file mode 100644 index a468c26..0000000 --- a/vendor/php-webdriver/webdriver/CHANGELOG.md +++ /dev/null @@ -1,266 +0,0 @@ -# Changelog -This project versioning adheres to [Semantic Versioning](http://semver.org/). - -## Unreleased - -## 1.15.2 - 2024-11-21 -### Fixed -- PHP 8.4 deprecation notices, especially in nullable type-hints. -- Docs: Fix static return types in RemoteWebElement phpDoc. -- Tests: Disable chrome 127+ search engine pop-up in tests -- Tests: Enable Shadow DOM tests in Geckodriver - -### Added -- Tests: Allow running tests in headfull (not headless) mode using `DISABLE_HEADLESS` environment variable. - -### Changed -- Docs: Update selenium server host URL in example. - -## 1.15.1 - 2023-10-20 -- Update `symfony/process` dependency to support upcoming Symfony 7. - -## 1.15.0 - 2023-08-29 -### Changed -- Capability key `ChromeOptions::CAPABILITY_W3C` used to set ChromeOptions is now deprecated in favor of `ChromeOptions::CAPABILITY`, which now also contains the W3C compatible value (`goog:chromeOptions`). -- ChromeOptions are now passed to the driver always as a W3C compatible key `goog:chromeOptions`, even in the deprecated OSS JsonWire payload (as ChromeDriver [supports](https://bugs.chromium.org/p/chromedriver/issues/detail?id=1786) this since 2017). -- Improve Safari compatibility for `` by its partial text (using `selectByVisiblePartialText()`). -- `XPathEscaper` helper class to quote XPaths containing both single and double quotes. -- `WebDriverSelectInterface`, to allow implementation of custom select-like components, eg. those not built around and actual select tag. - -### Changed -- `Symfony\Process` is used to start local WebDriver processes (when browsers are run directly, without Selenium server) to workaround some PHP bugs and improve portability. -- Clarified meaning of selenium server URL variable in methods of `RemoteWebDriver` class. -- Deprecated `setSessionID()` and `setCommandExecutor()` methods of `RemoteWebDriver` class; these values should be immutable and thus passed only via constructor. -- Deprecated `WebDriverExpectedCondition::textToBePresentInElement()` in favor of `elementTextContains()`. -- Throw an exception when attempting to deselect options of non-multiselect (it already didn't have any effect, but was silently ignored). -- Optimize performance of `(de)selectByIndex()` and `getAllSelectedOptions()` methods of `WebDriverSelect` when used with non-multiple select element. - -### Fixed -- XPath escaping in `select*()` and `deselect*()` methods of `WebDriverSelect`. - -## 1.2.0 - 2016-10-14 -- Added initial support of remote Microsoft Edge browser (but starting local EdgeDriver is still not supported). -- Utilize late static binding to make eg. `WebDriverBy` and `DesiredCapabilities` classes easily extensible. -- PHP version at least 5.5 is required. -- Fixed incompatibility with Appium, caused by redundant params present in requests to Selenium server. - -## 1.1.3 - 2016-08-10 -- Fixed FirefoxProfile to support installation of extensions with custom namespace prefix in their manifest file. -- Comply codestyle with [PSR-2](http://www.php-fig.org/psr/psr-2/). - -## 1.1.2 - 2016-06-04 -- Added ext-curl to composer.json. -- Added CHANGELOG.md. -- Added CONTRIBUTING.md with information and rules for contributors. - -## 1.1.1 - 2015-12-31 -- Fixed strict standards error in `ChromeDriver`. -- Added unit tests for `WebDriverCommand` and `DesiredCapabilities`. -- Fixed retrieving temporary path name in `FirefoxDriver` when `open_basedir` restriction is in effect. - -## 1.1.0 - 2015-12-08 -- FirefoxProfile improved - added possibility to set RDF file and to add datas for extensions. -- Fixed setting 0 second timeout of `WebDriverWait`. diff --git a/vendor/php-webdriver/webdriver/LICENSE.md b/vendor/php-webdriver/webdriver/LICENSE.md deleted file mode 100644 index 611fa73..0000000 --- a/vendor/php-webdriver/webdriver/LICENSE.md +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2004-2020 Facebook -Copyright (c) 2020-present [open-source contributors](https://github.com/php-webdriver/php-webdriver/graphs/contributors) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/php-webdriver/webdriver/README.md b/vendor/php-webdriver/webdriver/README.md deleted file mode 100644 index f3e8d38..0000000 --- a/vendor/php-webdriver/webdriver/README.md +++ /dev/null @@ -1,228 +0,0 @@ -# php-webdriver – Selenium WebDriver bindings for PHP - -[![Latest stable version](https://img.shields.io/packagist/v/php-webdriver/webdriver.svg?style=flat-square&label=Packagist)](https://packagist.org/packages/php-webdriver/webdriver) -[![GitHub Actions build status](https://img.shields.io/github/actions/workflow/status/php-webdriver/php-webdriver/tests.yaml?style=flat-square&label=GitHub%20Actions)](https://github.com/php-webdriver/php-webdriver/actions) -[![SauceLabs test status](https://img.shields.io/github/actions/workflow/status/php-webdriver/php-webdriver/sauce-labs.yaml?style=flat-square&label=SauceLabs)](https://saucelabs.com/u/php-webdriver) -[![Total downloads](https://img.shields.io/packagist/dd/php-webdriver/webdriver.svg?style=flat-square&label=Downloads)](https://packagist.org/packages/php-webdriver/webdriver) - -## Description -Php-webdriver library is PHP language binding for Selenium WebDriver, which allows you to control web browsers from PHP. - -This library is compatible with Selenium server version 2.x, 3.x and 4.x. - -The library supports modern [W3C WebDriver](https://w3c.github.io/webdriver/) protocol, as well -as legacy [JsonWireProtocol](https://www.selenium.dev/documentation/legacy/json_wire_protocol/). - -The concepts of this library are very similar to the "official" Java, JavaScript, .NET, Python and Ruby libraries -which are developed as part of the [Selenium project](https://github.com/SeleniumHQ/selenium/). - -## Installation - -Installation is possible using [Composer](https://getcomposer.org/). - -If you don't already use Composer, you can download the `composer.phar` binary: - - curl -sS https://getcomposer.org/installer | php - -Then install the library: - - php composer.phar require php-webdriver/webdriver - -## Upgrade from version <1.8.0 - -Starting from version 1.8.0, the project has been renamed from `facebook/php-webdriver` to `php-webdriver/webdriver`. - -In order to receive the new version and future updates, **you need to rename it in your composer.json**: - -```diff -"require": { -- "facebook/webdriver": "(version you use)", -+ "php-webdriver/webdriver": "(version you use)", -} -``` - -and run `composer update`. - -## Getting started - -### 1. Start server (aka. remote end) - -To control a browser, you need to start a *remote end* (server), which will listen to the commands sent -from this library and will execute them in the respective browser. - -This could be Selenium standalone server, but for local development, you can send them directly to so-called "browser driver" like Chromedriver or Geckodriver. - -#### a) Chromedriver - -📙 Below you will find a simple example. Make sure to read our wiki for [more information on Chrome/Chromedriver](https://github.com/php-webdriver/php-webdriver/wiki/Chrome). - -Install the latest Chrome and [Chromedriver](https://sites.google.com/chromium.org/driver/downloads). -Make sure to have a compatible version of Chromedriver and Chrome! - -Run `chromedriver` binary, you can pass `port` argument, so that it listens on port 4444: - -```sh -chromedriver --port=4444 -``` - -#### b) Geckodriver - -📙 Below you will find a simple example. Make sure to read our wiki for [more information on Firefox/Geckodriver](https://github.com/php-webdriver/php-webdriver/wiki/Firefox). - -Install the latest Firefox and [Geckodriver](https://github.com/mozilla/geckodriver/releases). -Make sure to have a compatible version of Geckodriver and Firefox! - -Run `geckodriver` binary (it start to listen on port 4444 by default): - -```sh -geckodriver -``` - -#### c) Selenium standalone server - -Selenium server can be useful when you need to execute multiple tests at once, -when you run tests in several different browsers (like on your CI server), or when you need to distribute tests amongst -several machines in grid mode (where one Selenium server acts as a hub, and others connect to it as nodes). - -Selenium server then act like a proxy and takes care of distributing commands to the respective nodes. - -The latest version can be found on the [Selenium download page](https://www.selenium.dev/downloads/). - -📙 You can find [further Selenium server information](https://github.com/php-webdriver/php-webdriver/wiki/Selenium-server) -in our wiki. - -#### d) Docker - -Selenium server could also be started inside Docker container - see [docker-selenium project](https://github.com/SeleniumHQ/docker-selenium). - -### 2. Create a Browser Session - -When creating a browser session, be sure to pass the url of your running server. - -For example: - -```php -// Chromedriver (if started using --port=4444 as above) -$serverUrl = 'http://localhost:4444'; -// Geckodriver -$serverUrl = 'http://localhost:4444'; -// selenium-server-standalone-#.jar (version 2.x or 3.x) -$serverUrl = 'http://localhost:4444/wd/hub'; -// selenium-server-standalone-#.jar (version 4.x) -$serverUrl = 'http://localhost:4444'; -``` - -Now you can start browser of your choice: - -```php -use Facebook\WebDriver\Remote\RemoteWebDriver; - -// Chrome -$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::chrome()); -// Firefox -$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::firefox()); -// Microsoft Edge -$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::microsoftEdge()); -``` - -### 3. Customize Desired Capabilities - -Desired capabilities define properties of the browser you are about to start. - -They can be customized: - -```php -use Facebook\WebDriver\Firefox\FirefoxOptions; -use Facebook\WebDriver\Remote\DesiredCapabilities; - -$desiredCapabilities = DesiredCapabilities::firefox(); - -// Disable accepting SSL certificates -$desiredCapabilities->setCapability('acceptSslCerts', false); - -// Add arguments via FirefoxOptions to start headless firefox -$firefoxOptions = new FirefoxOptions(); -$firefoxOptions->addArguments(['-headless']); -$desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); - -$driver = RemoteWebDriver::create($serverUrl, $desiredCapabilities); -``` - -Capabilities can also be used to [📙 configure a proxy server](https://github.com/php-webdriver/php-webdriver/wiki/HowTo-Work-with-proxy) which the browser should use. - -To configure browser-specific capabilities, you may use [📙 ChromeOptions](https://github.com/php-webdriver/php-webdriver/wiki/Chrome#chromeoptions) -or [📙 FirefoxOptions](https://github.com/php-webdriver/php-webdriver/wiki/Firefox#firefoxoptions). - -* See [legacy JsonWire protocol](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities) documentation or [W3C WebDriver specification](https://w3c.github.io/webdriver/#capabilities) for more details. - -### 4. Control your browser - -```php -// Go to URL -$driver->get('https://en.wikipedia.org/wiki/Selenium_(software)'); - -// Find search element by its id, write 'PHP' inside and submit -$driver->findElement(WebDriverBy::id('searchInput')) // find search input element - ->sendKeys('PHP') // fill the search box - ->submit(); // submit the whole form - -// Find element of 'History' item in menu by its css selector -$historyButton = $driver->findElement( - WebDriverBy::cssSelector('#ca-history a') -); -// Read text of the element and print it to output -echo 'About to click to a button with text: ' . $historyButton->getText(); - -// Click the element to navigate to revision history page -$historyButton->click(); - -// Make sure to always call quit() at the end to terminate the browser session -$driver->quit(); -``` - -See [example.php](example.php) for full example scenario. -Visit our GitHub wiki for [📙 php-webdriver command reference](https://github.com/php-webdriver/php-webdriver/wiki/Example-command-reference) and further examples. - -**NOTE:** Above snippets are not intended to be a working example by simply copy-pasting. See [example.php](example.php) for a working example. - -## Changelog -For latest changes see [CHANGELOG.md](CHANGELOG.md) file. - -## More information - -Some basic usage example is provided in [example.php](example.php) file. - -How-tos are provided right here in [📙 our GitHub wiki](https://github.com/php-webdriver/php-webdriver/wiki). - -If you don't use IDE, you may use [API documentation of php-webdriver](https://php-webdriver.github.io/php-webdriver/latest/). - -You may also want to check out the Selenium project [docs](https://selenium.dev/documentation/en/) and [wiki](https://github.com/SeleniumHQ/selenium/wiki). - -## Testing framework integration - -To take advantage of automatized testing you may want to integrate php-webdriver to your testing framework. -There are some projects already providing this: - -- [Symfony Panther](https://github.com/symfony/panther) uses php-webdriver and integrates with PHPUnit using `PantherTestCase` -- [Laravel Dusk](https://laravel.com/docs/dusk) is another project using php-webdriver, could be used for testing via `DuskTestCase` -- [Steward](https://github.com/lmc-eu/steward) integrates php-webdriver directly to [PHPUnit](https://phpunit.de/), and provides parallelization -- [Codeception](https://codeception.com/) testing framework provides BDD-layer on top of php-webdriver in its [WebDriver module](https://codeception.com/docs/modules/WebDriver) -- You can also check out this [blogpost](https://codeception.com/11-12-2013/working-with-phpunit-and-selenium-webdriver.html) + [demo project](https://github.com/DavertMik/php-webdriver-demo), describing simple [PHPUnit](https://phpunit.de/) integration - -## Support - -We have a great community willing to help you! - -❓ Do you have a **question, idea or some general feedback**? Visit our [Discussions](https://github.com/php-webdriver/php-webdriver/discussions) page. -(Alternatively, you can [look for many answered questions also on StackOverflow](https://stackoverflow.com/questions/tagged/php+selenium-webdriver)). - -🐛 Something isn't working, and you want to **report a bug**? [Submit it here](https://github.com/php-webdriver/php-webdriver/issues/new) as a new issue. - -📙 Looking for a **how-to** or **reference documentation**? See [our wiki](https://github.com/php-webdriver/php-webdriver/wiki). - -## Contributing ❤️ - -We love to have your help to make php-webdriver better. See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for more information about contributing and developing php-webdriver. - -Php-webdriver is community project - if you want to join the effort with maintaining and developing this library, the best is to look on [issues marked with "help wanted"](https://github.com/php-webdriver/php-webdriver/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) -label. Let us know in the issue comments if you want to contribute and if you want any guidance, and we will be delighted to help you to prepare your pull request. diff --git a/vendor/php-webdriver/webdriver/composer.json b/vendor/php-webdriver/webdriver/composer.json deleted file mode 100644 index bd21842..0000000 --- a/vendor/php-webdriver/webdriver/composer.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "name": "php-webdriver/webdriver", - "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", - "license": "MIT", - "type": "library", - "keywords": [ - "webdriver", - "selenium", - "php", - "geckodriver", - "chromedriver" - ], - "homepage": "https://github.com/php-webdriver/php-webdriver", - "require": { - "php": "^7.3 || ^8.0", - "ext-curl": "*", - "ext-json": "*", - "ext-zip": "*", - "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^5.0 || ^6.0 || ^7.0" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.20.0", - "ondram/ci-detector": "^4.0", - "php-coveralls/php-coveralls": "^2.4", - "php-mock/php-mock-phpunit": "^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpunit/phpunit": "^9.3", - "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" - }, - "replace": { - "facebook/webdriver": "*" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "minimum-stability": "dev", - "autoload": { - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - }, - "files": [ - "lib/Exception/TimeoutException.php" - ] - }, - "autoload-dev": { - "psr-4": { - "Facebook\\WebDriver\\": [ - "tests/unit", - "tests/functional" - ] - }, - "classmap": [ - "tests/functional/" - ] - }, - "config": { - "allow-plugins": { - "ergebnis/composer-normalize": true - }, - "sort-packages": true - }, - "scripts": { - "post-install-cmd": [ - "@composer install --working-dir=tools/php-cs-fixer --no-progress --no-interaction", - "@composer install --working-dir=tools/phpstan --no-progress --no-interaction" - ], - "post-update-cmd": [ - "@composer update --working-dir=tools/php-cs-fixer --no-progress --no-interaction", - "@composer update --working-dir=tools/phpstan --no-progress --no-interaction" - ], - "all": [ - "@lint", - "@analyze", - "@test" - ], - "analyze": [ - "@php tools/phpstan/vendor/bin/phpstan analyze -c phpstan.neon --ansi", - "@php tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff --dry-run -vvv --ansi", - "@php vendor/bin/phpcs --standard=PSR2 --ignore=*.js ./lib/ ./tests/" - ], - "fix": [ - "@composer normalize", - "@php tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff -vvv || exit 0", - "@php vendor/bin/phpcbf --standard=PSR2 --ignore=*.js ./lib/ ./tests/" - ], - "lint": [ - "@php vendor/bin/parallel-lint -j 10 ./lib ./tests example.php", - "@composer validate", - "@composer normalize --dry-run" - ], - "test": [ - "@php vendor/bin/phpunit --colors=always" - ] - } -} diff --git a/vendor/php-webdriver/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php b/vendor/php-webdriver/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php deleted file mode 100644 index 00aee24..0000000 --- a/vendor/php-webdriver/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php +++ /dev/null @@ -1,240 +0,0 @@ -getTagName(); - if ($tagName !== 'input') { - throw new UnexpectedTagNameException('input', $tagName); - } - - $this->name = $element->getAttribute('name'); - if ($this->name === null) { - throw new InvalidElementStateException('The input does not have a "name" attribute.'); - } - - $this->element = $element; - } - - public function getOptions() - { - return $this->getRelatedElements(); - } - - public function getAllSelectedOptions() - { - $selectedElement = []; - foreach ($this->getRelatedElements() as $element) { - if ($element->isSelected()) { - $selectedElement[] = $element; - - if (!$this->isMultiple()) { - return $selectedElement; - } - } - } - - return $selectedElement; - } - - public function getFirstSelectedOption() - { - foreach ($this->getRelatedElements() as $element) { - if ($element->isSelected()) { - return $element; - } - } - - throw new NoSuchElementException( - sprintf('No %s are selected', $this->type === 'radio' ? 'radio buttons' : 'checkboxes') - ); - } - - public function selectByIndex($index) - { - $this->byIndex($index); - } - - public function selectByValue($value) - { - $this->byValue($value); - } - - public function selectByVisibleText($text) - { - $this->byVisibleText($text); - } - - public function selectByVisiblePartialText($text) - { - $this->byVisibleText($text, true); - } - - /** - * Selects or deselects a checkbox or a radio button by its value. - * - * @param string $value - * @param bool $select - * @throws NoSuchElementException - */ - protected function byValue($value, $select = true) - { - $matched = false; - foreach ($this->getRelatedElements($value) as $element) { - $select ? $this->selectOption($element) : $this->deselectOption($element); - if (!$this->isMultiple()) { - return; - } - - $matched = true; - } - - if (!$matched) { - throw new NoSuchElementException( - sprintf('Cannot locate %s with value: %s', $this->type, $value) - ); - } - } - - /** - * Selects or deselects a checkbox or a radio button by its index. - * - * @param int $index - * @param bool $select - * @throws NoSuchElementException - */ - protected function byIndex($index, $select = true) - { - $elements = $this->getRelatedElements(); - if (!isset($elements[$index])) { - throw new NoSuchElementException(sprintf('Cannot locate %s with index: %d', $this->type, $index)); - } - - $select ? $this->selectOption($elements[$index]) : $this->deselectOption($elements[$index]); - } - - /** - * Selects or deselects a checkbox or a radio button by its visible text. - * - * @param string $text - * @param bool $partial - * @param bool $select - */ - protected function byVisibleText($text, $partial = false, $select = true) - { - foreach ($this->getRelatedElements() as $element) { - $normalizeFilter = sprintf( - $partial ? 'contains(normalize-space(.), %s)' : 'normalize-space(.) = %s', - XPathEscaper::escapeQuotes($text) - ); - - $xpath = 'ancestor::label'; - $xpathNormalize = sprintf('%s[%s]', $xpath, $normalizeFilter); - - $id = $element->getAttribute('id'); - if ($id !== null) { - $idFilter = sprintf('@for = %s', XPathEscaper::escapeQuotes($id)); - - $xpath .= sprintf(' | //label[%s]', $idFilter); - $xpathNormalize .= sprintf(' | //label[%s and %s]', $idFilter, $normalizeFilter); - } - - try { - $element->findElement(WebDriverBy::xpath($xpathNormalize)); - } catch (NoSuchElementException $e) { - if ($partial) { - continue; - } - - try { - // Since the mechanism of getting the text in xpath is not the same as - // webdriver, use the expensive getText() to check if nothing is matched. - if ($text !== $element->findElement(WebDriverBy::xpath($xpath))->getText()) { - continue; - } - } catch (NoSuchElementException $e) { - continue; - } - } - - $select ? $this->selectOption($element) : $this->deselectOption($element); - if (!$this->isMultiple()) { - return; - } - } - } - - /** - * Gets checkboxes or radio buttons with the same name. - * - * @param string|null $value - * @return WebDriverElement[] - */ - protected function getRelatedElements($value = null) - { - $valueSelector = $value ? sprintf(' and @value = %s', XPathEscaper::escapeQuotes($value)) : ''; - $formId = $this->element->getAttribute('form'); - if ($formId === null) { - $form = $this->element->findElement(WebDriverBy::xpath('ancestor::form')); - - $formId = $form->getAttribute('id'); - if ($formId === '' || $formId === null) { - return $form->findElements(WebDriverBy::xpath( - sprintf('.//input[@name = %s%s]', XPathEscaper::escapeQuotes($this->name), $valueSelector) - )); - } - } - - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form - return $this->element->findElements( - WebDriverBy::xpath(sprintf( - '//form[@id = %1$s]//input[@name = %2$s%3$s' - . ' and ((boolean(@form) = true() and @form = %1$s) or boolean(@form) = false())]' - . ' | //input[@form = %1$s and @name = %2$s%3$s]', - XPathEscaper::escapeQuotes($formId), - XPathEscaper::escapeQuotes($this->name), - $valueSelector - )) - ); - } - - /** - * Selects a checkbox or a radio button. - */ - protected function selectOption(WebDriverElement $element) - { - if (!$element->isSelected()) { - $element->click(); - } - } - - /** - * Deselects a checkbox or a radio button. - */ - protected function deselectOption(WebDriverElement $element) - { - if ($element->isSelected()) { - $element->click(); - } - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDevToolsDriver.php b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDevToolsDriver.php deleted file mode 100644 index 2d95d27..0000000 --- a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDevToolsDriver.php +++ /dev/null @@ -1,46 +0,0 @@ - 'POST', - 'url' => '/session/:sessionId/goog/cdp/execute', - ]; - - /** - * @var RemoteWebDriver - */ - private $driver; - - public function __construct(RemoteWebDriver $driver) - { - $this->driver = $driver; - } - - /** - * Executes a Chrome DevTools command - * - * @param string $command The DevTools command to execute - * @param array $parameters Optional parameters to the command - * @return array The result of the command - */ - public function execute($command, array $parameters = []) - { - $params = ['cmd' => $command, 'params' => (object) $parameters]; - - return $this->driver->executeCustomCommand( - self::SEND_COMMAND['url'], - self::SEND_COMMAND['method'], - $params - ); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriver.php b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriver.php deleted file mode 100644 index e947a49..0000000 --- a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriver.php +++ /dev/null @@ -1,107 +0,0 @@ - [ - 'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()], - ], - 'desiredCapabilities' => (object) $capabilities->toArray(), - ] - ); - - $response = $executor->execute($newSessionCommand); - - /* - * TODO: in next major version we may not need to use this method, because without OSS compatibility the - * driver creation is straightforward. - */ - return static::createFromResponse($response, $executor); - } - - /** - * @todo Remove in next major version. The class is internally no longer used and is kept only to keep BC. - * @deprecated Use start or startUsingDriverService method instead. - * @codeCoverageIgnore - * @internal - */ - public function startSession(DesiredCapabilities $desired_capabilities) - { - $command = WebDriverCommand::newSession( - [ - 'capabilities' => [ - 'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()], - ], - 'desiredCapabilities' => (object) $desired_capabilities->toArray(), - ] - ); - $response = $this->executor->execute($command); - $value = $response->getValue(); - - if (!$this->isW3cCompliant = isset($value['capabilities'])) { - $this->executor->disableW3cCompliance(); - } - - $this->sessionID = $response->getSessionID(); - } - - /** - * @return ChromeDevToolsDriver - */ - public function getDevTools() - { - if ($this->devTools === null) { - $this->devTools = new ChromeDevToolsDriver($this); - } - - return $this->devTools; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriverService.php b/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriverService.php deleted file mode 100644 index 902a48d..0000000 --- a/vendor/php-webdriver/webdriver/lib/Chrome/ChromeDriverService.php +++ /dev/null @@ -1,37 +0,0 @@ -toArray(); - } - - /** - * Sets the path of the Chrome executable. The path should be either absolute - * or relative to the location running ChromeDriver server. - * - * @param string $path - * @return ChromeOptions - */ - public function setBinary($path) - { - $this->binary = $path; - - return $this; - } - - /** - * @return ChromeOptions - */ - public function addArguments(array $arguments) - { - $this->arguments = array_merge($this->arguments, $arguments); - - return $this; - } - - /** - * Add a Chrome extension to install on browser startup. Each path should be - * a packed Chrome extension. - * - * @return ChromeOptions - */ - public function addExtensions(array $paths) - { - foreach ($paths as $path) { - $this->addExtension($path); - } - - return $this; - } - - /** - * @param array $encoded_extensions An array of base64 encoded of the extensions. - * @return ChromeOptions - */ - public function addEncodedExtensions(array $encoded_extensions) - { - foreach ($encoded_extensions as $encoded_extension) { - $this->addEncodedExtension($encoded_extension); - } - - return $this; - } - - /** - * Sets an experimental option which has not exposed officially. - * - * When using "prefs" to set Chrome preferences, please be aware they are so far not supported by - * Chrome running in headless mode, see https://bugs.chromium.org/p/chromium/issues/detail?id=775911 - * - * @param string $name - * @param mixed $value - * @return ChromeOptions - */ - public function setExperimentalOption($name, $value) - { - $this->experimentalOptions[$name] = $value; - - return $this; - } - - /** - * @return DesiredCapabilities The DesiredCapabilities for Chrome with this options. - */ - public function toCapabilities() - { - $capabilities = DesiredCapabilities::chrome(); - $capabilities->setCapability(self::CAPABILITY, $this); - - return $capabilities; - } - - /** - * @return \ArrayObject|array - */ - public function toArray() - { - // The selenium server expects a 'dictionary' instead of a 'list' when - // reading the chrome option. However, an empty array in PHP will be - // converted to a 'list' instead of a 'dictionary'. To fix it, we work - // with `ArrayObject` - $options = new \ArrayObject($this->experimentalOptions); - - if (!empty($this->binary)) { - $options['binary'] = $this->binary; - } - - if (!empty($this->arguments)) { - $options['args'] = $this->arguments; - } - - if (!empty($this->extensions)) { - $options['extensions'] = $this->extensions; - } - - return $options; - } - - /** - * Add a Chrome extension to install on browser startup. Each path should be a - * packed Chrome extension. - * - * @param string $path - * @return ChromeOptions - */ - private function addExtension($path) - { - $this->addEncodedExtension(base64_encode(file_get_contents($path))); - - return $this; - } - - /** - * @param string $encoded_extension Base64 encoded of the extension. - * @return ChromeOptions - */ - private function addEncodedExtension($encoded_extension) - { - $this->extensions[] = $encoded_extension; - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Cookie.php b/vendor/php-webdriver/webdriver/lib/Cookie.php deleted file mode 100644 index 2ae257b..0000000 --- a/vendor/php-webdriver/webdriver/lib/Cookie.php +++ /dev/null @@ -1,278 +0,0 @@ -validateCookieName($name); - $this->validateCookieValue($value); - - $this->cookie['name'] = $name; - $this->cookie['value'] = $value; - } - - /** - * @param array $cookieArray The cookie fields; must contain name and value. - * @return Cookie - */ - public static function createFromArray(array $cookieArray) - { - if (!isset($cookieArray['name'])) { - throw LogicException::forError('Cookie name should be set'); - } - if (!isset($cookieArray['value'])) { - throw LogicException::forError('Cookie value should be set'); - } - $cookie = new self($cookieArray['name'], $cookieArray['value']); - - if (isset($cookieArray['path'])) { - $cookie->setPath($cookieArray['path']); - } - if (isset($cookieArray['domain'])) { - $cookie->setDomain($cookieArray['domain']); - } - if (isset($cookieArray['expiry'])) { - $cookie->setExpiry($cookieArray['expiry']); - } - if (isset($cookieArray['secure'])) { - $cookie->setSecure($cookieArray['secure']); - } - if (isset($cookieArray['httpOnly'])) { - $cookie->setHttpOnly($cookieArray['httpOnly']); - } - if (isset($cookieArray['sameSite'])) { - $cookie->setSameSite($cookieArray['sameSite']); - } - - return $cookie; - } - - /** - * @return string - */ - public function getName() - { - return $this->offsetGet('name'); - } - - /** - * @return string - */ - public function getValue() - { - return $this->offsetGet('value'); - } - - /** - * The path the cookie is visible to. Defaults to "/" if omitted. - * - * @param string $path - */ - public function setPath($path) - { - $this->offsetSet('path', $path); - } - - /** - * @return string|null - */ - public function getPath() - { - return $this->offsetGet('path'); - } - - /** - * The domain the cookie is visible to. Defaults to the current browsing context's document's URL domain if omitted. - * - * @param string $domain - */ - public function setDomain($domain) - { - if (mb_strpos($domain, ':') !== false) { - throw LogicException::forError(sprintf('Cookie domain "%s" should not contain a port', $domain)); - } - - $this->offsetSet('domain', $domain); - } - - /** - * @return string|null - */ - public function getDomain() - { - return $this->offsetGet('domain'); - } - - /** - * The cookie's expiration date, specified in seconds since Unix Epoch. - * - * @param int $expiry - */ - public function setExpiry($expiry) - { - $this->offsetSet('expiry', (int) $expiry); - } - - /** - * @return int|null - */ - public function getExpiry() - { - return $this->offsetGet('expiry'); - } - - /** - * Whether this cookie requires a secure connection (https). Defaults to false if omitted. - * - * @param bool $secure - */ - public function setSecure($secure) - { - $this->offsetSet('secure', $secure); - } - - /** - * @return bool|null - */ - public function isSecure() - { - return $this->offsetGet('secure'); - } - - /** - * Whether the cookie is an HTTP only cookie. Defaults to false if omitted. - * - * @param bool $httpOnly - */ - public function setHttpOnly($httpOnly) - { - $this->offsetSet('httpOnly', $httpOnly); - } - - /** - * @return bool|null - */ - public function isHttpOnly() - { - return $this->offsetGet('httpOnly'); - } - - /** - * The cookie's same-site value. - * - * @param string $sameSite - */ - public function setSameSite($sameSite) - { - $this->offsetSet('sameSite', $sameSite); - } - - /** - * @return string|null - */ - public function getSameSite() - { - return $this->offsetGet('sameSite'); - } - - /** - * @return array - */ - public function toArray() - { - $cookie = $this->cookie; - if (!isset($cookie['secure'])) { - // Passing a boolean value for the "secure" flag is mandatory when using geckodriver - $cookie['secure'] = false; - } - - return $cookie; - } - - /** - * @param mixed $offset - * @return bool - */ - #[\ReturnTypeWillChange] - public function offsetExists($offset) - { - return isset($this->cookie[$offset]); - } - - /** - * @param mixed $offset - * @return mixed - */ - #[\ReturnTypeWillChange] - public function offsetGet($offset) - { - return $this->offsetExists($offset) ? $this->cookie[$offset] : null; - } - - /** - * @param mixed $offset - * @param mixed $value - * @return void - */ - #[\ReturnTypeWillChange] - public function offsetSet($offset, $value) - { - if ($value === null) { - unset($this->cookie[$offset]); - } else { - $this->cookie[$offset] = $value; - } - } - - /** - * @param mixed $offset - * @return void - */ - #[\ReturnTypeWillChange] - public function offsetUnset($offset) - { - unset($this->cookie[$offset]); - } - - /** - * @param string $name - */ - protected function validateCookieName($name) - { - if ($name === null || $name === '') { - throw LogicException::forError('Cookie name should be non-empty'); - } - - if (mb_strpos($name, ';') !== false) { - throw LogicException::forError('Cookie name should not contain a ";"'); - } - } - - /** - * @param string $value - */ - protected function validateCookieValue($value) - { - if ($value === null) { - throw LogicException::forError('Cookie value is required when setting a cookie'); - } - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Exception/DetachedShadowRootException.php b/vendor/php-webdriver/webdriver/lib/Exception/DetachedShadowRootException.php deleted file mode 100644 index b9bfb39..0000000 --- a/vendor/php-webdriver/webdriver/lib/Exception/DetachedShadowRootException.php +++ /dev/null @@ -1,10 +0,0 @@ -getCommandLine(), - $process->getErrorOutput() - ) - ); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Exception/Internal/UnexpectedResponseException.php b/vendor/php-webdriver/webdriver/lib/Exception/Internal/UnexpectedResponseException.php deleted file mode 100644 index 18cdc88..0000000 --- a/vendor/php-webdriver/webdriver/lib/Exception/Internal/UnexpectedResponseException.php +++ /dev/null @@ -1,51 +0,0 @@ -getMessage() - ) - ); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Exception/Internal/WebDriverCurlException.php b/vendor/php-webdriver/webdriver/lib/Exception/Internal/WebDriverCurlException.php deleted file mode 100644 index ac81f97..0000000 --- a/vendor/php-webdriver/webdriver/lib/Exception/Internal/WebDriverCurlException.php +++ /dev/null @@ -1,22 +0,0 @@ -results = $results; - } - - /** - * @return mixed - */ - public function getResults() - { - return $this->results; - } - - /** - * Throw WebDriverExceptions based on WebDriver status code. - * - * @param int|string $status_code - * @param string $message - * @param mixed $results - * - * @throws ElementClickInterceptedException - * @throws ElementNotInteractableException - * @throws ElementNotSelectableException - * @throws ElementNotVisibleException - * @throws ExpectedException - * @throws IMEEngineActivationFailedException - * @throws IMENotAvailableException - * @throws IndexOutOfBoundsException - * @throws InsecureCertificateException - * @throws InvalidArgumentException - * @throws InvalidCookieDomainException - * @throws InvalidCoordinatesException - * @throws InvalidElementStateException - * @throws InvalidSelectorException - * @throws InvalidSessionIdException - * @throws JavascriptErrorException - * @throws MoveTargetOutOfBoundsException - * @throws NoAlertOpenException - * @throws NoCollectionException - * @throws NoScriptResultException - * @throws NoStringException - * @throws NoStringLengthException - * @throws NoStringWrapperException - * @throws NoSuchAlertException - * @throws NoSuchCollectionException - * @throws NoSuchCookieException - * @throws NoSuchDocumentException - * @throws NoSuchDriverException - * @throws NoSuchElementException - * @throws NoSuchFrameException - * @throws NoSuchWindowException - * @throws NullPointerException - * @throws ScriptTimeoutException - * @throws SessionNotCreatedException - * @throws StaleElementReferenceException - * @throws TimeoutException - * @throws UnableToCaptureScreenException - * @throws UnableToSetCookieException - * @throws UnexpectedAlertOpenException - * @throws UnexpectedJavascriptException - * @throws UnknownCommandException - * @throws UnknownErrorException - * @throws UnknownMethodException - * @throws UnknownServerException - * @throws UnrecognizedExceptionException - * @throws UnsupportedOperationException - * @throws XPathLookupException - */ - public static function throwException($status_code, $message, $results) - { - if (is_string($status_code)) { - // @see https://w3c.github.io/webdriver/#errors - switch ($status_code) { - case 'element click intercepted': - throw new ElementClickInterceptedException($message, $results); - case 'element not interactable': - throw new ElementNotInteractableException($message, $results); - case 'insecure certificate': - throw new InsecureCertificateException($message, $results); - case 'invalid argument': - throw new InvalidArgumentException($message, $results); - case 'invalid cookie domain': - throw new InvalidCookieDomainException($message, $results); - case 'invalid element state': - throw new InvalidElementStateException($message, $results); - case 'invalid selector': - throw new InvalidSelectorException($message, $results); - case 'invalid session id': - throw new InvalidSessionIdException($message, $results); - case 'javascript error': - throw new JavascriptErrorException($message, $results); - case 'move target out of bounds': - throw new MoveTargetOutOfBoundsException($message, $results); - case 'no such alert': - throw new NoSuchAlertException($message, $results); - case 'no such cookie': - throw new NoSuchCookieException($message, $results); - case 'no such element': - throw new NoSuchElementException($message, $results); - case 'no such frame': - throw new NoSuchFrameException($message, $results); - case 'no such window': - throw new NoSuchWindowException($message, $results); - case 'no such shadow root': - throw new NoSuchShadowRootException($message, $results); - case 'script timeout': - throw new ScriptTimeoutException($message, $results); - case 'session not created': - throw new SessionNotCreatedException($message, $results); - case 'stale element reference': - throw new StaleElementReferenceException($message, $results); - case 'detached shadow root': - throw new DetachedShadowRootException($message, $results); - case 'timeout': - throw new TimeoutException($message, $results); - case 'unable to set cookie': - throw new UnableToSetCookieException($message, $results); - case 'unable to capture screen': - throw new UnableToCaptureScreenException($message, $results); - case 'unexpected alert open': - throw new UnexpectedAlertOpenException($message, $results); - case 'unknown command': - throw new UnknownCommandException($message, $results); - case 'unknown error': - throw new UnknownErrorException($message, $results); - case 'unknown method': - throw new UnknownMethodException($message, $results); - case 'unsupported operation': - throw new UnsupportedOperationException($message, $results); - default: - throw new UnrecognizedExceptionException($message, $results); - } - } - - switch ($status_code) { - case 1: - throw new IndexOutOfBoundsException($message, $results); - case 2: - throw new NoCollectionException($message, $results); - case 3: - throw new NoStringException($message, $results); - case 4: - throw new NoStringLengthException($message, $results); - case 5: - throw new NoStringWrapperException($message, $results); - case 6: - throw new NoSuchDriverException($message, $results); - case 7: - throw new NoSuchElementException($message, $results); - case 8: - throw new NoSuchFrameException($message, $results); - case 9: - throw new UnknownCommandException($message, $results); - case 10: - throw new StaleElementReferenceException($message, $results); - case 11: - throw new ElementNotVisibleException($message, $results); - case 12: - throw new InvalidElementStateException($message, $results); - case 13: - throw new UnknownServerException($message, $results); - case 14: - throw new ExpectedException($message, $results); - case 15: - throw new ElementNotSelectableException($message, $results); - case 16: - throw new NoSuchDocumentException($message, $results); - case 17: - throw new UnexpectedJavascriptException($message, $results); - case 18: - throw new NoScriptResultException($message, $results); - case 19: - throw new XPathLookupException($message, $results); - case 20: - throw new NoSuchCollectionException($message, $results); - case 21: - throw new TimeoutException($message, $results); - case 22: - throw new NullPointerException($message, $results); - case 23: - throw new NoSuchWindowException($message, $results); - case 24: - throw new InvalidCookieDomainException($message, $results); - case 25: - throw new UnableToSetCookieException($message, $results); - case 26: - throw new UnexpectedAlertOpenException($message, $results); - case 27: - throw new NoAlertOpenException($message, $results); - case 28: - throw new ScriptTimeoutException($message, $results); - case 29: - throw new InvalidCoordinatesException($message, $results); - case 30: - throw new IMENotAvailableException($message, $results); - case 31: - throw new IMEEngineActivationFailedException($message, $results); - case 32: - throw new InvalidSelectorException($message, $results); - case 33: - throw new SessionNotCreatedException($message, $results); - case 34: - throw new MoveTargetOutOfBoundsException($message, $results); - default: - throw new UnrecognizedExceptionException($message, $results); - } - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Exception/XPathLookupException.php b/vendor/php-webdriver/webdriver/lib/Exception/XPathLookupException.php deleted file mode 100644 index 86513db..0000000 --- a/vendor/php-webdriver/webdriver/lib/Exception/XPathLookupException.php +++ /dev/null @@ -1,10 +0,0 @@ -setProfile($profile->encode()); - * $capabilities = DesiredCapabilities::firefox(); - * $capabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); - */ - public const PROFILE = 'firefox_profile'; - - /** - * Creates a new FirefoxDriver using default configuration. - * This includes starting a new geckodriver process each time this method is called. However this may be - * unnecessary overhead - instead, you can start the process once using FirefoxDriverService and pass - * this instance to startUsingDriverService() method. - * - * @return static - */ - public static function start(?DesiredCapabilities $capabilities = null) - { - $service = FirefoxDriverService::createDefaultService(); - - return static::startUsingDriverService($service, $capabilities); - } - - /** - * Creates a new FirefoxDriver using given FirefoxDriverService. - * This is usable when you for example don't want to start new geckodriver process for each individual test - * and want to reuse the already started geckodriver, which will lower the overhead associated with spinning up - * a new process. - * - * @return static - */ - public static function startUsingDriverService( - FirefoxDriverService $service, - ?DesiredCapabilities $capabilities = null - ) { - if ($capabilities === null) { - $capabilities = DesiredCapabilities::firefox(); - } - - $executor = new DriverCommandExecutor($service); - $newSessionCommand = WebDriverCommand::newSession( - [ - 'capabilities' => [ - 'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()], - ], - ] - ); - - $response = $executor->execute($newSessionCommand); - - $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($response->getValue()['capabilities']); - $sessionId = $response->getSessionID(); - - return new static($executor, $sessionId, $returnedCapabilities, true); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriverService.php b/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriverService.php deleted file mode 100644 index 83c6a28..0000000 --- a/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxDriverService.php +++ /dev/null @@ -1,34 +0,0 @@ -setPreference(FirefoxPreferences::READER_PARSE_ON_LOAD_ENABLED, false); - // disable JSON viewer and let JSON be rendered as raw data - $this->setPreference(FirefoxPreferences::DEVTOOLS_JSONVIEW, false); - } - - /** - * Directly set firefoxOptions. - * Use `addArguments` to add command line arguments and `setPreference` to set Firefox about:config entry. - * - * @param string $name - * @param mixed $value - * @return self - */ - public function setOption($name, $value) - { - if ($name === self::OPTION_PREFS) { - throw LogicException::forError('Use setPreference() method to set Firefox preferences'); - } - if ($name === self::OPTION_ARGS) { - throw LogicException::forError('Use addArguments() method to add Firefox arguments'); - } - if ($name === self::OPTION_PROFILE) { - throw LogicException::forError('Use setProfile() method to set Firefox profile'); - } - - $this->options[$name] = $value; - - return $this; - } - - /** - * Command line arguments to pass to the Firefox binary. - * These must include the leading dash (-) where required, e.g. ['-headless']. - * - * @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#args - * @param string[] $arguments - * @return self - */ - public function addArguments(array $arguments) - { - $this->arguments = array_merge($this->arguments, $arguments); - - return $this; - } - - /** - * Set Firefox preference (about:config entry). - * - * @see http://kb.mozillazine.org/About:config_entries - * @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#prefs - * @param string $name - * @param string|bool|int $value - * @return self - */ - public function setPreference($name, $value) - { - $this->preferences[$name] = $value; - - return $this; - } - - /** - * @see https://github.com/php-webdriver/php-webdriver/wiki/Firefox#firefox-profile - * @return self - */ - public function setProfile(FirefoxProfile $profile) - { - $this->profile = $profile; - - return $this; - } - - /** - * @return array - */ - public function toArray() - { - $array = $this->options; - if (!empty($this->arguments)) { - $array[self::OPTION_ARGS] = $this->arguments; - } - if (!empty($this->preferences)) { - $array[self::OPTION_PREFS] = $this->preferences; - } - if (!empty($this->profile)) { - $array[self::OPTION_PROFILE] = $this->profile->encode(); - } - - return $array; - } - - #[ReturnTypeWillChange] - public function jsonSerialize() - { - return new \ArrayObject($this->toArray()); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxPreferences.php b/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxPreferences.php deleted file mode 100644 index 2a33fb0..0000000 --- a/vendor/php-webdriver/webdriver/lib/Firefox/FirefoxPreferences.php +++ /dev/null @@ -1,25 +0,0 @@ -extensions[] = $extension; - - return $this; - } - - /** - * @param string $extension_datas The path to the folder containing the datas to add to the extension - * @return FirefoxProfile - */ - public function addExtensionDatas($extension_datas) - { - if (!is_dir($extension_datas)) { - return null; - } - - $this->extensions_datas[basename($extension_datas)] = $extension_datas; - - return $this; - } - - /** - * @param string $rdf_file The path to the rdf file - * @return FirefoxProfile - */ - public function setRdfFile($rdf_file) - { - if (!is_file($rdf_file)) { - return null; - } - - $this->rdf_file = $rdf_file; - - return $this; - } - - /** - * @param string $key - * @param string|bool|int $value - * @throws LogicException - * @return FirefoxProfile - */ - public function setPreference($key, $value) - { - if (is_string($value)) { - $value = sprintf('"%s"', $value); - } else { - if (is_int($value)) { - $value = sprintf('%d', $value); - } else { - if (is_bool($value)) { - $value = $value ? 'true' : 'false'; - } else { - throw LogicException::forError( - 'The value of the preference should be either a string, int or bool.' - ); - } - } - } - $this->preferences[$key] = $value; - - return $this; - } - - /** - * @param mixed $key - * @return mixed - */ - public function getPreference($key) - { - if (array_key_exists($key, $this->preferences)) { - return $this->preferences[$key]; - } - - return null; - } - - /** - * @return string - */ - public function encode() - { - $temp_dir = $this->createTempDirectory('WebDriverFirefoxProfile'); - - if (isset($this->rdf_file)) { - copy($this->rdf_file, $temp_dir . DIRECTORY_SEPARATOR . 'mimeTypes.rdf'); - } - - foreach ($this->extensions as $extension) { - $this->installExtension($extension, $temp_dir); - } - - foreach ($this->extensions_datas as $dirname => $extension_datas) { - mkdir($temp_dir . DIRECTORY_SEPARATOR . $dirname); - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($extension_datas, RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::SELF_FIRST - ); - foreach ($iterator as $item) { - $target_dir = $temp_dir . DIRECTORY_SEPARATOR . $dirname . DIRECTORY_SEPARATOR - . $iterator->getSubPathName(); - - if ($item->isDir()) { - mkdir($target_dir); - } else { - copy($item, $target_dir); - } - } - } - - $content = ''; - foreach ($this->preferences as $key => $value) { - $content .= sprintf("user_pref(\"%s\", %s);\n", $key, $value); - } - file_put_contents($temp_dir . '/user.js', $content); - - // Intentionally do not use `tempnam()`, as it creates empty file which zip extension may not handle. - $temp_zip = sys_get_temp_dir() . '/' . uniqid('WebDriverFirefoxProfileZip', false); - - $zip = new ZipArchive(); - $zip->open($temp_zip, ZipArchive::CREATE); - - $dir = new RecursiveDirectoryIterator($temp_dir); - $files = new RecursiveIteratorIterator($dir); - - $dir_prefix = preg_replace( - '#\\\\#', - '\\\\\\\\', - $temp_dir . DIRECTORY_SEPARATOR - ); - - foreach ($files as $name => $object) { - if (is_dir($name)) { - continue; - } - - $path = preg_replace("#^{$dir_prefix}#", '', $name); - $zip->addFile($name, $path); - } - $zip->close(); - - $profile = base64_encode(file_get_contents($temp_zip)); - - // clean up - $this->deleteDirectory($temp_dir); - unlink($temp_zip); - - return $profile; - } - - /** - * @param string $extension The path to the extension. - * @param string $profileDir The path to the profile directory. - * @throws IOException - */ - private function installExtension($extension, $profileDir) - { - $extensionCommonName = $this->parseExtensionName($extension); - - // install extension to profile directory - $extensionDir = $profileDir . '/extensions/'; - if (!is_dir($extensionDir) && !mkdir($extensionDir, 0777, true) && !is_dir($extensionDir)) { - throw IOException::forFileError( - 'Cannot install Firefox extension - cannot create directory', - $extensionDir - ); - } - - if (!copy($extension, $extensionDir . $extensionCommonName . '.xpi')) { - throw IOException::forFileError( - 'Cannot install Firefox extension - cannot copy file', - $extension - ); - } - } - - /** - * @param string $prefix Prefix of the temp directory. - * - * @throws IOException - * @return string The path to the temp directory created. - */ - private function createTempDirectory($prefix = '') - { - $temp_dir = tempnam(sys_get_temp_dir(), $prefix); - if (file_exists($temp_dir)) { - unlink($temp_dir); - mkdir($temp_dir); - if (!is_dir($temp_dir)) { - throw IOException::forFileError( - 'Cannot install Firefox extension - cannot create directory', - $temp_dir - ); - } - } - - return $temp_dir; - } - - /** - * @param string $directory The path to the directory. - */ - private function deleteDirectory($directory) - { - $dir = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS); - $paths = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST); - - foreach ($paths as $path) { - if ($path->isDir() && !$path->isLink()) { - rmdir($path->getPathname()); - } else { - unlink($path->getPathname()); - } - } - - rmdir($directory); - } - - /** - * @param string $xpi The path to the .xpi extension. - * @param string $target_dir The path to the unzip directory. - * - * @throws IOException - * @return FirefoxProfile - */ - private function extractTo($xpi, $target_dir) - { - $zip = new ZipArchive(); - if (file_exists($xpi)) { - if ($zip->open($xpi)) { - $zip->extractTo($target_dir); - $zip->close(); - } else { - throw IOException::forFileError('Failed to open the firefox extension.', $xpi); - } - } else { - throw IOException::forFileError('Firefox extension doesn\'t exist.', $xpi); - } - - return $this; - } - - private function parseExtensionName($extensionPath) - { - $temp_dir = $this->createTempDirectory(); - - $this->extractTo($extensionPath, $temp_dir); - - $mozillaRsaPath = $temp_dir . '/META-INF/mozilla.rsa'; - $mozillaRsaBinaryData = file_get_contents($mozillaRsaPath); - $mozillaRsaHex = bin2hex($mozillaRsaBinaryData); - - //We need to find the plugin id. This is the second occurrence of object identifier "2.5.4.3 commonName". - - //That is marker "2.5.4.3 commonName" in hex: - $objectIdentifierHexMarker = '0603550403'; - - $firstMarkerPosInHex = strpos($mozillaRsaHex, $objectIdentifierHexMarker); // phpcs:ignore - - $secondMarkerPosInHexString = - strpos($mozillaRsaHex, $objectIdentifierHexMarker, $firstMarkerPosInHex + 2); // phpcs:ignore - - if ($secondMarkerPosInHexString === false) { - throw RuntimeException::forError('Cannot install extension. Cannot fetch extension commonName'); - } - - // phpcs:ignore - $commonNameStringPositionInBinary = ($secondMarkerPosInHexString + strlen($objectIdentifierHexMarker)) / 2; - - $commonNameStringLength = ord($mozillaRsaBinaryData[$commonNameStringPositionInBinary + 1]); - // phpcs:ignore - $extensionCommonName = substr( - $mozillaRsaBinaryData, - $commonNameStringPositionInBinary + 2, - $commonNameStringLength - ); - - $this->deleteDirectory($temp_dir); - - return $extensionCommonName; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php deleted file mode 100644 index 91cad24..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php +++ /dev/null @@ -1,16 +0,0 @@ -mouse->mouseUp($this->getActionLocation()); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAction.php deleted file mode 100644 index e21b883..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAction.php +++ /dev/null @@ -1,13 +0,0 @@ -mouse->click($this->getActionLocation()); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php deleted file mode 100644 index 5f5042c..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php +++ /dev/null @@ -1,16 +0,0 @@ -mouse->mouseDown($this->getActionLocation()); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php deleted file mode 100644 index 493978b..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php +++ /dev/null @@ -1,16 +0,0 @@ -mouse->contextClick($this->getActionLocation()); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverCoordinates.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverCoordinates.php deleted file mode 100644 index 3a92f0a..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverCoordinates.php +++ /dev/null @@ -1,77 +0,0 @@ -onScreen = $on_screen; - $this->inViewPort = $in_view_port; - $this->onPage = $on_page; - $this->auxiliary = $auxiliary; - } - - /** - * @throws UnsupportedOperationException - * @return WebDriverPoint - */ - public function onScreen() - { - throw new UnsupportedOperationException( - 'onScreen is planned but not yet supported by Selenium' - ); - } - - /** - * @return WebDriverPoint - */ - public function inViewPort() - { - return call_user_func($this->inViewPort); - } - - /** - * @return WebDriverPoint - */ - public function onPage() - { - return call_user_func($this->onPage); - } - - /** - * @return string The attached object id. - */ - public function getAuxiliary() - { - return $this->auxiliary; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php deleted file mode 100644 index 386c496..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php +++ /dev/null @@ -1,13 +0,0 @@ -mouse->doubleClick($this->getActionLocation()); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php deleted file mode 100644 index 415ebe7..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php +++ /dev/null @@ -1,12 +0,0 @@ -focusOnElement(); - $this->keyboard->pressKey($this->key); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php deleted file mode 100644 index 0cdb3a8..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php +++ /dev/null @@ -1,12 +0,0 @@ -focusOnElement(); - $this->keyboard->releaseKey($this->key); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php deleted file mode 100644 index a5ba087..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php +++ /dev/null @@ -1,43 +0,0 @@ -keyboard = $keyboard; - $this->mouse = $mouse; - $this->locationProvider = $location_provider; - } - - protected function focusOnElement() - { - if ($this->locationProvider) { - $this->mouse->click($this->locationProvider->getCoordinates()); - } - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php deleted file mode 100644 index ecb1127..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php +++ /dev/null @@ -1,44 +0,0 @@ -mouse = $mouse; - $this->locationProvider = $location_provider; - } - - /** - * @return null|WebDriverCoordinates - */ - protected function getActionLocation() - { - if ($this->locationProvider !== null) { - return $this->locationProvider->getCoordinates(); - } - - return null; - } - - protected function moveToLocation() - { - $this->mouse->mouseMove($this->locationProvider); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php deleted file mode 100644 index 1969f01..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php +++ /dev/null @@ -1,13 +0,0 @@ -mouse->mouseMove($this->getActionLocation()); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php deleted file mode 100644 index c865da4..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php +++ /dev/null @@ -1,43 +0,0 @@ -xOffset = $x_offset; - $this->yOffset = $y_offset; - } - - public function perform() - { - $this->mouse->mouseMove( - $this->getActionLocation(), - $this->xOffset, - $this->yOffset - ); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php deleted file mode 100644 index 2ed3cfd..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php +++ /dev/null @@ -1,35 +0,0 @@ -keys = $keys; - } - - public function perform() - { - $this->focusOnElement(); - $this->keyboard->sendKeys($this->keys); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.php deleted file mode 100644 index 6efe938..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.php +++ /dev/null @@ -1,54 +0,0 @@ -key = $key; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php deleted file mode 100644 index 25a1761..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php +++ /dev/null @@ -1,13 +0,0 @@ -touchScreen->doubleTap($this->locationProvider); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDownAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDownAction.php deleted file mode 100644 index 225726f..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverDownAction.php +++ /dev/null @@ -1,33 +0,0 @@ -x = $x; - $this->y = $y; - parent::__construct($touch_screen); - } - - public function perform() - { - $this->touchScreen->down($this->x, $this->y); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php deleted file mode 100644 index acdb7fc..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php +++ /dev/null @@ -1,33 +0,0 @@ -x = $x; - $this->y = $y; - parent::__construct($touch_screen); - } - - public function perform() - { - $this->touchScreen->flick($this->x, $this->y); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php deleted file mode 100644 index 28d3597..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php +++ /dev/null @@ -1,50 +0,0 @@ -x = $x; - $this->y = $y; - $this->speed = $speed; - parent::__construct($touch_screen, $element); - } - - public function perform() - { - $this->touchScreen->flickFromElement( - $this->locationProvider, - $this->x, - $this->y, - $this->speed - ); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php deleted file mode 100644 index 7c1a165..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php +++ /dev/null @@ -1,13 +0,0 @@ -touchScreen->longPress($this->locationProvider); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php deleted file mode 100644 index d0a5f85..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php +++ /dev/null @@ -1,27 +0,0 @@ -x = $x; - $this->y = $y; - parent::__construct($touch_screen); - } - - public function perform() - { - $this->touchScreen->move($this->x, $this->y); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php deleted file mode 100644 index 952d57e..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php +++ /dev/null @@ -1,27 +0,0 @@ -x = $x; - $this->y = $y; - parent::__construct($touch_screen); - } - - public function perform() - { - $this->touchScreen->scroll($this->x, $this->y); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.php deleted file mode 100644 index 217564d..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.php +++ /dev/null @@ -1,36 +0,0 @@ -x = $x; - $this->y = $y; - parent::__construct($touch_screen, $element); - } - - public function perform() - { - $this->touchScreen->scrollFromElement( - $this->locationProvider, - $this->x, - $this->y - ); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTapAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTapAction.php deleted file mode 100644 index 63527e8..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTapAction.php +++ /dev/null @@ -1,13 +0,0 @@ -touchScreen->tap($this->locationProvider); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchAction.php deleted file mode 100644 index 10100ea..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchAction.php +++ /dev/null @@ -1,38 +0,0 @@ -touchScreen = $touch_screen; - $this->locationProvider = $location_provider; - } - - /** - * @return null|WebDriverCoordinates - */ - protected function getActionLocation() - { - return $this->locationProvider !== null - ? $this->locationProvider->getCoordinates() : null; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php b/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php deleted file mode 100644 index ff9f9c4..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php +++ /dev/null @@ -1,109 +0,0 @@ -driver = $driver; - $this->keyboard = $driver->getKeyboard(); - $this->mouse = $driver->getMouse(); - $this->action = new WebDriverCompositeAction(); - } - - /** - * A convenience method for performing the actions without calling build(). - */ - public function perform() - { - $this->action->perform(); - } - - /** - * Mouse click. - * If $element is provided, move to the middle of the element first. - * - * @return WebDriverActions - */ - public function click(?WebDriverElement $element = null) - { - $this->action->addAction( - new WebDriverClickAction($this->mouse, $element) - ); - - return $this; - } - - /** - * Mouse click and hold. - * If $element is provided, move to the middle of the element first. - * - * @return WebDriverActions - */ - public function clickAndHold(?WebDriverElement $element = null) - { - $this->action->addAction( - new WebDriverClickAndHoldAction($this->mouse, $element) - ); - - return $this; - } - - /** - * Context-click (right click). - * If $element is provided, move to the middle of the element first. - * - * @return WebDriverActions - */ - public function contextClick(?WebDriverElement $element = null) - { - $this->action->addAction( - new WebDriverContextClickAction($this->mouse, $element) - ); - - return $this; - } - - /** - * Double click. - * If $element is provided, move to the middle of the element first. - * - * @return WebDriverActions - */ - public function doubleClick(?WebDriverElement $element = null) - { - $this->action->addAction( - new WebDriverDoubleClickAction($this->mouse, $element) - ); - - return $this; - } - - /** - * Drag and drop from $source to $target. - * - * @return WebDriverActions - */ - public function dragAndDrop(WebDriverElement $source, WebDriverElement $target) - { - $this->action->addAction( - new WebDriverClickAndHoldAction($this->mouse, $source) - ); - $this->action->addAction( - new WebDriverMouseMoveAction($this->mouse, $target) - ); - $this->action->addAction( - new WebDriverButtonReleaseAction($this->mouse, $target) - ); - - return $this; - } - - /** - * Drag $source and drop by offset ($x_offset, $y_offset). - * - * @param int $x_offset - * @param int $y_offset - * @return WebDriverActions - */ - public function dragAndDropBy(WebDriverElement $source, $x_offset, $y_offset) - { - $this->action->addAction( - new WebDriverClickAndHoldAction($this->mouse, $source) - ); - $this->action->addAction( - new WebDriverMoveToOffsetAction($this->mouse, null, $x_offset, $y_offset) - ); - $this->action->addAction( - new WebDriverButtonReleaseAction($this->mouse, null) - ); - - return $this; - } - - /** - * Mouse move by offset. - * - * @param int $x_offset - * @param int $y_offset - * @return WebDriverActions - */ - public function moveByOffset($x_offset, $y_offset) - { - $this->action->addAction( - new WebDriverMoveToOffsetAction($this->mouse, null, $x_offset, $y_offset) - ); - - return $this; - } - - /** - * Move to the middle of the given WebDriverElement. - * Extra shift, calculated from the top-left corner of the element, can be set by passing $x_offset and $y_offset - * parameters. - * - * @param int $x_offset - * @param int $y_offset - * @return WebDriverActions - */ - public function moveToElement(WebDriverElement $element, $x_offset = null, $y_offset = null) - { - $this->action->addAction(new WebDriverMoveToOffsetAction( - $this->mouse, - $element, - $x_offset, - $y_offset - )); - - return $this; - } - - /** - * Release the mouse button. - * If $element is provided, move to the middle of the element first. - * - * @return WebDriverActions - */ - public function release(?WebDriverElement $element = null) - { - $this->action->addAction( - new WebDriverButtonReleaseAction($this->mouse, $element) - ); - - return $this; - } - - /** - * Press a key on keyboard. - * If $element is provided, focus on that element first. - * - * @see WebDriverKeys for special keys like CONTROL, ALT, etc. - * @param string $key - * @return WebDriverActions - */ - public function keyDown(?WebDriverElement $element = null, $key = null) - { - $this->action->addAction( - new WebDriverKeyDownAction($this->keyboard, $this->mouse, $element, $key) - ); - - return $this; - } - - /** - * Release a key on keyboard. - * If $element is provided, focus on that element first. - * - * @see WebDriverKeys for special keys like CONTROL, ALT, etc. - * @param string $key - * @return WebDriverActions - */ - public function keyUp(?WebDriverElement $element = null, $key = null) - { - $this->action->addAction( - new WebDriverKeyUpAction($this->keyboard, $this->mouse, $element, $key) - ); - - return $this; - } - - /** - * Send keys by keyboard. - * If $element is provided, focus on that element first (using single mouse click). - * - * @see WebDriverKeys for special keys like CONTROL, ALT, etc. - * @param string $keys - * @return WebDriverActions - */ - public function sendKeys(?WebDriverElement $element = null, $keys = null) - { - $this->action->addAction( - new WebDriverSendKeysAction( - $this->keyboard, - $this->mouse, - $element, - $keys - ) - ); - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverCompositeAction.php b/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverCompositeAction.php deleted file mode 100644 index 955d619..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverCompositeAction.php +++ /dev/null @@ -1,48 +0,0 @@ -actions[] = $action; - - return $this; - } - - /** - * Get the number of actions in the sequence. - * - * @return int The number of actions. - */ - public function getNumberOfActions() - { - return count($this->actions); - } - - /** - * Perform the sequence of actions. - */ - public function perform() - { - foreach ($this->actions as $action) { - $action->perform(); - } - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverTouchActions.php b/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverTouchActions.php deleted file mode 100644 index fd32984..0000000 --- a/vendor/php-webdriver/webdriver/lib/Interactions/WebDriverTouchActions.php +++ /dev/null @@ -1,175 +0,0 @@ -touchScreen = $driver->getTouch(); - } - - /** - * @return WebDriverTouchActions - */ - public function tap(WebDriverElement $element) - { - $this->action->addAction( - new WebDriverTapAction($this->touchScreen, $element) - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * @return WebDriverTouchActions - */ - public function down($x, $y) - { - $this->action->addAction( - new WebDriverDownAction($this->touchScreen, $x, $y) - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * @return WebDriverTouchActions - */ - public function up($x, $y) - { - $this->action->addAction( - new WebDriverUpAction($this->touchScreen, $x, $y) - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * @return WebDriverTouchActions - */ - public function move($x, $y) - { - $this->action->addAction( - new WebDriverMoveAction($this->touchScreen, $x, $y) - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * @return WebDriverTouchActions - */ - public function scroll($x, $y) - { - $this->action->addAction( - new WebDriverScrollAction($this->touchScreen, $x, $y) - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * @return WebDriverTouchActions - */ - public function scrollFromElement(WebDriverElement $element, $x, $y) - { - $this->action->addAction( - new WebDriverScrollFromElementAction($this->touchScreen, $element, $x, $y) - ); - - return $this; - } - - /** - * @return WebDriverTouchActions - */ - public function doubleTap(WebDriverElement $element) - { - $this->action->addAction( - new WebDriverDoubleTapAction($this->touchScreen, $element) - ); - - return $this; - } - - /** - * @return WebDriverTouchActions - */ - public function longPress(WebDriverElement $element) - { - $this->action->addAction( - new WebDriverLongPressAction($this->touchScreen, $element) - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * @return WebDriverTouchActions - */ - public function flick($x, $y) - { - $this->action->addAction( - new WebDriverFlickAction($this->touchScreen, $x, $y) - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * @param int $speed - * @return WebDriverTouchActions - */ - public function flickFromElement(WebDriverElement $element, $x, $y, $speed) - { - $this->action->addAction( - new WebDriverFlickFromElementAction( - $this->touchScreen, - $element, - $x, - $y, - $speed - ) - ); - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Internal/WebDriverLocatable.php b/vendor/php-webdriver/webdriver/lib/Internal/WebDriverLocatable.php deleted file mode 100644 index 225a10d..0000000 --- a/vendor/php-webdriver/webdriver/lib/Internal/WebDriverLocatable.php +++ /dev/null @@ -1,16 +0,0 @@ - microtime(true)) { - if ($this->getHTTPResponseCode($url) === 200) { - return $this; - } - usleep(self::POLL_INTERVAL_MS); - } - - throw new TimeoutException(sprintf( - 'Timed out waiting for %s to become available after %d ms.', - $url, - $timeout_in_ms - )); - } - - public function waitUntilUnavailable($timeout_in_ms, $url) - { - $end = microtime(true) + $timeout_in_ms / 1000; - - while ($end > microtime(true)) { - if ($this->getHTTPResponseCode($url) !== 200) { - return $this; - } - usleep(self::POLL_INTERVAL_MS); - } - - throw new TimeoutException(sprintf( - 'Timed out waiting for %s to become unavailable after %d ms.', - $url, - $timeout_in_ms - )); - } - - private function getHTTPResponseCode($url) - { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - // The PHP doc indicates that CURLOPT_CONNECTTIMEOUT_MS constant is added in cURL 7.16.2 - // available since PHP 5.2.3. - if (!defined('CURLOPT_CONNECTTIMEOUT_MS')) { - define('CURLOPT_CONNECTTIMEOUT_MS', 156); // default value for CURLOPT_CONNECTTIMEOUT_MS - } - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, self::CONNECT_TIMEOUT_MS); - - $code = null; - - try { - curl_exec($ch); - $info = curl_getinfo($ch); - $code = $info['http_code']; - } catch (Exception $e) { - } - curl_close($ch); - - return $code; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/CustomWebDriverCommand.php b/vendor/php-webdriver/webdriver/lib/Remote/CustomWebDriverCommand.php deleted file mode 100644 index cef2c95..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/CustomWebDriverCommand.php +++ /dev/null @@ -1,82 +0,0 @@ -setCustomRequestParameters($url, $method); - - parent::__construct($session_id, DriverCommand::CUSTOM_COMMAND, $parameters); - } - - /** - * @throws WebDriverException - * @return string - */ - public function getCustomUrl() - { - if ($this->customUrl === null) { - throw LogicException::forError('URL of custom command is not set'); - } - - return $this->customUrl; - } - - /** - * @throws WebDriverException - * @return string - */ - public function getCustomMethod() - { - if ($this->customMethod === null) { - throw LogicException::forError('Method of custom command is not set'); - } - - return $this->customMethod; - } - - /** - * @param string $custom_url - * @param string $custom_method - * @throws WebDriverException - */ - protected function setCustomRequestParameters($custom_url, $custom_method) - { - $allowedMethods = [static::METHOD_GET, static::METHOD_POST]; - if (!in_array($custom_method, $allowedMethods, true)) { - throw LogicException::forError( - sprintf( - 'Invalid custom method "%s", must be one of [%s]', - $custom_method, - implode(', ', $allowedMethods) - ) - ); - } - $this->customMethod = $custom_method; - - if (mb_strpos($custom_url, '/') !== 0) { - throw LogicException::forError( - sprintf('URL of custom command has to start with / but is "%s"', $custom_url) - ); - } - $this->customUrl = $custom_url; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/DesiredCapabilities.php b/vendor/php-webdriver/webdriver/lib/Remote/DesiredCapabilities.php deleted file mode 100644 index 88aa6b1..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/DesiredCapabilities.php +++ /dev/null @@ -1,428 +0,0 @@ - 'platformName', - WebDriverCapabilityType::VERSION => 'browserVersion', - WebDriverCapabilityType::ACCEPT_SSL_CERTS => 'acceptInsecureCerts', - ]; - - public function __construct(array $capabilities = []) - { - $this->capabilities = $capabilities; - } - - public static function createFromW3cCapabilities(array $capabilities = []) - { - $w3cToOss = array_flip(self::$ossToW3c); - - foreach ($w3cToOss as $w3cCapability => $ossCapability) { - // Copy W3C capabilities to OSS ones - if (array_key_exists($w3cCapability, $capabilities)) { - $capabilities[$ossCapability] = $capabilities[$w3cCapability]; - } - } - - return new self($capabilities); - } - - /** - * @return string The name of the browser. - */ - public function getBrowserName() - { - return $this->get(WebDriverCapabilityType::BROWSER_NAME, ''); - } - - /** - * @param string $browser_name - * @return DesiredCapabilities - */ - public function setBrowserName($browser_name) - { - $this->set(WebDriverCapabilityType::BROWSER_NAME, $browser_name); - - return $this; - } - - /** - * @return string The version of the browser. - */ - public function getVersion() - { - return $this->get(WebDriverCapabilityType::VERSION, ''); - } - - /** - * @param string $version - * @return DesiredCapabilities - */ - public function setVersion($version) - { - $this->set(WebDriverCapabilityType::VERSION, $version); - - return $this; - } - - /** - * @param string $name - * @return mixed The value of a capability. - */ - public function getCapability($name) - { - return $this->get($name); - } - - /** - * @param string $name - * @param mixed $value - * @return DesiredCapabilities - */ - public function setCapability($name, $value) - { - // When setting 'moz:firefoxOptions' from an array and not from instance of FirefoxOptions, we must merge - // it with default FirefoxOptions to keep previous behavior (where the default preferences were added - // using FirefoxProfile, thus not overwritten by adding 'moz:firefoxOptions') - // TODO: remove in next major version, once FirefoxOptions are only accepted as object instance and not as array - if ($name === FirefoxOptions::CAPABILITY && is_array($value)) { - $defaultOptions = (new FirefoxOptions())->toArray(); - $value = array_merge($defaultOptions, $value); - } - - $this->set($name, $value); - - return $this; - } - - /** - * @return string The name of the platform. - */ - public function getPlatform() - { - return $this->get(WebDriverCapabilityType::PLATFORM, ''); - } - - /** - * @param string $platform - * @return DesiredCapabilities - */ - public function setPlatform($platform) - { - $this->set(WebDriverCapabilityType::PLATFORM, $platform); - - return $this; - } - - /** - * @param string $capability_name - * @return bool Whether the value is not null and not false. - */ - public function is($capability_name) - { - return (bool) $this->get($capability_name); - } - - /** - * @todo Remove in next major release (BC) - * @deprecated All browsers are always JS enabled except HtmlUnit and it's not meaningful to disable JS execution. - * @return bool Whether javascript is enabled. - */ - public function isJavascriptEnabled() - { - return $this->get(WebDriverCapabilityType::JAVASCRIPT_ENABLED, false); - } - - /** - * This is a htmlUnit-only option. - * - * @param bool $enabled - * @throws UnsupportedOperationException - * @return DesiredCapabilities - * @see https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#read-write-capabilities - */ - public function setJavascriptEnabled($enabled) - { - $browser = $this->getBrowserName(); - if ($browser && $browser !== WebDriverBrowserType::HTMLUNIT) { - throw new UnsupportedOperationException( - 'isJavascriptEnabled() is a htmlunit-only option. ' . - 'See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#read-write-capabilities.' - ); - } - - $this->set(WebDriverCapabilityType::JAVASCRIPT_ENABLED, $enabled); - - return $this; - } - - /** - * @todo Remove side-effects - not change eg. ChromeOptions::CAPABILITY from instance of ChromeOptions to an array - * @return array - */ - public function toArray() - { - if (isset($this->capabilities[ChromeOptions::CAPABILITY]) && - $this->capabilities[ChromeOptions::CAPABILITY] instanceof ChromeOptions - ) { - $this->capabilities[ChromeOptions::CAPABILITY] = - $this->capabilities[ChromeOptions::CAPABILITY]->toArray(); - } - - if (isset($this->capabilities[FirefoxOptions::CAPABILITY]) && - $this->capabilities[FirefoxOptions::CAPABILITY] instanceof FirefoxOptions - ) { - $this->capabilities[FirefoxOptions::CAPABILITY] = - $this->capabilities[FirefoxOptions::CAPABILITY]->toArray(); - } - - if (isset($this->capabilities[FirefoxDriver::PROFILE]) && - $this->capabilities[FirefoxDriver::PROFILE] instanceof FirefoxProfile - ) { - $this->capabilities[FirefoxDriver::PROFILE] = - $this->capabilities[FirefoxDriver::PROFILE]->encode(); - } - - return $this->capabilities; - } - - /** - * @return array - */ - public function toW3cCompatibleArray() - { - $allowedW3cCapabilities = [ - 'browserName', - 'browserVersion', - 'platformName', - 'acceptInsecureCerts', - 'pageLoadStrategy', - 'proxy', - 'setWindowRect', - 'timeouts', - 'strictFileInteractability', - 'unhandledPromptBehavior', - ]; - - $ossCapabilities = $this->toArray(); - $w3cCapabilities = []; - - foreach ($ossCapabilities as $capabilityKey => $capabilityValue) { - // Copy already W3C compatible capabilities - if (in_array($capabilityKey, $allowedW3cCapabilities, true)) { - $w3cCapabilities[$capabilityKey] = $capabilityValue; - } - - // Convert capabilities with changed name - if (array_key_exists($capabilityKey, self::$ossToW3c)) { - if ($capabilityKey === WebDriverCapabilityType::PLATFORM) { - $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue); - - // Remove platformName if it is set to "any" - if ($w3cCapabilities[self::$ossToW3c[$capabilityKey]] === 'any') { - unset($w3cCapabilities[self::$ossToW3c[$capabilityKey]]); - } - } else { - $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = $capabilityValue; - } - } - - // Copy vendor extensions - if (mb_strpos($capabilityKey, ':') !== false) { - $w3cCapabilities[$capabilityKey] = $capabilityValue; - } - } - - // Convert ChromeOptions - if (array_key_exists(ChromeOptions::CAPABILITY, $ossCapabilities)) { - $w3cCapabilities[ChromeOptions::CAPABILITY] = $ossCapabilities[ChromeOptions::CAPABILITY]; - } - - // Convert Firefox profile - if (array_key_exists(FirefoxDriver::PROFILE, $ossCapabilities)) { - // Convert profile only if not already set in moz:firefoxOptions - if (!array_key_exists(FirefoxOptions::CAPABILITY, $ossCapabilities) - || !array_key_exists('profile', $ossCapabilities[FirefoxOptions::CAPABILITY])) { - $w3cCapabilities[FirefoxOptions::CAPABILITY]['profile'] = $ossCapabilities[FirefoxDriver::PROFILE]; - } - } - - return $w3cCapabilities; - } - - /** - * @return static - */ - public static function android() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::ANDROID, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANDROID, - ]); - } - - /** - * @return static - */ - public static function chrome() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::CHROME, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, - ]); - } - - /** - * @return static - */ - public static function firefox() - { - $caps = new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::FIREFOX, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, - ]); - - $caps->setCapability(FirefoxOptions::CAPABILITY, new FirefoxOptions()); // to add default options - - return $caps; - } - - /** - * @return static - */ - public static function htmlUnit() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::HTMLUNIT, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, - ]); - } - - /** - * @return static - */ - public static function htmlUnitWithJS() - { - $caps = new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::HTMLUNIT, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, - ]); - - return $caps->setJavascriptEnabled(true); - } - - /** - * @return static - */ - public static function internetExplorer() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IE, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::WINDOWS, - ]); - } - - /** - * @return static - */ - public static function microsoftEdge() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::MICROSOFT_EDGE, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::WINDOWS, - ]); - } - - /** - * @return static - */ - public static function iphone() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IPHONE, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::MAC, - ]); - } - - /** - * @return static - */ - public static function ipad() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IPAD, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::MAC, - ]); - } - - /** - * @return static - */ - public static function opera() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::OPERA, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, - ]); - } - - /** - * @return static - */ - public static function safari() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::SAFARI, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, - ]); - } - - /** - * @deprecated PhantomJS is no longer developed and its support will be removed in next major version. - * Use headless Chrome or Firefox instead. - * @return static - */ - public static function phantomjs() - { - return new static([ - WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::PHANTOMJS, - WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, - ]); - } - - /** - * @param string $key - * @param mixed $value - * @return DesiredCapabilities - */ - private function set($key, $value) - { - $this->capabilities[$key] = $value; - - return $this; - } - - /** - * @param string $key - * @param mixed $default - * @return mixed - */ - private function get($key, $default = null) - { - return $this->capabilities[$key] ?? $default; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/DriverCommand.php b/vendor/php-webdriver/webdriver/lib/Remote/DriverCommand.php deleted file mode 100644 index a3a230b..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/DriverCommand.php +++ /dev/null @@ -1,153 +0,0 @@ - ['method' => 'POST', 'url' => '/session/:sessionId/accept_alert'], - DriverCommand::ADD_COOKIE => ['method' => 'POST', 'url' => '/session/:sessionId/cookie'], - DriverCommand::CLEAR_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/clear'], - DriverCommand::CLICK_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/click'], - DriverCommand::CLOSE => ['method' => 'DELETE', 'url' => '/session/:sessionId/window'], - DriverCommand::DELETE_ALL_COOKIES => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie'], - DriverCommand::DELETE_COOKIE => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie/:name'], - DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/dismiss_alert'], - DriverCommand::ELEMENT_EQUALS => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/equals/:other'], - DriverCommand::FIND_CHILD_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/element'], - DriverCommand::FIND_CHILD_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/elements'], - DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute'], - DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute_async'], - DriverCommand::FIND_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element'], - DriverCommand::FIND_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/elements'], - DriverCommand::SWITCH_TO_FRAME => ['method' => 'POST', 'url' => '/session/:sessionId/frame'], - DriverCommand::SWITCH_TO_PARENT_FRAME => ['method' => 'POST', 'url' => '/session/:sessionId/frame/parent'], - DriverCommand::SWITCH_TO_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window'], - DriverCommand::GET => ['method' => 'POST', 'url' => '/session/:sessionId/url'], - DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/active'], - DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert_text'], - DriverCommand::GET_ALL_COOKIES => ['method' => 'GET', 'url' => '/session/:sessionId/cookie'], - DriverCommand::GET_NAMED_COOKIE => ['method' => 'GET', 'url' => '/session/:sessionId/cookie/:name'], - DriverCommand::GET_ALL_SESSIONS => ['method' => 'GET', 'url' => '/sessions'], - DriverCommand::GET_AVAILABLE_LOG_TYPES => ['method' => 'GET', 'url' => '/session/:sessionId/log/types'], - DriverCommand::GET_CURRENT_URL => ['method' => 'GET', 'url' => '/session/:sessionId/url'], - DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window_handle'], - DriverCommand::GET_ELEMENT_ATTRIBUTE => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/element/:id/attribute/:name', - ], - DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/element/:id/css/:propertyName', - ], - DriverCommand::GET_ELEMENT_LOCATION => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/element/:id/location', - ], - DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/element/:id/location_in_view', - ], - DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/size'], - DriverCommand::GET_ELEMENT_TAG_NAME => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/name'], - DriverCommand::GET_ELEMENT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/text'], - DriverCommand::GET_LOG => ['method' => 'POST', 'url' => '/session/:sessionId/log'], - DriverCommand::GET_PAGE_SOURCE => ['method' => 'GET', 'url' => '/session/:sessionId/source'], - DriverCommand::GET_SCREEN_ORIENTATION => ['method' => 'GET', 'url' => '/session/:sessionId/orientation'], - DriverCommand::GET_CAPABILITIES => ['method' => 'GET', 'url' => '/session/:sessionId'], - DriverCommand::GET_TITLE => ['method' => 'GET', 'url' => '/session/:sessionId/title'], - DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window_handles'], - DriverCommand::GET_WINDOW_POSITION => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/window/:windowHandle/position', - ], - DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/:windowHandle/size'], - DriverCommand::GO_BACK => ['method' => 'POST', 'url' => '/session/:sessionId/back'], - DriverCommand::GO_FORWARD => ['method' => 'POST', 'url' => '/session/:sessionId/forward'], - DriverCommand::IS_ELEMENT_DISPLAYED => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/element/:id/displayed', - ], - DriverCommand::IS_ELEMENT_ENABLED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/enabled'], - DriverCommand::IS_ELEMENT_SELECTED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/selected'], - DriverCommand::MAXIMIZE_WINDOW => [ - 'method' => 'POST', - 'url' => '/session/:sessionId/window/:windowHandle/maximize', - ], - DriverCommand::MOUSE_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/buttondown'], - DriverCommand::MOUSE_UP => ['method' => 'POST', 'url' => '/session/:sessionId/buttonup'], - DriverCommand::CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/click'], - DriverCommand::DOUBLE_CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/doubleclick'], - DriverCommand::MOVE_TO => ['method' => 'POST', 'url' => '/session/:sessionId/moveto'], - DriverCommand::NEW_SESSION => ['method' => 'POST', 'url' => '/session'], - DriverCommand::QUIT => ['method' => 'DELETE', 'url' => '/session/:sessionId'], - DriverCommand::REFRESH => ['method' => 'POST', 'url' => '/session/:sessionId/refresh'], - DriverCommand::UPLOAD_FILE => ['method' => 'POST', 'url' => '/session/:sessionId/file'], // undocumented - DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/keys'], - DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert_text'], - DriverCommand::SEND_KEYS_TO_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/value'], - DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/implicit_wait'], - DriverCommand::SET_SCREEN_ORIENTATION => ['method' => 'POST', 'url' => '/session/:sessionId/orientation'], - DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], - DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/async_script'], - DriverCommand::SET_WINDOW_POSITION => [ - 'method' => 'POST', - 'url' => '/session/:sessionId/window/:windowHandle/position', - ], - DriverCommand::SET_WINDOW_SIZE => [ - 'method' => 'POST', - 'url' => '/session/:sessionId/window/:windowHandle/size', - ], - DriverCommand::STATUS => ['method' => 'GET', 'url' => '/status'], - DriverCommand::SUBMIT_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/submit'], - DriverCommand::SCREENSHOT => ['method' => 'GET', 'url' => '/session/:sessionId/screenshot'], - DriverCommand::TAKE_ELEMENT_SCREENSHOT => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/element/:id/screenshot', - ], - DriverCommand::TOUCH_SINGLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/click'], - DriverCommand::TOUCH_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/touch/down'], - DriverCommand::TOUCH_DOUBLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/doubleclick'], - DriverCommand::TOUCH_FLICK => ['method' => 'POST', 'url' => '/session/:sessionId/touch/flick'], - DriverCommand::TOUCH_LONG_PRESS => ['method' => 'POST', 'url' => '/session/:sessionId/touch/longclick'], - DriverCommand::TOUCH_MOVE => ['method' => 'POST', 'url' => '/session/:sessionId/touch/move'], - DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'], - DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'], - DriverCommand::CUSTOM_COMMAND => [], - ]; - /** - * @var array Will be merged with $commands - */ - protected static $w3cCompliantCommands = [ - DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/accept'], - DriverCommand::ACTIONS => ['method' => 'POST', 'url' => '/session/:sessionId/actions'], - DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'], - DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'], - DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'], - DriverCommand::FIND_ELEMENT_FROM_SHADOW_ROOT => [ - 'method' => 'POST', - 'url' => '/session/:sessionId/shadow/:id/element', - ], - DriverCommand::FIND_ELEMENTS_FROM_SHADOW_ROOT => [ - 'method' => 'POST', - 'url' => '/session/:sessionId/shadow/:id/elements', - ], - DriverCommand::FULLSCREEN_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/fullscreen'], - DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'GET', 'url' => '/session/:sessionId/element/active'], - DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'], - DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'], - DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], - DriverCommand::GET_ELEMENT_PROPERTY => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/element/:id/property/:name', - ], - DriverCommand::GET_ELEMENT_SHADOW_ROOT => [ - 'method' => 'GET', - 'url' => '/session/:sessionId/element/:id/shadow', - ], - DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], - DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window/handles'], - DriverCommand::GET_WINDOW_POSITION => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'], - DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'], - DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], - DriverCommand::MAXIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/maximize'], - DriverCommand::MINIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/minimize'], - DriverCommand::NEW_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/new'], - DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert/text'], - DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], - DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], - DriverCommand::SET_WINDOW_SIZE => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'], - DriverCommand::SET_WINDOW_POSITION => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'], - ]; - /** - * @var string - */ - protected $url; - /** - * @var resource - */ - protected $curl; - /** - * @var bool - */ - protected $isW3cCompliant = true; - - /** - * @param string $url - * @param string|null $http_proxy - * @param int|null $http_proxy_port - */ - public function __construct($url, $http_proxy = null, $http_proxy_port = null) - { - self::$w3cCompliantCommands = array_merge(self::$commands, self::$w3cCompliantCommands); - - $this->url = $url; - $this->curl = curl_init(); - - if (!empty($http_proxy)) { - curl_setopt($this->curl, CURLOPT_PROXY, $http_proxy); - if ($http_proxy_port !== null) { - curl_setopt($this->curl, CURLOPT_PROXYPORT, $http_proxy_port); - } - } - - // Get credentials from $url (if any) - $matches = null; - if (preg_match("/^(https?:\/\/)(.*):(.*)@(.*?)/U", $url, $matches)) { - $this->url = $matches[1] . $matches[4]; - $auth_creds = $matches[2] . ':' . $matches[3]; - curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); - curl_setopt($this->curl, CURLOPT_USERPWD, $auth_creds); - } - - curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS); - - $this->setConnectionTimeout(30 * 1000); // 30 seconds - $this->setRequestTimeout(180 * 1000); // 3 minutes - } - - public function disableW3cCompliance() - { - $this->isW3cCompliant = false; - } - - /** - * Set timeout for the connect phase - * - * @param int $timeout_in_ms Timeout in milliseconds - * @return HttpCommandExecutor - */ - public function setConnectionTimeout($timeout_in_ms) - { - // There is a PHP bug in some versions which didn't define the constant. - curl_setopt( - $this->curl, - /* CURLOPT_CONNECTTIMEOUT_MS */ - 156, - $timeout_in_ms - ); - - return $this; - } - - /** - * Set the maximum time of a request - * - * @param int $timeout_in_ms Timeout in milliseconds - * @return HttpCommandExecutor - */ - public function setRequestTimeout($timeout_in_ms) - { - // There is a PHP bug in some versions (at least for PHP 5.3.3) which - // didn't define the constant. - curl_setopt( - $this->curl, - /* CURLOPT_TIMEOUT_MS */ - 155, - $timeout_in_ms - ); - - return $this; - } - - /** - * @return WebDriverResponse - */ - public function execute(WebDriverCommand $command) - { - $http_options = $this->getCommandHttpOptions($command); - $http_method = $http_options['method']; - $url = $http_options['url']; - - $sessionID = $command->getSessionID(); - $url = str_replace(':sessionId', $sessionID ?? '', $url); - $params = $command->getParameters(); - foreach ($params as $name => $value) { - if ($name[0] === ':') { - $url = str_replace($name, $value, $url); - unset($params[$name]); - } - } - - if (is_array($params) && !empty($params) && $http_method !== 'POST') { - throw LogicException::forInvalidHttpMethod($url, $http_method, $params); - } - - curl_setopt($this->curl, CURLOPT_URL, $this->url . $url); - - // https://github.com/facebook/php-webdriver/issues/173 - if ($command->getName() === DriverCommand::NEW_SESSION) { - curl_setopt($this->curl, CURLOPT_POST, 1); - } else { - curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $http_method); - } - - if (in_array($http_method, ['POST', 'PUT'], true)) { - // Disable sending 'Expect: 100-Continue' header, as it is causing issues with eg. squid proxy - // https://tools.ietf.org/html/rfc7231#section-5.1.1 - curl_setopt($this->curl, CURLOPT_HTTPHEADER, array_merge(static::DEFAULT_HTTP_HEADERS, ['Expect:'])); - } else { - curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS); - } - - $encoded_params = null; - - if ($http_method === 'POST') { - if (is_array($params) && !empty($params)) { - $encoded_params = json_encode($params); - } elseif ($this->isW3cCompliant) { - // POST body must be valid JSON in W3C, even if empty: https://www.w3.org/TR/webdriver/#processing-model - $encoded_params = '{}'; - } - } - - curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params); - - $raw_results = trim(curl_exec($this->curl)); - - if ($error = curl_error($this->curl)) { - throw WebDriverCurlException::forCurlError($http_method, $url, $error, is_array($params) ? $params : null); - } - - $results = json_decode($raw_results, true); - - if ($results === null && json_last_error() !== JSON_ERROR_NONE) { - throw UnexpectedResponseException::forJsonDecodingError(json_last_error(), $raw_results); - } - - $value = null; - if (is_array($results) && array_key_exists('value', $results)) { - $value = $results['value']; - } - - $message = null; - if (is_array($value) && array_key_exists('message', $value)) { - $message = $value['message']; - } - - $sessionId = null; - if (is_array($value) && array_key_exists('sessionId', $value)) { - // W3C's WebDriver - $sessionId = $value['sessionId']; - } elseif (is_array($results) && array_key_exists('sessionId', $results)) { - // Legacy JsonWire - $sessionId = $results['sessionId']; - } - - // @see https://w3c.github.io/webdriver/#errors - if (isset($value['error'])) { - // W3C's WebDriver - WebDriverException::throwException($value['error'], $message, $results); - } - - $status = $results['status'] ?? 0; - if ($status !== 0) { - // Legacy JsonWire - WebDriverException::throwException($status, $message, $results); - } - - $response = new WebDriverResponse($sessionId); - - return $response - ->setStatus($status) - ->setValue($value); - } - - /** - * @return string - */ - public function getAddressOfRemoteServer() - { - return $this->url; - } - - /** - * @return array - */ - protected function getCommandHttpOptions(WebDriverCommand $command) - { - $commandName = $command->getName(); - if (!isset(self::$commands[$commandName])) { - if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) { - throw LogicException::forError($command->getName() . ' is not a valid command.'); - } - } - - if ($this->isW3cCompliant) { - $raw = self::$w3cCompliantCommands[$command->getName()]; - } else { - $raw = self::$commands[$command->getName()]; - } - - if ($command instanceof CustomWebDriverCommand) { - $url = $command->getCustomUrl(); - $method = $command->getCustomMethod(); - } else { - $url = $raw['url']; - $method = $raw['method']; - } - - return [ - 'url' => $url, - 'method' => $method, - ]; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/JsonWireCompat.php b/vendor/php-webdriver/webdriver/lib/Remote/JsonWireCompat.php deleted file mode 100644 index b9e1b5e..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/JsonWireCompat.php +++ /dev/null @@ -1,98 +0,0 @@ -getMechanism(); - $value = $by->getValue(); - - if ($isW3cCompliant) { - switch ($mechanism) { - // Convert to CSS selectors - case 'class name': - $mechanism = 'css selector'; - $value = sprintf('.%s', self::escapeSelector($value)); - break; - case 'id': - $mechanism = 'css selector'; - $value = sprintf('#%s', self::escapeSelector($value)); - break; - case 'name': - $mechanism = 'css selector'; - $value = sprintf('[name=\'%s\']', self::escapeSelector($value)); - break; - } - } - - return ['using' => $mechanism, 'value' => $value]; - } - - /** - * Escapes a CSS selector. - * - * Code adapted from the Zend Escaper project. - * - * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) - * @see https://github.com/zendframework/zend-escaper/blob/master/src/Escaper.php - * - * @param string $selector - * @return string - */ - private static function escapeSelector($selector) - { - return preg_replace_callback('/[^a-z0-9]/iSu', function ($matches) { - $chr = $matches[0]; - if (mb_strlen($chr) === 1) { - $ord = ord($chr); - } else { - $chr = mb_convert_encoding($chr, 'UTF-32BE', 'UTF-8'); - $ord = hexdec(bin2hex($chr)); - } - - return sprintf('\\%X ', $ord); - }, $selector); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/LocalFileDetector.php b/vendor/php-webdriver/webdriver/lib/Remote/LocalFileDetector.php deleted file mode 100644 index ea7e85e..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/LocalFileDetector.php +++ /dev/null @@ -1,20 +0,0 @@ -driver = $driver; - } - - /** - * @param string $command_name - * @return mixed - */ - public function execute($command_name, array $parameters = []) - { - return $this->driver->execute($command_name, $parameters); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteKeyboard.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteKeyboard.php deleted file mode 100644 index 095b0c5..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/RemoteKeyboard.php +++ /dev/null @@ -1,105 +0,0 @@ -executor = $executor; - $this->driver = $driver; - $this->isW3cCompliant = $isW3cCompliant; - } - - /** - * Send keys to active element - * @param string|array $keys - * @return $this - */ - public function sendKeys($keys) - { - if ($this->isW3cCompliant) { - $activeElement = $this->driver->switchTo()->activeElement(); - $activeElement->sendKeys($keys); - } else { - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ - 'value' => WebDriverKeys::encode($keys), - ]); - } - - return $this; - } - - /** - * Press a modifier key - * - * @see WebDriverKeys - * @param string $key - * @return $this - */ - public function pressKey($key) - { - if ($this->isW3cCompliant) { - $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [ - [ - 'type' => 'key', - 'id' => 'keyboard', - 'actions' => [['type' => 'keyDown', 'value' => $key]], - ], - ], - ]); - } else { - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ - 'value' => [(string) $key], - ]); - } - - return $this; - } - - /** - * Release a modifier key - * - * @see WebDriverKeys - * @param string $key - * @return $this - */ - public function releaseKey($key) - { - if ($this->isW3cCompliant) { - $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [ - [ - 'type' => 'key', - 'id' => 'keyboard', - 'actions' => [['type' => 'keyUp', 'value' => $key]], - ], - ], - ]); - } else { - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ - 'value' => [(string) $key], - ]); - } - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteMouse.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteMouse.php deleted file mode 100644 index fee209f..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/RemoteMouse.php +++ /dev/null @@ -1,290 +0,0 @@ -executor = $executor; - $this->isW3cCompliant = $isW3cCompliant; - } - - /** - * @return RemoteMouse - */ - public function click(?WebDriverCoordinates $where = null) - { - if ($this->isW3cCompliant) { - $moveAction = $where ? [$this->createMoveAction($where)] : []; - $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [ - [ - 'type' => 'pointer', - 'id' => 'mouse', - 'parameters' => ['pointerType' => 'mouse'], - 'actions' => array_merge($moveAction, $this->createClickActions()), - ], - ], - ]); - - return $this; - } - - $this->moveIfNeeded($where); - $this->executor->execute(DriverCommand::CLICK, [ - 'button' => self::BUTTON_LEFT, - ]); - - return $this; - } - - /** - * @return RemoteMouse - */ - public function contextClick(?WebDriverCoordinates $where = null) - { - if ($this->isW3cCompliant) { - $moveAction = $where ? [$this->createMoveAction($where)] : []; - $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [ - [ - 'type' => 'pointer', - 'id' => 'mouse', - 'parameters' => ['pointerType' => 'mouse'], - 'actions' => array_merge($moveAction, [ - [ - 'type' => 'pointerDown', - 'button' => self::BUTTON_RIGHT, - ], - [ - 'type' => 'pointerUp', - 'button' => self::BUTTON_RIGHT, - ], - ]), - ], - ], - ]); - - return $this; - } - - $this->moveIfNeeded($where); - $this->executor->execute(DriverCommand::CLICK, [ - 'button' => self::BUTTON_RIGHT, - ]); - - return $this; - } - - /** - * @return RemoteMouse - */ - public function doubleClick(?WebDriverCoordinates $where = null) - { - if ($this->isW3cCompliant) { - $clickActions = $this->createClickActions(); - $moveAction = $where === null ? [] : [$this->createMoveAction($where)]; - $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [ - [ - 'type' => 'pointer', - 'id' => 'mouse', - 'parameters' => ['pointerType' => 'mouse'], - 'actions' => array_merge($moveAction, $clickActions, $clickActions), - ], - ], - ]); - - return $this; - } - - $this->moveIfNeeded($where); - $this->executor->execute(DriverCommand::DOUBLE_CLICK); - - return $this; - } - - /** - * @return RemoteMouse - */ - public function mouseDown(?WebDriverCoordinates $where = null) - { - if ($this->isW3cCompliant) { - $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [ - [ - 'type' => 'pointer', - 'id' => 'mouse', - 'parameters' => ['pointerType' => 'mouse'], - 'actions' => [ - $this->createMoveAction($where), - [ - 'type' => 'pointerDown', - 'button' => self::BUTTON_LEFT, - ], - ], - ], - ], - ]); - - return $this; - } - - $this->moveIfNeeded($where); - $this->executor->execute(DriverCommand::MOUSE_DOWN); - - return $this; - } - - /** - * @param int|null $x_offset - * @param int|null $y_offset - * - * @return RemoteMouse - */ - public function mouseMove( - ?WebDriverCoordinates $where = null, - $x_offset = null, - $y_offset = null - ) { - if ($this->isW3cCompliant) { - $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [ - [ - 'type' => 'pointer', - 'id' => 'mouse', - 'parameters' => ['pointerType' => 'mouse'], - 'actions' => [$this->createMoveAction($where, $x_offset, $y_offset)], - ], - ], - ]); - - return $this; - } - - $params = []; - if ($where !== null) { - $params['element'] = $where->getAuxiliary(); - } - if ($x_offset !== null) { - $params['xoffset'] = $x_offset; - } - if ($y_offset !== null) { - $params['yoffset'] = $y_offset; - } - - $this->executor->execute(DriverCommand::MOVE_TO, $params); - - return $this; - } - - /** - * @return RemoteMouse - */ - public function mouseUp(?WebDriverCoordinates $where = null) - { - if ($this->isW3cCompliant) { - $moveAction = $where ? [$this->createMoveAction($where)] : []; - - $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [ - [ - 'type' => 'pointer', - 'id' => 'mouse', - 'parameters' => ['pointerType' => 'mouse'], - 'actions' => array_merge($moveAction, [ - [ - 'type' => 'pointerUp', - 'button' => self::BUTTON_LEFT, - ], - ]), - ], - ], - ]); - - return $this; - } - - $this->moveIfNeeded($where); - $this->executor->execute(DriverCommand::MOUSE_UP); - - return $this; - } - - protected function moveIfNeeded(?WebDriverCoordinates $where = null) - { - if ($where) { - $this->mouseMove($where); - } - } - - /** - * @param int|null $x_offset - * @param int|null $y_offset - * - * @return array - */ - private function createMoveAction( - ?WebDriverCoordinates $where = null, - $x_offset = null, - $y_offset = null - ) { - $move_action = [ - 'type' => 'pointerMove', - 'duration' => 100, // to simulate human delay - 'x' => $x_offset ?? 0, - 'y' => $y_offset ?? 0, - ]; - - if ($where !== null) { - $move_action['origin'] = [JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $where->getAuxiliary()]; - } else { - $move_action['origin'] = 'pointer'; - } - - return $move_action; - } - - /** - * @return array - */ - private function createClickActions() - { - return [ - [ - 'type' => 'pointerDown', - 'button' => self::BUTTON_LEFT, - ], - [ - 'type' => 'pointerUp', - 'button' => self::BUTTON_LEFT, - ], - ]; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteStatus.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteStatus.php deleted file mode 100644 index 73ebeba..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/RemoteStatus.php +++ /dev/null @@ -1,79 +0,0 @@ -isReady = (bool) $isReady; - $this->message = (string) $message; - - $this->setMeta($meta); - } - - /** - * @return RemoteStatus - */ - public static function createFromResponse(array $responseBody) - { - $object = new static($responseBody['ready'], $responseBody['message'], $responseBody); - - return $object; - } - - /** - * The remote end's readiness state. - * False if an attempt to create a session at the current time would fail. - * However, the value true does not guarantee that a New Session command will succeed. - * - * @return bool - */ - public function isReady() - { - return $this->isReady; - } - - /** - * An implementation-defined string explaining the remote end's readiness state. - * - * @return string - */ - public function getMessage() - { - return $this->message; - } - - /** - * Arbitrary meta information specific to remote-end implementation. - * - * @return array - */ - public function getMeta() - { - return $this->meta; - } - - protected function setMeta(array $meta) - { - unset($meta['ready'], $meta['message']); - - $this->meta = $meta; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteTargetLocator.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteTargetLocator.php deleted file mode 100644 index 979b9c4..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/RemoteTargetLocator.php +++ /dev/null @@ -1,149 +0,0 @@ -executor = $executor; - $this->driver = $driver; - $this->isW3cCompliant = $isW3cCompliant; - } - - /** - * @return RemoteWebDriver - */ - public function defaultContent() - { - $params = ['id' => null]; - $this->executor->execute(DriverCommand::SWITCH_TO_FRAME, $params); - - return $this->driver; - } - - /** - * @param WebDriverElement|null|int|string $frame The WebDriverElement, the id or the name of the frame. - * When null, switch to the current top-level browsing context When int, switch to the WindowProxy identified - * by the value. When an Element, switch to that Element. - * @return RemoteWebDriver - */ - public function frame($frame) - { - if ($this->isW3cCompliant) { - if ($frame instanceof WebDriverElement) { - $id = [JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $frame->getID()]; - } elseif ($frame === null) { - $id = null; - } elseif (is_int($frame)) { - $id = $frame; - } else { - throw LogicException::forError( - 'In W3C compliance mode frame must be either instance of WebDriverElement, integer or null' - ); - } - } else { - if ($frame instanceof WebDriverElement) { - $id = ['ELEMENT' => $frame->getID()]; - } elseif ($frame === null) { - $id = null; - } elseif (is_int($frame)) { - $id = $frame; - } else { - $id = (string) $frame; - } - } - - $params = ['id' => $id]; - $this->executor->execute(DriverCommand::SWITCH_TO_FRAME, $params); - - return $this->driver; - } - - /** - * Switch to the parent iframe. - * - * @return RemoteWebDriver This driver focused on the parent frame - */ - public function parent() - { - $this->executor->execute(DriverCommand::SWITCH_TO_PARENT_FRAME, []); - - return $this->driver; - } - - /** - * @param string $handle The handle of the window to be focused on. - * @return RemoteWebDriver - */ - public function window($handle) - { - if ($this->isW3cCompliant) { - $params = ['handle' => (string) $handle]; - } else { - $params = ['name' => (string) $handle]; - } - - $this->executor->execute(DriverCommand::SWITCH_TO_WINDOW, $params); - - return $this->driver; - } - - /** - * Creates a new browser window and switches the focus for future commands of this driver to the new window. - * - * @see https://w3c.github.io/webdriver/#new-window - * @param string $windowType The type of a new browser window that should be created. One of [tab, window]. - * The created window is not guaranteed to be of the requested type; if the driver does not support the requested - * type, a new browser window will be created of whatever type the driver does support. - * @throws LogicException - * @return RemoteWebDriver This driver focused on the given window - */ - public function newWindow($windowType = self::WINDOW_TYPE_TAB) - { - if ($windowType !== self::WINDOW_TYPE_TAB && $windowType !== self::WINDOW_TYPE_WINDOW) { - throw LogicException::forError('Window type must by either "tab" or "window"'); - } - - if (!$this->isW3cCompliant) { - throw LogicException::forError('New window is only supported in W3C mode'); - } - - $response = $this->executor->execute(DriverCommand::NEW_WINDOW, ['type' => $windowType]); - - $this->window($response['handle']); - - return $this->driver; - } - - public function alert() - { - return new WebDriverAlert($this->executor); - } - - /** - * @return RemoteWebElement - */ - public function activeElement() - { - $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); - $method = new RemoteExecuteMethod($this->driver); - - return new RemoteWebElement($method, JsonWireCompat::getElement($response), $this->isW3cCompliant); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteTouchScreen.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteTouchScreen.php deleted file mode 100644 index 951c861..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/RemoteTouchScreen.php +++ /dev/null @@ -1,177 +0,0 @@ -executor = $executor; - } - - /** - * @return RemoteTouchScreen The instance. - */ - public function tap(WebDriverElement $element) - { - $this->executor->execute( - DriverCommand::TOUCH_SINGLE_TAP, - ['element' => $element->getID()] - ); - - return $this; - } - - /** - * @return RemoteTouchScreen The instance. - */ - public function doubleTap(WebDriverElement $element) - { - $this->executor->execute( - DriverCommand::TOUCH_DOUBLE_TAP, - ['element' => $element->getID()] - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * - * @return RemoteTouchScreen The instance. - */ - public function down($x, $y) - { - $this->executor->execute(DriverCommand::TOUCH_DOWN, [ - 'x' => $x, - 'y' => $y, - ]); - - return $this; - } - - /** - * @param int $xspeed - * @param int $yspeed - * - * @return RemoteTouchScreen The instance. - */ - public function flick($xspeed, $yspeed) - { - $this->executor->execute(DriverCommand::TOUCH_FLICK, [ - 'xspeed' => $xspeed, - 'yspeed' => $yspeed, - ]); - - return $this; - } - - /** - * @param int $xoffset - * @param int $yoffset - * @param int $speed - * - * @return RemoteTouchScreen The instance. - */ - public function flickFromElement(WebDriverElement $element, $xoffset, $yoffset, $speed) - { - $this->executor->execute(DriverCommand::TOUCH_FLICK, [ - 'xoffset' => $xoffset, - 'yoffset' => $yoffset, - 'element' => $element->getID(), - 'speed' => $speed, - ]); - - return $this; - } - - /** - * @return RemoteTouchScreen The instance. - */ - public function longPress(WebDriverElement $element) - { - $this->executor->execute( - DriverCommand::TOUCH_LONG_PRESS, - ['element' => $element->getID()] - ); - - return $this; - } - - /** - * @param int $x - * @param int $y - * - * @return RemoteTouchScreen The instance. - */ - public function move($x, $y) - { - $this->executor->execute(DriverCommand::TOUCH_MOVE, [ - 'x' => $x, - 'y' => $y, - ]); - - return $this; - } - - /** - * @param int $xoffset - * @param int $yoffset - * - * @return RemoteTouchScreen The instance. - */ - public function scroll($xoffset, $yoffset) - { - $this->executor->execute(DriverCommand::TOUCH_SCROLL, [ - 'xoffset' => $xoffset, - 'yoffset' => $yoffset, - ]); - - return $this; - } - - /** - * @param int $xoffset - * @param int $yoffset - * - * @return RemoteTouchScreen The instance. - */ - public function scrollFromElement(WebDriverElement $element, $xoffset, $yoffset) - { - $this->executor->execute(DriverCommand::TOUCH_SCROLL, [ - 'element' => $element->getID(), - 'xoffset' => $xoffset, - 'yoffset' => $yoffset, - ]); - - return $this; - } - - /** - * @param int $x - * @param int $y - * - * @return RemoteTouchScreen The instance. - */ - public function up($x, $y) - { - $this->executor->execute(DriverCommand::TOUCH_UP, [ - 'x' => $x, - 'y' => $y, - ]); - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php deleted file mode 100644 index 3d65aaf..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php +++ /dev/null @@ -1,760 +0,0 @@ -executor = $commandExecutor; - $this->sessionID = $sessionId; - $this->isW3cCompliant = $isW3cCompliant; - $this->capabilities = $capabilities; - } - - /** - * Construct the RemoteWebDriver by a desired capabilities. - * - * @param string $selenium_server_url The url of the remote Selenium WebDriver server - * @param DesiredCapabilities|array $desired_capabilities The desired capabilities - * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server - * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server - * @param string|null $http_proxy The proxy to tunnel requests to the remote Selenium WebDriver through - * @param int|null $http_proxy_port The proxy port to tunnel requests to the remote Selenium WebDriver through - * @param DesiredCapabilities $required_capabilities The required capabilities - * - * @return static - */ - public static function create( - $selenium_server_url = 'http://localhost:4444/wd/hub', - $desired_capabilities = null, - $connection_timeout_in_ms = null, - $request_timeout_in_ms = null, - $http_proxy = null, - $http_proxy_port = null, - ?DesiredCapabilities $required_capabilities = null - ) { - $selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url); - - $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); - - $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port); - if ($connection_timeout_in_ms !== null) { - $executor->setConnectionTimeout($connection_timeout_in_ms); - } - if ($request_timeout_in_ms !== null) { - $executor->setRequestTimeout($request_timeout_in_ms); - } - - // W3C - $parameters = [ - 'capabilities' => [ - 'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()], - ], - ]; - - if ($required_capabilities !== null && !empty($required_capabilities->toArray())) { - $parameters['capabilities']['alwaysMatch'] = (object) $required_capabilities->toW3cCompatibleArray(); - } - - // Legacy protocol - if ($required_capabilities !== null) { - // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities. - // This has changed with the W3C WebDriver spec, but is the only way how to pass these - // values with the legacy protocol. - $desired_capabilities->setCapability('requiredCapabilities', (object) $required_capabilities->toArray()); - } - - $parameters['desiredCapabilities'] = (object) $desired_capabilities->toArray(); - - $command = WebDriverCommand::newSession($parameters); - - $response = $executor->execute($command); - - return static::createFromResponse($response, $executor); - } - - /** - * [Experimental] Construct the RemoteWebDriver by an existing session. - * - * This constructor can boost the performance by reusing the same browser for the whole test suite. On the other - * hand, because the browser is not pristine, this may lead to flaky and dependent tests. So carefully - * consider the tradeoffs. - * - * To create the instance, we need to know Capabilities of the previously created session. You can either - * pass them in $existingCapabilities parameter, or we will attempt to receive them from the Selenium Grid server. - * However, if Capabilities were not provided and the attempt to get them was not successful, - * exception will be thrown. - * - * @param string $session_id The existing session id - * @param string $selenium_server_url The url of the remote Selenium WebDriver server - * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server - * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server - * @param bool $isW3cCompliant True to use W3C WebDriver (default), false to use the legacy JsonWire protocol - * @param WebDriverCapabilities|null $existingCapabilities Provide capabilities of the existing previously created - * session. If not provided, we will attempt to read them, but this will only work when using Selenium Grid. - * @return static - */ - public static function createBySessionID( - $session_id, - $selenium_server_url = 'http://localhost:4444/wd/hub', - $connection_timeout_in_ms = null, - $request_timeout_in_ms = null - ) { - // BC layer to not break the method signature - $isW3cCompliant = func_num_args() > 4 ? func_get_arg(4) : true; - $existingCapabilities = func_num_args() > 5 ? func_get_arg(5) : null; - - $executor = new HttpCommandExecutor($selenium_server_url, null, null); - if ($connection_timeout_in_ms !== null) { - $executor->setConnectionTimeout($connection_timeout_in_ms); - } - if ($request_timeout_in_ms !== null) { - $executor->setRequestTimeout($request_timeout_in_ms); - } - - if (!$isW3cCompliant) { - $executor->disableW3cCompliance(); - } - - // if capabilities were not provided, attempt to read them from the Selenium Grid API - if ($existingCapabilities === null) { - $existingCapabilities = self::readExistingCapabilitiesFromSeleniumGrid($session_id, $executor); - } - - return new static($executor, $session_id, $existingCapabilities, $isW3cCompliant); - } - - /** - * Close the current window. - * - * @return RemoteWebDriver The current instance. - */ - public function close() - { - $this->execute(DriverCommand::CLOSE, []); - - return $this; - } - - /** - * Create a new top-level browsing context. - * - * @codeCoverageIgnore - * @deprecated Use $driver->switchTo()->newWindow() - * @return WebDriver The current instance. - */ - public function newWindow() - { - return $this->switchTo()->newWindow(); - } - - /** - * Find the first WebDriverElement using the given mechanism. - * - * @return RemoteWebElement NoSuchElementException is thrown in HttpCommandExecutor if no element is found. - * @see WebDriverBy - */ - public function findElement(WebDriverBy $by) - { - $raw_element = $this->execute( - DriverCommand::FIND_ELEMENT, - JsonWireCompat::getUsing($by, $this->isW3cCompliant) - ); - - return $this->newElement(JsonWireCompat::getElement($raw_element)); - } - - /** - * Find all WebDriverElements within the current page using the given mechanism. - * - * @return RemoteWebElement[] A list of all WebDriverElements, or an empty array if nothing matches - * @see WebDriverBy - */ - public function findElements(WebDriverBy $by) - { - $raw_elements = $this->execute( - DriverCommand::FIND_ELEMENTS, - JsonWireCompat::getUsing($by, $this->isW3cCompliant) - ); - - if (!is_array($raw_elements)) { - throw UnexpectedResponseException::forError('Server response to findElements command is not an array'); - } - - $elements = []; - foreach ($raw_elements as $raw_element) { - $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); - } - - return $elements; - } - - /** - * Load a new web page in the current browser window. - * - * @param string $url - * - * @return RemoteWebDriver The current instance. - */ - public function get($url) - { - $params = ['url' => (string) $url]; - $this->execute(DriverCommand::GET, $params); - - return $this; - } - - /** - * Get a string representing the current URL that the browser is looking at. - * - * @return string The current URL. - */ - public function getCurrentURL() - { - return $this->execute(DriverCommand::GET_CURRENT_URL); - } - - /** - * Get the source of the last loaded page. - * - * @return string The current page source. - */ - public function getPageSource() - { - return $this->execute(DriverCommand::GET_PAGE_SOURCE); - } - - /** - * Get the title of the current page. - * - * @return string The title of the current page. - */ - public function getTitle() - { - return $this->execute(DriverCommand::GET_TITLE); - } - - /** - * Return an opaque handle to this window that uniquely identifies it within this driver instance. - * - * @return string The current window handle. - */ - public function getWindowHandle() - { - return $this->execute( - DriverCommand::GET_CURRENT_WINDOW_HANDLE, - [] - ); - } - - /** - * Get all window handles available to the current session. - * - * Note: Do not use `end($driver->getWindowHandles())` to find the last open window, for proper solution see: - * https://github.com/php-webdriver/php-webdriver/wiki/Alert,-tabs,-frames,-iframes#switch-to-the-new-window - * - * @return array An array of string containing all available window handles. - */ - public function getWindowHandles() - { - return $this->execute(DriverCommand::GET_WINDOW_HANDLES, []); - } - - /** - * Quits this driver, closing every associated window. - */ - public function quit() - { - $this->execute(DriverCommand::QUIT); - $this->executor = null; - } - - /** - * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. - * The executed script is assumed to be synchronous and the result of evaluating the script will be returned. - * - * @param string $script The script to inject. - * @param array $arguments The arguments of the script. - * @return mixed The return value of the script. - */ - public function executeScript($script, array $arguments = []) - { - $params = [ - 'script' => $script, - 'args' => $this->prepareScriptArguments($arguments), - ]; - - return $this->execute(DriverCommand::EXECUTE_SCRIPT, $params); - } - - /** - * Inject a snippet of JavaScript into the page for asynchronous execution in the context of the currently selected - * frame. - * - * The driver will pass a callback as the last argument to the snippet, and block until the callback is invoked. - * - * You may need to define script timeout using `setScriptTimeout()` method of `WebDriverTimeouts` first. - * - * @param string $script The script to inject. - * @param array $arguments The arguments of the script. - * @return mixed The value passed by the script to the callback. - */ - public function executeAsyncScript($script, array $arguments = []) - { - $params = [ - 'script' => $script, - 'args' => $this->prepareScriptArguments($arguments), - ]; - - return $this->execute( - DriverCommand::EXECUTE_ASYNC_SCRIPT, - $params - ); - } - - /** - * Take a screenshot of the current page. - * - * @param string $save_as The path of the screenshot to be saved. - * @return string The screenshot in PNG format. - */ - public function takeScreenshot($save_as = null) - { - return (new ScreenshotHelper($this->getExecuteMethod()))->takePageScreenshot($save_as); - } - - /** - * Status returns information about whether a remote end is in a state in which it can create new sessions. - */ - public function getStatus() - { - $response = $this->execute(DriverCommand::STATUS); - - return RemoteStatus::createFromResponse($response); - } - - /** - * Construct a new WebDriverWait by the current WebDriver instance. - * Sample usage: - * - * ``` - * $driver->wait(20, 1000)->until( - * WebDriverExpectedCondition::titleIs('WebDriver Page') - * ); - * ``` - * @param int $timeout_in_second - * @param int $interval_in_millisecond - * - * @return WebDriverWait - */ - public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) - { - return new WebDriverWait( - $this, - $timeout_in_second, - $interval_in_millisecond - ); - } - - /** - * An abstraction for managing stuff you would do in a browser menu. For example, adding and deleting cookies. - * - * @return WebDriverOptions - */ - public function manage() - { - return new WebDriverOptions($this->getExecuteMethod(), $this->isW3cCompliant); - } - - /** - * An abstraction allowing the driver to access the browser's history and to navigate to a given URL. - * - * @return WebDriverNavigation - * @see WebDriverNavigation - */ - public function navigate() - { - return new WebDriverNavigation($this->getExecuteMethod()); - } - - /** - * Switch to a different window or frame. - * - * @return RemoteTargetLocator - * @see RemoteTargetLocator - */ - public function switchTo() - { - return new RemoteTargetLocator($this->getExecuteMethod(), $this, $this->isW3cCompliant); - } - - /** - * @return RemoteMouse - */ - public function getMouse() - { - if (!$this->mouse) { - $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->isW3cCompliant); - } - - return $this->mouse; - } - - /** - * @return RemoteKeyboard - */ - public function getKeyboard() - { - if (!$this->keyboard) { - $this->keyboard = new RemoteKeyboard($this->getExecuteMethod(), $this, $this->isW3cCompliant); - } - - return $this->keyboard; - } - - /** - * @return RemoteTouchScreen - */ - public function getTouch() - { - if (!$this->touch) { - $this->touch = new RemoteTouchScreen($this->getExecuteMethod()); - } - - return $this->touch; - } - - /** - * Construct a new action builder. - * - * @return WebDriverActions - */ - public function action() - { - return new WebDriverActions($this); - } - - /** - * Set the command executor of this RemoteWebdriver - * - * @deprecated To be removed in the future. Executor should be passed in the constructor. - * @internal - * @codeCoverageIgnore - * @param WebDriverCommandExecutor $executor Despite the typehint, it have be an instance of HttpCommandExecutor. - * @return RemoteWebDriver - */ - public function setCommandExecutor(WebDriverCommandExecutor $executor) - { - $this->executor = $executor; - - return $this; - } - - /** - * Get the command executor of this RemoteWebdriver - * - * @return HttpCommandExecutor - */ - public function getCommandExecutor() - { - return $this->executor; - } - - /** - * Set the session id of the RemoteWebDriver. - * - * @deprecated To be removed in the future. Session ID should be passed in the constructor. - * @internal - * @codeCoverageIgnore - * @param string $session_id - * @return RemoteWebDriver - */ - public function setSessionID($session_id) - { - $this->sessionID = $session_id; - - return $this; - } - - /** - * Get current selenium sessionID - * - * @return string - */ - public function getSessionID() - { - return $this->sessionID; - } - - /** - * Get capabilities of the RemoteWebDriver. - * - * @return WebDriverCapabilities|null - */ - public function getCapabilities() - { - return $this->capabilities; - } - - /** - * Returns a list of the currently active sessions. - * - * @deprecated Removed in W3C WebDriver. - * @param string $selenium_server_url The url of the remote Selenium WebDriver server - * @param int $timeout_in_ms - * @return array - */ - public static function getAllSessions($selenium_server_url = 'http://localhost:4444/wd/hub', $timeout_in_ms = 30000) - { - $executor = new HttpCommandExecutor($selenium_server_url, null, null); - $executor->setConnectionTimeout($timeout_in_ms); - - $command = new WebDriverCommand( - null, - DriverCommand::GET_ALL_SESSIONS, - [] - ); - - return $executor->execute($command)->getValue(); - } - - public function execute($command_name, $params = []) - { - // As we so far only use atom for IS_ELEMENT_DISPLAYED, this condition is hardcoded here. In case more atoms - // are used, this should be rewritten and separated from this class (e.g. to some abstract matcher logic). - if ($command_name === DriverCommand::IS_ELEMENT_DISPLAYED - && ( - // When capabilities are missing in php-webdriver 1.13.x, always fallback to use the atom - $this->getCapabilities() === null - // If capabilities are present, use the atom only if condition matches - || IsElementDisplayedAtom::match($this->getCapabilities()->getBrowserName()) - ) - ) { - return (new IsElementDisplayedAtom($this))->execute($params); - } - - $command = new WebDriverCommand( - $this->sessionID, - $command_name, - $params - ); - - if ($this->executor) { - $response = $this->executor->execute($command); - - return $response->getValue(); - } - - return null; - } - - /** - * Execute custom commands on remote end. - * For example vendor-specific commands or other commands not implemented by php-webdriver. - * - * @see https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands - * @param string $endpointUrl - * @param string $method - * @param array $params - * @return mixed|null - */ - public function executeCustomCommand($endpointUrl, $method = 'GET', $params = []) - { - $command = new CustomWebDriverCommand( - $this->sessionID, - $endpointUrl, - $method, - $params - ); - - if ($this->executor) { - $response = $this->executor->execute($command); - - return $response->getValue(); - } - - return null; - } - - /** - * @internal - * @return bool - */ - public function isW3cCompliant() - { - return $this->isW3cCompliant; - } - - /** - * Create instance based on response to NEW_SESSION command. - * Also detect W3C/OSS dialect and setup the driver/executor accordingly. - * - * @internal - * @return static - */ - protected static function createFromResponse(WebDriverResponse $response, HttpCommandExecutor $commandExecutor) - { - $responseValue = $response->getValue(); - - if (!$isW3cCompliant = isset($responseValue['capabilities'])) { - $commandExecutor->disableW3cCompliance(); - } - - if ($isW3cCompliant) { - $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($responseValue['capabilities']); - } else { - $returnedCapabilities = new DesiredCapabilities($responseValue); - } - - return new static($commandExecutor, $response->getSessionID(), $returnedCapabilities, $isW3cCompliant); - } - - /** - * Prepare arguments for JavaScript injection - * - * @return array - */ - protected function prepareScriptArguments(array $arguments) - { - $args = []; - foreach ($arguments as $key => $value) { - if ($value instanceof WebDriverElement) { - $args[$key] = [ - $this->isW3cCompliant ? - JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER - : 'ELEMENT' => $value->getID(), - ]; - } else { - if (is_array($value)) { - $value = $this->prepareScriptArguments($value); - } - $args[$key] = $value; - } - } - - return $args; - } - - /** - * @return RemoteExecuteMethod - */ - protected function getExecuteMethod() - { - if (!$this->executeMethod) { - $this->executeMethod = new RemoteExecuteMethod($this); - } - - return $this->executeMethod; - } - - /** - * Return the WebDriverElement with the given id. - * - * @param string $id The id of the element to be created. - * @return RemoteWebElement - */ - protected function newElement($id) - { - return new RemoteWebElement($this->getExecuteMethod(), $id, $this->isW3cCompliant); - } - - /** - * Cast legacy types (array or null) to DesiredCapabilities object. To be removed in future when instance of - * DesiredCapabilities will be required. - * - * @param array|DesiredCapabilities|null $desired_capabilities - * @return DesiredCapabilities - */ - protected static function castToDesiredCapabilitiesObject($desired_capabilities = null) - { - if ($desired_capabilities === null) { - return new DesiredCapabilities(); - } - - if (is_array($desired_capabilities)) { - return new DesiredCapabilities($desired_capabilities); - } - - return $desired_capabilities; - } - - protected static function readExistingCapabilitiesFromSeleniumGrid( - string $session_id, - HttpCommandExecutor $executor - ): DesiredCapabilities { - $getCapabilitiesCommand = new CustomWebDriverCommand($session_id, '/se/grid/session/:sessionId', 'GET', []); - - try { - $capabilitiesResponse = $executor->execute($getCapabilitiesCommand); - - $existingCapabilities = DesiredCapabilities::createFromW3cCapabilities( - $capabilitiesResponse->getValue()['capabilities'] - ); - if ($existingCapabilities === null) { - throw UnexpectedResponseException::forError('Empty capabilities received'); - } - } catch (\Exception $e) { - throw UnexpectedResponseException::forCapabilitiesRetrievalError($e); - } - - return $existingCapabilities; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php b/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php deleted file mode 100644 index e0ce43b..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php +++ /dev/null @@ -1,650 +0,0 @@ -executor = $executor; - $this->id = $id; - $this->fileDetector = new UselessFileDetector(); - $this->isW3cCompliant = $isW3cCompliant; - } - - /** - * Clear content editable or resettable element - * - * @return $this The current instance. - */ - public function clear() - { - $this->executor->execute( - DriverCommand::CLEAR_ELEMENT, - [':id' => $this->id] - ); - - return $this; - } - - /** - * Click this element. - * - * @return $this The current instance. - */ - public function click() - { - try { - $this->executor->execute( - DriverCommand::CLICK_ELEMENT, - [':id' => $this->id] - ); - } catch (ElementNotInteractableException $e) { - // An issue with geckodriver (https://github.com/mozilla/geckodriver/issues/653) prevents clicking on a link - // if the first child is a block-level element. - // The workaround in this case is to click on a child element. - $this->clickChildElement($e); - } - - return $this; - } - - /** - * Find the first WebDriverElement within this element using the given mechanism. - * - * When using xpath be aware that webdriver follows standard conventions: a search prefixed with "//" will - * search the entire document from the root, not just the children (relative context) of this current node. - * Use ".//" to limit your search to the children of this element. - * - * @return static NoSuchElementException is thrown in HttpCommandExecutor if no element is found. - * @see WebDriverBy - */ - public function findElement(WebDriverBy $by) - { - $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); - $params[':id'] = $this->id; - - $raw_element = $this->executor->execute( - DriverCommand::FIND_CHILD_ELEMENT, - $params - ); - - return $this->newElement(JsonWireCompat::getElement($raw_element)); - } - - /** - * Find all WebDriverElements within this element using the given mechanism. - * - * When using xpath be aware that webdriver follows standard conventions: a search prefixed with "//" will - * search the entire document from the root, not just the children (relative context) of this current node. - * Use ".//" to limit your search to the children of this element. - * - * @return static[] A list of all WebDriverElements, or an empty - * array if nothing matches - * @see WebDriverBy - */ - public function findElements(WebDriverBy $by) - { - $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); - $params[':id'] = $this->id; - $raw_elements = $this->executor->execute( - DriverCommand::FIND_CHILD_ELEMENTS, - $params - ); - - if (!is_array($raw_elements)) { - throw UnexpectedResponseException::forError('Server response to findChildElements command is not an array'); - } - - $elements = []; - foreach ($raw_elements as $raw_element) { - $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); - } - - return $elements; - } - - /** - * Get the value of the given attribute of the element. - * Attribute is meant what is declared in the HTML markup of the element. - * To read a value of a IDL "JavaScript" property (like `innerHTML`), use `getDomProperty()` method. - * - * @param string $attribute_name The name of the attribute. - * @return string|true|null The value of the attribute. If this is boolean attribute, return true if the element - * has it, otherwise return null. - */ - public function getAttribute($attribute_name) - { - $params = [ - ':name' => $attribute_name, - ':id' => $this->id, - ]; - - if ($this->isW3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { - $value = $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); - - if ($value === true) { - return 'true'; - } - - if ($value === false) { - return 'false'; - } - - if ($value !== null) { - return (string) $value; - } - } - - return $this->executor->execute(DriverCommand::GET_ELEMENT_ATTRIBUTE, $params); - } - - /** - * Gets the value of a IDL JavaScript property of this element (for example `innerHTML`, `tagName` etc.). - * - * @see https://developer.mozilla.org/en-US/docs/Glossary/IDL - * @see https://developer.mozilla.org/en-US/docs/Web/API/Element#properties - * @param string $propertyName - * @return mixed|null The property's current value or null if the value is not set or the property does not exist. - */ - public function getDomProperty($propertyName) - { - if (!$this->isW3cCompliant) { - throw new UnsupportedOperationException('This method is only supported in W3C mode'); - } - - $params = [ - ':name' => $propertyName, - ':id' => $this->id, - ]; - - return $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); - } - - /** - * Get the value of a given CSS property. - * - * @param string $css_property_name The name of the CSS property. - * @return string The value of the CSS property. - */ - public function getCSSValue($css_property_name) - { - $params = [ - ':propertyName' => $css_property_name, - ':id' => $this->id, - ]; - - return $this->executor->execute( - DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY, - $params - ); - } - - /** - * Get the location of element relative to the top-left corner of the page. - * - * @return WebDriverPoint The location of the element. - */ - public function getLocation() - { - $location = $this->executor->execute( - DriverCommand::GET_ELEMENT_LOCATION, - [':id' => $this->id] - ); - - return new WebDriverPoint($location['x'], $location['y']); - } - - /** - * Try scrolling the element into the view port and return the location of - * element relative to the top-left corner of the page afterwards. - * - * @return WebDriverPoint The location of the element. - */ - public function getLocationOnScreenOnceScrolledIntoView() - { - if ($this->isW3cCompliant) { - $script = <<executor->execute(DriverCommand::EXECUTE_SCRIPT, [ - 'script' => $script, - 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]], - ]); - $location = ['x' => $result['x'], 'y' => $result['y']]; - } else { - $location = $this->executor->execute( - DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW, - [':id' => $this->id] - ); - } - - return new WebDriverPoint($location['x'], $location['y']); - } - - /** - * @return WebDriverCoordinates - */ - public function getCoordinates() - { - $element = $this; - - $on_screen = null; // planned but not yet implemented - $in_view_port = static function () use ($element) { - return $element->getLocationOnScreenOnceScrolledIntoView(); - }; - $on_page = static function () use ($element) { - return $element->getLocation(); - }; - $auxiliary = $this->getID(); - - return new WebDriverCoordinates( - $on_screen, - $in_view_port, - $on_page, - $auxiliary - ); - } - - /** - * Get the size of element. - * - * @return WebDriverDimension The dimension of the element. - */ - public function getSize() - { - $size = $this->executor->execute( - DriverCommand::GET_ELEMENT_SIZE, - [':id' => $this->id] - ); - - return new WebDriverDimension($size['width'], $size['height']); - } - - /** - * Get the (lowercase) tag name of this element. - * - * @return string The tag name. - */ - public function getTagName() - { - // Force tag name to be lowercase as expected by JsonWire protocol for Opera driver - // until this issue is not resolved : - // https://github.com/operasoftware/operadriver/issues/102 - // Remove it when fixed to be consistent with the protocol. - return mb_strtolower($this->executor->execute( - DriverCommand::GET_ELEMENT_TAG_NAME, - [':id' => $this->id] - )); - } - - /** - * Get the visible (i.e. not hidden by CSS) innerText of this element, - * including sub-elements, without any leading or trailing whitespace. - * - * @return string The visible innerText of this element. - */ - public function getText() - { - return $this->executor->execute( - DriverCommand::GET_ELEMENT_TEXT, - [':id' => $this->id] - ); - } - - /** - * Is this element displayed or not? This method avoids the problem of having - * to parse an element's "style" attribute. - * - * @return bool - */ - public function isDisplayed() - { - return $this->executor->execute( - DriverCommand::IS_ELEMENT_DISPLAYED, - [':id' => $this->id] - ); - } - - /** - * Is the element currently enabled or not? This will generally return true - * for everything but disabled input elements. - * - * @return bool - */ - public function isEnabled() - { - return $this->executor->execute( - DriverCommand::IS_ELEMENT_ENABLED, - [':id' => $this->id] - ); - } - - /** - * Determine whether this element is selected or not. - * - * @return bool - */ - public function isSelected() - { - return $this->executor->execute( - DriverCommand::IS_ELEMENT_SELECTED, - [':id' => $this->id] - ); - } - - /** - * Simulate typing into an element, which may set its value. - * - * @param mixed $value The data to be typed. - * @return static The current instance. - */ - public function sendKeys($value) - { - $local_file = $this->fileDetector->getLocalFile($value); - - $params = []; - if ($local_file === null) { - if ($this->isW3cCompliant) { - // Work around the Geckodriver NULL issue by splitting on NULL and calling sendKeys multiple times. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1494661. - $encodedValues = explode(WebDriverKeys::NULL, WebDriverKeys::encode($value, true)); - foreach ($encodedValues as $encodedValue) { - $params[] = [ - 'text' => $encodedValue, - ':id' => $this->id, - ]; - } - } else { - $params[] = [ - 'value' => WebDriverKeys::encode($value), - ':id' => $this->id, - ]; - } - } else { - if ($this->isW3cCompliant) { - try { - // Attempt to upload the file to the remote browser. - // This is so far non-W3C compliant method, so it may fail - if so, we just ignore the exception. - // @see https://github.com/w3c/webdriver/issues/1355 - $fileName = $this->upload($local_file); - } catch (PhpWebDriverExceptionInterface $e) { - $fileName = $local_file; - } - - $params[] = [ - 'text' => $fileName, - ':id' => $this->id, - ]; - } else { - $params[] = [ - 'value' => WebDriverKeys::encode($this->upload($local_file)), - ':id' => $this->id, - ]; - } - } - - foreach ($params as $param) { - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $param); - } - - return $this; - } - - /** - * Set the fileDetector in order to let the RemoteWebElement to know that you are going to upload a file. - * - * Basically, if you want WebDriver trying to send a file, set the fileDetector - * to be LocalFileDetector. Otherwise, keep it UselessFileDetector. - * - * eg. `$element->setFileDetector(new LocalFileDetector);` - * - * @return $this - * @see FileDetector - * @see LocalFileDetector - * @see UselessFileDetector - */ - public function setFileDetector(FileDetector $detector) - { - $this->fileDetector = $detector; - - return $this; - } - - /** - * If this current element is a form, or an element within a form, then this will be submitted to the remote server. - * - * @return $this The current instance. - */ - public function submit() - { - if ($this->isW3cCompliant) { - // Submit method cannot be called directly in case an input of this form is named "submit". - // We use this polyfill to trigger 'submit' event using form.dispatchEvent(). - $submitPolyfill = <<executor->execute(DriverCommand::EXECUTE_SCRIPT, [ - 'script' => $submitPolyfill, - 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]], - ]); - - return $this; - } - - $this->executor->execute( - DriverCommand::SUBMIT_ELEMENT, - [':id' => $this->id] - ); - - return $this; - } - - /** - * Get the opaque ID of the element. - * - * @return string The opaque ID. - */ - public function getID() - { - return $this->id; - } - - /** - * Take a screenshot of a specific element. - * - * @param string $save_as The path of the screenshot to be saved. - * @return string The screenshot in PNG format. - */ - public function takeElementScreenshot($save_as = null) - { - return (new ScreenshotHelper($this->executor))->takeElementScreenshot($this->id, $save_as); - } - - /** - * Test if two elements IDs refer to the same DOM element. - * - * @return bool - */ - public function equals(WebDriverElement $other) - { - if ($this->isW3cCompliant) { - return $this->getID() === $other->getID(); - } - - return $this->executor->execute(DriverCommand::ELEMENT_EQUALS, [ - ':id' => $this->id, - ':other' => $other->getID(), - ]); - } - - /** - * Get representation of an element's shadow root for accessing the shadow DOM of a web component. - * - * @return ShadowRoot - */ - public function getShadowRoot() - { - if (!$this->isW3cCompliant) { - throw new UnsupportedOperationException('This method is only supported in W3C mode'); - } - - $response = $this->executor->execute( - DriverCommand::GET_ELEMENT_SHADOW_ROOT, - [ - ':id' => $this->id, - ] - ); - - return ShadowRoot::createFromResponse($this->executor, $response); - } - - /** - * Attempt to click on a child level element. - * - * This provides a workaround for geckodriver bug 653 whereby a link whose first element is a block-level element - * throws an ElementNotInteractableException could not scroll into view exception. - * - * The workaround provided here attempts to click on a child node of the element. - * In case the first child is hidden, other elements are processed until we run out of elements. - * - * @param ElementNotInteractableException $originalException The exception to throw if unable to click on any child - * @see https://github.com/mozilla/geckodriver/issues/653 - * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1374283 - */ - protected function clickChildElement(ElementNotInteractableException $originalException) - { - $children = $this->findElements(WebDriverBy::xpath('./*')); - foreach ($children as $child) { - try { - // Note: This does not use $child->click() as this would cause recursion into all children. - // Where the element is hidden, all children will also be hidden. - $this->executor->execute( - DriverCommand::CLICK_ELEMENT, - [':id' => $child->id] - ); - - return; - } catch (ElementNotInteractableException $e) { - // Ignore the ElementNotInteractableException exception on this node. Try the next child instead. - } - } - - throw $originalException; - } - - /** - * Return the WebDriverElement with $id - * - * @param string $id - * - * @return static - */ - protected function newElement($id) - { - return new static($this->executor, $id, $this->isW3cCompliant); - } - - /** - * Upload a local file to the server - * - * @param string $local_file - * - * @throws LogicException - * @return string The remote path of the file. - */ - protected function upload($local_file) - { - if (!is_file($local_file)) { - throw LogicException::forError('You may only upload files: ' . $local_file); - } - - $temp_zip_path = $this->createTemporaryZipArchive($local_file); - - $remote_path = $this->executor->execute( - DriverCommand::UPLOAD_FILE, - ['file' => base64_encode(file_get_contents($temp_zip_path))] - ); - - unlink($temp_zip_path); - - return $remote_path; - } - - /** - * @param string $fileToZip - * @return string - */ - protected function createTemporaryZipArchive($fileToZip) - { - // Create a temporary file in the system temp directory. - // Intentionally do not use `tempnam()`, as it creates empty file which zip extension may not handle. - $tempZipPath = sys_get_temp_dir() . '/' . uniqid('WebDriverZip', false); - - $zip = new ZipArchive(); - if (($errorCode = $zip->open($tempZipPath, ZipArchive::CREATE)) !== true) { - throw IOException::forFileError(sprintf('Error creating zip archive: %s', $errorCode), $tempZipPath); - } - - $info = pathinfo($fileToZip); - $file_name = $info['basename']; - $zip->addFile($fileToZip, $file_name); - $zip->close(); - - return $tempZipPath; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverCommandExecutor.php b/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverCommandExecutor.php deleted file mode 100644 index 5e3ef83..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverCommandExecutor.php +++ /dev/null @@ -1,53 +0,0 @@ -getURL()); - $this->service = $service; - } - - /** - * @throws \Exception - * @throws WebDriverException - * @return WebDriverResponse - */ - public function execute(WebDriverCommand $command) - { - if ($command->getName() === DriverCommand::NEW_SESSION) { - $this->service->start(); - } - - try { - $value = parent::execute($command); - if ($command->getName() === DriverCommand::QUIT) { - $this->service->stop(); - } - - return $value; - } catch (\Exception $e) { - if (!$this->service->isRunning()) { - throw new DriverServerDiedException($e); - } - throw $e; - } - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverService.php b/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverService.php deleted file mode 100644 index 028b8cd..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/Service/DriverService.php +++ /dev/null @@ -1,183 +0,0 @@ -setExecutable($executable); - $this->url = sprintf('http://localhost:%d', $port); - $this->args = $args; - $this->environment = $environment ?: $_ENV; - } - - /** - * @return string - */ - public function getURL() - { - return $this->url; - } - - /** - * @return DriverService - */ - public function start() - { - if ($this->process !== null) { - return $this; - } - - $this->process = $this->createProcess(); - $this->process->start(); - - $this->checkWasStarted($this->process); - - $checker = new URLChecker(); - $checker->waitUntilAvailable(20 * 1000, $this->url . '/status'); - - return $this; - } - - /** - * @return DriverService - */ - public function stop() - { - if ($this->process === null) { - return $this; - } - - $this->process->stop(); - $this->process = null; - - $checker = new URLChecker(); - $checker->waitUntilUnavailable(3 * 1000, $this->url . '/shutdown'); - - return $this; - } - - /** - * @return bool - */ - public function isRunning() - { - if ($this->process === null) { - return false; - } - - return $this->process->isRunning(); - } - - /** - * @deprecated Has no effect. Will be removed in next major version. Executable is now checked - * when calling setExecutable(). - * @param string $executable - * @return string - */ - protected static function checkExecutable($executable) - { - return $executable; - } - - /** - * @param string $executable - * @throws IOException - */ - protected function setExecutable($executable) - { - if ($this->isExecutable($executable)) { - $this->executable = $executable; - - return; - } - - throw IOException::forFileError( - 'File is not executable. Make sure the path is correct or use environment variable to specify' - . ' location of the executable.', - $executable - ); - } - - /** - * @param Process $process - */ - protected function checkWasStarted($process) - { - usleep(10000); // wait 10ms, otherwise the asynchronous process failure may not yet be propagated - - if (!$process->isRunning()) { - throw RuntimeException::forDriverError($process); - } - } - - private function createProcess(): Process - { - $commandLine = array_merge([$this->executable], $this->args); - - return new Process($commandLine, null, $this->environment); - } - - /** - * Check whether given file is executable directly or using system PATH - */ - private function isExecutable(string $filename): bool - { - if (is_executable($filename)) { - return true; - } - if ($filename !== basename($filename)) { // $filename is an absolute path, do no try to search it in PATH - return false; - } - - $paths = explode(PATH_SEPARATOR, getenv('PATH')); - foreach ($paths as $path) { - if (is_executable($path . DIRECTORY_SEPARATOR . $filename)) { - return true; - } - } - - return false; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/ShadowRoot.php b/vendor/php-webdriver/webdriver/lib/Remote/ShadowRoot.php deleted file mode 100644 index 5d419b1..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/ShadowRoot.php +++ /dev/null @@ -1,98 +0,0 @@ -executor = $executor; - $this->id = $id; - } - - /** - * @return self - */ - public static function createFromResponse(RemoteExecuteMethod $executor, array $response) - { - if (empty($response[self::SHADOW_ROOT_IDENTIFIER])) { - throw new UnknownErrorException('Shadow root is missing in server response'); - } - - return new self($executor, $response[self::SHADOW_ROOT_IDENTIFIER]); - } - - /** - * @return RemoteWebElement - */ - public function findElement(WebDriverBy $locator) - { - $params = JsonWireCompat::getUsing($locator, true); - $params[':id'] = $this->id; - - $rawElement = $this->executor->execute( - DriverCommand::FIND_ELEMENT_FROM_SHADOW_ROOT, - $params - ); - - return new RemoteWebElement($this->executor, JsonWireCompat::getElement($rawElement), true); - } - - /** - * @return WebDriverElement[] - */ - public function findElements(WebDriverBy $locator) - { - $params = JsonWireCompat::getUsing($locator, true); - $params[':id'] = $this->id; - - $rawElements = $this->executor->execute( - DriverCommand::FIND_ELEMENTS_FROM_SHADOW_ROOT, - $params - ); - - if (!is_array($rawElements)) { - throw UnexpectedResponseException::forError( - 'Server response to findElementsFromShadowRoot command is not an array' - ); - } - - $elements = []; - foreach ($rawElements as $rawElement) { - $elements[] = new RemoteWebElement($this->executor, JsonWireCompat::getElement($rawElement), true); - } - - return $elements; - } - - /** - * @return string - */ - public function getID() - { - return $this->id; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/UselessFileDetector.php b/vendor/php-webdriver/webdriver/lib/Remote/UselessFileDetector.php deleted file mode 100644 index 6bce0e0..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/UselessFileDetector.php +++ /dev/null @@ -1,11 +0,0 @@ -sessionID = $session_id; - $this->name = $name; - $this->parameters = $parameters; - } - - /** - * @return self - */ - public static function newSession(array $parameters) - { - // TODO: In 2.0 call empty constructor and assign properties directly. - return new self(null, DriverCommand::NEW_SESSION, $parameters); - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @return string|null Could be null for newSession command - */ - public function getSessionID() - { - return $this->sessionID; - } - - /** - * @return array - */ - public function getParameters() - { - return $this->parameters; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Remote/WebDriverResponse.php b/vendor/php-webdriver/webdriver/lib/Remote/WebDriverResponse.php deleted file mode 100644 index 39b19bf..0000000 --- a/vendor/php-webdriver/webdriver/lib/Remote/WebDriverResponse.php +++ /dev/null @@ -1,84 +0,0 @@ -sessionID = $session_id; - } - - /** - * @return null|int - */ - public function getStatus() - { - return $this->status; - } - - /** - * @param int $status - * @return WebDriverResponse - */ - public function setStatus($status) - { - $this->status = $status; - - return $this; - } - - /** - * @return mixed - */ - public function getValue() - { - return $this->value; - } - - /** - * @param mixed $value - * @return WebDriverResponse - */ - public function setValue($value) - { - $this->value = $value; - - return $this; - } - - /** - * @return null|string - */ - public function getSessionID() - { - return $this->sessionID; - } - - /** - * @param mixed $session_id - * @return WebDriverResponse - */ - public function setSessionID($session_id) - { - $this->sessionID = $session_id; - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriver.php b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriver.php deleted file mode 100644 index 21e5a68..0000000 --- a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriver.php +++ /dev/null @@ -1,394 +0,0 @@ -dispatcher = $dispatcher ?: new WebDriverDispatcher(); - if (!$this->dispatcher->getDefaultDriver()) { - $this->dispatcher->setDefaultDriver($this); - } - $this->driver = $driver; - } - - /** - * @return WebDriverDispatcher - */ - public function getDispatcher() - { - return $this->dispatcher; - } - - /** - * @return WebDriver - */ - public function getWebDriver() - { - return $this->driver; - } - - /** - * @param mixed $url - * @throws WebDriverException - * @return $this - */ - public function get($url) - { - $this->dispatch('beforeNavigateTo', $url, $this); - - try { - $this->driver->get($url); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - $this->dispatch('afterNavigateTo', $url, $this); - - return $this; - } - - /** - * @throws WebDriverException - * @return array - */ - public function findElements(WebDriverBy $by) - { - $this->dispatch('beforeFindBy', $by, null, $this); - $elements = []; - - try { - foreach ($this->driver->findElements($by) as $element) { - $elements[] = $this->newElement($element); - } - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - - $this->dispatch('afterFindBy', $by, null, $this); - - return $elements; - } - - /** - * @throws WebDriverException - * @return EventFiringWebElement - */ - public function findElement(WebDriverBy $by) - { - $this->dispatch('beforeFindBy', $by, null, $this); - - try { - $element = $this->newElement($this->driver->findElement($by)); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - - $this->dispatch('afterFindBy', $by, null, $this); - - return $element; - } - - /** - * @param string $script - * @throws WebDriverException - * @return mixed - */ - public function executeScript($script, array $arguments = []) - { - if (!$this->driver instanceof JavaScriptExecutor) { - throw new UnsupportedOperationException( - 'driver does not implement JavaScriptExecutor' - ); - } - - $this->dispatch('beforeScript', $script, $this); - - try { - $result = $this->driver->executeScript($script, $arguments); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - - $this->dispatch('afterScript', $script, $this); - - return $result; - } - - /** - * @param string $script - * @throws WebDriverException - * @return mixed - */ - public function executeAsyncScript($script, array $arguments = []) - { - if (!$this->driver instanceof JavaScriptExecutor) { - throw new UnsupportedOperationException( - 'driver does not implement JavaScriptExecutor' - ); - } - - $this->dispatch('beforeScript', $script, $this); - - try { - $result = $this->driver->executeAsyncScript($script, $arguments); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - $this->dispatch('afterScript', $script, $this); - - return $result; - } - - /** - * @throws WebDriverException - * @return $this - */ - public function close() - { - try { - $this->driver->close(); - - return $this; - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return string - */ - public function getCurrentURL() - { - try { - return $this->driver->getCurrentURL(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return string - */ - public function getPageSource() - { - try { - return $this->driver->getPageSource(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return string - */ - public function getTitle() - { - try { - return $this->driver->getTitle(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return string - */ - public function getWindowHandle() - { - try { - return $this->driver->getWindowHandle(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return array - */ - public function getWindowHandles() - { - try { - return $this->driver->getWindowHandles(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - */ - public function quit() - { - try { - $this->driver->quit(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @param null|string $save_as - * @throws WebDriverException - * @return string - */ - public function takeScreenshot($save_as = null) - { - try { - return $this->driver->takeScreenshot($save_as); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @param int $timeout_in_second - * @param int $interval_in_millisecond - * @throws WebDriverException - * @return WebDriverWait - */ - public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) - { - try { - return $this->driver->wait($timeout_in_second, $interval_in_millisecond); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return WebDriverOptions - */ - public function manage() - { - try { - return $this->driver->manage(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return EventFiringWebDriverNavigation - */ - public function navigate() - { - try { - return new EventFiringWebDriverNavigation( - $this->driver->navigate(), - $this->getDispatcher() - ); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return WebDriverTargetLocator - */ - public function switchTo() - { - try { - return $this->driver->switchTo(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return WebDriverTouchScreen - */ - public function getTouch() - { - try { - return $this->driver->getTouch(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - public function execute($name, $params) - { - try { - return $this->driver->execute($name, $params); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @return EventFiringWebElement - */ - protected function newElement(WebDriverElement $element) - { - return new EventFiringWebElement($element, $this->getDispatcher()); - } - - /** - * @param mixed $method - * @param mixed ...$arguments - */ - protected function dispatch($method, ...$arguments) - { - if (!$this->dispatcher) { - return; - } - - $this->dispatcher->dispatch($method, $arguments); - } - - protected function dispatchOnException(WebDriverException $exception) - { - $this->dispatch('onException', $exception, $this); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php deleted file mode 100644 index 27ea342..0000000 --- a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php +++ /dev/null @@ -1,135 +0,0 @@ -navigator = $navigator; - $this->dispatcher = $dispatcher; - } - - /** - * @return WebDriverDispatcher - */ - public function getDispatcher() - { - return $this->dispatcher; - } - - /** - * @return WebDriverNavigationInterface - */ - public function getNavigator() - { - return $this->navigator; - } - - public function back() - { - $this->dispatch( - 'beforeNavigateBack', - $this->getDispatcher()->getDefaultDriver() - ); - - try { - $this->navigator->back(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - } - $this->dispatch( - 'afterNavigateBack', - $this->getDispatcher()->getDefaultDriver() - ); - - return $this; - } - - public function forward() - { - $this->dispatch( - 'beforeNavigateForward', - $this->getDispatcher()->getDefaultDriver() - ); - - try { - $this->navigator->forward(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - } - $this->dispatch( - 'afterNavigateForward', - $this->getDispatcher()->getDefaultDriver() - ); - - return $this; - } - - public function refresh() - { - try { - $this->navigator->refresh(); - - return $this; - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - public function to($url) - { - $this->dispatch( - 'beforeNavigateTo', - $url, - $this->getDispatcher()->getDefaultDriver() - ); - - try { - $this->navigator->to($url); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - - $this->dispatch( - 'afterNavigateTo', - $url, - $this->getDispatcher()->getDefaultDriver() - ); - - return $this; - } - - /** - * @param mixed $method - * @param mixed ...$arguments - */ - protected function dispatch($method, ...$arguments) - { - if (!$this->dispatcher) { - return; - } - - $this->dispatcher->dispatch($method, $arguments); - } - - protected function dispatchOnException(WebDriverException $exception) - { - $this->dispatch('onException', $exception); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebElement.php b/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebElement.php deleted file mode 100644 index 6caa086..0000000 --- a/vendor/php-webdriver/webdriver/lib/Support/Events/EventFiringWebElement.php +++ /dev/null @@ -1,413 +0,0 @@ -element = $element; - $this->dispatcher = $dispatcher; - } - - /** - * @return WebDriverDispatcher - */ - public function getDispatcher() - { - return $this->dispatcher; - } - - /** - * @return WebDriverElement - */ - public function getElement() - { - return $this->element; - } - - /** - * @param mixed $value - * @throws WebDriverException - * @return $this - */ - public function sendKeys($value) - { - $this->dispatch('beforeChangeValueOf', $this); - - try { - $this->element->sendKeys($value); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - $this->dispatch('afterChangeValueOf', $this); - - return $this; - } - - /** - * @throws WebDriverException - * @return $this - */ - public function click() - { - $this->dispatch('beforeClickOn', $this); - - try { - $this->element->click(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - $this->dispatch('afterClickOn', $this); - - return $this; - } - - /** - * @throws WebDriverException - * @return EventFiringWebElement - */ - public function findElement(WebDriverBy $by) - { - $this->dispatch( - 'beforeFindBy', - $by, - $this, - $this->dispatcher->getDefaultDriver() - ); - - try { - $element = $this->newElement($this->element->findElement($by)); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - - $this->dispatch( - 'afterFindBy', - $by, - $this, - $this->dispatcher->getDefaultDriver() - ); - - return $element; - } - - /** - * @throws WebDriverException - * @return array - */ - public function findElements(WebDriverBy $by) - { - $this->dispatch( - 'beforeFindBy', - $by, - $this, - $this->dispatcher->getDefaultDriver() - ); - - try { - $elements = []; - foreach ($this->element->findElements($by) as $element) { - $elements[] = $this->newElement($element); - } - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - $this->dispatch( - 'afterFindBy', - $by, - $this, - $this->dispatcher->getDefaultDriver() - ); - - return $elements; - } - - /** - * @throws WebDriverException - * @return $this - */ - public function clear() - { - try { - $this->element->clear(); - - return $this; - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @param string $attribute_name - * @throws WebDriverException - * @return string - */ - public function getAttribute($attribute_name) - { - try { - return $this->element->getAttribute($attribute_name); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @param string $css_property_name - * @throws WebDriverException - * @return string - */ - public function getCSSValue($css_property_name) - { - try { - return $this->element->getCSSValue($css_property_name); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return WebDriverPoint - */ - public function getLocation() - { - try { - return $this->element->getLocation(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return WebDriverPoint - */ - public function getLocationOnScreenOnceScrolledIntoView() - { - try { - return $this->element->getLocationOnScreenOnceScrolledIntoView(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @return WebDriverCoordinates - */ - public function getCoordinates() - { - try { - return $this->element->getCoordinates(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return WebDriverDimension - */ - public function getSize() - { - try { - return $this->element->getSize(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return string - */ - public function getTagName() - { - try { - return $this->element->getTagName(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return string - */ - public function getText() - { - try { - return $this->element->getText(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return bool - */ - public function isDisplayed() - { - try { - return $this->element->isDisplayed(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return bool - */ - public function isEnabled() - { - try { - return $this->element->isEnabled(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return bool - */ - public function isSelected() - { - try { - return $this->element->isSelected(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return $this - */ - public function submit() - { - try { - $this->element->submit(); - - return $this; - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * @throws WebDriverException - * @return string - */ - public function getID() - { - try { - return $this->element->getID(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - /** - * Test if two element IDs refer to the same DOM element. - * - * @return bool - */ - public function equals(WebDriverElement $other) - { - try { - return $this->element->equals($other); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - public function takeElementScreenshot($save_as = null) - { - try { - return $this->element->takeElementScreenshot($save_as); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - public function getShadowRoot() - { - try { - return $this->element->getShadowRoot(); - } catch (WebDriverException $exception) { - $this->dispatchOnException($exception); - throw $exception; - } - } - - protected function dispatchOnException(WebDriverException $exception) - { - $this->dispatch( - 'onException', - $exception, - $this->dispatcher->getDefaultDriver() - ); - } - - /** - * @param mixed $method - * @param mixed ...$arguments - */ - protected function dispatch($method, ...$arguments) - { - if (!$this->dispatcher) { - return; - } - - $this->dispatcher->dispatch($method, $arguments); - } - - /** - * @return static - */ - protected function newElement(WebDriverElement $element) - { - return new static($element, $this->getDispatcher()); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Support/IsElementDisplayedAtom.php b/vendor/php-webdriver/webdriver/lib/Support/IsElementDisplayedAtom.php deleted file mode 100644 index d95e4f0..0000000 --- a/vendor/php-webdriver/webdriver/lib/Support/IsElementDisplayedAtom.php +++ /dev/null @@ -1,71 +0,0 @@ -driver = $driver; - } - - public static function match($browserName) - { - return !in_array($browserName, self::BROWSERS_WITH_ENDPOINT_SUPPORT, true); - } - - public function execute($params) - { - $element = new RemoteWebElement( - new RemoteExecuteMethod($this->driver), - $params[':id'], - $this->driver->isW3cCompliant() - ); - - return $this->executeAtom('isElementDisplayed', $element); - } - - protected function executeAtom($atomName, ...$params) - { - return $this->driver->executeScript( - sprintf('%s; return (%s).apply(null, arguments);', $this->loadAtomScript($atomName), $atomName), - $params - ); - } - - private function loadAtomScript($atomName) - { - return file_get_contents(__DIR__ . '/../scripts/' . $atomName . '.js'); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Support/ScreenshotHelper.php b/vendor/php-webdriver/webdriver/lib/Support/ScreenshotHelper.php deleted file mode 100644 index 956f561..0000000 --- a/vendor/php-webdriver/webdriver/lib/Support/ScreenshotHelper.php +++ /dev/null @@ -1,81 +0,0 @@ -executor = $executor; - } - - /** - * @param string|null $saveAs - * @throws WebDriverException - * @return string - */ - public function takePageScreenshot($saveAs = null) - { - $commandToExecute = [DriverCommand::SCREENSHOT]; - - return $this->takeScreenshot($commandToExecute, $saveAs); - } - - public function takeElementScreenshot($elementId, $saveAs = null) - { - $commandToExecute = [DriverCommand::TAKE_ELEMENT_SCREENSHOT, [':id' => $elementId]]; - - return $this->takeScreenshot($commandToExecute, $saveAs); - } - - private function takeScreenshot(array $commandToExecute, $saveAs = null) - { - $response = $this->executor->execute(...$commandToExecute); - - if (!is_string($response)) { - throw UnexpectedResponseException::forError( - 'Error taking screenshot, no data received from the remote end' - ); - } - - $screenshot = base64_decode($response, true); - - if ($screenshot === false) { - throw UnexpectedResponseException::forError('Error decoding screenshot data'); - } - - if ($saveAs !== null) { - $this->saveScreenshotToPath($screenshot, $saveAs); - } - - return $screenshot; - } - - private function saveScreenshotToPath($screenshot, $path) - { - $this->createDirectoryIfNotExists(dirname($path)); - - file_put_contents($path, $screenshot); - } - - private function createDirectoryIfNotExists($directoryPath) - { - if (!file_exists($directoryPath)) { - if (!mkdir($directoryPath, 0777, true) && !is_dir($directoryPath)) { - throw IOException::forFileError('Directory cannot be not created', $directoryPath); - } - } - } -} diff --git a/vendor/php-webdriver/webdriver/lib/Support/XPathEscaper.php b/vendor/php-webdriver/webdriver/lib/Support/XPathEscaper.php deleted file mode 100644 index eb85081..0000000 --- a/vendor/php-webdriver/webdriver/lib/Support/XPathEscaper.php +++ /dev/null @@ -1,32 +0,0 @@ - `concat('foo', "'" ,'"bar')` - * - * @param string $xpathToEscape The xpath to be converted. - * @return string The escaped string. - */ - public static function escapeQuotes($xpathToEscape) - { - // Single quotes not present => we can quote in them - if (mb_strpos($xpathToEscape, "'") === false) { - return sprintf("'%s'", $xpathToEscape); - } - - // Double quotes not present => we can quote in them - if (mb_strpos($xpathToEscape, '"') === false) { - return sprintf('"%s"', $xpathToEscape); - } - - // Both single and double quotes are present - return sprintf( - "concat('%s')", - str_replace("'", "', \"'\" ,'", $xpathToEscape) - ); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriver.php b/vendor/php-webdriver/webdriver/lib/WebDriver.php deleted file mode 100755 index 52120a7..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriver.php +++ /dev/null @@ -1,143 +0,0 @@ -wait(20, 1000)->until( - * WebDriverExpectedCondition::titleIs('WebDriver Page') - * ); - * - * @param int $timeout_in_second - * @param int $interval_in_millisecond - * @return WebDriverWait - */ - public function wait( - $timeout_in_second = 30, - $interval_in_millisecond = 250 - ); - - /** - * An abstraction for managing stuff you would do in a browser menu. For - * example, adding and deleting cookies. - * - * @return WebDriverOptions - */ - public function manage(); - - /** - * An abstraction allowing the driver to access the browser's history and to - * navigate to a given URL. - * - * @return WebDriverNavigationInterface - * @see WebDriverNavigation - */ - public function navigate(); - - /** - * Switch to a different window or frame. - * - * @return WebDriverTargetLocator - * @see WebDriverTargetLocator - */ - public function switchTo(); - - // TODO: Add in next major release (BC) - ///** - // * @return WebDriverTouchScreen - // */ - //public function getTouch(); - - /** - * @param string $name - * @param array $params - * @return mixed - */ - public function execute($name, $params); - - // TODO: Add in next major release (BC) - ///** - // * Execute custom commands on remote end. - // * For example vendor-specific commands or other commands not implemented by php-webdriver. - // * - // * @see https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands - // * @param string $endpointUrl - // * @param string $method - // * @param array $params - // * @return mixed|null - // */ - //public function executeCustomCommand($endpointUrl, $method = 'GET', $params = []); -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverAction.php b/vendor/php-webdriver/webdriver/lib/WebDriverAction.php deleted file mode 100644 index 3b3a784..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverAction.php +++ /dev/null @@ -1,11 +0,0 @@ -executor = $executor; - } - - /** - * Accept alert - * - * @return WebDriverAlert The instance. - */ - public function accept() - { - $this->executor->execute(DriverCommand::ACCEPT_ALERT); - - return $this; - } - - /** - * Dismiss alert - * - * @return WebDriverAlert The instance. - */ - public function dismiss() - { - $this->executor->execute(DriverCommand::DISMISS_ALERT); - - return $this; - } - - /** - * Get alert text - * - * @return string - */ - public function getText() - { - return $this->executor->execute(DriverCommand::GET_ALERT_TEXT); - } - - /** - * Send keystrokes to javascript prompt() dialog - * - * @param string $value - * @return WebDriverAlert - */ - public function sendKeys($value) - { - $this->executor->execute( - DriverCommand::SET_ALERT_VALUE, - ['text' => $value] - ); - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverBy.php b/vendor/php-webdriver/webdriver/lib/WebDriverBy.php deleted file mode 100644 index 4bead67..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverBy.php +++ /dev/null @@ -1,134 +0,0 @@ -mechanism = $mechanism; - $this->value = $value; - } - - /** - * @return string - */ - public function getMechanism() - { - return $this->mechanism; - } - - /** - * @return string - */ - public function getValue() - { - return $this->value; - } - - /** - * Locates elements whose class name contains the search value; compound class - * names are not permitted. - * - * @param string $class_name - * @return static - */ - public static function className($class_name) - { - return new static('class name', $class_name); - } - - /** - * Locates elements matching a CSS selector. - * - * @param string $css_selector - * @return static - */ - public static function cssSelector($css_selector) - { - return new static('css selector', $css_selector); - } - - /** - * Locates elements whose ID attribute matches the search value. - * - * @param string $id - * @return static - */ - public static function id($id) - { - return new static('id', $id); - } - - /** - * Locates elements whose NAME attribute matches the search value. - * - * @param string $name - * @return static - */ - public static function name($name) - { - return new static('name', $name); - } - - /** - * Locates anchor elements whose visible text matches the search value. - * - * @param string $link_text - * @return static - */ - public static function linkText($link_text) - { - return new static('link text', $link_text); - } - - /** - * Locates anchor elements whose visible text partially matches the search - * value. - * - * @param string $partial_link_text - * @return static - */ - public static function partialLinkText($partial_link_text) - { - return new static('partial link text', $partial_link_text); - } - - /** - * Locates elements whose tag name matches the search value. - * - * @param string $tag_name - * @return static - */ - public static function tagName($tag_name) - { - return new static('tag name', $tag_name); - } - - /** - * Locates elements matching an XPath expression. - * - * @param string $xpath - * @return static - */ - public static function xpath($xpath) - { - return new static('xpath', $xpath); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverCapabilities.php b/vendor/php-webdriver/webdriver/lib/WebDriverCapabilities.php deleted file mode 100644 index 75cb99d..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverCapabilities.php +++ /dev/null @@ -1,46 +0,0 @@ -type = $element->getAttribute('type'); - if ($this->type !== 'checkbox') { - throw new InvalidElementStateException('The input must be of type "checkbox".'); - } - } - - public function isMultiple() - { - return true; - } - - public function deselectAll() - { - foreach ($this->getRelatedElements() as $checkbox) { - $this->deselectOption($checkbox); - } - } - - public function deselectByIndex($index) - { - $this->byIndex($index, false); - } - - public function deselectByValue($value) - { - $this->byValue($value, false); - } - - public function deselectByVisibleText($text) - { - $this->byVisibleText($text, false, false); - } - - public function deselectByVisiblePartialText($text) - { - $this->byVisibleText($text, true, false); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverCommandExecutor.php b/vendor/php-webdriver/webdriver/lib/WebDriverCommandExecutor.php deleted file mode 100644 index 7f6bb3e..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverCommandExecutor.php +++ /dev/null @@ -1,17 +0,0 @@ -width = $width; - $this->height = $height; - } - - /** - * Get the height. - * - * @return int The height. - */ - public function getHeight() - { - return (int) $this->height; - } - - /** - * Get the width. - * - * @return int The width. - */ - public function getWidth() - { - return (int) $this->width; - } - - /** - * Check whether the given dimension is the same as the instance. - * - * @param WebDriverDimension $dimension The dimension to be compared with. - * @return bool Whether the height and the width are the same as the instance. - */ - public function equals(self $dimension) - { - return $this->height === $dimension->getHeight() && $this->width === $dimension->getWidth(); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverDispatcher.php b/vendor/php-webdriver/webdriver/lib/WebDriverDispatcher.php deleted file mode 100644 index fe1ecb0..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverDispatcher.php +++ /dev/null @@ -1,75 +0,0 @@ -driver = $driver; - - return $this; - } - - /** - * @return null|EventFiringWebDriver - */ - public function getDefaultDriver() - { - return $this->driver; - } - - /** - * @return $this - */ - public function register(WebDriverEventListener $listener) - { - $this->listeners[] = $listener; - - return $this; - } - - /** - * @return $this - */ - public function unregister(WebDriverEventListener $listener) - { - $key = array_search($listener, $this->listeners, true); - if ($key !== false) { - unset($this->listeners[$key]); - } - - return $this; - } - - /** - * @param mixed $method - * @param mixed $arguments - * @return $this - */ - public function dispatch($method, $arguments) - { - foreach ($this->listeners as $listener) { - call_user_func_array([$listener, $method], $arguments); - } - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverElement.php b/vendor/php-webdriver/webdriver/lib/WebDriverElement.php deleted file mode 100644 index 8ffa183..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverElement.php +++ /dev/null @@ -1,154 +0,0 @@ -apply = $apply; - } - - /** - * @return callable A callable function to be executed by WebDriverWait - */ - public function getApply() - { - return $this->apply; - } - - /** - * An expectation for checking the title of a page. - * - * @param string $title The expected title, which must be an exact match. - * @return static Condition returns whether current page title equals given string. - */ - public static function titleIs($title) - { - return new static( - function (WebDriver $driver) use ($title) { - return $title === $driver->getTitle(); - } - ); - } - - /** - * An expectation for checking substring of a page Title. - * - * @param string $title The expected substring of Title. - * @return static Condition returns whether current page title contains given string. - */ - public static function titleContains($title) - { - return new static( - function (WebDriver $driver) use ($title) { - return mb_strpos($driver->getTitle(), $title) !== false; - } - ); - } - - /** - * An expectation for checking current page title matches the given regular expression. - * - * @param string $titleRegexp The regular expression to test against. - * @return static Condition returns whether current page title matches the regular expression. - */ - public static function titleMatches($titleRegexp) - { - return new static( - function (WebDriver $driver) use ($titleRegexp) { - return (bool) preg_match($titleRegexp, $driver->getTitle()); - } - ); - } - - /** - * An expectation for checking the URL of a page. - * - * @param string $url The expected URL, which must be an exact match. - * @return static Condition returns whether current URL equals given one. - */ - public static function urlIs($url) - { - return new static( - function (WebDriver $driver) use ($url) { - return $url === $driver->getCurrentURL(); - } - ); - } - - /** - * An expectation for checking substring of the URL of a page. - * - * @param string $url The expected substring of the URL - * @return static Condition returns whether current URL contains given string. - */ - public static function urlContains($url) - { - return new static( - function (WebDriver $driver) use ($url) { - return mb_strpos($driver->getCurrentURL(), $url) !== false; - } - ); - } - - /** - * An expectation for checking current page URL matches the given regular expression. - * - * @param string $urlRegexp The regular expression to test against. - * @return static Condition returns whether current URL matches the regular expression. - */ - public static function urlMatches($urlRegexp) - { - return new static( - function (WebDriver $driver) use ($urlRegexp) { - return (bool) preg_match($urlRegexp, $driver->getCurrentURL()); - } - ); - } - - /** - * An expectation for checking that an element is present on the DOM of a page. - * This does not necessarily mean that the element is visible. - * - * @param WebDriverBy $by The locator used to find the element. - * @return static Condition returns the WebDriverElement which is located. - */ - public static function presenceOfElementLocated(WebDriverBy $by) - { - return new static( - function (WebDriver $driver) use ($by) { - try { - return $driver->findElement($by); - } catch (NoSuchElementException $e) { - return false; - } - } - ); - } - - /** - * An expectation for checking that there is at least one element present on a web page. - * - * @param WebDriverBy $by The locator used to find the element. - * @return static Condition return an array of WebDriverElement once they are located. - */ - public static function presenceOfAllElementsLocatedBy(WebDriverBy $by) - { - return new static( - function (WebDriver $driver) use ($by) { - $elements = $driver->findElements($by); - - return count($elements) > 0 ? $elements : null; - } - ); - } - - /** - * An expectation for checking that an element is present on the DOM of a page and visible. - * Visibility means that the element is not only displayed but also has a height and width that is greater than 0. - * - * @param WebDriverBy $by The locator used to find the element. - * @return static Condition returns the WebDriverElement which is located and visible. - */ - public static function visibilityOfElementLocated(WebDriverBy $by) - { - return new static( - function (WebDriver $driver) use ($by) { - try { - $element = $driver->findElement($by); - - return $element->isDisplayed() ? $element : null; - } catch (StaleElementReferenceException $e) { - return null; - } - } - ); - } - - /** - * An expectation for checking than at least one element in an array of elements is present on the - * DOM of a page and visible. - * Visibility means that the element is not only displayed but also has a height and width that is greater than 0. - * - * @param WebDriverBy $by The located used to find the element. - * @return static Condition returns the array of WebDriverElement that are located and visible. - */ - public static function visibilityOfAnyElementLocated(WebDriverBy $by) - { - return new static( - function (WebDriver $driver) use ($by) { - $elements = $driver->findElements($by); - $visibleElements = []; - - foreach ($elements as $element) { - try { - if ($element->isDisplayed()) { - $visibleElements[] = $element; - } - } catch (StaleElementReferenceException $e) { - } - } - - return count($visibleElements) > 0 ? $visibleElements : null; - } - ); - } - - /** - * An expectation for checking that an element, known to be present on the DOM of a page, is visible. - * Visibility means that the element is not only displayed but also has a height and width that is greater than 0. - * - * @param WebDriverElement $element The element to be checked. - * @return static Condition returns the same WebDriverElement once it is visible. - */ - public static function visibilityOf(WebDriverElement $element) - { - return new static( - function () use ($element) { - return $element->isDisplayed() ? $element : null; - } - ); - } - - /** - * An expectation for checking if the given text is present in the specified element. - * To check exact text match use elementTextIs() condition. - * - * @codeCoverageIgnore - * @deprecated Use WebDriverExpectedCondition::elementTextContains() instead - * @param WebDriverBy $by The locator used to find the element. - * @param string $text The text to be presented in the element. - * @return static Condition returns whether the text is present in the element. - */ - public static function textToBePresentInElement(WebDriverBy $by, $text) - { - return self::elementTextContains($by, $text); - } - - /** - * An expectation for checking if the given text is present in the specified element. - * To check exact text match use elementTextIs() condition. - * - * @param WebDriverBy $by The locator used to find the element. - * @param string $text The text to be presented in the element. - * @return static Condition returns whether the partial text is present in the element. - */ - public static function elementTextContains(WebDriverBy $by, $text) - { - return new static( - function (WebDriver $driver) use ($by, $text) { - try { - $element_text = $driver->findElement($by)->getText(); - - return mb_strpos($element_text, $text) !== false; - } catch (StaleElementReferenceException $e) { - return null; - } - } - ); - } - - /** - * An expectation for checking if the given text exactly equals the text in specified element. - * To check only partial substring of the text use elementTextContains() condition. - * - * @param WebDriverBy $by The locator used to find the element. - * @param string $text The expected text of the element. - * @return static Condition returns whether the element has text value equal to given one. - */ - public static function elementTextIs(WebDriverBy $by, $text) - { - return new static( - function (WebDriver $driver) use ($by, $text) { - try { - return $driver->findElement($by)->getText() == $text; - } catch (StaleElementReferenceException $e) { - return null; - } - } - ); - } - - /** - * An expectation for checking if the given regular expression matches the text in specified element. - * - * @param WebDriverBy $by The locator used to find the element. - * @param string $regexp The regular expression to test against. - * @return static Condition returns whether the element has text value equal to given one. - */ - public static function elementTextMatches(WebDriverBy $by, $regexp) - { - return new static( - function (WebDriver $driver) use ($by, $regexp) { - try { - return (bool) preg_match($regexp, $driver->findElement($by)->getText()); - } catch (StaleElementReferenceException $e) { - return null; - } - } - ); - } - - /** - * An expectation for checking if the given text is present in the specified elements value attribute. - * - * @codeCoverageIgnore - * @deprecated Use WebDriverExpectedCondition::elementValueContains() instead - * @param WebDriverBy $by The locator used to find the element. - * @param string $text The text to be presented in the element value. - * @return static Condition returns whether the text is present in value attribute. - */ - public static function textToBePresentInElementValue(WebDriverBy $by, $text) - { - return self::elementValueContains($by, $text); - } - - /** - * An expectation for checking if the given text is present in the specified elements value attribute. - * - * @param WebDriverBy $by The locator used to find the element. - * @param string $text The text to be presented in the element value. - * @return static Condition returns whether the text is present in value attribute. - */ - public static function elementValueContains(WebDriverBy $by, $text) - { - return new static( - function (WebDriver $driver) use ($by, $text) { - try { - $element_text = $driver->findElement($by)->getAttribute('value'); - - return mb_strpos($element_text, $text) !== false; - } catch (StaleElementReferenceException $e) { - return null; - } - } - ); - } - - /** - * Expectation for checking if iFrame exists. If iFrame exists switches driver's focus to the iFrame. - * - * @param string $frame_locator The locator used to find the iFrame - * expected to be either the id or name value of the i/frame - * @return static Condition returns object focused on new frame when frame is found, false otherwise. - */ - public static function frameToBeAvailableAndSwitchToIt($frame_locator) - { - return new static( - function (WebDriver $driver) use ($frame_locator) { - try { - return $driver->switchTo()->frame($frame_locator); - } catch (NoSuchFrameException $e) { - return false; - } - } - ); - } - - /** - * An expectation for checking that an element is either invisible or not present on the DOM. - * - * @param WebDriverBy $by The locator used to find the element. - * @return static Condition returns whether no visible element located. - */ - public static function invisibilityOfElementLocated(WebDriverBy $by) - { - return new static( - function (WebDriver $driver) use ($by) { - try { - return !$driver->findElement($by)->isDisplayed(); - } catch (NoSuchElementException|StaleElementReferenceException $e) { - return true; - } - } - ); - } - - /** - * An expectation for checking that an element with text is either invisible or not present on the DOM. - * - * @param WebDriverBy $by The locator used to find the element. - * @param string $text The text of the element. - * @return static Condition returns whether the text is found in the element located. - */ - public static function invisibilityOfElementWithText(WebDriverBy $by, $text) - { - return new static( - function (WebDriver $driver) use ($by, $text) { - try { - return !($driver->findElement($by)->getText() === $text); - } catch (NoSuchElementException|StaleElementReferenceException $e) { - return true; - } - } - ); - } - - /** - * An expectation for checking an element is visible and enabled such that you can click it. - * - * @param WebDriverBy $by The locator used to find the element - * @return static Condition return the WebDriverElement once it is located, visible and clickable. - */ - public static function elementToBeClickable(WebDriverBy $by) - { - $visibility_of_element_located = self::visibilityOfElementLocated($by); - - return new static( - function (WebDriver $driver) use ($visibility_of_element_located) { - $element = call_user_func( - $visibility_of_element_located->getApply(), - $driver - ); - - try { - if ($element !== null && $element->isEnabled()) { - return $element; - } - - return null; - } catch (StaleElementReferenceException $e) { - return null; - } - } - ); - } - - /** - * Wait until an element is no longer attached to the DOM. - * - * @param WebDriverElement $element The element to wait for. - * @return static Condition returns whether the element is still attached to the DOM. - */ - public static function stalenessOf(WebDriverElement $element) - { - return new static( - function () use ($element) { - try { - $element->isEnabled(); - - return false; - } catch (StaleElementReferenceException $e) { - return true; - } - } - ); - } - - /** - * Wrapper for a condition, which allows for elements to update by redrawing. - * - * This works around the problem of conditions which have two parts: find an element and then check for some - * condition on it. For these conditions it is possible that an element is located and then subsequently it is - * redrawn on the client. When this happens a StaleElementReferenceException is thrown when the second part of - * the condition is checked. - * - * @param WebDriverExpectedCondition $condition The condition wrapped. - * @return static Condition returns the return value of the getApply() of the given condition. - */ - public static function refreshed(self $condition) - { - return new static( - function (WebDriver $driver) use ($condition) { - try { - return call_user_func($condition->getApply(), $driver); - } catch (StaleElementReferenceException $e) { - return null; - } - } - ); - } - - /** - * An expectation for checking if the given element is selected. - * - * @param mixed $element_or_by Either the element or the locator. - * @return static Condition returns whether the element is selected. - */ - public static function elementToBeSelected($element_or_by) - { - return self::elementSelectionStateToBe( - $element_or_by, - true - ); - } - - /** - * An expectation for checking if the given element is selected. - * - * @param mixed $element_or_by Either the element or the locator. - * @param bool $selected The required state. - * @return static Condition returns whether the element is selected. - */ - public static function elementSelectionStateToBe($element_or_by, $selected) - { - if ($element_or_by instanceof WebDriverElement) { - return new static( - function () use ($element_or_by, $selected) { - return $element_or_by->isSelected() === $selected; - } - ); - } - - if ($element_or_by instanceof WebDriverBy) { - return new static( - function (WebDriver $driver) use ($element_or_by, $selected) { - try { - $element = $driver->findElement($element_or_by); - - return $element->isSelected() === $selected; - } catch (StaleElementReferenceException $e) { - return null; - } - } - ); - } - - throw LogicException::forError('Instance of either WebDriverElement or WebDriverBy must be given'); - } - - /** - * An expectation for whether an alert() box is present. - * - * @return static Condition returns WebDriverAlert if alert() is present, null otherwise. - */ - public static function alertIsPresent() - { - return new static( - function (WebDriver $driver) { - try { - // Unlike the Java code, we get a WebDriverAlert object regardless - // of whether there is an alert. Calling getText() will throw - // an exception if it is not really there. - $alert = $driver->switchTo()->alert(); - $alert->getText(); - - return $alert; - } catch (NoSuchAlertException $e) { - return null; - } - } - ); - } - - /** - * An expectation checking the number of opened windows. - * - * @param int $expectedNumberOfWindows - * @return static - */ - public static function numberOfWindowsToBe($expectedNumberOfWindows) - { - return new static( - function (WebDriver $driver) use ($expectedNumberOfWindows) { - return count($driver->getWindowHandles()) == $expectedNumberOfWindows; - } - ); - } - - /** - * An expectation with the logical opposite condition of the given condition. - * - * @param WebDriverExpectedCondition $condition The condition to be negated. - * @return mixed The negation of the result of the given condition. - */ - public static function not(self $condition) - { - return new static( - function (WebDriver $driver) use ($condition) { - $result = call_user_func($condition->getApply(), $driver); - - return !$result; - } - ); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverHasInputDevices.php b/vendor/php-webdriver/webdriver/lib/WebDriverHasInputDevices.php deleted file mode 100644 index efe41ae..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverHasInputDevices.php +++ /dev/null @@ -1,19 +0,0 @@ -executor = $executor; - } - - public function back() - { - $this->executor->execute(DriverCommand::GO_BACK); - - return $this; - } - - public function forward() - { - $this->executor->execute(DriverCommand::GO_FORWARD); - - return $this; - } - - public function refresh() - { - $this->executor->execute(DriverCommand::REFRESH); - - return $this; - } - - public function to($url) - { - $params = ['url' => (string) $url]; - $this->executor->execute(DriverCommand::GET, $params); - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverNavigationInterface.php b/vendor/php-webdriver/webdriver/lib/WebDriverNavigationInterface.php deleted file mode 100644 index 6fcd06e..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverNavigationInterface.php +++ /dev/null @@ -1,43 +0,0 @@ -executor = $executor; - $this->isW3cCompliant = $isW3cCompliant; - } - - /** - * Add a specific cookie. - * - * @see Cookie for description of possible cookie properties - * @param Cookie|array $cookie Cookie object. May be also created from array for compatibility reasons. - * @return WebDriverOptions The current instance. - */ - public function addCookie($cookie) - { - if (is_array($cookie)) { // @todo @deprecated remove in 2.0 - $cookie = Cookie::createFromArray($cookie); - } - if (!$cookie instanceof Cookie) { - throw LogicException::forError('Cookie must be set from instance of Cookie class or from array.'); - } - - $this->executor->execute( - DriverCommand::ADD_COOKIE, - ['cookie' => $cookie->toArray()] - ); - - return $this; - } - - /** - * Delete all the cookies that are currently visible. - * - * @return WebDriverOptions The current instance. - */ - public function deleteAllCookies() - { - $this->executor->execute(DriverCommand::DELETE_ALL_COOKIES); - - return $this; - } - - /** - * Delete the cookie with the given name. - * - * @param string $name - * @return WebDriverOptions The current instance. - */ - public function deleteCookieNamed($name) - { - $this->executor->execute( - DriverCommand::DELETE_COOKIE, - [':name' => $name] - ); - - return $this; - } - - /** - * Get the cookie with a given name. - * - * @param string $name - * @throws NoSuchCookieException In W3C compliant mode if no cookie with the given name is present - * @return Cookie|null The cookie, or null in JsonWire mode if no cookie with the given name is present - */ - public function getCookieNamed($name) - { - if ($this->isW3cCompliant) { - $cookieArray = $this->executor->execute( - DriverCommand::GET_NAMED_COOKIE, - [':name' => $name] - ); - - if (!is_array($cookieArray)) { // Microsoft Edge returns null even in W3C mode => emulate proper behavior - throw new NoSuchCookieException('no such cookie'); - } - - return Cookie::createFromArray($cookieArray); - } - - $cookies = $this->getCookies(); - foreach ($cookies as $cookie) { - if ($cookie['name'] === $name) { - return $cookie; - } - } - - return null; - } - - /** - * Get all the cookies for the current domain. - * - * @return Cookie[] The array of cookies presented. - */ - public function getCookies() - { - $cookieArrays = $this->executor->execute(DriverCommand::GET_ALL_COOKIES); - if (!is_array($cookieArrays)) { // Microsoft Edge returns null if there are no cookies... - return []; - } - - $cookies = []; - foreach ($cookieArrays as $cookieArray) { - $cookies[] = Cookie::createFromArray($cookieArray); - } - - return $cookies; - } - - /** - * Return the interface for managing driver timeouts. - * - * @return WebDriverTimeouts - */ - public function timeouts() - { - return new WebDriverTimeouts($this->executor, $this->isW3cCompliant); - } - - /** - * An abstraction allowing the driver to manipulate the browser's window - * - * @return WebDriverWindow - * @see WebDriverWindow - */ - public function window() - { - return new WebDriverWindow($this->executor, $this->isW3cCompliant); - } - - /** - * Get the log for a given log type. Log buffer is reset after each request. - * - * @param string $log_type The log type. - * @return array The list of log entries. - * @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#log-type - */ - public function getLog($log_type) - { - return $this->executor->execute( - DriverCommand::GET_LOG, - ['type' => $log_type] - ); - } - - /** - * Get available log types. - * - * @return array The list of available log types. - * @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#log-type - */ - public function getAvailableLogTypes() - { - return $this->executor->execute(DriverCommand::GET_AVAILABLE_LOG_TYPES); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverPlatform.php b/vendor/php-webdriver/webdriver/lib/WebDriverPlatform.php deleted file mode 100644 index a589f30..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverPlatform.php +++ /dev/null @@ -1,25 +0,0 @@ -x = $x; - $this->y = $y; - } - - /** - * Get the x-coordinate. - * - * @return int The x-coordinate of the point. - */ - public function getX() - { - return (int) $this->x; - } - - /** - * Get the y-coordinate. - * - * @return int The y-coordinate of the point. - */ - public function getY() - { - return (int) $this->y; - } - - /** - * Set the point to a new position. - * - * @param int $new_x - * @param int $new_y - * @return WebDriverPoint The same instance with updated coordinates. - */ - public function move($new_x, $new_y) - { - $this->x = $new_x; - $this->y = $new_y; - - return $this; - } - - /** - * Move the current by offsets. - * - * @param int $x_offset - * @param int $y_offset - * @return WebDriverPoint The same instance with updated coordinates. - */ - public function moveBy($x_offset, $y_offset) - { - $this->x += $x_offset; - $this->y += $y_offset; - - return $this; - } - - /** - * Check whether the given point is the same as the instance. - * - * @param WebDriverPoint $point The point to be compared with. - * @return bool Whether the x and y coordinates are the same as the instance. - */ - public function equals(self $point) - { - return $this->x === $point->getX() && - $this->y === $point->getY(); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverRadios.php b/vendor/php-webdriver/webdriver/lib/WebDriverRadios.php deleted file mode 100644 index aeaaaec..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverRadios.php +++ /dev/null @@ -1,52 +0,0 @@ -type = $element->getAttribute('type'); - if ($this->type !== 'radio') { - throw new InvalidElementStateException('The input must be of type "radio".'); - } - } - - public function isMultiple() - { - return false; - } - - public function deselectAll() - { - throw new UnsupportedOperationException('You cannot deselect radio buttons'); - } - - public function deselectByIndex($index) - { - throw new UnsupportedOperationException('You cannot deselect radio buttons'); - } - - public function deselectByValue($value) - { - throw new UnsupportedOperationException('You cannot deselect radio buttons'); - } - - public function deselectByVisibleText($text) - { - throw new UnsupportedOperationException('You cannot deselect radio buttons'); - } - - public function deselectByVisiblePartialText($text) - { - throw new UnsupportedOperationException('You cannot deselect radio buttons'); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverSearchContext.php b/vendor/php-webdriver/webdriver/lib/WebDriverSearchContext.php deleted file mode 100644 index 5fb1daa..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverSearchContext.php +++ /dev/null @@ -1,28 +0,0 @@ -` tag, providing helper methods to select and deselect options. - */ -class WebDriverSelect implements WebDriverSelectInterface -{ - /** @var WebDriverElement */ - private $element; - /** @var bool */ - private $isMulti; - - public function __construct(WebDriverElement $element) - { - $tag_name = $element->getTagName(); - - if ($tag_name !== 'select') { - throw new UnexpectedTagNameException('select', $tag_name); - } - $this->element = $element; - $value = $element->getAttribute('multiple'); - - /** - * There is a bug in safari webdriver that returns 'multiple' instead of 'true' which does not match the spec. - * Apple Feedback #FB12760673 - * - * @see https://www.w3.org/TR/webdriver2/#get-element-attribute - */ - $this->isMulti = $value === 'true' || $value === 'multiple'; - } - - public function isMultiple() - { - return $this->isMulti; - } - - public function getOptions() - { - return $this->element->findElements(WebDriverBy::tagName('option')); - } - - public function getAllSelectedOptions() - { - $selected_options = []; - foreach ($this->getOptions() as $option) { - if ($option->isSelected()) { - $selected_options[] = $option; - - if (!$this->isMultiple()) { - return $selected_options; - } - } - } - - return $selected_options; - } - - public function getFirstSelectedOption() - { - foreach ($this->getOptions() as $option) { - if ($option->isSelected()) { - return $option; - } - } - - throw new NoSuchElementException('No options are selected'); - } - - public function selectByIndex($index) - { - foreach ($this->getOptions() as $option) { - if ($option->getAttribute('index') === (string) $index) { - $this->selectOption($option); - - return; - } - } - - throw new NoSuchElementException(sprintf('Cannot locate option with index: %d', $index)); - } - - public function selectByValue($value) - { - $matched = false; - $xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']'; - $options = $this->element->findElements(WebDriverBy::xpath($xpath)); - - foreach ($options as $option) { - $this->selectOption($option); - if (!$this->isMultiple()) { - return; - } - $matched = true; - } - - if (!$matched) { - throw new NoSuchElementException( - sprintf('Cannot locate option with value: %s', $value) - ); - } - } - - public function selectByVisibleText($text) - { - $matched = false; - $xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']'; - $options = $this->element->findElements(WebDriverBy::xpath($xpath)); - - foreach ($options as $option) { - $this->selectOption($option); - if (!$this->isMultiple()) { - return; - } - $matched = true; - } - - // Since the mechanism of getting the text in xpath is not the same as - // webdriver, use the expensive getText() to check if nothing is matched. - if (!$matched) { - foreach ($this->getOptions() as $option) { - if ($option->getText() === $text) { - $this->selectOption($option); - if (!$this->isMultiple()) { - return; - } - $matched = true; - } - } - } - - if (!$matched) { - throw new NoSuchElementException( - sprintf('Cannot locate option with text: %s', $text) - ); - } - } - - public function selectByVisiblePartialText($text) - { - $matched = false; - $xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]'; - $options = $this->element->findElements(WebDriverBy::xpath($xpath)); - - foreach ($options as $option) { - $this->selectOption($option); - if (!$this->isMultiple()) { - return; - } - $matched = true; - } - - if (!$matched) { - throw new NoSuchElementException( - sprintf('Cannot locate option with text: %s', $text) - ); - } - } - - public function deselectAll() - { - if (!$this->isMultiple()) { - throw new UnsupportedOperationException('You may only deselect all options of a multi-select'); - } - - foreach ($this->getOptions() as $option) { - $this->deselectOption($option); - } - } - - public function deselectByIndex($index) - { - if (!$this->isMultiple()) { - throw new UnsupportedOperationException('You may only deselect options of a multi-select'); - } - - foreach ($this->getOptions() as $option) { - if ($option->getAttribute('index') === (string) $index) { - $this->deselectOption($option); - - return; - } - } - } - - public function deselectByValue($value) - { - if (!$this->isMultiple()) { - throw new UnsupportedOperationException('You may only deselect options of a multi-select'); - } - - $xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']'; - $options = $this->element->findElements(WebDriverBy::xpath($xpath)); - foreach ($options as $option) { - $this->deselectOption($option); - } - } - - public function deselectByVisibleText($text) - { - if (!$this->isMultiple()) { - throw new UnsupportedOperationException('You may only deselect options of a multi-select'); - } - - $xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']'; - $options = $this->element->findElements(WebDriverBy::xpath($xpath)); - foreach ($options as $option) { - $this->deselectOption($option); - } - } - - public function deselectByVisiblePartialText($text) - { - if (!$this->isMultiple()) { - throw new UnsupportedOperationException('You may only deselect options of a multi-select'); - } - - $xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]'; - $options = $this->element->findElements(WebDriverBy::xpath($xpath)); - foreach ($options as $option) { - $this->deselectOption($option); - } - } - - /** - * Mark option selected - */ - protected function selectOption(WebDriverElement $option) - { - if (!$option->isSelected()) { - $option->click(); - } - } - - /** - * Mark option not selected - */ - protected function deselectOption(WebDriverElement $option) - { - if ($option->isSelected()) { - $option->click(); - } - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverSelectInterface.php b/vendor/php-webdriver/webdriver/lib/WebDriverSelectInterface.php deleted file mode 100644 index 030a783..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverSelectInterface.php +++ /dev/null @@ -1,128 +0,0 @@ -Bar` - * - * @param string $value The value to match against. - * - * @throws NoSuchElementException - */ - public function selectByValue($value); - - /** - * Select all options that display text matching the argument. That is, when given "Bar" this would - * select an option like: - * - * `` - * - * @param string $text The visible text to match against. - * - * @throws NoSuchElementException - */ - public function selectByVisibleText($text); - - /** - * Select all options that display text partially matching the argument. That is, when given "Bar" this would - * select an option like: - * - * `` - * - * @param string $text The visible text to match against. - * - * @throws NoSuchElementException - */ - public function selectByVisiblePartialText($text); - - /** - * Deselect all options in multiple select tag. - * - * @throws UnsupportedOperationException If the SELECT does not support multiple selections - */ - public function deselectAll(); - - /** - * Deselect the option at the given index. - * - * @param int $index The index of the option. (0-based) - * @throws UnsupportedOperationException If the SELECT does not support multiple selections - */ - public function deselectByIndex($index); - - /** - * Deselect all options that have value attribute matching the argument. That is, when given "foo" this would - * deselect an option like: - * - * `` - * - * @param string $value The value to match against. - * @throws UnsupportedOperationException If the SELECT does not support multiple selections - */ - public function deselectByValue($value); - - /** - * Deselect all options that display text matching the argument. That is, when given "Bar" this would - * deselect an option like: - * - * `` - * - * @param string $text The visible text to match against. - * @throws UnsupportedOperationException If the SELECT does not support multiple selections - */ - public function deselectByVisibleText($text); - - /** - * Deselect all options that display text matching the argument. That is, when given "Bar" this would - * deselect an option like: - * - * `` - * - * @param string $text The visible text to match against. - * @throws UnsupportedOperationException If the SELECT does not support multiple selections - */ - public function deselectByVisiblePartialText($text); -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverTargetLocator.php b/vendor/php-webdriver/webdriver/lib/WebDriverTargetLocator.php deleted file mode 100644 index 8787f66..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverTargetLocator.php +++ /dev/null @@ -1,69 +0,0 @@ -executor = $executor; - $this->isW3cCompliant = $isW3cCompliant; - } - - /** - * Specify the amount of time the driver should wait when searching for an element if it is not immediately present. - * - * @param int $seconds Wait time in second. - * @return WebDriverTimeouts The current instance. - */ - public function implicitlyWait($seconds) - { - if ($this->isW3cCompliant) { - $this->executor->execute( - DriverCommand::IMPLICITLY_WAIT, - ['implicit' => $seconds * 1000] - ); - - return $this; - } - - $this->executor->execute( - DriverCommand::IMPLICITLY_WAIT, - ['ms' => $seconds * 1000] - ); - - return $this; - } - - /** - * Set the amount of time to wait for an asynchronous script to finish execution before throwing an error. - * - * @param int $seconds Wait time in second. - * @return WebDriverTimeouts The current instance. - */ - public function setScriptTimeout($seconds) - { - if ($this->isW3cCompliant) { - $this->executor->execute( - DriverCommand::SET_SCRIPT_TIMEOUT, - ['script' => $seconds * 1000] - ); - - return $this; - } - - $this->executor->execute( - DriverCommand::SET_SCRIPT_TIMEOUT, - ['ms' => $seconds * 1000] - ); - - return $this; - } - - /** - * Set the amount of time to wait for a page load to complete before throwing an error. - * - * @param int $seconds Wait time in second. - * @return WebDriverTimeouts The current instance. - */ - public function pageLoadTimeout($seconds) - { - if ($this->isW3cCompliant) { - $this->executor->execute( - DriverCommand::SET_SCRIPT_TIMEOUT, - ['pageLoad' => $seconds * 1000] - ); - - return $this; - } - - $this->executor->execute(DriverCommand::SET_TIMEOUT, [ - 'type' => 'page load', - 'ms' => $seconds * 1000, - ]); - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverUpAction.php b/vendor/php-webdriver/webdriver/lib/WebDriverUpAction.php deleted file mode 100644 index 3b045df..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverUpAction.php +++ /dev/null @@ -1,28 +0,0 @@ -x = $x; - $this->y = $y; - parent::__construct($touch_screen); - } - - public function perform() - { - $this->touchScreen->up($this->x, $this->y); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverWait.php b/vendor/php-webdriver/webdriver/lib/WebDriverWait.php deleted file mode 100644 index d2176b9..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverWait.php +++ /dev/null @@ -1,73 +0,0 @@ -driver = $driver; - $this->timeout = $timeout_in_second ?? 30; - $this->interval = $interval_in_millisecond ?: 250; - } - - /** - * Calls the function provided with the driver as an argument until the return value is not falsey. - * - * @param callable|WebDriverExpectedCondition $func_or_ec - * @param string $message - * - * @throws \Exception - * @throws NoSuchElementException - * @throws TimeoutException - * @return mixed The return value of $func_or_ec - */ - public function until($func_or_ec, $message = '') - { - $end = microtime(true) + $this->timeout; - $last_exception = null; - - while ($end > microtime(true)) { - try { - if ($func_or_ec instanceof WebDriverExpectedCondition) { - $ret_val = call_user_func($func_or_ec->getApply(), $this->driver); - } else { - $ret_val = call_user_func($func_or_ec, $this->driver); - } - if ($ret_val) { - return $ret_val; - } - } catch (NoSuchElementException $e) { - $last_exception = $e; - } - usleep($this->interval * 1000); - } - - if ($last_exception) { - throw $last_exception; - } - - throw new TimeoutException($message); - } -} diff --git a/vendor/php-webdriver/webdriver/lib/WebDriverWindow.php b/vendor/php-webdriver/webdriver/lib/WebDriverWindow.php deleted file mode 100644 index 2a69fe2..0000000 --- a/vendor/php-webdriver/webdriver/lib/WebDriverWindow.php +++ /dev/null @@ -1,188 +0,0 @@ -executor = $executor; - $this->isW3cCompliant = $isW3cCompliant; - } - - /** - * Get the position of the current window, relative to the upper left corner - * of the screen. - * - * @return WebDriverPoint The current window position. - */ - public function getPosition() - { - $position = $this->executor->execute( - DriverCommand::GET_WINDOW_POSITION, - [':windowHandle' => 'current'] - ); - - return new WebDriverPoint( - $position['x'], - $position['y'] - ); - } - - /** - * Get the size of the current window. This will return the outer window - * dimension, not just the view port. - * - * @return WebDriverDimension The current window size. - */ - public function getSize() - { - $size = $this->executor->execute( - DriverCommand::GET_WINDOW_SIZE, - [':windowHandle' => 'current'] - ); - - return new WebDriverDimension( - $size['width'], - $size['height'] - ); - } - - /** - * Minimizes the current window if it is not already minimized. - * - * @return WebDriverWindow The instance. - */ - public function minimize() - { - if (!$this->isW3cCompliant) { - throw new UnsupportedOperationException('Minimize window is only supported in W3C mode'); - } - - $this->executor->execute(DriverCommand::MINIMIZE_WINDOW, []); - - return $this; - } - - /** - * Maximizes the current window if it is not already maximized - * - * @return WebDriverWindow The instance. - */ - public function maximize() - { - if ($this->isW3cCompliant) { - $this->executor->execute(DriverCommand::MAXIMIZE_WINDOW, []); - } else { - $this->executor->execute( - DriverCommand::MAXIMIZE_WINDOW, - [':windowHandle' => 'current'] - ); - } - - return $this; - } - - /** - * Makes the current window full screen. - * - * @return WebDriverWindow The instance. - */ - public function fullscreen() - { - if (!$this->isW3cCompliant) { - throw new UnsupportedOperationException('The Fullscreen window command is only supported in W3C mode'); - } - - $this->executor->execute(DriverCommand::FULLSCREEN_WINDOW, []); - - return $this; - } - - /** - * Set the size of the current window. This will change the outer window - * dimension, not just the view port. - * - * @return WebDriverWindow The instance. - */ - public function setSize(WebDriverDimension $size) - { - $params = [ - 'width' => $size->getWidth(), - 'height' => $size->getHeight(), - ':windowHandle' => 'current', - ]; - $this->executor->execute(DriverCommand::SET_WINDOW_SIZE, $params); - - return $this; - } - - /** - * Set the position of the current window. This is relative to the upper left - * corner of the screen. - * - * @return WebDriverWindow The instance. - */ - public function setPosition(WebDriverPoint $position) - { - $params = [ - 'x' => $position->getX(), - 'y' => $position->getY(), - ':windowHandle' => 'current', - ]; - $this->executor->execute(DriverCommand::SET_WINDOW_POSITION, $params); - - return $this; - } - - /** - * Get the current browser orientation. - * - * @return string Either LANDSCAPE|PORTRAIT - */ - public function getScreenOrientation() - { - return $this->executor->execute(DriverCommand::GET_SCREEN_ORIENTATION); - } - - /** - * Set the browser orientation. The orientation should either - * LANDSCAPE|PORTRAIT - * - * @param string $orientation - * @throws IndexOutOfBoundsException - * @return WebDriverWindow The instance. - */ - public function setScreenOrientation($orientation) - { - $orientation = mb_strtoupper($orientation); - if (!in_array($orientation, ['PORTRAIT', 'LANDSCAPE'], true)) { - throw LogicException::forError('Orientation must be either PORTRAIT, or LANDSCAPE'); - } - - $this->executor->execute( - DriverCommand::SET_SCREEN_ORIENTATION, - ['orientation' => $orientation] - ); - - return $this; - } -} diff --git a/vendor/php-webdriver/webdriver/lib/scripts/isElementDisplayed.js b/vendor/php-webdriver/webdriver/lib/scripts/isElementDisplayed.js deleted file mode 100644 index f24bfa5..0000000 --- a/vendor/php-webdriver/webdriver/lib/scripts/isElementDisplayed.js +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Imported from WebdriverIO project. - * https://github.com/webdriverio/webdriverio/blob/main/packages/webdriverio/src/scripts/isElementDisplayed.ts - * - * Copyright (C) 2017 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * check if element is visible - * @param {HTMLElement} elem element to check - * @return {Boolean} true if element is within viewport - */ -function isElementDisplayed(element) { - function nodeIsElement(node) { - if (!node) { - return false; - } - - switch (node.nodeType) { - case Node.ELEMENT_NODE: - case Node.DOCUMENT_NODE: - case Node.DOCUMENT_FRAGMENT_NODE: - return true; - default: - return false; - } - } - function parentElementForElement(element) { - if (!element) { - return null; - } - return enclosingNodeOrSelfMatchingPredicate(element.parentNode, nodeIsElement); - } - function enclosingNodeOrSelfMatchingPredicate(targetNode, predicate) { - for (let node = targetNode; node && node !== targetNode.ownerDocument; node = node.parentNode) - if (predicate(node)) { - return node; - } - return null; - } - function enclosingElementOrSelfMatchingPredicate(targetElement, predicate) { - for (let element = targetElement; element && element !== targetElement.ownerDocument; element = parentElementForElement(element)) - if (predicate(element)) { - return element; - } - return null; - } - function cascadedStylePropertyForElement(element, property) { - if (!element || !property) { - return null; - } - // if document-fragment, skip it and use element.host instead. This happens - // when the element is inside a shadow root. - // window.getComputedStyle errors on document-fragment. - if (element instanceof ShadowRoot) { - element = element.host; - } - let computedStyle = window.getComputedStyle(element); - let computedStyleProperty = computedStyle.getPropertyValue(property); - if (computedStyleProperty && computedStyleProperty !== 'inherit') { - return computedStyleProperty; - } - // Ideally getPropertyValue would return the 'used' or 'actual' value, but - // it doesn't for legacy reasons. So we need to do our own poor man's cascade. - // Fall back to the first non-'inherit' value found in an ancestor. - // In any case, getPropertyValue will not return 'initial'. - // FIXME: will this incorrectly inherit non-inheritable CSS properties? - // I think all important non-inheritable properties (width, height, etc.) - // for our purposes here are specially resolved, so this may not be an issue. - // Specification is here: https://drafts.csswg.org/cssom/#resolved-values - let parentElement = parentElementForElement(element); - return cascadedStylePropertyForElement(parentElement, property); - } - function elementSubtreeHasNonZeroDimensions(element) { - let boundingBox = element.getBoundingClientRect(); - if (boundingBox.width > 0 && boundingBox.height > 0) { - return true; - } - // Paths can have a zero width or height. Treat them as shown if the stroke width is positive. - if (element.tagName.toUpperCase() === 'PATH' && boundingBox.width + boundingBox.height > 0) { - let strokeWidth = cascadedStylePropertyForElement(element, 'stroke-width'); - return !!strokeWidth && (parseInt(strokeWidth, 10) > 0); - } - let cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow'); - if (cascadedOverflow === 'hidden') { - return false; - } - // If the container's overflow is not hidden and it has zero size, consider the - // container to have non-zero dimensions if a child node has non-zero dimensions. - return Array.from(element.childNodes).some((childNode) => { - if (childNode.nodeType === Node.TEXT_NODE) { - return true; - } - if (nodeIsElement(childNode)) { - return elementSubtreeHasNonZeroDimensions(childNode); - } - return false; - }); - } - function elementOverflowsContainer(element) { - let cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow'); - if (cascadedOverflow !== 'hidden') { - return false; - } - // FIXME: this needs to take into account the scroll position of the element, - // the display modes of it and its ancestors, and the container it overflows. - // See Selenium's bot.dom.getOverflowState atom for an exhaustive list of edge cases. - return true; - } - function isElementSubtreeHiddenByOverflow(element) { - if (!element) { - return false; - } - if (!elementOverflowsContainer(element)) { - return false; - } - if (!element.childNodes.length) { - return false; - } - // This element's subtree is hidden by overflow if all child subtrees are as well. - return Array.from(element.childNodes).every((childNode) => { - // Returns true if the child node is overflowed or otherwise hidden. - // Base case: not an element, has zero size, scrolled out, or doesn't overflow container. - // Visibility of text nodes is controlled by parent - if (childNode.nodeType === Node.TEXT_NODE) { - return false; - } - if (!nodeIsElement(childNode)) { - return true; - } - if (!elementSubtreeHasNonZeroDimensions(childNode)) { - return true; - } - // Recurse. - return isElementSubtreeHiddenByOverflow(childNode); - }); - } - // walk up the tree testing for a shadow root - function isElementInsideShadowRoot(element) { - if (!element) { - return false; - } - if (element.parentNode && element.parentNode.host) { - return true; - } - return isElementInsideShadowRoot(element.parentNode); - } - // This is a partial reimplementation of Selenium's "element is displayed" algorithm. - // When the W3C specification's algorithm stabilizes, we should implement that. - // If this command is misdirected to the wrong document (and is NOT inside a shadow root), treat it as not shown. - if (!isElementInsideShadowRoot(element) && !document.contains(element)) { - return false; - } - // Special cases for specific tag names. - switch (element.tagName.toUpperCase()) { - case 'BODY': - return true; - case 'SCRIPT': - case 'NOSCRIPT': - return false; - case 'OPTGROUP': - case 'OPTION': { - // Option/optgroup are considered shown if the containing is considered not shown. - if (element.type === 'hidden') { - return false; - } - break; - // case 'MAP': - // FIXME: Selenium has special handling for elements. We don't do anything now. - default: - break; - } - if (cascadedStylePropertyForElement(element, 'visibility') !== 'visible') { - return false; - } - let hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element, (e) => { - return Number(cascadedStylePropertyForElement(e, 'opacity')) === 0; - }); - let hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element, (e) => { - return cascadedStylePropertyForElement(e, 'display') === 'none'; - }); - if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone) { - return false; - } - if (!elementSubtreeHasNonZeroDimensions(element)) { - return false; - } - if (isElementSubtreeHiddenByOverflow(element)) { - return false; - } - return true; -} - diff --git a/vendor/symfony/polyfill-mbstring/LICENSE b/vendor/symfony/polyfill-mbstring/LICENSE deleted file mode 100644 index 6e3afce..0000000 --- a/vendor/symfony/polyfill-mbstring/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2015-present Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/symfony/polyfill-mbstring/Mbstring.php b/vendor/symfony/polyfill-mbstring/Mbstring.php deleted file mode 100644 index 31e36a3..0000000 --- a/vendor/symfony/polyfill-mbstring/Mbstring.php +++ /dev/null @@ -1,1045 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Polyfill\Mbstring; - -/** - * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. - * - * Implemented: - * - mb_chr - Returns a specific character from its Unicode code point - * - mb_convert_encoding - Convert character encoding - * - mb_convert_variables - Convert character code in variable(s) - * - mb_decode_mimeheader - Decode string in MIME header field - * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED - * - mb_decode_numericentity - Decode HTML numeric string reference to character - * - mb_encode_numericentity - Encode character to HTML numeric string reference - * - mb_convert_case - Perform case folding on a string - * - mb_detect_encoding - Detect character encoding - * - mb_get_info - Get internal settings of mbstring - * - mb_http_input - Detect HTTP input character encoding - * - mb_http_output - Set/Get HTTP output character encoding - * - mb_internal_encoding - Set/Get internal character encoding - * - mb_list_encodings - Returns an array of all supported encodings - * - mb_ord - Returns the Unicode code point of a character - * - mb_output_handler - Callback function converts character encoding in output buffer - * - mb_scrub - Replaces ill-formed byte sequences with substitute characters - * - mb_strlen - Get string length - * - mb_strpos - Find position of first occurrence of string in a string - * - mb_strrpos - Find position of last occurrence of a string in a string - * - mb_str_split - Convert a string to an array - * - mb_strtolower - Make a string lowercase - * - mb_strtoupper - Make a string uppercase - * - mb_substitute_character - Set/Get substitution character - * - mb_substr - Get part of string - * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive - * - mb_stristr - Finds first occurrence of a string within another, case insensitive - * - mb_strrchr - Finds the last occurrence of a character in a string within another - * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive - * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive - * - mb_strstr - Finds first occurrence of a string within another - * - mb_strwidth - Return width of string - * - mb_substr_count - Count the number of substring occurrences - * - mb_ucfirst - Make a string's first character uppercase - * - mb_lcfirst - Make a string's first character lowercase - * - mb_trim - Strip whitespace (or other characters) from the beginning and end of a string - * - mb_ltrim - Strip whitespace (or other characters) from the beginning of a string - * - mb_rtrim - Strip whitespace (or other characters) from the end of a string - * - * Not implemented: - * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) - * - mb_ereg_* - Regular expression with multibyte support - * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable - * - mb_preferred_mime_name - Get MIME charset string - * - mb_regex_encoding - Returns current encoding for multibyte regex as string - * - mb_regex_set_options - Set/Get the default options for mbregex functions - * - mb_send_mail - Send encoded mail - * - mb_split - Split multibyte string using regular expression - * - mb_strcut - Get part of string - * - mb_strimwidth - Get truncated string with specified width - * - * @author Nicolas Grekas - * - * @internal - */ -final class Mbstring -{ - public const MB_CASE_FOLD = \PHP_INT_MAX; - - private const SIMPLE_CASE_FOLD = [ - ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], - ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], - ]; - - private static $encodingList = ['ASCII', 'UTF-8']; - private static $language = 'neutral'; - private static $internalEncoding = 'UTF-8'; - - public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) - { - if (\is_array($s)) { - $r = []; - foreach ($s as $str) { - $r[] = self::mb_convert_encoding($str, $toEncoding, $fromEncoding); - } - - return $r; - } - - if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) { - $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); - } else { - $fromEncoding = self::getEncoding($fromEncoding); - } - - $toEncoding = self::getEncoding($toEncoding); - - if ('BASE64' === $fromEncoding) { - $s = base64_decode($s); - $fromEncoding = $toEncoding; - } - - if ('BASE64' === $toEncoding) { - return base64_encode($s); - } - - if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { - if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { - $fromEncoding = 'Windows-1252'; - } - if ('UTF-8' !== $fromEncoding) { - $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); - } - - return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); - } - - if ('HTML-ENTITIES' === $fromEncoding) { - $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8'); - $fromEncoding = 'UTF-8'; - } - - return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); - } - - public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars) - { - $ok = true; - array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { - if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { - $ok = false; - } - }); - - return $ok ? $fromEncoding : false; - } - - public static function mb_decode_mimeheader($s) - { - return iconv_mime_decode($s, 2, self::$internalEncoding); - } - - public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) - { - trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING); - } - - public static function mb_decode_numericentity($s, $convmap, $encoding = null) - { - if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { - trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); - - return null; - } - - if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { - return false; - } - - if (null !== $encoding && !\is_scalar($encoding)) { - trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); - - return ''; // Instead of null (cf. mb_encode_numericentity). - } - - $s = (string) $s; - if ('' === $s) { - return ''; - } - - $encoding = self::getEncoding($encoding); - - if ('UTF-8' === $encoding) { - $encoding = null; - if (!preg_match('//u', $s)) { - $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); - } - } else { - $s = iconv($encoding, 'UTF-8//IGNORE', $s); - } - - $cnt = floor(\count($convmap) / 4) * 4; - - for ($i = 0; $i < $cnt; $i += 4) { - // collector_decode_htmlnumericentity ignores $convmap[$i + 3] - $convmap[$i] += $convmap[$i + 2]; - $convmap[$i + 1] += $convmap[$i + 2]; - } - - $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { - $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; - for ($i = 0; $i < $cnt; $i += 4) { - if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { - return self::mb_chr($c - $convmap[$i + 2]); - } - } - - return $m[0]; - }, $s); - - if (null === $encoding) { - return $s; - } - - return iconv('UTF-8', $encoding.'//IGNORE', $s); - } - - public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) - { - if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { - trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); - - return null; - } - - if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { - return false; - } - - if (null !== $encoding && !\is_scalar($encoding)) { - trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); - - return null; // Instead of '' (cf. mb_decode_numericentity). - } - - if (null !== $is_hex && !\is_scalar($is_hex)) { - trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING); - - return null; - } - - $s = (string) $s; - if ('' === $s) { - return ''; - } - - $encoding = self::getEncoding($encoding); - - if ('UTF-8' === $encoding) { - $encoding = null; - if (!preg_match('//u', $s)) { - $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); - } - } else { - $s = iconv($encoding, 'UTF-8//IGNORE', $s); - } - - static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; - - $cnt = floor(\count($convmap) / 4) * 4; - $i = 0; - $len = \strlen($s); - $result = ''; - - while ($i < $len) { - $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; - $uchr = substr($s, $i, $ulen); - $i += $ulen; - $c = self::mb_ord($uchr); - - for ($j = 0; $j < $cnt; $j += 4) { - if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { - $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; - $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; - continue 2; - } - } - $result .= $uchr; - } - - if (null === $encoding) { - return $result; - } - - return iconv('UTF-8', $encoding.'//IGNORE', $result); - } - - public static function mb_convert_case($s, $mode, $encoding = null) - { - $s = (string) $s; - if ('' === $s) { - return ''; - } - - $encoding = self::getEncoding($encoding); - - if ('UTF-8' === $encoding) { - $encoding = null; - if (!preg_match('//u', $s)) { - $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); - } - } else { - $s = iconv($encoding, 'UTF-8//IGNORE', $s); - } - - if (\MB_CASE_TITLE == $mode) { - static $titleRegexp = null; - if (null === $titleRegexp) { - $titleRegexp = self::getData('titleCaseRegexp'); - } - $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); - } else { - if (\MB_CASE_UPPER == $mode) { - static $upper = null; - if (null === $upper) { - $upper = self::getData('upperCase'); - } - $map = $upper; - } else { - if (self::MB_CASE_FOLD === $mode) { - static $caseFolding = null; - if (null === $caseFolding) { - $caseFolding = self::getData('caseFolding'); - } - $s = strtr($s, $caseFolding); - } - - static $lower = null; - if (null === $lower) { - $lower = self::getData('lowerCase'); - } - $map = $lower; - } - - static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; - - $i = 0; - $len = \strlen($s); - - while ($i < $len) { - $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; - $uchr = substr($s, $i, $ulen); - $i += $ulen; - - if (isset($map[$uchr])) { - $uchr = $map[$uchr]; - $nlen = \strlen($uchr); - - if ($nlen == $ulen) { - $nlen = $i; - do { - $s[--$nlen] = $uchr[--$ulen]; - } while ($ulen); - } else { - $s = substr_replace($s, $uchr, $i - $ulen, $ulen); - $len += $nlen - $ulen; - $i += $nlen - $ulen; - } - } - } - } - - if (null === $encoding) { - return $s; - } - - return iconv('UTF-8', $encoding.'//IGNORE', $s); - } - - public static function mb_internal_encoding($encoding = null) - { - if (null === $encoding) { - return self::$internalEncoding; - } - - $normalizedEncoding = self::getEncoding($encoding); - - if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) { - self::$internalEncoding = $normalizedEncoding; - - return true; - } - - if (80000 > \PHP_VERSION_ID) { - return false; - } - - throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding)); - } - - public static function mb_language($lang = null) - { - if (null === $lang) { - return self::$language; - } - - switch ($normalizedLang = strtolower($lang)) { - case 'uni': - case 'neutral': - self::$language = $normalizedLang; - - return true; - } - - if (80000 > \PHP_VERSION_ID) { - return false; - } - - throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang)); - } - - public static function mb_list_encodings() - { - return ['UTF-8']; - } - - public static function mb_encoding_aliases($encoding) - { - switch (strtoupper($encoding)) { - case 'UTF8': - case 'UTF-8': - return ['utf8']; - } - - return false; - } - - public static function mb_check_encoding($var = null, $encoding = null) - { - if (null === $encoding) { - if (null === $var) { - return false; - } - $encoding = self::$internalEncoding; - } - - if (!\is_array($var)) { - return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var); - } - - foreach ($var as $key => $value) { - if (!self::mb_check_encoding($key, $encoding)) { - return false; - } - if (!self::mb_check_encoding($value, $encoding)) { - return false; - } - } - - return true; - } - - public static function mb_detect_encoding($str, $encodingList = null, $strict = false) - { - if (null === $encodingList) { - $encodingList = self::$encodingList; - } else { - if (!\is_array($encodingList)) { - $encodingList = array_map('trim', explode(',', $encodingList)); - } - $encodingList = array_map('strtoupper', $encodingList); - } - - foreach ($encodingList as $enc) { - switch ($enc) { - case 'ASCII': - if (!preg_match('/[\x80-\xFF]/', $str)) { - return $enc; - } - break; - - case 'UTF8': - case 'UTF-8': - if (preg_match('//u', $str)) { - return 'UTF-8'; - } - break; - - default: - if (0 === strncmp($enc, 'ISO-8859-', 9)) { - return $enc; - } - } - } - - return false; - } - - public static function mb_detect_order($encodingList = null) - { - if (null === $encodingList) { - return self::$encodingList; - } - - if (!\is_array($encodingList)) { - $encodingList = array_map('trim', explode(',', $encodingList)); - } - $encodingList = array_map('strtoupper', $encodingList); - - foreach ($encodingList as $enc) { - switch ($enc) { - default: - if (strncmp($enc, 'ISO-8859-', 9)) { - return false; - } - // no break - case 'ASCII': - case 'UTF8': - case 'UTF-8': - } - } - - self::$encodingList = $encodingList; - - return true; - } - - public static function mb_strlen($s, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return \strlen($s); - } - - return @iconv_strlen($s, $encoding); - } - - public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return strpos($haystack, $needle, $offset); - } - - $needle = (string) $needle; - if ('' === $needle) { - if (80000 > \PHP_VERSION_ID) { - trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING); - - return false; - } - - return 0; - } - - return iconv_strpos($haystack, $needle, $offset, $encoding); - } - - public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return strrpos($haystack, $needle, $offset); - } - - if ($offset != (int) $offset) { - $offset = 0; - } elseif ($offset = (int) $offset) { - if ($offset < 0) { - if (0 > $offset += self::mb_strlen($needle)) { - $haystack = self::mb_substr($haystack, 0, $offset, $encoding); - } - $offset = 0; - } else { - $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); - } - } - - $pos = '' !== $needle || 80000 > \PHP_VERSION_ID - ? iconv_strrpos($haystack, $needle, $encoding) - : self::mb_strlen($haystack, $encoding); - - return false !== $pos ? $offset + $pos : false; - } - - public static function mb_str_split($string, $split_length = 1, $encoding = null) - { - if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { - trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING); - - return null; - } - - if (1 > $split_length = (int) $split_length) { - if (80000 > \PHP_VERSION_ID) { - trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); - - return false; - } - - throw new \ValueError('Argument #2 ($length) must be greater than 0'); - } - - if (null === $encoding) { - $encoding = mb_internal_encoding(); - } - - if ('UTF-8' === $encoding = self::getEncoding($encoding)) { - $rx = '/('; - while (65535 < $split_length) { - $rx .= '.{65535}'; - $split_length -= 65535; - } - $rx .= '.{'.$split_length.'})/us'; - - return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); - } - - $result = []; - $length = mb_strlen($string, $encoding); - - for ($i = 0; $i < $length; $i += $split_length) { - $result[] = mb_substr($string, $i, $split_length, $encoding); - } - - return $result; - } - - public static function mb_strtolower($s, $encoding = null) - { - return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding); - } - - public static function mb_strtoupper($s, $encoding = null) - { - return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding); - } - - public static function mb_substitute_character($c = null) - { - if (null === $c) { - return 'none'; - } - if (0 === strcasecmp($c, 'none')) { - return true; - } - if (80000 > \PHP_VERSION_ID) { - return false; - } - if (\is_int($c) || 'long' === $c || 'entity' === $c) { - return false; - } - - throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint'); - } - - public static function mb_substr($s, $start, $length = null, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return (string) substr($s, $start, null === $length ? 2147483647 : $length); - } - - if ($start < 0) { - $start = iconv_strlen($s, $encoding) + $start; - if ($start < 0) { - $start = 0; - } - } - - if (null === $length) { - $length = 2147483647; - } elseif ($length < 0) { - $length = iconv_strlen($s, $encoding) + $length - $start; - if ($length < 0) { - return ''; - } - } - - return (string) iconv_substr($s, $start, $length, $encoding); - } - - public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) - { - [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [ - self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding), - self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding), - ]); - - return self::mb_strpos($haystack, $needle, $offset, $encoding); - } - - public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) - { - $pos = self::mb_stripos($haystack, $needle, 0, $encoding); - - return self::getSubpart($pos, $part, $haystack, $encoding); - } - - public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - $pos = strrpos($haystack, $needle); - } else { - $needle = self::mb_substr($needle, 0, 1, $encoding); - $pos = iconv_strrpos($haystack, $needle, $encoding); - } - - return self::getSubpart($pos, $part, $haystack, $encoding); - } - - public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) - { - $needle = self::mb_substr($needle, 0, 1, $encoding); - $pos = self::mb_strripos($haystack, $needle, $encoding); - - return self::getSubpart($pos, $part, $haystack, $encoding); - } - - public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) - { - $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding); - $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding); - - $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack); - $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle); - - return self::mb_strrpos($haystack, $needle, $offset, $encoding); - } - - public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) - { - $pos = strpos($haystack, $needle); - if (false === $pos) { - return false; - } - if ($part) { - return substr($haystack, 0, $pos); - } - - return substr($haystack, $pos); - } - - public static function mb_get_info($type = 'all') - { - $info = [ - 'internal_encoding' => self::$internalEncoding, - 'http_output' => 'pass', - 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', - 'func_overload' => 0, - 'func_overload_list' => 'no overload', - 'mail_charset' => 'UTF-8', - 'mail_header_encoding' => 'BASE64', - 'mail_body_encoding' => 'BASE64', - 'illegal_chars' => 0, - 'encoding_translation' => 'Off', - 'language' => self::$language, - 'detect_order' => self::$encodingList, - 'substitute_character' => 'none', - 'strict_detection' => 'Off', - ]; - - if ('all' === $type) { - return $info; - } - if (isset($info[$type])) { - return $info[$type]; - } - - return false; - } - - public static function mb_http_input($type = '') - { - return false; - } - - public static function mb_http_output($encoding = null) - { - return null !== $encoding ? 'pass' === $encoding : 'pass'; - } - - public static function mb_strwidth($s, $encoding = null) - { - $encoding = self::getEncoding($encoding); - - if ('UTF-8' !== $encoding) { - $s = iconv($encoding, 'UTF-8//IGNORE', $s); - } - - $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); - - return ($wide << 1) + iconv_strlen($s, 'UTF-8'); - } - - public static function mb_substr_count($haystack, $needle, $encoding = null) - { - return substr_count($haystack, $needle); - } - - public static function mb_output_handler($contents, $status) - { - return $contents; - } - - public static function mb_chr($code, $encoding = null) - { - if (0x80 > $code %= 0x200000) { - $s = \chr($code); - } elseif (0x800 > $code) { - $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); - } elseif (0x10000 > $code) { - $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); - } else { - $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); - } - - if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { - $s = mb_convert_encoding($s, $encoding, 'UTF-8'); - } - - return $s; - } - - public static function mb_ord($s, $encoding = null) - { - if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { - $s = mb_convert_encoding($s, 'UTF-8', $encoding); - } - - if (1 === \strlen($s)) { - return \ord($s); - } - - $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; - if (0xF0 <= $code) { - return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; - } - if (0xE0 <= $code) { - return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; - } - if (0xC0 <= $code) { - return (($code - 0xC0) << 6) + $s[2] - 0x80; - } - - return $code; - } - - public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string - { - if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) { - throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH'); - } - - if (null === $encoding) { - $encoding = self::mb_internal_encoding(); - } else { - self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given'); - } - - if (self::mb_strlen($pad_string, $encoding) <= 0) { - throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string'); - } - - $paddingRequired = $length - self::mb_strlen($string, $encoding); - - if ($paddingRequired < 1) { - return $string; - } - - switch ($pad_type) { - case \STR_PAD_LEFT: - return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string; - case \STR_PAD_RIGHT: - return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding); - default: - $leftPaddingLength = floor($paddingRequired / 2); - $rightPaddingLength = $paddingRequired - $leftPaddingLength; - - return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding); - } - } - - public static function mb_ucfirst(string $string, ?string $encoding = null): string - { - if (null === $encoding) { - $encoding = self::mb_internal_encoding(); - } else { - self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); - } - - $firstChar = mb_substr($string, 0, 1, $encoding); - $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding); - - return $firstChar.mb_substr($string, 1, null, $encoding); - } - - public static function mb_lcfirst(string $string, ?string $encoding = null): string - { - if (null === $encoding) { - $encoding = self::mb_internal_encoding(); - } else { - self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); - } - - $firstChar = mb_substr($string, 0, 1, $encoding); - $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding); - - return $firstChar.mb_substr($string, 1, null, $encoding); - } - - private static function getSubpart($pos, $part, $haystack, $encoding) - { - if (false === $pos) { - return false; - } - if ($part) { - return self::mb_substr($haystack, 0, $pos, $encoding); - } - - return self::mb_substr($haystack, $pos, null, $encoding); - } - - private static function html_encoding_callback(array $m) - { - $i = 1; - $entities = ''; - $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8')); - - while (isset($m[$i])) { - if (0x80 > $m[$i]) { - $entities .= \chr($m[$i++]); - continue; - } - if (0xF0 <= $m[$i]) { - $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; - } elseif (0xE0 <= $m[$i]) { - $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; - } else { - $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; - } - - $entities .= '&#'.$c.';'; - } - - return $entities; - } - - private static function title_case(array $s) - { - return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); - } - - private static function getData($file) - { - if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { - return require $file; - } - - return false; - } - - private static function getEncoding($encoding) - { - if (null === $encoding) { - return self::$internalEncoding; - } - - if ('UTF-8' === $encoding) { - return 'UTF-8'; - } - - $encoding = strtoupper($encoding); - - if ('8BIT' === $encoding || 'BINARY' === $encoding) { - return 'CP850'; - } - - if ('UTF8' === $encoding) { - return 'UTF-8'; - } - - return $encoding; - } - - public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string - { - return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__); - } - - public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string - { - return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__); - } - - public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string - { - return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__); - } - - private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string - { - if (null === $encoding) { - $encoding = self::mb_internal_encoding(); - } else { - self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given'); - } - - if ('' === $characters) { - return null === $encoding ? $string : self::mb_convert_encoding($string, $encoding); - } - - if ('UTF-8' === $encoding) { - $encoding = null; - if (!preg_match('//u', $string)) { - $string = @iconv('UTF-8', 'UTF-8//IGNORE', $string); - } - if (null !== $characters && !preg_match('//u', $characters)) { - $characters = @iconv('UTF-8', 'UTF-8//IGNORE', $characters); - } - } else { - $string = iconv($encoding, 'UTF-8//IGNORE', $string); - - if (null !== $characters) { - $characters = iconv($encoding, 'UTF-8//IGNORE', $characters); - } - } - - if (null === $characters) { - $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}"; - } else { - $characters = preg_quote($characters); - } - - $string = preg_replace(sprintf($regex, $characters), '', $string); - - if (null === $encoding) { - return $string; - } - - return iconv('UTF-8', $encoding.'//IGNORE', $string); - } - - private static function assertEncoding(string $encoding, string $errorFormat): void - { - try { - $validEncoding = @self::mb_check_encoding('', $encoding); - } catch (\ValueError $e) { - throw new \ValueError(sprintf($errorFormat, $encoding)); - } - - // BC for PHP 7.3 and lower - if (!$validEncoding) { - throw new \ValueError(sprintf($errorFormat, $encoding)); - } - } -} diff --git a/vendor/symfony/polyfill-mbstring/README.md b/vendor/symfony/polyfill-mbstring/README.md deleted file mode 100644 index 478b40d..0000000 --- a/vendor/symfony/polyfill-mbstring/README.md +++ /dev/null @@ -1,13 +0,0 @@ -Symfony Polyfill / Mbstring -=========================== - -This component provides a partial, native PHP implementation for the -[Mbstring](https://php.net/mbstring) extension. - -More information can be found in the -[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). - -License -======= - -This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php deleted file mode 100644 index 512bba0..0000000 --- a/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php +++ /dev/null @@ -1,119 +0,0 @@ - 'i̇', - 'µ' => 'μ', - 'ſ' => 's', - 'ͅ' => 'ι', - 'ς' => 'σ', - 'ϐ' => 'β', - 'ϑ' => 'θ', - 'ϕ' => 'φ', - 'ϖ' => 'π', - 'ϰ' => 'κ', - 'ϱ' => 'ρ', - 'ϵ' => 'ε', - 'ẛ' => 'ṡ', - 'ι' => 'ι', - 'ß' => 'ss', - 'ʼn' => 'ʼn', - 'ǰ' => 'ǰ', - 'ΐ' => 'ΐ', - 'ΰ' => 'ΰ', - 'և' => 'եւ', - 'ẖ' => 'ẖ', - 'ẗ' => 'ẗ', - 'ẘ' => 'ẘ', - 'ẙ' => 'ẙ', - 'ẚ' => 'aʾ', - 'ẞ' => 'ss', - 'ὐ' => 'ὐ', - 'ὒ' => 'ὒ', - 'ὔ' => 'ὔ', - 'ὖ' => 'ὖ', - 'ᾀ' => 'ἀι', - 'ᾁ' => 'ἁι', - 'ᾂ' => 'ἂι', - 'ᾃ' => 'ἃι', - 'ᾄ' => 'ἄι', - 'ᾅ' => 'ἅι', - 'ᾆ' => 'ἆι', - 'ᾇ' => 'ἇι', - 'ᾈ' => 'ἀι', - 'ᾉ' => 'ἁι', - 'ᾊ' => 'ἂι', - 'ᾋ' => 'ἃι', - 'ᾌ' => 'ἄι', - 'ᾍ' => 'ἅι', - 'ᾎ' => 'ἆι', - 'ᾏ' => 'ἇι', - 'ᾐ' => 'ἠι', - 'ᾑ' => 'ἡι', - 'ᾒ' => 'ἢι', - 'ᾓ' => 'ἣι', - 'ᾔ' => 'ἤι', - 'ᾕ' => 'ἥι', - 'ᾖ' => 'ἦι', - 'ᾗ' => 'ἧι', - 'ᾘ' => 'ἠι', - 'ᾙ' => 'ἡι', - 'ᾚ' => 'ἢι', - 'ᾛ' => 'ἣι', - 'ᾜ' => 'ἤι', - 'ᾝ' => 'ἥι', - 'ᾞ' => 'ἦι', - 'ᾟ' => 'ἧι', - 'ᾠ' => 'ὠι', - 'ᾡ' => 'ὡι', - 'ᾢ' => 'ὢι', - 'ᾣ' => 'ὣι', - 'ᾤ' => 'ὤι', - 'ᾥ' => 'ὥι', - 'ᾦ' => 'ὦι', - 'ᾧ' => 'ὧι', - 'ᾨ' => 'ὠι', - 'ᾩ' => 'ὡι', - 'ᾪ' => 'ὢι', - 'ᾫ' => 'ὣι', - 'ᾬ' => 'ὤι', - 'ᾭ' => 'ὥι', - 'ᾮ' => 'ὦι', - 'ᾯ' => 'ὧι', - 'ᾲ' => 'ὰι', - 'ᾳ' => 'αι', - 'ᾴ' => 'άι', - 'ᾶ' => 'ᾶ', - 'ᾷ' => 'ᾶι', - 'ᾼ' => 'αι', - 'ῂ' => 'ὴι', - 'ῃ' => 'ηι', - 'ῄ' => 'ήι', - 'ῆ' => 'ῆ', - 'ῇ' => 'ῆι', - 'ῌ' => 'ηι', - 'ῒ' => 'ῒ', - 'ῖ' => 'ῖ', - 'ῗ' => 'ῗ', - 'ῢ' => 'ῢ', - 'ῤ' => 'ῤ', - 'ῦ' => 'ῦ', - 'ῧ' => 'ῧ', - 'ῲ' => 'ὼι', - 'ῳ' => 'ωι', - 'ῴ' => 'ώι', - 'ῶ' => 'ῶ', - 'ῷ' => 'ῶι', - 'ῼ' => 'ωι', - 'ff' => 'ff', - 'fi' => 'fi', - 'fl' => 'fl', - 'ffi' => 'ffi', - 'ffl' => 'ffl', - 'ſt' => 'st', - 'st' => 'st', - 'ﬓ' => 'մն', - 'ﬔ' => 'մե', - 'ﬕ' => 'մի', - 'ﬖ' => 'վն', - 'ﬗ' => 'մխ', -]; diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php deleted file mode 100644 index fac60b0..0000000 --- a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php +++ /dev/null @@ -1,1397 +0,0 @@ - 'a', - 'B' => 'b', - 'C' => 'c', - 'D' => 'd', - 'E' => 'e', - 'F' => 'f', - 'G' => 'g', - 'H' => 'h', - 'I' => 'i', - 'J' => 'j', - 'K' => 'k', - 'L' => 'l', - 'M' => 'm', - 'N' => 'n', - 'O' => 'o', - 'P' => 'p', - 'Q' => 'q', - 'R' => 'r', - 'S' => 's', - 'T' => 't', - 'U' => 'u', - 'V' => 'v', - 'W' => 'w', - 'X' => 'x', - 'Y' => 'y', - 'Z' => 'z', - 'À' => 'à', - 'Á' => 'á', - 'Â' => 'â', - 'Ã' => 'ã', - 'Ä' => 'ä', - 'Å' => 'å', - 'Æ' => 'æ', - 'Ç' => 'ç', - 'È' => 'è', - 'É' => 'é', - 'Ê' => 'ê', - 'Ë' => 'ë', - 'Ì' => 'ì', - 'Í' => 'í', - 'Î' => 'î', - 'Ï' => 'ï', - 'Ð' => 'ð', - 'Ñ' => 'ñ', - 'Ò' => 'ò', - 'Ó' => 'ó', - 'Ô' => 'ô', - 'Õ' => 'õ', - 'Ö' => 'ö', - 'Ø' => 'ø', - 'Ù' => 'ù', - 'Ú' => 'ú', - 'Û' => 'û', - 'Ü' => 'ü', - 'Ý' => 'ý', - 'Þ' => 'þ', - 'Ā' => 'ā', - 'Ă' => 'ă', - 'Ą' => 'ą', - 'Ć' => 'ć', - 'Ĉ' => 'ĉ', - 'Ċ' => 'ċ', - 'Č' => 'č', - 'Ď' => 'ď', - 'Đ' => 'đ', - 'Ē' => 'ē', - 'Ĕ' => 'ĕ', - 'Ė' => 'ė', - 'Ę' => 'ę', - 'Ě' => 'ě', - 'Ĝ' => 'ĝ', - 'Ğ' => 'ğ', - 'Ġ' => 'ġ', - 'Ģ' => 'ģ', - 'Ĥ' => 'ĥ', - 'Ħ' => 'ħ', - 'Ĩ' => 'ĩ', - 'Ī' => 'ī', - 'Ĭ' => 'ĭ', - 'Į' => 'į', - 'İ' => 'i̇', - 'IJ' => 'ij', - 'Ĵ' => 'ĵ', - 'Ķ' => 'ķ', - 'Ĺ' => 'ĺ', - 'Ļ' => 'ļ', - 'Ľ' => 'ľ', - 'Ŀ' => 'ŀ', - 'Ł' => 'ł', - 'Ń' => 'ń', - 'Ņ' => 'ņ', - 'Ň' => 'ň', - 'Ŋ' => 'ŋ', - 'Ō' => 'ō', - 'Ŏ' => 'ŏ', - 'Ő' => 'ő', - 'Œ' => 'œ', - 'Ŕ' => 'ŕ', - 'Ŗ' => 'ŗ', - 'Ř' => 'ř', - 'Ś' => 'ś', - 'Ŝ' => 'ŝ', - 'Ş' => 'ş', - 'Š' => 'š', - 'Ţ' => 'ţ', - 'Ť' => 'ť', - 'Ŧ' => 'ŧ', - 'Ũ' => 'ũ', - 'Ū' => 'ū', - 'Ŭ' => 'ŭ', - 'Ů' => 'ů', - 'Ű' => 'ű', - 'Ų' => 'ų', - 'Ŵ' => 'ŵ', - 'Ŷ' => 'ŷ', - 'Ÿ' => 'ÿ', - 'Ź' => 'ź', - 'Ż' => 'ż', - 'Ž' => 'ž', - 'Ɓ' => 'ɓ', - 'Ƃ' => 'ƃ', - 'Ƅ' => 'ƅ', - 'Ɔ' => 'ɔ', - 'Ƈ' => 'ƈ', - 'Ɖ' => 'ɖ', - 'Ɗ' => 'ɗ', - 'Ƌ' => 'ƌ', - 'Ǝ' => 'ǝ', - 'Ə' => 'ə', - 'Ɛ' => 'ɛ', - 'Ƒ' => 'ƒ', - 'Ɠ' => 'ɠ', - 'Ɣ' => 'ɣ', - 'Ɩ' => 'ɩ', - 'Ɨ' => 'ɨ', - 'Ƙ' => 'ƙ', - 'Ɯ' => 'ɯ', - 'Ɲ' => 'ɲ', - 'Ɵ' => 'ɵ', - 'Ơ' => 'ơ', - 'Ƣ' => 'ƣ', - 'Ƥ' => 'ƥ', - 'Ʀ' => 'ʀ', - 'Ƨ' => 'ƨ', - 'Ʃ' => 'ʃ', - 'Ƭ' => 'ƭ', - 'Ʈ' => 'ʈ', - 'Ư' => 'ư', - 'Ʊ' => 'ʊ', - 'Ʋ' => 'ʋ', - 'Ƴ' => 'ƴ', - 'Ƶ' => 'ƶ', - 'Ʒ' => 'ʒ', - 'Ƹ' => 'ƹ', - 'Ƽ' => 'ƽ', - 'DŽ' => 'dž', - 'Dž' => 'dž', - 'LJ' => 'lj', - 'Lj' => 'lj', - 'NJ' => 'nj', - 'Nj' => 'nj', - 'Ǎ' => 'ǎ', - 'Ǐ' => 'ǐ', - 'Ǒ' => 'ǒ', - 'Ǔ' => 'ǔ', - 'Ǖ' => 'ǖ', - 'Ǘ' => 'ǘ', - 'Ǚ' => 'ǚ', - 'Ǜ' => 'ǜ', - 'Ǟ' => 'ǟ', - 'Ǡ' => 'ǡ', - 'Ǣ' => 'ǣ', - 'Ǥ' => 'ǥ', - 'Ǧ' => 'ǧ', - 'Ǩ' => 'ǩ', - 'Ǫ' => 'ǫ', - 'Ǭ' => 'ǭ', - 'Ǯ' => 'ǯ', - 'DZ' => 'dz', - 'Dz' => 'dz', - 'Ǵ' => 'ǵ', - 'Ƕ' => 'ƕ', - 'Ƿ' => 'ƿ', - 'Ǹ' => 'ǹ', - 'Ǻ' => 'ǻ', - 'Ǽ' => 'ǽ', - 'Ǿ' => 'ǿ', - 'Ȁ' => 'ȁ', - 'Ȃ' => 'ȃ', - 'Ȅ' => 'ȅ', - 'Ȇ' => 'ȇ', - 'Ȉ' => 'ȉ', - 'Ȋ' => 'ȋ', - 'Ȍ' => 'ȍ', - 'Ȏ' => 'ȏ', - 'Ȑ' => 'ȑ', - 'Ȓ' => 'ȓ', - 'Ȕ' => 'ȕ', - 'Ȗ' => 'ȗ', - 'Ș' => 'ș', - 'Ț' => 'ț', - 'Ȝ' => 'ȝ', - 'Ȟ' => 'ȟ', - 'Ƞ' => 'ƞ', - 'Ȣ' => 'ȣ', - 'Ȥ' => 'ȥ', - 'Ȧ' => 'ȧ', - 'Ȩ' => 'ȩ', - 'Ȫ' => 'ȫ', - 'Ȭ' => 'ȭ', - 'Ȯ' => 'ȯ', - 'Ȱ' => 'ȱ', - 'Ȳ' => 'ȳ', - 'Ⱥ' => 'ⱥ', - 'Ȼ' => 'ȼ', - 'Ƚ' => 'ƚ', - 'Ⱦ' => 'ⱦ', - 'Ɂ' => 'ɂ', - 'Ƀ' => 'ƀ', - 'Ʉ' => 'ʉ', - 'Ʌ' => 'ʌ', - 'Ɇ' => 'ɇ', - 'Ɉ' => 'ɉ', - 'Ɋ' => 'ɋ', - 'Ɍ' => 'ɍ', - 'Ɏ' => 'ɏ', - 'Ͱ' => 'ͱ', - 'Ͳ' => 'ͳ', - 'Ͷ' => 'ͷ', - 'Ϳ' => 'ϳ', - 'Ά' => 'ά', - 'Έ' => 'έ', - 'Ή' => 'ή', - 'Ί' => 'ί', - 'Ό' => 'ό', - 'Ύ' => 'ύ', - 'Ώ' => 'ώ', - 'Α' => 'α', - 'Β' => 'β', - 'Γ' => 'γ', - 'Δ' => 'δ', - 'Ε' => 'ε', - 'Ζ' => 'ζ', - 'Η' => 'η', - 'Θ' => 'θ', - 'Ι' => 'ι', - 'Κ' => 'κ', - 'Λ' => 'λ', - 'Μ' => 'μ', - 'Ν' => 'ν', - 'Ξ' => 'ξ', - 'Ο' => 'ο', - 'Π' => 'π', - 'Ρ' => 'ρ', - 'Σ' => 'σ', - 'Τ' => 'τ', - 'Υ' => 'υ', - 'Φ' => 'φ', - 'Χ' => 'χ', - 'Ψ' => 'ψ', - 'Ω' => 'ω', - 'Ϊ' => 'ϊ', - 'Ϋ' => 'ϋ', - 'Ϗ' => 'ϗ', - 'Ϙ' => 'ϙ', - 'Ϛ' => 'ϛ', - 'Ϝ' => 'ϝ', - 'Ϟ' => 'ϟ', - 'Ϡ' => 'ϡ', - 'Ϣ' => 'ϣ', - 'Ϥ' => 'ϥ', - 'Ϧ' => 'ϧ', - 'Ϩ' => 'ϩ', - 'Ϫ' => 'ϫ', - 'Ϭ' => 'ϭ', - 'Ϯ' => 'ϯ', - 'ϴ' => 'θ', - 'Ϸ' => 'ϸ', - 'Ϲ' => 'ϲ', - 'Ϻ' => 'ϻ', - 'Ͻ' => 'ͻ', - 'Ͼ' => 'ͼ', - 'Ͽ' => 'ͽ', - 'Ѐ' => 'ѐ', - 'Ё' => 'ё', - 'Ђ' => 'ђ', - 'Ѓ' => 'ѓ', - 'Є' => 'є', - 'Ѕ' => 'ѕ', - 'І' => 'і', - 'Ї' => 'ї', - 'Ј' => 'ј', - 'Љ' => 'љ', - 'Њ' => 'њ', - 'Ћ' => 'ћ', - 'Ќ' => 'ќ', - 'Ѝ' => 'ѝ', - 'Ў' => 'ў', - 'Џ' => 'џ', - 'А' => 'а', - 'Б' => 'б', - 'В' => 'в', - 'Г' => 'г', - 'Д' => 'д', - 'Е' => 'е', - 'Ж' => 'ж', - 'З' => 'з', - 'И' => 'и', - 'Й' => 'й', - 'К' => 'к', - 'Л' => 'л', - 'М' => 'м', - 'Н' => 'н', - 'О' => 'о', - 'П' => 'п', - 'Р' => 'р', - 'С' => 'с', - 'Т' => 'т', - 'У' => 'у', - 'Ф' => 'ф', - 'Х' => 'х', - 'Ц' => 'ц', - 'Ч' => 'ч', - 'Ш' => 'ш', - 'Щ' => 'щ', - 'Ъ' => 'ъ', - 'Ы' => 'ы', - 'Ь' => 'ь', - 'Э' => 'э', - 'Ю' => 'ю', - 'Я' => 'я', - 'Ѡ' => 'ѡ', - 'Ѣ' => 'ѣ', - 'Ѥ' => 'ѥ', - 'Ѧ' => 'ѧ', - 'Ѩ' => 'ѩ', - 'Ѫ' => 'ѫ', - 'Ѭ' => 'ѭ', - 'Ѯ' => 'ѯ', - 'Ѱ' => 'ѱ', - 'Ѳ' => 'ѳ', - 'Ѵ' => 'ѵ', - 'Ѷ' => 'ѷ', - 'Ѹ' => 'ѹ', - 'Ѻ' => 'ѻ', - 'Ѽ' => 'ѽ', - 'Ѿ' => 'ѿ', - 'Ҁ' => 'ҁ', - 'Ҋ' => 'ҋ', - 'Ҍ' => 'ҍ', - 'Ҏ' => 'ҏ', - 'Ґ' => 'ґ', - 'Ғ' => 'ғ', - 'Ҕ' => 'ҕ', - 'Җ' => 'җ', - 'Ҙ' => 'ҙ', - 'Қ' => 'қ', - 'Ҝ' => 'ҝ', - 'Ҟ' => 'ҟ', - 'Ҡ' => 'ҡ', - 'Ң' => 'ң', - 'Ҥ' => 'ҥ', - 'Ҧ' => 'ҧ', - 'Ҩ' => 'ҩ', - 'Ҫ' => 'ҫ', - 'Ҭ' => 'ҭ', - 'Ү' => 'ү', - 'Ұ' => 'ұ', - 'Ҳ' => 'ҳ', - 'Ҵ' => 'ҵ', - 'Ҷ' => 'ҷ', - 'Ҹ' => 'ҹ', - 'Һ' => 'һ', - 'Ҽ' => 'ҽ', - 'Ҿ' => 'ҿ', - 'Ӏ' => 'ӏ', - 'Ӂ' => 'ӂ', - 'Ӄ' => 'ӄ', - 'Ӆ' => 'ӆ', - 'Ӈ' => 'ӈ', - 'Ӊ' => 'ӊ', - 'Ӌ' => 'ӌ', - 'Ӎ' => 'ӎ', - 'Ӑ' => 'ӑ', - 'Ӓ' => 'ӓ', - 'Ӕ' => 'ӕ', - 'Ӗ' => 'ӗ', - 'Ә' => 'ә', - 'Ӛ' => 'ӛ', - 'Ӝ' => 'ӝ', - 'Ӟ' => 'ӟ', - 'Ӡ' => 'ӡ', - 'Ӣ' => 'ӣ', - 'Ӥ' => 'ӥ', - 'Ӧ' => 'ӧ', - 'Ө' => 'ө', - 'Ӫ' => 'ӫ', - 'Ӭ' => 'ӭ', - 'Ӯ' => 'ӯ', - 'Ӱ' => 'ӱ', - 'Ӳ' => 'ӳ', - 'Ӵ' => 'ӵ', - 'Ӷ' => 'ӷ', - 'Ӹ' => 'ӹ', - 'Ӻ' => 'ӻ', - 'Ӽ' => 'ӽ', - 'Ӿ' => 'ӿ', - 'Ԁ' => 'ԁ', - 'Ԃ' => 'ԃ', - 'Ԅ' => 'ԅ', - 'Ԇ' => 'ԇ', - 'Ԉ' => 'ԉ', - 'Ԋ' => 'ԋ', - 'Ԍ' => 'ԍ', - 'Ԏ' => 'ԏ', - 'Ԑ' => 'ԑ', - 'Ԓ' => 'ԓ', - 'Ԕ' => 'ԕ', - 'Ԗ' => 'ԗ', - 'Ԙ' => 'ԙ', - 'Ԛ' => 'ԛ', - 'Ԝ' => 'ԝ', - 'Ԟ' => 'ԟ', - 'Ԡ' => 'ԡ', - 'Ԣ' => 'ԣ', - 'Ԥ' => 'ԥ', - 'Ԧ' => 'ԧ', - 'Ԩ' => 'ԩ', - 'Ԫ' => 'ԫ', - 'Ԭ' => 'ԭ', - 'Ԯ' => 'ԯ', - 'Ա' => 'ա', - 'Բ' => 'բ', - 'Գ' => 'գ', - 'Դ' => 'դ', - 'Ե' => 'ե', - 'Զ' => 'զ', - 'Է' => 'է', - 'Ը' => 'ը', - 'Թ' => 'թ', - 'Ժ' => 'ժ', - 'Ի' => 'ի', - 'Լ' => 'լ', - 'Խ' => 'խ', - 'Ծ' => 'ծ', - 'Կ' => 'կ', - 'Հ' => 'հ', - 'Ձ' => 'ձ', - 'Ղ' => 'ղ', - 'Ճ' => 'ճ', - 'Մ' => 'մ', - 'Յ' => 'յ', - 'Ն' => 'ն', - 'Շ' => 'շ', - 'Ո' => 'ո', - 'Չ' => 'չ', - 'Պ' => 'պ', - 'Ջ' => 'ջ', - 'Ռ' => 'ռ', - 'Ս' => 'ս', - 'Վ' => 'վ', - 'Տ' => 'տ', - 'Ր' => 'ր', - 'Ց' => 'ց', - 'Ւ' => 'ւ', - 'Փ' => 'փ', - 'Ք' => 'ք', - 'Օ' => 'օ', - 'Ֆ' => 'ֆ', - 'Ⴀ' => 'ⴀ', - 'Ⴁ' => 'ⴁ', - 'Ⴂ' => 'ⴂ', - 'Ⴃ' => 'ⴃ', - 'Ⴄ' => 'ⴄ', - 'Ⴅ' => 'ⴅ', - 'Ⴆ' => 'ⴆ', - 'Ⴇ' => 'ⴇ', - 'Ⴈ' => 'ⴈ', - 'Ⴉ' => 'ⴉ', - 'Ⴊ' => 'ⴊ', - 'Ⴋ' => 'ⴋ', - 'Ⴌ' => 'ⴌ', - 'Ⴍ' => 'ⴍ', - 'Ⴎ' => 'ⴎ', - 'Ⴏ' => 'ⴏ', - 'Ⴐ' => 'ⴐ', - 'Ⴑ' => 'ⴑ', - 'Ⴒ' => 'ⴒ', - 'Ⴓ' => 'ⴓ', - 'Ⴔ' => 'ⴔ', - 'Ⴕ' => 'ⴕ', - 'Ⴖ' => 'ⴖ', - 'Ⴗ' => 'ⴗ', - 'Ⴘ' => 'ⴘ', - 'Ⴙ' => 'ⴙ', - 'Ⴚ' => 'ⴚ', - 'Ⴛ' => 'ⴛ', - 'Ⴜ' => 'ⴜ', - 'Ⴝ' => 'ⴝ', - 'Ⴞ' => 'ⴞ', - 'Ⴟ' => 'ⴟ', - 'Ⴠ' => 'ⴠ', - 'Ⴡ' => 'ⴡ', - 'Ⴢ' => 'ⴢ', - 'Ⴣ' => 'ⴣ', - 'Ⴤ' => 'ⴤ', - 'Ⴥ' => 'ⴥ', - 'Ⴧ' => 'ⴧ', - 'Ⴭ' => 'ⴭ', - 'Ꭰ' => 'ꭰ', - 'Ꭱ' => 'ꭱ', - 'Ꭲ' => 'ꭲ', - 'Ꭳ' => 'ꭳ', - 'Ꭴ' => 'ꭴ', - 'Ꭵ' => 'ꭵ', - 'Ꭶ' => 'ꭶ', - 'Ꭷ' => 'ꭷ', - 'Ꭸ' => 'ꭸ', - 'Ꭹ' => 'ꭹ', - 'Ꭺ' => 'ꭺ', - 'Ꭻ' => 'ꭻ', - 'Ꭼ' => 'ꭼ', - 'Ꭽ' => 'ꭽ', - 'Ꭾ' => 'ꭾ', - 'Ꭿ' => 'ꭿ', - 'Ꮀ' => 'ꮀ', - 'Ꮁ' => 'ꮁ', - 'Ꮂ' => 'ꮂ', - 'Ꮃ' => 'ꮃ', - 'Ꮄ' => 'ꮄ', - 'Ꮅ' => 'ꮅ', - 'Ꮆ' => 'ꮆ', - 'Ꮇ' => 'ꮇ', - 'Ꮈ' => 'ꮈ', - 'Ꮉ' => 'ꮉ', - 'Ꮊ' => 'ꮊ', - 'Ꮋ' => 'ꮋ', - 'Ꮌ' => 'ꮌ', - 'Ꮍ' => 'ꮍ', - 'Ꮎ' => 'ꮎ', - 'Ꮏ' => 'ꮏ', - 'Ꮐ' => 'ꮐ', - 'Ꮑ' => 'ꮑ', - 'Ꮒ' => 'ꮒ', - 'Ꮓ' => 'ꮓ', - 'Ꮔ' => 'ꮔ', - 'Ꮕ' => 'ꮕ', - 'Ꮖ' => 'ꮖ', - 'Ꮗ' => 'ꮗ', - 'Ꮘ' => 'ꮘ', - 'Ꮙ' => 'ꮙ', - 'Ꮚ' => 'ꮚ', - 'Ꮛ' => 'ꮛ', - 'Ꮜ' => 'ꮜ', - 'Ꮝ' => 'ꮝ', - 'Ꮞ' => 'ꮞ', - 'Ꮟ' => 'ꮟ', - 'Ꮠ' => 'ꮠ', - 'Ꮡ' => 'ꮡ', - 'Ꮢ' => 'ꮢ', - 'Ꮣ' => 'ꮣ', - 'Ꮤ' => 'ꮤ', - 'Ꮥ' => 'ꮥ', - 'Ꮦ' => 'ꮦ', - 'Ꮧ' => 'ꮧ', - 'Ꮨ' => 'ꮨ', - 'Ꮩ' => 'ꮩ', - 'Ꮪ' => 'ꮪ', - 'Ꮫ' => 'ꮫ', - 'Ꮬ' => 'ꮬ', - 'Ꮭ' => 'ꮭ', - 'Ꮮ' => 'ꮮ', - 'Ꮯ' => 'ꮯ', - 'Ꮰ' => 'ꮰ', - 'Ꮱ' => 'ꮱ', - 'Ꮲ' => 'ꮲ', - 'Ꮳ' => 'ꮳ', - 'Ꮴ' => 'ꮴ', - 'Ꮵ' => 'ꮵ', - 'Ꮶ' => 'ꮶ', - 'Ꮷ' => 'ꮷ', - 'Ꮸ' => 'ꮸ', - 'Ꮹ' => 'ꮹ', - 'Ꮺ' => 'ꮺ', - 'Ꮻ' => 'ꮻ', - 'Ꮼ' => 'ꮼ', - 'Ꮽ' => 'ꮽ', - 'Ꮾ' => 'ꮾ', - 'Ꮿ' => 'ꮿ', - 'Ᏸ' => 'ᏸ', - 'Ᏹ' => 'ᏹ', - 'Ᏺ' => 'ᏺ', - 'Ᏻ' => 'ᏻ', - 'Ᏼ' => 'ᏼ', - 'Ᏽ' => 'ᏽ', - 'Ა' => 'ა', - 'Ბ' => 'ბ', - 'Გ' => 'გ', - 'Დ' => 'დ', - 'Ე' => 'ე', - 'Ვ' => 'ვ', - 'Ზ' => 'ზ', - 'Თ' => 'თ', - 'Ი' => 'ი', - 'Კ' => 'კ', - 'Ლ' => 'ლ', - 'Მ' => 'მ', - 'Ნ' => 'ნ', - 'Ო' => 'ო', - 'Პ' => 'პ', - 'Ჟ' => 'ჟ', - 'Რ' => 'რ', - 'Ს' => 'ს', - 'Ტ' => 'ტ', - 'Უ' => 'უ', - 'Ფ' => 'ფ', - 'Ქ' => 'ქ', - 'Ღ' => 'ღ', - 'Ყ' => 'ყ', - 'Შ' => 'შ', - 'Ჩ' => 'ჩ', - 'Ც' => 'ც', - 'Ძ' => 'ძ', - 'Წ' => 'წ', - 'Ჭ' => 'ჭ', - 'Ხ' => 'ხ', - 'Ჯ' => 'ჯ', - 'Ჰ' => 'ჰ', - 'Ჱ' => 'ჱ', - 'Ჲ' => 'ჲ', - 'Ჳ' => 'ჳ', - 'Ჴ' => 'ჴ', - 'Ჵ' => 'ჵ', - 'Ჶ' => 'ჶ', - 'Ჷ' => 'ჷ', - 'Ჸ' => 'ჸ', - 'Ჹ' => 'ჹ', - 'Ჺ' => 'ჺ', - 'Ჽ' => 'ჽ', - 'Ჾ' => 'ჾ', - 'Ჿ' => 'ჿ', - 'Ḁ' => 'ḁ', - 'Ḃ' => 'ḃ', - 'Ḅ' => 'ḅ', - 'Ḇ' => 'ḇ', - 'Ḉ' => 'ḉ', - 'Ḋ' => 'ḋ', - 'Ḍ' => 'ḍ', - 'Ḏ' => 'ḏ', - 'Ḑ' => 'ḑ', - 'Ḓ' => 'ḓ', - 'Ḕ' => 'ḕ', - 'Ḗ' => 'ḗ', - 'Ḙ' => 'ḙ', - 'Ḛ' => 'ḛ', - 'Ḝ' => 'ḝ', - 'Ḟ' => 'ḟ', - 'Ḡ' => 'ḡ', - 'Ḣ' => 'ḣ', - 'Ḥ' => 'ḥ', - 'Ḧ' => 'ḧ', - 'Ḩ' => 'ḩ', - 'Ḫ' => 'ḫ', - 'Ḭ' => 'ḭ', - 'Ḯ' => 'ḯ', - 'Ḱ' => 'ḱ', - 'Ḳ' => 'ḳ', - 'Ḵ' => 'ḵ', - 'Ḷ' => 'ḷ', - 'Ḹ' => 'ḹ', - 'Ḻ' => 'ḻ', - 'Ḽ' => 'ḽ', - 'Ḿ' => 'ḿ', - 'Ṁ' => 'ṁ', - 'Ṃ' => 'ṃ', - 'Ṅ' => 'ṅ', - 'Ṇ' => 'ṇ', - 'Ṉ' => 'ṉ', - 'Ṋ' => 'ṋ', - 'Ṍ' => 'ṍ', - 'Ṏ' => 'ṏ', - 'Ṑ' => 'ṑ', - 'Ṓ' => 'ṓ', - 'Ṕ' => 'ṕ', - 'Ṗ' => 'ṗ', - 'Ṙ' => 'ṙ', - 'Ṛ' => 'ṛ', - 'Ṝ' => 'ṝ', - 'Ṟ' => 'ṟ', - 'Ṡ' => 'ṡ', - 'Ṣ' => 'ṣ', - 'Ṥ' => 'ṥ', - 'Ṧ' => 'ṧ', - 'Ṩ' => 'ṩ', - 'Ṫ' => 'ṫ', - 'Ṭ' => 'ṭ', - 'Ṯ' => 'ṯ', - 'Ṱ' => 'ṱ', - 'Ṳ' => 'ṳ', - 'Ṵ' => 'ṵ', - 'Ṷ' => 'ṷ', - 'Ṹ' => 'ṹ', - 'Ṻ' => 'ṻ', - 'Ṽ' => 'ṽ', - 'Ṿ' => 'ṿ', - 'Ẁ' => 'ẁ', - 'Ẃ' => 'ẃ', - 'Ẅ' => 'ẅ', - 'Ẇ' => 'ẇ', - 'Ẉ' => 'ẉ', - 'Ẋ' => 'ẋ', - 'Ẍ' => 'ẍ', - 'Ẏ' => 'ẏ', - 'Ẑ' => 'ẑ', - 'Ẓ' => 'ẓ', - 'Ẕ' => 'ẕ', - 'ẞ' => 'ß', - 'Ạ' => 'ạ', - 'Ả' => 'ả', - 'Ấ' => 'ấ', - 'Ầ' => 'ầ', - 'Ẩ' => 'ẩ', - 'Ẫ' => 'ẫ', - 'Ậ' => 'ậ', - 'Ắ' => 'ắ', - 'Ằ' => 'ằ', - 'Ẳ' => 'ẳ', - 'Ẵ' => 'ẵ', - 'Ặ' => 'ặ', - 'Ẹ' => 'ẹ', - 'Ẻ' => 'ẻ', - 'Ẽ' => 'ẽ', - 'Ế' => 'ế', - 'Ề' => 'ề', - 'Ể' => 'ể', - 'Ễ' => 'ễ', - 'Ệ' => 'ệ', - 'Ỉ' => 'ỉ', - 'Ị' => 'ị', - 'Ọ' => 'ọ', - 'Ỏ' => 'ỏ', - 'Ố' => 'ố', - 'Ồ' => 'ồ', - 'Ổ' => 'ổ', - 'Ỗ' => 'ỗ', - 'Ộ' => 'ộ', - 'Ớ' => 'ớ', - 'Ờ' => 'ờ', - 'Ở' => 'ở', - 'Ỡ' => 'ỡ', - 'Ợ' => 'ợ', - 'Ụ' => 'ụ', - 'Ủ' => 'ủ', - 'Ứ' => 'ứ', - 'Ừ' => 'ừ', - 'Ử' => 'ử', - 'Ữ' => 'ữ', - 'Ự' => 'ự', - 'Ỳ' => 'ỳ', - 'Ỵ' => 'ỵ', - 'Ỷ' => 'ỷ', - 'Ỹ' => 'ỹ', - 'Ỻ' => 'ỻ', - 'Ỽ' => 'ỽ', - 'Ỿ' => 'ỿ', - 'Ἀ' => 'ἀ', - 'Ἁ' => 'ἁ', - 'Ἂ' => 'ἂ', - 'Ἃ' => 'ἃ', - 'Ἄ' => 'ἄ', - 'Ἅ' => 'ἅ', - 'Ἆ' => 'ἆ', - 'Ἇ' => 'ἇ', - 'Ἐ' => 'ἐ', - 'Ἑ' => 'ἑ', - 'Ἒ' => 'ἒ', - 'Ἓ' => 'ἓ', - 'Ἔ' => 'ἔ', - 'Ἕ' => 'ἕ', - 'Ἠ' => 'ἠ', - 'Ἡ' => 'ἡ', - 'Ἢ' => 'ἢ', - 'Ἣ' => 'ἣ', - 'Ἤ' => 'ἤ', - 'Ἥ' => 'ἥ', - 'Ἦ' => 'ἦ', - 'Ἧ' => 'ἧ', - 'Ἰ' => 'ἰ', - 'Ἱ' => 'ἱ', - 'Ἲ' => 'ἲ', - 'Ἳ' => 'ἳ', - 'Ἴ' => 'ἴ', - 'Ἵ' => 'ἵ', - 'Ἶ' => 'ἶ', - 'Ἷ' => 'ἷ', - 'Ὀ' => 'ὀ', - 'Ὁ' => 'ὁ', - 'Ὂ' => 'ὂ', - 'Ὃ' => 'ὃ', - 'Ὄ' => 'ὄ', - 'Ὅ' => 'ὅ', - 'Ὑ' => 'ὑ', - 'Ὓ' => 'ὓ', - 'Ὕ' => 'ὕ', - 'Ὗ' => 'ὗ', - 'Ὠ' => 'ὠ', - 'Ὡ' => 'ὡ', - 'Ὢ' => 'ὢ', - 'Ὣ' => 'ὣ', - 'Ὤ' => 'ὤ', - 'Ὥ' => 'ὥ', - 'Ὦ' => 'ὦ', - 'Ὧ' => 'ὧ', - 'ᾈ' => 'ᾀ', - 'ᾉ' => 'ᾁ', - 'ᾊ' => 'ᾂ', - 'ᾋ' => 'ᾃ', - 'ᾌ' => 'ᾄ', - 'ᾍ' => 'ᾅ', - 'ᾎ' => 'ᾆ', - 'ᾏ' => 'ᾇ', - 'ᾘ' => 'ᾐ', - 'ᾙ' => 'ᾑ', - 'ᾚ' => 'ᾒ', - 'ᾛ' => 'ᾓ', - 'ᾜ' => 'ᾔ', - 'ᾝ' => 'ᾕ', - 'ᾞ' => 'ᾖ', - 'ᾟ' => 'ᾗ', - 'ᾨ' => 'ᾠ', - 'ᾩ' => 'ᾡ', - 'ᾪ' => 'ᾢ', - 'ᾫ' => 'ᾣ', - 'ᾬ' => 'ᾤ', - 'ᾭ' => 'ᾥ', - 'ᾮ' => 'ᾦ', - 'ᾯ' => 'ᾧ', - 'Ᾰ' => 'ᾰ', - 'Ᾱ' => 'ᾱ', - 'Ὰ' => 'ὰ', - 'Ά' => 'ά', - 'ᾼ' => 'ᾳ', - 'Ὲ' => 'ὲ', - 'Έ' => 'έ', - 'Ὴ' => 'ὴ', - 'Ή' => 'ή', - 'ῌ' => 'ῃ', - 'Ῐ' => 'ῐ', - 'Ῑ' => 'ῑ', - 'Ὶ' => 'ὶ', - 'Ί' => 'ί', - 'Ῠ' => 'ῠ', - 'Ῡ' => 'ῡ', - 'Ὺ' => 'ὺ', - 'Ύ' => 'ύ', - 'Ῥ' => 'ῥ', - 'Ὸ' => 'ὸ', - 'Ό' => 'ό', - 'Ὼ' => 'ὼ', - 'Ώ' => 'ώ', - 'ῼ' => 'ῳ', - 'Ω' => 'ω', - 'K' => 'k', - 'Å' => 'å', - 'Ⅎ' => 'ⅎ', - 'Ⅰ' => 'ⅰ', - 'Ⅱ' => 'ⅱ', - 'Ⅲ' => 'ⅲ', - 'Ⅳ' => 'ⅳ', - 'Ⅴ' => 'ⅴ', - 'Ⅵ' => 'ⅵ', - 'Ⅶ' => 'ⅶ', - 'Ⅷ' => 'ⅷ', - 'Ⅸ' => 'ⅸ', - 'Ⅹ' => 'ⅹ', - 'Ⅺ' => 'ⅺ', - 'Ⅻ' => 'ⅻ', - 'Ⅼ' => 'ⅼ', - 'Ⅽ' => 'ⅽ', - 'Ⅾ' => 'ⅾ', - 'Ⅿ' => 'ⅿ', - 'Ↄ' => 'ↄ', - 'Ⓐ' => 'ⓐ', - 'Ⓑ' => 'ⓑ', - 'Ⓒ' => 'ⓒ', - 'Ⓓ' => 'ⓓ', - 'Ⓔ' => 'ⓔ', - 'Ⓕ' => 'ⓕ', - 'Ⓖ' => 'ⓖ', - 'Ⓗ' => 'ⓗ', - 'Ⓘ' => 'ⓘ', - 'Ⓙ' => 'ⓙ', - 'Ⓚ' => 'ⓚ', - 'Ⓛ' => 'ⓛ', - 'Ⓜ' => 'ⓜ', - 'Ⓝ' => 'ⓝ', - 'Ⓞ' => 'ⓞ', - 'Ⓟ' => 'ⓟ', - 'Ⓠ' => 'ⓠ', - 'Ⓡ' => 'ⓡ', - 'Ⓢ' => 'ⓢ', - 'Ⓣ' => 'ⓣ', - 'Ⓤ' => 'ⓤ', - 'Ⓥ' => 'ⓥ', - 'Ⓦ' => 'ⓦ', - 'Ⓧ' => 'ⓧ', - 'Ⓨ' => 'ⓨ', - 'Ⓩ' => 'ⓩ', - 'Ⰰ' => 'ⰰ', - 'Ⰱ' => 'ⰱ', - 'Ⰲ' => 'ⰲ', - 'Ⰳ' => 'ⰳ', - 'Ⰴ' => 'ⰴ', - 'Ⰵ' => 'ⰵ', - 'Ⰶ' => 'ⰶ', - 'Ⰷ' => 'ⰷ', - 'Ⰸ' => 'ⰸ', - 'Ⰹ' => 'ⰹ', - 'Ⰺ' => 'ⰺ', - 'Ⰻ' => 'ⰻ', - 'Ⰼ' => 'ⰼ', - 'Ⰽ' => 'ⰽ', - 'Ⰾ' => 'ⰾ', - 'Ⰿ' => 'ⰿ', - 'Ⱀ' => 'ⱀ', - 'Ⱁ' => 'ⱁ', - 'Ⱂ' => 'ⱂ', - 'Ⱃ' => 'ⱃ', - 'Ⱄ' => 'ⱄ', - 'Ⱅ' => 'ⱅ', - 'Ⱆ' => 'ⱆ', - 'Ⱇ' => 'ⱇ', - 'Ⱈ' => 'ⱈ', - 'Ⱉ' => 'ⱉ', - 'Ⱊ' => 'ⱊ', - 'Ⱋ' => 'ⱋ', - 'Ⱌ' => 'ⱌ', - 'Ⱍ' => 'ⱍ', - 'Ⱎ' => 'ⱎ', - 'Ⱏ' => 'ⱏ', - 'Ⱐ' => 'ⱐ', - 'Ⱑ' => 'ⱑ', - 'Ⱒ' => 'ⱒ', - 'Ⱓ' => 'ⱓ', - 'Ⱔ' => 'ⱔ', - 'Ⱕ' => 'ⱕ', - 'Ⱖ' => 'ⱖ', - 'Ⱗ' => 'ⱗ', - 'Ⱘ' => 'ⱘ', - 'Ⱙ' => 'ⱙ', - 'Ⱚ' => 'ⱚ', - 'Ⱛ' => 'ⱛ', - 'Ⱜ' => 'ⱜ', - 'Ⱝ' => 'ⱝ', - 'Ⱞ' => 'ⱞ', - 'Ⱡ' => 'ⱡ', - 'Ɫ' => 'ɫ', - 'Ᵽ' => 'ᵽ', - 'Ɽ' => 'ɽ', - 'Ⱨ' => 'ⱨ', - 'Ⱪ' => 'ⱪ', - 'Ⱬ' => 'ⱬ', - 'Ɑ' => 'ɑ', - 'Ɱ' => 'ɱ', - 'Ɐ' => 'ɐ', - 'Ɒ' => 'ɒ', - 'Ⱳ' => 'ⱳ', - 'Ⱶ' => 'ⱶ', - 'Ȿ' => 'ȿ', - 'Ɀ' => 'ɀ', - 'Ⲁ' => 'ⲁ', - 'Ⲃ' => 'ⲃ', - 'Ⲅ' => 'ⲅ', - 'Ⲇ' => 'ⲇ', - 'Ⲉ' => 'ⲉ', - 'Ⲋ' => 'ⲋ', - 'Ⲍ' => 'ⲍ', - 'Ⲏ' => 'ⲏ', - 'Ⲑ' => 'ⲑ', - 'Ⲓ' => 'ⲓ', - 'Ⲕ' => 'ⲕ', - 'Ⲗ' => 'ⲗ', - 'Ⲙ' => 'ⲙ', - 'Ⲛ' => 'ⲛ', - 'Ⲝ' => 'ⲝ', - 'Ⲟ' => 'ⲟ', - 'Ⲡ' => 'ⲡ', - 'Ⲣ' => 'ⲣ', - 'Ⲥ' => 'ⲥ', - 'Ⲧ' => 'ⲧ', - 'Ⲩ' => 'ⲩ', - 'Ⲫ' => 'ⲫ', - 'Ⲭ' => 'ⲭ', - 'Ⲯ' => 'ⲯ', - 'Ⲱ' => 'ⲱ', - 'Ⲳ' => 'ⲳ', - 'Ⲵ' => 'ⲵ', - 'Ⲷ' => 'ⲷ', - 'Ⲹ' => 'ⲹ', - 'Ⲻ' => 'ⲻ', - 'Ⲽ' => 'ⲽ', - 'Ⲿ' => 'ⲿ', - 'Ⳁ' => 'ⳁ', - 'Ⳃ' => 'ⳃ', - 'Ⳅ' => 'ⳅ', - 'Ⳇ' => 'ⳇ', - 'Ⳉ' => 'ⳉ', - 'Ⳋ' => 'ⳋ', - 'Ⳍ' => 'ⳍ', - 'Ⳏ' => 'ⳏ', - 'Ⳑ' => 'ⳑ', - 'Ⳓ' => 'ⳓ', - 'Ⳕ' => 'ⳕ', - 'Ⳗ' => 'ⳗ', - 'Ⳙ' => 'ⳙ', - 'Ⳛ' => 'ⳛ', - 'Ⳝ' => 'ⳝ', - 'Ⳟ' => 'ⳟ', - 'Ⳡ' => 'ⳡ', - 'Ⳣ' => 'ⳣ', - 'Ⳬ' => 'ⳬ', - 'Ⳮ' => 'ⳮ', - 'Ⳳ' => 'ⳳ', - 'Ꙁ' => 'ꙁ', - 'Ꙃ' => 'ꙃ', - 'Ꙅ' => 'ꙅ', - 'Ꙇ' => 'ꙇ', - 'Ꙉ' => 'ꙉ', - 'Ꙋ' => 'ꙋ', - 'Ꙍ' => 'ꙍ', - 'Ꙏ' => 'ꙏ', - 'Ꙑ' => 'ꙑ', - 'Ꙓ' => 'ꙓ', - 'Ꙕ' => 'ꙕ', - 'Ꙗ' => 'ꙗ', - 'Ꙙ' => 'ꙙ', - 'Ꙛ' => 'ꙛ', - 'Ꙝ' => 'ꙝ', - 'Ꙟ' => 'ꙟ', - 'Ꙡ' => 'ꙡ', - 'Ꙣ' => 'ꙣ', - 'Ꙥ' => 'ꙥ', - 'Ꙧ' => 'ꙧ', - 'Ꙩ' => 'ꙩ', - 'Ꙫ' => 'ꙫ', - 'Ꙭ' => 'ꙭ', - 'Ꚁ' => 'ꚁ', - 'Ꚃ' => 'ꚃ', - 'Ꚅ' => 'ꚅ', - 'Ꚇ' => 'ꚇ', - 'Ꚉ' => 'ꚉ', - 'Ꚋ' => 'ꚋ', - 'Ꚍ' => 'ꚍ', - 'Ꚏ' => 'ꚏ', - 'Ꚑ' => 'ꚑ', - 'Ꚓ' => 'ꚓ', - 'Ꚕ' => 'ꚕ', - 'Ꚗ' => 'ꚗ', - 'Ꚙ' => 'ꚙ', - 'Ꚛ' => 'ꚛ', - 'Ꜣ' => 'ꜣ', - 'Ꜥ' => 'ꜥ', - 'Ꜧ' => 'ꜧ', - 'Ꜩ' => 'ꜩ', - 'Ꜫ' => 'ꜫ', - 'Ꜭ' => 'ꜭ', - 'Ꜯ' => 'ꜯ', - 'Ꜳ' => 'ꜳ', - 'Ꜵ' => 'ꜵ', - 'Ꜷ' => 'ꜷ', - 'Ꜹ' => 'ꜹ', - 'Ꜻ' => 'ꜻ', - 'Ꜽ' => 'ꜽ', - 'Ꜿ' => 'ꜿ', - 'Ꝁ' => 'ꝁ', - 'Ꝃ' => 'ꝃ', - 'Ꝅ' => 'ꝅ', - 'Ꝇ' => 'ꝇ', - 'Ꝉ' => 'ꝉ', - 'Ꝋ' => 'ꝋ', - 'Ꝍ' => 'ꝍ', - 'Ꝏ' => 'ꝏ', - 'Ꝑ' => 'ꝑ', - 'Ꝓ' => 'ꝓ', - 'Ꝕ' => 'ꝕ', - 'Ꝗ' => 'ꝗ', - 'Ꝙ' => 'ꝙ', - 'Ꝛ' => 'ꝛ', - 'Ꝝ' => 'ꝝ', - 'Ꝟ' => 'ꝟ', - 'Ꝡ' => 'ꝡ', - 'Ꝣ' => 'ꝣ', - 'Ꝥ' => 'ꝥ', - 'Ꝧ' => 'ꝧ', - 'Ꝩ' => 'ꝩ', - 'Ꝫ' => 'ꝫ', - 'Ꝭ' => 'ꝭ', - 'Ꝯ' => 'ꝯ', - 'Ꝺ' => 'ꝺ', - 'Ꝼ' => 'ꝼ', - 'Ᵹ' => 'ᵹ', - 'Ꝿ' => 'ꝿ', - 'Ꞁ' => 'ꞁ', - 'Ꞃ' => 'ꞃ', - 'Ꞅ' => 'ꞅ', - 'Ꞇ' => 'ꞇ', - 'Ꞌ' => 'ꞌ', - 'Ɥ' => 'ɥ', - 'Ꞑ' => 'ꞑ', - 'Ꞓ' => 'ꞓ', - 'Ꞗ' => 'ꞗ', - 'Ꞙ' => 'ꞙ', - 'Ꞛ' => 'ꞛ', - 'Ꞝ' => 'ꞝ', - 'Ꞟ' => 'ꞟ', - 'Ꞡ' => 'ꞡ', - 'Ꞣ' => 'ꞣ', - 'Ꞥ' => 'ꞥ', - 'Ꞧ' => 'ꞧ', - 'Ꞩ' => 'ꞩ', - 'Ɦ' => 'ɦ', - 'Ɜ' => 'ɜ', - 'Ɡ' => 'ɡ', - 'Ɬ' => 'ɬ', - 'Ɪ' => 'ɪ', - 'Ʞ' => 'ʞ', - 'Ʇ' => 'ʇ', - 'Ʝ' => 'ʝ', - 'Ꭓ' => 'ꭓ', - 'Ꞵ' => 'ꞵ', - 'Ꞷ' => 'ꞷ', - 'Ꞹ' => 'ꞹ', - 'Ꞻ' => 'ꞻ', - 'Ꞽ' => 'ꞽ', - 'Ꞿ' => 'ꞿ', - 'Ꟃ' => 'ꟃ', - 'Ꞔ' => 'ꞔ', - 'Ʂ' => 'ʂ', - 'Ᶎ' => 'ᶎ', - 'Ꟈ' => 'ꟈ', - 'Ꟊ' => 'ꟊ', - 'Ꟶ' => 'ꟶ', - 'A' => 'a', - 'B' => 'b', - 'C' => 'c', - 'D' => 'd', - 'E' => 'e', - 'F' => 'f', - 'G' => 'g', - 'H' => 'h', - 'I' => 'i', - 'J' => 'j', - 'K' => 'k', - 'L' => 'l', - 'M' => 'm', - 'N' => 'n', - 'O' => 'o', - 'P' => 'p', - 'Q' => 'q', - 'R' => 'r', - 'S' => 's', - 'T' => 't', - 'U' => 'u', - 'V' => 'v', - 'W' => 'w', - 'X' => 'x', - 'Y' => 'y', - 'Z' => 'z', - '𐐀' => '𐐨', - '𐐁' => '𐐩', - '𐐂' => '𐐪', - '𐐃' => '𐐫', - '𐐄' => '𐐬', - '𐐅' => '𐐭', - '𐐆' => '𐐮', - '𐐇' => '𐐯', - '𐐈' => '𐐰', - '𐐉' => '𐐱', - '𐐊' => '𐐲', - '𐐋' => '𐐳', - '𐐌' => '𐐴', - '𐐍' => '𐐵', - '𐐎' => '𐐶', - '𐐏' => '𐐷', - '𐐐' => '𐐸', - '𐐑' => '𐐹', - '𐐒' => '𐐺', - '𐐓' => '𐐻', - '𐐔' => '𐐼', - '𐐕' => '𐐽', - '𐐖' => '𐐾', - '𐐗' => '𐐿', - '𐐘' => '𐑀', - '𐐙' => '𐑁', - '𐐚' => '𐑂', - '𐐛' => '𐑃', - '𐐜' => '𐑄', - '𐐝' => '𐑅', - '𐐞' => '𐑆', - '𐐟' => '𐑇', - '𐐠' => '𐑈', - '𐐡' => '𐑉', - '𐐢' => '𐑊', - '𐐣' => '𐑋', - '𐐤' => '𐑌', - '𐐥' => '𐑍', - '𐐦' => '𐑎', - '𐐧' => '𐑏', - '𐒰' => '𐓘', - '𐒱' => '𐓙', - '𐒲' => '𐓚', - '𐒳' => '𐓛', - '𐒴' => '𐓜', - '𐒵' => '𐓝', - '𐒶' => '𐓞', - '𐒷' => '𐓟', - '𐒸' => '𐓠', - '𐒹' => '𐓡', - '𐒺' => '𐓢', - '𐒻' => '𐓣', - '𐒼' => '𐓤', - '𐒽' => '𐓥', - '𐒾' => '𐓦', - '𐒿' => '𐓧', - '𐓀' => '𐓨', - '𐓁' => '𐓩', - '𐓂' => '𐓪', - '𐓃' => '𐓫', - '𐓄' => '𐓬', - '𐓅' => '𐓭', - '𐓆' => '𐓮', - '𐓇' => '𐓯', - '𐓈' => '𐓰', - '𐓉' => '𐓱', - '𐓊' => '𐓲', - '𐓋' => '𐓳', - '𐓌' => '𐓴', - '𐓍' => '𐓵', - '𐓎' => '𐓶', - '𐓏' => '𐓷', - '𐓐' => '𐓸', - '𐓑' => '𐓹', - '𐓒' => '𐓺', - '𐓓' => '𐓻', - '𐲀' => '𐳀', - '𐲁' => '𐳁', - '𐲂' => '𐳂', - '𐲃' => '𐳃', - '𐲄' => '𐳄', - '𐲅' => '𐳅', - '𐲆' => '𐳆', - '𐲇' => '𐳇', - '𐲈' => '𐳈', - '𐲉' => '𐳉', - '𐲊' => '𐳊', - '𐲋' => '𐳋', - '𐲌' => '𐳌', - '𐲍' => '𐳍', - '𐲎' => '𐳎', - '𐲏' => '𐳏', - '𐲐' => '𐳐', - '𐲑' => '𐳑', - '𐲒' => '𐳒', - '𐲓' => '𐳓', - '𐲔' => '𐳔', - '𐲕' => '𐳕', - '𐲖' => '𐳖', - '𐲗' => '𐳗', - '𐲘' => '𐳘', - '𐲙' => '𐳙', - '𐲚' => '𐳚', - '𐲛' => '𐳛', - '𐲜' => '𐳜', - '𐲝' => '𐳝', - '𐲞' => '𐳞', - '𐲟' => '𐳟', - '𐲠' => '𐳠', - '𐲡' => '𐳡', - '𐲢' => '𐳢', - '𐲣' => '𐳣', - '𐲤' => '𐳤', - '𐲥' => '𐳥', - '𐲦' => '𐳦', - '𐲧' => '𐳧', - '𐲨' => '𐳨', - '𐲩' => '𐳩', - '𐲪' => '𐳪', - '𐲫' => '𐳫', - '𐲬' => '𐳬', - '𐲭' => '𐳭', - '𐲮' => '𐳮', - '𐲯' => '𐳯', - '𐲰' => '𐳰', - '𐲱' => '𐳱', - '𐲲' => '𐳲', - '𑢠' => '𑣀', - '𑢡' => '𑣁', - '𑢢' => '𑣂', - '𑢣' => '𑣃', - '𑢤' => '𑣄', - '𑢥' => '𑣅', - '𑢦' => '𑣆', - '𑢧' => '𑣇', - '𑢨' => '𑣈', - '𑢩' => '𑣉', - '𑢪' => '𑣊', - '𑢫' => '𑣋', - '𑢬' => '𑣌', - '𑢭' => '𑣍', - '𑢮' => '𑣎', - '𑢯' => '𑣏', - '𑢰' => '𑣐', - '𑢱' => '𑣑', - '𑢲' => '𑣒', - '𑢳' => '𑣓', - '𑢴' => '𑣔', - '𑢵' => '𑣕', - '𑢶' => '𑣖', - '𑢷' => '𑣗', - '𑢸' => '𑣘', - '𑢹' => '𑣙', - '𑢺' => '𑣚', - '𑢻' => '𑣛', - '𑢼' => '𑣜', - '𑢽' => '𑣝', - '𑢾' => '𑣞', - '𑢿' => '𑣟', - '𖹀' => '𖹠', - '𖹁' => '𖹡', - '𖹂' => '𖹢', - '𖹃' => '𖹣', - '𖹄' => '𖹤', - '𖹅' => '𖹥', - '𖹆' => '𖹦', - '𖹇' => '𖹧', - '𖹈' => '𖹨', - '𖹉' => '𖹩', - '𖹊' => '𖹪', - '𖹋' => '𖹫', - '𖹌' => '𖹬', - '𖹍' => '𖹭', - '𖹎' => '𖹮', - '𖹏' => '𖹯', - '𖹐' => '𖹰', - '𖹑' => '𖹱', - '𖹒' => '𖹲', - '𖹓' => '𖹳', - '𖹔' => '𖹴', - '𖹕' => '𖹵', - '𖹖' => '𖹶', - '𖹗' => '𖹷', - '𖹘' => '𖹸', - '𖹙' => '𖹹', - '𖹚' => '𖹺', - '𖹛' => '𖹻', - '𖹜' => '𖹼', - '𖹝' => '𖹽', - '𖹞' => '𖹾', - '𖹟' => '𖹿', - '𞤀' => '𞤢', - '𞤁' => '𞤣', - '𞤂' => '𞤤', - '𞤃' => '𞤥', - '𞤄' => '𞤦', - '𞤅' => '𞤧', - '𞤆' => '𞤨', - '𞤇' => '𞤩', - '𞤈' => '𞤪', - '𞤉' => '𞤫', - '𞤊' => '𞤬', - '𞤋' => '𞤭', - '𞤌' => '𞤮', - '𞤍' => '𞤯', - '𞤎' => '𞤰', - '𞤏' => '𞤱', - '𞤐' => '𞤲', - '𞤑' => '𞤳', - '𞤒' => '𞤴', - '𞤓' => '𞤵', - '𞤔' => '𞤶', - '𞤕' => '𞤷', - '𞤖' => '𞤸', - '𞤗' => '𞤹', - '𞤘' => '𞤺', - '𞤙' => '𞤻', - '𞤚' => '𞤼', - '𞤛' => '𞤽', - '𞤜' => '𞤾', - '𞤝' => '𞤿', - '𞤞' => '𞥀', - '𞤟' => '𞥁', - '𞤠' => '𞥂', - '𞤡' => '𞥃', -); diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php deleted file mode 100644 index 2a8f6e7..0000000 --- a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php +++ /dev/null @@ -1,5 +0,0 @@ - 'A', - 'b' => 'B', - 'c' => 'C', - 'd' => 'D', - 'e' => 'E', - 'f' => 'F', - 'g' => 'G', - 'h' => 'H', - 'i' => 'I', - 'j' => 'J', - 'k' => 'K', - 'l' => 'L', - 'm' => 'M', - 'n' => 'N', - 'o' => 'O', - 'p' => 'P', - 'q' => 'Q', - 'r' => 'R', - 's' => 'S', - 't' => 'T', - 'u' => 'U', - 'v' => 'V', - 'w' => 'W', - 'x' => 'X', - 'y' => 'Y', - 'z' => 'Z', - 'µ' => 'Μ', - 'à' => 'À', - 'á' => 'Á', - 'â' => 'Â', - 'ã' => 'Ã', - 'ä' => 'Ä', - 'å' => 'Å', - 'æ' => 'Æ', - 'ç' => 'Ç', - 'è' => 'È', - 'é' => 'É', - 'ê' => 'Ê', - 'ë' => 'Ë', - 'ì' => 'Ì', - 'í' => 'Í', - 'î' => 'Î', - 'ï' => 'Ï', - 'ð' => 'Ð', - 'ñ' => 'Ñ', - 'ò' => 'Ò', - 'ó' => 'Ó', - 'ô' => 'Ô', - 'õ' => 'Õ', - 'ö' => 'Ö', - 'ø' => 'Ø', - 'ù' => 'Ù', - 'ú' => 'Ú', - 'û' => 'Û', - 'ü' => 'Ü', - 'ý' => 'Ý', - 'þ' => 'Þ', - 'ÿ' => 'Ÿ', - 'ā' => 'Ā', - 'ă' => 'Ă', - 'ą' => 'Ą', - 'ć' => 'Ć', - 'ĉ' => 'Ĉ', - 'ċ' => 'Ċ', - 'č' => 'Č', - 'ď' => 'Ď', - 'đ' => 'Đ', - 'ē' => 'Ē', - 'ĕ' => 'Ĕ', - 'ė' => 'Ė', - 'ę' => 'Ę', - 'ě' => 'Ě', - 'ĝ' => 'Ĝ', - 'ğ' => 'Ğ', - 'ġ' => 'Ġ', - 'ģ' => 'Ģ', - 'ĥ' => 'Ĥ', - 'ħ' => 'Ħ', - 'ĩ' => 'Ĩ', - 'ī' => 'Ī', - 'ĭ' => 'Ĭ', - 'į' => 'Į', - 'ı' => 'I', - 'ij' => 'IJ', - 'ĵ' => 'Ĵ', - 'ķ' => 'Ķ', - 'ĺ' => 'Ĺ', - 'ļ' => 'Ļ', - 'ľ' => 'Ľ', - 'ŀ' => 'Ŀ', - 'ł' => 'Ł', - 'ń' => 'Ń', - 'ņ' => 'Ņ', - 'ň' => 'Ň', - 'ŋ' => 'Ŋ', - 'ō' => 'Ō', - 'ŏ' => 'Ŏ', - 'ő' => 'Ő', - 'œ' => 'Œ', - 'ŕ' => 'Ŕ', - 'ŗ' => 'Ŗ', - 'ř' => 'Ř', - 'ś' => 'Ś', - 'ŝ' => 'Ŝ', - 'ş' => 'Ş', - 'š' => 'Š', - 'ţ' => 'Ţ', - 'ť' => 'Ť', - 'ŧ' => 'Ŧ', - 'ũ' => 'Ũ', - 'ū' => 'Ū', - 'ŭ' => 'Ŭ', - 'ů' => 'Ů', - 'ű' => 'Ű', - 'ų' => 'Ų', - 'ŵ' => 'Ŵ', - 'ŷ' => 'Ŷ', - 'ź' => 'Ź', - 'ż' => 'Ż', - 'ž' => 'Ž', - 'ſ' => 'S', - 'ƀ' => 'Ƀ', - 'ƃ' => 'Ƃ', - 'ƅ' => 'Ƅ', - 'ƈ' => 'Ƈ', - 'ƌ' => 'Ƌ', - 'ƒ' => 'Ƒ', - 'ƕ' => 'Ƕ', - 'ƙ' => 'Ƙ', - 'ƚ' => 'Ƚ', - 'ƞ' => 'Ƞ', - 'ơ' => 'Ơ', - 'ƣ' => 'Ƣ', - 'ƥ' => 'Ƥ', - 'ƨ' => 'Ƨ', - 'ƭ' => 'Ƭ', - 'ư' => 'Ư', - 'ƴ' => 'Ƴ', - 'ƶ' => 'Ƶ', - 'ƹ' => 'Ƹ', - 'ƽ' => 'Ƽ', - 'ƿ' => 'Ƿ', - 'Dž' => 'DŽ', - 'dž' => 'DŽ', - 'Lj' => 'LJ', - 'lj' => 'LJ', - 'Nj' => 'NJ', - 'nj' => 'NJ', - 'ǎ' => 'Ǎ', - 'ǐ' => 'Ǐ', - 'ǒ' => 'Ǒ', - 'ǔ' => 'Ǔ', - 'ǖ' => 'Ǖ', - 'ǘ' => 'Ǘ', - 'ǚ' => 'Ǚ', - 'ǜ' => 'Ǜ', - 'ǝ' => 'Ǝ', - 'ǟ' => 'Ǟ', - 'ǡ' => 'Ǡ', - 'ǣ' => 'Ǣ', - 'ǥ' => 'Ǥ', - 'ǧ' => 'Ǧ', - 'ǩ' => 'Ǩ', - 'ǫ' => 'Ǫ', - 'ǭ' => 'Ǭ', - 'ǯ' => 'Ǯ', - 'Dz' => 'DZ', - 'dz' => 'DZ', - 'ǵ' => 'Ǵ', - 'ǹ' => 'Ǹ', - 'ǻ' => 'Ǻ', - 'ǽ' => 'Ǽ', - 'ǿ' => 'Ǿ', - 'ȁ' => 'Ȁ', - 'ȃ' => 'Ȃ', - 'ȅ' => 'Ȅ', - 'ȇ' => 'Ȇ', - 'ȉ' => 'Ȉ', - 'ȋ' => 'Ȋ', - 'ȍ' => 'Ȍ', - 'ȏ' => 'Ȏ', - 'ȑ' => 'Ȑ', - 'ȓ' => 'Ȓ', - 'ȕ' => 'Ȕ', - 'ȗ' => 'Ȗ', - 'ș' => 'Ș', - 'ț' => 'Ț', - 'ȝ' => 'Ȝ', - 'ȟ' => 'Ȟ', - 'ȣ' => 'Ȣ', - 'ȥ' => 'Ȥ', - 'ȧ' => 'Ȧ', - 'ȩ' => 'Ȩ', - 'ȫ' => 'Ȫ', - 'ȭ' => 'Ȭ', - 'ȯ' => 'Ȯ', - 'ȱ' => 'Ȱ', - 'ȳ' => 'Ȳ', - 'ȼ' => 'Ȼ', - 'ȿ' => 'Ȿ', - 'ɀ' => 'Ɀ', - 'ɂ' => 'Ɂ', - 'ɇ' => 'Ɇ', - 'ɉ' => 'Ɉ', - 'ɋ' => 'Ɋ', - 'ɍ' => 'Ɍ', - 'ɏ' => 'Ɏ', - 'ɐ' => 'Ɐ', - 'ɑ' => 'Ɑ', - 'ɒ' => 'Ɒ', - 'ɓ' => 'Ɓ', - 'ɔ' => 'Ɔ', - 'ɖ' => 'Ɖ', - 'ɗ' => 'Ɗ', - 'ə' => 'Ə', - 'ɛ' => 'Ɛ', - 'ɜ' => 'Ɜ', - 'ɠ' => 'Ɠ', - 'ɡ' => 'Ɡ', - 'ɣ' => 'Ɣ', - 'ɥ' => 'Ɥ', - 'ɦ' => 'Ɦ', - 'ɨ' => 'Ɨ', - 'ɩ' => 'Ɩ', - 'ɪ' => 'Ɪ', - 'ɫ' => 'Ɫ', - 'ɬ' => 'Ɬ', - 'ɯ' => 'Ɯ', - 'ɱ' => 'Ɱ', - 'ɲ' => 'Ɲ', - 'ɵ' => 'Ɵ', - 'ɽ' => 'Ɽ', - 'ʀ' => 'Ʀ', - 'ʂ' => 'Ʂ', - 'ʃ' => 'Ʃ', - 'ʇ' => 'Ʇ', - 'ʈ' => 'Ʈ', - 'ʉ' => 'Ʉ', - 'ʊ' => 'Ʊ', - 'ʋ' => 'Ʋ', - 'ʌ' => 'Ʌ', - 'ʒ' => 'Ʒ', - 'ʝ' => 'Ʝ', - 'ʞ' => 'Ʞ', - 'ͅ' => 'Ι', - 'ͱ' => 'Ͱ', - 'ͳ' => 'Ͳ', - 'ͷ' => 'Ͷ', - 'ͻ' => 'Ͻ', - 'ͼ' => 'Ͼ', - 'ͽ' => 'Ͽ', - 'ά' => 'Ά', - 'έ' => 'Έ', - 'ή' => 'Ή', - 'ί' => 'Ί', - 'α' => 'Α', - 'β' => 'Β', - 'γ' => 'Γ', - 'δ' => 'Δ', - 'ε' => 'Ε', - 'ζ' => 'Ζ', - 'η' => 'Η', - 'θ' => 'Θ', - 'ι' => 'Ι', - 'κ' => 'Κ', - 'λ' => 'Λ', - 'μ' => 'Μ', - 'ν' => 'Ν', - 'ξ' => 'Ξ', - 'ο' => 'Ο', - 'π' => 'Π', - 'ρ' => 'Ρ', - 'ς' => 'Σ', - 'σ' => 'Σ', - 'τ' => 'Τ', - 'υ' => 'Υ', - 'φ' => 'Φ', - 'χ' => 'Χ', - 'ψ' => 'Ψ', - 'ω' => 'Ω', - 'ϊ' => 'Ϊ', - 'ϋ' => 'Ϋ', - 'ό' => 'Ό', - 'ύ' => 'Ύ', - 'ώ' => 'Ώ', - 'ϐ' => 'Β', - 'ϑ' => 'Θ', - 'ϕ' => 'Φ', - 'ϖ' => 'Π', - 'ϗ' => 'Ϗ', - 'ϙ' => 'Ϙ', - 'ϛ' => 'Ϛ', - 'ϝ' => 'Ϝ', - 'ϟ' => 'Ϟ', - 'ϡ' => 'Ϡ', - 'ϣ' => 'Ϣ', - 'ϥ' => 'Ϥ', - 'ϧ' => 'Ϧ', - 'ϩ' => 'Ϩ', - 'ϫ' => 'Ϫ', - 'ϭ' => 'Ϭ', - 'ϯ' => 'Ϯ', - 'ϰ' => 'Κ', - 'ϱ' => 'Ρ', - 'ϲ' => 'Ϲ', - 'ϳ' => 'Ϳ', - 'ϵ' => 'Ε', - 'ϸ' => 'Ϸ', - 'ϻ' => 'Ϻ', - 'а' => 'А', - 'б' => 'Б', - 'в' => 'В', - 'г' => 'Г', - 'д' => 'Д', - 'е' => 'Е', - 'ж' => 'Ж', - 'з' => 'З', - 'и' => 'И', - 'й' => 'Й', - 'к' => 'К', - 'л' => 'Л', - 'м' => 'М', - 'н' => 'Н', - 'о' => 'О', - 'п' => 'П', - 'р' => 'Р', - 'с' => 'С', - 'т' => 'Т', - 'у' => 'У', - 'ф' => 'Ф', - 'х' => 'Х', - 'ц' => 'Ц', - 'ч' => 'Ч', - 'ш' => 'Ш', - 'щ' => 'Щ', - 'ъ' => 'Ъ', - 'ы' => 'Ы', - 'ь' => 'Ь', - 'э' => 'Э', - 'ю' => 'Ю', - 'я' => 'Я', - 'ѐ' => 'Ѐ', - 'ё' => 'Ё', - 'ђ' => 'Ђ', - 'ѓ' => 'Ѓ', - 'є' => 'Є', - 'ѕ' => 'Ѕ', - 'і' => 'І', - 'ї' => 'Ї', - 'ј' => 'Ј', - 'љ' => 'Љ', - 'њ' => 'Њ', - 'ћ' => 'Ћ', - 'ќ' => 'Ќ', - 'ѝ' => 'Ѝ', - 'ў' => 'Ў', - 'џ' => 'Џ', - 'ѡ' => 'Ѡ', - 'ѣ' => 'Ѣ', - 'ѥ' => 'Ѥ', - 'ѧ' => 'Ѧ', - 'ѩ' => 'Ѩ', - 'ѫ' => 'Ѫ', - 'ѭ' => 'Ѭ', - 'ѯ' => 'Ѯ', - 'ѱ' => 'Ѱ', - 'ѳ' => 'Ѳ', - 'ѵ' => 'Ѵ', - 'ѷ' => 'Ѷ', - 'ѹ' => 'Ѹ', - 'ѻ' => 'Ѻ', - 'ѽ' => 'Ѽ', - 'ѿ' => 'Ѿ', - 'ҁ' => 'Ҁ', - 'ҋ' => 'Ҋ', - 'ҍ' => 'Ҍ', - 'ҏ' => 'Ҏ', - 'ґ' => 'Ґ', - 'ғ' => 'Ғ', - 'ҕ' => 'Ҕ', - 'җ' => 'Җ', - 'ҙ' => 'Ҙ', - 'қ' => 'Қ', - 'ҝ' => 'Ҝ', - 'ҟ' => 'Ҟ', - 'ҡ' => 'Ҡ', - 'ң' => 'Ң', - 'ҥ' => 'Ҥ', - 'ҧ' => 'Ҧ', - 'ҩ' => 'Ҩ', - 'ҫ' => 'Ҫ', - 'ҭ' => 'Ҭ', - 'ү' => 'Ү', - 'ұ' => 'Ұ', - 'ҳ' => 'Ҳ', - 'ҵ' => 'Ҵ', - 'ҷ' => 'Ҷ', - 'ҹ' => 'Ҹ', - 'һ' => 'Һ', - 'ҽ' => 'Ҽ', - 'ҿ' => 'Ҿ', - 'ӂ' => 'Ӂ', - 'ӄ' => 'Ӄ', - 'ӆ' => 'Ӆ', - 'ӈ' => 'Ӈ', - 'ӊ' => 'Ӊ', - 'ӌ' => 'Ӌ', - 'ӎ' => 'Ӎ', - 'ӏ' => 'Ӏ', - 'ӑ' => 'Ӑ', - 'ӓ' => 'Ӓ', - 'ӕ' => 'Ӕ', - 'ӗ' => 'Ӗ', - 'ә' => 'Ә', - 'ӛ' => 'Ӛ', - 'ӝ' => 'Ӝ', - 'ӟ' => 'Ӟ', - 'ӡ' => 'Ӡ', - 'ӣ' => 'Ӣ', - 'ӥ' => 'Ӥ', - 'ӧ' => 'Ӧ', - 'ө' => 'Ө', - 'ӫ' => 'Ӫ', - 'ӭ' => 'Ӭ', - 'ӯ' => 'Ӯ', - 'ӱ' => 'Ӱ', - 'ӳ' => 'Ӳ', - 'ӵ' => 'Ӵ', - 'ӷ' => 'Ӷ', - 'ӹ' => 'Ӹ', - 'ӻ' => 'Ӻ', - 'ӽ' => 'Ӽ', - 'ӿ' => 'Ӿ', - 'ԁ' => 'Ԁ', - 'ԃ' => 'Ԃ', - 'ԅ' => 'Ԅ', - 'ԇ' => 'Ԇ', - 'ԉ' => 'Ԉ', - 'ԋ' => 'Ԋ', - 'ԍ' => 'Ԍ', - 'ԏ' => 'Ԏ', - 'ԑ' => 'Ԑ', - 'ԓ' => 'Ԓ', - 'ԕ' => 'Ԕ', - 'ԗ' => 'Ԗ', - 'ԙ' => 'Ԙ', - 'ԛ' => 'Ԛ', - 'ԝ' => 'Ԝ', - 'ԟ' => 'Ԟ', - 'ԡ' => 'Ԡ', - 'ԣ' => 'Ԣ', - 'ԥ' => 'Ԥ', - 'ԧ' => 'Ԧ', - 'ԩ' => 'Ԩ', - 'ԫ' => 'Ԫ', - 'ԭ' => 'Ԭ', - 'ԯ' => 'Ԯ', - 'ա' => 'Ա', - 'բ' => 'Բ', - 'գ' => 'Գ', - 'դ' => 'Դ', - 'ե' => 'Ե', - 'զ' => 'Զ', - 'է' => 'Է', - 'ը' => 'Ը', - 'թ' => 'Թ', - 'ժ' => 'Ժ', - 'ի' => 'Ի', - 'լ' => 'Լ', - 'խ' => 'Խ', - 'ծ' => 'Ծ', - 'կ' => 'Կ', - 'հ' => 'Հ', - 'ձ' => 'Ձ', - 'ղ' => 'Ղ', - 'ճ' => 'Ճ', - 'մ' => 'Մ', - 'յ' => 'Յ', - 'ն' => 'Ն', - 'շ' => 'Շ', - 'ո' => 'Ո', - 'չ' => 'Չ', - 'պ' => 'Պ', - 'ջ' => 'Ջ', - 'ռ' => 'Ռ', - 'ս' => 'Ս', - 'վ' => 'Վ', - 'տ' => 'Տ', - 'ր' => 'Ր', - 'ց' => 'Ց', - 'ւ' => 'Ւ', - 'փ' => 'Փ', - 'ք' => 'Ք', - 'օ' => 'Օ', - 'ֆ' => 'Ֆ', - 'ა' => 'Ა', - 'ბ' => 'Ბ', - 'გ' => 'Გ', - 'დ' => 'Დ', - 'ე' => 'Ე', - 'ვ' => 'Ვ', - 'ზ' => 'Ზ', - 'თ' => 'Თ', - 'ი' => 'Ი', - 'კ' => 'Კ', - 'ლ' => 'Ლ', - 'მ' => 'Მ', - 'ნ' => 'Ნ', - 'ო' => 'Ო', - 'პ' => 'Პ', - 'ჟ' => 'Ჟ', - 'რ' => 'Რ', - 'ს' => 'Ს', - 'ტ' => 'Ტ', - 'უ' => 'Უ', - 'ფ' => 'Ფ', - 'ქ' => 'Ქ', - 'ღ' => 'Ღ', - 'ყ' => 'Ყ', - 'შ' => 'Შ', - 'ჩ' => 'Ჩ', - 'ც' => 'Ც', - 'ძ' => 'Ძ', - 'წ' => 'Წ', - 'ჭ' => 'Ჭ', - 'ხ' => 'Ხ', - 'ჯ' => 'Ჯ', - 'ჰ' => 'Ჰ', - 'ჱ' => 'Ჱ', - 'ჲ' => 'Ჲ', - 'ჳ' => 'Ჳ', - 'ჴ' => 'Ჴ', - 'ჵ' => 'Ჵ', - 'ჶ' => 'Ჶ', - 'ჷ' => 'Ჷ', - 'ჸ' => 'Ჸ', - 'ჹ' => 'Ჹ', - 'ჺ' => 'Ჺ', - 'ჽ' => 'Ჽ', - 'ჾ' => 'Ჾ', - 'ჿ' => 'Ჿ', - 'ᏸ' => 'Ᏸ', - 'ᏹ' => 'Ᏹ', - 'ᏺ' => 'Ᏺ', - 'ᏻ' => 'Ᏻ', - 'ᏼ' => 'Ᏼ', - 'ᏽ' => 'Ᏽ', - 'ᲀ' => 'В', - 'ᲁ' => 'Д', - 'ᲂ' => 'О', - 'ᲃ' => 'С', - 'ᲄ' => 'Т', - 'ᲅ' => 'Т', - 'ᲆ' => 'Ъ', - 'ᲇ' => 'Ѣ', - 'ᲈ' => 'Ꙋ', - 'ᵹ' => 'Ᵹ', - 'ᵽ' => 'Ᵽ', - 'ᶎ' => 'Ᶎ', - 'ḁ' => 'Ḁ', - 'ḃ' => 'Ḃ', - 'ḅ' => 'Ḅ', - 'ḇ' => 'Ḇ', - 'ḉ' => 'Ḉ', - 'ḋ' => 'Ḋ', - 'ḍ' => 'Ḍ', - 'ḏ' => 'Ḏ', - 'ḑ' => 'Ḑ', - 'ḓ' => 'Ḓ', - 'ḕ' => 'Ḕ', - 'ḗ' => 'Ḗ', - 'ḙ' => 'Ḙ', - 'ḛ' => 'Ḛ', - 'ḝ' => 'Ḝ', - 'ḟ' => 'Ḟ', - 'ḡ' => 'Ḡ', - 'ḣ' => 'Ḣ', - 'ḥ' => 'Ḥ', - 'ḧ' => 'Ḧ', - 'ḩ' => 'Ḩ', - 'ḫ' => 'Ḫ', - 'ḭ' => 'Ḭ', - 'ḯ' => 'Ḯ', - 'ḱ' => 'Ḱ', - 'ḳ' => 'Ḳ', - 'ḵ' => 'Ḵ', - 'ḷ' => 'Ḷ', - 'ḹ' => 'Ḹ', - 'ḻ' => 'Ḻ', - 'ḽ' => 'Ḽ', - 'ḿ' => 'Ḿ', - 'ṁ' => 'Ṁ', - 'ṃ' => 'Ṃ', - 'ṅ' => 'Ṅ', - 'ṇ' => 'Ṇ', - 'ṉ' => 'Ṉ', - 'ṋ' => 'Ṋ', - 'ṍ' => 'Ṍ', - 'ṏ' => 'Ṏ', - 'ṑ' => 'Ṑ', - 'ṓ' => 'Ṓ', - 'ṕ' => 'Ṕ', - 'ṗ' => 'Ṗ', - 'ṙ' => 'Ṙ', - 'ṛ' => 'Ṛ', - 'ṝ' => 'Ṝ', - 'ṟ' => 'Ṟ', - 'ṡ' => 'Ṡ', - 'ṣ' => 'Ṣ', - 'ṥ' => 'Ṥ', - 'ṧ' => 'Ṧ', - 'ṩ' => 'Ṩ', - 'ṫ' => 'Ṫ', - 'ṭ' => 'Ṭ', - 'ṯ' => 'Ṯ', - 'ṱ' => 'Ṱ', - 'ṳ' => 'Ṳ', - 'ṵ' => 'Ṵ', - 'ṷ' => 'Ṷ', - 'ṹ' => 'Ṹ', - 'ṻ' => 'Ṻ', - 'ṽ' => 'Ṽ', - 'ṿ' => 'Ṿ', - 'ẁ' => 'Ẁ', - 'ẃ' => 'Ẃ', - 'ẅ' => 'Ẅ', - 'ẇ' => 'Ẇ', - 'ẉ' => 'Ẉ', - 'ẋ' => 'Ẋ', - 'ẍ' => 'Ẍ', - 'ẏ' => 'Ẏ', - 'ẑ' => 'Ẑ', - 'ẓ' => 'Ẓ', - 'ẕ' => 'Ẕ', - 'ẛ' => 'Ṡ', - 'ạ' => 'Ạ', - 'ả' => 'Ả', - 'ấ' => 'Ấ', - 'ầ' => 'Ầ', - 'ẩ' => 'Ẩ', - 'ẫ' => 'Ẫ', - 'ậ' => 'Ậ', - 'ắ' => 'Ắ', - 'ằ' => 'Ằ', - 'ẳ' => 'Ẳ', - 'ẵ' => 'Ẵ', - 'ặ' => 'Ặ', - 'ẹ' => 'Ẹ', - 'ẻ' => 'Ẻ', - 'ẽ' => 'Ẽ', - 'ế' => 'Ế', - 'ề' => 'Ề', - 'ể' => 'Ể', - 'ễ' => 'Ễ', - 'ệ' => 'Ệ', - 'ỉ' => 'Ỉ', - 'ị' => 'Ị', - 'ọ' => 'Ọ', - 'ỏ' => 'Ỏ', - 'ố' => 'Ố', - 'ồ' => 'Ồ', - 'ổ' => 'Ổ', - 'ỗ' => 'Ỗ', - 'ộ' => 'Ộ', - 'ớ' => 'Ớ', - 'ờ' => 'Ờ', - 'ở' => 'Ở', - 'ỡ' => 'Ỡ', - 'ợ' => 'Ợ', - 'ụ' => 'Ụ', - 'ủ' => 'Ủ', - 'ứ' => 'Ứ', - 'ừ' => 'Ừ', - 'ử' => 'Ử', - 'ữ' => 'Ữ', - 'ự' => 'Ự', - 'ỳ' => 'Ỳ', - 'ỵ' => 'Ỵ', - 'ỷ' => 'Ỷ', - 'ỹ' => 'Ỹ', - 'ỻ' => 'Ỻ', - 'ỽ' => 'Ỽ', - 'ỿ' => 'Ỿ', - 'ἀ' => 'Ἀ', - 'ἁ' => 'Ἁ', - 'ἂ' => 'Ἂ', - 'ἃ' => 'Ἃ', - 'ἄ' => 'Ἄ', - 'ἅ' => 'Ἅ', - 'ἆ' => 'Ἆ', - 'ἇ' => 'Ἇ', - 'ἐ' => 'Ἐ', - 'ἑ' => 'Ἑ', - 'ἒ' => 'Ἒ', - 'ἓ' => 'Ἓ', - 'ἔ' => 'Ἔ', - 'ἕ' => 'Ἕ', - 'ἠ' => 'Ἠ', - 'ἡ' => 'Ἡ', - 'ἢ' => 'Ἢ', - 'ἣ' => 'Ἣ', - 'ἤ' => 'Ἤ', - 'ἥ' => 'Ἥ', - 'ἦ' => 'Ἦ', - 'ἧ' => 'Ἧ', - 'ἰ' => 'Ἰ', - 'ἱ' => 'Ἱ', - 'ἲ' => 'Ἲ', - 'ἳ' => 'Ἳ', - 'ἴ' => 'Ἴ', - 'ἵ' => 'Ἵ', - 'ἶ' => 'Ἶ', - 'ἷ' => 'Ἷ', - 'ὀ' => 'Ὀ', - 'ὁ' => 'Ὁ', - 'ὂ' => 'Ὂ', - 'ὃ' => 'Ὃ', - 'ὄ' => 'Ὄ', - 'ὅ' => 'Ὅ', - 'ὑ' => 'Ὑ', - 'ὓ' => 'Ὓ', - 'ὕ' => 'Ὕ', - 'ὗ' => 'Ὗ', - 'ὠ' => 'Ὠ', - 'ὡ' => 'Ὡ', - 'ὢ' => 'Ὢ', - 'ὣ' => 'Ὣ', - 'ὤ' => 'Ὤ', - 'ὥ' => 'Ὥ', - 'ὦ' => 'Ὦ', - 'ὧ' => 'Ὧ', - 'ὰ' => 'Ὰ', - 'ά' => 'Ά', - 'ὲ' => 'Ὲ', - 'έ' => 'Έ', - 'ὴ' => 'Ὴ', - 'ή' => 'Ή', - 'ὶ' => 'Ὶ', - 'ί' => 'Ί', - 'ὸ' => 'Ὸ', - 'ό' => 'Ό', - 'ὺ' => 'Ὺ', - 'ύ' => 'Ύ', - 'ὼ' => 'Ὼ', - 'ώ' => 'Ώ', - 'ᾀ' => 'ἈΙ', - 'ᾁ' => 'ἉΙ', - 'ᾂ' => 'ἊΙ', - 'ᾃ' => 'ἋΙ', - 'ᾄ' => 'ἌΙ', - 'ᾅ' => 'ἍΙ', - 'ᾆ' => 'ἎΙ', - 'ᾇ' => 'ἏΙ', - 'ᾐ' => 'ἨΙ', - 'ᾑ' => 'ἩΙ', - 'ᾒ' => 'ἪΙ', - 'ᾓ' => 'ἫΙ', - 'ᾔ' => 'ἬΙ', - 'ᾕ' => 'ἭΙ', - 'ᾖ' => 'ἮΙ', - 'ᾗ' => 'ἯΙ', - 'ᾠ' => 'ὨΙ', - 'ᾡ' => 'ὩΙ', - 'ᾢ' => 'ὪΙ', - 'ᾣ' => 'ὫΙ', - 'ᾤ' => 'ὬΙ', - 'ᾥ' => 'ὭΙ', - 'ᾦ' => 'ὮΙ', - 'ᾧ' => 'ὯΙ', - 'ᾰ' => 'Ᾰ', - 'ᾱ' => 'Ᾱ', - 'ᾳ' => 'ΑΙ', - 'ι' => 'Ι', - 'ῃ' => 'ΗΙ', - 'ῐ' => 'Ῐ', - 'ῑ' => 'Ῑ', - 'ῠ' => 'Ῠ', - 'ῡ' => 'Ῡ', - 'ῥ' => 'Ῥ', - 'ῳ' => 'ΩΙ', - 'ⅎ' => 'Ⅎ', - 'ⅰ' => 'Ⅰ', - 'ⅱ' => 'Ⅱ', - 'ⅲ' => 'Ⅲ', - 'ⅳ' => 'Ⅳ', - 'ⅴ' => 'Ⅴ', - 'ⅵ' => 'Ⅵ', - 'ⅶ' => 'Ⅶ', - 'ⅷ' => 'Ⅷ', - 'ⅸ' => 'Ⅸ', - 'ⅹ' => 'Ⅹ', - 'ⅺ' => 'Ⅺ', - 'ⅻ' => 'Ⅻ', - 'ⅼ' => 'Ⅼ', - 'ⅽ' => 'Ⅽ', - 'ⅾ' => 'Ⅾ', - 'ⅿ' => 'Ⅿ', - 'ↄ' => 'Ↄ', - 'ⓐ' => 'Ⓐ', - 'ⓑ' => 'Ⓑ', - 'ⓒ' => 'Ⓒ', - 'ⓓ' => 'Ⓓ', - 'ⓔ' => 'Ⓔ', - 'ⓕ' => 'Ⓕ', - 'ⓖ' => 'Ⓖ', - 'ⓗ' => 'Ⓗ', - 'ⓘ' => 'Ⓘ', - 'ⓙ' => 'Ⓙ', - 'ⓚ' => 'Ⓚ', - 'ⓛ' => 'Ⓛ', - 'ⓜ' => 'Ⓜ', - 'ⓝ' => 'Ⓝ', - 'ⓞ' => 'Ⓞ', - 'ⓟ' => 'Ⓟ', - 'ⓠ' => 'Ⓠ', - 'ⓡ' => 'Ⓡ', - 'ⓢ' => 'Ⓢ', - 'ⓣ' => 'Ⓣ', - 'ⓤ' => 'Ⓤ', - 'ⓥ' => 'Ⓥ', - 'ⓦ' => 'Ⓦ', - 'ⓧ' => 'Ⓧ', - 'ⓨ' => 'Ⓨ', - 'ⓩ' => 'Ⓩ', - 'ⰰ' => 'Ⰰ', - 'ⰱ' => 'Ⰱ', - 'ⰲ' => 'Ⰲ', - 'ⰳ' => 'Ⰳ', - 'ⰴ' => 'Ⰴ', - 'ⰵ' => 'Ⰵ', - 'ⰶ' => 'Ⰶ', - 'ⰷ' => 'Ⰷ', - 'ⰸ' => 'Ⰸ', - 'ⰹ' => 'Ⰹ', - 'ⰺ' => 'Ⰺ', - 'ⰻ' => 'Ⰻ', - 'ⰼ' => 'Ⰼ', - 'ⰽ' => 'Ⰽ', - 'ⰾ' => 'Ⰾ', - 'ⰿ' => 'Ⰿ', - 'ⱀ' => 'Ⱀ', - 'ⱁ' => 'Ⱁ', - 'ⱂ' => 'Ⱂ', - 'ⱃ' => 'Ⱃ', - 'ⱄ' => 'Ⱄ', - 'ⱅ' => 'Ⱅ', - 'ⱆ' => 'Ⱆ', - 'ⱇ' => 'Ⱇ', - 'ⱈ' => 'Ⱈ', - 'ⱉ' => 'Ⱉ', - 'ⱊ' => 'Ⱊ', - 'ⱋ' => 'Ⱋ', - 'ⱌ' => 'Ⱌ', - 'ⱍ' => 'Ⱍ', - 'ⱎ' => 'Ⱎ', - 'ⱏ' => 'Ⱏ', - 'ⱐ' => 'Ⱐ', - 'ⱑ' => 'Ⱑ', - 'ⱒ' => 'Ⱒ', - 'ⱓ' => 'Ⱓ', - 'ⱔ' => 'Ⱔ', - 'ⱕ' => 'Ⱕ', - 'ⱖ' => 'Ⱖ', - 'ⱗ' => 'Ⱗ', - 'ⱘ' => 'Ⱘ', - 'ⱙ' => 'Ⱙ', - 'ⱚ' => 'Ⱚ', - 'ⱛ' => 'Ⱛ', - 'ⱜ' => 'Ⱜ', - 'ⱝ' => 'Ⱝ', - 'ⱞ' => 'Ⱞ', - 'ⱡ' => 'Ⱡ', - 'ⱥ' => 'Ⱥ', - 'ⱦ' => 'Ⱦ', - 'ⱨ' => 'Ⱨ', - 'ⱪ' => 'Ⱪ', - 'ⱬ' => 'Ⱬ', - 'ⱳ' => 'Ⱳ', - 'ⱶ' => 'Ⱶ', - 'ⲁ' => 'Ⲁ', - 'ⲃ' => 'Ⲃ', - 'ⲅ' => 'Ⲅ', - 'ⲇ' => 'Ⲇ', - 'ⲉ' => 'Ⲉ', - 'ⲋ' => 'Ⲋ', - 'ⲍ' => 'Ⲍ', - 'ⲏ' => 'Ⲏ', - 'ⲑ' => 'Ⲑ', - 'ⲓ' => 'Ⲓ', - 'ⲕ' => 'Ⲕ', - 'ⲗ' => 'Ⲗ', - 'ⲙ' => 'Ⲙ', - 'ⲛ' => 'Ⲛ', - 'ⲝ' => 'Ⲝ', - 'ⲟ' => 'Ⲟ', - 'ⲡ' => 'Ⲡ', - 'ⲣ' => 'Ⲣ', - 'ⲥ' => 'Ⲥ', - 'ⲧ' => 'Ⲧ', - 'ⲩ' => 'Ⲩ', - 'ⲫ' => 'Ⲫ', - 'ⲭ' => 'Ⲭ', - 'ⲯ' => 'Ⲯ', - 'ⲱ' => 'Ⲱ', - 'ⲳ' => 'Ⲳ', - 'ⲵ' => 'Ⲵ', - 'ⲷ' => 'Ⲷ', - 'ⲹ' => 'Ⲹ', - 'ⲻ' => 'Ⲻ', - 'ⲽ' => 'Ⲽ', - 'ⲿ' => 'Ⲿ', - 'ⳁ' => 'Ⳁ', - 'ⳃ' => 'Ⳃ', - 'ⳅ' => 'Ⳅ', - 'ⳇ' => 'Ⳇ', - 'ⳉ' => 'Ⳉ', - 'ⳋ' => 'Ⳋ', - 'ⳍ' => 'Ⳍ', - 'ⳏ' => 'Ⳏ', - 'ⳑ' => 'Ⳑ', - 'ⳓ' => 'Ⳓ', - 'ⳕ' => 'Ⳕ', - 'ⳗ' => 'Ⳗ', - 'ⳙ' => 'Ⳙ', - 'ⳛ' => 'Ⳛ', - 'ⳝ' => 'Ⳝ', - 'ⳟ' => 'Ⳟ', - 'ⳡ' => 'Ⳡ', - 'ⳣ' => 'Ⳣ', - 'ⳬ' => 'Ⳬ', - 'ⳮ' => 'Ⳮ', - 'ⳳ' => 'Ⳳ', - 'ⴀ' => 'Ⴀ', - 'ⴁ' => 'Ⴁ', - 'ⴂ' => 'Ⴂ', - 'ⴃ' => 'Ⴃ', - 'ⴄ' => 'Ⴄ', - 'ⴅ' => 'Ⴅ', - 'ⴆ' => 'Ⴆ', - 'ⴇ' => 'Ⴇ', - 'ⴈ' => 'Ⴈ', - 'ⴉ' => 'Ⴉ', - 'ⴊ' => 'Ⴊ', - 'ⴋ' => 'Ⴋ', - 'ⴌ' => 'Ⴌ', - 'ⴍ' => 'Ⴍ', - 'ⴎ' => 'Ⴎ', - 'ⴏ' => 'Ⴏ', - 'ⴐ' => 'Ⴐ', - 'ⴑ' => 'Ⴑ', - 'ⴒ' => 'Ⴒ', - 'ⴓ' => 'Ⴓ', - 'ⴔ' => 'Ⴔ', - 'ⴕ' => 'Ⴕ', - 'ⴖ' => 'Ⴖ', - 'ⴗ' => 'Ⴗ', - 'ⴘ' => 'Ⴘ', - 'ⴙ' => 'Ⴙ', - 'ⴚ' => 'Ⴚ', - 'ⴛ' => 'Ⴛ', - 'ⴜ' => 'Ⴜ', - 'ⴝ' => 'Ⴝ', - 'ⴞ' => 'Ⴞ', - 'ⴟ' => 'Ⴟ', - 'ⴠ' => 'Ⴠ', - 'ⴡ' => 'Ⴡ', - 'ⴢ' => 'Ⴢ', - 'ⴣ' => 'Ⴣ', - 'ⴤ' => 'Ⴤ', - 'ⴥ' => 'Ⴥ', - 'ⴧ' => 'Ⴧ', - 'ⴭ' => 'Ⴭ', - 'ꙁ' => 'Ꙁ', - 'ꙃ' => 'Ꙃ', - 'ꙅ' => 'Ꙅ', - 'ꙇ' => 'Ꙇ', - 'ꙉ' => 'Ꙉ', - 'ꙋ' => 'Ꙋ', - 'ꙍ' => 'Ꙍ', - 'ꙏ' => 'Ꙏ', - 'ꙑ' => 'Ꙑ', - 'ꙓ' => 'Ꙓ', - 'ꙕ' => 'Ꙕ', - 'ꙗ' => 'Ꙗ', - 'ꙙ' => 'Ꙙ', - 'ꙛ' => 'Ꙛ', - 'ꙝ' => 'Ꙝ', - 'ꙟ' => 'Ꙟ', - 'ꙡ' => 'Ꙡ', - 'ꙣ' => 'Ꙣ', - 'ꙥ' => 'Ꙥ', - 'ꙧ' => 'Ꙧ', - 'ꙩ' => 'Ꙩ', - 'ꙫ' => 'Ꙫ', - 'ꙭ' => 'Ꙭ', - 'ꚁ' => 'Ꚁ', - 'ꚃ' => 'Ꚃ', - 'ꚅ' => 'Ꚅ', - 'ꚇ' => 'Ꚇ', - 'ꚉ' => 'Ꚉ', - 'ꚋ' => 'Ꚋ', - 'ꚍ' => 'Ꚍ', - 'ꚏ' => 'Ꚏ', - 'ꚑ' => 'Ꚑ', - 'ꚓ' => 'Ꚓ', - 'ꚕ' => 'Ꚕ', - 'ꚗ' => 'Ꚗ', - 'ꚙ' => 'Ꚙ', - 'ꚛ' => 'Ꚛ', - 'ꜣ' => 'Ꜣ', - 'ꜥ' => 'Ꜥ', - 'ꜧ' => 'Ꜧ', - 'ꜩ' => 'Ꜩ', - 'ꜫ' => 'Ꜫ', - 'ꜭ' => 'Ꜭ', - 'ꜯ' => 'Ꜯ', - 'ꜳ' => 'Ꜳ', - 'ꜵ' => 'Ꜵ', - 'ꜷ' => 'Ꜷ', - 'ꜹ' => 'Ꜹ', - 'ꜻ' => 'Ꜻ', - 'ꜽ' => 'Ꜽ', - 'ꜿ' => 'Ꜿ', - 'ꝁ' => 'Ꝁ', - 'ꝃ' => 'Ꝃ', - 'ꝅ' => 'Ꝅ', - 'ꝇ' => 'Ꝇ', - 'ꝉ' => 'Ꝉ', - 'ꝋ' => 'Ꝋ', - 'ꝍ' => 'Ꝍ', - 'ꝏ' => 'Ꝏ', - 'ꝑ' => 'Ꝑ', - 'ꝓ' => 'Ꝓ', - 'ꝕ' => 'Ꝕ', - 'ꝗ' => 'Ꝗ', - 'ꝙ' => 'Ꝙ', - 'ꝛ' => 'Ꝛ', - 'ꝝ' => 'Ꝝ', - 'ꝟ' => 'Ꝟ', - 'ꝡ' => 'Ꝡ', - 'ꝣ' => 'Ꝣ', - 'ꝥ' => 'Ꝥ', - 'ꝧ' => 'Ꝧ', - 'ꝩ' => 'Ꝩ', - 'ꝫ' => 'Ꝫ', - 'ꝭ' => 'Ꝭ', - 'ꝯ' => 'Ꝯ', - 'ꝺ' => 'Ꝺ', - 'ꝼ' => 'Ꝼ', - 'ꝿ' => 'Ꝿ', - 'ꞁ' => 'Ꞁ', - 'ꞃ' => 'Ꞃ', - 'ꞅ' => 'Ꞅ', - 'ꞇ' => 'Ꞇ', - 'ꞌ' => 'Ꞌ', - 'ꞑ' => 'Ꞑ', - 'ꞓ' => 'Ꞓ', - 'ꞔ' => 'Ꞔ', - 'ꞗ' => 'Ꞗ', - 'ꞙ' => 'Ꞙ', - 'ꞛ' => 'Ꞛ', - 'ꞝ' => 'Ꞝ', - 'ꞟ' => 'Ꞟ', - 'ꞡ' => 'Ꞡ', - 'ꞣ' => 'Ꞣ', - 'ꞥ' => 'Ꞥ', - 'ꞧ' => 'Ꞧ', - 'ꞩ' => 'Ꞩ', - 'ꞵ' => 'Ꞵ', - 'ꞷ' => 'Ꞷ', - 'ꞹ' => 'Ꞹ', - 'ꞻ' => 'Ꞻ', - 'ꞽ' => 'Ꞽ', - 'ꞿ' => 'Ꞿ', - 'ꟃ' => 'Ꟃ', - 'ꟈ' => 'Ꟈ', - 'ꟊ' => 'Ꟊ', - 'ꟶ' => 'Ꟶ', - 'ꭓ' => 'Ꭓ', - 'ꭰ' => 'Ꭰ', - 'ꭱ' => 'Ꭱ', - 'ꭲ' => 'Ꭲ', - 'ꭳ' => 'Ꭳ', - 'ꭴ' => 'Ꭴ', - 'ꭵ' => 'Ꭵ', - 'ꭶ' => 'Ꭶ', - 'ꭷ' => 'Ꭷ', - 'ꭸ' => 'Ꭸ', - 'ꭹ' => 'Ꭹ', - 'ꭺ' => 'Ꭺ', - 'ꭻ' => 'Ꭻ', - 'ꭼ' => 'Ꭼ', - 'ꭽ' => 'Ꭽ', - 'ꭾ' => 'Ꭾ', - 'ꭿ' => 'Ꭿ', - 'ꮀ' => 'Ꮀ', - 'ꮁ' => 'Ꮁ', - 'ꮂ' => 'Ꮂ', - 'ꮃ' => 'Ꮃ', - 'ꮄ' => 'Ꮄ', - 'ꮅ' => 'Ꮅ', - 'ꮆ' => 'Ꮆ', - 'ꮇ' => 'Ꮇ', - 'ꮈ' => 'Ꮈ', - 'ꮉ' => 'Ꮉ', - 'ꮊ' => 'Ꮊ', - 'ꮋ' => 'Ꮋ', - 'ꮌ' => 'Ꮌ', - 'ꮍ' => 'Ꮍ', - 'ꮎ' => 'Ꮎ', - 'ꮏ' => 'Ꮏ', - 'ꮐ' => 'Ꮐ', - 'ꮑ' => 'Ꮑ', - 'ꮒ' => 'Ꮒ', - 'ꮓ' => 'Ꮓ', - 'ꮔ' => 'Ꮔ', - 'ꮕ' => 'Ꮕ', - 'ꮖ' => 'Ꮖ', - 'ꮗ' => 'Ꮗ', - 'ꮘ' => 'Ꮘ', - 'ꮙ' => 'Ꮙ', - 'ꮚ' => 'Ꮚ', - 'ꮛ' => 'Ꮛ', - 'ꮜ' => 'Ꮜ', - 'ꮝ' => 'Ꮝ', - 'ꮞ' => 'Ꮞ', - 'ꮟ' => 'Ꮟ', - 'ꮠ' => 'Ꮠ', - 'ꮡ' => 'Ꮡ', - 'ꮢ' => 'Ꮢ', - 'ꮣ' => 'Ꮣ', - 'ꮤ' => 'Ꮤ', - 'ꮥ' => 'Ꮥ', - 'ꮦ' => 'Ꮦ', - 'ꮧ' => 'Ꮧ', - 'ꮨ' => 'Ꮨ', - 'ꮩ' => 'Ꮩ', - 'ꮪ' => 'Ꮪ', - 'ꮫ' => 'Ꮫ', - 'ꮬ' => 'Ꮬ', - 'ꮭ' => 'Ꮭ', - 'ꮮ' => 'Ꮮ', - 'ꮯ' => 'Ꮯ', - 'ꮰ' => 'Ꮰ', - 'ꮱ' => 'Ꮱ', - 'ꮲ' => 'Ꮲ', - 'ꮳ' => 'Ꮳ', - 'ꮴ' => 'Ꮴ', - 'ꮵ' => 'Ꮵ', - 'ꮶ' => 'Ꮶ', - 'ꮷ' => 'Ꮷ', - 'ꮸ' => 'Ꮸ', - 'ꮹ' => 'Ꮹ', - 'ꮺ' => 'Ꮺ', - 'ꮻ' => 'Ꮻ', - 'ꮼ' => 'Ꮼ', - 'ꮽ' => 'Ꮽ', - 'ꮾ' => 'Ꮾ', - 'ꮿ' => 'Ꮿ', - 'a' => 'A', - 'b' => 'B', - 'c' => 'C', - 'd' => 'D', - 'e' => 'E', - 'f' => 'F', - 'g' => 'G', - 'h' => 'H', - 'i' => 'I', - 'j' => 'J', - 'k' => 'K', - 'l' => 'L', - 'm' => 'M', - 'n' => 'N', - 'o' => 'O', - 'p' => 'P', - 'q' => 'Q', - 'r' => 'R', - 's' => 'S', - 't' => 'T', - 'u' => 'U', - 'v' => 'V', - 'w' => 'W', - 'x' => 'X', - 'y' => 'Y', - 'z' => 'Z', - '𐐨' => '𐐀', - '𐐩' => '𐐁', - '𐐪' => '𐐂', - '𐐫' => '𐐃', - '𐐬' => '𐐄', - '𐐭' => '𐐅', - '𐐮' => '𐐆', - '𐐯' => '𐐇', - '𐐰' => '𐐈', - '𐐱' => '𐐉', - '𐐲' => '𐐊', - '𐐳' => '𐐋', - '𐐴' => '𐐌', - '𐐵' => '𐐍', - '𐐶' => '𐐎', - '𐐷' => '𐐏', - '𐐸' => '𐐐', - '𐐹' => '𐐑', - '𐐺' => '𐐒', - '𐐻' => '𐐓', - '𐐼' => '𐐔', - '𐐽' => '𐐕', - '𐐾' => '𐐖', - '𐐿' => '𐐗', - '𐑀' => '𐐘', - '𐑁' => '𐐙', - '𐑂' => '𐐚', - '𐑃' => '𐐛', - '𐑄' => '𐐜', - '𐑅' => '𐐝', - '𐑆' => '𐐞', - '𐑇' => '𐐟', - '𐑈' => '𐐠', - '𐑉' => '𐐡', - '𐑊' => '𐐢', - '𐑋' => '𐐣', - '𐑌' => '𐐤', - '𐑍' => '𐐥', - '𐑎' => '𐐦', - '𐑏' => '𐐧', - '𐓘' => '𐒰', - '𐓙' => '𐒱', - '𐓚' => '𐒲', - '𐓛' => '𐒳', - '𐓜' => '𐒴', - '𐓝' => '𐒵', - '𐓞' => '𐒶', - '𐓟' => '𐒷', - '𐓠' => '𐒸', - '𐓡' => '𐒹', - '𐓢' => '𐒺', - '𐓣' => '𐒻', - '𐓤' => '𐒼', - '𐓥' => '𐒽', - '𐓦' => '𐒾', - '𐓧' => '𐒿', - '𐓨' => '𐓀', - '𐓩' => '𐓁', - '𐓪' => '𐓂', - '𐓫' => '𐓃', - '𐓬' => '𐓄', - '𐓭' => '𐓅', - '𐓮' => '𐓆', - '𐓯' => '𐓇', - '𐓰' => '𐓈', - '𐓱' => '𐓉', - '𐓲' => '𐓊', - '𐓳' => '𐓋', - '𐓴' => '𐓌', - '𐓵' => '𐓍', - '𐓶' => '𐓎', - '𐓷' => '𐓏', - '𐓸' => '𐓐', - '𐓹' => '𐓑', - '𐓺' => '𐓒', - '𐓻' => '𐓓', - '𐳀' => '𐲀', - '𐳁' => '𐲁', - '𐳂' => '𐲂', - '𐳃' => '𐲃', - '𐳄' => '𐲄', - '𐳅' => '𐲅', - '𐳆' => '𐲆', - '𐳇' => '𐲇', - '𐳈' => '𐲈', - '𐳉' => '𐲉', - '𐳊' => '𐲊', - '𐳋' => '𐲋', - '𐳌' => '𐲌', - '𐳍' => '𐲍', - '𐳎' => '𐲎', - '𐳏' => '𐲏', - '𐳐' => '𐲐', - '𐳑' => '𐲑', - '𐳒' => '𐲒', - '𐳓' => '𐲓', - '𐳔' => '𐲔', - '𐳕' => '𐲕', - '𐳖' => '𐲖', - '𐳗' => '𐲗', - '𐳘' => '𐲘', - '𐳙' => '𐲙', - '𐳚' => '𐲚', - '𐳛' => '𐲛', - '𐳜' => '𐲜', - '𐳝' => '𐲝', - '𐳞' => '𐲞', - '𐳟' => '𐲟', - '𐳠' => '𐲠', - '𐳡' => '𐲡', - '𐳢' => '𐲢', - '𐳣' => '𐲣', - '𐳤' => '𐲤', - '𐳥' => '𐲥', - '𐳦' => '𐲦', - '𐳧' => '𐲧', - '𐳨' => '𐲨', - '𐳩' => '𐲩', - '𐳪' => '𐲪', - '𐳫' => '𐲫', - '𐳬' => '𐲬', - '𐳭' => '𐲭', - '𐳮' => '𐲮', - '𐳯' => '𐲯', - '𐳰' => '𐲰', - '𐳱' => '𐲱', - '𐳲' => '𐲲', - '𑣀' => '𑢠', - '𑣁' => '𑢡', - '𑣂' => '𑢢', - '𑣃' => '𑢣', - '𑣄' => '𑢤', - '𑣅' => '𑢥', - '𑣆' => '𑢦', - '𑣇' => '𑢧', - '𑣈' => '𑢨', - '𑣉' => '𑢩', - '𑣊' => '𑢪', - '𑣋' => '𑢫', - '𑣌' => '𑢬', - '𑣍' => '𑢭', - '𑣎' => '𑢮', - '𑣏' => '𑢯', - '𑣐' => '𑢰', - '𑣑' => '𑢱', - '𑣒' => '𑢲', - '𑣓' => '𑢳', - '𑣔' => '𑢴', - '𑣕' => '𑢵', - '𑣖' => '𑢶', - '𑣗' => '𑢷', - '𑣘' => '𑢸', - '𑣙' => '𑢹', - '𑣚' => '𑢺', - '𑣛' => '𑢻', - '𑣜' => '𑢼', - '𑣝' => '𑢽', - '𑣞' => '𑢾', - '𑣟' => '𑢿', - '𖹠' => '𖹀', - '𖹡' => '𖹁', - '𖹢' => '𖹂', - '𖹣' => '𖹃', - '𖹤' => '𖹄', - '𖹥' => '𖹅', - '𖹦' => '𖹆', - '𖹧' => '𖹇', - '𖹨' => '𖹈', - '𖹩' => '𖹉', - '𖹪' => '𖹊', - '𖹫' => '𖹋', - '𖹬' => '𖹌', - '𖹭' => '𖹍', - '𖹮' => '𖹎', - '𖹯' => '𖹏', - '𖹰' => '𖹐', - '𖹱' => '𖹑', - '𖹲' => '𖹒', - '𖹳' => '𖹓', - '𖹴' => '𖹔', - '𖹵' => '𖹕', - '𖹶' => '𖹖', - '𖹷' => '𖹗', - '𖹸' => '𖹘', - '𖹹' => '𖹙', - '𖹺' => '𖹚', - '𖹻' => '𖹛', - '𖹼' => '𖹜', - '𖹽' => '𖹝', - '𖹾' => '𖹞', - '𖹿' => '𖹟', - '𞤢' => '𞤀', - '𞤣' => '𞤁', - '𞤤' => '𞤂', - '𞤥' => '𞤃', - '𞤦' => '𞤄', - '𞤧' => '𞤅', - '𞤨' => '𞤆', - '𞤩' => '𞤇', - '𞤪' => '𞤈', - '𞤫' => '𞤉', - '𞤬' => '𞤊', - '𞤭' => '𞤋', - '𞤮' => '𞤌', - '𞤯' => '𞤍', - '𞤰' => '𞤎', - '𞤱' => '𞤏', - '𞤲' => '𞤐', - '𞤳' => '𞤑', - '𞤴' => '𞤒', - '𞤵' => '𞤓', - '𞤶' => '𞤔', - '𞤷' => '𞤕', - '𞤸' => '𞤖', - '𞤹' => '𞤗', - '𞤺' => '𞤘', - '𞤻' => '𞤙', - '𞤼' => '𞤚', - '𞤽' => '𞤛', - '𞤾' => '𞤜', - '𞤿' => '𞤝', - '𞥀' => '𞤞', - '𞥁' => '𞤟', - '𞥂' => '𞤠', - '𞥃' => '𞤡', - 'ß' => 'SS', - 'ff' => 'FF', - 'fi' => 'FI', - 'fl' => 'FL', - 'ffi' => 'FFI', - 'ffl' => 'FFL', - 'ſt' => 'ST', - 'st' => 'ST', - 'և' => 'ԵՒ', - 'ﬓ' => 'ՄՆ', - 'ﬔ' => 'ՄԵ', - 'ﬕ' => 'ՄԻ', - 'ﬖ' => 'ՎՆ', - 'ﬗ' => 'ՄԽ', - 'ʼn' => 'ʼN', - 'ΐ' => 'Ϊ́', - 'ΰ' => 'Ϋ́', - 'ǰ' => 'J̌', - 'ẖ' => 'H̱', - 'ẗ' => 'T̈', - 'ẘ' => 'W̊', - 'ẙ' => 'Y̊', - 'ẚ' => 'Aʾ', - 'ὐ' => 'Υ̓', - 'ὒ' => 'Υ̓̀', - 'ὔ' => 'Υ̓́', - 'ὖ' => 'Υ̓͂', - 'ᾶ' => 'Α͂', - 'ῆ' => 'Η͂', - 'ῒ' => 'Ϊ̀', - 'ΐ' => 'Ϊ́', - 'ῖ' => 'Ι͂', - 'ῗ' => 'Ϊ͂', - 'ῢ' => 'Ϋ̀', - 'ΰ' => 'Ϋ́', - 'ῤ' => 'Ρ̓', - 'ῦ' => 'Υ͂', - 'ῧ' => 'Ϋ͂', - 'ῶ' => 'Ω͂', - 'ᾈ' => 'ἈΙ', - 'ᾉ' => 'ἉΙ', - 'ᾊ' => 'ἊΙ', - 'ᾋ' => 'ἋΙ', - 'ᾌ' => 'ἌΙ', - 'ᾍ' => 'ἍΙ', - 'ᾎ' => 'ἎΙ', - 'ᾏ' => 'ἏΙ', - 'ᾘ' => 'ἨΙ', - 'ᾙ' => 'ἩΙ', - 'ᾚ' => 'ἪΙ', - 'ᾛ' => 'ἫΙ', - 'ᾜ' => 'ἬΙ', - 'ᾝ' => 'ἭΙ', - 'ᾞ' => 'ἮΙ', - 'ᾟ' => 'ἯΙ', - 'ᾨ' => 'ὨΙ', - 'ᾩ' => 'ὩΙ', - 'ᾪ' => 'ὪΙ', - 'ᾫ' => 'ὫΙ', - 'ᾬ' => 'ὬΙ', - 'ᾭ' => 'ὭΙ', - 'ᾮ' => 'ὮΙ', - 'ᾯ' => 'ὯΙ', - 'ᾼ' => 'ΑΙ', - 'ῌ' => 'ΗΙ', - 'ῼ' => 'ΩΙ', - 'ᾲ' => 'ᾺΙ', - 'ᾴ' => 'ΆΙ', - 'ῂ' => 'ῊΙ', - 'ῄ' => 'ΉΙ', - 'ῲ' => 'ῺΙ', - 'ῴ' => 'ΏΙ', - 'ᾷ' => 'Α͂Ι', - 'ῇ' => 'Η͂Ι', - 'ῷ' => 'Ω͂Ι', -); diff --git a/vendor/symfony/polyfill-mbstring/bootstrap.php b/vendor/symfony/polyfill-mbstring/bootstrap.php deleted file mode 100644 index ff51ae0..0000000 --- a/vendor/symfony/polyfill-mbstring/bootstrap.php +++ /dev/null @@ -1,172 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\Polyfill\Mbstring as p; - -if (\PHP_VERSION_ID >= 80000) { - return require __DIR__.'/bootstrap80.php'; -} - -if (!function_exists('mb_convert_encoding')) { - function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } -} -if (!function_exists('mb_decode_mimeheader')) { - function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } -} -if (!function_exists('mb_encode_mimeheader')) { - function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } -} -if (!function_exists('mb_decode_numericentity')) { - function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } -} -if (!function_exists('mb_encode_numericentity')) { - function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } -} -if (!function_exists('mb_convert_case')) { - function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } -} -if (!function_exists('mb_internal_encoding')) { - function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } -} -if (!function_exists('mb_language')) { - function mb_language($language = null) { return p\Mbstring::mb_language($language); } -} -if (!function_exists('mb_list_encodings')) { - function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } -} -if (!function_exists('mb_encoding_aliases')) { - function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } -} -if (!function_exists('mb_check_encoding')) { - function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } -} -if (!function_exists('mb_detect_encoding')) { - function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } -} -if (!function_exists('mb_detect_order')) { - function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } -} -if (!function_exists('mb_parse_str')) { - function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; } -} -if (!function_exists('mb_strlen')) { - function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } -} -if (!function_exists('mb_strpos')) { - function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } -} -if (!function_exists('mb_strtolower')) { - function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } -} -if (!function_exists('mb_strtoupper')) { - function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } -} -if (!function_exists('mb_substitute_character')) { - function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } -} -if (!function_exists('mb_substr')) { - function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } -} -if (!function_exists('mb_stripos')) { - function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } -} -if (!function_exists('mb_stristr')) { - function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } -} -if (!function_exists('mb_strrchr')) { - function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } -} -if (!function_exists('mb_strrichr')) { - function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } -} -if (!function_exists('mb_strripos')) { - function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } -} -if (!function_exists('mb_strrpos')) { - function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } -} -if (!function_exists('mb_strstr')) { - function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } -} -if (!function_exists('mb_get_info')) { - function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } -} -if (!function_exists('mb_http_output')) { - function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } -} -if (!function_exists('mb_strwidth')) { - function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } -} -if (!function_exists('mb_substr_count')) { - function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } -} -if (!function_exists('mb_output_handler')) { - function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } -} -if (!function_exists('mb_http_input')) { - function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); } -} - -if (!function_exists('mb_convert_variables')) { - function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } -} - -if (!function_exists('mb_ord')) { - function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } -} -if (!function_exists('mb_chr')) { - function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } -} -if (!function_exists('mb_scrub')) { - function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } -} -if (!function_exists('mb_str_split')) { - function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } -} - -if (!function_exists('mb_str_pad')) { - function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } -} - -if (!function_exists('mb_ucfirst')) { - function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } -} - -if (!function_exists('mb_lcfirst')) { - function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } -} - -if (!function_exists('mb_trim')) { - function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } -} - -if (!function_exists('mb_ltrim')) { - function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } -} - -if (!function_exists('mb_rtrim')) { - function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } -} - - -if (extension_loaded('mbstring')) { - return; -} - -if (!defined('MB_CASE_UPPER')) { - define('MB_CASE_UPPER', 0); -} -if (!defined('MB_CASE_LOWER')) { - define('MB_CASE_LOWER', 1); -} -if (!defined('MB_CASE_TITLE')) { - define('MB_CASE_TITLE', 2); -} diff --git a/vendor/symfony/polyfill-mbstring/bootstrap80.php b/vendor/symfony/polyfill-mbstring/bootstrap80.php deleted file mode 100644 index 5236e6d..0000000 --- a/vendor/symfony/polyfill-mbstring/bootstrap80.php +++ /dev/null @@ -1,167 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\Polyfill\Mbstring as p; - -if (!function_exists('mb_convert_encoding')) { - function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); } -} -if (!function_exists('mb_decode_mimeheader')) { - function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); } -} -if (!function_exists('mb_encode_mimeheader')) { - function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); } -} -if (!function_exists('mb_decode_numericentity')) { - function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); } -} -if (!function_exists('mb_encode_numericentity')) { - function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); } -} -if (!function_exists('mb_convert_case')) { - function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); } -} -if (!function_exists('mb_internal_encoding')) { - function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); } -} -if (!function_exists('mb_language')) { - function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); } -} -if (!function_exists('mb_list_encodings')) { - function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); } -} -if (!function_exists('mb_encoding_aliases')) { - function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); } -} -if (!function_exists('mb_check_encoding')) { - function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); } -} -if (!function_exists('mb_detect_encoding')) { - function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); } -} -if (!function_exists('mb_detect_order')) { - function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); } -} -if (!function_exists('mb_parse_str')) { - function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; } -} -if (!function_exists('mb_strlen')) { - function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); } -} -if (!function_exists('mb_strpos')) { - function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } -} -if (!function_exists('mb_strtolower')) { - function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); } -} -if (!function_exists('mb_strtoupper')) { - function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); } -} -if (!function_exists('mb_substitute_character')) { - function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); } -} -if (!function_exists('mb_substr')) { - function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); } -} -if (!function_exists('mb_stripos')) { - function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } -} -if (!function_exists('mb_stristr')) { - function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } -} -if (!function_exists('mb_strrchr')) { - function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } -} -if (!function_exists('mb_strrichr')) { - function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } -} -if (!function_exists('mb_strripos')) { - function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } -} -if (!function_exists('mb_strrpos')) { - function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } -} -if (!function_exists('mb_strstr')) { - function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } -} -if (!function_exists('mb_get_info')) { - function mb_get_info(?string $type = 'all'): array|string|int|false|null { return p\Mbstring::mb_get_info((string) $type); } -} -if (!function_exists('mb_http_output')) { - function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); } -} -if (!function_exists('mb_strwidth')) { - function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); } -} -if (!function_exists('mb_substr_count')) { - function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); } -} -if (!function_exists('mb_output_handler')) { - function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); } -} -if (!function_exists('mb_http_input')) { - function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); } -} - -if (!function_exists('mb_convert_variables')) { - function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); } -} - -if (!function_exists('mb_ord')) { - function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); } -} -if (!function_exists('mb_chr')) { - function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); } -} -if (!function_exists('mb_scrub')) { - function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); } -} -if (!function_exists('mb_str_split')) { - function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); } -} - -if (!function_exists('mb_str_pad')) { - function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } -} - -if (!function_exists('mb_ucfirst')) { - function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } -} - -if (!function_exists('mb_lcfirst')) { - function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } -} - -if (!function_exists('mb_trim')) { - function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } -} - -if (!function_exists('mb_ltrim')) { - function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } -} - -if (!function_exists('mb_rtrim')) { - function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } -} - -if (extension_loaded('mbstring')) { - return; -} - -if (!defined('MB_CASE_UPPER')) { - define('MB_CASE_UPPER', 0); -} -if (!defined('MB_CASE_LOWER')) { - define('MB_CASE_LOWER', 1); -} -if (!defined('MB_CASE_TITLE')) { - define('MB_CASE_TITLE', 2); -} diff --git a/vendor/symfony/polyfill-mbstring/composer.json b/vendor/symfony/polyfill-mbstring/composer.json deleted file mode 100644 index daa07f8..0000000 --- a/vendor/symfony/polyfill-mbstring/composer.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "symfony/polyfill-mbstring", - "type": "library", - "description": "Symfony polyfill for the Mbstring extension", - "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], - "homepage": "https://symfony.com", - "license": "MIT", - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "require": { - "php": ">=7.2", - "ext-iconv": "*" - }, - "provide": { - "ext-mbstring": "*" - }, - "autoload": { - "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, - "files": [ "bootstrap.php" ] - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "minimum-stability": "dev", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - } -} diff --git a/vendor/symfony/process/CHANGELOG.md b/vendor/symfony/process/CHANGELOG.md deleted file mode 100644 index d730856..0000000 --- a/vendor/symfony/process/CHANGELOG.md +++ /dev/null @@ -1,134 +0,0 @@ -CHANGELOG -========= - - -7.3 ---- - - * Add `RunProcessMessage::fromShellCommandline()` to instantiate a Process via the fromShellCommandline method - -7.1 ---- - - * Add `Process::setIgnoredSignals()` to disable signal propagation to the child process - -6.4 ---- - - * Add `PhpSubprocess` to handle PHP subprocesses that take over the - configuration from their parent - * Add `RunProcessMessage` and `RunProcessMessageHandler` - -5.2.0 ------ - - * added `Process::setOptions()` to set `Process` specific options - * added option `create_new_console` to allow a subprocess to continue - to run after the main script exited, both on Linux and on Windows - -5.1.0 ------ - - * added `Process::getStartTime()` to retrieve the start time of the process as float - -5.0.0 ------ - - * removed `Process::inheritEnvironmentVariables()` - * removed `PhpProcess::setPhpBinary()` - * `Process` must be instantiated with a command array, use `Process::fromShellCommandline()` when the command should be parsed by the shell - * removed `Process::setCommandLine()` - -4.4.0 ------ - - * deprecated `Process::inheritEnvironmentVariables()`: env variables are always inherited. - * added `Process::getLastOutputTime()` method - -4.2.0 ------ - - * added the `Process::fromShellCommandline()` to run commands in a shell wrapper - * deprecated passing a command as string when creating a `Process` instance - * deprecated the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods - * added the `Process::waitUntil()` method to wait for the process only for a - specific output, then continue the normal execution of your application - -4.1.0 ------ - - * added the `Process::isTtySupported()` method that allows to check for TTY support - * made `PhpExecutableFinder` look for the `PHP_BINARY` env var when searching the php binary - * added the `ProcessSignaledException` class to properly catch signaled process errors - -4.0.0 ------ - - * environment variables will always be inherited - * added a second `array $env = []` argument to the `start()`, `run()`, - `mustRun()`, and `restart()` methods of the `Process` class - * added a second `array $env = []` argument to the `start()` method of the - `PhpProcess` class - * the `ProcessUtils::escapeArgument()` method has been removed - * the `areEnvironmentVariablesInherited()`, `getOptions()`, and `setOptions()` - methods of the `Process` class have been removed - * support for passing `proc_open()` options has been removed - * removed the `ProcessBuilder` class, use the `Process` class instead - * removed the `getEnhanceWindowsCompatibility()` and `setEnhanceWindowsCompatibility()` methods of the `Process` class - * passing a not existing working directory to the constructor of the `Symfony\Component\Process\Process` class is not - supported anymore - -3.4.0 ------ - - * deprecated the ProcessBuilder class - * deprecated calling `Process::start()` without setting a valid working directory beforehand (via `setWorkingDirectory()` or constructor) - -3.3.0 ------ - - * added command line arrays in the `Process` class - * added `$env` argument to `Process::start()`, `run()`, `mustRun()` and `restart()` methods - * deprecated the `ProcessUtils::escapeArgument()` method - * deprecated not inheriting environment variables - * deprecated configuring `proc_open()` options - * deprecated configuring enhanced Windows compatibility - * deprecated configuring enhanced sigchild compatibility - -2.5.0 ------ - - * added support for PTY mode - * added the convenience method "mustRun" - * deprecation: Process::setStdin() is deprecated in favor of Process::setInput() - * deprecation: Process::getStdin() is deprecated in favor of Process::getInput() - * deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types - -2.4.0 ------ - - * added the ability to define an idle timeout - -2.3.0 ------ - - * added ProcessUtils::escapeArgument() to fix the bug in escapeshellarg() function on Windows - * added Process::signal() - * added Process::getPid() - * added support for a TTY mode - -2.2.0 ------ - - * added ProcessBuilder::setArguments() to reset the arguments on a builder - * added a way to retrieve the standard and error output incrementally - * added Process:restart() - -2.1.0 ------ - - * added support for non-blocking processes (start(), wait(), isRunning(), stop()) - * enhanced Windows compatibility - * added Process::getExitCodeText() that returns a string representation for - the exit code returned by the process - * added ProcessBuilder diff --git a/vendor/symfony/process/Exception/ExceptionInterface.php b/vendor/symfony/process/Exception/ExceptionInterface.php deleted file mode 100644 index bd4a604..0000000 --- a/vendor/symfony/process/Exception/ExceptionInterface.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -/** - * Marker Interface for the Process Component. - * - * @author Johannes M. Schmitt - */ -interface ExceptionInterface extends \Throwable -{ -} diff --git a/vendor/symfony/process/Exception/InvalidArgumentException.php b/vendor/symfony/process/Exception/InvalidArgumentException.php deleted file mode 100644 index 926ee21..0000000 --- a/vendor/symfony/process/Exception/InvalidArgumentException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -/** - * InvalidArgumentException for the Process Component. - * - * @author Romain Neutron - */ -class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface -{ -} diff --git a/vendor/symfony/process/Exception/LogicException.php b/vendor/symfony/process/Exception/LogicException.php deleted file mode 100644 index be3d490..0000000 --- a/vendor/symfony/process/Exception/LogicException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -/** - * LogicException for the Process Component. - * - * @author Romain Neutron - */ -class LogicException extends \LogicException implements ExceptionInterface -{ -} diff --git a/vendor/symfony/process/Exception/ProcessFailedException.php b/vendor/symfony/process/Exception/ProcessFailedException.php deleted file mode 100644 index de8a9e9..0000000 --- a/vendor/symfony/process/Exception/ProcessFailedException.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -use Symfony\Component\Process\Process; - -/** - * Exception for failed processes. - * - * @author Johannes M. Schmitt - */ -class ProcessFailedException extends RuntimeException -{ - public function __construct( - private Process $process, - ) { - if ($process->isSuccessful()) { - throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); - } - - $error = \sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", - $process->getCommandLine(), - $process->getExitCode(), - $process->getExitCodeText(), - $process->getWorkingDirectory() - ); - - if (!$process->isOutputDisabled()) { - $error .= \sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", - $process->getOutput(), - $process->getErrorOutput() - ); - } - - parent::__construct($error); - - $this->process = $process; - } - - public function getProcess(): Process - { - return $this->process; - } -} diff --git a/vendor/symfony/process/Exception/ProcessSignaledException.php b/vendor/symfony/process/Exception/ProcessSignaledException.php deleted file mode 100644 index 3fd13e5..0000000 --- a/vendor/symfony/process/Exception/ProcessSignaledException.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -use Symfony\Component\Process\Process; - -/** - * Exception that is thrown when a process has been signaled. - * - * @author Sullivan Senechal - */ -final class ProcessSignaledException extends RuntimeException -{ - public function __construct( - private Process $process, - ) { - parent::__construct(\sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); - } - - public function getProcess(): Process - { - return $this->process; - } - - public function getSignal(): int - { - return $this->getProcess()->getTermSignal(); - } -} diff --git a/vendor/symfony/process/Exception/ProcessStartFailedException.php b/vendor/symfony/process/Exception/ProcessStartFailedException.php deleted file mode 100644 index 3725472..0000000 --- a/vendor/symfony/process/Exception/ProcessStartFailedException.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -use Symfony\Component\Process\Process; - -/** - * Exception for processes failed during startup. - */ -class ProcessStartFailedException extends ProcessFailedException -{ - public function __construct( - private Process $process, - ?string $message, - ) { - if ($process->isStarted()) { - throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.'); - } - - $error = \sprintf('The command "%s" failed.'."\n\nWorking directory: %s\n\nError: %s", - $process->getCommandLine(), - $process->getWorkingDirectory(), - $message ?? 'unknown' - ); - - // Skip parent constructor - RuntimeException::__construct($error); - } - - public function getProcess(): Process - { - return $this->process; - } -} diff --git a/vendor/symfony/process/Exception/ProcessTimedOutException.php b/vendor/symfony/process/Exception/ProcessTimedOutException.php deleted file mode 100644 index d3fe493..0000000 --- a/vendor/symfony/process/Exception/ProcessTimedOutException.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -use Symfony\Component\Process\Process; - -/** - * Exception that is thrown when a process times out. - * - * @author Johannes M. Schmitt - */ -class ProcessTimedOutException extends RuntimeException -{ - public const TYPE_GENERAL = 1; - public const TYPE_IDLE = 2; - - public function __construct( - private Process $process, - private int $timeoutType, - ) { - parent::__construct(\sprintf( - 'The process "%s" exceeded the timeout of %s seconds.', - $process->getCommandLine(), - $this->getExceededTimeout() - )); - } - - public function getProcess(): Process - { - return $this->process; - } - - public function isGeneralTimeout(): bool - { - return self::TYPE_GENERAL === $this->timeoutType; - } - - public function isIdleTimeout(): bool - { - return self::TYPE_IDLE === $this->timeoutType; - } - - public function getExceededTimeout(): ?float - { - return match ($this->timeoutType) { - self::TYPE_GENERAL => $this->process->getTimeout(), - self::TYPE_IDLE => $this->process->getIdleTimeout(), - default => throw new \LogicException(\sprintf('Unknown timeout type "%d".', $this->timeoutType)), - }; - } -} diff --git a/vendor/symfony/process/Exception/RunProcessFailedException.php b/vendor/symfony/process/Exception/RunProcessFailedException.php deleted file mode 100644 index e7219d3..0000000 --- a/vendor/symfony/process/Exception/RunProcessFailedException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -use Symfony\Component\Process\Messenger\RunProcessContext; - -/** - * @author Kevin Bond - */ -final class RunProcessFailedException extends RuntimeException -{ - public function __construct(ProcessFailedException $exception, public readonly RunProcessContext $context) - { - parent::__construct($exception->getMessage(), $exception->getCode()); - } -} diff --git a/vendor/symfony/process/Exception/RuntimeException.php b/vendor/symfony/process/Exception/RuntimeException.php deleted file mode 100644 index adead25..0000000 --- a/vendor/symfony/process/Exception/RuntimeException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Exception; - -/** - * RuntimeException for the Process Component. - * - * @author Johannes M. Schmitt - */ -class RuntimeException extends \RuntimeException implements ExceptionInterface -{ -} diff --git a/vendor/symfony/process/ExecutableFinder.php b/vendor/symfony/process/ExecutableFinder.php deleted file mode 100644 index 204558b..0000000 --- a/vendor/symfony/process/ExecutableFinder.php +++ /dev/null @@ -1,103 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process; - -/** - * Generic executable finder. - * - * @author Fabien Potencier - * @author Johannes M. Schmitt - */ -class ExecutableFinder -{ - private const CMD_BUILTINS = [ - 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', - 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', - 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', - 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', - 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', - ]; - - private array $suffixes = []; - - /** - * Replaces default suffixes of executable. - */ - public function setSuffixes(array $suffixes): void - { - $this->suffixes = $suffixes; - } - - /** - * Adds new possible suffix to check for executable, including the dot (.). - * - * $finder = new ExecutableFinder(); - * $finder->addSuffix('.foo'); - */ - public function addSuffix(string $suffix): void - { - $this->suffixes[] = $suffix; - } - - /** - * Finds an executable by name. - * - * @param string $name The executable name (without the extension) - * @param string|null $default The default to return if no executable is found - * @param array $extraDirs Additional dirs to check into - */ - public function find(string $name, ?string $default = null, array $extraDirs = []): ?string - { - // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes - if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) { - return $name; - } - - $dirs = array_merge( - explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path') ?: ''), - $extraDirs - ); - - $suffixes = $this->suffixes; - if ('\\' === \DIRECTORY_SEPARATOR) { - $pathExt = getenv('PATHEXT') ?: ''; - $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); - } - $suffixes = '' !== pathinfo($name, \PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); - foreach ($suffixes as $suffix) { - foreach ($dirs as $dir) { - if ('' === $dir) { - $dir = '.'; - } - if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { - return $file; - } - - if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) { - return $dir; - } - } - } - - if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { - return $default; - } - - $execResult = exec('command -v -- '.escapeshellarg($name)); - - if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { - return $executablePath; - } - - return $default; - } -} diff --git a/vendor/symfony/process/InputStream.php b/vendor/symfony/process/InputStream.php deleted file mode 100644 index 586e742..0000000 --- a/vendor/symfony/process/InputStream.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process; - -use Symfony\Component\Process\Exception\RuntimeException; - -/** - * Provides a way to continuously write to the input of a Process until the InputStream is closed. - * - * @author Nicolas Grekas - * - * @implements \IteratorAggregate - */ -class InputStream implements \IteratorAggregate -{ - private ?\Closure $onEmpty = null; - private array $input = []; - private bool $open = true; - - /** - * Sets a callback that is called when the write buffer becomes empty. - */ - public function onEmpty(?callable $onEmpty = null): void - { - $this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null; - } - - /** - * Appends an input to the write buffer. - * - * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, - * stream resource or \Traversable - */ - public function write(mixed $input): void - { - if (null === $input) { - return; - } - if ($this->isClosed()) { - throw new RuntimeException(\sprintf('"%s" is closed.', static::class)); - } - $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); - } - - /** - * Closes the write buffer. - */ - public function close(): void - { - $this->open = false; - } - - /** - * Tells whether the write buffer is closed or not. - */ - public function isClosed(): bool - { - return !$this->open; - } - - public function getIterator(): \Traversable - { - $this->open = true; - - while ($this->open || $this->input) { - if (!$this->input) { - yield ''; - continue; - } - $current = array_shift($this->input); - - if ($current instanceof \Iterator) { - yield from $current; - } else { - yield $current; - } - if (!$this->input && $this->open && null !== $onEmpty = $this->onEmpty) { - $this->write($onEmpty($this)); - } - } - } -} diff --git a/vendor/symfony/process/LICENSE b/vendor/symfony/process/LICENSE deleted file mode 100644 index 0138f8f..0000000 --- a/vendor/symfony/process/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2004-present Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/symfony/process/Messenger/RunProcessContext.php b/vendor/symfony/process/Messenger/RunProcessContext.php deleted file mode 100644 index 5e22304..0000000 --- a/vendor/symfony/process/Messenger/RunProcessContext.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Messenger; - -use Symfony\Component\Process\Process; - -/** - * @author Kevin Bond - */ -final class RunProcessContext -{ - public readonly ?int $exitCode; - public readonly ?string $output; - public readonly ?string $errorOutput; - - public function __construct( - public readonly RunProcessMessage $message, - Process $process, - ) { - $this->exitCode = $process->getExitCode(); - $this->output = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getOutput(); - $this->errorOutput = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getErrorOutput(); - } -} diff --git a/vendor/symfony/process/Messenger/RunProcessMessage.php b/vendor/symfony/process/Messenger/RunProcessMessage.php deleted file mode 100644 index d14ac23..0000000 --- a/vendor/symfony/process/Messenger/RunProcessMessage.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Messenger; - -/** - * @author Kevin Bond - */ -class RunProcessMessage implements \Stringable -{ - public ?string $commandLine = null; - - public function __construct( - public readonly array $command, - public readonly ?string $cwd = null, - public readonly ?array $env = null, - public readonly mixed $input = null, - public readonly ?float $timeout = 60.0, - ) { - } - - public function __toString(): string - { - return $this->commandLine ?? implode(' ', $this->command); - } - - /** - * Create a process message instance that will instantiate a Process using the fromShellCommandline method. - * - * @see Process::fromShellCommandline - */ - public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): self - { - $message = new self([], $cwd, $env, $input, $timeout); - $message->commandLine = $command; - - return $message; - } -} diff --git a/vendor/symfony/process/Messenger/RunProcessMessageHandler.php b/vendor/symfony/process/Messenger/RunProcessMessageHandler.php deleted file mode 100644 index 69bfa6a..0000000 --- a/vendor/symfony/process/Messenger/RunProcessMessageHandler.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Messenger; - -use Symfony\Component\Process\Exception\ProcessFailedException; -use Symfony\Component\Process\Exception\RunProcessFailedException; -use Symfony\Component\Process\Process; - -/** - * @author Kevin Bond - */ -final class RunProcessMessageHandler -{ - public function __invoke(RunProcessMessage $message): RunProcessContext - { - $process = match ($message->commandLine) { - null => new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout), - default => Process::fromShellCommandline($message->commandLine, $message->cwd, $message->env, $message->input, $message->timeout), - }; - - try { - return new RunProcessContext($message, $process->mustRun()); - } catch (ProcessFailedException $e) { - throw new RunProcessFailedException($e, new RunProcessContext($message, $e->getProcess())); - } - } -} diff --git a/vendor/symfony/process/PhpExecutableFinder.php b/vendor/symfony/process/PhpExecutableFinder.php deleted file mode 100644 index f9ed79e..0000000 --- a/vendor/symfony/process/PhpExecutableFinder.php +++ /dev/null @@ -1,98 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process; - -/** - * An executable finder specifically designed for the PHP executable. - * - * @author Fabien Potencier - * @author Johannes M. Schmitt - */ -class PhpExecutableFinder -{ - private ExecutableFinder $executableFinder; - - public function __construct() - { - $this->executableFinder = new ExecutableFinder(); - } - - /** - * Finds The PHP executable. - */ - public function find(bool $includeArgs = true): string|false - { - if ($php = getenv('PHP_BINARY')) { - if (!is_executable($php) && !$php = $this->executableFinder->find($php)) { - return false; - } - - if (@is_dir($php)) { - return false; - } - - return $php; - } - - $args = $this->findArguments(); - $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; - - // PHP_BINARY return the current sapi executable - if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) { - return \PHP_BINARY.$args; - } - - if ($php = getenv('PHP_PATH')) { - if (!@is_executable($php) || @is_dir($php)) { - return false; - } - - return $php; - } - - if ($php = getenv('PHP_PEAR_PHP_BIN')) { - if (@is_executable($php) && !@is_dir($php)) { - return $php; - } - } - - if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) { - return $php; - } - - $dirs = [\PHP_BINDIR]; - if ('\\' === \DIRECTORY_SEPARATOR) { - $dirs[] = 'C:\xampp\php\\'; - } - - if ($herdPath = getenv('HERD_HOME')) { - $dirs[] = $herdPath.\DIRECTORY_SEPARATOR.'bin'; - } - - return $this->executableFinder->find('php', false, $dirs); - } - - /** - * Finds the PHP executable arguments. - * - * @return list - */ - public function findArguments(): array - { - $arguments = []; - if ('phpdbg' === \PHP_SAPI) { - $arguments[] = '-qrr'; - } - - return $arguments; - } -} diff --git a/vendor/symfony/process/PhpProcess.php b/vendor/symfony/process/PhpProcess.php deleted file mode 100644 index 930f591..0000000 --- a/vendor/symfony/process/PhpProcess.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process; - -use Symfony\Component\Process\Exception\LogicException; -use Symfony\Component\Process\Exception\RuntimeException; - -/** - * PhpProcess runs a PHP script in an independent process. - * - * $p = new PhpProcess(''); - * $p->run(); - * print $p->getOutput()."\n"; - * - * @author Fabien Potencier - */ -class PhpProcess extends Process -{ - /** - * @param string $script The PHP script to run (as a string) - * @param string|null $cwd The working directory or null to use the working dir of the current PHP process - * @param array|null $env The environment variables or null to use the same environment as the current PHP process - * @param int $timeout The timeout in seconds - * @param array|null $php Path to the PHP binary to use with any additional arguments - */ - public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) - { - if (null === $php) { - $executableFinder = new PhpExecutableFinder(); - $php = $executableFinder->find(false); - $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); - } - if ('phpdbg' === \PHP_SAPI) { - $file = tempnam(sys_get_temp_dir(), 'dbg'); - file_put_contents($file, $script); - register_shutdown_function('unlink', $file); - $php[] = $file; - $script = null; - } - - parent::__construct($php, $cwd, $env, $script, $timeout); - } - - public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static - { - throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); - } - - /** - * @param (callable('out'|'err', string):void)|null $callback - */ - public function start(?callable $callback = null, array $env = []): void - { - if (null === $this->getCommandLine()) { - throw new RuntimeException('Unable to find the PHP executable.'); - } - - parent::start($callback, $env); - } -} diff --git a/vendor/symfony/process/PhpSubprocess.php b/vendor/symfony/process/PhpSubprocess.php deleted file mode 100644 index 8282f93..0000000 --- a/vendor/symfony/process/PhpSubprocess.php +++ /dev/null @@ -1,167 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process; - -use Symfony\Component\Process\Exception\LogicException; -use Symfony\Component\Process\Exception\RuntimeException; - -/** - * PhpSubprocess runs a PHP command as a subprocess while keeping the original php.ini settings. - * - * For this, it generates a temporary php.ini file taking over all the current settings and disables - * loading additional .ini files. Basically, your command gets prefixed using "php -n -c /tmp/temp.ini". - * - * Given your php.ini contains "memory_limit=-1" and you have a "MemoryTest.php" with the following content: - * - * run(); - * print $p->getOutput()."\n"; - * - * This will output "string(2) "-1", because the process is started with the default php.ini settings. - * - * $p = new PhpSubprocess(['MemoryTest.php'], null, null, 60, ['php', '-d', 'memory_limit=256M']); - * $p->run(); - * print $p->getOutput()."\n"; - * - * This will output "string(4) "256M"", because the process is started with the temporarily created php.ini settings. - * - * @author Yanick Witschi - * @author Partially copied and heavily inspired from composer/xdebug-handler by John Stevenson - */ -class PhpSubprocess extends Process -{ - /** - * @param array $command The command to run and its arguments listed as separate entries. They will automatically - * get prefixed with the PHP binary - * @param string|null $cwd The working directory or null to use the working dir of the current PHP process - * @param array|null $env The environment variables or null to use the same environment as the current PHP process - * @param int $timeout The timeout in seconds - * @param array|null $php Path to the PHP binary to use with any additional arguments - */ - public function __construct(array $command, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) - { - if (null === $php) { - $executableFinder = new PhpExecutableFinder(); - $php = $executableFinder->find(false); - $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); - } - - if (null === $php) { - throw new RuntimeException('Unable to find PHP binary.'); - } - - $tmpIni = $this->writeTmpIni($this->getAllIniFiles(), sys_get_temp_dir()); - - $php = array_merge($php, ['-n', '-c', $tmpIni]); - register_shutdown_function('unlink', $tmpIni); - - $command = array_merge($php, $command); - - parent::__construct($command, $cwd, $env, null, $timeout); - } - - public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static - { - throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); - } - - /** - * @param (callable('out'|'err', string):void)|null $callback - */ - public function start(?callable $callback = null, array $env = []): void - { - if (null === $this->getCommandLine()) { - throw new RuntimeException('Unable to find the PHP executable.'); - } - - parent::start($callback, $env); - } - - private function writeTmpIni(array $iniFiles, string $tmpDir): string - { - if (false === $tmpfile = @tempnam($tmpDir, '')) { - throw new RuntimeException('Unable to create temporary ini file.'); - } - - // $iniFiles has at least one item and it may be empty - if ('' === $iniFiles[0]) { - array_shift($iniFiles); - } - - $content = ''; - - foreach ($iniFiles as $file) { - // Check for inaccessible ini files - if (($data = @file_get_contents($file)) === false) { - throw new RuntimeException('Unable to read ini: '.$file); - } - // Check and remove directives after HOST and PATH sections - if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) { - $data = substr($data, 0, $matches[0][1]); - } - - $content .= $data."\n"; - } - - // Merge loaded settings into our ini content, if it is valid - $config = parse_ini_string($content); - $loaded = ini_get_all(null, false); - - if (false === $config || false === $loaded) { - throw new RuntimeException('Unable to parse ini data.'); - } - - $content .= $this->mergeLoadedConfig($loaded, $config); - - // Work-around for https://bugs.php.net/bug.php?id=75932 - $content .= "opcache.enable_cli=0\n"; - - if (false === @file_put_contents($tmpfile, $content)) { - throw new RuntimeException('Unable to write temporary ini file.'); - } - - return $tmpfile; - } - - private function mergeLoadedConfig(array $loadedConfig, array $iniConfig): string - { - $content = ''; - - foreach ($loadedConfig as $name => $value) { - if (!\is_string($value)) { - continue; - } - - if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { - // Double-quote escape each value - $content .= $name.'="'.addcslashes($value, '\\"')."\"\n"; - } - } - - return $content; - } - - private function getAllIniFiles(): array - { - $paths = [(string) php_ini_loaded_file()]; - - if (false !== $scanned = php_ini_scanned_files()) { - $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); - } - - return $paths; - } -} diff --git a/vendor/symfony/process/Pipes/AbstractPipes.php b/vendor/symfony/process/Pipes/AbstractPipes.php deleted file mode 100644 index 49a14da..0000000 --- a/vendor/symfony/process/Pipes/AbstractPipes.php +++ /dev/null @@ -1,204 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Pipes; - -use Symfony\Component\Process\Exception\InvalidArgumentException; - -/** - * @author Romain Neutron - * - * @internal - */ -abstract class AbstractPipes implements PipesInterface -{ - public array $pipes = []; - - private string $inputBuffer = ''; - /** @var resource|string|\Iterator */ - private $input; - private bool $blocked = true; - private ?string $lastError = null; - - /** - * @param resource|string|\Iterator $input - */ - public function __construct($input) - { - if (\is_resource($input) || $input instanceof \Iterator) { - $this->input = $input; - } else { - $this->inputBuffer = (string) $input; - } - } - - public function close(): void - { - foreach ($this->pipes as $pipe) { - if (\is_resource($pipe)) { - fclose($pipe); - } - } - $this->pipes = []; - } - - /** - * Returns true if a system call has been interrupted. - * - * stream_select() returns false when the `select` system call is interrupted by an incoming signal. - */ - protected function hasSystemCallBeenInterrupted(): bool - { - $lastError = $this->lastError; - $this->lastError = null; - - if (null === $lastError) { - return false; - } - - if (false !== stripos($lastError, 'interrupted system call')) { - return true; - } - - // on applications with a different locale than english, the message above is not found because - // it's translated. So we also check for the SOCKET_EINTR constant which is defined under - // Windows and UNIX-like platforms (if available on the platform). - return \defined('SOCKET_EINTR') && str_starts_with($lastError, 'stream_select(): Unable to select ['.\SOCKET_EINTR.']'); - } - - /** - * Unblocks streams. - */ - protected function unblock(): void - { - if (!$this->blocked) { - return; - } - - foreach ($this->pipes as $pipe) { - stream_set_blocking($pipe, false); - } - if (\is_resource($this->input)) { - stream_set_blocking($this->input, false); - } - - $this->blocked = false; - } - - /** - * Writes input to stdin. - * - * @throws InvalidArgumentException When an input iterator yields a non supported value - */ - protected function write(): ?array - { - if (!isset($this->pipes[0])) { - return null; - } - $input = $this->input; - - if ($input instanceof \Iterator) { - if (!$input->valid()) { - $input = null; - } elseif (\is_resource($input = $input->current())) { - stream_set_blocking($input, false); - } elseif (!isset($this->inputBuffer[0])) { - if (!\is_string($input)) { - if (!\is_scalar($input)) { - throw new InvalidArgumentException(\sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input))); - } - $input = (string) $input; - } - $this->inputBuffer = $input; - $this->input->next(); - $input = null; - } else { - $input = null; - } - } - - $r = $e = []; - $w = [$this->pipes[0]]; - - // let's have a look if something changed in streams - if (false === @stream_select($r, $w, $e, 0, 0)) { - return null; - } - - foreach ($w as $stdin) { - if (isset($this->inputBuffer[0])) { - if (false === $written = @fwrite($stdin, $this->inputBuffer)) { - return $this->closeBrokenInputPipe(); - } - $this->inputBuffer = substr($this->inputBuffer, $written); - if (isset($this->inputBuffer[0]) && isset($this->pipes[0])) { - return [$this->pipes[0]]; - } - } - - if ($input) { - while (true) { - $data = fread($input, self::CHUNK_SIZE); - if (!isset($data[0])) { - break; - } - if (false === $written = @fwrite($stdin, $data)) { - return $this->closeBrokenInputPipe(); - } - $data = substr($data, $written); - if (isset($data[0])) { - $this->inputBuffer = $data; - - return isset($this->pipes[0]) ? [$this->pipes[0]] : null; - } - } - if (feof($input)) { - if ($this->input instanceof \Iterator) { - $this->input->next(); - } else { - $this->input = null; - } - } - } - } - - // no input to read on resource, buffer is empty - if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) { - $this->input = null; - fclose($this->pipes[0]); - unset($this->pipes[0]); - } elseif (!$w) { - return [$this->pipes[0]]; - } - - return null; - } - - private function closeBrokenInputPipe(): void - { - $this->lastError = error_get_last()['message'] ?? null; - if (\is_resource($this->pipes[0] ?? null)) { - fclose($this->pipes[0]); - } - unset($this->pipes[0]); - - $this->input = null; - $this->inputBuffer = ''; - } - - /** - * @internal - */ - public function handleError(int $type, string $msg): void - { - $this->lastError = $msg; - } -} diff --git a/vendor/symfony/process/Pipes/PipesInterface.php b/vendor/symfony/process/Pipes/PipesInterface.php deleted file mode 100644 index 967f8de..0000000 --- a/vendor/symfony/process/Pipes/PipesInterface.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Pipes; - -/** - * PipesInterface manages descriptors and pipes for the use of proc_open. - * - * @author Romain Neutron - * - * @internal - */ -interface PipesInterface -{ - public const CHUNK_SIZE = 16384; - - /** - * Returns an array of descriptors for the use of proc_open. - */ - public function getDescriptors(): array; - - /** - * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. - * - * @return string[] - */ - public function getFiles(): array; - - /** - * Reads data in file handles and pipes. - * - * @param bool $blocking Whether to use blocking calls or not - * @param bool $close Whether to close pipes if they've reached EOF - * - * @return string[] An array of read data indexed by their fd - */ - public function readAndWrite(bool $blocking, bool $close = false): array; - - /** - * Returns if the current state has open file handles or pipes. - */ - public function areOpen(): bool; - - /** - * Returns if pipes are able to read output. - */ - public function haveReadSupport(): bool; - - /** - * Closes file handles and pipes. - */ - public function close(): void; -} diff --git a/vendor/symfony/process/Pipes/UnixPipes.php b/vendor/symfony/process/Pipes/UnixPipes.php deleted file mode 100644 index 5fbab21..0000000 --- a/vendor/symfony/process/Pipes/UnixPipes.php +++ /dev/null @@ -1,144 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Pipes; - -use Symfony\Component\Process\Process; - -/** - * UnixPipes implementation uses unix pipes as handles. - * - * @author Romain Neutron - * - * @internal - */ -class UnixPipes extends AbstractPipes -{ - public function __construct( - private ?bool $ttyMode, - private bool $ptyMode, - mixed $input, - private bool $haveReadSupport, - ) { - parent::__construct($input); - } - - public function __serialize(): array - { - throw new \BadMethodCallException('Cannot serialize '.__CLASS__); - } - - public function __unserialize(array $data): void - { - throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); - } - - public function __destruct() - { - $this->close(); - } - - public function getDescriptors(): array - { - if (!$this->haveReadSupport) { - $nullstream = fopen('/dev/null', 'c'); - - return [ - ['pipe', 'r'], - $nullstream, - $nullstream, - ]; - } - - if ($this->ttyMode) { - return [ - ['file', '/dev/tty', 'r'], - ['file', '/dev/tty', 'w'], - ['file', '/dev/tty', 'w'], - ]; - } - - if ($this->ptyMode && Process::isPtySupported()) { - return [ - ['pty'], - ['pty'], - ['pipe', 'w'], // stderr needs to be in a pipe to correctly split error and output, since PHP will use the same stream for both - ]; - } - - return [ - ['pipe', 'r'], - ['pipe', 'w'], // stdout - ['pipe', 'w'], // stderr - ]; - } - - public function getFiles(): array - { - return []; - } - - public function readAndWrite(bool $blocking, bool $close = false): array - { - $this->unblock(); - $w = $this->write(); - - $read = $e = []; - $r = $this->pipes; - unset($r[0]); - - // let's have a look if something changed in streams - set_error_handler($this->handleError(...)); - if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { - restore_error_handler(); - // if a system call has been interrupted, forget about it, let's try again - // otherwise, an error occurred, let's reset pipes - if (!$this->hasSystemCallBeenInterrupted()) { - $this->pipes = []; - } - - return $read; - } - restore_error_handler(); - - foreach ($r as $pipe) { - // prior PHP 5.4 the array passed to stream_select is modified and - // lose key association, we have to find back the key - $read[$type = array_search($pipe, $this->pipes, true)] = ''; - - do { - $data = @fread($pipe, self::CHUNK_SIZE); - $read[$type] .= $data; - } while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1]))); - - if (!isset($read[$type][0])) { - unset($read[$type]); - } - - if ($close && feof($pipe)) { - fclose($pipe); - unset($this->pipes[$type]); - } - } - - return $read; - } - - public function haveReadSupport(): bool - { - return $this->haveReadSupport; - } - - public function areOpen(): bool - { - return (bool) $this->pipes; - } -} diff --git a/vendor/symfony/process/Pipes/WindowsPipes.php b/vendor/symfony/process/Pipes/WindowsPipes.php deleted file mode 100644 index f4ab195..0000000 --- a/vendor/symfony/process/Pipes/WindowsPipes.php +++ /dev/null @@ -1,185 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process\Pipes; - -use Symfony\Component\Process\Exception\RuntimeException; -use Symfony\Component\Process\Process; - -/** - * WindowsPipes implementation uses temporary files as handles. - * - * @see https://bugs.php.net/51800 - * @see https://bugs.php.net/65650 - * - * @author Romain Neutron - * - * @internal - */ -class WindowsPipes extends AbstractPipes -{ - private array $files = []; - private array $fileHandles = []; - private array $lockHandles = []; - private array $readBytes = [ - Process::STDOUT => 0, - Process::STDERR => 0, - ]; - - public function __construct( - mixed $input, - private bool $haveReadSupport, - ) { - if ($this->haveReadSupport) { - // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. - // Workaround for this problem is to use temporary files instead of pipes on Windows platform. - // - // @see https://bugs.php.net/51800 - $pipes = [ - Process::STDOUT => Process::OUT, - Process::STDERR => Process::ERR, - ]; - $tmpDir = sys_get_temp_dir(); - $lastError = 'unknown reason'; - set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); - for ($i = 0;; ++$i) { - foreach ($pipes as $pipe => $name) { - $file = \sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); - - if (!$h = fopen($file.'.lock', 'w')) { - if (file_exists($file.'.lock')) { - continue 2; - } - restore_error_handler(); - throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError); - } - if (!flock($h, \LOCK_EX | \LOCK_NB)) { - continue 2; - } - if (isset($this->lockHandles[$pipe])) { - flock($this->lockHandles[$pipe], \LOCK_UN); - fclose($this->lockHandles[$pipe]); - } - $this->lockHandles[$pipe] = $h; - - if (!($h = fopen($file, 'w')) || !fclose($h) || !$h = fopen($file, 'r')) { - flock($this->lockHandles[$pipe], \LOCK_UN); - fclose($this->lockHandles[$pipe]); - unset($this->lockHandles[$pipe]); - continue 2; - } - $this->fileHandles[$pipe] = $h; - $this->files[$pipe] = $file; - } - break; - } - restore_error_handler(); - } - - parent::__construct($input); - } - - public function __serialize(): array - { - throw new \BadMethodCallException('Cannot serialize '.__CLASS__); - } - - public function __unserialize(array $data): void - { - throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); - } - - public function __destruct() - { - $this->close(); - } - - public function getDescriptors(): array - { - if (!$this->haveReadSupport) { - $nullstream = fopen('NUL', 'c'); - - return [ - ['pipe', 'r'], - $nullstream, - $nullstream, - ]; - } - - // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/51800) - // We're not using file handles as it can produce corrupted output https://bugs.php.net/65650 - // So we redirect output within the commandline and pass the nul device to the process - return [ - ['pipe', 'r'], - ['file', 'NUL', 'w'], - ['file', 'NUL', 'w'], - ]; - } - - public function getFiles(): array - { - return $this->files; - } - - public function readAndWrite(bool $blocking, bool $close = false): array - { - $this->unblock(); - $w = $this->write(); - $read = $r = $e = []; - - if ($blocking) { - if ($w) { - @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); - } elseif ($this->fileHandles) { - usleep((int) (Process::TIMEOUT_PRECISION * 1E6)); - } - } - foreach ($this->fileHandles as $type => $fileHandle) { - $data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]); - - if (isset($data[0])) { - $this->readBytes[$type] += \strlen($data); - $read[$type] = $data; - } - if ($close) { - ftruncate($fileHandle, 0); - fclose($fileHandle); - flock($this->lockHandles[$type], \LOCK_UN); - fclose($this->lockHandles[$type]); - unset($this->fileHandles[$type], $this->lockHandles[$type]); - } - } - - return $read; - } - - public function haveReadSupport(): bool - { - return $this->haveReadSupport; - } - - public function areOpen(): bool - { - return $this->pipes && $this->fileHandles; - } - - public function close(): void - { - parent::close(); - foreach ($this->fileHandles as $type => $handle) { - ftruncate($handle, 0); - fclose($handle); - flock($this->lockHandles[$type], \LOCK_UN); - fclose($this->lockHandles[$type]); - } - $this->fileHandles = $this->lockHandles = []; - } -} diff --git a/vendor/symfony/process/Process.php b/vendor/symfony/process/Process.php deleted file mode 100644 index 75d6d50..0000000 --- a/vendor/symfony/process/Process.php +++ /dev/null @@ -1,1676 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process; - -use Symfony\Component\Process\Exception\InvalidArgumentException; -use Symfony\Component\Process\Exception\LogicException; -use Symfony\Component\Process\Exception\ProcessFailedException; -use Symfony\Component\Process\Exception\ProcessSignaledException; -use Symfony\Component\Process\Exception\ProcessStartFailedException; -use Symfony\Component\Process\Exception\ProcessTimedOutException; -use Symfony\Component\Process\Exception\RuntimeException; -use Symfony\Component\Process\Pipes\UnixPipes; -use Symfony\Component\Process\Pipes\WindowsPipes; - -/** - * Process is a thin wrapper around proc_* functions to easily - * start independent PHP processes. - * - * @author Fabien Potencier - * @author Romain Neutron - * - * @implements \IteratorAggregate - */ -class Process implements \IteratorAggregate -{ - public const ERR = 'err'; - public const OUT = 'out'; - - public const STATUS_READY = 'ready'; - public const STATUS_STARTED = 'started'; - public const STATUS_TERMINATED = 'terminated'; - - public const STDIN = 0; - public const STDOUT = 1; - public const STDERR = 2; - - // Timeout Precision in seconds. - public const TIMEOUT_PRECISION = 0.2; - - public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking - public const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory - public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating - public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating - - /** - * @var \Closure('out'|'err', string):bool|null - */ - private ?\Closure $callback = null; - private array|string $commandline; - private ?string $cwd; - private array $env = []; - /** @var resource|string|\Iterator|null */ - private $input; - private ?float $starttime = null; - private ?float $lastOutputTime = null; - private ?float $timeout = null; - private ?float $idleTimeout = null; - private ?int $exitcode = null; - private array $fallbackStatus = []; - private array $processInformation; - private bool $outputDisabled = false; - /** @var resource */ - private $stdout; - /** @var resource */ - private $stderr; - /** @var resource|null */ - private $process; - private string $status = self::STATUS_READY; - private int $incrementalOutputOffset = 0; - private int $incrementalErrorOutputOffset = 0; - private bool $tty = false; - private bool $pty; - private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; - private array $ignoredSignals = []; - - private WindowsPipes|UnixPipes $processPipes; - - private ?int $latestSignal = null; - - private static ?bool $sigchild = null; - private static array $executables = []; - - /** - * Exit codes translation table. - * - * User-defined errors must use exit codes in the 64-113 range. - */ - public static array $exitCodes = [ - 0 => 'OK', - 1 => 'General error', - 2 => 'Misuse of shell builtins', - - 126 => 'Invoked command cannot execute', - 127 => 'Command not found', - 128 => 'Invalid exit argument', - - // signals - 129 => 'Hangup', - 130 => 'Interrupt', - 131 => 'Quit and dump core', - 132 => 'Illegal instruction', - 133 => 'Trace/breakpoint trap', - 134 => 'Process aborted', - 135 => 'Bus error: "access to undefined portion of memory object"', - 136 => 'Floating point exception: "erroneous arithmetic operation"', - 137 => 'Kill (terminate immediately)', - 138 => 'User-defined 1', - 139 => 'Segmentation violation', - 140 => 'User-defined 2', - 141 => 'Write to pipe with no one reading', - 142 => 'Signal raised by alarm', - 143 => 'Termination (request to terminate)', - // 144 - not defined - 145 => 'Child process terminated, stopped (or continued*)', - 146 => 'Continue if stopped', - 147 => 'Stop executing temporarily', - 148 => 'Terminal stop signal', - 149 => 'Background process attempting to read from tty ("in")', - 150 => 'Background process attempting to write to tty ("out")', - 151 => 'Urgent data available on socket', - 152 => 'CPU time limit exceeded', - 153 => 'File size limit exceeded', - 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', - 155 => 'Profiling timer expired', - // 156 - not defined - 157 => 'Pollable event', - // 158 - not defined - 159 => 'Bad syscall', - ]; - - /** - * @param array $command The command to run and its arguments listed as separate entries - * @param string|null $cwd The working directory or null to use the working dir of the current PHP process - * @param array|null $env The environment variables or null to use the same environment as the current PHP process - * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input - * @param int|float|null $timeout The timeout in seconds or null to disable - * - * @throws LogicException When proc_open is not installed - */ - public function __construct(array $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60) - { - if (!\function_exists('proc_open')) { - throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.'); - } - - $this->commandline = $command; - $this->cwd = $cwd; - - // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started - // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected - // @see : https://bugs.php.net/51800 - // @see : https://bugs.php.net/50524 - if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) { - $this->cwd = getcwd(); - } - if (null !== $env) { - $this->setEnv($env); - } - - $this->setInput($input); - $this->setTimeout($timeout); - $this->pty = false; - } - - /** - * Creates a Process instance as a command-line to be run in a shell wrapper. - * - * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.) - * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the - * shell wrapper and not to your commands. - * - * In order to inject dynamic values into command-lines, we strongly recommend using placeholders. - * This will save escaping values, which is not portable nor secure anyway: - * - * $process = Process::fromShellCommandline('my_command "${:MY_VAR}"'); - * $process->run(null, ['MY_VAR' => $theValue]); - * - * @param string $command The command line to pass to the shell of the OS - * @param string|null $cwd The working directory or null to use the working dir of the current PHP process - * @param array|null $env The environment variables or null to use the same environment as the current PHP process - * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input - * @param int|float|null $timeout The timeout in seconds or null to disable - * - * @throws LogicException When proc_open is not installed - */ - public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static - { - $process = new static([], $cwd, $env, $input, $timeout); - $process->commandline = $command; - - return $process; - } - - public function __serialize(): array - { - throw new \BadMethodCallException('Cannot serialize '.__CLASS__); - } - - public function __unserialize(array $data): void - { - throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); - } - - public function __destruct() - { - if ($this->options['create_new_console'] ?? false) { - $this->processPipes->close(); - } else { - $this->stop(0); - } - } - - public function __clone() - { - $this->resetProcessData(); - } - - /** - * Runs the process. - * - * The callback receives the type of output (out or err) and - * some bytes from the output in real-time. It allows to have feedback - * from the independent process during execution. - * - * The STDOUT and STDERR are also available after the process is finished - * via the getOutput() and getErrorOutput() methods. - * - * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR - * - * @return int The exit status code - * - * @throws ProcessStartFailedException When process can't be launched - * @throws RuntimeException When process is already running - * @throws ProcessTimedOutException When process timed out - * @throws ProcessSignaledException When process stopped after receiving signal - * @throws LogicException In case a callback is provided and output has been disabled - * - * @final - */ - public function run(?callable $callback = null, array $env = []): int - { - $this->start($callback, $env); - - return $this->wait(); - } - - /** - * Runs the process. - * - * This is identical to run() except that an exception is thrown if the process - * exits with a non-zero exit code. - * - * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR - * - * @return $this - * - * @throws ProcessFailedException When process didn't terminate successfully - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running - * @throws ProcessTimedOutException When process timed out - * @throws ProcessSignaledException When process stopped after receiving signal - * @throws LogicException In case a callback is provided and output has been disabled - * - * @final - */ - public function mustRun(?callable $callback = null, array $env = []): static - { - if (0 !== $this->run($callback, $env)) { - throw new ProcessFailedException($this); - } - - return $this; - } - - /** - * Starts the process and returns after writing the input to STDIN. - * - * This method blocks until all STDIN data is sent to the process then it - * returns while the process runs in the background. - * - * The termination of the process can be awaited with wait(). - * - * The callback receives the type of output (out or err) and some bytes from - * the output in real-time while writing the standard input to the process. - * It allows to have feedback from the independent process during execution. - * - * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR - * - * @throws ProcessStartFailedException When process can't be launched - * @throws RuntimeException When process is already running - * @throws LogicException In case a callback is provided and output has been disabled - */ - public function start(?callable $callback = null, array $env = []): void - { - if ($this->isRunning()) { - throw new RuntimeException('Process is already running.'); - } - - $this->resetProcessData(); - $this->starttime = $this->lastOutputTime = microtime(true); - $this->callback = $this->buildCallback($callback); - $descriptors = $this->getDescriptors(null !== $callback); - - if ($this->env) { - $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; - } - - $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); - - if (\is_array($commandline = $this->commandline)) { - $commandline = array_values(array_map(strval(...), $commandline)); - } else { - $commandline = $this->replacePlaceholders($commandline, $env); - } - - if ('\\' === \DIRECTORY_SEPARATOR) { - $commandline = $this->prepareWindowsCommandLine($commandline, $env); - } elseif ($this->isSigchildEnabled()) { - // last exit code is output on the fourth pipe and caught to work around --enable-sigchild - $descriptors[3] = ['pipe', 'w']; - - if (\is_array($commandline)) { - // exec is mandatory to deal with sending a signal to the process - $commandline = 'exec '.$this->buildShellCommandline($commandline); - } - - // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input - $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; - $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; - } - - $envPairs = []; - foreach ($env as $k => $v) { - if (false !== $v && !\in_array($k = (string) $k, ['', 'argc', 'argv', 'ARGC', 'ARGV'], true) && !str_contains($k, '=') && !str_contains($k, "\0")) { - $envPairs[] = $k.'='.$v; - } - } - - if (!is_dir($this->cwd)) { - throw new RuntimeException(\sprintf('The provided cwd "%s" does not exist.', $this->cwd)); - } - - $lastError = null; - set_error_handler(function ($type, $msg) use (&$lastError) { - $lastError = $msg; - - return true; - }); - - $oldMask = []; - - if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { - // we block signals we want to ignore, as proc_open will use fork / posix_spawn which will copy the signal mask this allow to block - // signals in the child process - pcntl_sigprocmask(\SIG_BLOCK, $this->ignoredSignals, $oldMask); - } - - try { - $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); - - // Ensure array vs string commands behave the same - if (!$process && \is_array($commandline)) { - $process = @proc_open('exec '.$this->buildShellCommandline($commandline), $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); - } - } finally { - if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { - // we restore the signal mask here to avoid any side effects - pcntl_sigprocmask(\SIG_SETMASK, $oldMask); - } - - restore_error_handler(); - } - - if (!$process) { - throw new ProcessStartFailedException($this, $lastError); - } - $this->process = $process; - $this->status = self::STATUS_STARTED; - - if (isset($descriptors[3])) { - $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); - } - - if ($this->tty) { - return; - } - - $this->updateStatus(false); - $this->checkTimeout(); - } - - /** - * Restarts the process. - * - * Be warned that the process is cloned before being started. - * - * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR - * - * @throws ProcessStartFailedException When process can't be launched - * @throws RuntimeException When process is already running - * - * @see start() - * - * @final - */ - public function restart(?callable $callback = null, array $env = []): static - { - if ($this->isRunning()) { - throw new RuntimeException('Process is already running.'); - } - - $process = clone $this; - $process->start($callback, $env); - - return $process; - } - - /** - * Waits for the process to terminate. - * - * The callback receives the type of output (out or err) and some bytes - * from the output in real-time while writing the standard input to the process. - * It allows to have feedback from the independent process during execution. - * - * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR - * - * @return int The exitcode of the process - * - * @throws ProcessTimedOutException When process timed out - * @throws ProcessSignaledException When process stopped after receiving signal - * @throws LogicException When process is not yet started - */ - public function wait(?callable $callback = null): int - { - $this->requireProcessIsStarted(__FUNCTION__); - - $this->updateStatus(false); - - if (null !== $callback) { - if (!$this->processPipes->haveReadSupport()) { - $this->stop(0); - throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".'); - } - $this->callback = $this->buildCallback($callback); - } - - do { - $this->checkTimeout(); - $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen()); - $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); - } while ($running); - - while ($this->isRunning()) { - $this->checkTimeout(); - usleep(1000); - } - - if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { - throw new ProcessSignaledException($this); - } - - return $this->exitcode; - } - - /** - * Waits until the callback returns true. - * - * The callback receives the type of output (out or err) and some bytes - * from the output in real-time while writing the standard input to the process. - * It allows to have feedback from the independent process during execution. - * - * @param (callable('out'|'err', string):bool)|null $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR - * - * @throws RuntimeException When process timed out - * @throws LogicException When process is not yet started - * @throws ProcessTimedOutException In case the timeout was reached - */ - public function waitUntil(callable $callback): bool - { - $this->requireProcessIsStarted(__FUNCTION__); - $this->updateStatus(false); - - if (!$this->processPipes->haveReadSupport()) { - $this->stop(0); - throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".'); - } - $callback = $this->buildCallback($callback); - - $ready = false; - while (true) { - $this->checkTimeout(); - $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); - $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); - - foreach ($output as $type => $data) { - if (3 !== $type) { - $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready; - } elseif (!isset($this->fallbackStatus['signaled'])) { - $this->fallbackStatus['exitcode'] = (int) $data; - } - } - if ($ready) { - return true; - } - if (!$running) { - return false; - } - - usleep(1000); - } - } - - /** - * Returns the Pid (process identifier), if applicable. - * - * @return int|null The process id if running, null otherwise - */ - public function getPid(): ?int - { - return $this->isRunning() ? $this->processInformation['pid'] : null; - } - - /** - * Sends a POSIX signal to the process. - * - * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) - * - * @return $this - * - * @throws LogicException In case the process is not running - * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed - * @throws RuntimeException In case of failure - */ - public function signal(int $signal): static - { - $this->doSignal($signal, true); - - return $this; - } - - /** - * Disables fetching output and error output from the underlying process. - * - * @return $this - * - * @throws RuntimeException In case the process is already running - * @throws LogicException if an idle timeout is set - */ - public function disableOutput(): static - { - if ($this->isRunning()) { - throw new RuntimeException('Disabling output while the process is running is not possible.'); - } - if (null !== $this->idleTimeout) { - throw new LogicException('Output cannot be disabled while an idle timeout is set.'); - } - - $this->outputDisabled = true; - - return $this; - } - - /** - * Enables fetching output and error output from the underlying process. - * - * @return $this - * - * @throws RuntimeException In case the process is already running - */ - public function enableOutput(): static - { - if ($this->isRunning()) { - throw new RuntimeException('Enabling output while the process is running is not possible.'); - } - - $this->outputDisabled = false; - - return $this; - } - - /** - * Returns true in case the output is disabled, false otherwise. - */ - public function isOutputDisabled(): bool - { - return $this->outputDisabled; - } - - /** - * Returns the current output of the process (STDOUT). - * - * @throws LogicException in case the output has been disabled - * @throws LogicException In case the process is not started - */ - public function getOutput(): string - { - $this->readPipesForOutput(__FUNCTION__); - - if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { - return ''; - } - - return $ret; - } - - /** - * Returns the output incrementally. - * - * In comparison with the getOutput method which always return the whole - * output, this one returns the new output since the last call. - * - * @throws LogicException in case the output has been disabled - * @throws LogicException In case the process is not started - */ - public function getIncrementalOutput(): string - { - $this->readPipesForOutput(__FUNCTION__); - - $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); - $this->incrementalOutputOffset = ftell($this->stdout); - - if (false === $latest) { - return ''; - } - - return $latest; - } - - /** - * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). - * - * @param int $flags A bit field of Process::ITER_* flags - * - * @return \Generator - * - * @throws LogicException in case the output has been disabled - * @throws LogicException In case the process is not started - */ - public function getIterator(int $flags = 0): \Generator - { - $this->readPipesForOutput(__FUNCTION__, false); - - $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); - $blocking = !(self::ITER_NON_BLOCKING & $flags); - $yieldOut = !(self::ITER_SKIP_OUT & $flags); - $yieldErr = !(self::ITER_SKIP_ERR & $flags); - - while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { - if ($yieldOut) { - $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); - - if (isset($out[0])) { - if ($clearOutput) { - $this->clearOutput(); - } else { - $this->incrementalOutputOffset = ftell($this->stdout); - } - - yield self::OUT => $out; - } - } - - if ($yieldErr) { - $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); - - if (isset($err[0])) { - if ($clearOutput) { - $this->clearErrorOutput(); - } else { - $this->incrementalErrorOutputOffset = ftell($this->stderr); - } - - yield self::ERR => $err; - } - } - - if (!$blocking && !isset($out[0]) && !isset($err[0])) { - yield self::OUT => ''; - } - - $this->checkTimeout(); - $this->readPipesForOutput(__FUNCTION__, $blocking); - } - } - - /** - * Clears the process output. - * - * @return $this - */ - public function clearOutput(): static - { - ftruncate($this->stdout, 0); - fseek($this->stdout, 0); - $this->incrementalOutputOffset = 0; - - return $this; - } - - /** - * Returns the current error output of the process (STDERR). - * - * @throws LogicException in case the output has been disabled - * @throws LogicException In case the process is not started - */ - public function getErrorOutput(): string - { - $this->readPipesForOutput(__FUNCTION__); - - if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { - return ''; - } - - return $ret; - } - - /** - * Returns the errorOutput incrementally. - * - * In comparison with the getErrorOutput method which always return the - * whole error output, this one returns the new error output since the last - * call. - * - * @throws LogicException in case the output has been disabled - * @throws LogicException In case the process is not started - */ - public function getIncrementalErrorOutput(): string - { - $this->readPipesForOutput(__FUNCTION__); - - $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); - $this->incrementalErrorOutputOffset = ftell($this->stderr); - - if (false === $latest) { - return ''; - } - - return $latest; - } - - /** - * Clears the process output. - * - * @return $this - */ - public function clearErrorOutput(): static - { - ftruncate($this->stderr, 0); - fseek($this->stderr, 0); - $this->incrementalErrorOutputOffset = 0; - - return $this; - } - - /** - * Returns the exit code returned by the process. - * - * @return int|null The exit status code, null if the Process is not terminated - */ - public function getExitCode(): ?int - { - $this->updateStatus(false); - - return $this->exitcode; - } - - /** - * Returns a string representation for the exit code returned by the process. - * - * This method relies on the Unix exit code status standardization - * and might not be relevant for other operating systems. - * - * @return string|null A string representation for the exit status code, null if the Process is not terminated - * - * @see http://tldp.org/LDP/abs/html/exitcodes.html - * @see http://en.wikipedia.org/wiki/Unix_signal - */ - public function getExitCodeText(): ?string - { - if (null === $exitcode = $this->getExitCode()) { - return null; - } - - return self::$exitCodes[$exitcode] ?? 'Unknown error'; - } - - /** - * Checks if the process ended successfully. - */ - public function isSuccessful(): bool - { - return 0 === $this->getExitCode(); - } - - /** - * Returns true if the child process has been terminated by an uncaught signal. - * - * It always returns false on Windows. - * - * @throws LogicException In case the process is not terminated - */ - public function hasBeenSignaled(): bool - { - $this->requireProcessIsTerminated(__FUNCTION__); - - return $this->processInformation['signaled']; - } - - /** - * Returns the number of the signal that caused the child process to terminate its execution. - * - * It is only meaningful if hasBeenSignaled() returns true. - * - * @throws RuntimeException In case --enable-sigchild is activated - * @throws LogicException In case the process is not terminated - */ - public function getTermSignal(): int - { - $this->requireProcessIsTerminated(__FUNCTION__); - - if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) { - throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.'); - } - - return $this->processInformation['termsig']; - } - - /** - * Returns true if the child process has been stopped by a signal. - * - * It always returns false on Windows. - * - * @throws LogicException In case the process is not terminated - */ - public function hasBeenStopped(): bool - { - $this->requireProcessIsTerminated(__FUNCTION__); - - return $this->processInformation['stopped']; - } - - /** - * Returns the number of the signal that caused the child process to stop its execution. - * - * It is only meaningful if hasBeenStopped() returns true. - * - * @throws LogicException In case the process is not terminated - */ - public function getStopSignal(): int - { - $this->requireProcessIsTerminated(__FUNCTION__); - - return $this->processInformation['stopsig']; - } - - /** - * Checks if the process is currently running. - */ - public function isRunning(): bool - { - if (self::STATUS_STARTED !== $this->status) { - return false; - } - - $this->updateStatus(false); - - return $this->processInformation['running']; - } - - /** - * Checks if the process has been started with no regard to the current state. - */ - public function isStarted(): bool - { - return self::STATUS_READY != $this->status; - } - - /** - * Checks if the process is terminated. - */ - public function isTerminated(): bool - { - $this->updateStatus(false); - - return self::STATUS_TERMINATED == $this->status; - } - - /** - * Gets the process status. - * - * The status is one of: ready, started, terminated. - */ - public function getStatus(): string - { - $this->updateStatus(false); - - return $this->status; - } - - /** - * Stops the process. - * - * @param int|float $timeout The timeout in seconds - * @param int|null $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) - * - * @return int|null The exit-code of the process or null if it's not running - */ - public function stop(float $timeout = 10, ?int $signal = null): ?int - { - $timeoutMicro = microtime(true) + $timeout; - if ($this->isRunning()) { - // given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here - $this->doSignal(15, false); - do { - usleep(1000); - } while ($this->isRunning() && microtime(true) < $timeoutMicro); - - if ($this->isRunning()) { - // Avoid exception here: process is supposed to be running, but it might have stopped just - // after this line. In any case, let's silently discard the error, we cannot do anything. - $this->doSignal($signal ?: 9, false); - } - } - - if ($this->isRunning()) { - if (isset($this->fallbackStatus['pid'])) { - unset($this->fallbackStatus['pid']); - - return $this->stop(0, $signal); - } - $this->close(); - } - - return $this->exitcode; - } - - /** - * Adds a line to the STDOUT stream. - * - * @internal - */ - public function addOutput(string $line): void - { - $this->lastOutputTime = microtime(true); - - fseek($this->stdout, 0, \SEEK_END); - fwrite($this->stdout, $line); - fseek($this->stdout, $this->incrementalOutputOffset); - } - - /** - * Adds a line to the STDERR stream. - * - * @internal - */ - public function addErrorOutput(string $line): void - { - $this->lastOutputTime = microtime(true); - - fseek($this->stderr, 0, \SEEK_END); - fwrite($this->stderr, $line); - fseek($this->stderr, $this->incrementalErrorOutputOffset); - } - - /** - * Gets the last output time in seconds. - */ - public function getLastOutputTime(): ?float - { - return $this->lastOutputTime; - } - - /** - * Gets the command line to be executed. - */ - public function getCommandLine(): string - { - return $this->buildShellCommandline($this->commandline); - } - - /** - * Gets the process timeout in seconds (max. runtime). - */ - public function getTimeout(): ?float - { - return $this->timeout; - } - - /** - * Gets the process idle timeout in seconds (max. time since last output). - */ - public function getIdleTimeout(): ?float - { - return $this->idleTimeout; - } - - /** - * Sets the process timeout (max. runtime) in seconds. - * - * To disable the timeout, set this value to null. - * - * @return $this - * - * @throws InvalidArgumentException if the timeout is negative - */ - public function setTimeout(?float $timeout): static - { - $this->timeout = $this->validateTimeout($timeout); - - return $this; - } - - /** - * Sets the process idle timeout (max. time since last output) in seconds. - * - * To disable the timeout, set this value to null. - * - * @return $this - * - * @throws LogicException if the output is disabled - * @throws InvalidArgumentException if the timeout is negative - */ - public function setIdleTimeout(?float $timeout): static - { - if (null !== $timeout && $this->outputDisabled) { - throw new LogicException('Idle timeout cannot be set while the output is disabled.'); - } - - $this->idleTimeout = $this->validateTimeout($timeout); - - return $this; - } - - /** - * Enables or disables the TTY mode. - * - * @return $this - * - * @throws RuntimeException In case the TTY mode is not supported - */ - public function setTty(bool $tty): static - { - if ('\\' === \DIRECTORY_SEPARATOR && $tty) { - throw new RuntimeException('TTY mode is not supported on Windows platform.'); - } - - if ($tty && !self::isTtySupported()) { - throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); - } - - $this->tty = $tty; - - return $this; - } - - /** - * Checks if the TTY mode is enabled. - */ - public function isTty(): bool - { - return $this->tty; - } - - /** - * Sets PTY mode. - * - * @return $this - */ - public function setPty(bool $bool): static - { - $this->pty = $bool; - - return $this; - } - - /** - * Returns PTY state. - */ - public function isPty(): bool - { - return $this->pty; - } - - /** - * Gets the working directory. - */ - public function getWorkingDirectory(): ?string - { - if (null === $this->cwd) { - // getcwd() will return false if any one of the parent directories does not have - // the readable or search mode set, even if the current directory does - return getcwd() ?: null; - } - - return $this->cwd; - } - - /** - * Sets the current working directory. - * - * @return $this - */ - public function setWorkingDirectory(string $cwd): static - { - $this->cwd = $cwd; - - return $this; - } - - /** - * Gets the environment variables. - */ - public function getEnv(): array - { - return $this->env; - } - - /** - * Sets the environment variables. - * - * @param array $env The new environment variables - * - * @return $this - */ - public function setEnv(array $env): static - { - $this->env = $env; - - return $this; - } - - /** - * Gets the Process input. - * - * @return resource|string|\Iterator|null - */ - public function getInput() - { - return $this->input; - } - - /** - * Sets the input. - * - * This content will be passed to the underlying process standard input. - * - * @param string|resource|\Traversable|self|null $input The content - * - * @return $this - * - * @throws LogicException In case the process is running - */ - public function setInput(mixed $input): static - { - if ($this->isRunning()) { - throw new LogicException('Input cannot be set while the process is running.'); - } - - $this->input = ProcessUtils::validateInput(__METHOD__, $input); - - return $this; - } - - /** - * Performs a check between the timeout definition and the time the process started. - * - * In case you run a background process (with the start method), you should - * trigger this method regularly to ensure the process timeout - * - * @throws ProcessTimedOutException In case the timeout was reached - */ - public function checkTimeout(): void - { - if (self::STATUS_STARTED !== $this->status) { - return; - } - - if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { - $this->stop(0); - - throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); - } - - if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { - $this->stop(0); - - throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); - } - } - - /** - * @throws LogicException in case process is not started - */ - public function getStartTime(): float - { - if (!$this->isStarted()) { - throw new LogicException('Start time is only available after process start.'); - } - - return $this->starttime; - } - - /** - * Defines options to pass to the underlying proc_open(). - * - * @see https://php.net/proc_open for the options supported by PHP. - * - * Enabling the "create_new_console" option allows a subprocess to continue - * to run after the main process exited, on both Windows and *nix - */ - public function setOptions(array $options): void - { - if ($this->isRunning()) { - throw new RuntimeException('Setting options while the process is running is not possible.'); - } - - $defaultOptions = $this->options; - $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console']; - - foreach ($options as $key => $value) { - if (!\in_array($key, $existingOptions)) { - $this->options = $defaultOptions; - throw new LogicException(\sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); - } - $this->options[$key] = $value; - } - } - - /** - * Defines a list of posix signals that will not be propagated to the process. - * - * @param list<\SIG*> $signals - */ - public function setIgnoredSignals(array $signals): void - { - if ($this->isRunning()) { - throw new RuntimeException('Setting ignored signals while the process is running is not possible.'); - } - - $this->ignoredSignals = $signals; - } - - /** - * Returns whether TTY is supported on the current operating system. - */ - public static function isTtySupported(): bool - { - static $isTtySupported; - - return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty')); - } - - /** - * Returns whether PTY is supported on the current operating system. - */ - public static function isPtySupported(): bool - { - static $result; - - if (null !== $result) { - return $result; - } - - if ('\\' === \DIRECTORY_SEPARATOR) { - return $result = false; - } - - return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes); - } - - /** - * Creates the descriptors needed by the proc_open. - */ - private function getDescriptors(bool $hasCallback): array - { - if ($this->input instanceof \Iterator) { - $this->input->rewind(); - } - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $hasCallback); - } else { - $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $hasCallback); - } - - return $this->processPipes->getDescriptors(); - } - - /** - * Builds up the callback used by wait(). - * - * The callbacks adds all occurred output to the specific buffer and calls - * the user callback (if present) with the received output. - * - * @param callable('out'|'err', string)|null $callback - * - * @return \Closure('out'|'err', string):bool - */ - protected function buildCallback(?callable $callback = null): \Closure - { - if ($this->outputDisabled) { - return fn ($type, $data): bool => null !== $callback && $callback($type, $data); - } - - return function ($type, $data) use ($callback): bool { - match ($type) { - self::OUT => $this->addOutput($data), - self::ERR => $this->addErrorOutput($data), - }; - - return null !== $callback && $callback($type, $data); - }; - } - - /** - * Updates the status of the process, reads pipes. - * - * @param bool $blocking Whether to use a blocking read call - */ - protected function updateStatus(bool $blocking): void - { - if (self::STATUS_STARTED !== $this->status) { - return; - } - - if ($this->processInformation['running'] ?? true) { - $this->processInformation = proc_get_status($this->process); - } - $running = $this->processInformation['running']; - - $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); - - if ($this->fallbackStatus && $this->isSigchildEnabled()) { - $this->processInformation = $this->fallbackStatus + $this->processInformation; - } - - if (!$running) { - $this->close(); - } - } - - /** - * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. - */ - protected function isSigchildEnabled(): bool - { - if (null !== self::$sigchild) { - return self::$sigchild; - } - - if (!\function_exists('phpinfo')) { - return self::$sigchild = false; - } - - ob_start(); - phpinfo(\INFO_GENERAL); - - return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild'); - } - - /** - * Reads pipes for the freshest output. - * - * @param string $caller The name of the method that needs fresh outputs - * @param bool $blocking Whether to use blocking calls or not - * - * @throws LogicException in case output has been disabled or process is not started - */ - private function readPipesForOutput(string $caller, bool $blocking = false): void - { - if ($this->outputDisabled) { - throw new LogicException('Output has been disabled.'); - } - - $this->requireProcessIsStarted($caller); - - $this->updateStatus($blocking); - } - - /** - * Validates and returns the filtered timeout. - * - * @throws InvalidArgumentException if the given timeout is a negative number - */ - private function validateTimeout(?float $timeout): ?float - { - $timeout = (float) $timeout; - - if (0.0 === $timeout) { - $timeout = null; - } elseif ($timeout < 0) { - throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); - } - - return $timeout; - } - - /** - * Reads pipes, executes callback. - * - * @param bool $blocking Whether to use blocking calls or not - * @param bool $close Whether to close file handles or not - */ - private function readPipes(bool $blocking, bool $close): void - { - $result = $this->processPipes->readAndWrite($blocking, $close); - - $callback = $this->callback; - foreach ($result as $type => $data) { - if (3 !== $type) { - $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); - } elseif (!isset($this->fallbackStatus['signaled'])) { - $this->fallbackStatus['exitcode'] = (int) $data; - } - } - } - - /** - * Closes process resource, closes file handles, sets the exitcode. - * - * @return int The exitcode - */ - private function close(): int - { - $this->processPipes->close(); - if ($this->process) { - proc_close($this->process); - $this->process = null; - } - $this->exitcode = $this->processInformation['exitcode']; - $this->status = self::STATUS_TERMINATED; - - if (-1 === $this->exitcode) { - if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { - // if process has been signaled, no exitcode but a valid termsig, apply Unix convention - $this->exitcode = 128 + $this->processInformation['termsig']; - } elseif ($this->isSigchildEnabled()) { - $this->processInformation['signaled'] = true; - $this->processInformation['termsig'] = -1; - } - } - - // Free memory from self-reference callback created by buildCallback - // Doing so in other contexts like __destruct or by garbage collector is ineffective - // Now pipes are closed, so the callback is no longer necessary - $this->callback = null; - - return $this->exitcode; - } - - /** - * Resets data related to the latest run of the process. - */ - private function resetProcessData(): void - { - $this->starttime = null; - $this->callback = null; - $this->exitcode = null; - $this->fallbackStatus = []; - $this->processInformation = []; - $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); - $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); - $this->process = null; - $this->latestSignal = null; - $this->status = self::STATUS_READY; - $this->incrementalOutputOffset = 0; - $this->incrementalErrorOutputOffset = 0; - } - - /** - * Sends a POSIX signal to the process. - * - * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) - * @param bool $throwException Whether to throw exception in case signal failed - * - * @throws LogicException In case the process is not running - * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed - * @throws RuntimeException In case of failure - */ - private function doSignal(int $signal, bool $throwException): bool - { - // Signal seems to be send when sigchild is enable, this allow blocking the signal correctly in this case - if ($this->isSigchildEnabled() && \in_array($signal, $this->ignoredSignals)) { - return false; - } - - if (null === $pid = $this->getPid()) { - if ($throwException) { - throw new LogicException('Cannot send signal on a non running process.'); - } - - return false; - } - - if ('\\' === \DIRECTORY_SEPARATOR) { - exec(\sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); - if ($exitCode && $this->isRunning()) { - if ($throwException) { - throw new RuntimeException(\sprintf('Unable to kill the process (%s).', implode(' ', $output))); - } - - return false; - } - } else { - if (!$this->isSigchildEnabled()) { - $ok = @proc_terminate($this->process, $signal); - } elseif (\function_exists('posix_kill')) { - $ok = @posix_kill($pid, $signal); - } elseif ($ok = proc_open(\sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { - $ok = false === fgets($pipes[2]); - } - if (!$ok) { - if ($throwException) { - throw new RuntimeException(\sprintf('Error while sending signal "%s".', $signal)); - } - - return false; - } - } - - $this->latestSignal = $signal; - $this->fallbackStatus['signaled'] = true; - $this->fallbackStatus['exitcode'] = -1; - $this->fallbackStatus['termsig'] = $this->latestSignal; - - return true; - } - - private function buildShellCommandline(string|array $commandline): string - { - if (\is_string($commandline)) { - return $commandline; - } - - if ('\\' === \DIRECTORY_SEPARATOR && isset($commandline[0][0]) && \strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) { - // On Windows, we don't rely on the OS to find the executable if possible to avoid lookups - // in the current directory which could be untrusted. Instead we use the ExecutableFinder. - $commandline[0] = (self::$executables[$commandline[0]] ??= (new ExecutableFinder())->find($commandline[0])) ?? $commandline[0]; - } - - return implode(' ', array_map($this->escapeArgument(...), $commandline)); - } - - private function prepareWindowsCommandLine(string|array $cmd, array &$env): string - { - $cmd = $this->buildShellCommandline($cmd); - $uid = bin2hex(random_bytes(4)); - $cmd = preg_replace_callback( - '/"(?:( - [^"%!^]*+ - (?: - (?: !LF! | "(?:\^[%!^])?+" ) - [^"%!^]*+ - )++ - ) | [^"]*+ )"/x', - function ($m) use (&$env, $uid) { - static $varCount = 0; - static $varCache = []; - if (!isset($m[1])) { - return $m[0]; - } - if (isset($varCache[$m[0]])) { - return $varCache[$m[0]]; - } - if (str_contains($value = $m[1], "\0")) { - $value = str_replace("\0", '?', $value); - } - if (false === strpbrk($value, "\"%!\n")) { - return '"'.$value.'"'; - } - - $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value); - $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; - $var = $uid.++$varCount; - - $env[$var] = $value; - - return $varCache[$m[0]] = '!'.$var.'!'; - }, - $cmd - ); - - static $comSpec; - - if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { - // Escape according to CommandLineToArgvW rules - $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec).'"'; - } - - $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; - foreach ($this->processPipes->getFiles() as $offset => $filename) { - $cmd .= ' '.$offset.'>"'.$filename.'"'; - } - - return $cmd; - } - - /** - * Ensures the process is running or terminated, throws a LogicException if the process has a not started. - * - * @throws LogicException if the process has not run - */ - private function requireProcessIsStarted(string $functionName): void - { - if (!$this->isStarted()) { - throw new LogicException(\sprintf('Process must be started before calling "%s()".', $functionName)); - } - } - - /** - * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated". - * - * @throws LogicException if the process is not yet terminated - */ - private function requireProcessIsTerminated(string $functionName): void - { - if (!$this->isTerminated()) { - throw new LogicException(\sprintf('Process must be terminated before calling "%s()".', $functionName)); - } - } - - /** - * Escapes a string to be used as a shell argument. - */ - private function escapeArgument(?string $argument): string - { - if ('' === $argument || null === $argument) { - return '""'; - } - if ('\\' !== \DIRECTORY_SEPARATOR) { - return "'".str_replace("'", "'\\''", $argument)."'"; - } - if (str_contains($argument, "\0")) { - $argument = str_replace("\0", '?', $argument); - } - if (!preg_match('/[()%!^"<>&|\s[\]=;*?\'$]/', $argument)) { - return $argument; - } - $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); - - return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; - } - - private function replacePlaceholders(string $commandline, array $env): string - { - return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { - if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { - throw new InvalidArgumentException(\sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); - } - - return $this->escapeArgument($env[$matches[1]]); - }, $commandline); - } - - private function getDefaultEnv(): array - { - $env = getenv(); - $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env; - - return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env); - } -} diff --git a/vendor/symfony/process/ProcessUtils.php b/vendor/symfony/process/ProcessUtils.php deleted file mode 100644 index a2dbde9..0000000 --- a/vendor/symfony/process/ProcessUtils.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process; - -use Symfony\Component\Process\Exception\InvalidArgumentException; - -/** - * ProcessUtils is a bunch of utility methods. - * - * This class contains static methods only and is not meant to be instantiated. - * - * @author Martin Hasoň - */ -class ProcessUtils -{ - /** - * This class should not be instantiated. - */ - private function __construct() - { - } - - /** - * Validates and normalizes a Process input. - * - * @param string $caller The name of method call that validates the input - * @param mixed $input The input to validate - * - * @throws InvalidArgumentException In case the input is not valid - */ - public static function validateInput(string $caller, mixed $input): mixed - { - if (null !== $input) { - if (\is_resource($input)) { - return $input; - } - if (\is_scalar($input)) { - return (string) $input; - } - if ($input instanceof Process) { - return $input->getIterator($input::ITER_SKIP_ERR); - } - if ($input instanceof \Iterator) { - return $input; - } - if ($input instanceof \Traversable) { - return new \IteratorIterator($input); - } - - throw new InvalidArgumentException(\sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); - } - - return $input; - } -} diff --git a/vendor/symfony/process/README.md b/vendor/symfony/process/README.md deleted file mode 100644 index afce5e4..0000000 --- a/vendor/symfony/process/README.md +++ /dev/null @@ -1,13 +0,0 @@ -Process Component -================= - -The Process component executes commands in sub-processes. - -Resources ---------- - - * [Documentation](https://symfony.com/doc/current/components/process.html) - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/process/composer.json b/vendor/symfony/process/composer.json deleted file mode 100644 index dda5575..0000000 --- a/vendor/symfony/process/composer.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "symfony/process", - "type": "library", - "description": "Executes commands in sub-processes", - "keywords": [], - "homepage": "https://symfony.com", - "license": "MIT", - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "require": { - "php": ">=8.2" - }, - "autoload": { - "psr-4": { "Symfony\\Component\\Process\\": "" }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "minimum-stability": "dev" -} From aa7704cea55ab033f3b8a707193d2842e3294162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 20 Mar 2026 13:21:02 +0100 Subject: [PATCH 45/60] Add missing license headers and remove unused files --- library/Pdfexport/ChromeDevTools/ChromeDevTools.php | 3 +++ library/Pdfexport/ChromeDevTools/Command.php | 3 +++ library/Pdfexport/WebDriver/Capabilities.php | 3 +++ library/Pdfexport/WebDriver/Command.php | 3 +++ library/Pdfexport/WebDriver/CommandExecutor.php | 3 +++ library/Pdfexport/WebDriver/CommandInterface.php | 3 +++ library/Pdfexport/WebDriver/CommandName.php | 3 +++ library/Pdfexport/WebDriver/ConditionInterface.php | 3 +++ library/Pdfexport/WebDriver/CustomCommand.php | 5 +++-- library/Pdfexport/WebDriver/ElementPresentCondition.php | 3 +++ library/Pdfexport/WebDriver/Response.php | 3 +++ library/Pdfexport/WebDriver/WebDriver.php | 3 +++ public/css/module.less | 2 -- 13 files changed, 36 insertions(+), 4 deletions(-) delete mode 100644 public/css/module.less diff --git a/library/Pdfexport/ChromeDevTools/ChromeDevTools.php b/library/Pdfexport/ChromeDevTools/ChromeDevTools.php index 20366ef..cde7d88 100644 --- a/library/Pdfexport/ChromeDevTools/ChromeDevTools.php +++ b/library/Pdfexport/ChromeDevTools/ChromeDevTools.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\ChromeDevTools; use Icinga\Module\Pdfexport\WebDriver\CustomCommand; diff --git a/library/Pdfexport/ChromeDevTools/Command.php b/library/Pdfexport/ChromeDevTools/Command.php index 5599800..4d6881a 100644 --- a/library/Pdfexport/ChromeDevTools/Command.php +++ b/library/Pdfexport/ChromeDevTools/Command.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\ChromeDevTools; class Command diff --git a/library/Pdfexport/WebDriver/Capabilities.php b/library/Pdfexport/WebDriver/Capabilities.php index 67dfd61..67f6b10 100644 --- a/library/Pdfexport/WebDriver/Capabilities.php +++ b/library/Pdfexport/WebDriver/Capabilities.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; class Capabilities diff --git a/library/Pdfexport/WebDriver/Command.php b/library/Pdfexport/WebDriver/Command.php index de5c80f..e74d696 100644 --- a/library/Pdfexport/WebDriver/Command.php +++ b/library/Pdfexport/WebDriver/Command.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; class Command implements CommandInterface diff --git a/library/Pdfexport/WebDriver/CommandExecutor.php b/library/Pdfexport/WebDriver/CommandExecutor.php index 20561f4..f28bec3 100644 --- a/library/Pdfexport/WebDriver/CommandExecutor.php +++ b/library/Pdfexport/WebDriver/CommandExecutor.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; use Exception; diff --git a/library/Pdfexport/WebDriver/CommandInterface.php b/library/Pdfexport/WebDriver/CommandInterface.php index 1edad6c..ad19244 100644 --- a/library/Pdfexport/WebDriver/CommandInterface.php +++ b/library/Pdfexport/WebDriver/CommandInterface.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; interface CommandInterface diff --git a/library/Pdfexport/WebDriver/CommandName.php b/library/Pdfexport/WebDriver/CommandName.php index a0190db..43f014d 100644 --- a/library/Pdfexport/WebDriver/CommandName.php +++ b/library/Pdfexport/WebDriver/CommandName.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; enum CommandName: string diff --git a/library/Pdfexport/WebDriver/ConditionInterface.php b/library/Pdfexport/WebDriver/ConditionInterface.php index b990c5f..5156289 100644 --- a/library/Pdfexport/WebDriver/ConditionInterface.php +++ b/library/Pdfexport/WebDriver/ConditionInterface.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; interface ConditionInterface diff --git a/library/Pdfexport/WebDriver/CustomCommand.php b/library/Pdfexport/WebDriver/CustomCommand.php index e4da073..3508540 100644 --- a/library/Pdfexport/WebDriver/CustomCommand.php +++ b/library/Pdfexport/WebDriver/CustomCommand.php @@ -1,8 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later -use Icinga\Module\Pdfexport\WebDriver\CommandInterface; +namespace Icinga\Module\Pdfexport\WebDriver; class CustomCommand implements CommandInterface { diff --git a/library/Pdfexport/WebDriver/ElementPresentCondition.php b/library/Pdfexport/WebDriver/ElementPresentCondition.php index 1f0ee58..d54cc62 100644 --- a/library/Pdfexport/WebDriver/ElementPresentCondition.php +++ b/library/Pdfexport/WebDriver/ElementPresentCondition.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; class ElementPresentCondition implements ConditionInterface diff --git a/library/Pdfexport/WebDriver/Response.php b/library/Pdfexport/WebDriver/Response.php index 5880d86..c984822 100644 --- a/library/Pdfexport/WebDriver/Response.php +++ b/library/Pdfexport/WebDriver/Response.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; class Response diff --git a/library/Pdfexport/WebDriver/WebDriver.php b/library/Pdfexport/WebDriver/WebDriver.php index 59a9962..a47fe7e 100644 --- a/library/Pdfexport/WebDriver/WebDriver.php +++ b/library/Pdfexport/WebDriver/WebDriver.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Module\Pdfexport\WebDriver; use Exception; diff --git a/public/css/module.less b/public/css/module.less deleted file mode 100644 index aad35f8..0000000 --- a/public/css/module.less +++ /dev/null @@ -1,2 +0,0 @@ -/* Icinga PDF Export | (c) 2026 Icinga GmbH | GPLv2 */ - From c5ab073bff5eacb9602dd8c0d3f66e6645412fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 20 Mar 2026 14:01:56 +0100 Subject: [PATCH 46/60] Code review suggestions --- .../ChromeDevTools/ChromeDevTools.php | 4 ++-- library/Pdfexport/ChromeDevTools/Command.php | 16 +++---------- library/Pdfexport/WebDriver/Response.php | 23 ++++--------------- library/Pdfexport/WebDriver/WebDriver.php | 4 ++-- module.info | 2 +- 5 files changed, 12 insertions(+), 37 deletions(-) diff --git a/library/Pdfexport/ChromeDevTools/ChromeDevTools.php b/library/Pdfexport/ChromeDevTools/ChromeDevTools.php index cde7d88..24c9102 100644 --- a/library/Pdfexport/ChromeDevTools/ChromeDevTools.php +++ b/library/Pdfexport/ChromeDevTools/ChromeDevTools.php @@ -18,8 +18,8 @@ public function __construct( public function execute(Command $command): mixed { $params = [ - 'cmd' => $command->getName(), - 'params' => $command->getParameters(), + 'cmd' => $command->name, + 'params' => $command->parameters, ]; $customCommand = new CustomCommand( diff --git a/library/Pdfexport/ChromeDevTools/Command.php b/library/Pdfexport/ChromeDevTools/Command.php index 4d6881a..781003b 100644 --- a/library/Pdfexport/ChromeDevTools/Command.php +++ b/library/Pdfexport/ChromeDevTools/Command.php @@ -5,24 +5,14 @@ namespace Icinga\Module\Pdfexport\ChromeDevTools; -class Command +readonly class Command { public function __construct( - protected string $name, - protected array $parameters = [], + public string $name, + public array $parameters = [], ) { } - public function getName(): string - { - return $this->name; - } - - public function getParameters(): array - { - return $this->parameters; - } - public static function enableConsole(): static { return new static('Console.enable'); diff --git a/library/Pdfexport/WebDriver/Response.php b/library/Pdfexport/WebDriver/Response.php index c984822..5ca8018 100644 --- a/library/Pdfexport/WebDriver/Response.php +++ b/library/Pdfexport/WebDriver/Response.php @@ -5,27 +5,12 @@ namespace Icinga\Module\Pdfexport\WebDriver; -class Response +readonly class Response { public function __construct( - protected string $sessionId, - protected int $status = 0, - protected mixed $value = null, + public string $sessionId, + public int $status = 0, + public mixed $value = null, ) { } - - public function getSessionId(): string - { - return $this->sessionId; - } - - public function getStatus(): int - { - return $this->status; - } - - public function getValue(): mixed - { - return $this->value; - } } diff --git a/library/Pdfexport/WebDriver/WebDriver.php b/library/Pdfexport/WebDriver/WebDriver.php index a47fe7e..6f0440b 100644 --- a/library/Pdfexport/WebDriver/WebDriver.php +++ b/library/Pdfexport/WebDriver/WebDriver.php @@ -35,7 +35,7 @@ public static function create(string $url, Capabilities $capabilities): static $response = $executor->execute(null, $cmd); - return new static($executor, $response->getSessionID()); + return new static($executor, $response->sessionId); } public function execute(CommandInterface $command): mixed @@ -46,7 +46,7 @@ public function execute(CommandInterface $command): mixed $response = $this->executor->execute($this->sessionId, $command); - return $response->getValue(); + return $response->value; } public function wait( diff --git a/module.info b/module.info index 0467d87..3124a9b 100644 --- a/module.info +++ b/module.info @@ -2,4 +2,4 @@ Module: PDF Export Version: 0.13.0 Requires: Libraries: icinga-php-library (>=1.0.0), icinga-php-thirdparty (>=1.0.0) -Description: PDF Export via Google Chrome/Chromium +Description: PDF Export via Google Chrome/WebDriver From 92a0d2b8fbcf326e5711aa9b8c2b1330c7f66c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 23 Mar 2026 09:50:15 +0100 Subject: [PATCH 47/60] Use ConfigForm in IW2 Now requires icinga/icingaweb2#5480 --- application/controllers/ConfigController.php | 3 +- application/forms/BackendConfigForm.php | 4 +- library/Pdfexport/Form/ConfigForm.php | 138 ------------------- library/Pdfexport/Web/ShowConfiguration.php | 90 ------------ 4 files changed, 4 insertions(+), 231 deletions(-) delete mode 100644 library/Pdfexport/Form/ConfigForm.php delete mode 100644 library/Pdfexport/Web/ShowConfiguration.php diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 251176f..b623b4e 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -22,7 +22,8 @@ public function init() public function backendAction() { - $form = new BackendConfigForm(Config::module('pdfexport')); + $form = new BackendConfigForm(); + $form->setConfig(Config::module('pdfexport')); $form->handleRequest($this->getServerRequest()); diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 9186dd1..0f3f06d 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -9,7 +9,7 @@ use Icinga\Module\Pdfexport\Backend\Chromedriver; use Icinga\Module\Pdfexport\Backend\Geckodriver; use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; -use Icinga\Module\Pdfexport\Form\ConfigForm; +use Icinga\Web\Form\ConfigForm; use ipl\Html\Html; use ipl\Validator\CallbackValidator; use ipl\Web\Common\CalloutType; @@ -94,7 +94,7 @@ public function assemble(): void 'A remote chrome instance and it\'s debug interface can be used to create PDFs.', ))); - $this->addElement('text', 'remote_chrome_host', [ + $this->addElement('text', 'remote_chrome__host', [ 'label' => $this->translate('Host'), 'description' => $this->translate('Host address of the server with the running web browser.'), 'validators' => [ diff --git a/library/Pdfexport/Form/ConfigForm.php b/library/Pdfexport/Form/ConfigForm.php deleted file mode 100644 index 9205159..0000000 --- a/library/Pdfexport/Form/ConfigForm.php +++ /dev/null @@ -1,138 +0,0 @@ - -// SPDX-License-Identifier: GPL-3.0-or-later - -namespace Icinga\Module\Pdfexport\Form; - -use Exception; -use Icinga\Application\Config; -use Icinga\Module\Pdfexport\Web\ShowConfiguration; -use ipl\Web\Compat\CompatForm; - -class ConfigForm extends CompatForm -{ - protected array $ignoredElements = []; - - public function __construct( - protected Config $config, - protected ?string $section = null, - ) { - } - - public function ensureAssembled(): static - { - if (! $this->hasBeenAssembled) { - parent::ensureAssembled(); - $this->populateFromConfig(); - } - - return $this; - } - - protected function populateFromConfig(): void - { - foreach ($this->getElements() as $element) { - [$section, $key] = $this->getIniKeyFromName($element->getName()); - if ($section === null && $key === null) { - continue; - } - $value = $this->getPopulatedValue($element->getName()) ?? $this->config->get($section, $key); - $this->populate([ - $element->getName() => $value, - ]); - } - } - - protected function getIniKeyFromName(string $name): ?array - { - if ($this->section !== null) { - return [$this->section, $name]; - } - - $parts = explode('__', $name, 2); - if (count($parts) !== 2) { - return [null, null]; - } - - return $parts; - } - - public function getConfigValue(string $name, $default = null): mixed - { - if (! $this->hasElement($name)) { - return $default; - } - - if (($value = $this->getPopulatedValue($name)) !== null) { - return $value; - } - - [$section, $key] = $this->getIniKeyFromName($name); - if ($section === null && $key === null) { - return $default; - } - - if (! $this->config->hasSection($section)) { - return $default; - } - - return $this->config->get($section, $key, $default); - } - - public function getConfigValues(): array - { - $values = []; - foreach ($this->getElements() as $element) { - if ($element->isIgnored()) { - continue; - } - - $values[$element->getName()] = $this->getConfigValue($element->getName()); - } - - return $values; - } - - protected function onSuccess(): void - { - foreach ($this->getElements() as $element) { - if (in_array($element->getName(), $this->ignoredElements)) { - continue; - } - [$section, $key] = $this->getIniKeyFromName($element->getName()); - if ($section === null || $key === null) { - continue; - } - $value = $this->getConfigValue($element->getName()); - - $configSection = $this->config->getSection($section); - if (empty($value)) { - unset($configSection[$key]); - } else { - $configSection->$key = $value; - } - - if ($configSection->isEmpty()) { - $this->config->removeSection($section); - } else { - $this->config->setSection($section, $configSection); - } - } - - try { - $this->config->saveIni(); - } catch (Exception $e) { - $content = $this->getContent(); - array_unshift( - $content, - new ShowConfiguration( - $this->config->getConfigFile(), - $this->config, - ) - ); - $this->setContent($content); - throw $e; - } - } -} diff --git a/library/Pdfexport/Web/ShowConfiguration.php b/library/Pdfexport/Web/ShowConfiguration.php deleted file mode 100644 index 9eb7ee7..0000000 --- a/library/Pdfexport/Web/ShowConfiguration.php +++ /dev/null @@ -1,90 +0,0 @@ - -// SPDX-License-Identifier: GPL-3.0-or-later - -namespace Icinga\Module\Pdfexport\Web; - -use Icinga\Application\Config; -use ipl\Html\BaseHtmlElement; -use ipl\Html\HtmlElement; -use ipl\Html\HtmlString; -use ipl\I18n\Translation; - -class ShowConfiguration extends BaseHtmlElement -{ - use Translation; - - protected $tag = 'div'; - - public function __construct( - protected string $filePath, - protected Config $config, - ) { - } - - protected function assemble(): void - { - $this->addHtml(HtmlElement::create( - 'h4', - null, - t('Saving Configuration Failed!'), - )); - - $this->addHtml(HtmlElement::create( - 'p', - null, - [ - sprintf( - t("The file %s couldn't be stored."), - $this->filePath, - ), - HtmlString::create('
'), - t('This could have one or more of the following reasons:'), - ], - )); - - $this->addHtml(HtmlElement::create( - 'ul', - null, - [ - HtmlElement::create('li', null, t("You don't have file-system permissions to write to the file")), - HtmlElement::create('li', null, t('Something went wrong while writing the file')), - HtmlElement::create( - 'li', - null, - t("There's an application error preventing you from persisting the configuration"), - ), - ], - )); - - $this->addHtml(HtmlElement::create( - 'p', - null, - [ - t( - 'Details can be found in the application log. ' . - "(If you don't have access to this log, call your administrator in this case)", - ), - HtmlString::create('
'), - t('In case you can access the file by yourself, you can open it and insert the config manually:'), - ], - )); - - $this->addHtml( - HtmlElement::create( - 'p', - null, - HtmlElement::create( - 'pre', - null, - HtmlElement::create( - 'code', - null, - (string) $this->config, - ), - ), - ), - ); - } -} From 085922bb2bf64a6b8d7f65fdcc7d867011826b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 23 Mar 2026 10:07:03 +0100 Subject: [PATCH 48/60] Use 0/1 instead of n/y --- application/forms/BackendConfigForm.php | 6 ++++-- library/Pdfexport/BackendLocator.php | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 0f3f06d..8f9a6db 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -170,8 +170,10 @@ public function assemble(): void ]); $this->addElement('checkbox', 'local_chrome__force_temp_storage', [ - 'label' => $this->translate('Use temp storage'), - 'description' => $this->translate('Use temp storage to transfer the html to the local chrome instance.'), + 'label' => $this->translate('Use temp storage'), + 'description' => $this->translate('Use temp storage to transfer the html to the local chrome instance.'), + 'checkedValue' => '1', + 'uncheckedValue' => '0', ]); $this->addElement('submit', 'submit', [ diff --git a/library/Pdfexport/BackendLocator.php b/library/Pdfexport/BackendLocator.php index d0cd196..a015cb2 100644 --- a/library/Pdfexport/BackendLocator.php +++ b/library/Pdfexport/BackendLocator.php @@ -127,7 +127,6 @@ protected function getSingleBackend($section): ?PfdPrintBackend public static function getForceTempStorage(): bool { - $value = Config::module('pdfexport')->get('chrome', 'force_temp_storage', 'n'); - return in_array($value, ['1', 'y']); + return Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0') === '1'; } } From eef4330543cb009a73bbe6703ae3626a1d16d59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 23 Mar 2026 10:07:36 +0100 Subject: [PATCH 49/60] Move script blocks into separate JavaScript files --- library/Pdfexport/Backend/Chromedriver.php | 44 +++++-------------- .../Backend/HeadlessChromeBackend.php | 32 +++++--------- public/js/activate-scripts.js | 32 ++++++++++++++ public/js/wait-for-layout.js | 19 ++++++++ 4 files changed, 73 insertions(+), 54 deletions(-) create mode 100644 public/js/activate-scripts.js create mode 100644 public/js/wait-for-layout.js diff --git a/library/Pdfexport/Backend/Chromedriver.php b/library/Pdfexport/Backend/Chromedriver.php index d0ff712..c83b14c 100644 --- a/library/Pdfexport/Backend/Chromedriver.php +++ b/library/Pdfexport/Backend/Chromedriver.php @@ -6,6 +6,7 @@ namespace Icinga\Module\Pdfexport\Backend; use Exception; +use Icinga\Application\Icinga; use Icinga\Module\Pdfexport\ChromeDevTools\ChromeDevTools; use Icinga\Module\Pdfexport\ChromeDevTools\Command as DevToolsCommand; use Icinga\Module\Pdfexport\WebDriver\Command; @@ -17,38 +18,6 @@ class Chromedriver extends WebdriverBackend { protected ?ChromeDevTools $dcp = null; - public const ACTIVATE_SCRIPTS = <<getModuleManager()->getModule('pdfexport'); + if (! method_exists($module, 'getJsDir')) { + $jsPath = join(DIRECTORY_SEPARATOR, [$module->getBaseDir(), 'public', 'js']); + } else { + $jsPath = $module->getJsDir(); + } + + $activeScripts = file_get_contents($jsPath . '/activate-scripts.js'); + $this->driver->execute( - Command::executeScript(self::ACTIVATE_SCRIPTS), + Command::executeScript($activeScripts), ); $this->driver->execute( Command::executeScript('new Layout().apply();'), diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index b09ad4c..1d4ec91 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -8,6 +8,7 @@ use Exception; use GuzzleHttp\Client as HttpClient; use GuzzleHttp\Exception\ServerException; +use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Application\Platform; use Icinga\File\Storage\StorageInterface; @@ -47,26 +48,6 @@ class HeadlessChromeBackend implements PfdPrintBackend /** @var string */ public const WAIT_FOR_NETWORK = 'wait-for-network'; - /** @var string Javascript Promise to wait for layout initialization */ - public const WAIT_FOR_LAYOUT = << { - let timeoutId = setTimeout(() => reject('fail'), 10000); - - if (document.documentElement.dataset.layoutReady === 'yes') { - clearTimeout(timeoutId); - fulfill(null); - return; - } - - document.addEventListener('layout-ready', e => { - clearTimeout(timeoutId); - fulfill(e.detail); - }, { - once: true - }); -}) -JS; - protected ?StorageInterface $fileStorage = null; protected bool $useFilesystemTransfer = false; @@ -469,11 +450,20 @@ protected function setContent(PrintableHtmlDocument $document): void 'expression' => 'setTimeout(() => new Layout().apply(), 0)', ]); + $module = Icinga::app()->getModuleManager()->getModule('pdfexport'); + if (! method_exists($module, 'getJsDir')) { + $jsPath = join(DIRECTORY_SEPARATOR, [$module->getBaseDir(), 'public', 'js']); + } else { + $jsPath = $module->getJsDir(); + } + + $waitForLayout = file_get_contents($jsPath . '/wait-for-layout.js'); + $promisedResult = $this->communicate($page, 'Runtime.evaluate', [ 'awaitPromise' => true, 'returnByValue' => true, 'timeout' => 1000, // Failsafe: doesn't apply to `await` it seems - 'expression' => static::WAIT_FOR_LAYOUT, + 'expression' => $waitForLayout, ]); if (isset($promisedResult['exceptionDetails'])) { if (isset($promisedResult['exceptionDetails']['exception']['description'])) { diff --git a/public/js/activate-scripts.js b/public/js/activate-scripts.js new file mode 100644 index 0000000..0f5ad6c --- /dev/null +++ b/public/js/activate-scripts.js @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2026 Icinga GmbH +// SPDX-License-Identifier: GPL-3.0-or-later + +function activateScripts(node) { + if (isScript(node) === true) { + node.parentNode.replaceChild(cloneScript(node) , node); + } else { + var i = -1, children = node.childNodes; + while (++i < children.length) { + activateScripts(children[i]); + } + } + + return node; +} + +function cloneScript(node) { + var script = document.createElement("script"); + script.text = node.innerHTML; + + var i = -1, attrs = node.attributes, attr; + while (++i < attrs.length) { + script.setAttribute((attr = attrs[i]).name, attr.value); + } + return script; +} + +function isScript(node) { + return node.tagName === 'SCRIPT'; +} + +activateScripts(document.documentElement); diff --git a/public/js/wait-for-layout.js b/public/js/wait-for-layout.js new file mode 100644 index 0000000..bf9b6e9 --- /dev/null +++ b/public/js/wait-for-layout.js @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Icinga GmbH +// SPDX-License-Identifier: GPL-3.0-or-later + +new Promise((fulfill, reject) => { + let timeoutId = setTimeout(() => reject('fail'), 10000); + + if (document.documentElement.dataset.layoutReady === 'yes') { + clearTimeout(timeoutId); + fulfill(null); + return; + } + + document.addEventListener('layout-ready', e => { + clearTimeout(timeoutId); + fulfill(e.detail); + }, { + once: true + }); +}) From 5854c673d5442e3327b7b3e06f15026ae378f531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 26 Mar 2026 10:40:04 +0100 Subject: [PATCH 50/60] Update config form to allow for an arbitrary number of backends No longer requires callouts --- application/controllers/ConfigController.php | 92 +++++- application/forms/BackendConfigForm.php | 306 +++++++++---------- configuration.php | 6 +- library/Pdfexport/BackendLocator.php | 17 +- 4 files changed, 254 insertions(+), 167 deletions(-) diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index b623b4e..dec5317 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -6,10 +6,19 @@ namespace Icinga\Module\Pdfexport\Controllers; use Icinga\Application\Config; +use Icinga\Application\Logger; use Icinga\Module\Pdfexport\Forms\BackendConfigForm; +use Icinga\Web\Form\ConfigForm; +use Icinga\Web\Notification; +use ipl\Html\Attributes; +use ipl\Html\Form; use ipl\Html\HtmlString; +use ipl\Html\Table; use ipl\Web\Compat\CompatController; use Icinga\Web\Widget\Tabs; +use ipl\Web\Widget\ButtonLink; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; class ConfigController extends CompatController { @@ -20,17 +29,96 @@ public function init() parent::init(); } - public function backendAction() + public function backendsAction(): void { + $button = new ButtonLink( + $this->translate('Create a New Backend'), + 'pdfexport/config/createbackend', + 'plus', + ['title' => $this->translate('Create a New Backend')], + ); + $button->setBaseTarget('_next'); + $this->addContent($button); + + $table = new Table(); + $table->setAttributes(Attributes::create([ + 'class' => 'table-row-selectable common-table', + 'data-base-target' => '_next', + ])); + $table->add(Table::tr([ + Table::th($this->translate('Backend')), + Table::th($this->translate('Priority')), + ])); + + $config = Config::module('pdfexport'); + + $sections = []; + foreach ($config as $name => $data) { + $sections[] = [$name, $data, (int) $data->get('priority')]; + } + + usort($sections, function ($a, $b) { + return $a[2] <=> $b[2]; + }); + + foreach ($sections as [$name, $data]) { + $table->add(Table::tr([ + Table::td([ + new Icon('print'), + new Link($name, 'pdfexport/config/backend?backend=' . $name), + ]), + Table::td($data->get('priority')), + ], [ + 'class' => 'clickable', + ])); + } + + $this->mergeTabs($this->Module()->getConfigTabs()->activate('backends')); + $this->addContent($table); + } + + public function backendAction(): void + { + $name = $this->params->shiftRequired('backend'); + $this->addTitleTab($this->translate(sprintf('Edit %s', $name))); + $form = new BackendConfigForm(); $form->setConfig(Config::module('pdfexport')); + $form->setSection($name); + + $form->on(Form::ON_SUBMIT, function () use ($form) { + Notification::success($this->translate('Updated print backend')); + $this->redirectNow('__CLOSE__'); + }); + + $form->on(ConfigForm::ON_DELETE, function () use ($form) { + Notification::success($this->translate('Print backend deleted')); + $this->redirectNow('__CLOSE__'); + }); $form->handleRequest($this->getServerRequest()); - $this->mergeTabs($this->Module()->getConfigTabs()->activate('backend')); $this->addContent(HtmlString::create($form->render())); } + public function createbackendAction(): void + { + $this->addTitleTab($this->translate(sprintf('Create Print Backend'))); + + $form = new BackendConfigForm(); + $form->setConfig(Config::module('pdfexport')); + $form->setIsCreateForm(true); + + $form->on(Form::ON_SUBMIT, function () { + Notification::success($this->translate('Created new print backend')); + $this->redirectNow('__CLOSE__'); + }); + + $form->handleRequest($this->getServerRequest()); + + $this->addContent($form); + } + protected function mergeTabs(Tabs $tabs): void { foreach ($tabs->getTabs() as $tab) { diff --git a/application/forms/BackendConfigForm.php b/application/forms/BackendConfigForm.php index 8f9a6db..8685c79 100644 --- a/application/forms/BackendConfigForm.php +++ b/application/forms/BackendConfigForm.php @@ -10,174 +10,164 @@ use Icinga\Module\Pdfexport\Backend\Geckodriver; use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; use Icinga\Web\Form\ConfigForm; -use ipl\Html\Html; use ipl\Validator\CallbackValidator; -use ipl\Web\Common\CalloutType; -use ipl\Web\Widget\Callout; class BackendConfigForm extends ConfigForm { public function assemble(): void { - $this->addHtml(new Callout( - CalloutType::Info, - t( - 'Backends are chosen in the order of this from.' - . ' Backends that are not configured are skipped and ones further down the list act as a fallback.', - ), - t('Info: Backend precedence'), - )); - - $this->add(Html::tag('h2', t("WebDriver"))); - $this->add(Html::tag('p', t( - 'WebDriver is a API that allows software to automatically control and interact with a web browser, ' . - 'commonly used for automating website testing through tools like Selenium WebDriver.', - ))); - - $this->addElement('text', 'webdriver__host', [ - 'label' => $this->translate('Host'), - 'description' => $this->translate('Host address of the webdriver server'), - 'validators' => [ - new CallbackValidator(function ($value, CallbackValidator $validator) { - if ($value === null) { - return true; - } - - $port = $this->getValue('webdriver__port') ?: 4444; - $type = $this->getValue('webdriver__type') ?: 'chrome'; - - try { - $url = "$value:$port"; - $backend = match ($type) { - 'chrome' => new Chromedriver($url), - 'firefox' => new Geckodriver($url), - default => throw new Exception("Invalid webdriver type $type"), - }; - - if (! $backend->isSupported()) { - $validator->addMessage( - t('The webdriver server reports that it is unable to generate PDFs'), - ); - return false; - } - } catch (Exception $e) { - $validator->addMessage($e->getMessage()); - return false; - } - return true; - }), - ], - ]); - - $this->addElement('number', 'webdriver__port', [ - 'label' => $this->translate('Port'), - 'description' => $this->translate('Port of the webdriver instance. (Default: 4444)'), - 'placeholder' => 4444, - 'min' => 1, - 'max' => 65535, + $this->addSectionNameElement(); + + $this->addElement('number', 'priority', [ + 'label' => $this->translate('Priority'), + 'required' => true, + 'placeholder' => 100, + 'min' => 0, + 'description' => $this->translate('The priority of the backend. A lower priority will be used first.'), ]); - $this->addElement('select', 'webdriver__type', [ - 'label' => $this->translate('Type'), - 'description' => $this->translate('The type of webdriver server.'), - 'multiOptions' => array_merge( - ['' => sprintf(' - %s - ', t('Please choose'))], - [ - 'firefox' => t('Firefox'), - 'chrome' => t('Chrome'), - ], - ), - ]); - - $this->add(Html::tag('h2', t("Remote Chrome"))); - $this->add(Html::tag('p', t( - 'A remote chrome instance and it\'s debug interface can be used to create PDFs.', - ))); - - $this->addElement('text', 'remote_chrome__host', [ - 'label' => $this->translate('Host'), - 'description' => $this->translate('Host address of the server with the running web browser.'), - 'validators' => [ - new CallbackValidator(function ($value, CallbackValidator $validator) { - if ($value === null) { - return true; - } - - $port = $this->getValue('remote_chrome__port') ?: 9222; - - try { - $chrome = HeadlessChromeBackend::createRemote($value, $port); - $version = $chrome->getVersion(); - } catch (Exception $e) { - $validator->addMessage($e->getMessage()); - return false; - } - - if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { - $validator->addMessage(t( - 'Chrome/Chromium supporting headless mode required' - . ' which is provided since version %s. Version detected: %s', - )); - return false; - } - - return true; - }), + $this->addElement('select', 'type', [ + 'label' => $this->translate('Type'), + 'multiOptions' => [ + '' => sprintf(' - %s - ', t('Please choose')), + 'chrome_webdriver' => t('Chrome WebDriver'), + 'firefox_webdriver' => t('Firefox WebDriver'), + 'remote_chrome' => t('Headless Chrome (Remote)'), + 'local_chrome' => t('Headless Chrome (Local)'), ], + 'required' => true, + 'class' => 'autosubmit', ]); - $this->addElement('number', 'remote_chrome__port', [ - 'label' => $this->translate('Port'), - 'description' => $this->translate('Port of the chrome developer tools. (Default: 9222)'), - 'placeholder' => 9222, - 'min' => 1, - 'max' => 65535, - ]); - - $this->add(Html::tag('h2', t("Local Chrome"))); - $this->add(Html::tag('p', t( - 'Start a chrome instance on the same server as icingaweb2. This is always attempted as a fallback.', - ))); - - $this->addElement('text', 'local_chrome__binary', [ - 'label' => $this->translate('Binary'), - 'placeholder' => '/usr/bin/google-chrome', - 'description' => $this->translate('Path to the binary of the web browser.'), - 'validators' => [ - new CallbackValidator(function ($value, CallbackValidator $validator) { - if (empty($value)) { - return true; - } - - try { - $chrome = (HeadlessChromeBackend::createLocal($value)); - $version = $chrome->getVersion(); - } catch (Exception $e) { - $validator->addMessage($e->getMessage()); - return false; - } - - if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { - $validator->addMessage(t( - 'Chrome/Chromium supporting headless mode required' - . ' which is provided since version %s. Version detected: %s', - )); - } - - return true; - }), - ], - ]); - - $this->addElement('checkbox', 'local_chrome__force_temp_storage', [ - 'label' => $this->translate('Use temp storage'), - 'description' => $this->translate('Use temp storage to transfer the html to the local chrome instance.'), - 'checkedValue' => '1', - 'uncheckedValue' => '0', - ]); - - $this->addElement('submit', 'submit', [ - 'label' => $this->translate('Store'), - ]); + $type = $this->getPopulatedValue('type') ?? $this->getConfigValue('type'); + + switch ($type) { + case 'remote_chrome': + $this->addElement('text', 'host', [ + 'label' => $this->translate('Host'), + 'description' => $this->translate('Host address of the server with the running web browser.'), + 'required' => true, + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { + $port = $this->getValue('port') ?: 9222; + + try { + $chrome = HeadlessChromeBackend::createRemote($value, $port); + $version = $chrome->getVersion(); + } catch (Exception $e) { + $validator->addMessage($e->getMessage()); + return false; + } + + if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { + $validator->addMessage(t( + 'Chrome/Chromium supporting headless mode required' + . ' which is provided since version %s. Version detected: %s', + )); + return false; + } + + return true; + }), + ], + ]); + + $this->addElement('number', 'port', [ + 'label' => $this->translate('Port'), + 'description' => $this->translate('Port of the chrome developer tools. (Default: 9222)'), + 'placeholder' => 9222, + 'min' => 1, + 'max' => 65535, + ]); + + break; + + case 'local_chrome': + $this->addElement('text', 'binary', [ + 'label' => $this->translate('Binary'), + 'placeholder' => '/usr/bin/google-chrome', + 'description' => $this->translate('Path to the binary of the web browser.'), + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { + if (empty($value)) { + return true; + } + + try { + $chrome = (HeadlessChromeBackend::createLocal($value)); + $version = $chrome->getVersion(); + } catch (Exception $e) { + $validator->addMessage($e->getMessage()); + return false; + } + + if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) { + $validator->addMessage(t( + 'Chrome/Chromium supporting headless mode required' + . ' which is provided since version %s. Version detected: %s', + )); + } + + return true; + }), + ], + ]); + + $this->addElement('checkbox', 'force_temp_storage', [ + 'label' => $this->translate('Use temp storage'), + 'description' => $this->translate( + 'Use temp storage to transfer the html to the local chrome instance.' + ), + 'checkedValue' => '1', + 'uncheckedValue' => '0', + ]); + + break; + + case 'firefox_webdriver': + case 'chrome_webdriver': + $this->addElement('text', 'host', [ + 'label' => $this->translate('Host'), + 'description' => $this->translate('Host address of the webdriver server'), + 'required' => true, + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) use ($type) { + $port = $this->getValue('port') ?: 4444; + + try { + $url = "$value:$port"; + $backend = match ($type) { + 'chrome_webdriver' => new Chromedriver($url), + 'firefox_webdriver' => new Geckodriver($url), + default => throw new Exception("Invalid webdriver type $type"), + }; + + if (! $backend->isSupported()) { + $validator->addMessage( + t('The webdriver server reports that it is unable to generate PDFs'), + ); + return false; + } + } catch (Exception $e) { + $validator->addMessage($e->getMessage()); + return false; + } + return true; + }), + ], + ]); + + $this->addElement('number', 'port', [ + 'label' => $this->translate('Port'), + 'description' => $this->translate('Port of the webdriver instance. (Default: 4444)'), + 'placeholder' => 4444, + 'min' => 1, + 'max' => 65535, + ]); + + break; + } + + $this->addButtonElements(); } } diff --git a/configuration.php b/configuration.php index 897fbda..5997cbd 100644 --- a/configuration.php +++ b/configuration.php @@ -5,8 +5,8 @@ /** @var \Icinga\Application\Modules\Module $this */ -$this->provideConfigTab('backend', array( +$this->provideConfigTab('backends', array( 'title' => $this->translate('Configure the Chrome/WebDriver connection'), - 'label' => $this->translate('Backend'), - 'url' => 'config/backend' + 'label' => $this->translate('Backends'), + 'url' => 'config/backends' )); diff --git a/library/Pdfexport/BackendLocator.php b/library/Pdfexport/BackendLocator.php index a015cb2..2c06c40 100644 --- a/library/Pdfexport/BackendLocator.php +++ b/library/Pdfexport/BackendLocator.php @@ -17,7 +17,15 @@ class BackendLocator { public function getFirstSupportedBackend(): ?PfdPrintBackend { - foreach (['webdriver', 'remote_chrome', 'local_chrome'] as $section) { + $sorted = []; + foreach (Config::module('pdfexport') as $section => $configs) { + $priority = (int) $configs->get('priority', 100); + $sorted[$section] = $priority; + } + + asort($sorted); + + foreach ($sorted as $section => $priority) { $backend = $this->getSingleBackend($section); if ($backend === null) { continue; @@ -40,8 +48,8 @@ protected function connectToWebDriver(string $section): ?PfdPrintBackend $url = "$host:$port"; $type = $config->get($section, 'type'); $backend = match ($type) { - 'chrome' => new Chromedriver($url), - 'firefox' => new Geckodriver($url), + 'chrome_webdriver' => new Chromedriver($url), + 'firefox_webdriver' => new Geckodriver($url), default => throw new Exception("Invalid webdriver type $type"), }; Logger::info("Connected WebDriver Backend: $section"); @@ -106,7 +114,8 @@ protected function getSingleBackend($section): ?PfdPrintBackend Logger::info("Connecting to backend $section."); - $backend = match ($section) { + $type = $config->get($section, 'type'); + $backend = match ($type) { 'local_chrome' => $this->connectToLocalChrome($section), 'remote_chrome' => $this->connectToRemoteChrome($section), default => $this->connectToWebDriver($section), From 1dc32179e27a903bb237f9cc8bd4e2df616474ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 26 Mar 2026 12:12:55 +0100 Subject: [PATCH 51/60] Document webdriver installation --- doc/02-Installation.md | 71 +------------------------------ doc/03-Configuration.md | 93 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 69 deletions(-) create mode 100644 doc/03-Configuration.md diff --git a/doc/02-Installation.md b/doc/02-Installation.md index 733c63a..24da547 100644 --- a/doc/02-Installation.md +++ b/doc/02-Installation.md @@ -8,79 +8,12 @@ * [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) ≥ 1.0.0 * [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) ≥ 1.0.0 -## Google Chrome/Chromium Setup - -The module needs Google Chrome or Chromium supporting headless mode. - -### RHEL/CentOS - -Add the Chrome repository from Google to yum, next to EPEL. - -``` -yum -y install epel-release - -cat >/etc/yum.repos.d/google-chrome-stable.repo < - -Add the Chrome repository from Google to apt. - -``` -apt-get -y install apt-transport-https gnupg wget - -wget -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - - -echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list - -apt-get update -``` - -Install Chrome. - -``` -apt-get install google-chrome-stable -``` - ## Module Installation 1. Install it [like any other module](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation). Use `pdfexport` as name. -2. You might need to set the absolute path to the Google Chrome / Chromium -binary, depending on your system. This can be done in -`Configuration -> Modules -> pdfexport -> Chrome` +2. You will need to install and configure at leaast one PDF export backend. +See more about it in the [Configuration](03-Configuration.md) section. This concludes the installation. PDF exports now use Google Chrome/Chromium for rendering. - -### Using a Remote Chrome/Chromium - -As an alternative to a local installation of Chrome/Chromium it is also possible -to launch and utilize a remote instance. - -Just install it as described above on a different machine and configure its connection -details in `Configuration -> Modules -> pdfexport -> Chrome`. - -To start a remote instance of Chrome/Chromium use the following commandline options: - -> google-chrome --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --headless --keep-alive-for-test --disable-gpu --disable-dev-shm-usage --no-sandbox --bwsi --no-first-run --user-data-dir=/tmp --homedir=/tmp - -Note that the browser does accept any and all connection attempts without any authentication. -Keep that in mind and let it listen on a public IP (or even on 0.0.0.0) only during tests or -with a proper firewall in place. diff --git a/doc/03-Configuration.md b/doc/03-Configuration.md new file mode 100644 index 0000000..c57a6e8 --- /dev/null +++ b/doc/03-Configuration.md @@ -0,0 +1,93 @@ +# Configuration + +The module needs to have at least one backend configured. +All module configuration can be done in `Configuration -> Modules -> pdfexport`. + +The priority of the backends is determined by their order in the list. +The first backend (lowest priority number) that is able to handle the request is used. + +## Using a Local Chrome/Chromium + +The module needs Google Chrome or Chromium supporting headless mode. + +### RHEL/CentOS + +Add the Chrome repository from Google to yum, next to EPEL. + +``` +yum -y install epel-release + +cat >/etc/yum.repos.d/google-chrome-stable.repo < + +Add the Chrome repository from Google to apt. + +``` +apt-get -y install apt-transport-https gnupg wget + +wget -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - + +echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list + +apt-get update +``` + +Install Chrome. + +``` +apt-get install google-chrome-stable +``` + +## Using a Remote Chrome/Chromium + +As an alternative to a local installation of Chrome/Chromium it is also possible +to launch and use a remote instance. + +Install it as described above on a different machine. + +To start a remote instance of Chrome/Chromium use the following commandline options: + +> google-chrome --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --headless --keep-alive-for-test --disable-gpu --disable-dev-shm-usage --no-sandbox --bwsi --no-first-run --user-data-dir=/tmp --homedir=/tmp + +Note that the browser does accept any connection attempt without any authentication. +Keep that in mind and let it listen on a public IP (or even on 0.0.0.0) only during tests or +with a proper firewall in place. + +Create a new backend in `Configuration -> Modules -> pdfexport -> Backends`, +give it a name and select `Headless Chrome (Remote)` as the backend type and configure its connection details. + +## Using a Chrome WebDriver Server + +Install your preferred Chrome WebDriver/Chromedriver server. +See [Chromedriver Downloads Page](https://developer.chrome.com/docs/chromedriver/downloads) +for instructions to download and install it. + +Create a new backend in `Configuration -> Modules -> pdfexport -> Backends`, +give it a name and select `Chrome WebDriver` as the backend type and configure its connection details. + +## Using a Firefox WebDriver Server + +Install your preferred Firefox WebDriver/Geckodriver server. +Check the [Geckodriver Releases Page](https://github.com/mozilla/geckodriver/releases) +for the latest version. + +Create a new backend in `Configuration -> Modules -> pdfexport -> Backends`, +give it a name and select `Firefox WebDriver` as the backend type and configure its connection details. From 322bbeb6ae7f0808bc8037047f9b514d19e7a7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 7 Apr 2026 13:17:49 +0200 Subject: [PATCH 52/60] Add docstrings --- library/Pdfexport/BackendLocator.php | 43 ++++++++++++--- library/Pdfexport/WebDriver/Capabilities.php | 24 +++++++++ library/Pdfexport/WebDriver/Command.php | 45 ++++++++++++++++ .../Pdfexport/WebDriver/CommandExecutor.php | 18 +++++++ .../Pdfexport/WebDriver/CommandInterface.php | 16 ++++++ library/Pdfexport/WebDriver/CommandName.php | 52 +++++++++++++++++++ .../WebDriver/ConditionInterface.php | 11 +++- library/Pdfexport/WebDriver/CustomCommand.php | 10 ++++ .../WebDriver/ElementPresentCondition.php | 2 +- library/Pdfexport/WebDriver/Response.php | 9 ++++ library/Pdfexport/WebDriver/WebDriver.php | 43 +++++++++++++++ 11 files changed, 264 insertions(+), 9 deletions(-) diff --git a/library/Pdfexport/BackendLocator.php b/library/Pdfexport/BackendLocator.php index 2c06c40..c180ea1 100644 --- a/library/Pdfexport/BackendLocator.php +++ b/library/Pdfexport/BackendLocator.php @@ -13,8 +13,16 @@ use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend; use Icinga\Module\Pdfexport\Backend\PfdPrintBackend; +/** + * Class implementing the traversal and fallback logic for the PDF backend selection. + */ class BackendLocator { + /** + * Get the first supported backend from the configuration which responded with a successful connection. + * First, in this context means the backend with the lowest priority. + * @return PfdPrintBackend|null the first supported backend or null if none is available + */ public function getFirstSupportedBackend(): ?PfdPrintBackend { $sorted = []; @@ -36,6 +44,13 @@ public function getFirstSupportedBackend(): ?PfdPrintBackend return null; } + /** + * Create and connect to a WebDriver backend. + * The backend is identified by the 'type' configuration option. + * @param string $section The configuration section to use for the backend. + * + * @return PfdPrintBackend|null The created and connected backend or null if the backend could not be created. + */ protected function connectToWebDriver(string $section): ?PfdPrintBackend { $config = Config::module('pdfexport'); @@ -60,6 +75,12 @@ protected function connectToWebDriver(string $section): ?PfdPrintBackend return null; } + /** + * Connect to a remote HeadlessChrome backend. + * @param string $section the configuration section to use for the backend + * + * @return PfdPrintBackend|null the created and connected backend or null if the backend could not be created + */ protected function connectToRemoteChrome(string $section): ?PfdPrintBackend { $config = Config::module('pdfexport'); @@ -83,6 +104,12 @@ protected function connectToRemoteChrome(string $section): ?PfdPrintBackend return null; } + /** + * Connect to a local HeadlessChrome backend. + * @param string $section the configuration section to use for the backend + * + * @return PfdPrintBackend|null the created and connected backend or null if the backend could not be created + */ protected function connectToLocalChrome(string $section): ?PfdPrintBackend { $config = Config::module('pdfexport'); @@ -93,7 +120,7 @@ protected function connectToLocalChrome(string $section): ?PfdPrintBackend } $backend = HeadlessChromeBackend::createLocal( $binary, - $this->getForceTempStorage(), + Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0') === '1', ); Logger::info("Connected WebDriver Backend: $section"); return $backend; @@ -105,7 +132,14 @@ protected function connectToLocalChrome(string $section): ?PfdPrintBackend return null; } - protected function getSingleBackend($section): ?PfdPrintBackend + /** + * Create and connect to a single backend. + * The type of the backend is determined by the 'type' configuration option. + * @param $section string the configuration section to use for the backend + * + * @return PfdPrintBackend|null the created and connected backend or null if the backend could not be created + */ + protected function getSingleBackend(string $section): ?PfdPrintBackend { $config = Config::module('pdfexport'); if (! $config->hasSection($section)) { @@ -133,9 +167,4 @@ protected function getSingleBackend($section): ?PfdPrintBackend return $backend; } - - public static function getForceTempStorage(): bool - { - return Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0') === '1'; - } } diff --git a/library/Pdfexport/WebDriver/Capabilities.php b/library/Pdfexport/WebDriver/Capabilities.php index 67f6b10..30f2be5 100644 --- a/library/Pdfexport/WebDriver/Capabilities.php +++ b/library/Pdfexport/WebDriver/Capabilities.php @@ -5,6 +5,10 @@ namespace Icinga\Module\Pdfexport\WebDriver; +/** + * A container for WebDriver capabilities. + * @link https://www.w3.org/TR/webdriver/#capabilities + */ class Capabilities { /** @var array */ @@ -14,11 +18,19 @@ class Capabilities 'acceptSslCerts' => 'acceptInsecureCerts', ]; + /** + * Construct a new Capabilities set. + * @param array $capabilities The capabilities to set. + */ public function __construct( protected array $capabilities = [], ) { } + /** + * Create a new Capabilities set with the default capabilities for Chrome. + * @return static + */ public static function chrome(): static { return new static([ @@ -27,6 +39,10 @@ public static function chrome(): static ]); } + /** + * Create a new Capabilities set with the default capabilities for Firefox. + * @return static + */ public static function firefox(): static { return new static([ @@ -41,6 +57,10 @@ public static function firefox(): static ]); } + /** + * Convert the capabilities to a W3C-compatible array. + * @return array + */ public function toW3cCompatibleArray(): array { $allowedW3cCapabilities = [ @@ -98,6 +118,10 @@ public function toW3cCompatibleArray(): array return $w3cCapabilities; } + /** + * Get the raw capabilities array. This is not necessarily W3C-compatible. + * @return array + */ public function toArray(): array { return $this->capabilities; diff --git a/library/Pdfexport/WebDriver/Command.php b/library/Pdfexport/WebDriver/Command.php index e74d696..603d5c1 100644 --- a/library/Pdfexport/WebDriver/Command.php +++ b/library/Pdfexport/WebDriver/Command.php @@ -5,14 +5,29 @@ namespace Icinga\Module\Pdfexport\WebDriver; +/** + * Wrapper class for standard WebDriver commands. + */ class Command implements CommandInterface { + /** + * Create a new command. + * @param CommandName $name the standardized command instance. + * @param array $parameters the parameters for the command + */ public function __construct( protected CommandName $name, protected array $parameters = [], ) { } + /** + * Convenience method to create a new ExecuteScript command. + * @param string $script the raw JavaScript to execute in the browser + * @param array $arguments the arguments to pass to the script + * + * @return static + */ public static function executeScript(string $script, array $arguments = []): static { $params = [ @@ -23,11 +38,23 @@ public static function executeScript(string $script, array $arguments = []): sta return new static(CommandName::ExecuteScript, $params); } + /** + * Convenience method to create a new GetPageSource command. + * This command is useful for debugging purposes and will return the entire HTML source of the current page. + * @return static + */ public static function getPageSource(): static { return new static(CommandName::GetPageSource); } + /** + * Find an element on the page using the specified method and value. + * @param string $method + * @param string $value + * + * @return static + */ public static function findElement(string $method, string $value): static { return new static(CommandName::FindElement, [ @@ -36,6 +63,13 @@ public static function findElement(string $method, string $value): static ]); } + /** + * Format script arguments for use in ExecuteScript commands. + * This method recursively converts nested arrays into JSON objects. + * @param array $arguments + * + * @return array + */ protected static function prepareScriptArguments(array $arguments): array { $args = []; @@ -50,6 +84,13 @@ protected static function prepareScriptArguments(array $arguments): array return $args; } + /** + * Print a page using the specified print parameters. + * The result of this command is a PDF file that is base64-encoded. + * @param array $printParameters + * + * @return static + */ public static function printPage(array $printParameters): static { return new static(CommandName::PrintPage, $printParameters); @@ -70,6 +111,10 @@ public function getParameters(): array return $this->parameters; } + /** + * Return the underlying standardized command name. + * @return CommandName + */ public function getName(): CommandName { return $this->name; diff --git a/library/Pdfexport/WebDriver/CommandExecutor.php b/library/Pdfexport/WebDriver/CommandExecutor.php index f28bec3..98ffdd8 100644 --- a/library/Pdfexport/WebDriver/CommandExecutor.php +++ b/library/Pdfexport/WebDriver/CommandExecutor.php @@ -7,9 +7,13 @@ use Exception; use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; use Icinga\Application\Logger; use RuntimeException; +/** + * An abstraction layer for executing WebDriver commands. + */ class CommandExecutor { protected const DEFAULT_HEADERS = [ @@ -19,6 +23,11 @@ class CommandExecutor protected ?Client $client = null; + /** + * Construct a new CommandExecutor given a base URL + * @param string $url the base URL to use for executing commands + * @param float|null $timeout the timeout in seconds for each command + */ public function __construct( protected string $url, protected ?float $timeout = 10, @@ -26,6 +35,15 @@ public function __construct( $this->client = new Client(); } + /** + * Execute a WebDriver command. + * @param string|null $sessionId the session ID to use for the command, required for each command except + * `newSession` + * @param CommandInterface $command the command to execute + * + * @return Response + * @throws GuzzleException + */ public function execute(?string $sessionId, CommandInterface $command): Response { $method = $command->getMethod(); diff --git a/library/Pdfexport/WebDriver/CommandInterface.php b/library/Pdfexport/WebDriver/CommandInterface.php index ad19244..f3a1422 100644 --- a/library/Pdfexport/WebDriver/CommandInterface.php +++ b/library/Pdfexport/WebDriver/CommandInterface.php @@ -5,11 +5,27 @@ namespace Icinga\Module\Pdfexport\WebDriver; +/** + * Interface for WebDriver commands. + * @link https://www.w3.org/TR/webdriver/#commands + */ interface CommandInterface { + /** + * Get the path to the endpoint of the command. + * @return string + */ public function getPath(): string; + /** + * The HTTP method to use for the command. + * @return string + */ public function getMethod(): string; + /** + * Get the command parameters. + * @return array + */ public function getParameters(): array; } diff --git a/library/Pdfexport/WebDriver/CommandName.php b/library/Pdfexport/WebDriver/CommandName.php index 43f014d..29d62fd 100644 --- a/library/Pdfexport/WebDriver/CommandName.php +++ b/library/Pdfexport/WebDriver/CommandName.php @@ -5,17 +5,65 @@ namespace Icinga\Module\Pdfexport\WebDriver; +/** + * Enum containing the implemented WebDriver commands. + * @link https://www.w3.org/TR/webdriver/#commands + */ enum CommandName: string { + /** + * Create a new session. + * @link https://www.w3.org/TR/webdriver/#dfn-new-sessions + */ case NewSession = 'newSession'; + + /** + * The the current status of the webdriver server. + * @link https://www.w3.org/TR/webdriver/#dfn-status + */ case Status = 'status'; + + /** + * Close the window + * @link https://www.w3.org/TR/webdriver/#dfn-close-window + */ case Close = 'close'; + + /** + * Close the webdriver session. + * Implicitly closes the current window. + * @link https://www.w3.org/TR/webdriver/#dfn-quit + */ case Quit = 'quit'; + + /** + * Execute JavaScript in the context of the currently selected frame or window. + * @link https://www.w3.org/TR/webdriver/#dfn-execute-script + */ case ExecuteScript = 'executeScript'; + + /** + * Get the source of the current page. + * @link https://www.w3.org/TR/webdriver/#dfn-get-page-source + */ case GetPageSource = 'getPageSource'; + + /** + * Print the current page. + * @link https://www.w3.org/TR/webdriver/#dfn-print-page + */ case PrintPage = 'printPage'; + + /** + * Find an element on the page. + * @link https://www.w3.org/TR/webdriver/#dfn-find-element + */ case FindElement = 'findElement'; + /** + * Get the path to the endpoint of the command. + * @return string + */ public function getPath(): string { return match ($this) { @@ -30,6 +78,10 @@ public function getPath(): string }; } + /** + * Get the HTTP method of the command. + * @return string + */ public function getMethod(): string { return match ($this) { diff --git a/library/Pdfexport/WebDriver/ConditionInterface.php b/library/Pdfexport/WebDriver/ConditionInterface.php index 5156289..233f05c 100644 --- a/library/Pdfexport/WebDriver/ConditionInterface.php +++ b/library/Pdfexport/WebDriver/ConditionInterface.php @@ -5,7 +5,16 @@ namespace Icinga\Module\Pdfexport\WebDriver; +/** + * Interface for conditions that can be used for searching elements in the browsers DOM. + */ interface ConditionInterface { - public function apply(WebDriver $driver): mixed; + /** + * Apply the condition to the given WebDriver instance and return whether the condition is met. + * @param WebDriver $driver the WebDriver instance to apply the condition to + * + * @return bool + */ + public function apply(WebDriver $driver): bool; } diff --git a/library/Pdfexport/WebDriver/CustomCommand.php b/library/Pdfexport/WebDriver/CustomCommand.php index 3508540..7cbd474 100644 --- a/library/Pdfexport/WebDriver/CustomCommand.php +++ b/library/Pdfexport/WebDriver/CustomCommand.php @@ -5,8 +5,18 @@ namespace Icinga\Module\Pdfexport\WebDriver; +/** + * A custom WebDriver command that allows sending arbitrary requests to the browser. + * These commands are not part of the WebDriver protocol and are not covered by the official documentation. + */ class CustomCommand implements CommandInterface { + /** + * Create a new custom command. + * @param string $method + * @param string $path + * @param array $parameters + */ public function __construct( protected string $method, protected string $path, diff --git a/library/Pdfexport/WebDriver/ElementPresentCondition.php b/library/Pdfexport/WebDriver/ElementPresentCondition.php index d54cc62..0c8ffb5 100644 --- a/library/Pdfexport/WebDriver/ElementPresentCondition.php +++ b/library/Pdfexport/WebDriver/ElementPresentCondition.php @@ -15,7 +15,7 @@ protected function __construct( ) { } - public function apply(WebDriver $driver): mixed + public function apply(WebDriver $driver): bool { $response = $driver->execute( Command::findElement($this->mechanism, $this->value), diff --git a/library/Pdfexport/WebDriver/Response.php b/library/Pdfexport/WebDriver/Response.php index 5ca8018..31ddc29 100644 --- a/library/Pdfexport/WebDriver/Response.php +++ b/library/Pdfexport/WebDriver/Response.php @@ -5,8 +5,17 @@ namespace Icinga\Module\Pdfexport\WebDriver; +/** + * Represents a WebDriver response. + */ readonly class Response { + /** + * Create a new response. + * @param string $sessionId the session ID of the response + * @param int $status a http status code for the response + * @param mixed|null $value the response value of the response + */ public function __construct( public string $sessionId, public int $status = 0, diff --git a/library/Pdfexport/WebDriver/WebDriver.php b/library/Pdfexport/WebDriver/WebDriver.php index 6f0440b..e52c08c 100644 --- a/library/Pdfexport/WebDriver/WebDriver.php +++ b/library/Pdfexport/WebDriver/WebDriver.php @@ -7,8 +7,18 @@ use Exception; +/** + * Partial implementation of the WebDriver protocol. + * @link https://www.w3.org/TR/webdriver/ + */ class WebDriver { + /** + * Create a new WebDriver instance. + * @param CommandExecutor|null $executor a command executor instance which is responsible for sending commands to + * the webdriver server + * @param string|null $sessionId the session if for the connection from the server from the `newSession` command + */ protected function __construct( protected ?CommandExecutor $executor, protected ?string $sessionId, @@ -20,6 +30,15 @@ public function __destruct() $this->quit(); } + /** + * Create a new WebDriver instance with a set of capabilities. + * + * @param string $url the host and port of the webdriver server + * @param Capabilities $capabilities the capabilities to use for the session + * + * @return static + * @throws Exception + */ public static function create(string $url, Capabilities $capabilities): static { $executor = new CommandExecutor($url); @@ -38,6 +57,13 @@ public static function create(string $url, Capabilities $capabilities): static return new static($executor, $response->sessionId); } + /** + * Execute a command on the webdriver server. + * @param CommandInterface $command the command to execute + * + * @return mixed the result of the command, the specifics of which depend on the command being executed + * @throws Exception + */ public function execute(CommandInterface $command): mixed { if ($this->sessionId === null) { @@ -49,6 +75,17 @@ public function execute(CommandInterface $command): mixed return $response->value; } + /** + * Wait synchronously for a condition to be met. + * This function uses polling to check the condition. + * + * @param ConditionInterface $condition the condition to wait for + * @param int $timeoutSeconds the maximum time to wait for the condition to be met + * @param int $intervalMs the time to wait between checks + * + * @return mixed + * @throws Exception + */ public function wait( ConditionInterface $condition, int $timeoutSeconds = 10, @@ -76,6 +113,12 @@ public function wait( return false; } + /** + * Cleanly close the webdriver session. + * + * @return void + * @throws Exception + */ public function quit(): void { if ($this->executor !== null) { From b08e5e24e558d5847802b4fbc9cbf7c0fdeddfb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 7 Apr 2026 14:38:00 +0200 Subject: [PATCH 53/60] Use ShellCommand abstraction --- .../Backend/HeadlessChromeBackend.php | 124 ++++-------------- library/Pdfexport/BackendLocator.php | 2 +- library/Pdfexport/ShellCommand.php | 106 +++++++++------ 3 files changed, 92 insertions(+), 140 deletions(-) diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index 1d4ec91..1bd2d3c 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -14,6 +14,7 @@ use Icinga\File\Storage\StorageInterface; use Icinga\File\Storage\TemporaryLocalFileStorage; use Icinga\Module\Pdfexport\PrintableHtmlDocument; +use Icinga\Module\Pdfexport\ShellCommand; use LogicException; use Throwable; use WebSocket\Client; @@ -23,21 +24,6 @@ class HeadlessChromeBackend implements PfdPrintBackend /** @var int */ public const MIN_SUPPORTED_CHROME_VERSION = 59; - /** @var int */ - protected const CHROME_START_MAX_WAIT_TIME = 10; - - /** @var int */ - protected const CHROME_CLOSE_MAX_WAIT_TIME = 5; - - /** @var int */ - protected const PROCESS_IDLE_TIME = 100000; - - /** @var int */ - protected const STREAM_WAIT_TIME = 200000; - - /** @var int */ - protected const STREAM_CHUNK_SIZE = 8192; - /** * Line of stderr output identifying the websocket url * @@ -62,9 +48,7 @@ class HeadlessChromeBackend implements PfdPrintBackend private array $interceptedEvents = []; - protected $process; - - protected array $pipes = []; + protected ?ShellCommand $process = null; protected ?string $socket = null; @@ -111,11 +95,6 @@ public static function createLocal(string $path, bool $useFile = false): static } $browserHome = $instance->getFileStorage()->resolvePath('HOME'); - $descriptors = [ - 0 => ['pipe', 'r'], // stdin - 1 => ['pipe', 'w'], // stdout - 2 => ['pipe', 'w'], // stderr - ]; $commandLine = join(' ', [ escapeshellarg($path), @@ -141,58 +120,25 @@ public static function createLocal(string $path, bool $useFile = false): static Logger::debug('Starting browser process: %s', $commandLine); } - $instance->process = proc_open($commandLine, $descriptors, $instance->pipes, null, $env); - - if (! is_resource($instance->process)) { - throw new Exception('Could not start browser process.'); - } - - // Non-blocking mode - stream_set_blocking($instance->pipes[2], false); - - $startTime = time(); - - while (true) { - $status = proc_get_status($instance->process); - - // Timeout handling - if ((time() - $startTime) > self::CHROME_START_MAX_WAIT_TIME) { - proc_terminate($instance->process, 6); // SIGABRT - Logger::error( - 'Browser timed out after %d seconds without the expected output', - self::CHROME_CLOSE_MAX_WAIT_TIME, - ); - - throw new Exception( - 'Received empty response or none at all from browser.' - . ' Please check the logs for further details.', - ); + $instance->process = new ShellCommand($commandLine, false, $env); + $instance->process->start(); + Logger::debug('Started browser process'); + $instance->process->wait(function ($stdout, $stderr) use ($instance) { + if ($stdout !== '') { + Logger::debug('Caught browser stdout: %d', mb_strlen($stdout)); } - - $read = [$instance->pipes[2]]; - $write = null; - $except = null; - - if (stream_select($read, $write, $except, 0, self::STREAM_WAIT_TIME)) { - $chunk = fread($instance->pipes[2], self::STREAM_CHUNK_SIZE); - - if ($chunk !== false && $chunk !== '') { - Logger::debug('Caught browser output: %s', $chunk); - - if (preg_match(self::DEBUG_ADDR_PATTERN, trim($chunk), $matches)) { - $instance->socket = $matches[1]; - $instance->browserId = $matches[2]; - break; - } + if ($stderr !== '') { + Logger::error('Browser process stderr: %d', mb_strlen($stderr)); + if (preg_match(self::DEBUG_ADDR_PATTERN, trim($stderr), $matches)) { + $instance->socket = $matches[1]; + $instance->browserId = $matches[2]; + + Logger::debug('Caught browser info socket: %s, id: %s', $instance->socket, $instance->browserId); + return false; } } - - if (! $status['running']) { - break; - } - - usleep(self::PROCESS_IDLE_TIME); - } + return true; + }); if ($instance->socket === null || $instance->browserId === null) { throw new Exception('Could not start browser process.'); @@ -203,35 +149,11 @@ public static function createLocal(string $path, bool $useFile = false): static protected function closeLocal(): void { - foreach ($this->pipes as $pipe) { - fclose($pipe); - } - $this->pipes = []; + Logger::debug('Closing local chrome instance'); if ($this->process !== null) { - proc_terminate($this->process); - - $start = time(); - $running = true; - - while ($running && (time() - $start) < self::CHROME_CLOSE_MAX_WAIT_TIME) { - $status = proc_get_status($this->process); - $running = $status['running']; - - if ($running) { - usleep(self::PROCESS_IDLE_TIME); - } - } - - // If still running after wait time seconds, force kills the entire process group - if ($running) { - $status = proc_get_status($this->process); - if (! empty($status['pid'])) { - posix_kill(-$status['pid'], SIGKILL); - } - } - - proc_close($this->process); + $code = $this->process->stop(); + Logger::error("Closed local chrome with exit code %d", $code); $this->process = null; } @@ -356,7 +278,7 @@ public function getPage(): Client try { $this->communicate($this->page, 'Console.enable'); - } catch (Exception $_) { + } catch (Exception) { // Deprecated, might fail } } @@ -526,7 +448,7 @@ private function parseApiResponse(string $payload) } } - private function registerEvent($method, $params) + private function registerEvent($method, $params): void { if (Logger::getInstance()->getLevel() === Logger::DEBUG) { $shortenValues = function ($params) use (&$shortenValues) { diff --git a/library/Pdfexport/BackendLocator.php b/library/Pdfexport/BackendLocator.php index c180ea1..b48d675 100644 --- a/library/Pdfexport/BackendLocator.php +++ b/library/Pdfexport/BackendLocator.php @@ -122,7 +122,7 @@ protected function connectToLocalChrome(string $section): ?PfdPrintBackend $binary, Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0') === '1', ); - Logger::info("Connected WebDriver Backend: $section"); + Logger::info("Connected local chrome Backend: $section"); return $backend; } catch (Exception $e) { Logger::warning( diff --git a/library/Pdfexport/ShellCommand.php b/library/Pdfexport/ShellCommand.php index bd79fee..ce07653 100644 --- a/library/Pdfexport/ShellCommand.php +++ b/library/Pdfexport/ShellCommand.php @@ -5,28 +5,42 @@ namespace Icinga\Module\Pdfexport; +use Exception; + class ShellCommand { /** @var string Command to execute */ - protected $command; + protected string $command; + + /** @var ?int Exit code of the command */ + protected ?int $exitCode = null; - /** @var int Exit code of the command */ - protected $exitCode; + /** @var array|null Environment variables */ + protected ?array $env; /** @var ?resource Process resource */ protected $resource; + /** @var object|null Named pipe resources */ + protected ?object $namedPipes; + + /** @var string collected stdout */ + protected string $stdout; + + /** @var string collected stderr */ + protected string $stderr; + /** * Create a new command * * @param string $command The command to execute * @param bool $escape Whether to escape the command + * @param array|null $env Environment variables */ - public function __construct($command, $escape = true) + public function __construct(string $command, bool $escape = true, ?array $env = null) { - $command = (string) $command; - $this->command = $escape ? escapeshellcmd($command) : $command; + $this->env = $env; } /** @@ -34,7 +48,7 @@ public function __construct($command, $escape = true) * * @return int */ - public function getExitCode() + public function getExitCode(): int { return $this->exitCode; } @@ -44,7 +58,7 @@ public function getExitCode() * * @return object */ - public function getStatus() + public function getStatus(): object { $status = (object) proc_get_status($this->resource); if ($status->running === false && $this->exitCode === null) { @@ -56,64 +70,67 @@ public function getStatus() return $status; } - /** - * Execute the command - * - * @return object - * - * @throws \Exception - */ - public function execute() + public function start(): void { if ($this->resource !== null) { - throw new \Exception('Command already started'); + throw new Exception('Command already started'); } $descriptors = [ ['pipe', 'r'], // stdin ['pipe', 'w'], // stdout - ['pipe', 'w'], // stderr + ['pipe', 'w'], // stderr ]; $this->resource = proc_open( $this->command, $descriptors, $pipes, + null, + $this->env, ); if (! is_resource($this->resource)) { - throw new \Exception(sprintf( + throw new Exception(sprintf( "Can't fork '%s'", $this->command, )); } - $namedpipes = (object) [ - 'stdin' => &$pipes[0], + $this->namedPipes = (object) [ + 'stdin' => &$pipes[0], 'stdout' => &$pipes[1], 'stderr' => &$pipes[2], ]; - fclose($namedpipes->stdin); + fclose($this->namedPipes->stdin); + } + + public function wait($callback = null): void + { + if ($this->resource === null) { + throw new Exception('Command not started'); + } - $read = [$namedpipes->stderr, $namedpipes->stdout]; + $read = [$this->namedPipes->stderr, $this->namedPipes->stdout]; $origRead = $read; - $write = null; // stdin not handled + // stdin not handled + $write = null; $except = null; - $stdout = ''; - $stderr = ''; + $this->stdout = ''; + $this->stderr = ''; - stream_set_blocking($namedpipes->stdout, false); // non-blocking - stream_set_blocking($namedpipes->stderr, false); + stream_set_blocking($this->namedPipes->stdout, false); + stream_set_blocking($this->namedPipes->stderr, false); while (stream_select($read, $write, $except, 0, 20000) !== false) { foreach ($read as $pipe) { - if ($pipe === $namedpipes->stdout) { - $stdout .= stream_get_contents($pipe); + if ($pipe === $this->namedPipes->stdout) { + $this->stdout .= stream_get_contents($pipe); } - if ($pipe === $namedpipes->stderr) { - $stderr .= stream_get_contents($pipe); + if ($pipe === $this->namedPipes->stderr) { + $this->stderr .= stream_get_contents($pipe); } } @@ -129,10 +146,25 @@ public function execute() // Reset pipes $read = $origRead; + + if ($callback !== null) { + $continue = call_user_func($callback, $this->stdout, $this->stderr); + if ($continue === false) { + break; + } + } } + } - fclose($namedpipes->stderr); - fclose($namedpipes->stdout); + public function stop(): int + { + if ($this->resource === null) { + throw new Exception('Command not started'); + } + fclose($this->namedPipes->stderr); + fclose($this->namedPipes->stdout); + + proc_terminate($this->resource); $exitCode = proc_close($this->resource); if ($this->exitCode === null) { @@ -140,10 +172,8 @@ public function execute() } $this->resource = null; + $this->namedPipes = null; - return (object) [ - 'stdout' => $stdout, - 'stderr' => $stderr, - ]; + return $exitCode; } } From 174faf2ecfb3d92cf401401401ade50b38c1b0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 8 Apr 2026 10:30:29 +0200 Subject: [PATCH 54/60] Add a function to check if adding a cover page is supported --- library/Pdfexport/Backend/Geckodriver.php | 6 ++++++ library/Pdfexport/Backend/HeadlessChromeBackend.php | 5 +++++ library/Pdfexport/Backend/PfdPrintBackend.php | 2 ++ library/Pdfexport/Backend/WebdriverBackend.php | 5 +++++ library/Pdfexport/ProvidedHook/Pdfexport.php | 2 +- 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/library/Pdfexport/Backend/Geckodriver.php b/library/Pdfexport/Backend/Geckodriver.php index ba8e83e..19f5f8e 100644 --- a/library/Pdfexport/Backend/Geckodriver.php +++ b/library/Pdfexport/Backend/Geckodriver.php @@ -13,4 +13,10 @@ public function __construct(string $rul) { parent::__construct($rul, Capabilities::firefox()); } + + public function supportsCoverPage(): bool + { + // Firefox generates compressed PDFs, which can't be merged by the `tcpi` libary + return false; + } } diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index 1bd2d3c..2226ce2 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -626,4 +626,9 @@ public function close(): void $this->closeBrowser(); $this->closeLocal(); } + + public function supportsCoverPage(): bool + { + return true; + } } diff --git a/library/Pdfexport/Backend/PfdPrintBackend.php b/library/Pdfexport/Backend/PfdPrintBackend.php index 03af0bd..75b03df 100644 --- a/library/Pdfexport/Backend/PfdPrintBackend.php +++ b/library/Pdfexport/Backend/PfdPrintBackend.php @@ -13,5 +13,7 @@ public function toPdf(PrintableHtmlDocument $document): string; public function isSupported(): bool; + public function supportsCoverPage(): bool; + public function close(): void; } diff --git a/library/Pdfexport/Backend/WebdriverBackend.php b/library/Pdfexport/Backend/WebdriverBackend.php index c26651f..d16e080 100644 --- a/library/Pdfexport/Backend/WebdriverBackend.php +++ b/library/Pdfexport/Backend/WebdriverBackend.php @@ -85,4 +85,9 @@ public function close(): void { $this->driver->quit(); } + + public function supportsCoverPage(): bool + { + return true; + } } diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index b02d2ac..fb0df2b 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -66,7 +66,7 @@ public function streamPdfFromHtml($html, $filename): void $pdf = $backend->toPdf($document); - if ($html instanceof PrintableHtmlDocument) { + if ($html instanceof PrintableHtmlDocument && $backend->supportsCoverPage()) { $coverPage = $html->getCoverPage(); if ($coverPage !== null) { $coverPageDocument = $this->getPrintableHtmlDocument($coverPage); From 958467f0e7e31ccf88bed8b2dc19b72a2173f260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 8 Apr 2026 10:31:10 +0200 Subject: [PATCH 55/60] Fix a deprecation with unset member access --- library/Pdfexport/Backend/HeadlessChromeBackend.php | 1 - 1 file changed, 1 deletion(-) diff --git a/library/Pdfexport/Backend/HeadlessChromeBackend.php b/library/Pdfexport/Backend/HeadlessChromeBackend.php index 2226ce2..a4ec7cf 100644 --- a/library/Pdfexport/Backend/HeadlessChromeBackend.php +++ b/library/Pdfexport/Backend/HeadlessChromeBackend.php @@ -159,7 +159,6 @@ protected function closeLocal(): void try { if ($this->fileStorage !== null) { - unset($this->fileStorage); $this->fileStorage = null; } } catch (Exception $exception) { From 0a5dfa2d4dc162ea3710e539aceb3db1ba4988ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 8 Apr 2026 10:31:45 +0200 Subject: [PATCH 56/60] fixme! Docstrings for ShellCommand --- library/Pdfexport/ShellCommand.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/library/Pdfexport/ShellCommand.php b/library/Pdfexport/ShellCommand.php index ce07653..ec37233 100644 --- a/library/Pdfexport/ShellCommand.php +++ b/library/Pdfexport/ShellCommand.php @@ -7,6 +7,9 @@ use Exception; +/** + * Abstraction for a running shell command. + */ class ShellCommand { /** @var string Command to execute */ @@ -70,6 +73,12 @@ public function getStatus(): object return $status; } + /** + * Run the command + * + * @return void + * @throws Exception + */ public function start(): void { if ($this->resource !== null) { @@ -106,6 +115,16 @@ public function start(): void fclose($this->namedPipes->stdin); } + /** + * Capture stdout and stderr of the command. + * This function will block until the command exits or the optional callback returns false. + * + * @param callable|null $callback A callback function that will be called with the captured stdout and stderr. + * The callback should return true to continue waiting, false to stop waiting. + * + * @return void + * @throws Exception + */ public function wait($callback = null): void { if ($this->resource === null) { @@ -156,6 +175,12 @@ public function wait($callback = null): void } } + /** + * Stop running command and return exit code + * + * @return int exit code + * @throws Exception + */ public function stop(): int { if ($this->resource === null) { From 907a1fdd73a91c493ae58cb29a998afdd9b64cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Wed, 8 Apr 2026 10:32:59 +0200 Subject: [PATCH 57/60] Bring back htmlToPdf function Makes pdfexport compatible with https://github.com/Icinga/icingaweb2/pull/5491 --- library/Pdfexport/ProvidedHook/Pdfexport.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index fb0df2b..06472bd 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -15,11 +15,12 @@ use Icinga\Module\Pdfexport\BackendLocator; use Icinga\Module\Pdfexport\PrintableHtmlDocument; use ipl\Html\HtmlString; +use ipl\Html\ValidHtml; use Karriere\PdfMerge\PdfMerge; class Pdfexport extends PdfexportHook { - public static function first() + public static function first(): static { $pdfexport = null; @@ -52,10 +53,18 @@ public function isSupported(): bool } } - public function streamPdfFromHtml($html, $filename): void + public function streamPdfFromHtml(ValidHtml $html, $filename): never { + $pdf = $this->htmlToPdf($html); $filename = basename($filename, '.pdf') . '.pdf'; + $this->emit($pdf, $filename); + + exit; + } + + public function htmlToPdf(ValidHtml $html): string + { $document = $this->getPrintableHtmlDocument($html); $locator = new BackendLocator(); @@ -84,9 +93,7 @@ public function streamPdfFromHtml($html, $filename): void $backend->close(); unset($coverPage); - $this->emit($pdf, $filename); - - exit; + return $pdf; } protected function emit(string $pdf, string $filename): void @@ -100,7 +107,7 @@ protected function emit(string $pdf, string $filename): void ->sendResponse(); } - protected function getPrintableHtmlDocument($html): PrintableHtmlDocument + protected function getPrintableHtmlDocument(ValidHtml $html): PrintableHtmlDocument { if ($html instanceof PrintableHtmlDocument) { return $html; From 68616968c4ede830324dd4e77b9f960ce165617f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 10 Apr 2026 10:37:34 +0200 Subject: [PATCH 58/60] Store the last working backend instance --- library/Pdfexport/BackendLocator.php | 19 ++++++++++++++++--- library/Pdfexport/ProvidedHook/Pdfexport.php | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/library/Pdfexport/BackendLocator.php b/library/Pdfexport/BackendLocator.php index b48d675..4067971 100644 --- a/library/Pdfexport/BackendLocator.php +++ b/library/Pdfexport/BackendLocator.php @@ -18,13 +18,26 @@ */ class BackendLocator { + /** + * The currently connected backend. + * This is used to avoid re-connecting to the same backend multiple times. + * @var PfdPrintBackend|null + */ + protected ?PfdPrintBackend $backend = null; + /** * Get the first supported backend from the configuration which responded with a successful connection. * First, in this context means the backend with the lowest priority. + * Note: This method caches the first successful backend and reuses it for subsequent calls. * @return PfdPrintBackend|null the first supported backend or null if none is available */ public function getFirstSupportedBackend(): ?PfdPrintBackend { + if ($this->backend !== null && $this->backend->isSupported()) { + return $this->backend; + } + + $this->backend = null; $sorted = []; foreach (Config::module('pdfexport') as $section => $configs) { $priority = (int) $configs->get('priority', 100); @@ -34,11 +47,11 @@ public function getFirstSupportedBackend(): ?PfdPrintBackend asort($sorted); foreach ($sorted as $section => $priority) { - $backend = $this->getSingleBackend($section); - if ($backend === null) { + $this->backend = $this->getSingleBackend($section); + if ($this->backend === null) { continue; } - return $backend; + return $this->backend; } return null; diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 06472bd..04f0d98 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -20,6 +20,8 @@ class Pdfexport extends PdfexportHook { + protected ?BackendLocator $locator = null; + public static function first(): static { $pdfexport = null; @@ -41,9 +43,21 @@ public static function first(): static return $pdfexport; } + /** + * Get the backend locator instance, creating it if necessary + * @return BackendLocator + */ + protected function getLocator(): BackendLocator + { + if (! $this->locator) { + $this->locator = new BackendLocator(); + } + return $this->locator; + } + public function isSupported(): bool { - $locator = new BackendLocator(); + $locator = $this->getLocator(); try { $backend = $locator->getFirstSupportedBackend(); return $backend !== null; @@ -67,7 +81,7 @@ public function htmlToPdf(ValidHtml $html): string { $document = $this->getPrintableHtmlDocument($html); - $locator = new BackendLocator(); + $locator = $this->getLocator(); $backend = $locator->getFirstSupportedBackend(); if ($backend === null) { Logger::warning("No supported PDF backend available."); From 50f297c948c02063dabff72ea53dec2a837c75b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 10 Apr 2026 13:09:14 +0200 Subject: [PATCH 59/60] Remove types to stay backward compatible --- library/Pdfexport/ProvidedHook/Pdfexport.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index 04f0d98..f71cf6d 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -55,7 +55,7 @@ protected function getLocator(): BackendLocator return $this->locator; } - public function isSupported(): bool + public function isSupported() { $locator = $this->getLocator(); try { @@ -67,7 +67,7 @@ public function isSupported(): bool } } - public function streamPdfFromHtml(ValidHtml $html, $filename): never + public function streamPdfFromHtml($html, $filename) { $pdf = $this->htmlToPdf($html); $filename = basename($filename, '.pdf') . '.pdf'; @@ -77,7 +77,7 @@ public function streamPdfFromHtml(ValidHtml $html, $filename): never exit; } - public function htmlToPdf(ValidHtml $html): string + public function htmlToPdf($html) { $document = $this->getPrintableHtmlDocument($html); From 0cec00441ed07b5efcf876987a1254274feff639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 10 Apr 2026 13:09:52 +0200 Subject: [PATCH 60/60] Change `first` method to mimic the behaviour of the base class This is only done to provide backward compatibility and can be removed after we decide to break compatability. --- library/Pdfexport/ProvidedHook/Pdfexport.php | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php index f71cf6d..fd08f37 100644 --- a/library/Pdfexport/ProvidedHook/Pdfexport.php +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -17,29 +17,29 @@ use ipl\Html\HtmlString; use ipl\Html\ValidHtml; use Karriere\PdfMerge\PdfMerge; +use RuntimeException; class Pdfexport extends PdfexportHook { protected ?BackendLocator $locator = null; - public static function first(): static + /** + * Get the first hook. + * Note: This function is the exact same as the one if the base class. + * It can be removed after we decide to remove compatibility with current + * reporting (1.1) and icingaweb2 (2.13) versions. + * + * @return static + */ + public static function first() { - $pdfexport = null; - - if (Hook::has('Pdfexport')) { - $pdfexport = Hook::first('Pdfexport'); - - if (! $pdfexport->isSupported()) { - throw new Exception( - sprintf("Can't export: %s does not support exporting PDFs", get_class($pdfexport)), - ); - } + if (! Hook::has('Pdfexport')) { + throw new RuntimeException('No PDF exporter available'); } - - if (! $pdfexport) { - throw new Exception("Can't export: No module found which provides PDF export"); + $pdfexport = Hook::first('Pdfexport'); + if (! $pdfexport->isSupported()) { + throw new RuntimeException('PDF exporter is not supported'); } - return $pdfexport; }