Completed
Push — new-committers ( 29cb6f...bcba16 )
by Sam
12:18 queued 33s
created

Requirements_Backend::process_combined_files()   F

Complexity

Conditions 32
Paths > 20000

Size

Total Lines 135
Code Lines 73

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 135
rs 2
cc 32
eloc 73
nc 35570
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Requirements tracker for JavaScript and CSS.
5
 *
6
 * @package framework
7
 * @subpackage view
8
 */
9
class Requirements implements Flushable {
10
11
	/**
12
	 * Triggered early in the request when a flush is requested
13
	 */
14
	public static function flush() {
15
		self::delete_all_combined_files();
16
	}
17
18
	/**
19
	 * Enable combining of css/javascript files.
20
	 *
21
	 * @param bool $enable
22
	 */
23
	public static function set_combined_files_enabled($enable) {
24
		self::backend()->set_combined_files_enabled($enable);
25
	}
26
27
	/**
28
	 * Checks whether combining of css/javascript files is enabled.
29
	 *
30
	 * @return bool
31
	 */
32
	public static function get_combined_files_enabled() {
33
		return self::backend()->get_combined_files_enabled();
34
	}
35
36
	/**
37
	 * Set the relative folder e.g. 'assets' for where to store combined files
38
	 *
39
	 * @param string $folder Path to folder
40
	 */
41
	public static function set_combined_files_folder($folder) {
42
		self::backend()->setCombinedFilesFolder($folder);
43
	}
44
45
	/**
46
	 * Set whether to add caching query params to the requests for file-based requirements.
47
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
48
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
49
	 * while automatically busting this cache every time the file is changed.
50
	 *
51
	 * @param bool
52
	 */
53
	public static function set_suffix_requirements($var) {
54
		self::backend()->set_suffix_requirements($var);
55
	}
56
57
	/**
58
	 * Check whether we want to suffix requirements
59
	 *
60
	 * @return bool
61
	 */
62
	public static function get_suffix_requirements() {
63
		return self::backend()->get_suffix_requirements();
64
	}
65
66
	/**
67
	 * Instance of the requirements for storage. You can create your own backend to change the
68
	 * default JS and CSS inclusion behaviour.
69
	 *
70
	 * @var Requirements_Backend
71
	 */
72
	private static $backend = null;
73
74
	public static function backend() {
75
		if(!self::$backend) {
76
			self::$backend = new Requirements_Backend();
77
		}
78
		return self::$backend;
79
	}
80
81
	/**
82
	 * Setter method for changing the Requirements backend
83
	 *
84
	 * @param Requirements_Backend $backend
85
	 */
86
	public static function set_backend(Requirements_Backend $backend) {
87
		self::$backend = $backend;
88
	}
89
90
	/**
91
	 * Register the given JavaScript file as required.
92
	 *
93
	 * @param string $file Relative to docroot
94
	 */
95
	public static function javascript($file) {
96
		self::backend()->javascript($file);
97
	}
98
99
	/**
100
	 * Register the given JavaScript code into the list of requirements
101
	 *
102
	 * @param string     $script       The script content as a string (without enclosing <script> tag)
103
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
104
	 */
105
	public static function customScript($script, $uniquenessID = null) {
106
		self::backend()->customScript($script, $uniquenessID);
107
	}
108
109
	/**
110
	 * Return all registered custom scripts
111
	 *
112
	 * @return array
113
	 */
114
	public static function get_custom_scripts() {
115
		return self::backend()->get_custom_scripts();
116
	}
117
118
	/**
119
	 * Register the given CSS styles into the list of requirements
120
	 *
121
	 * @param string     $script       CSS selectors as a string (without enclosing <style> tag)
122
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
123
	 */
124
	public static function customCSS($script, $uniquenessID = null) {
125
		self::backend()->customCSS($script, $uniquenessID);
126
	}
127
128
	/**
129
	 * Add the following custom HTML code to the <head> section of the page
130
	 *
131
	 * @param string     $html         Custom HTML code
132
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
133
	 */
134
	public static function insertHeadTags($html, $uniquenessID = null) {
135
		self::backend()->insertHeadTags($html, $uniquenessID);
136
	}
137
138
	/**
139
	 * Include the content of the given JavaScript file in the list of requirements. Dollar-sign
140
	 * variables will be interpolated with values from $vars similar to a .ss template.
141
	 *
142
	 * @param string         $file         The template file to load, relative to docroot
143
	 * @param string[]|int[] $vars         The array of variables to interpolate.
144
	 * @param string|int     $uniquenessID A unique ID that ensures a piece of code is only added once
145
	 */
146
	public static function javascriptTemplate($file, $vars, $uniquenessID = null) {
147
		self::backend()->javascriptTemplate($file, $vars, $uniquenessID);
148
	}
149
150
	/**
151
	 * Register the given stylesheet into the list of requirements.
152
	 *
153
	 * @param string $file  The CSS file to load, relative to site root
154
	 * @param string $media Comma-separated list of media types to use in the link tag
155
	 *                      (e.g. 'screen,projector')
156
	 */
157
	public static function css($file, $media = null) {
158
		self::backend()->css($file, $media);
159
	}
160
161
	/**
162
	 * Registers the given themeable stylesheet as required.
163
	 *
164
	 * A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
165
	 * and it that doesn't exist and the module parameter is set then a CSS file with that name in
166
	 * the module is used.
167
	 *
168
	 * @param string $name   The name of the file - eg '/css/File.css' would have the name 'File'
169
	 * @param string $module The module to fall back to if the css file does not exist in the
170
	 *                       current theme.
171
	 * @param string $media  Comma-separated list of media types to use in the link tag
172
	 *                       (e.g. 'screen,projector')
173
	 */
174
	public static function themedCSS($name, $module = null, $media = null) {
175
		return self::backend()->themedCSS($name, $module, $media);
176
	}
177
178
	/**
179
	 * Clear either a single or all requirements
180
	 *
181
	 * Caution: Clearing single rules added via customCSS and customScript only works if you
182
	 * originally specified a $uniquenessID.
183
	 *
184
	 * @param string|int $fileOrID
185
	 */
186
	public static function clear($fileOrID = null) {
187
		self::backend()->clear($fileOrID);
188
	}
189
190
	/**
191
	 * Restore requirements cleared by call to Requirements::clear
192
	 */
193
	public static function restore() {
194
		self::backend()->restore();
195
	}
196
197
	/**
198
	 * Block inclusion of a specific file
199
	 *
200
	 * The difference between this and {@link clear} is that the calling order does not matter;
201
	 * {@link clear} must be called after the initial registration, whereas {@link block} can be
202
	 * used in advance. This is useful, for example, to block scripts included by a superclass
203
	 * without having to override entire functions and duplicate a lot of code.
204
	 *
205
	 * Note that blocking should be used sparingly because it's hard to trace where an file is
206
	 * being blocked from.
207
	 *
208
	 * @param string|int $fileOrID
209
	 */
210
	public static function block($fileOrID) {
211
		self::backend()->block($fileOrID);
212
	}
213
214
	/**
215
	 * Remove an item from the block list
216
	 *
217
	 * @param string|int $fileOrID
218
	 */
219
	public static function unblock($fileOrID) {
220
		self::backend()->unblock($fileOrID);
221
	}
222
223
	/**
224
	 * Removes all items from the block list
225
	 */
226
	public static function unblock_all() {
227
		self::backend()->unblock_all();
228
	}
229
230
	/**
231
	 * Update the given HTML content with the appropriate include tags for the registered
232
	 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
233
	 * including a head and body tag.
234
	 *
235
	 * @param string $templateFile No longer used, only retained for compatibility
236
	 * @param string $content      HTML content that has already been parsed from the $templateFile
237
	 *                             through {@link SSViewer}
238
	 * @return string HTML content augmented with the requirements tags
239
	 */
240
	public static function includeInHTML($templateFile, $content) {
241
		return self::backend()->includeInHTML($templateFile, $content);
242
	}
243
244
	/**
245
	 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
246
	 * HTTP Response
247
	 *
248
	 * @param SS_HTTPResponse $response
249
	 */
250
	public static function include_in_response(SS_HTTPResponse $response) {
251
		return self::backend()->include_in_response($response);
252
	}
253
254
	/**
255
	 * Add i18n files from the given javascript directory. SilverStripe expects that the given
256
	 * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
257
	 * etc.
258
	 *
259
	 * @param string $langDir  The JavaScript lang directory, relative to the site root, e.g.,
260
	 *                         'framework/javascript/lang'
261
	 * @param bool   $return   Return all relative file paths rather than including them in
262
	 *                         requirements
263
	 * @param bool   $langOnly Only include language files, not the base libraries
264
	 *
265
	 * @return array
266
	 */
267
	public static function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
268
		return self::backend()->add_i18n_javascript($langDir, $return, $langOnly);
269
	}
