Completed
Push — master ( f548dd...46b15a )
by Hamish
23s
created

Requirements_Backend::themedCSS()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 2
eloc 8
c 3
b 1
f 0
nc 2
nop 2
dl 0
loc 11
rs 9.4285
1
<?php
2
3
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
4
use SilverStripe\View\ThemeResourceLoader;
5
6
/**
7
 * Requirements tracker for JavaScript and CSS.
8
 *
9
 * @package framework
10
 * @subpackage view
11
 */
12
class Requirements implements Flushable {
13
14
	/**
15
	 * Flag whether combined files should be deleted on flush.
16
	 *
17
	 * By default all combined files are deleted on flush. If combined files are stored in source control,
18
	 * and thus updated manually, you might want to turn this on to disable this behaviour.
19
	 *
20
	 * @config
21
	 * @var bool
22
	 */
23
	private static $disable_flush_combined = false;
24
25
	/**
26
	 * Triggered early in the request when a flush is requested
27
	 */
28
	public static function flush() {
29
		$disabled = Config::inst()->get(static::class, 'disable_flush_combined');
30
		if(!$disabled) {
31
			self::delete_all_combined_files();
32
		}
33
	}
34
35
	/**
36
	 * Enable combining of css/javascript files.
37
	 *
38
	 * @param bool $enable
39
	 */
40
	public static function set_combined_files_enabled($enable) {
41
		self::backend()->setCombinedFilesEnabled($enable);
42
	}
43
44
	/**
45
	 * Checks whether combining of css/javascript files is enabled.
46
	 *
47
	 * @return bool
48
	 */
49
	public static function get_combined_files_enabled() {
50
		return self::backend()->getCombinedFilesEnabled();
51
	}
52
53
	/**
54
	 * Set the relative folder e.g. 'assets' for where to store combined files
55
	 *
56
	 * @param string $folder Path to folder
57
	 */
58
	public static function set_combined_files_folder($folder) {
59
		self::backend()->setCombinedFilesFolder($folder);
60
	}
61
62
	/**
63
	 * Set whether to add caching query params to the requests for file-based requirements.
64
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
65
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
66
	 * while automatically busting this cache every time the file is changed.
67
	 *
68
	 * @param bool
69
	 */
70
	public static function set_suffix_requirements($var) {
71
		self::backend()->setSuffixRequirements($var);
72
	}
73
74
	/**
75
	 * Check whether we want to suffix requirements
76
	 *
77
	 * @return bool
78
	 */
79
	public static function get_suffix_requirements() {
80
		return self::backend()->getSuffixRequirements();
81
	}
82
83
	/**
84
	 * Instance of the requirements for storage. You can create your own backend to change the
85
	 * default JS and CSS inclusion behaviour.
86
	 *
87
	 * @var Requirements_Backend
88
	 */
89
	private static $backend = null;
90
91
	/**
92
	 * @return Requirements_Backend
93
	 */
94
	public static function backend() {
95
		if(!self::$backend) {
96
			self::$backend = Injector::inst()->create('Requirements_Backend');
97
		}
98
		return self::$backend;
99
	}
100
101
	/**
102
	 * Setter method for changing the Requirements backend
103
	 *
104
	 * @param Requirements_Backend $backend
105
	 */
106
	public static function set_backend(Requirements_Backend $backend) {
107
		self::$backend = $backend;
108
	}
109
110
	/**
111
	 * Register the given JavaScript file as required.
112
	 *
113
	 * @param string $file Relative to docroot
114
	 * @param array $options List of options. Available options include:
115
	 * - 'provides' : List of scripts files included in this file
116
	 * - 'async' : Boolean value to set async attribute to script tag
117
	 * - 'defer' : Boolean value to set defer attribute to script tag
118
	 */
119
	public static function javascript($file, $options = array()) {
120
		self::backend()->javascript($file, $options);
121
	}
122
123
	/**
124
	 * Register the given JavaScript code into the list of requirements
125
	 *
126
	 * @param string     $script       The script content as a string (without enclosing <script> tag)
127
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
128
	 */
129
	public static function customScript($script, $uniquenessID = null) {
130
		self::backend()->customScript($script, $uniquenessID);
131
	}
132
133
	/**
134
	 * Return all registered custom scripts
135
	 *
136
	 * @return array
137
	 */
138
	public static function get_custom_scripts() {
139
		return self::backend()->getCustomScripts();
140
	}
141
142
	/**
143
	 * Register the given CSS styles into the list of requirements
144
	 *
145
	 * @param string     $script       CSS selectors as a string (without enclosing <style> tag)
146
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
147
	 */
148
	public static function customCSS($script, $uniquenessID = null) {
149
		self::backend()->customCSS($script, $uniquenessID);
150
	}
151
152
	/**
153
	 * Add the following custom HTML code to the <head> section of the page
154
	 *
155
	 * @param string     $html         Custom HTML code
156
	 * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
157
	 */
158
	public static function insertHeadTags($html, $uniquenessID = null) {
159
		self::backend()->insertHeadTags($html, $uniquenessID);
160
	}
161
162
	/**
163
	 * Include the content of the given JavaScript file in the list of requirements. Dollar-sign
164
	 * variables will be interpolated with values from $vars similar to a .ss template.
165
	 *
166
	 * @param string         $file         The template file to load, relative to docroot
167
	 * @param string[]|int[] $vars         The array of variables to interpolate.
168
	 * @param string|int     $uniquenessID A unique ID that ensures a piece of code is only added once
169
	 */
170
	public static function javascriptTemplate($file, $vars, $uniquenessID = null) {
171
		self::backend()->javascriptTemplate($file, $vars, $uniquenessID);
172
	}
173
174
	/**
175
	 * Register the given stylesheet into the list of requirements.
176
	 *
177
	 * @param string $file  The CSS file to load, relative to site root
178
	 * @param string $media Comma-separated list of media types to use in the link tag
179
	 *                      (e.g. 'screen,projector')
180
	 */
181
	public static function css($file, $media = null) {
182
		self::backend()->css($file, $media);
183
	}
184
185
	/**
186
	 * Registers the given themeable stylesheet as required.
187
	 *
188
	 * A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
189
	 * and it that doesn't exist and the module parameter is set then a CSS file with that name in
190
	 * the module is used.
191
	 *
192
	 * @param string $name   The name of the file - eg '/css/File.css' would have the name 'File'
193
	 * @param string $media  Comma-separated list of media types to use in the link tag
194
	 *                       (e.g. 'screen,projector')
195
	 */
196
	public static function themedCSS($name, $media = null) {
197
		self::backend()->themedCSS($name, $media);
198
	}
199
200
	/**
201
	 * Registers the given themeable javascript as required.
202
	 *
203
	 * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
204
	 * and it that doesn't exist and the module parameter is set then a javascript file with that name in
205
	 * the module is used.
206
	 *
207
	 * @param string $name   The name of the file - eg '/javascript/File.js' would have the name 'File'
208
	 * @param string $type  Comma-separated list of types to use in the script tag
209
	 *                       (e.g. 'text/javascript,text/ecmascript')
210
	 */
211
	public static function themedJavascript($name, $type = null) {
212
		return self::backend()->themedJavascript($name, $type);
213
	}
214
215
	/**
216
	 * Clear either a single or all requirements
217
	 *
218
	 * Caution: Clearing single rules added via customCSS and customScript only works if you
219
	 * originally specified a $uniquenessID.
220
	 *
221
	 * @param string|int $fileOrID
222
	 */
223
	public static function clear($fileOrID = null) {
224
		self::backend()->clear($fileOrID);
225
	}
226
227
	/**
228
	 * Restore requirements cleared by call to Requirements::clear
229
	 */
230
	public static function restore() {
231
		self::backend()->restore();
232
	}
233
234
	/**
235
	 * Block inclusion of a specific file
236
	 *
237
	 * The difference between this and {@link clear} is that the calling order does not matter;
238
	 * {@link clear} must be called after the initial registration, whereas {@link block} can be
239
	 * used in advance. This is useful, for example, to block scripts included by a superclass
240
	 * without having to override entire functions and duplicate a lot of code.
241
	 *
242
	 * Note that blocking should be used sparingly because it's hard to trace where an file is
243
	 * being blocked from.
244
	 *
245
	 * @param string|int $fileOrID
246
	 */
247
	public static function block($fileOrID) {
248
		self::backend()->block($fileOrID);
249
	}
250
251
	/**
252
	 * Remove an item from the block list
253
	 *
254
	 * @param string|int $fileOrID
255
	 */
256
	public static function unblock($fileOrID) {
257
		self::backend()->unblock($fileOrID);
258
	}
259
260
	/**
261
	 * Removes all items from the block list
262
	 */
263
	public static function unblock_all() {
264
		self::backend()->unblockAll();
265
	}
266
267
	/**
268
	 * Update the given HTML content with the appropriate include tags for the registered
269
	 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
270
	 * including a head and body tag.
271
	 *
272
	 * @param string $content      HTML content that has already been parsed from the $templateFile
273
	 *                             through {@link SSViewer}
274
	 * @return string HTML content augmented with the requirements tags
275
	 */
276
	public static function includeInHTML($content) {
277
		if(func_num_args() > 1) {
278
			Deprecation::notice(
279
				'5.0',
280
				'$templateFile argument is deprecated. includeInHTML takes a sole $content parameter now.'
281
			);
282
			$content = func_get_arg(1);
283
		}
284
285
		return self::backend()->includeInHTML($content);
286
	}
287
288
	/**
289
	 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
290
	 * HTTP Response
291
	 *
292
	 * @param SS_HTTPResponse $response
293
	 */
294
	public static function include_in_response(SS_HTTPResponse $response) {
295
		self::backend()->includeInResponse($response);
296
	}
297
298
	/**
299
	 * Add i18n files from the given javascript directory. SilverStripe expects that the given
300
	 * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
301
	 * etc.
302
	 *
303
	 * @param string $langDir  The JavaScript lang directory, relative to the site root, e.g.,
304
	 *                         'framework/javascript/lang'
305
	 * @param bool   $return   Return all relative file paths rather than including them in
306
	 *                         requirements
307
	 * @param bool   $langOnly Only include language files, not the base libraries
308
	 *
309
	 * @return array
310
	 */
311
	public static function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
312
		return self::backend()->add_i18n_javascript($langDir, $return, $langOnly);
313
	}
