Passed
Pull Request — master (#2)
by
unknown
15:51
created

Config::extend()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 6
rs 10
1
<?php
2
3
/*
4
 * CKFinder
5
 * ========
6
 * http://cksource.com/ckfinder
7
 * Copyright (C) 2007-2016, CKSource - Frederico Knabben. All rights reserved.
8
 *
9
 * The software, this file and its contents are subject to the CKFinder
10
 * License. Please read the license.txt file before using, installing, copying,
11
 * modifying or distribute this file or part of its contents. The contents of
12
 * this file is part of the Source Code of CKFinder.
13
 */
14
15
namespace CKSource\CKFinder;
16
17
use CKSource\CKFinder\Exception\InvalidConfigException;
18
use CKSource\CKFinder\Exception\InvalidResourceTypeException;
19
20
/**
21
 * The Config class.
22
 *
23
 * Contains all configuration options and a set of config helper methods.
24
 *
25
 * @copyright 2016 CKSource - Frederico Knabben
26
 */
27
class Config
28
{
29
    /**
30
     * An array containing configuration options.
31
     *
32
     * @var array $options
33
     */
34
    protected $options;
35
36
    /**
37
     * Constructor.
38
     *
39
     * Depending on the type of the parameter passed to this function,
40
     * config array is used directly or it is loaded from a file.
41
     *
42
     * <b>Important</b>: If you use a PHP file to store your config, remember to use
43
     *                   the <code>return</code> statement inside the file scope to return
44
     *                   the array.
45
     *
46
     * @param array|string $config
47
     *
48
     * @throws InvalidConfigException if config was not loaded properly.
49
     */
50
    public function __construct($config)
51
    {
52
        setlocale(LC_ALL, "en_US.utf8");
53
54
        // Check if default timezone was set
55
        try {
56
            new \DateTime();
57
        } catch (\Exception $e) {
58
            date_default_timezone_set('UTC');
59
        }
60
61
        if (is_string($config) && is_readable($config)) {
62
            $options = require $config;
63
        } else {
64
            $options = $config;
65
        }
66
67
        if (!is_array($options)) {
68
            throw new InvalidConfigException("Couldn't load configuration. Please check configuration file.");
69
        }
70
71
        $this->options = $this->mergeDefaultOptions($options);
72
73
        $this->validate();
74
        $this->process();
75
    }
76
77
    /**
78
     * Merges default or missing configuration options.
79
     *
80
     * @param array $options options passed to CKFinder
81
     *
82
     * @return array
83
     */
84
    protected function mergeDefaultOptions($options)
85
    {
86
        $defaults = array(
87
            'authentication' => function () {
88
                return false;
89
            },
90
            'licenseName' => '',
91
            'licenseKey'  => '',
92
            'privateDir'  => array(
93
                'backend' => 'default',
94
                'tags'    => '.ckfinder/tags',
95
                'logs'    => '.ckfinder/logs',
96
                'cache'   => '.ckfinder/cache',
97
                'thumbs'  => '.ckfinder/cache/thumbs'
98
            ),
99
            'images' => array(
100
                'maxWidth'  => 500,
101
                'maxHeight' => 400,
102
                'quality'   => 80,
103
                'sizes' => array(
104
                    'small'  => array('width' => 480, 'height' => 320, 'quality' => 80),
105
                    'medium' => array('width' => 600, 'height' => 480, 'quality' => 80),
106
                    'large'  => array('width' => 800, 'height' => 600, 'quality' => 80)
107
                ),
108
                'threshold' => array('pixels'=> 80, 'percent' => 10)
109
            ),
110
            'thumbnails' => array(
111
                'enabled' => true,
112
                'sizes' => array(
113
                    array('width' => '150', 'height' => '150', 'quality' => 80),
114
                    array('width' => '300', 'height' => '300', 'quality' => 80),
115
                    array('width' => '500', 'height' => '500', 'quality' => 80),
116
                ),
117
                'bmpSupported' => true,
118
            ),
119
            'backends' => array(
120
                array(
121
                    'name'               => 'default',
122
                    'adapter'            => 'local',
123
                    'baseUrl'            => '/userfiles/',
124
                    'chmodFiles'         => 0777,
125
                    'chmodFolders'       => 0777,
126
                    'filesystemEncoding' => 'UTF-8'
127
                ),
128
            ),
129
            'defaultResourceTypes' => '',
130
            'resourceTypes' =>array(
131
                array(
132
                    'name'              => 'Files',
133
                    'directory'         => 'files',
134
                    'maxSize'           => 0,
135
                    'allowedExtensions' => '7z,aiff,asf,avi,bmp,csv,doc,docx,fla,flv,gif,gz,gzip,jpeg,jpg,mid,mov,mp3,mp4,mpc,mpeg,mpg,ods,odt,pdf,png,ppt,pptx,pxd,qt,ram,rar,rm,rmi,rmvb,rtf,sdc,sitd,swf,sxc,sxw,tar,tgz,tif,tiff,txt,vsd,wav,wma,wmv,xls,xlsx,zip',
136
                    'deniedExtensions'  => '',
137
                    'backend'           => 'default'
138
                ),
139
                array(
140
                    'name'              => 'Images',
141
                    'directory'         => 'images',
142
                    'maxSize'           => 0,
143
                    'allowedExtensions' => 'bmp,gif,jpeg,jpg,png',
144
                    'deniedExtensions'  => '',
145
                    'backend'           => 'default'
146
                )
147
            ),
148
            'roleSessionVar' => 'CKFinder_UserRole',
149
            'accessControl' => array(
150
                array(
151
                    'role'          => '*',
152
                    'resourceType'  => '*',
153
                    'folder'        => '/',
154
155
                    'FOLDER_VIEW'        => true,
156
                    'FOLDER_CREATE'      => true,
157
                    'FOLDER_RENAME'      => true,
158
                    'FOLDER_DELETE'      => true,
159
160
                    'FILE_VIEW'          => true,
161
                    'FILE_CREATE'        => true,
162
                    'FILE_RENAME'        => true,
163
                    'FILE_DELETE'        => true,
164
165
                    'IMAGE_RESIZE'        => true,
166
                    'IMAGE_RESIZE_CUSTOM' => true
167
                ),
168
            ),
169
            'overwriteOnUpload'        => false,
170
            'checkDoubleExtension'     => true,
171
            'disallowUnsafeCharacters' => false,
172
            'secureImageUploads'       => true,
173
            'checkSizeAfterScaling'    => true,
174
            'htmlExtensions'           => array('html', 'htm', 'xml', 'js'),
175
            'hideFolders'              => array(".*", "CVS", "__thumbs"),
176
            'hideFiles'                => array(".*"),
177
            'forceAscii'               => false,
178
            'xSendfile'                => false,
179
            'debug'                    => false,
180
            'pluginsDirectory'         => __DIR__ . '/plugins',
181
            'plugins'                  => array(),
182
            'debugLoggers'            => array('ckfinder_log', 'error_log', 'firephp'),
183
            'tempDirectory' => sys_get_temp_dir(),
184
            'sessionWriteClose' => true,
185
            'csrfProtection' => true,
186
            'headers' => array()
187
        );
188
189
        $options = array_merge($defaults, $options);
190
191
        foreach (array('privateDir', 'images', 'thumbnails') as $key) {
192
            $options[$key] = array_merge($defaults[$key], $options[$key]);
0 ignored issues
show
Bug introduced by
It seems like $defaults[$key] can also be of type boolean and callable and string; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

192
            $options[$key] = array_merge(/** @scrutinizer ignore-type */ $defaults[$key], $options[$key]);
Loading history...
193
        }
194
195
        $resourceTypeDefaults = array(
196
            'name'              => '',
197
            'directory'         => '',
198
            'maxSize'           => 0,
199
            'allowedExtensions' => '',
200
            'deniedExtensions'  => '',
201
            'backend'           => 'default'
202
        );
203
204
        foreach ($options['resourceTypes'] as &$resourceType) {
205
            $resourceType = array_merge($resourceTypeDefaults, $resourceType);
206
        }
207
208
        $localBackendDefaults = array(
209
            'chmodFiles'   => 0755,
210
            'chmodFolders' => 0755,
211
            'filesystemEncoding' => 'UTF-8'
212
        );
213
214
        foreach ($options['backends'] as &$backend) {
215
            if ($backend['adapter'] === 'local') {
216
                $backend = array_merge($localBackendDefaults, $backend);
217
            }
218
        }
219
220
        $cacheDefaults = array(
221
            'imagePreview' => 24 * 3600,
222
            'thumbnails'   => 24 * 3600 * 365,
223
            'proxyCommand' => 0
224
        );
225
226
        $options['cache'] = array_replace($cacheDefaults, isset($options['cache']) ? $options['cache'] : array());
227
228
        // #205 Backward compatibility for old debug_loggers option
229
        if (isset($options['debug_loggers'])) {
230
            $options['debugLoggers'] = $options['debug_loggers'];
231
        }
232
233
        return $options;
234
    }
235
236
    /**
237
     * Returns the configuration node under the path defined in the parameter.
238
     *
239
     * For easier access to nested configuration options the config `$name`
240
     * parameter can be passed also as a dot-separated path.
241
     * For example, to check if thumbnails are enabled you can use:
242
     *
243
     * $config->get('thumbnails.enabled')
244
     *
245
     * @param string $name config node name
246
     *
247
     * @return mixed config node value
248
     *
249
     */
250
    public function get($name)
251
    {
252
        if (isset($this->options[$name])) {
253
            return $this->options[$name];
254
        }
255
256
        $keys = explode('.', $name);
257
        $array = $this->options;
258
259
        do {
260
            $key = array_shift($keys);
261
            if (isset($array[$key])) {
262
                if ($keys) {
263
                    if (is_array($array[$key])) {
264
                        $array = $array[$key];
265
                    } else {
266
                        break;
267
                    }
268
                } else {
269
                    return $array[$key];
270
                }
271
            } else {
272
                break;
273
            }
274
        } while ($keys);
275
276
        return null;
277
    }
278
279
    /**
280
     * Validates the config array structure.
281
     *
282
     * @throws InvalidConfigException if config structure is invalid.
283
     */
284
    protected function validate()
285
    {
286
        $checkMissingNodes = function (array $required, array $actual, $prefix = '') {
287
            $missing = array_keys(array_diff_key(array_flip($required), $actual));
288
289
            if (!empty($missing)) {
290
                throw new InvalidConfigException(sprintf(
291
                    "CKFinder configuration doesn't contain all required fields. " .
292
                    "Please check configuration file. Missing fields: %s",
293
                    ($prefix ? "{$prefix}: " : '') . implode(', ', $missing)));
294
            }
295
        };
296
297
        $requiredRootNodes = array('authentication', 'licenseName', 'licenseKey', 'privateDir', 'images',
298
            'backends', 'defaultResourceTypes', 'resourceTypes', 'roleSessionVar', 'accessControl',
299
            'checkDoubleExtension', 'disallowUnsafeCharacters', 'secureImageUploads', 'checkSizeAfterScaling',
300
            'htmlExtensions', 'hideFolders', 'hideFiles', 'forceAscii', 'xSendfile', 'debug', 'pluginsDirectory', 'plugins');
301
302
        $checkMissingNodes($requiredRootNodes, $this->options);
303
        $checkMissingNodes(array('backend', 'tags', 'logs', 'cache', 'thumbs'), $this->options['privateDir'], '[privateDir]');
304
        $checkMissingNodes(array('maxWidth', 'maxHeight', 'quality'), $this->options['images'], '[images]');
305
306
        $backends = array();
307
308
        foreach ($this->options['backends'] as $i => $backendConfig) {
309
            $checkMissingNodes(array('name', 'adapter'), $backendConfig, "[backends][{$i}]");
310
            $backends[] = $backendConfig['name'];
311
        }
312
313
        foreach ($this->options['resourceTypes'] as $i => $resourceTypeConfig) {
314
            $checkMissingNodes(array('name', 'directory', 'maxSize', 'allowedExtensions', 'deniedExtensions', 'backend'),
315
                $resourceTypeConfig, "[resourceTypes][{$i}]");
316
317
            if (!in_array($resourceTypeConfig['backend'], $backends)) {
318
                throw new InvalidConfigException("Backend '{$resourceTypeConfig['backend']}' is not defined: [resourceTypes][{$i}]");
319
            }
320
        }
321
322
        foreach ($this->options['accessControl'] as $i => $aclConfig) {
323
            $checkMissingNodes(array('role', 'resourceType', 'folder'), $aclConfig, "[accessControl][{$i}]");
324
        }
325
326
        if (!is_callable($this->options['authentication'])) {
327
            throw new InvalidConfigException("CKFinder Authentication config field must be a PHP callable");
328
        }
329
330
        if (!is_writable($this->options['tempDirectory'])) {
331
            throw new InvalidConfigException("The temporary folder is not writable for CKFinder");
332
        }
333
    }
334
335
    /**
336
     * Processes the configuration array.
337
     */
338
    protected function process()
339
    {
340
        $this->options['defaultResourceTypes'] =
341
            array_filter(
342
                array_map('trim',
343
                    explode(',', $this->options['defaultResourceTypes'])
344
                ),
345
                'strlen');
346
347
348
        $formatToArray = function ($input) {
349
            $input = is_array($input) ? $input : explode(',', $input);
350
351
            return
352
                array_filter(
353
                    array_map('strtolower',
354
                        array_map('trim', $input)
355
                    ),
356
                    'strlen');
357
        };
358
359
        foreach ($this->options['resourceTypes'] as $resourceTypeKey => $resourceTypeConfig) {
360
            $resourceTypeConfig['allowedExtensions'] = $formatToArray($resourceTypeConfig['allowedExtensions']);
361
            $resourceTypeConfig['deniedExtensions'] = $formatToArray($resourceTypeConfig['deniedExtensions']);
362
            $resourceTypeConfig['maxSize'] = Utils::returnBytes((string) $resourceTypeConfig['maxSize']);
363
364
            $this->options['resourceTypes'][$resourceTypeConfig['name']] = $resourceTypeConfig;
365
366
            if ($resourceTypeKey !== $resourceTypeConfig['name']) {
367
                unset($this->options['resourceTypes'][$resourceTypeKey]);
368
            }
369
        }
370
371
        foreach ($this->options['backends'] as $backendKey => $backendConfig) {
372
            $this->options['backends'][$backendConfig['name']] = $backendConfig;
373
374
            if ($backendKey !== $backendConfig['name']) {
375
                unset($this->options['backends'][$backendKey]);
376
            }
377
        }
378
379
        $this->options['htmlExtensions'] = $formatToArray($this->options['htmlExtensions']);
380
    }
381
382
    /**
383
     * Returns the default resource types names.
384
     *
385
     * @return array
386
     */
387
    public function getDefaultResourceTypes()
388
    {
389
        return $this->options['defaultResourceTypes'];
390
    }
391
392
    /**
393
     * Returns all defined resource types names.
394
     *
395
     * @return array
396
     */
397
    public function getResourceTypes()
398
    {
399
        return array_keys($this->options['resourceTypes']);
400
    }
401
402
    /**
403
     * Returns the configuration node for a given resource type.
404
     *
405
     * @param string $resourceType resource type name
406
     *
407
     * @return array configuration node for the resource type
408
     *
409
     * @throws InvalidResourceTypeException if the resource type does not exist
410
     */
411
    public function getResourceTypeNode($resourceType)
412
    {
413
        if (array_key_exists($resourceType, $this->options['resourceTypes'])) {
414
            return $this->options['resourceTypes'][$resourceType];
415
        } else {
416
            throw new InvalidResourceTypeException("Invalid resource type: {$resourceType}");
417
        }
418
    }
419
420
    /**
421
     * Returns the regex used for hidden files check.
422
     * @return string
423
     */
424
    public function getHideFilesRegex()
425
    {
426
        static $hideFilesRegex;
427
428
        if (!isset($hideFilesRegex)) {
429
            $hideFilesConfig = $this->options['hideFiles'];
430
431
            if ($hideFilesConfig && is_array($hideFilesConfig)) {
432
                $hideFilesRegex = join("|", $hideFilesConfig);
433
                $hideFilesRegex = strtr($hideFilesRegex, array("?" => "__QMK__", "*" => "__AST__", "|" => "__PIP__"));
434
                $hideFilesRegex = preg_quote($hideFilesRegex, "/");
435
                $hideFilesRegex = strtr($hideFilesRegex, array("__QMK__" => ".", "__AST__" => ".*", "__PIP__" => "|"));
436
                $hideFilesRegex = "/^(?:" . $hideFilesRegex . ")$/uim";
437
            } else {
438
                $hideFilesRegex = "";
439
            }
440
        }
441
442
        return $hideFilesRegex;
443
    }
444
445
    /**
446
     * Returns the regex used for hidden folders check.
447
     * @return string
448
     */
449
    public function getHideFoldersRegex()
450
    {
451
        static $hideFoldersRegex;
452
453
        if (!isset($hideFoldersRegex)) {
454
            $hideFoldersConfig = $this->options['hideFolders'];
455
456
            if ($hideFoldersConfig && is_array($hideFoldersConfig)) {
457
                $hideFoldersRegex = join("|", $hideFoldersConfig);
458
                $hideFoldersRegex = strtr($hideFoldersRegex, array("?" => "__QMK__", "*" => "__AST__", "|" => "__PIP__"));
459
                $hideFoldersRegex = preg_quote($hideFoldersRegex, "/");
460
                $hideFoldersRegex = strtr($hideFoldersRegex, array("__QMK__" => ".", "__AST__" => ".*", "__PIP__" => "|"));
461
                $hideFoldersRegex = "/^(?:" . $hideFoldersRegex . ")$/uim";
462
            } else {
463
                $hideFoldersRegex = "";
464
            }
465
        }
466
467
        return $hideFoldersRegex;
468
    }
469
470
    /**
471
     * If the config node does not exist, creates the node with a given name and values.
472
     * Otherwise extends the config node with additional (default) values.
473
     *
474
     * @param string $nodeName
475
     * @param array  $values
476
     */
477
    public function extend($nodeName, array $values)
478
    {
479
        if (!isset($this->options[$nodeName])) {
480
            $this->options[$nodeName] = $values;
481
        } else {
482
            $this->options[$nodeName] = array_replace_recursive($values, $this->options[$nodeName]);
483
        }
484
    }
485
486
    /**
487
     * Returns the backend-relative private directory path.
488
     *
489
     * @param string $privateDirIdentifier
490
     *
491
     * @return mixed
492
     */
493
    public function getPrivateDirPath($privateDirIdentifier)
494
    {
495
        if (!array_key_exists($privateDirIdentifier, $this->options['privateDir'])) {
496
            throw new \InvalidArgumentException(sprintf('Private dir with identifier %s not found. Please check configuration file.', $privateDirIdentifier));
497
        }
498
499
        $privateDir = $this->options['privateDir'][$privateDirIdentifier];
500
501
        if (is_array($privateDir) && array_key_exists('path', $privateDir)) {
502
            return $privateDir['path'];
503
        }
504
505
        return $privateDir;
506
    }
507
508
    /**
509
     * Checks if the debug logger with a given name is enabled.
510
     * @param string $loggerName debug logger name
511
     *
512
     * @return bool `true` if enabled
513
     */
514
    public function isDebugLoggerEnabled($loggerName)
515
    {
516
        return in_array($loggerName, $this->options['debugLoggers']);
517
    }
518
519
    /**
520
     * Returns backend configuration by name.
521
     *
522
     * @param string $backendName
523
     *
524
     * @return array backend configuration node
525
     *
526
     * @throws \InvalidArgumentException
527
     */
528
    public function getBackendNode($backendName)
529
    {
530
        if (array_key_exists($backendName, $this->options['backends'])) {
531
            return $this->options['backends'][$backendName];
532
        } else {
533
            throw new \InvalidArgumentException(sprintf('Backend %s not found. Please check configuration file.', $backendName));
534
        }
535
    }
536
}
537