1 <?php
2
3 /*
4 * This file is part of the Icybee package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Icybee\Modules\Pages;
13
14 use ICanBoogie\Routing\Pattern;
15
16 use Icybee\Modules\Sites\Site;
17
18 /**
19 * Representation of a page.
20 *
21 * @property Page $parent Parent page of the page.
22 * @property \Icybee\Modules\Sites\Site $site The site the page belongs to.
23 * @property-read bool $is_accessible Whether the page is accessible or not.
24 * @property-read bool $is_active Wheter the page is active or not.
25 * @property-read bool $is_home Whether the page is the home page of the site or not.
26 * @property-read bool $is_trail Whether the page is in the navigation trail or not.
27 * @property-read array[]Page $navigation_children Navigation children of the page.
28 * @property-read int $descendents_count The number of descendents.
29 */
30 class Page extends \Icybee\Modules\Nodes\Node
31 {
32 const PARENTID = 'parentid';
33 const LOCATIONID = 'locationid';
34 const PATTERN = 'pattern';
35 const WEIGHT = 'weight';
36 const TEMPLATE = 'template';
37 const LABEL = 'label';
38 const IS_NAVIGATION_EXCLUDED = 'is_navigation_excluded';
39
40 /**
41 * The identifier of the parent page.
42 *
43 * @var int
44 */
45 public $parentid;
46
47 /**
48 * The identifier of the page the page is redirected to.
49 *
50 * @var int
51 */
52 public $locationid;
53
54 /**
55 * The pattern used to create the URL of the nodes displayed by the page.
56 *
57 * @var string
58 */
59 public $pattern;
60
61 /**
62 * Weight of the page in the hierarchy.
63 *
64 * @var int
65 */
66 public $weight;
67
68 /**
69 * Template used to render the page.
70 *
71 * @var string
72 */
73 public $template;
74
75 /**
76 * Returns the template for the page.
77 *
78 * This function is only called if the {@link pattern} property was empty during construct. The
79 * template is guested according the place of the page in the hierarchy:
80 *
81 * - The page is the home page: `home.html`
82 * - The page has a parent which is not the home page: the template of the parent.
83 * - Otherwise: `page.html`
84 *
85 * @return string
86 */
87 protected function get_template()
88 {
89 if ($this->is_home)
90 {
91 return 'home.html';
92 }
93 else if ($this->parent && !$this->parent->is_home)
94 {
95 return $this->parent->template;
96 }
97
98 return 'page.html';
99 }
100
101 /**
102 * Returns the extension used by the page's template.
103 *
104 * @return string ".html" if the template is "page.html".
105 */
106 protected function get_extension()
107 {
108 $extension = pathinfo($this->template, PATHINFO_EXTENSION);
109
110 return $extension ? '.' . $extension : null;
111 }
112
113 /**
114 * The text to use instead of the title when it is used in the navigation of the breadcrumb.
115 *
116 * @var string
117 */
118 public $label;
119
120 /**
121 * Returns the label for the page.
122 *
123 * This function is only called if the {@link label} property was empty during construct. It
124 * returns the {@link $title} property.
125 *
126 * @return string
127 */
128 protected function get_label()
129 {
130 return $this->title;
131 }
132
133 /**
134 * Whether the page is excluded from the navigation.
135 *
136 * @var bool
137 */
138 public $is_navigation_excluded;
139
140 /**
141 * @var string Part of the URL captured by the pattern.
142 * @todo-20130307: rename as "path_part"
143 */
144 public $url_part;
145
146 /**
147 * @var array Variables captured from the URL using the pattern.
148 * @todo-20130327: rename as "path_params"
149 */
150 public $url_variables = array();
151
152 /**
153 * @var Node Node object currently acting as the body of the page.
154 * @todo-20130327: use request's context instead
155 */
156 public $node;
157
158 /**
159 * @var bool true if the page is cachable, false otherwise.
160 */
161 public $cachable = true;
162
163 public function __construct($model='pages')
164 {
165 if (empty($this->label))
166 {
167 unset($this->label);
168 }
169
170 if (empty($this->template))
171 {
172 unset($this->template);
173 }
174
175 parent::__construct($model);
176 }
177
178 public function __sleep()
179 {
180 $keys = parent::__sleep();
181
182 // TODO-20130327: is this necessary?
183
184 if (isset($this->template))
185 {
186 $keys['template'] = 'template';
187 }
188
189 return $keys;
190 }
191
192 /**
193 * Returns the previous online sibling for the page.
194 *
195 * @return Page|false The previous sibling, or false if there is none.
196 */
197 protected function lazy_get_previous()
198 {
199 return $this->model
200 ->where('is_online = 1 AND nid != ? AND parentid = ? AND siteid = ? AND weight <= ?', $this->nid, $this->parentid, $this->siteid, $this->weight)
201 ->order('weight desc, created_at desc')->one;
202 }
203
204 /**
205 * Returns the next online sibling for the page.
206 *
207 * @return Page|false The next sibling, or false if there is none.
208 */
209 protected function lazy_get_next()
210 {
211 return $this->model
212 ->where('is_online = 1 AND nid != ? AND parentid = ? AND siteid = ? AND weight >= ?', $this->nid, $this->parentid, $this->siteid, $this->weight)
213 ->order('weight, created_at')->one;
214 }
215
216 /**
217 * Returns the URL of the page.
218 *
219 * @return string
220 */
221 protected function lazy_get_url()
222 {
223 global $core;
224
225 if ($this->location)
226 {
227 return $this->location->url;
228 }
229
230 $url_pattern = $this->url_pattern;
231
232 if ($this->is_home)
233 {
234 return $url_pattern;
235 }
236
237 $url = null;
238
239 if (Pattern::is_pattern($url_pattern))
240 {
241 if ($this->url_variables)
242 {
243 $url = Pattern::from($url_pattern)->format($this->url_variables);
244
245 // \ICanBoogie\log('URL %pattern rescued using URL variables', array('%pattern' => $pattern));
246 }
247 else
248 {
249 $page = isset($core->request->context->page) ? $core->request->context->page : null;
250
251 if ($page && $page->url_variables)
252 {
253 $url = Pattern::from($url_pattern)->format($page->url_variables);
254
255 // \ICanBoogie\log("URL pattern %pattern was resolved using current page's variables", array('%pattern' => $pattern));
256 }
257 else
258 {
259 $url = '#url-pattern-could-not-be-resolved';
260 }
261 }
262 }
263 else
264 {
265 $url = $url_pattern;
266 }
267
268 return $url;
269 }
270
271 /**
272 * Returns the absulte URL of the pages.
273 *
274 * @return string The absolute URL of the page.
275 */
276 protected function lazy_get_absolute_url()
277 {
278 $site = $this->site;
279
280 return $site->url . substr($this->url, strlen($site->path));
281 }
282
283 public function translation($language=null)
284 {
285 $translation = parent::translation($language);
286
287 if ($translation->nid != $this->nid && isset($this->url_variables))
288 {
289 $translation->url_variables = $this->url_variables;
290 }
291
292 return $translation;
293 }
294
295 protected function lazy_get_translations()
296 {
297 $translations = parent::lazy_get_translations();
298
299 if (!$translations || empty($this->url_variables))
300 {
301 return $translations;
302 }
303
304 foreach ($translations as $translation)
305 {
306 $translation->url_variables = $this->url_variables;
307 }
308
309 return $translations;
310 }
311
312 /**
313 * Returns the URL pattern of the page.
314 *
315 * @return string
316 */
317 protected function lazy_get_url_pattern()
318 {
319 $site = $this->site;
320
321 if ($this->is_home)
322 {
323 return $site->path . '/';
324 }
325
326 $parent = $this->parent;
327 $pattern = $this->pattern;
328
329 return ($parent ? $parent->url_pattern : $site->path . '/')
330 . ($pattern ? $pattern : $this->slug)
331 . ($this->has_child ? '/' : $this->extension);
332 }
333
334 /**
335 * Returns if the page is accessible or not in the navigation tree.
336 */
337 protected function get_is_accessible()
338 {
339 global $core;
340
341 if ($core->user->is_guest && $this->site->status != Site::STATUS_OK)
342 {
343 return false;
344 }
345
346 return ($this->parent && !$this->parent->is_accessible) ? false : $this->is_online;
347 }
348
349 /**
350 * Checks if the page is the home page.
351 *
352 * A page is considered a home page when the page has no parent, its weight value is zero and
353 * it is online.
354 *
355 * @return bool `true` if the page record is the home page, `false` otherwise.
356 */
357 protected function get_is_home()
358 {
359 return (!$this->parentid && !$this->weight && $this->is_online);
360 }
361
362 /**
363 * Checks if the page record is the active page.
364 *
365 * The global variable `page` must be defined in order to identify the active page.
366 *
367 * @return bool true if the page record is the active page, false otherwise.
368 *
369 * @todo-20130327: create the set_active_page() and get_active_page() helpers ?
370 */
371 protected function get_is_active()
372 {
373 global $core;
374
375 return $core->request->context->page->nid == $this->nid;
376 }
377
378 /**
379 * Checks if the page record is in the active page trail.
380 *
381 * The global variable `page` must be defined in order to identifiy the active page.
382 *
383 * @return bool true if the page is in the active page trail, false otherwise.
384 */
385 protected function get_is_trail()
386 {
387 global $core;
388
389 $node = $core->request->context->page; // TODO-20130327: use a get_active_page() helper?
390
391 while ($node)
392 {
393 if ($node->nid == $this->nid)
394 {
395 return true;
396 }
397
398 $node = $node->parent;
399 }
400
401 return false;
402 }
403
404 /**
405 * Returns the location target for the page record.
406 *
407 * @return Icybee\Modules\Pages\Page|null The location target, or null if there is none.
408 */
409 protected function get_location()
410 {
411 return $this->locationid ? $this->model[$this->locationid] : null;
412 }
413
414 /**
415 * Returns the home page for the page record.
416 *
417 * @return Icybee\Modules\Pages\Page
418 */
419 protected function get_home()
420 {
421 return $this->model->find_home($this->siteid);
422 }
423
424 /**
425 * Returns the parent of the page.
426 *
427 * @return Icybee\Modules\Pages\Page|null The parent page or null is the page has no parent.
428 */
429 protected function lazy_get_parent()
430 {
431 return $this->parentid ? $this->model[$this->parentid] : null;
432 }
433
434 /**
435 * Return the online children page for this page.
436 *
437 * TODO-20100629: The `children` virtual property should return *all* the children for the page,
438 * we should create a `online_children` virtual property that returns only _online_ children,
439 * or maybe a `accessible_children` virtual property ?
440 */
441 protected function lazy_get_children()
442 {
443 $blueprint = $this->model->blueprint($this->siteid);
444 $pages = $blueprint['pages'];
445
446 if (!$pages[$this->nid]->children)
447 {
448 return array();
449 }
450
451 $ids = array();
452
453 foreach ($pages[$this->nid]->children as $nid => $child)
454 {
455 if (!$child->is_online)
456 {
457 continue;
458 }
459
460 $ids[] = $nid;
461 }
462
463 return $this->model->find($ids);
464 }
465
466 /**
467 * Returns the page's children that are online and part of the navigation.
468 *
469 * @return array[int]Page
470 */
471 protected function lazy_get_navigation_children()
472 {
473 $index = $this->model->blueprint($this->siteid)->index;
474
475 if (empty($index[$this->nid]) || !$index[$this->nid]->children)
476 {
477 return array();
478 }
479
480 $ids = array();
481
482 foreach ($index[$this->nid]->children as $nid => $child)
483 {
484 if (!$child->is_online || $child->is_navigation_excluded || $child->pattern)
485 {
486 continue;
487 }
488
489 $ids[] = $nid;
490 }
491
492 if (!$ids)
493 {
494 return array();
495 }
496
497 return $this->model->find($ids);
498 }
499
500 /**
501 * Checks if the page as at least one child.
502 *
503 * @return boolean
504 */
505 protected function get_has_child()
506 {
507 return $this->model->blueprint($this->siteid)->has_children($this->nid);
508 }
509
510 /**
511 * Returns the number of children.
512 *
513 * @return int
514 */
515 protected function get_children_count()
516 {
517 return $this->model->blueprint($this->siteid)->children_count($this->nid);
518 }
519
520 /**
521 * Returns the number of descendent.
522 *
523 * @return int
524 */
525 protected function get_descendents_count()
526 {
527 return $this->model->blueprint($this->siteid)->index[$this->nid]->descendents_count;
528 }
529
530 /**
531 * Returns the depth level of this page in the navigation tree.
532 */
533 protected function get_depth()
534 {
535 return $this->parent ? $this->parent->depth + 1 : 0;
536 }
537
538 /**
539 * Returns the contents of the page as an array.
540 *
541 * Keys of the array are the contentid, values are the contents objects.
542 *
543 * @return array[string]\Icybee\Modules\Pages\Pages\Content
544 */
545 protected function lazy_get_contents()
546 {
547 global $core;
548
549 $entries = $core->models['pages/contents']->filter_by_pageid($this->nid);
550 $contents = array();
551
552 foreach ($entries as $entry)
553 {
554 $contents[$entry->contentid] = $entry;
555 }
556
557 return $contents;
558 }
559
560 /**
561 * Returns the body of this page.
562 *
563 * The body is the page's contents object with the 'body' identifier.
564 *
565 * @return \Icybee\Modules\Pages\Pages\Content
566 */
567 protected function lazy_get_body()
568 {
569 $contents = $this->contents;
570
571 return isset($contents['body']) ? $contents['body'] : null;
572 }
573
574 /**
575 * Replaces `type` value by "page" and `id` value by "page-id-<nid>".
576 *
577 * The following class names are added:
578 *
579 * - `slug`: "page-slug-<slug>"
580 * - `home`: true if the page is the home page.
581 * - `active`: true if the page is the active page.
582 * - `trail`: true if the page is in the breadcrumb trail.
583 * - `node-id`: "node-id-<nid>" if the page displays a node.
584 * - `node-constructor`: "node-constructor-<normalized_constructor>" if the page displays a node.
585 * - `template`: "template-<name>" the name of the page's template, without its extension.
586 */
587 protected function get_css_class_names()
588 {
589 $names = array_merge
590 (
591 parent::get_css_class_names(), array
592 (
593 'type' => 'page',
594 'id' => 'page-id-' . $this->nid,
595 'slug' => 'page-slug-'. $this->slug,
596 'home' => ($this->home->nid == $this->nid),
597 'active' => $this->is_active,
598 'trail' => $this->is_trail,
599 'template' => 'template-' . preg_replace('#\.(html|php)$#', '', $this->template),
600 'has-children' => count($this->navigation_children) != 0
601 )
602 );
603
604 if (isset($this->node))
605 {
606 $node = $this->node;
607
608 $names['node-id'] = 'node-id-' . $node->nid;
609 $names['node-constructor'] = 'node-constructor-' . \ICanBoogie\normalize($node->constructor);
610 }
611
612 return $names;
613 }
614
615 /**
616 * Return the description for the page.
617 */
618
619 // TODO-20101115: these should be methods added by the "SEO" module
620
621 protected function get_description()
622 {
623 return $this->metas['description'];
624 }
625
626 protected function get_document_title()
627 {
628 return $this->metas['document_title'] ? $this->metas['document_title'] : $this->title;
629 }
630 }