Completed
Push — member-groupset-delete ( a90a9a )
by Loz
11:22
created

Requirements_Backend::themedJavascript()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 5
nop 3
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
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
	 * Flag whether combined files should be deleted on flush.
13
	 *
14
	 * By default all combined files are deleted on flush. If combined files are stored in source control,
15
	 * and thus updated manually, you might want to turn this on to disable this behaviour.
16
	 *
17
	 * @config
18
	 * @var bool
19
	 */
20
	private static $disable_flush_combined = false;
21
22
	/**
23
	 * Triggered early in the request when a flush is requested
24
	 */
25
	public static function flush() {
26
		$disabled = Config::inst()->get(__CLASS__, 'disable_flush_combined');
27
		if(!$disabled) {
28
			self::delete_all_combined_files();
29
		}
30
	}
31
32
	/**
33
	 * Enable combining of css/javascript files.
34
	 *
35
	 * @param bool $enable
36
	 */
37
	public static function set_combined_files_enabled($enable) {
38
		self::backend()->set_combined_files_enabled($enable);
39
	}
40
41
	/**
42
	 * Checks whether combining of css/javascript files is enabled.
43
	 *
44
	 * @return bool
45
	 */
46
	public static function get_combined_files_enabled() {
47
		return self::backend()->get_combined_files_enabled();
48
	}
49
50
	/**
51
	 * Set the relative folder e.g. 'assets' for where to store combined files
52
	 *
53
	 * @param string $folder Path to folder
54
	 */
55
	public static function set_combined_files_folder($folder) {
56
		self::backend()->setCombinedFilesFolder($folder);
57
	}
58
59
	/**
60
	 * Set whether to add caching query params to the requests for file-based requirements.
61
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
62
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
63
	 * while automatically busting this cache every time the file is changed.
64
	 *
65
	 * @param bool
66
	 */
67
	public static function set_suffix_requirements($var) {
68
		self::backend()->set_suffix_requirements($var);
69
	}
70
71
	/**
72
	 * Check whether we want to suffix requirements
73
	 *
74
	 * @return bool
75
	 */
76
	public static function get_suffix_requirements() {
77
		return self::backend()->get_suffix_requirements();
78
	}
79
80
	/**
81
	 * Instance of the requirements for storage. You can create your own backend to change the
82
	 * default JS and CSS inclusion behaviour.
83
	 *
84
	 * @var Requirements_Backend
85
	 */
86
	private static $backend = null;
87
88
	public static function backend() {
89
		if(!self::$backend) {
90
			self::$backend = new Requirements_Backend();
91
		}
92
		return self::$backend;
93
	}
94
95
	/**
96
	 * Setter method for changing the Requirements backend
97
	 *
98
	 * @param Requirements_Backend $backend
99
	 */
100
	public static function set_backend(Requirements_Backend $backend) {
101
		self::$backend = $backend;
102
	}
103
104
	/**
105
	 * Register the given JavaScript file as required.
106
	 *
107
	 * @param string $file Relative to docroot
108
	 */
109
	public static function javascript($file) {
110
		self::backend()->javascript($file);
111
	}
112
113
	/**
114
	 * Register the given JavaScript code into the list of requirements
115
	 *
116
	 * @param string     $script       The script content as a string (without enclosing <script> tag)
117
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
118
	 */
119
	public static function customScript($script, $uniquenessID = null) {
120
		self::backend()->customScript($script, $uniquenessID);
121
	}
122
123
	/**
124
	 * Return all registered custom scripts
125
	 *
126
	 * @return array
127
	 */
128
	public static function get_custom_scripts() {
129
		return self::backend()->get_custom_scripts();
130
	}
131
132
	/**
133
	 * Register the given CSS styles into the list of requirements
134
	 *
135
	 * @param string     $script       CSS selectors as a string (without enclosing <style> tag)
136
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
137
	 */
138
	public static function customCSS($script, $uniquenessID = null) {
139
		self::backend()->customCSS($script, $uniquenessID);
140
	}
141
142
	/**
143
	 * Add the following custom HTML code to the <head> section of the page
144
	 *
145
	 * @param string     $html         Custom HTML code
146
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
147
	 */
148
	public static function insertHeadTags($html, $uniquenessID = null) {
149
		self::backend()->insertHeadTags($html, $uniquenessID);
150
	}
151
152
	/**
153
	 * Include the content of the given JavaScript file in the list of requirements. Dollar-sign
154
	 * variables will be interpolated with values from $vars similar to a .ss template.
155
	 *
156
	 * @param string         $file         The template file to load, relative to docroot
157
	 * @param string[]|int[] $vars         The array of variables to interpolate.
158
	 * @param string|int     $uniquenessID A unique ID that ensures a piece of code is only added once
159
	 */
160
	public static function javascriptTemplate($file, $vars, $uniquenessID = null) {
161
		self::backend()->javascriptTemplate($file, $vars, $uniquenessID);
162
	}
163
164
	/**
165
	 * Register the given stylesheet into the list of requirements.
166
	 *
167
	 * @param string $file  The CSS file to load, relative to site root
168
	 * @param string $media Comma-separated list of media types to use in the link tag
169
	 *                      (e.g. 'screen,projector')
170
	 */
171
	public static function css($file, $media = null) {
172
		self::backend()->css($file, $media);
173
	}
174
175
	/**
176
	 * Registers the given themeable stylesheet as required.
177
	 *
178
	 * A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
179
	 * and it that doesn't exist and the module parameter is set then a CSS file with that name in
180
	 * the module is used.
181
	 *
182
	 * @param string $name   The name of the file - eg '/css/File.css' would have the name 'File'
183
	 * @param string $module The module to fall back to if the css file does not exist in the
184
	 *                       current theme.
185
	 * @param string $media  Comma-separated list of media types to use in the link tag
186
	 *                       (e.g. 'screen,projector')
187
	 */
188
	public static function themedCSS($name, $module = null, $media = null) {
189
		return self::backend()->themedCSS($name, $module, $media);
190
	}