314
315
	/**
316
	 * Concatenate several css or javascript files into a single dynamically generated file. This
317
	 * increases performance by fewer HTTP requests.
318
	 *
319
	 * The combined file is regenerated based on every file modification time. Optionally a
320
	 * rebuild can be triggered by appending ?flush=1 to the URL.
321
	 *
322
	 * All combined files will have a comment on the start of each concatenated file denoting their
323
	 * original position.
324
	 *
325
	 * CAUTION: You're responsible for ensuring that the load order for combined files is
326
	 * retained - otherwise combining JavaScript files can lead to functional errors in the
327
	 * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also
328
	 * only include each file once across all includes and comibinations in a single page load.
329
	 *
330
	 * CAUTION: Combining CSS Files discards any "media" information.
331
	 *
332
	 * Example for combined JavaScript:
333
	 * <code>
334
	 * Requirements::combine_files(
335
	 *  'foobar.js',
336
	 *  array(
337
	 *        'mysite/javascript/foo.js',
338
	 *        'mysite/javascript/bar.js',
339
	 *    )
340
	 * );
341
	 * </code>
342
	 *
343
	 * Example for combined CSS:
344
	 * <code>
345
	 * Requirements::combine_files(
346
	 *  'foobar.css',
347
	 *    array(
348
	 *        'mysite/javascript/foo.css',
349
	 *        'mysite/javascript/bar.css',
350
	 *    )
351
	 * );
352
	 * </code>
353
	 *
354
	 * @param string $combinedFileName Filename of the combined file relative to docroot
355
	 * @param array  $files            Array of filenames relative to docroot
356
	 * @param array  $options          Array of options for combining files. Available options are:
357
	 * - 'media' : If including CSS Files, you can specify a media type
358
	 * - 'async' : If including JavaScript Files, boolean value to set async attribute to script tag
359
	 * - 'defer' : If including JavaScript Files, boolean value to set defer attribute to script tag
360
	 *
361
	 * @return bool|void
362
	 */
363
	public static function combine_files($combinedFileName, $files, $options = array()) {
364
        if(is_string($options))  {
365
            Deprecation::notice('4.0', 'Parameter media is deprecated. Use options array instead.');
366
            $options = array('media' => $options);
367
        }
368
		self::backend()->combineFiles($combinedFileName, $files, $options);
369
	}
370
371
	/**
372
	 * Return all combined files; keys are the combined file names, values are lists of
373
	 * associative arrays with 'files', 'type', and 'media' keys for details about this
374
	 * combined file.
375
	 *
376
	 * @return array
377
	 */
378
	public static function get_combine_files() {
379
		return self::backend()->getCombinedFiles();
380
	}
381
382
	/**
383
	 * Deletes all generated combined files in the configured combined files directory,
384
	 * but doesn't delete the directory itself
385
	 */
386
	public static function delete_all_combined_files() {
387
		self::backend()->deleteAllCombinedFiles();
388
	}
389
390
	/**
391
	 * Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
392
	 */
393
	public static function clear_combined_files() {
394
		self::backend()->clearCombinedFiles();
395
	}
396
397
	/**
398
	 * Do the heavy lifting involved in combining the combined files.
399
 	 */
400
	public static function process_combined_files() {
401
		self::backend()->processCombinedFiles();
402
	}
403
404
	/**
405
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
406
	 * head tag.
407
	 *
408
	 * @return bool
409
	 */
410
	public static function get_write_js_to_body() {
411
		return self::backend()->getWriteJavascriptToBody();
412
	}
413
414
	/**
415
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
416
	 * head tag.
417
	 *
418
	 * @param bool
419
	 */
420
	public static function set_write_js_to_body($var) {
421
		self::backend()->setWriteJavascriptToBody($var);
422
	}
423
424
	/**
425
	 * Get whether to force the JavaScript to end of the body. Useful if you use inline script tags
426
	 * that don't rely on scripts included via {@link Requirements::javascript()).
427
	 *
428
	 * @return bool
429
	 */
430
	public static function get_force_js_to_bottom() {
431
		return self::backend()->getForceJSToBottom();
432
	}
433
434
	/**
435
	 * Set whether to force the JavaScript to end of the body. Useful if you use inline script tags
436
	 * that don't rely on scripts included via {@link Requirements::javascript()).
437
	 *
438
	 * @param bool $var If true, force the JavaScript to be included at the bottom of the page
439
	 */
440
	public static function set_force_js_to_bottom($var) {
441
		self::backend()->setForceJSToBottom($var);
442
	}
443
444
	/**
445
	 * Check if JS minification is enabled
446
	 *
447
	 * @return bool
448
	 */
449
	public static function get_minify_combined_js_files() {
450
		return self::backend()->getMinifyCombinedJSFiles();
451
	}
452
453
	/**
454
	 * Enable or disable js minification
455
	 *
456
	 * @param bool $minify
457
	 */
458
	public static function set_minify_combined_js_files($minify) {
459
		self::backend()->setMinifyCombinedJSFiles($minify);
460
	}
461
462
	/**
463
	 * Check if header comments are written
464
	 *
465
	 * @return bool
466
	 */
467
	public static function get_write_header_comments() {
468
		return self::backend()->getWriteHeaderComment();
469
	}
470
471
	/**
472
	 * Flag whether header comments should be written for each combined file
473
	 *
474
	 * @param bool $write
475
	 */
476
	public function set_write_header_comments($write) {
477
		self::backend()->setWriteHeaderComment($write);
478
	}
479
480
481
	/**
482
	 * Output debugging information
483
	 */
484
	public static function debug() {
485
		self::backend()->debug();
486
	}
487
488
}
489
490
/**
491
 * @package framework
492
 * @subpackage view
493
 */
