Completed
Push — master ( 2dbb1e...a190f2 )
by Nazar
03:57
created

Page::error()   C

Complexity

Conditions 8
Paths 5

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8.7021

Importance

Changes 0
Metric Value
cc 8
eloc 18
nc 5
nop 2
dl 0
loc 27
ccs 14
cts 18
cp 0.7778
crap 8.7021
rs 5.3846
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package   CleverStyle Framework
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2011-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs;
9
use
10
	h,
11
	cs\Page\Includes,
12
	cs\Page\Meta;
13
use function
14
	cli\err;
15
16
/**
17
 * Provides next events:
18
 *  System/Page/includes_dependencies_and_map
19
 *  [
20
 *    'dependencies' => &$dependencies,
21
 *    'includes_map' => &$includes_map
22
 *  ]
23
 *
24
 *  System/Page/rebuild_cache
25
 *
26
 *  System/Page/requirejs
27
 *  [
28
 *    'paths'                 => &$paths,                // The same as `paths` in requirejs.config()
29
 *    'directories_to_browse' => &$directories_to_browse // Location of AMD modules (typically bower_components and node_modules directories, absolute paths)
30
 *  ]
31
 *
32
 * @method static $this instance($check = false)
33
 */
34
class Page {
35
	use
36
		Singleton,
37
		Includes;
38
	const INIT_STATE_METHOD = 'init';
39
	/**
40
	 * Complete page contents
41
	 *
42
	 * @var string
43
	 */
44
	public $Content;
45
	/**
46
	 * If `false` - only page content will be shown, without the rest of interface (useful for AJAX request, though for API it is set to `false` automatically)
47
	 *
48
	 * @var bool
49
	 */
50
	public $interface;
51
	/**
52
	 * @var string
53
	 */
54
	public $pre_Html;
55
	/**
56
	 * @var string
57
	 */
58
	public $Html;
59
	/**
60
	 * @var string
61
	 */
62
	public $Description;
63
	/**
64
	 * @var string|string[]
65
	 */
66
	public $Title;
67
	/**
68
	 * @var string
69
	 */
70
	public $Head;
71
	/**
72
	 * @var string
73
	 */
74
	public $pre_Body;
75
	/**
76
	 * @var string
77
	 */
78
	public $Left;
79
	/**
80
	 * @var string
81
	 */
82
	public $Top;
83
	/**
84
	 * @var string
85
	 */
86
	public $Right;
87
	/**
88
	 * @var string
89
	 */
90
	public $Bottom;
91
	/**
92
	 * @var string
93
	 */
94
	public $post_Body;
95
	/**
96
	 * @var string
97
	 */
98
	public $post_Html;
99
	/**
100
	 * Number of tabs by default for indentation the substitution of values into template
101
	 *
102
	 * @var array
103
	 */
104
	public $level;
105
	/**
106
	 * @var array[]
107
	 */
108
	protected $link;
109
	/**
110
	 * @var string[]
111
	 */
112
	protected $search_replace;
113
	/**
114
	 * @var false|string
115
	 */
116
	protected $canonical_url;
117
	protected $theme;
118
	/**
119
	 * @param string $property
120
	 *
121
	 * @return false|null|string
122
	 */
123
	public function __get ($property) {
124
		// Hack: for internal use by \cs\Meta class
125
		if ($property === 'canonical_url') {
126
			return $this->canonical_url;
127
		}
128
		return false;
129
	}
130 34
	protected function init () {
131 34
		$this->Content        = '';
132 34
		$this->interface      = true;
133 34
		$this->pre_Html       = '';
134 34
		$this->Html           = '';
135 34
		$this->Description    = '';
136 34
		$this->Title          = [];
137 34
		$this->Head           = '';
138 34
		$this->pre_Body       = '';
139 34
		$this->Left           = '';
140 34
		$this->Top            = '';
141 34
		$this->Right          = '';
142 34
		$this->Bottom         = '';
143 34
		$this->post_Body      = '';
144 34
		$this->post_Html      = '';
145 34
		$this->level          = [
146
			'Head'      => 0,
147
			'pre_Body'  => 1,
148
			'Left'      => 3,
149
			'Top'       => 3,
150
			'Content'   => 4,
151
			'Bottom'    => 3,
152
			'Right'     => 3,
153
			'post_Body' => 1
154
		];
155 34
		$this->link           = [];
156 34
		$this->search_replace = [];
157 34
		$this->canonical_url  = false;
158 34
		$this->theme          = null;
159 34
		$this->init_includes();
160 34
		$Config = Config::instance(true);
161
		/**
162
		 * We need Config for initialization
163
		 */
164 34
		if (!$Config) {
165 2
			Event::instance()->once(
166 2
				'System/Config/init/after',
167
				function () {
168
					$this->theme = Config::instance()->core['theme'];
169 2
				}
170
			);
171
		} else {
172 32
			$this->theme = Config::instance()->core['theme'];
173
		}
174 34
		Event::instance()->on(
175 34
			'System/Config/changed',
176 34
			function () {
177
				$this->theme = Config::instance()->core['theme'];
178 34
			}
179
		);
180 34
	}
181
	/**
182
	 * Adding of content on the page
183
	 *
184
	 * @param string   $add
185
	 * @param bool|int $level
186
	 *
187
	 * @return Page
188
	 */
189
	public function content ($add, $level = false) {
190
		if ($level !== false) {
191
			$this->Content .= h::level($add, $level);
192
		} else {
193
			$this->Content .= $add;
194
		}
195
		return $this;
196
	}
197
	/**
198
	 * Sets body with content, that is transformed into JSON format
199
	 *
200
	 * @param mixed $content
201
	 *
202
	 * @return Page
203
	 */
204 20
	public function json ($content) {
205 20
		Response::instance()->header('content-type', 'application/json; charset=utf-8');
206 20
		$this->interface = false;
207 20
		$this->Content   = _json_encode($content);
208 20
		return $this;
209
	}
210
	/**
211
	 * Loading of theme template
212
	 */
213 2
	protected function get_template () {
214
		/**
215
		 * Theme is fixed for administration, and may vary for other pages
216
		 */
217 2
		if (Request::instance()->admin_path) {
218
			$this->theme = 'CleverStyle';
219
		}
220 2
		ob_start();
221 2
		$theme_dir = THEMES."/$this->theme";
222 2
		_include("$theme_dir/index.php", false, false) || _include("$theme_dir/index.html");
223 2
		$this->Html = ob_get_clean();
224 2
	}
225
	/**
226
	 * Processing of template, substituting of content, preparing for the output
227
	 *
228
	 * @return Page
229
	 */
230 2
	protected function prepare () {
231 2
		$Config = Config::instance(true);
232
		/**
233
		 * Loading of template
234
		 */
235 2
		$this->get_template();
236
		/**
237
		 * Forming page title
238
		 */
239 2
		$this->Title = array_filter($this->Title, 'trim');
240 2
		array_unshift($this->Title, $Config->core['site_name']);
241 2
		$this->Title = $Config->core['title_reverse'] ? array_reverse($this->Title) : $this->Title;
242 2
		$this->Title = implode($Config->core['title_delimiter'] ?: '|', $this->Title);
243
		/**
244
		 * Addition of CSS, JavaScript and Web Components includes
245
		 */
246 2
		$this->add_includes_on_page();
247
		/**
248
		 * Forming <head> content
249
		 */
250 2
		$this->Head =
251 2
			h::title($this->Title).
252 2
			h::meta(
253
				[
254
					'charset' => 'utf-8'
255 2
				]
256
			).
257 2
			h::meta(
258 2
				$this->Description ? [
259
					'name'    => 'description',
260
					'content' => $this->Description
261 2
				] : false
262
			).
263 2
			h::meta(
264
				[
265 2
					'name'    => 'generator',
266
					'content' => 'CleverStyle Framework by Mokrynskyi Nazar'
267
				]
268
			).
269 2
			h::base(
270 2
				$Config ? [
271 2
					'href' => $Config->base_url().'/'
272 2
				] : false
273
			).
274 2
			$this->Head.
275 2
			h::link(
276
				[
277 2
					'rel'  => 'shortcut icon',
278 2
					'href' => $this->get_favicon_path()
279
				]
280
			).
281 2
			h::link(array_values($this->link) ?: false);
282
		/**
283
		 * Generation of Open Graph protocol information
284
		 */
285 2
		Meta::instance()->render();
286
		/**
287
		 * Substitution of information into template
288
		 */
289 2
		$this->Html = str_replace(
290
			[
291 2
				'<!--pre_Html-->',
292
				'<!--head-->',
293
				'<!--pre_Body-->',
294
				'<!--left_blocks-->',
295
				'<!--top_blocks-->',
296
				'<!--content-->',
297
				'<!--bottom_blocks-->',
298
				'<!--right_blocks-->',
299
				'<!--post_Body-->',
300
				'<!--post_Html-->'
301
			],
302
			_rtrim(
303
				[
304 2
					$this->pre_Html,
305 2
					$this->get_property_with_indentation('Head'),
306 2
					$this->get_property_with_indentation('pre_Body'),
307 2
					$this->get_property_with_indentation('Left'),
308 2
					$this->get_property_with_indentation('Top'),
309 2
					$this->get_property_with_indentation('Content'),
310 2
					$this->get_property_with_indentation('Bottom'),
311 2
					$this->get_property_with_indentation('Right'),
312 2
					$this->get_property_with_indentation('post_Body'),
313 2
					$this->post_Html
314
				],
315 2
				"\t"
316
			),
317 2
			$this->Html
318
		);
319 2
		return $this;
320
	}
321
	/**
322
	 * @return string
323
	 */
324 2
	protected function get_favicon_path () {
325 2
		$file = file_exists_with_extension(THEMES."/$this->theme/img/favicon", ['png', 'ico']);
326 2
		if ($file) {
327
			return str_replace(THEMES, 'themes', $file);
328
		}
329 2
		return str_replace(DIR.'/', '', file_exists_with_extension(DIR.'/favicon', ['png', 'ico']));
330
	}
331
	/**
332
	 * @param string $property
333
	 *
334
	 * @return string
335
	 */
336 2
	protected function get_property_with_indentation ($property) {
337 2
		return h::level($this->$property, $this->level[$property]);
338
	}
339
	/**
340
	 * Replacing anything in source code of finally generated page
341
	 *
342
	 * Parameters may be both simply strings for str_replace() and regular expressions for preg_replace()
343
	 *
344
	 * @param string|string[] $search
345
	 * @param string|string[] $replace
346
	 *
347
	 * @return Page
348
	 */
349
	public function replace ($search, $replace = '') {
350
		if (is_array($search)) {
351
			$this->search_replace = $search + $this->search_replace;
352
		} else {
353
			/** @noinspection OffsetOperationsInspection */
354
			$this->search_replace[$search] = $replace;
355
		}
356
		return $this;
357
	}
358
	/**
359
	 * Processing of replacing in content
360
	 *
361
	 * @param string $content
362
	 *
363
	 * @return string
364
	 */
365 22
	protected function process_replacing ($content) {
366 22
		foreach ($this->search_replace as $search => $replace) {
367
			$content = _preg_replace($search, $replace, $content) ?: str_replace($search, $replace, $content);
368
		}
369 22
		$this->search_replace = [];
370 22
		return $content;
371
	}
372
	/**
373
	 * Adding links
374
	 *
375
	 * @param array $data According to h class syntax
376
	 *
377
	 * @return Page
378
	 */
379
	public function link ($data) {
380
		if ($data !== false) {
381
			$this->link[] = $data;
382
		}
383
		return $this;
384
	}
385
	/**
386
	 * Simple wrapper of $Page->link() for inserting Atom feed on page
387
	 *
388
	 * @param string $href
389
	 * @param string $title
390
	 *
391
	 * @return Page
392
	 */
393
	public function atom ($href, $title = 'Atom Feed') {
394
		return $this->link(
395
			[
396
				'href'  => $href,
397
				'title' => $title,
398
				'rel'   => 'alternate',
399
				'type'  => 'application/atom+xml'
400
			]
401
		);
402
	}
403
	/**
404
	 * Simple wrapper of $Page->link() for inserting RSS feed on page
405
	 *
406
	 * @param string $href
407
	 * @param string $title
408
	 *
409
	 * @return Page
410
	 */
411
	public function rss ($href, $title = 'RSS Feed') {
412
		return $this->link(
413
			[
414
				'href'  => $href,
415
				'title' => $title,
416
				'rel'   => 'alternate',
417
				'type'  => 'application/rss+xml'
418
			]
419
		);
420
	}
421
	/**
422
	 * Specify canonical url of current page
423
	 *
424
	 * @param string $url
425
	 *
426
	 * @return Page
427
	 */
428
	public function canonical_url ($url) {
429
		$this->canonical_url         = $url;
430
		$this->link['canonical_url'] = [
431
			'href' => $this->canonical_url,
432
			'rel'  => 'canonical'
433
		];
434
		return $this;
435
	}
436
	/**
437
	 * Adding text to the title page
438
	 *
439
	 * @param string $title
440
	 * @param bool   $replace Replace whole title by this
441
	 *
442
	 * @return Page
443
	 */
444 2
	public function title ($title, $replace = false) {
445 2
		$title = htmlentities($title, ENT_COMPAT, 'utf-8');
446 2
		if ($replace) {
447
			$this->Title = [$title];
448
		} else {
449 2
			$this->Title[] = $title;
450
		}
451 2
		return $this;
452
	}
453
	/**
454
	 * Display success message
455
	 *
456
	 * @param string $success_text
457
	 *
458
	 * @return Page
459
	 */
460
	public function success ($success_text) {
461
		return $this->top_message($success_text, 'success');
462
	}
463
	/**
464
	 * Display notice message
465
	 *
466
	 * @param string $notice_text
467
	 *
468
	 * @return Page
469
	 */
470
	public function notice ($notice_text) {
471
		return $this->top_message($notice_text, 'warning');
472
	}
473
	/**
474
	 * Display warning message
475
	 *
476
	 * @param string $warning_text
477
	 *
478
	 * @return Page
479
	 */
480 2
	public function warning ($warning_text) {
481 2
		return $this->top_message($warning_text, 'error');
482
	}
483
	/**
484
	 * Generic method for 3 methods above
485
	 *
486
	 * @param string $message
487
	 * @param string $class_ending
488
	 *
489
	 * @return Page
490
	 */
491 2
	protected function top_message ($message, $class_ending) {
492 2
		$this->Top .= h::div(
493
			$message,
494
			[
495 2
				'class' => "cs-text-center cs-block-$class_ending cs-text-$class_ending"
496
			]
497
		);
498 2
		return $this;
499
	}
500
	/**
501
	 * Error pages processing
502
	 *
503
	 * @param null|string|string[] $custom_text Custom error text instead of text like "404 Not Found" or array with two elements: [error, error_description]
504
	 * @param bool                 $json        Force JSON return format
505
	 */
506 14
	public function error ($custom_text = null, $json = false) {
507 14
		$Request  = Request::instance();
508 14
		$Response = Response::instance();
509 14
		$code     = $Response->code;
510
		/**
511
		 * Hack for 403 after sign out in administration
512
		 */
513 14
		if ($code == 403 && !$Request->api_path && $Request->cookie('sign_out')) {
514
			$Response->redirect('/');
515
			return;
516
		}
517 14
		list($title, $description) = $this->error_title_description($code, $custom_text);
518 14
		if ($json || $Request->api_path) {
519 10
			$this->json(
520
				[
521 10
					'error'             => $code,
522 10
					'error_description' => $description
523
				]
524
			);
525 4
		} elseif ($Request->cli_path) {
526
			$content = $title != $description ? "$title\n$description" : $description;
527
			err("%r$content%n");
528
		} else {
529 4
			$this->Content = $this->error_page($title, $description);
530
		}
531 14
		$Response->body = $this->Content;
532 14
	}
533
	/**
534
	 * @param int                  $code
535
	 * @param null|string|string[] $custom_text
536
	 *
537
	 * @return string[]
538
	 */
539 14
	protected function error_title_description ($code, $custom_text) {
540 14
		$title       = status_code_string($code);
541 14
		$description = $custom_text ?: $title;
542 14
		if (is_array($custom_text)) {
543 2
			list($title, $description) = $custom_text;
544
		}
545 14
		return [$title, $description];
546
	}
547
	/**
548
	 * @param string $title
549
	 * @param string $description
550
	 *
551
	 * @return string
552
	 */
553 4
	protected function error_page ($title, $description) {
554 4
		ob_start();
555
		if (
556 4
			!_include(THEMES."/$this->theme/error.html", false, false) &&
557 4
			!_include(THEMES."/$this->theme/error.php", false, false)
558
		) {
559
			echo
560
				"<!doctype html>\n".
561 4
				h::title($title).
562 4
				$description;
563
		}
564 4
		return ob_get_clean();
565
	}
566
	/**
567
	 * Provides next events:
568
	 *  System/Page/render/before
569
	 *
570
	 *  System/Page/render/after
571
	 *
572
	 * Page generation
573
	 */
574 22
	public function render () {
575 22
		$Response = Response::instance();
576 22
		if (is_resource($Response->body_stream)) {
577
			return;
578
		}
579
		/**
580
		 * For CLI, API and generally JSON responses only content without page template
581
		 */
582 22
		$Request = Request::instance();
583 22
		if ($Request->cli_path || $Request->api_path || !$this->interface) {
584
			/**
585
			 * Processing of replacing in content
586
			 */
587 20
			$Response->body = $this->process_replacing($this->Content);
588
		} else {
589 2
			Event::instance()->fire('System/Page/render/before');
590
			/**
591
			 * Processing of template, substituting of content, preparing for the output
592
			 */
593 2
			$this->prepare();
594
			/**
595
			 * Processing of replacing in content
596
			 */
597 2
			$this->Html = $this->process_replacing($this->Html);
598 2
			Event::instance()->fire('System/Page/render/after');
599 2
			$Response->body = rtrim($this->Html);
600
		}
601 22
	}
602
}
603