270
271
	/**
272
	 * Concatenate several css or javascript files into a single dynamically generated file. This
273
	 * increases performance by fewer HTTP requests.
274
	 *
275
	 * The combined file is regenerated based on every file modification time. Optionally a
276
	 * rebuild can be triggered by appending ?flush=1 to the URL. If all files to be combined are
277
	 * JavaScript, we use the external JSMin library to minify the JavaScript.
278
	 *
279
	 * All combined files will have a comment on the start of each concatenated file denoting their
280
	 * original position. For easier debugging, we only minify JavaScript if not in development
281
	 * mode ({@link Director::isDev()}).
282
	 *
283
	 * CAUTION: You're responsible for ensuring that the load order for combined files is
284
	 * retained - otherwise combining JavaScript files can lead to functional errors in the
285
	 * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also
286
	 * only include each file once across all includes and comibinations in a single page load.
287
	 *
288
	 * CAUTION: Combining CSS Files discards any "media" information.
289
	 *
290
	 * Example for combined JavaScript:
291
	 * <code>
292
	 * Requirements::combine_files(
293
	 *  'foobar.js',
294
	 *  array(
295
	 *        'mysite/javascript/foo.js',
296
	 *        'mysite/javascript/bar.js',
297
	 *    )
298
	 * );
299
	 * </code>
300
	 *
301
	 * Example for combined CSS:
302
	 * <code>
303
	 * Requirements::combine_files(
304
	 *  'foobar.css',
305
	 *    array(
306
	 *        'mysite/javascript/foo.css',
307
	 *        'mysite/javascript/bar.css',
308
	 *    )
309
	 * );
310
	 * </code>
311
	 *
312
	 * @param string $combinedFileName Filename of the combined file relative to docroot
313
	 * @param array  $files            Array of filenames relative to docroot
314
	 * @param string $media
315
	 *
316
	 * @return bool|void
317
	 */
318
	public static function combine_files($combinedFileName, $files, $media = null) {
319
		self::backend()->combine_files($combinedFileName, $files, $media);
320
	}
321
322
	/**
323
	 * Return all combined files; keys are the combined file names, values are lists of
324
	 * files being combined.
325
	 *
326
	 * @return array
327
	 */
328
	public static function get_combine_files() {
329
		return self::backend()->get_combine_files();
330
	}
331
332
	/**
333
	 * Delete all dynamically generated combined files from the filesystem
334
	 *
335
	 * @param string $combinedFileName If left blank, all combined files are deleted.
336
	 */
337
	public static function delete_combined_files($combinedFileName = null) {
338
		return self::backend()->delete_combined_files($combinedFileName);
339
	}
340
341
	/**
342
	 * Deletes all generated combined files in the configured combined files directory,
343
	 * but doesn't delete the directory itself
344
	 */
345
	public static function delete_all_combined_files() {
346
		return self::backend()->delete_all_combined_files();
347
	}
348
349
	/**
350
	 * Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
351
	 */
352
	public static function clear_combined_files() {
353
		self::backend()->clear_combined_files();
354
	}
355
356
	/**
357
	 * Do the heavy lifting involved in combining (and, in the case of JavaScript minifying) the
358
	 * combined files.
359
 	 */
