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

267 lines
7.9 KiB
PHP

<?php namespace ProcessWire;
/**
* ProcessWire Pages Access
*
* Maintains the pages_access table which serves as a way to line up pages
* to the templates that maintain their access roles.
*
* This class serves as a way for pageFinder() to determine if a user has access to a page
* before actually loading it.
*
* The pages_access template contains just two columns:
*
* - pages_id: Any given page
* - templates_id: The template that sets this pages access
*
* Pages using templates that already define their access (determined by $template->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;
}
}