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

Page::json()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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