191
192
	/**
193
	 * Registers the given themeable javascript as required.
194
	 *
195
	 * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
196
	 * and it that doesn't exist and the module parameter is set then a javascript file with that name in
197
	 * the module is used.
198
	 *
199
	 * @param string $name   The name of the file - eg '/javascript/File.js' would have the name 'File'
200
	 * @param string $module The module to fall back to if the javascript file does not exist in the
201
	 *                       current theme.
202
	 * @param string $type  Comma-separated list of types to use in the script tag
203
	 *                       (e.g. 'text/javascript,text/ecmascript')
204
	 */
205
	public static function themedJavascript($name, $module = null, $type = null) {
206
		return self::backend()->themedJavascript($name, $module, $type);
207
	}
208
209
	/**
210
	 * Clear either a single or all requirements
211
	 *
212
	 * Caution: Clearing single rules added via customCSS and customScript only works if you
213
	 * originally specified a $uniquenessID.
214
	 *
215
	 * @param string|int $fileOrID
216
	 */
217
	public static function clear($fileOrID = null) {
218
		self::backend()->clear($fileOrID);
219
	}
220
221
	/**
222
	 * Restore requirements cleared by call to Requirements::clear
223
	 */
224
	public static function restore() {
225
		self::backend()->restore();
226
	}
227
228
	/**
229
	 * Block inclusion of a specific file
230
	 *
231
	 * The difference between this and {@link clear} is that the calling order does not matter;
232
	 * {@link clear} must be called after the initial registration, whereas {@link block} can be
233
	 * used in advance. This is useful, for example, to block scripts included by a superclass
234
	 * without having to override entire functions and duplicate a lot of code.
235
	 *
236
	 * Note that blocking should be used sparingly because it's hard to trace where an file is
237
	 * being blocked from.
238
	 *
239
	 * @param string|int $fileOrID
240
	 */
241
	public static function block($fileOrID) {
242
		self::backend()->block($fileOrID);
243
	}
244
245
	/**
246
	 * Remove an item from the block list
247
	 *
248
	 * @param string|int $fileOrID
249
	 */
250
	public static function unblock($fileOrID) {
251
		self::backend()->unblock($fileOrID);
252
	}
253
254
	/**
255
	 * Removes all items from the block list
256
	 */
257
	public static function unblock_all() {
258
		self::backend()->unblock_all();
259
	}
260
261
	/**
262
	 * Update the given HTML content with the appropriate include tags for the registered
263
	 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
264
	 * including a head and body tag.
265
	 *
266
	 * @param string $templateFile No longer used, only retained for compatibility
267
	 * @param string $content      HTML content that has already been parsed from the $templateFile
268
	 *                             through {@link SSViewer}
269
	 * @return string HTML content augmented with the requirements tags
270
	 */
271
	public static function includeInHTML($templateFile, $content) {
272
		return self::backend()->includeInHTML($templateFile, $content);
273
	}
274
275
	/**
276
	 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
277
	 * HTTP Response
278
	 *
279
	 * @param SS_HTTPResponse $response
280
	 */
281
	public static function include_in_response(SS_HTTPResponse $response) {
282
		return self::backend()->include_in_response($response);
283
	}
284
285
	/**
286
	 * Add i18n files from the given javascript directory. SilverStripe expects that the given
287
	 * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
288
	 * etc.
289
	 *
290
	 * @param string $langDir  The JavaScript lang directory, relative to the site root, e.g.,
291
	 *                         'framework/javascript/lang'
292
	 * @param bool   $return   Return all relative file paths rather than including them in
293
	 *                         requirements
294
	 * @param bool   $langOnly Only include language files, not the base libraries
295
	 *
296
	 * @return array
297
	 */
298
	public static function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
299
		return self::backend()->add_i18n_javascript($langDir, $return, $langOnly);
300
	}
301
302
	/**
303
	 * Concatenate several css or javascript files into a single dynamically generated file. This
304
	 * increases performance by fewer HTTP requests.
305
	 *
306
	 * The combined file is regenerated based on every file modification time. Optionally a
307
	 * rebuild can be triggered by appending ?flush=1 to the URL. If all files to be combined are
308
	 * JavaScript, we use the external JSMin library to minify the JavaScript.
309
	 *
310
	 * All combined files will have a comment on the start of each concatenated file denoting their
311
	 * original position. For easier debugging, we only minify JavaScript if not in development
312
	 * mode ({@link Director::isDev()}).
313
	 *
314
	 * CAUTION: You're responsible for ensuring that the load order for combined files is
315
	 * retained - otherwise combining JavaScript files can lead to functional errors in the
316
	 * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also
317
	 * only include each file once across all includes and comibinations in a single page load.
318
	 *
319
	 * CAUTION: Combining CSS Files discards any "media" information.
320
	 *
321
	 * Example for combined JavaScript:
322
	 * <code>
323
	 * Requirements::combine_files(
324
	 *  'foobar.js',
325
	 *  array(
326
	 *        'mysite/javascript/foo.js',
327
	 *        'mysite/javascript/bar.js',
328
	 *    )
329
	 * );
330
	 * </code>
331
	 *
332
	 * Example for combined CSS:
333
	 * <code>
334
	 * Requirements::combine_files(
335
	 *  'foobar.css',
336
	 *    array(
337
	 *        'mysite/javascript/foo.css',
338
	 *        'mysite/javascript/bar.css',
339
	 *    )
340
	 * );
341
	 * </code>
342
	 *
343
	 * @param string $combinedFileName Filename of the combined file relative to docroot
344
	 * @param array  $files            Array of filenames relative to docroot
345
	 * @param string $media
346
	 *
347
	 * @return bool|void
348
	 */
349
	public static function combine_files($combinedFileName, $files, $media = null) {
350
		self::backend()->combine_files($combinedFileName, $files, $media);
351
	}
352
353
	/**
354
	 * Return all combined files; keys are the combined file names, values are lists of
355
	 * files being combined.
356
	 *
357
	 * @return array
358
	 */
359
	public static function get_combine_files() {
360
		return self::backend()->get_combine_files();
361
	}
362
363
	/**
364
	 * Delete all dynamically generated combined files from the filesystem
365
	 *
366
	 * @param string $combinedFileName If left blank, all combined files are deleted.
367
	 */
