Completed
Push — master ( 9235b1...2182c9 )
by Nazar
04:49
created

Includes::add_versions_hash()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 11
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 16
rs 9.4285
ccs 10
cts 10
cp 1
crap 3
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 36
	protected function init_includes () {
88 36
		$this->core_html            = ['path' => []]; // No plain HTML in core
89 36
		$this->core_js              = ['path' => []]; // No plain JS in core
90 36
		$this->core_css             = ['path' => []]; // No plain CSS in core
91 36
		$this->core_config          = '';
92 36
		$this->html                 = ['path' => [], 'plain' => ''];
93 36
		$this->js                   = ['path' => [], 'plain' => ''];
94 36
		$this->css                  = ['path' => [], 'plain' => ''];
95 36
		$this->config               = '';
96 36
		$this->pcache_basename_path = '';
97 36
	}
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 4
	protected function html_internal ($add, $mode = 'file', $core = false) {
117 4
		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 4
	protected function js_internal ($add, $mode = 'file', $core = false) {
138 4
		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 4
	protected function css_internal ($add, $mode = 'file', $core = false) {
159 4
		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 4
	protected function include_common ($what, $add, $mode, $core) {
170 4
		if (!$add) {
171
			return $this;
172
		}
173 4
		if (is_array($add)) {
174 4
			foreach (array_filter($add) as $style) {
175 4
				$this->include_common($what, $style, $mode, $core);
176
			}
177
		} else {
178 4
			if ($core) {
179 4
				$what = "core_$what";
180
			}
181 4
			$target = &$this->$what;
182 4
			if ($mode == 'file') {
183 4
				$target['path'][] = $add;
184
			} elseif ($mode == 'code') {
185
				$target['plain'] .= "$add\n";
186
			}
187
		}
188 4
		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 4
	function config ($config_structure, $target) {
200 4
		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 4
	protected function config_internal ($config_structure, $target, $core = false) {
210 4
		$config = h::script(
211 4
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
212
			[
213 4
				'target' => $target,
214 4
				'class'  => 'cs-config',
215 4
				'type'   => 'application/json'
216
			]
217
		);
218 4
		if ($core) {
219 2
			$this->core_config .= $config;
220
		} else {
221 4
			$this->config .= $config;
222
		}
223 4
		return $this;
224
	}
225
	/**
226
	 * Getting of HTML, JS and CSS includes
227
	 *
228
	 * @return \cs\Page
229
	 */
230 4
	protected function add_includes_on_page () {
231 4
		$Config = Config::instance(true);
232 4
		if (!$Config) {
233
			return $this;
234
		}
235
		/**
236
		 * Base name for cache files
237
		 */
238 4
		$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 4
		$this->ie_edge();
241 4
		$Request = Request::instance();
242
		/**
243
		 * If CSS and JavaScript compression enabled
244
		 */
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);
250 4
			$this->webcomponents_polyfill($Request, $Config, true);
251 4
			list($includes, $preload) = $this->get_includes_and_preload_resource_for_page_with_compression($Request);
252
		} else {
253 2
			$this->webcomponents_polyfill($Request, $Config, false);
254
			/**
255
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
256
			 */
257 2
			$this->config_internal(Language::instance(), 'cs.Language', true);
258 2
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
259 2
			$includes = $this->get_includes_for_page_without_compression($Config, $Request);
260 2
			$preload  = [];
261
		}
262 4
		$this->css_internal($includes['css'], 'file', true);
263 4
		$this->js_internal($includes['js'], 'file', true);
264 4
		$this->html_internal($includes['html'], 'file', true);
265 4
		$this->add_includes_on_page_manually_added($Config, $Request, $preload);
266 4
		return $this;
267
	}
268
	/**
269
	 * @param Config  $Config
270
	 * @param Request $Request
271
	 *
272
	 * @return bool
273
	 */
274 4
	protected function page_compression_usage ($Config, $Request) {
275 4
		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 4
	protected function ie_edge () {
281 4
		if (!preg_match('/Trident|Edge/', Request::instance()->header('user-agent'))) {
282 4
			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 Config  $Config
295
	 * @param bool    $with_compression
296
	 */
297 4
	protected function webcomponents_polyfill ($Request, $Config, $with_compression) {
298 4
		if ($Request->cookie('shadow_dom') == 1) {
299
			return;
300
		}
301 4
		if ($with_compression) {
302 4
			$hash = file_get_contents(PUBLIC_CACHE.'/webcomponents.js.hash');
303 4
			$this->add_script_imports_to_document($Config, "<script src=\"/storage/pcache/webcomponents.js?$hash\"></script>\n");
304
		} else {
305 2
			$this->add_script_imports_to_document($Config, "<script src=\"/includes/js/WebComponents-polyfill/webcomponents-custom.min.js\"></script>\n");
306
		}
307 4
	}
308
	/**
309
	 * @param Config $Config
310
	 * @param string $content
311
	 */
312 4
	protected function add_script_imports_to_document ($Config, $content) {
313 4
		if ($Config->core['put_js_after_body']) {
314 4
			$this->post_Body .= $content;
315
		} else {
316 2
			$this->Head .= $content;
317
		}
318 4
	}
319
	/**
320
	 * @param Request $Request
321
	 *
322
	 * @return array[]
323
	 */
324 4
	protected function get_includes_and_preload_resource_for_page_with_compression ($Request) {
325 4
		list($dependencies, $compressed_includes_map, $not_embedded_resources_map) = file_get_json("$this->pcache_basename_path.json");
326 4
		$includes = $this->get_normalized_includes($dependencies, $compressed_includes_map, $Request);
327 4
		$preload  = [];
328 4
		foreach (array_merge(...array_values($includes)) as $path) {
329 4
			$preload[] = [$path];
330 4
			if (isset($not_embedded_resources_map[$path])) {
331 4
				$preload[] = $not_embedded_resources_map[$path];
332
			}
333
		}
334 4
		return [$includes, array_merge(...$preload)];
335
	}
336
	/**
337
	 * @param array      $dependencies
338
	 * @param string[][] $includes_map
339
	 * @param Request    $Request
340
	 *
341
	 * @return string[][]
342
	 */
343 4
	protected function get_normalized_includes ($dependencies, $includes_map, $Request) {
344 4
		$current_module = $Request->current_module;
345
		/**
346
		 * Current URL based on controller path (it better represents how page was rendered)
347
		 */
348 4
		$current_url = array_slice(App::instance()->controller_path, 1);
349 4
		$current_url = ($Request->admin_path ? "admin/" : '')."$current_module/".implode('/', $current_url);
350
		/**
351
		 * Narrow the dependencies to current module only
352
		 */
353 4
		$dependencies    = array_unique(
354
			array_merge(
355 4
				['System'],
356 4
				$dependencies['System'],
357 4
				isset($dependencies[$current_module]) ? $dependencies[$current_module] : []
358
			)
359
		);
360 4
		$system_includes = [];
361
		// Array with empty array in order to avoid `array_merge()` failure later
362 4
		$dependencies_includes = array_fill_keys($dependencies, [[]]);
363 4
		$includes              = [];
364 4
		foreach ($includes_map as $path => $local_includes) {
365 4
			if ($path == 'System') {
366 4
				$system_includes = $local_includes;
367 4
			} elseif ($component = $this->get_dependency_component($dependencies, $path, $Request)) {
368
				/**
369
				 * @var string $component
370
				 */
371
				$dependencies_includes[$component][] = $local_includes;
372 4
			} elseif (mb_strpos($current_url, $path) === 0) {
373 4
				$includes[] = $local_includes;
374
			}
375
		}
376
		// Convert to indexed array first
377 4
		$dependencies_includes = array_values($dependencies_includes);
378
		// Flatten array on higher level
379 4
		$dependencies_includes = array_merge(...$dependencies_includes);
380
		// Hack: 2 array_merge_recursive() just to be compatible with HHVM, simplify when https://github.com/facebook/hhvm/issues/7087 is resolved
381 4
		return _array(array_merge_recursive(array_merge_recursive($system_includes, ...$dependencies_includes), ...$includes));
382
	}
383
	/**
384
	 * @param array   $dependencies
385
	 * @param string  $url
386
	 * @param Request $Request
387
	 *
388
	 * @return false|string
389
	 */
390 4
	protected function get_dependency_component ($dependencies, $url, $Request) {
391 4
		$url_exploded = explode('/', $url);
392
		/** @noinspection NestedTernaryOperatorInspection */
393 4
		$url_component = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
394
		$is_dependency =
395 4
			$url_component !== Config::SYSTEM_MODULE &&
396 4
			in_array($url_component, $dependencies) &&
397
			(
398 4
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
399
			);
400 4
		return $is_dependency ? $url_component : false;
401
	}
402
	/**
403
	 * @param Config  $Config
404
	 * @param Request $Request
405
	 *
406
	 * @return string[][]
407
	 */
408 2
	protected function get_includes_for_page_without_compression ($Config, $Request) {
409
		// To determine all dependencies and stuff we need `$Config` object to be already created
410 2
		list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
411 2
		$includes = $this->get_normalized_includes($dependencies, $includes_map, $Request);
412 2
		return $this->add_versions_hash($this->absolute_path_to_relative($includes));
413
	}
414
	/**
415
	 * @param string[]|string[][] $path
416
	 *
417
	 * @return string[]|string[][]
418
	 */
419 4
	protected function absolute_path_to_relative ($path) {
420 4
		return _substr($path, strlen(DIR));
421
	}
422
	/**
423
	 * @param string[][] $includes
424
	 *
425
	 * @return string[][]
426
	 */
427 2
	protected function add_versions_hash ($includes) {
428 2
		$content     = array_reduce(
429 2
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
430
			function ($content, $file) {
431
				return $content.file_get_contents($file);
432 2
			}
433
		);
434 2
		$content_md5 = substr(md5($content), 0, 5);
435 2
		foreach ($includes as &$files) {
436 2
			foreach ($files as &$file) {
437 2
				$file .= "?$content_md5";
438
			}
439 2
			unset($file);
440
		}
441 2
		return $includes;
442
	}
443
	/**
444
	 * @param Config   $Config
445
	 * @param Request  $Request
446
	 * @param string[] $preload
447
	 */
448 4
	protected function add_includes_on_page_manually_added ($Config, $Request, $preload) {
449
		/** @noinspection NestedTernaryOperatorInspection */
450 4
		$this->Head .=
451 4
			array_reduce(
452 4
				array_merge($this->core_css['path'], $this->css['path']),
453
				function ($content, $href) {
454
					return "$content<link href=\"$href\" rel=\"stylesheet\">\n";
455 4
				}
456
			).
457 4
			h::style($this->css['plain'] ?: false);
458 4
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
459 4
			$this->add_includes_on_page_manually_added_frontend_load_optimization($Config);
460
		} else {
461 2
			$this->add_includes_on_page_manually_added_normal($Config, $preload);
462
		}
463 4
	}
464
	/**
465
	 * @param Config   $Config
466
	 * @param string[] $preload
467
	 */
468 2
	protected function add_includes_on_page_manually_added_normal ($Config, $preload) {
469 2
		$this->add_preload($preload);
470 2
		$configs      = $this->core_config.$this->config;
471
		$scripts      =
472 2
			array_reduce(
473 2
				array_merge($this->core_js['path'], $this->js['path']),
474
				function ($content, $src) {
475
					return "$content<script src=\"$src\"></script>\n";
476 2
				}
477
			).
478 2
			h::script($this->js['plain'] ?: false);
479
		$html_imports =
480 2
			array_reduce(
481 2
				array_merge($this->core_html['path'], $this->html['path']),
482 2
				function ($content, $href) {
483
					return "$content<link href=\"$href\" rel=\"import\">\n";
484 2
				}
485
			).
486 2
			$this->html['plain'];
487 2
		$this->Head .= $configs;
488 2
		$this->add_script_imports_to_document($Config, $scripts.$html_imports);
489 2
	}
490
	/**
491
	 * @param string[] $preload
492
	 */
493 4
	protected function add_preload ($preload) {
494 4
		$Response = Response::instance();
495 4
		foreach ($preload as $resource) {
496 4
			$extension = explode('?', file_extension($resource))[0];
497 4
			$as        = $this->extension_to_as[$extension];
498 4
			$resource  = str_replace(' ', '%20', $resource);
499 4
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
500
		}
501 4
	}
502
	/**
503
	 * @param Config $Config
504
	 */
505 4
	protected function add_includes_on_page_manually_added_frontend_load_optimization ($Config) {
506 4
		list($optimized_includes, $preload) = file_get_json("$this->pcache_basename_path.optimized.json");
507 4
		$this->add_preload(
508
			array_unique(
509
				array_merge(
510
					$preload,
511 4
					$this->core_css['path'],
512 4
					$this->css['path']
513
				)
514
			)
515
		);
516 4
		$system_scripts    = '';
517 4
		$optimized_scripts = [];
518 4
		$system_imports    = '';
519 4
		$optimized_imports = [];
520 4
		foreach (array_merge($this->core_js['path'], $this->js['path']) as $script) {
521 4
			if (isset($optimized_includes[$script])) {
522
				$optimized_scripts[] = $script;
523
			} else {
524 4
				$system_scripts .= "<script src=\"$script\"></script>\n";
525
			}
526
		}
527 4
		foreach (array_merge($this->core_html['path'], $this->html['path']) as $import) {
528 4
			if (isset($optimized_includes[$import])) {
529
				$optimized_imports[] = $import;
530
			} else {
531 4
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
532
			}
533
		}
534 4
		$scripts      = h::script($this->js['plain'] ?: false);
535 4
		$html_imports = $this->html['plain'];
536 4
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_includes');
537 4
		$this->Head .= $this->core_config.$this->config;
538 4
		$this->add_script_imports_to_document($Config, $system_scripts.$system_imports.$scripts.$html_imports);
539 4
	}
540
}
541