494
class Requirements_Backend
495
{
496
497
	/**
498
	 * Whether to add caching query params to the requests for file-based requirements.
499
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
500
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
501
	 * while automatically busting this cache every time the file is changed.
502
	 *
503
	 * @var bool
504
	 */
505
	protected $suffixRequirements = true;
506
507
	/**
508
	 * Whether to combine CSS and JavaScript files
509
	 *
510
	 * @var bool
511
	 */
512
	protected $combinedFilesEnabled = true;
513
514
	/**
515
	 * Determine if files should be combined automatically on dev mode.
516
	 *
517
	 * By default combined files will not be combined except in test or
518
	 * live environments. Turning this on will allow for pre-combining of files in development mode.
519
	 *
520
	 * @config
521
	 * @var bool
522
	 */
523
	private static $combine_in_dev = false;
524
525
	/**
526
	 * Paths to all required JavaScript files relative to docroot
527
	 *
528
	 * @var array
529
	 */
530
	protected $javascript = array();
531
532
	/**
533
	 * Map of included scripts to array of contained files.
534
	 * To be used alongside front-end combination mechanisms.
535
	 *
536
	 * @var array Map of providing filepath => array(provided filepaths)
537
	 */
538
	protected $providedJavascript = array();
539
540
	/**
541
	 * Paths to all required CSS files relative to the docroot.
542
	 *
543
	 * @var array
544
	 */
545
	protected $css = array();
546
547
	/**
548
	 * All custom javascript code that is inserted into the page's HTML
549
	 *
550
	 * @var array
551
	 */
552
	protected $customScript = array();
553
554
	/**
555
	 * All custom CSS rules which are inserted directly at the bottom of the HTML <head> tag
556
	 *
557
	 * @var array
558
	 */
559
	protected $customCSS = array();
560
561
	/**
562
	 * All custom HTML markup which is added before the closing <head> tag, e.g. additional
563
	 * metatags.
564
	 *
565
	 * @var array
566
	 */
567
	protected $customHeadTags = array();
568
569
	/**
570
	 * Remembers the file paths or uniquenessIDs of all Requirements cleared through
571
	 * {@link clear()}, so that they can be restored later.
572
	 *
573
	 * @var array
574
	 */
575
	protected $disabled = array();
576
577
	/**
578
	 * The file paths (relative to docroot) or uniquenessIDs of any included requirements which
579
	 * should be blocked when executing {@link inlcudeInHTML()}. This is useful, for example,
580
	 * to block scripts included by a superclass without having to override entire functions and
581
	 * duplicate a lot of code.
582
	 *
583
	 * Use {@link unblock()} or {@link unblock_all()} to revert changes.
584
	 *
585
	 * @var array
586
	 */
587
	protected $blocked = array();
588
589
	/**
590
	 * A list of combined files registered via {@link combine_files()}. Keys are the output file
591
	 * names, values are lists of input files.
592
	 *
593
	 * @var array
594
	 */
595
	protected $combinedFiles = array();
596
597
	/**
598
	 * Use the JSMin library to minify any javascript file passed to {@link combine_files()}.
599
	 *
600
	 * @var bool
601
	 */
602
	protected $minifyCombinedJSFiles = true;
603
604
	/**
605
	 * Whether or not file headers should be written when combining files
606
	 *
607
	 * @var boolean
608
	 */
609
	protected $writeHeaderComment = true;
610
611
	/**
612
	 * Where to save combined files. By default they're placed in assets/_combinedfiles, however
613
	 * this may be an issue depending on your setup, especially for CSS files which often contain
614
	 * relative paths.
615
	 *
616
	 * @var string
617
	 */
618
	protected $combinedFilesFolder = null;
619
620
	/**
621
	 * Put all JavaScript includes at the bottom of the template before the closing <body> tag,
622
	 * rather than the default behaviour of placing them at the end of the <head> tag. This means
623
	 * script downloads won't block other HTTP requests, which can be a performance improvement.
624
	 *
625
	 * @var bool
626
	 */
627
	public $writeJavascriptToBody = true;
628
629
	/**
630
	 * Force the JavaScript to the bottom of the page, even if there's a script tag in the body already
631
	 *
632
	 * @var boolean
633
	 */
634
	protected $forceJSToBottom = false;
635
636
	/**
637
	 * Configures the default prefix for combined files.
638
	 *
639
	 * This defaults to `_combinedfiles`, and is the folder within the configured asset backend that
640
	 * combined files will be stored in. If using a backend shared with other systems, it is usually
641
	 * necessary to distinguish combined files from other assets.
642
	 *
643
	 * @config
644
	 * @var string
645
	 */
646
	private static $default_combined_files_folder = '_combinedfiles';
647
648
	/**
649
	 * Flag to include the hash in the querystring instead of the filename for combined files.
650
	 *
651
	 * By default the `<hash>` of the source files is appended to the end of the combined file
652
	 * (prior to the file extension). If combined files are versioned in source control or running
653
	 * in a distributed environment (such as one where the newest version of a file may not always be
654
	 * immediately available) then it may sometimes be necessary to disable this. When this is set to true,
655
	 * the hash will instead be appended via a querystring parameter to enable cache busting, but not in
656
	 * the filename itself. I.e. `assets/_combinedfiles/name.js?m=<hash>`
657
	 *
658
	 * @config
659
	 * @var bool
660
	 */
661
	private static $combine_hash_querystring = false;
662
663
	/**
664
	 * @var GeneratedAssetHandler
665
	 */
666
	protected $assetHandler = null;
667
668
	/**
669
	 * Gets the backend storage for generated files
670
	 *
671
	 * @return GeneratedAssetHandler
672
	 */
673
	public function getAssetHandler() {
674
		return $this->assetHandler;
675
	}
676
677
	/**
678
	 * Set a new asset handler for this backend
679
	 *
680
	 * @param GeneratedAssetHandler $handler
681
	 */
682
	public function setAssetHandler(GeneratedAssetHandler $handler) {
683
		$this->assetHandler = $handler;
684
	}
685
686
	/**
687
	 * Enable or disable the combination of CSS and JavaScript files
688
	 *
689
	 * @param bool $enable
690
	 */
691
	public function setCombinedFilesEnabled($enable) {
692
		$this->combinedFilesEnabled = (bool) $enable;
693
	}
694
695
	/**
696
	 * Check if header comments are written
697
	 *
698
	 * @return bool
699
	 */
700
	public function getWriteHeaderComment() {
701
		return $this->writeHeaderComment;
702
	}
703
704
	/**
705
	 * Flag whether header comments should be written for each combined file
706
	 *
707
	 * @param bool $write
708
	 * @return $this
709
	 */
710
	public function setWriteHeaderComment($write) {
711
		$this->writeHeaderComment = $write;
712
		return $this;
713
	}
714
715
	/**
716
	 * Set the folder to save combined files in. By default they're placed in _combinedfiles,
717
	 * however this may be an issue depending on your setup, especially for CSS files which often
718
	 * contain relative paths.
719
	 *
720
	 * This must not include any 'assets' prefix
721
	 *
722
	 * @param string $folder
723
	 */
724
	public function setCombinedFilesFolder($folder) {
725
		$this->combinedFilesFolder = $folder;
726
	}
727
728
	/**
729
	 * Retrieve the combined files folder prefix
730
	 *
731
	 * @return string
732
	 */
733
	public function getCombinedFilesFolder() {
734
		if($this->combinedFilesFolder) {
735
			return $this->combinedFilesFolder;
736
		}
737
		return Config::inst()->get(__CLASS__, 'default_combined_files_folder');
738
	}
739
740
	/**
741
	 * Set whether to add caching query params to the requests for file-based requirements.
742
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
743
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
744
	 * while automatically busting this cache every time the file is changed.
745
	 *
746
	 * @param bool
747
	 */
748
	public function setSuffixRequirements($var) {
749
		$this->suffixRequirements = $var;
750
	}
751
752
	/**
753
	 * Check whether we want to suffix requirements
754
	 *
755
	 * @return bool
756
	 */
757
	public function getSuffixRequirements() {
758
		return $this->suffixRequirements;
759
	}
760
761
	/**
762
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
763
	 * head tag.
764
	 *
765
	 * @param bool
766
	 * @return $this
767
	 */
768
	public function setWriteJavascriptToBody($var) {
769
		$this->writeJavascriptToBody = $var;
770
		return $this;
771
	}
772
773
	/**
774
	 * Check whether you want to write the JS to the body of the page rather than at the end of the
775
	 * head tag.
776
	 *
777
	 * @return bool
778
	 */
779
	public function getWriteJavascriptToBody() {
780
		return $this->writeJavascriptToBody;
781
	}
782
783
	/**
784
	 * Forces the JavaScript requirements to the end of the body, right before the closing tag
785
	 *
786
	 * @param bool
787
	 * @return $this
788
	 */
789
	public function setForceJSToBottom($var) {
790
		$this->forceJSToBottom = $var;
791
		return $this;
792
	}
793
794
	/**
795
	 * Check if the JavaScript requirements are written to the end of the body, right before the closing tag
796
	 *
797
	 * @return bool
798
	 */
799
	public function getForceJSToBottom() {
800
		return $this->forceJSToBottom;
801
	}
802
803
	/**
804
	 * Check if minify js files should be combined
805
	 *
806
	 * @return bool
807
	 */
808
	public function getMinifyCombinedJSFiles() {
809
		return $this->minifyCombinedJSFiles;
810
	}
811
812
	/**
813
	 * Set if combined js files should be minified
814
	 *
815
	 * @param bool $minify
816
	 * @return $this
817
	 */
818
	public function setMinifyCombinedJSFiles($minify) {
819
		$this->minifyCombinedJSFiles = $minify;
820
		return $this;
821
	}
822
823
	/**
824
	 * Register the given JavaScript file as required.
825
	 *
826
	 * @param string $file Relative to docroot
827
	 * @param array $options List of options. Available options include:
828
	 * - 'provides' : List of scripts files included in this file
829
	 * - 'async' : Boolean value to set async attribute to script tag
830
	 * - 'defer' : Boolean value to set defer attribute to script tag
831
	 */
832
	public function javascript($file, $options = array()) {
833
	    // make sure that async/defer is set if it is set once even if file is included multiple times
834
        $async = (
835
            isset($options['async']) && isset($options['async']) == true
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
836
            || (
837
                isset($this->javascript[$file])
838
                && isset($this->javascript[$file]['async'])
839
                && $this->javascript[$file]['async'] == true
840
            )
841
        );
842
        $defer = (
843
            isset($options['defer']) && isset($options['defer']) == true
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
844
            || (
845
                isset($this->javascript[$file])
846
                && isset($this->javascript[$file]['defer'])
847
                && $this->javascript[$file]['defer'] == true
848
            )
849
        );
850
        $this->javascript[$file] = array(
851
            'async' => $async,
852
            'defer' => $defer,
853
        );
854
855
		// Record scripts included in this file
856
		if(isset($options['provides'])) {
857
			$this->providedJavascript[$file] = array_values($options['provides']);
858
	    }
859
860
	}
861
862
	/**
863
	 * Remove a javascript requirement
864
	 *
865
	 * @param string $file
866
	 */
867
	protected function unsetJavascript($file) {
868
		unset($this->javascript[$file]);
869
	}
870
871
	/**
872
	 * Gets all scripts that are already provided by prior scripts.
873
	 * This follows these rules:
874
	 *  - Files will not be considered provided if they are separately
875
	 *    included prior to the providing file.
876
	 *  - Providing files can be blocked, and don't provide anything
877
	 *  - Provided files can't be blocked (you need to block the provider)
878
	 *  - If a combined file includes files that are provided by prior
879
	 *    scripts, then these should be excluded from the combined file.
880
	 *  - If a combined file includes files that are provided by later
881
	 *    scripts, then these files should be included in the combined
882
	 *    file, but we can't block the later script either (possible double
883
	 *    up of file).
884
	 *
885
	 * @return array Array of provided files (map of $path => $path)
886
	 */
887
	public function getProvidedScripts() {
888
		$providedScripts = array();
889
		$includedScripts = array();
890
		foreach($this->javascript as $script => $options) {
891
			// Ignore scripts that are explicitly blocked
892
			if(isset($this->blocked[$script])) {
893
				continue;
894
			}
895
			// At this point, the file is included.
896
			// This might also be combined at this point, potentially.
897
			$includedScripts[$script] = true;
898
899
			// Record any files this provides, EXCEPT those already included by now
900
			if(isset($this->providedJavascript[$script])) {
901
				foreach($this->providedJavascript[$script] as $provided) {
902
					if(!isset($includedScripts[$provided])) {
903
						$providedScripts[$provided] = $provided;
904
					}
905
				}
906
			}
907
		}
908
		return $providedScripts;
909
	}
910
911
	/**
912
	 * Returns an array of required JavaScript, excluding blocked
913
	 * and duplicates of provided files.
914
	 *
915
	 * @return array
916
	 */
917
	public function getJavascript() {
918
		return array_diff_key(
919
			$this->javascript,
920
			$this->getBlocked(),
921
			$this->getProvidedScripts()
922
		);
923
	}
924
925
	/**
926
	 * Gets all javascript, including blocked files. Unwraps the array into a non-associative list
927
	 *
928
	 * @return array Indexed array of javascript files
929
	 */
930
	protected function getAllJavascript() {
931
		return $this->javascript;
932
	}
933
934
	/**
935
	 * Register the given JavaScript code into the list of requirements
936
	 *
937
	 * @param string     $script       The script content as a string (without enclosing <script> tag)
938
	 * @param string $uniquenessID A unique ID that ensures a piece of code is only added once
939
	 */
940
	public function customScript($script, $uniquenessID = null) {
941
		if($uniquenessID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uniquenessID 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...
942
			$this->customScript[$uniquenessID] = $script;
943
		} else {
944
			$this->customScript[] = $script;
945
		}
946
	}
947
948
	/**
949
	 * Return all registered custom scripts
950
	 *
951
	 * @return array
952
	 */
953
	public function getCustomScripts() {
954
		return array_diff_key($this->customScript, $this->blocked);
955
	}
956
957
	/**
958
	 * Register the given CSS styles into the list of requirements
959
	 *
960
	 * @param string     $script       CSS selectors as a string (without enclosing <style> tag)
961
	 * @param string $uniquenessID A unique ID that ensures a piece of code is only added once
962
	 */
963
	public function customCSS($script, $uniquenessID = null) {
964
		if($uniquenessID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uniquenessID 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...
965
			$this->customCSS[$uniquenessID] = $script;
966
		} else {
967
			$this->customCSS[] = $script;
968
		}
969
	}
970
971
	/**
972
	 * Return all registered custom CSS
973
	 *
974
	 * @return array
975
	 */
976
	public function getCustomCSS() {
977
		return array_diff_key($this->customCSS, $this->blocked);
978
	}
979
980
	/**
981
	 * Add the following custom HTML code to the <head> section of the page
982
	 *
983
	 * @param string     $html         Custom HTML code
984
	 * @param string $uniquenessID A unique ID that ensures a piece of code is only added once
985
	 */
986
	public function insertHeadTags($html, $uniquenessID = null) {
987
		if($uniquenessID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uniquenessID 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...
988
			$this->customHeadTags[$uniquenessID] = $html;
989
		} else {
990
			$this->customHeadTags[] = $html;
991
		}
992
	}
993
994
	/**
995
	 * Return all custom head tags
996
	 *
997
	 * @return array
998
	 */
999
	public function getCustomHeadTags() {
1000
		return array_diff_key($this->customHeadTags, $this->blocked);
1001
	}
1002
1003
	/**
1004
	 * Include the content of the given JavaScript file in the list of requirements. Dollar-sign
1005
	 * variables will be interpolated with values from $vars similar to a .ss template.
1006
	 *
1007
	 * @param string         $file         The template file to load, relative to docroot
1008
	 * @param string[] $vars The array of variables to interpolate.
1009
	 * @param string $uniquenessID A unique ID that ensures a piece of code is only added once
1010
	 */
1011
	public function javascriptTemplate($file, $vars, $uniquenessID = null) {
1012
		$script = file_get_contents(Director::getAbsFile($file));
1013
		$search = array();
1014
		$replace = array();
1015
1016
		if($vars) foreach($vars as $k => $v) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $vars of type string[] 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...
1017
			$search[] = '$' . $k;
1018
			$replace[] = str_replace("\\'","'", Convert::raw2js($v));
1019
		}
1020
1021
		$script = str_replace($search, $replace, $script);
1022
		$this->customScript($script, $uniquenessID);
1023
	}
