Completed
Push — master ( 9235b1...2182c9 )
by Nazar
04:49
created

Page::rss()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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