Passed
Push — master ( 18f234...5a0b28 )
by John
16:38 queued 11s
created

SCSSCacher::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 9
dl 0
loc 25
rs 9.7666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, John Molakvoæ ([email protected])
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author John Molakvoæ <[email protected]>
7
 * @author Julius Haertl <[email protected]>
8
 * @author Julius Härtl <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Roland Tapken <[email protected]>
13
 *
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
namespace OC\Template;
31
32
use OC\AppConfig;
33
use OC\Files\AppData\Factory;
34
use OC\Memcache\NullCache;
35
use OCP\AppFramework\Utility\ITimeFactory;
36
use OCP\Files\IAppData;
37
use OCP\Files\NotFoundException;
38
use OCP\Files\NotPermittedException;
39
use OCP\Files\SimpleFS\ISimpleFile;
40
use OCP\Files\SimpleFS\ISimpleFolder;
41
use OCP\ICache;
42
use OCP\ICacheFactory;
43
use OCP\IConfig;
44
use OCP\IMemcache;
45
use OCP\IURLGenerator;
46
use Psr\Log\LoggerInterface;
47
use ScssPhp\ScssPhp\Compiler;
48
use ScssPhp\ScssPhp\OutputStyle;
49
50
class SCSSCacher {
51
	protected LoggerInterface $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 ICache */
78
	private $isCachedCache;
79
80
	/** @var ITimeFactory */
81
	private $timeFactory;
82
83
	/** @var IMemcache */
84
	private $lockingCache;
85
	/** @var AppConfig */
86
	private $appConfig;
87
88
	/**
89
	 * @param string $serverRoot
90
	 */
91
	public function __construct(LoggerInterface $logger,
92
								Factory $appDataFactory,
93
								IURLGenerator $urlGenerator,
94
								IConfig $config,
95
								\OC_Defaults $defaults,
96
								$serverRoot,
97
								ICacheFactory $cacheFactory,
98
								ITimeFactory $timeFactory,
99
								AppConfig $appConfig) {
100
		$this->logger = $logger;
101
		$this->appData = $appDataFactory->get('css');
102
		$this->urlGenerator = $urlGenerator;
103
		$this->config = $config;
104
		$this->defaults = $defaults;
105
		$this->serverRoot = $serverRoot;
106
		$this->cacheFactory = $cacheFactory;
107
		$this->depsCache = $cacheFactory->createDistributed('SCSS-deps-' . md5($this->urlGenerator->getBaseUrl()));
108
		$this->isCachedCache = $cacheFactory->createDistributed('SCSS-cached-' . md5($this->urlGenerator->getBaseUrl()));
109
		$lockingCache = $cacheFactory->createDistributed('SCSS-locks-' . md5($this->urlGenerator->getBaseUrl()));
110
		if (!($lockingCache instanceof IMemcache)) {
111
			$lockingCache = new NullCache();
112
		}
113
		$this->lockingCache = $lockingCache;
114
		$this->timeFactory = $timeFactory;
115
		$this->appConfig = $appConfig;
116
	}
117
118
	/**
119
	 * Process the caching process if needed
120
	 *
121
	 * @param string $root Root path to the nextcloud installation
122
	 * @param string $file
123
	 * @param string $app The app name
124
	 * @return boolean
125
	 * @throws NotPermittedException
126
	 */
127
	public function process(string $root, string $file, string $app): bool {
128
		$path = explode('/', $root . '/' . $file);
129
130
		$fileNameSCSS = array_pop($path);
131
		$fileNameCSS = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS)), $app);
132
133
		$path = implode('/', $path);
134
		$webDir = $this->getWebDir($path, $app, $this->serverRoot, \OC::$WEBROOT);
135
136
		$this->logger->debug('SCSSCacher::process ordinary check follows', ['app' => 'scss_cacher']);
137
138
		try {
139
			$folder = $this->appData->getFolder($app);
140
		} catch (NotFoundException $e) {
141
			// creating css appdata folder
142
			$folder = $this->appData->newFolder($app);
143
		}
144
145
		$lockKey = $webDir . '/' . $fileNameSCSS;
146
147
		if (!$this->lockingCache->add($lockKey, 'locked!', 120)) {
148
			$this->logger->debug('SCSSCacher::process could not get lock for ' . $lockKey . ' and will wait 10 seconds for cached file to be available', ['app' => 'scss_cacher']);
149
			$retry = 0;
150
			sleep(1);
151
			while ($retry < 10) {
152
				$this->appConfig->clearCachedConfig();
153
				$this->logger->debug('SCSSCacher::process check in while loop follows', ['app' => 'scss_cacher']);
154
				if (!$this->variablesChanged() && $this->isCached($fileNameCSS, $app)) {
155
					// Inject icons vars css if any
156
					$this->logger->debug("SCSSCacher::process cached file for app '$app' and file '$fileNameCSS' is now available after $retry s. Moving on...", ['app' => 'scss_cacher']);
157
					return true;
158
				}
159
				sleep(1);
160
				$retry++;
161
			}
162
			$this->logger->debug('SCSSCacher::process Giving up scss caching for ' . $lockKey, ['app' => 'scss_cacher']);
163
			return false;
164
		}
