artabro/wire/core/Template.php
2024-08-27 11:35:37 +02:00

1621 lines
54 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Template
*
* #pw-summary Template is a Pages connection to fields (via a Fieldgroup), access control, and output via a template file.
* #pw-body Template objects also maintain several properties which can affect the render behavior of pages using it.
* #pw-order-groups identification,manipulation,family,URLs,access,files,cache,page-editor,behaviors,other
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @todo add multi-language option for redirectLogin setting
*
* Identification
*
* @property int $id Numeric database ID. #pw-group-identification
* @property string $name Name of template. #pw-group-identification
* @property string $label Optional short text label to describe Template. #pw-group-identification
* @property int $flags Flags (bitmask) assigned to this template. See the flag constants. #pw-group-identification
* @property string $ns Namespace found in the template file, or blank if not determined. #pw-group-identification
* @property string $pageClass Class for instantiated page objects. Page assumed if blank, or specify class name. #pw-group-identification
* @property int $modified Last modified time for template or template file
* @property string $icon Icon name specified with the template (preferable to use getIcon/setIcon methods instead). #pw-internal
*
* Fieldgroup/Fields
*
* @property int $fieldgroups_id Numeric ID of Fieldgroup assigned to this template. #pw-internal
* @property Fieldgroup|Field[] $fieldgroup The Fieldgroup used by the template. Can also be used to iterate a Template's fields. #pw-group-fields
* @property Fieldgroup|Field[] $fields Alias for the fieldgroup property. Use whatever makes more sense for your code readability. #pw-internal
* @property Fieldgroup|null $fieldgroupPrevious Previous fieldgroup, if it was changed. Null if not. #pw-group-fields
*
* Cache
*
* @property int $cache_time Number of seconds pages using this template should cache for, or 0 for no cache. Negative values indicates setting used for external caching engine like ProCache. #pw-internal (Note: cacheTime is an alias of this) #pw-group-cache
* @property int $cacheTime Number of seconds pages using this template should cache for, or 0 for no cache. Negative values indicates setting used for external caching engine like ProCache. #pw-group-cache
* @property string $noCacheGetVars GET vars that trigger disabling the cache (only when cache_time > 0) #pw-group-cache
* @property string $noCachePostVars POST vars that trigger disabling the cache (only when cache_time > 0) #pw-group-cache
* @property int $useCacheForUsers Use cache for: 0 = only guest users, 1 = guests and logged in users #pw-group-cache
* @property int $cacheExpire Expire the cache for all pages when page using this template is saved? (1 = yes, 0 = no- only current page) #pw-group-cache
* @property array $cacheExpirePages Array of Page IDs that should be expired, when cacheExpire == Template::cacheExpireSpecific #pw-group-cache
* @property string $cacheExpireSelector Selector string matching pages that should be expired, when cacheExpire == Template::cacheExpireSelector #pw-group-cache
*
* Access
*
* @property int|bool $useRoles Whether or not this template defines access. #pw-group-access
* @property PageArray $roles Roles assigned to this template for view access. #pw-group-access
* @property array $editRoles Array of Role IDs that may edit pages using this template. #pw-group-access
* @property array $addRoles Array of Role IDs that may add pages using this template. #pw-group-access
* @property array $createRoles Array of Role IDs that may create pages using this template. #pw-group-access
* @property array $rolesPermissions Override permissions: Array indexed by role ID with values as permission ID (add) or negative permission ID (revoke). #pw-group-access
* @property int $noInherit Disable role inheritance? Specify 1 to prevent edit/create/add access from inheriting to children, or 0 for default inherit behavior. #pw-group-access
* @property int $redirectLogin Redirect when no access: 0 = 404, 1 = login page, url = URL to redirect to, int(>1) = ID of page to redirect to. #pw-group-access
* @property int $guestSearchable Pages appear in search results even when user doesnt have access? (0=no, 1=yes) #pw-group-access
*
* Family
*
* @property int $childrenTemplatesID Template ID for child pages, or -1 if no children allowed. DEPRECATED #pw-internal
* @property string $sortfield Field that children of templates using this page should sort by (leave blank to let page decide, or specify "sort" for manual drag-n-drop). #pw-group-family
* @property int $noChildren Set to 1 to cancel use of childTemplates. #pw-group-family
* @property int $noParents Set to 1 to cancel use of parentTemplates, set to -1 to only allow one page using this template to exist. #pw-group-family
* @property array $childTemplates Array of template IDs that are allowed for children. Blank array indicates "any". #pw-group-family
* @property array $parentTemplates Array of template IDs that are allowed for parents. Blank array indicates "any". #pw-group-family
* @property string $childNameFormat Name format for child pages. when specified, the page-add UI step can be skipped when adding children. Counter appended till unique. Date format assumed if any non-pageName chars present. Use 'title' to pull from title field. #pw-group-family
*
* URLs
*
* @property int $allowPageNum Allow page numbers in URLs? (0=no, 1=yes) #pw-group-URLs
* @property int|string $urlSegments Allow URL segments on pages? (0=no, 1=yes (all), string=space separted list of segments to allow) #pw-group-URLs
* @property int $https Use https? (0 = http or https, 1 = https only, -1 = http only) #pw-group-URLs
* @property int $slashUrls Page URLs should have a trailing slash? 1 = yes, 0 = no #pw-group-URLs
* @property string|int $slashPageNum Should PageNum segments have a trailing slash? (0=either, 1=yes, -1=no) applies only if allowPageNum!=0. #pw-group-URLs
* @property string|int $slashUrlSegments Should last URL segment have a trailing slash? (0=either, 1=yes, -1=no) applies only if urlSegments!=0. #pw-group-URLs
*
* Files
*
* @property string $filename Template filename, including path (this is auto-generated from the name, though you may modify it at runtime if it suits your need). #pw-group-files
* @property string $altFilename Alternate filename for template file, if not based on template name. #pw-group-files
* @property string $contentType Content-type header or index (extension) of content type header from $config->contentTypes #pw-group-files
* @property int|bool $noPrependTemplateFile Disable automatic prepend of $config->prependTemplateFile (if in use). #pw-group-files
* @property int|bool $noAppendTemplateFile Disabe automatic append of $config->appendTemplateFile (if in use). #pw-group-files
* @property string $prependFile File to prepend to template file (separate from $config->prependTemplateFile). #pw-group-files
* @property string $appendFile File to append to template file (separate from $config->appendTemplateFile). #pw-group-files
* @property int $pagefileSecure Use secure pagefiles for pages using this template? 0=No/not set, 1=Yes (for non-public pages), 2=Always (3.0.166+) #pw-group-files
*
* Page Editor
*
* @property int $nameContentTab Pages should display the name field on the content tab? (0=no, 1=yes) #pw-group-page-editor
* @property string $tabContent Optional replacement for default "Content" label #pw-group-page-editor
* @property string $tabChildren Optional replacement for default "Children" label #pw-group-page-editor
* @property string $nameLabel Optional replacement for the default "Name" label on pages using this template #pw-group-page-editor
* @property int $errorAction Action to take when published page missing required field is saved (0=notify only, 1=restore prev value, 2=unpublish page) #pw-group-page-editor
*
* Behaviors
*
* @property int $allowChangeUser Allow the createdUser/created_users_id field of pages to be changed? (with API or in admin w/superuser only). 0=no, 1=yes #pw-group-behaviors
* @property int $noGlobal Template should ignore the global option of fields? (0=no, 1=yes) #pw-group-behaviors
* @property int $noMove Pages using this template are not moveable? (0=moveable, 1=not movable) #pw-group-behaviors
* @property int $noTrash Pages using this template may not go in trash? (i.e. they will be deleted not trashed) (0=trashable, 1=not trashable) #pw-group-behaviors
* @property int $noSettings Don't show a settings tab on pages using this template? (0=use settings tab, 1=no settings tab) #pw-group-behaviors
* @property int $noChangeTemplate Don't allow pages using this template to change their template? (0=template change allowed, 1=template change not allowed) #pw-group-behaviors
* @property int $noUnpublish Don't allow pages using this template to ever exist in an unpublished state - if page exists, it must be published. (0=page may be unpublished, 1=page may not be unpublished) #pw-group-behaviors
* @property int $noShortcut Don't allow pages using this template to appear in shortcut "add new page" menu. #pw-group-behaviors
* @property int $noLang Disable multi-language for this template (when language support active). #pw-group-behaviors
*
* Other
*
* @property int $compile Set to 1 to enable compilation, 2 to compile file and included files, 3 for auto, or 0 to disable. #pw-group-other
* @property string $tags Optional tags that can group this template with others in the admin templates list. #pw-group-tags
* @property string $pageLabelField CSV or space separated string of field names to be displayed by ProcessPageList (overrides those set with ProcessPageList config). #pw-group-other
* @property int|bool $_importMode Internal use property set by template importer when importing #pw-internal
* @property int|null $connectedFieldID ID of connected field or null or 0 if not applicable. #pw-internal
* @property string $editUrl URL to edit template, for administrator. #pw-internal
*
* Hookable methods
*
* @method Field|null getConnectedField() Get Field object connected to this field, or null if not applicable. #pw-internal
*
*
*/
class Template extends WireData implements Saveable, Exportable {
/**
* Flag used to indicate the field is a system-only field and thus can't be deleted or have it's name changed
*
*/
const flagSystem = 8;
/**
* Flag set if you need to override the system flag - set this first, then remove system flag in 2 operations.
*
*/
const flagSystemOverride = 32768;
/**
* Cache expiration options: expire only page cache
*
*/
const cacheExpirePage = 0;
/**
* Cache expiration options: expire entire site cache
*
*/
const cacheExpireSite = 1;
/**
* Cache expiration options: expire page and parents
*
*/
const cacheExpireParents = 2;
/**
* Cache expiration options: expire page and other specific pages (stored in cacheExpirePages)
*
*/
const cacheExpireSpecific = 3;
/**
* Cache expiration options: expire pages matching a selector
*
*/
const cacheExpireSelector = 4;
/**
* Cache expiration options: don't expire anything
*
*/
const cacheExpireNone = -1;
/**
* The PHP output filename used by this Template
*
*/
protected $filename;
/**
* Does the PHP template file exist?
*
*/
protected $filenameExists = null;
/**
* The Fieldgroup instance assigned to this Template
*
*/
protected $fieldgroup;
/**
* The previous Fieldgroup instance assigned to this template, if changed during runtime
*
*/
protected $fieldgroupPrevious = null;
/**
* Roles that pages using this template support
*
*/
protected $_roles = null;
/**
* Loaded state
*
* @var bool
*
*/
protected $loaded = true;
/**
* The template's settings, as they relate to database schema
*
*/
protected $settings = array(
'id' => 0,
'name' => '',
'fieldgroups_id' => 0,
'flags' => 0,
'cache_time' => 0,
);
/**
* Array where get/set properties are stored
*
*/
protected $data = array(
'useRoles' => 0, // does this template define access?
'editRoles' => array(), // IDs of roles that may edit pages using this template
'addRoles' => array(), // IDs of roles that may add children to pages using this template
'createRoles' => array(), // IDs of roles that may create pages using this template
'rolesPermissions' => array(), // Permission overrides by role: Array keys are role IDs, values are permission ID (add) or negative permission ID (revoke)
'noInherit' => 0, // Specify 1 to prevent edit/add/create access from inheriting to non-access controlled children, or 0 for default inherit behavior.
'childrenTemplatesID' => 0, // template ID for child pages, or -1 if no children allowed. DEPRECATED
'sortfield' => '', // Field that children of templates using this page should sort by. blank=page decides or 'sort'=manual drag-n-drop
'noChildren' => '', // set to 1 to cancel use of childTemplates
'noParents' => '', // set to 1 to cancel use of parentTemplates
'childTemplates' => array(), // array of template IDs that are allowed for children. blank array = any.
'parentTemplates' => array(), // array of template IDs that are allowed for parents. blank array = any.
'allowPageNum' => 0, // allow page numbers in URLs?
'allowChangeUser' => 0, // allow the createdUser/created_users_id field of pages to be changed? (with API or in admin w/superuser only)
'redirectLogin' => 0, // redirect when no access: 0 = 404, 1 = login page, 'url' = URL to redirec to
'urlSegments' => 0, // allow URL segments on pages? (0=no, 1=yes any, string=only these segments)
'https' => 0, // use https? 0 = http or https, 1 = https only, -1 = http only
'slashUrls' => 1, // page URLs should have a trailing slash? 1 = yes, 0 = no
'slashPageNum' => 0, // should page number segments end with a slash? 0=either, 1=yes, -1=no (applies only if allowPageNum=1)
'slashUrlSegments' => 0, // should URL segments end with a slash? 0=either, 1=yes, -1=no (applies only if urlSegments!=0)
'altFilename' => '', // alternate filename for template file, if not based on template name
'guestSearchable' => 0, // pages appear in search results even when user doesn't have access?
'pageClass' => '', // class for instantiated page objects. 'Page' assumed if blank, or specify class name.
'childNameFormat' => '', // Name format for child pages. when specified, the page-add UI step can be skipped when adding chilcren. Counter appended till unique. Date format assumed if any non-pageName chars present. Use 'title' to pull from title field.
'pageLabelField' => '', // CSV or space separated string of field names to be displayed by ProcessPageList (overrides those set with ProcessPageList config). May also be a markup {tag} format string.
'noGlobal' => 0, // template should ignore the 'global' option of fields?
'noMove' => 0, // pages using this template are not moveable?
'noTrash' => 0, // pages using this template may not go in trash? (i.e. they will be deleted not trashed)
'noSettings' => 0, // don't show a 'settings' tab on pages using this template?
'noChangeTemplate' => 0, // don't allow pages using this template to change their template?
'noShortcut' => 0, // don't allow pages using this template to appear in shortcut "add new page" menu
'noUnpublish' => 0, // don't allow pages using this template to ever exist in an unpublished state - if page exists, it must be published
'noLang' => 0, // disable languages for this template (if multi-language support active)
'compile' => 3, // Set to 1 to compile, set to 2 to compile file and included files, 3 for auto, or 0 to disable
'nameContentTab' => 0, // pages should display the 'name' field on the content tab?
'noCacheGetVars' => '', // GET vars that trigger disabling the cache (only when cache_time > 0)
'noCachePostVars' => '', // POST vars that trigger disabling the cache (only when cache_time > 0)
'useCacheForUsers' => 0, // use cache for: 0 = only guest users, 1 = guests and logged in users
'cacheExpire' => 0, // expire the cache for all pages when page using this template is saved? (1 = yes, 0 = no- only current page)
'cacheExpirePages' => array(), // array of Page IDs that should be expired, when cacheExpire == Template::cacheExpireSpecific
'cacheExpireSelector' => '', // selector string that matches pages to expire when cacheExpire == Template::cacheExpireSelector
'label' => '', // label that describes what this template is for (optional)
'tags' => '', // optional tags that can group this template with others in the admin templates list
'modified' => 0, // last modified time for template or template file
'titleNames' => 0, // future page title changes re-create the page names too? (recommend only if PagePathHistory is installed)
'noPrependTemplateFile' => 0, // disable automatic inclusion of $config->prependTemplateFile
'noAppendTemplateFile' => 0, // disable automatic inclusion of $config->appendTemplateFile
'prependFile' => '', // file to prepend (relative to /site/templates/)
'appendFile' => '', // file to append (relative to /site/templates/)
'pagefileSecure' => 0, // secure files connected with page? 0=Off, 1=Yes for unpub/non-public pages, 2=Always (3.0.166+)
'tabContent' => '', // label for the Content tab (if different from 'Content')
'tabChildren' => '', // label for the Children tab (if different from 'Children')
'nameLabel' => '', // label for the "name" property of the page (if something other than "Name")
'contentType' => '', // Content-type header or index of header from $config->contentTypes
'errorAction' => 0, // action to take on save when required field on published page is empty (0=notify,1=restore,2=unpublish)
'connectedFieldID' => null, // ID of connected field or null if not applicable
'ns' => '', // namespace found in the template file, or blank if not determined
);
/**
* Get or set loaded state
*
* When loaded state is false, we bypass some internal validations/checks that dont need to run while booting
*
* #pw-internal
*
* @param bool|null $loaded
* @return bool
* @since 3.0.153
*
*/
public function loaded($loaded = null) {
if($loaded !== null) $this->loaded = (bool) $loaded;
return $this->loaded;
}
/**
* Get a Template property
*
* #pw-internal
*
* @param string $key
* @return mixed
*
*/
public function get($key) {
if($key === 'filename') return $this->filename();
if($key === 'fields') $key = 'fieldgroup';
if($key === 'fieldgroup') return $this->fieldgroup;
if($key === 'fieldgroupPrevious') return $this->fieldgroupPrevious;
if($key === 'roles') return $this->getRoles();
if($key === 'cacheTime') $key = 'cache_time'; // for camel case consistency
if($key === 'icon') return $this->getIcon();
if($key === 'urlSegments') return $this->urlSegments();
if($key === 'editUrl') return $this->editUrl();
return isset($this->settings[$key]) ? $this->settings[$key] : parent::get($key);
}
/**
* Given different ways to refer to a role type return array of type, property and permission name
*
* @param string|Permission $type
* @return array Returns array of [ typeName, propertyName, permissionName ]
* @since 3.0.153
*
*/
protected function roleTypeNames($type) {
if($type instanceof Page) $type = $type->name;
if($type === 'view' || $type === 'roles' || $type === 'viewRoles' || $type === 'page-view') {
return array('view', 'roles', 'page-view');
} else if($type === 'edit' || $type === 'page-edit' || $type === 'editRoles') {
return array('edit', 'editRoles', 'page-edit');
} else if($type === 'create' || $type === 'page-create' || $type === 'createRoles') {
return array('create', 'createRoles', 'page-create');
} else if($type === 'add' || $type === 'page-add' || $type === 'addRoles') {
return array('add', 'addRoles', 'page-add');
}
return array('','','');
}
/**
* Get the role pages that are part of this template
*
* - This method returns a blank PageArray if roles havent yet been loaded into the template.
* - If the roles have previously been loaded as an array, then this method converts that array
* to a PageArray and returns it.
* - If you make changes to returned roles, make sure to set it back to the template again with setRoles().
* Its preferable to make changes with addRole() and removeRole() methods instead.
*
* #pw-group-access
*
* @param string $type Default is 'view', but you may also specify 'edit', 'create' or 'add' to retrieve those.
* @return PageArray of Role objects.
* @throws WireException if given an unknown roles type
*
*/
public function getRoles($type = 'view') {
if($type !== 'view') {
list($name, $propertyName, /*permissionName*/) = $this->roleTypeNames($type);
if($name !== 'view') {
if(empty($name)) throw new WireException("Unknown roles type: $type");
$roleIDs = $this->$propertyName;
if(empty($roleIDs)) return $this->wire()->pages->newPageArray();
return $this->wire()->pages->getById($roleIDs);
}
}
// type=view assumed from this point forward
if(is_null($this->_roles)) {
return $this->wire()->pages->newPageArray();
} else if($this->_roles instanceof PageArray) {
return $this->_roles;
} else if(is_array($this->_roles)) {
$errors = array();
$roles = $this->wire()->pages->newPageArray();
if(count($this->_roles)) {
$test = implode('0', $this->_roles); // test to see if it's all digits (IDs)
if(ctype_digit("$test")) {
$roles->import($this->pages->getById($this->_roles));
} else {
// role names
foreach($this->_roles as $name) {
$role = $this->wire()->roles->get($name);
if($role->id) {
$roles->add($role);
} else {
$errors[] = $name;
}
}
}
}
if(count($errors) && $this->useRoles) $this->error("Unable to load role(s): " . implode(', ', $errors));
$this->_roles = $roles;
return $this->_roles;
} else {
return $this->wire()->pages->newPageArray();
}
}
/**
* Does this template have the given Role?
*
* #pw-group-access
*
* @param string|Role|Page $role Name of Role or Role object.
* @param string|Permission Specify one of the following:
* - `view` (default)
* - `edit`
* - `create`
* - `add`
* - Or a `Permission` object of `page-view` or `page-edit`
* @return bool True if template has the role, false if not
*
*/
public function hasRole($role, $type = 'view') {
list($type, $property, /*permissionName*/) = $this->roleTypeNames($type);
$has = false;
$roles = $this->getRoles();
$rolePage = null;
if(is_string($role)) {
$has = $roles->has("name=$role");
} else if(is_int($role)) {
$has = $roles->has("id=$role");
$rolePage = $this->wire()->roles->get($role);
} else if($role instanceof Page) {
$has = $roles->has($role);
$rolePage = $role;
}
if($type === 'view') return $has;
if(!$has) return false; // page-view is a pre-requisite
if(!$rolePage || !$rolePage->id) $rolePage = $this->wire()->roles->get($role);
if(!$rolePage->id) return false;
$has = $property ? in_array($rolePage->id, $this->$property) : false;
return $has;
}
/**
* Set roles for this template
*
* #pw-group-access
* #pw-group-manipulation
*
* @param array|PageArray $value Role objects or array or Role IDs.
* @param string $type Specify one of the following:
* - `view` (default)
* - `edit`
* - `create`
* - `add`
* - Or a `Permission` object of `page-view` or `page-edit`
*
*/
public function setRoles($value, $type = 'view') {
list($type, $property, /* permissionName */) = $this->roleTypeNames($type);
if(empty($property)) {
// @todo Some other $type, delegate to permissionByRole
return;
}
if($type === 'view') {
if(is_array($value) || $value instanceof PageArray) {
$this->_roles = $value;
}
return;
}
if(!WireArray::iterable($value)) $value = array();
$roleIDs = array();
$roles = null;
foreach($value as $v) {
$id = 0;
if(is_int($v)) {
$id = $v;
} else if(is_string($v)) {
if(ctype_digit($v)) {
$id = (int) $v;
} else {
if($roles === null) $roles = $this->wire()->roles;
$id = $roles ? (int) $roles->get($v)->id : 0;
if(!$id && $this->_importMode && $this->useRoles) {
$this->error("Unable to load role '$v' for '$this.$type'");
}
}
} else if($v instanceof Page) {
$id = (int) $v->id;
}
if($id) $roleIDs[] = $id;
}
parent::set($property, $roleIDs);
}
/**
* Set roles/permissions
*
* #pw-internal
*
* @param array $value
* @since 3.0.153
*
*/
protected function setRolesPermissions($value) {
if(!is_array($value)) $value = array();
$a = array();
$roles = $this->wire()->roles;
$permissions = $this->wire()->permissions;
foreach($value as $roleID => $permissionIDs) {
if(!ctype_digit("$roleID")) {
// convert role name to ID
$roleID = $roles->get("name=$roleID")->id;
}
if(!$roleID) continue;
foreach($permissionIDs as $permissionID) {
$test = ltrim($permissionID, '-');
if(!ctype_digit($test)) {
// convert permission name to ID
$revoke = strpos($permissionID, '-') === 0;
$permissionID = $permissions->get("name=$test")->id;
if(!$permissionID) continue;
if($revoke) $permissionID = "-$permissionID";
}
// we force these as strings so that they can portable in JSON
$roleID = (string) ((int) $roleID);
$permissionID = (string) ((int) $permissionID);
if(!isset($a[$roleID])) $a[$roleID] = array();
$a[$roleID][] = $permissionID;
}
}
parent::set('rolesPermissions', $a);
}
/**
* Add a Role to this template for view, edit, create, or add permission
*
* @param Role|int|string $role Role instance, id or name
* @param string $type Type of role being added, one of: view, edit, create, add. (default=view)
* @return $this
* @throws WireException If given $role cannot be resolved
*
*/
public function addRole($role, $type = 'view') {
if(is_int($role) || is_string($role)) $role = $this->wire()->roles->get($role);
if(!$role instanceof Role) throw new WireException("addRole requires Role instance, name or id");
$roles = $this->getRoles($type);
if(!$roles->has($role)) {
$roles->add($role);
$this->setRoles($roles, $type);
}
return $this;
}
/**
* Remove a Role to this template for view, edit, create, or add permission
*
* @param Role|int|string $role Role instance, id or name
* @param string $type Type of role being added, one of: view, edit, create, add. (default=view)
* You may also specify “all” to remove the role entirely from all possible usages in the template.
* @return $this
* @throws WireException If given $role cannot be resolved
*
*/
public function removeRole($role, $type = 'view') {
if(is_int($role) || is_string($role)) {
$role = $this->wire()->roles->get($role);
}
if(!$role instanceof Role) {
throw new WireException("removeRole requires Role instance, name or id");
}
if($type == 'all') {
$types = array('create', 'add', 'edit', 'view');
$rolesPermissions = $this->rolesPermissions;
if(isset($rolesPermissions["$role->id"])) {
unset($rolesPermissions["$role->id"]);
$this->rolesPermissions = $rolesPermissions;
}
} else {
$types = array($type);
}
foreach($types as $t) {
$roles = $this->getRoles($t);
if($roles->has($role)) {
$roles->remove($role);
$this->setRoles($roles, $t);
}
}
return $this;
}
/**
* Add a permission that applies to users having a specific role with pages using this template
*
* Note that the change is not committed until you save() the template.
*
* @param Permission|int|string $permission Permission object, name, or id
* @param Role|int|string $role Role object, name or id
* @param bool $test Specify true to only test if an update would be made, without changing anything
* @return bool Returns true if an update was made (or would be made), false if not
*
*/
public function addPermissionByRole($permission, $role, $test = false) {
return $this->wire()->templates->setTemplatePermissionByRole($this, $permission, $role, false, $test);
}
/**
* Revoke a permission that applies to users having a specific role with pages using this template
*
* Note that the change is not committed until you save() the template.
*
* @param Permission|int|string $permission Permission object, name, or id
* @param Role|int|string $role Role object, name or id
* @param bool $test Specify true to only test if an update would be made, without changing anything
* @return bool Returns true if an update was made (or would be made), false if not
*
*/
public function revokePermissionByRole($permission, $role, $test = false) {
return $this->wire()->templates->setTemplatePermissionByRole($this, $permission, $role, true, $test);
}
/**
* Does this template have the given Field?
*
* #pw-group-fields
*
* @param string|int|Field $name May be field name, id or object.
* @return bool
*
*/
public function hasField($name) {
return $this->fieldgroup->hasField($name);
}
/**
* Set a Template property
*
* #pw-internal
*
* @param string $key
* @param mixed $value
* @return $this
* @throws WireException
*
*/
public function set($key, $value) {
if($key == 'cacheTime') $key = 'cache_time'; // alias
if($key == 'flags') {
$this->setFlags($value);
} else if(isset($this->settings[$key])) {
$this->setSetting($key, $value);
} else if($key == 'fieldgroup' || $key == 'fields') {
$this->setFieldgroup($value);
} else if($key == 'filename') {
$this->filename($value);
} else if($key == 'childrenTemplatesID') { // this can eventaully be removed
if($value < 0) {
parent::set('noChildren', 1);
} else if($value) {
$v = $this->childTemplates;
$v[] = (int) $value;
parent::set('childTemplates', $v);
}
} else if($key == 'sortfield') {
$value = $this->wire()->pages->sortfields()->decode($value, '');
parent::set($key, $value);
} else if($key === 'roles' || $key === 'addRoles' || $key === 'editRoles' || $key === 'createRoles') {
$this->setRoles($value, $key);
} else if($key === 'rolesPermissions') {
$this->setRolesPermissions($value);
} else if($key === 'childTemplates' || $key === 'parentTemplates') {
if($this->loaded) {
$this->familyTemplates($key, $value);
} else {
parent::set($key, $value);
}
} else if($key === 'noChildren' || $key === 'noParents') {
$value = (int) $value;
if(!$value) $value = null; // enforce null over 0
parent::set($key, $value);
} else if($key == 'cacheExpirePages') {
$this->setCacheExpirePages($value);
} else if($key == 'icon') {
$this->setIcon($value);
} else if($key == 'urlSegments') {
$this->urlSegments($value);
} else if($key == 'connectedFieldID') {
parent::set($key, (int) $value);
} else {
parent::set($key, $value);
}
return $this;
}
/**
* Set a setting
*
* #pw-internal
*
* @param string $key
* @param mixed $value
* @since 3.0.153
* @throws WireException
*
*/
protected function setSetting($key, $value) {
if($key === 'id') {
$value = (int) $value;
} else if($key === 'name') {
$value = $this->loaded ? $this->wire()->sanitizer->templateName($value) : $value;
} else if($key === 'fieldgroups_id' && $value) {
$fieldgroup = $this->wire()->fieldgroups->get($value);
if($fieldgroup) {
$this->setFieldgroup($fieldgroup);
} else if($this->id) {
$this->error("Unable to load fieldgroup '$value' for template $this->name");
}
return;
} else if($key == 'cache_time') {
$value = (int) $value;
} else {
// unknown or invalid setting
$value = '';
}
if($this->loaded && $this->settings[$key] != $value) {
if(($key === 'id' || $key === 'name') && $this->settings[$key] && ($this->settings['flags'] & Template::flagSystem)) {
throw new WireException("Template '$this' has the system flag and you may not change its 'id' or 'name' settings.");
}
$this->trackChange($key, $this->settings[$key], $value);
}
$this->settings[$key] = $value;
}
/**
* Set setting value without processing
*
* #pw-internal
*
* @param string $key
* @param mixed $value
* @since 3.0.194
*
*/
public function setRaw($key, $value) {
if($key === 'fieldgroups_id') {
$fieldgroup = $this->wire()->fieldgroups->get($value);
if($fieldgroup) {
$this->settings['fieldgroups_id'] = (int) $value;
$this->fieldgroup = $fieldgroup;
}
} else if(isset($this->settings[$key])) {
$this->settings[$key] = $value;
} else {
parent::set($key, $value);
}
}
/**
* Set the cacheExpirePages property
*
* @param array $value
*
*/
protected function setCacheExpirePages($value) {
if(!is_array($value)) $value = array();
foreach($value as $k => $v) {
if(is_object($v)) {
$v = $v->id;
} else if(!ctype_digit("$v")) {
$p = $this->wire()->pages->get($v);
if(!$p->id) $this->error("Unable to load page: $v");
$v = $p->id;
}
$value[(int) $k] = (int) $v;
}
parent::set('cacheExpirePages', $value);
}
/**
* Get or set allowed URL segments
*
* #pw-group-URLs
*
* @param array|int|bool|string $value Omit to return current value, or to set value:
* - Specify array of allowed URL segments, may include 'segment', 'segment/path' or 'regex:your-regex'.
* - Or specify boolean true or 1 to enable all URL segments.
* - Or specify integer 0, boolean false, or blank array to disable all URL segments.
* @return array|int Returns array of allowed URL segments, or 0 if disabled, or 1 if any allowed.
*
*/
public function urlSegments($value = '~') {
if($value === '~') {
// return current only
$value = $this->data['urlSegments'];
if(empty($value)) return 0;
if(is_array($value)) return $value;
return 1;
} else if(is_array($value)) {
// set array value
if(count($value)) {
// we'll take it
foreach($value as $k => $v) {
$v = trim($v); // trim whitespace
$v = trim($v, '/'); // remove leading/trailing slashes
if($v !== $value[$k]) $value[$k] = $v;
}
} else {
// blank array becomes 0
$value = 0;
}
} else {
// enforce 0 or 1
$value = empty($value) ? 0 : 1;
}
if(empty($this->data['urlSegments']) && empty($value)) {
// don't bother updating if both are considered empty
return $value;
}
if($this->data['urlSegments'] !== $value) {
// update current value
$this->trackChange('urlSegments', $this->data['urlSegments'], $value);
$this->data['urlSegments'] = $value;
}
return $value;
}
/**
* Is the given URL segment string allowed according to this templates settings?
*
* #pw-group-URLs
*
* @param string $urlSegmentStr
* @return bool
* @since 3.0.186
*
*/
public function isValidUrlSegmentStr($urlSegmentStr) {
$rules = $this->urlSegments();
$valid = false;
if(is_array($rules)) {
// only specific URL segments are allowed
$urlSegmentStr = trim($urlSegmentStr, '/');
foreach($rules as $rule) {
if(stripos($rule, 'regex:') === 0) {
$regex = '{' . trim(substr($rule, 6)) . '}';
$valid = preg_match($regex, $urlSegmentStr);
} else if($urlSegmentStr === $rule) {
$valid = true;
}
if($valid) break;
}
} else if($rules > 0 || $this->name === 'admin') {
// all URL segments are allowed
$valid = true;
}
return $valid;
}
/**
* Set the flags for this Template
*
* As a safety it prevents the system flag from being removed.
*
* @param int $value
*
*/
protected function setFlags($value) {
$value = (int) $value;
$override = $this->settings['flags'] & Template::flagSystemOverride;
if($this->settings['flags'] & Template::flagSystem) {
// prevent the system flag from being removed
if(!$override) $value = $value | Template::flagSystem;
}
$this->settings['flags'] = $value;
}
/**
* Set this template's filename, with or without path
*
* @param string $value The filename with or without path
* @deprecated Now just using filename() method
*
*/
protected function setFilename($value) {
$this->filename($value);
}
/**
* Set this Template's Fieldgroup
*
* #pw-group-fields
* #pw-group-manipulation
*
* @param Fieldgroup $fieldgroup
* @return $this
* @throws WireException
*
*/
public function setFieldgroup(Fieldgroup $fieldgroup) {
if($this->fieldgroup === null || $fieldgroup->id != $this->fieldgroup->id) {
if($this->loaded) $this->trackChange('fieldgroup', $this->fieldgroup, $fieldgroup);
}
if($this->fieldgroup && $fieldgroup->id != $this->fieldgroup->id) {
// save record of the previous fieldgroup so that unused fields can be deleted during save()
$this->fieldgroupPrevious = $this->fieldgroup;
if($this->flags & Template::flagSystem) {
throw new WireException("Can't change fieldgroup for template '{$this}' because it is a system template.");
}
$hasPermanentFields = false;
foreach($this->fieldgroup as $field) {
if($field->flags & Field::flagPermanent) $hasPermanentFields = true;
}
if($this->id && $hasPermanentFields) {
throw new WireException("Fieldgroup for template '{$this}' may not be changed because it has permanent fields.");
}
}
$this->fieldgroup = $fieldgroup;
$this->settings['fieldgroups_id'] = $fieldgroup->id;
return $this;
}
/**
* Return the number of pages used by this template.
*
* #pw-group-identification
*
* @return int
*
*/
public function getNumPages() {
return $this->wire()->templates->getNumPages($this);
}
/**
* Save the template to database
*
* This is the same as calling `$templates->save($template)`.
*
* #pw-group-manipulation
*
* @return Template|bool Returns Template if successful, or false if not
*
*/
public function save() {
$result = $this->wire()->templates->save($this);
return $result ? $this : false;
}
/**
* Return corresponding template filename including path, or set template filename
*
* #pw-group-files
*
* @param string $filename Specify basename or path+basename to set, or omit to get filename. This argument added 3.0.143.
* @return string
* @throws WireException
*
*/
public function filename($filename = null) {
$config = $this->wire()->config;
$path = $config->paths->templates;
if($filename !== null) {
// setting filename
if(empty($filename) || !is_string($filename)) {
// set to empty
$filename = '';
} else if(strpos($filename, '/') === false) {
// value is basename
$filename = $path . $filename;
} else if(strpos($filename, $config->paths->root) !== 0) {
// value is path outside of our installation root, which we do not accept
$filename = $path . basename($filename);
}
if($filename !== $this->filename) $this->filenameExists = null;
$this->filename = $filename;
} else if($this->filename) {
// get existing filename
$filename = $this->filename;
} else {
// get filename and determine what it is from template settings
$ext = '.' . $config->templateExtension;
$altFilename = $this->altFilename;
if($altFilename) {
$filename = $path . basename($altFilename, $ext) . $ext;
} else if(!$this->settings['name']) {
throw new WireException("Template must be assigned a name before 'filename' can be accessed");
} else {
$filename = $path . $this->settings['name'] . $ext;
}
$this->filename = $filename;
$this->filenameExists = null;
}
if($this->filenameExists === null && $filename) {
$this->filenameExists = file_exists($filename);
if($this->filenameExists) {
// if filename exists, keep track of last modification time
$isModified = false;
$modified = filemtime($filename);
if($modified > $this->modified) {
$isModified = true;
$this->modified = $modified;
}
if($isModified || !$this->ns) {
// determine namespace
$files = $this->wire()->files;
$templates = $this->wire()->templates;
$this->ns = $files->getNamespace($filename);
$templates->fileModified($this);
}
}
}
return $filename;
}
/**
* Saves a template after the request is complete
*
* #pw-internal
*
* @param HookEvent $e
*
*/
public function hookFinished(HookEvent $e) {
foreach($e->wire()->templates as $template) {
if($template->isChanged('modified') || $template->isChanged('ns')) $template->save();
}
}
/**
* Does the template filename exist?
*
* #pw-group-files
*
* @return bool
*
*/
public function filenameExists() {
if($this->filenameExists !== null) return $this->filenameExists;
$this->filenameExists = file_exists($this->filename());
return $this->filenameExists;
}
/**
* Per Saveable interface, get an array of this table's data
*
* We override this so that we can add our roles array to it.
*
* #pw-internal
*
*/
public function getArray() {
$a = parent::getArray();
if($this->useRoles) {
$a['roles'] = array();
foreach($this->getRoles() as $role) {
$a['roles'][] = $role->id;
}
} else {
unset($a['roles'], $a['editRoles'], $a['addRoles'], $a['createRoles'], $a['rolesPermissions']);
}
return $a;
}
/**
* Per Saveable interface: return data for storage in table
*
* #pw-internal
*
*/
public function getTableData() {
$tableData = $this->settings;
$data = $this->getArray();
// ensure sortfield is a signed integer or native name, rather than a custom fieldname
if(!empty($data['sortfield'])) {
$data['sortfield'] = $this->wire()->pages->sortfields()->encode($data['sortfield'], '');
}
$tableData['data'] = $data;
return $tableData;
}
/**
* Per Saveable interface: return data for external storage
*
* #pw-internal
*
*/
public function getExportData() {
return $this->wire()->templates->getExportData($this);
}
/**
* Given an array of export data, import it
*
* @param array $data
* @return bool True if successful, false if not
* @return array Returns 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)
* )
*
* #pw-internal
*
*/
public function setImportData(array $data) {
return $this->wire()->templates->setImportData($this, $data);
}
/**
* The string value of a Template is always it's name
*
*/
public function __toString() {
return $this->name;
}
/**
* Get or set parent templates (templates allowed for parent pages of pages using this template)
*
* - May be specified as template IDs or names in an array, or Template objects in a TemplatesArray.
* - To allow any template as parent, specify a blank array.
* - To disallow any parents (other than whats already in use) set the `$template->noParents` property to 1.
*
* #pw-group-family
*
* @param array|TemplatesArray|null $setValue Specify only when setting, an iterable value containing Template objects, IDs or names
* @return TemplatesArray
* @since 3.0.153
*
*/
public function parentTemplates($setValue = null) {
return $this->familyTemplates('parentTemplates', $setValue);
}
/**
* Get or set child templates (templates allowed for children of pages using this template)
*
* - May be specified as template IDs or names in an array, or Template objects in a TemplatesArray.
* - To allow any template to be used for children, specify a blank array.
* - To disallow any children (other than whats already in use) set the `$template->noChildren` property to 1.
*
* #pw-group-family
*
* @param array|TemplatesArray|null $setValue Specify only when setting, an iterable value containing Template objects, IDs or names
* @return TemplatesArray
* @since 3.0.153
*
*/
public function childTemplates($setValue = null) {
return $this->familyTemplates('childTemplates', $setValue);
}
/**
* Get or set childTemplates or parentTemplates
*
* #pw-internal
*
* @param string $property Specify either 'childTemplates' or 'parentTemplates'
* @param array|TemplatesArray|null $setValue Iterable value containing Template objects, IDs or names
* @return TemplatesArray
* @since 3.0.153
*
*/
protected function familyTemplates($property, $setValue = null) {
$templates = $this->wire()->templates;
$value = new TemplatesArray();
$this->wire($value);
if($setValue !== null && WireArray::iterable($setValue)) {
// set
$ids = array();
foreach($setValue as $v) {
$template = $v instanceof Template ? $v : $templates->get($v);
if($template) {
$ids[$template->id] = $template->id;
$value->add($template);
} else if($this->_importMode) {
$this->error("Unable to load template '$v' for '$this->name.$property'");
}
}
parent::set($property, array_values($ids));
} else {
// get
foreach($this->$property as $id) {
$template = $templates->get((int) $id);
if($template) $value->add($template);
}
}
return $value;
}
/**
* Allow new pages that use this template?
*
* #pw-group-family
*
* @return bool
* @since 3.0.153
*
*/
public function allowNewPages() {
$pages = $this->wire()->pages;
$noParents = (int) $this->noParents;
if($noParents === 1) {
// no new pages may be created
return false;
} else if($noParents === -1) {
// only one may exist
if($pages->has("template=$this")) return false;
}
return true;
}
/**
* 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.
*
* #pw-group-family
*
* @param bool $checkAccess Whether or not to check for user access to do this (default=false).
* @return Page|NullPage|null
*
*/
public function getParentPage($checkAccess = false) {
return $this->wire()->templates->getParentPage($this, $checkAccess);
}
/**
* Return all possible parent pages for this template
*
* #pw-group-family
*
* @param bool $checkAccess Specify true to exclude parents that user doesn't have access to add children to (default=false)
* @return PageArray
*
*/
public function getParentPages($checkAccess = false) {
return $this->wire()->templates->getParentPages($this, $checkAccess);
}
/**
* Return template label for current language, or specified language if provided
*
* If no template label, return template name.
* This is different from `$template->label` in that it knows about languages (when installed)
* and it will always return something. If there's no label, you'll still get the name.
*
* #pw-group-identification
*
* @param Page|Language $language Optional, if not used then user's current language is used
* @return string
*
*/
public function getLabel($language = null) {
if(is_null($language)) {
$language = $this->wire()->languages ? $this->wire()->user->language : null;
}
if($language) {
$label = (string) $this->get("label$language");
if(!strlen($label)) $label = $this->label;
} else {
$label = (string) $this->label;
}
if(!strlen($label)) $label = $this->name;
return $label;
}
/**
* Return page tab label for current language (or specified language if provided)
*
* #pw-group-page-editor
*
* @param string $tab Which tab? 'content' or 'children'
* @param Page|Language $language Optional, if not used then user's current language is used
* @return string Returns blank if default tab label not overridden
*
*/
public function getTabLabel($tab, $language = null) {
$tab = ucfirst(strtolower($tab));
if(is_null($language)) $language = $this->wire()->languages ? $this->wire()->user->language : null;
if(!$language || $language->isDefault()) $language = '';
$label = $this->get("tab$tab$language");
return $label;
}
/**
* Return the overriden "page name" label, or blank if not overridden
*
* #pw-group-page-editor
*
* @param Language|null $language
* @return string
*
*/
public function getNameLabel($language = null) {
if(is_null($language)) $language = $this->wire()->languages ? $this->wire()->user->language : null;
if(!$language || $language->isDefault()) $language = '';
return $this->get("nameLabel$language");
}
/**
* Return the icon name used by this template
*
* #pw-group-identification
*
* @param bool $prefix Specify true if you want the icon prefix (icon- or fa-) to be included (default=false).
* @return string Returns a font-awesome icon name
*
*/
public function getIcon($prefix = false) {
$label = $this->pageLabelField;
$icon = '';
if(strpos($label, 'icon-') !== false || strpos($label, 'fa-') !== false) {
if(preg_match('/\b(icon-|fa-)([^\s,]+)/', $label, $matches)) {
if($matches[1] == 'icon-') $matches[1] = 'fa-';
$icon = $prefix ? $matches[1] . $matches[2] : $matches[2];
}
}
return $icon;
}
/**
* Get languages allowed for this template or null if language support not active.
*
* #pw-group-identification
*
* @return PageArray|Languages|null Returns a PageArray of Language objects, or NULL if language support not active.
*
*/
public function getLanguages() {
$languages = $this->wire()->languages;
if(!$languages) return null;
if(!$this->noLang) return $languages;
$langs = $this->wire()->pages->newPageArray();
// if noLang set, then only default language is included
$langs->add($languages->getDefault());
return $langs;
}
/**
* Get class name to use for Page objects using this 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 the pageClass property.
*
* #pw-group-identification
*
* @param bool $withNamespace Returned class includes namespace? (default=true)
* @return string Returned page class includes namespace
* @since 3.0.152
*
*/
public function getPageClass($withNamespace = true) {
return $this->wire()->templates->getPageClass($this, $withNamespace);
}
/**
* Get tags array
*
* #pw-group-tags
*
* @return array
* @since 3.0.176
*
*/
public function getTags() {
$tags = array();
foreach(explode(' ', $this->tags) as $tag) {
if(!strlen($tag)) continue;
$tags[$tag] = $tag;
}
return $tags;
}
/**
* Does this template have given tag?
*
* #pw-group-tags
*
* @param string $tag
* @return bool
* @since 3.0.176
*
*/
public function hasTag($tag) {
$tags = $this->getTags();
return isset($tags[$tag]);
}
/**
* Add tag
*
* #pw-group-tags
*
* @param string $tag
* @return $this
* @since 3.0.176
*
*/
public function addTag($tag) {
$tags = $this->getTags();
if(isset($tags[$tag])) return $this;
$tags[$tag] = $tag;
$this->set('tags', implode(' ', $tags));
return $this;
}
/**
* Remove tag
*
* #pw-group-tags
*
* @param string $tag
* @return self
* @since 3.0.176
*
*/
public function removeTag($tag) {
$tags = $this->getTags();
if(!isset($tags[$tag])) return $this;
unset($tags[$tag]);
$this->set('tags', implode(' ', $tags));
return $this;
}
/**
* Check that all file asset paths are consistent with current pagefileSecure setting and access control
*
* #pw-internal
*
* @return int Returns quantity of renamed paths, or 0 if all is in order
* @since 3.0.166
*
*/
public function checkPagefileSecure() {
PagefilesManager::numRenamedPaths(true);
foreach($this->wire()->pages->findMany("template=$this, include=all") as $p) {
PagefilesManager::_path($p);
}
return PagefilesManager::numRenamedPaths(true);
}
/**
* Set the icon to use with this template
*
* #pw-group-identification
*
* @param string $icon Font-awesome icon name
* @return $this
*
*/
public function setIcon($icon) {
// This manipulates the pageLabelField property, since there isn't actually an icon property.
$icon = $this->wire()->sanitizer->pageName($icon);
$current = $this->getIcon(false);
$label = $this->pageLabelField;
if(strpos($icon, "icon-") === 0) $icon = str_replace("icon-", "fa-", $icon); // convert icon-str to fa-str
if($icon && strpos($icon, "fa-") !== 0) $icon = "fa-$icon"; // convert anon icon to fa-icon
if($current) {
// replace icon currently in pageLabelField with new one
$label = str_replace(array("fa-$current", "icon-$current"), $icon, $label);
} else if($icon) {
// add icon to pageLabelField where there wasn't one already
if(empty($label)) $label = $this->fieldgroup->hasField('title') ? 'title' : '';
$label = trim("$icon $label");
}
$this->pageLabelField = $label;
return $this;
}
/**
* Get Field object connected with this template
*
* #pw-internal
*
* @return Field|null Returns Field object or null if not applicable
* @since 3.0.142
*
*/
public function ___getConnectedField() {
$fields = $this->wire()->fields;
if($this->connectedFieldID) {
$field = $fields->get((int) $this->connectedFieldID);
} else {
$field = null;
}
if(!$field) {
$fieldName = '';
$templateName = $this->name;
$prefixes = array('field-', 'field_', 'repeater_');
foreach($prefixes as $prefix) {
if(strpos($templateName, $prefix) !== 0) continue;
list(,$fieldName) = explode($prefix, $templateName, 2);
break;
}
if($fieldName) {
$field = $fields->get($fieldName);
}
}
return $field;
}
/**
* URL to edit template settings (for administrator)
*
* @param bool $http Full http/https URL?
* @return string
* @since 3.0.170
*
*/
public function editUrl($http = false) {
return $this->wire()->config->urls($http ? 'httpAdmin' : 'admin') . "setup/template/edit?id=$this->id";
}
/**
* Ensures that isset() and empty() work for this classes properties.
*
* #pw-internal
*
* @param string $key
* @return bool
*
*/
public function __isset($key) {
return isset($this->settings[$key]) || isset($this->data[$key]);
}
public function __debugInfo() {
return array_merge(array('settings' => $this->settings), parent::__debugInfo());
}
}