Completed
Push — master ( 1c948d...3791fe )
by Nazar
04:21
created

Includes::include_common()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 9
nop 4
dl 0
loc 21
rs 7.551
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\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' => []]; // No plain CSS in core
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 ($this->page_compression_usage($Config, $Request)) {
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, $Request, $preload);
267
		return $this;
268
	}
269
	/**
270
	 * @param Config  $Config
271
	 * @param Request $Request
272
	 *
273
	 * @return bool
274
	 */
275
	protected function page_compression_usage ($Config, $Request) {
276
		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
	protected function ie_edge () {
282
		if (!preg_match('/Trident|Edge/', Request::instance()->header('user-agent'))) {
283
			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),
287
			'file',
288
			true
289
		);
290
	}
291
	protected function add_system_configs () {
292
		$Config         = Config::instance();
293
		$Request        = Request::instance();
294
		$User           = User::instance();
295
		$current_module = $Request->current_module;
296
		$this->config_internal(
297
			[
298
				'base_url'              => $Config->base_url(),
299
				'current_base_url'      => $Config->base_url().'/'.($Request->admin_path ? 'admin/' : '').$current_module,
300
				'public_key'            => Core::instance()->public_key,
301
				'module'                => $current_module,
302
				'in_admin'              => (int)$Request->admin_path,
303
				'is_admin'              => (int)$User->admin(),
304
				'is_user'               => (int)$User->user(),
305
				'is_guest'              => (int)$User->guest(),
306
				'password_min_length'   => (int)$Config->core['password_min_length'],
307
				'password_min_strength' => (int)$Config->core['password_min_strength'],
308
				'debug'                 => (int)DEBUG,
309
				'route'                 => $Request->route,
310
				'route_path'            => $Request->route_path,
311
				'route_ids'             => $Request->route_ids
312
			],
313
			'cs',
314
			true
315
		);
316
		if ($User->admin()) {
317
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
318
		}
319
	}
320
	/**
321
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
322
	 *
323
	 * @param Request $Request
324
	 * @param bool    $with_compression
325
	 */
326
	protected function webcomponents_polyfill ($Request, $with_compression) {
327
		if ($Request->cookie('shadow_dom') == 1) {
328
			return;
329
		}
330
		if ($with_compression) {
331
			$hash = file_get_contents(PUBLIC_CACHE.'/webcomponents.js.hash');
332
			$this->js_internal("/storage/pcache/webcomponents.js?$hash", 'file', true);
333
		} else {
334
			$this->js_internal('/includes/js/WebComponents-polyfill/webcomponents-custom.min.js', 'file', true);
335
		}
336
	}
337
	/**
338
	 * @param Config  $Config
339
	 * @param Request $Request
340
	 *
341
	 * @return array[]
342
	 */
343
	protected function get_includes_and_preload_resource_for_page_with_compression ($Config, $Request) {
344
		/**
345
		 * Rebuilding HTML, JS and CSS cache if necessary
346
		 */
347
		$this->rebuild_cache($Config);
348
		list($dependencies, $compressed_includes_map, $not_embedded_resources_map) = file_get_json("$this->pcache_basename_path.json");
349
		$includes = $this->get_normalized_includes($dependencies, $compressed_includes_map, $Request);
350
		$preload  = [];
351
		foreach (array_merge(...array_values($includes)) as $path) {
352
			$preload[] = [$path];
353
			if (isset($not_embedded_resources_map[$path])) {
354
				$preload[] = $not_embedded_resources_map[$path];
355
			}
356
		}
357
		return [$includes, array_merge(...$preload)];
358
	}
359
	/**
360
	 * @param array      $dependencies
361
	 * @param string[][] $includes_map
362
	 * @param Request    $Request
363
	 *
364
	 * @return string[][]
365
	 */