1024
1025
	/**
1026
	 * Register the given stylesheet into the list of requirements.
1027
	 *
1028
	 * @param string $file  The CSS file to load, relative to site root
1029
	 * @param string $media Comma-separated list of media types to use in the link tag
1030
	 *                      (e.g. 'screen,projector')
1031
	 */
1032
	public function css($file, $media = null) {
1033
		$this->css[$file] = array(
1034
			"media" => $media
1035
		);
1036
	}
1037
1038
	/**
1039
	 * Remove a css requirement
1040
	 *
1041
	 * @param string $file
1042
	 */
1043
	protected function unsetCSS($file) {
1044
		unset($this->css[$file]);
1045
	}
1046
1047
	/**
1048
	 * Get the list of registered CSS file requirements, excluding blocked files
1049
	 *
1050
	 * @return array Associative array of file to spec
1051
	 */
1052
	public function getCSS() {
1053
		return array_diff_key($this->css, $this->blocked);
1054
	}
1055
1056
	/**
1057
	 * Gets all CSS files requirements, including blocked
1058
	 *
1059
	 * @return array Associative array of file to spec
1060
	 */
1061
	protected function getAllCSS() {
1062
		return $this->css;
1063
	}
1064
1065
	/**
1066
	 * Gets the list of all blocked files
1067
	 *
1068
	 * @return array
1069
	 */
