Passed
Pull Request — master (#19)
by Jonathan
09:23
created

UpdateCommand::resolveChromeDriverDownloadUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 15
ccs 4
cts 4
cp 1
crap 2
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
    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
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'];
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
            $archive = $this->download($version, $os);
255 6
256
            $binary = $this->extract($version, $archive);
257 6
258
            $this->rename($binary, $os);
259 6
        }
260
    }
261 6
262
    /**
263 6
     * Download the ChromeDriver archive.
264
     *
265 6
     * @param string $version
266
     * @param string $os
267 6
     * @return string
268
     */
269 6
    protected function download($version, $os)
270
    {
271
        $archive = $this->directory.'chromedriver.zip';
272
273
        $url = $this->resolveChromeDriverDownloadUrl($version, $os);
274
275
        $ch = curl_init();
276
        curl_setopt($ch, CURLOPT_URL, $url);
277
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);
278
        curl_setopt($ch, CURLOPT_HTTPGET, true);
279 6
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
280
        file_put_contents($archive, curl_exec($ch));
281 6
        curl_close($ch);
282
283 6
        return $archive;
284
    }
285 6
286 6
    /**
287
     * Extract the ChromeDriver binary from the archive and delete the archive.
288
     *
289
     * @param string $version
290
     * @param string $archive
291
     * @return string
292
     */
293 7
    protected function extract($version, $archive)
294
    {
295 7
        $zip = new ZipArchive;
296
297 7
        $zip->open($archive);
298
299
        $zip->extractTo($this->directory);
300
301
        $binary = $zip->getNameIndex(version_compare($version, '115.0', '<') ? 0 : 1);
302
303
        $zip->close();
304
305
        unlink($archive);
306
307
        return $binary;
308
    }
309
310
    /**
311
     * Rename the ChromeDriver binary and make it executable.
312
     *
313
     * @param string $binary
314
     * @param string $os
315
     * @return void
316
     */
317
    protected function rename($binary, $os)
318
    {
319
        $binary = str_replace(DIRECTORY_SEPARATOR, '/', $binary);
320
321
        $newName = Str::contains($binary, '/')
322
            ? Str::after(str_replace('chromedriver', 'chromedriver-'.$os, $binary), '/')
323
            : str_replace('chromedriver', 'chromedriver-'.$os, $binary);
324
325
        rename($this->directory.$binary, $this->directory.$newName);
326
327
        chmod($this->directory.$newName, 0755);
328
    }
329
330
    /**
331
     * Detect the current operating system.
332
     *
333
     * @return string
334
     */
335
    protected function os()
336
    {
337
        return PHP_OS === 'WINNT' || Str::contains(php_uname(), 'Microsoft')
338
            ? 'win'
339
            : (PHP_OS === 'Darwin' ? 'mac' : 'linux');
340
    }
341
}
342