Passed
Push — master ( f81141...6d828b )
by Jonas
11:08
created

UpdateCommand::fetchChromeVersionFromUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
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
        'win' => '.exe',
23
    ];
24
25
    /**
26
     * The name and signature of the console command.
27
     *
28
     * @var string
29
     */
30
    protected $signature = 'dusk:update {version?}
31
        {--detect= : Detect the installed Chrome/Chromium version, optionally in a custom path}';
32
33
    /**
34
     * The console command description.
35
     *
36
     * @var string
37
     */
38
    protected $description = 'Update the Dusk ChromeDriver binaries';
39
40
    /**
41
     * The path to the binaries directory.
42
     *
43
     * @var string
44
     */
45
    protected $directory = __DIR__.'/../../../laravel/dusk/bin/';
46
47
    /**
48
     * Create a new console command instance.
49
     *
50
     * @return void
51
     */
52 8
    public function __construct()
53
    {
54 8
        if (defined('DUSK_UPDATER_TEST')) {
55 8
            $this->directory = __DIR__.'/../tests/bin/';
56
        }
57
58 8
        parent::__construct();
59
    }
60
61
    /**
62
     * Execute the console command.
63
     *
64
     * @return int
65
     */
66 8
    public function handle()
67
    {
68 8
        $detect = $this->input->hasParameterOption('--detect');
69
70 8
        $os = $this->os();
71
72 8
        $version = $this->version($detect, $os);
73
74 8
        if ($version === false) {
75 2
            $this->error('Could not determine the ChromeDriver version.');
76
77 2
            return 1;
78
        }
79
80 6
        if ($detect && $this->checkVersion($os, $version)) {
0 ignored issues
show
Bug introduced by
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

80
        if ($detect && $this->checkVersion($os, /** @scrutinizer ignore-type */ $version)) {
Loading history...
81 1
            $this->info(
82 1
                sprintf('No update necessary, your ChromeDriver binary is already on version %s.', $version)
0 ignored issues
show
Bug introduced by
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

82
                sprintf('No update necessary, your ChromeDriver binary is already on version %s.', /** @scrutinizer ignore-type */ $version)
Loading history...
83 1
            );
84
        } else {
85 6
            $this->update($detect, $os, $version);
0 ignored issues
show
Bug introduced by
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

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