Completed
Push — master ( 61bf0f...aec613 )
by Cheren
02:26
created

LessHelper::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 13
rs 9.4285
cc 1
eloc 9
nc 1
nop 1
1
<?php
2
/**
3
 * CakeCMS Core
4
 *
5
 * This file is part of the of the simple cms based on CakePHP 3.
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 *
9
 * @package   Core
10
 * @license   MIT
11
 * @copyright MIT License http://www.opensource.org/licenses/mit-license.php
12
 * @link      https://github.com/CakeCMS/Core".
13
 * @author    Sergey Kalistratov <[email protected]>
14
 */
15
16
namespace Core\View\Helper;
17
18
use Core\Plugin;
19
use JBZoo\Utils\FS;
20
use JBZoo\Utils\Str;
21
use Core\Lib\Less\Less;
22
use Cake\Routing\Router;
23
use Cake\Core\Configure;
24
use Cake\Filesystem\File;
25
use Cake\Utility\Inflector;
26
27
/**
28
 * Class LessHelper
29
 *
30
 * @package Core\View\Helper
31
 * @property \Cake\View\Helper\UrlHelper $Url
32
 */
33
class LessHelper extends AppHelper
34
{
35
36
    /**
37
     * List of helpers used by this helper.
38
     *
39
     * @var array
40
     */
41
    public $helpers = [
42
        'Url',
43
    ];
44
45
    /**
46
     * Constructor hook method.
47
     *
48
     * @param array $config
49
     */
50
    public function initialize(array $config)
51
    {
52
        $corePath  = Plugin::path('Core') . Configure::read('App.webroot') . '/' . Configure::read('App.lessBaseUrl');
53
        $cachePath = WWW_ROOT . Configure::read('App.cssBaseUrl') . Configure::read('App.cacheDir');
54
55
        $this->config('root_path', APP_ROOT);
56
        $this->config('cache_path', $cachePath);
57
        $this->config('root_url', Router::fullBaseUrl());
58
        $this->config('debug', Configure::read('debug'));
59
        $this->config('import_paths', [realpath($corePath) => realpath($corePath)]);
60
61
        parent::initialize($config);
62
    }
63
64
    /**
65
     * Process less file.
66
     *
67
     * @param string $source
68
     * @param bool $force
69
     * @return string|null
70
     * @throws \JBZoo\Less\Exception
71
     */
72
    public function process($source, $force = true)
73
    {
74
        list ($webRoot, $source) = $this->_findWebRoot($source);
75
        $lessFile = FS::clean($webRoot . Configure::read('App.lessBaseUrl') . $source, '/');
76
77
        $this->_setForce($force);
78
        $less = new Less($this->_config);
79
80
        if (!FS::isFile($lessFile)) {
81
            return null;
82
        }
83
84
        list($source, $isExpired) = $less->compile($lessFile);
85
86
        if ($isExpired) {
87
            $cacheId  = FS::firstLine($source);
88
            $comment  = '/* resource:' . str_replace(FS::clean(ROOT, '/'), '', $lessFile) . ' */' . PHP_EOL;
89
            $fileHead = implode('', [$cacheId, Str::low($comment)]);
90
91
            $css = $this->_normalizeContent($source, $fileHead);
92
            $this->_write($source, $css);
93
        }
94
95
        $source = str_replace(FS::clean(APP_ROOT . '/' . Configure::read('App.webroot'), '/'), '', $source);
96
        $source = str_replace(Configure::read('App.cssBaseUrl'), '', $source);
97
98
        return $source;
99
    }
100
101
    /**
102
     * CSS compressing.
103
     *
104
     * @param string $code
105
     * @param string $cacheId
106
     * @return string
107
     */
108
    protected function _compress($code, $cacheId)
109
    {
110
        $code = (string) $code;
111
112
        // remove comments
113
        $code = preg_replace('#/\*[^*]*\*+([^/][^*]*\*+)*/#ius', '', $code);
114
115
        $code = str_replace(
116
            ["\r\n", "\r", "\n", "\t", '  ', '    ', ' {', '{ ', ' }', '; ', ';;', ';;;', ';;;;', ';}'],
117
            ['', '', '', '', '', '', '{', '{', '}', ';', ';', ';', ';', '}'],
118
            $code
119
        ); // remove tabs, spaces, newlines, etc.
120
121
        // remove spaces after and before colons
122
        $code = preg_replace('#([a-z\-])(:\s*|\s*:\s*|\s*:)#ius', '$1:', $code);
123
124
        // spaces before "!important"
125
        $code = preg_replace('#(\s*\!important)#ius', '!important', $code);
126
        $code = Str::trim($code);
127
128
        return implode('', [$cacheId, $code]);
129
    }
130
131
    /**
132
     * Find source webroot dir.
133
     *
134
     * @param string $source
135
     * @return array
136
     */
137
    protected function _findWebRoot($source)
138
    {
139
        $webRootDir = Configure::read('App.webroot');
140
        $webRoot    = APP_ROOT . DS . $webRootDir . DS;
141
142
        list ($plugin, $source) = $this->_View->pluginSplit($source);
143
        if ($plugin !== null && Plugin::loaded($plugin)) {
144
            $webRoot = Plugin::path($plugin) . $webRootDir . DS;
145
        }
146
147
        return [FS::clean($webRoot, '/'), $source];
148
    }
149
150
    /**
151
     * Get asset url by source.
152
     *
153
     * @param string $source (Example: TestPlugin.path/to/image.png)
154
     * @return string
155
     */
156
    protected function _getAssetUrl($source)
157
    {
158
        return 'url("' . $this->Url->assetUrl($source, ['fullBase' => true]) . '")';
159
    }
160
161
    /**
162
     * Get plugin asset url.
163
     *
164
     * @param string $path
165
     * @return array
166
     */
167
    protected function _getPlgAssetUrl($path)
168
    {
169
        $isPlugin = false;
170
        $plgPaths = Configure::read('App.paths.plugins');
171
        foreach ($plgPaths as $plgPath) {
172
            $plgPath = FS::clean($plgPath, '/');
173 View Code Duplication
            if (preg_match('(' . quotemeta($plgPath) . ')', $path)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
174
                $path = str_replace($plgPath, '', $path);
175
                return [true, $path];
176
            }
177
        }
178
179
        foreach (Configure::read('plugins') as $name => $plgPath) {
180
            $plgPath = FS::clean($plgPath, '/');
181 View Code Duplication
            if (preg_match('(' . quotemeta($plgPath) . ')', $path)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
182
                $path = Str::low($name) . '/' . str_replace($plgPath, '', $path);
183
                return [true, $path];
184
            }
185
        }
186
187
        return [$isPlugin, $path];
188
    }
189
190
    /**
191
     * Normalize style file.
192
     *
193
     * @param string $path
194
     * @param string $fileHead
195
     * @return mixed
196
     */
197
    protected function _normalizeContent($path, $fileHead)
198
    {
199
        $css = file_get_contents($path);
200
        if (!$this->_configRead('debug')) {
201
            $css = $this->_compress($css, $fileHead);
202
        } else {
203
            list ($first, $second) = explode(PHP_EOL, $css, 2);
204
            if (preg_match('(\/* cacheid:)', $first)) {
205
                $css = $second;
206
            }
207
208
            $css = implode('', [$fileHead, $css]);
209
        }
210
211
        return $this->_replaceUrl($css);
212
    }
213
214
    /**
215
     * Normalize plugin asset url.
216
     *
217
     * @param string $path
218
     * @return string
219
     */
220
    protected function _normalizePlgAssetUrl($path)
221
    {
222
        $details = explode('/', $path, 3);
223
        $pluginName = Inflector::camelize(trim($details[0], '/'));
224
225
        dump($pluginName);
226
227
        if (Plugin::loaded($pluginName)) {
0 ignored issues
show
Bug introduced by
It seems like $pluginName defined by \Cake\Utility\Inflector:...trim($details[0], '/')) on line 223 can also be of type boolean; however, Cake\Core\Plugin::loaded() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
228
            unset($details[0]);
229
            $source = $pluginName . '.' . ltrim(implode('/', $details), '/');
230
            dump($source);
231
            dump($this->Url->assetUrl($source, ['fullBase' => true]));
232
            return $this->_getAssetUrl($source);
233
        }
234
235
        return $this->_getAssetUrl($path);
236
    }
