Completed
Push — master ( c7767b...f548dd )
by Daniel
11:31
created

Requirements::themedJavascript()   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
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 3
rs 10
1
<?php
2
3
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
4
use SilverStripe\View\TemplateLoader;
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 $module The module to fall back to if the css file does not exist in the
194
	 *                       current theme.
195
	 * @param string $media  Comma-separated list of media types to use in the link tag
196
	 *                       (e.g. 'screen,projector')
197
	 */
198
	public static function themedCSS($name, $module = null, $media = null) {
199
		self::backend()->themedCSS($name, $module, $media);
200
	}
201
202
	/**
203
	 * Registers the given themeable javascript as required.
204
	 *
205
	 * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
206
	 * and it that doesn't exist and the module parameter is set then a javascript file with that name in
207
	 * the module is used.
208
	 *
209
	 * @param string $name   The name of the file - eg '/javascript/File.js' would have the name 'File'
210
	 * @param string $module The module to fall back to if the javascript file does not exist in the
211
	 *                       current theme.
212
	 * @param string $type  Comma-separated list of types to use in the script tag
213
	 *                       (e.g. 'text/javascript,text/ecmascript')
214
	 */
215
	public static function themedJavascript($name, $module = null, $type = null) {
216
		return self::backend()->themedJavascript($name, $module, $type);
217
	}
218
219
	/**
220
	 * Clear either a single or all requirements
221
	 *
222
	 * Caution: Clearing single rules added via customCSS and customScript only works if you
223
	 * originally specified a $uniquenessID.
224
	 *
225
	 * @param string|int $fileOrID
226
	 */
227
	public static function clear($fileOrID = null) {
228
		self::backend()->clear($fileOrID);
229
	}
230
231
	/**
232
	 * Restore requirements cleared by call to Requirements::clear
233
	 */
234
	public static function restore() {
235
		self::backend()->restore();
236
	}
237
238
	/**
239
	 * Block inclusion of a specific file
240
	 *
241
	 * The difference between this and {@link clear} is that the calling order does not matter;
242
	 * {@link clear} must be called after the initial registration, whereas {@link block} can be
243
	 * used in advance. This is useful, for example, to block scripts included by a superclass
244
	 * without having to override entire functions and duplicate a lot of code.
245
	 *
246
	 * Note that blocking should be used sparingly because it's hard to trace where an file is
247
	 * being blocked from.
248
	 *
249
	 * @param string|int $fileOrID
250
	 */
251
	public static function block($fileOrID) {
252
		self::backend()->block($fileOrID);
253
	}
254
255
	/**
256
	 * Remove an item from the block list
257
	 *
258
	 * @param string|int $fileOrID
259
	 */
260
	public static function unblock($fileOrID) {
261
		self::backend()->unblock($fileOrID);
262
	}
263
264
	/**
265
	 * Removes all items from the block list
266
	 */
267
	public static function unblock_all() {
268
		self::backend()->unblockAll();
269
	}
270
271
	/**
272
	 * Update the given HTML content with the appropriate include tags for the registered
273
	 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
274
	 * including a head and body tag.
275
	 *
276
	 * @param string $content      HTML content that has already been parsed from the $templateFile
277
	 *                             through {@link SSViewer}
278
	 * @return string HTML content augmented with the requirements tags
279
	 */
280
	public static function includeInHTML($content) {
281
		if(func_num_args() > 1) {
282
			Deprecation::notice(
283
				'5.0',
284
				'$templateFile argument is deprecated. includeInHTML takes a sole $content parameter now.'
285
			);
286
			$content = func_get_arg(1);
287
		}
288
289
		return self::backend()->includeInHTML($content);
290
	}
291
292
	/**
293
	 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
294
	 * HTTP Response
295
	 *
296
	 * @param SS_HTTPResponse $response
297
	 */
298
	public static function include_in_response(SS_HTTPResponse $response) {
299
		self::backend()->includeInResponse($response);
300
	}
301
302
	/**
303
	 * Add i18n files from the given javascript directory. SilverStripe expects that the given
304
	 * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
305
	 * etc.
306
	 *
307
	 * @param string $langDir  The JavaScript lang directory, relative to the site root, e.g.,
308
	 *                         'framework/javascript/lang'
309
	 * @param bool   $return   Return all relative file paths rather than including them in
310
	 *                         requirements
311
	 * @param bool   $langOnly Only include language files, not the base libraries
312
	 *
313
	 * @return array
314
	 */
315
	public static function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
316
		return self::backend()->add_i18n_javascript($langDir, $return, $langOnly);
317
	}
318
319
	/**
320
	 * Concatenate several css or javascript files into a single dynamically generated file. This
321
	 * increases performance by fewer HTTP requests.
322
	 *
323
	 * The combined file is regenerated based on every file modification time. Optionally a
324
	 * rebuild can be triggered by appending ?flush=1 to the URL.
325
	 *
326
	 * All combined files will have a comment on the start of each concatenated file denoting their
327
	 * original position.
328
	 *
329
	 * CAUTION: You're responsible for ensuring that the load order for combined files is
330
	 * retained - otherwise combining JavaScript files can lead to functional errors in the
331
	 * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also
332
	 * only include each file once across all includes and comibinations in a single page load.
333
	 *
334
	 * CAUTION: Combining CSS Files discards any "media" information.
335
	 *
336
	 * Example for combined JavaScript:
337
	 * <code>
338
	 * Requirements::combine_files(
339
	 *  'foobar.js',
340
	 *  array(
341
	 *        'mysite/javascript/foo.js',
342
	 *        'mysite/javascript/bar.js',
343
	 *    )
344
	 * );
345
	 * </code>
346
	 *
347
	 * Example for combined CSS:
348
	 * <code>
349
	 * Requirements::combine_files(
350
	 *  'foobar.css',
351
	 *    array(
352
	 *        'mysite/javascript/foo.css',
353
	 *        'mysite/javascript/bar.css',
354
	 *    )
355
	 * );
356
	 * </code>
357
	 *
358
	 * @param string $combinedFileName Filename of the combined file relative to docroot
359
	 * @param array  $files            Array of filenames relative to docroot
360
	 * @param array  $options          Array of options for combining files. Available options are:
361
	 * - 'media' : If including CSS Files, you can specify a media type
362
	 * - 'async' : If including JavaScript Files, boolean value to set async attribute to script tag
363
	 * - 'defer' : If including JavaScript Files, boolean value to set defer attribute to script tag
364
	 *
365
	 * @return bool|void
366
	 */
367
	public static function combine_files($combinedFileName, $files, $options = array()) {
368
        if(is_string($options))  {
369
            Deprecation::notice('4.0', 'Parameter media is deprecated. Use options array instead.');
370
            $options = array('media' => $options);
371
        }
372
		self::backend()->combineFiles($combinedFileName, $files, $options);
373
	}
374
375
	/**
376
	 * Return all combined files; keys are the combined file names, values are lists of
377
	 * associative arrays with 'files', 'type', and 'media' keys for details about this
378
	 * combined file.
379
	 *
380
	 * @return array
381
	 */
382
	public static function get_combine_files() {
383
		return self::backend()->getCombinedFiles();
384
	}
385
386
	/**
387
	 * Deletes all generated combined files in the configured combined files directory,
388
	 * but doesn't delete the directory itself
389
	 */
390
	public static function delete_all_combined_files() {
391
		self::backend()->deleteAllCombinedFiles();
392
	}
393
394
	/**
395
	 * Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
396
	 */
397
	public static function clear_combined_files() {
398
		self::backend()->clearCombinedFiles();
399
	}
