Completed
Push — master ( 4c24ff...8a852c )
by Nazar
04:10
created

Includes::css_internal()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 3
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\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
	/**
38
	 * @var array
39
	 */
40
	protected $core_html;
41
	/**
42
	 * @var array
43
	 */
44
	protected $core_js;
45
	/**
46
	 * @var array
47
	 */
48
	protected $core_css;
49
	/**
50
	 * @var string
51
	 */
52
	protected $core_config;
53
	/**
54
	 * @var array
55
	 */
56
	protected $html;
57
	/**
58
	 * @var array
59
	 */
60
	protected $js;
61
	/**
62
	 * @var array
63
	 */
64
	protected $css;
65
	/**
66
	 * @var string
67
	 */
68
	protected $config;
69
	/**
70
	 * Base name is used as prefix when creating CSS/JS/HTML cache files in order to avoid collisions when having several themes and languages
71
	 * @var string
72
	 */
73
	protected $pcache_basename_path;
74
	protected function init_includes () {
75
		$this->core_html            = ['path' => [], 'plain' => ''];
76
		$this->core_js              = ['path' => [], 'plain' => ''];
77
		$this->core_css             = ['path' => [], 'plain' => ''];
78
		$this->core_config          = '';
79
		$this->html                 = ['path' => [], 'plain' => ''];
80
		$this->js                   = ['path' => [], 'plain' => ''];
81
		$this->css                  = ['path' => [], 'plain' => ''];
82
		$this->config               = '';
83
		$this->pcache_basename_path = '';
84
	}
85
	/**
86
	 * Including of Web Components
87
	 *
88
	 * @param string|string[] $add  Path to including file, or code
89
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
90
	 *
91
	 * @return \cs\Page
92
	 */
93
	function html ($add, $mode = 'file') {
94
		return $this->html_internal($add, $mode);
95
	}
96
	/**
97
	 * @param string|string[] $add
98
	 * @param string          $mode
99
	 * @param bool            $core
100
	 *
101
	 * @return \cs\Page
102
	 */
103
	protected function html_internal ($add, $mode = 'file', $core = false) {
104
		return $this->include_common('html', $add, $mode, $core);
105
	}
106
	/**
107
	 * Including of JavaScript
108
	 *
109
	 * @param string|string[] $add  Path to including file, or code
110
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
111
	 *
112
	 * @return \cs\Page
113
	 */
114
	function js ($add, $mode = 'file') {
115
		return $this->js_internal($add, $mode);
116
	}
117
	/**
118
	 * @param string|string[] $add
119
	 * @param string          $mode
120
	 * @param bool            $core
121
	 *
122
	 * @return \cs\Page
123
	 */
124
	protected function js_internal ($add, $mode = 'file', $core = false) {
125
		return $this->include_common('js', $add, $mode, $core);
126
	}
127
	/**
128
	 * Including of CSS
129
	 *
130
	 * @param string|string[] $add  Path to including file, or code
131
	 * @param string          $mode Can be <b>file</b> or <b>code</b>
132
	 *
133
	 * @return \cs\Page
134
	 */
135
	function css ($add, $mode = 'file') {
136
		return $this->css_internal($add, $mode);
137
	}
138
	/**
139
	 * @param string|string[] $add
140
	 * @param string          $mode
141
	 * @param bool            $core
142
	 *
143
	 * @return \cs\Page
144
	 */
145
	protected function css_internal ($add, $mode = 'file', $core = false) {
146
		return $this->include_common('css', $add, $mode, $core);
147
	}
148
	/**
149
	 * @param string          $what
150
	 * @param string|string[] $add
151
	 * @param string          $mode
152
	 * @param bool            $core
153
	 *
154
	 * @return \cs\Page
155
	 */
156
	protected function include_common ($what, $add, $mode, $core) {
157
		if (!$add) {
158
			return $this;
159
		}
160
		if (is_array($add)) {
161
			foreach (array_filter($add) as $style) {
162
				$this->include_common($what, $style, $mode, $core);
163
			}
164
		} else {
165
			if ($core) {
166
				$what = "core_$what";
167
			}
168
			$target = &$this->$what;
169
			if ($mode == 'file') {
170
				$target['path'][] = $add;
171
			} elseif ($mode == 'code') {
172
				$target['plain'] .= "$add\n";
173
			}
174
		}
175
		return $this;
176
	}
