Completed
Push — master ( 4697b9...ca7fa2 )
by Nazar
04:04
created

Includes::add_includes_on_page_manually_added()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 22
Code Lines 19

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 6.9811
cc 7
eloc 19
nc 6
nop 1
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);
1 ignored issue
show
Bug introduced by
The method get_requirejs_paths() cannot be called from this context as it is declared protected in class cs\Page\Includes\RequireJS.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
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
	 * Since modules, plugins and storage directories can be (at least theoretically) moved from default location - let's do proper path conversion
298
	 *
299
	 * @param string|string[] $path
300
	 *
301
	 * @return string|string[]
302
	 */
303
	protected function absolute_path_to_relative ($path) {
304
		if (is_array($path)) {
305
			foreach ($path as &$p) {
306
				$p = $this->absolute_path_to_relative($p);
307
			}
308
			return $path;
309
		}
310
		if (strpos($path, MODULES) === 0) {
311
			return 'components/modules'.substr($path, strlen(MODULES));
312
		}
313
		if (strpos($path, PLUGINS) === 0) {
314
			return 'components/plugins'.substr($path, strlen(PLUGINS));
315
		}
316
		if (strpos($path, STORAGE) === 0) {
317
			return 'storage'.substr($path, strlen(STORAGE));
318
		}
319
		if (strpos($path, THEMES) === 0) {
320
			return 'themes'.substr($path, strlen(THEMES));
321
		}
322
		return substr($path, strlen(DIR) + 1);
323
	}
324
	/**
325
	 * Add JS polyfills for IE/Edge
326
	 */
327
	protected function ie_edge () {
328
		if (preg_match('/Trident|Edge/', Request::instance()->header('user-agent'))) {
329
			$this->js_internal(
330
				get_files_list(DIR."/includes/js/microsoft_sh*t", "/.*\\.js$/i", 'f', "includes/js/microsoft_sh*t", true),
331
				'file',
332
				true
333
			);
334
		}
335
	}
336
	/**
337
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
338
	 *
339
	 * @param Request $Request
340
	 * @param bool    $with_compression
341
	 */
342
	protected function webcomponents_polyfill ($Request, $with_compression) {
343
		if ($Request->cookie('shadow_dom') != 1) {
344
			$file = 'includes/js/WebComponents-polyfill/webcomponents-custom.min.js';
345
			if ($with_compression) {
346
				$compressed_file = PUBLIC_CACHE.'/webcomponents.js';
347
				if (!file_exists($compressed_file)) {
348
					$content = file_get_contents(DIR."/$file");
349
					file_put_contents($compressed_file, gzencode($content, 9), LOCK_EX | FILE_BINARY);
350
					file_put_contents("$compressed_file.hash", substr(md5($content), 0, 5));
351
				}
352
				$hash = file_get_contents("$compressed_file.hash");
353
				$this->js_internal("storage/pcache/webcomponents.js?$hash", 'file', true);
354
			} else {
355
				$this->js_internal($file, 'file', true);
356
			}
357
		}
358
	}
359
	protected function add_system_configs () {
360
		$Config         = Config::instance();
361
		$Request        = Request::instance();
362
		$User           = User::instance();
363
		$current_module = $Request->current_module;
364
		$this->config_internal(
365
			[
366
				'base_url'              => $Config->base_url(),
367
				'current_base_url'      => $Config->base_url().'/'.($Request->admin_path ? 'admin/' : '').$current_module,
368
				'public_key'            => Core::instance()->public_key,
369
				'module'                => $current_module,
370
				'in_admin'              => (int)$Request->admin_path,
371
				'is_admin'              => (int)$User->admin(),
372
				'is_user'               => (int)$User->user(),
373
				'is_guest'              => (int)$User->guest(),
374
				'password_min_length'   => (int)$Config->core['password_min_length'],
375
				'password_min_strength' => (int)$Config->core['password_min_strength'],
376
				'debug'                 => (int)DEBUG,
377
				'route'                 => $Request->route,
378
				'route_path'            => $Request->route_path,
379
				'route_ids'             => $Request->route_ids
380
			],
381
			'cs',
382
			true
383
		);
384
		if ($User->admin()) {
385
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
386
		}
387
	}
388
	/**
389
	 * @return array[]
390
	 */
