Completed
Push — master ( b7b84b...78616b )
by Nazar
04:19
created

Includes::config_internal()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 16
rs 9.4285
cc 2
eloc 11
nc 2
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
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
		/**
287
		 * If CSS and JavaScript compression enabled
288
		 */
289
		if ($Config->core['cache_compress_js_css'] && !(Request::instance()->admin_path && isset($_GET['debug']))) {
290
			$this->webcomponents_polyfill(true);
291
			$includes = $this->get_includes_for_page_with_compression();
292
		} else {
293
			$this->webcomponents_polyfill(false);
294
			/**
295
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
296
			 */
297
			/**
298
			 * @var \cs\Page $this
299
			 */
300
			$this->config_internal(Language::instance(), 'cs.Language', true);
301
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
302
			$includes = $this->get_includes_for_page_without_compression($Config);
1 ignored issue
show
Documentation introduced by
$Config is of type object<cs\Singleton\Base>|object<cs\False_class>, but the function expects a object<cs\Config>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
303
		}
304
		$this->css_internal($includes['css'], 'file', true);
305
		$this->js_internal($includes['js'], 'file', true);
306
		$this->html_internal($includes['html'], 'file', true);
307
		$this->add_includes_on_page_manually_added($Config);
1 ignored issue
show
Documentation introduced by
$Config is of type object<cs\Singleton\Base>|object<cs\False_class>, but the function expects a object<cs\Config>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
308
		return $this;
309
	}
310
	/**
311
	 * @return string[]
312
	 */
313
	protected function get_requirejs_paths () {
314
		$Config = Config::instance();
315
		$paths  = [];
316
		foreach ($Config->components['modules'] as $module_name => $module_data) {
317
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
318
				continue;
319
			}
320
			$this->get_requirejs_paths_add_aliases(MODULES."/$module_name", $paths);
321
		}
322
		foreach ($Config->components['plugins'] as $plugin_name) {
323
			$this->get_requirejs_paths_add_aliases(PLUGINS."/$plugin_name", $paths);
324
		}
325
		$directories_to_browse = [
326
			DIR.'/bower_components',
327
			DIR.'/node_modules'
328
		];
329
		Event::instance()->fire(
330
			'System/Page/requirejs',
331
			[
332
				'paths'                 => &$paths,
333
				'directories_to_browse' => &$directories_to_browse
334
			]
335
		);
336
		foreach ($directories_to_browse as $dir) {
337
			foreach (get_files_list($dir, false, 'd', true) as $d) {
338
				$this->get_requirejs_paths_find_package($d, $paths);
339
			}
340
		}
341
		return $paths;
342
	}
343
	/**
344
	 * @param string   $dir
345
	 * @param string[] $paths
346
	 */
347
	protected function get_requirejs_paths_add_aliases ($dir, &$paths) {
348
		if (is_dir("$dir/includes/js")) {
349
			$name         = basename($dir);
350
			$paths[$name] = $this->absolute_path_to_relative("$dir/includes/js");
351
			foreach ((array)@file_get_json("$dir/meta.json")['provide'] as $p) {
352
				if (strpos($p, '/') !== false) {
353
					$paths[$p] = $paths[$name];
354
				}
355
			}
356
		}
357
	}
358
	/**
359
	 * @param string   $dir
360
	 * @param string[] $paths
361
	 */
362
	protected function get_requirejs_paths_find_package ($dir, &$paths) {
363
		$path = $this->get_requirejs_paths_find_package_bower($dir) ?: $this->get_requirejs_paths_find_package_npm($dir);
364
		if ($path) {
365
			$paths[basename($dir)] = $this->absolute_path_to_relative(substr($path, 0, -3));
366
		}
367
	}
368
	/**
369
	 * @param string $dir
370
	 *
371
	 * @return string
372
	 */
373
	protected function get_requirejs_paths_find_package_bower ($dir) {
374
		$bower = @file_get_json("$dir/bower.json");
375
		foreach (@(array)$bower['main'] as $main) {
376
			if (preg_match('/\.js$/', $main)) {
377
				$main = substr($main, 0, -3);
378
				// There is a chance that minified file is present
379
				$main = file_exists_with_extension("$dir/$main", ['min.js', 'js']);
380
				if ($main) {
381
					return $main;
382
				}
383
			}
384
		}
385
		return null;
386
	}
