Completed
Push — master ( ab3150...6d49f6 )
by Nazar
04:25
created

Includes::get_requirejs_paths_find_package()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
rs 9.4285
cc 3
eloc 4
nc 4
nop 2
1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2014-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\Page;
9
use
10
	cs\Core,
11
	cs\Config,
12
	cs\Event,
13
	cs\Index,
14
	cs\Language,
15
	cs\Route,
16
	cs\User,
17
	h;
18
19
/**
20
 * Provides next events:
21
 *  System/Page/includes_dependencies_and_map
22
 *  [
23
 *    'dependencies' => &$dependencies,
24
 *    'includes_map' => &$includes_map
25
 *  ]
26
 *
27
 *  System/Page/rebuild_cache
28
 *  [
29
 *    'key' => &$key //Reference to the key, that will be appended to all css and js files, can be changed to reflect JavaScript and CSS changes
30
 *  ]
31
 *
32
 *  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
	protected $core_html   = [0 => [], 1 => []];
49
	protected $core_js     = [0 => [], 1 => []];
50
	protected $core_css    = [0 => [], 1 => []];
51
	protected $core_config = '';
52
	protected $html        = [0 => [], 1 => []];
53
	protected $js          = [0 => [], 1 => []];
54
	protected $css         = [0 => [], 1 => []];
55
	protected $config      = '';
56
	/**
57
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
58
	 * @var string
59
	 */
60
	protected $pcache_basename;
61
	/**
62
	 * Including of Web Components
63
	 *
64
	 * @param string|string[] $add  Path to including file, or code
65
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
66
	 *
67
	 * @return \cs\Page
68
	 */
69
	function html ($add, $mode = 'file') {
70
		return $this->html_internal($add, $mode);
71
	}
72
	/**
73
	 * @param string|string[] $add
74
	 * @param string          $mode
75
	 * @param bool            $core
76
	 *
77
	 * @return \cs\Page
78
	 */
79
	protected function html_internal ($add, $mode = 'file', $core = false) {
80
		if (!$add) {
81
			return $this;
82
		}
83
		if (is_array($add)) {
84
			foreach (array_filter($add) as $script) {
85
				$this->html_internal($script, $mode, $core);
86
			}
87
		} else {
88
			if ($core) {
89
				$html = &$this->core_html;
90
			} else {
91
				$html = &$this->html;
92
			}
93
			if ($mode == 'file') {
94
				$html[0][] = h::link(
95
					[
96
						'href' => $add,
97
						'rel'  => 'import'
98
					]
99
				);
100
			} elseif ($mode == 'code') {
101
				$html[1][] = "$add\n";
102
			}
103
		}
104
		return $this;
105
	}
106
	/**
107
	 * Including of JavaScript
108
	 *
109
	 * @param string|string[] $add  Path to including file, or code
110
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
111
	 *
112
	 * @return \cs\Page
113
	 */
114
	function js ($add, $mode = 'file') {
115
		return $this->js_internal($add, $mode);
116
	}
117
	/**
118
	 * @param string|string[] $add
119
	 * @param string          $mode
120
	 * @param bool            $core
121
	 *
122
	 * @return \cs\Page
123
	 */
124
	protected function js_internal ($add, $mode = 'file', $core = false) {
125
		if (!$add) {
126
			return $this;
127
		}
128
		if (is_array($add)) {
129
			foreach (array_filter($add) as $script) {
130
				$this->js_internal($script, $mode, $core);
131
			}
132
		} else {
133
			if ($core) {
134
				$js = &$this->core_js;
135
			} else {
136
				$js = &$this->js;
137
			}
138
			if ($mode == 'file') {
139
				$js[0][] = h::script(
140
					[
141
						'src' => $add
142
					]
143
				);
144
			} elseif ($mode == 'code') {
145
				$js[1][] = "$add\n";
146
			}
147
		}
148
		return $this;
149
	}
150
	/**
151
	 * Including of CSS
152
	 *
153
	 * @param string|string[] $add  Path to including file, or code
154
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
155
	 *
156
	 * @return \cs\Page
157
	 */
158
	function css ($add, $mode = 'file') {
159
		return $this->css_internal($add, $mode);
160
	}
161
	/**
162
	 * @param string|string[] $add
163
	 * @param string          $mode
164
	 * @param bool            $core
165
	 *
166
	 * @return \cs\Page
167
	 */
