Completed
Push — master ( e59b98...2a95ca )
by Nazar
03:53
created

Includes::add_includes_on_page()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 38
Code Lines 22

Duplication

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