387
	/**
388
	 * @param string $dir
389
	 *
390
	 * @return false|string
391
	 */
392
	protected function get_requirejs_paths_find_package_npm ($dir) {
393
		$package = @file_get_json("$dir/package.json");
394
		// If we have browser-specific declaration - use it
395
		$main = @$package['browser'] ?: (@$package['jspm']['main'] ?: @$package['main']);
396
		if (preg_match('/\.js$/', $main)) {
397
			$main = substr($main, 0, -3);
398
		}
399
		if ($main) {
400
			// There is a chance that minified file is present
401
			return file_exists_with_extension("$dir/$main", ['min.js', 'js']) ?: file_exists_with_extension("$dir/dist/$main", ['min.js', 'js']);
402
		}
403
	}
404
	/**
405
	 * Since modules, plugins and storage directories can be (at least theoretically) moved from default location - let's do proper path conversion
406
	 *
407
	 * @param string|string[] $path
408
	 *
409
	 * @return string|string[]
1 ignored issue
show
Documentation introduced by
Should the return type not be array|string? Also, consider making the array more specific, something like array<String>, or String[].

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.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
410
	 */
411
	protected function absolute_path_to_relative ($path) {
412
		if (is_array($path)) {
413
			foreach ($path as &$p) {
414
				$p = $this->absolute_path_to_relative($p);
415
			}
416
			return $path;
417
		}
418
		if (strpos($path, MODULES) === 0) {
419
			return 'components/modules'.substr($path, strlen(MODULES));
420
		}
421
		if (strpos($path, PLUGINS) === 0) {
422
			return 'components/plugins'.substr($path, strlen(PLUGINS));
423
		}
424
		if (strpos($path, STORAGE) === 0) {
425
			return 'storage'.substr($path, strlen(STORAGE));
426
		}
427
		return substr($path, strlen(DIR) + 1);
428
	}
429
	/**
430
	 * Add JS polyfills for IE/Edge
431
	 */
432
	protected function ie_edge () {
433
		if (preg_match('/Trident|Edge/', Request::instance()->header('user-agent'))) {
434
			$this->js_internal(
435
				get_files_list(DIR."/includes/js/microsoft_sh*t", "/.*\\.js$/i", 'f', "includes/js/microsoft_sh*t", true),
436
				'file',
437
				true
438
			);
439
		}
440
	}
441
	/**
442
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
443
	 *
444
	 * TODO: Probably, some effective User Agent-based check might be used here
445
	 *
446
	 * @param bool $with_compression
447
	 */
448
	protected function webcomponents_polyfill ($with_compression) {
449
		if (!isset($_COOKIE['shadow_dom']) || $_COOKIE['shadow_dom'] != 1) {
450
			$file = 'includes/js/WebComponents-polyfill/webcomponents-custom.min.js';
451
			if ($with_compression) {
452
				$compressed_file = PUBLIC_CACHE.'/webcomponents.js';
453
				if (!file_exists($compressed_file)) {
454
					$content = file_get_contents(DIR."/$file");
455
					file_put_contents($compressed_file, gzencode($content, 9), LOCK_EX | FILE_BINARY);
456
					file_put_contents("$compressed_file.hash", substr(md5($content), 0, 5));
457
				}
458
				$hash = file_get_contents("$compressed_file.hash");
459
				$this->js_internal("storage/pcache/webcomponents.js?$hash", 'file', true);
460
			} else {
461
				$this->js_internal($file, 'file', true);
462
			}
463
		}
464
	}
465
	protected function add_system_configs () {
466
		$Config         = Config::instance();
467
		$Request        = Request::instance();
468
		$User           = User::instance();
469
		$current_module = $Request->current_module;
470
		$this->config_internal(
471
			[
472
				'base_url'              => $Config->base_url(),
473
				'current_base_url'      => $Config->base_url().'/'.($Request->admin_path ? 'admin/' : '').$current_module,
474
				'public_key'            => Core::instance()->public_key,
475
				'module'                => $current_module,
476
				'in_admin'              => (int)$Request->admin_path,
477
				'is_admin'              => (int)$User->admin(),
478
				'is_user'               => (int)$User->user(),
479
				'is_guest'              => (int)$User->guest(),
480
				'password_min_length'   => (int)$Config->core['password_min_length'],
481
				'password_min_strength' => (int)$Config->core['password_min_strength'],
482
				'debug'                 => (int)DEBUG,
483
				'route'                 => $Request->route,
484
				'route_path'            => $Request->route_path,
485
				'route_ids'             => $Request->route_ids
486
			],
487
			'cs',
488
			true
489
		);
490
		if ($User->admin()) {
491
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
492
		}
493
	}