168
	protected function css_internal ($add, $mode = 'file', $core = false) {
169
		if (!$add) {
170
			return $this;
171
		}
172
		if (is_array($add)) {
173
			foreach (array_filter($add) as $style) {
174
				$this->css_internal($style, $mode, $core);
175
			}
176
		} else {
177
			if ($core) {
178
				$css = &$this->core_css;
179
			} else {
180
				$css = &$this->css;
181
			}
182
			if ($mode == 'file') {
183
				$css[0][] = h::link(
184
					[
185
						'href'           => $add,
186
						'rel'            => 'stylesheet',
187
						'shim-shadowdom' => true
188
					]
189
				);
190
			} elseif ($mode == 'code') {
191
				$css[1][] = "$add\n";
192
			}
193
		}
194
		return $this;
195
	}
196
	/**
197
	 * Add config on page to make it available on frontend
198
	 *
199
	 * @param mixed  $config_structure        Any scalar type or array
200
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
201
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
202
	 *
203
	 * @return \cs\Page
204
	 */
205
	function config ($config_structure, $target) {
206
		return $this->config_internal($config_structure, $target);
207
	}
208
	/**
209
	 * @param mixed  $config_structure
210
	 * @param string $target
211
	 * @param bool   $core
212
	 *
213
	 * @return \cs\Page
214
	 */
215
	protected function config_internal ($config_structure, $target, $core = false) {
216
		$config = h::script(
217
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
218
			[
219
				'target' => $target,
220
				'class'  => 'cs-config',
221
				'type'   => 'application/json'
222
			]
223
		);
224
		if ($core) {
225
			$this->core_config .= $config;
226
		} else {
227
			$this->config .= $config;
228
		}
229
		return $this;
230
	}
231
	/**
232
	 * Getting of HTML, JS and CSS includes
233
	 *
234
	 * @return \cs\Page
235
	 */
236
	protected function add_includes_on_page () {
237
		$Config = Config::instance(true);
238
		if (!$Config) {
239
			return $this;
240
		}
241
		/**
242
		 * Base name for cache files
243
		 */
244
		$this->pcache_basename = "_{$this->theme}_".Language::instance()->clang;
245
		/**
246
		 * Some JS configs required by system
247
		 */
248
		$this->add_system_configs();
249
		// TODO: I hope some day we'll get rid of this sh*t :(
250
		$this->ie_edge();
251
		/**
252
		 * If CSS and JavaScript compression enabled
253
		 */
254
		if ($Config->core['cache_compress_js_css'] && !(admin_path() && isset($_GET['debug']))) {
255
			$this->webcomponents_polyfill(true);
256
			$includes = $this->get_includes_for_page_with_compression();
257
		} else {
258
			$this->webcomponents_polyfill(false);
259
			/**
260
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
261
			 */
262
			/**
263
			 * @var \cs\Page $this
264
			 */
265
			$this->config_internal(Language::instance(), 'cs.Language', true);
266
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
2 ignored issues
show
Bug introduced by
The method get_requirejs_paths() cannot be called from this context as it is declared protected in class cs\Page\Includes.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
Bug introduced by
The method config_internal() cannot be called from this context as it is declared protected in class cs\Page\Includes.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
267
			$includes = $this->get_includes_for_page_without_compression($Config);
268
		}
269
		$this->css_internal($includes['css'], 'file', true);
270
		$this->js_internal($includes['js'], 'file', true);
271
		$this->html_internal($includes['html'], 'file', true);
272
		$this->add_includes_on_page_manually_added($Config);
273
		return $this;
274
	}
275
	/**
276
	 * @return string[]
277
	 */
278
	protected function get_requirejs_paths () {
279
		$Config = Config::instance();
280
		$paths  = [];
281
		foreach ($Config->components['modules'] as $module_name => $module_data) {
282
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
283
				continue;
284
			}
285
			$this->get_requirejs_paths_add_aliases(MODULES."/$module_name", $paths);
286
		}
287
		foreach ($Config->components['plugins'] as $plugin_name) {
288
			$this->get_requirejs_paths_add_aliases(PLUGINS."/$plugin_name", $paths);
289
		}
290
		$directories_to_browse = [
291
			DIR.'/bower_components',
292
			DIR.'/node_modules'
293
		];
294
		Event::instance()->fire(
295
			'System/Page/requirejs',
296
			[
297
				'paths'                 => &$paths,
298
				'directories_to_browse' => &$directories_to_browse
299
			]
300
		);
301
		foreach ($directories_to_browse as $dir) {
302
			foreach (get_files_list($dir, false, 'd', true) as $d) {
303
				$this->get_requirejs_paths_find_package($d, $paths);
304
			}
305
		}
306
		return $paths;
307
	}
308
	/**
309
	 * @param string   $dir
310
	 * @param string[] $paths
311
	 */
