xref: /plugin/combo/ComboStrap/FetcherScreenshot.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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