Completed
Push — master ( def5f1...9e1e48 )
by Nazar
04:00
created

Includes::get_includes_list()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 30
Code Lines 20

Duplication

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