400
401
	/**
402
	 * Do the heavy lifting involved in combining the combined files.
403
 	 */
404
	public static function process_combined_files() {
405
		self::backend()->processCombinedFiles();
406
	}
407
408
	/**
409
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
410
	 * head tag.
411
	 *
412
	 * @return bool
413
	 */
414
	public static function get_write_js_to_body() {
415
		return self::backend()->getWriteJavascriptToBody();
416
	}
417
418
	/**
419
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
420
	 * head tag.
421
	 *
422
	 * @param bool
423
	 */
424
	public static function set_write_js_to_body($var) {
425
		self::backend()->setWriteJavascriptToBody($var);
426
	}
427
428
	/**
429
	 * Get whether to force the JavaScript to end of the body. Useful if you use inline script tags
430
	 * that don't rely on scripts included via {@link Requirements::javascript()).
431
	 *
432
	 * @return bool
433
	 */
434
	public static function get_force_js_to_bottom() {
435
		return self::backend()->getForceJSToBottom();
436
	}
437
438
	/**
439
	 * Set whether to force the JavaScript to end of the body. Useful if you use inline script tags
440
	 * that don't rely on scripts included via {@link Requirements::javascript()).
441
	 *
442
	 * @param bool $var If true, force the JavaScript to be included at the bottom of the page
443
	 */
444
	public static function set_force_js_to_bottom($var) {
445
		self::backend()->setForceJSToBottom($var);
446
	}
447
448
	/**
449
	 * Check if JS minification is enabled
450
	 *
451
	 * @return bool
452
	 */
453
	public static function get_minify_combined_js_files() {
454
		return self::backend()->getMinifyCombinedJSFiles();
455
	}
456
457
	/**
458
	 * Enable or disable js minification
459
	 *
460
	 * @param bool $minify
461
	 */
462
	public static function set_minify_combined_js_files($minify) {
463
		self::backend()->setMinifyCombinedJSFiles($minify);
464
	}
465
466
	/**
467
	 * Check if header comments are written
468
	 *
469
	 * @return bool
470
	 */
471
	public static function get_write_header_comments() {
472
		return self::backend()->getWriteHeaderComment();
473
	}
474
475
	/**
476
	 * Flag whether header comments should be written for each combined file
477
	 *
478
	 * @param bool $write
479
	 */
480
	public function set_write_header_comments($write) {
481
		self::backend()->setWriteHeaderComment($write);
482
	}
483
484
485
	/**
486
	 * Output debugging information
487
	 */
488
	public static function debug() {
489
		self::backend()->debug();
490
	}
491
492
}
493
494
/**
495
 * @package framework
496
 * @subpackage view
497
 */