368
	public static function delete_combined_files($combinedFileName = null) {
369
		return self::backend()->delete_combined_files($combinedFileName);
370
	}
371
372
	/**
373
	 * Deletes all generated combined files in the configured combined files directory,
374
	 * but doesn't delete the directory itself
375
	 */
376
	public static function delete_all_combined_files() {
377
		return self::backend()->delete_all_combined_files();
378
	}
379
380
	/**
381
	 * Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
382
	 */
383
	public static function clear_combined_files() {
384
		self::backend()->clear_combined_files();
385
	}
386
387
	/**
388
	 * Do the heavy lifting involved in combining (and, in the case of JavaScript minifying) the
389
	 * combined files.
390
 	 */
391
	public static function process_combined_files() {
392
		return self::backend()->process_combined_files();
393
	}
394
395
	/**
396
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
397
	 * head tag.
398
	 *
399
	 * @param bool
400
	 */
401
	public static function set_write_js_to_body($var) {
402
		self::backend()->set_write_js_to_body($var);
403
	}
404
405
	/**
406
	 * Set whether to force the JavaScript to end of the body. Useful if you use inline script tags
407
	 * that don't rely on scripts included via {@link Requirements::javascript()).
408
	 *
409
	 * @param boolean $var If true, force the JavaScript to be included at the bottom of the page
410
	 */
411
	public static function set_force_js_to_bottom($var) {
412
		self::backend()->set_force_js_to_bottom($var);
413
	}
414
415
	/**
416
	 * Output debugging information
417
	 */
418
	public static function debug() {
419
		return self::backend()->debug();
420
	}
421
422
}
423
424
/**
425
 * @package framework
426
 * @subpackage view
427
 */
428
class Requirements_Backend {
429
430
	/**
431
	 * Whether to add caching query params to the requests for file-based requirements.
432
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
433
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
434
	 * while automatically busting this cache every time the file is changed.
435
	 *
436
	 * @var bool
437
	 */
438
	protected $suffix_requirements = true;
439
440
	/**
441
	 * Whether to combine CSS and JavaScript files
442
	 *
443
	 * @var bool
444
	 */
445
	protected $combined_files_enabled = true;
446
447
	/**
448
	 * Paths to all required JavaScript files relative to docroot
449
	 *
450
	 * @var array $javascript
451
	 */
452
	protected $javascript = array();
453
454
	/**
455
	 * Paths to all required CSS files relative to the docroot.
456
	 *
457
	 * @var array $css
458
	 */
459
	protected $css = array();
460
461
	/**
462
	 * All custom javascript code that is inserted into the page's HTML
463
	 *
464
	 * @var array $customScript
465
	 */
466
	protected $customScript = array();
467
468
	/**
469
	 * All custom CSS rules which are inserted directly at the bottom of the HTML <head> tag
470
	 *
471
	 * @var array $customCSS
472
	 */
473
	protected $customCSS = array();
474
475
	/**
476
	 * All custom HTML markup which is added before the closing <head> tag, e.g. additional
477
	 * metatags.
478
	 */
479
	protected $customHeadTags = array();
480
481
	/**
482
	 * Remembers the file paths or uniquenessIDs of all Requirements cleared through
483
	 * {@link clear()}, so that they can be restored later.
484
	 *
485
	 * @var array $disabled
486
	 */
487
	protected $disabled = array();
488
489
	/**
490
	 * The file paths (relative to docroot) or uniquenessIDs of any included requirements which
491
	 * should be blocked when executing {@link inlcudeInHTML()}. This is useful, for example,
492
	 * to block scripts included by a superclass without having to override entire functions and
493
	 * duplicate a lot of code.
494
	 *
495
	 * Use {@link unblock()} or {@link unblock_all()} to revert changes.
496
	 *
497
	 * @var array $blocked
498
	 */
499
	protected $blocked = array();
500
501
	/**
502
	 * A list of combined files registered via {@link combine_files()}. Keys are the output file
503
	 * names, values are lists of input files.
504
	 *
505
	 * @var array $combine_files
506
	 */
507
	public $combine_files = array();
508
509
	/**
510
	 * Use the JSMin library to minify any javascript file passed to {@link combine_files()}.
511
	 *
512
	 * @var bool
513
	 */
514
	public $combine_js_with_jsmin = true;
515
516
	/**
517
	 * Whether or not file headers should be written when combining files
518
	 *
519
	 * @var boolean
520
	 */
521
	public $write_header_comment = true;
522
523
	/**
524
	 * Where to save combined files. By default they're placed in assets/_combinedfiles, however
525
	 * this may be an issue depending on your setup, especially for CSS files which often contain
526
	 * relative paths.
527
	 *
528
	 * @var string
529
	 */
530
	protected $combinedFilesFolder = null;
531
532
	/**
533
	 * Put all JavaScript includes at the bottom of the template before the closing <body> tag,
534
	 * rather than the default behaviour of placing them at the end of the <head> tag. This means
535
	 * script downloads won't block other HTTP requests, which can be a performance improvement.
536
	 *
537
	 * @var bool
538
	 */
539
	public $write_js_to_body = true;
540
541
	/**
542
	 * Force the JavaScript to the bottom of the page, even if there's a script tag in the body already
543
	 *
544
	 * @var boolean
545
	 */
546
	protected $force_js_to_bottom = false;
547
548
	/**
549
	 * Enable or disable the combination of CSS and JavaScript files
550
	 *
551
	 * @param $enable
552
	 */
553
	public function set_combined_files_enabled($enable) {
554
		$this->combined_files_enabled = (bool) $enable;
555
	}
556
557
	/**
558
	 * Check whether file combination is enabled.
559
	 *
560
	 * @return bool
561
	 */
562
	public function get_combined_files_enabled() {
563
		return $this->combined_files_enabled;
564
	}
565
566
	/**
567
	 * Set the folder to save combined files in. By default they're placed in assets/_combinedfiles,
568
	 * however this may be an issue depending on your setup, especially for CSS files which often
569
	 * contain relative paths.
570
	 *
571
	 * @param string $folder
572
	 */
573
	public function setCombinedFilesFolder($folder) {
574
		$this->combinedFilesFolder = $folder;
575
	}
576
577
	/**
578
	 * @return string Folder relative to the webroot
579
	 */
580
	public function getCombinedFilesFolder() {
581
		return ($this->combinedFilesFolder) ? $this->combinedFilesFolder : ASSETS_DIR . '/_combinedfiles';
582
	}
583
584
	/**
585
	 * Set whether to add caching query params to the requests for file-based requirements.
586
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
587
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
588
	 * while automatically busting this cache every time the file is changed.
589
	 *
590
	 * @param bool
591
	 */
592
	public function set_suffix_requirements($var) {
593
		$this->suffix_requirements = $var;
594
	}
595
596
	/**
597
	 * Check whether we want to suffix requirements
598
	 *
599
	 * @return bool
600
	 */
601
	public function get_suffix_requirements() {
602
		return $this->suffix_requirements;
603
	}
604
605
	/**
606
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
607
	 * head tag.
608
	 *
609
	 * @param bool
610
	 */
611
	public function set_write_js_to_body($var) {
612
		$this->write_js_to_body = $var;
613
	}
614
615
	/**
616
	 * Forces the JavaScript requirements to the end of the body, right before the closing tag
617
	 *
618
	 * @param bool
619
	 */
620
	public function set_force_js_to_bottom($var) {
621
		$this->force_js_to_bottom = $var;
622
	}
623
624
	/**
625
	 * Register the given JavaScript file as required.
626
	 *
627
	 * @param string $file Relative to docroot
628
	 */
629
	public function javascript($file) {
630
		$this->javascript[$file] = true;
631
	}
632
633
	/**
634
	 * Returns an array of all required JavaScript
635
	 *
636
	 * @return array
637
	 */
638
	public function get_javascript() {
639
		return array_keys(array_diff_key($this->javascript, $this->blocked));
640
	}
641
642
	/**
643
	 * Register the given JavaScript code into the list of requirements
644
	 *
645
	 * @param string     $script       The script content as a string (without enclosing <script> tag)
646
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
647
	 */
648
	public function customScript($script, $uniquenessID = null) {
649
		if($uniquenessID) $this->customScript[$uniquenessID] = $script;
650
		else $this->customScript[] = $script;
651
652
		$script .= "\n";
653
	}
654
655
	/**
656
	 * Return all registered custom scripts
657
	 *
658
	 * @return array
659
	 */
660
	public function get_custom_scripts() {
661
		$requirements = "";
662
663
		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...
664
			foreach($this->customScript as $script) {
665
				$requirements .= "$script\n";
666
			}
667
		}
668
669
		return $requirements;
670
	}
