Completed
Push — master ( ca7fa2...9e9c9a )
by Nazar
04:04
created

Includes::js()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2014-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\Page;
9
use
10
	cs\App,
11
	cs\Core,
12
	cs\Config,
13
	cs\Event,
14
	cs\Language,
15
	cs\Request,
16
	cs\User,
17
	h,
18
	cs\Page\Includes\RequireJS;
19
20
/**
21
 * Includes management for `cs\Page` class
22
 *
23
 * @property string $Title
24
 * @property string $Description
25
 * @property string $canonical_url
26
 * @property string $Head
27
 * @property string $post_Body
28
 * @property string $theme
29
 */
30
trait Includes {
31
	use
32
		RequireJS;
33
	/**
34
	 * @var array[]
35
	 */
36
	protected $core_html;
37
	/**
38
	 * @var array[]
39
	 */
40
	protected $core_js;
41
	/**
42
	 * @var array[]
43
	 */
44
	protected $core_css;
45
	/**
46
	 * @var string
47
	 */
48
	protected $core_config;
49
	/**
50
	 * @var array[]
51
	 */
52
	protected $html;
53
	/**
54
	 * @var array[]
55
	 */
56
	protected $js;
57
	/**
58
	 * @var array[]
59
	 */
60
	protected $css;
61
	/**
62
	 * @var string
63
	 */
64
	protected $config;
65
	/**
66
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
67
	 * @var string
68
	 */
69
	protected $pcache_basename;
70
	protected function init_includes () {
71
		$this->core_html       = [0 => [], 1 => []];
72
		$this->core_js         = [0 => [], 1 => []];
73
		$this->core_css        = [0 => [], 1 => []];
74
		$this->core_config     = '';
75
		$this->html            = [0 => [], 1 => []];
76
		$this->js              = [0 => [], 1 => []];
77
		$this->css             = [0 => [], 1 => []];
78
		$this->config          = '';
79
		$this->pcache_basename = '';
80
	}
81
	/**
82
	 * Including of Web Components
83
	 *
84
	 * @param string|string[] $add  Path to including file, or code
85
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
86
	 *
87
	 * @return \cs\Page
88
	 */
89
	function html ($add, $mode = 'file') {
90
		return $this->html_internal($add, $mode);
91
	}
92
	/**
93
	 * @param string|string[] $add
94
	 * @param string          $mode
95
	 * @param bool            $core
96
	 *
97
	 * @return \cs\Page
98
	 */
99
	protected function html_internal ($add, $mode = 'file', $core = false) {
100
		if (!$add) {
101
			return $this;
102
		}
103
		if (is_array($add)) {
104
			foreach (array_filter($add) as $script) {
105
				$this->html_internal($script, $mode, $core);
106
			}
107
		} else {
108
			if ($core) {
109
				$html = &$this->core_html;
110
			} else {
111
				$html = &$this->html;
112
			}
113
			if ($mode == 'file') {
114
				$html[0][] = h::link(
115
					[
116
						'href' => $add,
117
						'rel'  => 'import'
118
					]
119
				);
120
			} elseif ($mode == 'code') {
121
				$html[1][] = "$add\n";
122
			}
123
		}
124
		return $this;
125
	}
126
	/**
127
	 * Including of JavaScript
128
	 *
129
	 * @param string|string[] $add  Path to including file, or code
130
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
131
	 *
132
	 * @return \cs\Page
133
	 */
134
	function js ($add, $mode = 'file') {
135
		return $this->js_internal($add, $mode);
136
	}
137
	/**
138
	 * @param string|string[] $add
139
	 * @param string          $mode
140
	 * @param bool            $core
141
	 *
142
	 * @return \cs\Page
143
	 */
144
	protected function js_internal ($add, $mode = 'file', $core = false) {
145
		if (!$add) {
146
			return $this;
147
		}
148
		if (is_array($add)) {
149
			foreach (array_filter($add) as $script) {
150
				$this->js_internal($script, $mode, $core);
151
			}
152
		} else {
153
			if ($core) {
154
				$js = &$this->core_js;
155
			} else {
156
				$js = &$this->js;
157
			}
158
			if ($mode == 'file') {
159
				$js[0][] = h::script(
160
					[
161
						'src' => $add
162
					]
163
				);
164
			} elseif ($mode == 'code') {
165
				$js[1][] = "$add\n";
166
			}
167
		}
168
		return $this;
169
	}
170
	/**
171
	 * Including of CSS
172
	 *
173
	 * @param string|string[] $add  Path to including file, or code
174
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
175
	 *
176
	 * @return \cs\Page
177
	 */
178
	function css ($add, $mode = 'file') {
179
		return $this->css_internal($add, $mode);
180
	}
181
	/**
182
	 * @param string|string[] $add
183
	 * @param string          $mode
184
	 * @param bool            $core
185
	 *
186
	 * @return \cs\Page
187
	 */
188
	protected function css_internal ($add, $mode = 'file', $core = false) {
189
		if (!$add) {
190
			return $this;
191
		}
192
		if (is_array($add)) {
193
			foreach (array_filter($add) as $style) {
194
				$this->css_internal($style, $mode, $core);
195
			}
196
		} else {
197
			if ($core) {
198
				$css = &$this->core_css;
199
			} else {
200
				$css = &$this->css;
201
			}
202
			if ($mode == 'file') {
203
				$css[0][] = h::link(
204
					[
205
						'href'           => $add,
206
						'rel'            => 'stylesheet',
207
						'shim-shadowdom' => true
208
					]
209
				);
210
			} elseif ($mode == 'code') {
211
				$css[1][] = "$add\n";
212
			}
213
		}
214
		return $this;
215
	}
216
	/**
217
	 * Add config on page to make it available on frontend
218
	 *
219
	 * @param mixed  $config_structure        Any scalar type or array
220
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
221
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
222
	 *
223
	 * @return \cs\Page
224
	 */
225
	function config ($config_structure, $target) {
226
		return $this->config_internal($config_structure, $target);
227
	}
228
	/**
229
	 * @param mixed  $config_structure
230
	 * @param string $target
231
	 * @param bool   $core
232
	 *
233
	 * @return \cs\Page
234
	 */
235
	protected function config_internal ($config_structure, $target, $core = false) {
236
		$config = h::script(
237
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
238
			[
239
				'target' => $target,
240
				'class'  => 'cs-config',
241
				'type'   => 'application/json'
242
			]
243
		);
244
		if ($core) {
245
			$this->core_config .= $config;
246
		} else {
247
			$this->config .= $config;
248
		}
249
		return $this;
250
	}
251
	/**
252
	 * Getting of HTML, JS and CSS includes
253
	 *
254
	 * @return \cs\Page
255
	 */
256
	protected function add_includes_on_page () {
257
		$Config = Config::instance(true);
258
		if (!$Config) {
259
			return $this;
260
		}
261
		/**
262
		 * Base name for cache files
263
		 */
264
		$this->pcache_basename = "_{$this->theme}_".Language::instance()->clang;
265
		/**
266
		 * Some JS configs required by system
267
		 */
268
		$this->add_system_configs();
269
		// TODO: I hope some day we'll get rid of this sh*t :(
270
		$this->ie_edge();
271
		$Request = Request::instance();
272
		/**
273
		 * If CSS and JavaScript compression enabled
274
		 */
275
		if ($Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']))) {
276
			$this->webcomponents_polyfill($Request, true);
277
			$includes = $this->get_includes_for_page_with_compression();
278
		} else {
279
			$this->webcomponents_polyfill($Request, false);
280
			/**
281
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
282
			 */
283
			/**
284
			 * @var \cs\Page $this
285
			 */
286
			$this->config_internal(Language::instance(), 'cs.Language', true);
287
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
288
			$includes = $this->get_includes_for_page_without_compression($Config);
289
		}
290
		$this->css_internal($includes['css'], 'file', true);
291
		$this->js_internal($includes['js'], 'file', true);
292
		$this->html_internal($includes['html'], 'file', true);
293
		$this->add_includes_on_page_manually_added($Config);
294
		return $this;
295
	}
296
	/**
297
	 * @param string|string[] $path
298
	 *
299
	 * @return string|string[]
300
	 */
301
	protected function absolute_path_to_relative ($path) {
302
		return strpos($path, DIR.'/') === 0 ? substr($path, strlen(DIR) + 1) : $path;
303
	}
304
	/**
305
	 * Add JS polyfills for IE/Edge
306
	 */
307
	protected function ie_edge () {
308
		if (preg_match('/Trident|Edge/', Request::instance()->header('user-agent'))) {
309
			$this->js_internal(
310
				get_files_list(DIR."/includes/js/microsoft_sh*t", "/.*\\.js$/i", 'f', "includes/js/microsoft_sh*t", true),
311
				'file',
312
				true
313
			);
314
		}
315
	}
316
	/**
317
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
318
	 *
319
	 * @param Request $Request
320
	 * @param bool    $with_compression
321
	 */
322
	protected function webcomponents_polyfill ($Request, $with_compression) {
323
		if ($Request->cookie('shadow_dom') != 1) {
324
			$file = 'includes/js/WebComponents-polyfill/webcomponents-custom.min.js';
325
			if ($with_compression) {
326
				$compressed_file = PUBLIC_CACHE.'/webcomponents.js';
327
				if (!file_exists($compressed_file)) {
328
					$content = file_get_contents(DIR."/$file");
329
					file_put_contents($compressed_file, gzencode($content, 9), LOCK_EX | FILE_BINARY);
330
					file_put_contents("$compressed_file.hash", substr(md5($content), 0, 5));
331
				}
332
				$hash = file_get_contents("$compressed_file.hash");
333
				$this->js_internal("storage/pcache/webcomponents.js?$hash", 'file', true);
334
			} else {
335
				$this->js_internal($file, 'file', true);
336
			}
337
		}
338
	}
339
	protected function add_system_configs () {
340
		$Config         = Config::instance();
341
		$Request        = Request::instance();
342
		$User           = User::instance();
343
		$current_module = $Request->current_module;
344
		$this->config_internal(
345
			[
346
				'base_url'              => $Config->base_url(),
347
				'current_base_url'      => $Config->base_url().'/'.($Request->admin_path ? 'admin/' : '').$current_module,
348
				'public_key'            => Core::instance()->public_key,
349
				'module'                => $current_module,
350
				'in_admin'              => (int)$Request->admin_path,
351
				'is_admin'              => (int)$User->admin(),
352
				'is_user'               => (int)$User->user(),
353
				'is_guest'              => (int)$User->guest(),
354
				'password_min_length'   => (int)$Config->core['password_min_length'],
355
				'password_min_strength' => (int)$Config->core['password_min_strength'],
356
				'debug'                 => (int)DEBUG,
357
				'route'                 => $Request->route,
358
				'route_path'            => $Request->route_path,
359
				'route_ids'             => $Request->route_ids
360
			],
361
			'cs',
362
			true
363
		);
364
		if ($User->admin()) {
365
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
366
		}
367
	}
368
	/**
369
	 * @return array[]
370
	 */
371
	protected function get_includes_for_page_with_compression () {
372
		/**
373
		 * Rebuild cache if necessary
374
		 */
375
		if (!file_exists(PUBLIC_CACHE."/$this->pcache_basename.json")) {
376
			$this->rebuild_cache();
377
		}
378
		list($dependencies, $structure) = file_get_json(PUBLIC_CACHE."/$this->pcache_basename.json");
379
		$system_includes = [
380
			'css'  => ["storage/pcache/$this->pcache_basename.css?{$structure['']['css']}"],
381
			'js'   => ["storage/pcache/$this->pcache_basename.js?{$structure['']['js']}"],
382
			'html' => ["storage/pcache/$this->pcache_basename.html?{$structure['']['html']}"]
383
		];
384
		list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '+');
385
		foreach ($structure as $filename_prefix => $hashes) {
386
			if (!$filename_prefix) {
387
				continue;
388
			}
389
			$is_dependency = $this->get_includes_is_dependency($dependencies, $filename_prefix, '+');
390
			if ($is_dependency || mb_strpos($current_url, $filename_prefix) === 0) {
391
				foreach ($hashes as $extension => $hash) {
392
					if ($is_dependency) {
393
						$dependencies_includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
394
					} else {
395
						$includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
396
					}
397
				}
398
			}
399
		}
400
		return array_merge_recursive($system_includes, $dependencies_includes, $includes);
401
	}