360
	public static function process_combined_files() {
361
		return self::backend()->process_combined_files();
362
	}
363
364
	/**
365
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
366
	 * head tag.
367
	 *
368
	 * @param bool
369
	 */
370
	public static function set_write_js_to_body($var) {
371
		self::backend()->set_write_js_to_body($var);
372
	}
373
374
	/**
375
	 * Set whether to force the JavaScript to end of the body. Useful if you use inline script tags
376
	 * that don't rely on scripts included via {@link Requirements::javascript()).
377
	 *
378
	 * @param boolean $var If true, force the JavaScript to be included at the bottom of the page
379
	 */
380
	public static function set_force_js_to_bottom($var) {
381
		self::backend()->set_force_js_to_bottom($var);
382
	}
383
384
	/**
385
	 * Output debugging information
386
	 */
387
	public static function debug() {
388
		return self::backend()->debug();
389
	}
390
391
}
392
393
/**
394
 * @package framework
395
 * @subpackage view
396
 */
397
class Requirements_Backend {
398
399
	/**
400
	 * Whether to add caching query params to the requests for file-based requirements.
401
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
402
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
403
	 * while automatically busting this cache every time the file is changed.
404
	 *
405
	 * @var bool
406
	 */
407
	protected $suffix_requirements = true;
408
409
	/**
410
	 * Whether to combine CSS and JavaScript files
411
	 *
412
	 * @var bool
413
	 */
414
	protected $combined_files_enabled = true;
415
416
	/**
417
	 * Paths to all required JavaScript files relative to docroot
418
	 *
419
	 * @var array $javascript
420
	 */
421
	protected $javascript = array();
422
423
	/**
424
	 * Paths to all required CSS files relative to the docroot.
425
	 *
426
	 * @var array $css
427
	 */
428
	protected $css = array();
429
430
	/**
431
	 * All custom javascript code that is inserted into the page's HTML
432
	 *
433
	 * @var array $customScript
434
	 */
435
	protected $customScript = array();
436
437
	/**
438
	 * All custom CSS rules which are inserted directly at the bottom of the HTML <head> tag
439
	 *
440
	 * @var array $customCSS
441
	 */
442
	protected $customCSS = array();
443
444
	/**
445
	 * All custom HTML markup which is added before the closing <head> tag, e.g. additional
446
	 * metatags.
447
	 */
448
	protected $customHeadTags = array();
449
450
	/**
451
	 * Remembers the file paths or uniquenessIDs of all Requirements cleared through
452
	 * {@link clear()}, so that they can be restored later.
453
	 *
454
	 * @var array $disabled
455
	 */
456
	protected $disabled = array();
457
458
	/**
459
	 * The file paths (relative to docroot) or uniquenessIDs of any included requirements which
460
	 * should be blocked when executing {@link inlcudeInHTML()}. This is useful, for example,
461
	 * to block scripts included by a superclass without having to override entire functions and
462
	 * duplicate a lot of code.
463
	 *
464
	 * Use {@link unblock()} or {@link unblock_all()} to revert changes.
465
	 *
466
	 * @var array $blocked
467
	 */
468
	protected $blocked = array();
469
470
	/**
471
	 * A list of combined files registered via {@link combine_files()}. Keys are the output file
472
	 * names, values are lists of input files.
473
	 *
474
	 * @var array $combine_files
475
	 */
476
	public $combine_files = array();
477
478
	/**
479
	 * Use the JSMin library to minify any javascript file passed to {@link combine_files()}.
480
	 *
481
	 * @var bool
482
	 */
483
	public $combine_js_with_jsmin = true;
484
485
	/**
486
	 * Whether or not file headers should be written when combining files
487
	 *
488
	 * @var boolean
489
	 */
490
	public $write_header_comment = true;
491
492
	/**
493
	 * Where to save combined files. By default they're placed in assets/_combinedfiles, however
494
	 * this may be an issue depending on your setup, especially for CSS files which often contain
495
	 * relative paths.
496
	 *
497
	 * @var string
498
	 */
499
	protected $combinedFilesFolder = null;
500
501
	/**
502
	 * Put all JavaScript includes at the bottom of the template before the closing <body> tag,
503
	 * rather than the default behaviour of placing them at the end of the <head> tag. This means
504
	 * script downloads won't block other HTTP requests, which can be a performance improvement.
505
	 *
506
	 * @var bool
507
	 */
508
	public $write_js_to_body = true;
509
510
	/**
511
	 * Force the JavaScript to the bottom of the page, even if there's a script tag in the body already
512
	 *
513
	 * @var boolean
514
	 */
515
	protected $force_js_to_bottom = false;
516
517
	/**
518
	 * Enable or disable the combination of CSS and JavaScript files
519
	 *
520
	 * @param $enable
521
	 */
522
	public function set_combined_files_enabled($enable) {
523
		$this->combined_files_enabled = (bool) $enable;
524
	}
525
526
	/**
527
	 * Check whether file combination is enabled.
528
	 *
529
	 * @return bool
530
	 */
531
	public function get_combined_files_enabled() {
532
		return $this->combined_files_enabled;
533
	}
534
535
	/**
536
	 * Set the folder to save combined files in. By default they're placed in assets/_combinedfiles,
537
	 * however this may be an issue depending on your setup, especially for CSS files which often
538
	 * contain relative paths.
539
	 *
540
	 * @param string $folder
541
	 */
542
	public function setCombinedFilesFolder($folder) {
543
		$this->combinedFilesFolder = $folder;
544
	}
545
546
	/**
547
	 * @return string Folder relative to the webroot
548
	 */
549
	public function getCombinedFilesFolder() {
550
		return ($this->combinedFilesFolder) ? $this->combinedFilesFolder : ASSETS_DIR . '/_combinedfiles';
551
	}
552
553
	/**
554
	 * Set whether to add caching query params to the requests for file-based requirements.
555
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
556
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
557
	 * while automatically busting this cache every time the file is changed.
558
	 *
559
	 * @param bool
560
	 */
561
	public function set_suffix_requirements($var) {
562
		$this->suffix_requirements = $var;
563
	}
564
565
	/**
566
	 * Check whether we want to suffix requirements
567
	 *
568
	 * @return bool
569
	 */
570
	public function get_suffix_requirements() {
571
		return $this->suffix_requirements;
572
	}
573
574
	/**
575
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
576
	 * head tag.
577
	 *
578
	 * @param bool
579
	 */
580
	public function set_write_js_to_body($var) {
581
		$this->write_js_to_body = $var;
582
	}
583
584
	/**
585
	 * Forces the JavaScript requirements to the end of the body, right before the closing tag
586
	 *
587
	 * @param bool
588
	 */
589
	public function set_force_js_to_bottom($var) {
590
		$this->force_js_to_bottom = $var;
591
	}
592
593
	/**
594
	 * Register the given JavaScript file as required.
595
	 *
596
	 * @param string $file Relative to docroot
597
	 */
598
	public function javascript($file) {
599
		$this->javascript[$file] = true;
600
	}
601
602
	/**
603
	 * Returns an array of all required JavaScript
604
	 *
605
	 * @return array
606
	 */
607
	public function get_javascript() {
608
		return array_keys(array_diff_key($this->javascript, $this->blocked));
609
	}
610
611
	/**
612
	 * Register the given JavaScript code into the list of requirements
613
	 *
614
	 * @param string     $script       The script content as a string (without enclosing <script> tag)
615
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
616
	 */
617
	public function customScript($script, $uniquenessID = null) {
618
		if($uniquenessID) $this->customScript[$uniquenessID] = $script;
619
		else $this->customScript[] = $script;
620
621
		$script .= "\n";
622
	}
623
624
	/**
625
	 * Return all registered custom scripts
626
	 *
627
	 * @return array
628
	 */
629
	public function get_custom_scripts() {
630
		$requirements = "";
631
632
		if($this->customScript) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customScript of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
633
			foreach($this->customScript as $script) {
634
				$requirements .= "$script\n";
635
			}
636
		}
637
638
		return $requirements;
639
	}
