Completed
Push — master ( 853355...bb3d03 )
by Nazar
07:37
created

Includes::html()   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\Core,
11
	cs\Config,
12
	cs\Event,
13
	cs\Index,
14
	cs\Language,
15
	cs\Route,
16
	cs\User,
17
	h;
18
19
/**
20
 * Provides next events:
21
 *  System/Page/includes_dependencies_and_map
22
 *  [
23
 *    'dependencies' => &$dependencies,
24
 *    'includes_map' => &$includes_map
25
 *  ]
26
 *
27
 *  System/Page/rebuild_cache
28
 *  [
29
 *    'key' => &$key //Reference to the key, that will be appended to all css and js files, can be changed to reflect JavaScript and CSS changes
30
 *  ]
31
 *
32
 * Includes management for `cs\Page` class
33
 *
34
 * @property string $Title
35
 * @property string $Description
36
 * @property string $canonical_url
37
 * @property string $Head
38
 * @property string $post_Body
39
 * @property string $theme
40
 */
41
trait Includes {
42
	protected $core_html   = [0 => [], 1 => []];
43
	protected $core_js     = [0 => [], 1 => []];
44
	protected $core_css    = [0 => [], 1 => []];
45
	protected $core_config = '';
46
	protected $html        = [0 => [], 1 => []];
47
	protected $js          = [0 => [], 1 => []];
48
	protected $css         = [0 => [], 1 => []];
49
	protected $config      = '';
50
	/**
51
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
52
	 * @var string
53
	 */
54
	protected $pcache_basename;
55
	/**
56
	 * Including of Web Components
57
	 *
58
	 * @param string|string[] $add  Path to including file, or code
59
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
60
	 *
61
	 * @return \cs\Page
62
	 */
63
	function html ($add, $mode = 'file') {
64
		return $this->html_internal($add, $mode);
65
	}
66
	/**
67
	 * @param string|string[] $add
68
	 * @param string          $mode
69
	 * @param bool            $core
70
	 *
71
	 * @return \cs\Page
72
	 */
73
	protected function html_internal ($add, $mode = 'file', $core = false) {
74
		if (!$add) {
75
			return $this;
76
		}
77
		if (is_array($add)) {
78
			foreach (array_filter($add) as $script) {
79
				$this->html_internal($script, $mode, $core);
80
			}
81
		} else {
82
			if ($core) {
83
				$html = &$this->core_html;
84
			} else {
85
				$html = &$this->html;
86
			}
87
			if ($mode == 'file') {
88
				$html[0][] = h::link(
89
					[
90
						'href' => $add,
91
						'rel'  => 'import'
92
					]
93
				);
94
			} elseif ($mode == 'code') {
95
				$html[1][] = "$add\n";
96
			}
97
		}
98
		return $this;
99
	}
100
	/**
101
	 * Including of JavaScript
102
	 *
103
	 * @param string|string[] $add  Path to including file, or code
104
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
105
	 *
106
	 * @return \cs\Page
107
	 */
108
	function js ($add, $mode = 'file') {
109
		return $this->js_internal($add, $mode);
110
	}
111
	/**
112
	 * @param string|string[] $add
113
	 * @param string          $mode
114
	 * @param bool            $core
115
	 *
116
	 * @return \cs\Page
117
	 */
118
	protected function js_internal ($add, $mode = 'file', $core = false) {
119
		if (!$add) {
120
			return $this;
121
		}
122
		if (is_array($add)) {
123
			foreach (array_filter($add) as $script) {
124
				$this->js_internal($script, $mode, $core);
125
			}
126
		} else {
127
			if ($core) {
128
				$js = &$this->core_js;
129
			} else {
130
				$js = &$this->js;
131
			}
132
			if ($mode == 'file') {
133
				$js[0][] = h::script(
134
					[
135
						'src' => $add
136
					]
137
				);
138
			} elseif ($mode == 'code') {
139
				$js[1][] = "$add\n";
140
			}
141
		}
142
		return $this;
143
	}
144
	/**
145
	 * Including of CSS
146
	 *
147
	 * @param string|string[] $add  Path to including file, or code
148
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
149
	 *
150
	 * @return \cs\Page
151
	 */
152
	function css ($add, $mode = 'file') {
153
		return $this->css_internal($add, $mode);
154
	}
155
	/**
156
	 * @param string|string[] $add
157
	 * @param string          $mode
158
	 * @param bool            $core
159
	 *
160
	 * @return \cs\Page
161
	 */
162
	protected function css_internal ($add, $mode = 'file', $core = false) {
163
		if (!$add) {
164
			return $this;
165
		}
166
		if (is_array($add)) {
167
			foreach (array_filter($add) as $style) {
168
				$this->css_internal($style, $mode, $core);
169
			}
170
		} else {
171
			if ($core) {
172
				$css = &$this->core_css;
173
			} else {
174
				$css = &$this->css;
175
			}
176
			if ($mode == 'file') {
177
				$css[0][] = h::link(
178
					[
179
						'href'           => $add,
180
						'rel'            => 'stylesheet',
181
						'shim-shadowdom' => true
182
					]
183
				);
184
			} elseif ($mode == 'code') {
185
				$css[1][] = "$add\n";
186
			}
187
		}
188
		return $this;
189
	}
190
	/**
191
	 * Add config on page to make it available on frontend
192
	 *
193
	 * @param mixed  $config_structure        Any scalar type or array
194
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
195
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
196
	 *
197
	 * @return \cs\Page
198
	 */
199
	function config ($config_structure, $target) {
200
		return $this->config_internal($config_structure, $target);
201
	}
202
	/**
203
	 * @param mixed  $config_structure
204
	 * @param string $target
205
	 * @param bool   $core
206
	 *
207
	 * @return \cs\Page
208
	 */
209
	protected function config_internal ($config_structure, $target, $core = false) {
210
		$config = h::script(
211
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
212
			[
213
				'target' => $target,
214
				'class'  => 'cs-config',
215
				'type'   => 'application/json'
216
			]
217
		);
218
		if ($core) {
219
			$this->core_config .= $config;
220
		} else {
221
			$this->config .= $config;
222
		}
223
		return $this;
224
	}
225
	/**
226
	 * Getting of HTML, JS and CSS includes
227
	 *
228
	 * @return \cs\Page
229
	 */
230
	protected function add_includes_on_page () {
231
		$Config = Config::instance(true);
232
		if (!$Config) {
233
			return $this;
234
		}
235
		/**
236
		 * Base name for cache files
237
		 */
238
		$this->pcache_basename = "_{$this->theme}_".Language::instance()->clang;
239
		/**
240
		 * Some JS configs required by system
241
		 */
242
		$this->add_system_configs();
243
		// TODO: I hope some day we'll get rid of this sh*t :(
244
		$this->ie_edge();
245
		/**
246
		 * If CSS and JavaScript compression enabled
247
		 */
248
		if ($Config->core['cache_compress_js_css'] && !(admin_path() && isset($_GET['debug']))) {
249
			$this->webcomponents_polyfill(true);
250
			$includes = $this->get_includes_for_page_with_compression();
251
		} else {
252
			$this->webcomponents_polyfill(false);
253
			/**
254
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
255
			 */
256
			/**
257
			 * @var \cs\Page $this
258
			 */
259
			$this->config_internal(Language::instance(), 'cs.Language', true);
260
			$includes = $this->get_includes_for_page_without_compression($Config);
261
		}
262
		$this->css_internal($includes['css'], 'file', true);
263
		$this->js_internal($includes['js'], 'file', true);
264
		$this->html_internal($includes['html'], 'file', true);
265
		$this->add_includes_on_page_manually_added($Config);
266
		return $this;
267
	}
268
	/**
269
	 * Add JS polyfills for IE/Edge
270
	 */
271
	protected function ie_edge () {
272
		/**
273
		 * @var \cs\_SERVER $_SERVER
274
		 */
275
		if (preg_match('/Trident|Edge/', $_SERVER->user_agent)) {
276
			$this->js_internal(
277
				get_files_list(DIR."/includes/js/microsoft_sh*t", "/.*\\.js$/i", 'f', "includes/js/microsoft_sh*t", true),
278
				'file',
279
				true
280
			);
281
		}
282
	}
283
	/**
284
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
285
	 *
286
	 * TODO: Probably, some effective User Agent-based check might be used here
287
	 *
288
	 * @param bool $with_compression
289
	 */
290
	protected function webcomponents_polyfill ($with_compression) {
291
		if (!isset($_COOKIE['shadow_dom']) || $_COOKIE['shadow_dom'] != 1) {
292
			$file = 'includes/js/WebComponents-polyfill/webcomponents-custom.min.js';
293
			if ($with_compression) {
294
				$compressed_file = PUBLIC_CACHE.'/webcomponents.js';
295
				if (!file_exists($compressed_file)) {
296
					$content = file_get_contents(DIR."/$file");
297
					file_put_contents($compressed_file, gzencode($content, 9), LOCK_EX | FILE_BINARY);
298
					file_put_contents("$compressed_file.hash", substr(md5($content), 0, 5));
299
				}
300
				$hash = file_get_contents("$compressed_file.hash");
301
				$this->js_internal("storage/pcache/webcomponents.js?$hash", 'file', true);
302
			} else {
303
				$this->js_internal($file, 'file', true);
304
			}
305
		}
306
	}
307
	protected function add_system_configs () {
308
		$Config         = Config::instance();
309
		$Route          = Route::instance();
310
		$User           = User::instance();
311
		$current_module = current_module();
312
		$this->config_internal(
313
			[
314
				'base_url'              => $Config->base_url(),
315
				'current_base_url'      => $Config->base_url().'/'.(admin_path() ? 'admin/' : '').$current_module,
316
				'public_key'            => Core::instance()->public_key,
317
				'module'                => $current_module,
318
				'in_admin'              => (int)admin_path(),
319
				'is_admin'              => (int)$User->admin(),
320
				'is_user'               => (int)$User->user(),
321
				'is_guest'              => (int)$User->guest(),
322
				'password_min_length'   => (int)$Config->core['password_min_length'],
323
				'password_min_strength' => (int)$Config->core['password_min_strength'],
324
				'debug'                 => (int)DEBUG,
325
				'route'                 => $Route->route,
326
				'route_path'            => $Route->path,
327
				'route_ids'             => $Route->ids
328
			],
329
			'cs',
330
			true
331
		);
332
		if ($User->guest()) {
333
			$this->config_internal(get_core_ml_text('rules'), 'cs.rules_text', true);
334
		}
335
		if ($User->admin()) {
336
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
337
		}
338
	}
339
	/**
340
	 * @return array[]
341
	 */
342
	protected function get_includes_for_page_with_compression () {
343
		/**
344
		 * Rebuild cache if necessary
345
		 */
346
		if (!file_exists(PUBLIC_CACHE."/$this->pcache_basename.json")) {
347
			$this->rebuild_cache();
348
		}
349
		list($dependencies, $structure) = file_get_json(PUBLIC_CACHE."/$this->pcache_basename.json");
350
		$system_includes = [
351
			'css'  => ["storage/pcache/$this->pcache_basename.css?{$structure['']['css']}"],
352
			'js'   => ["storage/pcache/$this->pcache_basename.js?{$structure['']['js']}"],
353
			'html' => ["storage/pcache/$this->pcache_basename.html?{$structure['']['html']}"]
354
		];
355
		list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '+');
356
		foreach ($structure as $filename_prefix => $hashes) {
357
			if (!$filename_prefix) {
358
				continue;
359
			}
360
			$is_dependency = $this->get_includes_is_dependency($dependencies, $filename_prefix, '+');
361
			if ($is_dependency || mb_strpos($current_url, $filename_prefix) === 0) {
362
				foreach ($hashes as $extension => $hash) {
363
					if ($is_dependency) {
364
						$dependencies_includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
365
					} else {
366
						$includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
367
					}
368
				}
369
			}
370
		}
371
		return array_merge_recursive($system_includes, $dependencies_includes, $includes);
372
	}