498
class Requirements_Backend
499
{
500
501
	/**
502
	 * Whether to add caching query params to the requests for file-based requirements.
503
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
504
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
505
	 * while automatically busting this cache every time the file is changed.
506
	 *
507
	 * @var bool
508
	 */
509
	protected $suffixRequirements = true;
510
511
	/**
512
	 * Whether to combine CSS and JavaScript files
513
	 *
514
	 * @var bool
515
	 */
516
	protected $combinedFilesEnabled = true;
517
518
	/**
519
	 * Determine if files should be combined automatically on dev mode.
520
	 *
521
	 * By default combined files will not be combined except in test or
522
	 * live environments. Turning this on will allow for pre-combining of files in development mode.
523
	 *
524
	 * @config
525
	 * @var bool
526
	 */
527
	private static $combine_in_dev = false;
528
529
	/**
530
	 * Paths to all required JavaScript files relative to docroot
531
	 *
532
	 * @var array
533
	 */
534
	protected $javascript = array();
535
536
	/**
537
	 * Map of included scripts to array of contained files.
538
	 * To be used alongside front-end combination mechanisms.
539
	 *
540
	 * @var array Map of providing filepath => array(provided filepaths)
541
	 */
542
	protected $providedJavascript = array();
543
544
	/**
545
	 * Paths to all required CSS files relative to the docroot.
546
	 *
547
	 * @var array
548
	 */
549
	protected $css = array();
550
551
	/**
552
	 * All custom javascript code that is inserted into the page's HTML
553
	 *
554
	 * @var array
555
	 */
556
	protected $customScript = array();
557
558
	/**
559
	 * All custom CSS rules which are inserted directly at the bottom of the HTML <head> tag
560
	 *
561
	 * @var array
562
	 */
563
	protected $customCSS = array();
564
565
	/**
566
	 * All custom HTML markup which is added before the closing <head> tag, e.g. additional
567
	 * metatags.
568
	 *
569
	 * @var array
570
	 */
571
	protected $customHeadTags = array();
572
573
	/**
574
	 * Remembers the file paths or uniquenessIDs of all Requirements cleared through
575
	 * {@link clear()}, so that they can be restored later.
576
	 *
577
	 * @var array
578
	 */
579
	protected $disabled = array();
580
581
	/**
582
	 * The file paths (relative to docroot) or uniquenessIDs of any included requirements which
583
	 * should be blocked when executing {@link inlcudeInHTML()}. This is useful, for example,
584
	 * to block scripts included by a superclass without having to override entire functions and
585
	 * duplicate a lot of code.
586
	 *
587
	 * Use {@link unblock()} or {@link unblock_all()} to revert changes.
588
	 *
589
	 * @var array
590
	 */
591
	protected $blocked = array();
592
593
	/**
594
	 * A list of combined files registered via {@link combine_files()}. Keys are the output file
595
	 * names, values are lists of input files.
596
	 *
597
	 * @var array
598
	 */
599
	protected $combinedFiles = array();
600
601
	/**
602
	 * Use the JSMin library to minify any javascript file passed to {@link combine_files()}.
603
	 *
604
	 * @var bool
605
	 */
606
	protected $minifyCombinedJSFiles = true;
607
608
	/**
609
	 * Whether or not file headers should be written when combining files
610
	 *
611
	 * @var boolean
612
	 */
613
	protected $writeHeaderComment = true;
614
615
	/**
616
	 * Where to save combined files. By default they're placed in assets/_combinedfiles, however
617
	 * this may be an issue depending on your setup, especially for CSS files which often contain
618
	 * relative paths.
619
	 *
620
	 * @var string
621
	 */
622
	protected $combinedFilesFolder = null;
623
624
	/**
625
	 * Put all JavaScript includes at the bottom of the template before the closing <body> tag,
626
	 * rather than the default behaviour of placing them at the end of the <head> tag. This means
627
	 * script downloads won't block other HTTP requests, which can be a performance improvement.
628
	 *
629
	 * @var bool
630
	 */
631
	public $writeJavascriptToBody = true;
632
633
	/**
634
	 * Force the JavaScript to the bottom of the page, even if there's a script tag in the body already
635
	 *
636
	 * @var boolean
637
	 */
638
	protected $forceJSToBottom = false;
639
640
	/**
641
	 * Configures the default prefix for combined files.
642
	 *
643
	 * This defaults to `_combinedfiles`, and is the folder within the configured asset backend that
644
	 * combined files will be stored in. If using a backend shared with other systems, it is usually
645
	 * necessary to distinguish combined files from other assets.
646
	 *
647
	 * @config
648
	 * @var string
649
	 */
650
	private static $default_combined_files_folder = '_combinedfiles';
651
652
	/**
653
	 * Flag to include the hash in the querystring instead of the filename for combined files.
654
	 *
655
	 * By default the `<hash>` of the source files is appended to the end of the combined file
656
	 * (prior to the file extension). If combined files are versioned in source control or running
657
	 * in a distributed environment (such as one where the newest version of a file may not always be
658
	 * immediately available) then it may sometimes be necessary to disable this. When this is set to true,
659
	 * the hash will instead be appended via a querystring parameter to enable cache busting, but not in
660
	 * the filename itself. I.e. `assets/_combinedfiles/name.js?m=<hash>`
661
	 *
662
	 * @config
663
	 * @var bool
664
	 */
665
	private static $combine_hash_querystring = false;
666
667
	/**
668
	 * @var GeneratedAssetHandler
669
	 */
670
	protected $assetHandler = null;
671
672
	/**
673
	 * Gets the backend storage for generated files
674
	 *
675
	 * @return GeneratedAssetHandler
676
	 */
677
	public function getAssetHandler() {
678
		return $this->assetHandler;
679
	}
680
681
	/**
682
	 * Set a new asset handler for this backend
683
	 *
684
	 * @param GeneratedAssetHandler $handler
685
	 */
686
	public function setAssetHandler(GeneratedAssetHandler $handler) {
687
		$this->assetHandler = $handler;
688
	}
689
690
	/**
691
	 * Enable or disable the combination of CSS and JavaScript files
692
	 *
693
	 * @param bool $enable
694
	 */
695
	public function setCombinedFilesEnabled($enable) {
696
		$this->combinedFilesEnabled = (bool) $enable;
697
	}
698
699
	/**
700
	 * Check if header comments are written
701
	 *
702
	 * @return bool
703
	 */
704
	public function getWriteHeaderComment() {
705
		return $this->writeHeaderComment;
706
	}
707
708
	/**
709
	 * Flag whether header comments should be written for each combined file
710
	 *
711
	 * @param bool $write
712
	 * @return $this
713
	 */
714
	public function setWriteHeaderComment($write) {
715
		$this->writeHeaderComment = $write;
716
		return $this;
717
	}
718
719
	/**
720
	 * Set the folder to save combined files in. By default they're placed in _combinedfiles,
721
	 * however this may be an issue depending on your setup, especially for CSS files which often
722
	 * contain relative paths.
723
	 *
724
	 * This must not include any 'assets' prefix
725
	 *
726
	 * @param string $folder
727
	 */
728
	public function setCombinedFilesFolder($folder) {
729
		$this->combinedFilesFolder = $folder;
730
	}
731
732
	/**
733
	 * Retrieve the combined files folder prefix
734
	 *
735
	 * @return string
736
	 */
737
	public function getCombinedFilesFolder() {
738
		if($this->combinedFilesFolder) {
739
			return $this->combinedFilesFolder;
740
		}
741
		return Config::inst()->get(__CLASS__, 'default_combined_files_folder');
742
	}
743
744
	/**
745
	 * Set whether to add caching query params to the requests for file-based requirements.
746
	 * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
747
	 * filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
748
	 * while automatically busting this cache every time the file is changed.
749
	 *
750
	 * @param bool
751
	 */
752
	public function setSuffixRequirements($var) {
753
		$this->suffixRequirements = $var;
754
	}
755
756
	/**
757
	 * Check whether we want to suffix requirements
758
	 *
759
	 * @return bool
760
	 */
761
	public function getSuffixRequirements() {
762
		return $this->suffixRequirements;
763
	}
764
765
	/**
766
	 * Set whether you want to write the JS to the body of the page rather than at the end of the
767
	 * head tag.
768
	 *
769
	 * @param bool
770
	 * @return $this
771
	 */
772
	public function setWriteJavascriptToBody($var) {
773
		$this->writeJavascriptToBody = $var;
774
		return $this;
775
	}
776
777
	/**
778
	 * Check whether you want to write the JS to the body of the page rather than at the end of the
779
	 * head tag.
780
	 *
781
	 * @return bool
782
	 */
783
	public function getWriteJavascriptToBody() {
784
		return $this->writeJavascriptToBody;
785
	}
786
787
	/**
788
	 * Forces the JavaScript requirements to the end of the body, right before the closing tag
789
	 *
790
	 * @param bool
791
	 * @return $this
792
	 */
793
	public function setForceJSToBottom($var) {
794
		$this->forceJSToBottom = $var;
795
		return $this;
796
	}
797
798
	/**
799
	 * Check if the JavaScript requirements are written to the end of the body, right before the closing tag
800
	 *
801
	 * @return bool
802
	 */
803
	public function getForceJSToBottom() {
804
		return $this->forceJSToBottom;
805
	}
806
807
	/**
808
	 * Check if minify js files should be combined
809
	 *
810
	 * @return bool
811
	 */
812
	public function getMinifyCombinedJSFiles() {
813
		return $this->minifyCombinedJSFiles;
814
	}
815
816
	/**
817
	 * Set if combined js files should be minified
818
	 *
819
	 * @param bool $minify
820
	 * @return $this
821
	 */
822
	public function setMinifyCombinedJSFiles($minify) {
823
		$this->minifyCombinedJSFiles = $minify;
824
		return $this;
825
	}
826
827
	/**
828
	 * Register the given JavaScript file as required.
829
	 *
830
	 * @param string $file Relative to docroot
831
	 * @param array $options List of options. Available options include:
832
	 * - 'provides' : List of scripts files included in this file
833
	 * - 'async' : Boolean value to set async attribute to script tag
834
	 * - 'defer' : Boolean value to set defer attribute to script tag
835
	 */
836
	public function javascript($file, $options = array()) {
837
	    // make sure that async/defer is set if it is set once even if file is included multiple times
838
        $async = (
839
            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...
840
            || (
841
                isset($this->javascript[$file])
842
                && isset($this->javascript[$file]['async'])
843
                && $this->javascript[$file]['async'] == true
844
            )
845
        );
846
        $defer = (
847
            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...
848
            || (
849
                isset($this->javascript[$file])
850
                && isset($this->javascript[$file]['defer'])
851
                && $this->javascript[$file]['defer'] == true
852
            )
853
        );
854
        $this->javascript[$file] = array(
855
            'async' => $async,
856
            'defer' => $defer,
857
        );
858
859
		// Record scripts included in this file
860
		if(isset($options['provides'])) {
861
			$this->providedJavascript[$file] = array_values($options['provides']);
862
	    }
863
864
	}
865
866
	/**
867
	 * Remove a javascript requirement
868
	 *
869
	 * @param string $file
870
	 */
871
	protected function unsetJavascript($file) {
872
		unset($this->javascript[$file]);
873
	}
874
875
	/**
876
	 * Gets all scripts that are already provided by prior scripts.
877
	 * This follows these rules:
878
	 *  - Files will not be considered provided if they are separately
879
	 *    included prior to the providing file.
880
	 *  - Providing files can be blocked, and don't provide anything
881
	 *  - Provided files can't be blocked (you need to block the provider)
882
	 *  - If a combined file includes files that are provided by prior
883
	 *    scripts, then these should be excluded from the combined file.
884
	 *  - If a combined file includes files that are provided by later
885
	 *    scripts, then these files should be included in the combined
886
	 *    file, but we can't block the later script either (possible double
887
	 *    up of file).
888
	 *
889
	 * @return array Array of provided files (map of $path => $path)
890
	 */
891
	public function getProvidedScripts() {
892
		$providedScripts = array();
893
		$includedScripts = array();
894
		foreach($this->javascript as $script => $options) {
895
			// Ignore scripts that are explicitly blocked
896
			if(isset($this->blocked[$script])) {
897
				continue;
898
			}
899
			// At this point, the file is included.
900
			// This might also be combined at this point, potentially.
901
			$includedScripts[$script] = true;
902
903
			// Record any files this provides, EXCEPT those already included by now
904
			if(isset($this->providedJavascript[$script])) {
905
				foreach($this->providedJavascript[$script] as $provided) {
906
					if(!isset($includedScripts[$provided])) {
907
						$providedScripts[$provided] = $provided;
908
					}
909
				}
910
			}
911
		}
912
		return $providedScripts;
913
	}
914
915
	/**
916
	 * Returns an array of required JavaScript, excluding blocked
917
	 * and duplicates of provided files.
918
	 *
919
	 * @return array
920
	 */
921
	public function getJavascript() {
922
		return array_diff_key(
923
			$this->javascript,
924
			$this->getBlocked(),
925
			$this->getProvidedScripts()
926
		);
927
	}
928
929
	/**
930
	 * Gets all javascript, including blocked files. Unwraps the array into a non-associative list
931
	 *
932
	 * @return array Indexed array of javascript files
933
	 */
934
	protected function getAllJavascript() {
935
		return $this->javascript;
936
	}
937
938
	/**
939
	 * Register the given JavaScript code into the list of requirements
940
	 *
941
	 * @param string     $script       The script content as a string (without enclosing <script> tag)
942
	 * @param string $uniquenessID A unique ID that ensures a piece of code is only added once
943
	 */
944
	public function customScript($script, $uniquenessID = null) {
945
		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...
946
			$this->customScript[$uniquenessID] = $script;
947
		} else {
948
			$this->customScript[] = $script;
949
		}
950
	}
