Completed
Push — master ( a2f507...def5f1 )
by Nazar
04:11
created

Includes::get_includes_list()   D

Complexity

Conditions 9
Paths 18

Size

Total Lines 38
Code Lines 26

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 38
rs 4.909
cc 9
eloc 26
nc 18
nop 1
1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2014-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\Page;
9
use
10
	cs\App,
11
	cs\Core,
12
	cs\Config,
13
	cs\Event,
14
	cs\Language,
15
	cs\Request,
16
	cs\User,
17
	h;
18
19
/**
20
 * Provides next events:
21
 *  System/Page/includes_dependencies_and_map
22
 *  [
23
 *    'dependencies' => &$dependencies,
24
 *    'includes_map' => &$includes_map
25
 *  ]
26
 *
27
 *  System/Page/rebuild_cache
28
 *  [
29
 *    'key' => &$key //Reference to the key, that will be appended to all css and js files, can be changed to reflect JavaScript and CSS changes
30
 *  ]
31
 *
32
 *  System/Page/requirejs
33
 *  [
34
 *    'paths'                 => &$paths,                // The same as `paths` in requirejs.config()
35
 *    'directories_to_browse' => &$directories_to_browse // Where to look for AMD modules (typically bower_components and node_modules directories)
36
 *  ]
37
 *
38
 * Includes management for `cs\Page` class
39
 *
40
 * @property string $Title
41
 * @property string $Description
42
 * @property string $canonical_url
43
 * @property string $Head
44
 * @property string $post_Body
45
 * @property string $theme
46
 */