373
	/**
374
	 * @param Config $Config
375
	 *
376
	 * @return array[]
377
	 */
378
	protected function get_includes_for_page_without_compression ($Config) {
379
		// To determine all dependencies and stuff we need `$Config` object to be already created
380
		if ($Config) {
381
			list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
382
			$system_includes = $includes_map[''];
383
			list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '/');
384
			foreach ($includes_map as $url => $local_includes) {
385
				if (!$url) {
386
					continue;
387
				}
388
				$is_dependency = $this->get_includes_is_dependency($dependencies, $url, '/');
389
				if ($is_dependency) {
390
					$dependencies_includes = array_merge_recursive($dependencies_includes, $local_includes);
391
				} elseif (mb_strpos($current_url, $url) === 0) {
392
					$includes = array_merge_recursive($includes, $local_includes);
393
				}
394
			}
395
			$includes = array_merge_recursive($system_includes, $dependencies_includes, $includes);
396
			$includes = _substr($includes, strlen(DIR.'/'));
397
		} else {
398
			$includes = $this->get_includes_list();
399
		}
400
		return $this->add_versions_hash($includes);
401
	}
402
	/**
403
	 * @param array  $dependencies
404
	 * @param string $separator `+` or `/`
405
	 *
406
	 * @return array
407
	 */