494
	/**
495
	 * @return array[]
496
	 */
497
	protected function get_includes_for_page_with_compression () {
498
		/**
499
		 * Rebuild cache if necessary
500
		 */
501
		if (!file_exists(PUBLIC_CACHE."/$this->pcache_basename.json")) {
502
			$this->rebuild_cache();
503
		}
504
		list($dependencies, $structure) = file_get_json(PUBLIC_CACHE."/$this->pcache_basename.json");
505
		$system_includes = [
506
			'css'  => ["storage/pcache/$this->pcache_basename.css?{$structure['']['css']}"],
507
			'js'   => ["storage/pcache/$this->pcache_basename.js?{$structure['']['js']}"],
508
			'html' => ["storage/pcache/$this->pcache_basename.html?{$structure['']['html']}"]
509
		];
510
		list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '+');
511
		foreach ($structure as $filename_prefix => $hashes) {
512
			if (!$filename_prefix) {
513
				continue;
514
			}
515
			$is_dependency = $this->get_includes_is_dependency($dependencies, $filename_prefix, '+');
516
			if ($is_dependency || mb_strpos($current_url, $filename_prefix) === 0) {
517
				foreach ($hashes as $extension => $hash) {
518
					if ($is_dependency) {
519
						$dependencies_includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
520
					} else {
521
						$includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
522
					}
523
				}
524
			}
525
		}
526
		return array_merge_recursive($system_includes, $dependencies_includes, $includes);
527
	}
528
	/**
529
	 * @param Config $Config
530
	 *
531
	 * @return array[]
532
	 */
533
	protected function get_includes_for_page_without_compression ($Config) {
534
		// To determine all dependencies and stuff we need `$Config` object to be already created
535
		if ($Config) {
536
			list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
537
			$system_includes = $includes_map[''];
538
			list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '/');
539
			foreach ($includes_map as $url => $local_includes) {
540
				if (!$url) {
541
					continue;
542
				}
543
				$is_dependency = $this->get_includes_is_dependency($dependencies, $url, '/');
544
				if ($is_dependency) {
545
					$dependencies_includes = array_merge_recursive($dependencies_includes, $local_includes);
546
				} elseif (mb_strpos($current_url, $url) === 0) {
547
					$includes = array_merge_recursive($includes, $local_includes);
548
				}
549
			}
550
			$includes = array_merge_recursive($system_includes, $dependencies_includes, $includes);
551
			$includes = $this->absolute_path_to_relative($includes);
552
		} else {
553
			$includes = $this->get_includes_list();
554
		}
555
		return $this->add_versions_hash($includes);
556
	}
557
	/**
558
	 * @param array  $dependencies
559
	 * @param string $separator `+` or `/`
560
	 *
561
	 * @return array
1 ignored issue
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<array|string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
562
	 */
563
	protected function get_includes_prepare ($dependencies, $separator) {
564
		$Request               = Request::instance();
565
		$includes              = [
566
			'css'  => [],
567
			'js'   => [],
568
			'html' => []
569
		];
570
		$dependencies_includes = $includes;
571
		$current_module        = $Request->current_module;
572
		/**
573
		 * Current URL based on controller path (it better represents how page was rendered)
574
		 */
575
		$current_url = array_slice(App::instance()->controller_path, 1);
576
		$current_url = ($Request->admin_path ? "admin$separator" : '')."$current_module$separator".implode($separator, $current_url);
577
		/**
578
		 * Narrow the dependencies to current module only
579
		 */
580
		$dependencies = array_merge(
581
			isset($dependencies[$current_module]) ? $dependencies[$current_module] : [],
582
			$dependencies['System']
583
		);
584
		return [$includes, $dependencies_includes, $dependencies, $current_url];
585
	}
586
	/**
587
	 * @param array  $dependencies
588
	 * @param string $url
589
	 * @param string $separator `+` or `/`
590
	 *
591
	 * @return bool
592
	 */
