Completed
Push — master ( 677cdd...a74cf9 )
by Richard
10:49
created

Assets::saveAssetsPrefs()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 12
Ratio 100 %

Code Coverage

Tests 8
CRAP Score 3.0123
Metric Value
dl 12
loc 12
ccs 8
cts 9
cp 0.8889
rs 9.4286
cc 3
eloc 8
nc 4
nop 1
crap 3.0123
1
<?php
2
/*
3
 You may not change or alter any portion of this comment or credits
4
 of supporting developers from this source code or any supporting source code
5
 which is considered copyrighted (c) material of the original comment or credit authors.
6
7
 This program is distributed in the hope that it will be useful,
8
 but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
*/
11
12
namespace Xoops\Core;
13
14
use Assetic\AssetManager;
15
use Assetic\FilterManager;
16
use Assetic\Filter;
17
use Assetic\Factory\AssetFactory;
18
use Assetic\Factory\Worker\CacheBustingWorker;
19
use Assetic\AssetWriter;
20
use Assetic\Asset\AssetCollection;
21
use Assetic\Asset\FileAsset;
22
use Assetic\Asset\GlobAsset;
23
24
/**
25
 * Provides a standardized asset strategy
26
 *
27
 * @category  Assets
28
 * @package   Assets
29
 * @author    Richard Griffith <[email protected]>
30
 * @copyright 2014-2015 XOOPS Project (http://xoops.org)
31
 * @license   GNU GPL 2 or later (http://www.gnu.org/licenses/gpl-2.0.html)
32
 * @version   Release: 1.0
33
 * @link      http://xoops.org
34
 * @since     2.6.0
35
 */
