Completed
Push — master ( c8f771...6e0d71 )
by Nazar
03:50
created

Includes::add_system_configs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 17
nc 2
nop 0
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package   CleverStyle Framework
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\Language,
14
	cs\Request,
15
	cs\Response,
16
	cs\User,
17
	h,
18
	cs\Page\Includes\Cache,
19
	cs\Page\Includes\Collecting,
20
	cs\Page\Includes\RequireJS;
21
22
/**
23
 * Includes management for `cs\Page` class
24
 *
25
 * @property string $Title
26
 * @property string $Description
27
 * @property string $canonical_url
28
 * @property string $Head
29
 * @property string $post_Body
30
 * @property string $theme
31
 */
32
trait Includes {
33
	use
34
		Cache,
35
		Collecting,
36
		RequireJS;
37
	protected $extension_to_as = [
38
		'jpeg' => 'image',
39
		'jpe'  => 'image',
40
		'jpg'  => 'image',
41
		'gif'  => 'image',
42
		'png'  => 'image',
43
		'svg'  => 'image',
44
		'svgz' => 'image',
45
		'woff' => 'font',
46
		//'woff2' => 'font',
47
		'css'  => 'style',
48
		'js'   => 'script',
49
		'html' => 'document'
50
	];
51
	/**
52
	 * @var array
53
	 */
54
	protected $core_html;
55
	/**
56
	 * @var array
57
	 */
58
	protected $core_js;
59
	/**
60
	 * @var array
61
	 */
62
	protected $core_css;
63
	/**
64
	 * @var string
65
	 */
66
	protected $core_config;
67
	/**
68
	 * @var array
69
	 */
70
	protected $html;
71
	/**
72
	 * @var array
73
	 */
74
	protected $js;
75
	/**
76
	 * @var array
77
	 */
78
	protected $css;
79
	/**
80
	 * @var string
81
	 */
82
	protected $config;
83
	/**
84
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
85
	 * @var string
86
	 */
87
	protected $pcache_basename_path;
88
	protected function init_includes () {
89
		$this->core_html            = ['path' => []]; // No plain HTML in core
90
		$this->core_js              = ['path' => []]; // No plain JS in core
91
		$this->core_css             = ['path' => []]; // No plain CSS in core
92
		$this->core_config          = '';
93
		$this->html                 = ['path' => [], 'plain' => ''];
94
		$this->js                   = ['path' => [], 'plain' => ''];
95
		$this->css                  = ['path' => [], 'plain' => ''];
96
		$this->config               = '';
97
		$this->pcache_basename_path = '';
98
	}
99
	/**
100
	 * Including of Web Components
101
	 *
102
	 * @param string|string[] $add  Path to including file, or code
103
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
104
	 *
105
	 * @return \cs\Page
106
	 */
107
	function html ($add, $mode = 'file') {
108
		return $this->html_internal($add, $mode);
109
	}
110
	/**
111
	 * @param string|string[] $add
112
	 * @param string          $mode
113
	 * @param bool            $core
114
	 *
115
	 * @return \cs\Page
116
	 */
117
	protected function html_internal ($add, $mode = 'file', $core = false) {
118
		return $this->include_common('html', $add, $mode, $core);
119
	}
120
	/**
121
	 * Including of JavaScript
122
	 *
123
	 * @param string|string[] $add  Path to including file, or code
124
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
125
	 *
126
	 * @return \cs\Page
127
	 */
128
	function js ($add, $mode = 'file') {
129
		return $this->js_internal($add, $mode);
130
	}
131
	/**
132
	 * @param string|string[] $add
133
	 * @param string          $mode
134
	 * @param bool            $core
135
	 *
136
	 * @return \cs\Page
137
	 */
138
	protected function js_internal ($add, $mode = 'file', $core = false) {
139
		return $this->include_common('js', $add, $mode, $core);
140
	}
141
	/**
142
	 * Including of CSS
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 css ($add, $mode = 'file') {
150
		return $this->css_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 css_internal ($add, $mode = 'file', $core = false) {
160
		return $this->include_common('css', $add, $mode, $core);
161
	}
162
	/**
163
	 * @param string          $what
164
	 * @param string|string[] $add
165
	 * @param string          $mode
166
	 * @param bool            $core
167
	 *
168
	 * @return \cs\Page
169
	 */
170
	protected function include_common ($what, $add, $mode, $core) {
171
		if (!$add) {
172
			return $this;
173
		}
174
		if (is_array($add)) {
175
			foreach (array_filter($add) as $style) {
176
				$this->include_common($what, $style, $mode, $core);
177
			}
178
		} else {
179
			if ($core) {
180
				$what = "core_$what";
181
			}
182
			$target = &$this->$what;
183
			if ($mode == 'file') {
184
				$target['path'][] = $add;
185
			} elseif ($mode == 'code') {
186
				$target['plain'] .= "$add\n";
187
			}
188
		}
189
		return $this;
190
	}
191
	/**
192
	 * Add config on page to make it available on frontend
193
	 *
194
	 * @param mixed  $config_structure        Any scalar type or array
195
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
196
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
197
	 *
198
	 * @return \cs\Page
199
	 */
200
	function config ($config_structure, $target) {
201
		return $this->config_internal($config_structure, $target);
202
	}
203
	/**
204
	 * @param mixed  $config_structure
205
	 * @param string $target
206
	 * @param bool   $core
207
	 *
208
	 * @return \cs\Page
209
	 */
210
	protected function config_internal ($config_structure, $target, $core = false) {
211
		$config = h::script(
212
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
213
			[
214
				'target' => $target,
215
				'class'  => 'cs-config',
216
				'type'   => 'application/json'
217
			]
218
		);
219
		if ($core) {
220
			$this->core_config .= $config;
221
		} else {
222
			$this->config .= $config;
223
		}
224
		return $this;
225
	}
226
	/**
227
	 * Getting of HTML, JS and CSS includes
228
	 *
229
	 * @return \cs\Page
230
	 */
231
	protected function add_includes_on_page () {
232
		$Config = Config::instance(true);
233
		if (!$Config) {
234
			return $this;
235
		}
236
		/**
237
		 * Base name for cache files
238
		 */
239
		$this->pcache_basename_path = PUBLIC_CACHE.'/'.$this->theme.'_'.Language::instance()->clang;
240
		/**
241
		 * Some JS configs required by system
242
		 */
243
		$this->add_system_configs();
244
		// TODO: I hope some day we'll get rid of this sh*t :(
245
		$this->ie_edge();
246
		$Request = Request::instance();
247
		/**
248
		 * If CSS and JavaScript compression enabled
249
		 */
250
		if ($this->page_compression_usage($Config, $Request)) {
251
			/**
252
			 * Rebuilding HTML, JS and CSS cache if necessary
253
			 */
254
			$this->rebuild_cache($Config);
255
			$this->webcomponents_polyfill($Request, true);
256
			list($includes, $preload) = $this->get_includes_and_preload_resource_for_page_with_compression($Request);
257
		} else {
258
			$this->webcomponents_polyfill($Request, false);
259
			/**
260
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
261
			 */
262
			$this->config_internal(Language::instance(), 'cs.Language', true);
263
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
264
			$includes = $this->get_includes_for_page_without_compression($Config, $Request);
265
			$preload  = [];
266
		}
267
		$this->css_internal($includes['css'], 'file', true);
268
		$this->js_internal($includes['js'], 'file', true);
269
		$this->html_internal($includes['html'], 'file', true);
270
		$this->add_includes_on_page_manually_added($Config, $Request, $preload);
271
		return $this;
272
	}
273
	/**
274
	 * @param Config  $Config
275
	 * @param Request $Request
276
	 *
277
	 * @return bool
278
	 */
279
	protected function page_compression_usage ($Config, $Request) {
280
		return $Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']));
281
	}
282
	/**
283
	 * Add JS polyfills for IE/Edge
284
	 */
285
	protected function ie_edge () {
286
		if (!preg_match('/Trident|Edge/', Request::instance()->header('user-agent'))) {
287
			return;
288
		}
289
		$this->js_internal(
290
			get_files_list(DIR."/includes/js/microsoft_sh*t", "/.*\\.js$/i", 'f', "includes/js/microsoft_sh*t", true),
291
			'file',
292
			true
293
		);
294
	}
295
	protected function add_system_configs () {
296
		$Config  = Config::instance();
297
		$Request = Request::instance();
298
		$User    = User::instance();
299
		$this->config_internal(
300
			[
301
				'public_key'            => Core::instance()->public_key,
302
				'in_admin'              => (int)$Request->admin_path,
303
				'is_admin'              => (int)$User->admin(),
304
				'is_user'               => (int)$User->user(),
305
				'is_guest'              => (int)$User->guest(),
306
				'password_min_length'   => (int)$Config->core['password_min_length'],
307
				'password_min_strength' => (int)$Config->core['password_min_strength'],
308
				'debug'                 => (int)DEBUG
309
			],
310
			'cs',
311
			true
312
		);
313
		if ($User->admin()) {
314
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
315
		}
316
	}
317
	/**
318
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
319
	 *
320
	 * @param Request $Request
321
	 * @param bool    $with_compression
322
	 */
323
	protected function webcomponents_polyfill ($Request, $with_compression) {
324
		if ($Request->cookie('shadow_dom') == 1) {
325
			return;
326
		}
327
		if ($with_compression) {
328
			$hash = file_get_contents(PUBLIC_CACHE.'/webcomponents.js.hash');
329
			$this->js_internal("/storage/pcache/webcomponents.js?$hash", 'file', true);
330
		} else {
331
			$this->js_internal('/includes/js/WebComponents-polyfill/webcomponents-custom.min.js', 'file', true);
332
		}
333
	}
334
	/**
335
	 * @param Request $Request
336
	 *
337
	 * @return array[]
338
	 */
339
	protected function get_includes_and_preload_resource_for_page_with_compression ($Request) {
340
		list($dependencies, $compressed_includes_map, $not_embedded_resources_map) = file_get_json("$this->pcache_basename_path.json");
341
		$includes = $this->get_normalized_includes($dependencies, $compressed_includes_map, $Request);
342
		$preload  = [];
343
		foreach (array_merge(...array_values($includes)) as $path) {
344
			$preload[] = [$path];
345
			if (isset($not_embedded_resources_map[$path])) {
346
				$preload[] = $not_embedded_resources_map[$path];
347
			}
348
		}
349
		return [$includes, array_merge(...$preload)];
350
	}
351
	/**
352
	 * @param array      $dependencies
353
	 * @param string[][] $includes_map
354
	 * @param Request    $Request
355
	 *
356
	 * @return string[][]
357
	 */
358
	protected function get_normalized_includes ($dependencies, $includes_map, $Request) {
359
		$current_module = $Request->current_module;
360
		/**
361
		 * Current URL based on controller path (it better represents how page was rendered)
362
		 */
363
		$current_url = array_slice(App::instance()->controller_path, 1);
364
		$current_url = ($Request->admin_path ? "admin/" : '')."$current_module/".implode('/', $current_url);
365
		/**
366
		 * Narrow the dependencies to current module only
367
		 */
368
		$dependencies    = array_unique(
369
			array_merge(
370
				['System'],
371
				$dependencies['System'],
372
				isset($dependencies[$current_module]) ? $dependencies[$current_module] : []
373
			)
374
		);
375
		$system_includes = [];
376
		// Array with empty array in order to avoid `array_merge()` failure later
377
		$dependencies_includes = array_fill_keys($dependencies, [[]]);
378
		$includes              = [];
379
		foreach ($includes_map as $path => $local_includes) {
380
			if ($path == 'System') {
381
				$system_includes = $local_includes;
382
			} elseif ($component = $this->get_dependency_component($dependencies, $path, $Request)) {
383
				/**
384
				 * @var string $component
385
				 */
386
				$dependencies_includes[$component][] = $local_includes;
387
			} elseif (mb_strpos($current_url, $path) === 0) {
388
				$includes[] = $local_includes;
389
			}
390
		}
391
		// Convert to indexed array first
392
		$dependencies_includes = array_values($dependencies_includes);
393
		// Flatten array on higher level
394
		$dependencies_includes = array_merge(...$dependencies_includes);
395
		// Hack: 2 array_merge_recursive() just to be compatible with HHVM, simplify when https://github.com/facebook/hhvm/issues/7087 is resolved
396
		return _array(array_merge_recursive(array_merge_recursive($system_includes, ...$dependencies_includes), ...$includes));
397
	}
398
	/**
399
	 * @param array   $dependencies
400
	 * @param string  $url
401
	 * @param Request $Request
402
	 *
403
	 * @return false|string
404
	 */
405
	protected function get_dependency_component ($dependencies, $url, $Request) {
406
		$url_exploded = explode('/', $url);
407
		/** @noinspection NestedTernaryOperatorInspection */
408
		$url_component = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
409
		$is_dependency =
410
			$url_component !== Config::SYSTEM_MODULE &&
411
			in_array($url_component, $dependencies) &&
412
			(
413
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
414
			);
415
		return $is_dependency ? $url_component : false;
416
	}
417
	/**
418
	 * @param Config  $Config
419
	 * @param Request $Request
420
	 *
421
	 * @return string[][]
422
	 */
423
	protected function get_includes_for_page_without_compression ($Config, $Request) {
424
		// To determine all dependencies and stuff we need `$Config` object to be already created
425
		list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
426
		$includes = $this->get_normalized_includes($dependencies, $includes_map, $Request);
427
		return $this->add_versions_hash($this->absolute_path_to_relative($includes));
428
	}
429
	/**
430
	 * @param string[]|string[][] $path
431
	 *
432
	 * @return string[]|string[][]
433
	 */
434
	protected function absolute_path_to_relative ($path) {
435
		return _substr($path, strlen(DIR));
436
	}
437
	/**
438
	 * @param string[][] $includes
439
	 *
440
	 * @return string[][]
441
	 */
442
	protected function add_versions_hash ($includes) {
443
		$content     = array_reduce(
444
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
445
			function ($content, $file) {
446
				return $content.file_get_contents($file);
447
			}
448
		);
449
		$content_md5 = substr(md5($content), 0, 5);
450
		foreach ($includes as &$files) {
451
			foreach ($files as &$file) {
452
				$file .= "?$content_md5";
453
			}
454
			unset($file);
455
		}
456
		return $includes;
457
	}
458
	/**
459
	 * @param Config   $Config
460
	 * @param Request  $Request
461
	 * @param string[] $preload
462
	 */
463
	protected function add_includes_on_page_manually_added ($Config, $Request, $preload) {
464
		/** @noinspection NestedTernaryOperatorInspection */
465
		$this->Head .=
466
			array_reduce(
467
				array_merge($this->core_css['path'], $this->css['path']),
468
				function ($content, $href) {
469
					return "$content<link href=\"$href\" rel=\"stylesheet\" shim-shadowdom>\n";
470
				}
471
			).
472
			h::style($this->css['plain'] ?: false);
473
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
474
			$this->add_includes_on_page_manually_added_frontend_load_optimization($Config);
475
		} else {
476
			$this->add_includes_on_page_manually_added_normal($Config, $Request, $preload);
477
		}
478
	}