47
trait Includes {
48
	/**
49
	 * @var array[]
50
	 */
51
	protected $core_html;
52
	/**
53
	 * @var array[]
54
	 */
55
	protected $core_js;
56
	/**
57
	 * @var array[]
58
	 */
59
	protected $core_css;
60
	/**
61
	 * @var string
62
	 */
63
	protected $core_config;
64
	/**
65
	 * @var array[]
66
	 */
67
	protected $html;
68
	/**
69
	 * @var array[]
70
	 */
71
	protected $js;
72
	/**
73
	 * @var array[]
74
	 */
75
	protected $css;
76
	/**
77
	 * @var string
78
	 */
79
	protected $config;
80
	/**
81
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
82
	 * @var string
83
	 */
84
	protected $pcache_basename;
85
	protected function init_includes () {
86
		$this->core_html       = [0 => [], 1 => []];
87
		$this->core_js         = [0 => [], 1 => []];
88
		$this->core_css        = [0 => [], 1 => []];
89
		$this->core_config     = '';
90
		$this->html            = [0 => [], 1 => []];
91
		$this->js              = [0 => [], 1 => []];
92
		$this->css             = [0 => [], 1 => []];
93
		$this->config          = '';
94
		$this->pcache_basename = '';
95
	}
96
	/**
97
	 * Including of Web Components
98
	 *
99
	 * @param string|string[] $add  Path to including file, or code
100
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
101
	 *
102
	 * @return \cs\Page
103
	 */
104
	function html ($add, $mode = 'file') {
105
		return $this->html_internal($add, $mode);
106
	}
107
	/**
108
	 * @param string|string[] $add
109
	 * @param string          $mode
110
	 * @param bool            $core
111
	 *
112
	 * @return \cs\Page
113
	 */
114
	protected function html_internal ($add, $mode = 'file', $core = false) {
115
		if (!$add) {
116
			return $this;
117
		}
118
		if (is_array($add)) {
119
			foreach (array_filter($add) as $script) {
120
				$this->html_internal($script, $mode, $core);
121
			}
122
		} else {
123
			if ($core) {
124
				$html = &$this->core_html;
125
			} else {
126
				$html = &$this->html;
127
			}
128
			if ($mode == 'file') {
129
				$html[0][] = h::link(
130
					[
131
						'href' => $add,
132
						'rel'  => 'import'
133
					]
134
				);
135
			} elseif ($mode == 'code') {
136
				$html[1][] = "$add\n";
137
			}
138
		}
139
		return $this;
140
	}
141
	/**
142
	 * Including of JavaScript
143
	 *
144
	 * @param string|string[] $add  Path to including file, or code
145
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
146
	 *
147
	 * @return \cs\Page
148
	 */
149
	function js ($add, $mode = 'file') {
150
		return $this->js_internal($add, $mode);
151
	}
152
	/**
153
	 * @param string|string[] $add
154
	 * @param string          $mode
155
	 * @param bool            $core
156
	 *
157
	 * @return \cs\Page
158
	 */
159
	protected function js_internal ($add, $mode = 'file', $core = false) {
160
		if (!$add) {
161
			return $this;
162
		}
163
		if (is_array($add)) {
164
			foreach (array_filter($add) as $script) {
165
				$this->js_internal($script, $mode, $core);
166
			}
167
		} else {
168
			if ($core) {
169
				$js = &$this->core_js;
170
			} else {
171
				$js = &$this->js;
172
			}
173
			if ($mode == 'file') {
174
				$js[0][] = h::script(
175
					[
176
						'src' => $add
177
					]
178
				);
179
			} elseif ($mode == 'code') {
180
				$js[1][] = "$add\n";
181
			}
182
		}
183
		return $this;
184
	}
185
	/**
186
	 * Including of CSS
187
	 *
188
	 * @param string|string[] $add  Path to including file, or code
189
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
190
	 *
191
	 * @return \cs\Page
192
	 */
193
	function css ($add, $mode = 'file') {
194
		return $this->css_internal($add, $mode);
195
	}
196
	/**
197
	 * @param string|string[] $add
198
	 * @param string          $mode
199
	 * @param bool            $core
200
	 *
201
	 * @return \cs\Page
202
	 */
203
	protected function css_internal ($add, $mode = 'file', $core = false) {
204
		if (!$add) {
205
			return $this;
206
		}
207
		if (is_array($add)) {
208
			foreach (array_filter($add) as $style) {
209
				$this->css_internal($style, $mode, $core);
210
			}
211
		} else {
212
			if ($core) {
213
				$css = &$this->core_css;
214
			} else {
215
				$css = &$this->css;
216
			}
217
			if ($mode == 'file') {
218
				$css[0][] = h::link(
219
					[
220
						'href'           => $add,
221
						'rel'            => 'stylesheet',
222
						'shim-shadowdom' => true
223
					]
224
				);
225
			} elseif ($mode == 'code') {
226
				$css[1][] = "$add\n";
227
			}
228
		}
229
		return $this;
230
	}
231
	/**
232
	 * Add config on page to make it available on frontend
233
	 *
234
	 * @param mixed  $config_structure        Any scalar type or array
235
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
236
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
237
	 *
238
	 * @return \cs\Page
239
	 */
240
	function config ($config_structure, $target) {
241
		return $this->config_internal($config_structure, $target);
242
	}
243
	/**
244
	 * @param mixed  $config_structure
245
	 * @param string $target
246
	 * @param bool   $core
247
	 *
248
	 * @return \cs\Page
249
	 */
250
	protected function config_internal ($config_structure, $target, $core = false) {
251
		$config = h::script(
252
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
253
			[
254
				'target' => $target,
255
				'class'  => 'cs-config',
256
				'type'   => 'application/json'
257
			]
258
		);
259
		if ($core) {
260
			$this->core_config .= $config;
261
		} else {
262
			$this->config .= $config;
263
		}
264
		return $this;
265
	}
266
	/**
267
	 * Getting of HTML, JS and CSS includes
268
	 *
269
	 * @return \cs\Page
270
	 */
271
	protected function add_includes_on_page () {
272
		$Config = Config::instance(true);
273
		if (!$Config) {
274
			return $this;
275
		}
276
		/**
277
		 * Base name for cache files
278
		 */
279
		$this->pcache_basename = "_{$this->theme}_".Language::instance()->clang;
280
		/**
281
		 * Some JS configs required by system
282
		 */
283
		$this->add_system_configs();
284
		// TODO: I hope some day we'll get rid of this sh*t :(
285
		$this->ie_edge();
286
		$Request = Request::instance();
287
		/**
288
		 * If CSS and JavaScript compression enabled
289
		 */
290
		if ($Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']))) {
291
			$this->webcomponents_polyfill($Request, true);
292
			$includes = $this->get_includes_for_page_with_compression();
293
		} else {
294
			$this->webcomponents_polyfill($Request, false);
295
			/**
296
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
297
			 */
298
			/**
299
			 * @var \cs\Page $this
300
			 */
301
			$this->config_internal(Language::instance(), 'cs.Language', true);
302
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
303
			$includes = $this->get_includes_for_page_without_compression($Config);
304
		}
305
		$this->css_internal($includes['css'], 'file', true);
306
		$this->js_internal($includes['js'], 'file', true);
307
		$this->html_internal($includes['html'], 'file', true);
308
		$this->add_includes_on_page_manually_added($Config);
309
		return $this;
310
	}
311
	/**
312
	 * @return string[]
313
	 */
314
	protected function get_requirejs_paths () {
315
		$Config = Config::instance();
316
		$paths  = [];
317
		foreach ($Config->components['modules'] as $module_name => $module_data) {
318
			if ($module_data['active'] == Config\Module_Properties::UNINSTALLED) {
319
				continue;
320
			}
321
			$this->get_requirejs_paths_add_aliases(MODULES."/$module_name", $paths);
322
		}
323
		foreach ($Config->components['plugins'] as $plugin_name) {
324
			$this->get_requirejs_paths_add_aliases(PLUGINS."/$plugin_name", $paths);
325
		}
326
		$directories_to_browse = [
327
			DIR.'/bower_components',
328
			DIR.'/node_modules'
329
		];
330
		Event::instance()->fire(
331
			'System/Page/requirejs',
332
			[
333
				'paths'                 => &$paths,
334
				'directories_to_browse' => &$directories_to_browse
335
			]
336
		);
337
		foreach ($directories_to_browse as $dir) {
338
			foreach (get_files_list($dir, false, 'd', true) as $d) {
339
				$this->get_requirejs_paths_find_package($d, $paths);
340
			}
341
		}
342
		return $paths;
343
	}
344
	/**
345
	 * @param string   $dir
346
	 * @param string[] $paths
347
	 */
348
	protected function get_requirejs_paths_add_aliases ($dir, &$paths) {
349
		if (is_dir("$dir/includes/js")) {
350
			$name         = basename($dir);
351
			$paths[$name] = $this->absolute_path_to_relative("$dir/includes/js");
352
			foreach ((array)@file_get_json("$dir/meta.json")['provide'] as $p) {
353
				if (strpos($p, '/') !== false) {
354
					$paths[$p] = $paths[$name];
355
				}
356
			}
357
		}
358
	}
359
	/**
360
	 * @param string   $dir
361
	 * @param string[] $paths
362
	 */
363
	protected function get_requirejs_paths_find_package ($dir, &$paths) {
364
		$path = $this->get_requirejs_paths_find_package_bower($dir) ?: $this->get_requirejs_paths_find_package_npm($dir);
365
		if ($path) {
366
			$paths[basename($dir)] = $this->absolute_path_to_relative(substr($path, 0, -3));
367
		}
368
	}
369
	/**
370
	 * @param string $dir
371
	 *
372
	 * @return string
373
	 */
374
	protected function get_requirejs_paths_find_package_bower ($dir) {
375
		$bower = @file_get_json("$dir/bower.json");
376
		foreach (@(array)$bower['main'] as $main) {
377
			if (preg_match('/\.js$/', $main)) {
378
				$main = substr($main, 0, -3);
379
				// There is a chance that minified file is present
380
				$main = file_exists_with_extension("$dir/$main", ['min.js', 'js']);
381
				if ($main) {
382
					return $main;
383
				}
384
			}
385
		}
386
		return null;
387
	}
388
	/**
389
	 * @param string $dir
390
	 *
391
	 * @return false|string
392
	 */
393
	protected function get_requirejs_paths_find_package_npm ($dir) {
394
		$package = @file_get_json("$dir/package.json");
395
		// If we have browser-specific declaration - use it
396
		$main = @$package['browser'] ?: (@$package['jspm']['main'] ?: @$package['main']);
397
		if (preg_match('/\.js$/', $main)) {
398
			$main = substr($main, 0, -3);
399
		}
400
		if ($main) {
401
			// There is a chance that minified file is present
402
			return file_exists_with_extension("$dir/$main", ['min.js', 'js']) ?: file_exists_with_extension("$dir/dist/$main", ['min.js', 'js']);
403
		}
404
	}
405
	/**
406
	 * Since modules, plugins and storage directories can be (at least theoretically) moved from default location - let's do proper path conversion
407
	 *
408
	 * @param string|string[] $path
409
	 *
410
	 * @return string|string[]
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
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array>.

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