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

UpdateCommand::fetchChromeVersionFromUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 2
cts 2
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
            return 1;
76
        }
77
78
        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

78
        if ($detect && $this->checkVersion($os, /** @scrutinizer ignore-type */ $version)) {
Loading history...
79
            $this->info(
80
                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

80
                sprintf('No update necessary, your ChromeDriver binary is already on version %s.', /** @scrutinizer ignore-type */ $version)
Loading history...
81
            );
82
        } else {
83
            $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

83
            $this->update($detect, $os, /** @scrutinizer ignore-type */ $version);
Loading history...
84 7
85
            $this->info(
86 7
                sprintf('ChromeDriver %s successfully updated to version %s.', $detect ? 'binary' : 'binaries', $version)
87 7
            );
88
        }
89
90 7
        return 0;
91 7
    }
92
93
    /**
94
     * Get the desired ChromeDriver version.
95
     *
96
     * @param bool $detect
97
     * @param string $os
98 7
     * @return string|bool
99
     */
100 7
    protected function version($detect, $os)
101
    {
102 7
        if ($detect) {
103
            $version = $this->chromeVersion($os);
104 7
105
            if ($version === false) {
106 7
                return false;
107 1
            }
108
        } else {
109
            $version = $this->argument('version');
110 6
111 1
            if (!$version) {
112 1
                return $this->latestVersion();
113
            }
114
115 6
            if (!ctype_digit($version)) {
116
                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...
117 6
            }
118 6
119
            $version = (int) $version;
120
        }
121
122 6
        if ($version < 70) {
123
            return $this->legacyVersion($version);
124
        } elseif ($version < 115) {
125
            return $this->fetchChromeVersionFromUrl($version);
126
        }
127
128
        $milestones = $this->resolveChromeVersionsPerMilestone();
129
130
        return $milestones['milestones'][$version]['version']
131
            ?? $this->error('Could not determine the ChromeDriver version.');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->error('Could not ...ChromeDriver version.') targeting Illuminate\Console\Command::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
132 7
    }
133
134 7
    /**
135 3
     * Get the latest stable ChromeDriver version.
136
     *
137 3
     * @return string
138 3
     */
139
    protected function latestVersion()
140
    {
141 4
        $versions = json_decode(
142
            file_get_contents(
143 4
                'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json'
144 1
            ),
145
            true
146
        );
147 3
148 1
        return $versions['channels']['Stable']['version'];
149
    }
150
151 2
    /**
152
     * Get the Chrome version from URL.
153
     *
154 4
     * @param int $version
155 1
     * @return string
156
     */
157
    protected function fetchChromeVersionFromUrl($version)
158 3
    {
159
        return trim((string) file_get_contents(
160 3
            sprintf('https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%d', $version)
161
        ));
162
    }
163
164
    /**
165
     * Get the Chrome versions per milestone.
166
     *
167
     * @return array
168 1
     */
169
    protected function resolveChromeVersionsPerMilestone()
170 1
    {
171
        return json_decode(
172
            file_get_contents(
173
                'https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone-with-downloads.json'
174
            ),
175
            true
176
        );
177
    }
178
179 1
    /**
180
     * Resolve the download URL.
181 1
     *
182
     * @param int $version
183 1
     * @param string $os
184
     * @return string
185 1
     */
186
    protected function resolveChromeDriverDownloadUrl($version, $os)
187
    {
188
        $slug = static::chromeDriverSlug($os, $version);
189
190
        if (version_compare($version, '115.0', '<')) {
191
            return sprintf('https://chromedriver.storage.googleapis.com/%s/chromedriver_%s.zip', $version, $slug);
192
        }
193
194
        $milestone = (int) $version;
195 2
196
        $versions = $this->resolveChromeVersionsPerMilestone();
197 2
198
        $chromedrivers = $versions['milestones'][$milestone]['downloads']['chromedriver']
199 2
            ?? $this->error('Could not get the ChromeDriver version.');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->error('Could not ...ChromeDriver version.') targeting Illuminate\Console\Command::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
200
201 2
        return collect($chromedrivers)->firstWhere('platform', $slug)['url']
202
            ?? $this->error('Could not get the ChromeDriver version.');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->error('Could not ...ChromeDriver version.') targeting Illuminate\Console\Command::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
203 2
    }