593
	protected function get_includes_is_dependency ($dependencies, $url, $separator) {
594
		$url_exploded = explode($separator, $url);
595
		/** @noinspection NestedTernaryOperatorInspection */
596
		$url_module = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
597
		$Request    = Request::instance();
598
		return
599
			$url_module !== Config::SYSTEM_MODULE &&
600
			in_array($url_module, $dependencies) &&
601
			(
602
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
603
			);
604
	}
605
	protected function add_versions_hash ($includes) {
606
		$content = '';
607
		foreach (get_files_list(MODULES, false, 'd') as $module) {
608
			if (file_exists(MODULES."/$module/meta.json")) {
609
				$content .= file_get_contents(MODULES."/$module/meta.json");
610
			}
611
		}
612
		foreach (get_files_list(PLUGINS, false, 'd') as $plugin) {
613
			if (file_exists(PLUGINS."/$plugin/meta.json")) {
614
				$content .= file_get_contents(PLUGINS."/$plugin/meta.json");
615
			}
616
		}
617
		$hash = substr(md5($content), 0, 5);
618
		foreach ($includes as &$files) {
619
			foreach ($files as &$file) {
620
				$file .= "?$hash";
621
			}
622
			unset($file);
623
		}
624
		return $includes;
625
	}
626
	/**
627
	 * @param Config $Config
628
	 */
629
	protected function add_includes_on_page_manually_added ($Config) {
630
		foreach (['core_html', 'core_js', 'core_css', 'html', 'js', 'css'] as $type) {
631
			foreach ($this->$type as &$elements) {
632
				$elements = implode('', array_unique($elements));
633
			}
634
			unset($elements);
635
		}
636
		$this->Head .=
637
			$this->core_config.
638
			$this->config.
639
			$this->core_css[0].$this->css[0].
640
			h::style($this->core_css[1].$this->css[1] ?: false);
641
		$js_html_insert_to = $Config->core['put_js_after_body'] ? 'post_Body' : 'Head';
642
		$js_html           =
643
			$this->core_js[0].
644
			h::script($this->core_js[1] ?: false).
645
			$this->js[0].
646
			h::script($this->js[1] ?: false).
647
			$this->core_html[0].$this->html[0].
648
			$this->core_html[1].$this->html[1];
649
		$this->$js_html_insert_to .= $js_html;
650
	}
651
	/**
652
	 * Getting of HTML, JS and CSS files list to be included
653
	 *
654
	 * @param bool $absolute If <i>true</i> - absolute paths to files will be returned
655
	 *
656
	 * @return array
657
	 */
658
	protected function get_includes_list ($absolute = false) {
659
		$theme_dir  = THEMES."/$this->theme";
660
		$theme_pdir = "themes/$this->theme";
661
		$get_files  = function ($dir, $prefix_path) {
662
			$extension = basename($dir);
663
			$list      = get_files_list($dir, "/.*\\.$extension$/i", 'f', $prefix_path, true, 'name', '!include') ?: [];
664
			sort($list);
665
			return $list;
666
		};
667
		/**
668
		 * Get includes of system and theme
669
		 */
670
		$includes = [];
671
		foreach (['html', 'js', 'css'] as $type) {
672
			$includes[$type] = array_merge(
673
				$get_files(DIR."/includes/$type", $absolute ? true : "includes/$type"),
674
				$get_files("$theme_dir/$type", $absolute ? true : "$theme_pdir/$type")
675
			);
676
		}
677
		unset($theme_dir, $theme_pdir);
678
		$Config = Config::instance();
679
		foreach ($Config->components['modules'] as $module_name => $module_data) {
680
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
681
				continue;
682
			}
683
			foreach (['html', 'js', 'css'] as $type) {
684
				/** @noinspection SlowArrayOperationsInLoopInspection */
685
				$includes[$type] = array_merge(
686
					$includes[$type],
687
					$get_files(MODULES."/$module_name/includes/$type", $absolute ? true : "components/modules/$module_name/includes/$type")
688
				);
689
			}
690
		}
