Completed
Push — master ( 296b9a...f55c83 )
by Nazar
03:58
created

Includes::add_system_configs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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