1920 lines
64 KiB
PHP
1920 lines
64 KiB
PHP
<?php namespace ProcessWire;
|
||
|
||
/**
|
||
* ProcessWire Pages Editor
|
||
*
|
||
* Implements page manipulation methods of the $pages API variable
|
||
*
|
||
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||
* https://processwire.com
|
||
*
|
||
*/
|
||
|
||
class PagesEditor extends Wire {
|
||
|
||
/**
|
||
* Are we currently cloning a page?
|
||
*
|
||
* This is greater than 0 only when the clone() method is currently in progress.
|
||
*
|
||
* @var int
|
||
*
|
||
*/
|
||
protected $cloning = 0;
|
||
|
||
/**
|
||
* @var Pages
|
||
*
|
||
*/
|
||
protected $pages;
|
||
|
||
/**
|
||
* Construct
|
||
*
|
||
* @param Pages $pages
|
||
*
|
||
*/
|
||
public function __construct(Pages $pages) {
|
||
parent::__construct();
|
||
$this->pages = $pages;
|
||
|
||
$config = $pages->wire()->config;
|
||
if($config->dbStripMB4 && strtolower($config->dbCharset) != 'utf8mb4') {
|
||
$this->addHookAfter('Fieldtype::sleepValue', $this, 'hookFieldtypeSleepValueStripMB4');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Are we currently in a page clone?
|
||
*
|
||
* @param bool $getDepth Get depth (int) rather than state (bool)?
|
||
* @return bool|int
|
||
*
|
||
*/
|
||
public function isCloning($getDepth = false) {
|
||
return $getDepth ? $this->cloning : $this->cloning > 0;
|
||
}
|
||
|
||
/**
|
||
* Add a new page using the given template to the given parent
|
||
*
|
||
* If no name is specified one will be assigned based on the current timestamp.
|
||
*
|
||
* @param string|Template $template Template name or Template object
|
||
* @param string|int|Page $parent Parent path, ID or Page object
|
||
* @param string $name Optional name or title of page. If none provided, one will be automatically assigned based on microtime stamp.
|
||
* If you want to specify a different name and title then specify the $name argument, and $values['title'].
|
||
* @param array $values Field values to assign to page (optional). If $name is omitted, this may also be 3rd param.
|
||
* @return Page Returned page has output formatting off.
|
||
* @throws WireException When some criteria prevents the page from being saved.
|
||
*
|
||
*/
|
||
public function add($template, $parent, $name = '', array $values = array()) {
|
||
|
||
// the $values may optionally be the 3rd argument
|
||
if(is_array($name)) {
|
||
$values = $name;
|
||
$name = isset($values['name']) ? $values['name'] : '';
|
||
}
|
||
|
||
if(!is_object($template)) {
|
||
$template = $this->wire()->templates->get($template);
|
||
if(!$template) throw new WireException("Unknown template");
|
||
}
|
||
|
||
$options = array('template' => $template, 'parent' => $parent);
|
||
if(isset($values['pageClass'])) {
|
||
$options['pageClass'] = $values['pageClass'];
|
||
unset($values['pageClass']);
|
||
}
|
||
$page = $this->pages->newPage($options);
|
||
|
||
$exceptionMessage = "Unable to add new page using template '$template' and parent '{$page->parent->path}'.";
|
||
|
||
if(empty($values['title'])) {
|
||
// no title provided in $values, so we assume $name is $title
|
||
// but if no name is provided, then we default to: Untitled Page
|
||
if(!strlen($name)) $name = $this->_('Untitled Page');
|
||
// the setupNew method will convert $page->title to a unique $page->name
|
||
$page->title = $name;
|
||
|
||
} else {
|
||
// title was provided
|
||
$page->title = $values['title'];
|
||
// if name is provided we use it
|
||
// otherwise setupNew will take care of assign it from title
|
||
if(strlen($name)) $page->name = $name;
|
||
unset($values['title']);
|
||
}
|
||
|
||
if(isset($values['status'])) {
|
||
$page->status = $values['status'];
|
||
unset($values['status']);
|
||
}
|
||
|
||
// save page before setting $values just in case any fieldtypes
|
||
// require the page to have an ID already (like file-based)
|
||
if(!$this->pages->save($page)) throw new WireException($exceptionMessage);
|
||
|
||
// set field values, if provided
|
||
if(!empty($values)) {
|
||
unset($values['id'], $values['parent'], $values['template']); // fields that may not be set from this array
|
||
foreach($values as $key => $value) $page->set($key, $value);
|
||
$this->pages->save($page);
|
||
}
|
||
|
||
// get a fresh copy of the page
|
||
if($page->id) {
|
||
$inserted = $page->_inserted;
|
||
$of = $this->pages->outputFormatting;
|
||
if($of) $this->pages->setOutputFormatting(false);
|
||
$p = $this->pages->getById($page->id, $template, $page->parent_id);
|
||
if($p->id) $page = $p;
|
||
if($of) $this->pages->setOutputFormatting(true);
|
||
$page->setQuietly('_inserted', $inserted);
|
||
}
|
||
|
||
return $page;
|
||
}
|
||
|
||
/**
|
||
* Is the given page in a state where it can be saved from the API?
|
||
*
|
||
* @param Page $page
|
||
* @param string $reason Text containing the reason why it can't be saved (assuming it's not saveable)
|
||
* @param string|Field $fieldName Optional fieldname to limit check to.
|
||
* @param array $options Options array given to the original save method (optional)
|
||
* @return bool True if saveable, False if not
|
||
*
|
||
*/
|
||
public function isSaveable(Page $page, &$reason, $fieldName = '', array $options = array()) {
|
||
|
||
$saveable = false;
|
||
$outputFormattingReason = "Call \$page->of(false); before getting/setting values that will be modified and saved.";
|
||
$corrupted = array();
|
||
|
||
if($fieldName && is_object($fieldName)) {
|
||
/** @var Field $fieldName */
|
||
$fieldName = $fieldName->name;
|
||
/** @var string $fieldName */
|
||
}
|
||
|
||
if($page->hasStatus(Page::statusCorrupted)) {
|
||
$corruptedFields = $page->_statusCorruptedFields;
|
||
foreach($page->getChanges() as $change) {
|
||
if(isset($corruptedFields[$change])) $corrupted[] = $change;
|
||
}
|
||
// if focused on a specific field...
|
||
if($fieldName && !in_array($fieldName, $corrupted)) $corrupted = array();
|
||
}
|
||
|
||
if($page instanceof NullPage) {
|
||
$reason = "Pages of type NullPage are not saveable";
|
||
} else if(!$page->parent_id && $page->id !== 1 && (!$page->parent || $page->parent instanceof NullPage)) {
|
||
$reason = "It has no parent assigned";
|
||
} else if(!$page->template) {
|
||
$reason = "It has no template assigned";
|
||
} else if(!strlen(trim($page->name)) && $page->id != 1) {
|
||
$reason = "It has an empty 'name' field";
|
||
} else if(count($corrupted)) {
|
||
$reason = $outputFormattingReason . " [Page::statusCorrupted] fields: " . implode(', ', $corrupted);
|
||
} else if($page->id == 1 && !$page->template->useRoles) {
|
||
$reason = "Selected homepage template cannot be used because it does not define access.";
|
||
} else if($page->id == 1 && !$page->template->hasRole('guest')) {
|
||
$reason = "Selected homepage template cannot be used because it does not have required 'guest' role in its access settings.";
|
||
} else {
|
||
$saveable = true;
|
||
}
|
||
|
||
// check if they could corrupt a field by saving
|
||
if($saveable && $page->outputFormatting) {
|
||
// iternate through recorded changes to see if any custom fields involved
|
||
foreach($page->getChanges() as $change) {
|
||
if($fieldName && $change != $fieldName) continue;
|
||
if($page->template->fieldgroup->getField($change) !== null) {
|
||
$reason = $outputFormattingReason . " [$change]";
|
||
$saveable = false;
|
||
break;
|
||
}
|
||
}
|
||
// iterate through already-loaded data to see if any are objects that have changed
|
||
if($saveable) foreach($page->getArray() as $key => $value) {
|
||
if($fieldName && $key != $fieldName) continue;
|
||
if(!$page->template->fieldgroup->getField($key)) continue;
|
||
if($value instanceof Wire && $value->isChanged()) {
|
||
$reason = $outputFormattingReason . " [$key]";
|
||
$saveable = false;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// check for a parent change and whether it is allowed
|
||
if($saveable && $page->id && $page->parentPrevious && empty($options['ignoreFamily'])) {
|
||
// parent has changed, check that the move is allowed
|
||
$saveable = $this->isMoveable($page, $page->parentPrevious, $page->parent, $reason);
|
||
}
|
||
|
||
return $saveable;
|
||
}
|
||
|
||
/**
|
||
* Return whether given Page is moveable from $oldParent to $newParent
|
||
*
|
||
* @param Page $page Page to move
|
||
* @param Page $oldParent Current/old parent page
|
||
* @param Page $newParent New requested parent page
|
||
* @param string $reason Populated with reason why page is not moveable, if return false is false.
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isMoveable(Page $page, Page $oldParent, Page $newParent, &$reason) {
|
||
|
||
if($oldParent->id == $newParent->id) return true;
|
||
|
||
$config = $this->wire()->config;
|
||
$moveable = false;
|
||
$isSystem = $page->hasStatus(Page::statusSystem) || $page->hasStatus(Page::statusSystemID);
|
||
$toTrash = $newParent->id > 0 && $newParent->isTrash();
|
||
$wasTrash = $oldParent->id > 0 && $oldParent->isTrash();
|
||
|
||
// page was moved
|
||
if($page->template->noMove && ($isSystem || (!$toTrash && !$wasTrash))) {
|
||
// make sure the page template allows moves.
|
||
// only move always allowed is to the trash (or out of it), unless page has system status
|
||
$reason =
|
||
sprintf($this->_('Page using template “%s” is not moveable.'), $page->template->name) . ' ' .
|
||
"(Template::noMove) [{$oldParent->path} => {$newParent->path}]";
|
||
|
||
} else if($newParent->template->noChildren) {
|
||
// check if new parent disallows children
|
||
$reason = sprintf(
|
||
$this->_('Chosen parent “%1$s” uses template “%2$s” that does not allow children.'),
|
||
$newParent->path,
|
||
$newParent->template->name
|
||
);
|
||
|
||
} else if($newParent->id && $newParent->id != $config->trashPageID && count($newParent->template->childTemplates)
|
||
&& !in_array($page->template->id, $newParent->template->childTemplates)) {
|
||
// make sure the new parent's template allows pages with this template
|
||
$reason = sprintf(
|
||
$this->_('Cannot move “%1$s” because template “%2$s” used by page “%3$s” does not allow children using template “%4$s”.'),
|
||
$page->name,
|
||
$newParent->template->name,
|
||
$newParent->path,
|
||
$page->template->name
|
||
);
|
||
|
||
} else if(count($page->template->parentTemplates) && $newParent->id != $config->trashPageID
|
||
&& !in_array($newParent->template->id, $page->template->parentTemplates)) {
|
||
// check for allowed parentTemplates setting
|
||
$reason = sprintf(
|
||
$this->_('Cannot move “%1$s” because template “%2$s” used by new parent “%3$s” is not allowed by moved page template “%4$s”.'),
|
||
$page->name,
|
||
$newParent->template->name,
|
||
$newParent->path,
|
||
$page->template->name
|
||
);
|
||
|
||
} else if(count($newParent->children("name=$page->name, id!=$page->id, include=all"))) {
|
||
// check for page name collision
|
||
$reason = sprintf(
|
||
$this->_('Chosen parent “%1$s” already has a page named “%2$s”.'),
|
||
$newParent->path,
|
||
$page->name
|
||
);
|
||
|
||
} else {
|
||
$moveable = true;
|
||
}
|
||
|
||
return $moveable;
|
||
}
|
||
|
||
/**
|
||
* Is the given page deleteable from the API?
|
||
*
|
||
* Note: this does not account for user permission checking. It only checks if the page is in a state to be saveable via the API.
|
||
*
|
||
* @param Page $page
|
||
* @param bool $throw Throw WireException with additional details?
|
||
* @return bool True if deleteable, False if not
|
||
* @throws WireException If requested to do so via $throw argument
|
||
*
|
||
*/
|
||
public function isDeleteable(Page $page, $throw = false) {
|
||
|
||
$error = false;
|
||
|
||
if($page instanceof NullPage) {
|
||
$error = "it is a NullPage";
|
||
} else if(!$page->id) {
|
||
$error = "it has no id";
|
||
} else if($page->hasStatus(Page::statusSystemID) || $page->hasStatus(Page::statusSystem)) {
|
||
$error = "it has “system” and/or “systemID” status";
|
||
} else if($page->hasStatus(Page::statusLocked)) {
|
||
$error = "it has “locked” status";
|
||
} else if($page->id === $this->wire()->page->id && $this->wire()->config->installedAfter('2019-04-04')) {
|
||
$error = "it is the current page being viewed, try \$pages->trash() instead";
|
||
}
|
||
|
||
if($error === false) return true;
|
||
if($throw) throw new WireException("Page $page->path ($page->id) cannot be deleted: $error");
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Auto-populate some fields for a new page that does not yet exist
|
||
*
|
||
* Currently it does this:
|
||
*
|
||
* - Assigns a parent if one is not already assigned.
|
||
* - Sets up a unique page->name based on the format or title if one isn't provided already.
|
||
* - Assigns a sort value.
|
||
* - Populates any default values for fields.
|
||
*
|
||
* @param Page $page
|
||
* @throws \Exception|WireException|\PDOException if failure occurs while in DB transaction
|
||
*
|
||
*/
|
||
public function setupNew(Page $page) {
|
||
|
||
$parent = $page->parent();
|
||
|
||
// assign parent
|
||
if(!$parent->id) {
|
||
$parentTemplates = $page->template->parentTemplates;
|
||
$parent = null;
|
||
|
||
if(!empty($parentTemplates)) {
|
||
$idStr = implode('|', $parentTemplates);
|
||
$parent = $this->pages->get("include=hidden, template=$idStr");
|
||
if(!$parent->id) $parent = $this->pages->get("include=all, template=$idStr");
|
||
}
|
||
|
||
if($parent->id) $page->parent = $parent;
|
||
}
|
||
|
||
// assign page name
|
||
if(!strlen($page->name)) {
|
||
$this->pages->setupPageName($page); // call through $pages intended, so it can be hooked
|
||
}
|
||
|
||
// assign sort order
|
||
if($page->sort < 0) {
|
||
$page->sort = ($parent->id ? $parent->numChildren() : 0);
|
||
}
|
||
|
||
// assign any default values for fields
|
||
foreach($page->template->fieldgroup as $field) {
|
||
/** @var Field $field */
|
||
if($page->isLoaded($field->name)) continue; // value already set
|
||
if(!$page->hasField($field)) continue; // field not valid for page
|
||
if(!strlen((string) $field->get('defaultValue'))) continue; // no defaultValue property defined with Fieldtype config inputfields
|
||
try {
|
||
$blankValue = $field->type->getBlankValue($page, $field);
|
||
if(is_object($blankValue) || is_array($blankValue)) continue; // we don't currently handle complex types
|
||
$defaultValue = $field->type->getDefaultValue($page, $field);
|
||
if(is_object($defaultValue) || is_array($defaultValue)) continue; // we don't currently handle complex types
|
||
if("$blankValue" !== "$defaultValue") {
|
||
$page->set($field->name, $defaultValue);
|
||
}
|
||
} catch(\Exception $e) {
|
||
$this->trackException($e, false, true);
|
||
if($this->wire()->database->inTransaction()) throw $e;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Auto-assign a page name to this page
|
||
*
|
||
* Typically this would be used only if page had no name or if it had a temporary untitled name.
|
||
*
|
||
* Page will be populated with the name given. This method will not populate names to pages that
|
||
* already have a name, unless the name is "untitled"
|
||
*
|
||
* @param Page $page
|
||
* @param array $options
|
||
* - format: Optionally specify the format to use, or leave blank to auto-determine.
|
||
* @return string If a name was generated it is returned. If no name was generated blank is returned.
|
||
*
|
||
*/
|
||
public function setupPageName(Page $page, array $options = array()) {
|
||
return $this->pages->names()->setupNewPageName($page, isset($options['format']) ? $options['format'] : '');
|
||
}
|
||
|
||
/**
|
||
* Save a page object and it's fields to database.
|
||
*
|
||
* If the page is new, it will be inserted. If existing, it will be updated.
|
||
*
|
||
* This is the same as calling $page->save()
|
||
*
|
||
* If you want to just save a particular field in a Page, use $page->save($fieldName) instead.
|
||
*
|
||
* @param Page $page
|
||
* @param array $options Optional array with the following optional elements:
|
||
* - `uncacheAll` (boolean): Whether the memory cache should be cleared (default=true)
|
||
* - `resetTrackChanges` (boolean): Whether the page's change tracking should be reset (default=true)
|
||
* - `quiet` (boolean): When true, created/modified time+user will use values from $page rather than current user+time (default=false)
|
||
* - `adjustName` (boolean): Adjust page name to ensure it is unique within its parent (default=true)
|
||
* - `forceID` (integer): Use this ID instead of an auto-assigned on (new page) or current ID (existing page)
|
||
* - `ignoreFamily` (boolean): Bypass check of allowed family/parent settings when saving (default=false)
|
||
* - `noHooks` (boolean): Prevent before/after save hooks from being called (default=false)
|
||
* - `noFields` (boolean): Bypass saving of custom fields (default=false)
|
||
* @return bool True on success, false on failure
|
||
* @throws WireException
|
||
*
|
||
*/
|
||
public function save(Page $page, $options = array()) {
|
||
|
||
$defaultOptions = array(
|
||
'uncacheAll' => true,
|
||
'resetTrackChanges' => true,
|
||
'adjustName' => true,
|
||
'forceID' => 0,
|
||
'ignoreFamily' => false,
|
||
'noHooks' => false,
|
||
'noFields' => false,
|
||
);
|
||
|
||
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
|
||
$options = array_merge($defaultOptions, $options);
|
||
$user = $this->wire()->user;
|
||
$languages = $this->wire()->languages;
|
||
$language = null;
|
||
|
||
// if language support active, switch to default language so that saved fields and hooks don't need to be aware of language
|
||
if($languages && $page->id != $user->id && "$user->language") {
|
||
$language = $user->language;
|
||
$user->setLanguage($languages->getDefault());
|
||
}
|
||
|
||
$reason = '';
|
||
$isNew = $page->isNew();
|
||
if($isNew) $this->pages->setupNew($page);
|
||
|
||
if(!$this->isSaveable($page, $reason, '', $options)) {
|
||
if($language) $user->setLanguage($language);
|
||
throw new WireException(rtrim("Can’t save page (id=$page->id): $page->path", ": ") . ": $reason");
|
||
}
|
||
|
||
if($page->hasStatus(Page::statusUnpublished) && $page->template->noUnpublish) {
|
||
$page->removeStatus(Page::statusUnpublished);
|
||
}
|
||
|
||
if($page->parentPrevious && !$isNew) {
|
||
if($page->isTrash() && !$page->parentPrevious->isTrash()) {
|
||
$this->pages->trash($page, false);
|
||
} else if($page->parentPrevious->isTrash() && !$page->parent->isTrash()) {
|
||
$this->pages->restore($page, false);
|
||
}
|
||
}
|
||
|
||
if($options['adjustName']) $this->pages->names()->checkNameConflicts($page);
|
||
if(!$this->savePageQuery($page, $options)) return false;
|
||
$result = $this->savePageFinish($page, $isNew, $options);
|
||
if($language) $user->setLanguage($language); // restore language
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Execute query to save to pages table
|
||
*
|
||
* triggers hooks: saveReady, statusChangeReady (when status changed)
|
||
*
|
||
* @param Page $page
|
||
* @param array $options
|
||
* @return bool
|
||
* @throws WireException|\Exception
|
||
*
|
||
*/
|
||
protected function savePageQuery(Page $page, array $options) {
|
||
|
||
$isNew = $page->isNew();
|
||
$database = $this->wire()->database;
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
$config = $this->wire()->config;
|
||
$user = $this->wire()->user;
|
||
$userID = $user ? $user->id : $config->superUserPageID;
|
||
$systemVersion = $config->systemVersion;
|
||
$sql = '';
|
||
|
||
if(!$page->created_users_id) $page->created_users_id = $userID;
|
||
|
||
if($page->isChanged('status') && empty($options['noHooks'])) {
|
||
$this->pages->statusChangeReady($page);
|
||
}
|
||
|
||
if(empty($options['noHooks'])) {
|
||
$extraData = $this->pages->saveReady($page);
|
||
$this->pages->savePageOrFieldReady($page);
|
||
} else {
|
||
$extraData = array();
|
||
}
|
||
|
||
if($this->pages->names()->isUntitledPageName($page->name)) {
|
||
$this->pages->setupPageName($page);
|
||
}
|
||
|
||
$data = array(
|
||
'parent_id' => (int) $page->parent_id,
|
||
'templates_id' => (int) $page->template->id,
|
||
'name' => $sanitizer->pageName($page->name, Sanitizer::toAscii),
|
||
'status' => (int) $page->status,
|
||
'sort' => ($page->sort > -1 ? (int) $page->sort : 0)
|
||
);
|
||
|
||
if(is_array($extraData)) foreach($extraData as $column => $value) {
|
||
$column = $database->escapeCol($column);
|
||
$data[$column] = (strtoupper($value) === 'NULL' ? NULL : $value);
|
||
}
|
||
|
||
if($isNew) {
|
||
if($page->id) $data['id'] = (int) $page->id;
|
||
$data['created_users_id'] = (int) $userID;
|
||
}
|
||
|
||
if($options['forceID']) $data['id'] = (int) $options['forceID'];
|
||
|
||
if($page->template->allowChangeUser) {
|
||
$data['created_users_id'] = (int) $page->created_users_id;
|
||
}
|
||
|
||
if(empty($options['quiet'])) {
|
||
$sql = 'modified=NOW()';
|
||
$data['modified_users_id'] = (int) $userID;
|
||
} else {
|
||
// quiet option, use existing values already populated to page, when present
|
||
$data['modified_users_id'] = (int) ($page->modified_users_id ? $page->modified_users_id : $userID);
|
||
$data['created_users_id'] = (int) ($page->created_users_id ? $page->created_users_id : $userID);
|
||
if($page->modified > 0) {
|
||
$data['modified'] = date('Y-m-d H:i:s', $page->modified);
|
||
} else if($isNew) {
|
||
$sql = 'modified=NOW()';
|
||
}
|
||
if($page->created > 0) {
|
||
$data['created'] = date('Y-m-d H:i:s', $page->created);
|
||
}
|
||
}
|
||
|
||
$page->modified_users_id = $data['modified_users_id'];
|
||
if(isset($data['created_users_id'])) $page->created_users_id = $data['created_users_id'];
|
||
|
||
if(!$page->isUnpublished() && ($isNew || ($page->statusPrevious && ($page->statusPrevious & Page::statusUnpublished)))) {
|
||
// page is being published
|
||
if($systemVersion >= 12) {
|
||
$sql .= ($sql ? ', ' : '') . 'published=NOW()';
|
||
}
|
||
}
|
||
|
||
foreach($data as $column => $value) {
|
||
$sql .= ", $column=" . (is_null($value) ? "NULL" : ":$column");
|
||
}
|
||
|
||
$sql = trim($sql, ", ");
|
||
|
||
if($isNew) {
|
||
if(empty($data['created'])) $sql .= ', created=NOW()';
|
||
$query = $database->prepare("INSERT INTO pages SET $sql");
|
||
} else {
|
||
$query = $database->prepare("UPDATE pages SET $sql WHERE id=:page_id");
|
||
$query->bindValue(":page_id", (int) $page->id, \PDO::PARAM_INT);
|
||
}
|
||
|
||
foreach($data as $column => $value) {
|
||
if(is_null($value)) continue; // already bound above
|
||
$query->bindValue(":$column", $value, is_int($value) ? \PDO::PARAM_INT : \PDO::PARAM_STR);
|
||
}
|
||
|
||
$tries = 0;
|
||
$maxTries = 100;
|
||
|
||
do {
|
||
$result = false;
|
||
$keepTrying = false;
|
||
try {
|
||
$result = $database->execute($query);
|
||
} catch(\Exception $e) {
|
||
$keepTrying = $this->savePageQueryException($page, $query, $e, $options);
|
||
if(!$keepTrying) throw $e;
|
||
}
|
||
} while($keepTrying && (++$tries < $maxTries));
|
||
|
||
if($result && ($isNew || !$page->id)) {
|
||
$page->id = (int) $database->lastInsertId();
|
||
$page->setQuietly('_inserted', time());
|
||
}
|
||
|
||
if($options['forceID']) $page->id = (int) $options['forceID'];
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Handle Exception for savePageQuery()
|
||
*
|
||
* While setupNew() already attempts to uniqify a page name with an incrementing
|
||
* number, there is a chance that two processes running at once might end up with
|
||
* the same number, so we account for the possibility here by re-trying queries
|
||
* that trigger duplicate-entry exceptions.
|
||
*
|
||
* Example of actual exception text, for reference:
|
||
* Integrity constraint violation: 1062 Duplicate entry 'background-3552' for key 'name3894_parent_id'
|
||
*
|
||
* @param Page $page
|
||
* @param \PDOStatement $query
|
||
* @param \PDOException|\Exception $exception
|
||
* @param array $options
|
||
* @return bool True if it should give $query another shot, false if not
|
||
*
|
||
*/
|
||
protected function savePageQueryException(Page $page, $query, $exception, array $options) {
|
||
|
||
$errorCode = $exception->getCode();
|
||
|
||
// 23000=integrity constraint violation, duplicate entry
|
||
if($errorCode != 23000) return false;
|
||
|
||
if(!$this->pages->names()->hasAutogenName($page) && !$options['adjustName']) return false;
|
||
|
||
$languages = $this->wire()->languages;
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
|
||
// account for the duplicate possibly being a multi-language name field
|
||
// i.e. “Duplicate entry 'bienvenido-2-1001' for key 'name1013_parent_id'”
|
||
if($languages && preg_match('/\b(name\d*)_parent_id\b/', $exception->getMessage(), $matches)) {
|
||
$nameField = $matches[1];
|
||
} else {
|
||
$nameField = 'name';
|
||
}
|
||
|
||
// get either 'name' or 'name123' (where 123 is language ID)
|
||
$pageName = $page->get($nameField);
|
||
$pageName = $this->pages->names()->incrementName($pageName);
|
||
$page->set($nameField, $pageName);
|
||
$query->bindValue(":$nameField", $sanitizer->pageName($pageName, Sanitizer::toAscii));
|
||
|
||
// indicate that page has a modified name
|
||
$this->pages->names()->hasAdjustedName($page, true);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Save individual Page fields and supporting actions
|
||
*
|
||
* triggers hooks: saved, added, moved, renamed, templateChanged
|
||
*
|
||
* @param Page $page
|
||
* @param bool $isNew
|
||
* @param array $options
|
||
* @return bool
|
||
* @throws \Exception|WireException|\PDOException If any field-saving failure occurs while in a DB transaction
|
||
*
|
||
*/
|
||
protected function savePageFinish(Page $page, $isNew, array $options) {
|
||
|
||
$changes = $page->getChanges(2);
|
||
$changesValues = $page->getChanges(true);
|
||
|
||
// update children counts for current/previous parent
|
||
if($isNew) {
|
||
// new page
|
||
$page->parent->numChildren++;
|
||
|
||
} else if($page->parentPrevious && $page->parentPrevious->id != $page->parent->id) {
|
||
// parent changed
|
||
$page->parentPrevious->numChildren--;
|
||
$page->parent->numChildren++;
|
||
}
|
||
|
||
// save any needed updates to pages_parents table
|
||
$this->pages->parents()->save($page);
|
||
|
||
// if page hasn't changed, don't continue further
|
||
if(!$page->isChanged() && !$isNew) {
|
||
$this->pages->debugLog('save', '[not-changed]', true);
|
||
if(empty($options['noHooks'])) {
|
||
$this->pages->saved($page, array());
|
||
$this->pages->savedPageOrField($page, array());
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// if page has a files path (or might have previously), trigger filesManager's save
|
||
if(PagefilesManager::hasPath($page)) $page->filesManager->save();
|
||
|
||
// disable outputFormatting and save state
|
||
$of = $page->of();
|
||
$page->of(false);
|
||
|
||
// when a page is statusCorrupted, it records what fields are corrupted in _statusCorruptedFields array
|
||
$corruptedFields = $page->hasStatus(Page::statusCorrupted) ? $page->_statusCorruptedFields : array();
|
||
|
||
// save each individual Fieldtype data in the fields_* tables
|
||
foreach($page->fieldgroup as $field) {
|
||
/** @var Field $field */
|
||
$fieldtype = $field->type;
|
||
$name = $field->name;
|
||
if($options['noFields'] || isset($corruptedFields[$name]) || !$fieldtype || !$page->hasField($field)) {
|
||
unset($changes[$name]);
|
||
unset($changesValues[$name]);
|
||
} else {
|
||
try {
|
||
$fieldtype->savePageField($page, $field);
|
||
} catch(\Exception $e) {
|
||
$label = $field->getLabel();
|
||
$message = $e->getMessage();
|
||
if(strpos($message, $label) !== false) $label = $name;
|
||
$error = sprintf($this->_('Error saving field "%s"'), $label) . ' — ' . $message;
|
||
$this->trackException($e, true, $error);
|
||
if($this->wire()->database->inTransaction()) throw $e;
|
||
}
|
||
}
|
||
}
|
||
|
||
// return outputFormatting state
|
||
$page->of($of);
|
||
|
||
// sortfield for children
|
||
$templateSortfield = $page->template->sortfield;
|
||
if(empty($templateSortfield)) $this->pages->sortfields()->save($page);
|
||
|
||
if($options['resetTrackChanges']) {
|
||
if($options['noFields']) {
|
||
// reset for only fields that were saved
|
||
foreach($changes as $change) $page->untrackChange($change);
|
||
$page->setTrackChanges(true);
|
||
} else {
|
||
// reset all changes
|
||
$page->resetTrackChanges();
|
||
}
|
||
}
|
||
|
||
// determine whether we'll trigger the added() hook
|
||
if($isNew) {
|
||
$page->setIsNew(false);
|
||
$triggerAddedPage = $page;
|
||
} else {
|
||
$triggerAddedPage = null;
|
||
}
|
||
|
||
// check for template changes
|
||
if($page->templatePrevious && $page->templatePrevious->id != $page->template->id) {
|
||
// the template was changed, so we may have data in the DB that is no longer applicable
|
||
// find unused data and delete it
|
||
foreach($page->templatePrevious->fieldgroup as $field) {
|
||
if($page->hasField($field)) continue;
|
||
$field->type->deletePageField($page, $field);
|
||
$this->message("Deleted field '$field' on page {$page->url}", Notice::debug);
|
||
}
|
||
}
|
||
|
||
if($options['uncacheAll']) $this->pages->uncacheAll($page);
|
||
|
||
// determine whether the pages_access table needs to be updated so that pages->find()
|
||
// operations can be access controlled.
|
||
if($isNew || $page->parentPrevious || $page->templatePrevious) $this->wire(new PagesAccess($page));
|
||
|
||
// trigger hooks
|
||
if(empty($options['noHooks'])) {
|
||
$this->pages->saved($page, $changes, $changesValues);
|
||
$this->pages->savedPageOrField($page, $changes);
|
||
if($triggerAddedPage) $this->pages->added($triggerAddedPage);
|
||
if($page->namePrevious && $page->namePrevious != $page->name) $this->pages->renamed($page);
|
||
if($page->parentPrevious) $this->pages->moved($page);
|
||
if($page->templatePrevious) $this->pages->templateChanged($page);
|
||
if(in_array('status', $changes)) $this->pages->statusChanged($page);
|
||
}
|
||
|
||
$this->pages->debugLog('save', $page, true);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* TBD Identify if parent changed and call saveParentsTable() where appropriate
|
||
*
|
||
* @param Page $page Page to save parent(s) for
|
||
* @param bool $isNew If page is newly created during this save this should be true, otherwise false
|
||
*
|
||
protected function savePageParent(Page $page, $isNew) {
|
||
|
||
if($page->parentPrevious || $page->_forceSaveParents || $isNew) {
|
||
$this->pages->parents()->rebuild($page);
|
||
}
|
||
|
||
// saveParentsTable option is always true unless manually disabled from a hook
|
||
if($page->parentPrevious && !$isNew && $page->numChildren > 0) {
|
||
// existing page was moved and it has children
|
||
if($page->parent->numChildren == 1) {
|
||
// first child of new parent
|
||
$this->pages->parents()->rebuildPage($page->parent);
|
||
} else {
|
||
$this->pages->parents()->rebuildPage($page);
|
||
}
|
||
|
||
} else if(($page->parentPrevious && $page->parent->numChildren == 1) ||
|
||
($isNew && $page->parent->numChildren == 1) ||
|
||
($page->_forceSaveParents)) {
|
||
// page is moved and is the first child of its new parent
|
||
// OR page is NEW and is the first child of its parent
|
||
// OR $page->_forceSaveParents is set (debug/debug, can be removed later)
|
||
$this->pages->parents()->rebuildPage($page->parent);
|
||
|
||
} else if($page->parentPrevious && $page->parent->numChildren > 1 && $page->parent->parent_id > 1) {
|
||
$this->pages->parents()->rebuildPage($page->parent->parent);
|
||
}
|
||
|
||
if($page->parentPrevious && $page->parentPrevious->numChildren == 0) {
|
||
// $page was moved and its previous parent is now left with no children, this ensures the old entries get deleted
|
||
$this->pages->parents()->rebuild($page->parentPrevious->id);
|
||
}
|
||
}
|
||
*/
|
||
|
||
/**
|
||
* Save just a field from the given page as used by Page::save($field)
|
||
*
|
||
* This function is public, but the preferred manner to call it is with $page->save($field)
|
||
*
|
||
* @param Page $page
|
||
* @param string|Field $field Field object or name (string)
|
||
* @param array|string $options Specify options:
|
||
* - `quiet` (boolean): Specify true to bypass updating of modified_users_id and modified time (default=false).
|
||
* - `noHooks` (boolean): Specify true to bypass calling of before/after save hooks (default=false).
|
||
* @return bool True on success
|
||
* @throws WireException
|
||
*
|
||
*/
|
||
public function saveField(Page $page, $field, $options = array()) {
|
||
|
||
$reason = '';
|
||
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
|
||
|
||
if($page->isNew()) {
|
||
throw new WireException("Can't save field from a new page - please save the entire page first");
|
||
}
|
||
|
||
if(!$this->isSaveable($page, $reason, $field, $options)) {
|
||
throw new WireException("Can't save field from page {$page->id}: {$page->path}: $reason");
|
||
}
|
||
|
||
if($field && (is_string($field) || is_int($field))) {
|
||
$field = $this->wire()->fields->get($field);
|
||
}
|
||
|
||
if(!$field instanceof Field) {
|
||
throw new WireException("Unknown field supplied to saveField for page {$page->id}");
|
||
}
|
||
|
||
if(!$page->fieldgroup->hasField($field)) {
|
||
throw new WireException("Page {$page->id} does not have field {$field->name}");
|
||
}
|
||
|
||
$value = $page->get($field->name);
|
||
if($value instanceof Pagefiles || $value instanceof Pagefile) $page->filesManager()->save();
|
||
$page->trackChange($field->name);
|
||
|
||
if(empty($options['noHooks'])) {
|
||
$this->pages->saveFieldReady($page, $field);
|
||
$this->pages->savePageOrFieldReady($page, $field->name);
|
||
}
|
||
|
||
if($field->type->savePageField($page, $field)) {
|
||
// if page has a files path (or might have previously), trigger filesManager's save
|
||
if(PagefilesManager::hasPath($page)) $page->filesManager->save();
|
||
$page->untrackChange($field->name);
|
||
if(empty($options['quiet'])) {
|
||
$user = $this->wire()->user;
|
||
$userID = (int) ($user ? $user->id : $this->wire()->config->superUserPageID);
|
||
$database = $this->wire()->database;
|
||
$query = $database->prepare("UPDATE pages SET modified_users_id=:userID, modified=NOW() WHERE id=:pageID");
|
||
$query->bindValue(':userID', $userID, \PDO::PARAM_INT);
|
||
$query->bindValue(':pageID', $page->id, \PDO::PARAM_INT);
|
||
$database->execute($query);
|
||
}
|
||
$return = true;
|
||
if(empty($options['noHooks'])) {
|
||
$this->pages->savedField($page, $field);
|
||
$this->pages->savedPageOrField($page, array($field->name));
|
||
}
|
||
} else {
|
||
$return = false;
|
||
}
|
||
|
||
$this->pages->debugLog('saveField', "$page:$field", $return);
|
||
|
||
return $return;
|
||
}
|
||
|
||
/**
|
||
* Silently add status flag to a Page and save
|
||
*
|
||
* This action does not update the Page modified date.
|
||
* It updates the status for both the given instantiated Page object and the value in the DB.
|
||
*
|
||
* @param Page $page
|
||
* @param int $status Use Page::status* constants
|
||
* @return bool
|
||
* @since 3.0.146
|
||
* @see PagesEditor::setStatus(), PagesEditor::removeStatus()
|
||
*
|
||
*/
|
||
public function addStatus(Page $page, $status) {
|
||
if(!$page->hasStatus($status)) $page->addStatus($status);
|
||
return $this->savePageStatus($page, $status) > 0;
|
||
}
|
||
|
||
/**
|
||
* Silently remove status flag from a Page and save
|
||
*
|
||
* This action does not update the Page modified date.
|
||
* It updates the status for both the given instantiated Page object and the value in the DB.
|
||
*
|
||
* @param Page $page
|
||
* @param int $status Use Page::status* constants
|
||
* @return bool
|
||
* @since 3.0.146
|
||
* @see PagesEditor::setStatus(), PagesEditor::addStatus(), PagesEditor::saveStatus()
|
||
*
|
||
*/
|
||
public function removeStatus(Page $page, $status) {
|
||
if($page->hasStatus($status)) $page->removeStatus($status);
|
||
return $this->savePageStatus($page, $status, false, true) > 0;
|
||
}
|
||
|
||
/**
|
||
* Silently save whatever the given Page’s status currently is
|
||
*
|
||
* This action does not update the Page modified date.
|
||
*
|
||
* @param Page $page
|
||
* @return bool
|
||
* @since 3.0.146
|
||
*
|
||
*/
|
||
public function saveStatus(Page $page) {
|
||
return $this->savePageStatus($page, $page->status) > 0;
|
||
}
|
||
|
||
/**
|
||
* Add or remove a Page status and commit to DB, optionally recursive with the children, grandchildren, and so on.
|
||
*
|
||
* While this can be performed with other methods, this is here just to make it fast for internal/non-api use.
|
||
* See the trash and restore methods for an example.
|
||
*
|
||
* This action does not update the Page modified date. If given a Page or PageArray, also note that it does not update
|
||
* the status properties of those instantiated Page objects, it only updates the DB value.
|
||
*
|
||
* #pw-internal Please use addStatus() or removeStatus() instead, unless you need to perform a recursive add/remove status.
|
||
*
|
||
* @param int|array|Page|PageArray $pageID Page ID, Page, array of page IDs, or PageArray
|
||
* @param int $status Status per flags in Page::status* constants. Status will be OR'd with existing status, unless $remove is used.
|
||
* @param bool $recursive Should the status descend into the page's children, and grandchildren, etc? (default=false)
|
||
* @param bool|int $remove Should the status be removed rather than added? Use integer 2 to overwrite (default=false)
|
||
* @return int Number of pages updated
|
||
*
|
||
*/
|
||
public function savePageStatus($pageID, $status, $recursive = false, $remove = false) {
|
||
|
||
$database = $this->wire()->database;
|
||
$rowCount = 0;
|
||
$multi = is_array($pageID) || $pageID instanceof PageArray;
|
||
$status = (int) $status;
|
||
|
||
if($status < 0 || $status > Page::statusMax) {
|
||
throw new WireException("status must be between 0 and " . Page::statusMax);
|
||
}
|
||
|
||
$sql = "UPDATE pages SET status=";
|
||
|
||
if($remove === 2) {
|
||
// overwrite status (internal/undocumented)
|
||
$sql .= "status=$status";
|
||
} else if($remove) {
|
||
// remove status
|
||
$sql .= "status & ~$status";
|
||
} else {
|
||
// add status
|
||
$sql .= "status|$status";
|
||
}
|
||
|
||
if($multi && $recursive) {
|
||
// multiple page IDs combined with recursive option, must be handled individually
|
||
foreach($pageID as $id) {
|
||
$rowCount += $this->savePageStatus((int) "$id", $status, $recursive, $remove);
|
||
}
|
||
// exit early in this case
|
||
return $rowCount;
|
||
|
||
} else if($multi) {
|
||
// multiple page IDs without recursive option, can be handled in one query
|
||
$ids = array();
|
||
foreach($pageID as $id) {
|
||
$id = (int) "$id";
|
||
if($id > 0) $ids[$id] = $id;
|
||
}
|
||
if(!count($ids)) $ids[] = 0;
|
||
$query = $database->prepare("$sql WHERE id IN(" . implode(',', $ids) . ")");
|
||
$database->execute($query);
|
||
return $query->rowCount();
|
||
|
||
} else {
|
||
// single page ID or Page object
|
||
$pageID = (int) "$pageID";
|
||
$query = $database->prepare("$sql WHERE id=:page_id");
|
||
$query->bindValue(":page_id", $pageID, \PDO::PARAM_INT);
|
||
$database->execute($query);
|
||
$rowCount = $query->rowCount();
|
||
}
|
||
|
||
if(!$recursive) return $rowCount;
|
||
|
||
// recursive mode assumed from this point forward
|
||
$parentIDs = array($pageID);
|
||
|
||
do {
|
||
$parentID = array_shift($parentIDs);
|
||
|
||
// update all children to have the same status
|
||
$query = $database->prepare("$sql WHERE parent_id=:parent_id");
|
||
$query->bindValue(":parent_id", $parentID, \PDO::PARAM_INT);
|
||
$database->execute($query);
|
||
$rowCount += $query->rowCount();
|
||
$query->closeCursor();
|
||
|
||
// locate children that themselves have children
|
||
$query = $database->prepare(
|
||
"SELECT pages.id FROM pages " .
|
||
"JOIN pages AS pages2 ON pages2.parent_id=pages.id " .
|
||
"WHERE pages.parent_id=:parent_id " .
|
||
"GROUP BY pages.id " .
|
||
"ORDER BY pages.sort"
|
||
);
|
||
|
||
$query->bindValue(':parent_id', $parentID, \PDO::PARAM_INT);
|
||
$database->execute($query);
|
||
|
||
/** @noinspection PhpAssignmentInConditionInspection */
|
||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||
$parentIDs[] = (int) $row['id'];
|
||
}
|
||
|
||
$query->closeCursor();
|
||
|
||
} while(count($parentIDs));
|
||
|
||
return $rowCount;
|
||
}
|
||
|
||
/**
|
||
* Permanently delete a page and it's fields.
|
||
*
|
||
* Unlike trash(), pages deleted here are not restorable.
|
||
*
|
||
* If you attempt to delete a page with children, and don't specifically set the $recursive param to True, then
|
||
* this method will throw an exception. If a recursive delete fails for any reason, an exception will be thrown.
|
||
*
|
||
* @param Page $page
|
||
* @param bool|array $recursive If set to true, then this will attempt to delete all children too.
|
||
* If you don't need this argument, optionally provide $options array instead.
|
||
* @param array $options Optional settings to change behavior:
|
||
* - `uncacheAll` (bool): Whether to clear memory cache after delete (default=false)
|
||
* - `recursive` (bool): Same as $recursive argument, may be specified in $options array if preferred.
|
||
* @return bool|int Returns true (success), or integer of quantity deleted if recursive mode requested.
|
||
* @throws WireException on fatal error
|
||
*
|
||
*/
|
||
public function delete(Page $page, $recursive = false, array $options = array()) {
|
||
|
||
$defaults = array(
|
||
'uncacheAll' => false,
|
||
'recursive' => is_bool($recursive) ? $recursive : false,
|
||
// internal use properties:
|
||
'_level' => 0,
|
||
'_deleteBranch' => false,
|
||
);
|
||
|
||
if(is_array($recursive)) $options = $recursive;
|
||
$options = array_merge($defaults, $options);
|
||
|
||
$this->isDeleteable($page, true); // throws WireException
|
||
$numDeleted = 0;
|
||
$numChildren = $page->numChildren;
|
||
$deleteBranch = false;
|
||
|
||
if($numChildren) {
|
||
if(!$options['recursive']) {
|
||
throw new WireException("Can't delete Page $page because it has one or more children.");
|
||
}
|
||
if($options['_level'] === 0) {
|
||
$deleteBranch = true;
|
||
$options['_deleteBranch'] = $page;
|
||
$this->pages->deleteBranchReady($page, $options);
|
||
}
|
||
foreach($page->children('include=all') as $child) {
|
||
/** @var Page $child */
|
||
$options['_level']++;
|
||
$result = $this->pages->delete($child, true, $options);
|
||
$options['_level']--;
|
||
if(!$result) throw new WireException("Error doing recursive page delete, stopped by page $child");
|
||
$numDeleted += $result;
|
||
}
|
||
}
|
||
|
||
// trigger a hook to indicate delete is ready and WILL occur
|
||
$this->pages->deleteReady($page, $options);
|
||
|
||
$this->clear($page);
|
||
|
||
$database = $this->wire()->database;
|
||
$query = $database->prepare("DELETE FROM pages WHERE id=:page_id LIMIT 1"); // QA
|
||
$query->bindValue(":page_id", $page->id, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
|
||
$this->pages->sortfields()->delete($page);
|
||
$page->setTrackChanges(false);
|
||
$page->status = Page::statusDeleted; // no need for bitwise addition here, as this page is no longer relevant
|
||
$this->pages->deleted($page, $options);
|
||
$numDeleted++;
|
||
if($deleteBranch) $this->pages->deletedBranch($page, $options, $numDeleted);
|
||
if($options['uncacheAll']) $this->pages->uncacheAll($page);
|
||
$this->pages->debugLog('delete', $page, true);
|
||
|
||
return $options['recursive'] ? $numDeleted : true;
|
||
}
|
||
|
||
/**
|
||
* Clone an entire page (including fields, file assets, and optionally children) and return it.
|
||
*
|
||
* @param Page $page Page that you want to clone
|
||
* @param Page $parent New parent, if different (default=same parent)
|
||
* @param bool $recursive Clone the children too? (default=true)
|
||
* @param array|string $options Optional options that can be passed to clone or save
|
||
* - forceID (int): force a specific ID
|
||
* - set (array): Array of properties to set to the clone (you can also do this later)
|
||
* - recursionLevel (int): recursion level, for internal use only.
|
||
* @return Page|NullPage the newly cloned page or a NullPage() with id=0 if unsuccessful.
|
||
* @throws WireException|\Exception on fatal error
|
||
*
|
||
*/
|
||
public function _clone(Page $page, Page $parent = null, $recursive = true, $options = array()) {
|
||
|
||
$defaults = array(
|
||
'forceID' => 0,
|
||
'set' => array(),
|
||
'recursionLevel' => 0, // recursion level (internal use only)
|
||
);
|
||
|
||
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
|
||
$options = array_merge($defaults, $options);
|
||
if($parent === null) $parent = $page->parent;
|
||
|
||
if(count($options['set']) && !empty($options['set']['name'])) {
|
||
$name = $options['set']['name'];
|
||
} else {
|
||
$name = $this->pages->names()->uniquePageName(array(
|
||
'name' => $page->name,
|
||
'parent' => $parent
|
||
));
|
||
}
|
||
|
||
$of = $page->of();
|
||
$page->of(false);
|
||
|
||
// Ensure all data is loaded for the page
|
||
foreach($page->fieldgroup as $field) {
|
||
/** @var Field $field */
|
||
if($page->hasField($field->name)) $page->get($field->name);
|
||
}
|
||
|
||
/** @var User $user */
|
||
$user = $this->wire('user');
|
||
|
||
// clone in memory
|
||
$copy = clone $page;
|
||
$copy->setIsNew(true);
|
||
$copy->of(false);
|
||
$copy->setQuietly('_cloning', $page);
|
||
$copy->setQuietly('id', $options['forceID'] > 1 ? (int) $options['forceID'] : 0);
|
||
$copy->setQuietly('numChildren', 0);
|
||
$copy->setQuietly('created', time());
|
||
$copy->setQuietly('modified', time());
|
||
$copy->name = $name;
|
||
$copy->parent = $parent;
|
||
|
||
if(!isset($options['quiet']) || $options['quiet']) {
|
||
$options['quiet'] = true;
|
||
$copy->setQuietly('created_users_id', $user->id);
|
||
$copy->setQuietly('modified_users_id', $user->id);
|
||
}
|
||
|
||
// set any properties indicated in options
|
||
if(count($options['set'])) {
|
||
foreach($options['set'] as $key => $value) {
|
||
$copy->set($key, $value);
|
||
// quiet option required for setting modified time or user
|
||
if($key === 'modified' || $key === 'modified_users_id') $options['quiet'] = true;
|
||
}
|
||
}
|
||
|
||
// tell PW that all the data needs to be saved
|
||
foreach($copy->fieldgroup as $field) {
|
||
if($copy->hasField($field)) $copy->trackChange($field->name);
|
||
}
|
||
|
||
$this->pages->cloneReady($page, $copy);
|
||
$this->cloning++;
|
||
$options['ignoreFamily'] = true; // skip family checks during clone
|
||
try {
|
||
$this->pages->save($copy, $options);
|
||
} catch(\Exception $e) {
|
||
$this->cloning--;
|
||
$copy->setQuietly('_cloning', null);
|
||
$page->of($of);
|
||
throw $e;
|
||
}
|
||
$this->cloning--;
|
||
|
||
// check to make sure the clone has worked so far
|
||
if(!$copy->id || $copy->id == $page->id) {
|
||
$copy->setQuietly('_cloning', null);
|
||
$page->of($of);
|
||
return $this->pages->newNullPage();
|
||
}
|
||
|
||
// copy $page's files over to new page
|
||
if(PagefilesManager::hasFiles($page)) {
|
||
$copy->filesManager->init($copy);
|
||
$page->filesManager->copyFiles($copy->filesManager->path());
|
||
}
|
||
|
||
// if there are children, then recursively clone them too
|
||
if($page->numChildren && $recursive) {
|
||
$start = 0;
|
||
$limit = 200;
|
||
$numChildrenCopied = 0;
|
||
do {
|
||
$children = $page->children("include=all, start=$start, limit=$limit");
|
||
$numChildren = $children->count();
|
||
foreach($children as $child) {
|
||
/** @var Page $child */
|
||
$childCopy = $this->pages->clone($child, $copy, true, array(
|
||
'recursionLevel' => $options['recursionLevel'] + 1,
|
||
));
|
||
if($childCopy->id) $numChildrenCopied++;
|
||
}
|
||
$start += $limit;
|
||
$this->pages->uncacheAll();
|
||
} while($numChildren);
|
||
$copy->setQuietly('numChildren', $numChildrenCopied);
|
||
}
|
||
|
||
$copy->parentPrevious = null;
|
||
$copy->setQuietly('_cloning', null);
|
||
|
||
if($options['recursionLevel'] === 0) {
|
||
// update pages_parents table, only when at recursionLevel 0 since parents()->rebuild() already descends
|
||
/*
|
||
if($copy->numChildren) {
|
||
$copy->setIsNew(true);
|
||
$this->pages->parents()->rebuild($copy);
|
||
$copy->setIsNew(false);
|
||
}
|
||
*/
|
||
// update sort
|
||
if($copy->parent()->sortfield() == 'sort') {
|
||
$this->sortPage($copy, $copy->sort, true);
|
||
}
|
||
}
|
||
|
||
$copy->of($of);
|
||
$page->of($of);
|
||
$page->meta()->copyTo($copy->id);
|
||
$copy->resetTrackChanges();
|
||
$this->pages->cloned($page, $copy);
|
||
$this->pages->debugLog('clone', "page=$page, parent=$parent", $copy);
|
||
|
||
return $copy;
|
||
}
|
||
|
||
/**
|
||
* Update page modified/created/published time to now (or given time)
|
||
*
|
||
* @param Page|PageArray|array $pages May be Page, PageArray or array of page IDs (integers)
|
||
* @param null|int|string|array $options Omit (null) to update to now, or unix timestamp or strtotime() recognized time string,
|
||
* or if you do not need this argument, you may optionally substitute the $type argument here,
|
||
* or in 3.0.183+ you can also specify array of options here instead:
|
||
* - `time` (string|int|null): Unix timestamp or strtotime() recognized string to use, omit for use current time (default=null)
|
||
* - `type` (string): One of 'modified', 'created', 'published' (default='modified')
|
||
* - `user` (bool|User): True to also update modified/created user to current user, or specify User object to use (default=false)
|
||
* @param string $type Date type to update, one of 'modified', 'created' or 'published' (default='modified') Added 3.0.147
|
||
* Skip this argument if using options array for previous argument or if using the default type 'modified'.
|
||
* @throws WireException|\PDOException if given invalid format for $modified argument or failed database query
|
||
* @return bool True on success, false on fail
|
||
*
|
||
*/
|
||
public function touch($pages, $options = null, $type = 'modified') {
|
||
|
||
$defaults = array(
|
||
'time' => (is_string($options) || is_int($options) ? $options : null),
|
||
'type' => $type,
|
||
'user' => false,
|
||
);
|
||
|
||
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
|
||
$database = $this->wire()->database;
|
||
$time = $options['time'];
|
||
$type = $options['type'];
|
||
$user = $options['user'] === true ? $this->wire()->user : $options['user'];
|
||
$ids = array();
|
||
|
||
if($time === 'modified' || $time === 'created' || $time === 'published') {
|
||
// time argument was omitted and type supplied here instead
|
||
$type = $time;
|
||
$time = null;
|
||
}
|
||
|
||
// ensure $col property is created in this method and not copied directly from $type
|
||
if($type === 'modified') {
|
||
$col = 'modified';
|
||
} else if($type === 'created') {
|
||
$col = 'created';
|
||
} else if($type === 'published') {
|
||
$col = 'published';
|
||
} else {
|
||
throw new WireException("Unrecognized date type '$type' for Pages::touch()");
|
||
}
|
||
|
||
if($pages instanceof Page) {
|
||
$ids[] = (int) $pages->id;
|
||
|
||
} else if(WireArray::iterable($pages)) {
|
||
foreach($pages as $page) {
|
||
if(is_int($page)) {
|
||
// page ID integer
|
||
$ids[] = (int) $page;
|
||
} else if($page instanceof Page) {
|
||
// Page object
|
||
$ids[] = (int) $page->id;
|
||
} else if(ctype_digit("$page")) {
|
||
// Page ID string
|
||
$ids[] = (int) "$page";
|
||
} else {
|
||
// invalid
|
||
}
|
||
}
|
||
}
|
||
|
||
if(!count($ids)) return false;
|
||
|
||
$sql = "UPDATE pages SET $col=";
|
||
|
||
if(is_null($time)) {
|
||
$sql .= 'NOW() ';
|
||
|
||
} else if(is_int($time) || ctype_digit($time)) {
|
||
$time = (int) $time;
|
||
$sql .= ':time ';
|
||
|
||
} else if(is_string($time)) {
|
||
$time = strtotime($time);
|
||
if(!$time) throw new WireException("Unrecognized time format provided to Pages::touch()");
|
||
$sql .= ':time ';
|
||
}
|
||
|
||
if($user instanceof User && ($col === 'modified' || $col === 'created')) {
|
||
$sql .= ", {$col}_users_id=:user ";
|
||
}
|
||
|
||
$sql .= 'WHERE id IN(' . implode(',', $ids) . ')';
|
||
$query = $database->prepare($sql);
|
||
if(strpos($sql, ':time')) $query->bindValue(':time', date('Y-m-d H:i:s', $time));
|
||
if(strpos($sql, ':user')) $query->bindValue(':user', $user->id, \PDO::PARAM_INT);
|
||
|
||
return $database->execute($query);
|
||
}
|
||
|
||
/**
|
||
* Move page to specified parent (work in progress)
|
||
*
|
||
* This method is the same as changing a page parent and saving, but provides a useful shortcut
|
||
* for some cases with less code. This method:
|
||
*
|
||
* - Does not save the other custom fields of a page (if any are changed).
|
||
* - Does not require that output formatting be off (it manages that internally).
|
||
*
|
||
* @param Page $child Page that you want to move.
|
||
* @param Page|int|string $parent Parent to move it under (may be Page object, path string, or ID integer).
|
||
* @param array $options Options to modify behavior (see PagesEditor::save for options).
|
||
* @return bool True on success or false if not necessary.
|
||
* @throws WireException if given parent does not exist, or move is not allowed
|
||
*
|
||
*/
|
||
public function move(Page $child, $parent, array $options = array()) {
|
||
|
||
if(is_string($parent) || is_int($parent)) $parent = $this->pages->get($parent);
|
||
if(!$parent instanceof Page || !$parent->id) throw new WireException('Unable to locate parent for move');
|
||
|
||
$options['noFields'] = true;
|
||
$of = $child->of();
|
||
$child->of(false);
|
||
$child->parent = $parent;
|
||
$result = $child->parentPrevious ? $this->pages->save($child, $options) : false;
|
||
if($of) $child->of(true);
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Set page $sort value and increment siblings having same or greater sort value
|
||
*
|
||
* - This method is primarily applicable if configured sortfield is manual “sort” (or “none”).
|
||
* - This is typically used after a move, sort, clone or delete operation.
|
||
*
|
||
* @param Page $page Page that you want to set the sort value for
|
||
* @param int|null $sort New sort value for page or null to pull from $page->sort
|
||
* @param bool $after If another page already has the sort, make $page go after it rather than before it? (default=false)
|
||
* @throws WireException if given invalid arguments
|
||
* @return int Number of sibling pages that had to have sort adjusted
|
||
*
|
||
*/
|
||
public function sortPage(Page $page, $sort = null, $after = false) {
|
||
|
||
$database = $this->wire()->database;
|
||
|
||
// reorder siblings having same or greater sort value, when necessary
|
||
if($page->id <= 1) return 0;
|
||
if(is_null($sort)) $sort = $page->sort;
|
||
|
||
// determine if any other siblings have same sort value
|
||
$sql = 'SELECT id FROM pages WHERE parent_id=:parent_id AND sort=:sort AND id!=:id';
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(':parent_id', $page->parent_id, \PDO::PARAM_INT);
|
||
$query->bindValue(':sort', $sort, \PDO::PARAM_INT);
|
||
$query->bindValue(':id', $page->id, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
$rowCount = $query->rowCount();
|
||
$query->closeCursor();
|
||
|
||
// move sort to after if requested
|
||
if($after && $rowCount) $sort += $rowCount;
|
||
|
||
// update $page->sort property if needed
|
||
if($page->sort != $sort) $page->sort = $sort;
|
||
|
||
// make sure that $page has the sort value indicated
|
||
$sql = 'UPDATE pages SET sort=:sort WHERE id=:id';
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(':sort', $sort, \PDO::PARAM_INT);
|
||
$query->bindValue(':id', $page->id, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
$sortCnt = $query->rowCount();
|
||
|
||
// no need for $page to have 'sort' indicated as a change, since we just updated it above
|
||
$page->untrackChange('sort');
|
||
|
||
if($rowCount) {
|
||
// update order of all siblings
|
||
$sql = 'UPDATE pages SET sort=sort+1 WHERE parent_id=:parent_id AND sort>=:sort AND id!=:id';
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(':parent_id', $page->parent_id, \PDO::PARAM_INT);
|
||
$query->bindValue(':sort', $sort, \PDO::PARAM_INT);
|
||
$query->bindValue(':id', $page->id, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
$sortCnt += $query->rowCount();
|
||
}
|
||
|
||
// call the sorted hook
|
||
$this->pages->sorted($page, false, $sortCnt);
|
||
|
||
return $sortCnt;
|
||
}
|
||
|
||
/**
|
||
* Sort one page before another (for pages using manual sort)
|
||
*
|
||
* Note that if given $sibling parent is different from `$page` parent, then the `$pages->save()`
|
||
* method will also be called to perform that movement.
|
||
*
|
||
* @param Page $page Page to move/sort
|
||
* @param Page $sibling Sibling that page will be moved/sorted before
|
||
* @param bool $after Specify true to make $page move after $sibling instead of before (default=false)
|
||
* @throws WireException When conditions don't allow page insertions
|
||
*
|
||
*/
|
||
public function insertBefore(Page $page, Page $sibling, $after = false) {
|
||
$sortfield = $sibling->parent()->sortfield();
|
||
if($sortfield != 'sort') {
|
||
throw new WireException('Insert before/after operations can only be used with manually sorted pages');
|
||
}
|
||
if(!$sibling->id || !$page->id) {
|
||
throw new WireException('New pages must be saved before using insert before/after operations');
|
||
}
|
||
if($sibling->id == 1 || $page->id == 1) {
|
||
throw new WireException('Insert before/after operations cannot involve homepage');
|
||
}
|
||
$page->sort = $sibling->sort;
|
||
if($page->parent_id != $sibling->parent_id) {
|
||
// page needs to be moved first
|
||
$page->parent = $sibling->parent;
|
||
$page->save();
|
||
}
|
||
$this->sortPage($page, $page->sort, $after);
|
||
}
|
||
|
||
/**
|
||
* Rebuild the “sort” values for all children of the given $parent page, fixing duplicates and gaps
|
||
*
|
||
* If used on a $parent not currently sorted by by “sort” then it will update the “sort” index to be
|
||
* consistent with whatever the pages are sorted by.
|
||
*
|
||
* @param Page $parent
|
||
* @return int
|
||
*
|
||
*/
|
||
public function sortRebuild(Page $parent) {
|
||
|
||
if(!$parent->id || !$parent->numChildren) return 0;
|
||
$database = $this->wire()->database;
|
||
$sorts = array();
|
||
$sort = 0;
|
||
|
||
if($parent->sortfield() == 'sort') {
|
||
// pages are manually sorted, so we can find IDs directly from the database
|
||
$sql = 'SELECT id FROM pages WHERE parent_id=:parent_id ORDER BY sort, created';
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(':parent_id', $parent->id, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
|
||
// establish new sort values
|
||
do {
|
||
$id = (int) $query->fetch(\PDO::FETCH_COLUMN);
|
||
if(!$id) break;
|
||
$sorts[] = "($id,$sort)";
|
||
} while(++$sort);
|
||
|
||
$query->closeCursor();
|
||
|
||
} else {
|
||
// children of $parent don't currently use "sort" as sort property
|
||
// so we will update the "sort" of children to be consistent with that
|
||
// of whatever sort property is in use.
|
||
$o = array('findIDs' => 1, 'cache' => false);
|
||
foreach($parent->children('include=all', $o) as $id) {
|
||
$id = (int) $id;
|
||
$sorts[] = "($id,$sort)";
|
||
$sort++;
|
||
}
|
||
}
|
||
|
||
// update sort values
|
||
$query = $database->prepare(
|
||
'INSERT INTO pages (id,sort) VALUES ' . implode(',', $sorts) . ' ' .
|
||
'ON DUPLICATE KEY UPDATE sort=VALUES(sort)'
|
||
);
|
||
|
||
$query->execute();
|
||
|
||
return count($sorts);
|
||
}
|
||
|
||
/**
|
||
* Replace one page with another (work in progress)
|
||
*
|
||
* @param Page $oldPage
|
||
* @param Page $newPage
|
||
* @return Page
|
||
* @throws WireException
|
||
* @since 3.0.189 But not yet available in public API
|
||
*
|
||
*/
|
||
protected function replace(Page $oldPage, Page $newPage) {
|
||
|
||
if($newPage->numChildren) {
|
||
throw new WireException('Page with children cannot replace another');
|
||
}
|
||
|
||
$database = $this->wire()->database;
|
||
|
||
$this->pages->cacher()->uncache($oldPage);
|
||
$this->pages->cacher()->uncache($newPage);
|
||
|
||
$prevId = $newPage->id;
|
||
$id = $oldPage->id;
|
||
$parent = $oldPage->parent;
|
||
$prevTemplate = $oldPage->template;
|
||
|
||
$newPage->parent = $parent;
|
||
$newPage->templatePrevious = $prevTemplate;
|
||
|
||
$this->clear($oldPage, array(
|
||
'clearParents' => false,
|
||
'clearAccess' => $prevTemplate->id != $newPage->template->id,
|
||
'clearSortfield' => false,
|
||
));
|
||
|
||
$binds = array(
|
||
':id' => $id,
|
||
':parent_id' => $parent->id,
|
||
':prev_id' => $prevId,
|
||
);
|
||
|
||
$sqls = array();
|
||
$sqls[] = 'UPDATE pages SET id=:id, parent_id=:parent_id WHERE id=:prev_id';
|
||
|
||
foreach($newPage->template->fieldgroup as $field) {
|
||
/** @var Field $field */
|
||
$field->type->replacePageField($newPage, $oldPage, $field);
|
||
}
|
||
|
||
foreach($sqls as $sql) {
|
||
$query = $database->prepare($sql);
|
||
foreach($binds as $bindKey => $bindValue) {
|
||
if(strpos($sql, $bindKey) === false) continue;
|
||
$query->bindValue($bindKey, $bindValue);
|
||
$query->execute();
|
||
}
|
||
}
|
||
|
||
$newPage->id = $id;
|
||
|
||
$this->save($newPage);
|
||
|
||
$page = $this->pages->getById($id, $newPage->template, $parent->id);
|
||
|
||
return $page;
|
||
}
|
||
|
||
/**
|
||
* Clear a page of its data
|
||
*
|
||
* @param Page $page
|
||
* @param array $options
|
||
* @return bool
|
||
* @throws WireException
|
||
* @since 3.0.189
|
||
*
|
||
*/
|
||
public function clear(Page $page, array $options = array()) {
|
||
|
||
$defaults = array(
|
||
'clearMethod' => 'delete', // 'delete' or 'empty'
|
||
'haltOnError' => false,
|
||
'clearFields' => true,
|
||
'clearFiles' => true,
|
||
'clearMeta' => true,
|
||
'clearAccess' => true,
|
||
'clearSortfield' => true,
|
||
'clearParents' => true,
|
||
);
|
||
|
||
$options = array_merge($defaults, $options);
|
||
$errors = array();
|
||
$halt = false;
|
||
|
||
if($options['clearFields']) {
|
||
foreach($page->fieldgroup as $field) {
|
||
/** @var Field $field */
|
||
/** @var Fieldtype $fieldtype */
|
||
$fieldtype = $field->type;
|
||
if($options['clearMethod'] === 'delete') {
|
||
$result = $fieldtype->deletePageField($page, $field);
|
||
} else {
|
||
$result = $fieldtype->emptyPageField($page, $field);
|
||
}
|
||
if(!$result) {
|
||
$errors[] = "Unable to clear field '$field' from page $page";
|
||
$halt = $options['haltOnError'];
|
||
if($halt) break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if($options['clearFiles'] && !$halt) {
|
||
$error = "Error clearing files for page $page";
|
||
try {
|
||
if(PagefilesManager::hasPath($page)) {
|
||
$filesManager = $page->filesManager();
|
||
if(!$filesManager) {
|
||
// $filesManager will be null if page has deleted status
|
||
// so create our own instance
|
||
$filesManager = new PagefilesManager($page);
|
||
}
|
||
if(!$filesManager->emptyAllPaths()) {
|
||
$errors[] = $error;
|
||
$halt = $options['haltOnError'];
|
||
}
|
||
}
|
||
} catch(\Exception $e) {
|
||
$errors[] = $error . ' - ' . $e->getMessage();
|
||
$halt = $options['haltOnError'];
|
||
}
|
||
}
|
||
|
||
if($options['clearMeta'] && !$halt) {
|
||
try {
|
||
$page->meta()->removeAll();
|
||
} catch(\Exception $e) {
|
||
$errors[] = "Error clearing meta for page $page";
|
||
$halt = $options['haltOnError'];
|
||
}
|
||
}
|
||
|
||
if($options['clearAccess'] && !$halt) {
|
||
/** @var PagesAccess $access */
|
||
$access = $this->wire(new PagesAccess());
|
||
$access->deletePage($page);
|
||
}
|
||
|
||
if($options['clearParents'] && !$halt) {
|
||
// delete entirely from pages_parents table
|
||
$this->pages->parents()->delete($page);
|
||
}
|
||
|
||
if($options['clearSortfield'] && !$halt) {
|
||
$this->pages->sortfields()->delete($page);
|
||
}
|
||
|
||
if(count($errors) || $halt) {
|
||
foreach($errors as $error) {
|
||
$this->error($error);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Prepare options for Pages::new(), Pages::newPage()
|
||
*
|
||
* Converts given array, selector string, template name, object or int to array of options.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param array|string|int $options
|
||
* @return array
|
||
* @since 3.0.191
|
||
*
|
||
*/
|
||
public function newPageOptions($options) {
|
||
|
||
if(empty($options)) return array();
|
||
|
||
$template = null; /** @var Template|null $template */
|
||
$parent = null;
|
||
$class = '';
|
||
|
||
if(is_array($options)) {
|
||
// ok
|
||
} else if(is_string($options)) {
|
||
if(strpos($options, '=') !== false) {
|
||
$selectors = new Selectors($options);
|
||
$this->wire($selectors);
|
||
$options = array();
|
||
foreach($selectors as $selector) {
|
||
$options[$selector->field()] = $selector->value;
|
||
}
|
||
} else if(strpos($options, '/') === 0) {
|
||
$options = array('path' => $options);
|
||
} else {
|
||
$options = array('template' => $options);
|
||
}
|
||
} else if(is_object($options)) {
|
||
$options = $options instanceof Template ? array('template' => $options) : array();
|
||
} else if(is_int($options)) {
|
||
$template = $this->wire()->templates->get($options);
|
||
$options = $template ? array('template' => $template) : array();
|
||
} else {
|
||
$options = array();
|
||
}
|
||
|
||
// only use property 'parent' rather than 'parent_id'
|
||
if(!empty($options['parent_id']) && empty($options['parent'])) {
|
||
$options['parent'] = $options['parent_id'];
|
||
unset($options['parent_id']);
|
||
}
|
||
|
||
// only use property 'template' rather than 'templates_id'
|
||
if(!empty($options['templates_id']) && empty($options['template'])) {
|
||
$options['template'] = $options['templates_id'];
|
||
unset($options['templates_id']);
|
||
}
|
||
|
||
// page class (pageClass)
|
||
if(!empty($options['pageClass'])) {
|
||
// ok
|
||
$class = $options['pageClass'];
|
||
unset($options['pageClass']);
|
||
} else if(!empty($options['class']) && !$this->wire()->fields->get('class')) {
|
||
// alias for pageClass, so long as there is not a field named 'class'
|
||
$class = $options['class'];
|
||
unset($options['class']);
|
||
}
|
||
|
||
// identify requested template
|
||
if(isset($options['template'])) {
|
||
$template = $options['template'];
|
||
if(!is_object($template)) {
|
||
$template = empty($template) ? null : $this->wire()->templates->get($template);
|
||
}
|
||
unset($options['template']);
|
||
}
|
||
|
||
// convert parent path to parent page object
|
||
if(!empty($options['parent'])) {
|
||
if(is_object($options['parent'])) {
|
||
$parent = $options['parent'];
|
||
} else if(ctype_digit("$options[parent]")) {
|
||
$parent = (int) $options['parent'];
|
||
} else {
|
||
$parent = $this->pages->getByPath($options['parent']);
|
||
if(!$parent->id) $parent = null;
|
||
}
|
||
unset($options['parent']);
|
||
}
|
||
|
||
// name and parent can be detected from path, when specified
|
||
if(!empty($options['path'])) {
|
||
$path = trim($options['path'], '/');
|
||
if(strpos($path, '/') === false) $path = "/$path";
|
||
$parts = explode('/', $path); // note index[0] is blank
|
||
$name = array_pop($parts);
|
||
if(empty($options['name']) && !empty($name)) {
|
||
// detect name from path
|
||
$options['name'] = $name;
|
||
}
|
||
if(empty($parent) && !$this->pages->loader()->isLoading()) {
|
||
// detect parent from path
|
||
$parentPath = count($parts) ? implode('/', $parts) : '/';
|
||
$parent = $this->pages->getByPath($parentPath);
|
||
if(!$parent->id) $parent = null;
|
||
}
|
||
unset($options['path']);
|
||
}
|
||
|
||
// detect template from parent (when possible)
|
||
if(!$template && !empty($parent) && empty($options['id']) && !$this->pages->loader()->isLoading()) {
|
||
$parent = is_object($parent) ? $parent : $this->pages->get($parent);
|
||
if($parent->id) {
|
||
if(count($parent->template->childTemplates) === 1) {
|
||
$template = $parent->template->childTemplates()->first();
|
||
}
|
||
} else {
|
||
$parent = null;
|
||
}
|
||
}
|
||
|
||
// detect parent from template (when possible)
|
||
if($template && empty($parent) && empty($options['id']) && !$this->pages->loader()->isLoading()) {
|
||
if(count($template->parentTemplates) === 1) {
|
||
$parentTemplates = $template->parentTemplates();
|
||
if($parentTemplates->count()) {
|
||
$numParents = $this->pages->count("template=$parentTemplates, include=all");
|
||
if($numParents === 1) {
|
||
$parent = $this->pages->get("template=$parentTemplates");
|
||
if(!$parent->id) $parent = null;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// detect class from template
|
||
if(empty($class) && $template) $class = $template->getPageClass();
|
||
|
||
if($parent) $options['parent'] = $parent;
|
||
if($template) $options['template'] = $template;
|
||
if($class) $options['pageClass'] = $class;
|
||
|
||
if(isset($options['id'])) {
|
||
if(ctype_digit("$options[id]") && (int) $options['id'] > 0) {
|
||
$options['id'] = (int) $options['id'];
|
||
if($parent && "$options[id]" === "$parent") unset($options['parent']);
|
||
} else if(((int) $options['id']) === -1) {
|
||
$options['id'] = (int) $options['id']; // special case allowed for access control tests
|
||
} else {
|
||
unset($options['id']);
|
||
}
|
||
}
|
||
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* Hook after Fieldtype::sleepValue to remove MB4 characters when present and applicable
|
||
*
|
||
* This hook is only used if $config->dbStripMB4 is true and $config->dbEngine is not “utf8mb4”.
|
||
*
|
||
* @param HookEvent $event
|
||
*
|
||
*/
|
||
public function hookFieldtypeSleepValueStripMB4(HookEvent $event) {
|
||
$event->return = $this->wire()->sanitizer->removeMB4($event->return);
|
||
}
|
||
}
|