Completed
Push — master ( 29f185...9200bf )
by Michael
18s
created

Assets::copyFileAssets()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 34
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.0554
Metric Value
dl 0
loc 34
ccs 20
cts 23
cp 0.8696
rs 8.439
cc 5
eloc 25
nc 8
nop 3
crap 5.0554
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
use Xmf\Yaml;
24
25
/**
26
 * Provides a standardized asset strategy
27
 *
28
 * @category  Assets
29
 * @package   Assets
30
 * @author    Richard Griffith <[email protected]>
31
 * @copyright 2014-2015 XOOPS Project (http://xoops.org)
32
 * @license   GNU GPL 2 or later (http://www.gnu.org/licenses/gpl-2.0.html)
33
 * @version   Release: 1.0
34
 * @link      http://xoops.org
35
 * @since     2.6.0
36
 */
37
class Assets
38
{
39
    /**
40
     * @var boolean
41
     */
42
    private $debug = false;
43
44
    /**
45
     * @var array of default filter strings - may be overridden by prefs
46
     */
47
    private $default_filters = array(
48
            'css' => 'cssimport,cssembed,?cssmin',
49
            'js'  => '?jsqueeze',
50
    );
51
52
    /**
53
     * @var array of output locations in assets directory
54
     */
55
    private $default_output = array(
56
            'css' => 'css/*.css',
57
            'js'  => 'js/*.js',
58
    );
59
60
    /**
61
     * @var array of asset reference definitions - may be overridden by prefs
62
     */
63
    private $default_asset_refs = array(
64
        array(
65
            'name' => 'jquery',
66
            'assets' => array('media/jquery/jquery.js'),
67
            'filters' => null,
68
        ),
69
        array(
70
            'name' => 'jqueryui',
71
            'assets' => array('media/jquery/ui/jquery-ui.js'),
72
            'filters' => null,
73
        ),
74
        array(
75
            'name' => 'jgrowl',
76
            'assets' => array('media/jquery/plugins/jquery.jgrowl.js'),
77
            'filters' => null,
78
        ),
79
    );
80
81
    /**
82
     * @var AssetManager
83
     */
84
    private $assetManager = null;
85
86
    /**
87
     * @var string config file with assets prefs
88
     */
89
    private $assetsPrefsFilename = 'var/configs/system_assets_prefs.yml';
90
91
    /**
92
     * @var string config cache key
93
     */
94
    private $assetsPrefsCacheKey = 'system/assets/prefs';
95
96
    /**
97
     * @var string string to identify Assetic filters using instanceof
98
     */
99
100
    private $filterInterface = '\Assetic\Filter\FilterInterface';
101
    /**
102
     * __construct
103
     */
104 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...
105
    {
106 3
        $this->assetManager = new AssetManager();
107 3
        if (isset($_REQUEST['ASSET_DEBUG'])) {
108
            $this->setDebug();
109
        }
110 3
        $this->readAssetsPrefs();
111
        // register any asset references
112 3
        foreach ($this->default_asset_refs as $ref) {
113 3
            $this->registerAssetReference($ref['name'], $ref['assets'], $ref['filters']);
114
        }
115 3
    }
116
117
    /**
118
     * readAssetsPrefs - read configured asset preferences
119
     *
120
     * @return array of assets preferences
121
     */
122 3
    protected function readAssetsPrefs()
123
    {
124 3
        $xoops = \Xoops::getInstance();
125
126 3
        $assetsPrefs = array();
127
128
        try {
129 3
            $assetsPrefs = $xoops->cache()->read($this->assetsPrefsCacheKey);
130 3
            $file = $xoops->path($this->assetsPrefsFilename);
131 3
            $mtime = filemtime($file);
132 3
            if ($assetsPrefs===false || !isset($assetsPrefs['mtime']) || !$mtime
133 3
                || (isset($assetsPrefs['mtime']) && $assetsPrefs['mtime']<$mtime)) {
134 1
                if ($mtime) {
135
                    $assetsPrefs = Yaml::read($file);
136
                    if (!is_array($assetsPrefs)) {
137
                        $xoops->logger()->error("Invalid config in system_assets_prefs.yml");
138
                        $assetsPrefs = array();
139
                    } else {
140
                        $assetsPrefs['mtime']=$mtime;
141
                        $xoops->cache()->write($this->assetsPrefsCacheKey, $assetsPrefs);
142
                    }
143
                } else {
144
                    // use defaults to create file
145
                    $assetsPrefs = array(
146 1
                        'default_filters' => $this->default_filters,
147 1
                        'default_asset_refs' => $this->default_asset_refs,
148 1
                        'mtime' => time(),
149
                    );
150 1
                    $this->saveAssetsPrefs($assetsPrefs);
151
                }
152
            }
153 3
            if (!empty($assetsPrefs['default_filters']) && is_array($assetsPrefs['default_filters'])) {
154 3
                $this->default_filters = $assetsPrefs['default_filters'];
155
            }
156 3
            if (!empty($assetsPrefs['default_asset_refs']) && is_array($assetsPrefs['default_asset_refs'])) {
157 3
                $this->default_asset_refs = $assetsPrefs['default_asset_refs'];
158
            }
159
        } catch (\Exception $e) {
160
            $xoops->events()->triggerEvent('core.exception', $e);
161
            $assetsPrefs = array();
162
        }
163 3
        return $assetsPrefs;
164
    }
165
166
    /**
167
     * saveAssetsPrefs - record array of assets preferences in config file, and
168
     * update cache
169
     *
170
     * @param array $assets_prefs array of asset preferences to save
171
     *
172
     * @return void
173
     */
174 1 View Code Duplication
    protected function saveAssetsPrefs($assets_prefs)
175
    {
176 1
        if (is_array($assets_prefs)) {
177 1
            $xoops = \Xoops::getInstance();
178
            try {
179 1
                Yaml::save($assets_prefs, $xoops->path($this->assetsPrefsFilename));
180 1
                $xoops->cache()->write($this->assetsPrefsCacheKey, $assets_prefs);
181
            } catch (\Exception $e) {
182
                $xoops->events()->triggerEvent('core.exception', $e);
183
            }
184
        }
185 1
    }
186
187
188
    /**
189
     * getUrlToAssets
190
     *
191
     * Create an asset file from a list of assets
192
     *
193
     * @param string       $type    type of asset, css or js
194
     * @param array        $assets  list of source files to process
195
     * @param string|array $filters either a comma separated list of known namsed filters
196
     *                              or an array of named filters and/or filter object
197
     * @param string       $target  target path, will default to assets directory
198
     *
199
     * @return string URL to asset file
200
     */
201 1
    public function getUrlToAssets($type, $assets, $filters = 'default', $target = null)
202
    {
203 1
        if (is_scalar($assets)) {
204
            $assets = array($assets); // just a single path name
205
        }
206
207 1
        if ($filters==='default') {
208 1
            if (isset($this->default_filters[$type])) {
209 1
                $filters = $this->default_filters[$type];
210
            } else {
211
                $filters = '';
212
            }
213
        }
214
215 1 View Code Duplication
        if (!is_array($filters)) {
216 1
            if (empty($filters)) {
217
                $filters = array();
218
            } else {
219 1
                $filters = explode(',', str_replace(' ', '', $filters));
220
            }
221
        }
222
223 1
        if (isset($this->default_output[$type])) {
224 1
            $output = $this->default_output[$type];
225
        } else {
226
            $output = '';
227
        }
228
229 1
        $xoops = \Xoops::getInstance();
230
231 1
        if (isset($target)) {
232
            $target_path = $target;
233
        } else {
234 1
            $target_path = $xoops->path('assets');
235
        }
236
237
        try {
238 1
            $am = $this->assetManager;
239 1
            $fm = new FilterManager();
240
241 1
            foreach ($filters as $filter) {
242 1
                if (is_object($filter) && $filter instanceof $this->filterInterface) {
243
                    $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...
244
                } else {
245 1
                    switch (ltrim($filter, '?')) {
246 1
                        case 'cssembed':
247
                            $fm->set('cssembed', new Filter\PhpCssEmbedFilter());
248
                            break;
249 1
                        case 'cssmin':
250
                            $fm->set('cssmin', new Filter\CssMinFilter());
251
                            break;
252 1
                        case 'cssimport':
253
                            $fm->set('cssimport', new Filter\CssImportFilter());
254
                            break;
255 1
                        case 'cssrewrite':
256
                            $fm->set('cssrewrite', new Filter\CssRewriteFilter());
257
                            break;
258 1
                        case 'lessphp':
259
                            $fm->set('lessphp', new Filter\LessphpFilter());
260
                            break;
261 1
                        case 'scssphp':
262
                            $fm->set('scssphp', new Filter\ScssphpFilter());
263
                            break;
264 1
                        case 'jsmin':
265
                            $fm->set('jsmin', new Filter\JSMinFilter());
266
                            break;
267 1
                        case 'jsqueeze':
268 1
                            $fm->set('jsqueeze', new Filter\JSqueezeFilter());
269 1
                            break;
270
                        default:
271
                            throw new \Exception(sprintf('%s filter not implemented.', $filter));
272 1
                            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...
273
                    }
274
                }
275
            }
276
277
            // Factory setup
278 1
            $factory = new AssetFactory($target_path);
279 1
            $factory->setAssetManager($am);
280 1
            $factory->setFilterManager($fm);
281 1
            $factory->setDefaultOutput($output);
282 1
            $factory->setDebug($this->debug);
283 1
            $factory->addWorker(new CacheBustingWorker());
284
285
            // Prepare the assets writer
286 1
            $writer = new AssetWriter($target_path);
287
288
            // Translate asset paths, remove duplicates
289 1
            $translated_assets = array();
290 1
            foreach ($assets as $k => $v) {
291
                // translate path if not a reference or absolute path
292 1
                if (0 == preg_match("/^\\/|^\\\\|^[a-zA-Z]:|^@/", $v)) {
293 1
                    $v = $xoops->path($v);
294
                }
295 1
                if (!in_array($v, $translated_assets)) {
296 1
                    $translated_assets[] = $v;
297
                }
298
            }
299
300
            // Create the asset
301 1
            $asset = $factory->createAsset(
302
                $translated_assets,
303
                $filters
304
            );
305 1
            $asset_path = $asset->getTargetPath();
306 1
            if (!is_readable($target_path . $asset_path)) {
307
                $assetKey = 'Asset '.$asset_path;
308
                $xoops->events()->triggerEvent('debug.timer.start', $assetKey);
309
                $oldumask = umask(0002);
310
                $writer->writeAsset($asset);
311
                umask($oldumask);
312
                $xoops->events()->triggerEvent('debug.timer.stop', $assetKey);
313
            }
314
315 1
            return $xoops->url('assets/' . $asset_path);
316
317
        } catch (\Exception $e) {
318
            $xoops->events()->triggerEvent('core.exception', $e);
319
            return null;
320
        }
321
    }
322
323
324
    /**
325
     * setDebug enable debug mode, will skip filters prefixed with '?'
326
     *
327
     * @return true
328
     */
329 1
    public function setDebug()
330
    {
331 1
        $this->debug = true;
332 1
        return true;
333
    }
334
335
    /**
336
     * Add an asset reference to the asset manager
337
     *
338
     * @param string       $name    the name of the reference to be added
339
     * @param mixed        $assets  a string asset path, or an array of asset paths,
340
     *                              may include wildcard
341
     * @param string|array $filters either a comma separated list of known named filters
342
     *                              or an array of named filters and/or filter object
343
     *
344
     * @return boolean true if asset registers, false on error
345
     */
346 3
    public function registerAssetReference($name, $assets, $filters = null)
347
    {
348 3
        $xoops = \Xoops::getInstance();
349
350 3
        $assetArray = array();
351 3
        $filterArray = array();
352
353
        try {
354 3
            if (is_scalar($assets)) {
355 1
                $assets = array($assets);  // just a single path name
356
            }
357 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...
358
                // translate path if not a reference or absolute path
359 3
                if ((substr_compare($a, '@', 0, 1) != 0)
360 3
                    && (substr_compare($a, '/', 0, 1) != 0)) {
361 3
                    $a = $xoops->path($a);
362
                }
363 3
                if (false===strpos($a, '*')) {
364 3
                    $assetArray[] = new FileAsset($a); // single file
365
                } else {
366 3
                    $assetArray[] = new GlobAsset($a);  // wild card match
367
                }
368
            }
369
370 3 View Code Duplication
            if (!is_array($filters)) {
371 3
                if (empty($filters)) {
372 3
                    $filters = array();
373
                } else {
374
                    $filters = explode(',', str_replace(' ', '', $filters));
375
                }
376
            }
377 3
            foreach ($filters as $filter) {
378
                if (is_object($filter) && $filter instanceof $this->filterInterface) {
379
                    $filterArray[] = $filter;
380
                } else {
381
                    switch (ltrim($filter, '?')) {
382
                        case 'cssembed':
383
                            $filterArray[] = new Filter\PhpCssEmbedFilter();
384
                            break;
385
                        case 'cssmin':
386
                            $filterArray[] = new Filter\CssMinFilter();
387
                            break;
388
                        case 'cssimport':
389
                            $filterArray[] = new Filter\CssImportFilter();
390
                            break;
391
                        case 'cssrewrite':
392
                            $filterArray[] = new Filter\CssRewriteFilter();
393
                            break;
394
                        case 'lessphp':
395
                            $filterArray[] = new Filter\LessphpFilter();
396
                            break;
397
                        case 'scssphp':
398
                            $filterArray[] = new Filter\ScssphpFilter();
399
                            break;
400
                        case 'jsmin':
401
                            $filterArray[] = new Filter\JSMinFilter();
402
                            break;
403
                        case 'jsqueeze':
404
                            $filterArray[] = new Filter\JSqueezeFilter();
405
                            break;
406
                        default:
407
                            throw new \Exception(sprintf('%s filter not implemented.', $filter));
408
                            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...
409
                    }
410
                }
411
            }
412
413 3
            $collection = new AssetCollection($assetArray, $filterArray);
414 3
            $this->assetManager->set($name, $collection);
415
416 3
            return true;
417
        } catch (\Exception $e) {
418
            $xoops->events()->triggerEvent('core.exception', $e);
419
            return false;
420
        }
421
    }
