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

src/Installer/CKEditorInstaller.php (1 issue)

Severity
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
The condition $this->clear($options) === self::CLEAR_SKIP is always false.
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') ?:
194
                getenv('http_proxy_request_fulluri');
195
        }
196
197
        return stream_context_create($context, [
198
            'notification' => function (
199
                $code,
200
                $severity,
201
                $message,
202
                $messageCode,
203
                $transferred,
204
                $size
205
            ) use ($notifier) {
206
                if ($notifier === null) {
207
                    return;
208
                }
209
210
                switch ($code) {
211
                    case STREAM_NOTIFY_FILE_SIZE_IS:
212
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_SIZE, $size);
213
                        break;
214
215
                    case STREAM_NOTIFY_PROGRESS:
216
                        $this->notify($notifier, self::NOTIFY_DOWNLOAD_PROGRESS, $transferred);
217
                        break;
218
                }
219
            },
220
        ]);
221
    }
222
223
    /**
224
     * @param string  $path
225
     * @param mixed[] $options
226
     */
227
    private function extract($path, array $options)
228
    {
229
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT, $options['path']);
230
231
        $zip = new \ZipArchive();
232
        $zip->open($path);
233
234
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_SIZE, $zip->numFiles);
235
236
        $offset = 20 + strlen($options['release']) + strlen($options['version']);
237
238
        for ($i = 0; $i < $zip->numFiles; ++$i) {
239
            $this->extractFile(
240
                $file = $zip->getNameIndex($i),
241
                substr($file, $offset),
242
                $path,
243
                $options
244
            );
245
        }
246
247
        $zip->close();
248
249
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_COMPLETE);
250
        $this->notify($options['notifier'], self::NOTIFY_CLEAR_ARCHIVE, $path);
251
252
        if (!@unlink($path)) {
253
            throw $this->createException(sprintf('Unable to remove the CKEditor ZIP archive "%s".', $path));
254
        }
255
    }
256
257
    /**
258
     * @param string  $file
259
     * @param string  $rewrite
260
     * @param string  $origin
261
     * @param mixed[] $options
262
     */
263
    private function extractFile($file, $rewrite, $origin, array $options)
264
    {
265
        $this->notify($options['notifier'], self::NOTIFY_EXTRACT_PROGRESS, $rewrite);
266
267
        $from = 'zip://'.$origin.'#'.$file;
268
        $to = $options['path'].'/'.$rewrite;
269
270
        foreach ($options['excludes'] as $exclude) {
271
            if (strpos($rewrite, $exclude) === 0) {
272
                return;
273
            }
274
        }
275
276
        if (substr($from, -1) === '/') {
277
            if (!is_dir($to) && !@mkdir($to)) {
278
                throw $this->createException(sprintf('Unable to create the directory "%s".', $to));
279
            }
280
281
            return;
282
        }
283
284
        if (!@copy($from, $to)) {
285
            throw $this->createException(sprintf('Unable to extract the file "%s" to "%s".', $file, $to));
286
        }
287
    }
288
289
    /**
290
     * @param callable|null $notifier
291
     * @param string        $type
292
     * @param mixed         $data
293
     *
294
     * @return mixed
295
     */
296
    private function notify(callable $notifier = null, $type, $data = null)
297
    {
298
        if ($notifier !== null) {
299
            return $notifier($type, $data);
300
        }
301
    }
302
303
    /**
304
     * @param string $message
305
     *
306
     * @return \RuntimeException
307
     */
308
    private function createException($message)
309
    {
310
        $error = error_get_last();
311
312
        if (isset($error['message'])) {
313
            $message .= sprintf(' (%s)', $error['message']);
314
        }
315
316
        return new \RuntimeException($message);
317
    }
318
}
319