wire($this); $this->fieldgroups = $fieldgroups; } /** * Initialize the TemplatesArray and populate * * #pw-internal * */ public function init() { $this->getWireArray(); } /** * Return the WireArray that this DAO stores it's items in * * #pw-internal * */ public function getAll() { if($this->useLazy()) $this->loadAllLazyItems(); return $this->getWireArray(); } /** * Get WireArray container that items are stored in * * #pw-internal * * @return WireArray|TemplatesArray * @since 3.0.194 * */ public function getWireArray() { if($this->templatesArray === null) { $this->templatesArray = $this->wire(new TemplatesArray()); $this->load($this->templatesArray); } return $this->templatesArray; } /** * Make an item and populate with given data * * #pw-internal * * @param array $a Associative array of data to populate * @return Saveable|Wire|Template * @since 3.0.146 * */ public function makeItem(array $a = array()) { /** @var Template $template */ $template = $this->wire(new Template()); $template->loaded(false); if(!empty($a['data'])) { if(is_string($a['data'])) $a['data'] = $this->decodeData($a['data']); } else { unset($a['data']); } foreach(array('id', 'name', 'fieldgroups_id', 'flags', 'cache_time') as $key) { if(!isset($a[$key])) continue; $value = $key === 'name' ? $a[$key] : (int) $a[$key]; $template->setRaw($key, $value); unset($a[$key]); } foreach($a as $key => $value) { $template->set($key, $value); } $template->loaded(true); $template->resetTrackChanges(true); return $template; } /** * Load all lazy items * * #pw-internal * * @since 3.0.194 * */ public function loadAllLazyItems() { if(!$this->useLazy()) return; $this->wire()->fieldgroups->loadAllLazyItems(); parent::loadAllLazyItems(); } /** * Return a new blank item * * @return Template * * #pw-internal * */ public function makeBlankItem() { return $this->wire(new Template()); } /** * Return the name of the table that this DAO stores item records in * * #pw-internal * */ public function getTable() { return 'templates'; } /** * Return the field name that fields should initially be sorted by * * #pw-internal * */ public function getSort() { return $this->getTable() . ".name"; } /** * Add and save new template (and fieldgroup) with given name and return it * * @param string $name * @param array $properties Any additional properties to add to template * @return Template * @throws WireException if given invalid template name or template already exists * @since 3.0.170 * */ public function add($name, array $properties = array()) { if(!is_string($name)) { throw new WireException("You must specify the template name to add"); } $saniName = $this->wire()->sanitizer->templateName($name); if(empty($saniName)) { throw new WireException("Invalid template name: $name"); } $name = $saniName; $template = $this->get($name); if($template) { throw new WireException("Template '$name' cannot be added because it already exists"); } $fieldgroups = $this->wire()->fieldgroups; $fieldgroup = $fieldgroups->get($name); if(!$fieldgroup) { $fieldgroup = new Fieldgroup(); $this->wire($fieldgroup); $fieldgroup->name = $name; $fieldgroups->save($fieldgroup); } $template = new Template(); $this->wire($template); $template->name = $name; $template->fieldgroup = $fieldgroup; foreach($properties as $key => $value) $template->set($key, $value); $this->save($template); return $template; } /** * Get a template by name or ID * * Given a template ID or name, return the matching template or NULL if not found. * * @param string|int $key Template name or ID * @return Template|null|string * */ public function get($key) { if($key === 'path') return $this->wire()->config->paths->templates; return parent::get($key); } /** * Save a Template * * ~~~~~ * $templates->save($template); * ~~~~~ * * @param Saveable|Template $item Template to save * @return bool True on success, false on failure * @throws WireException * */ public function ___save(Saveable $item) { // If the template's fieldgroup has changed, then we delete data that's no longer applicable to the new fieldgroup. $isNew = $item->id < 1; if(!$item->fieldgroup) { throw new WireException("Template '$item' cannot be saved because it has no fieldgroup assigned"); } if(!$item->fieldgroup->id) { throw new WireException("You must save Fieldgroup '{$item->fieldgroup->name}' before adding to Template '$item'"); } $rolesChanged = $item->isChanged('useRoles'); if($this->wire()->pages->get('/')->template->id == $item->id) { if(!$item->useRoles) { throw new WireException("Template '$item' is used by the homepage and thus must manage access"); } if(!$item->hasRole('guest')) { throw new WireException("Template '$item' is used by the homepage and thus must have the 'guest' role assigned."); } } if(!$item->isChanged('modified')) $item->modified = time(); $result = parent::___save($item); if($result && !$isNew && $item->fieldgroupPrevious && $item->fieldgroupPrevious->id != $item->fieldgroup->id) { // the fieldgroup has been changed // remove data from all fields that are not part of the new fieldgroup /** @var FieldsArray $removeFields */ $removeFields = $this->wire(new FieldsArray()); foreach($item->fieldgroupPrevious as $field) { if(!$item->fieldgroup->has($field)) { $removeFields->add($field); } } if(count($removeFields)) { foreach($removeFields as $field) { /** @var Field $field */ $field->type->deleteTemplateField($item, $field); } /* $pages = $this->fuel('pages')->find("templates_id={$item->id}, check_access=0, status<" . Page::statusMax); foreach($pages as $page) { foreach($removeFields as $field) { $field->type->deletePageField($page, $field); if($this->fuel('config')->debug) $this->message("Removed field '$field' on page '{$page->url}'"); } } */ } } if($rolesChanged) { /** @var PagesAccess $access */ $access = $this->wire(new PagesAccess()); $access->updateTemplate($item); } $cache = $this->wire()->cache; $cache->maintenance($item); return $result; } /** * Delete a Template * * @param Template|Saveable $item Template to delete * @return bool True on success, false on failure * @throws WireException Thrown when you attempt to delete a template in use, or a system template. * */ public function ___delete(Saveable $item) { $name = $item->name; $id = $item->id; if($item->flags & Template::flagSystem) { throw new WireException("Can't delete template '$name' because it is a system template."); } $cnt = $item->getNumPages(); if($cnt > 0) { throw new WireException("Can't delete template '$name' because it is used by $cnt pages."); } $return = parent::___delete($item); if($return) { $fieldgroups = $this->wire()->fieldgroups; $fieldgroup = $fieldgroups->get($name); if($fieldgroup) { // also delete fieldgroup, if not used by any other templates $cnt = 0; foreach($this as $t) { /** @var Template $t */ if($t->id != $id && $t->fieldgroup->id == $fieldgroup->id) $cnt++; } if(!$cnt) $fieldgroups->delete($fieldgroup); } } $cache = $this->wire()->cache; $cache->maintenance($item); return $return; } /** * Clone the given Template * * Note that this also clones the Fieldgroup if the template being cloned has its own named fieldgroup. * * @todo: clone the fieldgroup context settings too. * * @param Template|Saveable $item Template to clone * @param string $name Name of new template that will be created, or omit to auto-assign. * @return bool|Template $item Returns the new Template on success, or false on failure * */ public function ___clone(Saveable $item, $name = '') { $original = $item; /** @var Template $item */ $item = clone $item; if($item->flags & Template::flagSystem) { // we want to avoid creating clones that have system flags $item->flags = $item->flags | Template::flagSystemOverride; $item->flags = $item->flags & ~Template::flagSystem; $item->flags = $item->flags & ~Template::flagSystemOverride; } $item->id = 0; // note this must be after removing system flags $fieldgroup = $item->fieldgroup; if($fieldgroup->name == $item->name) { // if the fieldgroup and the item have the same name, we'll also clone the fieldgroup $fieldgroup = $this->wire()->fieldgroups->clone($fieldgroup, $name); $item->fieldgroup = $fieldgroup; } /** @var Template|bool $item */ $item = parent::___clone($item, $name); if($item && $item->id && !$item->altFilename) { // now that we have a clone, lets also clone the template file, if it exists $config = $this->wire()->config; $files = $this->wire()->files; $path = $config->paths->templates; $ext = $config->templateExtension ? $config->templateExtension : 'php'; $file = "$path$item->name.$ext"; if($original->filenameExists() && is_writable($path) && !$files->exists($file)) { if($files->copy($original->filename, $file)) $item->filename = $file; } } return $item; } /** * Rename given template (and its fieldgroup, and file, when possible) * * Given template must have its previous 'name' still present, and new name provided in $name * argument to this method. * * @param Template $template * @param string $name New name to use * @since 3.0.170 * @throws WireException if rename cannot be completed * */ public function rename(Template $template, $name) { $config = $this->wire()->config; $saniName = $this->wire()->sanitizer->templateName($name); if(empty($saniName)) throw new WireException("Invalid template name: $name"); $name = $saniName; $basename = "$template->name.$config->templateExtension"; $filename = $template->filenameExists() ? $template->filename() : ''; $fieldgroup = $template->fieldgroup; $t = $this->get($name); if($t instanceof Template && $t->id != $template->id) { throw new WireException("Template '$name' already exists"); } if($fieldgroup->name === $template->name) { // rename fieldgroup too $fg = $this->wire()->fieldgroups->get($name); if($fg && $fg->id != $fieldgroup->id) throw new WireException("Fieldgroup '$name' already exists"); $fieldgroup->name = $name; $this->wire()->fieldgroups->save($fieldgroup); } $template->name = $name; $this->save($template); if($filename && basename($filename) === $basename) { $newFilename = $config->paths->templates . $name . $config->templateExtension; if(is_readable($filename) && is_writable($filename) && !file_exists($newFilename)) { // rename file $this->wire()->files->rename($filename, $newFilename); } } } /** * Return the number of pages using the provided Template * * @param Template $tpl Template you want to get count for * @return int Total number of pages in use by given Template * */ public function getNumPages(Template $tpl) { $database = $this->wire()->database; $query = $database->prepare("SELECT COUNT(*) AS total FROM pages WHERE templates_id=:template_id"); // QA $query->bindValue(":template_id", $tpl->id, \PDO::PARAM_INT); $query->execute(); return (int) $query->fetchColumn(); } /** * Overridden from WireSaveableItems to retain specific keys * * @param array $value * @return string * */ protected function encodeData(array $value) { return wireEncodeJSON($value, array('slashUrls', 'compile')); } /** * Export Template data for external use * * #pw-advanced * * @param Template $template Template you want to export * @return array Associative array of export data * */ public function ___getExportData(Template $template) { $template->set('_exportMode', true); $data = $template->getTableData(); // flatten foreach($data['data'] as $key => $value) { $data[$key] = $value; } // remove unnecessary unset($data['data'], $data['modified']); // convert fieldgroup to guid $fieldgroup = $this->wire()->fieldgroups->get((int) $data['fieldgroups_id']); if($fieldgroup) $data['fieldgroups_id'] = $fieldgroup->name; // convert family settings to guids foreach(array('parentTemplates', 'childTemplates') as $key) { if(!isset($data[$key])) continue; $values = array(); foreach($data[$key] as $id) { if(ctype_digit("$id")) $id = (int) $id; $t = $this->get($id); if($t) $values[] = $t->name; } $data[$key] = $values; } // convert roles to guids if($template->useRoles) { $roles = $this->wire()->roles; foreach(array('roles', 'editRoles', 'addRoles', 'createRoles') as $key) { if(!isset($data[$key])) continue; $values = array(); foreach($data[$key] as $id) { $role = $id instanceof Role ? $id : $roles->get((int) $id); $values[] = $role->name; } $data[$key] = $values; } } // convert pages to guids if(((int) $template->cache_time) != 0) { if(!empty($data['cacheExpirePages'])) { $pages = $this->wire()->pages; $values = array(); foreach($data['cacheExpirePages'] as $id) { $page = $pages->get((int) $id); if(!$page->id) continue; $values[] = $page->path; } } } $fieldgroupData = array('fields' => array(), 'contexts' => array()); if($template->fieldgroup) $fieldgroupData = $template->fieldgroup->getExportData(); $data['fieldgroupFields'] = $fieldgroupData['fields']; $data['fieldgroupContexts'] = $fieldgroupData['contexts']; unset($data['_lazy'], $data['_exportMode']); $template->set('_exportMode', false); return $data; } /** * Given an array of Template export data, import it to the given Template * * ~~~~~~ * // Example of return value * $returnValue = array( * 'property_name' => array( * 'old' => 'old value', // old value (in string comparison format) * 'new' => 'new value', // new value (in string comparison format) * 'error' => 'error message or blank if no error' // error message (string) or messages (array) * ), * 'another_property_name' => array( * // ... * ) * ); * ~~~~~~ * * #pw-advanced * * @param Template $template Template you want to import to * @param array $data Import data array (must have been exported from getExportData() method). * @return array Returns array with list of changes (see example in method description) * */ public function ___setImportData(Template $template, array $data) { $fieldgroups = $this->wire()->fieldgroups; $template->set('_importMode', true); $fieldgroupData = array(); $changes = array(); $_data = $this->getExportData($template); if(isset($data['fieldgroupFields'])) $fieldgroupData['fields'] = $data['fieldgroupFields']; if(isset($data['fieldgroupContexts'])) $fieldgroupData['contexts'] = $data['fieldgroupContexts']; unset($data['fieldgroupFields'], $data['fieldgroupContexts'], $data['id']); foreach($data as $key => $value) { if($key == 'fieldgroups_id' && !ctype_digit("$value")) { $fieldgroup = $fieldgroups->get($value); if(!$fieldgroup) { /** @var Fieldgroup $fieldgroup */ $fieldgroup = $this->wire(new Fieldgroup()); $fieldgroup->name = $value; } $oldValue = $template->fieldgroup ? $template->fieldgroup->name : ''; $newValue = $fieldgroup->name; $error = ''; try { $template->setFieldgroup($fieldgroup); } catch(\Exception $e) { $this->trackException($e, false); $error = $e->getMessage(); } if($oldValue != $fieldgroup->name) { if(!$fieldgroup->id) $newValue = "+$newValue"; $changes['fieldgroups_id'] = array( 'old' => $template->fieldgroup->name, 'new' => $newValue, 'error' => $error ); } } $template->errors("clear"); $oldValue = isset($_data[$key]) ? $_data[$key] : ''; $newValue = $value; if(is_array($oldValue)) $oldValue = wireEncodeJSON($oldValue, true, false); else if(is_object($oldValue)) $oldValue = (string) $oldValue; if(is_array($newValue)) $newValue = wireEncodeJSON($newValue, true, false); else if(is_object($newValue)) $newValue = (string) $newValue; // everything else if($oldValue == $newValue || (empty($oldValue) && empty($newValue))) { // no change needed } else { // changed try { $template->set($key, $value); if($key == 'roles') $template->getRoles(); // forces reload of roles (and resulting error messages) $error = $template->errors('clear'); } catch(\Exception $e) { $this->trackException($e, false); $error = array($e->getMessage()); } $changes[$key] = array( 'old' => $oldValue, 'new' => $newValue, 'error' => (count($error) ? $error : array()) ); } } if(count($fieldgroupData)) { $_changes = $template->fieldgroup->setImportData($fieldgroupData); if($_changes['fields']['new'] != $_changes['fields']['old']) { $changes['fieldgroupFields'] = $_changes['fields']; } if($_changes['contexts']['new'] != $_changes['contexts']['old']) { $changes['fieldgroupContexts'] = $_changes['contexts']; } } $template->errors('clear'); $template->set('_importMode', false); return $changes; } /** * Return the parent page that this template assumes new pages are added to * * - This is based on family settings, when applicable. * - It also takes into account user access, if requested (see arg 1). * - If there is no shortcut parent, NULL is returned. * - If there are multiple possible shortcut parents, a NullPage is returned. * * @param Template $template * @param bool $checkAccess Whether or not to check for user access to do this (default=false). * @param bool|int $getAll Specify true to return all possible parents (makes method always return a PageArray) * Or specify int of maximum allowed `Page::status*` constant for items in returned PageArray (since 3.0.138). * @return Page|NullPage|null|PageArray * */ public function getParentPage(Template $template, $checkAccess = false, $getAll = false) { $pages = $this->wire()->pages; $user = $this->wire()->user; $foundParent = null; $foundParents = $getAll ? $pages->newPageArray() : null; $foundParentQty = 0; $maxStatus = is_int($getAll) && $getAll ? ($getAll * 2) : 0; if($template->noShortcut || !count($template->parentTemplates)) return $foundParents; if($template->noParents == -1) { // only 1 page of this type allowed if($this->getNumPages($template) > 0) return $foundParents; } else if($template->noParents == 1) { return $foundParents; } foreach($template->parentTemplates as $parentTemplateID) { $parentTemplate = $this->get((int) $parentTemplateID); if(!$parentTemplate) continue; // if the parent template doesn't have this as an allowed child template, exclude it if($parentTemplate->noChildren) continue; if(!in_array($template->id, $parentTemplate->childTemplates)) continue; // sort=status ensures that a non-hidden page is given preference to a hidden page $include = $checkAccess ? "unpublished" : "all"; $selector = "templates_id=$parentTemplate->id, include=$include, sort=status"; if($maxStatus) { $selector .= ", status<$maxStatus"; } else if(!$getAll) { $selector .= ", limit=2"; } $parentPages = $pages->find($selector); $numParentPages = count($parentPages); // undetermined parent if(!$numParentPages) continue; if($getAll) { // build list of all parents (will check access outside loop) $foundParents->add($parentPages); continue; } else if($numParentPages > 1) { // multiple possible parents, we can early-exit $foundParentQty += $numParentPages; break; } else { // one possible parent $parentPage = $parentPages->first(); } if($checkAccess) { if($parentPage->id) { // single defined parent $p = $pages->newPage($template); if(!$parentPage->addable($p)) continue; } else { // multiple possible parents if(!$user->hasPermission('page-create', $template)) continue; } } if($parentPage && $parentPage->id) $foundParentQty++; $foundParent = $parentPage; if($foundParentQty > 1) break; } if($checkAccess && $getAll && $foundParents && $foundParents->count()) { $p = $pages->newPage($template); foreach($foundParents as $parentPage) { if(!$parentPage->addable($p)) $foundParents->remove($parentPage); } } if($getAll) return $foundParents; if($foundParentQty > 1) return $pages->newNullPage(); return $foundParent; } /** * Return all possible parent pages for the given template, if predefined * * @param Template $template * @param bool $checkAccess Specify true to exclude parent pages that user doesn't have access to add pages to (default=false) * @param int $maxStatus Max allowed `Page::status*` constant (default=0 which means not applicable). Since 3.0.138 * @return PageArray * */ public function getParentPages(Template $template, $checkAccess = false, $maxStatus = 0) { $getAll = $maxStatus ? $maxStatus : true; return $this->getParentPage($template, $checkAccess, $getAll); } /** * Get class name to use for pages using given Template * * Note that value can be different from the `$template->pageClass` property, since it is determined at runtime. * If it is different, then it is at least a class that extends the one defined by pageClass. * * @param Template $template * @param bool $withNamespace Include namespace? (default=true) * @return string Returned class name includes namespace * @since 3.0.152 * */ public function getPageClass(Template $template, $withNamespace = true) { if(isset($this->pageClassNames[$template->id])) { // use cached value when present $pageClass = $this->pageClassNames[$template->id]; if(!$withNamespace) $pageClass = wireClassName($pageClass, false); return $pageClass; } $corePageClass = __NAMESPACE__ . "\\Page"; $cacheable = true; // first check for class defined with Template 'pageClass' setting $pageClass = $template->pageClass; if($pageClass && $pageClass !== 'Page') { // page has custom class assignment in its template $nsPageClass = wireClassName($pageClass, true); // is this custom class available for instantiation? if(class_exists($nsPageClass)) { // class is available for use and has a namespace $pageClass = $nsPageClass; } else if(class_exists("\\$pageClass") && wireInstanceOf("\\$pageClass", $corePageClass)) { // class appears to be available in root namespace and it extends PW’s Page class (legacy) $pageClass = "\\$pageClass"; } else { // class is not available for instantiation $this->warning( "Template '$template' page class '$pageClass' is not available", Notice::debug | Notice::superuser | Notice::admin ); $pageClass = ''; // do not cache because maybe class will be available later $cacheable = false; } } $config = $this->wire()->config; $usePageClasses = $config->usePageClasses; if(empty($pageClass) || $pageClass === 'Page') { // if no custom Page class available, use default Page class with namespace if($usePageClasses) { // custom classes enabled if(!isset($this->pageClassNames[0])) { // index 0 holds cached default page class $defaultPageClass = __NAMESPACE__ . "\\DefaultPage"; if(!class_exists($defaultPageClass) || !wireInstanceOf($defaultPageClass, $corePageClass)) { $defaultPageClass = $corePageClass; } $this->pageClassNames[0] = $defaultPageClass; } $pageClass = $this->pageClassNames[0]; } else { $pageClass = $corePageClass; } } // determine if custom class available (3.0.152+) if($usePageClasses) { // generate a CamelCase name + 'Page' from template name, i.e. 'blog-post' => 'BlogPostPage' $className = ucwords(str_replace(array('-', '_', '.'), ' ', $template->name)); $className = __NAMESPACE__ . "\\" . str_replace(' ', '', $className) . 'Page'; if(class_exists($className) && wireInstanceOf($className, $corePageClass)) { $pageClass = $className; } } if($cacheable && $template->id) $this->pageClassNames[$template->id] = $pageClass; if(!$withNamespace) $pageClass = wireClassName($pageClass, false); return $pageClass; } /** * Get all tags used by templates * * @param bool $getTemplateNames Get arrays of template names for each tag? (default=false) * @return array In return value both key and value are the tag * @since 3.0.176 + hookable in 3.0.179 * */ public function ___getTags($getTemplateNames = false) { $tags = array(); foreach($this as $template) { /** @var Template $template */ $templateTags = $template->tags; if(empty($templateTags)) continue; $templateTags = explode(' ', $templateTags); foreach($templateTags as $tag) { if(empty($tag)) continue; if($getTemplateNames) { if(!isset($tags[$tag])) $tags[$tag] = array(); $tags[$tag][$template->name] = $template->name; } else { $tags[$tag] = $tag; } } } ksort($tags); return $tags; } /** * Set a Permission for a Template for and specific Role * * Note: you must also save() the template to commit the change. * * #pw-internal * * @param Template $template * @param Permission|string|int $permission * @param Role|string|int $role * @param bool $revoke Specify true to revoke the permission, or omit to add the permission * @param bool $test When true, no changes are made but return value still applicable * @return bool True if an update was made (or would be made), false if not * @throws WireException If given unknown Role or Permission * */ public function setTemplatePermissionByRole(Template $template, $permission, $role, $revoke = false, $test = false) { if(!$template->useRoles) throw new WireException("Template $template does not have access control enabled"); $defaultPermissions = array('page-view', 'page-edit', 'page-create', 'page-add'); $updated = false; if(is_string($role) || is_int($role)) $role = $this->wire()->roles->get($role); if(!$role instanceof Role) throw new WireException("Unknown role for Template::setPermissionByRole"); if(is_string($permission) && in_array($permission, $defaultPermissions)) { $permissionName = $permission; } else if($permission instanceof Permission) { $permissionName = $permission->name; } else { $permission = $this->wire()->permissions->get($permission); $permissionName = $permission ? $permission->name : ''; } if(in_array($permissionName, $defaultPermissions)) { // use pre-defined view/edit/create/add roles $roles = $template->getRoles($permissionName); $has = $roles->has($role); if($revoke) { if($has) { if($test) return true; $roles->remove($role); $template->setRoles($roles, $permissionName); $updated = true; } } else if(!$has) { if($test) return true; $roles->add($role); $template->setRoles($roles, $permissionName); $updated = true; } } else if($permission instanceof Permission) { $rolesPermissions = $template->get('rolesPermissions'); if(!is_array($rolesPermissions)) $rolesPermissions = array(); $rolePermissions = isset($rolesPermissions["$role->id"]) ? $rolesPermissions["$role->id"] : array(); $_rolePermissions = $rolePermissions; if($revoke) { $key = array_search("$permission->id", $rolePermissions); if($key !== false) unset($rolePermissions[$key]); if(!in_array("-$permission->id", $rolePermissions)) $rolePermissions[] = "-$permission->id"; } else { $key = array_search("-$permission->id", $rolePermissions); if($key !== false) unset($rolePermissions[$key]); if(!in_array("$permission->id", $rolePermissions)) $rolePermissions[] = "$permission->id"; } if($rolePermissions !== $_rolePermissions) { if($test) return true; $rolesPermissions["$role->id"] = $rolePermissions; $template->set('rolesPermissions', $rolesPermissions); $updated = true; } } else { throw new WireException("Unknown permission for Templates::setPermissionByRole"); } return $updated; } /** * Hook called when a Template detects that its file has changed * * Note that the hook is not called until something in the system (like a page render) asks for the template’s filename. * That’s because it would not be efficient for PW to check the file for every template in the system on every request. * * #pw-hooker * * @param Template $template * @since 3.0.141 * */ public function ___fileModified(Template $template) { if(empty($this->fileModTemplates)) { // add hook on first call $this->addHookAfter('ProcessWire::finished', $this, '_hookFinished'); } $this->fileModTemplates[$template->id] = $template; } /** * Saves templates that had modified files to update 'modified' and 'ns' properties after the request is complete * * #pw-internal * * @param HookEvent $e * @since 3.0.141 * */ public function _hookFinished(HookEvent $e) { foreach($this->fileModTemplates as /* $id => */ $template) { if($template->isChanged('modified') || $template->isChanged('ns')) { $template->save(); } } $this->fileModTemplates = array(); } /** * FUTURE USE: Is the parent/child relationship allowed? * * By default this method returns an associative array containing the following: * * - `allowed` (bool): Is the relationship allowed? * - `reasons` (array): Array of strings containing reasons why relationship is or is not allowed. * * If you specify the `false` for the `verbose` option then this method just returns a boolean. * * @param Template|Page $parent Parent Template or Page to test. * @param Template|Page $child Child Template or Page to test. * @param array $options Options to modify default behavior: * - `verbose` (bool): Return verbose array. When false, returns boolean rather than array (default=true). * - `strict` (bool): Disallow relationships that do not match rules, even if relationship already exists (default=false). * Note that this option only applies if method is given Page objects rather than Template objects. * @return array|bool Returns associative array by default, or bool if the verbose option is false. * @throws WireException if given invalid argument * public function allowRelationship($parent, $child, array $options = array()) { $defaults = array( 'verbose' => true, 'strict' => false, ); $options = array_merge($defaults, $options); $parentPage = null; $childPage = null; if($child instanceof Template) { $childTemplate = $child; } else if($child instanceof Page) { $childPage = $child; $childTemplate = $child->template; } else { throw new WireException('Invalid argument for child'); } if($parent instanceof Template) { $parentTemplate = $parent; } else if($parent instanceof Page) { $parentPage = $parent; $parentTemplate = $parent->template; } else { throw new WireException('Invalid argument for parent'); } $reasonsNo = array(); $reasonsYes = array(); $isAlreadyParent = $parentPage && $childPage && $childPage->parent_id == $parentPage->id; $isAlreadyParentNote = "parent/child allowed because relationship already exists"; if($isAlreadyParent) { if($options['strict']) { // in strict mode, existing relationships are ignored and we stick only to the rules $isAlreadyParent = false; } else { $reasonsYes[] = "Given child page ($childPage) already has this parent ($parentPage)"; } } if($parentTemplate->noChildren) { $reason = "Parent template “$parentTemplate” specifies “no children”"; if($isAlreadyParent) { $reasonsYes[] = "$reason - $isAlreadyParentNote"; } else { $reasonsNo[] = $reason; } } if($childTemplate->noMove) { $reason = "Child template “$childTemplate” specifies “no move”"; if($isAlreadyParent) { $reasonsYes[] = "$reason - $isAlreadyParentNote"; } else { $reasonsNo[] = $reason; } } if($childTemplate->noParents > 0) { $reason = "Child template “$childTemplate” specifies “no parents” option"; if($isAlreadyParent) { $reasonsYes[] = "$reason - $isAlreadyParentNote"; } else { $reasonsNo[] = $reason; } } if(count($parentTemplate->childTemplates)) { if(in_array($childTemplate->id, $parentTemplate->childTemplates)) { $reasonsYes[] = "Parent template “$parentTemplate” specifically allows children of “$childTemplate”"; } else { $reasonsNo[] = "Parent template “$parentTemplate” does not allow children using template “$childTemplate”"; } } if(count($childTemplate->parentTemplates)) { if(in_array($parentTemplate->id, $childTemplate->parentTemplates)) { $reasonsYes[] = "Child template “$childTemplate” specifically allows parents using template “$parentTemplate”"; } else { $reasonsNo[] = "Child template “$childTemplate” does not allow parents using template “$parentTemplate”"; } } $allowed = count($reasonsNo) ? false : true; if($options['verbose']) { return array( 'allowed' => $allowed, 'reasons' => $allowed ? $reasonsYes : $reasonsNo, ); } return $allowed; } */ }