391
	protected function get_includes_for_page_with_compression () {
392
		/**
393
		 * Rebuild cache if necessary
394
		 */
395
		if (!file_exists(PUBLIC_CACHE."/$this->pcache_basename.json")) {
396
			$this->rebuild_cache();
397
		}
398
		list($dependencies, $structure) = file_get_json(PUBLIC_CACHE."/$this->pcache_basename.json");
399
		$system_includes = [
400
			'css'  => ["storage/pcache/$this->pcache_basename.css?{$structure['']['css']}"],
401
			'js'   => ["storage/pcache/$this->pcache_basename.js?{$structure['']['js']}"],
402
			'html' => ["storage/pcache/$this->pcache_basename.html?{$structure['']['html']}"]
403
		];
404
		list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '+');
405
		foreach ($structure as $filename_prefix => $hashes) {
406
			if (!$filename_prefix) {
407
				continue;
408
			}
409
			$is_dependency = $this->get_includes_is_dependency($dependencies, $filename_prefix, '+');
410
			if ($is_dependency || mb_strpos($current_url, $filename_prefix) === 0) {
411
				foreach ($hashes as $extension => $hash) {
412
					if ($is_dependency) {
413
						$dependencies_includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
414
					} else {
415
						$includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
416
					}
417
				}
418
			}
419
		}
420
		return array_merge_recursive($system_includes, $dependencies_includes, $includes);
421
	}
422
	/**
423
	 * @param Config $Config
424
	 *
425
	 * @return array[]
426
	 */
427
	protected function get_includes_for_page_without_compression ($Config) {
428
		// To determine all dependencies and stuff we need `$Config` object to be already created
429
		if ($Config) {
430
			list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
431
			$system_includes = $includes_map[''];
432
			list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '/');
433
			foreach ($includes_map as $url => $local_includes) {
434
				if (!$url) {
435
					continue;
436
				}
437
				$is_dependency = $this->get_includes_is_dependency($dependencies, $url, '/');
438
				if ($is_dependency) {
439
					$dependencies_includes = array_merge_recursive($dependencies_includes, $local_includes);
440
				} elseif (mb_strpos($current_url, $url) === 0) {
441
					$includes = array_merge_recursive($includes, $local_includes);
442
				}
443
			}
444
			$includes = array_merge_recursive($system_includes, $dependencies_includes, $includes);
445
			$includes = $this->absolute_path_to_relative($includes);
446
		} else {
447
			$includes = $this->get_includes_list();
448
		}
449
		return $this->add_versions_hash($includes);
450
	}
451
	/**
452
	 * @param array  $dependencies
453
	 * @param string $separator `+` or `/`
454
	 *
455
	 * @return array
456
	 */
457
	protected function get_includes_prepare ($dependencies, $separator) {
458
		$Request               = Request::instance();
459
		$includes              = [
460
			'css'  => [],
461
			'js'   => [],
462
			'html' => []
463
		];
464
		$dependencies_includes = $includes;
465
		$current_module        = $Request->current_module;
466
		/**
467
		 * Current URL based on controller path (it better represents how page was rendered)
468
		 */
469
		$current_url = array_slice(App::instance()->controller_path, 1);
470
		$current_url = ($Request->admin_path ? "admin$separator" : '')."$current_module$separator".implode($separator, $current_url);
471
		/**
472
		 * Narrow the dependencies to current module only
473
		 */
474
		$dependencies = array_merge(
475
			isset($dependencies[$current_module]) ? $dependencies[$current_module] : [],
476
			$dependencies['System']
477
		);
478
		return [$includes, $dependencies_includes, $dependencies, $current_url];
479
	}
480
	/**
481
	 * @param array  $dependencies
482
	 * @param string $url
483
	 * @param string $separator `+` or `/`
484
	 *
485
	 * @return bool
486
	 */
487
	protected function get_includes_is_dependency ($dependencies, $url, $separator) {
488
		$url_exploded = explode($separator, $url);
489
		/** @noinspection NestedTernaryOperatorInspection */
490
		$url_module = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
491
		$Request    = Request::instance();
492
		return
493
			$url_module !== Config::SYSTEM_MODULE &&
494
			in_array($url_module, $dependencies) &&
495
			(
496
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
497
			);
498
	}
