Completed
Push — master ( c86a14...cd5fc7 )
by Eric
29:59
created

CKEditorInstaller::createException()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 6
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 ($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 91
    private function clear($path, $clear = null, callable $notifier = null)
130
    {
131 91
        if (!file_exists($path.'/ckeditor.js')) {
132 91
            return self::CLEAR_DROP;
133
        }
134
135 37
        if ($clear === null) {
136 10
            $clear = $this->notify($notifier, self::NOTIFY_CLEAR, $path);
137 8
        }
138
139 37
        if ($clear === null) {
140 9
            $clear = $this->clear;
141 7
        }
142
143 37
        if ($clear === self::CLEAR_DROP) {
144 19
            $files = new \RecursiveIteratorIterator(
145 19
                new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
146 4
                \RecursiveIteratorIterator::CHILD_FIRST
147 15
            );
148
149 19
            $this->notify($notifier, self::NOTIFY_CLEAR_SIZE, iterator_count($files));
150
151 19
            foreach ($files as $file) {
152 19
                $filePath = $file->getRealPath();
153 19
                $this->notify($notifier, self::NOTIFY_CLEAR_PROGRESS, $filePath);
154
155 19
                if ($dir = $file->isDir()) {
156 19
                    $success = @rmdir($filePath);
157 15
                } else {
158 19
                    $success = @unlink($filePath);
159
                }
160
161 19
                if (!$success) {
162
                    throw $this->createException(sprintf(
163
                        'Unable to remove the %s "%s".',
164 2
                        $dir ? 'directory' : 'file',
165
                        $filePath
166
                    ));
167
                }
168 15
            }
169
170 19
            $this->notify($notifier, self::NOTIFY_CLEAR_COMPLETE);
171 15
        }
172
173 37
        return $clear;
174
    }
175
176
    /**
177
     * @param string        $release
178
     * @param string        $version
179
     * @param callable|null $notifier
180
     *
181
     * @return string
182
     */
183 91
    private function download($release, $version, callable $notifier = null)
184
    {
185 91
        $url = sprintf(self::$archive, $release, $version);
186 91
        $this->notify($notifier, self::NOTIFY_DOWNLOAD, $url);
187
188 91
        $zip = @file_get_contents($url, false, $this->createStreamContext($notifier));
189
190 91
        if ($zip === false) {
191
            throw $this->createException(sprintf('Unable to download CKEditor ZIP archive from "%s".', $url));
192
        }
193
194 91
        $path = tempnam(sys_get_temp_dir(), sprintf('ckeditor-%s-%s-', $release, $version)).'.zip';
195
196 91
        if (!@file_put_contents($path, $zip)) {
197
            throw $this->createException(sprintf('Unable to write CKEditor ZIP archive to "%s".', $path));
198
        }
199
200 91
        $this->notify($notifier, self::NOTIFY_DOWNLOAD_COMPLETE, $path);
201
202 91
        return $path;
203
    }
204
205
    /**
206
     * @param callable|null $notifier
207
     *
208
     * @return resource
209
     */
210 91
    private function createStreamContext(callable $notifier = null)
211
    {
212 91
        return stream_context_create([], [
213 91
            'notification' => function (
214
                $code,
215
                $severity,
216
                $message,
217
                $messageCode,
218
                $transferred,
219
                $size
220
            ) use ($notifier) {
221 91
                if ($notifier === null) {
222 81
                    return;
223
                }
224
225
                switch ($code) {
226 10
                    case STREAM_NOTIFY_FILE_SIZE_IS:
227 10
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_SIZE, $size);
228 10
                        break;
229
230 10
                    case STREAM_NOTIFY_PROGRESS:
231 10
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_PROGRESS, $transferred);
232 10
                        break;
233
                }
234 91
            },
235 71
        ]);
236
    }
237
238
    /**
239
     * @param string        $origin
240
     * @param string        $destination
241
     * @param string        $release
242
     * @param string        $version
243
     * @param string[]      $excludes
244
     * @param callable|null $notifier
245
     */
246 91
    private function extract($origin, $destination, $release, $version, array $excludes, callable $notifier = null)
247
    {
248 91
        $this->notify($notifier, self::NOTIFY_EXTRACT, $destination);
249
250 91
        $zip = new \ZipArchive();
251 91
        $zip->open($origin);
252
253 91
        $this->notify($notifier, self::NOTIFY_EXTRACT_SIZE, $zip->numFiles);
254
255 91
        $offset = 20 + strlen($release) + strlen($version);
256
257 91
        for ($i = 0; $i < $zip->numFiles; ++$i) {
258 91
            $this->extractFile(
259 91
                $file = $zip->getNameIndex($i),
260 81
                substr($file, $offset),
261 81
                $origin,
262 81
                $destination,
263 81
                $excludes,
264 10
                $notifier
265 71
            );
266 71
        }
267
268 91
        $zip->close();
269
270 91
        $this->notify($notifier, self::NOTIFY_EXTRACT_COMPLETE);
271 91
        $this->notify($notifier, self::NOTIFY_CLEAR_ARCHIVE, $origin);
272
273 91
        if (!@unlink($origin)) {
274
            throw $this->createException(sprintf('Unable to remove the CKEditor ZIP archive "%s".', $origin));
275
        }
276 91
    }
277
278
    /**
279
     * @param string        $file
280
     * @param string        $rewrite
281
     * @param string        $origin
282
     * @param string        $destination
283
     * @param string[]      $excludes
284
     * @param callable|null $notifier
285
     */
286 91
    private function extractFile($file, $rewrite, $origin, $destination, array $excludes, callable $notifier = null)
287
    {
288 91
        $this->notify($notifier, self::NOTIFY_EXTRACT_PROGRESS, $rewrite);
289
290 91
        $from = 'zip://'.$origin.'#'.$file;
291 91
        $to = $destination.'/'.$rewrite;
292
293 91
        foreach ($excludes as $exclude) {
294 81
            if (strpos($rewrite, $exclude) === 0) {
295 81
                return;
296
            }
297 71
        }
298
299 91
        if (substr($from, -1) === '/') {
300 91
            if (!is_dir($to) && !@mkdir($to)) {
301
                throw $this->createException(sprintf('Unable to create the directory "%s".', $to));
302
            }
303
304 91
            return;
305
        }
306
307 91
        if (!@copy($from, $to)) {
308
            throw $this->createException(sprintf('Unable to extract the file "%s" to "%s".', $file, $to));
309
        }
310 91
    }
311
312
    /**
313
     * @param callable|null $notifier
314
     * @param string        $type
315
     * @param mixed         $data
316
     *
317
     * @return mixed
318
     */
319 91
    private function notify(callable $notifier = null, $type, $data = null)
320
    {
321 91
        if ($notifier !== null) {
322 10
            return $notifier($type, $data);
323
        }
324 81
    }
325
326
    /**
327
     * @param string $message
328
     *
329
     * @return \RuntimeException
330
     */
331
    private function createException($message)
332
    {
333
        $error = error_get_last();
334
335
        if (isset($error['message'])) {
336
            $message .= sprintf(' (%s)', $error['message']);
337
        }
338
339
        return new \RuntimeException($message);
340
    }
341
}
342