366
	protected function get_normalized_includes ($dependencies, $includes_map, $Request) {
367
		$current_module = $Request->current_module;
368
		/**
369
		 * Current URL based on controller path (it better represents how page was rendered)
370
		 */
371
		$current_url = array_slice(App::instance()->controller_path, 1);
372
		$current_url = ($Request->admin_path ? "admin/" : '')."$current_module/".implode('/', $current_url);
373
		/**
374
		 * Narrow the dependencies to current module only
375
		 */
376
		$dependencies    = array_unique(
377
			array_merge(
378
				['System'],
379
				$dependencies['System'],
380
				isset($dependencies[$current_module]) ? $dependencies[$current_module] : []
381
			)
382
		);
383
		$system_includes = [];
384
		// Array with empty array in order to avoid `array_merge()` failure later
385
		$dependencies_includes = array_fill_keys($dependencies, [[]]);
386
		$includes              = [];
387
		foreach ($includes_map as $path => $local_includes) {
388
			if ($path == 'System') {
389
				$system_includes = $local_includes;
390
			} elseif ($component = $this->get_dependency_component($dependencies, $path, $Request)) {
391
				/**
392
				 * @var string $component
393
				 */
394
				$dependencies_includes[$component][] = $local_includes;
395
			} elseif (mb_strpos($current_url, $path) === 0) {
396
				$includes[] = $local_includes;
397
			}
398
		}
399
		// Convert to indexed array first
400
		$dependencies_includes = array_values($dependencies_includes);
401
		// Flatten array on higher level
402
		$dependencies_includes = array_merge(...$dependencies_includes);
403
		return _array(array_merge_recursive($system_includes, ...$dependencies_includes, ...$includes));
404
	}
405
	/**
406
	 * @param array   $dependencies
407
	 * @param string  $url
408
	 * @param Request $Request
409
	 *
410
	 * @return false|string
411
	 */
412
	protected function get_dependency_component ($dependencies, $url, $Request) {
413
		$url_exploded = explode('/', $url);
414
		/** @noinspection NestedTernaryOperatorInspection */
415
		$url_component = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
416
		$is_dependency =
417
			$url_component !== Config::SYSTEM_MODULE &&
418
			in_array($url_component, $dependencies) &&
419
			(
420
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
421
			);
422
		return $is_dependency ? $url_component : false;
423
	}
424
	/**
425
	 * @param Config  $Config
426
	 * @param Request $Request
427
	 *
428
	 * @return string[][]
429
	 */
430
	protected function get_includes_for_page_without_compression ($Config, $Request) {
431
		// To determine all dependencies and stuff we need `$Config` object to be already created
432
		list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
433
		$includes = $this->get_normalized_includes($dependencies, $includes_map, $Request);
434
		return $this->add_versions_hash($this->absolute_path_to_relative($includes));
435
	}
436
	/**
437
	 * @param string[]|string[][] $path
438
	 *
439
	 * @return string[]|string[][]
440
	 */
441
	protected function absolute_path_to_relative ($path) {
442
		return _substr($path, strlen(DIR));
443
	}
444
	/**
445
	 * @param string[][] $includes
446
	 *
447
	 * @return string[][]
448
	 */
449
	protected function add_versions_hash ($includes) {
450
		$content     = array_reduce(
451
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
452
			function ($content, $file) {
453
				return $content.file_get_contents($file);
454
			}
455
		);
456
		$content_md5 = substr(md5($content), 0, 5);
457
		foreach ($includes as &$files) {
458
			foreach ($files as &$file) {
459
				$file .= "?$content_md5";
460
			}
461
			unset($file);
462
		}
463
		return $includes;
464
	}
465
	/**
466
	 * @param Config   $Config
467
	 * @param Request  $Request
468
	 * @param string[] $preload
469
	 */
470
	protected function add_includes_on_page_manually_added ($Config, $Request, $preload) {
471
		/** @noinspection NestedTernaryOperatorInspection */
472
		$this->Head .=
473
			array_reduce(
474
				array_merge($this->core_css['path'], $this->css['path']),
475
				function ($content, $href) {
476
					return "$content<link href=\"$href\" rel=\"stylesheet\" shim-shadowdom>\n";
477
				}
478
			).
479
			h::style($this->css['plain'] ?: false);
480
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
481
			$this->add_includes_on_page_manually_added_frontend_load_optimization($Config);
482
		} else {
483
			$this->add_includes_on_page_manually_added_normal($Config, $Request, $preload);
484
		}
