symphony/lib/toolkit/class.pagemanager.php
<?php
/**
* @package toolkit
*/
/**
* The `PageManager` class is responsible for providing basic CRUD operations
* for Symphony frontend pages. These pages are stored in the database in
* `tbl_pages` and are resolved to an instance of `FrontendPage` class from a URL.
* Additionally, this manager provides functions to access the Page's types,
* and any linked datasources or events.
*
* @since Symphony 2.3
*/
class PageManager
{
/**
* Given an associative array of data, where the key is the column name
* in `tbl_pages` and the value is the data, this function will create a new
* Page and return a Page ID on success.
*
* @param array $fields
* Associative array of field names => values for the Page
* @throws DatabaseException
* @return integer|boolean
* Returns the Page ID of the created Page on success, false otherwise.
*/
public static function add(array $fields)
{
if (!isset($fields['sortorder'])) {
$fields['sortorder'] = self::fetchNextSortOrder();
}
// Force parent to be null if empty
if (empty($fields['parent'])) {
$fields['parent'] = null;
}
$inserted = Symphony::Database()
->insert('tbl_pages')
->values($fields)
->execute()
->success();
return $inserted ? Symphony::Database()->getInsertID() : 0;
}
/**
* Return a Page title by the handle
*
* @param string $handle
* The handle of the page
* @return string
* The Page title
*/
public static function fetchTitleFromHandle($handle)
{
return Symphony::Database()
->select(['title'])
->from('tbl_pages')
->where(['handle' => $handle])
->limit(1)
->execute()
->string('title');
}
/**
* Return a Page ID by the handle
*
* @param string $handle
* The handle of the page
* @return int
* The Page ID
*/
public static function fetchIDFromHandle($handle)
{
return (int)Symphony::Database()
->select(['id'])
->from('tbl_pages')
->where(['handle' => $handle])
->limit(1)
->execute()
->integer('id');
}
/**
* Given a Page ID and an array of types, this function will add Page types
* to that Page. If a Page types are stored in `tbl_pages_types`.
*
* @param integer $page_id
* The Page ID to add the Types to
* @param array $types
* An array of page types
* @throws DatabaseException
* @return boolean
*/
public static function addPageTypesToPage($page_id = null, array $types)
{
if (!$page_id) {
return false;
}
PageManager::deletePageTypes($page_id);
foreach ($types as $type) {
Symphony::Database()
->insert('tbl_pages_types')
->values([
'page_id' => $page_id,
'type' => $type
])
->execute();
}
return true;
}
/**
* Returns the path to the page-template by looking at the
* `WORKSPACE/template/` directory, then at the `TEMPLATES`
* directory for `$name.xsl`. If the template is not found,
* false is returned
*
* @param string $name
* Name of the template
* @return mixed
* String, which is the path to the template if the template is found,
* false otherwise
*/
public static function getTemplate($name)
{
$format = '%s/%s.xsl';
if (file_exists($template = sprintf($format, WORKSPACE . '/template', $name))) {
return $template;
} elseif (file_exists($template = sprintf($format, TEMPLATE, $name))) {
return $template;
} else {
return false;
}
}
/**
* This function creates the initial `.xsl` template for the page, whether
* that be from the `TEMPLATES/blueprints.page.xsl` file, or from an existing
* template with the same name. This function will handle the renaming of a page
* by creating the new files using the old files as the templates then removing
* the old template. If a template already exists for a Page, it will not
* be overridden and the function will return true.
*
* @see toolkit.PageManager#resolvePageFileLocation()
* @see toolkit.PageManager#createHandle()
* @param string $new_path
* The path of the Page, which is the handles of the Page parents. If the
* page has multiple parents, they will be separated by a forward slash.
* eg. article/read. If a page has no parents, this parameter should be null.
* @param string $new_handle
* The new Page handle, generated using `PageManager::createHandle`.
* @param string $old_path (optional)
* This parameter is only required when renaming a Page. It should be the 'old
* path' before the Page was renamed.
* @param string $old_handle (optional)
* This parameter is only required when renaming a Page. It should be the 'old
* handle' before the Page was renamed.
* @throws Exception
* @return boolean
* true when the page files have been created successfully, false otherwise.
*/
public static function createPageFiles($new_path, $new_handle, $old_path = null, $old_handle = null)
{
$new = PageManager::resolvePageFileLocation($new_path, $new_handle);
$old = PageManager::resolvePageFileLocation($old_path, $old_handle);
// Nothing to do:
if (file_exists($new) && $new == $old) {
return true;
}
// Old file doesn't exist, use template:
if (!file_exists($old)) {
$data = file_get_contents(self::getTemplate('blueprints.page'));
} else {
$data = file_get_contents($old);
}
/**
* Just before a Page Template is about to be created & written to disk
*
* @delegate PageTemplatePreCreate
* @since Symphony 2.2.2
* @param string $context
* '/blueprints/pages/'
* @param string $file
* The path to the Page Template file
* @param string $contents
* The contents of the `$data`, passed by reference
*/
Symphony::ExtensionManager()->notifyMembers('PageTemplatePreCreate', '/blueprints/pages/', array('file' => $new, 'contents' => &$data));
if (PageManager::writePageFiles($new, $data)) {
// Remove the old file, in the case of a rename
General::deleteFile($old);
/**
* Just after a Page Template is saved after been created.
*
* @delegate PageTemplatePostCreate
* @since Symphony 2.2.2
* @param string $context
* '/blueprints/pages/'
* @param string $file
* The path to the Page Template file
*/
Symphony::ExtensionManager()->notifyMembers('PageTemplatePostCreate', '/blueprints/pages/', array('file' => $new));
return true;
}
return false;
}
/**
* A wrapper for `General::writeFile`, this function takes a `$path`
* and a `$data` and writes the new template to disk.
*
* @param string $path
* The path to write the template to
* @param string $data
* The contents of the template
* @return boolean
* true when written successfully, false otherwise
*/
public static function writePageFiles($path, $data)
{
return General::writeFile($path, $data, Symphony::Configuration()->get('write_mode', 'file'));
}
/**
* This function will update a Page in `tbl_pages` given a `$page_id`
* and an associative array of `$fields`. A third parameter, `$delete_types`
* will also delete the Page's associated Page Types if passed true.
*
* @see toolkit.PageManager#addPageTypesToPage()
* @param integer $page_id
* The ID of the Page that should be updated
* @param array $fields
* Associative array of field names => values for the Page.
* This array does need to contain every value for the Page, it
* can just be the changed values.
* @param boolean $delete_types
* If true, this parameter will cause the Page Types of the Page to
* be deleted. By default this is false.
* @return boolean
*/
public static function edit($page_id, array $fields, $delete_types = false)
{
if (!is_numeric($page_id)) {
return false;
}
if (isset($fields['id'])) {
unset($fields['id']);
}
// Force parent to be null if empty
if (isset($fields['parent']) && empty($fields['parent'])) {
$fields['parent'] = null;
}
if (Symphony::Database()
->update('tbl_pages')
->set($fields)
->where(['id' => General::intval($page_id)])
->execute()
->success()
) {
// If set, this will clear the page's types.
if ($delete_types) {
PageManager::deletePageTypes($page_id);
}
return true;
}
return false;
}
/**
* This function will update all children of a particular page (if any)
* by renaming/moving all related files to their new path and updating
* their database information. This is a recursive function and will work
* to any depth.
*
* @param integer $page_id
* The ID of the Page whose children need to be updated
* @param string $page_path
* The path of the Page, which is the handles of the Page parents. If the
* page has multiple parents, they will be separated by a forward slash.
* eg. article/read. If a page has no parents, this parameter should be null.
* @throws Exception
* @return boolean
*/
public static function editPageChildren($page_id = null, $page_path = null)
{
if (!is_int($page_id)) {
return false;
}
$page_path = trim($page_path, '/');
$children = PageManager::fetchChildPages($page_id);
foreach ($children as $child) {
$child_id = (int)$child['id'];
$fields = array(
'path' => $page_path
);
if (!PageManager::createPageFiles($page_path, $child['handle'], $child['path'], $child['handle'])) {
$success = false;
}
if (!PageManager::edit($child_id, $fields)) {
$success = false;
}
$success = PageManager::editPageChildren($child_id, $page_path . '/' . $child['handle']);
}
return $success;
}
/**
* This function takes a Page ID and removes the Page from the database
* in `tbl_pages` and it's associated Page Types in `tbl_pages_types`.
* This function does not delete any of the Page's children.
*
* @see toolkit.PageManager#deletePageTypes
* @see toolkit.PageManager#deletePageFiles
* @param integer $page_id
* The ID of the Page that should be deleted.
* @param boolean $delete_files
* If true, this parameter will remove the Page's templates from the
* the filesystem. By default this is true.
* @throws DatabaseException
* @throws Exception
* @return boolean
*/
public static function delete($page_id = null, $delete_files = true)
{
if (!is_int($page_id)) {
return false;
}
$can_proceed = true;
// Delete Files (if told to)
if ($delete_files) {
$page = PageManager::fetchPageByID($page_id, array('path', 'handle'));
if (empty($page)) {
return false;
}
$can_proceed = PageManager::deletePageFiles($page['path'], $page['handle']);
}
// Delete from tbl_pages/tbl_page_types
if ($can_proceed) {
PageManager::deletePageTypes($page_id);
Symphony::Database()
->delete('tbl_pages')
->where(['id' => General::intval($page_id)])
->execute();
Symphony::Database()
->update('tbl_pages')
->set(['sortorder' => '$sortorder - 1'])
->where(['sortorder' => ['>' => $page['sortorder']]])
->execute();
}
return $can_proceed;
}
/**
* Given a `$page_id`, this function will remove all associated
* Page Types from `tbl_pages_types`.
*
* @param integer $page_id
* The ID of the Page that should be deleted.
* @throws DatabaseException
* @return boolean
*/
public static function deletePageTypes($page_id = null)
{
if (!$page_id) {
return false;
}
return Symphony::Database()
->delete('tbl_pages_types')
->where(['page_id' => General::intval($page_id)])
->execute()
->success();
}
/**
* Given a Page's `$path` and `$handle`, this function will remove
* it's templates from the `PAGES` directory returning boolean on
* completion
*
* @param string $page_path
* The path of the Page, which is the handles of the Page parents. If the
* page has multiple parents, they will be separated by a forward slash.
* eg. article/read. If a page has no parents, this parameter should be null.
* @param string $handle
* A Page handle, generated using `PageManager::createHandle`.
* @throws Exception
* @return boolean
*/
public static function deletePageFiles($page_path, $handle)
{
$file = PageManager::resolvePageFileLocation($page_path, $handle);
// Nothing to do:
if (!file_exists($file)) {
return true;
}
// Delete it:
if (General::deleteFile($file)) {
return true;
}
return false;
}
/**
* This function will return an associative array of Page information. The
* information returned is defined by the `$include_types` and `$select`
* parameters, which will return the Page Types for the Page and allow
* a developer to restrict what information is returned about the Page.
* Optionally, `$where` and `$order_by` parameters allow a developer to
* further refine their query.
*
* @deprecated @since Symphony 3.0.0
* Use select() instead
* @param boolean $include_types
* Whether to include the resulting Page's Page Types in the return array,
* under the key `type`. Defaults to true.
* @param array $select (optional)
* Accepts an array of columns to return from `tbl_pages`. If omitted,
* all columns from the table will be returned.
* @param array $where (optional)
* Accepts an array of WHERE statements that will be appended with AND.
* If omitted, all pages will be returned.
* @param string $order_by (optional)
* Allows a developer to return the Pages in a particular order. The string
* passed will be appended to `ORDER BY`. If omitted this will return
* Pages ordered by `sortorder`.
* @param boolean $hierarchical (optional)
* If true, builds a multidimensional array representing the pages hierarchy.
* Defaults to false.
* @return array|null
* An associative array of Page information with the key being the column
* name from `tbl_pages` and the value being the data. If requested, the array
* can be made multidimensional to reflect the pages hierarchy. If no Pages are
* found, null is returned.
*/
public static function fetch($include_types = true, array $select = array(), array $where = array(), $order_by = null, $hierarchical = false)
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog('PageManager::fetch()', 'PageManager::select()');
}
if (empty($select)) {
$select = ['*'];
}
if (!$order_by) {
$order_by = ['sortorder' => 'ASC'];
}
$query = (new PageManager)->select($select);
if ($hierarchical && !in_array('*', $select)) {
$query->projection(['id', 'parent']);
}
if (is_array($where)) {
foreach ($where as $w) {
$where = $query->replaceTablePrefix($w);
$op = $query->containsSQLParts('where') ? 'AND' : 'WHERE';
$query->unsafe()->unsafeAppendSQLPart('where', "$op ($w)");
}
}
if (is_array($order_by)) {
$query->orderBy($order_by);
} elseif (is_string($order_by)) {
$order_by = $query->replaceTablePrefix($order_by);
$query->unsafe()->unsafeAppendSQLPart('order by', "ORDER BY $order_by");
}
// Fetch the Page Types for each page, if required
if ($include_types) {
$query->includeTypes();
}
$pages = $query->execute();
if ($hierarchical) {
return $pages->tree();
}
return $pages->rows();
}
/**
* Returns Pages that match the given `$page_id`. Developers can optionally
* choose to specify what Page information is returned using the `$select`
* parameter.
*
* @param integer|array $page_id
* The ID of the Page, or an array of ID's
* @param array $select (optional)
* Accepts an array of columns to return from `tbl_pages`. If omitted,
* all columns from the table will be returned.
* @return array|null
* An associative array of Page information with the key being the column
* name from `tbl_pages` and the value being the data. If multiple Pages
* are found, an array of Pages will be returned. If no Pages are found
* null is returned.
*/
public static function fetchPageByID($page_id = null, array $select = array())
{
if (!$page_id) {
return null;
}
if (!is_array($page_id)) {
$page_id = [$page_id];
}
if (empty($select)) {
$select = ['*'];
}
$page = (new PageManager)
->select($select)
->includeTypes()
->pages($page_id)
->execute()
->rows();
return count($page) == 1 ? current($page) : $page;
}
/**
* Returns the first Page that match the given `$type`.
*
* @since Symphony 3.0.0
* It returns only the first page of the specified type.
* @param string $type
* Where the type is one of the available Page Types.
* @return array|null
* An associative array of Page information with the key being the column
* name from `tbl_pages` and the value being the data. If multiple Pages
* are found, an array of Pages will be returned. If no Pages are found
* null is returned.
*/
public static function fetchPageByType($type)
{
General::ensureType([
'type' => ['var' => $type, 'type' => 'string'],
]);
$pageQuery = (new PageManager)
->select()
->innerJoin('tbl_pages_types')
->alias('pt')
->on(['p.id' => '$pt.page_id'])
->where(['pt.type' => $type])
->limit(1);
return $pageQuery->execute()->next();
}
/**
* Returns the child Pages (if any) of the given `$page_id`.
*
* @param integer $page_id
* The ID of the Page.
* @param array $select (optional)
* Accepts an array of columns to return from `tbl_pages`. If omitted,
* all columns from the table will be returned.
* @return array|null
* An associative array of Page information with the key being the column
* name from `tbl_pages` and the value being the data. If multiple Pages
* are found, an array of Pages will be returned. If no Pages are found
* null is returned.
*/
public static function fetchChildPages($page_id = null, array $select = array())
{
if (!$page_id) {
return null;
}
if (empty($select)) {
$select = ['*'];
}
return (new PageManager)
->select($select)
->where(['id' => ['!=' => $page_id]])
->where(['parent' => $page_id])
->execute()
->rows();
}
/**
* This function returns a Page's Page Types. If the `$page_id`
* parameter is given, the types returned will be for that Page.
*
* @param integer $page_id
* The ID of the Page.
* @return array
* An array of the Page Types
*/
public static function fetchPageTypes($page_id = null)
{
$sql = Symphony::Database()
->select(['pt.type'])
->from('tbl_pages_types')
->alias('pt')
->groupBy(['pt.type'])
->orderBy(['pt.type' => 'ASC']);
if ($page_id) {
$sql->where(['pt.page_id' => $page_id]);
}
return $sql->execute()->column('type');
}
/**
* Returns all the page types that exist in this Symphony install.
* There are 6 default system page types, and new types can be added
* by Developers via the Page Editor.
*
* @since Symphony 2.3 introduced the JSON type.
* @return array
* An array of strings of the page types used in this Symphony
* install. At the minimum, this will be an array with the values
* 'index', 'XML', 'JSON', 'admin', '404' and '403'.
*/
public static function fetchAvailablePageTypes()
{
$system_types = array('index', 'XML', 'JSON', 'admin', '404', '403');
$types = PageManager::fetchPageTypes();
return (!empty($types) ? General::array_remove_duplicates(array_merge($system_types, $types)) : $system_types);
}
/**
* Work out the next available sort order for a new page
*
* @return integer
* Returns the next sort order
*/
public static function fetchNextSortOrder()
{
return Symphony::Database()
->select(['MAX(sortorder)'])
->from('tbl_pages')
->execute()
->integer(0) + 1;
}
/**
* Fetch an associated array with Page ID's and the types they're using.
*
* @throws DatabaseException
* @return array
* A 2-dimensional associated array where the key is the page ID.
*/
public static function fetchAllPagesPageTypes()
{
$types = Symphony::Database()
->select(['page_id', 'type'])
->from('tbl_pages_types')
->execute()
->rows();
$page_types = [];
if (is_array($types)) {
foreach ($types as $type) {
$page_types[$type['page_id']][] = $type['type'];
}
}
return $page_types;
}
/**
* Given a name, this function will return a page handle. These handles
* will only contain latin characters
*
* @param string $name
* The Page name to generate a handle for
* @return string
*/
public static function createHandle($name)
{
return Lang::createHandle($name, 255, '-', false, true, array(
'@^[^a-z\d]+@i' => '',
'/[^\w\-\.]/i' => ''
));
}
/**
* This function takes a `$path` and `$handle` and generates a flattened
* string for use as a filename for a Page's template.
*
* @param string $path
* The path of the Page, which is the handles of the Page parents. If the
* page has multiple parents, they will be separated by a forward slash.
* eg. article/read. If a page has no parents, this parameter should be null.
* @param string $handle
* A Page handle, generated using `PageManager::createHandle`.
* @return string
*/
public static function createFilePath($path, $handle)
{
return trim(str_replace('/', '_', $path . '_' . $handle), '_');
}
/**
* This function will return the number of child pages for a given
* `$page_id`. This is a recursive function and will return the absolute
* count.
*
* @param integer $page_id
* The ID of the Page.
* @return integer
* The number of child pages for the given `$page_id`
*/
public static function getChildPagesCount($page_id = null)
{
if (is_null($page_id)) {
return null;
}
$children = (new PageManager)
->select()
->where(['parent' => $page_id])
->execute()
->rows();
$count = count($children);
if ($count > 0) {
foreach ($children as $c) {
$count += self::getChildPagesCount($c['id']);
}
}
return $count;
}
/**
* Returns boolean if a the given `$type` has been used by Symphony
* for a Page that is not `$page_id`.
*
* @param integer $page_id
* The ID of the Page to exclude from the query.
* @param string $type
* The Page Type to look for in `tbl_page_types`.
* @return boolean
* true if the type is used, false otherwise
*/
public static function hasPageTypeBeenUsed($page_id = null, $type)
{
return count(Symphony::Database()
->select(['pt.id'])
->from('tbl_pages_types', 'pt')
->where(['pt.page_id' => ['!=' => $page_id]])
->where(['pt.type' => $type])
->limit(1)
->execute()
->rows()) === 1;
}
/**
* Given a `$page_id`, this function returns boolean if the page
* has child pages.
*
* @param integer $page_id
* The ID of the Page to check
* @return boolean
* true if the page has children, false otherwise
*/
public static function hasChildPages($page_id)
{
return count(Symphony::Database()
->select(['p.id'])
->from('tbl_pages', 'p')
->where(['p.parent' => $page_id])
->limit(1)
->execute()
->rows()) === 1;
}
/**
* Resolves the path to this page's XSLT file. The Symphony convention
* is that they are stored in the `PAGES` folder. If this page has a parent
* it will be as if all the / in the URL have been replaced with _. ie.
* /articles/read/ will produce a file `articles_read.xsl`
*
* @see toolkit.PageManager#createFilePath()
* @param string $path
* The URL path to this page, excluding the current page. ie, /articles/read
* would make `$path` become articles/
* @param string $handle
* The handle of the page.
* @return string
* The path to the XSLT of the page
*/
public static function resolvePageFileLocation($path, $handle)
{
return PAGES . '/' . PageManager::createFilePath($path, $handle) . '.xsl';
}
/**
* Given the `$page_id` and a `$column`, this function will return an
* array of the given `$column` for the Page, including all parents.
*
* @param mixed $page_id
* The ID of the Page that currently being viewed, or the handle of the
* current Page
* @param string $column
* The column to return
* @return array
* An array of the current Page, containing the `$column`
* requested. The current page will be the last item the array, as all
* parent pages are prepended to the start of the array
*/
public static function resolvePage($page_id, $column)
{
$query = (new PageManager)
->select(['p.parent', "p.$column"])
->limit(1);
if (General::intval($page_id) > 0) {
$query->page($page_id);
} else {
$query->handle($page_id);
}
$page = $query->execute()->next();
if (empty($page)) {
return $page;
}
$path = array($page[$column]);
if (!empty($page['parent'])) {
$next_parent = $page['parent'];
while ($next_parent &&
$parent = (new PageManager)
->select(['p.parent', "p.$column"])
->page($next_parent)
->limit(1)
->execute()
->next()
) {
array_unshift($path, $parent[$column]);
$next_parent = $parent['parent'];
}
}
return $path;
}
/**
* Given the `$page_id`, return the complete title of the
* current page. Each part of the Page's title will be
* separated by ': '.
*
* @param mixed $page_id
* The ID of the Page that currently being viewed, or the handle of the
* current Page
* @return string
* The title of the current Page. If the page is a child of another
* it will be prepended by the parent and a colon, ie. Articles: Read
*/
public static function resolvePageTitle($page_id)
{
$path = PageManager::resolvePage($page_id, 'title');
return implode(': ', $path);
}
/**
* Given the `$page_id`, return the complete path to the
* current page. Each part of the Page's path will be
* separated by '/'.
*
* @param mixed $page_id
* The ID of the Page that currently being viewed, or the handle of the
* current Page
* @return string
* The complete path to the current Page including any parent
* Pages, ie. /articles/read
*/
public static function resolvePagePath($page_id)
{
$path = PageManager::resolvePage($page_id, 'handle');
return implode('/', $path);
}
/**
* Resolve a page by it's handle and path
*
* @param string $handle
* The handle of the page
* @param boolean $path
* The path to the page
* @return array
* array if found, null if not
*/
public static function resolvePageByPath($handle, $path = false)
{
return (new PageManager)
->select()
->handle($handle)
->path(!$path ? null : $path)
->limit(1)
->execute()
->next();
}
/**
* Check whether a data source is used or not
*
* @param string $handle
* The data source handle
* @return boolean
* true if used, false if not
*/
public static function isDataSourceUsed($handle)
{
return (new PageManager)
->select()
->count()
->where(['p.data_sources' => ['regexp' => "[[:<:]]{$handle}[[:>:]]"]])
->execute()
->integer(0) > 0;
}
/**
* Check whether a event is used or not
*
* @param string $handle
* The event handle
* @return boolean
* true if used, false if not
*/
public static function isEventUsed($handle)
{
return (new PageManager)
->select()
->count()
->where(['p.events' => ['regexp' => "[[:<:]]{$handle}[[:>:]]"]])
->execute()
->integer(0) > 0;
}
/**
* Factory method that creates a new PageQuery.
*
* @since Symphony 3.0.0
* @param array $projection
* The projection to select.
* If no projection gets added, it defaults to `PageQuery::getDefaultProjection()`.
* @return PageQuery
*/
public function select(array $projection = [])
{
return new PageQuery(Symphony::Database(), $projection);
}
}