1159 lines
29 KiB
PHP
Executable File
1159 lines
29 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Spatie\Browsershot;
|
|
|
|
use Spatie\Browsershot\Exceptions\CouldNotTakeBrowsershot;
|
|
use Spatie\Browsershot\Exceptions\ElementNotFound;
|
|
use Spatie\Browsershot\Exceptions\FileDoesNotExistException;
|
|
use Spatie\Browsershot\Exceptions\FileUrlNotAllowed;
|
|
use Spatie\Browsershot\Exceptions\HtmlIsNotAllowedToContainFile;
|
|
use Spatie\Browsershot\Exceptions\UnsuccessfulResponse;
|
|
use Spatie\Image\Image;
|
|
use Spatie\Image\Manipulations;
|
|
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
|
use Symfony\Component\Process\Exception\ProcessFailedException;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
/** @mixin \Spatie\Image\Manipulations */
|
|
class Browsershot
|
|
{
|
|
protected $nodeBinary = null;
|
|
protected $npmBinary = null;
|
|
protected $nodeModulePath = null;
|
|
protected $includePath = '$PATH:/usr/local/bin:/opt/homebrew/bin';
|
|
protected $binPath = null;
|
|
protected $html = '';
|
|
protected $noSandbox = false;
|
|
protected $proxyServer = '';
|
|
protected $showBackground = false;
|
|
protected $showScreenshotBackground = true;
|
|
protected $scale = null;
|
|
protected $screenshotType = 'png';
|
|
protected $screenshotQuality = null;
|
|
protected $taggedPdf = false;
|
|
protected $temporaryHtmlDirectory;
|
|
protected $timeout = 60;
|
|
protected $transparentBackground = false;
|
|
protected $url = '';
|
|
protected $postParams = [];
|
|
protected $additionalOptions = [];
|
|
protected $temporaryOptionsDirectory;
|
|
protected $tempPath = '';
|
|
protected $writeOptionsToFile = false;
|
|
protected $chromiumArguments = [];
|
|
|
|
/**
|
|
* @var ChromiumResult|null
|
|
*/
|
|
protected ChromiumResult|null $chromiumResult = null;
|
|
|
|
/** @var \Spatie\Image\Manipulations */
|
|
protected $imageManipulations;
|
|
|
|
public const POLLING_REQUEST_ANIMATION_FRAME = 'raf';
|
|
public const POLLING_MUTATION = 'mutation';
|
|
|
|
public static function url(string $url): static
|
|
{
|
|
return (new static())->setUrl($url);
|
|
}
|
|
|
|
public static function html(string $html): static
|
|
{
|
|
return (new static())->setHtml($html);
|
|
}
|
|
|
|
public static function htmlFromFilePath(string $filePath): static
|
|
{
|
|
return (new static())->setHtmlFromFilePath($filePath);
|
|
}
|
|
|
|
public function __construct(string $url = '', bool $deviceEmulate = false)
|
|
{
|
|
$this->url = $url;
|
|
|
|
$this->imageManipulations = new Manipulations();
|
|
|
|
if (! $deviceEmulate) {
|
|
$this->windowSize(800, 600);
|
|
}
|
|
}
|
|
|
|
public function setNodeBinary(string $nodeBinary)
|
|
{
|
|
$this->nodeBinary = $nodeBinary;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setNpmBinary(string $npmBinary)
|
|
{
|
|
$this->npmBinary = $npmBinary;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setIncludePath(string $includePath)
|
|
{
|
|
$this->includePath = $includePath;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setBinPath(string $binPath)
|
|
{
|
|
$this->binPath = $binPath;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setNodeModulePath(string $nodeModulePath)
|
|
{
|
|
$this->nodeModulePath = $nodeModulePath;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setChromePath(string $executablePath)
|
|
{
|
|
$this->setOption('executablePath', $executablePath);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setCustomTempPath(string $tempPath)
|
|
{
|
|
$this->tempPath = $tempPath;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function post(array $postParams = [])
|
|
{
|
|
$this->postParams = $postParams;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function useCookies(array $cookies, string $domain = null)
|
|
{
|
|
if (! count($cookies)) {
|
|
return $this;
|
|
}
|
|
|
|
if (is_null($domain)) {
|
|
$domain = parse_url($this->url)['host'];
|
|
}
|
|
|
|
$cookies = array_map(function ($value, $name) use ($domain) {
|
|
return compact('name', 'value', 'domain');
|
|
}, $cookies, array_keys($cookies));
|
|
|
|
if (isset($this->additionalOptions['cookies'])) {
|
|
$cookies = array_merge($this->additionalOptions['cookies'], $cookies);
|
|
}
|
|
|
|
$this->setOption('cookies', $cookies);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setExtraHttpHeaders(array $extraHTTPHeaders)
|
|
{
|
|
$this->setOption('extraHTTPHeaders', $extraHTTPHeaders);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setExtraNavigationHttpHeaders(array $extraNavigationHTTPHeaders)
|
|
{
|
|
$this->setOption('extraNavigationHTTPHeaders', $extraNavigationHTTPHeaders);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function authenticate(string $username, string $password)
|
|
{
|
|
$this->setOption('authentication', compact('username', 'password'));
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function click(string $selector, string $button = 'left', int $clickCount = 1, int $delay = 0)
|
|
{
|
|
$clicks = $this->additionalOptions['clicks'] ?? [];
|
|
|
|
$clicks[] = compact('selector', 'button', 'clickCount', 'delay');
|
|
|
|
$this->setOption('clicks', $clicks);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function selectOption(string $selector, string $value = '')
|
|
{
|
|
$dropdownSelects = $this->additionalOptions['selects'] ?? [];
|
|
|
|
$dropdownSelects[] = compact('selector', 'value');
|
|
|
|
$this->setOption('selects', $dropdownSelects);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function type(string $selector, string $text = '', int $delay = 0)
|
|
{
|
|
$types = $this->additionalOptions['types'] ?? [];
|
|
|
|
$types[] = compact('selector', 'text', 'delay');
|
|
|
|
$this->setOption('types', $types);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @deprecated This option is no longer supported by modern versions of Puppeteer.
|
|
*/
|
|
public function setNetworkIdleTimeout(int $networkIdleTimeout)
|
|
{
|
|
$this->setOption('networkIdleTimeout');
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function waitUntilNetworkIdle(bool $strict = true)
|
|
{
|
|
$this->setOption('waitUntil', $strict ? 'networkidle0' : 'networkidle2');
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function waitForFunction(string $function, $polling = self::POLLING_REQUEST_ANIMATION_FRAME, int $timeout = 0)
|
|
{
|
|
$this->setOption('functionPolling', $polling);
|
|
$this->setOption('functionTimeout', $timeout);
|
|
|
|
return $this->setOption('function', $function);
|
|
}
|
|
|
|
public function waitForSelector(string $selector, array $options = [])
|
|
{
|
|
$this->setOption('waitForSelector', $selector);
|
|
|
|
if (! empty($options)) {
|
|
$this->setOption('waitForSelectorOptions', $options);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setUrl(string $url)
|
|
{
|
|
if (Helpers::stringStartsWith(strtolower($url), 'file://')) {
|
|
throw FileUrlNotAllowed::make();
|
|
}
|
|
|
|
$this->url = $url;
|
|
$this->html = '';
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setHtmlFromFilePath(string $filePath): self
|
|
{
|
|
if (false === file_exists($filePath)) {
|
|
throw new FileDoesNotExistException($filePath);
|
|
}
|
|
|
|
$this->url = 'file://'.$filePath;
|
|
$this->html = '';
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setProxyServer(string $proxyServer)
|
|
{
|
|
$this->proxyServer = $proxyServer;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setHtml(string $html)
|
|
{
|
|
if (Helpers::stringContains(strtolower($html), 'file://')) {
|
|
throw HtmlIsNotAllowedToContainFile::make();
|
|
}
|
|
|
|
$this->html = $html;
|
|
$this->url = '';
|
|
|
|
$this->hideBrowserHeaderAndFooter();
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function clip(int $x, int $y, int $width, int $height)
|
|
{
|
|
return $this->setOption('clip', compact('x', 'y', 'width', 'height'));
|
|
}
|
|
|
|
public function preventUnsuccessfulResponse(bool $preventUnsuccessfulResponse = true)
|
|
{
|
|
return $this->setOption('preventUnsuccessfulResponse', $preventUnsuccessfulResponse);
|
|
}
|
|
|
|
public function select($selector, $index = 0)
|
|
{
|
|
$this->selectorIndex($index);
|
|
|
|
return $this->setOption('selector', $selector);
|
|
}
|
|
|
|
public function selectorIndex(int $index)
|
|
{
|
|
return $this->setOption('selectorIndex', $index);
|
|
}
|
|
|
|
public function showBrowserHeaderAndFooter()
|
|
{
|
|
return $this->setOption('displayHeaderFooter', true);
|
|
}
|
|
|
|
public function hideBrowserHeaderAndFooter()
|
|
{
|
|
return $this->setOption('displayHeaderFooter', false);
|
|
}
|
|
|
|
public function hideHeader()
|
|
{
|
|
return $this->headerHtml('<p></p>');
|
|
}
|
|
|
|
public function hideFooter()
|
|
{
|
|
return $this->footerHtml('<p></p>');
|
|
}
|
|
|
|
public function headerHtml(string $html)
|
|
{
|
|
return $this->setOption('headerTemplate', $html);
|
|
}
|
|
|
|
public function footerHtml(string $html)
|
|
{
|
|
return $this->setOption('footerTemplate', $html);
|
|
}
|
|
|
|
public function deviceScaleFactor(int $deviceScaleFactor)
|
|
{
|
|
// Google Chrome currently supports values of 1, 2, and 3.
|
|
return $this->setOption('viewport.deviceScaleFactor', max(1, min(3, $deviceScaleFactor)));
|
|
}
|
|
|
|
public function fullPage()
|
|
{
|
|
return $this->setOption('fullPage', true);
|
|
}
|
|
|
|
public function showBackground()
|
|
{
|
|
$this->showBackground = true;
|
|
$this->showScreenshotBackground = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function hideBackground()
|
|
{
|
|
$this->showBackground = false;
|
|
$this->showScreenshotBackground = false;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function transparentBackground()
|
|
{
|
|
$this->transparentBackground = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function taggedPdf()
|
|
{
|
|
$this->taggedPdf = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setScreenshotType(string $type, int $quality = null)
|
|
{
|
|
$this->screenshotType = $type;
|
|
|
|
if (! is_null($quality)) {
|
|
$this->screenshotQuality = $quality;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function ignoreHttpsErrors()
|
|
{
|
|
return $this->setOption('ignoreHttpsErrors', true);
|
|
}
|
|
|
|
public function mobile(bool $mobile = true)
|
|
{
|
|
return $this->setOption('viewport.isMobile', $mobile);
|
|
}
|
|
|
|
public function touch(bool $touch = true)
|
|
{
|
|
return $this->setOption('viewport.hasTouch', $touch);
|
|
}
|
|
|
|
public function landscape(bool $landscape = true)
|
|
{
|
|
return $this->setOption('landscape', $landscape);
|
|
}
|
|
|
|
public function margins(float $top, float $right, float $bottom, float $left, string $unit = 'mm')
|
|
{
|
|
return $this->setOption('margin', [
|
|
'top' => $top.$unit,
|
|
'right' => $right.$unit,
|
|
'bottom' => $bottom.$unit,
|
|
'left' => $left.$unit,
|
|
]);
|
|
}
|
|
|
|
public function noSandbox()
|
|
{
|
|
$this->noSandbox = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function dismissDialogs()
|
|
{
|
|
return $this->setOption('dismissDialogs', true);
|
|
}
|
|
|
|
public function disableJavascript()
|
|
{
|
|
return $this->setOption('disableJavascript', true);
|
|
}
|
|
|
|
public function disableImages()
|
|
{
|
|
return $this->setOption('disableImages', true);
|
|
}
|
|
|
|
public function blockUrls($array)
|
|
{
|
|
return $this->setOption('blockUrls', $array);
|
|
}
|
|
|
|
public function blockDomains($array)
|
|
{
|
|
return $this->setOption('blockDomains', $array);
|
|
}
|
|
|
|
public function pages(string $pages)
|
|
{
|
|
return $this->setOption('pageRanges', $pages);
|
|
}
|
|
|
|
public function paperSize(float $width, float $height, string $unit = 'mm')
|
|
{
|
|
return $this
|
|
->setOption('width', $width.$unit)
|
|
->setOption('height', $height.$unit);
|
|
}
|
|
|
|
// paper format
|
|
public function format(string $format)
|
|
{
|
|
return $this->setOption('format', $format);
|
|
}
|
|
|
|
public function scale(float $scale)
|
|
{
|
|
$this->scale = $scale;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function timeout(int $timeout)
|
|
{
|
|
$this->timeout = $timeout;
|
|
$this->setOption('timeout', $timeout * 1000);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function userAgent(string $userAgent)
|
|
{
|
|
$this->setOption('userAgent', $userAgent);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function device(string $device)
|
|
{
|
|
$this->setOption('device', $device);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function emulateMedia(?string $media)
|
|
{
|
|
$this->setOption('emulateMedia', $media);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function windowSize(int $width, int $height)
|
|
{
|
|
return $this
|
|
->setOption('viewport.width', $width)
|
|
->setOption('viewport.height', $height);
|
|
}
|
|
|
|
public function setDelay(int $delayInMilliseconds)
|
|
{
|
|
return $this->setOption('delay', $delayInMilliseconds);
|
|
}
|
|
|
|
public function delay(int $delayInMilliseconds)
|
|
{
|
|
return $this->setDelay($delayInMilliseconds);
|
|
}
|
|
|
|
public function setUserDataDir(string $absolutePath)
|
|
{
|
|
return $this->addChromiumArguments(['user-data-dir' => $absolutePath]);
|
|
}
|
|
|
|
public function userDataDir(string $absolutePath)
|
|
{
|
|
return $this->setUserDataDir($absolutePath);
|
|
}
|
|
|
|
public function writeOptionsToFile()
|
|
{
|
|
$this->writeOptionsToFile = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setOption($key, $value)
|
|
{
|
|
$this->arraySet($this->additionalOptions, $key, $value);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function addChromiumArguments(array $arguments)
|
|
{
|
|
foreach ($arguments as $argument => $value) {
|
|
if (is_numeric($argument)) {
|
|
$this->chromiumArguments[] = "--$value";
|
|
} else {
|
|
$this->chromiumArguments[] = "--$argument=$value";
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function __call($name, $arguments)
|
|
{
|
|
$this->imageManipulations->$name(...$arguments);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function save(string $targetPath)
|
|
{
|
|
$extension = strtolower(pathinfo($targetPath, PATHINFO_EXTENSION));
|
|
|
|
if ($extension === '') {
|
|
throw CouldNotTakeBrowsershot::outputFileDidNotHaveAnExtension($targetPath);
|
|
}
|
|
|
|
if ($extension === 'pdf') {
|
|
return $this->savePdf($targetPath);
|
|
}
|
|
|
|
$command = $this->createScreenshotCommand($targetPath);
|
|
|
|
$output = $this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
if (! file_exists($targetPath)) {
|
|
throw CouldNotTakeBrowsershot::chromeOutputEmpty($targetPath, $output, $command);
|
|
}
|
|
|
|
if (! $this->imageManipulations->isEmpty()) {
|
|
$this->applyManipulations($targetPath);
|
|
}
|
|
}
|
|
|
|
public function bodyHtml(): string
|
|
{
|
|
$command = $this->createBodyHtmlCommand();
|
|
$html = $this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return $html;
|
|
}
|
|
|
|
public function base64Screenshot(): string
|
|
{
|
|
$command = $this->createScreenshotCommand();
|
|
$encodedImage = $this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return $encodedImage;
|
|
}
|
|
|
|
public function screenshot(): string
|
|
{
|
|
if ($this->imageManipulations->isEmpty()) {
|
|
|
|
$command = $this->createScreenshotCommand();
|
|
$encodedImage = $this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return base64_decode($encodedImage);
|
|
}
|
|
|
|
$temporaryDirectory = (new TemporaryDirectory($this->tempPath))->create();
|
|
|
|
$this->save($temporaryDirectory->path('screenshot.png'));
|
|
|
|
$screenshot = file_get_contents($temporaryDirectory->path('screenshot.png'));
|
|
|
|
$temporaryDirectory->delete();
|
|
|
|
return $screenshot;
|
|
}
|
|
|
|
public function pdf(): string
|
|
{
|
|
$command = $this->createPdfCommand();
|
|
$encodedPdf = $this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return base64_decode($encodedPdf);
|
|
}
|
|
|
|
public function savePdf(string $targetPath)
|
|
{
|
|
$command = $this->createPdfCommand($targetPath);
|
|
$output = $this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
if (! file_exists($targetPath)) {
|
|
throw CouldNotTakeBrowsershot::chromeOutputEmpty($targetPath, $output);
|
|
}
|
|
}
|
|
|
|
public function base64pdf(): string
|
|
{
|
|
$command = $this->createPdfCommand();
|
|
$encodedPdf = $this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return $encodedPdf;
|
|
}
|
|
|
|
public function evaluate(string $pageFunction): string
|
|
{
|
|
$command = $this->createEvaluateCommand($pageFunction);
|
|
$evaluation = $this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return $evaluation;
|
|
}
|
|
|
|
/**
|
|
* @return null|array{url: string}
|
|
*/
|
|
public function triggeredRequests(): array|null
|
|
{
|
|
$requests = $this->chromiumResult?->getRequestsList();
|
|
|
|
if ($requests) {
|
|
return $requests;
|
|
}
|
|
|
|
$command = $this->createTriggeredRequestsListCommand();
|
|
$this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return $this->chromiumResult?->getRequestsList();
|
|
}
|
|
|
|
/**
|
|
* @return null|array{url: string, status: int, statusText: string, headers: array}
|
|
*/
|
|
public function redirectHistory(): array|null
|
|
{
|
|
$redirectHistory = $this->chromiumResult?->getredirectHistory();
|
|
|
|
if ($redirectHistory) {
|
|
return $redirectHistory;
|
|
}
|
|
|
|
$command = $this->createRedirectHistoryCommand();
|
|
|
|
$this->callBrowser($command);
|
|
|
|
return $this->chromiumResult?->getredirectHistory();
|
|
}
|
|
|
|
/**
|
|
* @return null|array{type: string, message: string, location:array}
|
|
*/
|
|
public function consoleMessages(): array|null
|
|
{
|
|
$messages = $this->chromiumResult?->getConsoleMessages();
|
|
|
|
if ($messages) {
|
|
return $messages;
|
|
}
|
|
|
|
$command = $this->createConsoleMessagesCommand();
|
|
|
|
$this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return $this->chromiumResult?->getConsoleMessages();
|
|
}
|
|
|
|
/**
|
|
* @return null|array{status: int, url: string}
|
|
*/
|
|
public function failedRequests(): array|null
|
|
{
|
|
$requests = $this->chromiumResult?->getFailedRequests();
|
|
|
|
if ($requests) {
|
|
return $requests;
|
|
}
|
|
|
|
$command = $this->createFailedRequestsCommand();
|
|
|
|
$this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return $this->chromiumResult?->getFailedRequests();
|
|
}
|
|
|
|
/**
|
|
* @return null|array{name: string, message: string}
|
|
*/
|
|
public function pageErrors(): array|null
|
|
{
|
|
$pageErrors = $this->chromiumResult?->getPageErrors();
|
|
|
|
if ($pageErrors) {
|
|
return $pageErrors;
|
|
}
|
|
|
|
$command = $this->createPageErrorsCommand();
|
|
|
|
$this->callBrowser($command);
|
|
|
|
$this->cleanupTemporaryHtmlFile();
|
|
|
|
return $this->chromiumResult?->getPageErrors();
|
|
}
|
|
|
|
public function applyManipulations(string $imagePath)
|
|
{
|
|
Image::load($imagePath)
|
|
->manipulate($this->imageManipulations)
|
|
->save();
|
|
}
|
|
|
|
public function createBodyHtmlCommand(): array
|
|
{
|
|
$url = $this->getFinalContentsUrl();
|
|
|
|
return $this->createCommand($url, 'content');
|
|
}
|
|
|
|
public function createScreenshotCommand($targetPath = null): array
|
|
{
|
|
$url = $this->getFinalContentsUrl();
|
|
|
|
$options = [
|
|
'type' => $this->screenshotType,
|
|
];
|
|
if ($targetPath) {
|
|
$options['path'] = $targetPath;
|
|
}
|
|
|
|
if ($this->screenshotQuality) {
|
|
$options['quality'] = $this->screenshotQuality;
|
|
}
|
|
|
|
$command = $this->createCommand($url, 'screenshot', $options);
|
|
|
|
if (! $this->showScreenshotBackground) {
|
|
$command['options']['omitBackground'] = true;
|
|
}
|
|
|
|
return $command;
|
|
}
|
|
|
|
public function createPdfCommand($targetPath = null): array
|
|
{
|
|
$url = $this->getFinalContentsUrl();
|
|
|
|
$options = [];
|
|
|
|
if ($targetPath) {
|
|
$options['path'] = $targetPath;
|
|
}
|
|
|
|
$command = $this->createCommand($url, 'pdf', $options);
|
|
|
|
if ($this->showBackground) {
|
|
$command['options']['printBackground'] = true;
|
|
}
|
|
|
|
if ($this->transparentBackground) {
|
|
$command['options']['omitBackground'] = true;
|
|
}
|
|
|
|
if ($this->taggedPdf) {
|
|
$command['options']['tagged'] = true;
|
|
}
|
|
|
|
if ($this->scale) {
|
|
$command['options']['scale'] = $this->scale;
|
|
}
|
|
|
|
return $command;
|
|
}
|
|
|
|
public function createEvaluateCommand(string $pageFunction): array
|
|
{
|
|
$url = $this->getFinalContentsUrl();
|
|
|
|
$options = [
|
|
'pageFunction' => $pageFunction,
|
|
];
|
|
|
|
return $this->createCommand($url, 'evaluate', $options);
|
|
}
|
|
|
|
public function createTriggeredRequestsListCommand(): array
|
|
{
|
|
$url = $this->html
|
|
? $this->createTemporaryHtmlFile()
|
|
: $this->url;
|
|
|
|
return $this->createCommand($url, 'requestsList');
|
|
}
|
|
|
|
public function createRedirectHistoryCommand(): array
|
|
{
|
|
$url = $this->html
|
|
? $this->createTemporaryHtmlFile()
|
|
: $this->url;
|
|
|
|
return $this->createCommand($url, 'redirectHistory');
|
|
}
|
|
|
|
public function createConsoleMessagesCommand(): array
|
|
{
|
|
$url = $this->html
|
|
? $this->createTemporaryHtmlFile()
|
|
: $this->url;
|
|
|
|
return $this->createCommand($url, 'consoleMessages');
|
|
}
|
|
|
|
public function createFailedRequestsCommand(): array
|
|
{
|
|
$url = $this->html
|
|
? $this->createTemporaryHtmlFile()
|
|
: $this->url;
|
|
|
|
return $this->createCommand($url, 'failedRequests');
|
|
}
|
|
|
|
public function createPageErrorsCommand(): array
|
|
{
|
|
$url = $this->html
|
|
? $this->createTemporaryHtmlFile()
|
|
: $this->url;
|
|
|
|
return $this->createCommand($url, 'pageErrors');
|
|
}
|
|
|
|
public function setRemoteInstance(string $ip = '127.0.0.1', int $port = 9222): self
|
|
{
|
|
// assuring that ip and port does actually contains a value
|
|
if ($ip && $port) {
|
|
$this->setOption('remoteInstanceUrl', 'http://'.$ip.':'.$port);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setWSEndpoint(string $endpoint): self
|
|
{
|
|
if (! is_null($endpoint)) {
|
|
$this->setOption('browserWSEndpoint', $endpoint);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function usePipe(): self
|
|
{
|
|
$this->setOption('pipe', true);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setEnvironmentOptions(array $options = []): self
|
|
{
|
|
return $this->setOption('env', $options);
|
|
}
|
|
|
|
public function setContentUrl(string $contentUrl): self
|
|
{
|
|
return $this->html ? $this->setOption('contentUrl', $contentUrl) : $this;
|
|
}
|
|
|
|
protected function getOptionArgs(): array
|
|
{
|
|
$args = $this->chromiumArguments;
|
|
|
|
if ($this->noSandbox) {
|
|
$args[] = '--no-sandbox';
|
|
}
|
|
|
|
if ($this->proxyServer) {
|
|
$args[] = '--proxy-server='.$this->proxyServer;
|
|
}
|
|
|
|
return $args;
|
|
}
|
|
|
|
protected function createCommand(string $url, string $action, array $options = []): array
|
|
{
|
|
$command = compact('url', 'action', 'options');
|
|
|
|
$command['options']['args'] = $this->getOptionArgs();
|
|
|
|
if (! empty($this->postParams)) {
|
|
$command['postParams'] = $this->postParams;
|
|
}
|
|
|
|
if (! empty($this->additionalOptions)) {
|
|
$command['options'] = array_merge_recursive($command['options'], $this->additionalOptions);
|
|
}
|
|
|
|
return $command;
|
|
}
|
|
|
|
protected function createTemporaryHtmlFile(): string
|
|
{
|
|
$this->temporaryHtmlDirectory = (new TemporaryDirectory($this->tempPath))->create();
|
|
|
|
file_put_contents($temporaryHtmlFile = $this->temporaryHtmlDirectory->path('index.html'), $this->html);
|
|
|
|
return "file://{$temporaryHtmlFile}";
|
|
}
|
|
|
|
protected function cleanupTemporaryHtmlFile()
|
|
{
|
|
if ($this->temporaryHtmlDirectory) {
|
|
$this->temporaryHtmlDirectory->delete();
|
|
}
|
|
}
|
|
|
|
protected function createTemporaryOptionsFile(string $command): string
|
|
{
|
|
$this->temporaryOptionsDirectory = (new TemporaryDirectory($this->tempPath))->create();
|
|
|
|
file_put_contents($temporaryOptionsFile = $this->temporaryOptionsDirectory->path('command.js'), $command);
|
|
|
|
return "file://{$temporaryOptionsFile}";
|
|
}
|
|
|
|
protected function cleanupTemporaryOptionsFile()
|
|
{
|
|
if ($this->temporaryOptionsDirectory) {
|
|
$this->temporaryOptionsDirectory->delete();
|
|
}
|
|
}
|
|
|
|
protected function callBrowser(array $command): string
|
|
{
|
|
$fullCommand = $this->getFullCommand($command);
|
|
|
|
$process = $this->isWindows() ? new Process($fullCommand) : Process::fromShellCommandline($fullCommand);
|
|
|
|
$process->setTimeout($this->timeout);
|
|
|
|
// clear additional output data fetched on last browser request
|
|
$this->chromiumResult = null;
|
|
|
|
$process->run();
|
|
|
|
$rawOutput = rtrim($process->getOutput());
|
|
|
|
$this->chromiumResult = new ChromiumResult(json_decode($rawOutput, true));
|
|
|
|
if ($process->isSuccessful()) {
|
|
return $this->chromiumResult?->getResult();
|
|
}
|
|
|
|
$this->cleanupTemporaryOptionsFile();
|
|
$process->clearOutput();
|
|
$exitCode = $process->getExitCode();
|
|
$errorOutput = $process->getErrorOutput();
|
|
|
|
if ($exitCode === 3) {
|
|
throw new UnsuccessfulResponse($this->url, $errorOutput ?? '');
|
|
}
|
|
|
|
if ($exitCode === 2) {
|
|
throw new ElementNotFound($this->additionalOptions['selector']);
|
|
}
|
|
|
|
throw new ProcessFailedException($process);
|
|
}
|
|
|
|
protected function getFullCommand(array $command)
|
|
{
|
|
$nodeBinary = $this->nodeBinary ?: 'node';
|
|
|
|
$binPath = $this->binPath ?: __DIR__.'/../bin/browser.cjs';
|
|
|
|
$optionsCommand = $this->getOptionsCommand(json_encode($command));
|
|
|
|
if ($this->isWindows()) {
|
|
// on Windows we will let Symfony/process handle the command escaping
|
|
// by passing an array to the process instance
|
|
return [
|
|
$nodeBinary,
|
|
$binPath,
|
|
$optionsCommand,
|
|
];
|
|
}
|
|
|
|
$setIncludePathCommand = "PATH={$this->includePath}";
|
|
|
|
$setNodePathCommand = $this->getNodePathCommand($nodeBinary);
|
|
|
|
return
|
|
$setIncludePathCommand.' '
|
|
.$setNodePathCommand.' '
|
|
.$nodeBinary.' '
|
|
.escapeshellarg($binPath).' '
|
|
.$optionsCommand;
|
|
}
|
|
|
|
protected function getNodePathCommand(string $nodeBinary): string
|
|
{
|
|
if ($this->nodeModulePath) {
|
|
return "NODE_PATH='{$this->nodeModulePath}'";
|
|
}
|
|
if ($this->npmBinary) {
|
|
return "NODE_PATH=`{$nodeBinary} {$this->npmBinary} root -g`";
|
|
}
|
|
|
|
return 'NODE_PATH=`npm root -g`';
|
|
}
|
|
|
|
protected function getOptionsCommand(string $command): string
|
|
{
|
|
if ($this->writeOptionsToFile) {
|
|
$temporaryOptionsFile = $this->createTemporaryOptionsFile($command);
|
|
$command = "-f {$temporaryOptionsFile}";
|
|
}
|
|
|
|
if ($this->isWindows()) {
|
|
return $command;
|
|
}
|
|
|
|
return escapeshellarg($command);
|
|
}
|
|
|
|
protected function arraySet(array &$array, string $key, $value): array
|
|
{
|
|
if (is_null($key)) {
|
|
return $array = $value;
|
|
}
|
|
|
|
$keys = explode('.', $key);
|
|
|
|
while (count($keys) > 1) {
|
|
$key = array_shift($keys);
|
|
|
|
// If the key doesn't exist at this depth, we will just create an empty array
|
|
// to hold the next value, allowing us to create the arrays to hold final
|
|
// values at the correct depth. Then we'll keep digging into the array.
|
|
if (! isset($array[$key]) || ! is_array($array[$key])) {
|
|
$array[$key] = [];
|
|
}
|
|
|
|
$array = &$array[$key];
|
|
}
|
|
|
|
$array[array_shift($keys)] = $value;
|
|
|
|
return $array;
|
|
}
|
|
|
|
public function initialPageNumber(int $initialPage = 1)
|
|
{
|
|
return $this
|
|
->setOption('initialPageNumber', ($initialPage - 1))
|
|
->pages($initialPage.'-');
|
|
}
|
|
|
|
public function getOutput(): ChromiumResult|null
|
|
{
|
|
return $this->chromiumResult;
|
|
}
|
|
|
|
private function isWindows()
|
|
{
|
|
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
|
}
|
|
|
|
private function getFinalContentsUrl(): string
|
|
{
|
|
$url = $this->html ? $this->createTemporaryHtmlFile() : $this->url;
|
|
|
|
return $url;
|
|
}
|
|
|
|
public function newHeadless(): self
|
|
{
|
|
return $this->setOption('newHeadless', true);
|
|
}
|
|
}
|