Completed
Push — master ( f0158e...2060ff )
by Blizzz
50:16 queued 36:57
created

SCSSCacher::getCachedCSS()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, John Molakvoæ ([email protected])
4
 *
5
 * @license GNU AGPL version 3 or any later version
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as
9
 * published by the Free Software Foundation, either version 3 of the
10
 * License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 *
20
 */
21
22
namespace OC\Template;
23
24
use Leafo\ScssPhp\Compiler;
25
use Leafo\ScssPhp\Exception\ParserException;
26
use Leafo\ScssPhp\Formatter\Crunched;
27
use Leafo\ScssPhp\Formatter\Expanded;
28
use OC\Files\AppData\Factory;
29
use OCP\Files\IAppData;
30
use OCP\Files\NotFoundException;
31
use OCP\Files\NotPermittedException;
32
use OCP\Files\SimpleFS\ISimpleFile;
33
use OCP\Files\SimpleFS\ISimpleFolder;
34
use OCP\ICache;
35
use OCP\IConfig;
36
use OCP\ILogger;
37
use OCP\IURLGenerator;
38
39
class SCSSCacher {
40
41
	/** @var ILogger */
42
	protected $logger;
43
44
	/** @var IAppData */
45
	protected $appData;
46
47
	/** @var IURLGenerator */
48
	protected $urlGenerator;
49
50
	/** @var IConfig */
51
	protected $config;
52
53
	/** @var string */
54
	protected $serverRoot;
55
56
	/** @var ICache */
57
	protected $depsCache;
58
59
	/**
60
	 * @param ILogger $logger
61
	 * @param Factory $appDataFactory
62
	 * @param IURLGenerator $urlGenerator
63
	 * @param IConfig $config
64
	 * @param \OC_Defaults $defaults
65
	 * @param string $serverRoot
66
	 * @param ICache $depsCache
67
	 */
68
	public function __construct(ILogger $logger,
69
								Factory $appDataFactory,
70
								IURLGenerator $urlGenerator,
71
								IConfig $config,
72
								\OC_Defaults $defaults,
73
								$serverRoot,
74
								ICache $depsCache) {
75
		$this->logger = $logger;
76
		$this->appData = $appDataFactory->get('css');
77
		$this->urlGenerator = $urlGenerator;
78
		$this->config = $config;
79
		$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...
80
		$this->serverRoot = $serverRoot;
81
		$this->depsCache = $depsCache;
82
	}
83
84
	/**
85
	 * Process the caching process if needed
86
	 * @param string $root Root path to the nextcloud installation
87
	 * @param string $file
88
	 * @param string $app The app name
89
	 * @return boolean
90
	 */
91
	public function process($root, $file, $app) {
92
		$path = explode('/', $root . '/' . $file);
93
94
		$fileNameSCSS = array_pop($path);
95
		$fileNameCSS = $this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS));
96
97
		$path = implode('/', $path);
98
99
		$webDir = substr($path, strlen($this->serverRoot)+1);
100
101
		try {
102
			$folder = $this->appData->getFolder($app);
103
		} catch(NotFoundException $e) {
104
			// creating css appdata folder
105
			$folder = $this->appData->newFolder($app);
106
		}
107
108
109
		if(!$this->variablesChanged() && $this->isCached($fileNameCSS, $folder)) {
110
			return true;
111
		}
112
		return $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir);
113
	}
114
115
	/**
116
	 * @param $appName
117
	 * @param $fileName
118
	 * @return ISimpleFile
119
	 */
120
	public function getCachedCSS($appName, $fileName) {
121
		$folder = $this->appData->getFolder($appName);
122
		return $folder->getFile($this->prependBaseurlPrefix($fileName));
123
	}
124
125
	/**
126
	 * Check if the file is cached or not
127
	 * @param string $fileNameCSS
128
	 * @param ISimpleFolder $folder
129
	 * @return boolean
130
	 */
131
	private function isCached($fileNameCSS, ISimpleFolder $folder) {
132
		try {
133
			$cachedFile = $folder->getFile($fileNameCSS);
134
			if ($cachedFile->getSize() > 0) {
135
				$depFileName = $fileNameCSS . '.deps';
136
				$deps = $this->depsCache->get($folder->getName() . '-' . $depFileName);
137
				if ($deps === null) {
138
					$depFile = $folder->getFile($depFileName);
139
					$deps = $depFile->getContent();
140
					//Set to memcache for next run
141
					$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
142
				}
143
				$deps = json_decode($deps, true);
144
145 View Code Duplication
				foreach ($deps as $file=>$mtime) {
146
					if (!file_exists($file) || filemtime($file) > $mtime) {
147
						return false;
148
					}
149
				}
150
			}
151
			return true;
152
		} catch(NotFoundException $e) {
153
			return false;
154
		}
155
	}
156
157
	/**
158
	 * Check if the variables file has changed
159
	 * @return bool
160
	 */
161
	private function variablesChanged() {
162
		$injectedVariables = $this->getInjectedVariables();
163
		if($this->config->getAppValue('core', 'scss.variables') !== md5($injectedVariables)) {
164
			$this->resetCache();
165
			$this->config->setAppValue('core', 'scss.variables', md5($injectedVariables));
166
			return true;
167
		}
168
		return false;
169
	}
