Completed
Push — master ( 7f5b44...b3ef7a )
by Nazar
04:15
created

Assets::get_hash_of()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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\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
		'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 34
	protected function init_assets () {
78 34
		$this->core_html   = [];
79 34
		$this->core_js     = [];
80 34
		$this->core_css    = [];
81 34
		$this->core_config = '';
82 34
		$this->html        = [];
83 34
		$this->js          = [];
84 34
		$this->css         = [];
85 34
		$this->config      = '';
86 34
	}
87
	/**
88
	 * @param string|string[] $add
89
	 *
90
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
91
	 */
92 4
	protected function core_html ($add) {
93 4
		return $this->include_common('html', $add, true);
94
	}
95
	/**
96
	 * @param string|string[] $add
97
	 *
98
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
99
	 */
100 4
	protected function core_js ($add) {
101 4
		return $this->include_common('js', $add, true);
102
	}
103
	/**
104
	 * @param string|string[] $add
105
	 *
106
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
107
	 */
108 4
	protected function core_css ($add) {
109 4
		return $this->include_common('css', $add, true);
110
	}
111
	/**
112
	 * Including of Web Components
113
	 *
114
	 * @param string|string[] $add Path to including file, or code
115
	 *
116
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
117
	 */
118 2
	public function html ($add) {
119 2
		return $this->include_common('html', $add, false);
120
	}
121
	/**
122
	 * Including of JavaScript
123
	 *
124
	 * @param string|string[] $add Path to including file, or code
125
	 *
126
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
127
	 */
128 2
	public function js ($add) {
129 2
		return $this->include_common('js', $add, false);
130
	}
131
	/**
132
	 * Including of CSS
133
	 *
134
	 * @param string|string[] $add Path to including file, or code
135
	 *
136
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
137
	 */
138 2
	public function css ($add) {
139 2
		return $this->include_common('css', $add, false);
140
	}
141
	/**
142
	 * @param string          $what
143
	 * @param string|string[] $add
144
	 * @param bool            $core
145
	 *
146
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
147
	 */
148 4
	protected function include_common ($what, $add, $core) {
149 4
		if (!$add) {
150 2
			return $this;
151
		}
152 4
		if (is_array($add)) {
153 4
			foreach (array_filter($add) as $a) {
154 4
				$this->include_common($what, $a, $core);
155
			}
156
		} else {
157 4
			if ($core) {
158 4
				$what = "core_$what";
159
			}
160 4
			$target   = &$this->$what;
161 4
			$target[] = $add;
162
		}
163 4
		return $this;
164
	}
165
	/**
166
	 * Add config on page to make it available on frontend
167
	 *
168
	 * @param mixed  $config_structure        Any scalar type or array
169
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
170
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
171
	 *
172
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
173
	 */
174 4
	public function config ($config_structure, $target) {
175 4
		return $this->config_internal($config_structure, $target);
176
	}
177
	/**
178
	 * @param mixed  $config_structure
179
	 * @param string $target
180
	 * @param bool   $core
181
	 *
182
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
183
	 */
184 4
	protected function config_internal ($config_structure, $target, $core = false) {
185 4
		$config = h::script(
186 4
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
187
			[
188 4
				'target' => $target,
189 4
				'class'  => 'cs-config',
190 4
				'type'   => 'application/json'
191
			]
192
		);
193 4
		if ($core) {
194 4
			$this->core_config .= $config;
195
		} else {
196 4
			$this->config .= $config;
197
		}
198 4
		return $this;
199
	}
200
	/**
201
	 * Getting of HTML, JS and CSS assets
202
	 *
203
	 * @return \cs\Page
0 ignored issues
show
Documentation introduced by
Should the return type not be Assets?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
204
	 */
