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