237
238
    /**
239
     * Preg replace url.
240
     *
241
     * @param string $css
242
     * @return mixed
243
     */
244
    protected function _replaceUrl($css)
245
    {
246
        $pattern = '/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/';
247
        return preg_replace_callback($pattern, [__CLASS__, '_replaceUrlCallback'], $css);
248
    }
249
250
    /**
251
     * Replace url callback.
252
     *
253
     * @param array $match
254
     * @return string
255
     */
256
    protected function _replaceUrlCallback(array $match)
257
    {
258
        $assetPath = str_replace(Router::fullBaseUrl(), '', $match[2]);
259
        $assetPath = trim(FS::clean($assetPath, '/'), '/');
260
        $appDir    = FS::clean(APP_ROOT, '/');
261
dump($assetPath);
262
dump($appDir);
263
        list ($isPlugin, $assetPath) = $this->_getPlgAssetUrl($assetPath);
264
265
        if (!$isPlugin) {
266
            $assetPath = str_replace($appDir, '', $assetPath);
267
        }
268
269
        dump($assetPath);
270
        $assetPath = str_replace(Configure::read('App.webroot'), '', $assetPath);
271
        $assetPath = FS::clean($assetPath, '/');
272
        dump($assetPath);
273
274
        if ($isPlugin) {
275
            return $this->_normalizePlgAssetUrl($assetPath);
276
        }
277
278
        return $this->_getAssetUrl($assetPath);
279
    }
280
281
    /**
282
     * Set less process force.
283
     *
284
     * @param bool $force
285
     * @return void
286
     */
287
    protected function _setForce($force = false)
288
    {
289
        if ($force) {
290
            $this->config('force', true);
291
        }
292
    }
293
294
    /**
295
     * Write content in to the file.
296
     *
297
     * @param string $path
298
     * @param string $content
299
     * @return void
300
     */
301
    protected function _write($path, $content)
302
    {
303
        $File = new File($path);
304
        $File->write($content);
305
        $File->exists();
306
    }
307
}
308