485
	}
486
	/**
487
	 * @param Config   $Config
488
	 * @param Request  $Request
489
	 * @param string[] $preload
490
	 */
491
	protected function add_includes_on_page_manually_added_normal ($Config, $Request, $preload) {
492
		$jquery    = $this->jquery($this->page_compression_usage($Config, $Request));
493
		$preload[] = $jquery;
494
		$this->add_preload($preload);
495
		$configs      = $this->core_config.$this->config;
496
		$scripts      =
497
			array_reduce(
498
				array_merge([$jquery], $this->core_js['path'], $this->js['path']),
499
				function ($content, $src) {
500
					return "$content<script src=\"$src\"></script>\n";
501
				}
502
			).
503
			h::script($this->js['plain'] ?: false);
504
		$html_imports =
505
			array_reduce(
506
				array_merge($this->core_html['path'], $this->html['path']),
507
				function ($content, $href) {
508
					return "$content<link href=\"$href\" rel=\"import\">\n";
509
				}
510
			).
511
			$this->html['plain'];
512
		$this->Head .= $configs;
513
		if ($Config->core['put_js_after_body']) {
514
			$this->post_Body .= $scripts.$html_imports;
515
		} else {
516
			$this->Head .= $scripts.$html_imports;
517
		}
518
	}
519
	/**
520
	 * Hack: jQuery is kind of special; it is only loaded directly in normal mode, during frontend load optimization it is loaded asynchronously in frontend
521
	 * TODO: In future we'll load jQuery as AMD module only and this thing will not be needed
522
	 *
523
	 * @param bool $with_compression
524
	 *
525
	 * @return string
526
	 */
527
	protected function jquery ($with_compression) {
528
		if ($with_compression) {
529
			$hash = file_get_contents(PUBLIC_CACHE.'/jquery.js.hash');
530
			return "/storage/pcache/jquery.js?$hash";
531
		} else {
532
			return '/includes/js/jquery/jquery-3.0.0-pre.js';
533
		}
534
	}
535
	/**
536
	 * @param string[] $preload
537
	 */
538
	protected function add_preload ($preload) {
539
		$Response = Response::instance();
540
		foreach ($preload as $resource) {
541
			$extension = explode('?', file_extension($resource))[0];
542
			$as        = $this->extension_to_as[$extension];
543
			$resource  = str_replace(' ', '%20', $resource);
544
			$Response->header('Link', "<$resource>; rel=preload; as=$as'", false);
545
		}
546
	}
547
	/**
548
	 * @param Config $Config
549
	 */
550
	protected function add_includes_on_page_manually_added_frontend_load_optimization ($Config) {
551
		list($optimized_includes, $preload) = file_get_json("$this->pcache_basename_path.optimized.json");
552
		$this->add_preload(
553
			array_unique(
554
				array_merge(
555
					$preload,
556
					$this->core_css['path'],
557
					$this->css['path']
558
				)
559
			)
560
		);
561
		$system_scripts    = '';
562
		$optimized_scripts = [$this->jquery(true)];
563
		$system_imports    = '';
564
		$optimized_imports = [];
565
		foreach (array_merge($this->core_js['path'], $this->js['path']) as $script) {
566
			if (isset($optimized_includes[$script])) {
567
				$optimized_scripts[] = $script;
568
			} else {
569
				$system_scripts .= "<script src=\"$script\"></script>\n";
570
			}
571
		}
572
		foreach (array_merge($this->core_html['path'], $this->html['path']) as $import) {
573
			if (isset($optimized_includes[$import])) {
574
				$optimized_imports[] = $import;
575
			} else {
576
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
577
			}
578
		}
579
		$scripts      = h::script($this->js['plain'] ?: false);
580
		$html_imports = $this->html['plain'];
581
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_includes');
582
		$this->Head .= $this->core_config.$this->config;
583
		if ($Config->core['put_js_after_body']) {
584
			$this->post_Body .= $system_scripts.$system_imports.$scripts.$html_imports;
585
		} else {
586
			$this->Head .= $system_scripts.$system_imports.$scripts.$html_imports;
587
		}
588
	}
589
}
590