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