165
166
		$this->logger->debug('SCSSCacher::process Lock acquired for ' . $lockKey, ['app' => 'scss_cacher']);
167
		try {
168
			$cached = $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir);
169
		} catch (\Exception $e) {
170
			$this->lockingCache->remove($lockKey);
171
			throw $e;
172
		}
173
174
		// Cleaning lock
175
		$this->lockingCache->remove($lockKey);
176
		$this->logger->debug('SCSSCacher::process Lock removed for ' . $lockKey, ['app' => 'scss_cacher']);
177
178
		return $cached;
179
	}
180
181
	/**
182
	 * @param $appName
183
	 * @param $fileName
184
	 * @return ISimpleFile
185
	 */
186
	public function getCachedCSS(string $appName, string $fileName): ISimpleFile {
187
		$folder = $this->appData->getFolder($appName);
188
		$cachedFileName = $this->prependVersionPrefix($this->prependBaseurlPrefix($fileName), $appName);
189
190
		return $folder->getFile($cachedFileName);
191
	}
192
193
	/**
194
	 * Check if the file is cached or not
195
	 * @param string $fileNameCSS
196
	 * @param string $app
197
	 * @return boolean
198
	 */
199
	private function isCached(string $fileNameCSS, string $app) {
200
		$key = $this->config->getSystemValue('version') . '/' . $app . '/' . $fileNameCSS;
201
202
		// If the file mtime is more recent than our cached one,
203
		// let's consider the file is properly cached
204
		if ($cacheValue = $this->isCachedCache->get($key)) {
205
			if ($cacheValue > $this->timeFactory->getTime()) {
206
				return true;
207
			}
208
		}
209
		$this->logger->debug("SCSSCacher::isCached $fileNameCSS isCachedCache is expired or unset", ['app' => 'scss_cacher']);
210
211
		// Creating file cache if none for further checks
212
		try {
213
			$folder = $this->appData->getFolder($app);
214
		} catch (NotFoundException $e) {
215
			$this->logger->debug("SCSSCacher::isCached app data folder for $app could not be fetched", ['app' => 'scss_cacher']);
216
			return false;
217
		}
218
219
		// Checking if file size is coherent
220
		// and if one of the css dependency changed
221
		try {
222
			$cachedFile = $folder->getFile($fileNameCSS);
223
			if ($cachedFile->getSize() > 0) {
224
				$depFileName = $fileNameCSS . '.deps';
225
				$deps = $this->depsCache->get($folder->getName() . '-' . $depFileName);
226
				if ($deps === null) {
227
					$depFile = $folder->getFile($depFileName);
228
					$deps = $depFile->getContent();
229
					// Set to memcache for next run
230
					$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
231
				}
232
				$deps = json_decode($deps, true);
233
234
				foreach ((array) $deps as $file => $mtime) {
235
					if (!file_exists($file) || filemtime($file) > $mtime) {
236
						$this->logger->debug("SCSSCacher::isCached $fileNameCSS is not considered as cached due to deps file $file", ['app' => 'scss_cacher']);
237
						return false;
238
					}
239
				}
240
241
				$this->logger->debug("SCSSCacher::isCached $fileNameCSS dependencies successfully cached for 5 minutes", ['app' => 'scss_cacher']);
242
				// It would probably make sense to adjust this timeout to something higher and see if that has some effect then
243
				$this->isCachedCache->set($key, $this->timeFactory->getTime() + 5 * 60);
244
				return true;
245
			}
246
			$this->logger->debug("SCSSCacher::isCached $fileNameCSS is not considered as cached cacheValue: $cacheValue", ['app' => 'scss_cacher']);
247
			return false;
248
		} catch (NotFoundException $e) {
249
			$this->logger->debug("SCSSCacher::isCached NotFoundException " . $e->getMessage(), ['app' => 'scss_cacher']);
250
			return false;
251
		}
252
	}
253
254
	/**
255
	 * Check if the variables file has changed
256
	 * @return bool
257
	 */
258
	private function variablesChanged(): bool {
259
		$cachedVariables = $this->config->getAppValue('core', 'theming.variables', '');
260
		$injectedVariables = $this->getInjectedVariables($cachedVariables);
261
		if ($cachedVariables !== md5($injectedVariables)) {
262
			$this->logger->debug('SCSSCacher::variablesChanged storedVariables: ' . json_encode($this->config->getAppValue('core', 'theming.variables')) . ' currentInjectedVariables: ' . json_encode($injectedVariables), ['app' => 'scss_cacher']);
263
			$this->config->setAppValue('core', 'theming.variables', md5($injectedVariables));
264
			$this->resetCache();
265
			return true;
266
		}
267
		return false;
268
	}
