Completed
Push — master ( a1e1e6...8edf6f )
by Nazar
04:25
created

Includes::edge()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.9765

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 0
loc 10
ccs 3
cts 8
cp 0.375
crap 2.9765
rs 9.4285
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 52
	protected function init_includes () {
87 52
		$this->core_html            = ['path' => []]; // No plain HTML in core
88 52
		$this->core_js              = ['path' => []]; // No plain JS in core
89 52
		$this->core_css             = ['path' => []]; // No plain CSS in core
90 52
		$this->core_config          = '';
91 52
		$this->html                 = ['path' => [], 'plain' => ''];
92 52
		$this->js                   = ['path' => [], 'plain' => ''];
93 52
		$this->css                  = ['path' => [], 'plain' => ''];
94 52
		$this->config               = '';
95 52
		$this->pcache_basename_path = '';
96 52
	}
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 4
			$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;
238
		// TODO: I hope some day we'll get rid of this sh*t :(
239 4
		$this->edge();
240 4
		$Request = Request::instance();
241
		/**
242
		 * If CSS and JavaScript compression enabled
243
		 */
244 4
		$L = Language::instance();
245 4
		if ($this->page_compression_usage($Config, $Request)) {
246
			/**
247
			 * Rebuilding HTML, JS and CSS cache if necessary
248
			 */
249 4
			$this->rebuild_cache($Config, $L);
250 4
			$this->webcomponents_polyfill($Request, $Config, true);
251 4
			$languages_hash = $this->get_hash_of(implode('', $Config->core['active_languages']));
252 4
			$language_hash  = file_get_json(PUBLIC_CACHE."/languages-$languages_hash.json")[$L->clanguage];
253 4
			$this->config_internal(
254
				[
255 4
					'language' => $L->clanguage,
256 4
					'hash'     => $language_hash
257
				],
258 4
				'cs.current_language',
259 4
				true
260
			);
261 4
			list($includes, $preload) = $this->get_includes_and_preload_resource_for_page_with_compression($Request);
262
		} else {
263 2
			$this->webcomponents_polyfill($Request, $Config, false);
264
			/**
265
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
266
			 */
267 2
			$this->config_internal($L, 'cs.Language', true);
268 2
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
269 2
			$includes = $this->get_includes_for_page_without_compression($Config, $Request);
270 2
			$preload  = [];
271
		}
272 4
		$this->css_internal($includes['css'], 'file', true);
273 4
		$this->js_internal($includes['js'], 'file', true);
274 4
		if (isset($includes['html'])) {
275 4
			$this->html_internal($includes['html'], 'file', true);
276
		}
277 4
		$this->add_includes_on_page_manually_added($Config, $Request, $preload);
278 4
		return $this;
279
	}
280
	/**
281
	 * @param Config  $Config
282
	 * @param Request $Request
283
	 *
284
	 * @return bool
285
	 */
286 4
	protected function page_compression_usage ($Config, $Request) {
287 4
		return $Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']));
288
	}
289
	/**
290
	 * Add JS polyfills for IE/Edge
291
	 */
292 4
	protected function edge () {
293 4
		if (strpos(Request::instance()->header('user-agent'), 'Edge') === false) {
294 4
			return;
295
		}
296
		$this->js_internal(
297
			get_files_list(DIR.'/includes/js/microsoft_sh*t', '/.*\.js$/i', 'f', 'includes/js/microsoft_sh*t', true),
298
			'file',
299
			true
300
		);
301
	}
302
	/**
303
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
304
	 *
305
	 * @param Request $Request
306
	 * @param Config  $Config
307
	 * @param bool    $with_compression
308
	 */
309 4
	protected function webcomponents_polyfill ($Request, $Config, $with_compression) {
310 4
		if (($this->theme != Config::SYSTEM_THEME && $Config->core['disable_webcomponents']) || $Request->cookie('shadow_dom') == 1) {
311
			return;
312
		}
313 4
		if ($with_compression) {
314 4
			$hash = file_get_contents(PUBLIC_CACHE.'/webcomponents.js.hash');
315 4
			$this->add_script_imports_to_document($Config, "<script src=\"/storage/pcache/webcomponents.js?$hash\"></script>\n");
316
		} else {
317 2
			$this->add_script_imports_to_document($Config, "<script src=\"/includes/js/WebComponents-polyfill/webcomponents-custom.min.js\"></script>\n");
318
		}
319 4
	}
320
	/**
321
	 * @param Config $Config
322
	 * @param string $content
323
	 */
324 4
	protected function add_script_imports_to_document ($Config, $content) {
325 4
		if ($Config->core['put_js_after_body']) {
326 4
			$this->post_Body .= $content;
327
		} else {
328 2
			$this->Head .= $content;
329
		}
330 4
	}
