Completed
Push — master ( 195cac...2c4aac )
by Nazar
05:02
created

Assets::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.0067

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 3
nop 1
dl 0
loc 16
ccs 10
cts 11
cp 0.9091
crap 3.0067
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-2017, 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\Assets\Cache,
17
	cs\Page\Assets\Collecting,
18
	cs\Page\Assets\RequireJS;
19
20
/**
21
 * Assets 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 Assets {
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
		'woff2' => 'font',
40
		'css'   => 'style',
41
		'js'    => 'script',
42
		'html'  => 'document'
43
	];
44
	/**
45
	 * @var array
46
	 */
47
	protected $core_html;
48
	/**
49
	 * @var array
50
	 */
51
	protected $core_js;
52
	/**
53
	 * @var array
54
	 */
55
	protected $core_css;
56
	/**
57
	 * @var string
58
	 */
59
	protected $core_config;
60
	/**
61
	 * @var array
62
	 */
63
	protected $html;
64
	/**
65
	 * @var array
66
	 */
67
	protected $js;
68
	/**
69
	 * @var array
70
	 */
71
	protected $css;
72
	/**
73
	 * @var string
74
	 */
75
	protected $config;
76 34
	protected function init_assets () {
77 34
		$this->core_html   = [];
78 34
		$this->core_js     = [];
79 34
		$this->core_css    = [];
80 34
		$this->core_config = '';
81 34
		$this->html        = [];
82 34
		$this->js          = [];
83 34
		$this->css         = [];
84 34
		$this->config      = '';
85 34
	}
86
	/**
87
	 * @param string|string[] $add
88
	 *
89
	 * @return \cs\Page
90
	 */
91 4
	protected function core_html ($add) {
92 4
		return $this->include_common('html', $add, true);
93
	}
94
	/**
95
	 * @param string|string[] $add
96
	 *
97
	 * @return \cs\Page
98
	 */
99 4
	protected function core_js ($add) {
100 4
		return $this->include_common('js', $add, true);
101
	}
102
	/**
103
	 * @param string|string[] $add
104
	 *
105
	 * @return \cs\Page
106
	 */
107 4
	protected function core_css ($add) {
108 4
		return $this->include_common('css', $add, true);
109
	}
110
	/**
111
	 * Including of Web Components
112
	 *
113
	 * @param string|string[] $add Path to including file, or code
114
	 *
115
	 * @return \cs\Page
116
	 */
117 2
	public function html ($add) {
118 2
		return $this->include_common('html', $add, false);
119
	}
120
	/**
121
	 * Including of JavaScript
122
	 *
123
	 * @param string|string[] $add Path to including file, or code
124
	 *
125
	 * @return \cs\Page
126
	 */
127 2
	public function js ($add) {
128 2
		return $this->include_common('js', $add, false);
129
	}
130
	/**
131
	 * Including of CSS
132
	 *
133
	 * @param string|string[] $add Path to including file, or code
134
	 *
135
	 * @return \cs\Page
136
	 */
137 2
	public function css ($add) {
138 2
		return $this->include_common('css', $add, false);
139
	}
140
	/**
141
	 * @param string          $what
142
	 * @param string|string[] $add
143
	 * @param bool            $core
144
	 *
145
	 * @return \cs\Page
146
	 */
147 4
	protected function include_common ($what, $add, $core) {
148 4
		if (!$add) {
149 2
			return $this;
150
		}
151 4
		if (is_array($add)) {
152 4
			foreach (array_filter($add) as $a) {
153 4
				$this->include_common($what, $a, $core);
154
			}
155
		} else {
156 4
			if ($core) {
157 4
				$what = "core_$what";
158
			}
159 4
			$target   = &$this->$what;
160 4
			$target[] = $add;
161
		}
162 4
		return $this;
163
	}
164
	/**
165
	 * Add config on page to make it available on frontend
166
	 *
167
	 * @param mixed  $config_structure        Any scalar type or array
168
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
169
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
170
	 *
171
	 * @return \cs\Page
172
	 */
173 4
	public function config ($config_structure, $target) {
174 4
		return $this->config_internal($config_structure, $target);
175
	}
176
	/**
177
	 * @param mixed  $config_structure
178
	 * @param string $target
179
	 * @param bool   $core
180
	 *
181
	 * @return \cs\Page
182
	 */