402
	/**
403
	 * @param Config $Config
404
	 *
405
	 * @return array[]
406
	 */
407
	protected function get_includes_for_page_without_compression ($Config) {
408
		// To determine all dependencies and stuff we need `$Config` object to be already created
409
		if ($Config) {
410
			list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
411
			$system_includes = $includes_map[''];
412
			list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '/');
413
			foreach ($includes_map as $url => $local_includes) {
414
				if (!$url) {
415
					continue;
416
				}
417
				$is_dependency = $this->get_includes_is_dependency($dependencies, $url, '/');
418
				if ($is_dependency) {
419
					$dependencies_includes = array_merge_recursive($dependencies_includes, $local_includes);
420
				} elseif (mb_strpos($current_url, $url) === 0) {
421
					$includes = array_merge_recursive($includes, $local_includes);
422
				}
423
			}
424
			$includes = array_merge_recursive($system_includes, $dependencies_includes, $includes);
425
			$includes = $this->absolute_path_to_relative($includes);
426
		} else {
427
			$includes = $this->get_includes_list();
428
		}
429
		return $this->add_versions_hash($includes);
430
	}
431
	/**
432
	 * @param array  $dependencies
433
	 * @param string $separator `+` or `/`
434
	 *
435
	 * @return array
436
	 */
