Completed
Push — master ( 4a3457...2febd0 )
by Nazar
04:30
created

Includes::add_preload()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 2
dl 0
loc 13
ccs 11
cts 11
cp 1
crap 3
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
	protected $extension_to_as = [
32
		'jpeg' => 'image',
33
		'jpe'  => 'image',
34
		'jpg'  => 'image',
35
		'gif'  => 'image',
36
		'png'  => 'image',
37
		'svg'  => 'image',
38
		'svgz' => 'image',
39
		'woff' => 'font',
40
		//'woff2' => 'font',
41
		'css'  => 'style',
42
		'js'   => 'script',
43
		'html' => 'document'
44
	];
45
	/**
46
	 * @var array
47
	 */
48
	protected $core_html;
49
	/**
50
	 * @var array
51
	 */
52
	protected $core_js;
53
	/**
54
	 * @var array
55
	 */
56
	protected $core_css;
57
	/**
58
	 * @var string
59
	 */
60
	protected $core_config;
61
	/**
62
	 * @var array
63
	 */
64
	protected $html;
65
	/**
66
	 * @var array
67
	 */
68
	protected $js;
69
	/**
70
	 * @var array
71
	 */
72
	protected $css;
73
	/**
74
	 * @var string
75
	 */
76
	protected $config;
77
	/**
78
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
79
	 * @var string
80
	 */
81
	protected $pcache_basename_path;
82 34
	protected function init_includes () {
83 34
		$this->core_html            = [];
84 34
		$this->core_js              = [];
85 34
		$this->core_css             = [];
86 34
		$this->core_config          = '';
87 34
		$this->html                 = [];
88 34
		$this->js                   = [];
89 34
		$this->css                  = [];
90 34
		$this->config               = '';
91 34
		$this->pcache_basename_path = '';
92 34
	}
93
	/**
94
	 * @param string|string[] $add
95
	 *
96
	 * @return \cs\Page
97
	 */
98 4
	protected function core_html ($add) {
99 4
		return $this->include_common('html', $add, true);
100
	}
101
	/**
102
	 * @param string|string[] $add
103
	 *
104
	 * @return \cs\Page
105
	 */
106 4
	protected function core_js ($add) {
107 4
		return $this->include_common('js', $add, true);
108
	}
109
	/**
110
	 * @param string|string[] $add
111
	 *
112
	 * @return \cs\Page
113
	 */
114 4
	protected function core_css ($add) {
115 4
		return $this->include_common('css', $add, true);
116
	}
117
	/**
118
	 * Including of Web Components
119
	 *
120
	 * @param string|string[] $add Path to including file, or code
121
	 *
122
	 * @return \cs\Page
123
	 */
124 2
	public function html ($add) {
125 2
		return $this->include_common('html', $add, false);
126
	}
127
	/**
128
	 * Including of JavaScript
129
	 *
130
	 * @param string|string[] $add Path to including file, or code
131
	 *
132
	 * @return \cs\Page
133
	 */
134 2
	public function js ($add) {
135 2
		return $this->include_common('js', $add, false);
136
	}
137
	/**
138
	 * Including of CSS
139
	 *
140
	 * @param string|string[] $add Path to including file, or code
141
	 *
142
	 * @return \cs\Page
143
	 */
144 2
	public function css ($add) {
145 2
		return $this->include_common('css', $add, false);
146
	}
147
	/**
148
	 * @param string          $what
149
	 * @param string|string[] $add
150
	 * @param bool            $core
151
	 *
152
	 * @return \cs\Page
153
	 */
154 4
	protected function include_common ($what, $add, $core) {
155 4
		if (!$add) {
156 2
			return $this;
157
		}
158 4
		if (is_array($add)) {
159 4
			foreach (array_filter($add) as $a) {
160 4
				$this->include_common($what, $a, $core);
161
			}
162
		} else {
163 4
			if ($core) {
164 4
				$what = "core_$what";
165
			}
166 4
			$target   = &$this->$what;
167 4
			$target[] = $add;
168
		}
169 4
		return $this;
170
	}
171
	/**
172
	 * Add config on page to make it available on frontend
173
	 *
174
	 * @param mixed  $config_structure        Any scalar type or array
175
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
176
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
177
	 *
178
	 * @return \cs\Page
179
	 */
180 4
	public function config ($config_structure, $target) {
181 4
		return $this->config_internal($config_structure, $target);
182
	}
