Completed
Push — master ( 6fb5df...671afb )
by Nazar
03:54
created

Includes::get_includes_prepare()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

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