praiadeseselle/wire/core/Templates.php

1159 lines
35 KiB
PHP
Raw Normal View History

2022-03-08 15:55:41 +01:00
<?php namespace ProcessWire;
/**
* ProcessWire Templates
*
* Manages and provides access to all the Template instances
*
2022-11-05 18:32:48 +01:00
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
2022-03-08 15:55:41 +01:00
* https://processwire.com
*
* #pw-summary Manages and provides access to all the Templates.
*
* @method TemplatesArray find($selector) Return the templates matching the the given selector query. #pw-internal
* @method bool save(Template $template) Save the given Template.
* @method bool delete() delete(Template $template) Delete the given Template. Note that this will throw a fatal error if the template is in use by any pages.
* @method bool|Saveable|Template clone(Saveable $item, $name = '') #pw-internal
* @method array getExportData(Template $template) Export Template data for external use. #pw-advanced
* @method array setImportData(Template $template, array $data) Given an array of Template export data, import it to the given Template. #pw-advanced
* @method void fileModified(Template $template) Hook called when a template detects that its file has been modified. #pw-hooker
* @method array getTags($getTemplateNames = false) Get tags for all templates (3.0.179+) #pw-advanced
*
*/
class Templates extends WireSaveableItems {
/**
* Reference to all the Fieldgroups
*
* @var Fieldgroups
*
*/
protected $fieldgroups = null;
/**
* WireArray of all Template instances
*
* @var TemplatesArray
*
*/
2022-11-05 18:32:48 +01:00
protected $templatesArray = null;
2022-03-08 15:55:41 +01:00
/**
* Templates that had changed files during this request
*
* @var array Array of Template objects indexed by id
*
*/
protected $fileModTemplates = array();
/**
* Cached template ID to page class names (for getPageClass method)
*
* @var array
*
*/
protected $pageClassNames = array();
/**
* Construct the Templates
*
* @param Fieldgroups $fieldgroups Reference to the Fieldgroups
*
*/
public function __construct(Fieldgroups $fieldgroups) {
2022-11-05 18:32:48 +01:00
parent::__construct();
2022-03-08 15:55:41 +01:00
$fieldgroups->wire($this);
2022-11-05 18:32:48 +01:00
$this->fieldgroups = $fieldgroups;
2022-03-08 15:55:41 +01:00
}
/**
* Initialize the TemplatesArray and populate
*
* #pw-internal
*
*/
public function init() {
2022-11-05 18:32:48 +01:00
$this->getWireArray();
2022-03-08 15:55:41 +01:00
}
/**
* Return the WireArray that this DAO stores it's items in
*
* #pw-internal
*
*/
public function getAll() {
2022-11-05 18:32:48 +01:00
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);
}
2022-03-08 15:55:41 +01:00
return $this->templatesArray;
}
/**
* Make an item and populate with given data
*
* #pw-internal
*
* @param array $a Associative array of data to populate
2022-11-05 18:32:48 +01:00
* @return Saveable|Wire|Template
2022-03-08 15:55:41 +01:00
* @since 3.0.146
*
*/
public function makeItem(array $a = array()) {
2022-11-05 18:32:48 +01:00
/** @var Template $template */
2022-03-08 15:55:41 +01:00
$template = $this->wire(new Template());
$template->loaded(false);
2022-11-05 18:32:48 +01:00
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]);
}
2022-03-08 15:55:41 +01:00
foreach($a as $key => $value) {
$template->set($key, $value);
}
2022-11-05 18:32:48 +01:00
2022-03-08 15:55:41 +01:00
$template->loaded(true);
$template->resetTrackChanges(true);
return $template;
}
2022-11-05 18:32:48 +01:00
/**
* Load all lazy items
*
* #pw-internal
*
* @since 3.0.194
*
*/
public function loadAllLazyItems() {
if(!$this->useLazy()) return;
$this->wire()->fieldgroups->loadAllLazyItems();
parent::loadAllLazyItems();
}
2022-03-08 15:55:41 +01:00
/**
* Return a new blank item
*
2022-11-05 18:32:48 +01:00
* @return Template
*
2022-03-08 15:55:41 +01:00
* #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) {
2022-11-05 18:32:48 +01:00
if($key === 'path') return $this->wire()->config->paths->templates;
2023-03-10 19:41:40 +01:00
return parent::get($key);
2022-03-08 15:55:41 +01:00
}
/**
* 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;
2022-11-05 18:32:48 +01:00
if(!$item->fieldgroup) {
throw new WireException("Template '$item' cannot be saved because it has no fieldgroup assigned");
}
if(!$item->fieldgroup->id) {
2023-03-10 19:41:40 +01:00
throw new WireException("You must save Fieldgroup '{$item->fieldgroup->name}' before adding to Template '$item'");
2022-11-05 18:32:48 +01:00
}
2022-03-08 15:55:41 +01:00
$rolesChanged = $item->isChanged('useRoles');
2022-11-05 18:32:48 +01:00
if($this->wire()->pages->get('/')->template->id == $item->id) {
if(!$item->useRoles) {
2023-03-10 19:41:40 +01:00
throw new WireException("Template '$item' is used by the homepage and thus must manage access");
2022-11-05 18:32:48 +01:00
}
if(!$item->hasRole('guest')) {
2023-03-10 19:41:40 +01:00
throw new WireException("Template '$item' is used by the homepage and thus must have the 'guest' role assigned.");
2022-11-05 18:32:48 +01:00
}
2022-03-08 15:55:41 +01:00
}
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
2023-03-10 19:41:40 +01:00
/** @var FieldsArray $removeFields */
2022-03-08 15:55:41 +01:00
$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) {
2023-03-10 19:41:40 +01:00
/** @var Field $field */
2022-03-08 15:55:41 +01:00
$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) {
2023-03-10 19:41:40 +01:00
/** @var PagesAccess $access */
2022-03-08 15:55:41 +01:00
$access = $this->wire(new PagesAccess());
$access->updateTemplate($item);
}
2022-11-05 18:32:48 +01:00
$cache = $this->wire()->cache;
2022-03-08 15:55:41 +01:00
$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) {
2022-11-05 18:32:48 +01:00
2023-03-10 19:41:40 +01:00
$name = $item->name;
$id = $item->id;
2022-11-05 18:32:48 +01:00
if($item->flags & Template::flagSystem) {
2023-03-10 19:41:40 +01:00
throw new WireException("Can't delete template '$name' because it is a system template.");
2022-11-05 18:32:48 +01:00
}
2022-03-08 15:55:41 +01:00
$cnt = $item->getNumPages();
2022-11-05 18:32:48 +01:00
if($cnt > 0) {
2023-03-10 19:41:40 +01:00
throw new WireException("Can't delete template '$name' because it is used by $cnt pages.");
2022-11-05 18:32:48 +01:00
}
2022-03-08 15:55:41 +01:00
$return = parent::___delete($item);
2023-03-10 19:41:40 +01:00
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);
}
}
2022-11-05 18:32:48 +01:00
$cache = $this->wire()->cache;
2022-03-08 15:55:41 +01:00
$cache->maintenance($item);
2022-11-05 18:32:48 +01:00
2022-03-08 15:55:41 +01:00
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.
2023-03-10 19:41:40 +01:00
* @return bool|Template $item Returns the new Template on success, or false on failure
2022-03-08 15:55:41 +01:00
*
*/
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
2022-11-05 18:32:48 +01:00
$fieldgroup = $this->wire()->fieldgroups->clone($fieldgroup, $name);
2022-03-08 15:55:41 +01:00
$item->fieldgroup = $fieldgroup;
}
2023-03-10 19:41:40 +01:00
/** @var Template|bool $item */
2022-03-08 15:55:41 +01:00
$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
2022-11-05 18:32:48 +01:00
$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;
2022-03-08 15:55:41 +01:00
}
}
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);
2023-03-10 19:41:40 +01:00
if($t instanceof Template && $t->id != $template->id) {
2022-03-08 15:55:41 +01:00
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) {
2022-11-05 18:32:48 +01:00
$database = $this->wire()->database;
2022-03-08 15:55:41 +01:00
$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
2022-11-05 18:32:48 +01:00
$fieldgroup = $this->wire()->fieldgroups->get((int) $data['fieldgroups_id']);
2022-03-08 15:55:41 +01:00
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;
2023-03-10 19:41:40 +01:00
$t = $this->get($id);
2022-03-08 15:55:41 +01:00
if($t) $values[] = $t->name;
}
$data[$key] = $values;
}
// convert roles to guids
if($template->useRoles) {
2023-03-10 19:41:40 +01:00
$roles = $this->wire()->roles;
2022-03-08 15:55:41 +01:00
foreach(array('roles', 'editRoles', 'addRoles', 'createRoles') as $key) {
if(!isset($data[$key])) continue;
$values = array();
foreach($data[$key] as $id) {
2023-03-10 19:41:40 +01:00
$role = $id instanceof Role ? $id : $roles->get((int) $id);
2022-03-08 15:55:41 +01:00
$values[] = $role->name;
}
$data[$key] = $values;
}
}
// convert pages to guids
if(((int) $template->cache_time) != 0) {
if(!empty($data['cacheExpirePages'])) {
2022-11-05 18:32:48 +01:00
$pages = $this->wire()->pages;
2022-03-08 15:55:41 +01:00
$values = array();
foreach($data['cacheExpirePages'] as $id) {
2022-11-05 18:32:48 +01:00
$page = $pages->get((int) $id);
2022-03-08 15:55:41 +01:00
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']);
2022-03-08 15:55:41 +01:00
$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) {
2022-11-05 18:32:48 +01:00
$fieldgroups = $this->wire()->fieldgroups;
2022-03-08 15:55:41 +01:00
$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")) {
2022-11-05 18:32:48 +01:00
$fieldgroup = $fieldgroups->get($value);
2022-03-08 15:55:41 +01:00
if(!$fieldgroup) {
2023-03-10 19:41:40 +01:00
/** @var Fieldgroup $fieldgroup */
2022-03-08 15:55:41 +01:00
$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)
2023-03-10 19:41:40 +01:00
$error = $template->errors('clear');
2022-03-08 15:55:41 +01:00
} 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) {
2022-11-05 18:32:48 +01:00
$pages = $this->wire()->pages;
$user = $this->wire()->user;
2022-03-08 15:55:41 +01:00
$foundParent = null;
2022-11-05 18:32:48 +01:00
$foundParents = $getAll ? $pages->newPageArray() : null;
2022-03-08 15:55:41 +01:00
$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) {
2022-11-05 18:32:48 +01:00
$parentTemplate = $this->get((int) $parentTemplateID);
2022-03-08 15:55:41 +01:00
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";
}
2022-11-05 18:32:48 +01:00
$parentPages = $pages->find($selector);
2022-03-08 15:55:41 +01:00
$numParentPages = count($parentPages);
// undetermined parent
if(!$numParentPages) continue;
if($getAll) {
// build list of all parents (will check access outside loop)
2023-03-10 19:41:40 +01:00
$foundParents->add($parentPages);
2022-03-08 15:55:41 +01:00
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
2022-11-05 18:32:48 +01:00
$p = $pages->newPage($template);
2022-03-08 15:55:41 +01:00
if(!$parentPage->addable($p)) continue;
} else {
// multiple possible parents
2022-11-05 18:32:48 +01:00
if(!$user->hasPermission('page-create', $template)) continue;
2022-03-08 15:55:41 +01:00
}
}
if($parentPage && $parentPage->id) $foundParentQty++;
$foundParent = $parentPage;
if($foundParentQty > 1) break;
}
if($checkAccess && $getAll && $foundParents && $foundParents->count()) {
2022-11-05 18:32:48 +01:00
$p = $pages->newPage($template);
2022-03-08 15:55:41 +01:00
foreach($foundParents as $parentPage) {
if(!$parentPage->addable($p)) $foundParents->remove($parentPage);
}
}
if($getAll) return $foundParents;
2022-11-05 18:32:48 +01:00
if($foundParentQty > 1) return $pages->newNullPage();
2022-03-08 15:55:41 +01:00
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";
2022-11-05 18:32:48 +01:00
$cacheable = true;
2022-03-08 15:55:41 +01:00
// 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 PWs Page class (legacy)
$pageClass = "\\$pageClass";
} else {
// class is not available for instantiation
2022-11-05 18:32:48 +01:00
$this->warning(
"Template '$template' page class '$pageClass' is not available",
Notice::debug | Notice::superuser | Notice::admin
);
2022-03-08 15:55:41 +01:00
$pageClass = '';
2022-11-05 18:32:48 +01:00
// do not cache because maybe class will be available later
$cacheable = false;
2022-03-08 15:55:41 +01:00
}
}
2022-11-05 18:32:48 +01:00
$config = $this->wire()->config;
2022-03-08 15:55:41 +01:00
$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;
}
}
2022-11-05 18:32:48 +01:00
if($cacheable && $template->id) $this->pageClassNames[$template->id] = $pageClass;
2022-03-08 15:55:41 +01:00
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;
2023-03-10 19:41:40 +01:00
if(is_string($role) || is_int($role)) $role = $this->wire()->roles->get($role);
2022-03-08 15:55:41 +01:00
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 {
2022-11-05 18:32:48 +01:00
$permission = $this->wire()->permissions->get($permission);
2022-03-08 15:55:41 +01:00
$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 templates filename.
* Thats 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) {
2023-03-10 19:41:40 +01:00
foreach($this->fileModTemplates as /* $id => */ $template) {
2022-03-08 15:55:41 +01:00
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;
}
*/
}