Completed
Pull Request — master (#40)
by Marko
116:46 queued 51:48
created

CKEditorInstaller   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 1
dl 0
loc 298
rs 8.6
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 21 1
A install() 0 12 2
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 notify() 0 6 2
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 FOS\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
84
    /**
85
     * @param mixed[] $options
86
     *
87
     * @return bool
88
     */
89
    public function install(array $options = [])
90
    {
91
        $options = $this->resolver->resolve($options);
92
93
        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
            return false;
95
        }
96
97
        $this->extract($this->download($options), $options);
98
99
        return true;
100
    }
101
102
    /**
103
     * @param mixed[] $options
104
     *
105
     * @return int
106
     */
107
    private function clear(array $options)
108
    {
109
        if (!file_exists($options['path'].'/ckeditor.js')) {
110
            return self::CLEAR_DROP;
111
        }
112
113
        if ($options['clear'] === null && $options['notifier'] !== null) {
114
            $options['clear'] = $this->notify($options['notifier'], self::NOTIFY_CLEAR, $options['path']);
115
        }
116
117
        if ($options['clear'] === null) {
118
            $options['clear'] = self::CLEAR_SKIP;
119
        }
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
            foreach ($files as $file) {
130
                $filePath = $file->getRealPath();
131
                $this->notify($options['notifier'], self::NOTIFY_CLEAR_PROGRESS, $filePath);
132
133
                if ($dir = $file->isDir()) {
134
                    $success = @rmdir($filePath);
135
                } else {
136
                    $success = @unlink($filePath);
137
                }
138
139
                if (!$success) {
140
                    throw $this->createException(sprintf(
141
                        'Unable to remove the %s "%s".',
142
                        $dir ? 'directory' : 'file',
143
                        $filePath
144
                    ));
145
                }
146
            }
147
148
            $this->notify($options['notifier'], self::NOTIFY_CLEAR_COMPLETE);
149
        }
150
151
        return $options['clear'];
152
    }
153
154
    /**
155
     * @param mixed[] $options
156
     *
157
     * @return string
158
     */
159
    private function download(array $options)
160
    {
161
        $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
166
        if ($zip === false) {
167
            throw $this->createException(sprintf('Unable to download CKEditor ZIP archive from "%s".', $url));
168
        }
169
170
        $path = tempnam(sys_get_temp_dir(), 'ckeditor-'.$options['release'].'-'.$options['version'].'.zip');
171
172
        if (!@file_put_contents($path, $zip)) {
173
            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
     *
184
     * @return resource
185
     */
186
    private function createStreamContext(callable $notifier = null)
187
    {
188
        $context = [];
189
        $proxy = getenv('https_proxy') ?: getenv('http_proxy');
190
191
        if ($proxy) {
192
            $context['proxy'] = $proxy;
193
            $context['request_fulluri'] = (bool) getenv('https_proxy_request_fulluri') ?: getenv('http_proxy_request_fulluri');
194
        }
195
196
        return stream_context_create($context, [
197
            'notification' => function (
198
                $code,
199
                $severity,
200
                $message,
201
                $messageCode,
202
                $transferred,
203
                $size
204
            ) use ($notifier) {
205
                if ($notifier === null) {
206
                    return;
207
                }
208
209
                switch ($code) {
210
                    case STREAM_NOTIFY_FILE_SIZE_IS:
211
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_SIZE, $size);
212
                        break;
213
214
                    case STREAM_NOTIFY_PROGRESS:
215
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_PROGRESS, $transferred);
216
                        break;
217
                }
218
            },
219
        ]);
220
    }
221
222
    /**
223
     * @param string  $path
224
     * @param mixed[] $options
225
     */
226
    private function extract($path, array $options)
227
    {
228
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT, $options['path']);
229
230
        $zip = new \ZipArchive();
231
        $zip->open($path);
232
233
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_SIZE, $zip->numFiles);
234
235
        $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
        $zip->close();
247
248
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_COMPLETE);
249
        $this->notify($options['notifier'], self::NOTIFY_CLEAR_ARCHIVE, $path);
250
251
        if (!@unlink($path)) {
252
            throw $this->createException(sprintf('Unable to remove the CKEditor ZIP archive "%s".', $path));
253
        }
254
    }
255
256
    /**
257
     * @param string  $file
258
     * @param string  $rewrite
259
     * @param string  $origin
260
     * @param mixed[] $options
261
     */
262
    private function extractFile($file, $rewrite, $origin, array $options)
263
    {
264
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_PROGRESS, $rewrite);
265
266
        $from = 'zip://'.$origin.'#'.$file;
267
        $to = $options['path'].'/'.$rewrite;
268
269
        foreach ($options['excludes'] as $exclude) {
270
            if (strpos($rewrite, $exclude) === 0) {
271
                return;
272
            }
273
        }
274
275
        if (substr($from, -1) === '/') {
276
            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
    }
287
288
    /**
289
     * @param callable|null $notifier
290
     * @param string        $type
291
     * @param mixed         $data
292
     *
293
     * @return mixed
294
     */
295
    private function notify(callable $notifier = null, $type, $data = null)
296
    {
297
        if ($notifier !== null) {
298
            return $notifier($type, $data);
299
        }
300
    }
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