499
	protected function add_versions_hash ($includes) {
500
		$content = '';
501
		foreach (get_files_list(MODULES, false, 'd') as $module) {
502
			if (file_exists(MODULES."/$module/meta.json")) {
503
				$content .= file_get_contents(MODULES."/$module/meta.json");
504
			}
505
		}
506
		foreach (get_files_list(PLUGINS, false, 'd') as $plugin) {
507
			if (file_exists(PLUGINS."/$plugin/meta.json")) {
508
				$content .= file_get_contents(PLUGINS."/$plugin/meta.json");
509
			}
510
		}
511
		$hash = substr(md5($content), 0, 5);
512
		foreach ($includes as &$files) {
513
			foreach ($files as &$file) {
514
				$file .= "?$hash";
515
			}
516
			unset($file);
517
		}
518
		return $includes;
519
	}
520
	/**
521
	 * @param Config $Config
522
	 */
523
	protected function add_includes_on_page_manually_added ($Config) {
524
		foreach (['core_html', 'core_js', 'core_css', 'html', 'js', 'css'] as $type) {
525
			foreach ($this->$type as &$elements) {
526
				$elements = implode('', array_unique($elements));
527
			}
528
			unset($elements);
529
		}
530
		$this->Head .=
531
			$this->core_config.
532
			$this->config.
533
			$this->core_css[0].$this->css[0].
534
			h::style($this->core_css[1].$this->css[1] ?: false);
535
		$js_html_insert_to = $Config->core['put_js_after_body'] ? 'post_Body' : 'Head';
536
		$js_html           =
537
			$this->core_js[0].
538
			h::script($this->core_js[1] ?: false).
539
			$this->js[0].
540
			h::script($this->js[1] ?: false).
541
			$this->core_html[0].$this->html[0].
542
			$this->core_html[1].$this->html[1];
543
		$this->$js_html_insert_to .= $js_html;
544
	}
545
	/**
546
	 * Getting of HTML, JS and CSS files list to be included
547
	 *
548
	 * @param bool $absolute If <i>true</i> - absolute paths to files will be returned
549
	 *
550
	 * @return string[][]
551
	 */
552
	protected function get_includes_list ($absolute = false) {
553
		$includes = [];
554
		/**
555
		 * Get includes of system and theme
556
		 */
557
		$this->get_includes_list_add_includes(DIR.'/includes', 'includes', $includes, $absolute);
558
		$this->get_includes_list_add_includes(THEMES."/$this->theme", "themes/$this->theme", $includes, $absolute);
559
		$Config = Config::instance();
560
		foreach ($Config->components['modules'] as $module_name => $module_data) {
561
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
562
				continue;
563
			}
564
			$this->get_includes_list_add_includes(MODULES."/$module_name/includes", "components/modules/$module_name/includes", $includes, $absolute);
565
		}
566
		foreach ($Config->components['plugins'] as $plugin_name) {
567
			$this->get_includes_list_add_includes(PLUGINS."/$plugin_name/includes", "components/plugins/$plugin_name/includes", $includes, $absolute);
568
		}
569
		return [
570
			'html' => array_merge(...$includes['html']),
571
			'js'   => array_merge(...$includes['js']),
572
			'css'  => array_merge(...$includes['css'])
573
		];
574
	}
575
	/**
576
	 * @param string     $base_dir
577
	 * @param string     $base_public_path
578
	 * @param string[][] $includes
579
	 * @param bool       $absolute
580
	 */
581
	protected function get_includes_list_add_includes ($base_dir, $base_public_path, &$includes, $absolute) {
582
		$includes['html'][] = $this->get_includes_list_add_includes_internal($base_dir, $base_public_path, 'html', $absolute);
583
		$includes['js'][]   = $this->get_includes_list_add_includes_internal($base_dir, $base_public_path, 'js', $absolute);
584
		$includes['css'][]  = $this->get_includes_list_add_includes_internal($base_dir, $base_public_path, 'css', $absolute);
585
	}
586
	/**
587
	 * @param string $base_dir
588
	 * @param string $base_public_path
589
	 * @param string $ext
590
	 * @param bool   $absolute
591
	 *
592
	 * @return array
593
	 */
