Version 4.6.0

This commit is contained in:
Neo committed 2025-12-19 05:55:31 -08:00
1 parent f79dcf067a
commit 517a5e1f70
2036 files changed
+110041 -26162

No files matched your search

+1 -1
View File
@@ -235,8 +235,8 @@ class _NodeType extends ObjectType
}
$where = array();
$sortBy = "{$orderBy} {$args['orderDir']}";
$class = static::$nodeClass::$contentItemClass;
$sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . "{$orderBy} {$args['orderDir']}";
// Container
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'] . '=?', $node->_id );
+1 -1
View File
@@ -87,7 +87,7 @@ class _NodeController extends \IPS\Api\Controller
}
/**
* View
* Delete
*
* @param int $id ID Number
* @param int|NULL $deleteChildrenOrMove -1 to delete all child nodes, or the new parent node ID to move the children to
+33 -5
View File
@@ -55,6 +55,13 @@ class _Controller extends \IPS\Dispatcher\Controller
/* Set the title */
$title = $nodeClass::$nodeTitle;
if( \IPS\Request::i()->subnode == 1 AND isset( $nodeClass::$subnodeClass ) )
{
$subnodeClass = $nodeClass::$subnodeClass;
$title = $subnodeClass::$nodeTitle;
}
$this->title = $title;
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( $title );
@@ -466,7 +473,7 @@ class _Controller extends \IPS\Dispatcher\Controller
}
/* Display */
\IPS\Output::i()->output .= $form;
\IPS\Output::i()->output .= $this->_showForm( $form );
}
/**
@@ -491,6 +498,17 @@ class _Controller extends \IPS\Dispatcher\Controller
return $form;
}
/**
* Get form
*
* @param \IPS\Helpers\Form $form The form as returned by _addEditForm()
* @return string
*/
protected function _showForm( \IPS\Helpers\Form $form )
{
return (string) $form;
}
/**
* Redirect after save
*
@@ -499,7 +517,7 @@ class _Controller extends \IPS\Dispatcher\Controller
* @param string $lastUsedTab The tab last used in the form
* @return void
*/
protected function _afterSave( \IPS\Node\Model $old = NULL, \IPS\Node\Model $new, $lastUsedTab = FALSE )
protected function _afterSave( ?\IPS\Node\Model $old, \IPS\Node\Model $new, $lastUsedTab = FALSE )
{
if( \IPS\Request::i()->isAjax() )
{
@@ -1163,13 +1181,23 @@ class _Controller extends \IPS\Dispatcher\Controller
if ( $values = $matrix->values() )
{
$_perms = array();
/* Check for "all" checkboxes */
foreach ( $nodeClass::$permissionMap as $k => $v )
{
if ( isset( \IPS\Request::i()->__all[ $k ] ) )
{
$_perms[ $v ] = '*';
$shouldCheckAll = TRUE;
foreach( $matrix->elements as $group => $element )
{
if ( isset( $element[ $k ] ) and $element[ $k ]->options['disabled'] )
{
$shouldCheckAll = FALSE;
break;
}
}
$_perms[ $v ] = $shouldCheckAll ? '*' : array();
}
else
{
@@ -1195,7 +1223,7 @@ class _Controller extends \IPS\Dispatcher\Controller
}
}
}
/* Finalise */
foreach ( $_perms as $k => $v )
{
+72 -115
View File
@@ -1015,9 +1015,10 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
* Get last comment time
*
* @note This should return the last comment time for this node only, not for children nodes
* @param \IPS\Member|NULL $member MemberObject
* @return \IPS\DateTime|NULL
*/
public function getLastCommentTime()
public function getLastCommentTime( \IPS\Member $member = NULL )
{
return NULL;
}
@@ -1599,6 +1600,54 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
return FALSE;
}
/**
* @brief Constant allow anonymous items
*/
const ANON_ITEMS = 1;
/**
* @brief Constant allow anonymous comments
*/
const ANON_COMMENTS = 2;
/**
* [Node] Does the currently logged in user have permission to post anonymously in this node?
*
* @return bool
*/
public function canPostAnonymously( $where = 0 )
{
if ( ! isset( static::$contentItemClass ) or ! \in_array( 'IPS\Content\Anonymous', class_implements( static::$contentItemClass ) ) )
{
return FALSE;
}
if( ! \IPS\Member::loggedIn()->member_id or ! \IPS\Member::loggedIn()->group['gbw_can_post_anonymously'] )
{
return FALSE;
}
/* 1 means both items and comments */
if ( $this->allow_anonymous === 1 )
{
return TRUE;
}
/* We are not checking the where, so either is fine */
if ( $where === 0 and $this->allow_anonymous > 0 )
{
return TRUE;
}
/* 2 means comments only */
if ( $where === static::ANON_COMMENTS and $this->allow_anonymous === 2 )
{
return TRUE;
}
return FALSE;
}
/* !Front-end permissions */
@@ -2826,114 +2875,6 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
}
}
/**
* @brief Cached array of IDs a member has posted in
*/
protected $contentPostedIn = array();
/**
* Retrieve an array of IDs a member has posted in.
*
* @param \IPS\Member|NULL $member The member (NULL for currently logged in member)
* @param array|NULL $inSet If supplied, checks will be restricted to only the ids provided
* @param array|NULL $additionalWhere Additional where clause
* @param array|NULL $commentJoinWhere Additional join clause for comments table
* @return array An array of content item ids
*/
public function contentPostedIn( $member=NULL, $inSet=NULL, $additionalWhere=NULL, $commentJoinWhere=NULL )
{
if ( $member === NULL )
{
$member = \IPS\Member::loggedIn();
}
if( !$member->member_id )
{
return array();
}
if ( !isset( static::$contentItemClass ) )
{
return array();
}
$_key = md5( $member->member_id . json_encode( $inSet ) );
if( isset( $this->contentPostedIn[ $_key ] ) )
{
return $this->contentPostedIn[ $_key ];
}
$contentItemClass = static::$contentItemClass;
$idColumn = static::$databaseColumnId;
if ( !$contentItemClass::$commentClass )
{
return array();
}
$commentClass = $contentItemClass::$commentClass;
if ( $contentItemClass::$firstCommentRequired AND \is_array( $inSet ) AND $additionalWhere === NULL AND $commentJoinWhere === NULL )
{
/* We can do this from one table */
$contentItemClass = static::$contentItemClass;
$commentClass = $contentItemClass::$commentClass;
$where = array(
array( $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . ' IN(' . implode( ',', $inSet ) . ')' ),
array( $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '=? ', $member->member_id )
);
$items = \IPS\Db::i()->select( $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'], $commentClass::$databaseTable, $where, NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT );
$ids = array();
foreach( $items AS $item )
{
$ids[$item] = $item;
}
}
else
{
$where = array();
if ( $contentItemClass::$firstCommentRequired )
{
$where[] = array( '(' . $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '=? )', $member->member_id );
}
else
{
$where[] = array( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['container'] . '=?', $this->$idColumn );
$where[] = array( '(' . $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '=? OR ' . $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['author'] . '=?)', $member->member_id, $member->member_id );
}
if( \is_array( $inSet ) AND \count( $inSet ) )
{
$where[] = array( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId . ' IN(' . implode( ',', $inSet ) . ')' );
}
if ( $additionalWhere )
{
$where[] = $additionalWhere;
}
$joinClause = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=' . $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId );
$items = \IPS\Db::i()->select( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId, $contentItemClass::$databaseTable, $where, NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT );
$items->join( $commentClass::$databaseTable, ( $commentJoinWhere !== NULL ) ? array( $joinClause, $commentJoinWhere ) : $joinClause );
$ids = array();
foreach( $items AS $item )
{
$ids[$item] = $item;
}
}
$this->contentPostedIn[ $_key ] = $ids;
return $ids;
}
/**
* Alter permissions for an individual group
*
@@ -3142,7 +3083,12 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
{
\IPS\Lang::deleteCustom( ( static::$permApp !== NULL ) ? static::$permApp : 'core', static::$titleLangPrefix . $this->_id . static::$descriptionLangSuffix );
}
/* Delete Rss Imports */
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_rss_import', array( 'rss_import_class=? AND rss_import_node_id=?', \get_called_class() , $this->id ) ), 'IPS\core\Rss\Import' ) as $import )
{
$import->delete();
}
return parent::delete();
}
@@ -3653,11 +3599,12 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
if ( $values = $form->values() )
{
\IPS\Db::i()->insert( 'core_ratings', array(
'class' => \get_called_class(),
'item_id' => $this->$idColumn,
'member' => \IPS\Member::loggedIn()->member_id,
'rating' => $values['rating'],
'ip' => \IPS\Request::i()->ipAddress()
'class' => \get_called_class(),
'item_id' => $this->$idColumn,
'member' => \IPS\Member::loggedIn()->member_id,
'rating' => $values['rating'],
'ip' => \IPS\Request::i()->ipAddress(),
'rating_date' => time()
), TRUE );
if ( isset( static::$ratingColumnMap['rating_average'] ) )
@@ -3753,4 +3700,14 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
{
return array();
}
/**
* [Node] Get the fully qualified node type (mostly for Pages because Pages is Pages)
*
* @return string
*/
public static function fullyQualifiedType()
{
return static::$nodeTitle . '_sg';
}
}
+290
View File
@@ -0,0 +1,290 @@
<?php
/**
* @brief Node Statistics Model
* @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
* @since 17 Nov 2020
*/
namespace IPS\Node;
/* 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;
}
/**
* Statistics Trait
*/
trait Statistics
{
/**
* @brief Cached array of IDs a member has posted in
*/
protected $authorsPostedIn = array();
/**
* Get authors posted in
*
* @param array $inSet The item_ids to check
* @return array
*/
public function authorsPostedIn( array $inSet )
{
if ( !isset( static::$contentItemClass ) )
{
return array();
}
$_key = md5( json_encode( $inSet ) );
if( isset( $this->authorsPostedIn[ $_key ] ) )
{
return $this->authorsPostedIn[ $_key ];
}
$contentItemClass = static::$contentItemClass;
$commentClass = $contentItemClass::$commentClass;
$itemIdField = $contentItemClass::$databaseColumnId;
if ( !$contentItemClass::$commentClass )
{
return array();
}
$items = iterator_to_array( \IPS\Db::i()->select( 'map_item_id, map_member_id', 'core_item_member_map', array( array( 'map_class=?', $contentItemClass ), array( \IPS\Db::i()->in( 'map_item_id', $inSet ) ) ) ) );
$reload = FALSE;
$itemIds = array();
/* Do we have the items we need? */
if ( \count( $items ) )
{
foreach ( $items as $row )
{
$itemIds[] = $row['map_item_id'];
}
$diff = array_diff( $inSet, $itemIds );
if ( \count( $diff ) )
{
$reload = TRUE;
$this->rebuildPostedIn( $diff );
}
}
else
{
/* We got nothing */
$reload = TRUE;
$this->rebuildPostedIn( $inSet );
}
/* Do we need to reload? */
if ( $reload )
{
$items = iterator_to_array( \IPS\Db::i()->select( 'map_item_id, map_member_id', 'core_item_member_map', array(array('map_class=?', $contentItemClass), array(\IPS\Db::i()->in( 'map_item_id', $inSet ) ) ) ) );
if ( $itemIds )
{
/* Check to see if there are any item IDs that can't be set because of missing posts, etc. This prevents an attempted rebuild from occuring over and over again */
$diff = array_diff( $inSet, $itemIds );
if ( \count( $diff ) )
{
foreach( $diff as $id )
{
\IPS\Db::i()->replace( 'core_item_member_map', array( 'map_class' => $contentItemClass, 'map_item_id' => $id, 'map_member_id' => NULL ), TRUE );
}
}
}
}
$return = array();
foreach( $items as $row )
{
$return[ $row['map_member_id'] ][] = $row['map_item_id'];
}
$this->authorsPostedIn[ $_key ] = $return;
return $return;
}
/**
* @brief Cached array of IDs a member has posted in (calculated the expensive way so cached separately)
*/
protected $contentPostedIn = array();
/**
* Retrieve an array of IDs a member has posted in.
*
* @param \IPS\Member|NULL $member The member (NULL for currently logged in member)
* @param array|NULL $inSet If supplied, checks will be restricted to only the ids provided
* @param array|NULL $additionalWhere Additional where clause
* @param array|NULL $commentJoinWhere Additional join clause for comments table
* @return array An array of content item ids
*/
public function contentPostedIn( $member=NULL, $inSet=NULL, $additionalWhere=NULL, $commentJoinWhere=NULL )
{
if ( $member === NULL )
{
$member = \IPS\Member::loggedIn();
}
if( !$member->member_id )
{
return array();
}
if ( !isset( static::$contentItemClass ) )
{
return array();
}
if ( \is_array( $inSet ) AND $additionalWhere === NULL AND $commentJoinWhere === NULL )
{
/* Hooray, the easy efficient way */
$members = $this->authorsPostedIn( $inSet );
if ( isset( $members[ $member->member_id ] ) )
{
return array_values( $members[ $member->member_id ] );
}
return array();
}
else
{
/* The complicated way, which doesn't happen that often */
$contentItemClass = static::$contentItemClass;
$commentClass = $contentItemClass::$commentClass;
$idColumn = static::$databaseColumnId;
$_key = md5( $member->member_id . json_encode( $inSet ) );
if( isset( $this->contentPostedIn[ $_key ] ) )
{
return $this->contentPostedIn[ $_key ];
}
$where = array();
if ( $contentItemClass::$firstCommentRequired )
{
$where[] = array( '(' . $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '=? )', $member->member_id );
}
else
{
$where[] = array( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['container'] . '=?', $this->$idColumn );
$where[] = array( '(' . $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '=? OR ' . $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['author'] . '=?)', $member->member_id, $member->member_id );
}
if( \is_array( $inSet ) AND \count( $inSet ) )
{
$where[] = array( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId . ' IN(' . implode( ',', $inSet ) . ')' );
}
if ( $additionalWhere )
{
$where[] = $additionalWhere;
}
$joinClause = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=' . $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId );
$items = \IPS\Db::i()->select( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId, $contentItemClass::$databaseTable, $where, NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT );
$items->join( $commentClass::$databaseTable, ( $commentJoinWhere !== NULL ) ? array( $joinClause, $commentJoinWhere ) : $joinClause );
$ids = array();
foreach( $items AS $item )
{
$ids[$item] = $item;
}
$this->contentPostedIn[ $_key ] = $ids;
return $ids;
}
}
/**
* Populate the item posted in data
*
* Note that we do fetch guests content here. That is to prevent content from being rebuilt on each click if it solely populated by guests
*
* @param array $inSet Array of item IDs
* @param array $members Optional array of member objects to limit by
*/
public function rebuildPostedIn( array $inSet, $members=array() )
{
$contentItemClass = static::$contentItemClass;
$commentClass = $contentItemClass::$commentClass;
$commentItemField = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'];
$authorColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'];
$dateColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['date'];
$where = array();
$deleteWhere = array( array( \IPS\Db::i()->in( 'map_item_id', $inSet ) ) );
if ( \count( $members ) )
{
$memberIds = array();
foreach ( $members as $member )
{
$memberIds[] = $member->member_id;
}
$where[] = array(\IPS\Db::i()->in( $authorColumn, $memberIds ));
$deleteWhere[] = array( \IPS\Db::i()->in( 'map_member_id', $memberIds ) );
}
\IPS\Db::i()->delete( 'core_item_member_map', $deleteWhere );
/* Do the item first if the first comment is not required */
if ( ! $contentItemClass::$firstCommentRequired )
{
$itemAuthorColumn = $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['author'];
$itemDateColumn = $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['date'];
if ( isset( $contentItemClass::$databaseColumnMap['approved'] ) )
{
$approvedColumn = $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['approved'];
$itemWhere[] = array( \IPS\Db::i()->in( $approvedColumn, array( 1, 2 ) ) ); # We want approved comments but also comments hidden because the item is hidden
}
if ( isset( $contentItemClass::$databaseColumnMap['hidden'] ) )
{
$hiddenColumn = $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['hidden'];
$itemWhere[] = array( \IPS\Db::i()->in( $hiddenColumn, array( 0, 2 ) ) ); # We want approved comments but also comments hidden because the item is hidden
}
$itemWhere[] = array( \IPS\Db::i()->in( $contentItemClass::$databaseColumnId, $inSet ) );
$insertFromSelect = \IPS\Db::i()->select( "'" . str_replace( '\\', '\\\\', $contentItemClass ) . "', `{$contentItemClass::$databaseColumnId}`, `{$itemAuthorColumn}`, `{$itemDateColumn}`" , $contentItemClass::$databaseTable, $itemWhere );
\IPS\Db::i()->replace( 'core_item_member_map', $insertFromSelect, TRUE );
}
/* Do the comments */
if ( isset( $commentClass::$databaseColumnMap['approved'] ) )
{
$approvedColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['approved'];
$where[] = array(\IPS\Db::i()->in( $approvedColumn, array(1, 2) )); # We want approved comments but also comments hidden because the item is hidden
}
if ( isset( $commentClass::$databaseColumnMap['hidden'] ) )
{
$hiddenColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'];
$where[] = array(\IPS\Db::i()->in( $hiddenColumn, array(0, 2) )); # We want approved comments but also comments hidden because the item is hidden
}
if ( $commentClass::commentWhere() !== NULL )
{
$where[] = $commentClass::commentWhere();
}
$where[] = array(\IPS\Db::i()->in( $commentItemField, $inSet ));
$insertFromSelect = \IPS\Db::i()->select( "'" . str_replace( '\\', '\\\\', $contentItemClass ) . "', `{$commentItemField}`, `{$authorColumn}`, `{$dateColumn}`", $commentClass::$databaseTable, $where, $dateColumn . ' ASC' );
\IPS\Db::i()->replace( 'core_item_member_map', $insertFromSelect, TRUE );
}
}