640
641
	/**
642
	 * Register the given CSS styles into the list of requirements
643
	 *
644
	 * @param string     $script       CSS selectors as a string (without enclosing <style> tag)
645
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
646
	 */
647
	public function customCSS($script, $uniquenessID = null) {
648
		if($uniquenessID) $this->customCSS[$uniquenessID] = $script;
649
		else $this->customCSS[] = $script;
650
	}
651
652
	/**
653
	 * Add the following custom HTML code to the <head> section of the page
654
	 *
655
	 * @param string     $html         Custom HTML code
656
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
657
	 */
658
	public function insertHeadTags($html, $uniquenessID = null) {
659
		if($uniquenessID) $this->customHeadTags[$uniquenessID] = $html;
660
		else $this->customHeadTags[] = $html;
661
	}
662
663
	/**
664
	 * Include the content of the given JavaScript file in the list of requirements. Dollar-sign
665
	 * variables will be interpolated with values from $vars similar to a .ss template.
666
	 *
667
	 * @param string         $file         The template file to load, relative to docroot
668
	 * @param string[]|int[] $vars         The array of variables to interpolate.
669
	 * @param string|int     $uniquenessID A unique ID that ensures a piece of code is only added once
670
	 */
671
	public function javascriptTemplate($file, $vars, $uniquenessID = null) {
672
		$script = file_get_contents(Director::getAbsFile($file));
673
		$search = array();
674
		$replace = array();
675
676
		if($vars) foreach($vars as $k => $v) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $vars of type array<string|integer> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
677
			$search[] = '$' . $k;
678
			$replace[] = str_replace("\\'","'", Convert::raw2js($v));
679
		}
680
681
		$script = str_replace($search, $replace, $script);
682
		$this->customScript($script, $uniquenessID);
683
	}
684
685
	/**
686
	 * Register the given stylesheet into the list of requirements.
687
	 *
688
	 * @param string $file  The CSS file to load, relative to site root
689
	 * @param string $media Comma-separated list of media types to use in the link tag
690
	 *                      (e.g. 'screen,projector')
691
	 */
692
	public function css($file, $media = null) {
693
		$this->css[$file] = array(
694
			"media" => $media
695
		);
696
	}
697
698
	/**
699
	 * Get the list of registered CSS file requirements, excluding blocked files
700
	 *
701
	 * @return array
702
	 */
703
	public function get_css() {
704
		return array_diff_key($this->css, $this->blocked);
705
	}
706
707
	/**
708
	 * Clear either a single or all requirements
709
	 *
710
	 * Caution: Clearing single rules added via customCSS and customScript only works if you
711
	 * originally specified a $uniquenessID.
712
	 *
713
	 * @param string|int $fileOrID
714
	 */
715
	public function clear($fileOrID = null) {
716
		if($fileOrID) {
717
			foreach(array('javascript','css', 'customScript', 'customCSS', 'customHeadTags') as $type) {
718
				if(isset($this->{$type}[$fileOrID])) {
719
					$this->disabled[$type][$fileOrID] = $this->{$type}[$fileOrID];
720
					unset($this->{$type}[$fileOrID]);
721
				}
722
			}
723
		} else {
724
			$this->disabled['javascript'] = $this->javascript;
725
			$this->disabled['css'] = $this->css;
726
			$this->disabled['customScript'] = $this->customScript;
727
			$this->disabled['customCSS'] = $this->customCSS;
728
			$this->disabled['customHeadTags'] = $this->customHeadTags;
729
730
			$this->javascript = array();
731
			$this->css = array();
732
			$this->customScript = array();
733
			$this->customCSS = array();
734
			$this->customHeadTags = array();
735
		}
736
	}
737
738
	/**
739
	 * Restore requirements cleared by call to Requirements::clear
740
	 */