594
	protected function get_includes_list_add_includes_internal ($base_dir, $base_public_path, $ext, $absolute) {
595
		return get_files_list("$base_dir/$ext", "/.*\\.$ext\$/i", 'f', $absolute ? true : "$base_public_path/$ext", true, 'name', '!include') ?: [];
596
	}
597
	/**
598
	 * Rebuilding of HTML, JS and CSS cache
599
	 *
600
	 * @return \cs\Page
601
	 */
602
	protected function rebuild_cache () {
603
		list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
604
		$structure = [];
605
		foreach ($includes_map as $filename_prefix => $includes) {
606
			// We replace `/` by `+` to make it suitable for filename
607
			$filename_prefix             = str_replace('/', '+', $filename_prefix);
608
			$structure[$filename_prefix] = $this->create_cached_includes_files($filename_prefix, $includes);
609
		}
610
		unset($includes_map, $filename_prefix, $includes);
611
		file_put_json(
612
			PUBLIC_CACHE."/$this->pcache_basename.json",
613
			[$dependencies, $structure]
614
		);
615
		unset($structure);
616
		Event::instance()->fire('System/Page/rebuild_cache');
617
		return $this;
618
	}
619
	/**
620
	 * Creates cached version of given HTML, JS and CSS files.
621
	 * Resulting file name consists of <b>$filename_prefix</b> and <b>$this->pcache_basename</b>
622
	 *
623
	 * @param string $filename_prefix
624
	 * @param array  $includes Array of paths to files, may have keys: <b>css</b> and/or <b>js</b> and/or <b>html</b>
625
	 *
626
	 * @return array
627
	 */
628
	protected function create_cached_includes_files ($filename_prefix, $includes) {
629
		$cache_hash = [];
630
		/** @noinspection AlterInForeachInspection */
631
		foreach ($includes as $extension => $files) {
632
			$content = $this->create_cached_includes_files_process_files(
633
				$extension,
634
				$filename_prefix,
635
				$files
636
			);
637
			file_put_contents(PUBLIC_CACHE."/$filename_prefix$this->pcache_basename.$extension", gzencode($content, 9), LOCK_EX | FILE_BINARY);
638
			$cache_hash[$extension] = substr(md5($content), 0, 5);
639
		}
640
		return $cache_hash;
641
	}
642
	protected function create_cached_includes_files_process_files ($extension, $filename_prefix, $files) {
643
		$content = '';
644
		switch ($extension) {
645
			/**
646
			 * Insert external elements into resulting css file.
647
			 * It is needed, because those files will not be copied into new destination of resulting css file.
648
			 */
649
			case 'css':
650
				$callback = function ($content, $file) {
651
					return
652
						$content.
653
						Includes_processing::css(
654
							file_get_contents($file),
655
							$file
656
						);
657
				};
658
				break;
659
			/**
660
			 * Combine css and js files for Web Component into resulting files in order to optimize loading process
661
			 */
662
			case 'html':
663
				/**
664
				 * For CSP-compatible HTML files we need to know destination to put there additional JS/CSS files
665
				 */
666
				$destination = Config::instance()->core['vulcanization'] ? false : PUBLIC_CACHE;
667
				$callback    = function ($content, $file) use ($filename_prefix, $destination) {
668
					return
669
						$content.
670
						Includes_processing::html(
671
							file_get_contents($file),
672
							$file,
673
							"$filename_prefix$this->pcache_basename-".basename($file).'+'.substr(md5($file), 0, 5),
674
							$destination
675
						);
676
				};
677
				break;
678
			case 'js':
679
				$callback = function ($content, $file) {
680
					return
681
						$content.
682
						Includes_processing::js(file_get_contents($file));
683
				};
684
				if ($filename_prefix == '') {
685
					$content = 'window.cs={Language:'._json_encode(Language::instance()).'};';
686
					$content .= 'window.requirejs={paths:'._json_encode($this->get_requirejs_paths()).'};';
687
				}
688
		}
689
		/** @noinspection PhpUndefinedVariableInspection */
690
		return array_reduce(array_filter($files, 'file_exists'), $callback, $content);
691
	}
692
	/**
693
	 * 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
694
	 *
695
	 * @return array[] [$dependencies, $includes_map]
696
	 */