331
	/**
332
	 * @param Request $Request
333
	 *
334
	 * @return array[]
335
	 */
336 4
	protected function get_includes_and_preload_resource_for_page_with_compression ($Request) {
337 4
		list($dependencies, $compressed_includes_map, $not_embedded_resources_map) = file_get_json("$this->pcache_basename_path.json");
338 4
		$includes = $this->get_normalized_includes($dependencies, $compressed_includes_map, $Request);
339 4
		$preload  = [];
340 4
		foreach (array_merge(...array_values($includes)) as $path) {
341 4
			$preload[] = [$path];
342 4
			if (isset($not_embedded_resources_map[$path])) {
343 4
				$preload[] = $not_embedded_resources_map[$path];
344
			}
345
		}
346 4
		return [$includes, array_merge(...$preload)];
347
	}
348
	/**
349
	 * @param array      $dependencies
350
	 * @param string[][] $includes_map
351
	 * @param Request    $Request
352
	 *
353
	 * @return string[][]
354
	 */
355 4
	protected function get_normalized_includes ($dependencies, $includes_map, $Request) {
356 4
		$current_module = $Request->current_module;
357
		/**
358
		 * Current URL based on controller path (it better represents how page was rendered)
359
		 */
360 4
		$current_url = array_slice(App::instance()->controller_path, 1);
361 4
		$current_url = ($Request->admin_path ? 'admin/' : '')."$current_module/".implode('/', $current_url);
362
		/**
363
		 * Narrow the dependencies to current module only
364
		 */
365 4
		$dependencies    = array_unique(
366
			array_merge(
367 4
				['System'],
368 4
				$dependencies['System'],
369 4
				isset($dependencies[$current_module]) ? $dependencies[$current_module] : []
370
			)
371
		);
372 4
		$system_includes = [];
373
		// Array with empty array in order to avoid `array_merge()` failure later
374 4
		$dependencies_includes = array_fill_keys($dependencies, [[]]);
375 4
		$includes              = [];
376 4
		foreach ($includes_map as $path => $local_includes) {
377 4
			if ($path == 'System') {
378 4
				$system_includes = $local_includes;
379 4
			} elseif ($component = $this->get_dependency_component($dependencies, $path, $Request)) {
380
				/**
381
				 * @var string $component
382
				 */
383
				$dependencies_includes[$component][] = $local_includes;
384 4
			} elseif (mb_strpos($current_url, $path) === 0) {
385 4
				$includes[] = $local_includes;
386
			}
387
		}
388
		// Convert to indexed array first
389 4
		$dependencies_includes = array_values($dependencies_includes);
390
		// Flatten array on higher level
391 4
		$dependencies_includes = array_merge(...$dependencies_includes);
392
		// Hack: 2 array_merge_recursive() just to be compatible with HHVM, simplify when https://github.com/facebook/hhvm/issues/7087 is resolved
393 4
		return _array(array_merge_recursive(array_merge_recursive($system_includes, ...$dependencies_includes), ...$includes));
394
	}
395
	/**
396
	 * @param array   $dependencies
397
	 * @param string  $url
398
	 * @param Request $Request
399
	 *
400
	 * @return false|string
401
	 */
402 4
	protected function get_dependency_component ($dependencies, $url, $Request) {
403 4
		$url_exploded = explode('/', $url);
404
		/** @noinspection NestedTernaryOperatorInspection */
405 4
		$url_component = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
406
		$is_dependency =
407 4
			$url_component !== Config::SYSTEM_MODULE &&
408 4
			in_array($url_component, $dependencies) &&
409
			(
410 4
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
411
			);
412 4
		return $is_dependency ? $url_component : false;
413
	}
414
	/**
415
	 * @param Config  $Config
416
	 * @param Request $Request
417
	 *
418
	 * @return string[][]
419
	 */
420 2
	protected function get_includes_for_page_without_compression ($Config, $Request) {
421
		// To determine all dependencies and stuff we need `$Config` object to be already created
422 2
		list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
423 2
		$includes = $this->get_normalized_includes($dependencies, $includes_map, $Request);
424 2
		return $this->add_versions_hash($this->absolute_path_to_relative($includes));
425
	}
426
	/**
427
	 * @param string[]|string[][] $path
428
	 *
429
	 * @return string[]|string[][]
430
	 */
431 4
	protected function absolute_path_to_relative ($path) {
432 4
		return _substr($path, strlen(DIR));
433
	}
434
	/**
435
	 * @param string[][] $includes
436
	 *
437
	 * @return string[][]
438
	 */