408
	protected function get_includes_prepare ($dependencies, $separator) {
409
		$includes              = [
410
			'css'  => [],
411
			'js'   => [],
412
			'html' => []
413
		];
414
		$dependencies_includes = $includes;
415
		$current_module        = current_module();
416
		/**
417
		 * Current URL based on controller path (it better represents how page was rendered)
418
		 */
419
		$current_url = array_slice(Index::instance()->controller_path, 1);
420
		$current_url = (admin_path() ? "admin$separator" : '')."$current_module$separator".implode($separator, $current_url);
421
		/**
422
		 * Narrow the dependencies to current module only
423
		 */
424
		$dependencies = isset($dependencies[$current_module]) ? $dependencies[$current_module] : [];
425
		return [$includes, $dependencies_includes, $dependencies, $current_url];
426
	}
427
	/**
428
	 * @param array  $dependencies
429
	 * @param string $url
430
	 * @param string $separator `+` or `/`
431
	 *
432
	 * @return bool
433
	 */
434
	protected function get_includes_is_dependency ($dependencies, $url, $separator) {
435
		$url_exploded = explode($separator, $url);
436
		/** @noinspection NestedTernaryOperatorInspection */
437
		$url_module = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
438
		return
439
			$url_module !== Config::SYSTEM_MODULE &&
440
			in_array($url_module, $dependencies) &&
441
			(
442
				admin_path() || admin_path() == ($url_exploded[0] == 'admin')
443
			);
444
	}
