Completed
Push — master ( ebe41b...21ecde )
by Nazar
04:54
created

Includes::webcomponents_polyfill()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.0488

Importance

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