Completed
Push — master ( 8032e3...4751f1 )
by Morris
15:26
created

SCSSCacher::prependVersionPrefix()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, John Molakvoæ ([email protected])
4
 *
5
 * @author John Molakvoæ (skjnldsv) <[email protected]>
6
 * @author Julius Haertl <[email protected]>
7
 * @author Julius Härtl <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 *
12
 * @license GNU AGPL version 3 or any later version
13
 *
14
 * This program is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License as
16
 * published by the Free Software Foundation, either version 3 of the
17
 * License, or (at your option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
29
namespace OC\Template;
30
31
use Leafo\ScssPhp\Compiler;
32
use Leafo\ScssPhp\Exception\ParserException;
33
use Leafo\ScssPhp\Formatter\Crunched;
34
use Leafo\ScssPhp\Formatter\Expanded;
35
use OCP\Files\IAppData;
36
use OCP\Files\NotFoundException;
37
use OCP\Files\NotPermittedException;
38
use OCP\Files\SimpleFS\ISimpleFile;
39
use OCP\Files\SimpleFS\ISimpleFolder;
40
use OCP\ICache;
41
use OCP\ICacheFactory;
42
use OCP\IConfig;
43
use OCP\ILogger;
44
use OCP\IURLGenerator;
45
use OC\Files\AppData\Factory;
46
use OC\Template\IconsCacher;
47
48
class SCSSCacher {
49
50
	/** @var ILogger */
51
	protected $logger;
52
53
	/** @var IAppData */
54
	protected $appData;
55
56
	/** @var IURLGenerator */
57
	protected $urlGenerator;
58
59
	/** @var IConfig */
60
	protected $config;
61
62
	/** @var \OC_Defaults */
63
	private $defaults;
64
65
	/** @var string */
66
	protected $serverRoot;
67
68
	/** @var ICache */
69
	protected $depsCache;
70
71
	/** @var null|string */
72
	private $injectedVariables;
73
74
	/** @var ICacheFactory */
75
	private $cacheFactory;
76
77
	/** @var IconsCacher */
78
	private $iconsCacher;
79
80
	/**
81
	 * @param ILogger $logger
82
	 * @param Factory $appDataFactory
83
	 * @param IURLGenerator $urlGenerator
84
	 * @param IConfig $config
85
	 * @param \OC_Defaults $defaults
86
	 * @param string $serverRoot
87
	 * @param ICacheFactory $cacheFactory
88
	 * @param IconsCacher $iconsCacher
89
	 */
90
	public function __construct(ILogger $logger,
91
								Factory $appDataFactory,
92
								IURLGenerator $urlGenerator,
93
								IConfig $config,
94
								\OC_Defaults $defaults,
95
								$serverRoot,
96
								ICacheFactory $cacheFactory,
97
								IconsCacher $iconsCacher) {
98
		$this->logger       = $logger;
99
		$this->appData      = $appDataFactory->get('css');
100
		$this->urlGenerator = $urlGenerator;
101
		$this->config       = $config;
102
		$this->defaults     = $defaults;
103
		$this->serverRoot   = $serverRoot;
104
		$this->cacheFactory = $cacheFactory;
105
		$this->depsCache    = $cacheFactory->createDistributed('SCSS-' . md5($this->urlGenerator->getBaseUrl()));
106
		$this->iconsCacher = $iconsCacher;
107
	}
108
109
	/**
110
	 * Process the caching process if needed
111
	 *
112
	 * @param string $root Root path to the nextcloud installation
113
	 * @param string $file
114
	 * @param string $app The app name
115
	 * @return boolean
116
	 * @throws NotPermittedException
117
	 */
118
	public function process(string $root, string $file, string $app): bool {
119
		$path = explode('/', $root . '/' . $file);
120
121
		$fileNameSCSS = array_pop($path);
122
		$fileNameCSS  = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS)), $app);
123
124
		$path   = implode('/', $path);
125
		$webDir = $this->getWebDir($path, $app, $this->serverRoot, \OC::$WEBROOT);
126
127
		try {
128
			$folder = $this->appData->getFolder($app);
129
		} catch (NotFoundException $e) {
130
			// creating css appdata folder
131
			$folder = $this->appData->newFolder($app);
132
		}
133
134
		if (!$this->variablesChanged() && $this->isCached($fileNameCSS, $folder)) {
135
			// Inject icons vars css if any
136
			if ($this->iconsCacher->getCachedCSS() && $this->iconsCacher->getCachedCSS()->getSize() > 0) {
137
				$this->iconsCacher->injectCss();
138
			}
139
			return true;
140
		}
141
142
		$cached = $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir);
143
144
		// Inject icons vars css if any
145
		if ($this->iconsCacher->getCachedCSS() && $this->iconsCacher->getCachedCSS()->getSize() > 0) {
146
			$this->iconsCacher->injectCss();
147
		}
148
149
		return $cached;
150
	}
151
152
	/**
153
	 * @param $appName
154
	 * @param $fileName
155
	 * @return ISimpleFile
156
	 */
