Completed
Push — master ( ddfbfc...1b0350 )
by Nazar
04:29
created

Includes::rebuild_cache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 17
rs 9.4286
cc 2
eloc 13
nc 2
nop 0
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[]
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...
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(admin_path());
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
	 * @param bool $with_disabled
470
	 *
471
	 * @return array
472
	 */
473
	protected function get_includes_list ($absolute = false, $with_disabled = false) {
474
		$theme_dir  = THEMES."/$this->theme";
475
		$theme_pdir = "themes/$this->theme";
476
		$get_files  = function ($dir, $prefix_path) {
477
			$extension = basename($dir);
478
			$list      = get_files_list($dir, "/.*\\.$extension$/i", 'f', $prefix_path, true, 'name', '!include') ?: [];
479
			sort($list);
480
			return $list;
481
		};
482
		/**
483
		 * Get includes of system and theme
484
		 */
485
		$includes = [];
486
		foreach (['html', 'js', 'css'] as $type) {
487
			$includes[$type] = array_merge(
488
				$get_files(DIR."/includes/$type", $absolute ? true : "includes/$type"),
489
				$get_files("$theme_dir/$type", $absolute ? true : "$theme_pdir/$type")
490
			);
491
		}
492
		unset($theme_dir, $theme_pdir);
493
		$Config = Config::instance();
494
		foreach ($Config->components['modules'] as $module_name => $module_data) {
495
			if (
496
				$module_data['active'] == Config\Module_Properties::UNINSTALLED ||
497
				(
498
					$module_data['active'] == Config\Module_Properties::DISABLED &&
499
					!$with_disabled
500
				)
501
			) {
502
				continue;
503
			}
504
			foreach (['html', 'js', 'css'] as $type) {
505
				/** @noinspection SlowArrayOperationsInLoopInspection */
506
				$includes[$type] = array_merge(
507
					$includes[$type],
508
					$get_files(MODULES."/$module_name/includes/$type", $absolute ? true : "components/modules/$module_name/includes/$type")
509
				);
510
			}
511
		}
512
		foreach ($Config->components['plugins'] as $plugin_name) {
513
			foreach (['html', 'js', 'css'] as $type) {
514
				/** @noinspection SlowArrayOperationsInLoopInspection */
515
				$includes[$type] = array_merge(
516
					$includes[$type],
517
					$get_files(PLUGINS."/$plugin_name/includes/$type", $absolute ? true : "components/plugins/$plugin_name/includes/$type")
518
				);
519
			}
520
		}
521
		return $includes;
522
	}
523
	/**
524
	 * Rebuilding of HTML, JS and CSS cache
525
	 *
526
	 * @return \cs\Page
527
	 */
528
	protected function rebuild_cache () {
529
		list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
530
		$structure = [];
531
		foreach ($includes_map as $filename_prefix => $includes) {
532
			// We replace `/` by `+` to make it suitable for filename
533
			$filename_prefix             = str_replace('/', '+', $filename_prefix);
534
			$structure[$filename_prefix] = $this->create_cached_includes_files($filename_prefix, $includes);
535
		}
536
		unset($includes_map, $filename_prefix, $includes);
537
		file_put_json(
538
			PUBLIC_CACHE."/$this->pcache_basename.json",
539
			[$dependencies, $structure]
540
		);
541
		unset($structure);
542
		Event::instance()->fire('System/Page/rebuild_cache');
543
		return $this;
544
	}
545
	/**
546
	 * Creates cached version of given HTML, JS and CSS files.
547
	 * Resulting file name consists of <b>$filename_prefix</b> and <b>$this->pcache_basename</b>
548
	 *
549
	 * @param string $filename_prefix
550
	 * @param array  $includes Array of paths to files, may have keys: <b>css</b> and/or <b>js</b> and/or <b>html</b>
551
	 *
552
	 * @return array
553
	 */
554
	protected function create_cached_includes_files ($filename_prefix, $includes) {
555
		$cache_hash = [];
556
		/** @noinspection AlterInForeachInspection */
557
		foreach ($includes as $extension => $files) {
558
			$content = $this->create_cached_includes_files_process_files(
559
				$extension,
560
				$filename_prefix,
561
				$files
562
			);
563
			file_put_contents(PUBLIC_CACHE."/$filename_prefix$this->pcache_basename.$extension", gzencode($content, 9), LOCK_EX | FILE_BINARY);
564
			$cache_hash[$extension] = substr(md5($content), 0, 5);
565
		}
566
		return $cache_hash;
567
	}