671
672
	/**
673
	 * Register the given CSS styles into the list of requirements
674
	 *
675
	 * @param string     $script       CSS selectors as a string (without enclosing <style> tag)
676
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
677
	 */
678
	public function customCSS($script, $uniquenessID = null) {
679
		if($uniquenessID) $this->customCSS[$uniquenessID] = $script;
680
		else $this->customCSS[] = $script;
681
	}
682
683
	/**
684
	 * Add the following custom HTML code to the <head> section of the page
685
	 *
686
	 * @param string     $html         Custom HTML code
687
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
688
	 */
689
	public function insertHeadTags($html, $uniquenessID = null) {
690
		if($uniquenessID) $this->customHeadTags[$uniquenessID] = $html;
691
		else $this->customHeadTags[] = $html;
692
	}
693
694
	/**
695
	 * Include the content of the given JavaScript file in the list of requirements. Dollar-sign
696
	 * variables will be interpolated with values from $vars similar to a .ss template.
697
	 *
698
	 * @param string         $file         The template file to load, relative to docroot
699
	 * @param string[]|int[] $vars         The array of variables to interpolate.
700
	 * @param string|int     $uniquenessID A unique ID that ensures a piece of code is only added once
701
	 */
702
	public function javascriptTemplate($file, $vars, $uniquenessID = null) {
703
		$script = file_get_contents(Director::getAbsFile($file));
704
		$search = array();
705
		$replace = array();
706
707
		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...
708
			$search[] = '$' . $k;
709
			$replace[] = str_replace("\\'","'", Convert::raw2js($v));
710
		}
711
712
		$script = str_replace($search, $replace, $script);
713
		$this->customScript($script, $uniquenessID);
714
	}
715
716
	/**
717
	 * Register the given stylesheet into the list of requirements.
718
	 *
719
	 * @param string $file  The CSS file to load, relative to site root
720
	 * @param string $media Comma-separated list of media types to use in the link tag
721
	 *                      (e.g. 'screen,projector')
722
	 */
723
	public function css($file, $media = null) {
724
		$this->css[$file] = array(
725
			"media" => $media
726
		);
727
	}
728
729
	/**
730
	 * Get the list of registered CSS file requirements, excluding blocked files
731
	 *
732
	 * @return array
733
	 */
734
	public function get_css() {
735
		return array_diff_key($this->css, $this->blocked);
736
	}
737
738
	/**
739
	 * Clear either a single or all requirements
740
	 *
741
	 * Caution: Clearing single rules added via customCSS and customScript only works if you
742
	 * originally specified a $uniquenessID.
743
	 *
744
	 * @param string|int $fileOrID
745
	 */
746
	public function clear($fileOrID = null) {
747
		if($fileOrID) {
748
			foreach(array('javascript','css', 'customScript', 'customCSS', 'customHeadTags') as $type) {
749
				if(isset($this->{$type}[$fileOrID])) {
750
					$this->disabled[$type][$fileOrID] = $this->{$type}[$fileOrID];
751
					unset($this->{$type}[$fileOrID]);
752
				}
753
			}
754
		} else {
755
			$this->disabled['javascript'] = $this->javascript;
756
			$this->disabled['css'] = $this->css;
757
			$this->disabled['customScript'] = $this->customScript;
758
			$this->disabled['customCSS'] = $this->customCSS;
759
			$this->disabled['customHeadTags'] = $this->customHeadTags;
760
761
			$this->javascript = array();
762
			$this->css = array();
763
			$this->customScript = array();
764
			$this->customCSS = array();
765
			$this->customHeadTags = array();
766
		}
767
	}
