Completed
Push — master ( 1424b3...847bd0 )
by Morris
58:48 queued 38:02
created

SCSSCacher::rebaseUrls()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 5
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\IConfig;
43
use OCP\ILogger;
44
use OCP\IURLGenerator;
45
46
class SCSSCacher {
47
48
	/** @var ILogger */
49
	protected $logger;
50
51
	/** @var IAppData */
52
	protected $appData;
53
54
	/** @var IURLGenerator */
55
	protected $urlGenerator;
56
57
	/** @var IConfig */
58
	protected $config;
59
60
	/** @var \OC_Defaults */
61
	private $defaults;
62
63
	/** @var string */
64
	protected $serverRoot;
65
66
	/** @var ICache */
67
	protected $depsCache;
68
69
	/** @var null|string */
70
	private $injectedVariables;
71
72
	/**
73
	 * @param ILogger $logger
74
	 * @param Factory $appDataFactory
75
	 * @param IURLGenerator $urlGenerator
76
	 * @param IConfig $config
77
	 * @param \OC_Defaults $defaults
78
	 * @param string $serverRoot
79
	 * @param ICache $depsCache
80
	 */
81
	public function __construct(ILogger $logger,
82
								Factory $appDataFactory,
83
								IURLGenerator $urlGenerator,
84
								IConfig $config,
85
								\OC_Defaults $defaults,
86
								$serverRoot,
87
								ICache $depsCache) {
88
		$this->logger = $logger;
89
		$this->appData = $appDataFactory->get('css');
90
		$this->urlGenerator = $urlGenerator;
91
		$this->config = $config;
92
		$this->defaults = $defaults;
93
		$this->serverRoot = $serverRoot;
94
		$this->depsCache = $depsCache;
95
	}
96
97
	/**
98
	 * Process the caching process if needed
99
	 *
100
	 * @param string $root Root path to the nextcloud installation
101
	 * @param string $file
102
	 * @param string $app The app name
103
	 * @return boolean
104
	 * @throws NotPermittedException
105
	 */
106
	public function process(string $root, string $file, string $app): bool {
107
		$path = explode('/', $root . '/' . $file);
108
109
		$fileNameSCSS = array_pop($path);
110
		$fileNameCSS = $this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS));
111
112
		$path = implode('/', $path);
113
		$webDir = $this->getWebDir($path, $app, $this->serverRoot, \OC::$WEBROOT);
114
115
		try {
116
			$folder = $this->appData->getFolder($app);
117
		} catch(NotFoundException $e) {
118
			// creating css appdata folder
119
			$folder = $this->appData->newFolder($app);
120
		}
121
122
123
		if(!$this->variablesChanged() && $this->isCached($fileNameCSS, $folder)) {
124
			return true;
125
		}
126
		return $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir);
127
	}
128
129
	/**
130
	 * @param $appName
131
	 * @param $fileName
132
	 * @return ISimpleFile
133
	 */
134
	public function getCachedCSS(string $appName, string $fileName): ISimpleFile {
135
		$folder = $this->appData->getFolder($appName);
136
		return $folder->getFile($this->prependBaseurlPrefix($fileName));
137
	}
138
139
	/**
140
	 * Check if the file is cached or not
141
	 * @param string $fileNameCSS
142
	 * @param ISimpleFolder $folder
143
	 * @return boolean
144
	 */
145
	private function isCached(string $fileNameCSS, ISimpleFolder $folder) {
146
		try {
147
			$cachedFile = $folder->getFile($fileNameCSS);
148
			if ($cachedFile->getSize() > 0) {
149
				$depFileName = $fileNameCSS . '.deps';
150
				$deps = $this->depsCache->get($folder->getName() . '-' . $depFileName);
151
				if ($deps === null) {
152
					$depFile = $folder->getFile($depFileName);
153
					$deps = $depFile->getContent();
154
					//Set to memcache for next run
155
					$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
156
				}
157
				$deps = json_decode($deps, true);
158
159 View Code Duplication
				foreach ((array)$deps as $file=>$mtime) {
160
					if (!file_exists($file) || filemtime($file) > $mtime) {
161
						return false;
162
					}
163
				}
164
				return true;
165
			}
166
			return false;
167
		} catch(NotFoundException $e) {
168
			return false;
169
		}
170
	}
171
172
	/**
173
	 * Check if the variables file has changed
174
	 * @return bool
175
	 */
176
	private function variablesChanged(): bool {
177
		$injectedVariables = $this->getInjectedVariables();
178
		if($this->config->getAppValue('core', 'scss.variables') !== md5($injectedVariables)) {
179
			$this->resetCache();
180
			$this->config->setAppValue('core', 'scss.variables', md5($injectedVariables));
181
			return true;
182
		}
183
		return false;
184
	}
185
186
	/**
187
	 * Cache the file with AppData
188
	 *
189
	 * @param string $path
190
	 * @param string $fileNameCSS
191
	 * @param string $fileNameSCSS
192
	 * @param ISimpleFolder $folder
193
	 * @param string $webDir
194
	 * @return boolean
195
	 * @throws NotPermittedException
196
	 */
