Completed
Pull Request — master (#298)
by Eric
12:28
created

CKEditorInstaller::extractFile()   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.116

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 13
cts 15
cp 0.8667
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 13
nc 9
nop 6
crap 7.116
1
<?php
2
3
/*
4
 * This file is part of the Ivory CKEditor package.
5
 *
6
 * (c) Eric GELOEN <[email protected]>
7
 *
8
 * For the full copyright and license information, please read the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Ivory\CKEditorBundle\Installer;
13
14
/**
15
 * @author GeLo <[email protected]>
16
 */
17
class CKEditorInstaller
18
{
19
    const RELEASE_BASIC = 'basic';
20
    const RELEASE_FULL = 'full';
21
    const RELEASE_STANDARD = 'standard';
22
23
    const VERSION_LATEST = 'latest';
24
25
    const CLEAR_DROP = 'drop';
26
    const CLEAR_KEEP = 'keep';
27
    const CLEAR_ABORT = 'abort';
28
29
    const NOTIFY_CLEAR = 'clear';
30
    const NOTIFY_CLEAR_ARCHIVE = 'clear-archive';
31
    const NOTIFY_CLEAR_COMPLETE = 'clear-complete';
32
    const NOTIFY_CLEAR_PROGRESS = 'clear-progress';
33
    const NOTIFY_CLEAR_QUESTION = 'clear-question';
34
    const NOTIFY_CLEAR_SIZE = 'clear-size';
35
36
    const NOTIFY_DOWNLOAD = 'download';
37
    const NOTIFY_DOWNLOAD_COMPLETE = 'download-complete';
38
    const NOTIFY_DOWNLOAD_PROGRESS = 'download-progress';
39
    const NOTIFY_DOWNLOAD_SIZE = 'download-size';
40
41
    const NOTIFY_EXTRACT = 'extract';
42
    const NOTIFY_EXTRACT_COMPLETE = 'extract-complete';
43
    const NOTIFY_EXTRACT_PROGRESS = 'extract-progress';
44
    const NOTIFY_EXTRACT_SIZE = 'extract-size';
45
46
    /**
47
     * @var string
48
     */
49
    private static $archive = 'https://github.com/ckeditor/ckeditor-releases/archive/%s/%s.zip';
50
51
    /**
52
     * @var string
53
     */
54
    private $path;
55
56
    /**
57
     * @var string
58
     */
59
    private $release;
60
61
    /**
62
     * @var string
63
     */
64
    private $version;
65
66
    /**
67
     * @var string
68
     */
69
    private $clear;
70
71
    /**
72
     * @var string[]
73
     */
74
    private $excludes;
75
76
    /**
77
     * @param string|null $path
78
     * @param string|null $release
79
     * @param string|null $version
80
     * @param string|null $clear
81
     * @param string[]    $excludes
82
     */
83 91
    public function __construct(
84
        $path = null,
85
        $release = null,
86
        $version = null,
87
        $clear = null,
88
        array $excludes = ['samples']
89
    ) {
90 91
        $this->path = $path ?: dirname(__DIR__).'/Resources/public';
91 91
        $this->release = $release ?: self::RELEASE_FULL;
92 91
        $this->version = $version ?: self::VERSION_LATEST;
93 91
        $this->clear = $clear ?: self::CLEAR_DROP;
94 91
        $this->excludes = $excludes;
95 91
    }
96
97
    /**
98
     * @param mixed[] $options
99
     *
100
     * @return bool
101
     */
102 91
    public function install(array $options = [])
103
    {
104 91
        $path = rtrim(isset($options['path']) ? $options['path'] : $this->path, '/');
105 91
        $clear = isset($options['clear']) ? $options['clear'] : null;
106 91
        $notifier = isset($options['notifier']) ? $options['notifier'] : null;
107
108 91
        if (file_exists($path.'/ckeditor.js') && $this->clear($path, $clear, $notifier) === self::CLEAR_ABORT) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $this->clear($path, $clear, $notifier) (integer) and self::CLEAR_ABORT (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
109 9
            return false;
110
        }
111
112 91
        $release = isset($options['release']) ? $options['release'] : $this->release;
113 91
        $version = isset($options['version']) ? $options['version'] : $this->version;
114 91
        $excludes = isset($options['excludes']) ? $options['excludes'] : $this->excludes;
115
116 91
        $zip = $this->download($release, $version, $notifier);
117 91
        $this->extract($zip, $path, $release, $version, $excludes, $notifier);
118
119 91
        return true;
120
    }
121
122
    /**
123
     * @param string        $path
124
     * @param int|null      $clear
125
     * @param callable|null $notifier
126
     *
127
     * @return int
128
     */
129 37
    private function clear($path, $clear = null, callable $notifier = null)
130
    {
131 37
        if ($clear === null) {
132 10
            $clear = $this->notify($notifier, self::NOTIFY_CLEAR, $path);
133 8
        }
134
135 37
        if ($clear === null) {
136 9
            $clear = $this->clear;
137 7
        }
138
139 37
        if ($clear === self::CLEAR_DROP) {
140 19
            $files = new \RecursiveIteratorIterator(
141 19
                new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
142 4
                \RecursiveIteratorIterator::CHILD_FIRST
143 15
            );
144
145 19
            $this->notify($notifier, self::NOTIFY_CLEAR_SIZE, iterator_count($files));
146
147 19
            foreach ($files as $file) {
148 19
                $filePath = $file->getRealPath();
149 19
                $this->notify($notifier, self::NOTIFY_CLEAR_PROGRESS, $filePath);
150
151 19
                if ($dir = $file->isDir()) {
152 19
                    $success = @rmdir($filePath);
153 15
                } else {
154 19
                    $success = @unlink($filePath);
155
                }
156
157 19
                if (!$success) {
158
                    throw $this->createException(sprintf(
159
                        'Unable to remove the %s "%s".',
160 2
                        $dir ? 'directory' : 'file',
161
                        $filePath
162
                    ));
163
                }
164 15
            }
165
166 19
            $this->notify($notifier, self::NOTIFY_CLEAR_COMPLETE);
167 15
        }
168
169 37
        return $clear;
170
    }
171
172
    /**
173
     * @param string        $release
174
     * @param string        $version
175
     * @param callable|null $notifier
176
     *
177
     * @return string
178
     */
179 91
    private function download($release, $version, callable $notifier = null)
180
    {
181 91
        $url = sprintf(self::$archive, $release, $version);
182 91
        $this->notify($notifier, self::NOTIFY_DOWNLOAD, $url);
183
184 91
        $zip = @file_get_contents($url, false, $this->createStreamContext($notifier));
185
186 91
        if ($zip === false) {
187
            throw $this->createException(sprintf('Unable to download CKEditor ZIP archive from "%s".', $url));
188
        }
189
190 91
        $path = tempnam(sys_get_temp_dir(), sprintf('ckeditor-%s-%s-', $release, $version)).'.zip';
191
192 91
        if (!@file_put_contents($path, $zip)) {
193
            throw $this->createException(sprintf('Unable to write CKEditor ZIP archive to "%s".', $path));
194
        }
195
196 91
        $this->notify($notifier, self::NOTIFY_DOWNLOAD_COMPLETE, $path);
197
198 91
        return $path;
199
    }
200
201
    /**
202
     * @param callable|null $notifier
203
     *
204
     * @return resource
205
     */
206 91
    private function createStreamContext(callable $notifier = null)
207
    {
208 91
        return stream_context_create([], [
209 91
            'notification' => function (
210
                $code,
211
                $severity,
212
                $message,
213
                $messageCode,
214
                $transferred,
215
                $size
216
            ) use ($notifier) {
217 91
                if ($notifier === null) {
218 81
                    return;
219
                }
220
221
                switch ($code) {
222 10
                    case STREAM_NOTIFY_FILE_SIZE_IS:
223 10
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_SIZE, $size);
224 10
                        break;
225
226 10
                    case STREAM_NOTIFY_PROGRESS:
227 10
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_PROGRESS, $transferred);
228 10
                        break;
229
                }
230 91
            },
231 71
        ]);
232
    }
