Passed
Push — master ( 439e21...1c68a6 )
by Morris
12:52 queued 12s
created
lib/private/Template/SCSSCacher.php 1 patch
Indentation   +473 added lines, -473 removed lines patch added patch discarded remove patch
@@ -51,477 +51,477 @@
 block discarded – undo
51 51
 
52 52
 class SCSSCacher {
53 53
 
54
-	/** @var ILogger */
55
-	protected $logger;
56
-
57
-	/** @var IAppData */
58
-	protected $appData;
59
-
60
-	/** @var IURLGenerator */
61
-	protected $urlGenerator;
62
-
63
-	/** @var IConfig */
64
-	protected $config;
65
-
66
-	/** @var \OC_Defaults */
67
-	private $defaults;
68
-
69
-	/** @var string */
70
-	protected $serverRoot;
71
-
72
-	/** @var ICache */
73
-	protected $depsCache;
74
-
75
-	/** @var null|string */
76
-	private $injectedVariables;
77
-
78
-	/** @var ICacheFactory */
79
-	private $cacheFactory;
80
-
81
-	/** @var IconsCacher */
82
-	private $iconsCacher;
83
-
84
-	/** @var ICache */
85
-	private $isCachedCache;
86
-
87
-	/** @var ITimeFactory */
88
-	private $timeFactory;
89
-
90
-	/** @var IMemcache */
91
-	private $lockingCache;
92
-
93
-	/**
94
-	 * @param ILogger $logger
95
-	 * @param Factory $appDataFactory
96
-	 * @param IURLGenerator $urlGenerator
97
-	 * @param IConfig $config
98
-	 * @param \OC_Defaults $defaults
99
-	 * @param string $serverRoot
100
-	 * @param ICacheFactory $cacheFactory
101
-	 * @param IconsCacher $iconsCacher
102
-	 * @param ITimeFactory $timeFactory
103
-	 */
104
-	public function __construct(ILogger $logger,
105
-								Factory $appDataFactory,
106
-								IURLGenerator $urlGenerator,
107
-								IConfig $config,
108
-								\OC_Defaults $defaults,
109
-								$serverRoot,
110
-								ICacheFactory $cacheFactory,
111
-								IconsCacher $iconsCacher,
112
-								ITimeFactory $timeFactory) {
113
-		$this->logger = $logger;
114
-		$this->appData = $appDataFactory->get('css');
115
-		$this->urlGenerator = $urlGenerator;
116
-		$this->config = $config;
117
-		$this->defaults = $defaults;
118
-		$this->serverRoot = $serverRoot;
119
-		$this->cacheFactory = $cacheFactory;
120
-		$this->depsCache = $cacheFactory->createDistributed('SCSS-deps-' . md5($this->urlGenerator->getBaseUrl()));
121
-		$this->isCachedCache = $cacheFactory->createDistributed('SCSS-cached-' . md5($this->urlGenerator->getBaseUrl()));
122
-		$lockingCache = $cacheFactory->createDistributed('SCSS-locks-' . md5($this->urlGenerator->getBaseUrl()));
123
-		if (!($lockingCache instanceof IMemcache)) {
124
-			$lockingCache = new NullCache();
125
-		}
126
-		$this->lockingCache = $lockingCache;
127
-		$this->iconsCacher = $iconsCacher;
128
-		$this->timeFactory = $timeFactory;
129
-	}
130
-
131
-	/**
132
-	 * Process the caching process if needed
133
-	 *
134
-	 * @param string $root Root path to the nextcloud installation
135
-	 * @param string $file
136
-	 * @param string $app The app name
137
-	 * @return boolean
138
-	 * @throws NotPermittedException
139
-	 */
140
-	public function process(string $root, string $file, string $app): bool {
141
-		$path = explode('/', $root . '/' . $file);
142
-
143
-		$fileNameSCSS = array_pop($path);
144
-		$fileNameCSS = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS)), $app);
145
-
146
-		$path = implode('/', $path);
147
-		$webDir = $this->getWebDir($path, $app, $this->serverRoot, \OC::$WEBROOT);
148
-
149
-		$this->logger->debug('SCSSCacher::process ordinary check follows', ['app' => 'scss_cacher']);
150
-		if (!$this->variablesChanged() && $this->isCached($fileNameCSS, $app)) {
151
-			// Inject icons vars css if any
152
-			return $this->injectCssVariablesIfAny();
153
-		}
154
-
155
-		try {
156
-			$folder = $this->appData->getFolder($app);
157
-		} catch (NotFoundException $e) {
158
-			// creating css appdata folder
159
-			$folder = $this->appData->newFolder($app);
160
-		}
161
-
162
-		$lockKey = $webDir . '/' . $fileNameSCSS;
163
-
164
-		if (!$this->lockingCache->add($lockKey, 'locked!', 120)) {
165
-			$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']);
166
-			$retry = 0;
167
-			sleep(1);
168
-			while ($retry < 10) {
169
-				$this->logger->debug('SCSSCacher::process check in while loop follows', ['app' => 'scss_cacher']);
170
-				if (!$this->variablesChanged() && $this->isCached($fileNameCSS, $app)) {
171
-					// Inject icons vars css if any
172
-					$this->logger->debug("SCSSCacher::process cached file for app '$app' and file '$fileNameCSS' is now available after $retry s. Moving on...", ['app' => 'scss_cacher']);
173
-					return $this->injectCssVariablesIfAny();
174
-				}
175
-				sleep(1);
176
-				$retry++;
177
-			}
178
-			$this->logger->debug('SCSSCacher::process Giving up scss caching for ' . $lockKey, ['app' => 'scss_cacher']);
179
-			return false;
180
-		}
181
-
182
-		$this->logger->debug('SCSSCacher::process Lock acquired for ' . $lockKey, ['app' => 'scss_cacher']);
183
-		try {
184
-			$cached = $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir);
185
-		} catch (\Exception $e) {
186
-			$this->lockingCache->remove($lockKey);
187
-			throw $e;
188
-		}
189
-
190
-		// Cleaning lock
191
-		$this->lockingCache->remove($lockKey);
192
-		$this->logger->debug('SCSSCacher::process Lock removed for ' . $lockKey, ['app' => 'scss_cacher']);
193
-
194
-		// Inject icons vars css if any
195
-		if ($this->iconsCacher->getCachedCSS() && $this->iconsCacher->getCachedCSS()->getSize() > 0) {
196
-			$this->iconsCacher->injectCss();
197
-		}
198
-
199
-		return $cached;
200
-	}
201
-
202
-	/**
203
-	 * @param $appName
204
-	 * @param $fileName
205
-	 * @return ISimpleFile
206
-	 */
207
-	public function getCachedCSS(string $appName, string $fileName): ISimpleFile {
208
-		$folder = $this->appData->getFolder($appName);
209
-		$cachedFileName = $this->prependVersionPrefix($this->prependBaseurlPrefix($fileName), $appName);
210
-
211
-		return $folder->getFile($cachedFileName);
212
-	}
213
-
214
-	/**
215
-	 * Check if the file is cached or not
216
-	 * @param string $fileNameCSS
217
-	 * @param string $app
218
-	 * @return boolean
219
-	 */
220
-	private function isCached(string $fileNameCSS, string $app) {
221
-		$key = $this->config->getSystemValue('version') . '/' . $app . '/' . $fileNameCSS;
222
-
223
-		// If the file mtime is more recent than our cached one,
224
-		// let's consider the file is properly cached
225
-		if ($cacheValue = $this->isCachedCache->get($key)) {
226
-			if ($cacheValue > $this->timeFactory->getTime()) {
227
-				return true;
228
-			}
229
-		}
230
-		$this->logger->debug("SCSSCacher::isCached $fileNameCSS isCachedCache is expired or unset", ['app' => 'scss_cacher']);
231
-
232
-		// Creating file cache if none for further checks
233
-		try {
234
-			$folder = $this->appData->getFolder($app);
235
-		} catch (NotFoundException $e) {
236
-			$this->logger->debug("SCSSCacher::isCached app data folder for $app could not be fetched", ['app' => 'scss_cacher']);
237
-			return false;
238
-		}
239
-
240
-		// Checking if file size is coherent
241
-		// and if one of the css dependency changed
242
-		try {
243
-			$cachedFile = $folder->getFile($fileNameCSS);
244
-			if ($cachedFile->getSize() > 0) {
245
-				$depFileName = $fileNameCSS . '.deps';
246
-				$deps = $this->depsCache->get($folder->getName() . '-' . $depFileName);
247
-				if ($deps === null) {
248
-					$depFile = $folder->getFile($depFileName);
249
-					$deps = $depFile->getContent();
250
-					// Set to memcache for next run
251
-					$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
252
-				}
253
-				$deps = json_decode($deps, true);
254
-
255
-				foreach ((array) $deps as $file => $mtime) {
256
-					if (!file_exists($file) || filemtime($file) > $mtime) {
257
-						$this->logger->debug("SCSSCacher::isCached $fileNameCSS is not considered as cached due to deps file $file", ['app' => 'scss_cacher']);
258
-						return false;
259
-					}
260
-				}
261
-
262
-				$this->logger->debug("SCSSCacher::isCached $fileNameCSS dependencies successfully cached for 5 minutes", ['app' => 'scss_cacher']);
263
-				// It would probably make sense to adjust this timeout to something higher and see if that has some effect then
264
-				$this->isCachedCache->set($key, $this->timeFactory->getTime() + 5 * 60);
265
-				return true;
266
-			}
267
-			$this->logger->debug("SCSSCacher::isCached $fileNameCSS is not considered as cached cacheValue: $cacheValue", ['app' => 'scss_cacher']);
268
-			return false;
269
-		} catch (NotFoundException $e) {
270
-			$this->logger->debug("SCSSCacher::isCached NotFoundException " . $e->getMessage(), ['app' => 'scss_cacher']);
271
-			return false;
272
-		}
273
-	}
274
-
275
-	/**
276
-	 * Check if the variables file has changed
277
-	 * @return bool
278
-	 */
279
-	private function variablesChanged(): bool {
280
-		$injectedVariables = $this->getInjectedVariables();
281
-		if ($this->config->getAppValue('core', 'theming.variables') !== md5($injectedVariables)) {
282
-			$this->logger->debug('SCSSCacher::variablesChanged storedVariables: ' . json_encode($this->config->getAppValue('core', 'theming.variables')) . ' currentInjectedVariables: ' . json_encode($injectedVariables), ['app' => 'scss_cacher']);
283
-			$this->config->setAppValue('core', 'theming.variables', md5($injectedVariables));
284
-			$this->resetCache();
285
-			return true;
286
-		}
287
-		return false;
288
-	}
289
-
290
-	/**
291
-	 * Cache the file with AppData
292
-	 *
293
-	 * @param string $path
294
-	 * @param string $fileNameCSS
295
-	 * @param string $fileNameSCSS
296
-	 * @param ISimpleFolder $folder
297
-	 * @param string $webDir
298
-	 * @return boolean
299
-	 * @throws NotPermittedException
300
-	 */
301
-	private function cache(string $path, string $fileNameCSS, string $fileNameSCSS, ISimpleFolder $folder, string $webDir) {
302
-		$scss = new Compiler();
303
-		$scss->setImportPaths([
304
-			$path,
305
-			$this->serverRoot . '/core/css/'
306
-		]);
307
-
308
-		// Continue after throw
309
-		$scss->setIgnoreErrors(true);
310
-		if ($this->config->getSystemValue('debug')) {
311
-			// Debug mode
312
-			$scss->setFormatter(Expanded::class);
313
-			$scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
314
-		} else {
315
-			// Compression
316
-			$scss->setFormatter(Crunched::class);
317
-		}
318
-
319
-		try {
320
-			$cachedfile = $folder->getFile($fileNameCSS);
321
-		} catch (NotFoundException $e) {
322
-			$cachedfile = $folder->newFile($fileNameCSS);
323
-		}
324
-
325
-		$depFileName = $fileNameCSS . '.deps';
326
-		try {
327
-			$depFile = $folder->getFile($depFileName);
328
-		} catch (NotFoundException $e) {
329
-			$depFile = $folder->newFile($depFileName);
330
-		}
331
-
332
-		// Compile
333
-		try {
334
-			$compiledScss = $scss->compile(
335
-				'$webroot: \'' . $this->getRoutePrefix() . '\';' .
336
-				$this->getInjectedVariables() .
337
-				'@import "variables.scss";' .
338
-				'@import "functions.scss";' .
339
-				'@import "' . $fileNameSCSS . '";');
340
-		} catch (ParserException $e) {
341
-			$this->logger->logException($e, ['app' => 'scss_cacher']);
342
-
343
-			return false;
344
-		}
345
-
346
-		// Parse Icons and create related css variables
347
-		$compiledScss = $this->iconsCacher->setIconsCss($compiledScss);
348
-
349
-		// Gzip file
350
-		try {
351
-			$gzipFile = $folder->getFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
352
-		} catch (NotFoundException $e) {
353
-			$gzipFile = $folder->newFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
354
-		}
355
-
356
-		try {
357
-			$data = $this->rebaseUrls($compiledScss, $webDir);
358
-			$cachedfile->putContent($data);
359
-			$deps = json_encode($scss->getParsedFiles());
360
-			$depFile->putContent($deps);
361
-			$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
362
-			$gzipFile->putContent(gzencode($data, 9));
363
-			$this->logger->debug('SCSSCacher::cache ' . $webDir . '/' . $fileNameSCSS . ' compiled and successfully cached', ['app' => 'scss_cacher']);
364
-
365
-			return true;
366
-		} catch (NotPermittedException $e) {
367
-			$this->logger->error('SCSSCacher::cache unable to cache: ' . $fileNameSCSS, ['app' => 'scss_cacher']);
368
-
369
-			return false;
370
-		}
371
-	}
372
-
373
-	/**
374
-	 * Reset scss cache by deleting all generated css files
375
-	 * We need to regenerate all files when variables change
376
-	 */
377
-	public function resetCache() {
378
-		$this->logger->debug('SCSSCacher::resetCache', ['app' => 'scss_cacher']);
379
-		if (!$this->lockingCache->add('resetCache', 'locked!', 120)) {
380
-			$this->logger->debug('SCSSCacher::resetCache Locked', ['app' => 'scss_cacher']);
381
-			return;
382
-		}
383
-		$this->logger->debug('SCSSCacher::resetCache Lock acquired', ['app' => 'scss_cacher']);
384
-		$this->injectedVariables = null;
385
-
386
-		// do not clear locks
387
-		$this->cacheFactory->createDistributed('SCSS-deps-')->clear();
388
-		$this->cacheFactory->createDistributed('SCSS-cached-')->clear();
389
-
390
-		$appDirectory = $this->appData->getDirectoryListing();
391
-		foreach ($appDirectory as $folder) {
392
-			foreach ($folder->getDirectoryListing() as $file) {
393
-				try {
394
-					$file->delete();
395
-				} catch (NotPermittedException $e) {
396
-					$this->logger->logException($e, ['message' => 'SCSSCacher::resetCache unable to delete file: ' . $file->getName(), 'app' => 'scss_cacher']);
397
-				}
398
-			}
399
-		}
400
-		$this->logger->debug('SCSSCacher::resetCache css cache cleared!', ['app' => 'scss_cacher']);
401
-		$this->lockingCache->remove('resetCache');
402
-		$this->logger->debug('SCSSCacher::resetCache Locking removed', ['app' => 'scss_cacher']);
403
-	}
404
-
405
-	/**
406
-	 * @return string SCSS code for variables from OC_Defaults
407
-	 */
408
-	private function getInjectedVariables(): string {
409
-		if ($this->injectedVariables !== null) {
410
-			return $this->injectedVariables;
411
-		}
412
-		$variables = '';
413
-		foreach ($this->defaults->getScssVariables() as $key => $value) {
414
-			$variables .= '$' . $key . ': ' . $value . ' !default;';
415
-		}
416
-
417
-		// check for valid variables / otherwise fall back to defaults
418
-		try {
419
-			$scss = new Compiler();
420
-			$scss->compile($variables);
421
-			$this->injectedVariables = $variables;
422
-		} catch (ParserException $e) {
423
-			$this->logger->logException($e, ['app' => 'scss_cacher']);
424
-		}
425
-
426
-		return $variables;
427
-	}
428
-
429
-	/**
430
-	 * Add the correct uri prefix to make uri valid again
431
-	 * @param string $css
432
-	 * @param string $webDir
433
-	 * @return string
434
-	 */
435
-	private function rebaseUrls(string $css, string $webDir): string {
436
-		$re = '/url\([\'"]([^\/][\.\w?=\/-]*)[\'"]\)/x';
437
-		$subst = 'url(\'' . $webDir . '/$1\')';
438
-
439
-		return preg_replace($re, $subst, $css);
440
-	}
441
-
442
-	/**
443
-	 * Return the cached css file uri
444
-	 * @param string $appName the app name
445
-	 * @param string $fileName
446
-	 * @return string
447
-	 */
448
-	public function getCachedSCSS(string $appName, string $fileName): string {
449
-		$tmpfileLoc = explode('/', $fileName);
450
-		$fileName = array_pop($tmpfileLoc);
451
-		$fileName = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileName)), $appName);
452
-
453
-		return substr($this->urlGenerator->linkToRoute('core.Css.getCss', [
454
-			'fileName' => $fileName,
455
-			'appName' => $appName,
456
-			'v' => $this->config->getAppValue('core', 'theming.variables', '0')
457
-		]), \strlen(\OC::$WEBROOT) + 1);
458
-	}
459
-
460
-	/**
461
-	 * Prepend hashed base url to the css file
462
-	 * @param string $cssFile
463
-	 * @return string
464
-	 */
465
-	private function prependBaseurlPrefix(string $cssFile): string {
466
-		return substr(md5($this->urlGenerator->getBaseUrl() . $this->getRoutePrefix()), 0, 4) . '-' . $cssFile;
467
-	}
468
-
469
-	private function getRoutePrefix() {
470
-		$frontControllerActive = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true');
471
-		$prefix = \OC::$WEBROOT . '/index.php';
472
-		if ($frontControllerActive) {
473
-			$prefix = \OC::$WEBROOT;
474
-		}
475
-		return $prefix;
476
-	}
477
-
478
-	/**
479
-	 * Prepend hashed app version hash
480
-	 * @param string $cssFile
481
-	 * @param string $appId
482
-	 * @return string
483
-	 */
484
-	private function prependVersionPrefix(string $cssFile, string $appId): string {
485
-		$appVersion = \OC_App::getAppVersion($appId);
486
-		if ($appVersion !== '0') {
487
-			return substr(md5($appVersion), 0, 4) . '-' . $cssFile;
488
-		}
489
-		$coreVersion = \OC_Util::getVersionString();
490
-
491
-		return substr(md5($coreVersion), 0, 4) . '-' . $cssFile;
492
-	}
493
-
494
-	/**
495
-	 * Get WebDir root
496
-	 * @param string $path the css file path
497
-	 * @param string $appName the app name
498
-	 * @param string $serverRoot the server root path
499
-	 * @param string $webRoot the nextcloud installation root path
500
-	 * @return string the webDir
501
-	 */
502
-	private function getWebDir(string $path, string $appName, string $serverRoot, string $webRoot): string {
503
-		// Detect if path is within server root AND if path is within an app path
504
-		if (strpos($path, $serverRoot) === false && $appWebPath = \OC_App::getAppWebPath($appName)) {
505
-			// Get the file path within the app directory
506
-			$appDirectoryPath = explode($appName, $path)[1];
507
-			// Remove the webroot
508
-
509
-			return str_replace($webRoot, '', $appWebPath . $appDirectoryPath);
510
-		}
511
-
512
-		return $webRoot . substr($path, strlen($serverRoot));
513
-	}
514
-
515
-	/**
516
-	 * Add the icons css cache in the header if needed
517
-	 *
518
-	 * @return boolean true
519
-	 */
520
-	private function injectCssVariablesIfAny() {
521
-		// Inject icons vars css if any
522
-		if ($this->iconsCacher->getCachedCSS() && $this->iconsCacher->getCachedCSS()->getSize() > 0) {
523
-			$this->iconsCacher->injectCss();
524
-		}
525
-		return true;
526
-	}
54
+    /** @var ILogger */
55
+    protected $logger;
56
+
57
+    /** @var IAppData */
58
+    protected $appData;
59
+
60
+    /** @var IURLGenerator */
61
+    protected $urlGenerator;
62
+
63
+    /** @var IConfig */
64
+    protected $config;
65
+
66
+    /** @var \OC_Defaults */
67
+    private $defaults;
68
+
69
+    /** @var string */
70
+    protected $serverRoot;
71
+
72
+    /** @var ICache */
73
+    protected $depsCache;
74
+
75
+    /** @var null|string */
76
+    private $injectedVariables;
77
+
78
+    /** @var ICacheFactory */
79
+    private $cacheFactory;
80
+
81
+    /** @var IconsCacher */
82
+    private $iconsCacher;
83
+
84
+    /** @var ICache */
85
+    private $isCachedCache;
86
+
87
+    /** @var ITimeFactory */
88
+    private $timeFactory;
89
+
90
+    /** @var IMemcache */
91
+    private $lockingCache;
92
+
93
+    /**
94
+     * @param ILogger $logger
95
+     * @param Factory $appDataFactory
96
+     * @param IURLGenerator $urlGenerator
97
+     * @param IConfig $config
98
+     * @param \OC_Defaults $defaults
99
+     * @param string $serverRoot
100
+     * @param ICacheFactory $cacheFactory
101
+     * @param IconsCacher $iconsCacher
102
+     * @param ITimeFactory $timeFactory
103
+     */
104
+    public function __construct(ILogger $logger,
105
+                                Factory $appDataFactory,
106
+                                IURLGenerator $urlGenerator,
107
+                                IConfig $config,
108
+                                \OC_Defaults $defaults,
109
+                                $serverRoot,
110
+                                ICacheFactory $cacheFactory,
111
+                                IconsCacher $iconsCacher,
112
+                                ITimeFactory $timeFactory) {
113
+        $this->logger = $logger;
114
+        $this->appData = $appDataFactory->get('css');
115
+        $this->urlGenerator = $urlGenerator;
116
+        $this->config = $config;
117
+        $this->defaults = $defaults;
118
+        $this->serverRoot = $serverRoot;
119
+        $this->cacheFactory = $cacheFactory;
120
+        $this->depsCache = $cacheFactory->createDistributed('SCSS-deps-' . md5($this->urlGenerator->getBaseUrl()));
121
+        $this->isCachedCache = $cacheFactory->createDistributed('SCSS-cached-' . md5($this->urlGenerator->getBaseUrl()));
122
+        $lockingCache = $cacheFactory->createDistributed('SCSS-locks-' . md5($this->urlGenerator->getBaseUrl()));
123
+        if (!($lockingCache instanceof IMemcache)) {
124
+            $lockingCache = new NullCache();
125
+        }
126
+        $this->lockingCache = $lockingCache;
127
+        $this->iconsCacher = $iconsCacher;
128
+        $this->timeFactory = $timeFactory;
129
+    }
130
+
131
+    /**
132
+     * Process the caching process if needed
133
+     *
134
+     * @param string $root Root path to the nextcloud installation
135
+     * @param string $file
136
+     * @param string $app The app name
137
+     * @return boolean
138
+     * @throws NotPermittedException
139
+     */
140
+    public function process(string $root, string $file, string $app): bool {
141
+        $path = explode('/', $root . '/' . $file);
142
+
143
+        $fileNameSCSS = array_pop($path);
144
+        $fileNameCSS = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS)), $app);
145
+
146
+        $path = implode('/', $path);
147
+        $webDir = $this->getWebDir($path, $app, $this->serverRoot, \OC::$WEBROOT);
148
+
149
+        $this->logger->debug('SCSSCacher::process ordinary check follows', ['app' => 'scss_cacher']);
150
+        if (!$this->variablesChanged() && $this->isCached($fileNameCSS, $app)) {
151
+            // Inject icons vars css if any
152
+            return $this->injectCssVariablesIfAny();
153
+        }
154
+
155
+        try {
156
+            $folder = $this->appData->getFolder($app);
157
+        } catch (NotFoundException $e) {
158
+            // creating css appdata folder
159
+            $folder = $this->appData->newFolder($app);
160
+        }
161
+
162
+        $lockKey = $webDir . '/' . $fileNameSCSS;
163
+
164
+        if (!$this->lockingCache->add($lockKey, 'locked!', 120)) {
165
+            $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']);
166
+            $retry = 0;
167
+            sleep(1);
168
+            while ($retry < 10) {
169
+                $this->logger->debug('SCSSCacher::process check in while loop follows', ['app' => 'scss_cacher']);
170
+                if (!$this->variablesChanged() && $this->isCached($fileNameCSS, $app)) {
171
+                    // Inject icons vars css if any
172
+                    $this->logger->debug("SCSSCacher::process cached file for app '$app' and file '$fileNameCSS' is now available after $retry s. Moving on...", ['app' => 'scss_cacher']);
173
+                    return $this->injectCssVariablesIfAny();
174
+                }
175
+                sleep(1);
176
+                $retry++;
177
+            }
178
+            $this->logger->debug('SCSSCacher::process Giving up scss caching for ' . $lockKey, ['app' => 'scss_cacher']);
179
+            return false;
180
+        }
181
+
182
+        $this->logger->debug('SCSSCacher::process Lock acquired for ' . $lockKey, ['app' => 'scss_cacher']);
183
+        try {
184
+            $cached = $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir);
185
+        } catch (\Exception $e) {
186
+            $this->lockingCache->remove($lockKey);
187
+            throw $e;
188
+        }
189
+
190
+        // Cleaning lock
191
+        $this->lockingCache->remove($lockKey);
192
+        $this->logger->debug('SCSSCacher::process Lock removed for ' . $lockKey, ['app' => 'scss_cacher']);
193
+
194
+        // Inject icons vars css if any
195
+        if ($this->iconsCacher->getCachedCSS() && $this->iconsCacher->getCachedCSS()->getSize() > 0) {
196
+            $this->iconsCacher->injectCss();
197
+        }
198
+
199
+        return $cached;
200
+    }
201
+
202
+    /**
203
+     * @param $appName
204
+     * @param $fileName
205
+     * @return ISimpleFile
206
+     */
207
+    public function getCachedCSS(string $appName, string $fileName): ISimpleFile {
208
+        $folder = $this->appData->getFolder($appName);
209
+        $cachedFileName = $this->prependVersionPrefix($this->prependBaseurlPrefix($fileName), $appName);
210
+
211
+        return $folder->getFile($cachedFileName);
212
+    }
213
+
214
+    /**
215
+     * Check if the file is cached or not
216
+     * @param string $fileNameCSS
217
+     * @param string $app
218
+     * @return boolean
219
+     */
220
+    private function isCached(string $fileNameCSS, string $app) {
221
+        $key = $this->config->getSystemValue('version') . '/' . $app . '/' . $fileNameCSS;
222
+
223
+        // If the file mtime is more recent than our cached one,
224
+        // let's consider the file is properly cached
225
+        if ($cacheValue = $this->isCachedCache->get($key)) {
226
+            if ($cacheValue > $this->timeFactory->getTime()) {
227
+                return true;
228
+            }
229
+        }
230
+        $this->logger->debug("SCSSCacher::isCached $fileNameCSS isCachedCache is expired or unset", ['app' => 'scss_cacher']);
231
+
232
+        // Creating file cache if none for further checks
233
+        try {
234
+            $folder = $this->appData->getFolder($app);
235
+        } catch (NotFoundException $e) {
236
+            $this->logger->debug("SCSSCacher::isCached app data folder for $app could not be fetched", ['app' => 'scss_cacher']);
237
+            return false;
238
+        }
239
+
240
+        // Checking if file size is coherent
241
+        // and if one of the css dependency changed
242
+        try {
243
+            $cachedFile = $folder->getFile($fileNameCSS);
244
+            if ($cachedFile->getSize() > 0) {
245
+                $depFileName = $fileNameCSS . '.deps';
246
+                $deps = $this->depsCache->get($folder->getName() . '-' . $depFileName);
247
+                if ($deps === null) {
248
+                    $depFile = $folder->getFile($depFileName);
249
+                    $deps = $depFile->getContent();
250
+                    // Set to memcache for next run
251
+                    $this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
252
+                }
253
+                $deps = json_decode($deps, true);
254
+
255
+                foreach ((array) $deps as $file => $mtime) {
256
+                    if (!file_exists($file) || filemtime($file) > $mtime) {
257
+                        $this->logger->debug("SCSSCacher::isCached $fileNameCSS is not considered as cached due to deps file $file", ['app' => 'scss_cacher']);
258
+                        return false;
259
+                    }
260
+                }
261
+
262
+                $this->logger->debug("SCSSCacher::isCached $fileNameCSS dependencies successfully cached for 5 minutes", ['app' => 'scss_cacher']);
263
+                // It would probably make sense to adjust this timeout to something higher and see if that has some effect then
264
+                $this->isCachedCache->set($key, $this->timeFactory->getTime() + 5 * 60);
265
+                return true;
266
+            }
267
+            $this->logger->debug("SCSSCacher::isCached $fileNameCSS is not considered as cached cacheValue: $cacheValue", ['app' => 'scss_cacher']);
268
+            return false;
269
+        } catch (NotFoundException $e) {
270
+            $this->logger->debug("SCSSCacher::isCached NotFoundException " . $e->getMessage(), ['app' => 'scss_cacher']);
271
+            return false;
272
+        }
273
+    }
274
+
275
+    /**
276
+     * Check if the variables file has changed
277
+     * @return bool
278
+     */
279
+    private function variablesChanged(): bool {
280
+        $injectedVariables = $this->getInjectedVariables();
281
+        if ($this->config->getAppValue('core', 'theming.variables') !== md5($injectedVariables)) {
282
+            $this->logger->debug('SCSSCacher::variablesChanged storedVariables: ' . json_encode($this->config->getAppValue('core', 'theming.variables')) . ' currentInjectedVariables: ' . json_encode($injectedVariables), ['app' => 'scss_cacher']);
283
+            $this->config->setAppValue('core', 'theming.variables', md5($injectedVariables));
284
+            $this->resetCache();
285
+            return true;
286
+        }
287
+        return false;
288
+    }
289
+
290
+    /**
291
+     * Cache the file with AppData
292
+     *
293
+     * @param string $path
294
+     * @param string $fileNameCSS
295
+     * @param string $fileNameSCSS
296
+     * @param ISimpleFolder $folder
297
+     * @param string $webDir
298
+     * @return boolean
299
+     * @throws NotPermittedException
300
+     */
301
+    private function cache(string $path, string $fileNameCSS, string $fileNameSCSS, ISimpleFolder $folder, string $webDir) {
302
+        $scss = new Compiler();
303
+        $scss->setImportPaths([
304
+            $path,
305
+            $this->serverRoot . '/core/css/'
306
+        ]);
307
+
308
+        // Continue after throw
309
+        $scss->setIgnoreErrors(true);
310
+        if ($this->config->getSystemValue('debug')) {
311
+            // Debug mode
312
+            $scss->setFormatter(Expanded::class);
313
+            $scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
314
+        } else {
315
+            // Compression
316
+            $scss->setFormatter(Crunched::class);
317
+        }
318
+
319
+        try {
320
+            $cachedfile = $folder->getFile($fileNameCSS);
321
+        } catch (NotFoundException $e) {
322
+            $cachedfile = $folder->newFile($fileNameCSS);
323
+        }
324
+
325
+        $depFileName = $fileNameCSS . '.deps';
326
+        try {
327
+            $depFile = $folder->getFile($depFileName);
328
+        } catch (NotFoundException $e) {
329
+            $depFile = $folder->newFile($depFileName);
330
+        }
331
+
332
+        // Compile
333
+        try {
334
+            $compiledScss = $scss->compile(
335
+                '$webroot: \'' . $this->getRoutePrefix() . '\';' .
336
+                $this->getInjectedVariables() .
337
+                '@import "variables.scss";' .
338
+                '@import "functions.scss";' .
339
+                '@import "' . $fileNameSCSS . '";');
340
+        } catch (ParserException $e) {
341
+            $this->logger->logException($e, ['app' => 'scss_cacher']);
342
+
343
+            return false;
344
+        }
345
+
346
+        // Parse Icons and create related css variables
347
+        $compiledScss = $this->iconsCacher->setIconsCss($compiledScss);
348
+
349
+        // Gzip file
350
+        try {
351
+            $gzipFile = $folder->getFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
352
+        } catch (NotFoundException $e) {
353
+            $gzipFile = $folder->newFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
354
+        }
355
+
356
+        try {
357
+            $data = $this->rebaseUrls($compiledScss, $webDir);
358
+            $cachedfile->putContent($data);
359
+            $deps = json_encode($scss->getParsedFiles());
360
+            $depFile->putContent($deps);
361
+            $this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
362
+            $gzipFile->putContent(gzencode($data, 9));
363
+            $this->logger->debug('SCSSCacher::cache ' . $webDir . '/' . $fileNameSCSS . ' compiled and successfully cached', ['app' => 'scss_cacher']);
364
+
365
+            return true;
366
+        } catch (NotPermittedException $e) {
367
+            $this->logger->error('SCSSCacher::cache unable to cache: ' . $fileNameSCSS, ['app' => 'scss_cacher']);
368
+
369
+            return false;
370
+        }
371
+    }
372
+
373
+    /**
374
+     * Reset scss cache by deleting all generated css files
375
+     * We need to regenerate all files when variables change
376
+     */
377
+    public function resetCache() {
378
+        $this->logger->debug('SCSSCacher::resetCache', ['app' => 'scss_cacher']);
379
+        if (!$this->lockingCache->add('resetCache', 'locked!', 120)) {
380
+            $this->logger->debug('SCSSCacher::resetCache Locked', ['app' => 'scss_cacher']);
381
+            return;
382
+        }
383
+        $this->logger->debug('SCSSCacher::resetCache Lock acquired', ['app' => 'scss_cacher']);
384
+        $this->injectedVariables = null;
385
+
386
+        // do not clear locks
387
+        $this->cacheFactory->createDistributed('SCSS-deps-')->clear();
388
+        $this->cacheFactory->createDistributed('SCSS-cached-')->clear();
389
+
390
+        $appDirectory = $this->appData->getDirectoryListing();
391
+        foreach ($appDirectory as $folder) {
392
+            foreach ($folder->getDirectoryListing() as $file) {
393
+                try {
394
+                    $file->delete();
395
+                } catch (NotPermittedException $e) {
396
+                    $this->logger->logException($e, ['message' => 'SCSSCacher::resetCache unable to delete file: ' . $file->getName(), 'app' => 'scss_cacher']);
397
+                }
398
+            }
399
+        }
400
+        $this->logger->debug('SCSSCacher::resetCache css cache cleared!', ['app' => 'scss_cacher']);
401
+        $this->lockingCache->remove('resetCache');
402
+        $this->logger->debug('SCSSCacher::resetCache Locking removed', ['app' => 'scss_cacher']);
403
+    }
404
+
405
+    /**
406
+     * @return string SCSS code for variables from OC_Defaults
407
+     */
408
+    private function getInjectedVariables(): string {
409
+        if ($this->injectedVariables !== null) {
410
+            return $this->injectedVariables;
411
+        }
412
+        $variables = '';
413
+        foreach ($this->defaults->getScssVariables() as $key => $value) {
414
+            $variables .= '$' . $key . ': ' . $value . ' !default;';
415
+        }
416
+
417
+        // check for valid variables / otherwise fall back to defaults
418
+        try {
419
+            $scss = new Compiler();
420
+            $scss->compile($variables);
421
+            $this->injectedVariables = $variables;
422
+        } catch (ParserException $e) {
423
+            $this->logger->logException($e, ['app' => 'scss_cacher']);
424
+        }
425
+
426
+        return $variables;
427
+    }
428
+
429
+    /**
430
+     * Add the correct uri prefix to make uri valid again
431
+     * @param string $css
432
+     * @param string $webDir
433
+     * @return string
434
+     */
435
+    private function rebaseUrls(string $css, string $webDir): string {
436
+        $re = '/url\([\'"]([^\/][\.\w?=\/-]*)[\'"]\)/x';
437
+        $subst = 'url(\'' . $webDir . '/$1\')';
438
+
439
+        return preg_replace($re, $subst, $css);
440
+    }
441
+
442
+    /**
443
+     * Return the cached css file uri
444
+     * @param string $appName the app name
445
+     * @param string $fileName
446
+     * @return string
447
+     */
448
+    public function getCachedSCSS(string $appName, string $fileName): string {
449
+        $tmpfileLoc = explode('/', $fileName);
450
+        $fileName = array_pop($tmpfileLoc);
451
+        $fileName = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileName)), $appName);
452
+
453
+        return substr($this->urlGenerator->linkToRoute('core.Css.getCss', [
454
+            'fileName' => $fileName,
455
+            'appName' => $appName,
456
+            'v' => $this->config->getAppValue('core', 'theming.variables', '0')
457
+        ]), \strlen(\OC::$WEBROOT) + 1);
458
+    }
459
+
460
+    /**
461
+     * Prepend hashed base url to the css file
462
+     * @param string $cssFile
463
+     * @return string
464
+     */
465
+    private function prependBaseurlPrefix(string $cssFile): string {
466
+        return substr(md5($this->urlGenerator->getBaseUrl() . $this->getRoutePrefix()), 0, 4) . '-' . $cssFile;
467
+    }
468
+
469
+    private function getRoutePrefix() {
470
+        $frontControllerActive = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true');
471
+        $prefix = \OC::$WEBROOT . '/index.php';
472
+        if ($frontControllerActive) {
473
+            $prefix = \OC::$WEBROOT;
474
+        }
475
+        return $prefix;
476
+    }
477
+
478
+    /**
479
+     * Prepend hashed app version hash
480
+     * @param string $cssFile
481
+     * @param string $appId
482
+     * @return string
483
+     */
484
+    private function prependVersionPrefix(string $cssFile, string $appId): string {
485
+        $appVersion = \OC_App::getAppVersion($appId);
486
+        if ($appVersion !== '0') {
487
+            return substr(md5($appVersion), 0, 4) . '-' . $cssFile;
488
+        }
489
+        $coreVersion = \OC_Util::getVersionString();
490
+
491
+        return substr(md5($coreVersion), 0, 4) . '-' . $cssFile;
492
+    }
493
+
494
+    /**
495
+     * Get WebDir root
496
+     * @param string $path the css file path
497
+     * @param string $appName the app name
498
+     * @param string $serverRoot the server root path
499
+     * @param string $webRoot the nextcloud installation root path
500
+     * @return string the webDir
501
+     */
502
+    private function getWebDir(string $path, string $appName, string $serverRoot, string $webRoot): string {
503
+        // Detect if path is within server root AND if path is within an app path
504
+        if (strpos($path, $serverRoot) === false && $appWebPath = \OC_App::getAppWebPath($appName)) {
505
+            // Get the file path within the app directory
506
+            $appDirectoryPath = explode($appName, $path)[1];
507
+            // Remove the webroot
508
+
509
+            return str_replace($webRoot, '', $appWebPath . $appDirectoryPath);
510
+        }
511
+
512
+        return $webRoot . substr($path, strlen($serverRoot));
513
+    }
514
+
515
+    /**
516
+     * Add the icons css cache in the header if needed
517
+     *
518
+     * @return boolean true
519
+     */
520
+    private function injectCssVariablesIfAny() {
521
+        // Inject icons vars css if any
522
+        if ($this->iconsCacher->getCachedCSS() && $this->iconsCacher->getCachedCSS()->getSize() > 0) {
523
+            $this->iconsCacher->injectCss();
524
+        }
525
+        return true;
526
+    }
527 527
 }
Please login to merge, or discard this patch.