Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Content * @since 15 Jan 2014 */ namespace IPS\cms\Pages; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * @brief Page Model */ class _Page extends \IPS\Node\Model implements \IPS\Node\Permissions { /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'cms_pages'; /** * @brief [ActiveRecord] Database Prefix */ public static $databasePrefix = 'page_'; /** * @brief [ActiveRecord] ID Database Column */ public static $databaseColumnId = 'id'; /** * @brief [ActiveRecord] Database ID Fields */ protected static $databaseIdFields = array('page_seo_name', 'page_full_path'); /** * @brief [ActiveRecord] Multiton Map */ protected static $multitonMap = array(); /** * @brief [Node] Parent ID Database Column */ public static $databaseColumnParent = 'folder_id'; /** * @brief [Node] Parent Node ID Database Column */ public static $parentNodeColumnId = 'folder_id'; /** * @brief [Node] Parent Node Class */ public static $parentNodeClass = 'IPS\cms\Pages\Folder'; /** * @brief [Node] Parent ID Database Column */ public static $databaseColumnOrder = 'seo_name'; /** * @brief [Node] Automatically set position for new nodes */ public static $automaticPositionDetermination = FALSE; /** * @brief [Node] Show forms modally? */ public static $modalForms = TRUE; /** * @brief [Node] Title */ public static $nodeTitle = 'page'; /** * @brief [Node] ACP Restrictions * @code array( 'app' => 'core', // The application key which holds the restrictrions 'module' => 'foo', // The module key which holds the restrictions 'map' => array( // [Optional] The key for each restriction - can alternatively use "prefix" 'add' => 'foo_add', 'edit' => 'foo_edit', 'permissions' => 'foo_perms', 'delete' => 'foo_delete' ), 'all' => 'foo_manage', // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4) 'prefix' => 'foo_', // [Optional] Rather than specifying each key in the map, you can specify a prefix, and it will automatically look for restrictions with the key "[prefix]_add/edit/permissions/delete" * @endcode */ protected static $restrictions = array( 'app' => 'cms', 'module' => 'pages', 'prefix' => 'page_' ); /** * @brief [Node] App for permission index */ public static $permApp = 'cms'; /** * @brief [Node] Type for permission index */ public static $permType = 'pages'; /** * @brief The map of permission columns */ public static $permissionMap = array( 'view' => 'view' ); /** * @brief [Node] Prefix string that is automatically prepended to permission matrix language strings */ public static $permissionLangPrefix = 'perm_content_page_'; /** * @brief [Page] Loaded pages from paths */ protected static $loadedPagesFromPath = array(); /** * @brief [Page] Currently loaded page */ public static $currentPage = NULL; /** * @brief [Page] Default page */ public static $defaultPage = array(); /** * @brief Pre save flag */ const PRE_SAVE = 1; /** * @brief Post save flag */ const POST_SAVE = 2; /** * Set Default Values * * @return void */ public function setDefaultValues() { $this->js_css_ids = ''; $this->content = ''; $this->meta_keywords = ''; $this->meta_description = ''; $this->template = ''; $this->full_path = ''; $this->js_css_objects = ''; } /** * @brief [Node] Title prefix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}" as the key */ public static $titleLangPrefix = 'cms_page_'; /** * Load record based on a URL * * @param \IPS\Http\Url $url URL to load from * @return \IPS\cms\Pages\Page * @throws \InvalidArgumentException * @throws \OutOfRangeException */ public static function loadFromUrl( \IPS\Http\Url $url ) { $qs = array_merge( $url->hiddenQueryString, $url->queryString ); if ( isset( $qs['id'] ) ) { if ( method_exists( get_called_class(), 'loadAndCheckPerms' ) ) { return static::loadAndCheckPerms( $qs['id'] ); } else { return static::load( $qs['id'] ); } } else if ( isset( $qs['path'] ) ) { try { $return = static::load( $qs['path'], 'page_full_path' ); } catch( \OutOfRangeException $ex ) { $return = static::loadFromPath( $qs['path'] ); } if ( method_exists( $return, 'can' ) ) { if ( !$return->can( 'view' ) ) { throw new \OutOfRangeException; } } return $return; } throw new \InvalidArgumentException; } /** * Get the page based on the database ID * * @param int $databaseId * @return \IPS\cms\Pages\Page object * @throws \OutOfRangeException */ public static function loadByDatabaseId( $databaseId ) { return static::load( \IPS\cms\Databases::load( $databaseId )->page_id ); } /** * Resets a page path * * @param int $folderId Folder ID to reset * @return void */ public static function resetPath( $folderId ) { try { $path = \IPS\cms\Pages\Folder::load( $folderId )->path; } catch ( \OutOfRangeException $ex ) { throw new \OutOfRangeException; } $children = static::getChildren( $folderId ); foreach( $children as $id => $obj ) { $obj->setFullPath( $path ); } } /** * Get all children of a specific folder. * * @param INT $folderId Folder ID to fetch children from * @return array */ public static function getChildren( $folderId=0 ) { $children = array(); foreach( \IPS\Db::i()->select( '*', static::$databaseTable, array( 'page_folder_id=?', intval( $folderId ) ), 'page_seo_name ASC' ) as $child ) { $children[ $child[ static::$databasePrefix . static::$databaseColumnId ] ] = static::load( $child[ static::$databasePrefix . static::$databaseColumnId ] ); } return $children; } /** * Returns a page object (or NULL) based on the path * * @param string $path Path /like/this/ok.html * @return NULL|\IPS\cms\Pages\Page object */ public static function loadFromPath( $path ) { $path = trim( $path, '/' ); if ( ! array_key_exists( $path, static::$loadedPagesFromPath ) ) { static::$loadedPagesFromPath[ $path ] = NULL; /* Try the simplest option */ try { static::$loadedPagesFromPath[ $path ] = static::load( $path, 'page_full_path' ); } catch ( \OutOfRangeException $e ) { /* Nope - try a folder */ try { if ( $path ) { $class = static::$parentNodeClass; $folder = $class::load( $path, 'folder_path' ); static::$loadedPagesFromPath[ $path ] = static::getDefaultPage( $folder->id ); } else { static::$loadedPagesFromPath[ $path ] = static::getDefaultPage( 0 ); } } catch ( \OutOfRangeException $e ) { /* May contain a database path */ if ( \strstr( $path, '/' ) ) { $bits = explode( '/', $path ); $pathsToTry = array(); while( count( $bits ) ) { $pathsToTry[] = implode( '/', $bits ); array_pop($bits); } try { static::$loadedPagesFromPath[ $path ] = static::constructFromData( \IPS\Db::i()->select( '*', 'cms_pages', \IPS\Db::i()->in( 'page_full_path', $pathsToTry ), 'page_full_path DESC' )->first() ); } catch( \UnderFlowException $e ) { /* Last chance saloon */ foreach( \IPS\Db::i()->select( '*', 'cms_pages', array( '? LIKE CONCAT( page_full_path, \'%\')', $path ), 'page_full_path DESC' ) as $page ) { if ( mb_stristr( $page['page_content'], '{database' ) ) { static::$loadedPagesFromPath[ $path ] = static::constructFromData( $page ); break; } } /* Still here? It's possible this is a legacy URL that starts with "page" - last ditch effort */ if ( static::$loadedPagesFromPath[ $path ] === NULL AND mb_substr( $path, 0, 5 ) === 'page/' ) { $pathWithoutPage = str_replace( 'page/', '', $path ); try { /* Pass back recursively so we don't have to duplicate all of the checks again */ static::$loadedPagesFromPath[ $path ] = static::loadFromPath( $pathWithoutPage ); } catch( \OutOfRangeException $e ) {} } } } } } } if ( static::$loadedPagesFromPath[ $path ] === NULL ) { throw new \OutOfRangeException; } return static::$loadedPagesFromPath[ $path ]; } /** * Load from path history so we can 301 to the correct record. * * @param string $slug Thing that lives in the garden and eats your plants * @param string|NULL $queryString Any query string to add to the end * @return \IPS\cms\Pages\Page */ public static function getUrlFromHistory( $slug, $queryString=NULL ) { $slug = trim( $slug, '/' ); try { $row = \IPS\Db::i()->select( '*', 'cms_url_store', array( 'store_type=? and store_path=?', 'page', $slug ) )->first(); return static::load( $row['store_current_id'] )->url(); } catch( \UnderflowException $ex ) { /* Ok, perhaps this is a full URL with the page name at the beginning */ foreach( \IPS\Db::i()->select( '*', 'cms_url_store', array( 'store_type=? and ? LIKE CONCAT( store_path, \'%\') OR store_path=?', 'page', $slug, $slug ) ) as $item ) { $url = static::load( $item['store_current_id'] )->url(); $url = $url->setPath( '/' . trim( str_replace( $item['store_path'], trim( $url->data['path'], '/' ), $slug ), '/' ) ); if ( $queryString !== NULL ) { $url = $url->setQueryString( $queryString ); } return $url; } /* Still here? Ok, now we may have changed the folder name at some point, so lets look for that */ foreach( \IPS\Db::i()->select( '*', 'cms_url_store', array( 'store_type=? and ? LIKE CONCAT( store_path, \'%\') OR store_path=?', 'folder', $slug, $slug ) ) as $item ) { try { $folder = \IPS\cms\Pages\Folder::load( $item['store_current_id'] ); /* Attempt to build the new path */ $newPath = str_replace( $item['store_path'], $folder->path, $slug ); /* Do we have a page with this path? */ try { return static::load( $newPath, 'page_full_path' ); } catch( \OutOfRangeException $ex ) { /* This is not the path you are looking for */ } } catch( \OutOfRangeException $ex ) { /* This also is not the path you are looking for */ } } } /* Still here? Consistent with AR pattern */ throw new \OutOfRangeException(); } /** * Return the default page for this folder * * @param INT $folderId Folder ID to fetch children from * @return \IPS\cms\Pages\Page */ public static function getDefaultPage( $folderId=0 ) { if ( ! isset( static::$defaultPage[ $folderId ] ) ) { /* Try the easiest method first */ try { static::$defaultPage[ $folderId ] = \IPS\cms\Pages\Page::load( \IPS\Db::i()->select( 'page_id', static::$databaseTable, array( 'page_default=? AND page_folder_id=?', 1, intval( $folderId ) ) )->first() ); } catch( \Exception $ex ) { throw new \OutOfRangeException; } /* Got a page called index? */ if ( ! isset( static::$defaultPage[ $folderId ] ) ) { foreach( static::getChildren( $folderId ) as $id => $obj ) { if ( \mb_substr( $obj->seo_name, 0, 5 ) === 'index' ) { return $obj; } } reset( $children ); /* Just return the first, then */ static::$defaultPage[ $folderId ] = array_shift( $children ); } } return ( isset( static::$defaultPage[ $folderId ] ) ) ? static::$defaultPage[ $folderId ] : NULL; } /** * Delete compiled versions * * @param int|array $ids Integer ID or Array IDs to remove * @return void */ public static function deleteCompiled( $ids ) { if ( is_numeric( $ids ) ) { $ids = array( $ids ); } foreach( $ids as $id ) { $functionName = 'content_pages_' . $id; if ( isset( \IPS\Data\Store::i()->$functionName ) ) { unset( \IPS\Data\Store::i()->$functionName ); } } } /** * Removes all include objects from all pages * * @param boolean $url The URL to find and remove * @return void */ static public function deleteCachedIncludes( $url=NULL ) { /* Remove them all */ if ( $url === NULL ) { /* Remove from DB */ if ( \IPS\Db::i()->checkForTable( 'cms_pages' ) ) { \IPS\Db::i()->update( 'cms_pages', array( 'page_js_css_objects' => NULL ) ); } /* Remove from file system */ \IPS\File::getClass( 'cms_Pages')->deleteContainer('page_objects'); } else { $bits = explode( '/', (string ) $url ); $name = array_pop( $bits ); /* Remove selectively */ foreach( \IPS\Db::i()->select( '*', 'cms_pages', array( "page_js_css_objects LIKE '%" . \IPS\Db::i()->escape_string( $name ) . "%'" ) ) as $row ) { \IPS\Db::i()->update( 'cms_pages', array( 'page_js_css_objects' => NULL ), array( 'page_id=?', $row['page_id'] ) ); } } } /** * Show a custom error page * * @param string $title Title of the page * @param string $message language key for error message * @param mixed $code Error code * @param int $httpStatusCode HTTP Status Code * @param array $httpHeaders Additional HTTP Headers * @return void */ static public function errorPage( $title, $message, $code, $httpStatusCode, $httpHeaders ) { try { $page = static::load( \IPS\Settings::i()->cms_error_page ); $content = $page->getHtmlContent(); $content = str_replace( '{error_message}', $message, $content ); $content = str_replace( '{error_code}', $code, $content ); /* Pages are compiled and cached, which we do not want for the error page as the {error_*} tags are saved with their text content */ $functionName = 'content_pages_' . $page->id; if ( isset( \IPS\Data\Store::i()->$functionName ) ) { unset( \IPS\Data\Store::i()->$functionName ); } $page->output( $title, $httpStatusCode, $httpHeaders, $content ); } catch( \Exception $ex ) { \IPS\Output::i()->sidebar['enabled'] = FALSE; \IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( $title, \IPS\Theme::i()->getTemplate( 'global', 'core' )->error( $title, $message, $code, NULL, \IPS\Member::loggedIn() ), array( 'app' => \IPS\Dispatcher::i()->application ? \IPS\Dispatcher::i()->application->directory : NULL, 'module' => \IPS\Dispatcher::i()->module ? \IPS\Dispatcher::i()->module->key : NULL, 'controller' => \IPS\Dispatcher::i()->controller ) ), $httpStatusCode, 'text/html', $httpHeaders, FALSE, FALSE ); } } /** * Form elements * * @param object|null $item Page object or NULL * @return array */ static public function formElements( $item=NULL ) { $return = array(); $pageType = isset( \IPS\Request::i()->page_type ) ? \IPS\Request::i()->page_type : ( $item ? $item->type : 'html' ); $return['tab_details'] = array( 'content_page_form_tab__details', NULL, NULL, 'ipsForm_horizontal' ); $return['page_name'] = new \IPS\Helpers\Form\Translatable( 'page_name', NULL, TRUE, array( 'app' => 'cms', 'key' => ( $item and $item->id ) ? "cms_page_" . $item->id : NULL, 'maxLength' => 64 ), function( $val ) { if ( empty( $val ) ) { throw new \DomainException('form_required'); } }, NULL, NULL, 'page_name' ); $return['page_seo_name'] = new \IPS\Helpers\Form\Text( 'page_seo_name', $item ? $item->seo_name : '', FALSE, array( 'maxLength' => 255 ), function( $val ) { if ( empty( $val ) ) { $val = \IPS\Http\Url\Friendly::seoTitle( $val ); } /* We cannot have a page name the same as a folder name in this folder */ try { $testFolder = \IPS\cms\Pages\Folder::load( $val, 'folder_name' ); /* Ok, we have a folder, but is it on the same tree as us ?*/ if ( intval( \IPS\Request::i()->page_folder_id ) == $testFolder->parent_id ) { /* Yep, this will break designers' mode and may confuse the FURL engine so we cannot allow this */ throw new \InvalidArgumentException('content_folder_name_furl_collision_pages'); } } catch ( \OutOfRangeException $e ) { /* Nothing with the same name, so that's proper good that is */ } /* If we hit here, we don't have an existing name so that's good */ if ( ! \IPS\Request::i()->page_folder_id and \IPS\cms\Pages\Page::isFurlCollision( $val ) ) { throw new \InvalidArgumentException('content_folder_name_furl_collision_pages_app'); } try { $test = \IPS\cms\Pages\Page::load( $val, 'page_seo_name' ); if ( isset( \IPS\Request::i()->id ) ) { if ( $test->id == \IPS\Request::i()->id ) { /* Just us.. */ return TRUE; } } /* Not us */ if ( intval( \IPS\Request::i()->page_folder_id ) == $test->folder_id ) { throw new \InvalidArgumentException( 'content_page_file_name_in_use' ); } } catch ( \OutOfRangeException $e ) { /* An exception means we don't have a match, so that is good */ } }, NULL, NULL, 'page_seo_name' ); $return['page_folder_id'] = new \IPS\Helpers\Form\Node( 'page_folder_id', ( $item ? intval( $item->folder_id ) : ( ( isset( \IPS\Request::i()->parent ) and \IPS\Request::i()->parent ) ? \IPS\Request::i()->parent : 0 ) ), FALSE, array( 'class' => 'IPS\cms\Pages\Folder', 'zeroVal' => 'node_no_parent', 'subnodes' => false ), NULL, NULL, NULL, 'page_folder_id' ); $return['page_ipb_wrapper'] = new \IPS\Helpers\Form\YesNo( 'page_ipb_wrapper', $item AND $item->id ? $item->ipb_wrapper : 1, TRUE, array( 'togglesOn' => array( 'page_show_sidebar' ), 'togglesOff' => array( 'page_wrapper_template' ) ), NULL, NULL, NULL, 'page_ipb_wrapper' ); $return['page_show_sidebar'] = new \IPS\Helpers\Form\YesNo( 'page_show_sidebar', $item ? $item->show_sidebar : TRUE, FALSE, array(), NULL, NULL, NULL, 'page_show_sidebar' ); $wrapperTemplates = array( '_none_' => \IPS\Member::loggedIn()->language()->addToStack('cms_page_wrapper_template_none') ); foreach( \IPS\cms\Templates::getTemplates( \IPS\cms\Templates::RETURN_PAGE + \IPS\cms\Templates::RETURN_DATABASE_AND_IN_DEV ) as $id => $obj ) { if ( $obj->isSuitableForCustomWrapper() ) { $wrapperTemplates[ \IPS\cms\Templates::readableGroupName( $obj->group ) ][ $obj->group . '__' . $obj->title . '__' . $obj->key ] = \IPS\cms\Templates::readableGroupName( $obj->title ); } } /* List of templates */ $return['page_wrapper_template'] = new \IPS\Helpers\Form\Select( 'page_wrapper_template', ( $item ? $item->wrapper_template : NULL ), FALSE, array( 'options' => $wrapperTemplates ), NULL, NULL, \IPS\Theme::i()->getTemplate( 'pages', 'cms', 'admin' )->previewTemplateLink(), 'page_wrapper_template' ); if ( count( \IPS\Theme::themes() ) > 1 ) { $themes = array( 0 => 'cms_page_theme_id_default' ); foreach ( \IPS\Theme::themes() as $theme ) { $themes[ $theme->id ] = $theme->_title; } $return['page_theme'] = new \IPS\Helpers\Form\Select( 'page_theme', $item ? $item->theme : 0, FALSE, array( 'options' => $themes ), NULL, NULL, NULL, 'page_theme' ); } $builderTemplates = array(); foreach( \IPS\cms\Templates::getTemplates( \IPS\cms\Templates::RETURN_PAGE + \IPS\cms\Templates::RETURN_DATABASE_AND_IN_DEV ) as $id => $obj ) { if ( $obj->isSuitableForBuilderWrapper() ) { $builderTemplates[ \IPS\cms\Templates::readableGroupName( $obj->group ) ][ $obj->group . '__' . $obj->title . '__' . $obj->key ] = \IPS\cms\Templates::readableGroupName( $obj->title ); } } $return['page_template'] = new \IPS\Helpers\Form\Select( 'page_template', ( $item and $item->template ) ? $item->template : FALSE, FALSE, array( 'options' => $builderTemplates ), NULL, NULL, NULL, 'page_template' ); /* Page CSS and JS */ $js = \IPS\cms\Templates::getTemplates( \IPS\cms\Templates::RETURN_ONLY_JS + \IPS\cms\Templates::RETURN_DATABASE_ONLY ); $css = \IPS\cms\Templates::getTemplates( \IPS\cms\Templates::RETURN_ONLY_CSS + \IPS\cms\Templates::RETURN_DATABASE_ONLY ); if ( count( $js ) OR count( $css ) ) { $return['tab_js_css'] = array( 'content_page_form_tab__includes', NULL, NULL, 'ipsForm_horizontal' ); $return['msg_js_css'] = array( 'cms_page_includes_message', 'ipsMessage ipsMessage_info ipsCmsIncludesMessage' ); if ( count( $js ) ) { $jsincludes = array(); foreach( $js as $obj ) { $jsincludes[ $obj->key ] = \IPS\cms\Templates::readableGroupName( $obj->group ) . '/' . \IPS\cms\Templates::readableGroupName( $obj->title ); } $return['page_includes_js'] = new \IPS\Helpers\Form\Select( 'page_includes_js', $item ? $item->js_includes : FALSE, FALSE, array( 'options' => $jsincludes, 'multiple' => true ), NULL, NULL, NULL, 'page_includes_js' ); } if ( count( $css ) ) { $cssincludes = array(); foreach( $css as $obj ) { $cssincludes[ $obj->key ] = \IPS\cms\Templates::readableGroupName( $obj->group ) . '/' . \IPS\cms\Templates::readableGroupName( $obj->title ); } $return['page_includes_css'] = new \IPS\Helpers\Form\Select( 'page_includes_css', $item ? $item->css_includes : FALSE, FALSE, array( 'options' => $cssincludes, 'multiple' => true ), NULL, NULL, NULL, 'page_includes_css' ); } } if ( $pageType === 'html' ) { $return['tab_content'] = array( 'content_page_form_tab__content', NULL, NULL, 'ipsForm_vertical' ); $tags = array(); if ( $item and $item->id == \IPS\Settings::i()->cms_error_page ) { $tags['cms_error_page']['{error_message}'] = 'cms_error_page_message'; $tags['cms_error_page']['{error_code}'] = 'cms_error_page_code'; } foreach( \IPS\cms\Databases::roots( NULL, NULL ) as $id => $db ) { if ( ! $db->page_id ) { $tags['cms_tag_databases']['{database="' . $db->key . '"}'] = $db->_title; } } foreach( \IPS\cms\Blocks\Block::roots( NULL, NULL ) as $id => $block ) { $tags['cms_tag_blocks']['{block="' . $block->key . '"}'] = $block->_title; } foreach( \IPS\Db::i()->select( '*', 'cms_pages', NULL, 'page_full_path ASC', array( 0, 50 ) ) as $page ) { $tags['cms_tag_pages']['{pageurl="' . $page['page_full_path'] . '"}' ] = \IPS\Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $page['page_id'] ); } $return['page_content'] = new \IPS\Helpers\Form\Codemirror( 'page_content', $item ? htmlentities( $item->content, \IPS\HTMLENTITIES, 'UTF-8', TRUE ) : NULL, FALSE, array( 'tags' => $tags, 'height' => 600 ), function( $val ) { /* Test */ try { \IPS\Theme::checkTemplateSyntax( $val ); } catch( \LogicException $e ) { throw new \LogicException('cms_page_error_bad_syntax'); } /* New page? quick check to see if we added a DB tag, and if so, make sure it's not being used on another page. */ if ( ! isset( \IPS\Request::i()->id ) ) { preg_match( '#{database="([^"]+?)"#', $val, $matches ); if ( isset( $matches[1] ) ) { try { if ( is_numeric( $matches[1] ) ) { $database = \IPS\cms\Databases::load( intval( $matches[1] ) ); } else { $database = \IPS\cms\Databases::load( $matches[1], 'database_key' ); } if ( $database->page_id ) { throw new \LogicException('cms_err_db_in_use_other_page'); } } catch( \OutOfRangeException $ex ) { throw new \LogicException('cms_err_db_does_not_exist'); } } } }, NULL, NULL, 'page_content' ); } $return['tab_meta'] = array( 'content_page_form_tab__meta', NULL, NULL, 'ipsForm_horizontal' ); $return['page_title'] = new \IPS\Helpers\Form\Text( 'page_title', $item ? $item->title : '', FALSE, array( 'maxLength' => 64 ), NULL, NULL, NULL, 'page_title' ); $return['page_meta_keywords'] = new \IPS\Helpers\Form\TextArea( 'page_meta_keywords', $item ? $item->meta_keywords : '', FALSE, array(), NULL, NULL, NULL, 'page_meta_keywords' ); $return['page_meta_description'] = new \IPS\Helpers\Form\TextArea( 'page_meta_description', $item ? $item->meta_description : '', FALSE, array(), NULL, NULL, NULL, 'page_meta_description' ); \IPS\Output::i()->globalControllers[] = 'cms.admin.pages.form'; \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'admin_pages.js', 'cms' ) ); return $return; } /** * Create a new page from a form. Pretty much what the function says. * * @param array $values Array of form values * @param string $pageType Type of page. 'html', 'builder' or 'editor' * @return \IPS\cms\Pages\Page object */ static public function createFromForm( $values, $pageType=NULL ) { $page = new self; $page->type = $pageType; $page->save(); $page->saveForm( $page->formatFormValues( $values ), $pageType ); /* Set permissions */ \IPS\Db::i()->update( 'core_permission_index', array( 'perm_view' => '*' ), array( 'app=? and perm_type=? and perm_type_id=?', 'cms', 'pages', $page->id ) ); return $page; } /** * Ensure there aren't any collision issues when the CMS is the default app and folders such as "forums" are created when * the forums app is installed. * * @param string $path Path to check * @return boolean */ static public function isFurlCollision( $path ) { $path = trim( $path , '/'); $bits = explode( '/', $path ); $folder = $bits[0]; /* Ensure we cannot have a structure that starts with core/interface as we have this partial URL blacklisted in \IPS\Text\Parser::safeIframeRegexp() */ if ( mb_substr( $path, 0, 15 ) == 'core/interface/' ) { return TRUE; } $defaultApplication = \IPS\Db::i()->select( 'app_directory', 'core_applications', 'app_default=1' )->first(); foreach( \IPS\Application::applications() as $key => $app ) { if ( $app->directory === 'cms' ) { continue; } $furlDefinitionFile = \IPS\ROOT_PATH . "/applications/{$app->directory}/data/furl.json"; if ( file_exists( $furlDefinitionFile ) ) { $furlDefinition = json_decode( preg_replace( '/\/\*.+?\*\//s', '', file_get_contents( $furlDefinitionFile ) ), TRUE ); if ( isset( $furlDefinition['topLevel'] ) ) { if ( $furlDefinition['topLevel'] == $folder ) { return TRUE; } if ( isset( $furlDefinition['pages'] ) ) { foreach( $furlDefinition['pages'] as $name => $data ) { if ( isset( $data['friendly'] ) ) { $furlBits = explode( '/', $data['friendly'] ); if ( $furlBits[0] == $folder ) { return TRUE; } } } } } } } /* Still here? Some apps use very loose matching, like calendar looks for {id}-{?} which may conflict with a page with a filename of 123-foo */ try { $url = \IPS\Http\Url::createFromString( \IPS\Http\Url::baseUrl() . $path ); if ( $url and $url instanceof \IPS\Http\Url\Friendly and $url->seoTemplate !== 'content_page_path' ) { return TRUE; } } catch( \Exception $ex ) { /* If we get an error, then it cannot be a legitimate link */ } return FALSE; } /** * Delete all stored includes so they can be rebuilt on demand. * * @return void */ public static function deleteCompiledIncludes() { \IPS\Db::i()->update( 'cms_pages', array( 'page_js_css_objects' => NULL ) ); \IPS\cms\Templates::deleteCompiledFiles(); } /** * Create a datastore object of page IDs and URLs. * * @return void */ public static function buildPageUrlStore() { /* This fails because hooks are not installed when this is attempted to build via admin/install */ if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation === 'setup' ) { return; } /* This also fails if we're installing via ACP */ if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation === 'admin' and isset( \IPS\Request::i()->do ) and \IPS\Request::i()->do == 'install' ) { return; } $store = array(); foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'cms_pages' ), 'IPS\cms\Pages\Page' ) as $page ) { $perms = $page->permissions(); $store[ $page->id ] = array( 'url' => (string) $page->url(), 'perm' => $perms['perm_view'] ); } \IPS\Data\Store::i()->pages_page_urls = $store; } /** * Returns (and builds if required) the pages id => url datastore * * @return array */ public static function getPageUrlStore() { if ( ! isset( \IPS\Data\Store::i()->pages_page_urls ) ) { static::buildPageUrlStore(); } return \IPS\Data\Store::i()->pages_page_urls; } /** * Set JS/CSS include keys * * @param string|array $value * @return void */ public function set__js_css_ids( $value ) { $this->_data['js_css_ids'] = ( is_array( $value ) ? json_encode( $value ) : $value ); } /** * Get JS/CSS include keys * * @return array|null */ protected function get__js_css_ids() { if ( ! is_array( $this->_data['js_css_ids'] ) ) { $this->_data['js_css_ids'] = json_decode( $this->_data['js_css_ids'], true ); } return ( is_array( $this->_data['js_css_ids'] ) ) ? $this->_data['js_css_ids'] : array(); } /** * Get JS include keys * * @return array */ protected function get_js_includes() { /* Makes sure js_css_ids is unpacked if required */ $this->_js_css_ids; if ( isset( $this->_data['js_css_ids']['js'] ) ) { return $this->_data['js_css_ids']['js']; } return array(); } /** * Get CSS include keys * * @return array */ protected function get_css_includes() { /* Makes sure js_css_ids is unpacked if required */ $this->_js_css_ids; if ( isset( $this->_data['js_css_ids']['css'] ) ) { return $this->_data['js_css_ids']['css']; } return array(); } /** * Get JS/CSS Objects * * @return array */ public function getIncludes() { $return = array( 'css' => NULL, 'js' => NULL ); if ( \IPS\Theme::designersModeEnabled() ) { foreach( array( 'js', 'css' ) as $thing ) { $includeKey = $thing . '_includes'; if ( count( $this->$includeKey ) ) { /* Build a file object for each thing */ foreach( $this->$includeKey as $key ) { try { $template = \IPS\cms\Templates::load( $key ); $return[ $thing ][ $key ] = \IPS\Http\Url::createFromString( \IPS\Http\Url::baseUrl() . "/applications/cms/interface/developer/developer.php" ) ->setQueryString( 'file', '/themes/cms/' . $template->location . '/' . $template->group . '/' . $template->title ); } catch( \OutOfRangeException $e ) { continue; } } } } return $return; } /* Empty? Lets take a look and see if we need to compile anything */ if ( empty( $this->_data['js_css_objects'] ) ) { if ( count( $this->js_includes ) ) { /* Build a file object for each JS */ foreach( $this->js_includes as $key ) { try { $template = \IPS\cms\Templates::load( $key ); $object = $template->_file_object; $return['js'][ $key ] = $object; } catch( \OutOfRangeException $e ) { continue; } } } if ( count( $this->css_includes ) ) { /* Build a file object for each JS */ foreach( $this->css_includes as $key ) { try { $template = \IPS\cms\Templates::load( $key ); $object = $template->_file_object; $return['css'][ $key ] = $object; } catch( \Exception $e ) { continue; } } } if ( ! \IPS\Theme::designersModeEnabled() ) { /* Save this to prevent it looking for includes on every page refresh */ $this->js_css_objects = json_encode( $return ); $this->save(); } } else { $return = json_decode( $this->_data['js_css_objects'], TRUE ); } foreach( $return as $type => $data ) { if ( is_array( $data ) ) { foreach( $data as $key => $object ) { $return[ $type ][ $key ] = (string) \IPS\File::get( 'cms_Pages', $object )->url; } } } return $return; } /** * Get the content type of this page. Calculates based on page extension * * @return string */ public function getContentType() { $map = array( 'js' => 'text/javascript', 'css' => 'text/css', 'txt' => 'text/plain', 'xml' => 'text/xml', 'rss' => 'text/xml', 'html' => 'text/html', 'json' => 'application/json' ); $extension = mb_substr( $this->seo_name, ( mb_strrpos( $this->seo_name, '.' ) + 1 ) ); if ( in_array( $extension, array_keys( $map ) ) ) { return $map[ $extension ]; } return 'text/html'; } /** * Return the title for the publicly viewable HTML page * * @return string Title to use between tags */ public function getHtmlTitle() { if ( $this->title ) { return $this->title; } if ( $this->_title ) { return $this->_title; } return $this->name; } /** * Return the content for the publicly viewable HTML page * * @return string HTML to use on the page */ public function getHtmlContent() { $functionName = 'content_pages_' . $this->id; if ( \IPS\Theme::i()->designersModeEnabled() and file_exists( \IPS\ROOT_PATH . '/themes/cms/pages/' . $this->full_path ) ) { $contents = @file_get_contents( \IPS\ROOT_PATH . '/themes/cms/pages/' . $this->full_path ); $contents = preg_replace( '#^<ips:pages.+?/>(\r\n?|\n)#', '', $contents ); \IPS\Theme::runProcessFunction( \IPS\Theme::compileTemplate( $contents, $functionName, null, true ), $functionName ); } else { if ( ! isset( \IPS\Data\Store::i()->$functionName ) ) { \IPS\Data\Store::i()->$functionName = \IPS\Theme::compileTemplate( $this->content, $functionName, null, true ); } \IPS\Theme::runProcessFunction( \IPS\Data\Store::i()->$functionName, $functionName ); } return call_user_func( 'IPS\\Theme\\'. $functionName ); } /** * @brief Cached widgets */ protected $cachedWidgets = NULL; /** * Return the blocks for this page * * @return array */ public function getWidgets() { if( $this->cachedWidgets !== NULL ) { return $this->cachedWidgets; } $this->cachedWidgets = array(); $widgets = array(); $dbWidgets = array(); $areas = array(); foreach( \IPS\Db::i()->select( '*', 'cms_page_widget_areas', array( 'area_page_id=?', $this->id ) ) as $k => $widgetMain ) { $widgets[ $widgetMain['area_area'] ] = json_decode( $widgetMain['area_widgets'], TRUE ); $areas[ $widgetMain['area_area'] ] = $widgetMain; /* We need to execute database widgets first as this sets up the Database dispatcher correctly */ foreach( $widgets[ $widgetMain['area_area'] ] as $widget ) { /* Do not attempt to re-parse database widgets if we already have */ if ( ! \IPS\cms\Databases\Dispatcher::i()->databaseId AND $widget['key'] === 'Database' ) { $orientation = ( ( $widgetMain['area_area'] == 'sidebar' ) ? 'vertical' : ( ( $widgetMain['area_area'] === 'header' OR $widgetMain['area_area'] === 'footer' ) ? 'horizontal' : $widgetMain['area_orientation'] ) ); $dbWidgets[ $widget['unique'] ] = \IPS\Widget::load( \IPS\Application::load( $widget['app'] ), $widget['key'], $widget['unique'], ( isset( $widget['configuration'] ) ) ? $widget['configuration'] : array(), ( isset( $widget['restrict'] ) ? $widget['restrict'] : null ), $orientation ); $dbWidgets[ $widget['unique'] ]->render(); } } } if( count( $widgets ) ) { foreach ( $widgets as $areaKey => $area ) { foreach ( $area as $widget ) { try { if ( $widget['key'] == 'Database' and array_key_exists( $widget['unique'], $dbWidgets ) ) { $_widget = $dbWidgets[ $widget['unique'] ]; } else { $orientation = ( ( $areaKey == 'sidebar' ) ? 'vertical' : ( ( $areaKey === 'header' OR $areaKey === 'footer' ) ? 'horizontal' : $areas[ $areaKey ]['area_orientation'] ) ); if ( isset( $widget['app'] ) and $widget['app'] ) { $appOrPlugin = \IPS\Application::load( $widget['app'] ); } else { $appOrPlugin = \IPS\Plugin::load( $widget['plugin'] ); } $_widget = \IPS\Widget::load( $appOrPlugin, $widget['key'], $widget['unique'], ( isset( $widget['configuration'] ) ) ? $widget['configuration'] : array(), ( isset( $widget['restrict'] ) ? $widget['restrict'] : null ), $orientation ); } if ( in_array( $areaKey, array('header', 'footer', 'sidebar' ) ) ) { \IPS\Output::i()->sidebar['widgets'][ $areaKey ][] = $_widget; } $this->cachedWidgets[ $areaKey ][] = $_widget; } catch ( \Exception $e ) { \IPS\Log::debug( $e, 'pages_widgets' ); } } } } return $this->cachedWidgets; } /** * [Node] Get buttons to display in tree * Example code explains return value * @endcode * @param string $url Base URL * @param bool $subnode Is this a subnode? * @return array */ public function getButtons( $url, $subnode=FALSE ) { $buttons = parent::getButtons( $url, $subnode ); $return = array(); if ( isset( $buttons['add'] ) ) { unset( $buttons['add'] ); } if ( $this->type === 'builder' and isset( $buttons['edit'] ) ) { $return['builder'] = array( 'icon' => 'magic', 'title' => 'content_launch_page_builder', 'link' => $this->url()->setQueryString( array( '_blockManager' => 1 ) ), 'target' => '_blank' ); } else { $return['view'] = array( 'icon' => 'search', 'title' => 'content_launch_page_view', 'link' => $this->url(), 'target' => '_blank' ); } if ( isset( $buttons['edit'] ) ) { $buttons['edit']['title'] = \IPS\Member::loggedIn()->language()->addToStack('content_edit_page'); $buttons['edit']['data'] = null; } /* Re-arrange */ if ( isset( $buttons['edit'] ) ) { $return['edit'] = $buttons['edit']; } if ( isset( $buttons['edit_content'] ) ) { $return['edit_content'] = $buttons['edit_content']; } if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'cms', 'pages', 'page_edit' ) ) { $return['default'] = array( 'icon' => $this->default ? 'star' : 'star-o', 'title' => 'content_default_page', 'link' => $url->setQueryString( array( 'id' => $this->id, 'subnode' => 1, 'do' => 'setAsDefault' ) ) ); } $return['view'] = array( 'icon' => 'search', 'title' => 'content_launch_page', 'link' => $this->url(), 'target' => '_blank' ); if ( isset( $buttons['permissions'] ) ) { $return['permissions'] = $buttons['permissions']; } if ( isset( $buttons['delete'] ) ) { $return['delete'] = $buttons['delete']; } return $return; } /** * [Node] Add/Edit Form * * @param \IPS\Helpers\Form $form The form * @return void */ public function form( &$form ) { /* Build form */ if ( ! $this->id ) { $form->hiddenValues['page_type'] = $pageType = \IPS\Request::i()->page_type; } else { $pageType = $this->type; } /* We shut off the main class and add it per-tab to allow the content field to look good */ $form->class = ''; foreach( static::formElements( $this ) as $name => $field ) { if ( $pageType !== 'html' and in_array( $name, array( 'page_ipb_wrapper', 'page_show_sidebar', 'page_wrapper_template' ) ) ) { continue; } if ( $pageType === 'html' and in_array( $name, array( 'page_template' ) ) ) { continue; } if ( is_array( $field ) ) { if ( mb_substr( $name, 0, 4 ) === 'tab_' ) { call_user_func_array( array( $form, 'addTab' ), $field ); } else if ( mb_substr( $name, 0, 4 ) === 'msg_' ) { call_user_func_array( array( $form, 'addMessage' ), $field ); } } else { $form->add( $field ); } } if ( ! $this->id ) { $form->addTab( 'content_page_form_tab__menu' ); $toggles = array( 'menu_manager_access_type', 'menu_parent' ); $formFields = array(); foreach( \IPS\cms\extensions\core\FrontNavigation\Pages::configuration( array() ) as $field ) { if ( $field->name !== 'menu_content_page' ) { $toggles[] = $field->name; $formFields[ $field->name ] = $field; } } $form->add( new \IPS\Helpers\Form\YesNo( 'page_add_to_menu', FALSE, FALSE, array( 'togglesOn' => $toggles ) ) ); $roots = array(); foreach ( \IPS\core\FrontNavigation::i()->roots( FALSE ) as $item ) { $roots[ $item->id ] = $item->title(); } $form->add( new \IPS\Helpers\Form\Select( 'menu_parent', '*', NULL, array( 'options' => $roots ), NULL, NULL, NULL, 'menu_parent' ) ); foreach( $formFields as $name => $field ) { $form->add( $field ); } $groups = array(); foreach ( \IPS\Member\Group::groups() as $group ) { $groups[ $group->g_id ] = $group->name; } $form->add( new \IPS\Helpers\Form\Radio( 'menu_manager_access_type', 0, TRUE, array( 'options' => array( 0 => 'menu_manager_access_type_inherit', 1 => 'menu_manager_access_type_override' ), 'toggles' => array( 1 => array( 'menu_manager_access' ) ) ), NULL, NULL, NULL, 'menu_manager_access_type' ) ); $form->add( new \IPS\Helpers\Form\Select( 'menu_manager_access', '*', NULL, array( 'multiple' => TRUE, 'options' => $groups, 'unlimited' => '*', 'unlimitedLang' => 'everyone' ), NULL, NULL, NULL, 'menu_manager_access' ) ); } if ( $pageType === 'builder' ) { if ( $this->id ) { \IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->message( \IPS\Member::loggedIn()->language()->addToStack('content_acp_page_builder_msg_edit', TRUE, array( 'sprintf' => array( $this->url() ) ) ), 'information', NULL, FALSE ); } else { \IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->message( \IPS\Member::loggedIn()->language()->addToStack('content_acp_page_builder_msg_new' ), 'information', NULL, FALSE ); } } if( $this->id ) { $form->canSaveAndReload = true; } \IPS\Output::i()->title = $this->id ? \IPS\Member::loggedIn()->language()->addToStack('content_editing_page', NULL, array( 'sprintf' => array( $this->_title ) ) ) : \IPS\Member::loggedIn()->language()->addToStack('content_add_page'); } /** * [Node] Format form values from add/edit form for save * * @param array $values Values from the form * @return array */ public function formatFormValues( $values ) { $isNew = $this->_new; if ( ! $this->id ) { $this->type = \IPS\Request::i()->page_type; $this->save(); if ( $this->type === 'editor' ) { \IPS\File::claimAttachments( 'page-content/pages-' . $this->id, $this->id ); } } if( isset( $values['page_name'] ) ) { $_copied = $values['page_name']; $values['page_seo_name'] = empty( $values['page_seo_name'] ) ? ( is_array( $_copied ) ? array_shift( $_copied ) : $_copied ) : $values['page_seo_name']; $bits = explode( '.', $values['page_seo_name'] ); foreach( $bits as $i => $v ) { $bits[ $i ] = \IPS\Http\Url\Friendly::seoTitle( $v ); } $values['page_seo_name'] = implode( '.', $bits ); \IPS\Lang::saveCustom( 'cms', "cms_page_" . $this->id, $values['page_name'] ); } if ( isset( $values['page_folder_id'] ) AND ( ! empty( $values['page_folder_id'] ) OR $values['page_folder_id'] === 0 ) ) { $values['page_folder_id'] = ( $values['page_folder_id'] === 0 ) ? 0 : $values['page_folder_id']->id; } if ( isset( $values['page_includes_js'] ) OR isset( $values['page_includes_css'] ) ) { $includes = array(); if ( isset( $values['page_includes_js'] ) ) { $includes['js'] = $values['page_includes_js']; } if ( isset( $values['page_includes_css'] ) ) { $includes['css'] = $values['page_includes_css']; } $this->_js_css_ids = $includes; /* Trash file objects to be sure */ $this->js_css_objects = NULL; $values['js_css_objects'] = NULL; unset( $values['page_includes_js'], $values['page_includes_css'] ); } try { $this->processContent( static::PRE_SAVE ); } catch( \LogicException $ex ) { throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('content_err_page_save_exception', FALSE, array( 'sprintf' => \IPS\Member::loggedIn()->language()->addToStack( $ex->getMessage() ) ) ) ); } /* Page filename changed? */ if ( ! $isNew and $values['page_seo_name'] !== $this->seo_name ) { $this->storeUrl(); } /* Menu stuffs */ if ( isset( $values['page_add_to_menu'] ) ) { if( $values['page_add_to_menu'] ) { $permission = $values['menu_manager_access'] == '*' ? '*' : implode( ',', $values['menu_manager_access'] ); if ( $values['menu_manager_access_type'] === 0 ) { $permission = ''; } $save = array( 'app' => 'cms', 'extension' => 'Pages', 'config' => '', 'parent' => $values['menu_parent'], 'permissions' => $permission ); try { $save['position'] = \IPS\Db::i()->select( 'MAX(position)', 'core_menu', array( 'parent=?', \IPS\Request::i()->parent ) )->first() + 1; } catch ( \UnderflowException $e ) { $save['position'] = 1; } $id = \IPS\Db::i()->insert( 'core_menu', $save ); $values = \IPS\cms\extensions\core\FrontNavigation\Pages::parseConfiguration( $values, $id ); $config = array( 'menu_content_page' => $this->id ); foreach( array( 'menu_title_page_type', 'menu_title_page' ) as $field ) { if ( isset( $values[ $field ] ) ) { $config[ $field ] = $values[ $field ]; } } \IPS\Db::i()->update( 'core_menu', array( 'config' => json_encode( $config ) ), array( 'id=?', $id ) ); unset( \IPS\Data\Store::i()->frontNavigation ); } unset( $values['page_add_to_menu'], $values['menu_title_page_type'], $values['menu_title_page'], $values['menu_parent'], $values['menu_manager_access'], $values['menu_manager_access_type'] ); } return $values; } /** * [Node] Perform actions after saving the form * * @param array $values Values from the form * @return void */ public function postSaveForm( $values ) { $this->setFullPath( ( $this->folder_id ? \IPS\cms\Pages\Folder::load( $this->folder_id )->path : '' ) ); $this->save(); try { $this->processContent( static::POST_SAVE ); } catch( \LogicException $ex ) { throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('content_err_page_save_exception', FALSE, array( 'sprintf' => \IPS\Member::loggedIn()->language()->addToStack( $ex->getMessage() ) ) ) ); } } /** * Stores the URL so when its changed, the old can 301 to the new location * * @return void */ public function storeUrl() { \IPS\Db::i()->insert( 'cms_url_store', array( 'store_path' => $this->full_path, 'store_current_id' => $this->_id, 'store_type' => 'page' ) ); } /** * Get the Database ID from the page * * @return null|int */ public function getDatabase() { try { return \IPS\cms\Databases::load( $this->id, 'database_page_id' ); } catch( \OutOfRangeException $e ) { } return null; } /** * Get the database ID from the page content * * @return int */ public function getDatabaseIdFromHtml() { if ( $this->type !== 'html' ) { throw new \LogicException('cms_page_not_html'); } preg_match( '#{database="([^"]+?)"#', $this->content, $matches ); if ( isset( $matches[1] ) ) { if ( is_numeric( $matches[1] ) ) { return intval( $matches[1] ); } else { try { $database = \IPS\cms\Databases::load( $matches[1], 'database_key' ); return $database->id; } catch( \OutOfRangeException $ex ) { return NULL; } } } return NULL; } /** * @brief Cached URL */ protected $_url = NULL; /** * Get URL * * @return \IPS\Http\Url object */ public function url() { if( $this->_url === NULL ) { if ( ( \IPS\Application::load('cms')->default OR \IPS\Settings::i()->cms_use_different_gateway ) AND $this->default AND ! $this->folder_id ) { /* Are we using the gateway file? */ if ( \IPS\Settings::i()->cms_use_different_gateway ) { /* Yes, work out the proper URL. */ $this->_url = \IPS\Http\Url::createFromString( \IPS\Settings::i()->cms_root_page_url, TRUE ); } else { /* No - that's easy */ $this->_url = \IPS\Http\Url::internal( '', 'front' ); } } else { $this->_url = \IPS\Http\Url::internal( 'app=cms&module=pages&controller=page&path=' . $this->full_path, 'front', 'content_page_path', array( $this->full_path ) ); } } return $this->_url; } /** * Process the content to see if there are any tags and so on that need action * * @param integer $flag Pre or post save flag * @return void * @throws \LogicException */ public function processContent( $flag ) { if ( $this->type === 'html' ) { $seen = array(); preg_match_all( '/\{([a-z]+?=([\'"]).+?\\2 ?+)}/', $this->content, $matches, PREG_SET_ORDER ); /* Work out the plugin and the values to pass */ foreach( $matches as $index => $array ) { preg_match_all( '/(.+?)='.$array[2].'(.+?)'.$array[2].'\s?/', $array[1], $submatches ); $plugin = array_shift( $submatches[1] ); $pluginClass = 'IPS\\Output\\Plugin\\' . ucfirst( $plugin ); $value = array_shift( $submatches[2] ); $options = array(); foreach ( $submatches[1] as $k => $v ) { $options[ $v ] = $submatches[2][ $k ]; } $seen[ mb_strtolower( $plugin ) ] = array( 'value' => $value, 'options' => $options ); /* Work out if this plugin belongs to an application, and if so, include it */ if( !class_exists( $pluginClass ) ) { foreach ( \IPS\Application::applications() as $app ) { if ( file_exists( \IPS\ROOT_PATH . "/applications/{$app->directory}/extensions/core/OutputPlugins/" . ucfirst( $plugin ) . ".php" ) ) { $pluginClass = 'IPS\\' . $app->directory . '\\extensions\\core\\OutputPlugins\\' . ucfirst( $plugin ); } } } $method = ( $flag === static::PRE_SAVE ) ? 'preSaveProcess' : 'postSaveProcess'; if ( method_exists( $pluginClass, $method ) ) { try { call_user_func( array( $pluginClass, $method ), $value, $options, $this ); } catch( \Exception $ex ) { throw new \LogicException( $ex->getMessage() ); } } } /* Check to see if we're expecting a database to be here */ $database = $this->getDatabase(); if ( $database !== NULL ) { /* We're expecting a database tag, is there one? */ if ( isset( $seen['database'] ) ) { /* Yep, is it the same ID? */ if ( $seen['database']['value'] != $database->id AND $seen['database']['value'] != $database->key ) { /* There's been a change.. */ try { $this->removeDatabaseMap(); } catch( \LogicException $e ) { } $this->mapToDatabase( intval( $database->id ) ); } } else { /* Nope, not database tag spotted, assume it has been removed */ try { $this->removeDatabaseMap(); } catch( \LogicException $e ) { } } } } } /** * Set Theme * * @return void */ public function setTheme() { if ( $this->theme ) { try { \IPS\Theme::switchTheme( $this->theme ); } catch ( \Exception $e ) { } } } /** * Once widget ordering has ocurred, post process if required * * @return void */ public function postWidgetOrderSave() { /* Check for database changes and update mapping if required */ $databaseUsed = NULL; foreach ( \IPS\Db::i()->select( '*', 'cms_page_widget_areas', array( 'area_page_id=?', $this->id ) ) as $item ) { $pageBlocks = json_decode( $item['area_widgets'], TRUE ); $resaveBlock = NULL; foreach( $pageBlocks as $id => $pageBlock ) { if( isset( $pageBlock['app'] ) and $pageBlock['app'] == 'cms' AND $pageBlock['key'] == 'Database' AND ! empty( $pageBlock['configuration']['database'] ) ) { if ( $databaseUsed === NULL ) { $databaseUsed = $pageBlock['configuration']['database']; } else { /* Already got a database, so remove this one */ $resaveBlock = $pageBlocks; unset( $resaveBlock[ $id ] ); } } } if ( $resaveBlock !== NULL ) { \IPS\Db::i()->update( 'cms_page_widget_areas', array( 'area_widgets' => json_encode( $resaveBlock ) ), array( 'area_page_id=? and area_area=?', $this->id, $item['area_area'] ) ); } } if ( $databaseUsed === NULL and $this->type === 'html' ) { $databaseUsed = $this->getDatabaseIdFromHtml(); } if ( $databaseUsed !== NULL ) { $this->mapToDatabase( intval( $databaseUsed ) ); } else { try { $this->removeDatabaseMap(); } catch( \LogicException $e ) { } } } /** * Map this database to a specific page * * @param int $databaseId Page ID * @return boolean * @throws \LogicException */ public function mapToDatabase( $databaseId ) { try { /* is this page already in use */ $database = \IPS\cms\Databases::load( $this->id, 'database_page_id' ); if ( $database->id === $databaseId ) { /* Nothing to update as this page is mapped to this database */ return TRUE; } else { /* We're using another DB on this page */ throw new \LogicException('cms_err_db_already_on_page'); } } catch( \OutOfRangeException $e ) { /* We didn't load a database based on this page, so make sure the database we want isn't being used elsewhere */ $database = \IPS\cms\Databases::load( $databaseId ); if ( $database->page_id > 0 ) { /* We're using another DB on this page */ throw new \LogicException('cms_err_db_in_use_other_page'); } else { /* Ok here as this DB is not in use, and this page doesn't have a DB in use */ $database->page_id = $this->id; $database->save(); /* Restore content in the search index */ \IPS\Task::queue( 'core', 'RebuildSearchIndex', array( 'class' => 'IPS\cms\Records' . $database->id ) ); } return TRUE; } } /** * Removes all mapped DBs for this page * * @return void */ public function removeDatabaseMap() { try { $database = \IPS\cms\Databases::load( $this->id, 'database_page_id' ); $database->page_id = 0; $database->save(); /* Remove from search */ \IPS\Content\Search\Index::i()->removeClassFromSearchIndex( 'IPS\cms\Records' . $database->id ); } catch( \OutOfRangeException $ex ) { /* Page was never mapped */ throw new \LogicException('cms_err_db_page_never_used'); } } /** * [ActiveRecord] Delete Record * * @return void */ public function delete() { try { $this->removeDatabaseMap(); } catch( \LogicException $e ) { } $delete = $this->getMenuItemIds(); if ( count( $delete ) ) { \IPS\Db::i()->delete( 'core_menu', \IPS\Db::i()->in( 'id', $delete ) ); } unset( \IPS\Data\Store::i()->frontNavigation ); parent::delete(); if ( $this->full_path and \IPS\Theme::designersModeEnabled() ) { static::cleanUpDesignersModeFiles(); } } /** * Returns core_menu ids for all menu items associated with this page * * @return array */ public function getMenuItemIds() { $items = array(); foreach( \IPS\Db::i()->select( '*', 'core_menu', array( 'app=? AND extension=?', 'cms', 'Pages' ) ) as $item ) { $json = json_decode( $item['config'], TRUE ); if ( isset( $json['menu_content_page'] ) ) { if ( $json['menu_content_page'] == $this->id ) { $items[] = $item['id']; } } } return $items; } /** * Set the permission index permissions * * @param array $insert Permission data to insert * @param object \IPS\Helpers\Form\Matrix * @return void */ public function setPermissions( $insert, \IPS\Helpers\Form\Matrix $matrix ) { parent::setPermissions( $insert, $matrix ); static::buildPageUrlStore(); /* Update perms if we have a child database */ try { $database = \IPS\cms\Databases::load( $this->id, 'database_page_id' ); foreach( \IPS\Db::i()->select( '*', 'cms_database_categories', array( 'category_database_id=?', $database->id ) ) as $cat ) { $class = '\IPS\cms\Categories' . $database->id; $category = $class::constructFromData( $cat ); \IPS\Content\Search\Index::i()->massUpdate( 'IPS\cms\Records' . $database->id, $category->_id, NULL, $category->readPermissionMergeWithPage() ); \IPS\Content\Search\Index::i()->massUpdate( 'IPS\cms\Records\Comment' . $database->id, $category->_id, NULL, $category->readPermissionMergeWithPage() ); \IPS\Content\Search\Index::i()->massUpdate( 'IPS\cms\Records\Review' . $database->id, $category->_id, NULL, $category->readPermissionMergeWithPage() ); } } catch( \Exception $e ) { } } /** * Save data * * @return void */ public function save() { if ( $this->id ) { static::deleteCompiled( $this->id ); } parent::save(); static::buildPageUrlStore(); if ( $this->full_path and \IPS\Theme::designersModeEnabled() ) { static::exportDesignersMode( $this->id ); } } /** * Get sortable name * * @return string */ public function getSortableName() { return $this->seo_name; } /** * Set default * * @return void */ public function setAsDefault() { \IPS\Db::i()->update( 'cms_pages', array( 'page_default' => 0 ), array( 'page_folder_id=?', $this->folder_id ) ); \IPS\Db::i()->update( 'cms_pages', array( 'page_default' => 1 ), array( 'page_id=?', $this->id ) ); static::buildPageUrlStore(); } /** * Resets a folder path * * @param string $path Path to reset * @return void */ public function setFullPath( $path ) { $this->full_path = trim( $path . '/' . $this->seo_name, '/' ); $this->save(); } /** * Displays a page * * @param string|NULL $title The Page title * @param int|NULL $httpStatusCode HTTP Status Code * @param array|NULL $httpHeaders Additional HTTP Headers * @param string|NULL $content Optional content to use. Useful if dynamic replacements need to be made at runtime * @throws \ErrorException * @return void */ public function output( $title=NULL, $httpStatusCode=NULL, $httpHeaders=NULL, $content=NULL ) { $includes = $this->getIncludes(); if ( isset( $includes['js'] ) and is_array( $includes['js'] ) ) { \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, array_values( $includes['js'] ) ); } /* Display */ if ( $this->ipb_wrapper or $this->type === 'builder' ) { $this->setTheme(); $nav = array(); \IPS\Output::i()->title = $this->getHtmlTitle(); /* This has to be done after setTheme(), otherwise \IPS\Theme::switchTheme() can wipe out CSS includes */ if ( isset( $includes['css'] ) and is_array( $includes['css'] ) ) { \IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, array_values( $includes['css'] ) ); } if ( $this->type === 'builder' ) { list( $group, $name, $key ) = explode( '__', $this->template ); \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate('pages')->globalWrap( $nav, \IPS\cms\Theme::i()->getTemplate($group, 'cms', 'page')->$name( $this, $this->getWidgets() ), $this ); } else { /* Populate \IPS\Output::i()->sidebar['widgets'] sidebar/header/footer widgets */ $this->getWidgets(); \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'pages', 'cms' )->globalWrap( $nav, $content ?: $this->getHtmlContent(), $this ); } /* Set the meta tags, but do not reset them if they are already set - articles can define custom meta tags and this code overwrites the ones set by articles if we don't verify they aren't set first */ if ( $this->meta_keywords AND ( !isset( \IPS\Output::i()->metaTags['keywords'] ) OR !\IPS\Output::i()->metaTags['keywords'] ) ) { \IPS\Output::i()->metaTags['keywords'] = $this->meta_keywords; } if ( $this->meta_description AND ( !isset( \IPS\Output::i()->metaTags['description'] ) OR !\IPS\Output::i()->metaTags['description'] ) ) { \IPS\Output::i()->metaTags['description'] = $this->meta_description; } /* If this is a default page, we may be accessing this from the folder only. The isset() check is to ensure canonical tags for more specific things (like databases) are not overridden. */ if ( $this->default AND !isset( \IPS\Output::i()->linkTags['canonical'] ) ) { \IPS\Output::i()->linkTags['canonical'] = (string) $this->url(); } /* Can only disable sidebar if HTML page */ if ( ! $this->show_sidebar and $this->type === 'html' ) { \IPS\Output::i()->sidebar['enabled'] = false; } if ( isset( \IPS\Settings::i()->cms_error_page ) and \IPS\Settings::i()->cms_error_page and \IPS\Settings::i()->cms_error_page == $this->id ) { \IPS\Output::i()->sidebar['enabled'] = false; } \IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'pages/page.css', 'cms', 'front' ) ); if ( ! ( \IPS\Application::load('cms')->default AND ! $this->folder_id AND $this->default ) ) { \IPS\Output::i()->breadcrumb['module'] = array( $this->url(), $this->_title ); } if ( isset( \IPS\Settings::i()->cms_error_page ) and \IPS\Settings::i()->cms_error_page and \IPS\Settings::i()->cms_error_page == $this->id ) { /* Set the title */ \IPS\Output::i()->title = ( $title ) ? $title : $this->getHtmlTitle(); \IPS\Output::i()->output = $content ?: $this->getHtmlContent(); \IPS\Member::loggedIn()->language()->parseOutputForDisplay( \IPS\Output::i()->title ); /* Send straight to the output engine */ \IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), ( $httpStatusCode ? $httpStatusCode : 200 ), 'text/html', ( $httpHeaders ? $httpHeaders : \IPS\Output::i()->httpHeaders ) ); } else { \IPS\Output::i()->allowDefaultWidgets = FALSE; /* Let the dispatcher finish off and show page */ return; } } else { $this->setTheme(); if ( isset( $includes['css'] ) and is_array( $includes['css'] ) ) { \IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, array_values( $includes['css'] ) ); } if ( $this->meta_keywords AND ( !isset( \IPS\Output::i()->metaTags['keywords'] ) OR !\IPS\Output::i()->metaTags['keywords'] ) ) { \IPS\Output::i()->metaTags['keywords'] = $this->meta_keywords; } if ( $this->meta_description AND ( !isset( \IPS\Output::i()->metaTags['description'] ) OR !\IPS\Output::i()->metaTags['description'] ) ) { \IPS\Output::i()->metaTags['description'] = $this->meta_description; } /* Meta tags */ \IPS\Output::i()->buildMetaTags(); /* Ensure MFA pop up shows */ $mfa = \IPS\Dispatcher\Front::i()->checkMfa( TRUE ); $mfa = $mfa ?: ''; if ( $this->wrapper_template and $this->wrapper_template !== '_none_' and ! \IPS\Request::i()->isAjax() ) { try { list( $group, $name, $key ) = explode( '__', $this->wrapper_template ); $content = $content ?: $this->getHtmlContent(); $content .= $mfa; \IPS\Output::i()->sendOutput( \IPS\cms\Theme::i()->getTemplate($group, 'cms', 'page')->$name( $content, $this->getHtmlTitle() ), 200, $this->getContentType(), \IPS\Output::i()->httpHeaders ); } catch( \OutOfRangeException $e ) { } } /* Set the title */ \IPS\Output::i()->title = ( $title ) ? $title : $this->getHtmlTitle(); \IPS\Member::loggedIn()->language()->parseOutputForDisplay( \IPS\Output::i()->title ); /* Send straight to the output engine */ $content = $content ?: $this->getHtmlContent(); $content .= $mfa; \IPS\Output::i()->sendOutput( $content, ( $httpStatusCode ? $httpStatusCode : 200 ), $this->getContentType(), ( $httpHeaders ? $httpHeaders : \IPS\Output::i()->httpHeaders ) ); } } /** * Write HTML pages to disk for designer's mode * * @param NULL|int $pageId Single page to export * @return void */ public static function exportDesignersMode( $pageId=NULL) { $where = array( array( 'page_type=?', 'html' ) ); $seen = array(); if ( $pageId ) { $where[] = array( 'page_id=?', $pageId ); } foreach( \IPS\Db::i()->select( '*', 'cms_pages', $where ) as $page ) { /* We could use recursive mode but it wouldn't correctly chmod the intermediate dirs */ $bits = explode( '/', "/themes/cms/pages/" . $page['page_full_path'] ); $dir = ''; $filename = array_pop( $bits ); foreach( $bits as $part ) { $dir .= $part . '/'; if ( ! is_dir( \IPS\ROOT_PATH . '/' . trim( $dir, '/' ) ) ) { mkdir( \IPS\ROOT_PATH . '/' . trim( $dir, '/' ), \IPS\IPS_FOLDER_PERMISSION ); chmod( \IPS\ROOT_PATH . '/' . trim( $dir, '/' ), \IPS\IPS_FOLDER_PERMISSION ); } } $headers = array(); foreach( array( 'page_seo_name', 'page_ipb_wrapper', 'page_title') as $field ) { $headers[ $field ] = $field . '="' . str_replace( '"', '\\"', $page[ $field ] ) . '"'; } $header = "<ips:pages " . implode( " ", $headers ) . " />"; try { \file_put_contents( \IPS\ROOT_PATH . '/' . trim( $dir, '/' ) . '/' . $filename, $header . "\n" . $page['page_content'] ); @chmod( \IPS\ROOT_PATH . '/' . trim( $dir, '/' ) . '/' . $filename, \IPS\IPS_FILE_PERMISSION ); } catch( \RuntimeException $e ) { } } /* Clear out any older designer mode files */ if ( ! $pageId ) { static::cleanUpDesignersModeFiles(); } } /** * Remove any old designer mode files * * @return void */ public static function cleanUpDesignersModeFiles() { $diskPaths = array(); static::getCurrentDesignersModeFilePaths( $diskPaths, \IPS\ROOT_PATH . "/themes/cms/pages/" ); $databasePaths = array(); foreach( \IPS\Db::i()->select( '*', 'cms_pages', array( 'page_type=?', 'html' ) ) as $page ) { $databasePaths[] = $page['page_full_path']; } if ( count( $diskPaths ) ) { foreach( $diskPaths as $path ) { if ( ! in_array( $path, $databasePaths ) ) { @unlink( \IPS\ROOT_PATH . "/themes/cms/pages/" . $path ); } } } } /** * Get existing designer's mode page paths * * @param array $paths Array of processed page paths * @param string $path Path to look into * @return void */ public static function getCurrentDesignersModeFilePaths( &$paths, $path ) { if ( is_dir( $path ) ) { foreach ( new \DirectoryIterator( $path ) as $dir ) { if ( $dir->isDot() || mb_substr( $dir->getFilename(), 0, 1 ) === '.' ) { continue; } if ( $dir->isDir() ) { static::getCurrentDesignersModeFilePaths( $paths, $dir->getRealPath() ); } else { $paths[] = trim( str_replace( \IPS\ROOT_PATH . '/themes/cms/pages/', '', $dir->getRealPath() ), '/' ); } } } } /** * Import media from disk for designer's mode * * @return void */ public static function importDesignersMode() { $path = \IPS\ROOT_PATH . '/themes/cms/pages'; $seen = array(); /* Grab folder data */ if ( is_dir( $path ) ) { static::importDesignersModeRecurse( $seen, $path ); } \IPS\Db::i()->delete( 'cms_pages', array( 'page_type=? and ' . \IPS\Db::i()->in( 'page_id', $seen, TRUE ), 'html' ) ); } /** * Import media from disk for designer's mode recursive method * * @param array $seen Array of processed page IDs * @param string $path Path to look into * @return void */ public static function importDesignersModeRecurse( &$seen, $path ) { if ( is_dir( $path ) ) { foreach ( new \DirectoryIterator( $path ) as $dir ) { if ( $dir->isDot() || mb_substr( $dir->getFilename(), 0, 1 ) === '.' ) { continue; } if ( $dir->isDir() ) { static::importDesignersModeRecurse( $seen, $dir->getRealPath() ); } else { $folders = \IPS\cms\Pages\Folder::roots(); $contents = \file_get_contents( $dir->getRealPath() ); $params = array(); /* Parse the header tag */ preg_match( '#^<ips:pages(.+?)/>(\r\n?|\n)#', $contents, $params ); /* Strip it */ $contents = ( isset($params[0]) ) ? str_replace( $params[0], '', $contents ) : $contents; /* Get fields */ preg_match_all( '#([a-z\-_]+?)="(.+?)"#', $params[1], $data, PREG_SET_ORDER ); $fields = array(); foreach( $data as $id => $matches ) { $fields[ $data[ $id ][1] ] = $data[ $id ][2]; } /* Existing page, or a new one? */ $fullPath = trim( str_replace( str_replace( '\\', '/', \IPS\ROOT_PATH ) . '/themes/cms/pages', '', str_replace( '\\', '/', $dir->getRealPath() ) ), '/' ); $bits = explode( '/', $fullPath ); $fileName = array_pop( $bits ); $path = implode( '/', $bits ); try { $page = \IPS\cms\Pages\Page::load( $fullPath, 'page_full_path' ); $page->content = $contents; $page->save(); } catch( \OutOfRangeException $e ) { $folderId = 0; foreach( $folders as $folder ) { if ( $folder->path == $path ) { $folderId = $folder->id; } } /* Doesn't exist, so this is new */ $page = new \IPS\cms\Pages\Page; $page->content = $contents; $page->full_path = $fullPath; $page->seo_name = $fileName; $page->type = 'html'; $page->title = ( isset( $fields['page_title'] ) ) ? $fields['page_title'] : $fileName; $page->ipb_wrapper = ( isset( $fields['page_ipb_wrapper'] ) ) ? $fields['page_ipb_wrapper'] : 1; $page->folder_id = $folderId; $page->save(); \IPS\Lang::saveCustom( 'cms', "cms_page_" . $page->id, $page->title ); } $seen[] = $page->id; } } } } }