157
	public function getCachedCSS(string $appName, string $fileName): ISimpleFile {
158
		$folder         = $this->appData->getFolder($appName);
159
		$cachedFileName = $this->prependVersionPrefix($this->prependBaseurlPrefix($fileName), $appName);
160
161
		return $folder->getFile($cachedFileName);
162
	}
163
164
	/**
165
	 * Check if the file is cached or not
166
	 * @param string $fileNameCSS
167
	 * @param ISimpleFolder $folder
168
	 * @return boolean
169
	 */
170
	private function isCached(string $fileNameCSS, ISimpleFolder $folder) {
171
		try {
172
			$cachedFile = $folder->getFile($fileNameCSS);
173
			if ($cachedFile->getSize() > 0) {
174
				$depFileName = $fileNameCSS . '.deps';
175
				$deps        = $this->depsCache->get($folder->getName() . '-' . $depFileName);
176
				if ($deps === null) {
177
					$depFile = $folder->getFile($depFileName);
178
					$deps    = $depFile->getContent();
179
					//Set to memcache for next run
180
					$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
181
				}
182
				$deps = json_decode($deps, true);
183
184 View Code Duplication
				foreach ((array) $deps as $file => $mtime) {
185
					if (!file_exists($file) || filemtime($file) > $mtime) {
186
						return false;
187
					}
188
				}
189
190
				return true;
191
			}
192
193
			return false;
194
		} catch (NotFoundException $e) {
195
			return false;
196
		}
197
	}
198
199
	/**
200
	 * Check if the variables file has changed
201
	 * @return bool
202
	 */
203
	private function variablesChanged(): bool {
204
		$injectedVariables = $this->getInjectedVariables();
205
		if ($this->config->getAppValue('core', 'scss.variables') !== md5($injectedVariables)) {
206
			$this->resetCache();
207
			$this->config->setAppValue('core', 'scss.variables', md5($injectedVariables));
208
209
			return true;
210
		}
211
212
		return false;
213
	}
214
215
	/**
216
	 * Cache the file with AppData
217
	 *
218
	 * @param string $path
219
	 * @param string $fileNameCSS
220
	 * @param string $fileNameSCSS
221
	 * @param ISimpleFolder $folder
222
	 * @param string $webDir
223
	 * @return boolean
224
	 * @throws NotPermittedException
225
	 */
226
	private function cache(string $path, string $fileNameCSS, string $fileNameSCSS, ISimpleFolder $folder, string $webDir) {
227
		$scss = new Compiler();
228
		$scss->setImportPaths([
229
			$path,
230
			$this->serverRoot . '/core/css/'
231
		]);
232
233
		// Continue after throw
234
		$scss->setIgnoreErrors(true);
235
		if ($this->config->getSystemValue('debug')) {
236
			// Debug mode
237
			$scss->setFormatter(Expanded::class);
238
			$scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
239
		} else {
240
			// Compression
241
			$scss->setFormatter(Crunched::class);
242
		}
243
244
		try {
245
			$cachedfile = $folder->getFile($fileNameCSS);
246
		} catch (NotFoundException $e) {
247
			$cachedfile = $folder->newFile($fileNameCSS);
248
		}
249
250
		$depFileName = $fileNameCSS . '.deps';
251
		try {
252
			$depFile = $folder->getFile($depFileName);
253
		} catch (NotFoundException $e) {
254
			$depFile = $folder->newFile($depFileName);
255
		}
256
257
		// Compile
258
		try {
259
			$compiledScss = $scss->compile(
260
				'$webroot: \'' . $this->getRoutePrefix() . '\';' .
261
				'@import "variables.scss";' .
262
				'@import "functions.scss";' .
263
				$this->getInjectedVariables() .
264
				'@import "' . $fileNameSCSS . '";');
265
		} catch (ParserException $e) {
0 ignored issues
show
Bug introduced by
The class Leafo\ScssPhp\Exception\ParserException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
266
			$this->logger->error($e, ['app' => 'core']);
267
268
			return false;
269
		}
270
271
		// Parse Icons and create related css variables
272
		$compiledScss = $this->iconsCacher->setIconsCss($compiledScss);
273
274
		// Gzip file
275
		try {
276
			$gzipFile = $folder->getFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
277
		} catch (NotFoundException $e) {
278
			$gzipFile = $folder->newFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
279
		}
280
281
		try {
282
			$data = $this->rebaseUrls($compiledScss, $webDir);
283
			$cachedfile->putContent($data);
284
			$deps = json_encode($scss->getParsedFiles());
285
			$depFile->putContent($deps);
286
			$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
287
			$gzipFile->putContent(gzencode($data, 9));
288
			$this->logger->debug('SCSSCacher: ' . $webDir . '/' . $fileNameSCSS . ' compiled and successfully cached', ['app' => 'core']);
289
290
			return true;
291
		} catch (NotPermittedException $e) {
292
			$this->logger->error('SCSSCacher: unable to cache: ' . $fileNameSCSS);
293
294
			return false;
295
		}
296
	}