691
		foreach ($Config->components['plugins'] as $plugin_name) {
692
			foreach (['html', 'js', 'css'] as $type) {
693
				/** @noinspection SlowArrayOperationsInLoopInspection */
694
				$includes[$type] = array_merge(
695
					$includes[$type],
696
					$get_files(PLUGINS."/$plugin_name/includes/$type", $absolute ? true : "components/plugins/$plugin_name/includes/$type")
697
				);
698
			}
699
		}
700
		return $includes;
701
	}
702
	/**
703
	 * Rebuilding of HTML, JS and CSS cache
704
	 *
705
	 * @return \cs\Page
706
	 */
707
	protected function rebuild_cache () {
708
		list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
709
		$structure = [];
710
		foreach ($includes_map as $filename_prefix => $includes) {
711
			// We replace `/` by `+` to make it suitable for filename
712
			$filename_prefix             = str_replace('/', '+', $filename_prefix);
713
			$structure[$filename_prefix] = $this->create_cached_includes_files($filename_prefix, $includes);
714
		}
715
		unset($includes_map, $filename_prefix, $includes);
716
		file_put_json(
717
			PUBLIC_CACHE."/$this->pcache_basename.json",
718
			[$dependencies, $structure]
719
		);
720
		unset($structure);
721
		Event::instance()->fire('System/Page/rebuild_cache');
722
		return $this;
723
	}
724
	/**
725
	 * Creates cached version of given HTML, JS and CSS files.
726
	 * Resulting file name consists of <b>$filename_prefix</b> and <b>$this->pcache_basename</b>
727
	 *
728
	 * @param string $filename_prefix
729
	 * @param array  $includes Array of paths to files, may have keys: <b>css</b> and/or <b>js</b> and/or <b>html</b>
730
	 *
731
	 * @return array
732
	 */
733
	protected function create_cached_includes_files ($filename_prefix, $includes) {
734
		$cache_hash = [];
735
		/** @noinspection AlterInForeachInspection */
736
		foreach ($includes as $extension => $files) {
737
			$content = $this->create_cached_includes_files_process_files(
738
				$extension,
739
				$filename_prefix,
740
				$files
741
			);
742
			file_put_contents(PUBLIC_CACHE."/$filename_prefix$this->pcache_basename.$extension", gzencode($content, 9), LOCK_EX | FILE_BINARY);
743
			$cache_hash[$extension] = substr(md5($content), 0, 5);
744
		}
745
		return $cache_hash;
746
	}
747
	protected function create_cached_includes_files_process_files ($extension, $filename_prefix, $files) {
748
		$content = '';
749
		switch ($extension) {
750
			/**
751
			 * Insert external elements into resulting css file.
752
			 * It is needed, because those files will not be copied into new destination of resulting css file.
753
			 */
754
			case 'css':
755
				$callback = function ($content, $file) {
756
					return
757
						$content.
758
						Includes_processing::css(
759
							file_get_contents($file),
760
							$file
761
						);
762
				};
763
				break;
764
			/**
765
			 * Combine css and js files for Web Component into resulting files in order to optimize loading process
766
			 */
767
			case 'html':
768
				/**
769
				 * For CSP-compatible HTML files we need to know destination to put there additional JS/CSS files
770
				 */
771
				$destination = Config::instance()->core['vulcanization'] ? false : PUBLIC_CACHE;
772
				$callback    = function ($content, $file) use ($filename_prefix, $destination) {
773
					return
774
						$content.
775
						Includes_processing::html(
776
							file_get_contents($file),
777
							$file,
778
							"$filename_prefix$this->pcache_basename-".basename($file).'+'.substr(md5($file), 0, 5),
779
							$destination
780
						);
781
				};
782
				break;
783
			case 'js':
784
				$callback = function ($content, $file) {
785
					return
786
						$content.
787
						Includes_processing::js(file_get_contents($file));
788
				};
789
				if ($filename_prefix == '') {
790
					$content = 'window.cs={Language:'._json_encode(Language::instance()).'};';
791
					$content .= 'window.requirejs={paths:'._json_encode($this->get_requirejs_paths()).'};';
792
				}
793
		}
794
		/** @noinspection PhpUndefinedVariableInspection */
795
		return array_reduce(array_filter($files, 'file_exists'), $callback, $content);
796
	}
797
	/**
798
	 * 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
799
	 *
800
	 * @return array[] [$dependencies, $includes_map]
801
	 */