437
	protected function get_includes_prepare ($dependencies, $separator) {
438
		$Request               = Request::instance();
439
		$includes              = [
440
			'css'  => [],
441
			'js'   => [],
442
			'html' => []
443
		];
444
		$dependencies_includes = $includes;
445
		$current_module        = $Request->current_module;
446
		/**
447
		 * Current URL based on controller path (it better represents how page was rendered)
448
		 */
449
		$current_url = array_slice(App::instance()->controller_path, 1);
450
		$current_url = ($Request->admin_path ? "admin$separator" : '')."$current_module$separator".implode($separator, $current_url);
451
		/**
452
		 * Narrow the dependencies to current module only
453
		 */
454
		$dependencies = array_merge(
455
			isset($dependencies[$current_module]) ? $dependencies[$current_module] : [],
456
			$dependencies['System']
457
		);
458
		return [$includes, $dependencies_includes, $dependencies, $current_url];
459
	}
460
	/**
461
	 * @param array  $dependencies
462
	 * @param string $url
463
	 * @param string $separator `+` or `/`
464
	 *
465
	 * @return bool
466
	 */
467
	protected function get_includes_is_dependency ($dependencies, $url, $separator) {
468
		$url_exploded = explode($separator, $url);
469
		/** @noinspection NestedTernaryOperatorInspection */
470
		$url_module = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
471
		$Request    = Request::instance();
472
		return
473
			$url_module !== Config::SYSTEM_MODULE &&
474
			in_array($url_module, $dependencies) &&
475
			(
476
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
477
			);
478
	}
