Completed
Pull Request — master (#318)
by Eric
64:13
created

CKEditorInstaller::install()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 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
    public function __construct(array $options = [])
63
    {
64
        $this->resolver = (new OptionsResolver())
65
            ->setDefaults(array_merge([
66
                'clear'    => null,
67
                'excludes' => ['samples'],
68
                'notifier' => null,
69
                'path'     => dirname(__DIR__).'/Resources/public',
70
                'release'  => self::RELEASE_FULL,
71
                'version'  => self::VERSION_LATEST,
72
            ], $options))
73
            ->setAllowedTypes('excludes', 'array')
74
            ->setAllowedTypes('notifier', ['null', 'callable'])
75
            ->setAllowedTypes('path', 'string')
76
            ->setAllowedTypes('version', 'string')
77
            ->setAllowedValues('clear', [self::CLEAR_DROP, self::CLEAR_KEEP, self::CLEAR_SKIP, null])
78
            ->setAllowedValues('release', [self::RELEASE_BASIC, self::RELEASE_FULL, self::RELEASE_STANDARD])
79
            ->setNormalizer('path', function (Options $options, $path) {
80
                return rtrim($path, '/');
81
            });
82
    }
83 102
84
    /**
85
     * @param mixed[] $options
86
     *
87
     * @return bool
88
     */
89
    public function install(array $options = [])
90 102
    {
91 102
        $options = $this->resolver->resolve($options);
92 102
93 102
        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 102
            return false;
95 102
        }
96
97
        $this->extract($this->download($options), $options);
98
99
        return true;
100
    }
101
102 102
    /**
103
     * @param mixed[] $options
104 102
     *
105 102
     * @return int
106 102
     */
107
    private function clear(array $options)
108 102
    {
109 20
        if (!file_exists($options['path'].'/ckeditor.js')) {
110
            return self::CLEAR_DROP;
111
        }
112 102
113 102
        if ($options['clear'] === null && $options['notifier'] !== null) {
114 102
            $options['clear'] = $this->notify($options['notifier'], self::NOTIFY_CLEAR, $options['path']);
115
        }
116 102
117 102
        if ($options['clear'] === null) {
118
            $options['clear'] = self::CLEAR_SKIP;
119 102
        }
120
121
        if ($options['clear'] === self::CLEAR_DROP) {
122
            $files = new \RecursiveIteratorIterator(
123
                new \RecursiveDirectoryIterator($options['path'], \RecursiveDirectoryIterator::SKIP_DOTS),
124
                \RecursiveIteratorIterator::CHILD_FIRST
125
            );
126
127
            $this->notify($options['notifier'], self::NOTIFY_CLEAR_SIZE, iterator_count($files));
128
129 102
            foreach ($files as $file) {
130
                $filePath = $file->getRealPath();
131 102
                $this->notify($options['notifier'], self::NOTIFY_CLEAR_PROGRESS, $filePath);
132 102
133
                if ($dir = $file->isDir()) {
134
                    $success = @rmdir($filePath);
135 42
                } else {
136 12
                    $success = @unlink($filePath);
137 10
                }
138
139 42
                if (!$success) {
140 10
                    throw $this->createException(sprintf(
141 8
                        'Unable to remove the %s "%s".',
142
                        $dir ? 'directory' : 'file',
143 42
                        $filePath
144 12
                    ));
145 12
                }
146 2
            }
147 10
148
            $this->notify($options['notifier'], self::NOTIFY_CLEAR_COMPLETE);
149 12
        }
150
151 12
        return $options['clear'];
152 12
    }
153 12
154
    /**
155 12
     * @param mixed[] $options
156 12
     *
157 10
     * @return string
158 12
     */
159
    private function download(array $options)
160
    {
161 12
        $url = sprintf(self::$archive, $options['release'], $options['version']);
162
        $this->notify($options['notifier'], self::NOTIFY_DOWNLOAD, $url);
163
164
        $zip = @file_get_contents($url, false, $this->createStreamContext($options['notifier']));
165 2
166
        if ($zip === false) {
167
            throw $this->createException(sprintf('Unable to download CKEditor ZIP archive from "%s".', $url));
168 10
        }
169
170 12
        $path = tempnam(sys_get_temp_dir(), 'ckeditor-'.$options['release'].'-'.$options['version'].'.zip');
171 10
172
        if (!@file_put_contents($path, $zip)) {
173 42
            throw $this->createException(sprintf('Unable to write CKEditor ZIP archive to "%s".', $path));
174
        }
175
176
        $this->notify($options['notifier'], self::NOTIFY_DOWNLOAD_COMPLETE, $path);
177
178
        return $path;
179
    }