170
171
	/**
172
	 * Cache the file with AppData
173
	 * @param string $path
174
	 * @param string $fileNameCSS
175
	 * @param string $fileNameSCSS
176
	 * @param ISimpleFolder $folder
177
	 * @param string $webDir
178
	 * @return boolean
179
	 */
180
	private function cache($path, $fileNameCSS, $fileNameSCSS, ISimpleFolder $folder, $webDir) {
181
		$scss = new Compiler();
182
		$scss->setImportPaths([
183
			$path,
184
			\OC::$SERVERROOT . '/core/css/',
185
		]);
186
		if($this->config->getSystemValue('debug')) {
187
			// Debug mode
188
			$scss->setFormatter(Expanded::class);
189
			$scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
190
		} else {
191
			// Compression
192
			$scss->setFormatter(Crunched::class);
193
		}
194
195
		try {
196
			$cachedfile = $folder->getFile($fileNameCSS);
197
		} catch(NotFoundException $e) {
198
			$cachedfile = $folder->newFile($fileNameCSS);
199
		}
200
201
		$depFileName = $fileNameCSS . '.deps';
202
		try {
203
			$depFile = $folder->getFile($depFileName);
204
		} catch (NotFoundException $e) {
205
			$depFile = $folder->newFile($depFileName);
206
		}
207
208
		// Compile
209
		try {
210
			$compiledScss = $scss->compile(
211
				'@import "variables.scss";' .
212
				$this->getInjectedVariables() .
213
				'@import "'.$fileNameSCSS.'";');
214
		} 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...
215
			$this->logger->error($e, ['app' => 'core']);
216
			return false;
217
		}
218
219
		// Gzip file
220
		try {
221
			$gzipFile = $folder->getFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
222
		} catch (NotFoundException $e) {
223
			$gzipFile = $folder->newFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz
224
		}
225
226
		try {
227
			$data = $this->rebaseUrls($compiledScss, $webDir);
228
			$cachedfile->putContent($data);
229
			$deps = json_encode($scss->getParsedFiles());
230
			$depFile->putContent($deps);
231
			$this->depsCache->set($folder->getName() . '-' . $depFileName, $deps);
232
			$gzipFile->putContent(gzencode($data, 9));
233
			$this->logger->debug($webDir.'/'.$fileNameSCSS.' compiled and successfully cached', ['app' => 'core']);
234
			return true;
235
		} catch(NotPermittedException $e) {
236
			return false;
237
		}
238
	}
239
240
	/**
241
	 * Reset scss cache by deleting all generated css files
242
	 * We need to regenerate all files when variables change
243
	 */
244
	private function resetCache() {
245
		$appDirectory = $this->appData->getDirectoryListing();
246
		if(empty($appDirectory)){
247
			return;
248
		}
249
		foreach ($appDirectory as $folder) {
250
			foreach ($folder->getDirectoryListing() as $file) {
251
				if (substr($file->getName(), -3) === "css" || substr($file->getName(), -4) === "deps") {
252
					$file->delete();
253
				}
254
			}
255
		}
256
	}
257
258
	/**
259
	 * @return string SCSS code for variables from OC_Defaults
260
	 */
261
	private function getInjectedVariables() {
262
		$variables = '';
263
		foreach ($this->defaults->getScssVariables() as $key => $value) {
264
			$variables .= '$' . $key . ': ' . $value . ';';
265
		}
266
		return $variables;
267
	}
268
269
	/**
270
	 * Add the correct uri prefix to make uri valid again
271
	 * @param string $css
272
	 * @param string $webDir
273
	 * @return string
274
	 */
275
	private function rebaseUrls($css, $webDir) {
276
		$re = '/url\([\'"]([\.\w?=\/-]*)[\'"]\)/x';
277
		// OC\Route\Router:75
278
		if(($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true')) {
279
			$subst = 'url(\'../../'.$webDir.'/$1\')';	
280
		} else {
281
			$subst = 'url(\'../../../'.$webDir.'/$1\')';
282
		}
283
		return preg_replace($re, $subst, $css);
284
	}
285
286
	/**
287
	 * Return the cached css file uri
288
	 * @param string $appName the app name
289
	 * @param string $fileName
290
	 * @return string
291
	 */
292 View Code Duplication
	public function getCachedSCSS($appName, $fileName) {
293
		$tmpfileLoc = explode('/', $fileName);
294
		$fileName = array_pop($tmpfileLoc);
295
		$fileName = $this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileName));
296
297
		return substr($this->urlGenerator->linkToRoute('core.Css.getCss', array('fileName' => $fileName, 'appName' => $appName)), strlen(\OC::$WEBROOT) + 1);
298
	}
299
300
	/**
301
	 * Prepend hashed base url to the css file
302
	 * @param $cssFile
303
	 * @return string
304
	 */
305
	private function prependBaseurlPrefix($cssFile) {
306
		$frontendController = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true');
307
		return md5($this->urlGenerator->getBaseUrl() . $frontendController) . '-' . $cssFile;
308
	}
309
}
310