741
	public function restore() {
742
		$this->javascript = $this->disabled['javascript'];
743
		$this->css = $this->disabled['css'];
744
		$this->customScript = $this->disabled['customScript'];
745
		$this->customCSS = $this->disabled['customCSS'];
746
		$this->customHeadTags = $this->disabled['customHeadTags'];
747
	}
748
	/**
749
	 * Block inclusion of a specific file
750
	 *
751
	 * The difference between this and {@link clear} is that the calling order does not matter;
752
	 * {@link clear} must be called after the initial registration, whereas {@link block} can be
753
	 * used in advance. This is useful, for example, to block scripts included by a superclass
754
	 * without having to override entire functions and duplicate a lot of code.
755
	 *
756
	 * Note that blocking should be used sparingly because it's hard to trace where an file is
757
	 * being blocked from.
758
	 *
759
	 * @param string|int $fileOrID
760
	 */
761
	public function block($fileOrID) {
762
		$this->blocked[$fileOrID] = $fileOrID;
763
	}
764
765
	/**
766
	 * Remove an item from the block list
767
	 *
768
	 * @param string|int $fileOrID
769
	 */
770
	public function unblock($fileOrID) {
771
		if(isset($this->blocked[$fileOrID])) unset($this->blocked[$fileOrID]);
772
	}
773
774
	/**
775
	 * Removes all items from the block list
776
	 */
777
	public function unblock_all() {
778
		$this->blocked = array();
779
	}
780
781
	/**
782
	 * Update the given HTML content with the appropriate include tags for the registered
783
	 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
784
	 * including a head and body tag.
785
	 *
786
	 * @param string $templateFile No longer used, only retained for compatibility
787
	 * @param string $content      HTML content that has already been parsed from the $templateFile
788
	 *                             through {@link SSViewer}
789
	 * @return string HTML content augmented with the requirements tags
790
	 */
791
	public function includeInHTML($templateFile, $content) {
0 ignored issues
show
Unused Code introduced by
The parameter $templateFile is not used and could be removed.

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

Loading history...
792
		if(
793
			(strpos($content, '</head>') !== false || strpos($content, '</head ') !== false)
794
			&& ($this->css || $this->javascript || $this->customCSS || $this->customScript || $this->customHeadTags)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->css of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->javascript of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->customCSS of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->customScript of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->customHeadTags of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
795
		) {
796
			$requirements = '';
797
			$jsRequirements = '';
798
799
			// Combine files - updates $this->javascript and $this->css
800
			$this->process_combined_files();
801
802 View Code Duplication
			foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
803
				$path = Convert::raw2xml($this->path_for_file($file));
0 ignored issues
show
Bug introduced by
It seems like $this->path_for_file($file) targeting Requirements_Backend::path_for_file() can also be of type boolean; however, Convert::raw2xml() does only seem to accept array|string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
804
				if($path) {
805
					$jsRequirements .= "<script type=\"text/javascript\" src=\"$path\"></script>\n";
806
				}
807
			}
808
809
			// Add all inline JavaScript *after* including external files they might rely on
810
			if($this->customScript) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customScript of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
811
				foreach(array_diff_key($this->customScript,$this->blocked) as $script) {
812
					$jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n";
813
					$jsRequirements .= "$script\n";
814
					$jsRequirements .= "\n//]]>\n</script>\n";
815
				}
816
			}
817
818 View Code Duplication
			foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
819
				$path = Convert::raw2xml($this->path_for_file($file));
0 ignored issues
show
Bug introduced by
It seems like $this->path_for_file($file) targeting Requirements_Backend::path_for_file() can also be of type boolean; however, Convert::raw2xml() does only seem to accept array|string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
820
				if($path) {
821
					$media = (isset($params['media']) && !empty($params['media']))
822
						? " media=\"{$params['media']}\"" : "";
823
					$requirements .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$path\" />\n";
824
				}
825
			}
826
827
			foreach(array_diff_key($this->customCSS, $this->blocked) as $css) {
828
				$requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
829
			}
830
831
			foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) {
832
				$requirements .= "$customHeadTag\n";
833
			}
834
835
			if ($this->force_js_to_bottom) {
836
				// Remove all newlines from code to preserve layout
837
				$jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements);
838
839
				// Forcefully put the scripts at the bottom of the body instead of before the first
840
				// script tag.
841
				$content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content);
842
843
				// Put CSS at the bottom of the head
844
				$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
845
			} elseif($this->write_js_to_body) {
846
				// Remove all newlines from code to preserve layout
847
				$jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements);
848
849
				// If your template already has script tags in the body, then we try to put our script
850
				// tags just before those. Otherwise, we put it at the bottom.
851
				$p2 = stripos($content, '<body');
852
				$p1 = stripos($content, '<script', $p2);
853
854
				$commentTags = array();
855
				$canWriteToBody = ($p1 !== false)
856
					&&
857
					// Check that the script tag is not inside a html comment tag
858
					!(
859
						preg_match('/.*(?|(<!--)|(-->))/U', $content, $commentTags, 0, $p1)
860
						&&
861
						$commentTags[1] == '-->'
862
					);
863
864
				if($canWriteToBody) {
865
					$content = substr($content,0,$p1) . $jsRequirements . substr($content,$p1);
866
				} else {
867
					$content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content);
868
				}
869
870
				// Put CSS at the bottom of the head
871
				$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
872
			} else {
873
				$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
874
				$content = preg_replace("/(<\/head>)/i", $jsRequirements . "\\1", $content);
875
			}
876
		}
877
878
		return $content;
879
	}
880
881
	/**
882
	 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
883
	 * HTTP Response
884
	 *
885
	 * @param SS_HTTPResponse $response
886
	 */
887
	public function include_in_response(SS_HTTPResponse $response) {
888
		$this->process_combined_files();
889
		$jsRequirements = array();
890
		$cssRequirements = array();
891
892 View Code Duplication
		foreach(array_diff_key($this->javascript, $this->blocked) as $file => $dummy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
893
			$path = $this->path_for_file($file);
894
			if($path) {
895
				$jsRequirements[] = str_replace(',', '%2C', $path);
896
			}
897
		}
898
899
		$response->addHeader('X-Include-JS', implode(',', $jsRequirements));
900
901 View Code Duplication
		foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
902
			$path = $this->path_for_file($file);
903
			if($path) {
904
				$path = str_replace(',', '%2C', $path);
905
				$cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path;
906
			}
907
		}