233
234
    /**
235
     * @param string        $origin
236
     * @param string        $destination
237
     * @param string        $release
238
     * @param string        $version
239
     * @param string[]      $excludes
240
     * @param callable|null $notifier
241
     */
242 91
    private function extract($origin, $destination, $release, $version, array $excludes, callable $notifier = null)
243
    {
244 91
        $this->notify($notifier, self::NOTIFY_EXTRACT, $destination);
245
246 91
        $zip = new \ZipArchive();
247 91
        $zip->open($origin);
248
249 91
        $this->notify($notifier, self::NOTIFY_EXTRACT_SIZE, $zip->numFiles);
250
251 91
        $offset = 20 + strlen($release) + strlen($version);
252
253 91
        for ($i = 0; $i < $zip->numFiles; ++$i) {
254 91
            $this->extractFile(
255 91
                $file = $zip->getNameIndex($i),
256 81
                substr($file, $offset),
257 81
                $origin,
258 81
                $destination,
259 81
                $excludes,
260 10
                $notifier
261 71
            );
262 71
        }
263
264 91
        $zip->close();
265
266 91
        $this->notify($notifier, self::NOTIFY_EXTRACT_COMPLETE);
267 91
        $this->notify($notifier, self::NOTIFY_CLEAR_ARCHIVE, $origin);
268
269 91
        if (!@unlink($origin)) {
270
            throw $this->createException(sprintf('Unable to remove the CKEditor ZIP archive "%s".', $origin));
271
        }
272 91
    }