1070
	public function getBlocked() {
1071
		return $this->blocked;
1072
	}
1073
1074
	/**
1075
	 * Clear either a single or all requirements
1076
	 *
1077
	 * Caution: Clearing single rules added via customCSS and customScript only works if you
1078
	 * originally specified a $uniquenessID.
1079
	 *
1080
	 * @param string|int $fileOrID
1081
	 */
1082
	public function clear($fileOrID = null) {
1083
		if($fileOrID) {
1084
			foreach(array('javascript','css', 'customScript', 'customCSS', 'customHeadTags') as $type) {
1085
				if(isset($this->{$type}[$fileOrID])) {
1086
					$this->disabled[$type][$fileOrID] = $this->{$type}[$fileOrID];
1087
					unset($this->{$type}[$fileOrID]);
1088
				}
1089
			}
1090
		} else {
1091
			$this->disabled['javascript'] = $this->javascript;
1092
			$this->disabled['css'] = $this->css;
1093
			$this->disabled['customScript'] = $this->customScript;
1094
			$this->disabled['customCSS'] = $this->customCSS;
1095
			$this->disabled['customHeadTags'] = $this->customHeadTags;
1096
1097
			$this->javascript = array();
1098
			$this->css = array();
1099
			$this->customScript = array();
1100
			$this->customCSS = array();
1101
			$this->customHeadTags = array();
1102
		}
1103
	}
1104
1105
	/**
1106
	 * Restore requirements cleared by call to Requirements::clear
1107
	 */
1108
	public function restore() {
1109
		$this->javascript = $this->disabled['javascript'];
1110
		$this->css = $this->disabled['css'];
1111
		$this->customScript = $this->disabled['customScript'];
1112
		$this->customCSS = $this->disabled['customCSS'];
1113
		$this->customHeadTags = $this->disabled['customHeadTags'];
1114
	}
1115
1116
	/**
1117
	 * Block inclusion of a specific file
1118
	 *
1119
	 * The difference between this and {@link clear} is that the calling order does not matter;
1120
	 * {@link clear} must be called after the initial registration, whereas {@link block} can be
1121
	 * used in advance. This is useful, for example, to block scripts included by a superclass
1122
	 * without having to override entire functions and duplicate a lot of code.
1123
	 *
1124
	 * Note that blocking should be used sparingly because it's hard to trace where an file is
1125
	 * being blocked from.
1126
	 *
1127
	 * @param string|int $fileOrID
1128
	 */
1129
	public function block($fileOrID) {
1130
		$this->blocked[$fileOrID] = $fileOrID;
1131
	}
1132
1133
	/**
1134
	 * Remove an item from the block list
1135
	 *
1136
	 * @param string|int $fileOrID
1137
	 */
1138
	public function unblock($fileOrID) {
1139
		unset($this->blocked[$fileOrID]);
1140
	}
1141
1142
	/**
1143
	 * Removes all items from the block list
1144
	 */
1145
	public function unblockAll() {
1146
		$this->blocked = array();
1147
	}
1148
1149
	/**
1150
	 * Update the given HTML content with the appropriate include tags for the registered
1151
	 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
1152
	 * including a head and body tag.
1153
	 *
1154
	 * @param string $content      HTML content that has already been parsed from the $templateFile
1155
	 *                             through {@link SSViewer}
1156
	 * @return string HTML content augmented with the requirements tags
1157
	 */
1158
	public function includeInHTML($content) {
1159
		if(func_num_args() > 1) {
1160
			Deprecation::notice(
1161
				'5.0',
1162
				'$templateFile argument is deprecated. includeInHTML takes a sole $content parameter now.'
1163
			);
1164
			$content = func_get_arg(1);
1165
		}
1166
1167
		// Skip if content isn't injectable, or there is nothing to inject
1168
		$tagsAvailable = preg_match('#</head\b#', $content);
1169
		$hasFiles = $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...
1170
		if(!$tagsAvailable || !$hasFiles) {
1171
			return $content;
1172
		}
1173
		$requirements = '';
1174
		$jsRequirements = '';
1175
1176
		// Combine files - updates $this->javascript and $this->css
1177
		$this->processCombinedFiles();
1178
1179
		foreach($this->getJavascript() as $file => $attributes) {
1180
		    $async = (isset($attributes['async']) && $attributes['async'] == true) ? " async" : "";
1181
		    $defer = (isset($attributes['defer']) && $attributes['defer'] == true) ? " defer" : "";
1182
			$path = Convert::raw2att($this->pathForFile($file));
0 ignored issues
show
Bug introduced by
It seems like $this->pathForFile($file) targeting Requirements_Backend::pathForFile() can also be of type boolean; however, Convert::raw2att() 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...
1183
			if($path) {
1184
				$jsRequirements .= "<script type=\"application/javascript\" src=\"$path\"{$async}{$defer}></script>";
1185
			}
1186
		}
1187
1188
		// Add all inline JavaScript *after* including external files they might rely on
1189
		foreach($this->getCustomScripts() as $script) {
1190
			$jsRequirements .= "<script type=\"application/javascript\">//<![CDATA[\n";
1191
			$jsRequirements .= "$script\n";
1192
			$jsRequirements .= "//]]></script>";
1193
		}
1194
1195
		foreach($this->getCSS() as $file => $params) {
1196
			$path = Convert::raw2att($this->pathForFile($file));
0 ignored issues
show
Bug introduced by
It seems like $this->pathForFile($file) targeting Requirements_Backend::pathForFile() can also be of type boolean; however, Convert::raw2att() 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...
1197
			if($path) {
1198
				$media = (isset($params['media']) && !empty($params['media']))
1199
					? " media=\"{$params['media']}\"" : "";
1200
				$requirements .= "<link rel=\"stylesheet\" type=\"text/css\" {$media} href=\"$path\" />\n";
1201
			}
1202
		}
1203
1204
		foreach($this->getCustomCSS() as $css) {
1205
			$requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
1206
		}
1207
1208
		foreach($this->getCustomHeadTags() as $customHeadTag) {
1209
			$requirements .= "$customHeadTag\n";
1210
		}
1211
1212
		// Inject CSS  into body
1213
		$content = $this->insertTagsIntoHead($requirements, $content);
1214
1215
		// Inject scripts
1216
		if ($this->getForceJSToBottom()) {
1217
			$content = $this->insertScriptsAtBottom($jsRequirements, $content);
1218
		} elseif($this->getWriteJavascriptToBody()) {
1219
			$content = $this->insertScriptsIntoBody($jsRequirements, $content);
1220
		} else {
1221
			$content = $this->insertTagsIntoHead($jsRequirements, $content);
1222
		}
1223
		return $content;
1224
	}
1225
1226
	/**
1227
	 * Given a block of HTML, insert the given scripts at the bottom before
1228
	 * the closing </body> tag
1229
	 *
1230
	 * @param string $jsRequirements String containing one or more javascript <script /> tags
1231
	 * @param string $content HTML body
1232
	 * @return string Merged HTML
1233
	 */