297
298
	/**
299
	 * Reset scss cache by deleting all generated css files
300
	 * We need to regenerate all files when variables change
301
	 */
302
	public function resetCache() {
303
		$this->injectedVariables = null;
304
		$this->cacheFactory->createDistributed('SCSS-')->clear();
305
		$appDirectory = $this->appData->getDirectoryListing();
306
		foreach ($appDirectory as $folder) {
307
			foreach ($folder->getDirectoryListing() as $file) {
308
				try {
309
					$file->delete();
310
				} catch (NotPermittedException $e) {
311
					$this->logger->logException($e, ['message' => 'SCSSCacher: unable to delete file: ' . $file->getName()]);
0 ignored issues
show
Documentation introduced by
$e is of type object<OCP\Files\NotPermittedException>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
312
				}
313
			}
314
		}
315
	}
316
317
	/**
318
	 * @return string SCSS code for variables from OC_Defaults
319
	 */
320
	private function getInjectedVariables(): string {
321
		if ($this->injectedVariables !== null) {
322
			return $this->injectedVariables;
323
		}
324
		$variables = '';
325
		foreach ($this->defaults->getScssVariables() as $key => $value) {
326
			$variables .= '$' . $key . ': ' . $value . ';';
327
		}
328
329
		// check for valid variables / otherwise fall back to defaults
330
		try {
331
			$scss = new Compiler();
332
			$scss->compile($variables);
333
			$this->injectedVariables = $variables;
334
		} catch (ParserException $e) {
0 ignored issues
show
Bug introduced by
The class Leafo\ScssPhp\Exception\ParserException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
335
			$this->logger->error($e, ['app' => 'core']);
336
		}
337
338
		return $variables;
339
	}
340
341
	/**
342
	 * Add the correct uri prefix to make uri valid again
343
	 * @param string $css
344
	 * @param string $webDir
345
	 * @return string
346
	 */
347
	private function rebaseUrls(string $css, string $webDir): string {
348
		$re    = '/url\([\'"]([^\/][\.\w?=\/-]*)[\'"]\)/x';
349
		$subst = 'url(\'' . $webDir . '/$1\')';
350
351
		return preg_replace($re, $subst, $css);
352
	}
353
354
	/**
355
	 * Return the cached css file uri
356
	 * @param string $appName the app name
357
	 * @param string $fileName
358
	 * @return string
359
	 */
360 View Code Duplication
	public function getCachedSCSS(string $appName, string $fileName): string {
361
		$tmpfileLoc = explode('/', $fileName);
362
		$fileName   = array_pop($tmpfileLoc);
363
		$fileName   = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileName)), $appName);
364
365
		return substr($this->urlGenerator->linkToRoute('core.Css.getCss', ['fileName' => $fileName, 'appName' => $appName]), strlen(\OC::$WEBROOT) + 1);
366
	}
367
368
	/**
369
	 * Prepend hashed base url to the css file
370
	 * @param string $cssFile
371
	 * @return string
372
	 */
373
	private function prependBaseurlPrefix(string $cssFile): string {
374
		return substr(md5($this->urlGenerator->getBaseUrl() . $this->getRoutePrefix()), 0, 4) . '-' . $cssFile;
375
	}
376
377
	private function getRoutePrefix() {
378
		$frontControllerActive = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true');
379
		$prefix = \OC::$WEBROOT . '/index.php';
380
		if ($frontControllerActive) {
381
			$prefix = \OC::$WEBROOT;
382
		}
383
		return $prefix;
384
	}
385
386
	/**
387
	 * Prepend hashed app version hash
388
	 * @param string $cssFile
389
	 * @param string $appId
390
	 * @return string
391
	 */
392
	private function prependVersionPrefix(string $cssFile, string $appId): string {
393
		$appVersion = \OC_App::getAppVersion($appId);
394
		if ($appVersion !== '0') {
395
			return substr(md5($appVersion), 0, 4) . '-' . $cssFile;
396
		}
397
		$coreVersion = \OC_Util::getVersionString();
398
399
		return substr(md5($coreVersion), 0, 4) . '-' . $cssFile;
400
	}
401
402
	/**
403
	 * Get WebDir root
404
	 * @param string $path the css file path
405
	 * @param string $appName the app name
406
	 * @param string $serverRoot the server root path
407
	 * @param string $webRoot the nextcloud installation root path
408
	 * @return string the webDir
409
	 */
410
	private function getWebDir(string $path, string $appName, string $serverRoot, string $webRoot): string {
411
		// Detect if path is within server root AND if path is within an app path
412
		if (strpos($path, $serverRoot) === false && $appWebPath = \OC_App::getAppWebPath($appName)) {
413
			// Get the file path within the app directory
414
			$appDirectoryPath = explode($appName, $path)[1];
415
			// Remove the webroot
416
417
			return str_replace($webRoot, '', $appWebPath . $appDirectoryPath);
418
		}
419
420
		return $webRoot . substr($path, strlen($serverRoot));
421
	}
422
}
423