951
952
	/**
953
	 * Return all registered custom scripts
954
	 *
955
	 * @return array
956
	 */
957
	public function getCustomScripts() {
958
		return array_diff_key($this->customScript, $this->blocked);
959
	}
960
961
	/**
962
	 * Register the given CSS styles into the list of requirements
963
	 *
964
	 * @param string     $script       CSS selectors as a string (without enclosing <style> tag)
965
	 * @param string $uniquenessID A unique ID that ensures a piece of code is only added once
966
	 */
967
	public function customCSS($script, $uniquenessID = null) {
968
		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...
969
			$this->customCSS[$uniquenessID] = $script;
970
		} else {
971
			$this->customCSS[] = $script;
972
		}
973
	}
974
975
	/**
976
	 * Return all registered custom CSS
977
	 *
978
	 * @return array
979
	 */
980
	public function getCustomCSS() {
981
		return array_diff_key($this->customCSS, $this->blocked);
982
	}
983
984
	/**
985
	 * Add the following custom HTML code to the <head> section of the page
986
	 *
987
	 * @param string     $html         Custom HTML code
988
	 * @param string $uniquenessID A unique ID that ensures a piece of code is only added once
989
	 */
990
	public function insertHeadTags($html, $uniquenessID = null) {
991
		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...
992
			$this->customHeadTags[$uniquenessID] = $html;
993
		} else {
994
			$this->customHeadTags[] = $html;
995
		}
996
	}
997
998
	/**
999
	 * Return all custom head tags
1000
	 *
1001
	 * @return array
1002
	 */
1003
	public function getCustomHeadTags() {
1004
		return array_diff_key($this->customHeadTags, $this->blocked);
1005
	}
1006
1007
	/**
1008
	 * Include the content of the given JavaScript file in the list of requirements. Dollar-sign
1009
	 * variables will be interpolated with values from $vars similar to a .ss template.
1010
	 *
1011
	 * @param string         $file         The template file to load, relative to docroot
1012
	 * @param string[] $vars The array of variables to interpolate.
1013
	 * @param string $uniquenessID A unique ID that ensures a piece of code is only added once
1014
	 */
1015
	public function javascriptTemplate($file, $vars, $uniquenessID = null) {
1016
		$script = file_get_contents(Director::getAbsFile($file));
1017
		$search = array();
1018
		$replace = array();
1019
1020
		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...
1021
			$search[] = '$' . $k;
1022
			$replace[] = str_replace("\\'","'", Convert::raw2js($v));
1023
		}
1024
1025
		$script = str_replace($search, $replace, $script);
1026
		$this->customScript($script, $uniquenessID);
1027
	}
1028
1029
	/**
1030
	 * Register the given stylesheet into the list of requirements.
1031
	 *
1032
	 * @param string $file  The CSS file to load, relative to site root
1033
	 * @param string $media Comma-separated list of media types to use in the link tag
1034
	 *                      (e.g. 'screen,projector')
1035
	 */
1036
	public function css($file, $media = null) {
1037
		$this->css[$file] = array(
1038
			"media" => $media
1039
		);
1040
	}
1041
1042
	/**
1043
	 * Remove a css requirement
1044
	 *
1045
	 * @param string $file
1046
	 */
1047
	protected function unsetCSS($file) {
1048
		unset($this->css[$file]);
1049
	}
1050
1051
	/**
1052
	 * Get the list of registered CSS file requirements, excluding blocked files
1053
	 *
1054
	 * @return array Associative array of file to spec
1055
	 */
1056
	public function getCSS() {
1057
		return array_diff_key($this->css, $this->blocked);
1058
	}
1059
1060
	/**
1061
	 * Gets all CSS files requirements, including blocked
1062
	 *
1063
	 * @return array Associative array of file to spec
1064
	 */
1065
	protected function getAllCSS() {
1066
		return $this->css;
1067
	}
1068
1069
	/**
1070
	 * Gets the list of all blocked files
1071
	 *
1072
	 * @return array
1073
	 */
1074
	public function getBlocked() {
1075
		return $this->blocked;
1076
	}
1077
1078
	/**
1079
	 * Clear either a single or all requirements
1080
	 *
1081
	 * Caution: Clearing single rules added via customCSS and customScript only works if you
1082
	 * originally specified a $uniquenessID.
1083
	 *
1084
	 * @param string|int $fileOrID
1085
	 */
1086
	public function clear($fileOrID = null) {
1087
		if($fileOrID) {
1088
			foreach(array('javascript','css', 'customScript', 'customCSS', 'customHeadTags') as $type) {
1089
				if(isset($this->{$type}[$fileOrID])) {
1090
					$this->disabled[$type][$fileOrID] = $this->{$type}[$fileOrID];
1091
					unset($this->{$type}[$fileOrID]);
1092
				}
1093
			}
1094
		} else {
1095
			$this->disabled['javascript'] = $this->javascript;
1096
			$this->disabled['css'] = $this->css;
1097
			$this->disabled['customScript'] = $this->customScript;
1098
			$this->disabled['customCSS'] = $this->customCSS;
1099
			$this->disabled['customHeadTags'] = $this->customHeadTags;
1100
1101
			$this->javascript = array();
1102
			$this->css = array();
1103
			$this->customScript = array();
1104
			$this->customCSS = array();
1105
			$this->customHeadTags = array();
1106
		}
1107
	}
1108
1109
	/**
1110
	 * Restore requirements cleared by call to Requirements::clear
1111
	 */
1112
	public function restore() {
1113
		$this->javascript = $this->disabled['javascript'];
1114
		$this->css = $this->disabled['css'];
1115
		$this->customScript = $this->disabled['customScript'];
1116
		$this->customCSS = $this->disabled['customCSS'];
1117
		$this->customHeadTags = $this->disabled['customHeadTags'];
1118
	}