1234
	protected function insertScriptsAtBottom($jsRequirements, $content) {
1235
		// Forcefully put the scripts at the bottom of the body instead of before the first
1236
		// script tag.
1237
		$content = preg_replace(
1238
			'/(<\/body[^>]*>)/i',
1239
			$this->escapeReplacement($jsRequirements) . '\\1',
1240
			$content
1241
		);
1242
		return $content;
1243
	}
1244
1245
	/**
1246
	 * Given a block of HTML, insert the given scripts inside the <body></body>
1247
	 *
1248
	 * @param string $jsRequirements String containing one or more javascript <script /> tags
1249
	 * @param string $content HTML body
1250
	 * @return string Merged HTML
1251
	 */
1252
	protected function insertScriptsIntoBody($jsRequirements, $content) {
1253
		// If your template already has script tags in the body, then we try to put our script
1254
		// tags just before those. Otherwise, we put it at the bottom.
1255
		$bodyTagPosition = stripos($content, '<body');
1256
		$scriptTagPosition = stripos($content, '<script', $bodyTagPosition);
1257
1258
		$commentTags = array();
1259
		$canWriteToBody = ($scriptTagPosition !== false)
1260
			&&
1261
			// Check that the script tag is not inside a html comment tag
1262
			!(
1263
				preg_match('/.*(?|(<!--)|(-->))/U', $content, $commentTags, 0, $scriptTagPosition)
1264
				&&
1265
				$commentTags[1] == '-->'
1266
			);
1267
1268
		if($canWriteToBody) {
1269
			// Insert content before existing script tags
1270
			$content = substr($content, 0, $scriptTagPosition)
1271
				. $jsRequirements
1272
				. substr($content, $scriptTagPosition);
1273
		} else {
1274
			// Insert content at bottom of page otherwise
1275
			$content = $this->insertScriptsAtBottom($jsRequirements, $content);
1276
		}
1277
1278
		return $content;
1279
	}
1280
1281
	/**
1282
	 * Given a block of HTML, insert the given code inside the <head></head> block
1283
	 *
1284
	 * @param string $jsRequirements String containing one or more html tags
1285
	 * @param string $content HTML body
1286
	 * @return string Merged HTML
1287
	 */
1288
	protected function insertTagsIntoHead($jsRequirements, $content) {
1289
		$content = preg_replace(
1290
			'/(<\/head>)/i',
1291
			$this->escapeReplacement($jsRequirements) . '\\1',
1292
			$content
1293
		);
1294
		return $content;
1295
	}
1296
1297
	/**
1298
	 * Safely escape a literal string for use in preg_replace replacement
1299
	 *
1300
	 * @param string $replacement
1301
	 * @return string
1302
	 */
1303
	protected function escapeReplacement($replacement) {
1304
		return addcslashes($replacement, '\\$');
1305
	}
1306
1307
	/**
1308
	 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
1309
	 * HTTP Response
1310
	 *
1311
	 * @param SS_HTTPResponse $response
1312
	 */
1313
	public function includeInResponse(SS_HTTPResponse $response) {
1314
		$this->processCombinedFiles();
1315
		$jsRequirements = array();
1316
		$cssRequirements = array();
1317
1318
		foreach($this->getJavascript() as $file => $attributes) {
1319
			$path = $this->pathForFile($file);
1320
			if($path) {
1321
				$jsRequirements[] = str_replace(',', '%2C', $path);
1322
			}
1323
		}
1324
1325
		if(count($jsRequirements)) {
1326
			$response->addHeader('X-Include-JS', implode(',', $jsRequirements));
1327
		}
1328
1329
		foreach($this->getCSS() as $file => $params) {
1330
			$path = $this->pathForFile($file);
1331
			if($path) {
1332
				$path = str_replace(',', '%2C', $path);
1333
				$cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path;
1334
			}
1335
		}
1336
1337
		if(count($cssRequirements)) {
1338
			$response->addHeader('X-Include-CSS', implode(',', $cssRequirements));
1339
		}
1340
	}
1341
1342
	/**
1343
	 * Add i18n files from the given javascript directory. SilverStripe expects that the given
1344
	 * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
1345
	 * etc.
1346
	 *
1347
	 * @param string $langDir  The JavaScript lang directory, relative to the site root, e.g.,
1348
	 *                         'framework/javascript/lang'
1349
	 * @param bool   $return   Return all relative file paths rather than including them in
1350
	 *                         requirements
1351
	 * @param bool   $langOnly Only include language files, not the base libraries
1352
	 *
1353
	 * @return array|null All relative files if $return is true, or null otherwise
1354
	 */
1355
	public function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
1356
		$files = array();
1357
		$base = Director::baseFolder() . '/';
1358
		if(i18n::config()->js_i18n) {
1359
			// Include i18n.js even if no languages are found.  The fact that
1360
			// add_i18n_javascript() was called indicates that the methods in
1361
			// here are needed.
1362
			if(!$langOnly) $files[] = FRAMEWORK_DIR . '/client/dist/js/i18n.js';
1363
1364
			if(substr($langDir,-1) != '/') $langDir .= '/';
1365
1366
			$candidates = array(
1367
				'en.js',
1368
				'en_US.js',
1369
				i18n::get_lang_from_locale(i18n::config()->default_locale) . '.js',
1370
				i18n::config()->default_locale . '.js',
1371
				i18n::get_lang_from_locale(i18n::get_locale()) . '.js',
1372
				i18n::get_locale() . '.js',
1373
			);
1374
			foreach($candidates as $candidate) {
1375
				if(file_exists($base . DIRECTORY_SEPARATOR . $langDir . $candidate)) {
1376
					$files[] = $langDir . $candidate;
1377
				}
1378
			}
1379
		} else {
1380
			// Stub i18n implementation for when i18n is disabled.
1381
			if(!$langOnly) {
1382
				$files[] = FRAMEWORK_DIR . '/client/dist/js/i18nx.js';
1383
			}
1384
		}
1385
1386
		if($return) {
1387
			return $files;
1388
		} else {
1389
			foreach($files as $file) {
1390
				$this->javascript($file);
1391
			}
1392
			return null;
1393
		}
1394
	}
1395
1396
	/**
1397
	 * Finds the path for specified file
1398
	 *
1399
	 * @param string $fileOrUrl
1400
	 * @return string|bool
1401
	 */
1402
	protected function pathForFile($fileOrUrl) {
1403
		// Since combined urls could be root relative, treat them as urls here.
1404
		if(preg_match('{^(//)|(http[s]?:)}', $fileOrUrl) || Director::is_root_relative_url($fileOrUrl)) {
1405
			return $fileOrUrl;
1406
		} elseif(Director::fileExists($fileOrUrl)) {
1407
			$filePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $fileOrUrl);
1408
			$prefix = Director::baseURL();
1409
			$mtimesuffix = "";
1410
			$suffix = '';
1411
			if($this->getSuffixRequirements()) {
1412
				$mtimesuffix = "?m=" . filemtime($filePath);
1413
				$suffix = '&';
1414
			}
1415
			if(strpos($fileOrUrl, '?') !== false) {
1416
				if (strlen($suffix) == 0) {
1417
					$suffix = '?';
1418
				}
1419
				$suffix .= substr($fileOrUrl, strpos($fileOrUrl, '?')+1);
1420
				$fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?'));
1421
			} else {
1422
				$suffix = '';
1423
			}
1424
			return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}";
1425
		} else {
1426
			throw new InvalidArgumentException("File {$fileOrUrl} does not exist");
1427
		}
1428
	}