697
	protected function includes_dependencies_and_map () {
698
		/**
699
		 * Get all includes
700
		 */
701
		$all_includes = $this->get_includes_list(true);
702
		$includes_map = [];
703
		/**
704
		 * Array [package => [list of packages it depends on]]
705
		 */
706
		$dependencies    = [];
707
		$functionalities = [];
708
		/**
709
		 * According to components's maps some files should be included only on specific pages.
710
		 * Here we read this rules, and remove from whole includes list such items, that should be included only on specific pages.
711
		 * Also collect dependencies.
712
		 */
713
		$Config = Config::instance();
714
		foreach ($Config->components['modules'] as $module_name => $module_data) {
715
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
716
				continue;
717
			}
718
			if (file_exists(MODULES."/$module_name/meta.json")) {
719
				$this->process_meta(
720
					file_get_json(MODULES."/$module_name/meta.json"),
721
					$dependencies,
722
					$functionalities
723
				);
724
			}
725
			if (file_exists(MODULES."/$module_name/includes/map.json")) {
726
				$this->process_map(
727
					file_get_json_nocomments(MODULES."/$module_name/includes/map.json"),
728
					MODULES."/$module_name/includes",
729
					$includes_map,
730
					$all_includes
731
				);
732
			}
733
		}
734
		unset($module_name, $module_data);
735
		foreach ($Config->components['plugins'] as $plugin_name) {
736
			if (file_exists(PLUGINS."/$plugin_name/meta.json")) {
737
				$this->process_meta(
738
					file_get_json(PLUGINS."/$plugin_name/meta.json"),
739
					$dependencies,
740
					$functionalities
741
				);
742
			}
743
			if (file_exists(PLUGINS."/$plugin_name/includes/map.json")) {
744
				$this->process_map(
745
					file_get_json_nocomments(PLUGINS."/$plugin_name/includes/map.json"),
746
					PLUGINS."/$plugin_name/includes",
747
					$includes_map,
748
					$all_includes
749
				);
750
			}
751
		}
752
		unset($plugin_name);
753
		/**
754
		 * For consistency
755
		 */
756
		$includes_map[''] = $all_includes;
757
		Event::instance()->fire(
758
			'System/Page/includes_dependencies_and_map',
759
			[
760
				'dependencies' => &$dependencies,
761
				'includes_map' => &$includes_map
762
			]
763
		);
764
		$dependencies = $this->normalize_dependencies($dependencies, $functionalities);
765
		$includes_map = $this->clean_includes_arrays_without_files($dependencies, $includes_map);
766
		$dependencies = array_map('array_values', $dependencies);
767
		$dependencies = array_filter($dependencies);
768
		return [$dependencies, $includes_map];
769
	}
770
	/**
771
	 * Process meta information and corresponding entries to dependencies and functionalities
772
	 *
773
	 * @param array $meta
774
	 * @param array $dependencies
775
	 * @param array $functionalities
776
	 */
777
	protected function process_meta ($meta, &$dependencies, &$functionalities) {
778
		$package = $meta['package'];
779
		if (isset($meta['require'])) {
780
			foreach ((array)$meta['require'] as $r) {
781
				/**
782
				 * Get only name of package or functionality
783
				 */
784
				$r                        = preg_split('/[=<>]/', $r, 2)[0];
785
				$dependencies[$package][] = $r;
786
			}
787
		}
788
		if (isset($meta['optional'])) {
789
			foreach ((array)$meta['optional'] as $o) {
790
				/**
791
				 * Get only name of package or functionality
792
				 */
793
				$o                        = preg_split('/[=<>]/', $o, 2)[0];
794
				$dependencies[$package][] = $o;
795
			}
796
			unset($o);
797
		}
798
		if (isset($meta['provide'])) {
799
			foreach ((array)$meta['provide'] as $p) {
800
				/**
801
				 * If provides sub-functionality for other component (for instance, `Blog/post_patch`) - inverse "providing" to "dependency"
802
				 * Otherwise it is just functionality alias to package name
803
				 */
804
				if (strpos($p, '/') !== false) {
805
					/**
806
					 * Get name of package or functionality
807
					 */
808
					$p                  = explode('/', $p)[0];
809
					$dependencies[$p][] = $package;
810
				} else {
811
					$functionalities[$p] = $package;
812
				}
813
			}
814
			unset($p);
815
		}
816
	}
