Completed
Push — master ( 18465e...962139 )
by Daniel
26s
created

Requirements_Backend::getWriteHeaderComment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
1657
					$newCSS = array(); // Assoc array of css file => spec
1658
					foreach($this->getAllCSS() as $css => $spec) {
1659
						if(!in_array($css, $fileList)) {
1660
							$newCSS[$css] = $spec;
1661
						} 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...
1662
							$newCSS[$combinedURL] = array('media' => (isset($options['media']) ? $options['media'] : null));
1663
							$included = true;
1664
						}
1665
						// If already included, or otherwise blocked, then don't add into CSS
1666
					}
1667
					$this->css = $newCSS;
1668
					break;
1669
				}
1670
				case 'js': {
0 ignored issues
show
Coding Style introduced by
case statements should 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.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

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