205 4
	protected function add_assets_on_page () {
206 4
		$Config = Config::instance(true);
207 4
		if (!$Config) {
208 2
			return $this;
209
		}
210
		// TODO: I hope some day we'll get rid of this sh*t :(
211 4
		$this->edge();
212 4
		$Request = Request::instance();
213
		/**
214
		 * If CSS and JavaScript compression enabled
215
		 */
216 4
		$L = Language::instance();
217 4
		if ($this->page_compression_usage($Config, $Request)) {
218
			/**
219
			 * Rebuilding HTML, JS and CSS cache if necessary
220
			 */
221 4
			(new Cache)->rebuild($Config, $L, $this->theme);
222 4
			$this->webcomponents_polyfill($Request, $Config, true);
223 4
			$languages_hash = md5(implode('', $Config->core['active_languages']));
224 4
			$language_hash  = file_get_json(PUBLIC_CACHE."/languages-$languages_hash.json")[$L->clanguage];
225 4
			$this->config_internal(
226
				[
227 4
					'language' => $L->clanguage,
228 4
					'hash'     => $language_hash
229
				],
230 4
				'cs.current_language',
231 4
				true
232
			);
233 4
			list($assets, $preload) = $this->get_assets_and_preload_resource_for_page_with_compression($Request);
234
		} else {
235 2
			$this->webcomponents_polyfill($Request, $Config, false);
236
			/**
237
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
238
			 */
239 2
			$this->config_internal($L, 'cs.Language', true);
240 2
			$this->config_internal(RequireJS::get_config(), 'requirejs', true);
241 2
			$assets  = $this->get_assets_for_page_without_compression($Config, $Request);
242 2
			$preload = [];
243
		}
244 4
		$this->core_css($assets['css']);
245 4
		$this->core_js($assets['js']);
246 4
		if (isset($assets['html'])) {
247 4
			$this->core_html($assets['html']);
248
		}
249 4
		$this->add_assets_on_page_manually_added($Config, $Request, $preload);
250 4
		return $this;
251
	}
252
	/**
253
	 * @param Config  $Config
254
	 * @param Request $Request
255
	 *
256
	 * @return bool
257
	 */
258 4
	protected function page_compression_usage ($Config, $Request) {
259 4
		return $Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']));
260
	}
261
	/**
262
	 * Add JS polyfills for IE/Edge
263
	 */
264 4
	protected function edge () {
265 4
		if (strpos(Request::instance()->header('user-agent'), 'Edge') === false) {
266 4
			return;
267
		}
268 2
		$this->core_js(
269 2
			get_files_list(DIR.'/assets/js/microsoft_sh*t', '/.*\.js$/i', 'f', 'assets/js/microsoft_sh*t', true)
270
		);
271 2
	}
272
	/**
273
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
274
	 *
275
	 * @param Request $Request
276
	 * @param Config  $Config
277
	 * @param bool    $with_compression
278
	 */
279 4
	protected function webcomponents_polyfill ($Request, $Config, $with_compression) {
280 4
		if (($this->theme != Config::SYSTEM_THEME && $Config->core['disable_webcomponents']) || $Request->cookie('shadow_dom') == 1) {
281 2
			return;
282
		}
283 4
		if ($with_compression) {
284 4
			$hash = file_get_contents(PUBLIC_CACHE.'/webcomponents.js.hash');
285 4
			$this->add_script_imports_to_document($Config, "<script src=\"/storage/public_cache/$hash.js\"></script>\n");
286
		} else {
287 2
			$this->add_script_imports_to_document($Config, "<script src=\"/assets/js/WebComponents-polyfill/webcomponents-custom.min.js\"></script>\n");
288
		}
289 4
	}
290
	/**
291
	 * @param Config $Config
292
	 * @param string $content
293
	 */
294 4
	protected function add_script_imports_to_document ($Config, $content) {
295 4
		if ($Config->core['put_js_after_body']) {
296 4
			$this->post_Body .= $content;
297
		} else {
298 2
			$this->Head .= $content;
299
		}
300 4
	}
301
	/**
302
	 * @param Request $Request
303
	 *
304
	 * @return array[]
305
	 */
306 4
	protected function get_assets_and_preload_resource_for_page_with_compression ($Request) {
307 4
		list($dependencies, $compressed_assets_map, $not_embedded_resources_map) = file_get_json(PUBLIC_CACHE."/$this->theme.json");
308 4
		$assets  = $this->get_normalized_assets($dependencies, $compressed_assets_map, $Request);
309 4
		$preload = [];
310 4
		foreach (array_merge(...array_values($assets)) as $path) {
311 4
			$preload[] = [$path];
312 4
			if (isset($not_embedded_resources_map[$path])) {
313 4
				$preload[] = $not_embedded_resources_map[$path];
314
			}
315
		}
316 4
		return [$assets, array_merge(...$preload)];
317
	}