908
909
		$response->addHeader('X-Include-CSS', implode(',', $cssRequirements));
910
	}
911
912
	/**
913
	 * Add i18n files from the given javascript directory. SilverStripe expects that the given
914
	 * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
915
	 * etc.
916
	 *
917
	 * @param string $langDir  The JavaScript lang directory, relative to the site root, e.g.,
918
	 *                         'framework/javascript/lang'
919
	 * @param bool   $return   Return all relative file paths rather than including them in
920
	 *                         requirements
921
	 * @param bool   $langOnly Only include language files, not the base libraries
922
	 *
923
	 * @return array
924
	 */
925
	public function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
926
		$files = array();
927
		$base = Director::baseFolder() . '/';
928
		if(i18n::config()->js_i18n) {
929
			// Include i18n.js even if no languages are found.  The fact that
930
			// add_i18n_javascript() was called indicates that the methods in
931
			// here are needed.
932
			if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18n.js';
933
934
			if(substr($langDir,-1) != '/') $langDir .= '/';
935
936
			$candidates = array(
937
				'en.js',
938
				'en_US.js',
939
				i18n::get_lang_from_locale(i18n::default_locale()) . '.js',
0 ignored issues
show
Deprecated Code introduced by
The method i18n::default_locale() has been deprecated with message: since version 4.0; Use the "i18n.default_locale" config setting instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
940
				i18n::default_locale() . '.js',
0 ignored issues
show
Deprecated Code introduced by
The method i18n::default_locale() has been deprecated with message: since version 4.0; Use the "i18n.default_locale" config setting instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
941
				i18n::get_lang_from_locale(i18n::get_locale()) . '.js',
942
				i18n::get_locale() . '.js',
943
			);
944
			foreach($candidates as $candidate) {
945
				if(file_exists($base . DIRECTORY_SEPARATOR . $langDir . $candidate)) {
946
					$files[] = $langDir . $candidate;
947
				}
948
			}
949
		} else {
950
			// Stub i18n implementation for when i18n is disabled.
951
			if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18nx.js';
952
		}
953
954
		if($return) {
955
			return $files;
956
		} else {
957
			foreach($files as $file) $this->javascript($file);
958
		}
959
	}
960
961
	/**
962
	 * Finds the path for specified file
963
	 *
964
	 * @param string $fileOrUrl
965
	 * @return string|bool
966
	 */
967
	protected function path_for_file($fileOrUrl) {
968
		if(preg_match('{^//|http[s]?}', $fileOrUrl)) {
969
			return $fileOrUrl;
970
		} elseif(Director::fileExists($fileOrUrl)) {
971
			$filePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $fileOrUrl);
972
			$prefix = Director::baseURL();
973
			$mtimesuffix = "";
974
			$suffix = '';
975
			if($this->suffix_requirements) {
976
				$mtimesuffix = "?m=" . filemtime($filePath);
977
				$suffix = '&';
978
			}
979
			if(strpos($fileOrUrl, '?') !== false) {
980
				if (strlen($suffix) == 0) {
981
					$suffix = '?';
982
				}
983
				$suffix .= substr($fileOrUrl, strpos($fileOrUrl, '?')+1);
984
				$fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?'));
985
			} else {
986
				$suffix = '';
987
			}
988
			return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}";
989
		} else {
990
			return false;
991
		}
992
	}
993
994
	/**
995
	 * Concatenate several css or javascript files into a single dynamically generated file. This
996
	 * increases performance by fewer HTTP requests.
997
	 *
998
	 * The combined file is regenerated based on every file modification time. Optionally a
999
	 * rebuild can be triggered by appending ?flush=1 to the URL. If all files to be combined are
1000
	 * JavaScript, we use the external JSMin library to minify the JavaScript. This can be
1001
	 * controlled using {@link $combine_js_with_jsmin}.
1002
	 *
1003
	 * All combined files will have a comment on the start of each concatenated file denoting their
1004
	 * original position. For easier debugging, we only minify JavaScript if not in development
1005
	 * mode ({@link Director::isDev()}).
1006
	 *
1007
	 * CAUTION: You're responsible for ensuring that the load order for combined files is
1008
	 * retained - otherwise combining JavaScript files can lead to functional errors in the
1009
	 * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also
1010
	 * only include each file once across all includes and combinations in a single page load.
1011
	 *
1012
	 * CAUTION: Combining CSS Files discards any "media" information.
1013
	 *
1014
	 * Example for combined JavaScript:
1015
	 * <code>
1016
	 * Requirements::combine_files(
1017
	 *  'foobar.js',
1018
	 *  array(
1019
	 *        'mysite/javascript/foo.js',
1020
	 *        'mysite/javascript/bar.js',
1021
	 *    )
1022
	 * );
1023
	 * </code>
1024
	 *
1025
	 * Example for combined CSS:
1026
	 * <code>
1027
	 * Requirements::combine_files(
1028
	 *  'foobar.css',
1029
	 *    array(
1030
	 *        'mysite/javascript/foo.css',
1031
	 *        'mysite/javascript/bar.css',
1032
	 *    )
1033
	 * );
1034
	 * </code>
1035
	 *
1036
	 * @param string $combinedFileName Filename of the combined file relative to docroot
1037
	 * @param array  $files            Array of filenames relative to docroot
1038
	 * @param string $media
1039
	 *
1040
	 * @return bool|void
1041
	 */