445
	protected function add_versions_hash ($includes) {
446
		$content = '';
447
		foreach (get_files_list(MODULES, false, 'd') as $module) {
448
			if (file_exists(MODULES."/$module/meta.json")) {
449
				$content .= file_get_contents(MODULES."/$module/meta.json");
450
			}
451
		}
452
		foreach (get_files_list(PLUGINS, false, 'd') as $plugin) {
453
			if (file_exists(PLUGINS."/$plugin/meta.json")) {
454
				$content .= file_get_contents(PLUGINS."/$plugin/meta.json");
455
			}
456
		}
457
		$hash = substr(md5($content), 0, 5);
458
		foreach ($includes as &$files) {
459
			foreach ($files as &$file) {
460
				$file .= "?$hash";
461
			}
462
			unset($file);
463
		}
464
		return $includes;
465
	}
466
	/**
467
	 * @param Config $Config
468
	 */
469
	protected function add_includes_on_page_manually_added ($Config) {
470
		foreach (['core_html', 'core_js', 'core_css', 'html', 'js', 'css'] as $type) {
471
			foreach ($this->$type as &$elements) {
472
				$elements = implode('', array_unique($elements));
473
			}
474
			unset($elements);
475
		}
476
		$this->Head .=
477
			$this->core_config.
478
			$this->config.
479
			$this->core_css[0].$this->css[0].
480
			h::style($this->core_css[1].$this->css[1] ?: false);
481
		$js_html_insert_to = $Config->core['put_js_after_body'] ? 'post_Body' : 'Head';
482
		$js_html           =
483
			$this->core_js[0].
484
			h::script($this->core_js[1] ?: false).
485
			$this->js[0].
486
			h::script($this->js[1] ?: false).
487
			$this->core_html[0].$this->html[0].
488
			$this->core_html[1].$this->html[1];
489
		$this->$js_html_insert_to .= $js_html;
490
	}