269
270
	/**
271
	 * Cache the file with AppData
272
	 *
273
	 * @param string $path
274
	 * @param string $fileNameCSS
275
	 * @param string $fileNameSCSS
276
	 * @param ISimpleFolder $folder
277
	 * @param string $webDir
278
	 * @return boolean
279
	 * @throws NotPermittedException
280
	 */
281
	private function cache(string $path, string $fileNameCSS, string $fileNameSCSS, ISimpleFolder $folder, string $webDir) {
282
		$scss = new Compiler();
283
		$scss->setImportPaths([
284
			$path,
285
			$this->serverRoot . '/core/css/'
286
		]);
287
288
		// Continue after throw
289
		if ($this->config->getSystemValue('debug')) {
290
			// Debug mode
291
			$scss->setOutputStyle(OutputStyle::EXPANDED);
292
		} else {
293
			// Compression
294
			$scss->setOutputStyle(OutputStyle::COMPRESSED);
295
		}
296
297
		try {
298
			$cachedfile = $folder->getFile($fileNameCSS);
299
		} catch (NotFoundException $e) {
300
			$cachedfile = $folder->newFile($fileNameCSS);
301
		}
302
303
		$depFileName = $fileNameCSS . '.deps';
304
		try {
305
			$depFile = $folder->getFile($depFileName);
306
		} catch (NotFoundException $e) {
307
			$depFile = $folder->newFile($depFileName);
308
		}
309
310
		// Compile
311
		try {
312
			$compiledScss = $scss->compile(
313
				'$webroot: \'' . $this->getRoutePrefix() . '\';' .
314
				$this->getInjectedVariables() .
315
				'@import "variables.scss";' .
316
				'@import "functions.scss";' .
317
				'@import "' . $fileNameSCSS . '";');
318
		} catch (\Exception $e) {
319
			$this->logger->error($e->getMessage(), ['app' => 'scss_cacher', 'exception' => $e]);
320
321
			return false;
322
		}
323
324
		// Gzip file
325
		try {
326
			$gzipFile = $folder->getFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
327
		} catch (NotFoundException $e) {
328
			$gzipFile = $folder->newFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
329
		}
330
331
		try {
332
			$data = $this->rebaseUrls($compiledScss, $webDir);
333
			$cachedfile->putContent($data);
334
			$deps = json_encode($scss->getParsedFiles());
0 ignored issues
show
Deprecated Code introduced by
The function ScssPhp\ScssPhp\Compiler::getParsedFiles() has been deprecated. ( Ignorable by Annotation )

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

334
			$deps = json_encode(/** @scrutinizer ignore-deprecated */ $scss->getParsedFiles());
Loading history...
335
			$depFile->putContent($deps);
336
			$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
337
			$gzipFile->putContent(gzencode($data, 9));
338
			$this->logger->debug('SCSSCacher::cache ' . $webDir . '/' . $fileNameSCSS . ' compiled and successfully cached', ['app' => 'scss_cacher']);
339
340
			return true;
341
		} catch (NotPermittedException $e) {
342
			$this->logger->error('SCSSCacher::cache unable to cache: ' . $fileNameSCSS, ['app' => 'scss_cacher']);
343
344
			return false;
345
		}
346
	}
347
348
	/**
349
	 * Reset scss cache by deleting all generated css files
350
	 * We need to regenerate all files when variables change
351
	 */
352
	public function resetCache() {
353
		$this->logger->debug('SCSSCacher::resetCache', ['app' => 'scss_cacher']);
354
		if (!$this->lockingCache->add('resetCache', 'locked!', 120)) {
355
			$this->logger->debug('SCSSCacher::resetCache Locked', ['app' => 'scss_cacher']);
356
			return;
357
		}
358
		$this->logger->debug('SCSSCacher::resetCache Lock acquired', ['app' => 'scss_cacher']);
359
		$this->injectedVariables = null;
360
361
		// do not clear locks
362
		$this->depsCache->clear();
363
		$this->isCachedCache->clear();
364
365
		$appDirectory = $this->appData->getDirectoryListing();
366
		foreach ($appDirectory as $folder) {
367
			foreach ($folder->getDirectoryListing() as $file) {
368
				try {
369
					$file->delete();
370
				} catch (NotPermittedException $e) {
371
					$this->logger->error('SCSSCacher::resetCache unable to delete file: ' . $file->getName(), ['exception' => $e, 'app' => 'scss_cacher']);
372
				}
373
			}
374
		}
375
		$this->logger->debug('SCSSCacher::resetCache css cache cleared!', ['app' => 'scss_cacher']);
376
		$this->lockingCache->remove('resetCache');
377
		$this->logger->debug('SCSSCacher::resetCache Locking removed', ['app' => 'scss_cacher']);
378
	}