1042
	public function combine_files($combinedFileName, $files, $media = null) {
1043
		// duplicate check
1044
		foreach($this->combine_files as $_combinedFileName => $_files) {
1045
			$duplicates = array_intersect($_files, $files);
1046
			if($duplicates && $combinedFileName != $_combinedFileName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $duplicates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1047
				user_error("Requirements_Backend::combine_files(): Already included files " . implode(',', $duplicates)
1048
					. " in combined file '{$_combinedFileName}'", E_USER_NOTICE);
1049
				return false;
1050
			}
1051
		}
1052
		foreach($files as $index=>$file) {
1053
			if(is_array($file)) {
1054
				// Either associative array path=>path type=>type or numeric 0=>path 1=>type
1055
				// Otherwise, assume path is the first item
1056
				if (isset($file['type']) && in_array($file['type'], array('css', 'javascript', 'js'))) {
1057 View Code Duplication
					switch ($file['type']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1058
						case 'css':
1059
							$this->css($file['path'], $media);
1060
							break;
1061
						default:
1062
							$this->javascript($file['path']);
1063
							break;
1064
					}
1065
					$files[$index] = $file['path'];
1066
				} elseif (isset($file[1]) && in_array($file[1], array('css', 'javascript', 'js'))) {
1067 View Code Duplication
					switch ($file[1]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1068
						case 'css':
1069
							$this->css($file[0], $media);
1070
							break;
1071
						default:
1072
							$this->javascript($file[0]);
1073
							break;
1074
					}
1075
					$files[$index] = $file[0];
1076
				} else {
1077
					$file = array_shift($file);
1078
				}
1079
			}
1080
			if (!is_array($file)) {
1081
				if(substr($file, -2) == 'js') {
1082
					$this->javascript($file);
1083
				} elseif(substr($file, -3) == 'css') {
1084
					$this->css($file, $media);
1085
				} else {
1086
					user_error("Requirements_Backend::combine_files(): Couldn't guess file type for file '$file', "
1087
						. "please specify by passing using an array instead.", E_USER_NOTICE);
1088
				}
1089
			}
1090
		}
1091
		$this->combine_files[$combinedFileName] = $files;
1092
	}
1093
1094
	/**
1095
	 * Return all combined files; keys are the combined file names, values are lists of
1096
	 * files being combined.
1097
	 *
1098
	 * @return array
1099
	 */
1100
	public function get_combine_files() {
1101
		return $this->combine_files;
1102
	}
1103
1104
	/**
1105
	 * Delete all dynamically generated combined files from the filesystem
1106
	 *
1107
	 * @param string $combinedFileName If left blank, all combined files are deleted.
1108
	 */
1109
	public function delete_combined_files($combinedFileName = null) {
1110
		$combinedFiles = ($combinedFileName) ? array($combinedFileName => null) : $this->combine_files;
1111
		$combinedFolder = ($this->getCombinedFilesFolder()) ?
1112
			(Director::baseFolder() . '/' . $this->combinedFilesFolder) : Director::baseFolder();
1113
		foreach($combinedFiles as $combinedFile => $sourceItems) {
1114
			$filePath = $combinedFolder . '/' . $combinedFile;
1115
			if(file_exists($filePath)) {
1116
				unlink($filePath);
1117
			}
1118
		}
1119
	}
1120
1121
	/**
1122
	 * Deletes all generated combined files in the configured combined files directory,
1123
	 * but doesn't delete the directory itself.
1124
	 */
1125
	public function delete_all_combined_files() {
1126
		$combinedFolder = $this->getCombinedFilesFolder();
1127
		if(!$combinedFolder) return false;
1128
1129
		$path = Director::baseFolder() . '/' . $combinedFolder;
1130
		if(file_exists($path)) {
1131
			Filesystem::removeFolder($path, true);
1132
		}
1133
	}
1134
1135
	/**
1136
	 * Clear all registered CSS and JavaScript file combinations
1137
	 */
1138
	public function clear_combined_files() {
1139
		$this->combine_files = array();
1140
	}
1141
1142
	/**
1143
	 * Do the heavy lifting involved in combining (and, in the case of JavaScript minifying) the
1144
	 * combined files.
1145
	 */