479
	/**
480
	 * @param Config   $Config
481
	 * @param Request  $Request
482
	 * @param string[] $preload
483
	 */
484
	protected function add_includes_on_page_manually_added_normal ($Config, $Request, $preload) {
485
		$jquery    = $this->jquery($this->page_compression_usage($Config, $Request));
486
		$preload[] = $jquery;
487
		$this->add_preload($preload);
488
		$configs      = $this->core_config.$this->config;
489
		$scripts      =
490
			array_reduce(
491
				array_merge([$jquery], $this->core_js['path'], $this->js['path']),
492
				function ($content, $src) {
493
					return "$content<script src=\"$src\"></script>\n";
494
				}
495
			).
496
			h::script($this->js['plain'] ?: false);
497
		$html_imports =
498
			array_reduce(
499
				array_merge($this->core_html['path'], $this->html['path']),
500
				function ($content, $href) {
501
					return "$content<link href=\"$href\" rel=\"import\">\n";
502
				}
503
			).
504
			$this->html['plain'];
505
		$this->Head .= $configs;
506
		if ($Config->core['put_js_after_body']) {
507
			$this->post_Body .= $scripts.$html_imports;
508
		} else {
509
			$this->Head .= $scripts.$html_imports;
510
		}
511
	}
512
	/**
513
	 * Hack: jQuery is kind of special; it is only loaded directly in normal mode, during frontend load optimization it is loaded asynchronously in frontend
514
	 * TODO: In future we'll load jQuery as AMD module only and this thing will not be needed
515
	 *
516
	 * @param bool $with_compression
517
	 *
518
	 * @return string
519
	 */
