Completed
Push — master ( 12e15f...de9719 )
by Nazar
04:28
created

Includes::css()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
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-2015, 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
		/**
244
		 * If CSS and JavaScript compression enabled
245
		 */
246
		if ($Config->core['cache_compress_js_css'] && !(admin_path() && current_module() == Config::SYSTEM_MODULE)) {
247
			$includes = $this->get_includes_for_page_with_compression();
248
		} else {
249
			/**
250
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
251
			 */
252
			/**
253
			 * @var \cs\Page $this
254
			 */
255
			$this->config_internal(Language::instance(), 'cs.Language', true);
256
			$includes = $this->get_includes_for_page_without_compression($Config);
257
		}
258
		$this->css_internal($includes['css'], 'file', true);
259
		$this->js_internal($includes['js'], 'file', true);
260
		$this->html_internal($includes['html'], 'file', true);
261
		$this->add_includes_on_page_manually_added($Config);
262
		return $this;
263
	}
264
	protected function add_system_configs () {
265
		$Config         = Config::instance();
266
		$Index          = Index::instance();
267
		$Route          = Route::instance();
268
		$User           = User::instance();
269
		$current_module = current_module();
270
		/**
271
		 * @var \cs\_SERVER $_SERVER
272
		 */
273
		$this->config_internal(
274
			[
275
				'base_url'              => $Config->base_url(),
276
				'current_base_url'      => $Config->base_url().'/'.($Index->in_admin() ? 'admin/' : '').$current_module,
277
				'public_key'            => Core::instance()->public_key,
278
				'module'                => $current_module,
279
				'in_admin'              => (int)$Index->in_admin(),
280
				'is_admin'              => (int)$User->admin(),
281
				'is_user'               => (int)$User->user(),
282
				'is_guest'              => (int)$User->guest(),
283
				'password_min_length'   => (int)$Config->core['password_min_length'],
284
				'password_min_strength' => (int)$Config->core['password_min_strength'],
285
				'debug'                 => (int)DEBUG,
286
				'cookie_prefix'         => $Config->core['cookie_prefix'],
287
				'cookie_domain'         => $Config->core['cookie_domain'][$Route->mirror_index],
288
				'protocol'              => $_SERVER->protocol,
289
				'route'                 => $Route->route,
290
				'route_path'            => $Route->path,
291
				'route_ids'             => $Route->ids
292
			],
293
			'cs',
294
			true
295
		);
296
		if ($User->guest()) {
297
			$this->config_internal(get_core_ml_text('rules'), 'cs.rules_text', true);
298
		}
299
		if ($User->admin()) {
300
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
301
		}
302
	}
303
	/**
304
	 * @return array[]
1 ignored issue
show
Documentation introduced by
Should the return type not be array<string,array>?

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

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