177
	/**
178
	 * Add config on page to make it available on frontend
179
	 *
180
	 * @param mixed  $config_structure        Any scalar type or array
181
	 * @param string $target                  Target is property of `window` object where config will be inserted as value, nested properties like `cs.sub.prop`
182
	 *                                        are supported and all nested properties are created on demand. It is recommended to use sub-properties of `cs`
183
	 *
184
	 * @return \cs\Page
185
	 */
186
	function config ($config_structure, $target) {
187
		return $this->config_internal($config_structure, $target);
188
	}
189
	/**
190
	 * @param mixed  $config_structure
191
	 * @param string $target
192
	 * @param bool   $core
193
	 *
194
	 * @return \cs\Page
195
	 */
196
	protected function config_internal ($config_structure, $target, $core = false) {
197
		$config = h::script(
198
			json_encode($config_structure, JSON_UNESCAPED_UNICODE),
199
			[
200
				'target' => $target,
201
				'class'  => 'cs-config',
202
				'type'   => 'application/json'
203
			]
204
		);
205
		if ($core) {
206
			$this->core_config .= $config;
207
		} else {
208
			$this->config .= $config;
209
		}
210
		return $this;
211
	}
212
	/**
213
	 * Getting of HTML, JS and CSS includes
214
	 *
215
	 * @return \cs\Page
216
	 */
217
	protected function add_includes_on_page () {
218
		$Config = Config::instance(true);
219
		if (!$Config) {
220
			return $this;
221
		}
222
		/**
223
		 * Base name for cache files
224
		 */
225
		$this->pcache_basename_path = PUBLIC_CACHE.'/'.$this->theme.'_'.Language::instance()->clang;
226
		/**
227
		 * Some JS configs required by system
228
		 */
229
		$this->add_system_configs();
230
		// TODO: I hope some day we'll get rid of this sh*t :(
231
		$this->ie_edge();
232
		$Request = Request::instance();
233
		/**
234
		 * If CSS and JavaScript compression enabled
235
		 */
236
		if ($Config->core['cache_compress_js_css'] && !($Request->admin_path && isset($Request->query['debug']))) {
237
			$this->webcomponents_polyfill($Request, true);
238
			$includes = $this->get_includes_for_page_with_compression($Config);
239
		} else {
240
			$this->webcomponents_polyfill($Request, false);
241
			/**
242
			 * Language translation is added explicitly only when compression is disabled, otherwise it will be in compressed JS file
243
			 */
244
			$this->config_internal(Language::instance(), 'cs.Language', true);
245
			$this->config_internal($this->get_requirejs_paths(), 'requirejs.paths', true);
246
			$includes = $this->get_includes_for_page_without_compression($Config);
247
		}
248
		$this->css_internal($includes['css'], 'file', true);
249
		$this->js_internal($includes['js'], 'file', true);
250
		$this->html_internal($includes['html'], 'file', true);
251
		$this->add_includes_on_page_manually_added($Config);
252
		return $this;
253
	}
254
	/**
255
	 * @param string[]|string[][] $path
256
	 *
257
	 * @return string[]|string[][]
258
	 */
259
	protected function absolute_path_to_relative ($path) {
260
		return _substr($path, strlen(DIR) + 1);
261
	}
262
	/**
263
	 * Add JS polyfills for IE/Edge
264
	 */
265
	protected function ie_edge () {
266
		if (!preg_match('/Trident|Edge/', Request::instance()->header('user-agent'))) {
267
			return;
268
		}
269
		$this->js_internal(
270
			get_files_list(DIR."/includes/js/microsoft_sh*t", "/.*\\.js$/i", 'f', "includes/js/microsoft_sh*t", true),
271
			'file',
272
			true
273
		);
274
	}
275
	/**
276
	 * Hack: Add WebComponents Polyfill for browsers without native Shadow DOM support
277
	 *
278
	 * @param Request $Request
279
	 * @param bool    $with_compression
280
	 */