318
	/**
319
	 * @param array      $dependencies
320
	 * @param string[][] $assets_map
321
	 * @param Request    $Request
322
	 *
323
	 * @return string[][]
324
	 */
325 4
	protected function get_normalized_assets ($dependencies, $assets_map, $Request) {
326 4
		$current_module = $Request->current_module;
327
		/**
328
		 * Current URL based on controller path (it better represents how page was rendered)
329
		 */
330 4
		$current_url = array_slice(App::instance()->controller_path, 1);
331 4
		$current_url = ($Request->admin_path ? 'admin/' : '')."$current_module/".implode('/', $current_url);
332
		/**
333
		 * Narrow the dependencies to current module only
334
		 */
335 4
		$dependencies  = array_unique(
336
			array_merge(
337 4
				['System'],
338 4
				$dependencies['System'],
339 4
				isset($dependencies[$current_module]) ? $dependencies[$current_module] : []
340
			)
341
		);
342 4
		$system_assets = [];
343
		// Array with empty array in order to avoid `array_merge()` failure later
344 4
		$dependencies_assets = array_fill_keys($dependencies, [[]]);
345 4
		$assets              = [];
346 4
		foreach ($assets_map as $path => $local_assets) {
347 4
			if ($path == 'System') {
348 4
				$system_assets = $local_assets;
349 4
			} elseif ($component = $this->get_dependency_component($dependencies, $path, $Request)) {
350
				/**
351
				 * @var string $component
352
				 */
353 2
				$dependencies_assets[$component][] = $local_assets;
354 4
			} elseif (mb_strpos($current_url, $path) === 0) {
355 4
				$assets[] = $local_assets;
356
			}
357
		}
358
		// Convert to indexed array first
359 4
		$dependencies_assets = array_values($dependencies_assets);
360
		// Flatten array on higher level
361 4
		$dependencies_assets = array_merge(...$dependencies_assets);
362
		// Hack: 2 array_merge_recursive() just to be compatible with HHVM, simplify when https://github.com/facebook/hhvm/issues/7087 is resolved
363 4
		return _array(array_merge_recursive(array_merge_recursive($system_assets, ...$dependencies_assets), ...$assets));
364
	}
365
	/**
366
	 * @param array   $dependencies
367
	 * @param string  $url
368
	 * @param Request $Request
369
	 *
370
	 * @return false|string
371
	 */
372 4
	protected function get_dependency_component ($dependencies, $url, $Request) {
373 4
		$url_exploded = explode('/', $url);
374
		/** @noinspection NestedTernaryOperatorInspection */
375 4
		$url_component = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
376
		$is_dependency =
377 4
			$url_component !== Config::SYSTEM_MODULE &&
378 4
			in_array($url_component, $dependencies) &&
379
			(
380 4
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
381
			);
382 4
		return $is_dependency ? $url_component : false;
383
	}
384
	/**
385
	 * @param Config  $Config
386
	 * @param Request $Request
387
	 *
388
	 * @return string[][]
389
	 */
390 2
	protected function get_assets_for_page_without_compression ($Config, $Request) {
391
		// To determine all dependencies and stuff we need `$Config` object to be already created
392 2
		list($dependencies, $assets_map) = Collecting::get_assets_dependencies_and_map($Config, $this->theme);
393 2
		$assets = $this->get_normalized_assets($dependencies, $assets_map, $Request);
394 2
		return $this->add_versions_hash(_substr($assets, strlen(DIR)));
395
	}
396
	/**
397
	 * @param string[][] $assets
398
	 *
399
	 * @return string[][]
400
	 */
401 2
	protected function add_versions_hash ($assets) {
402 2
		$content      = array_reduce(
403 2
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
404
			function ($content, $file) {
405
				return $content.file_get_contents($file);
406 2
			}
407
		);
408 2
		$content_hash = substr(md5($content), 0, 5);
409 2
		foreach ($assets as &$files) {
410 2
			foreach ($files as &$file) {
411 2
				$file .= "?$content_hash";
412
			}
413 2
			unset($file);
414
		}
415 2
		return $assets;
416
	}
