Version 5.0.13

This commit is contained in:
Neo committed 2025-12-19 19:04:57 -08:00
1 parent b89858b2ff
commit 5efe744c5f
1407 files changed
+69690 -30177

No files matched your search

+8 -4
View File
@@ -531,14 +531,18 @@ EOF;
$block_editor = ( isset( Request::i()->block_editor ) ) ? Request::i()->block_editor : ( $this->id ? $this->getConfig('editor') : null );
$block_plugin = ( isset( Request::i()->block_plugin ) ) ? Request::i()->block_plugin : ( $this->id ? $this->plugin : null );
/* Build form */
$form->addTab( 'content_block_form_tab__details' );
/* Double check that we didn't force the PHP blocks */
if( $block_editor == 'php' and !Bridge::i()->featureIsEnabled( 'phpblocks' ) )
{
/* If PHP blocks are disabled, stop here */
$form->addMessage( 'content_block_php_disabled', 'ipsMessage ipsMessage--warning' );
$block_editor = 'html';
}
/* Build form */
$form->addTab( 'content_block_form_tab__details' );
$form->add( new Translatable( 'block_name', NULL, TRUE, array(
'app' => 'cms',
'key' => ( $this->id ? "content_block_name_" . $this->id : NULL )
@@ -1111,7 +1115,7 @@ EOF;
}
elseif( $widget['key'] == 'Codemirror' )
{
if( isset( $widget['configuration'] ) and $widget['configuration']['content'] )
if( isset( $widget['configuration']['content'] ) )
{
preg_match_all( '/\{block="(.+?)"}/is', $widget['configuration']['content'], $matches );
foreach( $matches[1] as $blockKey )
@@ -423,7 +423,18 @@ class Categories extends Model implements Permissions
if( array_key_exists( 'page_can_view', $values ) )
{
$this->club_view_permission= implode( ",", $values['page_can_view'] );
$values['club_node_public'] = intval( in_array( 'nonmember', $values['page_can_view'] ) or mb_strpos( '*', $this->club_view_permission ) !== false );
if( !in_array( 'nonmember', $values['page_can_view'] ) and !in_array( 'member', $values['page_can_view'] ) )
{
$values['club_node_public'] = Club::NODE_MODERATORS;
}
elseif( in_array( 'nonmember', $values['page_can_view'] ) or mb_strpos( '*', $this->club_view_permission ) !== false )
{
$values['club_node_public'] = Club::NODE_PUBLIC;
}
else
{
$values['club_node_public'] = Club::NODE_PRIVATE;
}
unset( $values['page_can_view'] );
}
@@ -2243,7 +2254,7 @@ class Categories extends Model implements Permissions
*/
public function metaDescription(): ?string
{
return $this->meta_description ?: static::database()->metaDescription();
return $this->meta_description ?: parent::metaDescription();
}
/**
@@ -2253,7 +2264,7 @@ class Categories extends Model implements Permissions
*/
public function metaTitle(): string
{
return $this->page_title ?: static::database()->metaTitle();
return $this->page_title ?: parent::metaTitle();
}
/**
@@ -1023,7 +1023,7 @@ class Databases extends Model implements Permissions
/* Editor Areas */
Member::loggedIn()->language()->words['editor__cms_Records' . $this->id ] = $this->_title;
foreach( array( 'pin', 'unpin', 'feature', 'unfeature', 'edit', 'hide', 'unhide', 'view_hidden', 'future_publish', 'view_future', 'move', 'lock', 'unlock', 'reply_to_locked', 'delete', 'feature_comments', 'unfeature_comments', 'add_item_message', 'edit_item_message', 'delete_item_message', 'view_reports' ) as $lang )
foreach( array( 'pin', 'unpin', 'feature', 'unfeature', 'edit', 'hide', 'unhide', 'view_hidden', 'future_publish', 'view_future', 'move', 'lock', 'unlock', 'reply_to_locked', 'delete', 'feature_comments', 'unfeature_comments', 'add_item_message', 'edit_item_message', 'delete_item_message', 'view_reports', 'assign' ) as $lang )
{
Member::loggedIn()->language()->words['can_' . $lang . '_content_db_lang_sl_' . $this->id ] = Member::loggedIn()->language()->addToStack( 'can_' . $lang . '_record', FALSE, array( 'sprintf' => array( $this->recordWord(1) ) ) );
Member::loggedIn()->language()->words['can_' . $lang . '_content_db_lang_su_' . $this->id ] = Member::loggedIn()->language()->addToStack( 'can_' . $lang . '_record', FALSE, array( 'sprintf' => array( $this->recordWord(1, TRUE) ) ) );
@@ -21,8 +21,10 @@ use IPS\cms\Records;
use IPS\core\DataLayer;
use IPS\Dispatcher as SystemDispatcher;
use IPS\Dispatcher\Controller;
use IPS\Dispatcher\Front;
use IPS\Http\Url;
use IPS\Output;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Settings;
use OutOfRangeException;
@@ -52,7 +54,12 @@ class Dispatcher extends SystemDispatcher
* @brief Controller location
*/
public string $controllerLocation = 'front';
/**
* Controller
*/
public ?string $controller = NULL;
/**
* @brief Database Id
*/
@@ -198,7 +205,7 @@ class Dispatcher extends SystemDispatcher
}
}
list( $path, $pageNumber ) = Page::getStrippedPagePath( $path );
[ $path, $pageNumber ] = Page::getStrippedPagePath( $path );
$this->databaseId = $database->id;
@@ -210,16 +217,16 @@ class Dispatcher extends SystemDispatcher
/* Got a specific category ID? */
if ( $this->categoryId !== NULL and ! $path and ( ( $database->use_categories and $database->cat_index_type !== 1 ) OR ( ! $database->use_categories and isset( Request::i()->do ) ) ) )
{
$this->module = 'category';
$this->controller = 'category';
}
else if ( isset( Request::i()->c ) AND is_numeric( Request::i()->c ) )
{
$this->categoryId = Request::i()->c;
$this->module = 'category';
$this->controller = 'category';
}
else if ( empty( $path ) )
{
$this->module = 'index';
$this->controller = 'index';
}
else
{
@@ -322,18 +329,18 @@ class Dispatcher extends SystemDispatcher
$this->_redirectToCorrectUrl( $record->url() );
}
$this->module = 'record';
$this->controller = 'record';
}
else
{
/* It's a category listing */
$this->module = 'category';
$this->controller = 'category';
}
}
$this->url = Url::internal( $url, 'front', 'content_page_path' );
$className = '\\IPS\\cms\\modules\\front\\database\\' . $this->module;
$className = '\\IPS\\cms\\modules\\front\\database\\' . $this->controller;
/* Init class */
if( !class_exists( $className ) )
{
@@ -372,7 +379,7 @@ class Dispatcher extends SystemDispatcher
$controller->execute();
/* Data Layer Context */
if ( $this->databaseId AND Settings::i()->core_datalayer_enabled )
if ( $this->databaseId AND DataLayer::enabled() )
{
try
{
@@ -445,6 +452,25 @@ class Dispatcher extends SystemDispatcher
*/
public function finish() : void
{
Bridge::i()->frontDispatcherFinish();
if( $this->recordId )
{
Output::i()->bodyAttributes['contentClass'] = 'IPS\cms\Records' . $this->databaseId;
}
elseif( $this->categoryId )
{
Output::i()->bodyAttributes['contentClass'] = 'IPS\cms\Categories' . $this->databaseId;
}
else
{
Output::i()->bodyAttributes['contentClass'] = 'IPS\cms\Databases';
}
/* Data Attributes */
Output::i()->setBodyAttributes();
Front::checkAlerts( $this );
Output::i()->output = $this->output ?: Output::i()->output;
/* Loader Extension */
+2 -2
View File
@@ -1290,9 +1290,9 @@ class Fields extends CustomField implements Permissions
/**
* Get the "extra" field
*
* @return array
* @return array|null
*/
public function get_extra(): mixed
public function get_extra(): ?array
{
return isset( $this->_data['extra'] ) ? json_decode( $this->_data['extra'], TRUE ) : [];
}
@@ -52,7 +52,7 @@ class Soundcloud extends Text
* @param string|null $suffix HTML to show after input field
* @param string|null $id The ID to add to the row
*/
public function __construct( string $name, mixed $defaultValue=NULL, bool $required=FALSE, array $options=array(), callable $customValidationCode=NULL, string $prefix=NULL, string $suffix=NULL, string $id=NULL )
public function __construct( string $name, mixed $defaultValue=NULL, ?bool $required=FALSE, array $options=array(), callable $customValidationCode=NULL, string $prefix=NULL, string $suffix=NULL, string $id=NULL )
{
$this->childDefaultOptions['placeholder'] = Member::loggedIn()->language()->addToStack('field_placeholder_soundcloud');
+1 -1
View File
@@ -52,7 +52,7 @@ class Spotify extends Text
* @param string|null $suffix HTML to show after input field
* @param string|null $id The ID to add to the row
*/
public function __construct( string $name, mixed $defaultValue=null, bool $required=FALSE, array $options=array(), callable $customValidationCode=null, string $prefix=null, string $suffix=null, string $id=null )
public function __construct( string $name, mixed $defaultValue=null, ?bool $required=FALSE, array $options=array(), callable $customValidationCode=null, string $prefix=null, string $suffix=null, string $id=null )
{
$this->childDefaultOptions['placeholder'] = Member::loggedIn()->language()->addToStack('field_placeholder_spotify');
+1 -1
View File
@@ -52,7 +52,7 @@ class Youtube extends Text
* @param string|null $suffix HTML to show after input field
* @param string|null $id The ID to add to the row
*/
public function __construct( string $name, mixed $defaultValue=NULL, bool $required=FALSE, array $options=array(), callable $customValidationCode=NULL, string $prefix=NULL, string $suffix=NULL, string $id=NULL )
public function __construct( string $name, mixed $defaultValue=NULL, ?bool $required=FALSE, array $options=array(), callable $customValidationCode=NULL, string $prefix=NULL, string $suffix=NULL, string $id=NULL )
{
$this->childDefaultOptions['placeholder'] = Member::loggedIn()->language()->addToStack('field_placeholder_youtube');
+314 -32
View File
@@ -92,7 +92,7 @@ class Page extends Model implements Permissions
* @brief [ActiveRecord] Caches
* @note Defined cache keys will be cleared automatically as needed
*/
protected array $caches = array( 'frontNavigation' );
protected array $caches = array( 'frontNavigation', 'pageDefaults' );
/**
* @brief [ActiveRecord] Database Table
@@ -348,11 +348,11 @@ class Page extends Model implements Permissions
/* @var $class Model */
$folder = $class::load( $path, 'folder_path' );
static::$loadedPagesFromPath[ $path ] = static::getDefaultPage( $folder->id );
static::$loadedPagesFromPath[ $path ] = static::getDefaultForMember( $folder->id );
}
else
{
static::$loadedPagesFromPath[ $path ] = static::getDefaultPage();
static::$loadedPagesFromPath[ $path ] = static::getDefaultForMember();
}
}
catch ( OutOfRangeException $e )
@@ -474,6 +474,13 @@ class Page extends Model implements Permissions
/* Still here? Consistent with AR pattern */
throw new OutOfRangeException();
}
/**
* @brief Constants for default page settings
*/
const PAGE_NODEFAULT = 0;
const PAGE_DEFAULT = 1;
const PAGE_DEFAULT_OVERRIDE = 2;
/**
* Return the default page for this folder
@@ -488,7 +495,7 @@ class Page extends Model implements Permissions
/* Try the easiest method first */
try
{
static::$defaultPage[ $folderId ] = Page::load( Db::i()->select( 'page_id', static::$databaseTable, array( 'page_default=? AND page_folder_id=?', 1, $folderId ) )->first() );
static::$defaultPage[ $folderId ] = Page::load( Db::i()->select( 'page_id', static::$databaseTable, array( 'page_default=? AND page_folder_id=?', static::PAGE_DEFAULT, $folderId ) )->first() );
}
catch( Exception $ex )
{
@@ -510,6 +517,159 @@ class Page extends Model implements Permissions
return ( isset( static::$defaultPage[ $folderId ] ) ) ? static::$defaultPage[ $folderId ] : NULL;
}
/**
* Build a list of default pages for each group
*
* @return array
*/
public static function loadDefaultsPerGroup() : array
{
try
{
return Store::i()->pageDefaults;
}
catch( OutOfRangeException ){}
$folderRoots = iterator_to_array(
Db::i()->select( 'folder_id', 'cms_folders' )
);
array_unshift( $folderRoots, 0 );
$defaults = [];
foreach( $folderRoots as $folderId )
{
$defaults[ $folderId ] = [];
foreach( Db::i()->select( 'page_id,page_group_defaults', static::$databaseTable, [ 'page_default=? and page_folder_id=?', static::PAGE_DEFAULT_OVERRIDE, $folderId ] ) as $row )
{
foreach( explode( ",", $row['page_group_defaults'] ) as $groupId )
{
$defaults[ $folderId ][ $groupId ] = $row['page_id'];
}
}
try
{
$primaryDefault = static::getDefaultPage( $folderId );
foreach( Group::groups() as $group )
{
if( !isset( $defaults[ $folderId ][ $group->g_id ] ) )
{
$defaults[ $folderId ][ $group->g_id ] = $primaryDefault->_id;
}
}
}
catch( OutOfRangeException ){}
}
Store::i()->pageDefaults = $defaults;
return $defaults;
}
/**
* Load the default application for this member
*
* @param int $folderId
* @param Member|null $member
* @return static
*/
public static function getDefaultForMember( int $folderId=0, ?Member $member=null ) : static
{
/* We're only going to use the primary group here */
$member = $member ?? Member::loggedIn();
$pageId = static::loadDefaultsPerGroup()[ $folderId ][ $member->member_group_id ] ?? null;
if( $pageId )
{
try
{
return static::load( $pageId );
}
catch( OutOfRangeException ){}
}
return static::getDefaultPage( $folderId );
}
/**
* Set the page as a default for specific groups
*
* @param array $groups
* @return void
*/
public function setAsDefaultForGroups( array $groups ) : void
{
$currentDefaults = static::loadDefaultsPerGroup();
$newGroups = [];
$pagesToUpdate = [];
foreach( $groups as $group )
{
$groupId = ( $group instanceof Group ) ? $group->g_id : $group;
$currentPage = $currentDefaults[ $this->folder_id ][ $groupId ] ?? null;
if( $currentPage !== null and $currentPage != $this->id )
{
$pagesToUpdate[ $currentPage ] = [];
foreach( $currentDefaults[ $this->folder_id ] as $k => $v )
{
if( $v == $currentPage and $k != $groupId )
{
$pagesToUpdate[ $currentPage ][] = $k;
}
}
}
$newGroups[] = $groupId;
}
/* Did we make this the default for all the groups? */
if( count( $newGroups ) == count( Group::groups() ) )
{
$this->setAsDefault();
}
else
{
if( empty( $newGroups ) )
{
$this->default = static::PAGE_NODEFAULT;
$this->group_defaults = null;
}
else
{
$this->default = static::PAGE_DEFAULT_OVERRIDE;
$this->group_defaults = implode( ",", $newGroups );
}
$this->save();
}
/* Now update any pages that need to be changed */
foreach( $pagesToUpdate as $pageId => $_values )
{
try
{
$page = static::load( $pageId );
/* If it's set as default 1, we can leave that alone */
if( $page->default == static::PAGE_DEFAULT_OVERRIDE )
{
$groupsToUse = array_diff( $_values, $newGroups );
if( empty( $groupsToUse ) )
{
$page->default = static::PAGE_NODEFAULT;
$page->group_defaults = null;
}
else
{
$page->group_defaults = implode( ",", $groupsToUse );
}
$page->save();
}
}
catch( OutOfRangeException ){}
}
}
/**
* Delete compiled versions
@@ -756,7 +916,7 @@ class Page extends Model implements Permissions
if ( $pageType == 'html' )
{
$return['tab_content'] = array( 'content_page_form_tab__content', NULL, NULL, 'ipsForm_vertical' );
$return['tab_content'] = array( 'content_page_form_tab__content', NULL, NULL, 'ipsForm--vertical' );
$tagSource = Url::internal( "app=cms&module=pages&controller=ajax&do=loadTags" );
if ( $item )
@@ -809,7 +969,6 @@ class Page extends Model implements Permissions
{
$page = new self;
$page->type = $pageType;
$page->save();
$page->saveForm( $page->formatFormValues( $values ) );
@@ -1241,6 +1400,17 @@ class Page extends Model implements Permissions
public function getAreasFromDatabase( ?string $area=null ) : array
{
$areas = array();
if( $this->_version !== null )
{
foreach( $this->_version->data['areas'] as $id => $tree )
{
$areas[ $id ] = new Area( $tree, $id );
}
return $areas;
}
$where = [ array( 'area_page_id=?', $this->id ) ];
if ( $area !== null )
@@ -1267,10 +1437,23 @@ class Page extends Model implements Permissions
* Save an area to the database and link it to the page
*
* @param Area $area
* @param bool $backup Should we store a copy?
* @return void
*/
public function saveArea( Area $area ) : void
public function saveArea( Area $area, bool $backup=true ) : void
{
/* If we are viewing an old version, do nothing! These should be read-only */
if( $this->_version !== null )
{
return;
}
/* Create a backup before we do anything here */
if( $backup )
{
Revision::store( $this );
}
/* If we have no content, clear this out entirely */
if( !$area->hasWidgets() )
{
@@ -1429,22 +1612,33 @@ class Page extends Model implements Permissions
$return['edit'] = $buttons['edit'];
}
if ( Member::loggedIn()->hasAcpRestriction( 'cms', 'pages', 'page_edit' ) and !$this->default )
if ( Member::loggedIn()->hasAcpRestriction( 'cms', 'pages', 'page_edit' ) )
{
$return['default'] = array(
'icon' => $this->default ? 'star' : 'regular fa-star',
'title' => 'content_default_page',
'link' => $url->setQueryString( array( 'id' => $this->id, 'subnode' => 1, 'do' => 'setAsDefault' ) )->csrf()
);
}
$return['default'] = array(
'icon' => ( $this->default == static::PAGE_DEFAULT ) ? 'star' : 'regular fa-star',
'title' => 'content_default_page',
'link' => $url->setQueryString( array( 'id' => $this->id, 'subnode' => 1, 'do' => 'setAsDefault' ) ),
'data' => [ 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'content_default_page' ) ]
);
if ( Member::loggedIn()->hasAcpRestriction( 'cms', 'pages', 'page_edit' ) and $this->type !== 'builder' )
{
$return['default_error'] = array(
'icon' => Settings::i()->cms_error_page == $this->id ? 'exclamation-circle' : 'exclamation',
'title' => Settings::i()->cms_error_page == $this->id ? 'content_remove_error_page' : 'content_default_error_page',
'link' => $url->setQueryString( array( 'id' => Settings::i()->cms_error_page ? 0 : $this->id, 'subnode' => 1, 'do' => 'toggleDefaultError' ) )->csrf()
);
if( $this->type !== 'builder' )
{
$return['default_error'] = array(
'icon' => Settings::i()->cms_error_page == $this->id ? 'exclamation-circle' : 'exclamation',
'title' => Settings::i()->cms_error_page == $this->id ? 'content_remove_error_page' : 'content_default_error_page',
'link' => $url->setQueryString( array( 'id' => Settings::i()->cms_error_page ? 0 : $this->id, 'subnode' => 1, 'do' => 'toggleDefaultError' ) )->csrf()
);
}
/* Do we have at least one stored version? */
if( $previous = Revision::previousVersion( $this ) )
{
$return['history'] = array(
'icon' => 'history',
'title' => 'content_page_history',
'link' => $url->setQueryString( array( 'id' => $this->id, 'subnode' => 1, 'do' => 'history' ) )
);
}
}
$return['view'] = array(
@@ -1587,6 +1781,12 @@ class Page extends Model implements Permissions
$this->save();
}
/* If this is not a new page, store the current page in the history before we make any changes */
if( !$isNew )
{
Revision::store( $this );
}
if( isset( $values['page_name'] ) )
{
$_copied = $values['page_name'];
@@ -1810,7 +2010,7 @@ class Page extends Model implements Permissions
{
if( $this->_url === NULL )
{
if ( Application::load('cms')->default AND $this->default AND ! $this->folder_id )
if ( Application::load('cms')->default AND $this->default == static::PAGE_DEFAULT AND ! $this->folder_id )
{
/* No - that's easy */
$this->_url = Url::internal( '', 'front' );
@@ -1841,6 +2041,33 @@ class Page extends Model implements Permissions
}
}
/**
* @var Revision|null
*/
protected ?Revision $_version = null;
/**
* View as a specific version
*
* @param int|null $version
* @return void
*/
public function setVersion( ?int $version=null ) : void
{
/* Clear it first */
$this->_version = null;
try
{
$this->_version = Revision::load( $version );
foreach( $this->_version->data['settings'] as $k => $v )
{
$this->_data[ $k ] = $v;
}
}
catch( OutOfRangeException ){}
}
/**
* Map this database to a specific page
*
@@ -1951,6 +2178,9 @@ class Page extends Model implements Permissions
/* Remove any widgets for this page */
Db::i()->delete( 'cms_page_widget_areas', array( 'area_page_id=?', $this->id ) );
/* Clear history */
Db::i()->delete( 'cms_page_revisions', array( 'revision_page_id=?', $this->id ) );
parent::delete();
@@ -2020,10 +2250,22 @@ class Page extends Model implements Permissions
*/
public function save(): void
{
/* If we are viewing an older version, don't allow any changes */
if( $this->_version !== null )
{
return;
}
if ( $this->id )
{
static::deleteCompiled( $this->id );
}
/* If this is not new and the folder changed, check defaults */
if( !$this->_new and isset( $this->changed['folder_id'] ) )
{
$this->checkDefaultPage();
}
parent::save();
@@ -2047,10 +2289,39 @@ class Page extends Model implements Permissions
*/
public function setAsDefault() : void
{
Db::i()->update( 'cms_pages', array( 'page_default' => 0 ), array( 'page_folder_id=?', $this->folder_id ) );
Db::i()->update( 'cms_pages', array( 'page_default' => 1 ), array( 'page_id=?', $this->id ) );
Db::i()->update( 'cms_pages', array( 'page_default' => static::PAGE_NODEFAULT, 'page_group_defaults' => null ), array( 'page_folder_id=? and page_default=?', $this->folder_id, static::PAGE_DEFAULT ) );
Db::i()->update( 'cms_pages', array( 'page_default' => static::PAGE_DEFAULT, 'page_group_defaults' => null ), array( 'page_id=?', $this->id ) );
static::buildPageUrlStore();
unset( Store::i()->pageDefaults );
}
/**
* Make sure that we only have one default page per folder.
* This is called only when the folder is changed.
*
* @return void
*/
protected function checkDefaultPage() : void
{
if( $this->default == static::PAGE_DEFAULT )
{
try
{
$currentDefault = Db::i()->select( 'page_id', 'cms_pages', [ 'page_default=? and page_folder_id=? and page_id <> ?', static::PAGE_DEFAULT, $this->folder_id, (int) $this->_id ] )->first();
/* If we found a different default page, remove the default from this one */
$this->default = static::PAGE_NODEFAULT;
}
catch( UnderflowException ){}
}
elseif( $this->default == static::PAGE_DEFAULT_OVERRIDE )
{
/* Group overrides are always removed when we move a page to another folder */
$this->default = static::PAGE_NODEFAULT;
$this->group_defaults = null;
}
}
/**
@@ -2121,7 +2392,7 @@ class Page extends Model implements Permissions
/* Make sure we initialize the main area */
Output::i()->sidebar['widgets']['col1'] = [];
Output::i()->output = Theme::i()->getTemplate( 'pages' )->globalWrap( $nav, $this->getPageContent(), $this );
Output::i()->output = Theme::i()->getTemplate( 'pages', 'cms' )->globalWrap( $nav, $this->getPageContent(), $this );
if ( isset( Settings::i()->cms_error_page ) and Settings::i()->cms_error_page and Settings::i()->cms_error_page == $this->id )
{
@@ -2138,7 +2409,7 @@ class Page extends Model implements Permissions
}
catch ( OutOfRangeException | OutOfBoundsException )
{
if ( !( Application::load( 'cms' )->default and !$this->folder_id and $this->default ) )
if ( !( Application::load( 'cms' )->default and !$this->folder_id and $this->default == static::PAGE_DEFAULT ) )
{
Output::i()->breadcrumb['module'] = array( $this->url(), $this->_title );
}
@@ -2156,10 +2427,6 @@ class Page extends Model implements Permissions
/* Send straight to the output engine */
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( Output::i()->title, Output::i()->output, array( 'app' => Dispatcher::i()->application->directory, 'module' => Dispatcher::i()->module->key, 'controller' => Dispatcher::i()->controller ) ), ( $httpStatusCode ?: 200 ), 'text/html', ( $httpHeaders ?: array() ) );
}
else
{
Output::i()->allowDefaultWidgets = FALSE;
}
}
else
{
@@ -2200,9 +2467,15 @@ class Page extends Model implements Permissions
*/
protected function getPageContent() : string
{
$content = '';
if( $this->_version !== null )
{
Output::i()->customHeader .= Theme::i()->getTemplate( 'pages', 'cms' )->revision( $this->_version );
}
if( $this->type == 'html' )
{
return $this->getHtmlContent();
return $content . $this->getHtmlContent();
}
$widgets = [];
@@ -2216,6 +2489,15 @@ class Page extends Model implements Permissions
}
$tree = $item['area_tree'] ? json_decode( $item['area_tree'], true ) : [];
if( $this->_version !== null )
{
if ( isset( $this->_version->data['areas'][ $item['area_area'] ] ) )
{
/* We have a previous version we want to show */
$tree = $this->_version->data['areas'][ $item['area_area'] ];
}
}
if ( empty( $tree ) )
{
/* If the tree is empty, generate one from the database rows */
@@ -2242,7 +2524,7 @@ class Page extends Model implements Permissions
$widgets['col1'] = new Area( [], 'col1' );
}
return Theme::i()->getTemplate( 'pages', 'cms' )->mainArea( $this, $widgets );
return $content . Theme::i()->getTemplate( 'pages', 'cms' )->mainArea( $this, $widgets );
}
/**
+211
View File
@@ -0,0 +1,211 @@
<?php
/**
* @brief Revision
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage
* @since 6/25/2025
*/
namespace IPS\cms\Pages;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Db;
use IPS\Lang;
use IPS\Member;
use IPS\Patterns\ActiveRecord;
use IPS\Widget\Area;
use UnderflowException;
use function count;
use function is_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
class Revision extends ActiveRecord
{
/**
* @brief Database Table
*/
public static ?string $databaseTable = 'cms_page_revisions';
/**
* @brief Database Prefix
*/
public static string $databasePrefix = 'revision_';
/**
* @brief Multiton Store
*/
protected static array $multitons;
/**
* @return Page
*/
public function get_page() : Page
{
return Page::load( $this->page_id );
}
/**
* @return array
*/
public function get_data() : array
{
return isset( $this->_data['data'] ) ? json_decode( $this->_data['data'], true ) : [];
}
/**
* @param mixed $val
* @return void
*/
public function set_data( mixed $val ) : void
{
$this->_data['data'] = ( is_array( $val ) and count( $val ) ) ? json_encode( $val ) : null;
}
/**
* @return Member
*/
public function author() : Member
{
return Member::load( $this->member_id );
}
/**
* Store a version of the page history
*
* @param Page $page
* @param bool $manualSave Whether this is being called manually (e.g. from finish editing)
* @return void
*/
public static function store( Page $page, bool $manualSave=false ) : void
{
$data = [
'settings' => [
'name' => Member::loggedIn()->language()->get( Page::$titleLangPrefix . $page->id ),
'js_css_ids' => $page->js_css_ids,
'wrapper_template' => $page->wrapper_template,
'theme' => $page->theme,
'template' => $page->template,
'ipb_wrapper' => $page->ipb_wrapper,
'default' => $page->default
],
'areas' => []
];
if( $page->type == 'html' )
{
$data['settings']['content'] = $page->content;
}
/* Now handle all the areas */
foreach( $page->getAreasFromDatabase() as $area )
{
$data['areas'][ $area->id ] = $area->toArray();
}
/* Get the latest revision; if nothing has changed, don't store it */
if( $previous = static::previousVersion( $page ) )
{
if( ! $manualSave and ( $previous->data === $data ) )
{
return;
}
}
$obj = new static;
$obj->page_id = $page->id;
$obj->data = $data;
$obj->manual_save = (int) $manualSave;
$obj->save();
}
/**
* Make this the latest version.
* If no version is specified, just use the latest
*
* @return void
*/
public function revert() : void
{
/* @var Page $page */
$page = $this->page;
/* First, store the current page as is */
static::store( $page );
/* Now loop through the page configuration and set all properties */
foreach( $this->data['settings'] as $k => $v )
{
$page->$k = $v;
if( $k == 'name' )
{
Lang::saveCustom( 'cms', Page::$titleLangPrefix . $page->id, $v );
}
}
$page->save();
/* Load the current areas, in case we need to remove one */
$currentAreas = $page->getAreasFromDatabase();
/* Now the areas */
$newAreas = [];
foreach( $this->data['areas'] as $id => $tree )
{
$newAreas[] = $id;
$area = new Area( $tree, $id );
$page->saveArea( $area, false );
}
/* Remove any areas that we no longer need */
foreach( $currentAreas as $area )
{
if( !in_array( $area->id, $newAreas ) )
{
Db::i()->delete( 'cms_page_widget_areas', [ 'area_area=? and area_page_id=?', $area->id, $page->id ] );
}
}
}
/**
* Get the previously saved version
*
* @param Page $page
* @return static|null
*/
public static function previousVersion( Page $page ) : static|null
{
try
{
$row = Db::i()->select( '*', static::$databaseTable, [ 'revision_page_id=?', $page->id ], 'revision_date desc', [ 0, 1 ] )->first();
return static::constructFromData( $row );
}
catch( UnderflowException ){}
return null;
}
/**
* Save Changed Columns
*
* @return void
*/
public function save() : void
{
if( $this->_new )
{
$this->member_id = Member::loggedIn()->member_id;
$this->date = time();
}
parent::save();
}
}
+3 -1
View File
@@ -23,6 +23,7 @@ use IPS\Content\Filter;
use IPS\Content\Hideable;
use IPS\Content\Featurable;
use IPS\Content\Reactable;
use IPS\Content\Recognizable;
use IPS\Content\Reportable;
use IPS\Content\Shareable;
use IPS\Db;
@@ -51,7 +52,8 @@ class Comment extends ContentComment implements Embeddable,
Shareable,
EditHistory,
Hideable,
Featurable;
Featurable,
Recognizable;
/**
* @brief [ActiveRecord] Multiton Store
+21 -21
View File
@@ -46,6 +46,7 @@ use IPS\Content\Shareable;
use IPS\Content\Taggable;
use IPS\Content\Statistics;
use IPS\Content\ViewUpdates;
use IPS\core\DataLayer;
use IPS\DateTime;
use IPS\Db;
use IPS\Events\Event;
@@ -287,20 +288,22 @@ class Records extends Item implements
* Most, if not all of these are the same for different events, so we can just have one method
*
* @param Comment|null $comment A comment item, leave null for these keys to be omitted
* @param array $createOrEditValues =[] Values from the create or edit form, if applicable.
* @param bool $clearCache=false
*
* @return array
*/
public function getDataLayerProperties( ?Comment $comment = null ): array
public function getDataLayerProperties( ?Comment $comment = null, array $createOrEditValues = [], bool $clearCache = false ): array
{
$commentIdColumn = $comment ? $comment::$databaseColumnId : null;
$index = $commentIdColumn ? ( $comment->$commentIdColumn ?: 0 ) : 0;
if ( isset( $this->_dataLayerProperties[$index] ) )
$index = "idx_" . ( $commentIdColumn ? ( $comment->$commentIdColumn ?: 0 ) : -1 );
if ( !$clearCache and isset( $this->_dataLayerProperties[$index] ) )
{
return $this->_dataLayerProperties[$index];
}
/* Set the content_type and comment_type to lower case for consistency */
$properties = parent::getDataLayerProperties( $comment );
$properties = parent::getDataLayerProperties( $comment, $createOrEditValues, $clearCache );
if ( isset( $properties['content_type'] ) )
{
$properties['content_type'] = strtolower( Lang::load( Lang::defaultLanguage() )->addToStack( static::$title ) );
@@ -1520,9 +1523,18 @@ class Records extends Item implements
{
if ( ! isset( $this->customDisplayFields['all'][ $type ] ) )
{
$this->customDisplayFields['all'][ $type ] = [];
$fieldsClass = '\IPS\cms\Fields' . static::$customDatabaseId;
/* @var $fieldsClass Fields */
$this->customDisplayFields['all'][ $type ] = $fieldsClass::display( $this->fieldValues(), $type, $this->container(), 'key', $this );
/* @var $fieldsClass Fields */
foreach( $fieldsClass::display( $this->fieldValues(), $type, $this->container(), 'key', $this ) as $k => $fieldValue )
{
/* We only show these if there is a value anyway */
if( $fieldValue )
{
$this->customDisplayFields['all'][ $type ][ $k ] = $fieldValue;
}
}
}
return $this->customDisplayFields['all'][ $type ];
@@ -1924,7 +1936,7 @@ class Records extends Item implements
*/
public function get__record_image_thumb() : string
{
return $this->record_image_thumb ?: $this->record_image;
return $this->record_image_thumb ?: ( $this->record_image ?? '' );
}
/**
@@ -3027,19 +3039,7 @@ class Records extends Item implements
return TRUE;
}
/* Am I a Moderator with edit permissions ?*/
if ( static::modPermission( 'edit', $member, $this->containerWrapper() ) )
{
return TRUE;
}
if ( parent::canEdit( $member ) )
{
/* Test against specific perms for this category */
return $this->container()->can( 'edit', $member );
}
return FALSE;
return parent::canEdit( $member );
}
/**
@@ -3814,7 +3814,7 @@ class Records extends Item implements
*/
public function isTopicSyncEnabled() : bool
{
return $this->_forum_record and $this->_forum_comments and Application::appIsEnabled('forums');
return $this->_forum_record and Application::appIsEnabled('forums');
}
/**
+12 -12
View File
@@ -70,15 +70,15 @@ class Widget extends SystemWidget
$uniqueIds = array();
foreach( Db::i()->select( '*', 'cms_page_widget_areas' ) as $item )
{
if( $item['area_widgets'] )
if( !empty( $item['area_tree'] ) )
{
$area = new Area( json_decode( $item['area_tree'], true ), $item['area_area'] );
$widgets = $area->getAllWidgets();
}
elseif( $item['area_widgets'] )
{
$widgets = json_decode( $item['area_widgets'], TRUE );
}
else
{
$area = new Area( json_decode( $item['area_tree'], true ), $item['area_area'] );
$widgets = $area->getAllWidgets();
}
if( !is_array( $widgets ) or !count( $widgets ) )
{
@@ -99,15 +99,15 @@ class Widget extends SystemWidget
foreach( Db::i()->select( '*', 'core_widget_areas' ) as $item )
{
if( $item['widgets'] )
if( !empty( $item['tree'] ) )
{
$area = new Area( json_decode( $item['tree'], true ), $item['area'] );
$widgets = $area->getAllWidgets();
}
elseif( $item['widgets'] )
{
$widgets = json_decode( $item['widgets'], TRUE );
}
else
{
$area = new Area( $item['area'], json_decode( $item['tree'], true ) );
$widgets = $area->getAllWidgets();
}
if( !is_array( $widgets ) or !count( $widgets ) )
{