281
	protected function webcomponents_polyfill ($Request, $with_compression) {
282
		if ($Request->cookie('shadow_dom') == 1) {
283
			return;
284
		}
285
		$file = 'includes/js/WebComponents-polyfill/webcomponents-custom.min.js';
286
		if ($with_compression) {
287
			$compressed_file = PUBLIC_CACHE.'/webcomponents.js';
288
			if (!file_exists($compressed_file)) {
289
				$content = file_get_contents(DIR."/$file");
290
				file_put_contents($compressed_file, gzencode($content, 9), LOCK_EX | FILE_BINARY);
291
				file_put_contents("$compressed_file.hash", substr(md5($content), 0, 5));
292
			}
293
			$hash = file_get_contents("$compressed_file.hash");
294
			$this->js_internal("storage/pcache/webcomponents.js?$hash", 'file', true);
295
		} else {
296
			$this->js_internal($file, 'file', true);
297
		}
298
	}
299
	protected function add_system_configs () {
300
		$Config         = Config::instance();
301
		$Request        = Request::instance();
302
		$User           = User::instance();
303
		$current_module = $Request->current_module;
304
		$this->config_internal(
305
			[
306
				'base_url'              => $Config->base_url(),
307
				'current_base_url'      => $Config->base_url().'/'.($Request->admin_path ? 'admin/' : '').$current_module,
308
				'public_key'            => Core::instance()->public_key,
309
				'module'                => $current_module,
310
				'in_admin'              => (int)$Request->admin_path,
311
				'is_admin'              => (int)$User->admin(),
312
				'is_user'               => (int)$User->user(),
313
				'is_guest'              => (int)$User->guest(),
314
				'password_min_length'   => (int)$Config->core['password_min_length'],
315
				'password_min_strength' => (int)$Config->core['password_min_strength'],
316
				'debug'                 => (int)DEBUG,
317
				'route'                 => $Request->route,
318
				'route_path'            => $Request->route_path,
319
				'route_ids'             => $Request->route_ids
320
			],
321
			'cs',
322
			true
323
		);
324
		if ($User->admin()) {
325
			$this->config_internal((int)$Config->core['simple_admin_mode'], 'cs.simple_admin_mode', true);
326
		}
327
	}
328
	/**
329
	 * @param Config $Config
330
	 *
331
	 * @return string[][]
332
	 */
333
	protected function get_includes_for_page_with_compression ($Config) {
334
		/**
335
		 * Rebuilding HTML, JS and CSS cache if necessary
336
		 */
337
		if (!file_exists("$this->pcache_basename_path.json")) {
338
			list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
339
			$compressed_includes_map = [];
340
			foreach ($includes_map as $filename_prefix => $local_includes) {
341
				// We replace `/` by `+` to make it suitable for filename
342
				$filename_prefix                           = str_replace('/', '+', $filename_prefix);
343
				$compressed_includes_map[$filename_prefix] = $this->cache_compressed_includes_files(
344
					"$this->pcache_basename_path:$filename_prefix",
345
					$local_includes,
346
					$Config->core['vulcanization']
347
				);
348
			}
349
			unset($includes_map, $filename_prefix, $local_includes);
350
			file_put_json("$this->pcache_basename_path.json", [$dependencies, $compressed_includes_map]);
351
			Event::instance()->fire('System/Page/rebuild_cache');
352
		}
353
		list($dependencies, $compressed_includes_map) = file_get_json("$this->pcache_basename_path.json");
354
		return $this->get_normalized_includes($dependencies, $compressed_includes_map, '+');
355
	}
356
	/**
357
	 * @param array      $dependencies
358
	 * @param string[][] $includes_map
359
	 * @param string     $separator `+` or `/`
360
	 *
361
	 * @return array
362
	 */
