Completed
Push — master ( b28d44...6f2c13 )
by Marko
40:03 queued 25:04
created

src/Installer/CKEditorInstaller.php (2 issues)

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