273
274
    /**
275
     * @param string        $file
276
     * @param string        $rewrite
277
     * @param string        $origin
278
     * @param string        $destination
279
     * @param string[]      $excludes
280
     * @param callable|null $notifier
281
     */
282 91
    private function extractFile($file, $rewrite, $origin, $destination, array $excludes, callable $notifier = null)
283
    {
284 91
        $this->notify($notifier, self::NOTIFY_EXTRACT_PROGRESS, $rewrite);
285
286 91
        $from = 'zip://'.$origin.'#'.$file;
287 91
        $to = $destination.'/'.$rewrite;
288
289 91
        foreach ($excludes as $exclude) {
290 81
            if (strpos($rewrite, $exclude) === 0) {
291 81
                return;
292
            }
293 71
        }
294
295 91
        if (substr($from, -1) === '/') {
296 91
            if (!is_dir($to) && !@mkdir($to)) {
297
                throw $this->createException(sprintf('Unable to create the directory "%s".', $to));
298
            }
299
300 91
            return;
301
        }
302
303 91
        if (!@copy($from, $to)) {
304
            throw $this->createException(sprintf('Unable to extract the file "%s" to "%s".', $file, $to));
305
        }
306 91
    }
307
308
    /**
309
     * @param callable|null $notifier
310
     * @param string        $type
311
     * @param mixed         $data
312
     *
313
     * @return mixed
314
     */
315 91
    private function notify(callable $notifier = null, $type, $data = null)
316
    {
317 91
        if ($notifier !== null) {
318 10
            return $notifier($type, $data);
319
        }
320 81
    }
321
322
    /**
323
     * @param string $message
324
     *
325
     * @return \RuntimeException
326
     */
327
    private function createException($message)
328
    {
329
        $error = error_get_last();
330
331
        if (isset($error['message'])) {
332
            $message .= sprintf(' (%s)', $error['message']);
333
        }
334
335
        return new \RuntimeException($message);
336
    }
337
}
338