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