Completed
Push — master ( d82b6b...206636 )
by Nazar
08:39
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),
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal /includes/js/microsoft_sh*t does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal /.*\\.js$/i does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal includes/js/microsoft_sh*t does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
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);
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal admin/ does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
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);
461
		} else {
462 2
			$this->add_includes_on_page_manually_added_normal($Config, $preload);
463
		}
464 4
	}
465
	/**
466
	 * @param Config   $Config
467
	 * @param string[] $preload
468
	 */
469 2
	protected function add_includes_on_page_manually_added_normal ($Config, $preload) {
470 2
		$this->add_preload($preload);
471 2
		$configs      = $this->core_config.$this->config;
472
		$scripts      =
473 2
			array_reduce(
474 2
				array_merge($this->core_js['path'], $this->js['path']),
475
				function ($content, $src) {
476 2
					return "$content<script src=\"$src\"></script>\n";
477 2
				}
478
			).
479 2
			h::script($this->js['plain'] ?: false);
480
		$html_imports =
481 2
			array_reduce(
482 2
				array_merge($this->core_html['path'], $this->html['path']),
483 2
				function ($content, $href) {
484 2
					return "$content<link href=\"$href\" rel=\"import\">\n";
485 2
				}
486
			).
487 2
			$this->html['plain'];
488 2
		$this->Head .= $configs;
489 2
		$this->add_script_imports_to_document($Config, $scripts.$html_imports);
490 2
	}
491
	/**
492
	 * @param string[] $preload
493
	 */
494 4
	protected function add_preload ($preload) {
495 4
		$Response = Response::instance();
496 4
		foreach ($preload as $resource) {
497 4
			$extension = explode('?', file_extension($resource))[0];
498 4
			$as        = $this->extension_to_as[$extension];
499 4
			$resource  = str_replace(' ', '%20', $resource);
500 4
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
501
		}
502 4
	}
503
	/**
504
	 * @param Config $Config
505
	 */
506 4
	protected function add_includes_on_page_manually_added_frontend_load_optimization ($Config) {
507 4
		list($optimized_includes, $preload) = file_get_json("$this->pcache_basename_path.optimized.json");
508 4
		$this->add_preload(
509
			array_unique(
510
				array_merge(
511
					$preload,
512 4
					$this->core_css['path'],
513 4
					$this->css['path']
514
				)
515
			)
516
		);
517 4
		$system_scripts    = '';
518 4
		$optimized_scripts = [];
519 4
		$system_imports    = '';
520 4
		$optimized_imports = [];
521 4
		foreach (array_merge($this->core_js['path'], $this->js['path']) as $script) {
522 4
			if (isset($optimized_includes[$script])) {
523
				$optimized_scripts[] = $script;
524
			} else {
525 4
				$system_scripts .= "<script src=\"$script\"></script>\n";
526
			}
527
		}
528 4
		foreach (array_merge($this->core_html['path'], $this->html['path']) as $import) {
529 4
			if (isset($optimized_includes[$import])) {
530
				$optimized_imports[] = $import;
531
			} else {
532 4
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
533
			}
534
		}
535 4
		$scripts      = h::script($this->js['plain'] ?: false);
536 4
		$html_imports = $this->html['plain'];
537 4
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_includes');
538 4
		$this->Head .= $this->core_config.$this->config;
539 4
		$this->add_script_imports_to_document($Config, $system_scripts.$system_imports.$scripts.$html_imports);
540 4
	}
541
}
542