479
	protected function add_versions_hash ($includes) {
480
		$content = '';
481
		foreach (get_files_list(MODULES, false, 'd') as $module) {
482
			if (file_exists(MODULES."/$module/meta.json")) {
483
				$content .= file_get_contents(MODULES."/$module/meta.json");
484
			}
485
		}
486
		foreach (get_files_list(PLUGINS, false, 'd') as $plugin) {
487
			if (file_exists(PLUGINS."/$plugin/meta.json")) {
488
				$content .= file_get_contents(PLUGINS."/$plugin/meta.json");
489
			}
490
		}
491
		$hash = substr(md5($content), 0, 5);
492
		foreach ($includes as &$files) {
493
			foreach ($files as &$file) {
494
				$file .= "?$hash";
495
			}
496
			unset($file);
497
		}
498
		return $includes;
499
	}
500
	/**
501
	 * @param Config $Config
502
	 */
503
	protected function add_includes_on_page_manually_added ($Config) {
504
		foreach (['core_html', 'core_js', 'core_css', 'html', 'js', 'css'] as $type) {
505
			foreach ($this->$type as &$elements) {
506
				$elements = implode('', array_unique($elements));
507
			}
508
			unset($elements);
509
		}
510
		$this->Head .=
511
			$this->core_config.
512
			$this->config.
513
			$this->core_css[0].$this->css[0].
514
			h::style($this->core_css[1].$this->css[1] ?: false);
515
		$js_html_insert_to = $Config->core['put_js_after_body'] ? 'post_Body' : 'Head';
516
		$js_html           =
517
			$this->core_js[0].
518
			h::script($this->core_js[1] ?: false).
519
			$this->js[0].
520
			h::script($this->js[1] ?: false).
521
			$this->core_html[0].$this->html[0].
522
			$this->core_html[1].$this->html[1];
523
		$this->$js_html_insert_to .= $js_html;
524
	}