312
	protected function get_requirejs_paths_add_aliases ($dir, &$paths) {
313
		if (is_dir("$dir/includes/js")) {
314
			$name         = basename($dir);
315
			$paths[$name] = $this->absolute_path_to_relative("$dir/includes/js");
316
			foreach ((array)@file_get_json("$dir/meta.json")['provide'] as $p) {
317
				if (strpos($p, '/') !== false) {
318
					$paths[$p] = $paths[$name];
319
				}
320
			}
321
		}
322
	}
323
	/**
324
	 * @param string   $dir
325
	 * @param string[] $paths
326
	 */
327
	protected function get_requirejs_paths_find_package ($dir, &$paths) {
328
		$path = $this->get_requirejs_paths_find_package_bower($dir) ?: $this->get_requirejs_paths_find_package_npm($dir);
329
		if ($path) {
330
			$paths[basename($dir)] = $this->absolute_path_to_relative(substr($path, 0, -3));
331
		}
332
	}
333
	/**
334
	 * @param string $dir
335
	 *
336
	 * @return string
337
	 */
338
	protected function get_requirejs_paths_find_package_bower ($dir) {
339
		$bower = @file_get_json("$dir/bower.json");
340
		foreach (@(array)$bower['main'] as $main) {
341
			if (preg_match('/\.js$/', $main)) {
342
				$main = substr($main, 0, -3);
343
				// There is a chance that minified file is present
344
				$main = file_exists_with_extension("$dir/$main", ['min.js', 'js']);
345
				if ($main) {
346
					return $main;
347
				}
348
			}
349
		}
350
		return null;
351
	}
352
	/**
353
	 * @param string $dir
354
	 *
355
	 * @return false|string
356
	 */
357
	protected function get_requirejs_paths_find_package_npm ($dir) {
358
		$package = @file_get_json("$dir/package.json");
359
		// If we have browser-specific declaration - use it
360
		$main = @$package['browser'] ?: (@$package['jspm']['main'] ?: @$package['main']);
361
		if (preg_match('/\.js$/', $main)) {
362
			$main = substr($main, 0, -3);
363
		}
364
		if ($main) {
365
			// There is a chance that minified file is present
366
			return file_exists_with_extension("$dir/$main", ['min.js', 'js']) ?: file_exists_with_extension("$dir/dist/$main", ['min.js', 'js']);
367
		}
368
	}
369
	/**
370
	 * Since modules, plugins and storage directories can be (at least theoretically) moved from default location - let's do proper path conversion
371
	 *
372
	 * @param string|string[] $path
373
	 *
374
	 * @return 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...
375
	 */
376
	protected function absolute_path_to_relative ($path) {
377
		if (is_array($path)) {
378
			foreach ($path as &$p) {
379
				$p = $this->absolute_path_to_relative($p);
380
			}
381
			return $path;
382
		}
383
		if (strpos($path, MODULES) === 0) {
384
			return 'components/modules'.substr($path, strlen(MODULES));
385
		}
386
		if (strpos($path, PLUGINS) === 0) {
387
			return 'components/plugins'.substr($path, strlen(PLUGINS));
388
		}
389
		if (strpos($path, STORAGE) === 0) {
390
			return 'storage'.substr($path, strlen(STORAGE));
391
		}
392
		return substr($path, strlen(DIR) + 1);
393
	}
394
	/**
395
	 * Add JS polyfills for IE/Edge
396
	 */
397
	protected function ie_edge () {
398
		/**
399
		 * @var \cs\_SERVER $_SERVER
400
		 */
401
		if (preg_match('/Trident|Edge/', $_SERVER->user_agent)) {
402
			$this->js_internal(
403
				get_files_list(DIR."/includes/js/microsoft_sh*t", "/.*\\.js$/i", 'f', "includes/js/microsoft_sh*t", true),
404
				'file',
405
				true
406
			);
407
		}
408
	}
409
	/**
410
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
411
	 *
412
	 * TODO: Probably, some effective User Agent-based check might be used here
413
	 *
414
	 * @param bool $with_compression
415
	 */
416
	protected function webcomponents_polyfill ($with_compression) {
417
		if (!isset($_COOKIE['shadow_dom']) || $_COOKIE['shadow_dom'] != 1) {
418
			$file = 'includes/js/WebComponents-polyfill/webcomponents-custom.min.js';
419
			if ($with_compression) {
420
				$compressed_file = PUBLIC_CACHE.'/webcomponents.js';
421
				if (!file_exists($compressed_file)) {
422
					$content = file_get_contents(DIR."/$file");
423
					file_put_contents($compressed_file, gzencode($content, 9), LOCK_EX | FILE_BINARY);
424
					file_put_contents("$compressed_file.hash", substr(md5($content), 0, 5));
425
				}
426
				$hash = file_get_contents("$compressed_file.hash");
427
				$this->js_internal("storage/pcache/webcomponents.js?$hash", 'file', true);
428
			} else {
429
				$this->js_internal($file, 'file', true);
430
			}
431
		}
432
	}