1429
1430
	/**
1431
	 * Concatenate several css or javascript files into a single dynamically generated file. This
1432
	 * increases performance by fewer HTTP requests.
1433
	 *
1434
	 * The combined file is regenerated based on every file modification time. Optionally a
1435
	 * rebuild can be triggered by appending ?flush=1 to the URL.
1436
	 *
1437
	 * All combined files will have a comment on the start of each concatenated file denoting their
1438
	 * original position.
1439
	 *
1440
	 * CAUTION: You're responsible for ensuring that the load order for combined files is
1441
	 * retained - otherwise combining JavaScript files can lead to functional errors in the
1442
	 * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also
1443
	 * only include each file once across all includes and combinations in a single page load.
1444
	 *
1445
	 * CAUTION: Combining CSS Files discards any "media" information.
1446
	 *
1447
	 * Example for combined JavaScript:
1448
	 * <code>
1449
	 * Requirements::combine_files(
1450
	 *    'foobar.js',
1451
	 *    array(
1452
	 *        'mysite/javascript/foo.js',
1453
	 *        'mysite/javascript/bar.js',
1454
	 *    ),
1455
	 *    array(
1456
	 *        'async' => true,
1457
	 *        'defer' => true,
1458
	 *    )
1459
	 * );
1460
	 * </code>
1461
	 *
1462
	 * Example for combined CSS:
1463
	 * <code>
1464
	 * Requirements::combine_files(
1465
	 *    'foobar.css',
1466
	 *    array(
1467
	 *        'mysite/javascript/foo.css',
1468
	 *        'mysite/javascript/bar.css',
1469
	 *    ),
1470
	 *    array(
1471
	 *        'media' => 'print',
1472
	 *    )
1473
	 * );
1474
	 * </code>
1475
	 *
1476
	 * @param string $combinedFileName Filename of the combined file relative to docroot
1477
	 * @param array  $files            Array of filenames relative to docroot
1478
	 * @param array  $options          Array of options for combining files. Available options are:
1479
	 * - 'media' : If including CSS Files, you can specify a media type
1480
	 * - 'async' : If including JavaScript Files, boolean value to set async attribute to script tag
1481
	 * - 'defer' : If including JavaScript Files, boolean value to set defer attribute to script tag
1482
	 */
1483
	public function combineFiles($combinedFileName, $files, $options = array()) {
1484
	    if(is_string($options))  {
1485
            Deprecation::notice('4.0', 'Parameter media is deprecated. Use options array instead.');
1486
            $options = array('media' => $options);
1487
	    }
1488
		// Skip this combined files if already included
1489
		if(isset($this->combinedFiles[$combinedFileName])) {
1490
			return;
1491
		}
1492
1493
		// Add all files to necessary type list
1494
		$paths = array();
1495
		$combinedType = null;
1496
		foreach($files as $file) {
1497
			// Get file details
1498
			list($path, $type) = $this->parseCombinedFile($file);
1499
			if($type === 'javascript') {
1500
				$type = 'js';
1501
		    }
1502
			if($combinedType && $type && $combinedType !== $type) {
1503
				throw new InvalidArgumentException(
1504
					"Cannot mix js and css files in same combined file {$combinedFileName}"
1505
				);
1506
			}
1507
			switch($type) {
1508
				case 'css':
1509
					$this->css($path, (isset($options['media']) ? $options['media'] : null));
1510
					break;
1511
				case 'js':
1512
					$this->javascript($path, $options);
1513
					break;
1514
				default:
1515
					throw new InvalidArgumentException("Invalid combined file type: {$type}");
1516
			}
1517
			$combinedType = $type;
1518
			$paths[] = $path;
1519
		}
1520
1521
		// Duplicate check
1522
		foreach($this->combinedFiles as $existingCombinedFilename => $combinedItem) {
1523
			$existingFiles = $combinedItem['files'];
1524
			$duplicates = array_intersect($existingFiles, $paths);
1525
			if($duplicates) {
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...
1526
				throw new InvalidArgumentException(sprintf(
1527
					"Requirements_Backend::combine_files(): Already included file(s) %s in combined file '%s'",
1528
					implode(',', $duplicates),
1529
					$existingCombinedFilename
1530
				));
1531
			}
1532
		}
1533
1534
		$this->combinedFiles[$combinedFileName] = array(
1535
			'files' => $paths,
1536
			'type' => $combinedType,
1537
			'options' => $options,
1538
		);
1539
	}
1540
1541
	/**
1542
	 * Return path and type of given combined file
1543
	 *
1544
	 * @param string|array $file Either a file path, or an array spec
1545
	 * @return array array with two elements, path and type of file
1546
	 */
1547
	protected function parseCombinedFile($file) {
1548
		// Array with path and type keys
1549
		if(is_array($file) && isset($file['path']) && isset($file['type'])) {
1550
			return array($file['path'], $file['type']);
1551
				}
1552
1553
		// Extract value from indexed array
1554
		if(is_array($file)) {
1555
			$path = array_shift($file);
1556
1557
			// See if there's a type specifier
1558
			if($file) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file 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...
1559
				$type = array_shift($file);
1560
				return array($path, $type);
1561
			}
1562
1563
			// Otherwise convent to string
1564
			$file = $path;
1565
		}
1566
1567
		$type = File::get_file_extension($file);
1568
		return array($file, $type);
1569
	}
1570
1571
	/**
1572
	 * Return all combined files; keys are the combined file names, values are lists of
1573
	 * associative arrays with 'files', 'type', and 'media' keys for details about this
1574
	 * combined file.
1575
	 *
1576
	 * @return array
1577
	 */
1578
	public function getCombinedFiles() {
1579
		return array_diff_key($this->combinedFiles, $this->blocked);
1580
	}
1581
1582
	/**
1583
	 * Includes all combined files, including blocked ones
1584
	 *
1585
	 * @return array
1586
	 */
1587
	protected function getAllCombinedFiles() {
1588
		return $this->combinedFiles;
1589
	}
1590
1591
	/**
1592
	 * Clears all combined files
1593
	 */
1594
	public function deleteAllCombinedFiles() {
1595
		$combinedFolder = $this->getCombinedFilesFolder();
1596
		if($combinedFolder) {
1597
			$this->getAssetHandler()->removeContent($combinedFolder);
1598
		}
1599
	}
1600
1601
	/**
1602
	 * Clear all registered CSS and JavaScript file combinations
1603
	 */
1604
	public function clearCombinedFiles() {
1605
		$this->combinedFiles = array();
1606
	}
1607
1608
	/**
1609
	 * Do the heavy lifting involved in combining the combined files.
1610
	 */
1611
	public function processCombinedFiles() {
1612
		// Check if combining is enabled
1613
		if(!$this->getCombinedFilesEnabled()) {
1614
			return;
1615
		}
1616
1617
		// Before scripts are modified, detect files that are provided by preceding ones
1618
		$providedScripts = $this->getProvidedScripts();
1619
1620
		// Process each combined files
1621
		foreach($this->getAllCombinedFiles() as $combinedFile => $combinedItem) {
1622
			$fileList = $combinedItem['files'];
1623
			$type = $combinedItem['type'];
1624
			$options = $combinedItem['options'];
1625
1626
			// Generate this file, unless blocked
1627
			$combinedURL = null;
1628
			if(!isset($this->blocked[$combinedFile])) {
1629
				// Filter files for blocked / provided
1630
				$filteredFileList = array_diff(
1631
					$fileList,
1632
					$this->getBlocked(),
1633
					$providedScripts
1634
				);
1635
				$combinedURL = $this->getCombinedFileURL($combinedFile, $filteredFileList, $type);
1636
			}
1637
1638
			// Replace all existing files, injecting the combined file at the position of the first item
1639
			// in order to preserve inclusion order.
1640
			// Note that we iterate across blocked files in order to get the correct order, and validate
1641
			// that the file is included in the correct location (regardless of which files are blocked).
1642
			$included = false;
1643
			switch($type) {
1644
				case 'css': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1645
					$newCSS = array(); // Assoc array of css file => spec
1646
					foreach($this->getAllCSS() as $css => $spec) {
1647
						if(!in_array($css, $fileList)) {
1648
							$newCSS[$css] = $spec;
1649
						} elseif(!$included && $combinedURL) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $combinedURL of type null|string 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...
1650
							$newCSS[$combinedURL] = array('media' => (isset($options['media']) ? $options['media'] : null));
1651
							$included = true;
1652
						}
1653
						// If already included, or otherwise blocked, then don't add into CSS
1654
					}
1655
					$this->css = $newCSS;
1656
					break;
1657
				}
1658
				case 'js': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1659
					// Assoc array of file => attributes
1660
					$newJS = array();
1661
					foreach($this->getAllJavascript() as $script => $attributes) {
1662
						if(!in_array($script, $fileList)) {
1663
							$newJS[$script] = $attributes;
1664
						} elseif(!$included && $combinedURL) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $combinedURL of type null|string 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...
1665
							$newJS[$combinedURL] = $options;
1666
							$included = true;
1667
						}
1668
						// If already included, or otherwise blocked, then don't add into scripts
1669
					}