491
	/**
492
	 * Getting of HTML, JS and CSS files list to be included
493
	 *
494
	 * @param bool $absolute If <i>true</i> - absolute paths to files will be returned
495
	 *
496
	 * @return array
497
	 */
498
	protected function get_includes_list ($absolute = false) {
499
		$theme_dir  = THEMES."/$this->theme";
500
		$theme_pdir = "themes/$this->theme";
501
		$get_files  = function ($dir, $prefix_path) {
502
			$extension = basename($dir);
503
			$list      = get_files_list($dir, "/.*\\.$extension$/i", 'f', $prefix_path, true, 'name', '!include') ?: [];
504
			sort($list);
505
			return $list;
506
		};
507
		/**
508
		 * Get includes of system and theme
509
		 */
510
		$includes = [];
511
		foreach (['html', 'js', 'css'] as $type) {
512
			$includes[$type] = array_merge(
513
				$get_files(DIR."/includes/$type", $absolute ? true : "includes/$type"),
514
				$get_files("$theme_dir/$type", $absolute ? true : "$theme_pdir/$type")
515
			);
516
		}
517
		unset($theme_dir, $theme_pdir);
518
		$Config = Config::instance();
519
		foreach ($Config->components['modules'] as $module_name => $module_data) {
520
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
521
				continue;
522
			}
523
			foreach (['html', 'js', 'css'] as $type) {
524
				/** @noinspection SlowArrayOperationsInLoopInspection */
525
				$includes[$type] = array_merge(
526
					$includes[$type],
527
					$get_files(MODULES."/$module_name/includes/$type", $absolute ? true : "components/modules/$module_name/includes/$type")
528
				);
529
			}
530
		}
531
		foreach ($Config->components['plugins'] as $plugin_name) {
532
			foreach (['html', 'js', 'css'] as $type) {
533
				/** @noinspection SlowArrayOperationsInLoopInspection */
534
				$includes[$type] = array_merge(
535
					$includes[$type],
536
					$get_files(PLUGINS."/$plugin_name/includes/$type", $absolute ? true : "components/plugins/$plugin_name/includes/$type")
537
				);
538
			}
539
		}
540
		return $includes;
541
	}
542
	/**
543
	 * Rebuilding of HTML, JS and CSS cache
544
	 *
545
	 * @return \cs\Page
546
	 */
547
	protected function rebuild_cache () {
548
		list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
549
		$structure = [];
550
		foreach ($includes_map as $filename_prefix => $includes) {
551
			// We replace `/` by `+` to make it suitable for filename
552
			$filename_prefix             = str_replace('/', '+', $filename_prefix);
553
			$structure[$filename_prefix] = $this->create_cached_includes_files($filename_prefix, $includes);
554
		}
555
		unset($includes_map, $filename_prefix, $includes);
556
		file_put_json(
557
			PUBLIC_CACHE."/$this->pcache_basename.json",
558
			[$dependencies, $structure]
559
		);
560
		unset($structure);
561
		Event::instance()->fire('System/Page/rebuild_cache');
562
		return $this;
563
	}