204
205 2
    /**
206
     * Get the ChromeDriver version for a legacy version of Chrome.
207
     *
208
     * @param int $version
209
     * @return string
210
     */
211
    protected function legacyVersion($version)
212
    {
213
        $legacy = file_get_contents(__DIR__.'/../resources/legacy.json');
214
215
        $legacy = json_decode($legacy, true);
216 6
217
        return $legacy[$version];
218 6
    }
219 6
220 2
    /**
221
     * Check whether the ChromeDriver binary needs to be updated.
222
     *
223 6
     * @param string $os
224
     * @param string $version
225 6
     * @return bool
226
     */
227 6
    protected function checkVersion($os, $version)
228
    {
229 6
        $binary = $this->directory.'chromedriver-'.$os.static::$extensions[$os];
230
231
        $process = new Process([$binary, '--version']);
232
233
        $process->run();
234
235
        preg_match('/[\d.]+/', $process->getOutput(), $matches);
236
237
        return isset($matches[0]) ? $matches[0] === $version : false;
238 6
    }
239
240 6
    /**
241
     * Update the ChromeDriver binaries.
242 6
     *
243
     * @param bool $detect
244 6
     * @param string $currentOs
245
     * @param string $version
246 6
     * @return void
247
     */
248
    protected function update($detect, $currentOs, $version)
249
    {
250
        foreach (static::all() as $os) {
251
            if ($detect && $os !== $currentOs) {
252
                continue;
253
            }
254
255 6
            $archive = $this->download($version, $os);
256
257 6
            $binary = $this->extract($version, $archive);
258
259 6
            $this->rename($binary, $os);
260
        }
261 6
    }
262
263 6
    /**
264
     * Download the ChromeDriver archive.
265 6
     *
266
     * @param string $version
267 6
     * @param string $os
268
     * @return string
269 6
     */
270
    protected function download($version, $os)
271
    {
272
        $archive = $this->directory.'chromedriver.zip';
273
274
        $url = $this->resolveChromeDriverDownloadUrl($version, $os);
0 ignored issues
show
Bug introduced by
$version of type string is incompatible with the type integer expected by parameter $version of Staudenmeir\DuskUpdater\...romeDriverDownloadUrl(). ( Ignorable by Annotation )

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

274
        $url = $this->resolveChromeDriverDownloadUrl(/** @scrutinizer ignore-type */ $version, $os);
Loading history...
275
276
        $ch = curl_init();
277
        curl_setopt($ch, CURLOPT_URL, $url);
278
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);
279 6
        curl_setopt($ch, CURLOPT_HTTPGET, true);
280
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
281 6
        file_put_contents($archive, curl_exec($ch));
282
        curl_close($ch);
283 6
284
        return $archive;
285 6
    }
286 6
287
    /**
288
     * Extract the ChromeDriver binary from the archive and delete the archive.
289
     *
290
     * @param string $version
291
     * @param string $archive
292
     * @return string
293 7
     */
294
    protected function extract($version, $archive)
295 7
    {
296
        $zip = new ZipArchive;
297 7
298
        $zip->open($archive);
299
300
        $zip->extractTo($this->directory);
301
302
        $binary = $zip->getNameIndex(version_compare($version, '115.0', '<') ? 0 : 1);
303
304
        $zip->close();
305
306
        unlink($archive);
307
308
        return $binary;
309
    }
310
311
    /**
312
     * Rename the ChromeDriver binary and make it executable.
313
     *
314
     * @param string $binary
315
     * @param string $os
316
     * @return void
317
     */
318
    protected function rename($binary, $os)
319
    {
320
        $binary = str_replace(DIRECTORY_SEPARATOR, '/', $binary);
321
322
        $newName = Str::contains($binary, '/')
323
            ? Str::after(str_replace('chromedriver', 'chromedriver-'.$os, $binary), '/')
324
            : str_replace('chromedriver', 'chromedriver-'.$os, $binary);
325
326
        rename($this->directory.$binary, $this->directory.$newName);
327
328
        chmod($this->directory.$newName, 0755);
329
    }
330
331
    /**
332
     * Detect the current operating system.
333
     *
334
     * @return string
335
     */
336
    protected function os()
337
    {
338
        return PHP_OS === 'WINNT' || Str::contains(php_uname(), 'Microsoft')
339
            ? 'win'
340
            : (PHP_OS === 'Darwin' ? 'mac' : 'linux');
341
    }
342
}
343