Completed
Pull Request — master (#8999)
by John
53:27 queued 34:41
created

SCSSCacher::prependVersionPrefix()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, John Molakvoæ ([email protected])
4
 *
5
 * @author John Molakvoæ (skjnldsv) <[email protected]>
6
 * @author Julius Haertl <[email protected]>
7
 * @author Julius Härtl <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 *
12
 * @license GNU AGPL version 3 or any later version
13
 *
14
 * This program is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License as
16
 * published by the Free Software Foundation, either version 3 of the
17
 * License, or (at your option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
29
namespace OC\Template;
30
31
use Leafo\ScssPhp\Compiler;
32
use Leafo\ScssPhp\Exception\ParserException;
33
use Leafo\ScssPhp\Formatter\Crunched;
34
use Leafo\ScssPhp\Formatter\Expanded;
35
use OC\Files\AppData\Factory;
36
use OCP\Files\IAppData;
37
use OCP\Files\NotFoundException;
38
use OCP\Files\NotPermittedException;
39
use OCP\Files\SimpleFS\ISimpleFile;
40
use OCP\Files\SimpleFS\ISimpleFolder;
41
use OCP\ICache;
42
use OCP\ICacheFactory;
43
use OCP\IConfig;
44
use OCP\ILogger;
45
use OCP\IURLGenerator;
46
47
class SCSSCacher {
48
49
	/** @var ILogger */
50
	protected $logger;
51
52
	/** @var IAppData */
53
	protected $appData;
54
55
	/** @var IURLGenerator */
56
	protected $urlGenerator;
57
58
	/** @var IConfig */
59
	protected $config;
60
61
	/** @var \OC_Defaults */
62
	private $defaults;
63
64
	/** @var string */
65
	protected $serverRoot;
66
67
	/** @var ICache */
68
	protected $depsCache;
69
70
	/** @var null|string */
71
	private $injectedVariables;
72
73
	/** @var ICacheFactory */
74
	private $cacheFactory;
75
76
	/**
77
	 * @param ILogger $logger
78
	 * @param Factory $appDataFactory
79
	 * @param IURLGenerator $urlGenerator
80
	 * @param IConfig $config
81
	 * @param \OC_Defaults $defaults
82
	 * @param string $serverRoot
83
	 * @param ICacheFactory $cacheFactory
84
	 */
85
	public function __construct(ILogger $logger,
86
								Factory $appDataFactory,
87
								IURLGenerator $urlGenerator,
88
								IConfig $config,
89
								\OC_Defaults $defaults,
90
								$serverRoot,
91
								ICacheFactory $cacheFactory) {
92
		$this->logger = $logger;
93
		$this->appData = $appDataFactory->get('css');
94
		$this->urlGenerator = $urlGenerator;
95
		$this->config = $config;
96
		$this->defaults = $defaults;
97
		$this->serverRoot = $serverRoot;
98
		$this->cacheFactory = $cacheFactory;
99
		$this->depsCache = $cacheFactory->createDistributed('SCSS-' . md5($this->urlGenerator->getBaseUrl()));
100
	}
101
102
	/**
103
	 * Process the caching process if needed
104
	 *
105
	 * @param string $root Root path to the nextcloud installation
106
	 * @param string $file
107
	 * @param string $app The app name
108
	 * @return boolean
109
	 * @throws NotPermittedException
110
	 */
111
	public function process(string $root, string $file, string $app): bool {
112
		$path = explode('/', $root . '/' . $file);
113
114
		$fileNameSCSS = array_pop($path);
115
		$fileNameCSS = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS)), $app);
116
117
		$path = implode('/', $path);
118
		$webDir = $this->getWebDir($path, $app, $this->serverRoot, \OC::$WEBROOT);
119
120
		try {
121
			$folder = $this->appData->getFolder($app);
122
		} catch(NotFoundException $e) {
123
			// creating css appdata folder
124
			$folder = $this->appData->newFolder($app);
125
		}
126
127
128
		if(!$this->variablesChanged() && $this->isCached($fileNameCSS, $folder)) {
129
			return true;
130
		}
131
		return $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir);
132
	}
133
134
	/**
135
	 * @param $appName
136
	 * @param $fileName
137
	 * @return ISimpleFile
138
	 */
139
	public function getCachedCSS(string $appName, string $fileName): ISimpleFile {
140
		$folder = $this->appData->getFolder($appName);
141
		$cachedFileName = $this->prependVersionPrefix($this->prependBaseurlPrefix($fileName), $appName);
142
		return $folder->getFile($cachedFileName);
143
	}