1146
	public function process_combined_files() {
1147
		// The class_exists call prevents us loading SapphireTest.php (slow) just to know that
1148
		// SapphireTest isn't running :-)
1149
		if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test();
1150
		else $runningTest = false;
1151
1152
		if((Director::isDev() && !$runningTest && !isset($_REQUEST['combine'])) || !$this->combined_files_enabled) {
1153
			return;
1154
		}
1155
1156
		// Make a map of files that could be potentially combined
1157
		$combinerCheck = array();
1158
		foreach($this->combine_files as $combinedFile => $sourceItems) {
1159
			foreach($sourceItems as $sourceItem) {
1160
				if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){
1161
					user_error("Requirements_Backend::process_combined_files - file '$sourceItem' appears in two " .
1162
						"combined files:" .	" '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING);
1163
				}
1164
				$combinerCheck[$sourceItem] = $combinedFile;
1165
1166
			}
1167
		}
1168
1169
		// Work out the relative URL for the combined files from the base folder
1170
		$combinedFilesFolder = ($this->getCombinedFilesFolder()) ? ($this->getCombinedFilesFolder() . '/') : '';
1171
1172
		// Figure out which ones apply to this request
1173
		$combinedFiles = array();
1174
		$newJSRequirements = array();
1175
		$newCSSRequirements = array();
1176
		foreach($this->javascript as $file => $dummy) {
1177
			if(isset($combinerCheck[$file])) {
1178
				$newJSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
1179
				$combinedFiles[$combinerCheck[$file]] = true;
1180
			} else {
1181
				$newJSRequirements[$file] = true;
1182
			}
1183
		}
1184
1185
		foreach($this->css as $file => $params) {
1186
			if(isset($combinerCheck[$file])) {
1187
				// Inherit the parameters from the last file in the combine set.
1188
				$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = $params;
1189
				$combinedFiles[$combinerCheck[$file]] = true;
1190
			} else {
1191
				$newCSSRequirements[$file] = $params;
1192
			}
1193
		}
1194
1195
		// Process the combined files
1196
		$base = Director::baseFolder() . '/';
1197
		foreach(array_diff_key($combinedFiles, $this->blocked) as $combinedFile => $dummy) {
1198
			$fileList = $this->combine_files[$combinedFile];
1199
			$combinedFilePath = $base . $combinedFilesFolder . '/' . $combinedFile;
1200
1201
1202
			// Make the folder if necessary
1203
			if(!file_exists(dirname($combinedFilePath))) {
1204
				Filesystem::makeFolder(dirname($combinedFilePath));
1205
			}
1206
1207
			// If the file isn't writeable, don't even bother trying to make the combined file and return. The
1208
			// files will be included individually instead. This is a complex test because is_writable fails
1209
			// if the file doesn't exist yet.
1210
			if((file_exists($combinedFilePath) && !is_writable($combinedFilePath))
1211
				|| (!file_exists($combinedFilePath) && !is_writable(dirname($combinedFilePath)))
1212
			) {
1213
				user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'",
1214
					E_USER_WARNING);
1215
				return false;
1216
			}
1217
1218
			// Determine if we need to build the combined include
1219
			if(file_exists($combinedFilePath)) {
1220
				// file exists, check modification date of every contained file
1221
				$srcLastMod = 0;
1222
				foreach($fileList as $file) {
1223
					if(file_exists($base . $file)) {
1224
						$srcLastMod = max(filemtime($base . $file), $srcLastMod);
1225
					}
1226
				}
1227
				$refresh = $srcLastMod > filemtime($combinedFilePath);
1228
			} else {
1229
				// File doesn't exist, or refresh was explicitly required
1230
				$refresh = true;
1231
			}
1232
1233
			if(!$refresh) continue;
1234
1235
			$failedToMinify = false;
1236
			$combinedData = "";
1237
			foreach(array_diff($fileList, $this->blocked) as $file) {
1238
				$fileContent = file_get_contents($base . $file);
1239
1240
				try{
1241
					$fileContent = $this->minifyFile($file, $fileContent);
1242
				}catch(Exception $e){
1243
					$failedToMinify = true;
1244
				}
1245
1246
				if ($this->write_header_comment) {
1247
					// Write a header comment for each file for easier identification and debugging. The semicolon between each file is required for jQuery to be combined properly and protects against unterminated statements.
1248
					$combinedData .= "/****** FILE: $file *****/\n";
1249
				}
1250
1251
				$combinedData .= $fileContent . "\n";
1252
			}
1253
1254
			$successfulWrite = false;
1255
			$fh = fopen($combinedFilePath, 'wb');
1256
			if($fh) {
1257
				if(fwrite($fh, $combinedData) == strlen($combinedData)) $successfulWrite = true;
1258
				fclose($fh);
1259
				unset($fh);
1260
			}
1261
1262
			if($failedToMinify){
1263
				// Failed to minify, use unminified files instead. This warning is raised at the end to allow code execution
1264
				// to complete in case this warning is caught inside a try-catch block.
1265
				user_error('Failed to minify '.$file.', exception: '.$e->getMessage(), E_USER_WARNING);
0 ignored issues
show
Bug introduced by
The variable $file does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $e does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1266
			}
1267
1268
			// Unsuccessful write - just include the regular JS files, rather than the combined one
1269
			if(!$successfulWrite) {
1270
				user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'",
1271
					E_USER_WARNING);
1272
				continue;
1273
			}
1274
		}
1275
1276
		// Note: Alters the original information, which means you can't call this method repeatedly - it will behave
1277
		// differently on the subsequent calls
1278
		$this->javascript = $newJSRequirements;
1279
		$this->css = $newCSSRequirements;
1280
	}
1281
1282
	/**
1283
	 * Minify the given $content according to the file type indicated in $filename
1284
	 *
1285
	 * @param string $filename
1286
	 * @param string $content
1287
	 * @return string
1288
	 */
1289
	protected function minifyFile($filename, $content) {
1290
		// if we have a javascript file and jsmin is enabled, minify the content
1291
		$isJS = stripos($filename, '.js');
1292
		if($isJS && $this->combine_js_with_jsmin) {
1293
			require_once('thirdparty/jsmin/jsmin.php');
1294
1295
			increase_time_limit_to();
1296
			$content = JSMin::minify($content);
1297
		}
1298
		$content .= ($isJS ? ';' : '') . "\n";
1299
		return $content;
1300
	}
1301
1302
	/**
1303
	 * Registers the given themeable stylesheet as required.
1304
	 *
1305
	 * A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
1306
	 * and it that doesn't exist and the module parameter is set then a CSS file with that name in
1307
	 * the module is used.
1308
	 *
1309
	 * @param string $name   The name of the file - eg '/css/File.css' would have the name 'File'
1310
	 * @param string $module The module to fall back to if the css file does not exist in the
1311
	 *                       current theme.
1312
	 * @param string $media  Comma-separated list of media types to use in the link tag
1313
	 *                       (e.g. 'screen,projector')
1314
	 */
1315
	public function themedCSS($name, $module = null, $media = null) {
1316
		$theme = SSViewer::get_theme_folder();
1317
		$project = project();
1318
		$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
1319
		$abstheme = $absbase . $theme;
1320
		$absproject = $absbase . $project;
1321
		$css = "/css/$name.css";
1322
1323
		if(file_exists($absproject . $css)) {
1324
			$this->css($project . $css, $media);
1325
		} elseif($module && file_exists($abstheme . '_' . $module.$css)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $module of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1326
			$this->css($theme . '_' . $module . $css, $media);
1327
		} elseif(file_exists($abstheme . $css)) {
1328
			$this->css($theme . $css, $media);
1329
		} elseif($module) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $module of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1330
			$this->css($module . $css, $media);
1331
		}
1332
	}
1333
1334
	/**
1335
	 * Output debugging information.
1336
	 */
1337
	public function debug() {
1338
		Debug::show($this->javascript);
1339
		Debug::show($this->css);
1340
		Debug::show($this->customCSS);
1341
		Debug::show($this->customScript);
1342
		Debug::show($this->customHeadTags);
1343
		Debug::show($this->combine_files);
1344
	}
1345
1346
}
1347