183 4
	protected function config_internal ($config_structure, $target, $core = false) {
184 4
		$config = h::script(
185 4
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
186
			[
187 4
				'target' => $target,
188 4
				'class'  => 'cs-config',
189 4
				'type'   => 'application/json'
190
			]
191
		);
192 4
		if ($core) {
193 4
			$this->core_config .= $config;
194
		} else {
195 4
			$this->config .= $config;
196
		}
197 4
		return $this;
198
	}
199
	/**
200
	 * Getting of HTML, JS and CSS assets
201
	 *
202
	 * @return \cs\Page
203
	 */
204 4
	protected function add_assets_on_page () {
205 4
		$Config = Config::instance(true);
206 4
		if (!$Config) {
207 2
			return $this;
208
		}
209 4
		$Request = Request::instance();
210
		/**
211
		 * If CSS and JavaScript compression enabled
212
		 */
213 4
		$L = Language::instance();
214 4
		if ($this->page_compression_usage($Config, $Request)) {
215
			/**
216
			 * Rebuilding HTML, JS and CSS cache if necessary
217
			 */
218 4
			(new Cache)->rebuild($Config, $L, $this->theme);
219 4
			$this->webcomponents_polyfill($Request, $Config, true);
220 4
			$languages_hash = md5(implode('', $Config->core['active_languages']));
221 4
			$language_hash  = file_get_json(PUBLIC_CACHE."/languages-$languages_hash.json")[$L->clanguage];
222 4
			$this->config_internal(
223
				[
224 4
					'language' => $L->clanguage,
225 4
					'hash'     => $language_hash
226
				],
227 4
				'cs.current_language',
228 4
				true
229
			);
230 4
			list($assets, $preload) = $this->get_assets_and_preload_resource_for_page_with_compression($Request);
231
		} else {
232 2
			$this->webcomponents_polyfill($Request, $Config, false);
233
			/**
234
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
235
			 */
236 2
			$this->config_internal($L, 'cs.Language', true);
237 2
			$this->config_internal(RequireJS::get_config(), 'requirejs', true);
238 2
			$assets  = $this->get_assets_for_page_without_compression($Config, $Request);
239 2
			$preload = [];
240
		}
241 4
		$this->core_css($assets['css']);
242 4
		$this->core_js($assets['js']);
243 4
		if (isset($assets['html'])) {
244 4
			$this->core_html($assets['html']);
245
		}
246 4
		$this->add_assets_on_page_manually_added($Config, $Request, $preload);
247 4
		return $this;
248
	}
249
	/**
250
	 * @param Config  $Config
251
	 * @param Request $Request
252
	 *
253
	 * @return bool
254
	 */
255 4
	protected function page_compression_usage ($Config, $Request) {
256 4
		return $Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']));
257
	}
258
	/**
259
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
260
	 *
261
	 * @param Request $Request
262
	 * @param Config  $Config
263
	 * @param bool    $with_compression
264
	 */
265 4
	protected function webcomponents_polyfill ($Request, $Config, $with_compression) {
266 4
		if (($this->theme != Config::SYSTEM_THEME && $Config->core['disable_webcomponents']) || $Request->cookie('shadow_dom_v1') == 1) {
267 2
			return;
268
		}
269 4
		if ($with_compression) {
270 4
			$hash = file_get_contents(PUBLIC_CACHE.'/webcomponents.js.hash');
271 4
			$this->add_script_imports_to_document($Config, "<script src=\"/storage/public_cache/$hash.js\"></script>\n");
272
		} else {
273 2
			$this->add_script_imports_to_document($Config, "<script src=\"/assets/js/WebComponents-polyfill/webcomponents-hi-sd-ce.min.js\"></script>\n");
274
		}
275 4
	}
276
	/**
277
	 * @param Config $Config
278
	 * @param string $content
279
	 */
280 4
	protected function add_script_imports_to_document ($Config, $content) {
281 4
		if ($Config->core['put_js_after_body']) {
282 4
			$this->post_Body .= $content;
283
		} else {
284 2
			$this->Head .= $content;
285
		}
286 4
	}
287
	/**
288
	 * @param Request $Request
289
	 *
290
	 * @return array[]
291
	 */