144
145
	/**
146
	 * Check if the file is cached or not
147
	 * @param string $fileNameCSS
148
	 * @param ISimpleFolder $folder
149
	 * @return boolean
150
	 */
151
	private function isCached(string $fileNameCSS, ISimpleFolder $folder) {
152
		try {
153
			$cachedFile = $folder->getFile($fileNameCSS);
154
			if ($cachedFile->getSize() > 0) {
155
				$depFileName = $fileNameCSS . '.deps';
156
				$deps = $this->depsCache->get($folder->getName() . '-' . $depFileName);
157
				if ($deps === null) {
158
					$depFile = $folder->getFile($depFileName);
159
					$deps = $depFile->getContent();
160
					//Set to memcache for next run
161
					$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
162
				}
163
				$deps = json_decode($deps, true);
164
165 View Code Duplication
				foreach ((array)$deps as $file=>$mtime) {
166
					if (!file_exists($file) || filemtime($file) > $mtime) {
167
						return false;
168
					}
169
				}
170
				return true;
171
			}
172
			return false;
173
		} catch(NotFoundException $e) {
174
			return false;
175
		}
176
	}
177
178
	/**
179
	 * Check if the variables file has changed
180
	 * @return bool
181
	 */
182
	private function variablesChanged(): bool {
183
		$injectedVariables = $this->getInjectedVariables();
184
		if($this->config->getAppValue('core', 'scss.variables') !== md5($injectedVariables)) {
185
			$this->resetCache();
186
			$this->config->setAppValue('core', 'scss.variables', md5($injectedVariables));
187
			return true;
188
		}
189
		return false;
190
	}
191
192
	/**
193
	 * Cache the file with AppData
194
	 *
195
	 * @param string $path
196
	 * @param string $fileNameCSS
197
	 * @param string $fileNameSCSS
198
	 * @param ISimpleFolder $folder
199
	 * @param string $webDir
200
	 * @return boolean
201
	 * @throws NotPermittedException
202
	 */
203
	private function cache(string $path, string $fileNameCSS, string $fileNameSCSS, ISimpleFolder $folder, string $webDir) {
204
		$scss = new Compiler();
205
		$scss->setImportPaths([
206
			$path,
207
			$this->serverRoot . '/core/css/',
208
		]);
209
		// Continue after throw
210
		$scss->setIgnoreErrors(true);
211
		if($this->config->getSystemValue('debug')) {
212
			// Debug mode
213
			$scss->setFormatter(Expanded::class);
214
			$scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
215
		} else {
216
			// Compression
217
			$scss->setFormatter(Crunched::class);
218
		}
219
220
		try {
221
			$cachedfile = $folder->getFile($fileNameCSS);
222
		} catch(NotFoundException $e) {
223
			$cachedfile = $folder->newFile($fileNameCSS);
224
		}
225
226
		$depFileName = $fileNameCSS . '.deps';
227
		try {
228
			$depFile = $folder->getFile($depFileName);
229
		} catch (NotFoundException $e) {
230
			$depFile = $folder->newFile($depFileName);
231
		}
232
233
		// Compile
234
		try {
235
			$compiledScss = $scss->compile(
236
				'@import "variables.scss";' .
237
				$this->getInjectedVariables() .
238
				'@import "'.$fileNameSCSS.'";');
239
		} catch(ParserException $e) {
0 ignored issues
show
Bug introduced by
The class Leafo\ScssPhp\Exception\ParserException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
240
			$this->logger->error($e, ['app' => 'core']);
241
			return false;
242
		}
243
244
		// Gzip file
245
		try {
246
			$gzipFile = $folder->getFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
247
		} catch (NotFoundException $e) {
248
			$gzipFile = $folder->newFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
249
		}
250
251
		try {
252
			$data = $this->rebaseUrls($compiledScss, $webDir);
253
			$cachedfile->putContent($data);
254
			$deps = json_encode($scss->getParsedFiles());
255
			$depFile->putContent($deps);
256
			$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
257
			$gzipFile->putContent(gzencode($data, 9));
258
			$this->logger->debug('SCSSCacher: '.$webDir.'/'.$fileNameSCSS.' compiled and successfully cached', ['app' => 'core']);
259
			return true;
260
		} catch(NotPermittedException $e) {
261
			$this->logger->error('SCSSCacher: unable to cache: ' . $fileNameSCSS);
262
			return false;
263
		}
264
	}