564
	/**
565
	 * Creates cached version of given HTML, JS and CSS files.
566
	 * Resulting file name consists of <b>$filename_prefix</b> and <b>$this->pcache_basename</b>
567
	 *
568
	 * @param string $filename_prefix
569
	 * @param array  $includes Array of paths to files, may have keys: <b>css</b> and/or <b>js</b> and/or <b>html</b>
570
	 *
571
	 * @return array
572
	 */
573
	protected function create_cached_includes_files ($filename_prefix, $includes) {
574
		$cache_hash = [];
575
		/** @noinspection AlterInForeachInspection */
576
		foreach ($includes as $extension => $files) {
577
			$content = $this->create_cached_includes_files_process_files(
578
				$extension,
579
				$filename_prefix,
580
				$files
581
			);
582
			file_put_contents(PUBLIC_CACHE."/$filename_prefix$this->pcache_basename.$extension", gzencode($content, 9), LOCK_EX | FILE_BINARY);
583
			$cache_hash[$extension] = substr(md5($content), 0, 5);
584
		}
585
		return $cache_hash;
586
	}
587
	protected function create_cached_includes_files_process_files ($extension, $filename_prefix, $files) {
588
		$content = '';
589
		switch ($extension) {
590
			/**
591
			 * Insert external elements into resulting css file.
592
			 * It is needed, because those files will not be copied into new destination of resulting css file.
593
			 */
594
			case 'css':
595
				$callback = function ($content, $file) {
596
					return
597
						$content.
598
						Includes_processing::css(
599
							file_get_contents($file),
600
							$file
601
						);
602
				};
603
				break;
604
			/**
605
			 * Combine css and js files for Web Component into resulting files in order to optimize loading process
606
			 */
607
			case 'html':
608
				/**
609
				 * For CSP-compatible HTML files we need to know destination to put there additional JS/CSS files
610
				 */
611
				$destination = Config::instance()->core['vulcanization'] ? false : PUBLIC_CACHE;
612
				$callback    = function ($content, $file) use ($filename_prefix, $destination) {
613
					return
614
						$content.
615
						Includes_processing::html(
616
							file_get_contents($file),
617
							$file,
618
							"$filename_prefix$this->pcache_basename-".basename($file).'+'.substr(md5($file), 0, 5),
619
							$destination
620
						);
621
				};
622
				break;
623
			case 'js':
624
				$callback = function ($content, $file) {
625
					return
626
						$content.
627
						Includes_processing::js(file_get_contents($file));
628
				};
629
				if ($filename_prefix == '') {
630
					$content = 'window.cs={Language:'._json_encode(Language::instance()).'};';
631
				}
632
		}
633
		/** @noinspection PhpUndefinedVariableInspection */
634
		return array_reduce(array_filter($files, 'file_exists'), $callback, $content);
635
	}
636
	/**
637
	 * 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
638
	 *
639
	 * @return array[] [$dependencies, $includes_map]
640
	 */
641
	protected function includes_dependencies_and_map () {
642
		/**
643
		 * Get all includes
644
		 */
645
		$all_includes = $this->get_includes_list(true);
646
		$includes_map = [];
647
		/**
648
		 * Array [package => [list of packages it depends on]]
649
		 */
650
		$dependencies    = [];
651
		$functionalities = [];
652
		/**
653
		 * According to components's maps some files should be included only on specific pages.
654
		 * Here we read this rules, and remove from whole includes list such items, that should be included only on specific pages.
655
		 * Also collect dependencies.
656
		 */
657
		$Config = Config::instance();
658
		foreach ($Config->components['modules'] as $module_name => $module_data) {
659
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
660
				continue;
661
			}
662
			if (file_exists(MODULES."/$module_name/meta.json")) {
663
				$this->process_meta(
664
					file_get_json(MODULES."/$module_name/meta.json"),
665
					$dependencies,
666
					$functionalities
667
				);
668
			}
669
			if (file_exists(MODULES."/$module_name/includes/map.json")) {
670
				$this->process_map(
671
					file_get_json_nocomments(MODULES."/$module_name/includes/map.json"),
672
					MODULES."/$module_name/includes",
673
					$includes_map,
674
					$all_includes
675
				);
676
			}
677
		}