568
	protected function create_cached_includes_files_process_files ($extension, $filename_prefix, $files) {
569
		$content = '';
570
		switch ($extension) {
571
			/**
572
			 * Insert external elements into resulting css file.
573
			 * It is needed, because those files will not be copied into new destination of resulting css file.
574
			 */
575
			case 'css':
576
				$callback = function ($content, $file) {
577
					return
578
						$content.
579
						Includes_processing::css(
580
							file_get_contents($file),
581
							$file
582
						);
583
				};
584
				break;
585
			/**
586
			 * Combine css and js files for Web Component into resulting files in order to optimize loading process
587
			 */
588
			case 'html':
589
				/**
590
				 * For CSP-compatible HTML files we need to know destination to put there additional JS/CSS files
591
				 */
592
				$destination = Config::instance()->core['vulcanization'] ? false : PUBLIC_CACHE;
593
				$callback    = function ($content, $file) use ($filename_prefix, $destination) {
594
					return
595
						$content.
596
						Includes_processing::html(
597
							file_get_contents($file),
598
							$file,
599
							"$filename_prefix$this->pcache_basename-".basename($file).'+'.substr(md5($file), 0, 5),
600
							$destination
601
						);
602
				};
603
				break;
604
			case 'js':
605
				$callback = function ($content, $file) {
606
					return
607
						$content.
608
						Includes_processing::js(file_get_contents($file));
609
				};
610
				if ($filename_prefix == '') {
611
					$content = 'window.cs={Language:'._json_encode(Language::instance()).'};';
612
				}
613
		}
614
		/** @noinspection PhpUndefinedVariableInspection */
615
		return array_reduce(array_filter($files, 'file_exists'), $callback, $content);
616
	}
617
	/**
618
	 * 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
619
	 *
620
	 * @param bool $with_disabled
621
	 *
622
	 * @return array[] [$dependencies, $includes_map]
623
	 */
624
	protected function includes_dependencies_and_map ($with_disabled = false) {
625
		/**
626
		 * Get all includes
627
		 */
628
		$all_includes = $this->get_includes_list(true, $with_disabled);
629
		$includes_map = [];
630
		/**
631
		 * Array [package => [list of packages it depends on]]
632
		 */
633
		$dependencies    = [];
634
		$functionalities = [];
635
		/**
636
		 * According to components's maps some files should be included only on specific pages.
637
		 * Here we read this rules, and remove from whole includes list such items, that should be included only on specific pages.
638
		 * Also collect dependencies.
639
		 */
640
		$Config = Config::instance();
641
		foreach ($Config->components['modules'] as $module_name => $module_data) {
642
			if (
643
				$module_data['active'] == Config\Module_Properties::UNINSTALLED ||
644
				(
645
					$module_data['active'] == Config\Module_Properties::DISABLED &&
646
					!$with_disabled
647
				)
648
			) {
649
				continue;
650
			}
651
			if (file_exists(MODULES."/$module_name/meta.json")) {
652
				$this->process_meta(
653
					file_get_json(MODULES."/$module_name/meta.json"),
654
					$dependencies,
655
					$functionalities
656
				);
657
			}
658
			if (file_exists(MODULES."/$module_name/includes/map.json")) {
659
				$this->process_map(
660
					file_get_json_nocomments(MODULES."/$module_name/includes/map.json"),
661
					MODULES."/$module_name/includes",
662
					$includes_map,
663
					$all_includes
664
				);
665
			}
666
		}
667
		unset($module_name, $module_data);
668
		foreach ($Config->components['plugins'] as $plugin_name) {
669
			if (file_exists(PLUGINS."/$plugin_name/meta.json")) {
670
				$this->process_meta(
671
					file_get_json(PLUGINS."/$plugin_name/meta.json"),
672
					$dependencies,
673
					$functionalities
674
				);
675
			}
676
			if (file_exists(PLUGINS."/$plugin_name/includes/map.json")) {
677
				$this->process_map(
678
					file_get_json_nocomments(PLUGINS."/$plugin_name/includes/map.json"),
679
					PLUGINS."/$plugin_name/includes",
680
					$includes_map,
681
					$all_includes
682
				);
683
			}
684
		}
685
		unset($plugin_name);
686
		/**
687
		 * For consistency
688
		 */
689
		$includes_map[''] = $all_includes;
690
		Event::instance()->fire(
691
			'System/Page/includes_dependencies_and_map',
692
			[
693
				'dependencies' => &$dependencies,
694
				'includes_map' => &$includes_map
695
			]
696
		);
697
		$dependencies = $this->normalize_dependencies($dependencies, $functionalities);
698
		$includes_map = $this->clean_includes_arrays_without_files($dependencies, $includes_map);
699
		$dependencies = array_map('array_values', $dependencies);
700
		$dependencies = array_filter($dependencies);
701
		return [$dependencies, $includes_map];
702
	}
703
	/**
704
	 * Process meta information and corresponding entries to dependencies and functionalities
705
	 *
706
	 * @param array $meta
707
	 * @param array $dependencies
708
	 * @param array $functionalities
709
	 */
