Passed
Pull Request — master (#19)
by Jonathan
08:43
created

UpdateCommand::resolveChromeVersionsPerMilestone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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