678
		unset($module_name, $module_data);
679
		foreach ($Config->components['plugins'] as $plugin_name) {
680
			if (file_exists(PLUGINS."/$plugin_name/meta.json")) {
681
				$this->process_meta(
682
					file_get_json(PLUGINS."/$plugin_name/meta.json"),
683
					$dependencies,
684
					$functionalities
685
				);
686
			}
687
			if (file_exists(PLUGINS."/$plugin_name/includes/map.json")) {
688
				$this->process_map(
689
					file_get_json_nocomments(PLUGINS."/$plugin_name/includes/map.json"),
690
					PLUGINS."/$plugin_name/includes",
691
					$includes_map,
692
					$all_includes
693
				);
694
			}
695
		}
696
		unset($plugin_name);
697
		/**
698
		 * For consistency
699
		 */
700
		$includes_map[''] = $all_includes;
701
		Event::instance()->fire(
702
			'System/Page/includes_dependencies_and_map',
703
			[
704
				'dependencies' => &$dependencies,
705
				'includes_map' => &$includes_map
706
			]
707
		);
708
		$dependencies = $this->normalize_dependencies($dependencies, $functionalities);
709
		$includes_map = $this->clean_includes_arrays_without_files($dependencies, $includes_map);
710
		$dependencies = array_map('array_values', $dependencies);
711
		$dependencies = array_filter($dependencies);
712
		return [$dependencies, $includes_map];
713
	}
714
	/**
715
	 * Process meta information and corresponding entries to dependencies and functionalities
716
	 *
717
	 * @param array $meta
718
	 * @param array $dependencies
719
	 * @param array $functionalities
720
	 */
721
	protected function process_meta ($meta, &$dependencies, &$functionalities) {
722
		$package = $meta['package'];
723
		if (isset($meta['require'])) {
724
			foreach ((array)$meta['require'] as $r) {
725
				/**
726
				 * Get only name of package or functionality
727
				 */
728
				$r                        = preg_split('/[=<>]/', $r, 2)[0];
729
				$dependencies[$package][] = $r;
730
			}
731
		}
732
		if (isset($meta['optional'])) {
733
			foreach ((array)$meta['optional'] as $o) {
734
				/**
735
				 * Get only name of package or functionality
736
				 */
737
				$o                        = preg_split('/[=<>]/', $o, 2)[0];
738
				$dependencies[$package][] = $o;
739
			}
740
			unset($o);
741
		}
742
		if (isset($meta['provide'])) {
743
			foreach ((array)$meta['provide'] as $p) {
744
				/**
745
				 * If provides sub-functionality for other component (for instance, `Blog/post_patch`) - inverse "providing" to "dependency"
746
				 * Otherwise it is just functionality alias to package name
747
				 */
748
				if (strpos($p, '/') !== false) {
749
					/**
750
					 * Get name of package or functionality
751
					 */
752
					$p                  = explode('/', $p)[0];
753
					$dependencies[$p][] = $package;
754
				} else {
755
					$functionalities[$p] = $package;
756
				}
757
			}
758
			unset($p);
759
		}
760
	}
761
	/**
762
	 * Process map structure, fill includes map and remove files from list of all includes (remaining files will be included on all pages)
763
	 *
764
	 * @param array  $map
765
	 * @param string $includes_dir
766
	 * @param array  $includes_map
767
	 * @param array  $all_includes
768
	 */
