Completed
Push — master ( db24d0...9b8153 )
by Nazar
04:35
created

Includes::add_preloads()   D

Complexity

Conditions 18
Paths 18

Size

Total Lines 38
Code Lines 35

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 38
rs 4.947
cc 18
eloc 35
nc 18
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @package   CleverStyle CMS
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\Event,
14
	cs\Language,
15
	cs\Request,
16
	cs\Response,
17
	cs\User,
18
	h,
19
	cs\Page\Includes\Cache,
20
	cs\Page\Includes\Collecting,
21
	cs\Page\Includes\RequireJS;
22
23
/**
24
 * Includes management for `cs\Page` class
25
 *
26
 * @property string $Title
27
 * @property string $Description
28
 * @property string $canonical_url
29
 * @property string $Head
30
 * @property string $post_Body
31
 * @property string $theme
32
 */
33
trait Includes {
34
	use
35
		Cache,
36
		Collecting,
37
		RequireJS;
38
	/**
39
	 * @var array
40
	 */
41
	protected $core_html;
42
	/**
43
	 * @var array
44
	 */
45
	protected $core_js;
46
	/**
47
	 * @var array
48
	 */
49
	protected $core_css;
50
	/**
51
	 * @var string
52
	 */
53
	protected $core_config;
54
	/**
55
	 * @var array
56
	 */
57
	protected $html;
58
	/**
59
	 * @var array
60
	 */
61
	protected $js;
62
	/**
63
	 * @var array
64
	 */
65
	protected $css;
66
	/**
67
	 * @var string
68
	 */
69
	protected $config;
70
	/**
71
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
72
	 * @var string
73
	 */
74
	protected $pcache_basename_path;
75
	protected function init_includes () {
76
		$this->core_html            = ['path' => [], 'plain' => ''];
77
		$this->core_js              = ['path' => [], 'plain' => ''];
78
		$this->core_css             = ['path' => [], 'plain' => ''];
79
		$this->core_config          = '';
80
		$this->html                 = ['path' => [], 'plain' => ''];
81
		$this->js                   = ['path' => [], 'plain' => ''];
82
		$this->css                  = ['path' => [], 'plain' => ''];
83
		$this->config               = '';
84
		$this->pcache_basename_path = '';
85
	}
86
	/**
87
	 * Including of Web Components
88
	 *
89
	 * @param string|string[] $add  Path to including file, or code
90
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
91
	 *
92
	 * @return \cs\Page
93
	 */
94
	function html ($add, $mode = 'file') {
95
		return $this->html_internal($add, $mode);
96
	}
97
	/**
98
	 * @param string|string[] $add
99
	 * @param string          $mode
100
	 * @param bool            $core
101
	 *
102
	 * @return \cs\Page
103
	 */
104
	protected function html_internal ($add, $mode = 'file', $core = false) {
105
		return $this->include_common('html', $add, $mode, $core);
106
	}
107
	/**
108
	 * Including of JavaScript
109
	 *
110
	 * @param string|string[] $add  Path to including file, or code
111
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
112
	 *
113
	 * @return \cs\Page
114
	 */
115
	function js ($add, $mode = 'file') {
116
		return $this->js_internal($add, $mode);
117
	}
118
	/**
119
	 * @param string|string[] $add
120
	 * @param string          $mode
121
	 * @param bool            $core
122
	 *
123
	 * @return \cs\Page
124
	 */
125
	protected function js_internal ($add, $mode = 'file', $core = false) {
126
		return $this->include_common('js', $add, $mode, $core);
127
	}
128
	/**
129
	 * Including of CSS
130
	 *
131
	 * @param string|string[] $add  Path to including file, or code
132
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
133
	 *
134
	 * @return \cs\Page
135
	 */
136
	function css ($add, $mode = 'file') {
137
		return $this->css_internal($add, $mode);
138
	}
139
	/**
140
	 * @param string|string[] $add
141
	 * @param string          $mode
142
	 * @param bool            $core
143
	 *
144
	 * @return \cs\Page
145
	 */
146
	protected function css_internal ($add, $mode = 'file', $core = false) {
147
		return $this->include_common('css', $add, $mode, $core);
148
	}
149
	/**
150
	 * @param string          $what
151
	 * @param string|string[] $add
152
	 * @param string          $mode
153
	 * @param bool            $core
154
	 *
155
	 * @return \cs\Page
156
	 */
157
	protected function include_common ($what, $add, $mode, $core) {
158
		if (!$add) {
159
			return $this;
160
		}
161
		if (is_array($add)) {
162
			foreach (array_filter($add) as $style) {
163
				$this->include_common($what, $style, $mode, $core);
164
			}
165
		} else {
166
			if ($core) {
167
				$what = "core_$what";
168
			}
169
			$target = &$this->$what;
170
			if ($mode == 'file') {
171
				$target['path'][] = $add;
172
			} elseif ($mode == 'code') {
173
				$target['plain'] .= "$add\n";
174
			}
175
		}
176
		return $this;
177
	}
178
	/**
179
	 * Add config on page to make it available on frontend
180
	 *
181
	 * @param mixed  $config_structure        Any scalar type or array
182
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
183
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
184
	 *
185
	 * @return \cs\Page
186
	 */
187
	function config ($config_structure, $target) {
188
		return $this->config_internal($config_structure, $target);
189
	}
190
	/**
191
	 * @param mixed  $config_structure
192
	 * @param string $target
193
	 * @param bool   $core
194
	 *
195
	 * @return \cs\Page
196
	 */
197
	protected function config_internal ($config_structure, $target, $core = false) {
198
		$config = h::script(
199
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
200
			[
201
				'target' => $target,
202
				'class'  => 'cs-config',
203
				'type'   => 'application/json'
204
			]
205
		);
206
		if ($core) {
207
			$this->core_config .= $config;
208
		} else {
209
			$this->config .= $config;
210
		}
211
		return $this;
212
	}
213
	/**
214
	 * Getting of HTML, JS and CSS includes
215
	 *
216
	 * @return \cs\Page
217
	 */
218
	protected function add_includes_on_page () {
219
		$Config = Config::instance(true);
220
		if (!$Config) {
221
			return $this;
222
		}
223
		/**
224
		 * Base name for cache files
225
		 */
226
		$this->pcache_basename_path = PUBLIC_CACHE.'/'.$this->theme.'_'.Language::instance()->clang;
227
		/**
228
		 * Some JS configs required by system
229
		 */
230
		$this->add_system_configs();
231
		// TODO: I hope some day we'll get rid of this sh*t :(
232
		$this->ie_edge();
233
		$Request = Request::instance();
234
		/**
235
		 * If CSS and JavaScript compression enabled
236
		 */
237
		if ($Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']))) {
238
			$this->webcomponents_polyfill($Request, true);
239
			list($includes, $preload) = $this->get_includes_and_preload_resource_for_page_with_compression($Config, $Request);
240
			$this->add_preloads($preload);
241
		} else {
242
			$this->webcomponents_polyfill($Request, false);
243
			/**
244
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
245
			 */
246
			$this->config_internal(Language::instance(), 'cs.Language', true);
247
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
248
			$includes = $this->get_includes_for_page_without_compression($Config, $Request);
249
		}
250
		$this->css_internal($includes['css'], 'file', true);
251
		$this->js_internal($includes['js'], 'file', true);
252
		$this->html_internal($includes['html'], 'file', true);
253
		$this->add_includes_on_page_manually_added($Config);
254
		return $this;
255
	}
256
	/**
257
	 * Add JS polyfills for IE/Edge
258
	 */
259
	protected function ie_edge () {
260
		if (!preg_match('/Trident|Edge/', Request::instance()->header('user-agent'))) {
261
			return;
262
		}
263
		$this->js_internal(
264
			get_files_list(DIR."/includes/js/microsoft_sh*t", "/.*\\.js$/i", 'f', "includes/js/microsoft_sh*t", true),
265
			'file',
266
			true
267
		);
268
	}
269
	protected function add_system_configs () {
270
		$Config         = Config::instance();
271
		$Request        = Request::instance();
272
		$User           = User::instance();
273
		$current_module = $Request->current_module;
274
		$this->config_internal(
275
			[
276
				'base_url'              => $Config->base_url(),
277
				'current_base_url'      => $Config->base_url().'/'.($Request->admin_path ? 'admin/' : '').$current_module,
278
				'public_key'            => Core::instance()->public_key,
279
				'module'                => $current_module,
280
				'in_admin'              => (int)$Request->admin_path,
281
				'is_admin'              => (int)$User->admin(),
282
				'is_user'               => (int)$User->user(),
283
				'is_guest'              => (int)$User->guest(),
284
				'password_min_length'   => (int)$Config->core['password_min_length'],
285
				'password_min_strength' => (int)$Config->core['password_min_strength'],
286
				'debug'                 => (int)DEBUG,
287
				'route'                 => $Request->route,
288
				'route_path'            => $Request->route_path,
289
				'route_ids'             => $Request->route_ids
290
			],
291
			'cs',
292
			true
293
		);
294
		if ($User->admin()) {
295
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
296
		}
297
	}
298
	/**
299
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
300
	 *
301
	 * @param Request $Request
302
	 * @param bool    $with_compression
303
	 */
304
	protected function webcomponents_polyfill ($Request, $with_compression) {
305
		if ($Request->cookie('shadow_dom') == 1) {
306
			return;
307
		}
308
		$file = 'includes/js/WebComponents-polyfill/webcomponents-custom.min.js';
309
		if ($with_compression) {
310
			$compressed_file = PUBLIC_CACHE.'/webcomponents.js';
311
			if (!file_exists($compressed_file)) {
312
				$content = file_get_contents(DIR."/$file");
313
				file_put_contents($compressed_file, gzencode($content, 9), LOCK_EX | FILE_BINARY);
314
				file_put_contents("$compressed_file.hash", substr(md5($content), 0, 5));
315
			}
316
			$hash = file_get_contents("$compressed_file.hash");
317
			$this->js_internal("storage/pcache/webcomponents.js?$hash", 'file', true);
318
		} else {
319
			$this->js_internal($file, 'file', true);
320
		}
321
	}
322
	/**
323
	 * @param string[] $preload
324
	 */
325
	protected function add_preloads ($preload) {
326
		$Response = Response::instance();
327
		foreach ($preload as $resource) {
328
			$extension = explode('?', file_extension($resource))[0];
329
			switch ($extension) {
330
				case 'jpeg':
331
				case 'jpe':
332
				case 'jpg':
333
				case 'gif':
334
				case 'png':
335
				case 'svg':
336
				case 'svgz':
337
					$as = 'image';
338
					break;
339
				case 'ttf':
340
				case 'ttc':
341
				case 'otf':
342
				case 'woff':
343
				case 'woff2':
344
				case 'eot':
345
					$as = 'font';
346
					break;
347
				case 'css':
348
					$as = 'style';
349
					break;
350
				case 'js':
351
					$as = 'script';
352
					break;
353
				case 'html':
354
					$as = 'document';
355
					break;
356
				default:
357
					continue 2;
358
			}
359
			$resource = str_replace(' ', '%20', $resource);
360
			$Response->header('Link', "<$resource>; rel=preload; as=$as'", false);
361
		}
362
	}
363
	/**
364
	 * @param Config  $Config
365
	 * @param Request $Request
366
	 *
367
	 * @return array
1 ignored issue
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
368
	 */
369
	protected function get_includes_and_preload_resource_for_page_with_compression ($Config, $Request) {
370
		/**
371
		 * Rebuilding HTML, JS and CSS cache if necessary
372
		 */
373
		if (!file_exists("$this->pcache_basename_path.json")) {
374
			list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
375
			$compressed_includes_map    = [];
376
			$not_embedded_resources_map = [];
377
			foreach ($includes_map as $filename_prefix => $local_includes) {
378
				// We replace `/` by `+` to make it suitable for filename
379
				$filename_prefix                           = str_replace('/', '+', $filename_prefix);
380
				$compressed_includes_map[$filename_prefix] = $this->cache_compressed_includes_files(
381
					"$this->pcache_basename_path:$filename_prefix",
382
					$local_includes,
383
					$Config->core['vulcanization'],
384
					$not_embedded_resources_map
385
				);
386
			}
387
			unset($includes_map, $filename_prefix, $local_includes);
388
			file_put_json("$this->pcache_basename_path.json", [$dependencies, $compressed_includes_map, array_filter($not_embedded_resources_map)]);
389
			Event::instance()->fire('System/Page/rebuild_cache');
390
		}
391
		list($dependencies, $compressed_includes_map, $not_embedded_resources_map) = file_get_json("$this->pcache_basename_path.json");
392
		$includes = $this->get_normalized_includes($dependencies, $compressed_includes_map, '+', $Request);
393
		$preload  = [];
394
		foreach (array_merge(...array_values($includes)) as $path) {
395
			if (isset($not_embedded_resources_map[$path])) {
396
				$preload[] = $not_embedded_resources_map[$path];
397
			}
398
		}
399
		return [$includes, array_merge(...$preload)];
400
	}
401
	/**
402
	 * @param array      $dependencies
403
	 * @param string[][] $includes_map
404
	 * @param string     $separator `+` or `/`
405
	 * @param Request    $Request
406
	 *
407
	 * @return array
408
	 */
409
	protected function get_normalized_includes ($dependencies, $includes_map, $separator, $Request) {
410
		$current_module = $Request->current_module;
411
		/**
412
		 * Current URL based on controller path (it better represents how page was rendered)
413
		 */
414
		$current_url = array_slice(App::instance()->controller_path, 1);
415
		$current_url = ($Request->admin_path ? "admin$separator" : '')."$current_module$separator".implode($separator, $current_url);
416
		/**
417
		 * Narrow the dependencies to current module only
418
		 */
419
		$dependencies          = array_merge(
420
			isset($dependencies[$current_module]) ? $dependencies[$current_module] : [],
421
			$dependencies['System']
422
		);
423
		$system_includes       = [];
424
		$dependencies_includes = [];
425
		$includes              = [];
426
		foreach ($includes_map as $path => $local_includes) {
427
			if ($path == 'System') {
428
				$system_includes = $local_includes;
429
			} elseif ($this->is_dependency($dependencies, $path, '/', $Request)) {
430
				$dependencies_includes[] = $local_includes;
431
			} elseif (mb_strpos($current_url, $path) === 0) {
432
				$includes[] = $local_includes;
433
			}
434
		}
435
		return array_merge_recursive($system_includes, ...$dependencies_includes, ...$includes);
436
	}
437
	/**
438
	 * @param array   $dependencies
439
	 * @param string  $url
440
	 * @param string  $separator `+` or `/`
441
	 * @param Request $Request
442
	 *
443
	 * @return bool
444
	 */
445
	protected function is_dependency ($dependencies, $url, $separator, $Request) {
446
		$url_exploded = explode($separator, $url);
447
		/** @noinspection NestedTernaryOperatorInspection */
448
		$url_module = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
449
		return
450
			$url_module !== Config::SYSTEM_MODULE &&
451
			in_array($url_module, $dependencies) &&
452
			(
453
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
454
			);
455
	}
456
	/**
457
	 * @param Config  $Config
458
	 * @param Request $Request
459
	 *
460
	 * @return string[][]
461
	 */
462
	protected function get_includes_for_page_without_compression ($Config, $Request) {
463
		// To determine all dependencies and stuff we need `$Config` object to be already created
464
		list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
465
		$includes = $this->get_normalized_includes($dependencies, $includes_map, '/', $Request);
466
		return $this->add_versions_hash($this->absolute_path_to_relative($includes));
467
	}
468
	/**
469
	 * @param string[]|string[][] $path
470
	 *
471
	 * @return string[]|string[][]
472
	 */
473
	protected function absolute_path_to_relative ($path) {
474
		return _substr($path, strlen(DIR) + 1);
475
	}
476
	/**
477
	 * @param string[][] $includes
478
	 *
479
	 * @return string[][]
480
	 */
481
	protected function add_versions_hash ($includes) {
482
		$content     = array_reduce(
483
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
484
			function ($content, $file) {
485
				return $content.file_get_contents($file);
486
			}
487
		);
488
		$content_md5 = substr(md5($content), 0, 5);
489
		foreach ($includes as &$files) {
490
			foreach ($files as &$file) {
491
				$file .= "?$content_md5";
492
			}
493
			unset($file);
494
		}
495
		return $includes;
496
	}
497
	/**
498
	 * @param Config $Config
499
	 */
500
	protected function add_includes_on_page_manually_added ($Config) {
501
		$configs = $this->core_config.$this->config;
502
		/** @noinspection NestedTernaryOperatorInspection */
503
		$styles =
504
			array_reduce(
505
				array_merge($this->core_css['path'], $this->css['path']),
506
				function ($content, $href) {
507
					return "$content<link href=\"/$href\" rel=\"stylesheet\" shim-shadowdom>\n";
508
				}
509
			).
510
			h::style($this->core_css['plain'].$this->css['plain'] ?: false);
511
		/** @noinspection NestedTernaryOperatorInspection */
512
		$scripts      =
513
			array_reduce(
514
				array_merge($this->core_js['path'], $this->js['path']),
515
				function ($content, $src) {
516
					return "$content<script src=\"/$src\"></script>\n";
517
				}
518
			).
519
			h::script($this->core_js['plain'].$this->js['plain'] ?: false);
520
		$html_imports =
521
			array_reduce(
522
				array_merge($this->core_html['path'], $this->html['path']),
523
				function ($content, $href) {
524
					return "$content<link href=\"/$href\" rel=\"import\">\n";
525
				}
526
			).
527
			$this->core_html['plain'].$this->html['plain'];
528
		$this->Head .= $configs.$styles;
529
		if ($Config->core['put_js_after_body']) {
530
			$this->post_Body .= $scripts.$html_imports;
531
		} else {
532
			$this->Head .= $scripts.$html_imports;
533
		}
534
	}
535
}
536