1<?php 2 3namespace ComboStrap; 4 5use ComboStrap\Web\Url; 6use Facebook\WebDriver\Chrome\ChromeOptions; 7use Facebook\WebDriver\Exception\NoSuchElementException; 8use Facebook\WebDriver\Exception\TimeoutException; 9use Facebook\WebDriver\Exception\UnsupportedOperationException; 10use Facebook\WebDriver\Exception\WebDriverCurlException; 11use Facebook\WebDriver\Remote\DesiredCapabilities; 12use Facebook\WebDriver\Remote\RemoteWebDriver; 13use Facebook\WebDriver\WebDriverBy; 14use Facebook\WebDriver\WebDriverDimension; 15 16/** 17 * Download chrome driver with the same version 18 * https://chromedriver.chromium.org/downloads 19 * 20 * Then run: 21 * ``` 22 * chromedriver.exe --port=4444 23 * ``` 24 */ 25class FetcherScreenshot extends FetcherImage 26{ 27 28 29 const WEB_DRIVER_ENDPOINT = 'http://localhost:4444/'; 30 const CANONICAL = "snapshot"; 31 const URL = "url"; 32 33 private Url $url; 34 35 36 public static function createSnapshotFromUrl(Url $urlToSnapshot): FetcherScreenshot 37 { 38 return (new FetcherScreenshot()) 39 ->setUrlToSnapshot($urlToSnapshot); 40 41 } 42 43 function getFetchUrl(Url $url = null): Url 44 { 45 $url = parent::getFetchUrl($url); 46 try { 47 $url->addQueryParameter(self::URL, $this->getUrlToSnapshot()); 48 } catch (ExceptionNotFound $e) { 49 // ok 50 } 51 return $url; 52 } 53 54 55 /** 56 * @throws ExceptionBadSyntax 57 * @throws ExceptionBadArgument 58 */ 59 public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherScreenshot 60 { 61 $urlString = $tagAttributes->getValue(self::URL); 62 if ($urlString === null) { 63 throw new ExceptionBadArgument("The `url` property is mandatory"); 64 } 65 $this->url = Url::createFromString($urlString); 66 parent::buildFromTagAttributes($tagAttributes); 67 return $this; 68 } 69 70 71 /** 72 * @return LocalPath 73 * @throws ExceptionNotFound 74 * @throws NoSuchElementException 75 * @throws TimeoutException 76 * @throws UnsupportedOperationException 77 * @throws ExceptionInternal 78 */ 79 function getFetchPath(): LocalPath 80 { 81 82 $url = $this->getUrlToSnapshot(); 83 84 $capabilities = DesiredCapabilities::chrome(); 85 $options = new ChromeOptions(); 86 $options->addArguments(['--headless']); 87 $options->addArguments(["window-size=1024,768"]); 88 89 /** 90 * 91 * https://docs.travis-ci.com/user/chrome#sandboxing 92 * For security reasons, Google Chrome is unable to provide sandboxing when it is running in the container-based environment. 93 * Error: 94 * * unknown error: Chrome failed to start: crashed 95 * * The SUID sandbox helper binary was found, but is not configured correctly 96 */ 97 if (PluginUtility::isCi()) { 98 $options->addArguments(["--no-sandbox"]); 99 } 100 101// $options->addArguments(['--start-fullscreen']); 102// $options->addArguments(['--start-maximized']); 103 104 $capabilities->setCapability(ChromeOptions::CAPABILITY, $options); 105 106 try { 107 $webDriver = RemoteWebDriver::create( 108 self::WEB_DRIVER_ENDPOINT, 109 $capabilities, 110 1000 111 ); 112 } /** @noinspection PhpRedundantCatchClauseInspection */ catch (WebDriverCurlException $e) { 113 // this exception is thrown even if it's not advertised 114 throw new ExceptionInternal("Web driver is not available at " . self::WEB_DRIVER_ENDPOINT . ". Did you run `chromedriver.exe --port=4444` ? Error: {$e->getMessage()}"); 115 } 116 try { 117 118 // navigate to the page 119 $webDriver->get($url->toAbsoluteUrlString()); 120 121 // wait until the target page is loaded 122 // https://github.com/php-webdriver/php-webdriver/wiki/HowTo-Wait 123 $webDriver->wait(2, 500)->until( 124 function () use ($webDriver) { 125 $state = $webDriver->executeScript("return document.readyState"); 126 return $state === "complete"; 127 }, 128 'The page was not loaded' 129 ); 130 131 /** 132 * Scroll to the end to download the image 133 */ 134 $body = $webDriver->findElement(WebDriverBy::tagName('body')); 135 $webDriver->executeScript("window.scrollTo(0, document.body.scrollHeight)"); 136 // Scrolling by sending keys does not work to download lazy loaded image 137 // $body->sendKeys(WebDriverKeys::encode([WebDriverKeys::CONTROL, WebDriverKeys::END])); 138 // Let the time to the image to download, we could also scroll to each image and get the status ? 139 $webDriver->wait(2, 500)->until( 140 function () use ($webDriver) { 141 $images = $webDriver->findElements(WebDriverBy::tagName("img")); 142 foreach ($images as $img) { 143 $complete = DataType::toBoolean($img->getAttribute("complete"), false); 144 if ($complete === true) { 145 $naturalHeight = DataType::toInteger($img->getAttribute("naturalHeight"), 0); 146 if ($naturalHeight !== 0) 147 return false; 148 } 149 } 150 return true; 151 }, 152 'The image were not loaded on time' 153 ); 154 155 /** 156 * Get the new dimension 157 */ 158 $bodyOffsetHeight = $body->getDomProperty("offsetHeight"); 159 $bodyOffsetWidth = $body->getDomProperty("offsetWidth"); 160 161 /** 162 * Because each page has a different height if you want 163 * to take a full height and width, you need to set it manually after 164 * the DOM has rendered 165 */ 166 $heightCorrection = 15; // don't know why but yeah 167 $fullPageDimension = new WebDriverDimension($bodyOffsetWidth, $bodyOffsetHeight + $heightCorrection); 168 $webDriver->manage() 169 ->window() 170 ->setSize($fullPageDimension); 171 172 173 if (!PluginUtility::isDevOrTest()) { 174 // Cache 175 $screenShotPath = FetcherCache::createFrom($this)->getFile(); 176 } else { 177 // Desktop 178 try { 179 $lastNameWithoutExtension = $url->getLastNameWithoutExtension(); 180 } catch (ExceptionNotFound $e) { 181 $lastNameWithoutExtension = $url->getHost(); 182 } 183 $screenShotPath = LocalPath::createDesktopDirectory() 184 ->resolve($lastNameWithoutExtension . "." . $this->getMime()->getExtension()); 185 } 186 $webDriver->takeScreenshot($screenShotPath); 187 return $screenShotPath; 188 189 } finally { 190 /** 191 * terminate the session and close the browser 192 */ 193 $webDriver->quit(); 194 } 195 196 } 197 198 199 /** 200 * @throws \ReflectionException 201 * @throws ExceptionNotFound 202 */ 203 function getBuster(): string 204 { 205 return FileSystems::getCacheBuster(ClassUtility::getClassPath(FetcherScreenshot::class)); 206 } 207 208 public function getMime(): Mime 209 { 210 return Mime::createFromExtension("png"); 211 } 212 213 public function getFetcherName(): string 214 { 215 return self::CANONICAL; 216 } 217 218 public function getIntrinsicWidth(): int 219 { 220 try { 221 return $this->getRequestedWidth(); 222 } catch (ExceptionNotFound $e) { 223 return 1024; 224 } 225 } 226 227 public function getIntrinsicHeight(): int 228 { 229 try { 230 return $this->getRequestedHeight(); 231 } catch (ExceptionNotFound $e) { 232 return 768; 233 } 234 } 235 236 private function setUrlToSnapshot(Url $urlToSnapshot): FetcherScreenshot 237 { 238 $this->url = $urlToSnapshot; 239 return $this; 240 } 241 242 /** 243 * @throws ExceptionNotFound 244 */ 245 private function getUrlToSnapshot(): Url 246 { 247 if (!isset($this->url)) { 248 throw new ExceptionNotFound("No url to snapshot could be determined"); 249 } 250 return $this->url; 251 } 252 253 public function getLabel(): string 254 { 255 return self::CANONICAL; 256 } 257 258} 259