36
class Assets
37
{
38
    /**
39
     * @var boolean
40
     */
41
    private $debug = false;
42
43
    /**
44
     * @var array of default filter strings - may be overridden by prefs
45
     */
46
    private $default_filters = array(
47
            'css' => 'cssimport,cssembed,?cssmin',
48
            'js'  => '?jsqueeze',
49
    );
50
51
    /**
52
     * @var array of output locations in assets directory
53
     */
54
    private $default_output = array(
55
            'css' => 'css/*.css',
56
            'js'  => 'js/*.js',
57
    );
58
59
    /**
60
     * @var array of asset reference definitions - may be overridden by prefs
61
     */
62
    private $default_asset_refs = array(
63
        array(
64
            'name' => 'jquery',
65
            'assets' => array('media/jquery/jquery.js'),
66
            'filters' => null,
67
        ),
68
        array(
69
            'name' => 'jqueryui',
70
            'assets' => array('media/jquery/ui/jquery-ui.js'),
71
            'filters' => null,
72
        ),
73
        array(
74
            'name' => 'jgrowl',
75
            'assets' => array('media/jquery/plugins/jquery.jgrowl.js'),
76
            'filters' => null,
77
        ),
78
    );
79
80
    /**
81
     * @var AssetManager
82
     */
83
    private $assetManager = null;
84
85
    /**
86
     * @var string config file with assets prefs
87
     */
88
    private $assetsPrefsFilename = 'var/configs/system_assets_prefs.yml';
89
90
    /**
91
     * @var string config cache key
92
     */
93
    private $assetsPrefsCacheKey = 'system/assets/prefs';
94
95
    /**
96
     * @var string string to identify Assetic filters using instanceof
97
     */
98
99
    private $filterInterface = '\Assetic\Filter\FilterInterface';
100
    /**
101
     * __construct
102
     */
103 3
    public function __construct()
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
104
    {
105 3
        $this->assetManager = new AssetManager();
106 3
        if (isset($_REQUEST['ASSET_DEBUG'])) {
107
            $this->setDebug();
108
        }
109 3
        $this->readAssetsPrefs();
110
        // register any asset references
111 3
        foreach ($this->default_asset_refs as $ref) {
112 3
            $this->registerAssetReference($ref['name'], $ref['assets'], $ref['filters']);
113 3
        }
114 3
    }
115
116
    /**
117
     * readAssetsPrefs - read configured asset preferences
118
     *
119
     * @return array of assets preferences
120
     */
121 3
    protected function readAssetsPrefs()
122
    {
123 3
        $xoops = \Xoops::getInstance();
124
125 3
        $assetsPrefs = array();
126
127
        try {
128 3
            $assetsPrefs = $xoops->cache()->read($this->assetsPrefsCacheKey);
129 3
            $file = $xoops->path($this->assetsPrefsFilename);
130 3
            $mtime = filemtime($file);
131 3
            if ($assetsPrefs===false || !isset($assetsPrefs['mtime']) || !$mtime
132 3
                || (isset($assetsPrefs['mtime']) && $assetsPrefs['mtime']<$mtime)) {
133 1
                if ($mtime) {
134
                    $assetsPrefs = Yaml::read($file);
135
                    if (!is_array($assetsPrefs)) {
136
                        $xoops->logger()->error("Invalid config in system_assets_prefs.yml");
137
                        $assetsPrefs = array();
138
                    } else {
139
                        $assetsPrefs['mtime']=$mtime;
140
                        $xoops->cache()->write($this->assetsPrefsCacheKey, $assetsPrefs);
141
                    }
142
                } else {
143
                    // use defaults to create file
144
                    $assetsPrefs = array(
145 1
                        'default_filters' => $this->default_filters,
146 1
                        'default_asset_refs' => $this->default_asset_refs,
147 1
                        'mtime' => time(),
148 1
                    );
149 1
                    $this->saveAssetsPrefs($assetsPrefs);
150
                }
151 1
            }
152 3
            if (!empty($assetsPrefs['default_filters']) && is_array($assetsPrefs['default_filters'])) {
153 3
                $this->default_filters = $assetsPrefs['default_filters'];
154 3
            }
155 3
            if (!empty($assetsPrefs['default_asset_refs']) && is_array($assetsPrefs['default_asset_refs'])) {
156 3
                $this->default_asset_refs = $assetsPrefs['default_asset_refs'];
157 3
            }
158 3
        } catch (\Exception $e) {
159
            $xoops->events()->triggerEvent('core.exception', $e);
160
            $assetsPrefs = array();
161
        }
162 3
        return $assetsPrefs;
163
    }
164
165
    /**
166
     * saveAssetsPrefs - record array of assets preferences in config file, and
167
     * update cache
168
     *
169
     * @param array $assets_prefs array of asset preferences to save
170
     *
171
     * @return void
172
     */
173 1 View Code Duplication
    protected function saveAssetsPrefs($assets_prefs)
174
    {
175 1
        if (is_array($assets_prefs)) {
176 1
            $xoops = \Xoops::getInstance();
177
            try {
178 1
                Yaml::save($assets_prefs, $xoops->path($this->assetsPrefsFilename));
179 1
                $xoops->cache()->write($this->assetsPrefsCacheKey, $assets_prefs);
180 1
            } catch (\Exception $e) {
181
                $xoops->events()->triggerEvent('core.exception', $e);
182
            }
183 1
        }
184 1
    }
185
186
187
    /**
188
     * getUrlToAssets
189
     *
190
     * Create an asset file from a list of assets
191
     *
192
     * @param string       $type    type of asset, css or js
193
     * @param array        $assets  list of source files to process
194
     * @param string|array $filters either a comma separated list of known namsed filters
195
     *                              or an array of named filters and/or filter object
196
     * @param string       $target  target path, will default to assets directory
197
     *
198
     * @return string URL to asset file
199
     */
200
    public function getUrlToAssets($type, $assets, $filters = 'default', $target = null)
201
    {
202
        if (is_scalar($assets)) {
203
            $assets = array($assets); // just a single path name
204
        }
205
206
        if ($filters==='default') {
207
            if (isset($this->default_filters[$type])) {
208
                $filters = $this->default_filters[$type];
209
            } else {
210
                $filters = '';
211
            }
212
        }
213
214 View Code Duplication
        if (!is_array($filters)) {
215
            if (empty($filters)) {
216
                $filters = array();
217
            } else {
218
                $filters = explode(',', str_replace(' ', '', $filters));
219
            }
220
        }
221
222
        if (isset($this->default_output[$type])) {
223
            $output = $this->default_output[$type];
224
        } else {
225
            $output = '';
226
        }
227
228
        $xoops = \Xoops::getInstance();
229
230
        if (isset($target)) {
231
            $target_path = $target;
232
        } else {
233
            $target_path = $xoops->path('assets');
234
        }
235
236
        try {
237
            $am = $this->assetManager;
238
            $fm = new FilterManager();
239
240
            foreach ($filters as $filter) {
241
                if (is_object($filter) && $filter instanceof $this->filterInterface) {
242
                    $filterArray[] = $filter;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$filterArray was never initialized. Although not strictly required by PHP, it is generally a good practice to add $filterArray = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
243
                } else {
244
                    switch (ltrim($filter, '?')) {
245
                        case 'cssembed':
246
                            $fm->set('cssembed', new Filter\PhpCssEmbedFilter());
247
                            break;
248
                        case 'cssmin':
249
                            $fm->set('cssmin', new Filter\CssMinFilter());
250
                            break;
251
                        case 'cssimport':
252
                            $fm->set('cssimport', new Filter\CssImportFilter());
253
                            break;
254
                        case 'cssrewrite':
255
                            $fm->set('cssrewrite', new Filter\CssRewriteFilter());
256
                            break;
257
                        case 'lessphp':
258
                            $fm->set('lessphp', new Filter\LessphpFilter());
259
                            break;
260
                        case 'scssphp':
261
                            $fm->set('scssphp', new Filter\ScssphpFilter());
262
                            break;
263
                        case 'jsmin':
264
                            $fm->set('jsmin', new Filter\JSMinFilter());
265
                            break;
266
                        case 'jsqueeze':
267
                            $fm->set('jsqueeze', new Filter\JSqueezeFilter());
268
                            break;
269
                        default:
270
                            throw new \Exception(sprintf('%s filter not implemented.', $filter));
271
                            break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
272
                    }
273
                }
274
            }
275
276
            // Factory setup
277
            $factory = new AssetFactory($target_path);
278
            $factory->setAssetManager($am);
279
            $factory->setFilterManager($fm);
280
            $factory->setDefaultOutput($output);
281
            $factory->setDebug($this->debug);
282
            $factory->addWorker(new CacheBustingWorker());
283
284
            // Prepare the assets writer
285
            $writer = new AssetWriter($target_path);
286
287
            // Translate asset paths, remove duplicates
288
            $translated_assets = array();
289
            foreach ($assets as $k => $v) {
290
                // translate path if not a reference or absolute path
291
                if (0 == preg_match("/^\\/|^\\\\|^[a-zA-Z]:|^@/", $v)) {
292
                    $v = $xoops->path($v);
293
                }
294
                if (!in_array($v, $translated_assets)) {
295
                    $translated_assets[] = $v;
296
                }
297
            }
298
299
            // Create the asset
300
            $asset = $factory->createAsset(
301
                $translated_assets,
302
                $filters
303
            );
304
            $asset_path = $asset->getTargetPath();
305
            if (!is_readable($target_path . $asset_path)) {
306
                $assetKey = 'Asset '.$asset_path;
307
                $xoops->events()->triggerEvent('debug.timer.start', $assetKey);
308
                $oldumask = umask(0002);
309
                $writer->writeAsset($asset);
310
                umask($oldumask);
311
                $xoops->events()->triggerEvent('debug.timer.stop', $assetKey);
312
            }
313
314
            return $xoops->url('assets/' . $asset_path);
315
316
        } catch (\Exception $e) {
317
            $xoops->events()->triggerEvent('core.exception', $e);
318
            return null;
319
        }
320
    }
321
322
323
    /**
324
     * setDebug enable debug mode, will skip filters prefixed with '?'
325
     *
326
     * @return true
327
     */
328 1
    public function setDebug()
329
    {
330 1
        $this->debug = true;
331 1
        return true;
332
    }
333
334
    /**
335
     * Add an asset reference to the asset manager
336
     *
337
     * @param string       $name    the name of the reference to be added
338
     * @param mixed        $assets  a string asset path, or an array of asset paths,
339
     *                              may include wildcard
340
     * @param string|array $filters either a comma separated list of known named filters
341
     *                              or an array of named filters and/or filter object
342
     *
343
     * @return boolean true if asset registers, false on error
344
     */
345 3
    public function registerAssetReference($name, $assets, $filters = null)
346
    {
347 3
        $xoops = \Xoops::getInstance();
348
349 3
        $assetArray = array();
350 3
        $filterArray = array();
351
352
        try {
353 3
            if (is_scalar($assets)) {
354 1
                $assets = array($assets);  // just a single path name
355 1
            }
356 3
            foreach ($assets as $a) {
0 ignored issues
show
Bug introduced by
The expression $assets of type object|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
357
                // translate path if not a reference or absolute path
358 3
                if ((substr_compare($a, '@', 0, 1) != 0)
359 3
                    && (substr_compare($a, '/', 0, 1) != 0)) {
360 3
                    $a = $xoops->path($a);
361 3
                }
362 3
                if (false===strpos($a, '*')) {
363 3
                    $assetArray[] = new FileAsset($a); // single file
364 3
                } else {
365
                    $assetArray[] = new GlobAsset($a);  // wild card match
366
                }
367 3
            }
368
369 3 View Code Duplication
            if (!is_array($filters)) {
370 3
                if (empty($filters)) {
371 3
                    $filters = array();
372 3
                } else {
373
                    $filters = explode(',', str_replace(' ', '', $filters));
374
                }
375 3
            }
376 3
            foreach ($filters as $filter) {
377
                if (is_object($filter) && $filter instanceof $this->filterInterface) {
378
                    $filterArray[] = $filter;
379
                } else {
380
                    switch (ltrim($filter, '?')) {
381
                        case 'cssembed':
382
                            $filterArray[] = new Filter\PhpCssEmbedFilter();
383
                            break;
384
                        case 'cssmin':
385
                            $filterArray[] = new Filter\CssMinFilter();
386
                            break;
387
                        case 'cssimport':
388
                            $filterArray[] = new Filter\CssImportFilter();
389
                            break;
390
                        case 'cssrewrite':
391
                            $filterArray[] = new Filter\CssRewriteFilter();
392
                            break;
393
                        case 'lessphp':
394
                            $filterArray[] = new Filter\LessphpFilter();
395
                            break;
396
                        case 'scssphp':
397
                            $filterArray[] = new Filter\ScssphpFilter();
398
                            break;
399
                        case 'jsmin':
400
                            $filterArray[] = new Filter\JSMinFilter();
401
                            break;
402
                        case 'jsqueeze':
403
                            $filterArray[] = new Filter\JSqueezeFilter();
404
                            break;
405
                        default:
406
                            throw new \Exception(sprintf('%s filter not implemented.', $filter));
407
                            break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
408
                    }
409
                }
410 3
            }
411
412 3
            $collection = new AssetCollection($assetArray, $filterArray);
413 3
            $this->assetManager->set($name, $collection);
414
415 3
            return true;
416
        } catch (\Exception $e) {
417
            $xoops->events()->triggerEvent('core.exception', $e);
418
            return false;
419
        }
420
    }
421
422
    /**
423
     * copyFileAssets - copy files to the appropriate asset directory.
424
     *
425
     * Copying is normally only needed for fonts or images when they are referenced by a
426
     * relative url in stylesheet, or are located outside of the web root.
427
     *
428
     * @param string $from_path path to files to copy
429
     * @param string $pattern   glob pattern to match files to be copied
430
     * @param string $output    output type (css, fonts, images, js)
431
     *
432
     * @return mixed boolean false if target directory is not writable, otherwise
433
     *               integer count of files copied
434
     */
435 1
    public function copyFileAssets($from_path, $pattern, $output)
436
    {
437 1
        $xoops = \Xoops::getInstance();
438
439 1
        $to_path = $xoops->path('assets') . '/' . $output . '/';
440 1
        $from = glob($from_path . '/' . $pattern);
441 1
        $xoops->events()->triggerEvent('debug.log', $from);
442
443 1
        if (!is_dir($to_path)) {
444 1
            $oldUmask = umask(0);
445 1
            mkdir($to_path, 0775, true);
446 1
            umask($oldUmask);
447 1
        }
448
449 1
        if (!is_writable($to_path)) {
450
            $xoops->logger()->warning('Asset directory is not writable. ' . $output);
451
            return false;
452
        } else {
453 1
            $count = 0;
454 1
            $oldUmask = umask(0002);
455 1
            foreach ($from as $filepath) {
456
                $filename = basename($filepath);
457
                $status=copy($filepath, $to_path.$filename);
0 ignored issues
show
Unused Code introduced by
$status is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
458
                if (false) {
0 ignored issues
show
Bug introduced by
Avoid IF statements that are always true or false
Loading history...
459
                    $xoops->logger()->warning('Failed to copy asset '.$filename);
460
                } else {
461
                    $xoops->logger()->debug('Copied asset '.$filename);
462
                    ++$count;
463
                }
464 1
            }
465 1
            umask($oldUmask);
466 1
            return $count;
467
        }
468
    }
469
}
470