525
	/**
526
	 * Getting of HTML, JS and CSS files list to be included
527
	 *
528
	 * @param bool $absolute If <i>true</i> - absolute paths to files will be returned
529
	 *
530
	 * @return string[][]
1 ignored issue
show
Documentation introduced by
Should the return type not be string|string[]|array<string,string[]>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
531
	 */
532
	protected function get_includes_list ($absolute = false) {
533
		$includes = [];
534
		/**
535
		 * Get includes of system and theme
536
		 */
537
		$this->get_includes_list_add_includes(DIR.'/includes', $includes);
538
		$this->get_includes_list_add_includes(THEMES."/$this->theme", $includes);
539
		$Config = Config::instance();
540
		foreach ($Config->components['modules'] as $module_name => $module_data) {
541
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
542
				continue;
543
			}
544
			$this->get_includes_list_add_includes(MODULES."/$module_name/includes", $includes);
545
		}
546
		foreach ($Config->components['plugins'] as $plugin_name) {
547
			$this->get_includes_list_add_includes(PLUGINS."/$plugin_name/includes", $includes);
548
		}
549
		$includes = [
550
			'html' => array_merge(...$includes['html']),
551
			'js'   => array_merge(...$includes['js']),
552
			'css'  => array_merge(...$includes['css'])
553
		];
554
		if (!$absolute) {
555
			$includes = $this->absolute_path_to_relative($includes);
1 ignored issue
show
Documentation introduced by
$includes is of type array<string,array<integ...rray<integer,string>"}>, but the function expects a string|array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
556
		}
557
		return $includes;
558
	}
559
	/**
560
	 * @param string     $base_dir
561
	 * @param string[][] $includes
562
	 */
563
	protected function get_includes_list_add_includes ($base_dir, &$includes) {
564
		$includes['html'][] = $this->get_includes_list_add_includes_internal($base_dir, 'html');
565
		$includes['js'][]   = $this->get_includes_list_add_includes_internal($base_dir, 'js');
566
		$includes['css'][]  = $this->get_includes_list_add_includes_internal($base_dir, 'css');
567
	}
568
	/**
569
	 * @param string $base_dir
570
	 * @param string $ext
571
	 *
572
	 * @return array
573
	 */
574
	protected function get_includes_list_add_includes_internal ($base_dir, $ext) {
575
		return get_files_list("$base_dir/$ext", "/.*\\.$ext\$/i", 'f', true, true, 'name', '!include') ?: [];
576
	}
577
	/**
578
	 * Rebuilding of HTML, JS and CSS cache
579
	 *
580
	 * @return \cs\Page
581
	 */
582
	protected function rebuild_cache () {
583
		list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
584
		$structure = [];
585
		foreach ($includes_map as $filename_prefix => $includes) {
586
			// We replace `/` by `+` to make it suitable for filename
587
			$filename_prefix             = str_replace('/', '+', $filename_prefix);
588
			$structure[$filename_prefix] = $this->create_cached_includes_files($filename_prefix, $includes);
589
		}
590
		unset($includes_map, $filename_prefix, $includes);
591
		file_put_json(
592
			PUBLIC_CACHE."/$this->pcache_basename.json",
593
			[$dependencies, $structure]
594
		);
595
		unset($structure);
596
		Event::instance()->fire('System/Page/rebuild_cache');
597
		return $this;
598
	}