817
	/**
818
	 * Process map structure, fill includes map and remove files from list of all includes (remaining files will be included on all pages)
819
	 *
820
	 * @param array  $map
821
	 * @param string $includes_dir
822
	 * @param array  $includes_map
823
	 * @param array  $all_includes
824
	 */
825
	protected function process_map ($map, $includes_dir, &$includes_map, &$all_includes) {
826
		foreach ($map as $path => $files) {
827
			foreach ((array)$files as $file) {
828
				$extension = file_extension($file);
829
				if (in_array($extension, ['css', 'js', 'html'])) {
830
					$file                              = "$includes_dir/$extension/$file";
831
					$includes_map[$path][$extension][] = $file;
832
					$all_includes[$extension]          = array_diff($all_includes[$extension], [$file]);
833
				} else {
834
					$file = rtrim($file, '*');
835
					/**
836
					 * Wildcard support, it is possible to specify just path prefix and all files with this prefix will be included
837
					 */
838
					$found_files = array_filter(
839
						get_files_list($includes_dir, '/.*\.(css|js|html)$/i', 'f', '', true, 'name', '!include') ?: [],
840
						function ($f) use ($file) {
841
							// We need only files with specified mask and only those located in directory that corresponds to file's extension
842
							return preg_match("#^(css|js|html)/$file.*\\1$#i", $f);
843
						}
844
					);
845
					// Drop first level directory
846
					$found_files = _preg_replace('#^[^/]+/(.*)#', '$1', $found_files);
847
					$this->process_map([$path => $found_files], $includes_dir, $includes_map, $all_includes);
848
				}
849
			}
850
		}
851
	}
852
	/**
853
	 * Replace functionalities by real packages names, take into account recursive dependencies
854
	 *
855
	 * @param array $dependencies
856
	 * @param array $functionalities
857
	 *
858
	 * @return array
859
	 */
860
	protected function normalize_dependencies ($dependencies, $functionalities) {
861
		/**
862
		 * First of all remove packages without any dependencies
863
		 */
864
		$dependencies = array_filter($dependencies);
865
		/**
866
		 * First round, process aliases among keys
867
		 */
868
		foreach (array_keys($dependencies) as $d) {
869
			if (isset($functionalities[$d])) {
870
				$package = $functionalities[$d];
871
				/**
872
				 * Add dependencies to existing package dependencies
873
				 */
874
				foreach ($dependencies[$d] as $dependency) {
875
					$dependencies[$package][] = $dependency;
876
				}
877
				/**
878
				 * Drop alias
879
				 */
880
				unset($dependencies[$d]);
881
			}
882
		}
883
		unset($d, $dependency);
884
		/**
885
		 * Second round, process aliases among dependencies
886
		 */
887
		foreach ($dependencies as &$depends_on) {
888
			foreach ($depends_on as &$dependency) {
889
				if (isset($functionalities[$dependency])) {
890
					$dependency = $functionalities[$dependency];
891
				}
892
			}
893
		}
894
		unset($depends_on, $dependency);
895
		/**
896
		 * Third round, process recursive dependencies
897
		 */
898
		foreach ($dependencies as &$depends_on) {
899
			foreach ($depends_on as &$dependency) {
900
				if ($dependency != 'System' && isset($dependencies[$dependency])) {
901
					foreach (array_diff($dependencies[$dependency], $depends_on) as $new_dependency) {
902
						$depends_on[] = $new_dependency;
903
					}
904
				}
905
			}
906
		}
907
		return array_map('array_unique', $dependencies);
908
	}
909
	/**
910
	 * Includes array is composed from dependencies and sometimes dependencies doesn't have any files, so we'll clean that
911
	 *
912
	 * @param array $dependencies
913
	 * @param array $includes_map
914
	 *
915
	 * @return array
916
	 */
917
	protected function clean_includes_arrays_without_files ($dependencies, $includes_map) {
918
		foreach ($dependencies as &$depends_on) {
919
			foreach ($depends_on as $index => &$dependency) {
920
				if (!isset($includes_map[$dependency])) {
921
					unset($depends_on[$index]);
922
				}
923
			}
924
			unset($dependency);
925
		}
926
		return $includes_map;
927
	}
928
}
929