710
	protected function process_meta ($meta, &$dependencies, &$functionalities) {
711
		$package = $meta['package'];
712
		if (isset($meta['require'])) {
713
			foreach ((array)$meta['require'] as $r) {
714
				/**
715
				 * Get only name of package or functionality
716
				 */
717
				$r                        = preg_split('/[=<>]/', $r, 2)[0];
718
				$dependencies[$package][] = $r;
719
			}
720
		}
721
		if (isset($meta['optional'])) {
722
			foreach ((array)$meta['optional'] as $o) {
723
				/**
724
				 * Get only name of package or functionality
725
				 */
726
				$o                        = preg_split('/[=<>]/', $o, 2)[0];
727
				$dependencies[$package][] = $o;
728
			}
729
			unset($o);
730
		}
731
		if (isset($meta['provide'])) {
732
			foreach ((array)$meta['provide'] as $p) {
733
				/**
734
				 * If provides sub-functionality for other component (for instance, `Blog/post_patch`) - inverse "providing" to "dependency"
735
				 * Otherwise it is just functionality alias to package name
736
				 */
737
				if (strpos($p, '/') !== false) {
738
					/**
739
					 * Get name of package or functionality
740
					 */
741
					$p                  = explode('/', $p)[0];
742
					$dependencies[$p][] = $package;
743
				} else {
744
					$functionalities[$p] = $package;
745
				}
746
			}
747
			unset($p);
748
		}
749
	}
750
	/**
751
	 * Process map structure, fill includes map and remove files from list of all includes (remaining files will be included on all pages)
752
	 *
753
	 * @param array  $map
754
	 * @param string $includes_dir
755
	 * @param array  $includes_map
756
	 * @param array  $all_includes
757
	 */
758
	protected function process_map ($map, $includes_dir, &$includes_map, &$all_includes) {
759
		foreach ($map as $path => $files) {
760
			foreach ((array)$files as $file) {
761
				$extension = file_extension($file);
762
				switch ($extension) {
763
					case 'css':
764
					case 'js':
765
					case 'html':
766
						$file                              = "$includes_dir/$extension/$file";
767
						$includes_map[$path][$extension][] = $file;
768
						$all_includes[$extension]          = array_diff($all_includes[$extension], [$file]);
769
						break;
770
					default:
771
						$file = rtrim($file, '*');
772
						/**
773
						 * Wildcard support, it is possible to specify just path prefix and all files with this prefix will be included
774
						 */
775
						foreach (['css', 'js', 'html'] as $extension) {
776
							$base_path   = "$includes_dir/$extension/$file";
777
							$found_files = get_files_list("$includes_dir/$extension", "/.*\\.$extension$/i", 'f', true, true, 'name', '!include') ?: [];
778
							foreach ($found_files as $f) {
779
								if (strpos($f, $base_path) === 0) {
780
									$includes_map[$path][$extension][] = $f;
781
									$all_includes[$extension]          = array_diff($all_includes[$extension], [$f]);
782
								}
783
							}
784
						}
785
				}
786
			}
787
		}
788
	}
789
	/**
790
	 * Replace functionalities by real packages names, take into account recursive dependencies
791
	 *
792
	 * @param array $dependencies
793
	 * @param array $functionalities
794
	 *
795
	 * @return array
796
	 */
797
	protected function normalize_dependencies ($dependencies, $functionalities) {
798
		/**
799
		 * First of all remove packages without any dependencies
800
		 */
801
		$dependencies = array_filter($dependencies);
802
		/**
803
		 * First round, process aliases among keys
804
		 */
805
		foreach (array_keys($dependencies) as $d) {
806
			if (isset($functionalities[$d])) {
807
				$package = $functionalities[$d];
808
				/**
809
				 * Add dependencies to existing package dependencies
810
				 */
811
				foreach ($dependencies[$d] as $dependency) {
812
					$dependencies[$package][] = $dependency;
813
				}
814
				/**
815
				 * Drop alias
816
				 */
817
				unset($dependencies[$d]);
818
			}
819
		}
820
		unset($d, $dependency);
821
		/**
822
		 * Second round, process aliases among dependencies
823
		 */
824
		foreach ($dependencies as &$depends_on) {
825
			foreach ($depends_on as &$dependency) {
826
				if (isset($functionalities[$dependency])) {
827
					$dependency = $functionalities[$dependency];
828
				}
829
			}
830
		}
831
		unset($depends_on, $dependency);
832
		/**
833
		 * Third round, process recursive dependencies
834
		 */
835
		foreach ($dependencies as &$depends_on) {
836
			foreach ($depends_on as &$dependency) {
837
				if ($dependency != 'System' && isset($dependencies[$dependency])) {
838
					foreach (array_diff($dependencies[$dependency], $depends_on) as $new_dependency) {
839
						$depends_on[] = $new_dependency;
840
					}
841
				}
842
			}
843
		}
844
		return array_map('array_unique', $dependencies);
845
	}
846
	/**
847
	 * Includes array is composed from dependencies and sometimes dependencies doesn't have any files, so we'll clean that
848
	 *
849
	 * @param array $dependencies
850
	 * @param array $includes_map
851
	 *
852
	 * @return array
853
	 */
854
	protected function clean_includes_arrays_without_files ($dependencies, $includes_map) {
855
		foreach ($dependencies as &$depends_on) {
856
			foreach ($depends_on as $index => &$dependency) {
857
				if (!isset($includes_map[$dependency])) {
858
					unset($depends_on[$index]);
859
				}
860
			}
861
			unset($dependency);
862
		}
863
		return $includes_map;
864
	}
865
}
866