599
	/**
600
	 * Creates cached version of given HTML, JS and CSS files.
601
	 * Resulting file name consists of <b>$filename_prefix</b> and <b>$this->pcache_basename</b>
602
	 *
603
	 * @param string $filename_prefix
604
	 * @param array  $includes Array of paths to files, may have keys: <b>css</b> and/or <b>js</b> and/or <b>html</b>
605
	 *
606
	 * @return array
607
	 */
608
	protected function create_cached_includes_files ($filename_prefix, $includes) {
609
		$cache_hash = [];
610
		/** @noinspection AlterInForeachInspection */
611
		foreach ($includes as $extension => $files) {
612
			$content = $this->create_cached_includes_files_process_files(
613
				$extension,
614
				$filename_prefix,
615
				$files
616
			);
617
			file_put_contents(PUBLIC_CACHE."/$filename_prefix$this->pcache_basename.$extension", gzencode($content, 9), LOCK_EX | FILE_BINARY);
618
			$cache_hash[$extension] = substr(md5($content), 0, 5);
619
		}
620
		return $cache_hash;
621
	}
622
	protected function create_cached_includes_files_process_files ($extension, $filename_prefix, $files) {
623
		$content = '';
624
		switch ($extension) {
625
			/**
626
			 * Insert external elements into resulting css file.
627
			 * It is needed, because those files will not be copied into new destination of resulting css file.
628
			 */
629
			case 'css':
630
				$callback = function ($content, $file) {
631
					return
632
						$content.
633
						Includes_processing::css(
634
							file_get_contents($file),
635
							$file
636
						);
637
				};
638
				break;
639
			/**
640
			 * Combine css and js files for Web Component into resulting files in order to optimize loading process
641
			 */
642
			case 'html':
643
				/**
644
				 * For CSP-compatible HTML files we need to know destination to put there additional JS/CSS files
645
				 */
646
				$destination = Config::instance()->core['vulcanization'] ? false : PUBLIC_CACHE;
647
				$callback    = function ($content, $file) use ($filename_prefix, $destination) {
648
					return
649
						$content.
650
						Includes_processing::html(
651
							file_get_contents($file),
652
							$file,
653
							"$filename_prefix$this->pcache_basename-".basename($file).'+'.substr(md5($file), 0, 5),
654
							$destination
655
						);
656
				};
657
				break;
658
			case 'js':
659
				$callback = function ($content, $file) {
660
					return
661
						$content.
662
						Includes_processing::js(file_get_contents($file));
663
				};
664
				if ($filename_prefix == '') {
665
					$content = 'window.cs={Language:'._json_encode(Language::instance()).'};';
666
					$content .= 'window.requirejs={paths:'._json_encode($this->get_requirejs_paths()).'};';
667
				}
668
		}
669
		/** @noinspection PhpUndefinedVariableInspection */
670
		return array_reduce(array_filter($files, 'file_exists'), $callback, $content);
671
	}
672
	/**
673
	 * Get dependencies of components between each other (only that contains some HTML, JS and CSS files) and mapping HTML, JS and CSS files to URL paths
674
	 *
675
	 * @return array[] [$dependencies, $includes_map]
676
	 */