292 4
	protected function get_assets_and_preload_resource_for_page_with_compression ($Request) {
293 4
		list($dependencies, $compressed_assets_map, $not_embedded_resources_map) = file_get_json(PUBLIC_CACHE."/$this->theme.json");
294 4
		$assets  = $this->get_normalized_assets($dependencies, $compressed_assets_map, $Request);
295 4
		$preload = [];
296 4
		foreach (array_merge(...array_values($assets)) as $path) {
297 4
			$preload[] = [$path];
298 4
			if (isset($not_embedded_resources_map[$path])) {
299 4
				$preload[] = $not_embedded_resources_map[$path];
300
			}
301
		}
302 4
		return [$assets, array_merge(...$preload)];
303
	}
304
	/**
305
	 * @param array      $dependencies
306
	 * @param string[][] $assets_map
307
	 * @param Request    $Request
308
	 *
309
	 * @return string[][]
310
	 */
311 4
	protected function get_normalized_assets ($dependencies, $assets_map, $Request) {
312 4
		$current_module = $Request->current_module;
313
		/**
314
		 * Current URL based on controller path (it better represents how page was rendered)
315
		 */
316 4
		$current_url = array_slice(App::instance()->controller_path, 1);
317 4
		$current_url = ($Request->admin_path ? 'admin/' : '')."$current_module/".implode('/', $current_url);
318
		/**
319
		 * Narrow the dependencies to current module only
320
		 */
321 4
		$dependencies  = array_unique(
322
			array_merge(
323 4
				['System'],
324 4
				$dependencies['System'],
325 4
				isset($dependencies[$current_module]) ? $dependencies[$current_module] : []
326
			)
327
		);
328 4
		$system_assets = [];
329
		// Array with empty array in order to avoid `array_merge()` failure later
330 4
		$dependencies_assets = array_fill_keys($dependencies, [[]]);
331 4
		$assets              = [];
332 4
		foreach ($assets_map as $path => $local_assets) {
333 4
			if ($path == 'System') {
334 4
				$system_assets = $local_assets;
335 4
			} elseif ($component = $this->get_dependency_component($dependencies, $path, $Request)) {
336
				/**
337
				 * @var string $component
338
				 */
339 2
				$dependencies_assets[$component][] = $local_assets;
340 4
			} elseif (mb_strpos($current_url, $path) === 0) {
341 4
				$assets[] = $local_assets;
342
			}
343
		}
344
		// Convert to indexed array first
345 4
		$dependencies_assets = array_values($dependencies_assets);
346
		// Flatten array on higher level
347 4
		$dependencies_assets = array_merge(...$dependencies_assets);
348
		// Hack: 2 array_merge_recursive() just to be compatible with HHVM, simplify when https://github.com/facebook/hhvm/issues/7087 is resolved
349 4
		return _array(array_merge_recursive(array_merge_recursive($system_assets, ...$dependencies_assets), ...$assets));
350
	}
351
	/**
352
	 * @param array   $dependencies
353
	 * @param string  $url
354
	 * @param Request $Request
355
	 *
356
	 * @return false|string
357
	 */
358 4
	protected function get_dependency_component ($dependencies, $url, $Request) {
359 4
		$url_exploded = explode('/', $url);
360
		/** @noinspection NestedTernaryOperatorInspection */
361 4
		$url_component = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
362
		$is_dependency =
363 4
			$url_component !== Config::SYSTEM_MODULE &&
364 4
			in_array($url_component, $dependencies) &&
365
			(
366 4
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
367
			);
368 4
		return $is_dependency ? $url_component : false;
369
	}
370
	/**
371
	 * @param Config  $Config
372
	 * @param Request $Request
373
	 *
374
	 * @return string[][]
375
	 */
376 2
	protected function get_assets_for_page_without_compression ($Config, $Request) {
377
		// To determine all dependencies and stuff we need `$Config` object to be already created
378 2
		list($dependencies, $assets_map) = Collecting::get_assets_dependencies_and_map($Config, $this->theme);
379 2
		$assets = $this->get_normalized_assets($dependencies, $assets_map, $Request);
380 2
		return $this->add_versions_hash(_substr($assets, strlen(DIR)));
381
	}
382
	/**
383
	 * @param string[][] $assets
384
	 *
385
	 * @return string[][]
386
	 */
