Issues (7)

src/UpdateCommand.php (4 issues)

1
<?php
2
3
namespace Staudenmeir\DuskUpdater;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Support\Str;
7
use Symfony\Component\Process\Process;
8
use ZipArchive;
9
10
class UpdateCommand extends Command
11
{
12
    use DetectsChromeVersion;
13
14
    /**
15
     * The file extensions of the ChromeDriver binaries.
16
     *
17
     * @var array
18
     */
19
    public static $extensions = [
20
        'linux' => '',
21
        'mac' => '',
22
        'mac-intel' => '',
23
        'mac-arm' => '',
24
        'win' => '.exe',
25
    ];
26
27
    /**
28
     * The name and signature of the console command.
29
     *
30
     * @var string
31
     */
32
    protected $signature = 'dusk:update {version?}
33
        {--detect= : Detect the installed Chrome/Chromium version, optionally in a custom path}';
34
35
    /**
36
     * The console command description.
37
     *
38
     * @var string
39
     */
40
    protected $description = 'Update the Dusk ChromeDriver binaries';
41
42
    /**
43
     * The path to the binaries directory.
44
     *
45
     * @var string
46
     */
47
    protected $directory = __DIR__.'/../../../laravel/dusk/bin/';
48
49
    /**
50
     * Create a new console command instance.
51
     *
52
     * @return void
53
     */
54 8
    public function __construct()
55
    {
56 8
        if (defined('DUSK_UPDATER_TEST')) {
57 8
            $this->directory = __DIR__.'/../tests/bin/';
58
        }
59
60 8
        parent::__construct();
61
    }
62
63
    /**
64
     * Execute the console command.
65
     *
66
     * @return int
67
     */
68 8
    public function handle()
69
    {
70 8
        $detect = $this->input->hasParameterOption('--detect');
71
72 8
        $os = $this->os();
73
74 8
        $version = $this->version($detect, $os);
75
76 8
        if ($version === false) {
77 2
            $this->error('Could not determine the ChromeDriver version.');
78
79 2
            return 1;
80
        }
81
82 6
        if ($detect && $this->checkVersion($os, $version)) {
0 ignored issues
show
It seems like $version can also be of type true; however, parameter $version of Staudenmeir\DuskUpdater\...Command::checkVersion() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
        if ($detect && $this->checkVersion($os, /** @scrutinizer ignore-type */ $version)) {
Loading history...
83 1
            $this->info(
84 1
                sprintf('No update necessary, your ChromeDriver binary is already on version %s.', $version)
0 ignored issues
show
It seems like $version can also be of type true; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

84
                sprintf('No update necessary, your ChromeDriver binary is already on version %s.', /** @scrutinizer ignore-type */ $version)
Loading history...
85 1
            );
86
        } else {
87 6
            $this->update($detect, $os, $version);
0 ignored issues
show
It seems like $version can also be of type true; however, parameter $version of Staudenmeir\DuskUpdater\UpdateCommand::update() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

87
            $this->update($detect, $os, /** @scrutinizer ignore-type */ $version);
Loading history...
88
89 6
            $this->info(
90 6
                sprintf('ChromeDriver %s successfully updated to version %s.', $detect ? 'binary' : 'binaries', $version)
91 6
            );
92
        }
93
94 6
        return 0;
95
    }
96
97
    /**
98
     * Get the desired ChromeDriver version.
99
     *
100
     * @param bool $detect
101
     * @param string $os
102
     * @return string|bool
103
     */
104 8
    protected function version($detect, $os)
105
    {
106 8
        if ($detect) {
107 3
            $version = $this->chromeVersion($os);
108
109 3
            if ($version === false) {
110 3
                return false;
111
            }
112
        } else {
113 5
            $version = $this->argument('version');
114
115 5
            if (!$version) {
116 1
                return $this->latestVersion();
117
            }
118
119 4
            if (!ctype_digit($version)) {
120 1
                return $version;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $version also could return the type array which is incompatible with the documented return type boolean|string.
Loading history...
121
            }
122
123 3
            $version = (int) $version;
124
        }
125
126 5
        if ($version < 70) {
127 1
            return $this->legacyVersion($version);
128 4
        } elseif ($version < 115) {
129 1
            return $this->fetchChromeVersionFromUrl($version);
130
        }
131
132 3
        $milestones = $this->resolveChromeVersionsPerMilestone();
133
134 3
        return $milestones['milestones'][$version]['version'] ?? false;
135
    }
136
137
    /**
138
     * Get the latest stable ChromeDriver version.
139
     *
140
     * @return string|false
141
     */
142 1
    protected function latestVersion()
143
    {
144 1
        $versions = json_decode(
145 1
            file_get_contents(
146 1
                'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json'
147 1
            ),
148 1
            true
149 1
        );
150
151 1
        return $versions['channels']['Stable']['version'] ?? false;
152
    }
153
154
    /**
155
     * Get the Chrome version from URL.
156
     *
157
     * @param int $version
158
     * @return string
159
     */
160 1
    protected function fetchChromeVersionFromUrl($version)
161
    {
162 1
        return trim((string) file_get_contents(
163 1
            sprintf('https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%d', $version)
164 1
        ));
165
    }
166
167
    /**
168
     * Get the Chrome versions per milestone.
169
     *
170
     * @return array
171
     */
172 4
    protected function resolveChromeVersionsPerMilestone()
173
    {
174 4
        return json_decode(
175 4
            file_get_contents(
176 4
                'https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone-with-downloads.json'
177 4
            ),
178 4
            true
179 4
        );
180
    }
181
182
    /**
183
     * Resolve the download URL.
184
     *
185
     * @param string $version
186
     * @param string $os
187
     * @return string
188
     */
189 6
    protected function resolveChromeDriverDownloadUrl($version, $os)
190
    {
191 6
        $slug = static::chromeDriverSlug($os, $version);
192
193 6
        if (version_compare($version, '115.0', '<')) {
194 3
            return sprintf('https://chromedriver.storage.googleapis.com/%s/chromedriver_%s.zip', $version, $slug);
195
        }
196
197 3
        $milestone = (int) $version;
198
199 3
        $versions = $this->resolveChromeVersionsPerMilestone();
200
201 3
        $chromedrivers = $versions['milestones'][$milestone]['downloads']['chromedriver'];
202
203 3
        return collect($chromedrivers)->firstWhere('platform', $slug)['url'];
204
    }
205
206
    /**
207
     * Get the ChromeDriver version for a legacy version of Chrome.
208
     *
209
     * @param int $version
210
     * @return string
211
     */
212 1
    protected function legacyVersion($version)
213
    {
214 1
        $legacy = file_get_contents(__DIR__.'/../resources/legacy.json');
215
216 1
        $legacy = json_decode($legacy, true);
217
218 1
        return $legacy[$version];
219
    }
220
221
    /**
222
     * Check whether the ChromeDriver binary needs to be updated.
223
     *
224
     * @param string $os
225
     * @param string $version
226
     * @return bool
227
     */
228 2
    protected function checkVersion($os, $version)
229
    {
230 2
        $binary = $this->directory.'chromedriver-'.$os.static::$extensions[$os];
231
232 2
        $process = new Process([$binary, '--version']);
233
234 2
        $process->run();
235
236 2
        preg_match('/[\d.]+/', $process->getOutput(), $matches);
237
238 2
        return isset($matches[0]) ? $matches[0] === $version : false;
239
    }
240
241
    /**
242
     * Update the ChromeDriver binaries.
243
     *
244
     * @param bool $detect
245
     * @param string $currentOs
246
     * @param string $version
247
     * @return void
248
     */
249 6
    protected function update($detect, $currentOs, $version)
250
    {
251 6
        foreach (static::all() as $os) {
252 6
            if ($detect && $os !== $currentOs) {
253 2
                continue;
254
            }
255
256 6
            if (version_compare($version, '115.0', '<') && $os === 'mac-arm') {
257 3
                continue;
258
            }
259
260 6
            $archive = $this->download($version, $os);
261
262 6
            $binary = $this->extract($version, $archive);
263
264 6
            $this->rename($binary, $os);
265
        }
266
    }
267
268
    /**
269
     * Download the ChromeDriver archive.
270
     *
271
     * @param string $version
272
     * @param string $os
273
     * @return string
274
     */
275 6
    protected function download($version, $os)
276
    {
277 6
        $archive = $this->directory.'chromedriver.zip';
278
279 6
        $url = $this->resolveChromeDriverDownloadUrl($version, $os);
280
281 6
        $ch = curl_init();
282 6
        curl_setopt($ch, CURLOPT_URL, $url);
283 6
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);
284 6
        curl_setopt($ch, CURLOPT_HTTPGET, true);
285 6
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
286 6
        file_put_contents($archive, curl_exec($ch));
287 6
        curl_close($ch);
288
289 6
        return $archive;
290
    }
291
292
    /**
293
     * Extract the ChromeDriver binary from the archive and delete the archive.
294
     *
295
     * @param string $version
296
     * @param string $archive
297
     * @return string
298
     */
299 6
    protected function extract($version, $archive)
300
    {
301 6
        $zip = new ZipArchive();
302
303 6
        $zip->open($archive);
304
305 6
        $zip->extractTo($this->directory);
306
307 6
        $binary = $zip->getNameIndex(version_compare($version, '115.0', '<') ? 0 : 1);
308
309 6
        $zip->close();
310
311 6
        unlink($archive);
312
313 6
        return $binary;
314
    }
315
316
    /**
317
     * Rename the ChromeDriver binary and make it executable.
318
     *
319
     * @param string $binary
320
     * @param string $os
321
     * @return void
322
     */
323 6
    protected function rename($binary, $os)
324
    {
325 6
        $binary = str_replace(DIRECTORY_SEPARATOR, '/', $binary);
326
327 6
        $newName = Str::contains($binary, '/')
328 3
            ? Str::after(str_replace('chromedriver', 'chromedriver-'.$os, $binary), '/')
329 6
            : str_replace('chromedriver', 'chromedriver-'.$os, $binary);
330
331 6
        rename($this->directory.$binary, $this->directory.$newName);
332
333 6
        chmod($this->directory.$newName, 0755);
334
    }
335
}
336