praiadeseselle/wire/core/Template.php

1558 lines
53 KiB
PHP
Raw Permalink Normal View History

2022-03-08 15:55:41 +01:00
<?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 2019 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 $fieldgroup The Fieldgroup used by the template. Can also be used to iterate a Template's fields. #pw-group-fields
* @property Fieldgroup $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(is_object($type) && $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();
foreach($value as $roleID => $permissionIDs) {
if(!ctype_digit("$roleID")) {
// convert role name to ID
$roleID = $this->wire('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 = $this->wire('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')->name($value) : $value;
} else if($key == 'fieldgroups_id' && $value) {
$fieldgroup = $this->wire('fieldgroups')->get($value);
if($fieldgroup) {
$this->setFieldgroup($fieldgroup);
} else {
$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 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;
}
/**
* 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) {
/** @var Config $config */
$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');
/** @var WireFileTools $files */
$templates = $this->wire('templates');
/** @var Templates $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) {
/** @var Templates $templates */
$templates = $this->wire('templates');
$value = new TemplatesArray();
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'); /** @var Pages $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 = $this->get("label$language");
if(!strlen($label)) $label = $this->label;
} else {
$label = $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() {
/** @var Languages $languages */
$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() {
if($this->connectedFieldID) {
$field = $this->wire('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 = $this->wire('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());
}
}