1670
					$this->javascript = $newJS;
1671
					break;
1672
				}
1673
			}
1674
		}
1675
	}
1676
1677
	/**
1678
	 * Given a set of files, combine them (as necessary) and return the url
1679
	 *
1680
	 * @param string $combinedFile Filename for this combined file
1681
	 * @param array $fileList List of files to combine
1682
	 * @param string $type Either 'js' or 'css'
1683
	 * @return string|null URL to this resource, if there are files to combine
1684
	 */
1685
	protected function getCombinedFileURL($combinedFile, $fileList, $type) {
1686
		// Skip empty lists
1687
		if(empty($fileList)) {
1688
			return null;
1689
		}
1690
1691
		// Generate path (Filename)
1692
		$hashQuerystring = Config::inst()->get(static::class, 'combine_hash_querystring');
1693
		if(!$hashQuerystring) {
1694
			$combinedFile = $this->hashedCombinedFilename($combinedFile, $fileList);
1695
			}
1696
		$combinedFileID =  File::join_paths($this->getCombinedFilesFolder(), $combinedFile);
1697
1698
		// Send file combination request to the backend, with an optional callback to perform regeneration
1699
		$minify = $this->getMinifyCombinedJSFiles();
1700
		$combinedURL = $this
1701
			->getAssetHandler()
1702
			->getContentURL(
1703
				$combinedFileID,
1704
				function() use ($fileList, $minify, $type) {
1705
					// Physically combine all file content
1706
					$combinedData = '';
1707
					$base = Director::baseFolder() . '/';
1708
					$minifier = Injector::inst()->get('Requirements_Minifier');
1709
					foreach($fileList as $file) {
1710
						$fileContent = file_get_contents($base . $file);
1711
						// Use configured minifier
1712
						if($minify) {
1713
							$fileContent = $minifier->minify($fileContent, $type, $file);
1714
			}
1715
1716
						if ($this->writeHeaderComment) {
1717
							// Write a header comment for each file for easier identification and debugging.
1718
							$combinedData .= "/****** FILE: $file *****/\n";
1719
					}
1720
						$combinedData .= $fileContent . "\n";
1721
				}
1722
					return $combinedData;
1723
			}
1724
			);
1725
1726
		// If the name isn't hashed, we will need to append the querystring m= parameter instead
1727
		// Since url won't be automatically suffixed, add it in here
1728
		if($hashQuerystring && $this->getSuffixRequirements()) {
1729
			$hash = $this->hashOfFiles($fileList);
1730
			$q = stripos($combinedURL, '?') === false ? '?' : '&';
1731
			$combinedURL .= "{$q}m={$hash}";
1732
		}
1733
1734
		return $combinedURL;
1735
	}
1736
1737
	/**
1738
	 * Given a filename and list of files, generate a new filename unique to these files
1739
	 *
1740
	 * @param string $combinedFile
1741
	 * @param array $fileList
1742
	 * @return string
1743
	 */
1744
	protected function hashedCombinedFilename($combinedFile, $fileList) {
1745
		$name = pathinfo($combinedFile, PATHINFO_FILENAME);
1746
		$hash = $this->hashOfFiles($fileList);
1747
		$extension = File::get_file_extension($combinedFile);
1748
		return $name . '-' . substr($hash, 0, 7) . '.' . $extension;
1749
	}
1750
1751
	/**
1752
	 * Check if combined files are enabled
1753
	 *
1754
	 * @return bool
1755
	 */
1756
	public function getCombinedFilesEnabled() {
0 ignored issues
show
Coding Style introduced by
getCombinedFilesEnabled 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...
1757
		if(!$this->combinedFilesEnabled) {
1758
			return false;
1759
		}
1760
1761
		// Tests should be combined
1762
		if(class_exists('SapphireTest', false) && SapphireTest::is_running_test()) {
1763
			return true;
1764
		}
1765
1766
		// Check if specified via querystring
1767
		if(isset($_REQUEST['combine'])) {
1768
			return true;
1769
		}
1770
1771
		// Non-dev sites are always combined
1772
		if(!Director::isDev()) {
1773
			return true;
1774
		}
1775
1776
		// Fallback to default
1777
		return Config::inst()->get(__CLASS__, 'combine_in_dev');
1778
	}
1779
1780
	/**
1781
	 * For a given filelist, determine some discriminating value to determine if
1782
	 * any of these files have changed.
1783
	 *
1784
	 * @param array $fileList List of files
1785
	 * @return string SHA1 bashed file hash
1786
	 */
1787
	protected function hashOfFiles($fileList) {
1788
		// Get hash based on hash of each file
1789
		$base = Director::baseFolder() . '/';
1790
		$hash = '';
1791
		foreach($fileList as $file) {
1792
			if(file_exists($base . $file)) {
1793
				$hash .= sha1_file($base . $file);
1794
			} else {
1795
				throw new InvalidArgumentException("Combined file {$file} does not exist");
1796
			}
1797
		}
1798
		return sha1($hash);
1799
	}
1800
1801
	/**
1802
	 * Registers the given themeable stylesheet as required.
1803
	 *
1804
	 * A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
1805
	 * and it that doesn't exist and the module parameter is set then a CSS file with that name in
1806
	 * the module is used.
1807
	 *
1808
	 * @param string $name   The name of the file - eg '/css/File.css' would have the name 'File'
1809
	 * @param string $media  Comma-separated list of media types to use in the link tag
1810
	 *                       (e.g. 'screen,projector')
1811
	 */
1812
	public function themedCSS($name, $media = null) {
1813
		$path = ThemeResourceLoader::instance()->findThemedCSS($name, SSViewer::get_themes());
1814
		if($path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path 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...
1815
			$this->css($path, $media);
1816
		} else {
1817
			throw new \InvalidArgumentException(
1818
				"The css file doesn't exists. Please check if the file $name.css exists in any context or search for "
1819
				. "themedCSS references calling this file in your templates."
1820
			);
1821
		}
1822
	}
1823
1824
	/**
1825
	 * Registers the given themeable javascript as required.
1826
	 *
1827
	 * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
1828
	 * and it that doesn't exist and the module parameter is set then a javascript file with that name in
1829
	 * the module is used.
1830
	 *
1831
	 * @param string $name   The name of the file - eg '/js/File.js' would have the name 'File'
1832
	 * @param string $type  Comma-separated list of types to use in the script tag
1833
	 *                       (e.g. 'text/javascript,text/ecmascript')
1834
	 */
1835
	public function themedJavascript($name, $type = null) {
1836
        $path = ThemeResourceLoader::instance()->findThemedJavascript($name, SSViewer::get_themes());
1837
		if($path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path 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...
1838
			$opts = [];
1839
			if($type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type 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...
1840
				$opts['type'] = $type;
1841
			}
1842
			$this->javascript($path, $opts);
1843
		} else {
1844
			throw new \InvalidArgumentException(
1845
				"The javascript file doesn't exists. Please check if the file $name.js exists in any "
1846
				. "context or search for themedJavascript references calling this file in your templates."
1847
			);
1848
		}
1849
	}
1850
1851
	/**
1852
	 * Output debugging information.
1853
	 */
1854
	public function debug() {
1855
		Debug::show($this->javascript);
1856
		Debug::show($this->css);
1857
		Debug::show($this->customCSS);
1858
		Debug::show($this->customScript);
1859
		Debug::show($this->customHeadTags);
1860
		Debug::show($this->combinedFiles);
1861
	}
1862
1863
}
1864
1865
/**
1866
 * Provides an abstract interface for minifying content
1867
 */
1868
interface Requirements_Minifier {
1869
1870
	/**
1871
	 * Minify the given content
1872
	 *
1873
	 * @param string $content
1874
	 * @param string $type Either js or css
1875
	 * @param string $filename Name of file to display in case of error
1876
	 * @return string minified content
1877
	 */
1878
	public function minify($content, $type, $filename);
1879
}
1880