768
769
	/**
770
	 * Restore requirements cleared by call to Requirements::clear
771
	 */
772
	public function restore() {
773
		$this->javascript = $this->disabled['javascript'];
774
		$this->css = $this->disabled['css'];
775
		$this->customScript = $this->disabled['customScript'];
776
		$this->customCSS = $this->disabled['customCSS'];
777
		$this->customHeadTags = $this->disabled['customHeadTags'];
778
	}
779
	/**
780
	 * Block inclusion of a specific file
781
	 *
782
	 * The difference between this and {@link clear} is that the calling order does not matter;
783
	 * {@link clear} must be called after the initial registration, whereas {@link block} can be
784
	 * used in advance. This is useful, for example, to block scripts included by a superclass
785
	 * without having to override entire functions and duplicate a lot of code.
786
	 *
787
	 * Note that blocking should be used sparingly because it's hard to trace where an file is
788
	 * being blocked from.
789
	 *
790
	 * @param string|int $fileOrID
791
	 */
792
	public function block($fileOrID) {
793
		$this->blocked[$fileOrID] = $fileOrID;
794
	}
795
796
	/**
797
	 * Remove an item from the block list
798
	 *
799
	 * @param string|int $fileOrID
800
	 */
801
	public function unblock($fileOrID) {
802
		if(isset($this->blocked[$fileOrID])) unset($this->blocked[$fileOrID]);
803
	}
804
805
	/**
806
	 * Removes all items from the block list
807
	 */
808
	public function unblock_all() {
809
		$this->blocked = array();
810
	}
811
812
	/**
813
	 * Update the given HTML content with the appropriate include tags for the registered
814
	 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
815
	 * including a head and body tag.
816
	 *
817
	 * @param string $templateFile No longer used, only retained for compatibility
818
	 * @param string $content      HTML content that has already been parsed from the $templateFile
819
	 *                             through {@link SSViewer}
820
	 * @return string HTML content augmented with the requirements tags
821
	 */
822
	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...
823
		if(
824
			(strpos($content, '</head>') !== false || strpos($content, '</head ') !== false)
825
			&& ($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...
826
		) {
827
			$requirements = '';
828
			$jsRequirements = '';
829
830
			// Combine files - updates $this->javascript and $this->css
831
			$this->process_combined_files();
832
833
			foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) {
834
				$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...
835
				if($path) {
836
					$jsRequirements .= "<script type=\"text/javascript\" src=\"$path\"></script>\n";
837
				}
838
			}
839
840
			// Add all inline JavaScript *after* including external files they might rely on
841
			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...
842
				foreach(array_diff_key($this->customScript,$this->blocked) as $script) {
843
					$jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n";
844
					$jsRequirements .= "$script\n";
845
					$jsRequirements .= "\n//]]>\n</script>\n";
846
				}
847
			}
848
849
			foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
850
				$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...
851
				if($path) {
852
					$media = (isset($params['media']) && !empty($params['media']))
853
						? " media=\"{$params['media']}\"" : "";
854
					$requirements .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$path\" />\n";
855
				}
856
			}
857
858
			foreach(array_diff_key($this->customCSS, $this->blocked) as $css) {
859
				$requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
860
			}
861
862
			foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) {
863
				$requirements .= "$customHeadTag\n";
864
			}
865
866
			if ($this->force_js_to_bottom) {
867
				// Remove all newlines from code to preserve layout
868
				$jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements);
869
870
				// Forcefully put the scripts at the bottom of the body instead of before the first
871
				// script tag.
872
				$content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content);
873
874
				// Put CSS at the bottom of the head
875
				$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
876
			} elseif($this->write_js_to_body) {
877
				// Remove all newlines from code to preserve layout
878
				$jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements);
879
880
				// If your template already has script tags in the body, then we try to put our script
881
				// tags just before those. Otherwise, we put it at the bottom.
882
				$p2 = stripos($content, '<body');
883
				$p1 = stripos($content, '<script', $p2);
884
885
				$commentTags = array();
886
				$canWriteToBody = ($p1 !== false)
887
					&&
888
					// Check that the script tag is not inside a html comment tag
889
					!(
890
						preg_match('/.*(?|(<!--)|(-->))/U', $content, $commentTags, 0, $p1)
891
						&&
892
						$commentTags[1] == '-->'
893
					);
894
895
				if($canWriteToBody) {
896
					$content = substr($content,0,$p1) . $jsRequirements . substr($content,$p1);
897
				} else {
898
					$content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content);
899
				}
900
901
				// Put CSS at the bottom of the head
902
				$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
903
			} else {
904
				$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
905
				$content = preg_replace("/(<\/head>)/i", $jsRequirements . "\\1", $content);
906
			}
907
		}
908
909
		return $content;
910
	}
911
912
	/**
913
	 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
914
	 * HTTP Response
915
	 *
916
	 * @param SS_HTTPResponse $response
917
	 */
918
	public function include_in_response(SS_HTTPResponse $response) {
919
		$this->process_combined_files();
920
		$jsRequirements = array();
921
		$cssRequirements = array();
922
923
		foreach(array_diff_key($this->javascript, $this->blocked) as $file => $dummy) {
924
			$path = $this->path_for_file($file);
925
			if($path) {
926
				$jsRequirements[] = str_replace(',', '%2C', $path);
927
			}
928
		}
929
930
		$response->addHeader('X-Include-JS', implode(',', $jsRequirements));
931
932
		foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
933
			$path = $this->path_for_file($file);
934
			if($path) {
935
				$path = str_replace(',', '%2C', $path);
936
				$cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path;
937
			}
938
		}
939
940
		$response->addHeader('X-Include-CSS', implode(',', $cssRequirements));
941
	}
942
943
	/**
944
	 * Add i18n files from the given javascript directory. SilverStripe expects that the given
945
	 * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
946
	 * etc.
947
	 *
948
	 * @param string $langDir  The JavaScript lang directory, relative to the site root, e.g.,
949
	 *                         'framework/javascript/lang'
950
	 * @param bool   $return   Return all relative file paths rather than including them in
951
	 *                         requirements
952
	 * @param bool   $langOnly Only include language files, not the base libraries
953
	 *
954
	 * @return array
955
	 */