363
	protected function get_normalized_includes ($dependencies, $includes_map, $separator) {
364
		$Request        = Request::instance();
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$separator" : '')."$current_module$separator".implode($separator, $current_url);
371
		/**
372
		 * Narrow the dependencies to current module only
373
		 */
374
		$dependencies          = array_merge(
375
			isset($dependencies[$current_module]) ? $dependencies[$current_module] : [],
376
			$dependencies['System']
377
		);
378
		$system_includes       = [];
379
		$dependencies_includes = [];
380
		$includes              = [];
381
		foreach ($includes_map as $path => $local_includes) {
382
			if ($path == 'System') {
383
				$system_includes = $local_includes;
384
			} elseif ($this->is_dependency($dependencies, $path, '/')) {
385
				$dependencies_includes[] = $local_includes;
386
			} elseif (mb_strpos($current_url, $path) === 0) {
387
				$includes[] = $local_includes;
388
			}
389
		}
390
		return array_merge_recursive($system_includes, ...$dependencies_includes, ...$includes);
391
	}
392
	/**
393
	 * @param array  $dependencies
394
	 * @param string $url
395
	 * @param string $separator `+` or `/`
396
	 *
397
	 * @return bool
398
	 */
399
	protected function is_dependency ($dependencies, $url, $separator) {
400
		$url_exploded = explode($separator, $url);
401
		/** @noinspection NestedTernaryOperatorInspection */
402
		$url_module = $url_exploded[0] != 'admin' ? $url_exploded[0] : (@$url_exploded[1] ?: '');
403
		$Request    = Request::instance();
404
		return
405
			$url_module !== Config::SYSTEM_MODULE &&
406
			in_array($url_module, $dependencies) &&
407
			(
408
				$Request->admin_path || $Request->admin_path == ($url_exploded[0] == 'admin')
409
			);
410
	}
411
	/**
412
	 * @param Config $Config
413
	 *
414
	 * @return string[][]
415
	 */
416
	protected function get_includes_for_page_without_compression ($Config) {
417
		// To determine all dependencies and stuff we need `$Config` object to be already created
418
		list($dependencies, $includes_map) = $this->get_includes_dependencies_and_map($Config);
419
		$includes = $this->get_normalized_includes($dependencies, $includes_map, '/');
420
		return $this->add_versions_hash($this->absolute_path_to_relative($includes));
421
	}
422
	/**
423
	 * @param string[][] $includes
424
	 *
425
	 * @return string[][]
426
	 */
427
	protected function add_versions_hash ($includes) {
428
		$content     = array_reduce(
429
			get_files_list(DIR.'/components', '/^meta\.json$/', 'f', true, true),
430
			function ($content, $file) {
431
				return $content.file_get_contents($file);
432
			}
433
		);
434
		$content_md5 = substr(md5($content), 0, 5);
435
		foreach ($includes as &$files) {
436
			foreach ($files as &$file) {
437
				$file .= "?$content_md5";
438
			}
439
			unset($file);
440
		}
441
		return $includes;
442
	}
443
	/**
444
	 * @param Config $Config
445
	 */
446
	protected function add_includes_on_page_manually_added ($Config) {
447
		$configs = $this->core_config.$this->config;
448
		/** @noinspection NestedTernaryOperatorInspection */
449
		$styles =
450
			array_reduce(
451
				array_merge($this->core_css['path'], $this->css['path']),
452
				function ($content, $href) {
453
					return "$content<link href=\"/$href\" rel=\"stylesheet\" shim-shadowdom>\n";
454
				}
455
			).
456
			h::style($this->core_css['plain'].$this->css['plain'] ?: false);
457
		/** @noinspection NestedTernaryOperatorInspection */
458
		$scripts      =
459
			array_reduce(
460
				array_merge($this->core_js['path'], $this->js['path']),
461
				function ($content, $src) {
462
					return "$content<script src=\"/$src\"></script>\n";
463
				}
464
			).
465
			h::script($this->core_js['plain'].$this->js['plain'] ?: false);
466
		$html_imports =
467
			array_reduce(
468
				array_merge($this->core_html['path'], $this->html['path']),
469
				function ($content, $href) {
470
					return "$content<link href=\"/$href\" rel=\"import\">\n";
471
				}
472
			).
473
			$this->core_html['plain'].$this->html['plain'];
474
		$this->Head .= $configs.$styles;
475
		if ($Config->core['put_js_after_body']) {
476
			$this->post_Body .= $scripts.$html_imports;
477
		} else {
478
			$this->Head .= $scripts.$html_imports;
479
		}
480
	}
481
}
482