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