417
	/**
418
	 * @param Config   $Config
419
	 * @param Request  $Request
420
	 * @param string[] $preload
421
	 */
422 4
	protected function add_assets_on_page_manually_added ($Config, $Request, $preload) {
423
		/** @noinspection NestedTernaryOperatorInspection */
424 4
		$this->Head .= array_reduce(
425 4
			array_merge($this->core_css, $this->css),
426
			function ($content, $href) {
427 4
				return "$content<link href=\"$href\" rel=\"stylesheet\">\n";
428 4
			}
429
		);
430 4
		if ($this->page_compression_usage($Config, $Request) && $Config->core['frontend_load_optimization']) {
431 4
			$this->add_assets_on_page_manually_added_frontend_load_optimization($Config, $Request);
432
		} else {
433 2
			$this->add_assets_on_page_manually_added_normal($Config, $Request, $preload);
434
		}
435 4
	}
436
	/**
437
	 * @param Config   $Config
438
	 * @param Request  $Request
439
	 * @param string[] $preload
440
	 */
441 2
	protected function add_assets_on_page_manually_added_normal ($Config, $Request, $preload) {
442 2
		$this->add_preload($preload, $Request);
443 2
		$configs      = $this->core_config.$this->config;
444 2
		$scripts      = array_reduce(
445 2
			array_merge($this->core_js, $this->js),
446
			function ($content, $src) {
447 2
				return "$content<script src=\"$src\"></script>\n";
448 2
			}
449
		);
450 2
		$html_imports = array_reduce(
451 2
			array_merge($this->core_html, $this->html),
452 2
			function ($content, $href) {
453 2
				return "$content<link href=\"$href\" rel=\"import\">\n";
454 2
			}
455
		);
456 2
		$this->Head .= $configs;
457 2
		$this->add_script_imports_to_document($Config, $scripts.$html_imports);
458 2
	}
459
	/**
460
	 * @param string[] $preload
461
	 * @param Request  $Request
462
	 */
463 4
	protected function add_preload ($preload, $Request) {
464 4
		if ($Request->cookie('pushed')) {
465 2
			return;
466
		}
467 4
		$Response = Response::instance();
468 4
		$Response->cookie('pushed', 1, 0, true);
469 4
		foreach (array_unique($preload) as $resource) {
470 4
			$extension = explode('?', file_extension($resource))[0];
471 4
			$as        = $this->extension_to_as[$extension];
472 4
			$resource  = str_replace(' ', '%20', $resource);
473 4
			$Response->header('Link', "<$resource>; rel=preload; as=$as", false);
474
		}
475 4
	}
476
	/**
477
	 * @param Config  $Config
478
	 * @param Request $Request
479
	 */
480 4
	protected function add_assets_on_page_manually_added_frontend_load_optimization ($Config, $Request) {
481 4
		list($optimized_assets, $preload) = file_get_json(PUBLIC_CACHE."/$this->theme.optimized.json");
482 4
		$this->add_preload(
483 4
			array_merge($preload, $this->core_css, $this->css),
484
			$Request
485
		);
486 4
		$optimized_assets  = array_flip($optimized_assets);
487 4
		$system_scripts    = '';
488 4
		$optimized_scripts = [];
489 4
		$system_imports    = '';
490 4
		$optimized_imports = [];
491 4
		foreach (array_merge($this->core_js, $this->js) as $script) {
492 4
			if (isset($optimized_assets[$script])) {
493 2
				$optimized_scripts[] = $script;
494
			} else {
495 4
				$system_scripts .= "<script src=\"$script\"></script>\n";
496
			}
497
		}
498 4
		foreach (array_merge($this->core_html, $this->html) as $import) {
499 4
			if (isset($optimized_assets[$import])) {
500 2
				$optimized_imports[] = $import;
501
			} else {
502 4
				$system_imports .= "<link href=\"$import\" rel=\"import\">\n";
503
			}
504
		}
505 4
		$this->config([$optimized_scripts, $optimized_imports], 'cs.optimized_assets');
506 4
		$this->Head .= $this->core_config.$this->config;
507 4
		$this->add_script_imports_to_document($Config, $system_scripts.$system_imports);
508 4
	}
509
}
510