433
	protected function add_system_configs () {
434
		$Config         = Config::instance();
435
		$Route          = Route::instance();
436
		$User           = User::instance();
437
		$current_module = current_module();
438
		$this->config_internal(
439
			[
440
				'base_url'              => $Config->base_url(),
441
				'current_base_url'      => $Config->base_url().'/'.(admin_path() ? 'admin/' : '').$current_module,
442
				'public_key'            => Core::instance()->public_key,
443
				'module'                => $current_module,
444
				'in_admin'              => (int)admin_path(),
445
				'is_admin'              => (int)$User->admin(),
446
				'is_user'               => (int)$User->user(),
447
				'is_guest'              => (int)$User->guest(),
448
				'password_min_length'   => (int)$Config->core['password_min_length'],
449
				'password_min_strength' => (int)$Config->core['password_min_strength'],
450
				'debug'                 => (int)DEBUG,
451
				'route'                 => $Route->route,
452
				'route_path'            => $Route->path,
453
				'route_ids'             => $Route->ids
454
			],
455
			'cs',
456
			true
457
		);
458
		if ($User->guest()) {
459
			$this->config_internal(get_core_ml_text('rules'), 'cs.rules_text', true);
460
		}
461
		if ($User->admin()) {
462
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
463
		}
464
	}
465
	/**
466
	 * @return array[]
467
	 */
468
	protected function get_includes_for_page_with_compression () {
469
		/**
470
		 * Rebuild cache if necessary
471
		 */
472
		if (!file_exists(PUBLIC_CACHE."/$this->pcache_basename.json")) {
473
			$this->rebuild_cache();
474
		}
475
		list($dependencies, $structure) = file_get_json(PUBLIC_CACHE."/$this->pcache_basename.json");
476
		$system_includes = [
477
			'css'  => ["storage/pcache/$this->pcache_basename.css?{$structure['']['css']}"],
478
			'js'   => ["storage/pcache/$this->pcache_basename.js?{$structure['']['js']}"],
479
			'html' => ["storage/pcache/$this->pcache_basename.html?{$structure['']['html']}"]
480
		];
481
		list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '+');
482
		foreach ($structure as $filename_prefix => $hashes) {
483
			if (!$filename_prefix) {
484
				continue;
485
			}
486
			$is_dependency = $this->get_includes_is_dependency($dependencies, $filename_prefix, '+');
487
			if ($is_dependency || mb_strpos($current_url, $filename_prefix) === 0) {
488
				foreach ($hashes as $extension => $hash) {
489
					if ($is_dependency) {
490
						$dependencies_includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
491
					} else {
492
						$includes[$extension][] = "storage/pcache/$filename_prefix$this->pcache_basename.$extension?$hash";
493
					}
494
				}
495
			}
496
		}
497
		return array_merge_recursive($system_includes, $dependencies_includes, $includes);
498
	}
499
	/**
500
	 * @param Config $Config
501
	 *
502
	 * @return array[]
503
	 */
504
	protected function get_includes_for_page_without_compression ($Config) {
505
		// To determine all dependencies and stuff we need `$Config` object to be already created
506
		if ($Config) {
507
			list($dependencies, $includes_map) = $this->includes_dependencies_and_map();
508
			$system_includes = $includes_map[''];
509
			list($includes, $dependencies_includes, $dependencies, $current_url) = $this->get_includes_prepare($dependencies, '/');
510
			foreach ($includes_map as $url => $local_includes) {
511
				if (!$url) {
512
					continue;
513
				}
514
				$is_dependency = $this->get_includes_is_dependency($dependencies, $url, '/');
515
				if ($is_dependency) {
516
					$dependencies_includes = array_merge_recursive($dependencies_includes, $local_includes);
517
				} elseif (mb_strpos($current_url, $url) === 0) {
518
					$includes = array_merge_recursive($includes, $local_includes);
519
				}
520
			}
521
			$includes = array_merge_recursive($system_includes, $dependencies_includes, $includes);
522
			$includes = $this->absolute_path_to_relative($includes);
1 ignored issue
show
Documentation introduced by
$includes is of type array<string,array>, but the function expects a string|array<integer,string>.

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