1119
1120
	/**
1121
	 * Block inclusion of a specific file
1122
	 *
1123
	 * The difference between this and {@link clear} is that the calling order does not matter;
1124
	 * {@link clear} must be called after the initial registration, whereas {@link block} can be
1125
	 * used in advance. This is useful, for example, to block scripts included by a superclass
1126
	 * without having to override entire functions and duplicate a lot of code.
1127
	 *
1128
	 * Note that blocking should be used sparingly because it's hard to trace where an file is
1129
	 * being blocked from.
1130
	 *
1131
	 * @param string|int $fileOrID
1132
	 */
1133
	public function block($fileOrID) {
1134
		$this->blocked[$fileOrID] = $fileOrID;
1135
	}
1136
1137
	/**
1138
	 * Remove an item from the block list
1139
	 *
1140
	 * @param string|int $fileOrID
1141
	 */
1142
	public function unblock($fileOrID) {
1143
		unset($this->blocked[$fileOrID]);
1144
	}
1145
1146
	/**
1147
	 * Removes all items from the block list
1148
	 */
1149
	public function unblockAll() {
1150
		$this->blocked = array();
1151
	}
1152
1153
	/**
1154
	 * Update the given HTML content with the appropriate include tags for the registered
1155
	 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
1156
	 * including a head and body tag.
1157
	 *
1158
	 * @param string $content      HTML content that has already been parsed from the $templateFile
1159
	 *                             through {@link SSViewer}
1160
	 * @return string HTML content augmented with the requirements tags
1161
	 */
1162
	public function includeInHTML($content) {
1163
		if(func_num_args() > 1) {
1164
			Deprecation::notice(
1165
				'5.0',
1166
				'$templateFile argument is deprecated. includeInHTML takes a sole $content parameter now.'
1167
			);
1168
			$content = func_get_arg(1);
1169
		}
1170
1171
		// Skip if content isn't injectable, or there is nothing to inject
1172
		$tagsAvailable = preg_match('#</head\b#', $content);
1173
		$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...
1174
		if(!$tagsAvailable || !$hasFiles) {
1175
			return $content;
1176
		}
1177
		$requirements = '';
1178
		$jsRequirements = '';
1179
1180
		// Combine files - updates $this->javascript and $this->css
1181
		$this->processCombinedFiles();
1182
1183
		foreach($this->getJavascript() as $file => $attributes) {
1184
		    $async = (isset($attributes['async']) && $attributes['async'] == true) ? " async" : "";
1185
		    $defer = (isset($attributes['defer']) && $attributes['defer'] == true) ? " defer" : "";
1186
			$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...
1187
			if($path) {
1188
				$jsRequirements .= "<script type=\"application/javascript\" src=\"$path\"{$async}{$defer}></script>";
1189
			}
1190
		}
1191
1192
		// Add all inline JavaScript *after* including external files they might rely on
1193
		foreach($this->getCustomScripts() as $script) {
1194
			$jsRequirements .= "<script type=\"application/javascript\">//<![CDATA[\n";
1195
			$jsRequirements .= "$script\n";
1196
			$jsRequirements .= "//]]></script>";
1197
		}
1198
1199
		foreach($this->getCSS() as $file => $params) {
1200
			$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...
1201
			if($path) {
1202
				$media = (isset($params['media']) && !empty($params['media']))
1203
					? " media=\"{$params['media']}\"" : "";
1204
				$requirements .= "<link rel=\"stylesheet\" type=\"text/css\" {$media} href=\"$path\" />\n";
1205
			}
1206
		}
1207
1208
		foreach($this->getCustomCSS() as $css) {
1209
			$requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
1210
		}
1211
1212
		foreach($this->getCustomHeadTags() as $customHeadTag) {
1213
			$requirements .= "$customHeadTag\n";
1214
		}
1215
1216
		// Inject CSS  into body
1217
		$content = $this->insertTagsIntoHead($requirements, $content);
1218
1219
		// Inject scripts
1220
		if ($this->getForceJSToBottom()) {
1221
			$content = $this->insertScriptsAtBottom($jsRequirements, $content);
1222
		} elseif($this->getWriteJavascriptToBody()) {
1223
			$content = $this->insertScriptsIntoBody($jsRequirements, $content);
1224
		} else {
1225
			$content = $this->insertTagsIntoHead($jsRequirements, $content);
1226
		}
1227
		return $content;
1228
	}
1229
1230
	/**
1231
	 * Given a block of HTML, insert the given scripts at the bottom before
1232
	 * the closing </body> tag
1233
	 *
1234
	 * @param string $jsRequirements String containing one or more javascript <script /> tags
1235
	 * @param string $content HTML body
1236
	 * @return string Merged HTML
1237
	 */
1238
	protected function insertScriptsAtBottom($jsRequirements, $content) {
1239
		// Forcefully put the scripts at the bottom of the body instead of before the first
1240
		// script tag.
1241
		$content = preg_replace(
1242
			'/(<\/body[^>]*>)/i',
1243
			$this->escapeReplacement($jsRequirements) . '\\1',
1244
			$content
1245
		);
1246
		return $content;
1247
	}
1248
1249
	/**
1250
	 * Given a block of HTML, insert the given scripts inside the <body></body>
1251
	 *
1252
	 * @param string $jsRequirements String containing one or more javascript <script /> tags
1253
	 * @param string $content HTML body
1254
	 * @return string Merged HTML
1255
	 */
1256
	protected function insertScriptsIntoBody($jsRequirements, $content) {
1257
		// If your template already has script tags in the body, then we try to put our script
1258
		// tags just before those. Otherwise, we put it at the bottom.
1259
		$bodyTagPosition = stripos($content, '<body');
1260
		$scriptTagPosition = stripos($content, '<script', $bodyTagPosition);
1261
1262
		$commentTags = array();
1263
		$canWriteToBody = ($scriptTagPosition !== false)
1264
			&&
1265
			// Check that the script tag is not inside a html comment tag
1266
			!(
1267
				preg_match('/.*(?|(<!--)|(-->))/U', $content, $commentTags, 0, $scriptTagPosition)
1268
				&&
1269
				$commentTags[1] == '-->'
1270
			);
1271
1272
		if($canWriteToBody) {
1273
			// Insert content before existing script tags
1274
			$content = substr($content, 0, $scriptTagPosition)
1275
				. $jsRequirements
1276
				. substr($content, $scriptTagPosition);
1277
		} else {
1278
			// Insert content at bottom of page otherwise
1279
			$content = $this->insertScriptsAtBottom($jsRequirements, $content);
1280
		}
1281
1282
		return $content;
1283
	}
1284
1285
	/**
1286
	 * Given a block of HTML, insert the given code inside the <head></head> block
1287
	 *
1288
	 * @param string $jsRequirements String containing one or more html tags
1289
	 * @param string $content HTML body
1290
	 * @return string Merged HTML
1291
	 */
1292
	protected function insertTagsIntoHead($jsRequirements, $content) {
1293
		$content = preg_replace(
1294
			'/(<\/head>)/i',
1295
			$this->escapeReplacement($jsRequirements) . '\\1',
1296
			$content
1297
		);
1298
		return $content;
1299
	}
1300
1301
	/**
1302
	 * Safely escape a literal string for use in preg_replace replacement
1303
	 *
1304
	 * @param string $replacement
1305
	 * @return string
1306
	 */
1307
	protected function escapeReplacement($replacement) {
1308
		return addcslashes($replacement, '\\$');
1309
	}
1310
1311
	/**
1312
	 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
1313
	 * HTTP Response
1314
	 *
1315
	 * @param SS_HTTPResponse $response
1316
	 */