379
380
	/**
381
	 * @return string SCSS code for variables from OC_Defaults
382
	 */
383
	private function getInjectedVariables(string $cache = ''): string {
384
		if ($this->injectedVariables !== null) {
385
			return $this->injectedVariables;
386
		}
387
		$variables = '';
388
		foreach ($this->defaults->getScssVariables() as $key => $value) {
389
			$variables .= '$' . $key . ': ' . $value . ' !default;';
390
		}
391
392
		/*
393
		 * If we are trying to return the same variables as that are cached
394
		 * Then there is no need to do the compile step
395
		 */
396
		if ($cache === md5($variables)) {
397
			$this->injectedVariables = $variables;
398
			return $variables;
399
		}
400
401
		// check for valid variables / otherwise fall back to defaults
402
		try {
403
			$scss = new Compiler();
404
			$scss->compile($variables);
405
			$this->injectedVariables = $variables;
406
		} catch (\Exception $e) {
407
			$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'scss_cacher']);
408
		}
409
410
		return $variables;
411
	}
412
413
	/**
414
	 * Add the correct uri prefix to make uri valid again
415
	 * @param string $css
416
	 * @param string $webDir
417
	 * @return string
418
	 */
419
	private function rebaseUrls(string $css, string $webDir): string {
420
		$re = '/url\([\'"]([^\/][\.\w?=\/-]*)[\'"]\)/x';
421
		$subst = 'url(\'' . $webDir . '/$1\')';
422
423
		return preg_replace($re, $subst, $css);
424
	}
425
426
	/**
427
	 * Return the cached css file uri
428
	 * @param string $appName the app name
429
	 * @param string $fileName
430
	 * @return string
431
	 */
432
	public function getCachedSCSS(string $appName, string $fileName): string {
433
		$tmpfileLoc = explode('/', $fileName);
434
		$fileName = array_pop($tmpfileLoc);
435
		$fileName = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileName)), $appName);
436
437
		return substr($this->urlGenerator->linkToRoute('core.Css.getCss', [
438
			'fileName' => $fileName,
439
			'appName' => $appName,
440
			'v' => $this->config->getAppValue('core', 'theming.variables', '0')
441
		]), \strlen(\OC::$WEBROOT) + 1);
442
	}
443
444
	/**
445
	 * Prepend hashed base url to the css file
446
	 * @param string $cssFile
447
	 * @return string
448
	 */
449
	private function prependBaseurlPrefix(string $cssFile): string {
450
		return substr(md5($this->urlGenerator->getBaseUrl() . $this->getRoutePrefix()), 0, 4) . '-' . $cssFile;
451
	}
452
453
	private function getRoutePrefix() {
454
		$frontControllerActive = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true');
455
		$prefix = \OC::$WEBROOT . '/index.php';
456
		if ($frontControllerActive) {
457
			$prefix = \OC::$WEBROOT;
458
		}
459
		return $prefix;
460
	}
461
462
	/**
463
	 * Prepend hashed app version hash
464
	 * @param string $cssFile
465
	 * @param string $appId
466
	 * @return string
467
	 */
468
	private function prependVersionPrefix(string $cssFile, string $appId): string {
469
		$appVersion = \OC_App::getAppVersion($appId);
470
		if ($appVersion !== '0') {
471
			return substr(md5($appVersion), 0, 4) . '-' . $cssFile;
472
		}
473
		$coreVersion = \OC_Util::getVersionString();
474
475
		return substr(md5($coreVersion), 0, 4) . '-' . $cssFile;
476
	}
477
478
	/**
479
	 * Get WebDir root
480
	 * @param string $path the css file path
481
	 * @param string $appName the app name
482
	 * @param string $serverRoot the server root path
483
	 * @param string $webRoot the nextcloud installation root path
484
	 * @return string the webDir
485
	 */
486
	private function getWebDir(string $path, string $appName, string $serverRoot, string $webRoot): string {
487
		// Detect if path is within server root AND if path is within an app path
488
		if (strpos($path, $serverRoot) === false && $appWebPath = \OC_App::getAppWebPath($appName)) {
489
			// Get the file path within the app directory
490
			$appDirectoryPath = explode($appName, $path)[1];
491
			// Remove the webroot
492
493
			return str_replace($webRoot, '', $appWebPath . $appDirectoryPath);
494
		}
495
496
		return $webRoot . substr($path, strlen($serverRoot));
497
	}
498
}
499