520
	protected function jquery ($with_compression) {
521
		if ($with_compression) {
522
			$hash = file_get_contents(PUBLIC_CACHE.'/jquery.js.hash');
523
			return "/storage/pcache/jquery.js?$hash";
524
		} else {
525
			return '/includes/js/jquery/jquery-3.0.0-pre.js';
526
		}
527
	}
528
	/**
529
	 * @param string[] $preload
530
	 */
531
	protected function add_preload ($preload) {
532
		$Response = Response::instance();
533
		foreach ($preload as $resource) {
534
			$extension = explode('?', file_extension($resource))[0];
535
			$as        = $this->extension_to_as[$extension];
536
			$resource  = str_replace(' ', '%20', $resource);
537
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
538
		}
539
	}
540
	/**
541
	 * @param Config $Config
542
	 */
543
	protected function add_includes_on_page_manually_added_frontend_load_optimization ($Config) {
544
		list($optimized_includes, $preload) = file_get_json("$this->pcache_basename_path.optimized.json");
545
		$this->add_preload(
546
			array_unique(
547
				array_merge(
548
					$preload,
549
					$this->core_css['path'],
550
					$this->css['path']
551
				)
552
			)
553
		);
554
		$system_scripts    = '';
555
		$optimized_scripts = [$this->jquery(true)];
556
		$system_imports    = '';
557
		$optimized_imports = [];
558
		foreach (array_merge($this->core_js['path'], $this->js['path']) as $script) {
559
			if (isset($optimized_includes[$script])) {
560
				$optimized_scripts[] = $script;
561
			} else {
562
				$system_scripts .= "<script src=\"$script\"></script>\n";
563
			}
564
		}
565
		foreach (array_merge($this->core_html['path'], $this->html['path']) as $import) {
566
			if (isset($optimized_includes[$import])) {
567
				$optimized_imports[] = $import;
568
			} else {
569
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
570
			}
571
		}
572
		$scripts      = h::script($this->js['plain'] ?: false);
573
		$html_imports = $this->html['plain'];
574
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_includes');
575
		$this->Head .= $this->core_config.$this->config;
576
		if ($Config->core['put_js_after_body']) {
577
			$this->post_Body .= $system_scripts.$system_imports.$scripts.$html_imports;
578
		} else {
579
			$this->Head .= $system_scripts.$system_imports.$scripts.$html_imports;
580
		}
581
	}
582
}
583