268 lines
7.9 KiB
PHP
268 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;
|
||
|
}
|
||
|
|
||
|
|
||
|
}
|