802
	protected function includes_dependencies_and_map () {
803
		/**
804
		 * Get all includes
805
		 */
806
		$all_includes = $this->get_includes_list(true);
807
		$includes_map = [];
808
		/**
809
		 * Array [package => [list of packages it depends on]]
810
		 */
811
		$dependencies    = [];
812
		$functionalities = [];
813
		/**
814
		 * According to components's maps some files should be included only on specific pages.
815
		 * Here we read this rules, and remove from whole includes list such items, that should be included only on specific pages.
816
		 * Also collect dependencies.
817
		 */
818
		$Config = Config::instance();
819
		foreach ($Config->components['modules'] as $module_name => $module_data) {
820
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
821
				continue;
822
			}
823
			if (file_exists(MODULES."/$module_name/meta.json")) {
824
				$this->process_meta(
825
					file_get_json(MODULES."/$module_name/meta.json"),
826
					$dependencies,
827
					$functionalities
828
				);
829
			}
830
			if (file_exists(MODULES."/$module_name/includes/map.json")) {
831
				$this->process_map(
832
					file_get_json_nocomments(MODULES."/$module_name/includes/map.json"),
833
					MODULES."/$module_name/includes",
834
					$includes_map,
835
					$all_includes
836
				);
837
			}
838
		}
839
		unset($module_name, $module_data);
840
		foreach ($Config->components['plugins'] as $plugin_name) {
841
			if (file_exists(PLUGINS."/$plugin_name/meta.json")) {
842
				$this->process_meta(
843
					file_get_json(PLUGINS."/$plugin_name/meta.json"),
844
					$dependencies,
845
					$functionalities
846
				);
847
			}
848
			if (file_exists(PLUGINS."/$plugin_name/includes/map.json")) {
849
				$this->process_map(
850
					file_get_json_nocomments(PLUGINS."/$plugin_name/includes/map.json"),
851
					PLUGINS."/$plugin_name/includes",
852
					$includes_map,
853
					$all_includes
854
				);
855
			}
856
		}
857
		unset($plugin_name);
858
		/**
859
		 * For consistency
860
		 */
861
		$includes_map[''] = $all_includes;
862
		Event::instance()->fire(
863
			'System/Page/includes_dependencies_and_map',
864
			[
865
				'dependencies' => &$dependencies,
866
				'includes_map' => &$includes_map
867
			]
868
		);
869
		$dependencies = $this->normalize_dependencies($dependencies, $functionalities);
870
		$includes_map = $this->clean_includes_arrays_without_files($dependencies, $includes_map);
871
		$dependencies = array_map('array_values', $dependencies);
872
		$dependencies = array_filter($dependencies);
873
		return [$dependencies, $includes_map];
874
	}
875
	/**
876
	 * Process meta information and corresponding entries to dependencies and functionalities
877
	 *
878
	 * @param array $meta
879
	 * @param array $dependencies
880
	 * @param array $functionalities
881
	 */
882
	protected function process_meta ($meta, &$dependencies, &$functionalities) {
883
		$package = $meta['package'];
884
		if (isset($meta['require'])) {
885
			foreach ((array)$meta['require'] as $r) {
886
				/**
887
				 * Get only name of package or functionality
888
				 */
889
				$r                        = preg_split('/[=<>]/', $r, 2)[0];
890
				$dependencies[$package][] = $r;
891
			}
892
		}
893
		if (isset($meta['optional'])) {
894
			foreach ((array)$meta['optional'] as $o) {
895
				/**
896
				 * Get only name of package or functionality
897
				 */
898
				$o                        = preg_split('/[=<>]/', $o, 2)[0];
899
				$dependencies[$package][] = $o;
900
			}
901
			unset($o);
902
		}
903
		if (isset($meta['provide'])) {
904
			foreach ((array)$meta['provide'] as $p) {
905
				/**
906
				 * If provides sub-functionality for other component (for instance, `Blog/post_patch`) - inverse "providing" to "dependency"
907
				 * Otherwise it is just functionality alias to package name
908
				 */
909
				if (strpos($p, '/') !== false) {
910
					/**
911
					 * Get name of package or functionality
912
					 */
913
					$p                  = explode('/', $p)[0];
914
					$dependencies[$p][] = $package;
915
				} else {
916
					$functionalities[$p] = $package;
917
				}
918
			}
919
			unset($p);
920
		}
921
	}
