Completed
Push — master ( 7fdd90...9d4372 )
by Morris
17:39
created

SCSSCacher::getWebDir()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 2
b 0
f 0
nc 2
nop 4
dl 0
loc 10
rs 9.4285
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 string */
61
	protected $serverRoot;
62
63
	/** @var ICache */
64
	protected $depsCache;
65
66
	/**
67
	 * @param ILogger $logger
68
	 * @param Factory $appDataFactory
69
	 * @param IURLGenerator $urlGenerator
70
	 * @param IConfig $config
71
	 * @param \OC_Defaults $defaults
72
	 * @param string $serverRoot
73
	 * @param ICache $depsCache
74
	 */
75
	public function __construct(ILogger $logger,
76
								Factory $appDataFactory,
77
								IURLGenerator $urlGenerator,
78
								IConfig $config,
79
								\OC_Defaults $defaults,
80
								$serverRoot,
81
								ICache $depsCache) {
82
		$this->logger = $logger;
83
		$this->appData = $appDataFactory->get('css');
84
		$this->urlGenerator = $urlGenerator;
85
		$this->config = $config;
86
		$this->defaults = $defaults;
0 ignored issues
show
Bug introduced by
The property defaults does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

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