Completed
Push — master ( 42d6d9...78fce3 )
by Eric
112:12 queued 110:42
created

CKEditorInstaller   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 88.72%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 1
dl 0
loc 298
ccs 118
cts 133
cp 0.8872
rs 8.6
c 0
b 0
f 0

9 Methods

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