183
	/**
184
	 * @param mixed  $config_structure
185
	 * @param string $target
186
	 * @param bool   $core
187
	 *
188
	 * @return \cs\Page
189
	 */
190 4
	protected function config_internal ($config_structure, $target, $core = false) {
191 4
		$config = h::script(
192 4
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
193
			[
194 4
				'target' => $target,
195 4
				'class'  => 'cs-config',
196 4
				'type'   => 'application/json'
197
			]
198
		);
199 4
		if ($core) {
200 4
			$this->core_config .= $config;
201
		} else {
202 4
			$this->config .= $config;
203
		}
204 4
		return $this;
205
	}
206
	/**
207
	 * Getting of HTML, JS and CSS includes
208
	 *
209
	 * @return \cs\Page
210
	 */
211 4
	protected function add_includes_on_page () {
212 4
		$Config = Config::instance(true);
213 4
		if (!$Config) {
214 2
			return $this;
215
		}
216
		/**
217
		 * Base name for cache files
218
		 */
219 4
		$this->pcache_basename_path = PUBLIC_CACHE.'/'.$this->theme;
220
		// TODO: I hope some day we'll get rid of this sh*t :(
221 4
		$this->edge();
222 4
		$Request = Request::instance();
223
		/**
224
		 * If CSS and JavaScript compression enabled
225
		 */
226 4
		$L = Language::instance();
227 4
		if ($this->page_compression_usage($Config, $Request)) {
228
			/**
229
			 * Rebuilding HTML, JS and CSS cache if necessary
230
			 */
231 4
			Cache::rebuild($Config, $L, $this->pcache_basename_path, $this->theme);
232 4
			$this->webcomponents_polyfill($Request, $Config, true);
233 4
			$languages_hash = $this->get_hash_of(implode('', $Config->core['active_languages']));
234 4
			$language_hash  = file_get_json(PUBLIC_CACHE."/languages-$languages_hash.json")[$L->clanguage];
235 4
			$this->config_internal(
236
				[
237 4
					'language' => $L->clanguage,
238 4
					'hash'     => $language_hash
239
				],
240 4
				'cs.current_language',
241 4
				true
242
			);
243 4
			list($includes, $preload) = $this->get_includes_and_preload_resource_for_page_with_compression($Request);
244
		} else {
245 2
			$this->webcomponents_polyfill($Request, $Config, false);
246
			/**
247
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
248
			 */
249 2
			$this->config_internal($L, 'cs.Language', true);
250 2
			$this->config_internal(RequireJS::get_paths(), 'requirejs.paths', true);
251 2
			$includes = $this->get_includes_for_page_without_compression($Config, $Request);
252 2
			$preload  = [];
253
		}
254 4
		$this->core_css($includes['css']);
255 4
		$this->core_js($includes['js']);
256 4
		if (isset($includes['html'])) {
257 4
			$this->core_html($includes['html']);
258
		}
259 4
		$this->add_includes_on_page_manually_added($Config, $Request, $preload);
260 4
		return $this;
261
	}
262
	/**
263
	 * @param string $content
264
	 *
265
	 * @return string
266
	 */
267 4
	protected function get_hash_of ($content) {
268 4
		return substr(md5($content), 0, 5);
269
	}
270
	/**
271
	 * @param Config  $Config
272
	 * @param Request $Request
273
	 *
274
	 * @return bool
275
	 */
276 4
	protected function page_compression_usage ($Config, $Request) {
277 4
		return $Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']));
278
	}
279
	/**
280
	 * Add JS polyfills for IE/Edge
281
	 */
282 4
	protected function edge () {
283 4
		if (strpos(Request::instance()->header('user-agent'), 'Edge') === false) {
284 4
			return;
285
		}
286 2
		$this->core_js(
287 2
			get_files_list(DIR.'/includes/js/microsoft_sh*t', '/.*\.js$/i', 'f', 'includes/js/microsoft_sh*t', true)
288
		);
289 2
	}
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 (($this->theme != Config::SYSTEM_THEME && $Config->core['disable_webcomponents']) || $Request->cookie('shadow_dom') == 1) {
299 2
			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 2
				$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) = Collecting::get_includes_dependencies_and_map($Config, $this->theme);
411 2
		$includes = $this->get_normalized_includes($dependencies, $includes_map, $Request);
412 2
		return $this->add_versions_hash(_substr($includes, strlen(DIR)));
413
	}