769
	protected function process_map ($map, $includes_dir, &$includes_map, &$all_includes) {
770
		foreach ($map as $path => $files) {
771
			foreach ((array)$files as $file) {
772
				$extension = file_extension($file);
773
				if (in_array($extension, ['css', 'js', 'html'])) {
774
					$file                              = "$includes_dir/$extension/$file";
775
					$includes_map[$path][$extension][] = $file;
776
					$all_includes[$extension]          = array_diff($all_includes[$extension], [$file]);
777
				} else {
778
					$file = rtrim($file, '*');
779
					/**
780
					 * Wildcard support, it is possible to specify just path prefix and all files with this prefix will be included
781
					 */
782
					$found_files = array_filter(
783
						get_files_list($includes_dir, '/.*\.(css|js|html)$/i', 'f', '', true, 'name', '!include') ?: [],
784
						function ($f) use ($file) {
785
							// We need only files with specified mask and only those located in directory that corresponds to file's extension
786
							return preg_match("#^(css|js|html)/$file.*\\1$#i", $f);
787
						}
788
					);
789
					// Drop first level directory
790
					$found_files = _preg_replace('#^[^/]+/(.*)#', '$1', $found_files);
791
					$this->process_map([$path => $found_files], $includes_dir, $includes_map, $all_includes);
792
				}
793
			}
794
		}
795
	}
796
	/**
797
	 * Replace functionalities by real packages names, take into account recursive dependencies
798
	 *
799
	 * @param array $dependencies
800
	 * @param array $functionalities
801
	 *
802
	 * @return array
803
	 */
804
	protected function normalize_dependencies ($dependencies, $functionalities) {
805
		/**
806
		 * First of all remove packages without any dependencies
807
		 */
808
		$dependencies = array_filter($dependencies);
809
		/**
810
		 * First round, process aliases among keys
811
		 */
812
		foreach (array_keys($dependencies) as $d) {
813
			if (isset($functionalities[$d])) {
814
				$package = $functionalities[$d];
815
				/**
816
				 * Add dependencies to existing package dependencies
817
				 */
818
				foreach ($dependencies[$d] as $dependency) {
819
					$dependencies[$package][] = $dependency;
820
				}
821
				/**
822
				 * Drop alias
823
				 */
824
				unset($dependencies[$d]);
825
			}
826
		}
827
		unset($d, $dependency);
828
		/**
829
		 * Second round, process aliases among dependencies
830
		 */
831
		foreach ($dependencies as &$depends_on) {
832
			foreach ($depends_on as &$dependency) {
833
				if (isset($functionalities[$dependency])) {
834
					$dependency = $functionalities[$dependency];
835
				}
836
			}
837
		}
838
		unset($depends_on, $dependency);
839
		/**
840
		 * Third round, process recursive dependencies
841
		 */
842
		foreach ($dependencies as &$depends_on) {
843
			foreach ($depends_on as &$dependency) {
844
				if ($dependency != 'System' && isset($dependencies[$dependency])) {
845
					foreach (array_diff($dependencies[$dependency], $depends_on) as $new_dependency) {
846
						$depends_on[] = $new_dependency;
847
					}
848
				}
849
			}
850
		}
851
		return array_map('array_unique', $dependencies);
852
	}
853
	/**
854
	 * Includes array is composed from dependencies and sometimes dependencies doesn't have any files, so we'll clean that
855
	 *
856
	 * @param array $dependencies
857
	 * @param array $includes_map
858
	 *
859
	 * @return array
860
	 */
861
	protected function clean_includes_arrays_without_files ($dependencies, $includes_map) {
862
		foreach ($dependencies as &$depends_on) {
863
			foreach ($depends_on as $index => &$dependency) {
864
				if (!isset($includes_map[$dependency])) {
865
					unset($depends_on[$index]);
866
				}
867
			}
868
			unset($dependency);
869
		}
870
		return $includes_map;
871
	}
872
}
873