1: <?php
2:
3: /**
4: * ProcessWire Page
5: *
6: * Page is the class used by all instantiated pages and it provides functionality for:
7: *
8: * 1. Providing get/set access to the Page's properties
9: * 2. Accessing the related hierarchy of pages (i.e. parents, children, sibling pages)
10: *
11: * ProcessWire 2.x
12: * Copyright (C) 2010 by Ryan Cramer
13: * Licensed under GNU/GPL v2, see LICENSE.TXT
14: *
15: * http://www.processwire.com
16: * http://www.ryancramer.com
17: *
18: */
19:
20: class Page extends WireData {
21:
22: /*
23: * The following constant flags are specific to a Page's 'status' field. A page can have 1 or more flags using bitwise logic.
24: * Status levels 1024 and above are excluded from search by the core. Status levels 16384 and above are runtime only and not
25: * stored in the DB unless for logging or page history.
26: *
27: * If the under 1024 status flags are expanded in the future, it must be ensured that the combined value of the searchable flags
28: * never exceeds 1024, otherwise issues in Pages::find() will need to be considered.
29: *
30: * The status levels 16384 and above can safely be changed as needed as they are runtime only.
31: *
32: */
33: const statusOn = 1; // base status for all pages
34: const statusLocked = 4; // page locked for changes. Not enforced by the core, but checked by Process modules.
35: const statusSystemID = 8; // page is for the system and may not be deleted or have it's id changed (everything else, okay)
36: const statusSystem = 16; // page is for the system and may not be deleted or have it's id, name, template or parent changed
37: const statusHidden = 1024; // page is excluded selector methods like $pages->find() and $page->children() unless status is specified, like "status&1"
38: const statusUnpublished = 2048; // page is not published and is not renderable.
39: const statusTrash = 8192; // page is in the trash
40: const statusDeleted = 16384; // page is deleted (runtime only)
41: const statusSystemOverride = 32768; // page is in a state where system flags may be overridden
42: const statusCorrupted = 131072; // page was corrupted at runtime and is NOT saveable: see setFieldValue() and $outputFormatting. (runtime)
43: const statusMax = 9999999; // number to use for max status comparisons, runtime only
44:
45: /**
46: * The Template this page is using (object)
47: *
48: */
49: protected $template;
50:
51: /**
52: * The previous template used by the page, if it was changed during runtime.
53: *
54: * Allows Pages::save() to delete data that's no longer used.
55: *
56: */
57: private $templatePrevious;
58:
59: /**
60: * Parent Page - Instance of Page
61: *
62: */
63: protected $parent = null;
64:
65: /**
66: * The previous parent used by the page, if it was changed during runtime.
67: *
68: * Allows Pages::save() to identify when the parent has changed
69: *
70: */
71: private $parentPrevious;
72:
73: /**
74: * Reference to the Page's template file, used for output. Instantiated only when asked for.
75: *
76: */
77: private $output;
78:
79: /**
80: * Instance of PageFilesManager, which manages and migrates file versions for this page
81: *
82: * Only instantiated upon request, so access only from filesManager() method in Page class.
83: * Outside API can use $page->filesManager.
84: *
85: */
86: private $filesManager = null;
87:
88: /**
89: * RolesArray containing Role instances that are assigned to this page.
90: *
91: * Dynamically set as needed, refer only to roles() method rather than this directly.
92: *
93: private $rolesArray = null;
94: */
95:
96: /**
97: * Field data that queues while the page is loading.
98: *
99: * Once setIsLoaded(true) is called, this data is processed and instantiated into the Page and the fieldDataQueue is emptied (and no longer relevant)
100: *
101: */
102: protected $fieldDataQueue = array();
103:
104: /**
105: * Is this a new page (not yet existing in the database)?
106: *
107: */
108: protected $isNew = true;
109:
110: /**
111: * Is this Page finished loading from the DB (i.e. Pages::getById)?
112: *
113: * When false, it is assumed that any values set need to be woken up.
114: * When false, it also assumes that built-in properties (like name) don't need to be sanitized.
115: *
116: * Note: must be kept in the 'true' state. Pages::getById sets it to false before populating data and then back to true when done.
117: *
118: */
119: protected $isLoaded = true;
120:
121: /**
122: * Is this page allowing it's output to be formatted?
123: *
124: * If so, the page may not be saveable because calls to $page->get(field) are returning versions of
125: * variables that may have been formatted at runtime for output. An exception will be thrown if you
126: * attempt to set the value of a formatted field when $outputFormatting is on.
127: *
128: * Output formatting should be turned off for pages that you are manipulating and saving.
129: * Whereas it should be turned on for pages that are being used for output on a public site.
130: * Having it on means that Textformatters and any other output formatters will be executed
131: * on any values returned by this page. Likewise, any values you set to the page while outputFormatting
132: * is set to true are considered potentially corrupt.
133: *
134: */
135: protected $outputFormatting = false;
136:
137: /**
138: * A unique instance ID assigned to the page at the time it's loaded (for debugging purposes only)
139: *
140: */
141: protected $instanceID = 0;
142:
143: /**
144: * IDs for all the instances of pages, used for debugging and testing.
145: *
146: * Indexed by $instanceID => $pageID
147: *
148: */
149: static public $instanceIDs = array();
150:
151: /**
152: * Stack of ID indexed Page objects that are currently in the loading process.
153: *
154: * Used to avoid possible circular references when multiple pages referencing each other are being populated at the same time.
155: *
156: */
157: static public $loadingStack = array();
158:
159: /**
160: * The current page number, starting from 1
161: *
162: */
163: protected $pageNum = 1;
164:
165: /**
166: * Reference to main config, optimization so that get() method doesn't get called
167: *
168: */
169: protected $config = null;
170:
171: /**
172: * Page-specific settings which are either saved in pages table, or generated at runtime.
173: *
174: */
175: protected $settings = array(
176: 'id' => 0,
177: 'name' => '',
178: 'status' => 1,
179: 'numChildren' => 0,
180: 'sort' => 0,
181: 'sortfield' => 'sort',
182: 'modified_users_id',
183: 'created_users_id',
184: );
185:
186: /**
187: * Create a new page in memory.
188: *
189: * @param Template $tpl Template object this page should use.
190: *
191: */
192: public function __construct(Template $tpl = null) {
193:
194: if(!is_null($tpl)) $this->template = $tpl;
195: $this->useFuel(false); // prevent fuel from being in local scope
196: $this->parentPrevious = null;
197: $this->templatePrevious = null;
198: }
199:
200: /**
201: * Destruct this page instance
202: *
203: */
204: public function __destruct() {
205: if($this->instanceID) {
206: // remove from the record of instanceID, so that we have record of page's that HAVEN'T been destructed.
207: unset(self::$instanceIDs[$this->instanceID]);
208: }
209: }
210:
211: /**
212: * Clone this page instance
213: *
214: */
215: public function __clone() {
216: $track = $this->trackChanges();
217: $this->setTrackChanges(false);
218: if($this->filesManager) {
219: $this->filesManager = clone $this->filesManager;
220: $this->filesManager->setPage($this);
221: }
222: foreach($this->template->fieldgroup as $field) {
223: $name = $field->name;
224: if(!$field->type->isAutoload() && !isset($this->data[$name])) continue; // important for draft loading
225: $value = $this->get($name);
226: if(is_object($value)) {
227: if(!$value instanceof Page) $this->set($name, clone $value); // attempt re-commit
228: if($value instanceof Pagefiles) $this->get($name)->setPage($this);
229: }
230: }
231: $this->instanceID .= ".clone";
232: if($track) $this->setTrackChanges(true);
233: }
234:
235: /**
236: * Set the value of a page property
237: *
238: * @param string $key Property to set
239: * @param mixed $value
240: * @return Page Reference to this Page
241: * @see __set
242: *
243: */
244: public function set($key, $value) {
245:
246: if(($key == 'id' || $key == 'name') && $this->settings[$key] && $value != $this->settings[$key])
247: if( ($key == 'id' && (($this->settings['status'] & Page::statusSystem) || ($this->settings['status'] & Page::statusSystemID))) ||
248: ($key == 'name' && (($this->settings['status'] & Page::statusSystem)))) {
249: throw new WireException("You may not modify '$key' on page '{$this->path}' because it is a system page");
250: }
251:
252: switch($key) {
253: case 'id':
254: if(!$this->isLoaded) Page::$loadingStack[(int) $value] = $this;
255: case 'sort':
256: case 'numChildren':
257: case 'num_children':
258: if($key == 'num_children') $key = 'numChildren';
259: if($this->settings[$key] !== $value) $this->trackChange($key);
260: $this->settings[$key] = (int) $value;
261: break;
262: case 'status':
263: $this->setStatus($value);
264: break;
265: case 'name':
266: if($this->isLoaded) {
267: $beautify = empty($this->settings[$key]);
268: $value = $this->fuel('sanitizer')->pageName($value, $beautify);
269: if($this->settings[$key] !== $value) $this->trackChange($key);
270: }
271: $this->settings[$key] = $value;
272: break;
273: case 'parent':
274: case 'parent_id':
275: if(($key == 'parent_id' || is_int($value)) && $value) $value = $this->fuel('pages')->get((int)$value);
276: else if(is_string($value)) $value = $this->fuel('pages')->get($value);
277: if($value) $this->setParent($value);
278: break;
279: case 'template':
280: case 'templates_id':
281: if($key == 'templates_id' && $this->template && $this->template->id == $value) break;
282: if($key == 'templates_id') $value = $this->fuel('templates')->get((int)$value);
283: $this->setTemplate($value);
284: break;
285: case 'created':
286: case 'modified':
287: if(!ctype_digit("$value")) $value = strtotime($value);
288: $this->settings[$key] = (int) $value;
289: break;
290: case 'created_users_id':
291: case 'modified_users_id':
292: $this->settings[$key] = (int) $value;
293: break;
294: case 'createdUser':
295: case 'modifiedUser':
296: $this->setUser($value, strpos($key, 'created') === 0 ? 'created' : 'modified');
297: break;
298: case 'sortfield':
299: $value = $this->fuel('pages')->sortfields()->decode($value);
300: if($this->settings[$key] != $value) $this->trackChange($key);
301: $this->settings[$key] = $value;
302: break;
303: case 'isLoaded':
304: $this->setIsLoaded($value);
305: break;
306: case 'pageNum':
307: $this->pageNum = ((int) $value) > 1 ? (int) $value : 1;
308: break;
309: case 'instanceID':
310: $this->instanceID = $value;
311: self::$instanceIDs[$value] = $this->settings['id'];
312: break;
313: default:
314: $this->setFieldValue($key, $value, $this->isLoaded);
315:
316: }
317: return $this;
318: }
319:
320:
321: /**
322: * Set the value of a field that is defined in the page's Fieldgroup
323: *
324: * This may not be called when outputFormatting is on.
325: *
326: * This is for internal use. API should generally use the set() method, but this is kept public for the minority of instances where it's useful.
327: *
328: * @param string $key
329: * @param mixed $value
330: * @param bool $load Should the existing value be loaded for change comparisons? (applicable only to non-autoload fields)
331: *
332: */
333: public function setFieldValue($key, $value, $load = true) {
334:
335: if(!$this->template) throw new WireException("You must assign a template to the page before setting custom field values.");
336:
337: // if the page is not yet loaded and a '__' field was set, then we queue it so that the loaded() method can
338: // instantiate all those fields knowing that all parts of them are present for wakeup.
339: if(!$this->isLoaded && strpos($key, '__')) {
340: list($key, $subKey) = explode('__', $key);
341: if(!isset($this->fieldDataQueue[$key])) $this->fieldDataQueue[$key] = array();
342: $this->fieldDataQueue[$key][$subKey] = $value;
343: return;
344: }
345:
346: if(!$field = $this->template->fieldgroup->getField($key)) {
347: // not a known/saveable field, let them use it for runtime storage
348: return parent::set($key, $value);
349: }
350:
351: // if a null value is set, then ensure the proper blank type is set to the field
352: if(is_null($value)) {
353: return parent::set($key, $field->type->getBlankValue($this, $field));
354: }
355:
356: // if the page is currently loading from the database, we assume that any set values are 'raw' and need to be woken up
357: if(!$this->isLoaded) {
358:
359: // send the value to the Fieldtype to be woken up for storage in the page
360: $value = $field->type->wakeupValue($this, $field, $value);
361:
362: // page is currently loading, so we don't need to continue any further
363: return parent::set($key, $value);
364: }
365:
366: // check if the field hasn't been already loaded
367: if(is_null(parent::get($key))) {
368: // this field is not currently loaded. if the $load param is true, then ...
369: // retrieve old value first in case it's not autojoined so that change comparisons and save's work
370: if($load && $this->isLoaded) $this->get($key);
371:
372: } else if($this->outputFormatting && $field->type->formatValue($this, $field, $value) != $value) {
373: // The field has been loaded or dereferenced from the API, and this field changes when formatters are applied to it.
374: // There is a good chance they are trying to set a formatted value, and we don't allow this situation because the
375: // possibility of data corruption is high. We set the Page::statusCorrupted status so that Pages::save() can abort.
376: $this->set('status', $this->status | self::statusCorrupted);
377: }
378:
379: // ensure that the value is in a safe format and set it
380: $value = $field->type->sanitizeValue($this, $field, $value);
381:
382: parent::set($key, $value);
383: }
384:
385: /**
386: * Get the value of a requested Page property
387: *
388: * @param string $key
389: * @return mixed
390: * @see __get
391: *
392: */
393: public function get($key) {
394: $value = null;
395: switch($key) {
396: case 'parent_id':
397: case 'parentID':
398: $value = $this->parent ? $this->parent->id : 0;
399: break;
400: case 'child':
401: $value = $this->child();
402: break;
403: case 'children':
404: case 'subpages': // PW1
405: $value = $this->children();
406: break;
407: case 'parent':
408: case 'parents':
409: case 'rootParent':
410: case 'siblings':
411: case 'next':
412: case 'prev':
413: case 'url':
414: case 'path':
415: case 'outputFormatting':
416: case 'isTrash':
417: $value = $this->{$key}();
418: break;
419: case 'httpUrl':
420: case 'httpURL':
421: $value = $this->httpUrl();
422: break;
423: case 'fieldgroup':
424: case 'fields':
425: $value = $this->template->fieldgroup;
426: break;
427: case 'template':
428: case 'templatePrevious':
429: case 'parentPrevious':
430: case 'isLoaded':
431: case 'isNew':
432: case 'pageNum':
433: case 'instanceID':
434: $value = $this->$key;
435: break;
436: case 'out':
437: case 'output':
438: $value = $this->getTemplateFile();
439: break;
440: case 'filesManager':
441: $value = $this->filesManager();
442: break;
443: case 'name':
444: $value = $this->settings['name'];
445: break;
446: case 'modified_users_id':
447: case 'modifiedUsersID':
448: case 'modifiedUserID':
449: $value = $this->settings['created_users_id'];
450: break;
451: case 'created_users_id':
452: case 'createdUsersID':
453: case 'createdUserID':
454: $value = $this->settings['modified_users_id'];
455: break;
456: case 'modifiedUser':
457: if(!$value = $this->fuel('users')->get($this->settings['modified_users_id'])) $value = new NullUser();
458: break;
459: case 'createdUser':
460: if(!$value = $this->fuel('users')->get($this->settings['created_users_id'])) $value = new NullUser();
461: break;
462: case 'urlSegment':
463: $value = $this->fuel('input')->urlSegment1; // deprecated, but kept for backwards compatibility
464: break;
465: case 'accessTemplate':
466: $value = $this->getAccessTemplate();
467: break;
468: default:
469: if($key && isset($this->settings[(string)$key])) return $this->settings[$key];
470:
471: if(($value = $this->getFieldFirstValue($key)) === null) {
472: if(($value = $this->getFieldValue($key)) === null) {
473: // if there is a selector, we'll assume they are using the get() method to get a child
474: if(Selectors::stringHasOperator($key)) $value = $this->child($key);
475: }
476: }
477: }
478:
479: return $value;
480: }
481:
482: /**
483: * Given a Multi Key, determine if there are multiple keys requested and return the first non-empty value
484: *
485: * A Multi Key is a string with multiple field names split by pipes, i.e. headline|title
486: *
487: * Example: browser_title|headline|title - Return the value of the first field that is non-empty
488: *
489: * @param string $key
490: * @return null|mixed Returns null if no values match, or if there aren't multiple keys split by "|" chars
491: *
492: */
493: protected function getFieldFirstValue($multiKey) {
494:
495: // looking multiple keys split by "|" chars, and not an '=' selector
496: if(strpos($multiKey, '|') === false || strpos($multiKey, '=') !== false) return null;
497:
498: $value = null;
499: $keys = explode('|', $multiKey);
500:
501: foreach($keys as $key) {
502: $value = $this->getFieldValue($key);
503: if(is_string($value)) $value = trim($value);
504: if($value) break;
505: }
506:
507: return $value;
508: }
509:
510: /**
511: * Get the value for a non-native page field, and call upon Fieldtype to join it if not autojoined
512: *
513: * @param string $key
514: * @return null|mixed
515: *
516: */
517: protected function getFieldValue($key) {
518: if(!$this->template) return null;
519: $field = $this->template->fieldgroup->getField($key);
520: $value = parent::get($key);
521: if(!$field) return $value; // likely a runtime field, not part of our data
522:
523: // if the value is already loaded, return it
524: if(!is_null($value)) return $this->outputFormatting ? $field->type->formatValue($this, $field, $value) : $value;
525: $track = $this->trackChanges();
526: $this->setTrackChanges(false);
527: $value = $field->type->loadPageField($this, $field);
528: if(is_null($value)) $value = $field->type->getDefaultValue($this, $field);
529: else $value = $field->type->wakeupValue($this, $field, $value);
530:
531: // if outputFormatting is being used, turn it off because it's not necessary here and may throw an exception
532: $outputFormatting = $this->outputFormatting;
533: if($outputFormatting) $this->setOutputFormatting(false);
534: $this->setFieldValue($key, $value, false);
535: if($outputFormatting) $this->setOutputFormatting(true);
536:
537: $value = parent::get($key);
538: if(is_object($value) && $value instanceof Wire) $value->setTrackChanges(true);
539: if($track) $this->setTrackChanges(true);
540: return $this->outputFormatting ? $field->type->formatValue($this, $field, $value) : $value;
541: }
542:
543: /**
544: * Get the raw/unformatted value of a field, regardless of what $this->outputFormatting is set at
545: *
546: */
547: public function getUnformatted($key) {
548: $outputFormatting = $this->outputFormatting;
549: if($outputFormatting) $this->setOutputFormatting(false);
550: $value = $this->get($key);
551: if($outputFormatting) $this->setOutputFormatting(true);
552: return $value;
553: }
554:
555:
556: /**
557: * @see get
558: *
559: */
560: public function __get($key) {
561: return $this->get($key);
562: }
563:
564: /**
565: * @see set
566: *
567: */
568: public function __set($key, $value) {
569: $this->set($key, $value);
570: }
571:
572: /**
573: * Set the 'status' setting, with some built-in protections
574: *
575: */
576: protected function setStatus($value) {
577: $value = (int) $value;
578: $override = $this->settings['status'] & Page::statusSystemOverride;
579: if(!$override) {
580: if($this->settings['status'] & Page::statusSystemID) $value = $value | Page::statusSystemID;
581: if($this->settings['status'] & Page::statusSystem) $value = $value | Page::statusSystem;
582: }
583: if($this->settings['status'] != $value) $this->trackChange('status');
584: $this->settings['status'] = $value;
585: }
586:
587: /**
588: * Set this Page's Template object
589: *
590: */
591: protected function setTemplate($tpl) {
592: if(!is_object($tpl)) $tpl = $this->fuel('templates')->get($tpl);
593: if(!$tpl instanceof Template) throw new WireException("Invalid value sent to Page::setTemplate");
594: if($this->template && $this->template->id != $tpl->id) {
595: if($this->settings['status'] & Page::statusSystem) throw new WireException("Template changes are disallowed on this page");
596: if(is_null($this->templatePrevious)) $this->templatePrevious = $this->template;
597: $this->trackChange('template');
598: }
599: $this->template = $tpl;
600: return $this;
601: }
602:
603:
604: /**
605: * Set this page's parent Page
606: *
607: */
608: protected function setParent(Page $parent) {
609: if($this->parent && $this->parent->id == $parent->id) return $this;
610: $this->trackChange('parent');
611: if(($this->parent && $this->parent->id) && $this->parent->id != $parent->id) {
612: if($this->settings['status'] & Page::statusSystem) throw new WireException("Parent changes are disallowed on this page");
613: $this->parentPrevious = $this->parent;
614: }
615: $this->parent = $parent;
616: return $this;
617: }
618:
619:
620: /**
621: * Set either the createdUser or the modifiedUser
622: *
623: * @param User|int|string User object or integer/string representation of User
624: * @param string $userType Must be either 'created' or 'modified'
625: * @return this
626: *
627: */
628: protected function setUser($user, $userType) {
629:
630: if(!$user instanceof User) $user = $this->fuel('users')->get($user);
631:
632: // if they are setting an invalid user or unknown user, then the Page defaults to the super user
633: if(!$user || !$user->id) $user = $this->fuel('users')->get(User::superUserID);
634:
635: if($userType == 'created') $field = 'createdUser';
636: else if($userType == 'modified') $field = 'modifiedUser';
637: else throw new WireException("Unknown user type in Page::setUser(user, type)");
638:
639: $existingUser = $this->$field;
640: if($existingUser && $existingUser->id != $user->id) $this->trackChange($field);
641: $this->$field = $user;
642: return $this;
643: }
644:
645: /**
646: * Return this page's parent Page
647: *
648: */
649: public function parent() {
650: return $this->parent ? $this->parent : new NullPage();
651: }
652:
653: /**
654: * Find Pages in the descendent hierarchy
655: *
656: * Same as Pages::find() except that the results are limited to descendents of this Page
657: *
658: * @param string $selector
659: *
660: */
661: public function find($selector = '', $options = array()) {
662: if(!$this->numChildren) return new PageArray();
663: $selector = "has_parent={$this->id}, $selector";
664: return $this->fuel('pages')->find(trim($selector, ", "), $options);
665: }
666:
667: /**
668: * Return this page's children pages, optionally filtered by a selector
669: *
670: * @param string $selector Selector to use, or blank to return all children
671: * @return PageArray
672: *
673: */
674: public function children($selector = '', $options = array()) {
675: if(!$this->numChildren) return new PageArray();
676: if($selector) $selector .= ", ";
677: $selector = "parent_id={$this->id}, $selector";
678: if(strpos($selector, 'sort=') === false) $selector .= "sort={$this->sortfield}";
679: return $this->fuel('pages')->find(trim($selector, ", "), $options);
680: }
681:
682: /**
683: * Return the page's first single child that matches the given selector.
684: *
685: * Same as children() but returns a Page object or NullPage (with id=0) rather than a PageArray
686: *
687: * @param string $selector Selector to use, or blank to return the first child.
688: * @return Page|NullPage
689: *
690: */
691: public function child($selector = '', $options = array()) {
692: $selector .= ($selector ? ', ' : '') . "limit=1";
693: if(strpos($selector, 'start=') === false) $selector .= ", start=0"; // prevent pagination
694: $children = $this->children($selector);
695: return count($children) ? $children->first() : new NullPage();
696: }
697:
698: /**
699: * Return this page's NON-HIDDEN children pages, optionally filtered by a selector
700: *
701: * This is suitable to call for generating navigation.
702: *
703: * @deprecated This functionality is now handled by default with the regular children() method.
704: *
705: */
706: public function navChildren($selector = '') {
707: if($this->fuel('config')->debug) throw new WireException("Deprecated function call: please use children() rather than navChildren()");
708: return $this->children($selector);
709: }
710:
711: /**
712: * Return this page's parent pages.
713: *
714: */
715: public function parents() {
716: $parents = new PageArray();
717: $parent = $this->parent();
718: while($parent && $parent->id) {
719: $parents->prepend($parent);
720: $parent = $parent->parent();
721: }
722: return $parents;
723: }
724:
725: /**
726: * Get the lowest-level, non-homepage parent of this page
727: *
728: * rootParents typically comprise the first level of navigation on a site.
729: *
730: * @return Page
731: *
732: */
733: public function rootParent() {
734: if(!$this->parent || !$this->parent->id || $this->parent->id === 1) return $this;
735: $parents = $this->parents();
736: $parents->shift(); // shift off homepage
737: return $parents->first();
738: }
739:
740: /**
741: * Return this Page's sibling pages, optionally filtered by a selector.
742: *
743: */
744: public function siblings($selector = '') {
745: if($selector) $selector .= ", ";
746: $selector = "parent_id={$this->parent_id}, $selector";
747: if(strpos($selector, 'sort=') === false) $selector .= "sort=" . ($this->parent ? $this->parent->sortfield : 'sort');
748: return $this->fuel('pages')->find(trim($selector, ", "));
749: }
750:
751: /**
752: * Return the next sibling page
753: *
754: * If given a PageArray of siblings (containing the current) it will return the next sibling relative to the provided PageArray.
755: *
756: * Be careful with this function when the page has a lot of siblings. It has to load them all, so this function is best
757: * avoided at large scale, unless you provide your own already-reduced siblings list (like from pagination)
758: *
759: * @param PageArray $siblings Optional siblings to use instead of the default.
760: * @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
761: *
762: */
763: public function next(PageArray $siblings = null) {
764: if(is_null($siblings)) $siblings = $this->parent->children();
765: $next = $siblings->getNext($this);
766: if(is_null($next)) $next = new NullPage();
767: return $next;
768: }
769:
770: /**
771: * Return the previous sibling page
772: *
773: * If given a PageArray of siblings (containing the current) it will return the previous sibling relative to the provided PageArray.
774: *
775: * Be careful with this function when the page has a lot of siblings. It has to load them all, so this function is best
776: * avoided at large scale, unless you provide your own already-reduced siblings list (like from pagination)
777: *
778: * @param PageArray $siblings Optional siblings to use instead of the default.
779: * @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
780: *
781: */
782: public function prev(PageArray $siblings = null) {
783: if(is_null($siblings)) $siblings = $this->parent->children();
784: $prev = $siblings->getPrev($this);
785: if(is_null($prev)) $prev = new NullPage();
786: return $prev;
787: }
788:
789: /**
790: * Save this page to the database.
791: *
792: * To hook into this (___save), use 'Pages::save'
793: * To hook into a field-only save, use 'Pages::saveField'
794: *
795: * @param Field|string $field Optional field to save (name of field or Field object)
796: *
797: */
798: public function save($field = null) {
799: if(!is_null($field)) return $this->fuel('pages')->saveField($this, $field);
800: return $this->fuel('pages')->save($this);
801: }
802:
803: /**
804: * Delete this page from the Database
805: *
806: * Throws WireException if action not allowed.
807: * See Pages::delete for a hookable version.
808: *
809: * @return bool True on success
810: *
811: */
812: public function delete() {
813: return $this->fuel('pages')->delete($this);
814: }
815:
816: /**
817: * Move this page to the trash
818: *
819: * Throws WireException if action is not allowed.
820: * See Pages::trash for a hookable version.
821: *
822: * @return bool True on success
823: *
824: */
825: public function trash() {
826: return $this->fuel('pages')->trash($this);
827: }
828:
829: /**
830: * Allow iteration of the page's properties with foreach(), fulfilling IteratorAggregate interface.
831: *
832: */
833: public function getIterator() {
834: $a = $this->settings;
835: foreach($this->template->fieldgroup as $field) {
836: $a[$field->name] = $this->get($field->name);
837: }
838: return new ArrayObject($a);
839: }
840:
841:
842: /**
843: * Has the Page (or optionally one of it's fields) changed since it was loaded?
844: *
845: * Assumes that Pages has turned on this Page's change tracking with a call to setTrackChanges().
846: * Pages that are new (i.e. don't yet exist in the DB) always return true.
847: *
848: * @param string $what If specified, only checks the given property for changes rather than the whole page.
849: * @return bool
850: *
851: */
852: public function isChanged($what = '') {
853: if($this->isNew()) return true;
854: if(parent::isChanged($what)) return true;
855: $changed = false;
856: if($what) {
857: $value = $this->get($what);
858: if(is_object($value) && $value instanceof Wire)
859: $changed = $value->isChanged();
860: } else {
861: foreach($this->data as $key => $value) {
862: if(is_object($value) && $value instanceof Wire)
863: $changed = $value->isChanged();
864: if($changed) break;
865: }
866: }
867:
868: return $changed;
869: }
870:
871:
872: /**
873: * Returns the Page's ID in a string
874: *
875: */
876: public function __toString() {
877: return "{$this->id}";
878: }
879:
880: /**
881: * Returns the Page's path from the site root.
882: *
883: */
884: public function path() {
885: return self::isHooked('Page::path()') ? $this->__call('path', array()) : $this->___path();
886: }
887:
888: /**
889: * Provides the hookable implementation for the path() method.
890: *
891: * The method we're using here by having a real path() function above is slightly quicker than just letting
892: * PW's hook handler handle it all. We're taking this approach since path() is a function that can feasibly
893: * be called hundreds or thousands of times in a request, so we want it as optimized as possible.
894: *
895: */
896: protected function ___path() {
897: if($this->id === 1) return '/';
898: $path = '';
899: $parents = $this->parents();
900: foreach($parents as $parent) if($parent->id > 1) $path .= "/{$parent->name}";
901: return $path . '/' . $this->name . '/';
902: }
903:
904: /**
905: * Like path() but comes from server document root (which may or may not be different)
906: *
907: * Does not include urlSegment, if applicable.
908: * Does not include protocol and hostname -- use httpUrl() for that.
909: *
910: * @see path
911: *
912: */
913: public function url() {
914: $url = rtrim($this->fuel('config')->urls->root, "/") . $this->path();
915: if($this->template->slashUrls === 0 && $this->settings['id'] > 1) $url = rtrim($url, '/');
916: return $url;
917: }
918:
919: /**
920: * Like URL, but includes the protocol and hostname
921: *
922: */
923: public function httpUrl() {
924:
925: switch($this->template->https) {
926: case -1: $protocol = 'http'; break;
927: case 1: $protocol = 'https'; break;
928: default: $protocol = $this->fuel('config')->https ? 'https' : 'http';
929: }
930:
931: return "$protocol://" . $this->fuel('config')->httpHost . $this->url();
932: }
933:
934: /**
935: * Get the output TemplateFile object for rendering this page
936: *
937: * You can retrieve the results of this by calling $page->out or $page->output
938: *
939: * @return TemplateFile
940: *
941: */
942: protected function getTemplateFile() {
943: if($this->output) return $this->output;
944: if(!$this->template) return null;
945: $this->output = new TemplateFile($this->template->filename);
946: $fuel = self::getAllFuel();
947: $this->output->set('wire', $fuel);
948: foreach($fuel as $key => $value) $this->output->set($key, $value);
949: $this->output->set('page', $this);
950: return $this->output;
951: }
952:
953:
954: /**
955: * Return a Inputfield object that contains all the custom Inputfield objects required to edit this page
956: *
957: */
958: public function getInputfields() {
959: return $this->template ? $this->template->fieldgroup->getPageInputfields($this) : null;
960: }
961:
962:
963: /**
964: * Does this page have the specified status number or template name?
965: *
966: * See status flag constants at top of Page class
967: *
968: * @param int|string|Selectors $status Status number or Template name or selector string/object
969: * @return bool
970: *
971: */
972: public function is($status) {
973:
974: if(is_int($status)) {
975: return ((bool) ($this->status & $status));
976:
977: } else if(is_string($status) && $this->fuel('sanitizer')->name($status) == $status) {
978: // valid template name
979: if($this->template->name == $status) return true;
980:
981: } else if($this->matches($status)) {
982: // Selectors object or selector string
983: return true;
984: }
985:
986: return false;
987: }
988:
989: /**
990: * Given a Selectors object or a selector string, return whether this Page matches it
991: *
992: * @param string|Selectors $s
993: * @return bool
994: *
995: */
996: public function matches($s) {
997:
998: if(is_string($s)) {
999: if(!Selectors::stringHasOperator($s)) return false;
1000: $selectors = new Selectors($s);
1001:
1002: } else if($s instanceof Selectors) {
1003: $selectors = $s;
1004:
1005: } else {
1006: return false;
1007: }
1008:
1009: $matches = false;
1010:
1011: foreach($selectors as $selector) {
1012: $name = $selector->field;
1013: if(in_array($name, array('limit', 'start', 'sort', 'include'))) continue;
1014: $matches = true;
1015: $value = $this->get($name);
1016: if(!$selector->matches("$value")) {
1017: $matches = false;
1018: break;
1019: }
1020: }
1021:
1022: return $matches;
1023: }
1024:
1025: /**
1026: * Add the specified status flag to this page's status
1027: *
1028: * @param int $statusFlag
1029: * @return this
1030: *
1031: */
1032: public function addStatus($statusFlag) {
1033: $statusFlag = (int) $statusFlag;
1034: $this->status = $this->status | $statusFlag;
1035: return $this;
1036: }
1037:
1038: /**
1039: * Remove the specified status flag from this page's status
1040: *
1041: * @param int $statusFlag
1042: * @return this
1043: *
1044: */
1045: public function removeStatus($statusFlag) {
1046: $statusFlag = (int) $statusFlag;
1047: $override = $this->settings['status'] & Page::statusSystemOverride;
1048: if($statusFlag == Page::statusSystem || $statusFlag == Page::statusSystemID) {
1049: if(!$override) throw new WireException("You may not remove the 'system' status from a page");
1050: }
1051: $this->status = $this->status & ~$statusFlag;
1052: return $this;
1053: }
1054:
1055: /**
1056: * Does this page have a 'hidden' status?
1057: *
1058: * @return bool
1059: *
1060: */
1061: public function isHidden() {
1062: return $this->is(self::statusHidden);
1063: }
1064:
1065: /**
1066: * Is this Page new? (i.e. doesn't yet exist in DB)
1067: *
1068: */
1069: public function isNew() {
1070: return $this->isNew;
1071: }
1072:
1073: /**
1074: * Is the page fully loaded?
1075: *
1076: */
1077: public function isLoaded() {
1078: return $this->isLoaded;
1079: }
1080:
1081: /**
1082: * Is this Page in the trash?
1083: *
1084: * @return bool
1085: *
1086: */
1087: public function isTrash() {
1088: if($this->is(self::statusTrash)) return true;
1089: $trashPageID = $this->fuel('config')->trashPageID;
1090: if($this->id == $trashPageID) return true;
1091: // this is so that isTrash() still returns the correct result, even if the page was just trashed and not yet saved
1092: foreach($this->parents() as $parent) if($parent->id == $trashPageID) return true;
1093: return false;
1094: }
1095:
1096: /**
1097: * Set the value for isNew, i.e. doesn't exist in the DB
1098: *
1099: * @param bool @isNew
1100: * @return this
1101: *
1102: */
1103: public function setIsNew($isNew) {
1104: $this->isNew = $isNew ? true : false;
1105: return $this;
1106: }
1107:
1108: /**
1109: * Set that the Page is fully loaded
1110: *
1111: * Pages::getById sets this once it has completed loading the page
1112: * This method also triggers the loaded() method that hooks may listen to
1113: *
1114: * @param bool $isLoaded
1115: *
1116: */
1117: public function setIsLoaded($isLoaded) {
1118: if($isLoaded) {
1119: $this->processFieldDataQueue();
1120: unset(Page::$loadingStack[$this->settings['id']]);
1121: }
1122: $this->isLoaded = $isLoaded ? true : false;
1123: if($isLoaded) $this->loaded();
1124: return $this;
1125: }
1126:
1127: /**
1128: * Process and instantiate any data in the fieldDataQueue
1129: *
1130: * This happens after setIsLoaded(true) is called
1131: *
1132: */
1133: protected function processFieldDataQueue() {
1134:
1135: foreach($this->fieldDataQueue as $key => $value) {
1136:
1137: $field = $this->fieldgroup->get($key);
1138: if(!$field) continue;
1139:
1140: // check for autojoin multi fields, which may have multiple values bundled into one string
1141: // as a result of an sql group_concat() function
1142: if($field->type instanceof FieldtypeMulti && ($field->flags & Field::flagAutojoin)) {
1143: foreach($value as $k => $v) {
1144: if(is_string($v) && strpos($v, FieldtypeMulti::multiValueSeparator) !== false) {
1145: $value[$k] = explode(FieldtypeMulti::multiValueSeparator, $v);
1146: }
1147: }
1148: }
1149:
1150: // if all there is in the array is 'data', then we make that the value rather than keeping an array
1151: // this is so that Fieldtypes that only need to interact with a single value don't have to receive an array of data
1152: if(count($value) == 1 && array_key_exists('data', $value)) $value = $value['data'];
1153:
1154: $this->setFieldValue($key, $value, false);
1155: }
1156: $this->fieldDataQueue = array(); // empty it out, no longer needed
1157: }
1158:
1159:
1160: /**
1161: * For hooks to listen to, triggered when page is loaded and ready
1162: *
1163: */
1164: public function ___loaded() { }
1165:
1166:
1167: /**
1168: * Set if this page's output is allowed to be filtered by runtime formatters.
1169: *
1170: * Pages used for output should have it on.
1171: * Pages you intend to manipulate and save should have it off.
1172: *
1173: * @param bool @outputFormatting Optional, default true
1174: * @return this
1175: *
1176: */
1177: public function setOutputFormatting($outputFormatting = true) {
1178: $this->outputFormatting = $outputFormatting ? true : false;
1179: return $this;
1180: }
1181:
1182: /**
1183: * Return true if outputFormatting is on, false if not.
1184: *
1185: * @return bool
1186: *
1187: */
1188: public function outputFormatting() {
1189: return $this->outputFormatting;
1190: }
1191:
1192: /**
1193: * Shorter version of setOutputFormatting() and outputFormatting() function
1194: *
1195: * Always returns the current state of outputFormatting like the outputFormatting() function (and unlike setOutputFormatting())
1196: * You may optionally specify a boolean value for $outputFormatting which will set the current state, like setOutputFormatting().
1197: *
1198: * @param bool $outputFormatting If specified, sets outputFormatting ON or OFF. If not specified, outputFormatting status does not change.
1199: * @return bool Current outputFormatting state.
1200: *
1201: */
1202: public function of($outputFormatting = null) {
1203: if(!is_null($outputFormatting)) $this->outputFormatting = $outputFormatting ? true : false;
1204: return $this->outputFormatting;
1205: }
1206:
1207: /**
1208: * Return instance of PagefileManager specific to this Page
1209: *
1210: * @return PageFilesManager
1211: *
1212: */
1213: public function filesManager() {
1214: if(is_null($this->filesManager)) $this->filesManager = new PagefilesManager($this);
1215: return $this->filesManager;
1216: }
1217:
1218: /**
1219: * Prepare the page and it's fields for removal from runtime memory, called primarily by Pages::uncache()
1220: *
1221: */
1222: public function uncache() {
1223: if($this->template) {
1224: foreach($this->template->fieldgroup as $field) {
1225: $value = parent::get($field->name);
1226: if($value != null && is_object($value)) {
1227: if(method_exists($value, 'uncache')) $value->uncache();
1228: parent::set($field->name, null);
1229: }
1230: }
1231: }
1232: if($this->filesManager) $this->filesManager->uncache();
1233: $this->filesManager = null;
1234: }
1235:
1236: /**
1237: * Ensures that isset() and empty() work for this classes properties.
1238: *
1239: */
1240: public function __isset($key) {
1241: if(isset($this->settings[$key])) return true;
1242: return parent::__isset($key);
1243: }
1244:
1245: /**
1246: * Returns the parent page that has the template from which we get our role/access settings from
1247: *
1248: * @return Page|NullPage Returns NullPage if none found
1249: *
1250: */
1251: public function getAccessParent() {
1252: if($this->template->useRoles || $this->settings['id'] === 1) return $this;
1253: $parent = $this->parent();
1254: if($parent->id) return $parent->getAccessParent();
1255: return new NullPage();
1256: }
1257:
1258: /**
1259: * Returns the template from which we get our role/access settings from
1260: *
1261: * @return Template|null Returns null if none
1262: *
1263: */
1264: public function getAccessTemplate() {
1265: $parent = $this->getAccessParent();
1266: if(!$parent->id) return null;
1267: return $parent->template;
1268: }
1269:
1270: /**
1271: * Return the PageArray of roles that have access to this page
1272: *
1273: * This is determined from the page's template. If the page's template has roles turned off,
1274: * then it will go down the tree till it finds usable roles to use.
1275: *
1276: * @return PageArray
1277: *
1278: */
1279: public function getAccessRoles() {
1280: $template = $this->getAccessTemplate();
1281: if($template) return $template->roles;
1282: return new PageArray();
1283: }
1284:
1285: /**
1286: * Returns whether this page has the given access role
1287: *
1288: * Given access role may be a role name, role ID or Role object
1289: *
1290: * @param string|int|Role $role
1291: * @return bool
1292: *
1293: */
1294: public function hasAccessRole($role) {
1295: $roles = $this->getAccessRoles();
1296: if(is_string($role)) return $roles->has("name=$role");
1297: if($role instanceof Role) return $roles->has($role);
1298: if(is_int($role)) return $roles->has("id=$role");
1299: return false;
1300: }
1301:
1302:
1303: /** REMOVED
1304: public function roles() {}
1305: public function addRole($role) {}
1306: public function addsRole($role) {}
1307: public function hasRole($role) {}
1308: public function removeRole($role) {}
1309: public function removesRole($role) {}
1310: */
1311:
1312: }
1313:
1314: /**
1315: * Placeholder class for non-existant and non-saveable Page
1316: *
1317: */
1318: class NullPage extends Page {
1319:
1320: // public function roles() { return new RolesArray(); }
1321: public function path() { return ''; }
1322: public function url() { return ''; }
1323: public function set($key, $value) { return $this; }
1324: public function parent() { return null; }
1325: public function parents() { return new PageArray(); }
1326: public function __toString() { return ""; }
1327: public function isHidden() { return true; }
1328: public function filesManager() { return null; }
1329: public function rootParent() { return new NullPage(); }
1330: public function siblings($selector = '', $options = array()) { return new PageArray(); }
1331: public function children($selector = '', $options = array()) { return new PageArray(); }
1332: public function getAccessParent() { return new NullPage(); }
1333: public function getAccessRoles() { return new PageArray(); }
1334: public function hasAccessRole($role) { return false; }
1335:
1336: }
1337:
1338:
1339:
1340: