Issues (752)

server/includes/loader.php (3 issues)

1
<?php
2
3
/**
4
 * Manager for including JS and CSS files into the desired order.
5
 */
6
class FileLoader {
7
	private $source;
8
	private $cacheFile;
9
	private $cacheSum;
10
	private $extjsFiles;
11
	private $webappFiles;
12
	private $pluginFiles;
13
	private $remoteFiles;
14
15
	public function __construct() {
16
		// Unique cache file per grommunio Web location.
17
		$basePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . '.' . md5(realpath(__FILE__));
18
		$this->cacheFile = "{$basePath}-loadcache";
19
		$this->cacheSum = "{$basePath}-loadsum";
20
		$this->source = DEBUG_LOADER === LOAD_SOURCE;
21
	}
22
23
	/**
24
	 * Obtain the list of Extjs & UX files.
25
	 *
26
	 * @param number $load the LOAD_RELEASE | LOAD_DEBUG | LOAD_SOURCE flag
27
	 *                     to indicate which files should be loaded
28
	 *
29
	 * @return array The array of Javascript files
30
	 */
31
	public function getExtjsJavascriptFiles($load) {
32
		$jsLoadingSequence = [];
33
34
		if ($load == LOAD_RELEASE) {
35
			$jsLoadingSequence[] = "client/extjs/ext-base-all.js";
36
			$jsLoadingSequence[] = "client/extjs/ux/ux-all.js";
37
			$jsLoadingSequence[] = "client/extjs-mod/extjs-mod.js";
38
			$jsLoadingSequence[] = "client/tinymce/tinymce.min.js";
39
			$jsLoadingSequence[] = "client/third-party/ux-thirdparty.js";
40
			$jsLoadingSequence[] = "client/dompurify/purify.js";
41
		}
42
		elseif ($load == LOAD_DEBUG) {
43
			$jsLoadingSequence[] = "client/extjs/ext-base-debug.js";
44
			$jsLoadingSequence[] = "client/extjs/ext-all-debug.js";
45
			$jsLoadingSequence[] = "client/extjs/ux/ux-all-debug.js";
46
			$jsLoadingSequence[] = "client/extjs-mod/extjs-mod-debug.js";
47
			$jsLoadingSequence[] = "client/tinymce/tinymce.js";
48
			$jsLoadingSequence[] = "client/third-party/ux-thirdparty-debug.js";
49
			$jsLoadingSequence[] = "client/dompurify/purify.js";
50
		}
51
		else {
52
			$jsLoadingSequence[] = "client/extjs/ext-base-debug.js";
53
			$jsLoadingSequence[] = "client/extjs/ext-all-debug.js";
54
			$jsLoadingSequence[] = "client/extjs/ux/ux-all-debug.js";
55
			$jsLoadingSequence = array_merge(
56
				$jsLoadingSequence,
57
				$this->buildJSLoadingSequence(
58
					$this->getListOfFiles('js', 'client/extjs-mod')
59
				)
60
			);
61
			$jsLoadingSequence[] = "client/tinymce/tinymce.js";
62
			$jsLoadingSequence[] = "client/dompurify/purify.js";
63
			$jsLoadingSequence = array_merge(
64
				$jsLoadingSequence,
65
				$this->buildJSLoadingSequence(
66
					$this->getListOfFiles('js', 'client/third-party')
67
				)
68
			);
69
		}
70
71
		return $jsLoadingSequence;
72
	}
73
74
	/**
75
	 * Obtain the list of Extjs & UX files.
76
	 *
77
	 * @param number $load the LOAD_RELEASE | LOAD_DEBUG | LOAD_SOURCE flag
78
	 *                     to indicate which files should be loaded
79
	 *
80
	 * @return array The array of CSS files
81
	 */
82
	public function getExtjsCSSFiles($load) {
0 ignored issues
show
The parameter $load is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

82
	public function getExtjsCSSFiles(/** @scrutinizer ignore-unused */ $load) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
83
		return ["client/extjs/resources/css/ext-all-ux.css"];
84
	}
85
86
	/**
87
	 * Obtain the list of grommunio Web files.
88
	 *
89
	 * @param number $load     the LOAD_RELEASE | LOAD_DEBUG | LOAD_SOURCE flag
90
	 *                         to indicate which files should be loaded
91
	 * @param array  $libFiles (optional) library files when $load = LOAD_SOURCE
92
	 *
93
	 * @return array The array of Javascript files
94
	 */
95
	public function getZarafaJavascriptFiles($load, $libFiles = []) {
96
		$jsLoadingSequence = [];
97
98
		if ($load == LOAD_RELEASE) {
99
			$jsLoadingSequence[] = "client/grommunio.js";
100
		}
101
		elseif ($load == LOAD_DEBUG) {
102
			$jsLoadingSequence[] = "client/grommunio-debug.js";
103
		}
104
		else {
105
			$jsLoadingSequence = array_merge(
106
				$jsLoadingSequence,
107
				$this->buildJSLoadingSequence(
108
					$this->getListOfFiles('js', 'client/zarafa'),
109
					['client/zarafa/core'],
110
					$libFiles
111
				)
112
			);
113
		}
114
115
		return $jsLoadingSequence;
116
	}
117
118
	/**
119
	 * Obtain the list of all Javascript files as registered by the plugins.
120
	 *
121
	 * @param number $load     the LOAD_RELEASE | LOAD_DEBUG | LOAD_SOURCE flag
122
	 *                         to indicate which files should be loaded
123
	 * @param array  $libFiles (optional) library files when $load = LOAD_SOURCE
124
	 *
125
	 * @return array The array of Javascript files
126
	 */
127
	public function getPluginJavascriptFiles($load, $libFiles = []) {
128
		if ($load === LOAD_SOURCE) {
129
			return $this->buildJSLoadingSequence(
130
				$GLOBALS['PluginManager']->getClientFiles($load),
131
				[],
132
				$libFiles
133
			);
134
		}
135
136
		return $GLOBALS['PluginManager']->getClientFiles($load);
137
	}
138
139
	/**
140
	 * Obtain the list of all CSS files as registered by the plugins.
141
	 *
142
	 * @param number $load the LOAD_RELEASE | LOAD_DEBUG | LOAD_SOURCE flag
143
	 *                     to indicate which files should be loaded
144
	 *
145
	 * @return array The array of CSS files
146
	 */
147
	public function getPluginCSSFiles($load) {
148
		return $GLOBALS['PluginManager']->getResourceFiles($load);
149
	}
150
151
	/**
152
	 * Obtain the list of all Javascript files as provided by plugins using PluginManager#triggerHook
153
	 * for the hook 'server.main.include.jsfiles'.
154
	 *
155
	 * @param number $load the LOAD_RELEASE | LOAD_DEBUG | LOAD_SOURCE flag
156
	 *                     to indicate which files should be loaded
157
	 *
158
	 * @return array The array of Javascript files
159
	 */
160
	public function getRemoteJavascriptFiles($load) {
161
		$files = [];
162
		$GLOBALS['PluginManager']->triggerHook('server.main.include.jsfiles', ['load' => $load, 'files' => &$files]);
163
164
		return $files;
165
	}
166
167
	/**
168
	 * Obtain the list of all CSS files as provided by plugins using PluginManager#triggerHook
169
	 * for the hook 'server.main.include.cssfiles'.
170
	 *
171
	 * @param number $load the LOAD_RELEASE | LOAD_DEBUG | LOAD_SOURCE flag
172
	 *                     to indicate which files should be loaded
173
	 *
174
	 * @return array The array of CSS files
175
	 */
176
	public function getRemoteCSSFiles($load) {
177
		$files = [];
178
		$GLOBALS['PluginManager']->triggerHook('server.main.include.cssfiles', ['load' => $load, 'files' => &$files]);
179
180
		return $files;
181
	}
182
183
	/**
184
	 * Print each file on a new line using the given $template.
185
	 *
186
	 * @param array  $files         The files to print
187
	 * @param string $template      The template used to print each file, the string {file} will
188
	 *                              be replaced with the filename
189
	 * @param bool   $base          True if only the basename of the file must be printed
190
	 * @param bool   $concatVersion true if concatenate unique webapp version
191
	 *                              with file name to avoid the caching issue
192
	 */
193
	public function printFiles($files, $template = '{file}', $base = false, $concatVersion = true) {
194
		foreach ($files as $file) {
195
			$file = $base === true ? basename((string) $file) : $file;
196
			if ($concatVersion) {
197
				$file = $file . "?version=" . $this->getVersion();
198
			}
199
			echo str_replace('{file}', $file, $template) . PHP_EOL;
200
		}
201
	}
202
203
	/**
204
	 * Return grommunio Web version.
205
	 *
206
	 * @return string returns grommunio Web version
207
	 */
208
	public function getVersion() {
209
		return trim(file_get_contents('version'));
210
	}
211
212
	/**
213
	 * getJavascriptFiles.
214
	 *
215
	 * Scanning files and subdirectories that can be found within the supplied
216
	 * path and add all located Javascript files to a list.
217
	 *
218
	 * @param $path         String Path of the directory to scan
219
	 * @param $recursive    Boolean If set to true scans subdirectories as well
220
	 * @param $excludeFiles Array Optional Paths of files or directories that
221
	 *                      are excluded from the search
222
	 *
223
	 * @return array list of arrays containing the paths to files that have to be included
224
	 */
225
	public function getJavascriptFiles($path, $recursive = true, $excludeFiles = []) {
226
		return $this->getListOfFiles('js', $path, $recursive, $excludeFiles);
227
	}
228
229
	/**
230
	 * getCSSFiles.
231
	 *
232
	 * Scanning files and subdirectories that can be found within the supplied
233
	 * path and add all located CSS files to a list.
234
	 *
235
	 * @param $path         String Path of the directory to scan
236
	 * @param $recursive    Boolean If set to true scans subdirectories as well
237
	 * @param $excludeFiles Array Optional Paths of files or directories that
238
	 *                      are excluded from the search
239
	 *
240
	 * @return array list of arrays containing the paths to files that have to be included
241
	 */
242
	public function getCSSFiles($path, $recursive = true, $excludeFiles = []) {
243
		return $this->getListOfFiles('css', $path, $recursive, $excludeFiles);
244
	}
245
246
	/**
247
	 * getListOfFiles.
248
	 *
249
	 * Scanning files and subdirectories that can be found within the supplied
250
	 * path and add the files to a list.
251
	 *
252
	 * @param $ext          The extension of files that are included ("js" or "css")
253
	 * @param $path         String Path of the directory to scan
254
	 * @param $recursive    Boolean If set to true scans subdirectories as well
255
	 * @param $excludeFiles Array Optional Paths of files or directories that
256
	 *                      are excluded from the search
257
	 *
258
	 * @return array list of arrays containing the paths to files that have to be included
259
	 */
260
	private function getListOfFiles($ext, $path, $recursive = true, $excludeFiles = []) {
261
		/*
262
		 * We are using two lists of files to make sure the files from the
263
		 * subdirectories are added after the current directory files.
264
		 */
265
		$files = [];
266
		$subDirFiles = [];
267
268
		$dir = opendir($path);
269
		if (!is_resource($dir)) {
270
			return $files;
271
		}
272
273
		while (($file = readdir($dir)) !== false) {
274
			$filepath = $path . '/' . $file;
275
			// Skip entries like ".", ".." and ".svn"
276
			if (!str_starts_with($file, ".") && !in_array($filepath, $excludeFiles)) {
277
				// Make sure we have files to include
278
				$info = pathinfo($filepath, PATHINFO_EXTENSION);
279
280
				if (is_file($filepath) && $info == $ext) {
281
					$files[] = $filepath;
282
				// Subdirectories will be scanned as well
283
				}
284
				elseif ($recursive && is_dir($filepath)) {
285
					$subDirFiles = array_merge($subDirFiles, $this->getListOfFiles($ext, $filepath, $recursive, $excludeFiles));
286
				}
287
			}
288
		}
289
290
		/*
291
		 * Make the lists alphabetically sorted, doing this separate makes sure
292
		 * the subdirectories are added after the files in the directory above.
293
		 */
294
		sort($files);
295
		sort($subDirFiles);
296
297
		return array_merge($files, $subDirFiles);
298
	}
299
300
	/**
301
	 * buildJSLoadingSequence.
302
	 *
303
	 * Will build the correct loading sequence for the JS files in application based on the class,
304
	 * extends and depends statements in the files itself. It will first extract the class
305
	 * definitions and dependencies. It will put that information in a list that holds the
306
	 * dependencies for each file. With that list the proper sequence of loading can be constructed.
307
	 * Files that originate from any of the specified coreFiles folders will be marked as core files.
308
	 *
309
	 * @param $files     Array List of files that have to be included
310
	 * @param $coreFiles Array (Optional) List of folders that contain core files
311
	 * @param $libFiles  Array (Optional) List of files that is used as library (and can contain
312
	 *                   classed which are depended upon by the given files)
313
	 *
314
	 * @return array List of files that are sorted in the correct sequence
315
	 */
316
	private function buildJSLoadingSequence($files, $coreFiles = [], $libFiles = []) {
317
		// Create a lookup table to easily get the classes which are defined in the library files
318
		$libFileLookup = [];
319
		// Create a lookup table to easily get the name of the file the class is defined in
320
		$classFileLookup = [];
321
322
		$fileDataLookup = [];
323
		$fileDependencies = [];
324
325
		// Read all library files to determine the classes which are defined
326
		for ($i = 0, $len = count($libFiles); $i < $len; ++$i) {
327
			$filename = $libFiles[$i];
328
			$content = $this->getFileContents($filename);
329
330
			$class = [];
331
			preg_match_all('(@class\W([^\n\r]*))', $content, $class);
332
333
			$libFileLookup[$filename] = [
334
				'class' => $class[1],
335
			];
336
337
			for ($j = 0, $lenJ = count($class[1]); $j < $lenJ; ++$j) {
338
				$libFileLookup[$class[1][$j]] = true;
339
			}
340
		}
341
342
		for ($i = 0, $len = count($files); $i < $len; ++$i) {
343
			$content = $this->getFileContents($files[$i]);
344
			$filename = $files[$i];
345
346
			$extends = [];
347
			$dependsFile = [];
348
			$class = [];
349
350
			preg_match_all('(@extends\W([^\n\r]*))', $content, $extends);
351
			preg_match_all('(@class\W([^\n\r]*))', $content, $class);
352
			preg_match_all('(#dependsFile\W([^\n\r\*]+))', $content, $dependsFile);
353
			$core = (str_contains($content, '#core')) ? true : false;
354
355
			for ($j = 0, $lenJ = count($coreFiles); $j < $lenJ; ++$j) {
356
				if (str_starts_with((string) $filename, (string) $coreFiles[$j])) {
357
					$core = true;
358
					break;
359
				}
360
			}
361
362
			$fileDataLookup[$filename] = [
363
				'class' => $class[1],
364
				'extends' => $extends[1],
365
				'dependsFile' => $dependsFile[1],
366
			];
367
			$fileDependencies[$filename] = [
368
				'depends' => [],
369
				'core' => $core,		// Based on tag or on class or on file path?
370
			];
371
372
			for ($j = 0, $lenJ = count($class[1]); $j < $lenJ; ++$j) {
373
				$classFileLookup[$class[1][$j]] = $filename;
374
			}
375
		}
376
377
		// Convert dependencies found by searching for @extends to a filename.
378
		foreach ($fileDataLookup as $filename => &$fileData) {
379
			// First get the extended class dependencies. We also have to convert them into files names using the $classFileLookup.
380
			for ($i = 0, $len = count($fileData['extends']); $i < $len; ++$i) {
381
				// The check if it extends the Zarafa namespace is needed because we do not index other namespaces.
382
				if (str_starts_with($fileData['extends'][$i], 'Zarafa')) {
383
					if (isset($libFileLookup[$fileData['extends'][$i]])) {
384
						// The @extends is found in the library file.
385
						// No need to update the dependencies
386
					}
387
					elseif (isset($classFileLookup[$fileData['extends'][$i]])) {
388
						// The @extends is found as @class in another file
389
						// Convert the class dependency into a filename
390
						$dependencyFilename = $classFileLookup[$fileData['extends'][$i]];
391
						// Make sure the file does not depend on itself
392
						if ($dependencyFilename != $filename) {
393
							$fileDependencies[$filename]['depends'][] = $dependencyFilename;
394
						}
395
					}
396
					else {
397
						trigger_error('Unable to find @extends dependency "' . $fileData['extends'][$i] . '" for file "' . $filename . '"');
398
					}
399
				}
400
			}
401
402
			// Add the file dependencies that have been added by using #dependsFile in the file.
403
			for ($i = 0, $len = count($fileData['dependsFile']); $i < $len; ++$i) {
404
				$dependencyFilename = $fileData['dependsFile'][$i];
405
				// Check if the file exists to prevent non-existent dependencies
406
				if (isset($fileDataLookup[$dependencyFilename])) {
407
					// Make sure the file does not depend on itself
408
					if ($dependencyFilename != $filename) {
409
						$fileDependencies[$filename]['depends'][] = $dependencyFilename;
410
					}
411
				}
412
				else {
413
					trigger_error('Unable to find file #dependsFile dependency "' . $fileData['dependsFile'][$i] . '" for file "' . $filename . '"');
414
				}
415
			}
416
		}
417
		unset($fileData);
418
419
		$fileSequence = $this->generateDependencyBasedFileSeq($fileDependencies);
420
421
		return $fileSequence;
422
	}
423
424
	/**
425
	 * generateDependencyBasedFileSeq.
426
	 *
427
	 * This function will generate a loading sequence for the supplied list of files and their
428
	 * dependencies. This function calculates the depth of each file in the dependencytree. Based on
429
	 * that depth it calculates a weight for each file and that will determine the order in which
430
	 * the files will be included.
431
	 * The weight consists of two times the depth of the node and a penalty for files that have not
432
	 * been marked as a core file. This way core files get included prior to other files at the same
433
	 * depth. Files with the same weight are added in the order they are in the list and that should
434
	 * be alphabetically.
435
	 *
436
	 * @param $fileData Array List of files with dependency data in the format of
437
	 *                  $fileData[ FILENAME ] = Array(
438
	 *                  'depends' => Array(FILENAME1, FILENAME2),
439
	 *                  'core' => true|false
440
	 *                  );
441
	 *
442
	 * @return array List of filenames in the calculated loading sequence
443
	 */
444
	private function generateDependencyBasedFileSeq($fileData) {
445
		$fileDepths = [];
446
447
		$changed = true;
448
		while ($changed && (count($fileDepths) < count($fileData))) {
449
			$changed = false;
450
451
			// Loop through all the files and see if for each file we can get a depth assigned based on their parents depth.
452
			foreach ($fileData as $file => $dependencyData) {
453
				$dependencies = $dependencyData['depends'];
454
455
				if (!isset($fileDepths[$file])) {
456
					if (count($dependencies) > 0) {
457
						$parentsDepthAssigned = true;
458
						$highestParentDepth = 0;
459
						// See if all the parents already have a depth assigned and if so take the highest one.
460
						$dependenciesCount = count($dependencies);
461
						for ($i = 0; $i < $dependenciesCount; ++$i) {
462
							// Not all parents depths have been assigned yet, wait another turn
463
							if (!isset($fileDepths[$dependencies[$i]])) {
464
								$parentsDepthAssigned = false;
465
								break;
466
							}
467
							// We should only take the highest depth
468
							$highestParentDepth = max($highestParentDepth, $fileDepths[$dependencies[$i]]);
469
						}
470
						// All parents have a depth assigned, we can calculate the one for this node.
471
						if ($parentsDepthAssigned) {
472
							$fileDepths[$file] = $highestParentDepth + 1;
473
							$changed = true;
474
						}
475
					// The node does not have any dependencies so its a root node.
476
					}
477
					else {
478
						$fileDepths[$file] = 0;
479
						$changed = true;
480
					}
481
				}
482
			}
483
		}
484
485
		// If not all the files have been assigned a depth, but nothing changed the last round there
486
		// must be something wrong with the dependencies of the skipped files. So lets tell someone.
487
		if (count($fileDepths) < count($fileData)) {
488
			$errorMsg = '[LOADER] Could not compute all dependencies. The following files cannot be resolved properly: ';
489
			$errorMsg .= implode(', ', array_diff(array_keys($fileData), array_keys($fileDepths)));
490
			trigger_error($errorMsg);
491
		}
492
493
		$fileWeights = [];
494
		// Now lets determine each file's weight
495
		foreach ($fileData as $file => $dependencyData) {
496
			if ($fileDepths[$file] !== null) {
497
				$weight = $fileDepths[$file] * 2;
498
				// Add a penalty of 1 to non-core files to up the core-files in the sequence.
499
				if (!$dependencyData['core']) {
500
					++$weight;
501
				}
502
			}
503
			else {
504
				// Make up a weight to put it at the end
505
				$weight = count($fileData);
506
			}
507
			if (!isset($fileWeights[$weight])) {
508
				$fileWeights[$weight] = [];
509
			}
510
			$fileWeights[$weight][] = $file;
511
		}
512
513
		// The weights have not been added in the correct order, so sort it first on the keys.
514
		ksort($fileWeights);
515
516
		// Now put it all in the correct order. Files with the same weight are added in the order
517
		// they are in the list. This order should still be alphabetically.
518
		$fileSequence = [];
519
		foreach ($fileWeights as $weight => $fileList) {
520
			$fileListCount = count($fileList);
521
			for ($i = 0; $i < $fileListCount; ++$i) {
522
				$fileSequence[] = $fileList[$i];
523
			}
524
		}
525
526
		return $fileSequence;
527
	}
528
529
	/**
530
	 * getFileContents.
531
	 *
532
	 * Returns the content of the supplied file name.
533
	 *
534
	 * @param $fn String File name
535
	 *
536
	 * @return string Content of the file
537
	 */
538
	private function getFileContents($fn) {
539
		$fn = strtok($fn, '?');
540
541
		$fc = "";
542
		$fh = fopen($fn, "r");
543
		if ($fh) {
0 ignored issues
show
$fh is of type resource, thus it always evaluated to false.
Loading history...
544
			while (!feof($fh)) {
545
				$fc .= fgets($fh, 4096);
546
			}
547
			fclose($fh);
548
		}
549
550
		return $fc;
551
	}
552
553
	/**
554
	 * The JavaScript load order for grommunio Web. The loader order is cached when grommunio Web
555
	 * is LOAD_SOURCE mode, since calculating the loader is quite expensive.
556
	 */
557
	public function jsOrder() {
558
		if ($this->source) {
559
			if ($this->cacheExists()) {
560
				echo file_get_contents($this->cacheFile);
561
562
				return;
563
			}
564
			ob_start();
565
		}
566
567
		[$extjsFiles, $webappFiles, $pluginFiles, $remoteFiles] = $this->getJsFiles();
568
569
		$jsTemplate = "\t\t<script src=\"{file}\"></script>";
570
		$this->printFiles($extjsFiles, $jsTemplate);
571
		$this->printFiles($webappFiles, $jsTemplate);
572
		$this->printFiles($pluginFiles, $jsTemplate);
573
		$this->printFiles($remoteFiles, $jsTemplate);
574
575
		if ($this->source) {
576
			$contents = ob_get_contents();
577
			ob_end_clean();
578
			echo $contents;
579
			file_put_contents($this->cacheFile, $contents);
580
		}
581
	}
582
583
	/**
584
	 * Returns an array with all javascript files. The array has four entries, for the ExtJS files,
585
	 * the Zarafa files, the plugin files and the remote files respectively.
586
	 * This function will make sure that the directories are read only once.
587
	 *
588
	 * @return array An array that contains the names of all the javascript files that should be loaded
589
	 */
590
	private function getJsFiles() {
591
		if (!isset($this->extjsfiles)) {
0 ignored issues
show
The property extjsfiles does not exist on FileLoader. Did you mean extjsFiles?
Loading history...
592
			$this->extjsFiles = $this->getExtjsJavascriptFiles(DEBUG_LOADER);
593
			$this->webappFiles = $this->getZarafaJavascriptFiles(DEBUG_LOADER, $this->extjsFiles);
594
			$this->pluginFiles = $this->getPluginJavascriptFiles(DEBUG_LOADER, array_merge($this->extjsFiles, $this->webappFiles));
595
			$this->remoteFiles = $this->getRemoteJavascriptFiles(DEBUG_LOADER);
596
		}
597
598
		return [$this->extjsFiles, $this->webappFiles, $this->pluginFiles, $this->remoteFiles];
599
	}
600
601
	/**
602
	 * The CSS load order for grommunio Web.
603
	 */
604
	public function cssOrder() {
605
		$cssTemplate = "\t\t<link rel=\"stylesheet\" type=\"text/css\" href=\"{file}\">";
606
		$extjsFiles = $this->getExtjsCSSFiles(DEBUG_LOADER);
607
		$this->printFiles($extjsFiles, $cssTemplate);
608
609
		// Since we only have one css file, we can add that directly
610
		$this->printFiles(["client/resources/css/grommunio.css"], $cssTemplate);
611
612
		$pluginFiles = $this->getPluginCSSFiles(DEBUG_LOADER);
613
		$this->printFiles($pluginFiles, $cssTemplate);
614
615
		$remoteFiles = $this->getRemoteCSSFiles(DEBUG_LOADER);
616
		$this->printFiles($remoteFiles, $cssTemplate);
617
	}
618
619
	/**
620
	 * Checks if the JavaScript or CSS files on disk have been changed
621
	 * and writes a new md5 of the files to the disk.
622
	 *
623
	 * return boolean False if cache is outdated
624
	 */
625
	private function cacheExists() {
626
		[$extjsFiles, $webappFiles, $pluginFiles, $remoteFiles] = $this->getJsFiles();
627
		$files = [$extjsFiles, $webappFiles, $pluginFiles, $remoteFiles];
628
		$md5 = md5(json_encode($files));
629
630
		if (!file_exists($this->cacheSum) || file_get_contents($this->cacheSum) !== $md5) {
631
			file_put_contents($this->cacheSum, $md5);
632
633
			return false;
634
		}
635
636
		return true;
637
	}
638
}
639