414
	/**
415
	 * @param string[][] $includes
416
	 *
417
	 * @return string[][]
418
	 */
419 2
	protected function add_versions_hash ($includes) {
420 2
		$content      = array_reduce(
421 2
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
422
			function ($content, $file) {
423
				return $content.file_get_contents($file);
424 2
			}
425
		);
426 2
		$content_hash = $this->get_hash_of($content);
427 2
		foreach ($includes as &$files) {
428 2
			foreach ($files as &$file) {
429 2
				$file .= "?$content_hash";
430
			}
431 2
			unset($file);
432
		}
433 2
		return $includes;
434
	}
435
	/**
436
	 * @param Config   $Config
437
	 * @param Request  $Request
438
	 * @param string[] $preload
439
	 */
440 4
	protected function add_includes_on_page_manually_added ($Config, $Request, $preload) {
441
		/** @noinspection NestedTernaryOperatorInspection */
442 4
		$this->Head .= array_reduce(
443 4
			array_merge($this->core_css, $this->css),
444
			function ($content, $href) {
445 4
				return "$content<link href=\"$href\" rel=\"stylesheet\">\n";
446 4
			}
447
		);
448 4
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
449 4
			$this->add_includes_on_page_manually_added_frontend_load_optimization($Config, $Request);
450
		} else {
451 2
			$this->add_includes_on_page_manually_added_normal($Config, $Request, $preload);
452
		}
453 4
	}
454
	/**
455
	 * @param Config   $Config
456
	 * @param Request  $Request
457
	 * @param string[] $preload
458
	 */
459 2
	protected function add_includes_on_page_manually_added_normal ($Config, $Request, $preload) {
460 2
		$this->add_preload($preload, $Request);
461 2
		$configs      = $this->core_config.$this->config;
462 2
		$scripts      = array_reduce(
463 2
			array_merge($this->core_js, $this->js),
464
			function ($content, $src) {
465 2
				return "$content<script src=\"$src\"></script>\n";
466 2
			}
467
		);
468 2
		$html_imports = array_reduce(
469 2
			array_merge($this->core_html, $this->html),
470 2
			function ($content, $href) {
471 2
				return "$content<link href=\"$href\" rel=\"import\">\n";
472 2
			}
473
		);
474 2
		$this->Head .= $configs;
475 2
		$this->add_script_imports_to_document($Config, $scripts.$html_imports);
476 2
	}
477
	/**
478
	 * @param string[] $preload
479
	 * @param Request  $Request
480
	 */
481 4
	protected function add_preload ($preload, $Request) {
482 4
		if ($Request->cookie('pushed')) {
483 2
			return;
484
		}
485 4
		$Response = Response::instance();
486 4
		$Response->cookie('pushed', 1, 0, true);
487 4
		foreach ($preload as $resource) {
488 4
			$extension = explode('?', file_extension($resource))[0];
489 4
			$as        = $this->extension_to_as[$extension];
490 4
			$resource  = str_replace(' ', '%20', $resource);
491 4
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
492
		}
493 4
	}
494
	/**
495
	 * @param Config  $Config
496
	 * @param Request $Request
497
	 */
498 4
	protected function add_includes_on_page_manually_added_frontend_load_optimization ($Config, $Request) {
499 4
		list($optimized_includes, $preload) = file_get_json("$this->pcache_basename_path.optimized.json");
500 4
		$this->add_preload(
501
			array_unique(
502 4
				array_merge($preload, $this->core_css, $this->css)
503
			),
504
			$Request
505
		);
506 4
		$system_scripts    = '';
507 4
		$optimized_scripts = [];
508 4
		$system_imports    = '';
509 4
		$optimized_imports = [];
510 4
		foreach (array_merge($this->core_js, $this->js) as $script) {
511 4
			if (isset($optimized_includes[$script])) {
512 2
				$optimized_scripts[] = $script;
513
			} else {
514 4
				$system_scripts .= "<script src=\"$script\"></script>\n";
515
			}
516
		}
517 4
		foreach (array_merge($this->core_html, $this->html) as $import) {
518 4
			if (isset($optimized_includes[$import])) {
519 2
				$optimized_imports[] = $import;
520
			} else {
521 4
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
522
			}
523
		}
524 4
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_includes');
525 4
		$this->Head .= $this->core_config.$this->config;
526 4
		$this->add_script_imports_to_document($Config, $system_scripts.$system_imports);
527 4
	}
528
}
529