956
	public function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
957
		$files = array();
958
		$base = Director::baseFolder() . '/';
959
		if(i18n::config()->js_i18n) {
960
			// Include i18n.js even if no languages are found.  The fact that
961
			// add_i18n_javascript() was called indicates that the methods in
962
			// here are needed.
963
			if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18n.js';
964
965
			if(substr($langDir,-1) != '/') $langDir .= '/';
966
967
			$candidates = array(
968
				'en.js',
969
				'en_US.js',
970
				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...
971
				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...
972
				i18n::get_lang_from_locale(i18n::get_locale()) . '.js',
973
				i18n::get_locale() . '.js',
974
			);
975
			foreach($candidates as $candidate) {
976
				if(file_exists($base . DIRECTORY_SEPARATOR . $langDir . $candidate)) {
977
					$files[] = $langDir . $candidate;
978
				}
979
			}
980
		} else {
981
			// Stub i18n implementation for when i18n is disabled.
982
			if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18nx.js';
983
		}
984
985
		if($return) {
986
			return $files;
987
		} else {
988
			foreach($files as $file) $this->javascript($file);
989
		}
990
	}
991
992
	/**
993
	 * Finds the path for specified file
994
	 *
995
	 * @param string $fileOrUrl
996
	 * @return string|bool
997
	 */
998
	protected function path_for_file($fileOrUrl) {
999
		if(preg_match('{^//|http[s]?}', $fileOrUrl)) {
1000
			return $fileOrUrl;
1001
		} elseif(Director::fileExists($fileOrUrl)) {
1002
			$filePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $fileOrUrl);
1003
			$prefix = Director::baseURL();
1004
			$mtimesuffix = "";
1005
			$suffix = '';
1006
			if($this->suffix_requirements) {
1007
				$mtimesuffix = "?m=" . filemtime($filePath);
1008
				$suffix = '&';
1009
			}
1010
			if(strpos($fileOrUrl, '?') !== false) {
1011
				if (strlen($suffix) == 0) {
1012
					$suffix = '?';
1013
				}
1014
				$suffix .= substr($fileOrUrl, strpos($fileOrUrl, '?')+1);
1015
				$fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?'));
1016
			} else {
1017
				$suffix = '';
1018
			}
1019
			return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}";
1020
		} else {
1021
			return false;
1022
		}
1023
	}
1024
1025
	/**
1026
	 * Concatenate several css or javascript files into a single dynamically generated file. This
1027
	 * increases performance by fewer HTTP requests.
1028
	 *
1029
	 * The combined file is regenerated based on every file modification time. Optionally a
1030
	 * rebuild can be triggered by appending ?flush=1 to the URL. If all files to be combined are
1031
	 * JavaScript, we use the external JSMin library to minify the JavaScript. This can be
1032
	 * controlled using {@link $combine_js_with_jsmin}.
1033
	 *
1034
	 * All combined files will have a comment on the start of each concatenated file denoting their
1035
	 * original position. For easier debugging, we only minify JavaScript if not in development
1036
	 * mode ({@link Director::isDev()}).
1037
	 *
1038
	 * CAUTION: You're responsible for ensuring that the load order for combined files is
1039
	 * retained - otherwise combining JavaScript files can lead to functional errors in the
1040
	 * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also
1041
	 * only include each file once across all includes and combinations in a single page load.
1042
	 *
1043
	 * CAUTION: Combining CSS Files discards any "media" information.
1044
	 *
1045
	 * Example for combined JavaScript:
1046
	 * <code>
1047
	 * Requirements::combine_files(
1048
	 *  'foobar.js',
1049
	 *  array(
1050
	 *        'mysite/javascript/foo.js',
1051
	 *        'mysite/javascript/bar.js',
1052
	 *    )
1053
	 * );
1054
	 * </code>
1055
	 *
1056
	 * Example for combined CSS:
1057
	 * <code>
1058
	 * Requirements::combine_files(
1059
	 *  'foobar.css',
1060
	 *    array(
1061
	 *        'mysite/javascript/foo.css',
1062
	 *        'mysite/javascript/bar.css',
1063
	 *    )
1064
	 * );
1065
	 * </code>
1066
	 *
1067
	 * @param string $combinedFileName Filename of the combined file relative to docroot
1068
	 * @param array  $files            Array of filenames relative to docroot
1069
	 * @param string $media
1070
	 *
1071
	 * @return bool|void
1072
	 */
1073
	public function combine_files($combinedFileName, $files, $media = null) {
1074
		// duplicate check
1075
		foreach($this->combine_files as $_combinedFileName => $_files) {
1076
			$duplicates = array_intersect($_files, $files);
1077
			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...
1078
				user_error("Requirements_Backend::combine_files(): Already included files " . implode(',', $duplicates)
1079
					. " in combined file '{$_combinedFileName}'", E_USER_NOTICE);
1080
				return false;
1081
			}
1082
		}
1083
		foreach($files as $index=>$file) {
1084
			if(is_array($file)) {
1085
				// Either associative array path=>path type=>type or numeric 0=>path 1=>type
1086
				// Otherwise, assume path is the first item
1087
				if (isset($file['type']) && in_array($file['type'], array('css', 'javascript', 'js'))) {
1088
					switch ($file['type']) {
1089
						case 'css':
1090
							$this->css($file['path'], $media);
1091
							break;
1092
						default:
1093
							$this->javascript($file['path']);
1094
							break;
1095
					}
1096
					$files[$index] = $file['path'];
1097
				} elseif (isset($file[1]) && in_array($file[1], array('css', 'javascript', 'js'))) {
1098
					switch ($file[1]) {
1099
						case 'css':
1100
							$this->css($file[0], $media);
1101
							break;
1102
						default:
1103
							$this->javascript($file[0]);
1104
							break;
1105
					}
1106
					$files[$index] = $file[0];
1107
				} else {
1108
					$file = array_shift($file);
1109
				}
1110
			}
1111
			if (!is_array($file)) {
1112
				if(substr($file, -2) == 'js') {
1113
					$this->javascript($file);
1114
				} elseif(substr($file, -3) == 'css') {
1115
					$this->css($file, $media);
1116
				} else {
1117
					user_error("Requirements_Backend::combine_files(): Couldn't guess file type for file '$file', "
1118
						. "please specify by passing using an array instead.", E_USER_NOTICE);
1119
				}
1120
			}
1121
		}
