Completed
Push — master ( 031d4a...13e14b )
by Nazar
04:19
created

Includes::normalize_dependencies()   C

Complexity

Conditions 12
Paths 80

Size

Total Lines 49
Code Lines 20

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 49
rs 5.1474
cc 12
eloc 20
nc 80
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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