439 2
	protected function add_versions_hash ($includes) {
440 2
		$content      = array_reduce(
441 2
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
442
			function ($content, $file) {
443
				return $content.file_get_contents($file);
444 2
			}
445
		);
446 2
		$content_hash = $this->get_hash_of($content);
447 2
		foreach ($includes as &$files) {
448 2
			foreach ($files as &$file) {
449 2
				$file .= "?$content_hash";
450
			}
451 2
			unset($file);
452
		}
453 2
		return $includes;
454
	}
455
	/**
456
	 * @param Config   $Config
457
	 * @param Request  $Request
458
	 * @param string[] $preload
459
	 */
460 4
	protected function add_includes_on_page_manually_added ($Config, $Request, $preload) {
461
		/** @noinspection NestedTernaryOperatorInspection */
462 4
		$this->Head .=
463 4
			array_reduce(
464 4
				array_merge($this->core_css['path'], $this->css['path']),
465
				function ($content, $href) {
466 4
					return "$content<link href=\"$href\" rel=\"stylesheet\">\n";
467 4
				}
468
			).
469 4
			h::style($this->css['plain'] ?: false);
470 4
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
471 4
			$this->add_includes_on_page_manually_added_frontend_load_optimization($Config, $Request);
472
		} else {
473 2
			$this->add_includes_on_page_manually_added_normal($Config, $Request, $preload);
474
		}
475 4
	}
476
	/**
477
	 * @param Config   $Config
478
	 * @param Request  $Request
479
	 * @param string[] $preload
480
	 */
481 2
	protected function add_includes_on_page_manually_added_normal ($Config, $Request, $preload) {
482 2
		$this->add_preload($preload, $Request);
483 2
		$configs      = $this->core_config.$this->config;
484
		$scripts      =
485 2
			array_reduce(
486 2
				array_merge($this->core_js['path'], $this->js['path']),
487
				function ($content, $src) {
488 2
					return "$content<script src=\"$src\"></script>\n";
489 2
				}
490
			).
491 2
			h::script($this->js['plain'] ?: false);
492
		$html_imports =
493 2
			array_reduce(
494 2
				array_merge($this->core_html['path'], $this->html['path']),
495 2
				function ($content, $href) {
496 2
					return "$content<link href=\"$href\" rel=\"import\">\n";
497 2
				}
498
			).
499 2
			$this->html['plain'];
500 2
		$this->Head .= $configs;
501 2
		$this->add_script_imports_to_document($Config, $scripts.$html_imports);
502 2
	}
503
	/**
504
	 * @param string[] $preload
505
	 * @param Request  $Request
506
	 */
507 4
	protected function add_preload ($preload, $Request) {
508 4
		if ($Request->cookie('pushed')) {
509
			return;
510
		}
511 4
		$Response = Response::instance();
512 4
		$Response->cookie('pushed', 1, 0, true);
513 4
		foreach ($preload as $resource) {
514 4
			$extension = explode('?', file_extension($resource))[0];
515 4
			$as        = $this->extension_to_as[$extension];
516 4
			$resource  = str_replace(' ', '%20', $resource);
517 4
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
518
		}
519 4
	}
520
	/**
521
	 * @param Config  $Config
522
	 * @param Request $Request
523
	 */
524 4
	protected function add_includes_on_page_manually_added_frontend_load_optimization ($Config, $Request) {
525 4
		list($optimized_includes, $preload) = file_get_json("$this->pcache_basename_path.optimized.json");
526 4
		$this->add_preload(
527
			array_unique(
528
				array_merge(
529
					$preload,
530 4
					$this->core_css['path'],
531 4
					$this->css['path']
532
				)
533
			),
534
			$Request
535
		);
536 4
		$system_scripts    = '';
537 4
		$optimized_scripts = [];
538 4
		$system_imports    = '';
539 4
		$optimized_imports = [];
540 4
		foreach (array_merge($this->core_js['path'], $this->js['path']) as $script) {
541 4
			if (isset($optimized_includes[$script])) {
542
				$optimized_scripts[] = $script;
543
			} else {
544 4
				$system_scripts .= "<script src=\"$script\"></script>\n";
545
			}
546
		}
547 4
		foreach (array_merge($this->core_html['path'], $this->html['path']) as $import) {
548 4
			if (isset($optimized_includes[$import])) {
549
				$optimized_imports[] = $import;
550
			} else {
551 4
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
552
			}
553
		}
554 4
		$scripts      = h::script($this->js['plain'] ?: false);
555 4
		$html_imports = $this->html['plain'];
556 4
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_includes');
557 4
		$this->Head .= $this->core_config.$this->config;
558 4
		$this->add_script_imports_to_document($Config, $system_scripts.$system_imports.$scripts.$html_imports);
559 4
	}
560
}
561