1122
		$this->combine_files[$combinedFileName] = $files;
1123
	}
1124
1125
	/**
1126
	 * Return all combined files; keys are the combined file names, values are lists of
1127
	 * files being combined.
1128
	 *
1129
	 * @return array
1130
	 */
1131
	public function get_combine_files() {
1132
		return $this->combine_files;
1133
	}
1134
1135
	/**
1136
	 * Delete all dynamically generated combined files from the filesystem
1137
	 *
1138
	 * @param string $combinedFileName If left blank, all combined files are deleted.
1139
	 */
1140
	public function delete_combined_files($combinedFileName = null) {
1141
		$combinedFiles = ($combinedFileName) ? array($combinedFileName => null) : $this->combine_files;
1142
		$combinedFolder = ($this->getCombinedFilesFolder()) ?
1143
			(Director::baseFolder() . '/' . $this->combinedFilesFolder) : Director::baseFolder();
1144
		foreach($combinedFiles as $combinedFile => $sourceItems) {
1145
			$filePath = $combinedFolder . '/' . $combinedFile;
1146
			if(file_exists($filePath)) {
1147
				unlink($filePath);
1148
			}
1149
		}
1150
	}
1151
1152
	/**
1153
	 * Deletes all generated combined files in the configured combined files directory,
1154
	 * but doesn't delete the directory itself.
1155
	 */
1156
	public function delete_all_combined_files() {
1157
		$combinedFolder = $this->getCombinedFilesFolder();
1158
		if(!$combinedFolder) return false;
1159
1160
		$path = Director::baseFolder() . '/' . $combinedFolder;
1161
		if(file_exists($path)) {
1162
			Filesystem::removeFolder($path, true);
1163
		}
1164
	}
1165
1166
	/**
1167
	 * Clear all registered CSS and JavaScript file combinations
1168
	 */
1169
	public function clear_combined_files() {
1170
		$this->combine_files = array();
1171
	}
1172
1173
	/**
1174
	 * Do the heavy lifting involved in combining (and, in the case of JavaScript minifying) the
1175
	 * combined files.
1176
	 */
1177
	public function process_combined_files() {
0 ignored issues
show
Coding Style introduced by
process_combined_files uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1178
		// The class_exists call prevents us loading SapphireTest.php (slow) just to know that
1179
		// SapphireTest isn't running :-)
1180
		if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test();
1181
		else $runningTest = false;
1182
1183
		if((Director::isDev() && !$runningTest && !isset($_REQUEST['combine'])) || !$this->combined_files_enabled) {
1184
			return;
1185
		}
1186
1187
		// Make a map of files that could be potentially combined
1188
		$combinerCheck = array();
1189
		foreach($this->combine_files as $combinedFile => $sourceItems) {
1190
			foreach($sourceItems as $sourceItem) {
1191
				if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){
1192
					user_error("Requirements_Backend::process_combined_files - file '$sourceItem' appears in two " .
1193
						"combined files:" .	" '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING);
1194
				}
1195
				$combinerCheck[$sourceItem] = $combinedFile;
1196
1197
			}
1198
		}
1199
1200
		// Work out the relative URL for the combined files from the base folder
1201
		$combinedFilesFolder = ($this->getCombinedFilesFolder()) ? ($this->getCombinedFilesFolder() . '/') : '';
1202
1203
		// Figure out which ones apply to this request
1204
		$combinedFiles = array();
1205
		$newJSRequirements = array();
1206
		$newCSSRequirements = array();
1207
		foreach($this->javascript as $file => $dummy) {
1208
			if(isset($combinerCheck[$file])) {
1209
				$newJSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
1210
				$combinedFiles[$combinerCheck[$file]] = true;
1211
			} else {
1212
				$newJSRequirements[$file] = true;
1213
			}
1214
		}
1215
1216
		foreach($this->css as $file => $params) {
1217
			if(isset($combinerCheck[$file])) {
1218
				// Inherit the parameters from the last file in the combine set.
1219
				$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = $params;
1220
				$combinedFiles[$combinerCheck[$file]] = true;
1221
			} else {
1222
				$newCSSRequirements[$file] = $params;
1223
			}
1224
		}
1225
1226
		// Process the combined files
1227
		$base = Director::baseFolder() . '/';
1228
		foreach(array_diff_key($combinedFiles, $this->blocked) as $combinedFile => $dummy) {
1229
			$fileList = $this->combine_files[$combinedFile];
1230
			$combinedFilePath = $base . $combinedFilesFolder . '/' . $combinedFile;
1231
1232
1233
			// Make the folder if necessary
1234
			if(!file_exists(dirname($combinedFilePath))) {
1235
				Filesystem::makeFolder(dirname($combinedFilePath));
1236
			}
1237
1238
			// If the file isn't writeable, don't even bother trying to make the combined file and return. The
1239
			// files will be included individually instead. This is a complex test because is_writable fails
1240
			// if the file doesn't exist yet.
1241
			if((file_exists($combinedFilePath) && !is_writable($combinedFilePath))
1242
				|| (!file_exists($combinedFilePath) && !is_writable(dirname($combinedFilePath)))
1243
			) {
1244
				user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'",
1245
					E_USER_WARNING);
1246
				return false;
1247
			}
1248
1249
			// Determine if we need to build the combined include
1250
			if(file_exists($combinedFilePath)) {
1251
				// file exists, check modification date of every contained file
1252
				$srcLastMod = 0;
1253
				foreach($fileList as $file) {
1254
					if(file_exists($base . $file)) {
1255
						$srcLastMod = max(filemtime($base . $file), $srcLastMod);
1256
					}
1257
				}
1258
				$refresh = $srcLastMod > filemtime($combinedFilePath);
1259
			} else {
1260
				// File doesn't exist, or refresh was explicitly required
1261
				$refresh = true;
1262
			}
1263
1264
			if(!$refresh) continue;
1265
1266
			$failedToMinify = false;
1267
			$combinedData = "";
1268
			foreach(array_diff($fileList, $this->blocked) as $file) {
1269
				$fileContent = file_get_contents($base . $file);
1270
1271
				try{
1272
					$fileContent = $this->minifyFile($file, $fileContent);
1273
				}catch(Exception $e){
1274
					$failedToMinify = true;
1275
				}
1276
1277
				if ($this->write_header_comment) {
1278
					// 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.
1279
					$combinedData .= "/****** FILE: $file *****/\n";
1280
				}
1281
1282
				$combinedData .= $fileContent . "\n";
1283
			}
1284
1285
			$successfulWrite = false;
1286
			$fh = fopen($combinedFilePath, 'wb');
1287
			if($fh) {
1288
				if(fwrite($fh, $combinedData) == strlen($combinedData)) $successfulWrite = true;
1289
				fclose($fh);
1290
				unset($fh);
1291
			}
1292
1293
			if($failedToMinify){
1294
				// Failed to minify, use unminified files instead. This warning is raised at the end to allow code execution
1295
				// to complete in case this warning is caught inside a try-catch block.
1296
				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...
1297
			}
1298
1299
			// Unsuccessful write - just include the regular JS files, rather than the combined one
1300
			if(!$successfulWrite) {
1301
				user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'",
1302
					E_USER_WARNING);
1303
				continue;
1304
			}