922
	/**
923
	 * Process map structure, fill includes map and remove files from list of all includes (remaining files will be included on all pages)
924
	 *
925
	 * @param array  $map
926
	 * @param string $includes_dir
927
	 * @param array  $includes_map
928
	 * @param array  $all_includes
929
	 */
930
	protected function process_map ($map, $includes_dir, &$includes_map, &$all_includes) {
931
		foreach ($map as $path => $files) {
932
			foreach ((array)$files as $file) {
933
				$extension = file_extension($file);
934
				if (in_array($extension, ['css', 'js', 'html'])) {
935
					$file                              = "$includes_dir/$extension/$file";
936
					$includes_map[$path][$extension][] = $file;
937
					$all_includes[$extension]          = array_diff($all_includes[$extension], [$file]);
938
				} else {
939
					$file = rtrim($file, '*');
940
					/**
941
					 * Wildcard support, it is possible to specify just path prefix and all files with this prefix will be included
942
					 */
943
					$found_files = array_filter(
944
						get_files_list($includes_dir, '/.*\.(css|js|html)$/i', 'f', '', true, 'name', '!include') ?: [],
945
						function ($f) use ($file) {
946
							// We need only files with specified mask and only those located in directory that corresponds to file's extension
947
							return preg_match("#^(css|js|html)/$file.*\\1$#i", $f);
948
						}
949
					);
950
					// Drop first level directory
951
					$found_files = _preg_replace('#^[^/]+/(.*)#', '$1', $found_files);
952
					$this->process_map([$path => $found_files], $includes_dir, $includes_map, $all_includes);
953
				}
954
			}
955
		}
956
	}
957
	/**
958
	 * Replace functionalities by real packages names, take into account recursive dependencies
959
	 *
960
	 * @param array $dependencies
961
	 * @param array $functionalities
962
	 *
963
	 * @return array
964
	 */
965
	protected function normalize_dependencies ($dependencies, $functionalities) {
966
		/**
967
		 * First of all remove packages without any dependencies
968
		 */
969
		$dependencies = array_filter($dependencies);
970
		/**
971
		 * First round, process aliases among keys
972
		 */
973
		foreach (array_keys($dependencies) as $d) {
974
			if (isset($functionalities[$d])) {
975
				$package = $functionalities[$d];
976
				/**
977
				 * Add dependencies to existing package dependencies
978
				 */
979
				foreach ($dependencies[$d] as $dependency) {
980
					$dependencies[$package][] = $dependency;
981
				}
982
				/**
983
				 * Drop alias
984
				 */
985
				unset($dependencies[$d]);
986
			}
987
		}
988
		unset($d, $dependency);
989
		/**
990
		 * Second round, process aliases among dependencies
991
		 */
992
		foreach ($dependencies as &$depends_on) {
993
			foreach ($depends_on as &$dependency) {
994
				if (isset($functionalities[$dependency])) {
995
					$dependency = $functionalities[$dependency];
996
				}
997
			}
998
		}
999
		unset($depends_on, $dependency);
1000
		/**
1001
		 * Third round, process recursive dependencies
1002
		 */
1003
		foreach ($dependencies as &$depends_on) {
1004
			foreach ($depends_on as &$dependency) {
1005
				if ($dependency != 'System' && isset($dependencies[$dependency])) {
1006
					foreach (array_diff($dependencies[$dependency], $depends_on) as $new_dependency) {
1007
						$depends_on[] = $new_dependency;
1008
					}
1009
				}
1010
			}
1011
		}
1012
		return array_map('array_unique', $dependencies);
1013
	}
1014
	/**
1015
	 * Includes array is composed from dependencies and sometimes dependencies doesn't have any files, so we'll clean that
1016
	 *
1017
	 * @param array $dependencies
1018
	 * @param array $includes_map
1019
	 *
1020
	 * @return array
1021
	 */
1022
	protected function clean_includes_arrays_without_files ($dependencies, $includes_map) {
1023
		foreach ($dependencies as &$depends_on) {
1024
			foreach ($depends_on as $index => &$dependency) {
1025
				if (!isset($includes_map[$dependency])) {
1026
					unset($depends_on[$index]);
1027
				}
1028
			}
1029
			unset($dependency);
1030
		}
1031
		return $includes_map;
1032
	}
1033
}
1034