useRoles) * are omitted from the pages_access table, as they aren't necessary. * * ProcessWire 3.x, Copyright 2023 by Ryan Cramer * https://processwire.com * * */ class PagesAccess extends Wire { /** * Cached templates that don't define access * */ protected $_templates = array(); /** * Cached templates that DO define access * */ protected $_accessTemplates = array(); /** * Array of page parent IDs that have already been completed * */ protected $completedParentIDs = array(); /** * Construct a PagesAccess instance, optionally specifying a Page or Template * * If Page or Template specified, then the updateTemplate or updatePage method is assumed. * * @param Page|Template * */ public function __construct($item = null) { parent::__construct(); if(!$item) return; if($item instanceof Page) { $this->updatePage($item); } else if($item instanceof Template) { $this->updateTemplate($item); } } /** * Rebuild the entire pages_access table (or a part of it) starting from the given parent_id * * @param int $parent_id * @param int $accessTemplateID * @param bool $doDeletions * */ protected function rebuild($parent_id = 1, $accessTemplateID = 0, $doDeletions = true) { $insertions = array(); $deletions = array(); $accessTemplates = $this->getAccessTemplates(); $parent_id = (int) $parent_id; $accessTemplateID = (int) $accessTemplateID; $database = $this->wire()->database; if(!$accessTemplateID && $this->wire()->config->debug) $this->message("Rebuilding pages_access"); if($parent_id == 1) { // if we're going to be rebuilding the entire tree, then just delete all of them now $database->exec("DELETE FROM pages_access"); // QA $doDeletions = false; } // no access template supplied (likely because of blank call to rebuild() // so we determine it automatically if(!$accessTemplateID) { $parent = $this->pages->get($parent_id); $accessTemplateID = $parent->getAccessTemplate()->id; } // if the accessTemplate has the guest role, it does not need to be in our pages_access table // since access to it is assumed for everyone. So we tell it not to perform insertions, but // it should continue through the page tree $template = $this->templates->get($accessTemplateID); $doInsertions = !$template->hasRole('guest'); $sql = "SELECT pages.id, pages.templates_id, count(children.id) AS numChildren " . "FROM pages " . "LEFT JOIN pages AS children ON children.parent_id=pages.id " . "WHERE pages.parent_id=:parent_id " . "GROUP BY pages.id "; $query = $database->prepare($sql); $query->bindValue(":parent_id", $parent_id, \PDO::PARAM_INT); $query->execute(); while($row = $query->fetch(\PDO::FETCH_NUM)) { list($id, $templates_id, $numChildren) = $row; if(isset($accessTemplates[$templates_id])) { // this page is defining access with it's template // if there are children, rebuild those children with this template for access if($numChildren) $this->rebuild($id, $templates_id, $doDeletions); } else { // this template is not defining access... if($doInsertions) { // ...so save an entry for the page and the template that IS defining access $insertions[$id] = (int) $accessTemplateID; } else if($doDeletions) { // ...or delete existing entries if guest access is present $deletions[] = (int) $id; } // if there are children, rebuild any of them with this access template where applicable if($numChildren) $this->rebuild($id, $accessTemplateID, $doDeletions); } } $query->closeCursor(); if(count($insertions)) { // add the entries to the pages_access table $sql = "INSERT INTO pages_access (pages_id, templates_id) VALUES "; foreach($insertions as $id => $templates_id) { $id = (int) $id; $templates_id = (int) $templates_id; $sql .= "($id, $templates_id),"; } $sql = rtrim($sql, ",") . " " . "ON DUPLICATE KEY UPDATE templates_id=VALUES(templates_id) "; $query = $database->prepare($sql); $query->execute(); } else if(count($deletions)) { $sql = "DELETE FROM pages_access WHERE pages_id IN(" . implode(',', $deletions) . ')'; $query = $database->prepare($sql); $query->execute(); } } /** * Update the pages_access table for the given Template * * To be called when a template's 'useRoles' property has changed. * * @param Template $template * */ public function updateTemplate(Template $template) { $this->rebuild(); } /** * Save to pages_access table to indicate what template each page is getting it's access from * * This should be called a page has been saved and it's parent or template has changed. * Or, when a new page is added. * * If there is no entry in this table, then the page is getting it's access from it's existing template. * * This is used by PageFinder to determine what pages to include in a find() operation based on user access. * * @param Page $page * */ public function updatePage(Page $page) { $page_id = (int) $page->id; if(!$page_id) return; // this is the template where access is defined for this page $accessParent = $page->getAccessParent(); $accessTemplate = $accessParent->template; $database = $this->wire()->database; if(!$accessParent->id || $accessParent->id == $page->id) { // page is the same as the one that defines access, so it doesn't need to be here $query = $database->prepare("DELETE FROM pages_access WHERE pages_id=:page_id"); } else { $template_id = (int) $accessParent->template->id; $sql = "INSERT INTO pages_access (pages_id, templates_id) " . "VALUES(:page_id, :template_id) " . "ON DUPLICATE KEY UPDATE templates_id=VALUES(templates_id) "; $query = $database->prepare($sql); $query->bindValue(":template_id", $template_id, \PDO::PARAM_INT); } $query->bindValue(":page_id", $page_id, \PDO::PARAM_INT); $query->execute(); if($page->numChildren > 0) { if($page->parentPrevious && $accessParent->id != $page->id) { // the page has children, it's parent was changed, and access is coming from the parent // so the children entries need to be updated to reflect this change $this->rebuild($page->id, $accessTemplate->id); } else if($page->templatePrevious) { // the page's template changed, so this may affect the children as well $this->rebuild($page->id, $accessTemplate->id); } } } /** * Delete a page from the pages_access table * * @param Page $page * */ public function deletePage(Page $page) { $database = $this->wire()->database; $query = $database->prepare("DELETE FROM pages_access WHERE pages_id=:page_id"); $query->bindValue(":page_id", $page->id, \PDO::PARAM_INT); $query->execute(); } /** * Returns an array of templates that DON'T define access * */ protected function getTemplates() { if(count($this->_templates)) return $this->_templates; foreach($this->templates as $template) { if($template->useRoles) { $this->_accessTemplates[$template->id] = $template; } else { $this->_templates[$template->id] = $template; } } return $this->_templates; } /** * Returns an array of templates that DO define access * */ protected function getAccessTemplates() { if(count($this->_accessTemplates)) return $this->_accessTemplates; $this->getTemplates(); return $this->_accessTemplates; } }