677
	protected function includes_dependencies_and_map () {
678
		/**
679
		 * Get all includes
680
		 */
681
		$all_includes = $this->get_includes_list(true);
682
		$includes_map = [];
683
		/**
684
		 * Array [package => [list of packages it depends on]]
685
		 */
686
		$dependencies    = [];
687
		$functionalities = [];
688
		/**
689
		 * According to components's maps some files should be included only on specific pages.
690
		 * Here we read this rules, and remove from whole includes list such items, that should be included only on specific pages.
691
		 * Also collect dependencies.
692
		 */
693
		$Config = Config::instance();
694
		foreach ($Config->components['modules'] as $module_name => $module_data) {
695
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
696
				continue;
697
			}
698
			if (file_exists(MODULES."/$module_name/meta.json")) {
699
				$this->process_meta(
700
					file_get_json(MODULES."/$module_name/meta.json"),
701
					$dependencies,
702
					$functionalities
703
				);
704
			}
705
			if (file_exists(MODULES."/$module_name/includes/map.json")) {
706
				$this->process_map(
707
					file_get_json_nocomments(MODULES."/$module_name/includes/map.json"),
708
					MODULES."/$module_name/includes",
709
					$includes_map,
710
					$all_includes
1 ignored issue
show
Bug introduced by
It seems like $all_includes defined by $this->get_includes_list(true) on line 681 can also be of type string; however, cs\Page\Includes::process_map() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
711
				);
712
			}
713
		}
714
		unset($module_name, $module_data);
715
		foreach ($Config->components['plugins'] as $plugin_name) {
716
			if (file_exists(PLUGINS."/$plugin_name/meta.json")) {
717
				$this->process_meta(
718
					file_get_json(PLUGINS."/$plugin_name/meta.json"),
719
					$dependencies,
720
					$functionalities
721
				);
722
			}
723
			if (file_exists(PLUGINS."/$plugin_name/includes/map.json")) {
724
				$this->process_map(
725
					file_get_json_nocomments(PLUGINS."/$plugin_name/includes/map.json"),
726
					PLUGINS."/$plugin_name/includes",
727
					$includes_map,
728
					$all_includes
1 ignored issue
show
Bug introduced by
It seems like $all_includes defined by $this->get_includes_list(true) on line 681 can also be of type string; however, cs\Page\Includes::process_map() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
729
				);
730
			}
731
		}
732
		unset($plugin_name);
733
		/**
734
		 * For consistency
735
		 */
736
		$includes_map[''] = $all_includes;
737
		Event::instance()->fire(
738
			'System/Page/includes_dependencies_and_map',
739
			[
740
				'dependencies' => &$dependencies,
741
				'includes_map' => &$includes_map
742
			]
743
		);
744
		$dependencies = $this->normalize_dependencies($dependencies, $functionalities);
745
		$includes_map = $this->clean_includes_arrays_without_files($dependencies, $includes_map);
746
		$dependencies = array_map('array_values', $dependencies);
747
		$dependencies = array_filter($dependencies);
748
		return [$dependencies, $includes_map];
749
	}
750
	/**
751
	 * Process meta information and corresponding entries to dependencies and functionalities
752
	 *
753
	 * @param array $meta
754
	 * @param array $dependencies
755
	 * @param array $functionalities
756
	 */
757
	protected function process_meta ($meta, &$dependencies, &$functionalities) {
758
		$package = $meta['package'];
759
		if (isset($meta['require'])) {
760
			foreach ((array)$meta['require'] as $r) {
761
				/**
762
				 * Get only name of package or functionality
763
				 */
764
				$r                        = preg_split('/[=<>]/', $r, 2)[0];
765
				$dependencies[$package][] = $r;
766
			}
767
		}
768
		if (isset($meta['optional'])) {
769
			foreach ((array)$meta['optional'] as $o) {
770
				/**
771
				 * Get only name of package or functionality
772
				 */
773
				$o                        = preg_split('/[=<>]/', $o, 2)[0];
774
				$dependencies[$package][] = $o;
775
			}
776
		}
777
		if (isset($meta['provide'])) {
778
			foreach ((array)$meta['provide'] as $p) {
779
				/**
780
				 * If provides sub-functionality for other component (for instance, `Blog/post_patch`) - inverse "providing" to "dependency"
781
				 * Otherwise it is just functionality alias to package name
782
				 */
783
				if (strpos($p, '/') !== false) {
784
					/**
785
					 * Get name of package or functionality
786
					 */
787
					$p                  = explode('/', $p)[0];
788
					$dependencies[$p][] = $package;
789
				} else {
790
					$functionalities[$p] = $package;
791
				}
792
			}
793
		}
794
	}