1317
	public function includeInResponse(SS_HTTPResponse $response) {
1318
		$this->processCombinedFiles();
1319
		$jsRequirements = array();
1320
		$cssRequirements = array();
1321
1322
		foreach($this->getJavascript() as $file => $attributes) {
1323
			$path = $this->pathForFile($file);
1324
			if($path) {
1325
				$jsRequirements[] = str_replace(',', '%2C', $path);
1326
			}
1327
		}
1328
1329
		if(count($jsRequirements)) {
1330
			$response->addHeader('X-Include-JS', implode(',', $jsRequirements));
1331
		}
1332
1333
		foreach($this->getCSS() as $file => $params) {
1334
			$path = $this->pathForFile($file);
1335
			if($path) {
1336
				$path = str_replace(',', '%2C', $path);
1337
				$cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path;
1338
			}
1339
		}
1340
1341
		if(count($cssRequirements)) {
1342
			$response->addHeader('X-Include-CSS', implode(',', $cssRequirements));
1343
		}
1344
	}
1345
1346
	/**
1347
	 * Add i18n files from the given javascript directory. SilverStripe expects that the given
1348
	 * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
1349
	 * etc.
1350
	 *
1351
	 * @param string $langDir  The JavaScript lang directory, relative to the site root, e.g.,
1352
	 *                         'framework/javascript/lang'
1353
	 * @param bool   $return   Return all relative file paths rather than including them in
1354
	 *                         requirements
1355
	 * @param bool   $langOnly Only include language files, not the base libraries
1356
	 *
1357
	 * @return array|null All relative files if $return is true, or null otherwise
1358
	 */
1359
	public function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
1360
		$files = array();
1361
		$base = Director::baseFolder() . '/';
1362
		if(i18n::config()->js_i18n) {
1363
			// Include i18n.js even if no languages are found.  The fact that
1364
			// add_i18n_javascript() was called indicates that the methods in
1365
			// here are needed.
1366
			if(!$langOnly) $files[] = FRAMEWORK_DIR . '/client/dist/js/i18n.js';
1367
1368
			if(substr($langDir,-1) != '/') $langDir .= '/';
1369
1370
			$candidates = array(
1371
				'en.js',
1372
				'en_US.js',
1373
				i18n::get_lang_from_locale(i18n::config()->default_locale) . '.js',
1374
				i18n::config()->default_locale . '.js',
1375
				i18n::get_lang_from_locale(i18n::get_locale()) . '.js',
1376
				i18n::get_locale() . '.js',
1377
			);
1378
			foreach($candidates as $candidate) {
1379
				if(file_exists($base . DIRECTORY_SEPARATOR . $langDir . $candidate)) {
1380
					$files[] = $langDir . $candidate;
1381
				}
1382
			}
1383
		} else {
1384
			// Stub i18n implementation for when i18n is disabled.
1385
			if(!$langOnly) {
1386
				$files[] = FRAMEWORK_DIR . '/client/dist/js/i18nx.js';
1387
			}
1388
		}
1389
1390
		if($return) {
1391
			return $files;
1392
		} else {
1393
			foreach($files as $file) {
1394
				$this->javascript($file);
1395
			}
1396
			return null;
1397
		}
1398
	}
1399
1400
	/**
1401
	 * Finds the path for specified file
1402
	 *
1403
	 * @param string $fileOrUrl
1404
	 * @return string|bool
1405
	 */
1406
	protected function pathForFile($fileOrUrl) {
1407
		// Since combined urls could be root relative, treat them as urls here.
1408
		if(preg_match('{^(//)|(http[s]?:)}', $fileOrUrl) || Director::is_root_relative_url($fileOrUrl)) {
1409
			return $fileOrUrl;
1410
		} elseif(Director::fileExists($fileOrUrl)) {
1411
			$filePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $fileOrUrl);
1412
			$prefix = Director::baseURL();
1413
			$mtimesuffix = "";
1414
			$suffix = '';
1415
			if($this->getSuffixRequirements()) {
1416
				$mtimesuffix = "?m=" . filemtime($filePath);
1417
				$suffix = '&';
1418
			}
1419
			if(strpos($fileOrUrl, '?') !== false) {
1420
				if (strlen($suffix) == 0) {
1421
					$suffix = '?';
1422
				}
1423
				$suffix .= substr($fileOrUrl, strpos($fileOrUrl, '?')+1);
1424
				$fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?'));
1425
			} else {
1426
				$suffix = '';
1427
			}
1428
			return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}";
1429
		} else {
1430
			throw new InvalidArgumentException("File {$fileOrUrl} does not exist");
1431
		}
1432
	}
1433
1434
	/**
1435
	 * Concatenate several css or javascript files into a single dynamically generated file. This
1436
	 * increases performance by fewer HTTP requests.
1437
	 *
1438
	 * The combined file is regenerated based on every file modification time. Optionally a
1439
	 * rebuild can be triggered by appending ?flush=1 to the URL.
1440
	 *
1441
	 * All combined files will have a comment on the start of each concatenated file denoting their
1442
	 * original position.
1443
	 *
1444
	 * CAUTION: You're responsible for ensuring that the load order for combined files is
1445
	 * retained - otherwise combining JavaScript files can lead to functional errors in the
1446
	 * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also
1447
	 * only include each file once across all includes and combinations in a single page load.
1448
	 *
1449
	 * CAUTION: Combining CSS Files discards any "media" information.
1450
	 *
1451
	 * Example for combined JavaScript:
1452
	 * <code>
1453
	 * Requirements::combine_files(
1454
	 *    'foobar.js',
1455
	 *    array(
1456
	 *        'mysite/javascript/foo.js',
1457
	 *        'mysite/javascript/bar.js',
1458
	 *    ),
1459
	 *    array(
1460
	 *        'async' => true,
1461
	 *        'defer' => true,
1462
	 *    )
1463
	 * );
1464
	 * </code>
1465
	 *
1466
	 * Example for combined CSS:
1467
	 * <code>
1468
	 * Requirements::combine_files(
1469
	 *    'foobar.css',
1470
	 *    array(
1471
	 *        'mysite/javascript/foo.css',
1472
	 *        'mysite/javascript/bar.css',
1473
	 *    ),
1474
	 *    array(
1475
	 *        'media' => 'print',
1476
	 *    )
1477
	 * );
1478
	 * </code>
1479
	 *
1480
	 * @param string $combinedFileName Filename of the combined file relative to docroot
1481
	 * @param array  $files            Array of filenames relative to docroot
1482
	 * @param array  $options          Array of options for combining files. Available options are:
1483
	 * - 'media' : If including CSS Files, you can specify a media type
1484
	 * - 'async' : If including JavaScript Files, boolean value to set async attribute to script tag
1485
	 * - 'defer' : If including JavaScript Files, boolean value to set defer attribute to script tag
1486
	 */