387 2
	protected function add_versions_hash ($assets) {
388 2
		$content      = array_reduce(
389 2
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
390
			function ($content, $file) {
391
				return $content.file_get_contents($file);
392 2
			}
393
		);
394 2
		$content_hash = substr(md5($content), 0, 5);
395 2
		foreach ($assets as &$files) {
396 2
			foreach ($files as &$file) {
397 2
				$file .= "?$content_hash";
398
			}
399 2
			unset($file);
400
		}
401 2
		return $assets;
402
	}
403
	/**
404
	 * @param Config   $Config
405
	 * @param Request  $Request
406
	 * @param string[] $preload
407
	 */
408 4
	protected function add_assets_on_page_manually_added ($Config, $Request, $preload) {
409
		/** @noinspection NestedTernaryOperatorInspection */
410 4
		$this->Head .= array_reduce(
411 4
			array_merge($this->core_css, $this->css),
412
			function ($content, $href) {
413 4
				return "$content<link href=\"$href\" rel=\"stylesheet\">\n";
414 4
			}
415
		);
416 4
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
417 4
			$this->add_assets_on_page_manually_added_frontend_load_optimization($Config, $Request);
418
		} else {
419 2
			$this->add_assets_on_page_manually_added_normal($Config, $Request, $preload);
420
		}
421 4
	}
422
	/**
423
	 * @param Config   $Config
424
	 * @param Request  $Request
425
	 * @param string[] $preload
426
	 */
427 2
	protected function add_assets_on_page_manually_added_normal ($Config, $Request, $preload) {
428 2
		$this->add_preload($preload, $Request);
429 2
		$configs      = $this->core_config.$this->config;
430 2
		$scripts      = array_reduce(
431 2
			array_merge($this->core_js, $this->js),
432
			function ($content, $src) {
433 2
				return "$content<script src=\"$src\"></script>\n";
434 2
			}
435
		);
436 2
		$html_imports = array_reduce(
437 2
			array_merge($this->core_html, $this->html),
438 2
			function ($content, $href) {
439 2
				return "$content<link href=\"$href\" rel=\"import\">\n";
440 2
			}
441
		);
442 2
		$this->Head   .= $configs;
443 2
		$this->add_script_imports_to_document($Config, $scripts.$html_imports);
444 2
	}
445
	/**
446
	 * @param string[] $preload
447
	 * @param Request  $Request
448
	 */
449 4
	protected function add_preload ($preload, $Request) {
450 4
		if ($Request->cookie('pushed')) {
451 2
			return;
452
		}
453 4
		$Response = Response::instance();
454 4
		$Response->cookie('pushed', 1, 0, true);
455 4
		foreach (array_unique($preload) as $resource) {
456 4
			$extension = explode('?', file_extension($resource))[0];
457 4
			$as        = $this->extension_to_as[$extension];
458 4
			$resource  = str_replace(' ', '%20', $resource);
459 4
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
460
		}
461 4
	}
462
	/**
463
	 * @param Config  $Config
464
	 * @param Request $Request
465
	 */
466 4
	protected function add_assets_on_page_manually_added_frontend_load_optimization ($Config, $Request) {
467 4
		list($optimized_assets, $preload) = file_get_json(PUBLIC_CACHE."/$this->theme.optimized.json");
468 4
		$this->add_preload(
469 4
			array_merge($preload, $this->core_css, $this->css),
470
			$Request
471
		);
472 4
		$optimized_assets  = array_flip($optimized_assets);
473 4
		$system_scripts    = '';
474 4
		$optimized_scripts = [];
475 4
		$system_imports    = '';
476 4
		$optimized_imports = [];
477 4
		foreach (array_merge($this->core_js, $this->js) as $script) {
478 4
			if (isset($optimized_assets[$script])) {
479 2
				$optimized_scripts[] = $script;
480
			} else {
481 4
				$system_scripts .= "<script src=\"$script\"></script>\n";
482
			}
483
		}
484 4
		foreach (array_merge($this->core_html, $this->html) as $import) {
485 4
			if (isset($optimized_assets[$import])) {
486 2
				$optimized_imports[] = $import;
487
			} else {
488 4
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
489
			}
490
		}
491 4
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_assets');
492 4
		$this->Head .= $this->core_config.$this->config;
493 4
		$this->add_script_imports_to_document($Config, $system_scripts.$system_imports);
494 4
	}
495
}
496