795
	/**
796
	 * Process map structure, fill includes map and remove files from list of all includes (remaining files will be included on all pages)
797
	 *
798
	 * @param array  $map
799
	 * @param string $includes_dir
800
	 * @param array  $includes_map
801
	 * @param array  $all_includes
802
	 */
803
	protected function process_map ($map, $includes_dir, &$includes_map, &$all_includes) {
804
		foreach ($map as $path => $files) {
805
			foreach ((array)$files as $file) {
806
				$extension = file_extension($file);
807
				if (in_array($extension, ['css', 'js', 'html'])) {
808
					$file                              = "$includes_dir/$extension/$file";
809
					$includes_map[$path][$extension][] = $file;
810
					$all_includes[$extension]          = array_diff($all_includes[$extension], [$file]);
811
				} else {
812
					$file = rtrim($file, '*');
813
					/**
814
					 * Wildcard support, it is possible to specify just path prefix and all files with this prefix will be included
815
					 */
816
					$found_files = array_filter(
817
						get_files_list($includes_dir, '/.*\.(css|js|html)$/i', 'f', '', true, 'name', '!include') ?: [],
818
						function ($f) use ($file) {
819
							// We need only files with specified mask and only those located in directory that corresponds to file's extension
820
							return preg_match("#^(css|js|html)/$file.*\\1$#i", $f);
821
						}
822
					);
823
					// Drop first level directory
824
					$found_files = _preg_replace('#^[^/]+/(.*)#', '$1', $found_files);
825
					$this->process_map([$path => $found_files], $includes_dir, $includes_map, $all_includes);
826
				}
827
			}
828
		}
829
	}
830
	/**
831
	 * Replace functionalities by real packages names, take into account recursive dependencies
832
	 *
833
	 * @param array $dependencies
834
	 * @param array $functionalities
835
	 *
836
	 * @return array
837
	 */
838
	protected function normalize_dependencies ($dependencies, $functionalities) {
839
		/**
840
		 * First of all remove packages without any dependencies
841
		 */
842
		$dependencies = array_filter($dependencies);
843
		/**
844
		 * First round, process aliases among keys
845
		 */
846
		foreach (array_keys($dependencies) as $d) {
847
			if (isset($functionalities[$d])) {
848
				$package = $functionalities[$d];
849
				/**
850
				 * Add dependencies to existing package dependencies
851
				 */
852
				foreach ($dependencies[$d] as $dependency) {
853
					$dependencies[$package][] = $dependency;
854
				}
855
				/**
856
				 * Drop alias
857
				 */
858
				unset($dependencies[$d]);
859
			}
860
		}
861
		unset($d, $dependency);
862
		/**
863
		 * Second round, process aliases among dependencies
864
		 */
865
		foreach ($dependencies as &$depends_on) {
866
			foreach ($depends_on as &$dependency) {
867
				if (isset($functionalities[$dependency])) {
868
					$dependency = $functionalities[$dependency];
869
				}
870
			}
871
		}
872
		unset($depends_on, $dependency);
873
		/**
874
		 * Third round, process recursive dependencies
875
		 */
876
		foreach ($dependencies as &$depends_on) {
877
			foreach ($depends_on as &$dependency) {
878
				if ($dependency != 'System' && isset($dependencies[$dependency])) {
879
					foreach (array_diff($dependencies[$dependency], $depends_on) as $new_dependency) {
880
						$depends_on[] = $new_dependency;
881
					}
882
				}
883
			}
884
		}
885
		return array_map('array_unique', $dependencies);
886
	}
887
	/**
888
	 * Includes array is composed from dependencies and sometimes dependencies doesn't have any files, so we'll clean that
889
	 *
890
	 * @param array $dependencies
891
	 * @param array $includes_map
892
	 *
893
	 * @return array
894
	 */
895
	protected function clean_includes_arrays_without_files ($dependencies, $includes_map) {
896
		foreach ($dependencies as &$depends_on) {
897
			foreach ($depends_on as $index => &$dependency) {
898
				if (!isset($includes_map[$dependency])) {
899
					unset($depends_on[$index]);
900
				}
901
			}
902
			unset($dependency);
903
		}
904
		return $includes_map;
905
	}
906
}
907