1487
	public function combineFiles($combinedFileName, $files, $options = array()) {
1488
	    if(is_string($options))  {
1489
            Deprecation::notice('4.0', 'Parameter media is deprecated. Use options array instead.');
1490
            $options = array('media' => $options);
1491
	    }
1492
		// Skip this combined files if already included
1493
		if(isset($this->combinedFiles[$combinedFileName])) {
1494
			return;
1495
		}
1496
1497
		// Add all files to necessary type list
1498
		$paths = array();
1499
		$combinedType = null;
1500
		foreach($files as $file) {
1501
			// Get file details
1502
			list($path, $type) = $this->parseCombinedFile($file);
1503
			if($type === 'javascript') {
1504
				$type = 'js';
1505
		    }
1506
			if($combinedType && $type && $combinedType !== $type) {
1507
				throw new InvalidArgumentException(
1508
					"Cannot mix js and css files in same combined file {$combinedFileName}"
1509
				);
1510
			}
1511
			switch($type) {
1512
				case 'css':
1513
					$this->css($path, (isset($options['media']) ? $options['media'] : null));
1514
					break;
1515
				case 'js':
1516
					$this->javascript($path, $options);
1517
					break;
1518
				default:
1519
					throw new InvalidArgumentException("Invalid combined file type: {$type}");
1520
			}
1521
			$combinedType = $type;
1522
			$paths[] = $path;
1523
		}
1524
1525
		// Duplicate check
1526
		foreach($this->combinedFiles as $existingCombinedFilename => $combinedItem) {
1527
			$existingFiles = $combinedItem['files'];
1528
			$duplicates = array_intersect($existingFiles, $paths);
1529
			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...
1530
				throw new InvalidArgumentException(sprintf(
1531
					"Requirements_Backend::combine_files(): Already included file(s) %s in combined file '%s'",
1532
					implode(',', $duplicates),
1533
					$existingCombinedFilename
1534
				));
1535
			}
1536
		}
1537
1538
		$this->combinedFiles[$combinedFileName] = array(
1539
			'files' => $paths,
1540
			'type' => $combinedType,
1541
			'options' => $options,
1542
		);
1543
	}
1544
1545
	/**
1546
	 * Return path and type of given combined file
1547
	 *
1548
	 * @param string|array $file Either a file path, or an array spec
1549
	 * @return array array with two elements, path and type of file
1550
	 */
1551
	protected function parseCombinedFile($file) {
1552
		// Array with path and type keys
1553
		if(is_array($file) && isset($file['path']) && isset($file['type'])) {
1554
			return array($file['path'], $file['type']);
1555
				}
1556
1557
		// Extract value from indexed array
1558
		if(is_array($file)) {
1559
			$path = array_shift($file);
1560
1561
			// See if there's a type specifier
1562
			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...
1563
				$type = array_shift($file);
1564
				return array($path, $type);
1565
			}
1566
1567
			// Otherwise convent to string
1568
			$file = $path;
1569
		}
1570
1571
		$type = File::get_file_extension($file);
1572
		return array($file, $type);
1573
	}
1574
1575
	/**
1576
	 * Return all combined files; keys are the combined file names, values are lists of
1577
	 * associative arrays with 'files', 'type', and 'media' keys for details about this
1578
	 * combined file.
1579
	 *
1580
	 * @return array
1581
	 */
1582
	public function getCombinedFiles() {
1583
		return array_diff_key($this->combinedFiles, $this->blocked);
1584
	}
1585
1586
	/**
1587
	 * Includes all combined files, including blocked ones
1588
	 *
1589
	 * @return array
1590
	 */
1591
	protected function getAllCombinedFiles() {
1592
		return $this->combinedFiles;
1593
	}
1594
1595
	/**
1596
	 * Clears all combined files
1597
	 */
1598
	public function deleteAllCombinedFiles() {
1599
		$combinedFolder = $this->getCombinedFilesFolder();
1600
		if($combinedFolder) {
1601
			$this->getAssetHandler()->removeContent($combinedFolder);
1602
		}
1603
	}
1604
1605
	/**
1606
	 * Clear all registered CSS and JavaScript file combinations
1607
	 */
1608
	public function clearCombinedFiles() {
1609
		$this->combinedFiles = array();
1610
	}
1611
1612
	/**
1613
	 * Do the heavy lifting involved in combining the combined files.
1614
	 */
1615
	public function processCombinedFiles() {
1616
		// Check if combining is enabled
1617
		if(!$this->getCombinedFilesEnabled()) {
1618
			return;
1619
		}
1620
1621
		// Before scripts are modified, detect files that are provided by preceding ones
1622
		$providedScripts = $this->getProvidedScripts();
1623
1624
		// Process each combined files
1625
		foreach($this->getAllCombinedFiles() as $combinedFile => $combinedItem) {
1626
			$fileList = $combinedItem['files'];
1627
			$type = $combinedItem['type'];
1628
			$options = $combinedItem['options'];
1629
1630
			// Generate this file, unless blocked
1631
			$combinedURL = null;
1632
			if(!isset($this->blocked[$combinedFile])) {
1633
				// Filter files for blocked / provided
1634
				$filteredFileList = array_diff(
1635
					$fileList,
1636
					$this->getBlocked(),
1637
					$providedScripts
1638
				);
1639
				$combinedURL = $this->getCombinedFileURL($combinedFile, $filteredFileList, $type);
1640
			}
1641
1642
			// Replace all existing files, injecting the combined file at the position of the first item
1643
			// in order to preserve inclusion order.
1644
			// Note that we iterate across blocked files in order to get the correct order, and validate
1645
			// that the file is included in the correct location (regardless of which files are blocked).
1646
			$included = false;
1647
			switch($type) {
1648
				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...
1649
					$newCSS = array(); // Assoc array of css file => spec
1650
					foreach($this->getAllCSS() as $css => $spec) {
1651
						if(!in_array($css, $fileList)) {
1652
							$newCSS[$css] = $spec;
1653
						} 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...
1654
							$newCSS[$combinedURL] = array('media' => (isset($options['media']) ? $options['media'] : null));
1655
							$included = true;
1656
						}
1657
						// If already included, or otherwise blocked, then don't add into CSS
1658
					}
1659
					$this->css = $newCSS;
1660
					break;
1661
				}
1662
				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...
1663
					// Assoc array of file => attributes
1664
					$newJS = array();
1665
					foreach($this->getAllJavascript() as $script => $attributes) {
1666
						if(!in_array($script, $fileList)) {
1667
							$newJS[$script] = $attributes;
1668
						} 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...
1669
							$newJS[$combinedURL] = $options;
1670
							$included = true;
1671
						}
1672
						// If already included, or otherwise blocked, then don't add into scripts
1673
					}
1674
					$this->javascript = $newJS;
1675
					break;
1676
				}
1677
			}
1678
		}
1679
	}
1680
1681
	/**
1682
	 * Given a set of files, combine them (as necessary) and return the url
1683
	 *
1684
	 * @param string $combinedFile Filename for this combined file
1685
	 * @param array $fileList List of files to combine
1686
	 * @param string $type Either 'js' or 'css'
1687
	 * @return string|null URL to this resource, if there are files to combine
1688
	 */
