Completed
Push — master ( 0a4126...a2d163 )
by Maximilian
171:00 queued 156:01
created

src/Installer/CKEditorInstaller.php (1 issue)

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