1305
		}
1306
1307
		// Note: Alters the original information, which means you can't call this method repeatedly - it will behave
1308
		// differently on the subsequent calls
1309
		$this->javascript = $newJSRequirements;
1310
		$this->css = $newCSSRequirements;
1311
	}
1312
1313
	/**
1314
	 * Minify the given $content according to the file type indicated in $filename
1315
	 *
1316
	 * @param string $filename
1317
	 * @param string $content
1318
	 * @return string
1319
	 */
1320
	protected function minifyFile($filename, $content) {
1321
		// if we have a javascript file and jsmin is enabled, minify the content
1322
		$isJS = stripos($filename, '.js');
1323
		if($isJS && $this->combine_js_with_jsmin) {
1324
			require_once('thirdparty/jsmin/jsmin.php');
1325
1326
			increase_time_limit_to();
1327
			$content = JSMin::minify($content);
1328
		}
1329
		$content .= ($isJS ? ';' : '') . "\n";
1330
		return $content;
1331
	}
1332
1333
	/**
1334
	 * Registers the given themeable stylesheet as required.
1335
	 *
1336
	 * A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
1337
	 * and it that doesn't exist and the module parameter is set then a CSS file with that name in
1338
	 * the module is used.
1339
	 *
1340
	 * @param string $name   The name of the file - eg '/css/File.css' would have the name 'File'
1341
	 * @param string $module The module to fall back to if the css file does not exist in the
1342
	 *                       current theme.
1343
	 * @param string $media  Comma-separated list of media types to use in the link tag
1344
	 *                       (e.g. 'screen,projector')
1345
	 */
1346
	public function themedCSS($name, $module = null, $media = null) {
1347
		$theme = SSViewer::get_theme_folder();
1348
		$project = project();
1349
		$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
1350
		$abstheme = $absbase . $theme;
1351
		$absproject = $absbase . $project;
1352
		$css = "/css/$name.css";
1353
1354
		if(file_exists($absproject . $css)) {
1355
			$this->css($project . $css, $media);
1356
		} 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...
1357
			$this->css($theme . '_' . $module . $css, $media);
1358
		} elseif(file_exists($abstheme . $css)) {
1359
			$this->css($theme . $css, $media);
1360
		} 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...
1361
			$this->css($module . $css, $media);
1362
		} else {
1363
			throw new InvalidArgumentException("The css file doesn't exists. Please check if the file $name.css exists in any context or search for themedCSS references calling this file in your templates.");
1364
		}
1365
	}
1366
1367
	/**
1368
	 * Registers the given themeable javascript as required.
1369
	 *
1370
	 * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
1371
	 * and it that doesn't exist and the module parameter is set then a javascript file with that name in
1372
	 * the module is used.
1373
	 *
1374
	 * @param string $name   The name of the file - eg '/js/File.js' would have the name 'File'
1375
	 * @param string $module The module to fall back to if the javascript file does not exist in the
1376
	 *                       current theme.
1377
	 * @param string $type  Comma-separated list of types to use in the script tag
1378
	 *                       (e.g. 'text/javascript,text/ecmascript')
1379
	 */
1380
	public function themedJavascript($name, $module = null, $type = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $type 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...
1381
		$theme = SSViewer::get_theme_folder();
1382
		$project = project();
1383
		$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
1384
		$abstheme = $absbase . $theme;
1385
		$absproject = $absbase . $project;
1386
		$js = "/javascript/$name.js";
1387
1388
		if(file_exists($absproject . $js)) {
1389
			$this->javascript($project . $js);
1390
		} elseif($module && file_exists($abstheme . '_' . $module.$js)) {
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...
1391
			$this->javascript($theme . '_' . $module . $js);
1392
		} elseif(file_exists($abstheme . $js)) {
1393
			$this->javascript($theme . $js);
1394
		} 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...
1395
			$this->javascript($module . $js);
1396
		} else {
1397
			throw new InvalidArgumentException("The javascript file doesn't exists. Please check if the file $name.js exists in any context or search for themedJavascript references calling this file in your templates.");
1398
		}
1399
	}
1400
1401
	/**
1402
	 * Output debugging information.
1403
	 */
1404
	public function debug() {
1405
		Debug::show($this->javascript);
1406
		Debug::show($this->css);
1407
		Debug::show($this->customCSS);
1408
		Debug::show($this->customScript);
1409
		Debug::show($this->customHeadTags);
1410
		Debug::show($this->combine_files);
1411
	}
1412
1413
}
1414