1689
	protected function getCombinedFileURL($combinedFile, $fileList, $type) {
1690
		// Skip empty lists
1691
		if(empty($fileList)) {
1692
			return null;
1693
		}
1694
1695
		// Generate path (Filename)
1696
		$hashQuerystring = Config::inst()->get(static::class, 'combine_hash_querystring');
1697
		if(!$hashQuerystring) {
1698
			$combinedFile = $this->hashedCombinedFilename($combinedFile, $fileList);
1699
			}
1700
		$combinedFileID =  File::join_paths($this->getCombinedFilesFolder(), $combinedFile);
1701
1702
		// Send file combination request to the backend, with an optional callback to perform regeneration
1703
		$minify = $this->getMinifyCombinedJSFiles();
1704
		$combinedURL = $this
1705
			->getAssetHandler()
1706
			->getContentURL(
1707
				$combinedFileID,
1708
				function() use ($fileList, $minify, $type) {
1709
					// Physically combine all file content
1710
					$combinedData = '';
1711
					$base = Director::baseFolder() . '/';
1712
					$minifier = Injector::inst()->get('Requirements_Minifier');
1713
					foreach($fileList as $file) {
1714
						$fileContent = file_get_contents($base . $file);
1715
						// Use configured minifier
1716
						if($minify) {
1717
							$fileContent = $minifier->minify($fileContent, $type, $file);
1718
			}
1719
1720
						if ($this->writeHeaderComment) {
1721
							// Write a header comment for each file for easier identification and debugging.
1722
							$combinedData .= "/****** FILE: $file *****/\n";
1723
					}
1724
						$combinedData .= $fileContent . "\n";
1725
				}
1726
					return $combinedData;
1727
			}
1728
			);
1729
1730
		// If the name isn't hashed, we will need to append the querystring m= parameter instead
1731
		// Since url won't be automatically suffixed, add it in here
1732
		if($hashQuerystring && $this->getSuffixRequirements()) {
1733
			$hash = $this->hashOfFiles($fileList);
1734
			$q = stripos($combinedURL, '?') === false ? '?' : '&';
1735
			$combinedURL .= "{$q}m={$hash}";
1736
		}
1737
1738
		return $combinedURL;
1739
	}
1740
1741
	/**
1742
	 * Given a filename and list of files, generate a new filename unique to these files
1743
	 *
1744
	 * @param string $combinedFile
1745
	 * @param array $fileList
1746
	 * @return string
1747
	 */
1748
	protected function hashedCombinedFilename($combinedFile, $fileList) {
1749
		$name = pathinfo($combinedFile, PATHINFO_FILENAME);
1750
		$hash = $this->hashOfFiles($fileList);
1751
		$extension = File::get_file_extension($combinedFile);
1752
		return $name . '-' . substr($hash, 0, 7) . '.' . $extension;
1753
	}
1754
1755
	/**
1756
	 * Check if combined files are enabled
1757
	 *
1758
	 * @return bool
1759
	 */
1760
	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...
1761
		if(!$this->combinedFilesEnabled) {
1762
			return false;
1763
		}
1764
1765
		// Tests should be combined
1766
		if(class_exists('SapphireTest', false) && SapphireTest::is_running_test()) {
1767
			return true;
1768
		}
1769
1770
		// Check if specified via querystring
1771
		if(isset($_REQUEST['combine'])) {
1772
			return true;
1773
		}
1774
1775
		// Non-dev sites are always combined
1776
		if(!Director::isDev()) {
1777
			return true;
1778
		}
1779
1780
		// Fallback to default
1781
		return Config::inst()->get(__CLASS__, 'combine_in_dev');
1782
	}
1783
1784
	/**
1785
	 * For a given filelist, determine some discriminating value to determine if
1786
	 * any of these files have changed.
1787
	 *
1788
	 * @param array $fileList List of files
1789
	 * @return string SHA1 bashed file hash
1790
	 */
1791
	protected function hashOfFiles($fileList) {
1792
		// Get hash based on hash of each file
1793
		$base = Director::baseFolder() . '/';
1794
		$hash = '';
1795
		foreach($fileList as $file) {
1796
			if(file_exists($base . $file)) {
1797
				$hash .= sha1_file($base . $file);
1798
			} else {
1799
				throw new InvalidArgumentException("Combined file {$file} does not exist");
1800
			}
1801
		}
1802
		return sha1($hash);
1803
	}
1804
1805
	/**
1806
	 * Registers the given themeable stylesheet as required.
1807
	 *
1808
	 * A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
1809
	 * and it that doesn't exist and the module parameter is set then a CSS file with that name in
1810
	 * the module is used.
1811
	 *
1812
	 * @param string $name   The name of the file - eg '/css/File.css' would have the name 'File'
1813
	 * @param string $module The module to fall back to if the css file does not exist in the
1814
	 *                       current theme.
1815
	 * @param string $media  Comma-separated list of media types to use in the link tag
1816
	 *                       (e.g. 'screen,projector')
1817
	 */
1818
	public function themedCSS($name, $module = null, $media = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $module is not used and could be removed.

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

Loading history...
1819
		$css = "/css/$name.css";
1820
1821
		$project = project();
1822
		$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
1823
		$absproject = $absbase . $project;
1824
1825
		if(file_exists($absproject . $css)) {
1826
			return $this->css($project . $css, $media);
1827
		}
1828
1829
		foreach(SSViewer::get_themes() as $theme) {
0 ignored issues
show
Bug introduced by
The expression \SSViewer::get_themes() of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1830
			$path = TemplateLoader::instance()->getPath($theme);
1831
			$abspath = BASE_PATH . '/' . $path;
1832
1833
			if(file_exists($abspath . $css)) {
1834
				return $this->css($path . $css, $media);
1835
			}
1836
		}
1837
        throw new \InvalidArgumentException(
1838
            "The css file doesn't exists. Please check if the file $name.css exists in any context or search for "
1839
            . "themedCSS references calling this file in your templates."
1840
        );
1841
	}
1842
1843
	/**
1844
	 * Registers the given themeable javascript as required.
1845
	 *
1846
	 * A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
1847
	 * and it that doesn't exist and the module parameter is set then a javascript file with that name in
1848
	 * the module is used.
1849
	 *
1850
	 * @param string $name   The name of the file - eg '/js/File.js' would have the name 'File'
1851
	 * @param string $module The module to fall back to if the javascript file does not exist in the
1852
	 *                       current theme.
1853
	 * @param string $type  Comma-separated list of types to use in the script tag
1854
	 *                       (e.g. 'text/javascript,text/ecmascript')
1855
	 */
1856
	public function themedJavascript($name, $module = null, $type = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $module is not used and could be removed.

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

Loading history...
1857
        $js = "/javascript/$name.js";
1858
1859
        $opts = array(
1860
            'type' => $type,
1861
        );
1862
1863
        $project = project();
1864
        $absbase = BASE_PATH . DIRECTORY_SEPARATOR;
1865
        $absproject = $absbase . $project;
1866
1867
        if(file_exists($absproject . $js)) {
1868
            return $this->javascript($project . $js, $opts);
1869
        }
1870
1871
        foreach(SSViewer::get_themes() as $theme) {
0 ignored issues
show
Bug introduced by
The expression \SSViewer::get_themes() of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1872
            $path = TemplateLoader::instance()->getPath($theme);
1873
            $abspath = BASE_PATH . '/' . $path;
1874
1875
            if(file_exists($abspath . $js)) {
1876
                return $this->javascript($path . $js, $opts);
1877
            }
1878
        }
1879
		throw new \InvalidArgumentException(
1880
		    "The javascript file doesn't exists. Please check if the file $name.js exists in any context or search for "
1881
            . "themedJavascript references calling this file in your templates."
1882
        );
1883
	}
1884
1885
	/**
1886
	 * Output debugging information.
1887
	 */
1888
	public function debug() {
1889
		Debug::show($this->javascript);
1890
		Debug::show($this->css);
1891
		Debug::show($this->customCSS);
1892
		Debug::show($this->customScript);
1893
		Debug::show($this->customHeadTags);
1894
		Debug::show($this->combinedFiles);
1895
	}
1896
1897
}
1898
1899
/**
1900
 * Provides an abstract interface for minifying content
1901
 */
1902
interface Requirements_Minifier {
1903
1904
	/**
1905
	 * Minify the given content
1906
	 *
1907
	 * @param string $content
1908
	 * @param string $type Either js or css
1909
	 * @param string $filename Name of file to display in case of error
1910
	 * @return string minified content
1911
	 */
1912
	public function minify($content, $type, $filename);
1913
}
1914