180
181
    /**
182
     * @param callable|null $notifier
183 102
     *
184
     * @return resource
185 102
     */
186 102
    private function createStreamContext(callable $notifier = null)
187
    {
188 102
        $context = [];
189
        $proxy = getenv('https_proxy') ?: getenv('http_proxy');
190 102
191
        if ($proxy) {
192
            $context['proxy'] = $proxy;
193
            $context['request_fulluri'] = (bool) getenv('https_proxy_request_fulluri') ?: getenv('http_proxy_request_fulluri');
194 102
        }
195
196 102
        return stream_context_create($context, [
197
            'notification' => function (
198
                $code,
199
                $severity,
200 102
                $message,
201
                $messageCode,
202 102
                $transferred,
203
                $size
204
            ) use ($notifier) {
205
                if ($notifier === null) {
206
                    return;
207
                }
208
209
                switch ($code) {
210 102
                    case STREAM_NOTIFY_FILE_SIZE_IS:
211
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_SIZE, $size);
212 102
                        break;
213 102
214
                    case STREAM_NOTIFY_PROGRESS:
215
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_PROGRESS, $transferred);
216
                        break;
217
                }
218
            },
219
        ]);
220
    }
221 102
222 90
    /**
223
     * @param string  $path
224
     * @param mixed[] $options
225
     */
226 12
    private function extract($path, array $options)
227 12
    {
228 12
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT, $options['path']);
229
230 12
        $zip = new \ZipArchive();
231 12
        $zip->open($path);
232 12
233
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_SIZE, $zip->numFiles);
234 102
235 82
        $offset = 20 + strlen($options['release']) + strlen($options['version']);
236
237
        for ($i = 0; $i < $zip->numFiles; ++$i) {
238
            $this->extractFile(
239
                $file = $zip->getNameIndex($i),
240
                substr($file, $offset),
241
                $path,
242
                $options
243
            );
244
        }
245
246 102
        $zip->close();
247
248 102
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_COMPLETE);
249
        $this->notify($options['notifier'], self::NOTIFY_CLEAR_ARCHIVE, $path);
250 102
251 102
        if (!@unlink($path)) {
252
            throw $this->createException(sprintf('Unable to remove the CKEditor ZIP archive "%s".', $path));
253 102
        }
254
    }
255 102
256
    /**
257 102
     * @param string  $file
258 102
     * @param string  $rewrite
259 102
     * @param string  $origin
260 102
     * @param mixed[] $options
261 102
     */
262 102
    private function extractFile($file, $rewrite, $origin, array $options)
263 102
    {
264 20
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_PROGRESS, $rewrite);
265 82
266 82
        $from = 'zip://'.$origin.'#'.$file;
267
        $to = $options['path'].'/'.$rewrite;
268 102
269
        foreach ($options['excludes'] as $exclude) {
270 102
            if (strpos($rewrite, $exclude) === 0) {
271 102
                return;
272
            }
273 102
        }
274
275
        if (substr($from, -1) === '/') {
276 102
            if (!is_dir($to) && !@mkdir($to)) {
277
                throw $this->createException(sprintf('Unable to create the directory "%s".', $to));
278
            }
279
280
            return;
281
        }
282
283
        if (!@copy($from, $to)) {
284
            throw $this->createException(sprintf('Unable to extract the file "%s" to "%s".', $file, $to));
285
        }
286 102
    }
287
288 102
    /**
289
     * @param callable|null $notifier
290 102
     * @param string        $type
291 102
     * @param mixed         $data
292
     *
293 102
     * @return mixed
294 90
     */
295 90
    private function notify(callable $notifier = null, $type, $data = null)
296
    {
297 82
        if ($notifier !== null) {
298
            return $notifier($type, $data);
299 102
        }
300 102
    }
301
302
    /**
303
     * @param string $message
304 102
     *
305
     * @return \RuntimeException
306
     */
307 102
    private function createException($message)
308
    {
309
        $error = error_get_last();
310 102
311
        if (isset($error['message'])) {
312
            $message .= sprintf(' (%s)', $error['message']);
313
        }
314
315
        return new \RuntimeException($message);
316
    }
317
}
318