get($key); // test if any parts of key can potentially refer to API variables foreach($keys as $key) { if($page->wire($key) || $key === 'pass') return null; } $value = $page; $values = array(); $wireArrayProperties = array('first', 'last', 'count', 'keys', 'values'); do { $key = array_shift($keys); $k = $key; $index = ''; // k is key without brackets (if any were present) if(strpos($k, '[')) { list($k, $index) = explode('[', $k, 2); $index = rtrim($index, ']'); if(ctype_digit($index)) $index = (int) $index; } if($value instanceof Page && !in_array($key, $wireArrayProperties)) { // value is a Page if(isset(PageProperties::$traversalReturnTypes[$k])) { // traversal property: Page or PageArray // parent, rootParent, child, next, prev, children, parents, siblings $value = $value->$key(); } else { // native base property or custom field value $value = $value->get($key); } } else if($value instanceof WireArray) { if(in_array($k, $wireArrayProperties)) { $value = $value->getProperty($k); } else { $value = $value->each($k); // convert PHP array to WireArray if there are keys remaining (for next round) if(count($keys)) $value = $page->wire(WireArray($value)); } if(is_int($index)) { // index is integer if(WireArray::iterable($value)) { $value = isset($value[$index]) ? $value[$index] : null; } } else if($index) { // index is selector if($value instanceof WireArray) $value = $value->find($index); } } else if($value instanceof WireData) { $v = $value->get($key); if($v === null) switch($key) { // self-generated equivalents for WireArray properties/methods case 'first': case 'last': $v = $value; break; case 'count': $v = 1; break; case 'values': $v = array($value); break; case 'keys': $v = ("$value" === $value->className() ? array(0) : array("$value")); break; } $value = $v; } else if(is_array($value)) { foreach($value as $kk => $vv) { $value[$kk] = $vv instanceof Wire ? $vv->$k : $vv; } } else { $value = null; } $values[] = $value; } while($value !== null && count($keys)); // if($value === null && count($values) && $values[0] !== null) { // if first key didn't return null then try again with WireData native method // $value = $page->getDot($_key); // } return $value; } /** * Get value that ends with square brackets to get iterable value, filtered value or property value * * ~~~~~ * $iterableValue = $page->get('field_name[]'); * ~~~~~ * Note: When requesting an iterable value, this method will return an empty array in cases where * the Page::get() method would return null. * * @param Page $page * @param string $key * @param mixed $value Value to use rather than pulling from $page * @return array|mixed|Page|PageArray|Wire|WireArray|WireData|string|\Traversable * */ public function getBracketValue(Page $page, $key, $value = null) { if(strpos($key, '.')) return $this->getDotValue($page, $key); if(substr($key, -1) !== ']') return null; $property = ''; $selector = ''; $getIterable = true; $key = rtrim($key, ']'); list($key, $index) = explode('[', $key, 2); if(strpos($index, '][')) { // i.e. field[selector][0] list($selector, $index) = explode('][', $index); } if(ctype_digit($index)) { $index = (int) $index; $getIterable = false; } if($index !== '' && !is_int($index)) { if(ctype_alnum(str_replace('_', '', $index))) { $property = $index; } else { $selector = $index; } $index = ''; } if($value === null) { if($selector) { // filter FieldtypeMulti values at DB level // using $page->field_name($selector) method feature $value = $page->$key($selector); $selector = ''; } else { $value = $page->get($key); } } if($value === null) { return $getIterable ? array() : null; } if(is_object($value)) { if($value instanceof Page) { $value = $page->wire()->pages->newPageArray()->add($value); } else if($value instanceof WireArrayItem) { $value = $value->getWireArray(); } else if($value instanceof WireData) { $value = $page->wire(WireArray([$value])); } else if($value instanceof \Traversable) { // WireArray or other } else { $value = array($value); } } else if($getIterable) { $value = is_array($value) ? $value : array($value); } if($property !== '') { if($value instanceof WireArray) { $value = $value->each($property); } } else if($selector !== '') { if($value instanceof WireArray) { $value = $value->find($selector); $value->resetTrackChanges(); } } else if(is_int($index)) { if($value instanceof WireArray){ $value = $value->eq($index); } else if(is_array($value) || $value instanceof \ArrayAccess) { $value = isset($value[$index]) ? $value[$index] : null; } else if(WireArray::iterable($value)) { $n = 0; $found = false; foreach($value as $v) { if($n === $index) { $value = $v; $found = true; break; } $n++; } if(!$found) $value = null; } } return $value; } /** * Get multiple Page property/field values in an array * * This method works exactly the same as the `get()` method except that it accepts an * array (or CSV string) of properties/fields to get, and likewise returns an array * of those property/field values. By default it returns a regular (non-indexed) PHP * array in the same order given. To instead get an associative array indexed by the * property/field names given, specify `true` for the `$assoc` argument. * * ~~~~~ * // returns regular array i.e. [ 'foo val', 'bar val' ] * $a = $page->getMultiple([ 'foo', 'bar' ]); * list($foo, $bar) = $a; * * // returns associative array i.e. [ 'foo' => 'foo val', 'bar' => 'bar val' ] * $a = $page->getMultiple([ 'foo', 'bar' ], true); * $foo = $a['foo']; * $bar = $a['bar']; * * // CSV string can also be used instead of array * $a = $page->getMultiple('foo,bar'); * ~~~~~ * * @param page $page * @param array|string $keys Array or CSV string of properties to get. * @param bool $assoc Get associative array indexed by given properties? (default=false) * @return array * */ public function getMultiple(Page $page, $keys, $assoc = false) { if(!is_array($keys)) { $keys = (string) $keys; if(strpos($keys, ',') !== false) { $keys = explode(',', $keys); } else { $keys = array($keys); } } $values = array(); foreach($keys as $key) { $key = trim("$key"); $value = strlen($key) ? $page->get($key) : null; if($assoc) { $values[$key] = $value; } else { $values[] = $value; } } return $values; } /** * Given a Multi Key, determine if there are multiple keys requested and return the first non-empty value * * A Multi Key is a string with multiple field names split by pipes, i.e. headline|title * * Example: browser_title|headline|title - Return the value of the first field that is non-empty * * @param page $page * @param string $multiKey * @param bool $getKey Specify true to get the first matching key (name) rather than value * @return null|mixed Returns null if no values match, or if there aren't multiple keys split by "|" chars * */ public function getFieldFirstValue(Page $page, $multiKey, $getKey = false) { // looking multiple keys split by "|" chars, and not an '=' selector if(strpos($multiKey, '|') === false || strpos($multiKey, '=') !== false) return null; $value = null; $keys = explode('|', $multiKey); foreach($keys as $key) { $v = $page->getUnformatted($key); if(is_array($v) || $v instanceof WireArray) { // array or WireArray if(!count($v)) continue; } else if(is_object($v)) { // like LanguagesPageFieldValue $str = trim((string) $v); if(!strlen($str)) continue; } else if(is_string($v)) { $v = trim($v); } if($v) { if($page->of()) { $v = $page->get($key); } if($v) { $value = $getKey ? $key : $v; break; } } } return $value; } /** * Return the markup value for a given field name or {tag} string * * 1. If given a field name (or `name.subname` or `name1|name2|name3`) it will return the * markup value as defined by the fieldtype. * 2. If given a string with field names referenced in `{tags}`, it will populate those * tags and return the populated string. * * @param Page $page * @param string $key Field name or markup string with field {name} tags in it * @return string * @see Page::getText() * */ public function getMarkup(Page $page, $key) { if(strpos($key, '{') !== false && strpos($key, '}')) { // populate a string with {tags} // note that the wirePopulateStringTags() function calls back on this method // to retrieve the markup values for each of the found field names return wirePopulateStringTags($key, $page); } if(strpos($key, '|') !== false) { $key = $this->getFieldFirstValue($page, $key, true); if(!$key) return ''; } if($this->wire()->sanitizer->name($key) != $key) { // not a possible field name return ''; } $parts = strpos($key, '.') ? explode('.', $key) : array($key); $value = $page; do { $name = array_shift($parts); $field = $page->getField($name); if(!$field && $this->wire($name)) { // disallow API vars $value = ''; break; } if($value instanceof Page) { $value = $value->getFormatted($name); } else if($value instanceof WireData) { $value = $value->get($name); } else { $value = $value->$name; } if($field && count($parts) < 2) { // this is a field that will provide its own formatted value $subname = count($parts) == 1 ? array_shift($parts) : ''; if(!$subname || !$this->wire($subname)) { $value = $field->type->markupValue($page, $field, $value, $subname); } } } while(is_object($value) && count($parts)); if(is_object($value)) { if($value instanceof Page) $value = $value->getFormatted('title|name'); if($value instanceof PageArray) $value = $value->getMarkup(); } if(!is_string($value)) $value = (string) $value; return $value; } /** * Same as getMarkup() except returned value is plain text * * If no `$entities` argument is provided, returned value is entity encoded when output formatting * is on, and not entity encoded when output formatting is off. * * @param Page $page * @param string $key Field name or string with field {name} tags in it. * @param bool $oneLine Specify true if returned value must be on single line. * @param bool|null $entities True to entity encode, false to not. Null for auto, which follows page's outputFormatting state. * @return string * @see Page::getMarkup() * */ public function getText(Page $page, $key, $oneLine = false, $entities = null) { $value = $page->getMarkup($key); $length = strlen($value); if(!$length) return ''; $options = array( 'entities' => ($entities === null ? $page->of() : (bool) $entities) ); $sanitizer = $this->wire()->sanitizer; if($oneLine) { $value = $sanitizer->markupToLine($value, $options); } else { $value = $sanitizer->markupToText($value, $options); } // if stripping tags from non-empty value made it empty, just indicate that it was markup and length if(!strlen(trim($value))) $value = "markup($length)"; return $value; } /** * Set the status setting, with some built-in protections * * This method is also used when you set status directly, i.e. `$page->status = $value;`. * * ~~~~~ * // set status to unpublished * $page->setStatus('unpublished'); * * // set status to hidden and unpublished * $page->setStatus('hidden, unpublished'); * * // set status to hidden + unpublished using Page constant bitmask * $page->setStatus(Page::statusHidden | Page::statusUnpublished); * ~~~~~ * * @param Page $page * @param int|array|string Status value, array of status names or values, or status name string. * @return Page * @see Page::addStatus(), Page::removeStatus() * */ public function setStatus(Page $page, $value) { if(!is_int($value)) { // status provided as something other than integer if(is_string($value) && !ctype_digit($value)) { // string of one or more status names if(strpos($value, ',') !== false) $value = str_replace(array(', ', ','), ' ', $value); $value = explode(' ', strtolower($value)); } if(is_array($value)) { // array of status names or numbers $status = 0; foreach($value as $v) { if(is_int($v) || ctype_digit("$v")) { // integer $status = $status | ((int) $v); } else if(is_string($v) && isset(PageProperties::$statuses[$v])) { // string (status name) $status = $status | PageProperties::$statuses[$v]; } } if($status) $value = $status; } // note if $value started as an integer string, i.e. "123", it gets passed through to below } $value = (int) $value; $status = $page->_getSetting('status'); $override = $status & Page::statusSystemOverride; if(!$override) { if($status & Page::statusSystemID) $value = $value | Page::statusSystemID; if($status & Page::statusSystem) $value = $value | Page::statusSystem; } $page->_setSetting('status', $value); if($value & Page::statusDeleted) { // disable any instantiated filesManagers after page has been marked deleted // example: uncache method polls filesManager $page->__unset('filesManager'); } return $page; } /** * Remove the specified status from this page * * This is the preferred way to remove a status from a page. There is also a corresponding `Page::addStatus()` method. * * ~~~~~ * // Remove hidden status from the page using status name * $page->removeStatus('hidden'); * * // Remove hidden status from the page using status constant * $page->removeStatus(Page::statusHidden); * ~~~~~ * * @param Page $page * @param int|string $statusFlag Status flag constant or string representation (hidden, locked, unpublished, etc.) * @return Page * @throws WireException If you attempt to remove `Page::statusSystem` or `Page::statusSystemID` statuses without first adding `Page::statusSystemOverride` status. * @see Page::addStatus(), Page::hasStatus() * */ public function removeStatus(Page $page, $statusFlag) { if(is_string($statusFlag) && isset(PageProperties::$statuses[$statusFlag])) { $statusFlag = PageProperties::$statuses[$statusFlag]; } $statusFlag = (int) $statusFlag; $status = $page->_getSetting('status'); $override = $status & Page::statusSystemOverride; if($statusFlag == Page::statusSystem || $statusFlag == Page::statusSystemID) { if(!$override) throw new WireException('Cannot remove statusSystem from page without statusSystemOverride'); } $page->status = $status & ~$statusFlag; return $page; } /** * Set the page name, optionally for specific language * * ~~~~~ * // Set page name (default language) * $page->setName('my-page-name'); * * // This is equivalent to the above * $page->name = 'my-page-name'; * * // Set page name for Spanish language * $page->setName('la-cerveza', 'es'); * ~~~~~ * * @param string $value Page name that you want to set * @param Language|string|int|null $language Set language for name (can also be language name or string in format "name1234") * @return Page * */ public function setName(Page $page, $value, $language = null) { $key = 'name'; $charset = $this->wire()->config->pageNameCharset; $sanitizer = $this->wire()->sanitizer; $isLoaded = $page->isLoaded(); if($isLoaded) { if(is_int($language)) { $key .= $language; $existingValue = $page->get($key); } else if($language && $language !== 'name') { // update $key to contain language ID when applicable $languages = $this->wire()->languages; if($languages) { if(!is_object($language)) { if(strpos($language, 'name') === 0) $language = (int) substr($language, 4); $language = $languages->getLanguage($language); if(!$language || !$language->id || $language->isDefault()) $language = ''; } if(!$language) return $page; $key .= $language->id; } $existingValue = $page->get($key); } else { $existingValue = $page->_getSetting($key); if($existingValue === null) $existingValue = ''; } // name is being set after page has already been loaded if($charset === 'UTF8') { // UTF8 page names allowed but decoding not allowed $value = $sanitizer->pageNameUTF8($value); } else if(empty($existingValue)) { // ascii, and beautify if there is no existing value $value = $sanitizer->pageName($value, true); } else { // ascii page name and do not beautify $value = $sanitizer->pageName($value, false); } } else { // name being set while page is loading if($charset === 'UTF8' && strpos("$value", 'xn-') === 0) { // allow decode of UTF8 name while page is loading $value = $sanitizer->pageName($value, Sanitizer::toUTF8); } else { // regular ascii page name while page is loading, do nothing to it } if($language) { if(ctype_digit("$language")) { $key = "name$language"; } else if(is_string($language)) { $key = $language; // i.e. name1234 } } } if($key === 'name') { $page->_setSetting($key, $value); } else if(!$isLoaded || $page->_getSetting('quietMode')) { $page->_parentSet($key, $value); } else { $this->setFieldValue($page, $key, $value, $isLoaded); // i.e. name1234 } return $page; } /** * Return all Inputfield objects necessary to edit this page * * This method returns an InputfieldWrapper object that contains all the custom Inputfield objects * required to edit this page. You may also specify a `$fieldName` argument to limit what is contained * in the returned InputfieldWrapper. * * Please note this method deals only with custom fields, not system fields name 'name' or 'status', etc., * as those are exclusive to the ProcessPageEdit page editor. * * #pw-advanced * * @param string|array $fieldName Optional field to limit to, typically the name of a fieldset or tab. * - Or optionally specify array of $options (See `Fieldgroup::getPageInputfields()` for options). * @return null|InputfieldWrapper Returns an InputfieldWrapper array of Inputfield objects, or NULL on failure. * */ public function getInputfields(Page $page, $fieldName = '') { $of = $page->of(); if($of) $page->of(false); $template = $page->template(); $fieldgroup = $template ? $template->fieldgroup : null; if($fieldgroup) { if(is_array($fieldName) && !ctype_digit(implode('', array_keys($fieldName)))) { // fieldName is an associative array of options for Fieldgroup::getPageInputfields $wrapper = $fieldgroup->getPageInputfields($page, $fieldName); } else { $wrapper = $fieldgroup->getPageInputfields($page, '', $fieldName); } } else { $wrapper = null; } if($of) $page->of(true); return $wrapper; } /** * Get a single Inputfield for the given field name * * - If requested field name refers to a single field, an Inputfield object is returned. * - If requested field name refers to a fieldset or tab, then an InputfieldWrapper representing will be returned. * - Returned Inputfield already has values populated to it. * - Please note this method deals only with custom fields, not system fields name 'name' or 'status', etc., * as those are exclusive to the ProcessPageEdit page editor. * * #pw-advanced * * @param string $fieldName * @return Inputfield|InputfieldWrapper|null Returns Inputfield, or null if given field name doesn't match field for this page. * */ public function getInputfield(Page $page, $fieldName) { $inputfields = $this->getInputfields($page, $fieldName); if($inputfields) { $field = $this->wire()->fields->get($fieldName); if($field && $field->type instanceof FieldtypeFieldsetOpen) { // requested field name is a fieldset, returns InputfieldWrapper return $inputfields; } else { // requested field name is a single field, return Inputfield return $inputfields->children()->first(); } } else { // requested field name is not applicable to this page return null; } } /** * Get the icon name associated with this Page (if applicable) * * #pw-internal * * @param Page $page * @return string * */ public function getIcon(Page $page) { $template = $page->template(); if(!$template) return ''; if($page->hasField('process')) { $process = $page->getUnformatted('process'); if($process) { $info = $this->wire()->modules->getModuleInfoVerbose($process); if(!empty($info['icon'])) return $info['icon']; } } return $template->getIcon(); } /** * Process and instantiate any data in the fieldDataQueue * * This happens after setIsLoaded(true) is called * * #pw-internal * * @param Page $page * @param array $fieldDataQueue * @return bool * */ public function processFieldDataQueue(Page $page, array $fieldDataQueue) { $template = $page->template(); if(!$template) return false; $fieldgroup = $template->fieldgroup; if(!$fieldgroup) return false; foreach($fieldDataQueue as $key => $value) { $field = $fieldgroup->get($key); if(!$field) continue; // check for autojoin multi fields, which may have multiple values bundled into one string // as a result of an sql group_concat() function if($field->type instanceof FieldtypeMulti && ($field->flags & Field::flagAutojoin)) { foreach($value as $k => $v) { if(is_string($v) && strpos($v, FieldtypeMulti::multiValueSeparator) !== false) { $value[$k] = explode(FieldtypeMulti::multiValueSeparator, $v); } } } // if all there is in the array is 'data', then we make that the value rather than keeping an array // this is so that Fieldtypes that only need to interact with a single value don't have to receive an array of data if(count($value) == 1 && array_key_exists('data', $value)) $value = $value['data']; $this->setFieldValue($page, $key, $value, false); } $page->fieldDataQueue(array()); return true; } /** * Get a Field object in context or NULL if not valid for this page * * Field in context is only returned when output formatting is on. * * #pw-advanced * * @param Page $page * @param string|int|Field $field * @return Field|null * @todo determine if we can always retrieve in context regardless of output formatting. * */ public function getField(Page $page, $field) { $template = $page->template(); $fieldgroup = $template ? $template->fieldgroup : null; if(!$fieldgroup) return null; if($page->of() && $fieldgroup->hasFieldContext($field)) { $value = $fieldgroup->getFieldContext($field); } else { $value = $fieldgroup->getField($field); } return $value; } /** * Returns a FieldsArray of all Field objects in the context of this Page * * Unlike $page->fieldgroup (or its alias $page->fields), the fields returned from * this method are in the context of this page/template. Meaning returned Field * objects may have some properties that are different from the Field outside of * the context of this page. * * #pw-advanced * * @param Page $page * @return FieldsArray of Field objects * */ public function getFields(Page $page) { $template = $page->template(); $fields = new FieldsArray(); $this->wire($fields); if(!$template) return $fields; $fieldgroup = $template->fieldgroup; foreach($fieldgroup as $field) { if($fieldgroup->hasFieldContext($field)) { $field = $fieldgroup->getFieldContext($field); } if($field) $fields->add($field); } return $fields; } /** * Returns whether or not given $field name, ID or object is valid for this Page * * Note that this only indicates validity, not whether the field is populated. * * #pw-advanced * * @param Page $page * @param int|string|Field|array $field Field name, object or ID to check. * - In 3.0.126+ this may also be an array or pipe "|" separated string of field names to check. * @return bool|string True if valid, false if not. * - In 3.0.126+ returns first matching field name if given an array of field names or pipe separated string of field names. * */ public function hasField(Page $page, $field) { $template = $page->template(); if(!$template) return false; if(is_string($field) && strpos($field, '|') !== false) { $field = explode('|', $field); } if(is_array($field)) { $result = false; foreach($field as $f) { $f = trim($f); if(!empty($f) && $this->hasField($page, $f)) $result = $f; if($result) break; } } else { $result = $template->fieldgroup->hasField($field); } return $result; } /** * Get the output TemplateFile object for rendering this page (internal use only) * * You can retrieve the results of this by calling $page->out or $page->output * * #pw-internal * * @param bool $forceNew Forces it to return a new (non-cached) TemplateFile object (default=false) * @return TemplateFile|null * */ public function output(Page $page, $forceNew = false) { $template = $page->template(); if(!$template) return null; /** @var TemplateFile $output */ $output = $this->wire(new TemplateFile()); $output->setThrowExceptions(false); $output->setFilename($template->filename); $fuel = $this->wire()->fuel->getArray(); $output->set('wire', $this->wire()); foreach($fuel as $key => $value) $output->set($key, $value); $output->set('page', $page); return $output; } /** * Get the value for a non-native page field, and call upon Fieldtype to join it if not autojoined * * @param string $key Name of field to get * @param string $selector Optional selector to filter load by... * ...or, if not in selector format, it becomes an __invoke() argument for object values . * @return null|mixed * */ public function getFieldValue(Page $page, $key, $selector = '') { $template = $page->template(); if(!$template) return $page->_parentGet($key); $field = $this->getField($page, $key); $value = $page->_parentGet($key); if(!$field) return $value; // likely a runtime field, not part of our data /** @var Fieldtype $fieldtype */ $fieldtype = $field->type; $invokeArgument = ''; if($value !== null && $page->wakeupNameQueue($key)) { $value = $fieldtype->_callHookMethod('wakeupValue', array($page, $field, $value)); $value = $fieldtype->sanitizeValue($page, $field, $value); $trackChanges = $page->trackChanges(true); $page->setTrackChanges(false); $page->_parentSet($key, $value); $page->setTrackChanges($trackChanges); $page->wakeupNameQueue($key, false); } if($field->useRoles && $page->of()) { // API access may be limited when output formatting is ON if($field->flags & Field::flagAccessAPI) { // API access always allowed because of flag } else if($page->viewable($field)) { // User has view permission for this field } else { // API access is denied when output formatting is ON // so just return a blank value as defined by the Fieldtype // note: we do not store this blank value in the Page, so that // the real value can potentially be loaded later without output formatting $value = $fieldtype->getBlankValue($page, $field); return $this->formatFieldValue($page, $field, $value); } } if($value !== null && empty($selector)) { // if the non-filtered value is already loaded, return it return $this->formatFieldValue($page, $field, $value); } $track = $page->trackChanges(); $page->setTrackChanges(false); if(!$fieldtype) return null; if($selector && !Selectors::stringHasSelector($selector)) { // if selector argument provided, but isn't valid, we assume it // to instead be an argument for the value's __invoke() method $invokeArgument = $selector; $selector = ''; } if($selector) { $value = $fieldtype->loadPageFieldFilter($page, $field, $selector); } else { $value = $fieldtype->_callHookMethod('loadPageField', array($page, $field)); } if($value === null) { $value = $fieldtype->getDefaultValue($page, $field); } else { $value = $fieldtype->_callHookMethod('wakeupValue', array($page, $field, $value)); } // turn off output formatting and set the field value, which may apply additional changes $of = $page->of(); if($of) $page->of(false); $this->setFieldValue($page, $key, $value, false); if($of) $page->of(true); $value = $page->_parentGet($key); // prevent storage of value if it was filtered when loaded if(!empty($selector)) $page->__unset($key); if($value instanceof Wire && !$value instanceof Page) $value->resetTrackChanges(true); if($track) $page->setTrackChanges(true); $value = $this->formatFieldValue($page, $field, $value); if($invokeArgument && is_object($value) && method_exists($value, '__invoke')) { $value = $value->__invoke($invokeArgument); } return $value; } /** * Return a value consistent with the page’s output formatting state * * This is primarily for use as a helper to the getFieldValue() method. * * @param Page $page * @param Field $field * @param mixed $value * @return mixed * */ public function formatFieldValue(Page $page, Field $field, $value) { $hasInterface = $value instanceof PageFieldValueInterface; if($hasInterface) { $value->setPage($page); $value->setField($field); } if($page->of()) { // output formatting is enabled so return a formatted value //$value = $field->type->formatValue($this, $field, $value); $value = $field->type->_callHookMethod('formatValue', array($page, $field, $value)); // check again for interface since value may now be different if($hasInterface) $hasInterface = $value instanceof PageFieldValueInterface; if($hasInterface) $value->formatted(true); } else if($hasInterface && $value->formatted()) { // unformatted requested, and value is already formatted so load a fresh copy $page->__unset($field->name); $value = $this->getFieldValue($page, $field->name); } return $value; } /** * Set the value of a field that is defined in the page's Fieldgroup * * This may not be called when outputFormatting is on. * * 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. * * #pw-internal * * @param Page $page * @param string $key * @param mixed $value * @param bool $load Should the existing value be loaded for change comparisons? (applicable only to non-autoload fields) * @return Page Returns reference to this Page * @throws WireException * */ public function setFieldValue(Page $page, $key, $value, $load = true) { if(!$page->template()) { throw new WireException("You must assign a template to the page before setting field values ($key)"); } $isLoaded = $page->isLoaded(); // if the page is not yet loaded and a '__' field was set, then we queue it so that the loaded() method can // instantiate all those fields knowing that all parts of them are present for wakeup. if(!$isLoaded && strpos($key, '__')) { list($key, $subKey) = explode('__', $key, 2); $fieldData = $page->fieldDataQueue($key); if($fieldData === null) $fieldData = array(); $fieldData[$subKey] = $value; $page->fieldDataQueue($key, $fieldData); return $page; } // check if the given key resolves to a Field or not $field = $this->getField($page, $key); if(!$field) { // not a known/saveable field, let them use it for runtime storage $valPrevious = $page->_parentGet($key); if($valPrevious !== null && $page->_parentGet("-$key") === null && $valPrevious !== $value) { // store previous value (if set) in a "-$key" version $page->setQuietly("-$key", $valPrevious); } $page->_parentSet($key, $value); return $page; } /** @var Fieldtype $fieldtype */ $fieldtype = $field->type; // if a null value is set, then ensure the proper blank type is set to the field if($value === null) { $page->_parentSet($key, $fieldtype->getBlankValue($page, $field)); return $page; } // if the page is currently loading from the database, we assume that any set values are 'raw' and need to be woken up if(!$page->isLoaded()) { // queue for wakeup and sanitize on first field access $page->wakeupNameQueue($key, true); // page is currently loading, so we don't need to continue any further $page->_parentSet($key, $value); return $page; } // check if the field hasn't been already loaded if($page->_parentGet($key) === null) { // this field is not currently loaded. if the $load param is true, then ... // retrieve old value first in case it's not autojoined so that change comparisons and save's work if($load) $page->get($key); } else if($page->wakeupNameQueue($key)) { // autoload value: we don't yet have a "woke" value suitable for change detection, so let it wakeup if($page->trackChanges() && $load) { // if changes are being tracked, load existing value for comparison $this->getFieldValue($page, $key); } else { // if changes aren't being tracked, the existing value can be discarded $page->wakeupNameQueue($key, false); } } else { // check if the field is corrupted $isCorrupted = false; if($value instanceof PageFieldValueInterface) { // value indicates it is already formatted, so would corrupt the page for saving if($value->formatted()) $isCorrupted = true; } else if($page->of()) { // check if value is modified by being formatted $result = $fieldtype->_callHookMethod('formatValue', array($page, $field, $value)); if($result != $value) $isCorrupted = true; } if($isCorrupted) { // The field has been loaded or dereferenced from the API, and this field changes when formatters are applied to it. // There is a good chance they are trying to set a formatted value, and we don't allow this situation because the // possibility of data corruption is high. We set the Page::statusCorrupted status so that Pages::save() can abort. $page->set('status', $page->status | Page::statusCorrupted); $corruptedFields = $page->get('_statusCorruptedFields'); if(!is_array($corruptedFields)) $corruptedFields = array(); $corruptedFields[$field->name] = $field->name; $page->set('_statusCorruptedFields', $corruptedFields); } } // isLoaded so sanitizeValue can determine if it can perform a typecast rather than a full sanitization (when helpful) // we don't use setIsLoaded() so as to avoid triggering any other functions $isLoaded = $page->isLoaded(); if(!$load) $page->setIsLoaded(false, true); // true=set quietly // ensure that the value is in a safe format and set it $value = $fieldtype->sanitizeValue($page, $field, $value); // Silently restore isLoaded state if(!$load) $page->setIsLoaded($isLoaded, true); $page->_parentSet($key, $value); return $page; } }