197
	private function cache(string $path, string $fileNameCSS, string $fileNameSCSS, ISimpleFolder $folder, string $webDir) {
198
		$scss = new Compiler();
199
		$scss->setImportPaths([
200
			$path,
201
			$this->serverRoot . '/core/css/',
202
		]);
203
		// Continue after throw
204
		$scss->setIgnoreErrors(true);
205
		if($this->config->getSystemValue('debug')) {
206
			// Debug mode
207
			$scss->setFormatter(Expanded::class);
208
			$scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
209
		} else {
210
			// Compression
211
			$scss->setFormatter(Crunched::class);
212
		}
213
214
		try {
215
			$cachedfile = $folder->getFile($fileNameCSS);
216
		} catch(NotFoundException $e) {
217
			$cachedfile = $folder->newFile($fileNameCSS);
218
		}
219
220
		$depFileName = $fileNameCSS . '.deps';
221
		try {
222
			$depFile = $folder->getFile($depFileName);
223
		} catch (NotFoundException $e) {
224
			$depFile = $folder->newFile($depFileName);
225
		}
226
227
		// Compile
228
		try {
229
			$compiledScss = $scss->compile(
230
				'@import "variables.scss";' .
231
				$this->getInjectedVariables() .
232
				'@import "'.$fileNameSCSS.'";');
233
		} 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...
234
			$this->logger->error($e, ['app' => 'core']);
235
			return false;
236
		}
237
238
		// Gzip file
239
		try {
240
			$gzipFile = $folder->getFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
241
		} catch (NotFoundException $e) {
242
			$gzipFile = $folder->newFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
243
		}
244
245
		try {
246
			$data = $this->rebaseUrls($compiledScss, $webDir);
247
			$cachedfile->putContent($data);
248
			$deps = json_encode($scss->getParsedFiles());
249
			$depFile->putContent($deps);
250
			$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
251
			$gzipFile->putContent(gzencode($data, 9));
252
			$this->logger->debug('SCSSCacher: '.$webDir.'/'.$fileNameSCSS.' compiled and successfully cached', ['app' => 'core']);
253
			return true;
254
		} catch(NotPermittedException $e) {
255
			$this->logger->error('SCSSCacher: unable to cache: ' . $fileNameSCSS);
256
			return false;
257
		}
258
	}
259
260
	/**
261
	 * Reset scss cache by deleting all generated css files
262
	 * We need to regenerate all files when variables change
263
	 */
264
	public function resetCache() {
265
		$this->injectedVariables = null;
266
		$this->depsCache->clear();
267
		$appDirectory = $this->appData->getDirectoryListing();
268
		foreach ($appDirectory as $folder) {
269
			foreach ($folder->getDirectoryListing() as $file) {
270
				$file->delete();
271
			}
272
		}
273
	}
274
275
	/**
276
	 * @return string SCSS code for variables from OC_Defaults
277
	 */
278
	private function getInjectedVariables(): string {
279
		if ($this->injectedVariables !== null) {
280
			return $this->injectedVariables;
281
		}
282
		$variables = '';
283
		foreach ($this->defaults->getScssVariables() as $key => $value) {
284
			$variables .= '$' . $key . ': ' . $value . ';';
285
		}
286
287
		// check for valid variables / otherwise fall back to defaults
288
		try {
289
			$scss = new Compiler();
290
			$scss->compile($variables);
291
			$this->injectedVariables = $variables;
292
		} 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...
293
			$this->logger->error($e, ['app' => 'core']);
294
		}
295
296
		return $variables;
297
	}
298
299
	/**
300
	 * Add the correct uri prefix to make uri valid again
301
	 * @param string $css
302
	 * @param string $webDir
303
	 * @return string
304
	 */
305
	private function rebaseUrls(string $css, string $webDir): string {
306
		$re = '/url\([\'"]([^\/][\.\w?=\/-]*)[\'"]\)/x';
307
		$subst = 'url(\''.$webDir.'/$1\')';
308
		return preg_replace($re, $subst, $css);
309
	}
310
311
	/**
312
	 * Return the cached css file uri
313
	 * @param string $appName the app name
314
	 * @param string $fileName
315
	 * @return string
316
	 */
317 View Code Duplication
	public function getCachedSCSS(string $appName, string $fileName): string {
318
		$tmpfileLoc = explode('/', $fileName);
319
		$fileName = array_pop($tmpfileLoc);
320
		$fileName = $this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileName));
321
322
		return substr($this->urlGenerator->linkToRoute('core.Css.getCss', ['fileName' => $fileName, 'appName' => $appName]), strlen(\OC::$WEBROOT) + 1);
323
	}
324
325
	/**
326
	 * Prepend hashed base url to the css file
327
	 * @param string$cssFile
328
	 * @return string
329
	 */
330
	private function prependBaseurlPrefix(string $cssFile): string {
331
		$frontendController = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true');
332
		return substr(md5($this->urlGenerator->getBaseUrl() . $frontendController), 0, 8) . '-' . $cssFile;
333
	}
334
335
	/**
336
	 * Get WebDir root
337
	 * @param string $path the css file path
338
	 * @param string $appName the app name
339
	 * @param string $serverRoot the server root path
340
	 * @param string $webRoot the nextcloud installation root path
341
	 * @return string the webDir
342
	 */
343
	private function getWebDir(string $path, string $appName, string $serverRoot, string $webRoot): string {
344
		// Detect if path is within server root AND if path is within an app path
345
		if ( strpos($path, $serverRoot) === false && $appWebPath = \OC_App::getAppWebPath($appName)) {
346
			// Get the file path within the app directory
347
			$appDirectoryPath = explode($appName, $path)[1];
348
			// Remove the webroot
349
			return str_replace($webRoot, '', $appWebPath.$appDirectoryPath);
350
		}
351
		return $webRoot.substr($path, strlen($serverRoot));
352
	}
353
}
354