Completed
Push — master ( 3afa85...ec4288 )
by Nazar
04:11
created

Includes::js_internal()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 3
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
	cs\Page\Includes\RequireJS;
19
20
/**
21
 * Includes management for `cs\Page` class
22
 *
23
 * @property string $Title
24
 * @property string $Description
25
 * @property string $canonical_url
26
 * @property string $Head
27
 * @property string $post_Body
28
 * @property string $theme
29
 */
30
trait Includes {
31
	use
32
		RequireJS;
33
	/**
34
	 * @var array[]
35
	 */
36
	protected $core_html;
37
	/**
38
	 * @var array[]
39
	 */
40
	protected $core_js;
41
	/**
42
	 * @var array[]
43
	 */
44
	protected $core_css;
45
	/**
46
	 * @var string
47
	 */
48
	protected $core_config;
49
	/**
50
	 * @var array[]
51
	 */
52
	protected $html;
53
	/**
54
	 * @var array[]
55
	 */
56
	protected $js;
57
	/**
58
	 * @var array[]
59
	 */
60
	protected $css;
61
	/**
62
	 * @var string
63
	 */
64
	protected $config;
65
	/**
66
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
67
	 * @var string
68
	 */
69
	protected $pcache_basename;
70
	protected function init_includes () {
71
		$this->core_html       = ['path' => [], 'plain' => ''];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('path' => array(), 'plain' => '') of type array<string,array|strin...ray","plain":"string"}> is incompatible with the declared type array<integer,array> of property $core_html.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
72
		$this->core_js         = ['path' => [], 'plain' => ''];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('path' => array(), 'plain' => '') of type array<string,array|strin...ray","plain":"string"}> is incompatible with the declared type array<integer,array> of property $core_js.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
73
		$this->core_css        = ['path' => [], 'plain' => ''];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('path' => array(), 'plain' => '') of type array<string,array|strin...ray","plain":"string"}> is incompatible with the declared type array<integer,array> of property $core_css.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
74
		$this->core_config     = '';
75
		$this->html            = ['path' => [], 'plain' => ''];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('path' => array(), 'plain' => '') of type array<string,array|strin...ray","plain":"string"}> is incompatible with the declared type array<integer,array> of property $html.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
76
		$this->js              = ['path' => [], 'plain' => ''];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('path' => array(), 'plain' => '') of type array<string,array|strin...ray","plain":"string"}> is incompatible with the declared type array<integer,array> of property $js.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
77
		$this->css             = ['path' => [], 'plain' => ''];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('path' => array(), 'plain' => '') of type array<string,array|strin...ray","plain":"string"}> is incompatible with the declared type array<integer,array> of property $css.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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