422
423
    /**
424
     * copyFileAssets - copy files to the appropriate asset directory.
425
     *
426
     * Copying is normally only needed for fonts or images when they are referenced by a
427
     * relative url in stylesheet, or are located outside of the web root.
428
     *
429
     * @param string $from_path path to files to copy
430
     * @param string $pattern   glob pattern to match files to be copied
431
     * @param string $output    output type (css, fonts, images, js)
432
     *
433
     * @return mixed boolean false if target directory is not writable, otherwise
434
     *               integer count of files copied
435
     */
436 1
    public function copyFileAssets($from_path, $pattern, $output)
437
    {
438 1
        $xoops = \Xoops::getInstance();
439
440 1
        $to_path = $xoops->path('assets') . '/' . $output . '/';
441 1
        $from = glob($from_path . '/' . $pattern);
442 1
        $xoops->events()->triggerEvent('debug.log', $from);
443
444 1
        if (!is_dir($to_path)) {
445 1
            $oldUmask = umask(0);
446 1
            mkdir($to_path, 0775, true);
447 1
            umask($oldUmask);
448
        }
449
450 1
        if (!is_writable($to_path)) {
451
            $xoops->logger()->warning('Asset directory is not writable. ' . $output);
452
            return false;
453
        } else {
454 1
            $count = 0;
455 1
            $oldUmask = umask(0002);
456 1
            foreach ($from as $filepath) {
457 1
                $filename = basename($filepath);
458 1
                $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...
459 1
                if (false) {
0 ignored issues
show
Bug introduced by
Avoid IF statements that are always true or false
Loading history...
460
                    $xoops->logger()->warning('Failed to copy asset '.$filename);
461
                } else {
462 1
                    $xoops->logger()->debug('Copied asset '.$filename);
463 1
                    ++$count;
464
                }
465
            }
466 1
            umask($oldUmask);
467 1
            return $count;
468
        }
469
    }
470
}
471