265
266
	/**
267
	 * Reset scss cache by deleting all generated css files
268
	 * We need to regenerate all files when variables change
269
	 */
270 View Code Duplication
	public function resetCache() {
271
		$this->injectedVariables = null;
272
		$this->cacheFactory->createDistributed('SCSS-')->clear();
273
		$appDirectory = $this->appData->getDirectoryListing();
274
		foreach ($appDirectory as $folder) {
275
			foreach ($folder->getDirectoryListing() as $file) {
276
				$file->delete();
277
			}
278
		}
279
	}
280
281
	/**
282
	 * @return string SCSS code for variables from OC_Defaults
283
	 */
284
	private function getInjectedVariables(): string {
285
		if ($this->injectedVariables !== null) {
286
			return $this->injectedVariables;
287
		}
288
		$variables = '';
289
		foreach ($this->defaults->getScssVariables() as $key => $value) {
290
			$variables .= '$' . $key . ': ' . $value . ';';
291
		}
292
293
		// check for valid variables / otherwise fall back to defaults
294
		try {
295
			$scss = new Compiler();
296
			$scss->compile($variables);
297
			$this->injectedVariables = $variables;
298
		} catch (ParserException $e) {
0 ignored issues
show
Bug introduced by
The class Leafo\ScssPhp\Exception\ParserException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
299
			$this->logger->error($e, ['app' => 'core']);
300
		}
301
302
		return $variables;
303
	}
304
305
	/**
306
	 * Add the correct uri prefix to make uri valid again
307
	 * @param string $css
308
	 * @param string $webDir
309
	 * @return string
310
	 */
311
	private function rebaseUrls(string $css, string $webDir): string {
312
		$re = '/url\([\'"]([^\/][\.\w?=\/-]*)[\'"]\)/x';
313
		$subst = 'url(\''.$webDir.'/$1\')';
314
		return preg_replace($re, $subst, $css);
315
	}
316
317
	/**
318
	 * Return the cached css file uri
319
	 * @param string $appName the app name
320
	 * @param string $fileName
321
	 * @return string
322
	 */
323 View Code Duplication
	public function getCachedSCSS(string $appName, string $fileName): string {
324
		$tmpfileLoc = explode('/', $fileName);
325
		$fileName = array_pop($tmpfileLoc);
326
		$fileName = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileName)), $appName);
327
328
		return substr($this->urlGenerator->linkToRoute('core.Css.getCss', ['fileName' => $fileName, 'appName' => $appName]), strlen(\OC::$WEBROOT) + 1);
329
	}
330
331
	/**
332
	 * Prepend hashed base url to the css file
333
	 * @param string $cssFile
334
	 * @return string
335
	 */
336
	private function prependBaseurlPrefix(string $cssFile): string {
337
		$frontendController = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true');
338
		return substr(md5($this->urlGenerator->getBaseUrl() . $frontendController), 0, 4) . '-' . $cssFile;
339
	}
340
341
	/**
342
	 * Prepend hashed app version hash
343
	 * @param string $cssFile
344
	 * @param string $appId
345
	 * @return string
346
	 */
347
	private function prependVersionPrefix(string $cssFile, string $appId): string {
348
		$appVersion = \OC_App::getAppVersion($appId);
349
		if ($appVersion !== '0') {
350
			return substr(md5($appVersion), 0, 4) . '-' . $cssFile;
351
		}
352
		$coreVersion = \OC_Util::getVersionString();
353
		return substr(md5($coreVersion), 0, 4) . '-' . $cssFile;
354
	}
355
356
	/**
357
	 * Get WebDir root
358
	 * @param string $path the css file path
359
	 * @param string $appName the app name
360
	 * @param string $serverRoot the server root path
361
	 * @param string $webRoot the nextcloud installation root path
362
	 * @return string the webDir
363
	 */
364
	private function getWebDir(string $path, string $appName, string $serverRoot, string $webRoot): string {
365
		// Detect if path is within server root AND if path is within an app path
366
		if ( strpos($path, $serverRoot) === false && $appWebPath = \OC_App::getAppWebPath($appName)) {
367
			// Get the file path within the app directory
368
			$appDirectoryPath = explode($appName, $path)[1];
369
			// Remove the webroot
370
			return str_replace($webRoot, '', $appWebPath.$appDirectoryPath);
371
		}
372
		return $webRoot.substr($path, strlen($serverRoot));
373
	}
374
}
375